Skip to content

Commit d362568

Browse files
authored
Merge pull request #11 from supermemoryai/12-17-use_effect_streams
use effect streams
2 parents 026c37d + f6bc364 commit d362568

File tree

7 files changed

+275
-171
lines changed

7 files changed

+275
-171
lines changed

biome.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,23 @@
2828
"enabled": true,
2929
"useIgnoreFile": true,
3030
"clientKind": "git"
31-
}
31+
},
32+
"overrides": [
33+
{
34+
"includes": ["test/**/*.ts"],
35+
"linter": {
36+
"rules": {
37+
"style": {
38+
"noNonNullAssertion": "off"
39+
},
40+
"suspicious": {
41+
"noNonNullAssertedOptionalChain": "off"
42+
},
43+
"correctness": {
44+
"noUnsafeOptionalChaining": "off"
45+
}
46+
}
47+
}
48+
}
49+
]
3250
}

src/chunk.ts

Lines changed: 124 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Effect } from 'effect'
1+
import { Effect, Stream } from 'effect'
22
import {
33
chunk as chunkInternal,
44
streamChunks as streamChunksInternal,
@@ -7,7 +7,13 @@ 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+
Language,
14+
ParseResult,
15+
ScopeTree,
16+
} from './types'
1117

1218
/**
1319
* Error thrown when chunking fails
@@ -138,6 +144,114 @@ export async function chunk(
138144
return Effect.runPromise(chunkEffect(filepath, code, options))
139145
}
140146

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

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-
}
288+
const { parseResult, scopeTree, language } = prepared
210289

211-
// Stream chunks from the internal generator, passing filepath for context
290+
// Stream chunks from the internal generator
212291
const chunkGenerator = streamChunksInternal(
213292
parseResult.tree.rootNode,
214293
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)