Skip to content

Commit ea62f50

Browse files
committed
use effect streams
1 parent 026c37d commit ea62f50

File tree

2 files changed

+126
-45
lines changed

2 files changed

+126
-45
lines changed

src/chunk.ts

Lines changed: 125 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Effect } from 'effect'
1+
import { Chunk as EffectChunk, Effect, Stream } from 'effect'
22
import {
33
chunk as chunkInternal,
44
streamChunks as streamChunksInternal,
@@ -7,7 +7,14 @@ import { extractEntities } from './extract'
77
import { parseCode } from './parser'
88
import { detectLanguage } from './parser/languages'
99
import { buildScopeTree } from './scope'
10-
import type { Chunk, ChunkOptions, Language } from './types'
10+
import type {
11+
Chunk,
12+
ChunkOptions,
13+
ExtractedEntity,
14+
Language,
15+
ParseResult,
16+
ScopeTree,
17+
} from './types'
1118

1219
/**
1320
* Error thrown when chunking fails
@@ -138,6 +145,114 @@ export async function chunk(
138145
return Effect.runPromise(chunkEffect(filepath, code, options))
139146
}
140147

148+
/**
149+
* Prepare the chunking pipeline (parse, extract, build scope tree)
150+
* Returns the parsed result and scope tree needed for chunking
151+
*/
152+
const prepareChunking = (
153+
filepath: string,
154+
code: string,
155+
options?: ChunkOptions,
156+
): Effect.Effect<
157+
{ parseResult: ParseResult; scopeTree: ScopeTree; language: Language },
158+
ChunkingError | UnsupportedLanguageError
159+
> => {
160+
return Effect.gen(function* () {
161+
// Step 1: Detect language (or use override)
162+
const language: Language | null =
163+
options?.language ?? detectLanguage(filepath)
164+
165+
if (!language) {
166+
return yield* Effect.fail(new UnsupportedLanguageError(filepath))
167+
}
168+
169+
// Step 2: Parse the code
170+
const parseResult = yield* Effect.tryPromise({
171+
try: () => parseCode(code, language),
172+
catch: (error: unknown) =>
173+
new ChunkingError('Failed to parse code', error),
174+
})
175+
176+
// Step 3: Extract entities from AST
177+
const entities = yield* Effect.mapError(
178+
extractEntities(parseResult.tree.rootNode, language, code),
179+
(error: unknown) =>
180+
new ChunkingError('Failed to extract entities', error),
181+
)
182+
183+
// Step 4: Build scope tree
184+
const scopeTree = yield* Effect.mapError(
185+
buildScopeTree(entities),
186+
(error: unknown) =>
187+
new ChunkingError('Failed to build scope tree', error),
188+
)
189+
190+
return { parseResult, scopeTree, language }
191+
})
192+
}
193+
194+
/**
195+
* Create an Effect Stream that yields chunks
196+
*
197+
* This is the Effect-native streaming API. Use this if you're working
198+
* within the Effect ecosystem and want full composability.
199+
*
200+
* @param filepath - The file path (used for language detection)
201+
* @param code - The source code to chunk
202+
* @param options - Optional chunking configuration
203+
* @returns Effect Stream of chunks with context
204+
*
205+
* @example
206+
* ```ts
207+
* import { chunkStreamEffect } from 'astchunk'
208+
* import { Effect, Stream } from 'effect'
209+
*
210+
* const program = Stream.runForEach(
211+
* chunkStreamEffect('src/utils.ts', sourceCode),
212+
* (chunk) => Effect.log(chunk.text)
213+
* )
214+
*
215+
* Effect.runPromise(program)
216+
* ```
217+
*/
218+
export const chunkStreamEffect = (
219+
filepath: string,
220+
code: string,
221+
options?: ChunkOptions,
222+
): Stream.Stream<Chunk, ChunkingError | UnsupportedLanguageError> => {
223+
return Stream.unwrap(
224+
Effect.map(prepareChunking(filepath, code, options), (prepared) => {
225+
const { parseResult, scopeTree, language } = prepared
226+
227+
// Create stream from the internal generator
228+
return Stream.fromAsyncIterable(
229+
streamChunksInternal(
230+
parseResult.tree.rootNode,
231+
code,
232+
scopeTree,
233+
language,
234+
options,
235+
filepath,
236+
),
237+
(error) => new ChunkingError('Stream iteration failed', error),
238+
).pipe(
239+
// Attach parse error to chunks if present
240+
Stream.map((chunk) =>
241+
parseResult.error
242+
? {
243+
...chunk,
244+
context: {
245+
...chunk.context,
246+
parseError: parseResult.error,
247+
},
248+
}
249+
: chunk,
250+
),
251+
)
252+
}),
253+
)
254+
}
255+
141256
/**
142257
* Stream source code chunks as they are generated
143258
*
@@ -154,9 +269,9 @@ export async function chunk(
154269
*
155270
* @example
156271
* ```ts
157-
* import { stream } from 'astchunk'
272+
* import { chunkStream } from 'astchunk'
158273
*
159-
* for await (const chunk of stream('src/utils.ts', sourceCode)) {
274+
* for await (const chunk of chunkStream('src/utils.ts', sourceCode)) {
160275
* console.log(chunk.text, chunk.context)
161276
* }
162277
* ```
@@ -166,49 +281,14 @@ export async function* chunkStream(
166281
code: string,
167282
options?: ChunkOptions,
168283
): AsyncGenerator<Chunk> {
169-
// Detect language (or use override)
170-
const language: Language | null =
171-
options?.language ?? detectLanguage(filepath)
172-
173-
if (!language) {
174-
throw new UnsupportedLanguageError(filepath)
175-
}
176-
177-
// Parse the code
178-
let parseResult: Awaited<ReturnType<typeof parseCode>>
179-
try {
180-
parseResult = await parseCode(code, language)
181-
} catch (error) {
182-
throw new ChunkingError('Failed to parse code', error)
183-
}
184-
185-
// Extract entities from AST
186-
let entities: Awaited<
187-
ReturnType<typeof extractEntities> extends Effect.Effect<infer A, unknown>
188-
? A
189-
: never
190-
>
191-
try {
192-
entities = await Effect.runPromise(
193-
extractEntities(parseResult.tree.rootNode, language, code),
194-
)
195-
} catch (error) {
196-
throw new ChunkingError('Failed to extract entities', error)
197-
}
284+
// Prepare the chunking pipeline
285+
const prepared = await Effect.runPromise(
286+
prepareChunking(filepath, code, options),
287+
)
198288

199-
// Build scope tree
200-
let scopeTree: Awaited<
201-
ReturnType<typeof buildScopeTree> extends Effect.Effect<infer A, unknown>
202-
? A
203-
: never
204-
>
205-
try {
206-
scopeTree = await Effect.runPromise(buildScopeTree(entities))
207-
} catch (error) {
208-
throw new ChunkingError('Failed to build scope tree', error)
209-
}
289+
const { parseResult, scopeTree, language } = prepared
210290

211-
// Stream chunks from the internal generator, passing filepath for context
291+
// Stream chunks from the internal generator
212292
const chunkGenerator = streamChunksInternal(
213293
parseResult.tree.rootNode,
214294
code,

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export {
1313
ChunkingError,
1414
chunk,
1515
chunkStream,
16+
chunkStreamEffect,
1617
UnsupportedLanguageError,
1718
} from './chunk'
1819

0 commit comments

Comments
 (0)