@gobing-ai/ts-llm-jsonl-importer 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,6 +56,16 @@ result.skippedDuplicates;
56
56
 
57
57
  All modes preserve parse and validation issues in the returned `ImportResult`; malformed rows are not inserted.
58
58
 
59
+ ## Streaming
60
+
61
+ The importer uses `FileSystem.readFileStream` when available (ADR-021), reading one line at a time
62
+ for **O(line) memory usage** — enabling multi-MB or multi-GB LLM history files without buffering
63
+ the entire file. Behavior is identical to the previous `readFile` + split approach (same `source_line`
64
+ values, same `ImportResult`).
65
+
66
+ When `readFileStream` is unavailable (e.g. Cloudflare Workers), the importer falls back to
67
+ `readFile` + split transparently.
68
+
59
69
  ## Import Specific Files
60
70
 
61
71
  ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAER,aAAa,EACb,YAAY,EAEZ,cAAc,EAGjB,MAAM,SAAS,CAAC;AAajB,0DAA0D;AAC1D,wBAAsB,wBAAwB,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAOrF;AAED,+DAA+D;AAC/D,wBAAsB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA6G1G"}
1
+ {"version":3,"file":"importer.d.ts","sourceRoot":"","sources":["../src/importer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAER,aAAa,EACb,YAAY,EAEZ,cAAc,EAGjB,MAAM,SAAS,CAAC;AAajB,0DAA0D;AAC1D,wBAAsB,wBAAwB,CAAC,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAOrF;AAED,+DAA+D;AAC/D,wBAAsB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA4G1G"}
package/dist/importer.js CHANGED
@@ -32,11 +32,11 @@ export async function runJsonlImport(source, options) {
32
32
  let checkpointUpdates = 0;
33
33
  for (const file of files) {
34
34
  const checkpoint = mode === 'incremental' ? await readCheckpoint(options.db, source, file) : 0;
35
- const lines = (await fileSystem.readFile(file)).split(/\r?\n/);
36
- for (let index = 0; index < lines.length; index += 1) {
37
- const lineNumber = index + 1;
38
- const line = lines[index]?.trim();
39
- if (line === undefined || line.length === 0 || lineNumber <= checkpoint)
35
+ let lineNumber = 0;
36
+ for await (const rawLine of readLines(fileSystem, file)) {
37
+ lineNumber += 1;
38
+ const line = rawLine.trim();
39
+ if (line.length === 0 || lineNumber <= checkpoint)
40
40
  continue;
41
41
  processedLines += 1;
42
42
  const raw = parseJsonLine(line, file, lineNumber, parseErrors);
@@ -209,7 +209,8 @@ async function ledgerExists(db, recordHash) {
209
209
  async function insertRecord(db, targetTable, recordHash, sourceFile, sourceLine, splitIndex, payload, now) {
210
210
  const table = targetTableFor(targetTable);
211
211
  await db.run(`INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
212
- VALUES (?, ?, ?, ?, ?, ?)`, recordHash, sourceFile, sourceLine, splitIndex, JSON.stringify(payload), timestamp(now));
212
+ VALUES (?, ?, ?, ?, ?, ?)
213
+ ON CONFLICT(record_hash) DO NOTHING`, recordHash, sourceFile, sourceLine, splitIndex, JSON.stringify(payload), timestamp(now));
213
214
  }
214
215
  async function insertLedger(db, recordHash, source, sourceFile, sourceLine, splitIndex, targetTable, now) {
215
216
  await db.run(`INSERT INTO history_import_ledger (record_hash, source, source_file, source_line, split_index, target_table, imported_at)
@@ -224,3 +225,21 @@ function targetTableFor(table) {
224
225
  function timestamp(now) {
225
226
  return (now?.() ?? new Date()).toISOString();
226
227
  }
228
+ /**
229
+ * Yield lines from a file, streaming when the FileSystem supports it.
230
+ *
231
+ * WHY: large JSONL history files (100MB+) must not be loaded into memory all at
232
+ * once. When `fileSystem.readFileStream` is available, lines are streamed from
233
+ * disk in chunks. Otherwise, falls back to `readFile` + `split` — same behavior
234
+ * as before, preserving parity for stubs like CF Workers.
235
+ */
236
+ async function* readLines(fileSystem, file) {
237
+ if (fileSystem.readFileStream) {
238
+ yield* fileSystem.readFileStream(file);
239
+ return;
240
+ }
241
+ const content = await fileSystem.readFile(file);
242
+ for (const line of content.split(/\r?\n/)) {
243
+ yield line;
244
+ }
245
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-llm-jsonl-importer",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "@gobing-ai/ts-llm-jsonl-importer — Generic JSONL importer for LLM agent history files.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,9 +47,9 @@
47
47
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-llm-jsonl-importer-v<version> && git push --tags' && exit 1"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-db": "^0.4.5",
51
- "@gobing-ai/ts-runtime": "^0.4.5",
52
- "@gobing-ai/ts-utils": "^0.4.5",
50
+ "@gobing-ai/ts-db": "^0.4.7",
51
+ "@gobing-ai/ts-runtime": "^0.4.7",
52
+ "@gobing-ai/ts-utils": "^0.4.7",
53
53
  "zod": "^4.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/importer.ts CHANGED
@@ -56,12 +56,11 @@ export async function runJsonlImport(source: LlmJsonlSource, options: ImportOpti
56
56
 
57
57
  for (const file of files) {
58
58
  const checkpoint = mode === 'incremental' ? await readCheckpoint(options.db, source, file) : 0;
59
- const lines = (await fileSystem.readFile(file)).split(/\r?\n/);
60
-
61
- for (let index = 0; index < lines.length; index += 1) {
62
- const lineNumber = index + 1;
63
- const line = lines[index]?.trim();
64
- if (line === undefined || line.length === 0 || lineNumber <= checkpoint) continue;
59
+ let lineNumber = 0;
60
+ for await (const rawLine of readLines(fileSystem, file)) {
61
+ lineNumber += 1;
62
+ const line = rawLine.trim();
63
+ if (line.length === 0 || lineNumber <= checkpoint) continue;
65
64
  processedLines += 1;
66
65
 
67
66
  const raw = parseJsonLine(line, file, lineNumber, parseErrors);
@@ -298,7 +297,8 @@ async function insertRecord(
298
297
  const table = targetTableFor(targetTable);
299
298
  await db.run(
300
299
  `INSERT INTO ${table} (record_hash, source_file, source_line, split_index, payload_json, imported_at)
301
- VALUES (?, ?, ?, ?, ?, ?)`,
300
+ VALUES (?, ?, ?, ?, ?, ?)
301
+ ON CONFLICT(record_hash) DO NOTHING`,
302
302
  recordHash,
303
303
  sourceFile,
304
304
  sourceLine,
@@ -341,3 +341,22 @@ function targetTableFor(table: string): string {
341
341
  function timestamp(now: ImportOptions['now']): string {
342
342
  return (now?.() ?? new Date()).toISOString();
343
343
  }
344
+
345
+ /**
346
+ * Yield lines from a file, streaming when the FileSystem supports it.
347
+ *
348
+ * WHY: large JSONL history files (100MB+) must not be loaded into memory all at
349
+ * once. When `fileSystem.readFileStream` is available, lines are streamed from
350
+ * disk in chunks. Otherwise, falls back to `readFile` + `split` — same behavior
351
+ * as before, preserving parity for stubs like CF Workers.
352
+ */
353
+ async function* readLines(fileSystem: FileSystem, file: string): AsyncGenerator<string> {
354
+ if (fileSystem.readFileStream) {
355
+ yield* fileSystem.readFileStream(file);
356
+ return;
357
+ }
358
+ const content = await fileSystem.readFile(file);
359
+ for (const line of content.split(/\r?\n/)) {
360
+ yield line;
361
+ }
362
+ }