sliftutils 1.7.32 → 1.7.34

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.
Files changed (116) hide show
  1. package/builders/extensionBuild.ts +1 -1
  2. package/bundler/bundleEntry.ts +1 -3
  3. package/bundler/bundler.ts +2 -4
  4. package/bundler/sourceMaps.ts +1 -2
  5. package/examplestorage/browser.tsx +1 -2
  6. package/examplestorage/exampleserver.ts +1 -2
  7. package/index.d.ts +134 -102
  8. package/misc/dist/environment.ts.cache +2 -2
  9. package/misc/dist/getSecret.ts.cache +2 -2
  10. package/misc/dist/strings.ts.cache +2 -2
  11. package/misc/dist/zip.ts.cache +2 -2
  12. package/misc/https/dist/certs.ts.cache +2 -2
  13. package/misc/https/dist/dns.ts.cache +7 -16
  14. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  15. package/misc/https/dns.d.ts +1 -4
  16. package/misc/https/dns.ts +5 -14
  17. package/misc/https/hostServer.d.ts +2 -0
  18. package/misc/https/hostServer.ts +6 -7
  19. package/misc/https/httpsCerts.ts +4 -10
  20. package/misc/openrouter.ts +1 -2
  21. package/package.json +1 -1
  22. package/render-utils/FullscreenModal.tsx +1 -4
  23. package/render-utils/GenericFormat.tsx +2 -8
  24. package/render-utils/Input.tsx +5 -13
  25. package/render-utils/InputLabel.tsx +4 -6
  26. package/render-utils/InputPicker.tsx +1 -2
  27. package/render-utils/URLParam.ts +2 -3
  28. package/render-utils/dist/observer.tsx.cache +2 -2
  29. package/render-utils/mobxTyped.ts +2 -3
  30. package/render-utils/niceStringify.ts +2 -4
  31. package/storage/ArchivesDisk.ts +4 -14
  32. package/storage/BulkDatabase2/BulkDatabase2.d.ts +1 -2
  33. package/storage/BulkDatabase2/BulkDatabase2.ts +4 -11
  34. package/storage/BulkDatabase2/BulkDatabaseBase.ts +42 -114
  35. package/storage/BulkDatabase2/BulkDatabaseFormat.ts +31 -91
  36. package/storage/BulkDatabase2/BulkDatabaseMerge.ts +19 -57
  37. package/storage/BulkDatabase2/BulkDatabaseReader.ts +4 -12
  38. package/storage/BulkDatabase2/LoadedIndex.ts +6 -18
  39. package/storage/BulkDatabase2/WriteOverlay.ts +3 -8
  40. package/storage/BulkDatabase2/blockCache.ts +12 -28
  41. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +40 -103
  42. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +19 -50
  43. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +18 -50
  44. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +7 -15
  45. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +9 -21
  46. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +6 -11
  47. package/storage/BulkDatabase2/dist/blockCache.ts.cache +15 -31
  48. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +10 -29
  49. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +10 -18
  50. package/storage/BulkDatabase2/dist/streamLog.ts.cache +17 -43
  51. package/storage/BulkDatabase2/dist/syncClient.ts.cache +11 -27
  52. package/storage/BulkDatabase2/mergeLock.ts +7 -26
  53. package/storage/BulkDatabase2/mergeMarkers.ts +7 -15
  54. package/storage/BulkDatabase2/streamLog.ts +14 -40
  55. package/storage/BulkDatabase2/syncClient.ts +8 -24
  56. package/storage/FileFolderAPI.tsx +28 -75
  57. package/storage/IArchives.d.ts +3 -3
  58. package/storage/IArchives.ts +21 -50
  59. package/storage/IStorage.ts +2 -4
  60. package/storage/PendingManager.tsx +2 -3
  61. package/storage/TransactionStorage.ts +11 -24
  62. package/storage/backblaze.ts +12 -43
  63. package/storage/dist/ArchivesDisk.ts.cache +7 -17
  64. package/storage/dist/FileFolderAPI.tsx.cache +25 -63
  65. package/storage/dist/IArchives.ts.cache +10 -20
  66. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
  67. package/storage/dist/JSONStorage.ts.cache +2 -2
  68. package/storage/dist/PendingManager.tsx.cache +5 -6
  69. package/storage/dist/TransactionStorage.ts.cache +14 -27
  70. package/storage/dist/backblaze.ts.cache +15 -46
  71. package/storage/dist/embeddingFormats.ts.cache +11 -24
  72. package/storage/dist/fileSystemPointer.ts.cache +4 -7
  73. package/storage/dist/remoteFileStorage.ts.cache +17 -36
  74. package/storage/embeddingFormats.ts +11 -27
  75. package/storage/fileSystemPointer.ts +2 -7
  76. package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +8 -18
  77. package/storage/proxydatabase/dist/transactionSet.ts.cache +4 -7
  78. package/storage/proxydatabase/inMemoryDatabase.ts +1 -3
  79. package/storage/proxydatabase/ivfEmbeddingDatabase.ts +8 -23
  80. package/storage/proxydatabase/transactionSet.ts +2 -5
  81. package/storage/remoteFileServer.ts +20 -48
  82. package/storage/remoteFileStorage.ts +14 -33
  83. package/storage/remoteStorage/ArchivesRemote.d.ts +1 -4
  84. package/storage/remoteStorage/ArchivesRemote.ts +11 -26
  85. package/storage/remoteStorage/ArchivesUrl.ts +2 -4
  86. package/storage/remoteStorage/accessPage.tsx +1 -5
  87. package/storage/remoteStorage/blobStore.d.ts +5 -13
  88. package/storage/remoteStorage/blobStore.ts +52 -130
  89. package/storage/remoteStorage/createArchives.d.ts +22 -14
  90. package/storage/remoteStorage/createArchives.ts +54 -188
  91. package/storage/remoteStorage/deployTakeover.d.ts +18 -32
  92. package/storage/remoteStorage/deployTakeover.ts +70 -384
  93. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +12 -24
  94. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +5 -7
  95. package/storage/remoteStorage/dist/blobStore.ts.cache +47 -117
  96. package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
  97. package/storage/remoteStorage/dist/createArchives.ts.cache +58 -189
  98. package/storage/remoteStorage/dist/deployTakeover.ts.cache +81 -387
  99. package/storage/remoteStorage/dist/intermediateSources.ts.cache +119 -0
  100. package/storage/remoteStorage/dist/remoteConfig.ts.cache +36 -24
  101. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +20 -40
  102. package/storage/remoteStorage/dist/storageClientController.ts.cache +4 -6
  103. package/storage/remoteStorage/dist/storageController.ts.cache +37 -74
  104. package/storage/remoteStorage/dist/storageServerState.ts.cache +345 -188
  105. package/storage/remoteStorage/grantAccessCli.ts +1 -3
  106. package/storage/remoteStorage/intermediateSources.d.ts +21 -0
  107. package/storage/remoteStorage/intermediateSources.ts +117 -0
  108. package/storage/remoteStorage/remoteConfig.ts +33 -20
  109. package/storage/remoteStorage/sourceWrapper.d.ts +8 -19
  110. package/storage/remoteStorage/sourceWrapper.ts +19 -41
  111. package/storage/remoteStorage/storageClientController.ts +1 -3
  112. package/storage/remoteStorage/storageController.d.ts +7 -6
  113. package/storage/remoteStorage/storageController.ts +36 -82
  114. package/storage/remoteStorage/storageServer.ts +20 -34
  115. package/storage/remoteStorage/storageServerState.d.ts +41 -5
  116. package/storage/remoteStorage/storageServerState.ts +355 -216
@@ -1,7 +1,5 @@
1
1
  // File format (all integers little-endian):
2
- // u32 headerLength, then headerLength bytes of JSON: { rowCount, columns: [{ name, offset, length }] }, where offset is relative to the end of the header.
3
- // Each column blob is: u32 offsets[rowCount + 1] (byte offsets into the column's data section), u8 types[rowCount], then the data section (each value's bytes, concatenated).
4
- // Values are encoded with an explicit type tag per value (see the TYPE_ constants). Reads only fetch the byte ranges they need.
2
+ // u32 headerLength, then headerLength bytes of JSON: { rowCount, columns: [{ name, offset, length }] }, where offset is relative to the end of the header. Each column blob is: u32 offsets[rowCount + 1] (byte offsets into the column's data section), u8 types[rowCount], then the data section (each value's bytes, concatenated). Values are encoded with an explicit type tag per value (see the TYPE_ constants). Reads only fetch the byte ranges they need.
5
3
 
6
4
  export const KEY_COLUMN = "key";
7
5
 
@@ -20,9 +18,7 @@ const TYPE_INT32_ARRAY = 10;
20
18
  const TYPE_UINT32_ARRAY = 11;
21
19
  const TYPE_FLOAT32_ARRAY = 12;
22
20
  const TYPE_FLOAT64_ARRAY = 13;
23
- // A cell whose row never set this column at all — as opposed to TYPE_UNDEFINED, an explicitly stored
24
- // undefined. On read, ABSENT falls through to older readers for that column; a stored undefined stops
25
- // the fall-through (it's a real value that clears the column).
21
+ // A cell whose row never set this column at all — as opposed to TYPE_UNDEFINED, an explicitly stored undefined. On read, ABSENT falls through to older readers for that column; a stored undefined stops the fall-through (it's a real value that clears the column).
26
22
  const TYPE_ABSENT = 14;
27
23
 
28
24
  const TYPED_ARRAY_TYPES: { type: number; ctor: { new(buffer: ArrayBuffer): ArrayBufferView; BYTES_PER_ELEMENT: number; name: string } }[] = [
@@ -39,54 +35,35 @@ const TYPED_ARRAY_TYPES: { type: number; ctor: { new(buffer: ArrayBuffer): Array
39
35
 
40
36
  export const EMPTY_BUFFER = Buffer.alloc(0) as Buffer;
41
37
 
42
- // Sentinel a reader returns for a cell whose row never set this column, so the join can fall through
43
- // to an older reader for that column. Distinct from a stored undefined, which is a real clearing value.
38
+ // Sentinel a reader returns for a cell whose row never set this column, so the join can fall through to an older reader for that column. Distinct from a stored undefined, which is a real clearing value.
44
39
  export const ABSENT = Symbol("absent");
45
40
 
46
- // One value exactly as it sits on disk: its type tag + raw bytes. A cell's encoding is position-
47
- // independent (nothing about it depends on which row/file it lives in), so a merge copies the winning
48
- // RawCell straight from an input column into the output — never decoding it to a JS value and
49
- // re-encoding. That's far less memory (no object materialization) and much faster (a byte copy, not a
50
- // JPEG/typed-array decode + re-encode).
41
+ // One value exactly as it sits on disk: its type tag + raw bytes. A cell's encoding is position- independent (nothing about it depends on which row/file it lives in), so a merge copies the winning RawCell straight from an input column into the output — never decoding it to a JS value and re-encoding. That's far less memory (no object materialization) and much faster (a byte copy, not a JPEG/typed-array decode + re-encode).
51
42
  export type RawCell = { type: number; bytes: Buffer };
52
43
 
53
- // The type tag a cell carries when its row never set this column — read it from the column's `types`
54
- // array to detect ABSENT without decoding. Re-exported so the merge planner (which works at the
55
- // column-index level, no value materialization) can check it.
44
+ // The type tag a cell carries when its row never set this column — read it from the column's `types` array to detect ABSENT without decoding. Re-exported so the merge planner (which works at the column-index level, no value materialization) can check it.
56
45
  export const TYPE_ABSENT_TAG = TYPE_ABSENT;
57
46
 
58
- // A column's index alone (offsets + types — small, ~5 bytes/row) plus a primitive to read a CONTIGUOUS
59
- // row range's raw value bytes. Used by the planned merge: it loads the index across every (source,
60
- // column) once (cheap), uses types to detect ABSENT and offsets to size each cell, plans the byte
61
- // layout of every output file from those — then in the execute phase reads only the runs of bytes it
62
- // actually needs, copying them straight into pre-laid-out output buffers. No cell value is ever
63
- // materialized as a JS object.
47
+ // A column's index alone (offsets + types — small, ~5 bytes/row) plus a primitive to read a CONTIGUOUS row range's raw value bytes. Used by the planned merge: it loads the index across every (source, column) once (cheap), uses types to detect ABSENT and offsets to size each cell, plans the byte layout of every output file from those — then in the execute phase reads only the runs of bytes it actually needs, copying them straight into pre-laid-out output buffers. No cell value is ever materialized as a JS object.
64
48
  export type ColumnIndex = {
65
49
  // offsets[i]..offsets[i+1] is row i's value byte range within the column's data section.
66
50
  offsets: Uint32Array;
67
51
  // Per-row type tag; TYPE_ABSENT_TAG marks "this row never set this column" (fall-through).
68
52
  types: Uint8Array;
69
- // Read the contiguous bytes of value(s) for rows [startRow, endRow). Length is exactly
70
- // offsets[endRow] - offsets[startRow] — the cells in that range concatenated. The caller composes
71
- // larger reads from consecutive rows (one source-side getRange per run).
53
+ // Read the contiguous bytes of value(s) for rows [startRow, endRow). Length is exactly offsets[endRow] - offsets[startRow] — the cells in that range concatenated. The caller composes larger reads from consecutive rows (one source-side getRange per run).
72
54
  readValueBytes: (startRow: number, endRow: number) => Promise<Buffer>;
73
55
  };
74
56
 
75
- // Hidden per-row column holding each row's write-time (so reads can resolve a key to its latest value
76
- // by actual time). NUL-prefixed so it can't collide with a user column; excluded from `columns`.
57
+ // Hidden per-row column holding each row's write-time (so reads can resolve a key to its latest value by actual time). NUL-prefixed so it can't collide with a user column; excluded from `columns`.
77
58
  const TIME_COLUMN = String.fromCharCode(0) + "t";
78
59
 
79
60
  type FileHeader = {
80
61
  rowCount: number;
81
62
  columns: { name: string; offset: number; length: number }[];
82
- // Oldest/newest write-time of the data in this file (from the stream entries it was folded from,
83
- // carried through merges). Lets the merge planner pick files overlapping a time range. Absent (0)
84
- // in files written before this existed.
63
+ // Oldest/newest write-time of the data in this file (from the stream entries it was folded from, carried through merges). Lets the merge planner pick files overlapping a time range. Absent (0) in files written before this existed.
85
64
  minTime?: number;
86
65
  maxTime?: number;
87
- // Lexicographically smallest/largest key in this file (rows are stored key-sorted). Lets the merge
88
- // planner group/dedup by key range and lets a single-key read skip files whose range excludes the
89
- // key. Absent (undefined) in files written before this existed — treated as "spans all keys".
66
+ // Lexicographically smallest/largest key in this file (rows are stored key-sorted). Lets the merge planner group/dedup by key range and lets a single-key read skip files whose range excludes the key. Absent (undefined) in files written before this existed — treated as "spans all keys".
90
67
  minKey?: string;
91
68
  maxKey?: string;
92
69
  };
@@ -177,9 +154,7 @@ function encodeBulkData(data: unknown[]): Buffer {
177
154
  return Buffer.concat([offsets, types, ...parts]);
178
155
  }
179
156
 
180
- // Like encodeBulkData but the values are already encoded (a merge supplies the winning cells' raw bytes
181
- // straight from the input files). Lays out the same offsets / types / data section without touching the
182
- // values themselves.
157
+ // Like encodeBulkData but the values are already encoded (a merge supplies the winning cells' raw bytes straight from the input files). Lays out the same offsets / types / data section without touching the values themselves.
183
158
  function encodeBulkDataRaw(cells: RawCell[]): Buffer {
184
159
  const n = cells.length;
185
160
  const offsets = Buffer.alloc(4 * (n + 1));
@@ -209,11 +184,7 @@ function decodeBulkData(blob: Buffer, rowCount: number): unknown[] {
209
184
  return values;
210
185
  }
211
186
 
212
- // Target logical (uncompressed) size of one bulk file. A rows array bigger than this is split across
213
- // multiple key-contiguous files, so files stay around this size (the merge policy's target) and no
214
- // single column blob / Buffer.concat ever approaches the ~2GB Buffer length limit. The estimate is
215
- // rough on purpose — it only needs to keep each chunk near the target, not be exact. A single row
216
- // bigger than the target still becomes its own (oversized) file, since we never split within a key.
187
+ // Target logical (uncompressed) size of one bulk file. A rows array bigger than this is split across multiple key-contiguous files, so files stay around this size (the merge policy's target) and no single column blob / Buffer.concat ever approaches the ~2GB Buffer length limit. The estimate is rough on purpose — it only needs to keep each chunk near the target, not be exact. A single row bigger than the target still becomes its own (oversized) file, since we never split within a key.
217
188
  export const TARGET_FILE_BYTES = 256 * 1024 * 1024;
218
189
 
219
190
  // Per-value on-disk overhead: a 4-byte offset entry plus a 1-byte type tag.
@@ -237,9 +208,7 @@ function estimateRowBytes(row: Record<string, unknown>): number {
237
208
  return total;
238
209
  }
239
210
 
240
- // Concatenates already-encoded column blobs into a complete file (4-byte header length + header JSON +
241
- // blobs), computing the header's time + key bounds from the per-row times and keys. Shared by the
242
- // object-based builder (buildOneFile) and the raw-splice builder (buildOneFileRaw).
211
+ // Concatenates already-encoded column blobs into a complete file (4-byte header length + header JSON + blobs), computing the header's time + key bounds from the per-row times and keys. Shared by the object-based builder (buildOneFile) and the raw-splice builder (buildOneFileRaw).
243
212
  function assembleFile(columnNames: string[], blobs: Buffer[], rowCount: number, times: number[], keys: string[]): Buffer {
244
213
  let offset = 0;
245
214
  const columns = columnNames.map((name, i) => {
@@ -281,21 +250,15 @@ function buildOneFile(rows: Record<string, unknown>[], times: number[]): Buffer
281
250
  return assembleFile(columnNames, blobs, rows.length, times, rows.map(r => r[KEY_COLUMN] as string));
282
251
  }
283
252
 
284
- // A resolved output row for the raw-splice merge: its key, write-time, and the winning raw cell for each
285
- // column it has (a column it lacks is written ABSENT — fall-through). The cell bytes are copied straight
286
- // from the input files; no value is ever decoded.
253
+ // A resolved output row for the raw-splice merge: its key, write-time, and the winning raw cell for each column it has (a column it lacks is written ABSENT — fall-through). The cell bytes are copied straight from the input files; no value is ever decoded.
287
254
  export type RawRow = { key: string; time: number; cells: Map<string, RawCell> };
288
255
 
289
- // Size of a column blob's INDEX section (offsets array + types array) for a column of N rows. The data
290
- // section follows immediately after. The planned merge uses this to size output buffers exactly.
256
+ // Size of a column blob's INDEX section (offsets array + types array) for a column of N rows. The data section follows immediately after. The planned merge uses this to size output buffers exactly.
291
257
  export function columnIndexByteLength(rowCount: number): number {
292
258
  return 4 * (rowCount + 1) + rowCount;
293
259
  }
294
260
 
295
- // Assembles a complete bulk file from a set of pre-built value column blobs plus the keys + times. Auto-
296
- // adds the KEY_COLUMN (keys) and hidden TIME_COLUMN (times) — they're small enough to encode in one shot
297
- // here. Used by the planned merge: it builds each value column's blob by-hand (offsets/types + raw bytes
298
- // copied directly from inputs, no value materialization) and hands the result here for header + bounds.
261
+ // Assembles a complete bulk file from a set of pre-built value column blobs plus the keys + times. Auto- adds the KEY_COLUMN (keys) and hidden TIME_COLUMN (times) — they're small enough to encode in one shot here. Used by the planned merge: it builds each value column's blob by-hand (offsets/types + raw bytes copied directly from inputs, no value materialization) and hands the result here for header + bounds.
299
262
  export function assemblePlannedFile(config: {
300
263
  valueColumns: { name: string; blob: Buffer }[];
301
264
  keys: string[];
@@ -313,8 +276,7 @@ export function assemblePlannedFile(config: {
313
276
  const ABSENT_CELL: RawCell = { type: TYPE_ABSENT, bytes: EMPTY_BUFFER };
314
277
 
315
278
  function buildOneFileRaw(rows: RawRow[]): Buffer {
316
- // Columns present in this chunk, in first-seen order (matches buildOneFile, which only emits columns
317
- // some row actually has).
279
+ // Columns present in this chunk, in first-seen order (matches buildOneFile, which only emits columns some row actually has).
318
280
  const valueColumns: string[] = [];
319
281
  const seen = new Set<string>();
320
282
  for (const row of rows) for (const col of row.cells.keys()) {
@@ -338,16 +300,10 @@ function estimateRawRowBytes(row: RawRow): number {
338
300
  return total;
339
301
  }
340
302
 
341
- // One complete, independent file: the encoded buffer plus its key range + row count (the caller logs the
342
- // range when a merge splits across several files).
303
+ // One complete, independent file: the encoded buffer plus its key range + row count (the caller logs the range when a merge splits across several files).
343
304
  export interface BuiltFile { buffer: Buffer; minKey: string; maxKey: string; rowCount: number; }
344
305
 
345
- // Returns one complete, independent file per chunk of rows. Rows are first sorted by key, then
346
- // partitioned into key-contiguous chunks of ~targetBytes each — so every returned file is key-sorted
347
- // (tight minKey/maxKey for the read-skip + merge planner) and stays near the target size, and no
348
- // single column blob / Buffer.concat approaches the ~2GB limit. The chunks have disjoint key ranges,
349
- // so the caller just writes each as its own file. A normal-sized write returns a single file.
350
- // `times[i]` is row i's write-time, stored per row so reads resolve a key to its latest value by time.
306
+ // Returns one complete, independent file per chunk of rows. Rows are first sorted by key, then partitioned into key-contiguous chunks of ~targetBytes each — so every returned file is key-sorted (tight minKey/maxKey for the read-skip + merge planner) and stays near the target size, and no single column blob / Buffer.concat approaches the ~2GB limit. The chunks have disjoint key ranges, so the caller just writes each as its own file. A normal-sized write returns a single file. `times[i]` is row i's write-time, stored per row so reads resolve a key to its latest value by time.
351
307
  export function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes = TARGET_FILE_BYTES): BuiltFile[] {
352
308
  // A chunk is already key-sorted, so its first/last row are its min/max key.
353
309
  const make = (rs: Record<string, unknown>[], ts: number[]): BuiltFile => ({
@@ -381,9 +337,7 @@ export function buildFileBuffer(rows: Record<string, unknown>[], times: number[]
381
337
  return result;
382
338
  }
383
339
 
384
- // The raw-splice counterpart of buildFileBuffer, used by merges: the rows already carry their winning
385
- // cells as raw on-disk bytes (no JS values), so this just sorts by key, chunks to ~targetBytes, and
386
- // concatenates the bytes. Same output guarantees: key-contiguous, disjoint, ascending files.
340
+ // The raw-splice counterpart of buildFileBuffer, used by merges: the rows already carry their winning cells as raw on-disk bytes (no JS values), so this just sorts by key, chunks to ~targetBytes, and concatenates the bytes. Same output guarantees: key-contiguous, disjoint, ascending files.
387
341
  export function buildFileBufferRaw(rows: RawRow[], targetBytes = TARGET_FILE_BYTES): BuiltFile[] {
388
342
  const make = (rs: RawRow[]): BuiltFile => ({
389
343
  buffer: buildOneFileRaw(rs),
@@ -410,8 +364,7 @@ export function buildFileBufferRaw(rows: RawRow[], targetBytes = TARGET_FILE_BYT
410
364
  }
411
365
 
412
366
  export type BaseBulkDatabaseReader = {
413
- // Identifies the source this reader came from (the bulk file name, or "(streams)") so the join can
414
- // name the offending file when one of its reads fails. Undefined for readers built without one.
367
+ // Identifies the source this reader came from (the bulk file name, or "(streams)") so the join can name the offending file when one of its reads fails. Undefined for readers built without one.
415
368
  name?: string;
416
369
  rowCount: number;
417
370
  totalBytes: number;
@@ -424,33 +377,23 @@ export type BaseBulkDatabaseReader = {
424
377
  // Keys is special, it's always automatically decoded, even though it is stored as a normal column
425
378
  keys: string[];
426
379
  columns: { column: string; byteSize: number }[];
427
- // Each key's row write-time (the time of its newest write in this reader). The join compares these
428
- // across readers to resolve a key to its latest value.
380
+ // Each key's row write-time (the time of its newest write in this reader). The join compares these across readers to resolve a key to its latest value.
429
381
  keyTimes: Map<string, number>;
430
- // Per-key tombstone time: the key was deleted at this time. The join treats a delete like any other
431
- // event — a delete only wins if it's newer than every set for the key. Only the stream reader sets it.
382
+ // Per-key tombstone time: the key was deleted at this time. The join treats a delete like any other event — a delete only wins if it's newer than every set for the key. Only the stream reader sets it.
432
383
  deleteTimes?: Map<string, number>;
433
- // Each key's value for the column plus the row's write-time. value may be ABSENT (the row never set
434
- // this column — the join then falls through to an older reader for that key/column).
384
+ // Each key's value for the column plus the row's write-time. value may be ABSENT (the row never set this column — the join then falls through to an older reader for that key/column).
435
385
  getColumn: (column: string) => Promise<{ key: string; value: unknown; time: number }[]>;
436
- // Like getColumn but returns each cell's raw on-disk encoding (type tag + bytes) keyed by key, WITHOUT
437
- // decoding the value — for merges, which splice the winning bytes straight into the output. ABSENT
438
- // cells are omitted (a missing key means this reader didn't set this column, so the merge falls
439
- // through to an older reader). Each cell's write-time is the reader's keyTimes value for that key.
386
+ // Like getColumn but returns each cell's raw on-disk encoding (type tag + bytes) keyed by key, WITHOUT decoding the value — for merges, which splice the winning bytes straight into the output. ABSENT cells are omitted (a missing key means this reader didn't set this column, so the merge falls through to an older reader). Each cell's write-time is the reader's keyTimes value for that key.
440
387
  getRawColumn: (column: string) => Promise<Map<string, RawCell>>;
441
- // Returns the column's INDEX (offsets + types) plus a contiguous row-range byte reader. The planned
442
- // merge loads this for every (source, column) once — small (~5B/row), no value bytes pulled — and
443
- // uses types/offsets to plan the output's byte layout. Execute then reads only the needed runs.
388
+ // Returns the column's INDEX (offsets + types) plus a contiguous row-range byte reader. The planned merge loads this for every (source, column) once — small (~5B/row), no value bytes pulled — and uses types/offsets to plan the output's byte layout. Execute then reads only the needed runs.
444
389
  getColumnIndex: (column: string) => Promise<ColumnIndex>;
445
- // Maps a key to its row index in this reader (and undefined if absent). The planned merge uses this
446
- // to look up a winning cell's source row without going through a column read.
390
+ // Maps a key to its row index in this reader (and undefined if absent). The planned merge uses this to look up a winning cell's source row without going through a column read.
447
391
  rowOfKey: (key: string) => number | undefined;
448
392
  // The value + write-time for (key, column), or ABSENT if this reader has no such cell.
449
393
  getSingleField: (key: string, column: string) => Promise<{ value: unknown; time: number } | typeof ABSENT>;
450
394
  };
451
395
 
452
- // Reads just the file header (the 4-byte length + header JSON) — no column data. Used by the merge
453
- // planner to get each file's row count, time range, and key range cheaply across many files.
396
+ // Reads just the file header (the 4-byte length + header JSON) — no column data. Used by the merge planner to get each file's row count, time range, and key range cheaply across many files.
454
397
  export type BulkHeaderInfo = { rowCount: number; minTime: number; maxTime: number; minKey?: string; maxKey?: string; columns: { column: string; byteSize: number }[] };
455
398
  export async function loadBulkHeader(getRange: (start: number, end: number) => Promise<Buffer>, totalBytes: number): Promise<BulkHeaderInfo> {
456
399
  const headerLength = (await getRange(0, 4)).readUInt32LE(0);
@@ -499,8 +442,7 @@ export async function loadBulkDatabase(config: {
499
442
  });
500
443
  const keyIndex = new Map(keys.map((key, i) => [key, i]));
501
444
 
502
- // Per-row write-times. Old files (written before this column existed) fall back to the file's header
503
- // time (or 0) for every row — fine, since such files predate concurrent-time resolution.
445
+ // Per-row write-times. Old files (written before this column existed) fall back to the file's header time (or 0) for every row — fine, since such files predate concurrent-time resolution.
504
446
  const times: number[] = colByName.has(TIME_COLUMN)
505
447
  ? (await readWholeColumn(TIME_COLUMN)).map(v => typeof v === "number" ? v : 0)
506
448
  : keys.map(() => header.maxTime || 0);
@@ -538,8 +480,7 @@ export async function loadBulkDatabase(config: {
538
480
  async getColumnIndex(column) {
539
481
  const col = colByName.get(column);
540
482
  if (!col) {
541
- // File lacks this column — present an all-ABSENT index so the planner can treat it
542
- // uniformly with files that have the column.
483
+ // File lacks this column — present an all-ABSENT index so the planner can treat it uniformly with files that have the column.
543
484
  const offsets = new Uint32Array(rowCount + 1);
544
485
  const types = new Uint8Array(rowCount).fill(TYPE_ABSENT);
545
486
  return {
@@ -550,8 +491,7 @@ export async function loadBulkDatabase(config: {
550
491
  }
551
492
  const colBase = dataBase + col.offset;
552
493
  const indexSize = 4 * (rowCount + 1) + rowCount;
553
- // One read pulls offsets + types (small — ~5 B/row). Block cache makes subsequent value reads
554
- // of nearby rows cheap. Decoded into aligned typed arrays so the executor can do O(1) lookups.
494
+ // One read pulls offsets + types (small — ~5 B/row). Block cache makes subsequent value reads of nearby rows cheap. Decoded into aligned typed arrays so the executor can do O(1) lookups.
555
495
  const indexBuf = await config.getRange(colBase, colBase + indexSize);
556
496
  const offsets = new Uint32Array(rowCount + 1);
557
497
  for (let i = 0; i <= rowCount; i++) offsets[i] = indexBuf.readUInt32LE(4 * i);
@@ -10,13 +10,10 @@ import {
10
10
  TYPE_ABSENT_TAG,
11
11
  } from "./BulkDatabaseFormat";
12
12
 
13
- // Default total in-flight buffer budget for the executor: how much output value-data we hold in memory at
14
- // once. Multiple output files (≤ TARGET_FILE_BYTES each) are built together inside this budget so reading
15
- // inputs once amortizes their I/O across several outputs. Tighter budget → more passes, less memory.
13
+ // Default total in-flight buffer budget for the executor: how much output value-data we hold in memory at once. Multiple output files (≤ TARGET_FILE_BYTES each) are built together inside this budget so reading inputs once amortizes their I/O across several outputs. Tighter budget → more passes, less memory.
16
14
  const DEFAULT_OUTPUT_BATCH_BYTES = 2 * 1024 * 1024 * 1024;
17
15
 
18
- // Per-value on-disk overhead (4-byte offset entry + 1-byte type tag). Used for the partition-cut size
19
- // estimate; the executor doesn't use it (offsets/types are sized exactly from the plan).
16
+ // Per-value on-disk overhead (4-byte offset entry + 1-byte type tag). Used for the partition-cut size estimate; the executor doesn't use it (offsets/types are sized exactly from the plan).
20
17
  const PER_VALUE_OVERHEAD = 5;
21
18
 
22
19
  function fmtBytes(n: number): string {
@@ -26,9 +23,7 @@ function fmtBytes(n: number): string {
26
23
  return (n / 1024 / 1024 / 1024).toFixed(2) + "GB";
27
24
  }
28
25
 
29
- // A contiguous copy operation: take `sourceEndRow - sourceStartRow` consecutive rows of one source
30
- // column, splice the resulting bytes into the output column's data section at outputByteStart. Adjacent
31
- // per-row decisions get RLE'd into one copy op — one source-side read + memcpy instead of one per row.
26
+ // A contiguous copy operation: take `sourceEndRow - sourceStartRow` consecutive rows of one source column, splice the resulting bytes into the output column's data section at outputByteStart. Adjacent per-row decisions get RLE'd into one copy op — one source-side read + memcpy instead of one per row.
32
27
  type CopyRun = {
33
28
  sourceIdx: number;
34
29
  sourceStartRow: number;
@@ -39,8 +34,7 @@ type CopyRun = {
39
34
 
40
35
  type PlannedOutputColumn = {
41
36
  name: string;
42
- // offsets[i]..offsets[i+1] = row i's value byte range in the column's data section. Populated by the
43
- // planner; the executor writes it verbatim into the output column blob.
37
+ // offsets[i]..offsets[i+1] = row i's value byte range in the column's data section. Populated by the planner; the executor writes it verbatim into the output column blob.
44
38
  offsets: Uint32Array;
45
39
  // Per-row type tag (TYPE_ABSENT for fall-through). Populated by the planner.
46
40
  types: Uint8Array;
@@ -71,17 +65,7 @@ export type PlannedMergeOutput = {
71
65
  sources: Map<string, number>;
72
66
  };
73
67
 
74
- // Plan a merge over `sources` and execute it, writing one or more output files via the caller's
75
- // `writeFile`. Two phases:
76
- // • Planning: load only per-column INDEXES (offsets + types — ~5 B/row, small even on 20GB
77
- // collections), determine the winning cell per (live key, column) by newest write-time + non-ABSENT
78
- // fall-through, sort keys, partition into ~targetFileBytes output files, and pre-compute each output
79
- // column's offsets/types arrays plus a run-length-encoded copy plan (no value data touched yet).
80
- // • Execute: group outputs into ~targetBatchBytes batches; per batch, allocate column blobs with
81
- // offsets/types already filled in, then iterate input sources copying contiguous byte runs straight
82
- // into the output buffers. A source with no contribution to this batch is skipped entirely.
83
- // Returns the new output file descriptors plus any tombstones whose newest event is still a delete
84
- // (carried forward when older files outside the merge could still hold a now-stale set for that key).
68
+ // Plan a merge over `sources` and execute it, writing one or more output files via the caller's `writeFile`. Two phases: • Planning: load only per-column INDEXES (offsets + types — ~5 B/row, small even on 20GB collections), determine the winning cell per (live key, column) by newest write-time + non-ABSENT fall-through, sort keys, partition into ~targetFileBytes output files, and pre-compute each output column's offsets/types arrays plus a run-length-encoded copy plan (no value data touched yet). • Execute: group outputs into ~targetBatchBytes batches; per batch, allocate column blobs with offsets/types already filled in, then iterate input sources copying contiguous byte runs straight into the output buffers. A source with no contribution to this batch is skipped entirely. Returns the new output file descriptors plus any tombstones whose newest event is still a delete (carried forward when older files outside the merge could still hold a now-stale set for that key).
85
69
  export async function runPlannedMerge(config: {
86
70
  sources: BaseBulkDatabaseReader[];
87
71
  sourceNames: string[];
@@ -97,11 +81,7 @@ export async function runPlannedMerge(config: {
97
81
  const targetBatchBytes = config.targetBatchBytes ?? DEFAULT_OUTPUT_BATCH_BYTES;
98
82
  const log = config.log ?? (line => console.log(`${blue(config.collectionName)} ${line}`));
99
83
 
100
- // ─────────────────────────────────────────── Phase 1: plan ───────────────────────────────────────────
101
- // Load per-source column indexes FIRST so a file that's gone (deleted between caller's listFiles and
102
- // now) gets caught here and excluded from the merge entirely. Each source's load is wrapped in a
103
- // try/catch; on failure we mark the source bad, log a "skipped" line, and the rest of planning +
104
- // execute pretends the source doesn't exist. The caller only deletes inputs that were actually used.
84
+ // ─────────────────────────────────────────── Phase 1: plan ─────────────────────────────────────────── Load per-source column indexes FIRST so a file that's gone (deleted between caller's listFiles and now) gets caught here and excluded from the merge entirely. Each source's load is wrapped in a try/catch; on failure we mark the source bad, log a "skipped" line, and the rest of planning + execute pretends the source doesn't exist. The caller only deletes inputs that were actually used.
105
85
  const sourceOk = new Array<boolean>(config.sources.length).fill(true);
106
86
  const indexesPerSource: Map<string, ColumnIndex>[] = await Promise.all(config.sources.map(async (src, si) => {
107
87
  const map = new Map<string, ColumnIndex>();
@@ -112,8 +92,7 @@ export async function runPlannedMerge(config: {
112
92
  } catch (e) {
113
93
  sourceOk[si] = false;
114
94
  log(`${magenta("skipped")} ${config.sourceNames[si]}: ${(e as Error).message}`);
115
- // Stack goes straight to console (not the buffered step log) so the merge summary stays
116
- // readable but we can still trace where the failure came from.
95
+ // Stack goes straight to console (not the buffered step log) so the merge summary stays readable but we can still trace where the failure came from.
117
96
  console.log(`${config.collectionName} skipped ${config.sourceNames[si]}:`, (e as Error).stack || e);
118
97
  }
119
98
  return map;
@@ -123,9 +102,7 @@ export async function runPlannedMerge(config: {
123
102
  if (sourceOk[si]) usedSourceNames.add(config.sourceNames[si]);
124
103
  }
125
104
 
126
- // Aggregate keyTimes + deleteTimes across GOOD sources only (max per key). A bad source's cached
127
- // metadata might claim a key as newest, but we can't actually read its cells — letting it win the
128
- // time resolution would just drop that key out of the output.
105
+ // Aggregate keyTimes + deleteTimes across GOOD sources only (max per key). A bad source's cached metadata might claim a key as newest, but we can't actually read its cells — letting it win the time resolution would just drop that key out of the output.
129
106
  const deleteTime = new Map<string, number>();
130
107
  const keyTime = new Map<string, number>();
131
108
  for (let si = 0; si < config.sources.length; si++) {
@@ -143,8 +120,7 @@ export async function runPlannedMerge(config: {
143
120
  }
144
121
  }
145
122
 
146
- // Live keys (newest set strictly newer than newest delete) + tombstones to carry forward (newest
147
- // event is a delete and the merge doesn't include the oldest data — that's the caller's call).
123
+ // Live keys (newest set strictly newer than newest delete) + tombstones to carry forward (newest event is a delete and the merge doesn't include the oldest data — that's the caller's call).
148
124
  const liveKeys: string[] = [];
149
125
  for (const [k, t] of keyTime) {
150
126
  const dT = deleteTime.get(k) ?? -Infinity;
@@ -156,8 +132,7 @@ export async function runPlannedMerge(config: {
156
132
  if (dT >= sT) carriedDeletes.set(k, dT);
157
133
  }
158
134
 
159
- // All distinct value columns from GOOD sources (KEY_COLUMN excluded — it's added at file-assembly
160
- // time). Order: first-seen, matching the existing builders.
135
+ // All distinct value columns from GOOD sources (KEY_COLUMN excluded — it's added at file-assembly time). Order: first-seen, matching the existing builders.
161
136
  const allColumns: string[] = [];
162
137
  const seenCols = new Set<string>();
163
138
  for (let si = 0; si < config.sources.length; si++) {
@@ -169,9 +144,7 @@ export async function runPlannedMerge(config: {
169
144
  }
170
145
  }
171
146
 
172
- // For each live key, for each column, find the winning source: among sources that have this key, the
173
- // one whose keyTime is largest AND whose column-index reports non-ABSENT. Record source + sourceRow +
174
- // byteLen + type so the planner can size offsets and the executor can copy the bytes.
147
+ // For each live key, for each column, find the winning source: among sources that have this key, the one whose keyTime is largest AND whose column-index reports non-ABSENT. Record source + sourceRow + byteLen + type so the planner can size offsets and the executor can copy the bytes.
175
148
  type CellChoice = { sourceIdx: number; sourceRow: number; byteLen: number; type: number };
176
149
  const cellsPerKey = new Map<string, (CellChoice | undefined)[]>();
177
150
  for (const key of liveKeys) {
@@ -206,12 +179,10 @@ export async function runPlannedMerge(config: {
206
179
  cellsPerKey.set(key, cells);
207
180
  }
208
181
 
209
- // Sort live keys lexicographically so each output file is key-contiguous (tight minKey/maxKey + fast
210
- // single-key reads via header skip).
182
+ // Sort live keys lexicographically so each output file is key-contiguous (tight minKey/maxKey + fast single-key reads via header skip).
211
183
  liveKeys.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
212
184
 
213
- // Per-key estimated bytes for partition cutting: KEY_COLUMN cell + per value-column cell (bytes + 5B
214
- // overhead) + TIME_COLUMN cell. Rough — only needs to keep each output near the target.
185
+ // Per-key estimated bytes for partition cutting: KEY_COLUMN cell + per value-column cell (bytes + 5B overhead) + TIME_COLUMN cell. Rough — only needs to keep each output near the target.
215
186
  function keyTotalBytes(key: string): number {
216
187
  const cells = cellsPerKey.get(key)!;
217
188
  let total = key.length * 2 + PER_VALUE_OVERHEAD;
@@ -220,8 +191,7 @@ export async function runPlannedMerge(config: {
220
191
  return total;
221
192
  }
222
193
 
223
- // Walk sorted keys, cut each file when accumulated bytes would exceed targetFileBytes. A single key
224
- // larger than the target still becomes its own (oversized) file — we never split within a key.
194
+ // Walk sorted keys, cut each file when accumulated bytes would exceed targetFileBytes. A single key larger than the target still becomes its own (oversized) file — we never split within a key.
225
195
  const fileKeyRanges: { start: number; end: number }[] = [];
226
196
  {
227
197
  let chunkStart = 0;
@@ -243,9 +213,7 @@ export async function runPlannedMerge(config: {
243
213
 
244
214
  log(`${magenta("plan")}: ${formatNumber(liveKeys.length)} live keys, ${plans.length} output file(s), ${formatNumber(carriedDeletes.size)} tombstones carried`);
245
215
 
246
- // ───────────────────────────────────────── Phase 2: execute ──────────────────────────────────────────
247
- // Group output files into batches that fit within targetBatchBytes so we read inputs once per batch
248
- // (and skip inputs that contribute nothing to this batch).
216
+ // ───────────────────────────────────────── Phase 2: execute ────────────────────────────────────────── Group output files into batches that fit within targetBatchBytes so we read inputs once per batch (and skip inputs that contribute nothing to this batch).
249
217
  const batches: PlannedOutputFile[][] = [];
250
218
  {
251
219
  let start = 0;
@@ -305,9 +273,7 @@ function buildOutputPlan(
305
273
  }
306
274
  types[i] = cell.type;
307
275
  sourceCounts.set(cell.sourceIdx, (sourceCounts.get(cell.sourceIdx) ?? 0) + 1);
308
- // RLE: extend the current run iff the next cell is the next row of the same source (its
309
- // source-side bytes sit immediately after, so one read suffices) AND non-ABSENT (we never
310
- // landed in the break-path above).
276
+ // RLE: extend the current run iff the next cell is the next row of the same source (its source-side bytes sit immediately after, so one read suffices) AND non-ABSENT (we never landed in the break-path above).
311
277
  if (currentRun && currentRun.sourceIdx === cell.sourceIdx && currentRun.sourceEndRow === cell.sourceRow) {
312
278
  currentRun.sourceEndRow = cell.sourceRow + 1;
313
279
  currentRun.byteLength += cell.byteLen;
@@ -329,8 +295,7 @@ function buildOutputPlan(
329
295
  return { name: colName, offsets, types, dataLength: outputByte, runs };
330
296
  });
331
297
 
332
- // File size estimate: KEY_COLUMN blob + value column blobs + TIME_COLUMN blob + header guess. Used to
333
- // batch output files; only needs to be close, not exact.
298
+ // File size estimate: KEY_COLUMN blob + value column blobs + TIME_COLUMN blob + header guess. Used to batch output files; only needs to be close, not exact.
334
299
  let estimatedFileBytes = 4 + 2048;
335
300
  let keyBytes = 0;
336
301
  for (const k of keys) keyBytes += k.length * 2;
@@ -357,8 +322,7 @@ async function executeBatch(
357
322
  writeFile: (data: Buffer) => Promise<{ name: string; size: number }>,
358
323
  log: (line: string) => void,
359
324
  ): Promise<PlannedMergeOutput[]> {
360
- // Allocate one big buffer per (output file, column) — offsets + types are written immediately from
361
- // the plan, the data section is filled below by the per-source copy loop.
325
+ // Allocate one big buffer per (output file, column) — offsets + types are written immediately from the plan, the data section is filled below by the per-source copy loop.
362
326
  type ColumnBuffer = { name: string; blob: Buffer; dataStart: number };
363
327
  const blobsPerFile: ColumnBuffer[][] = plans.map(plan => plan.columns.map(col => {
364
328
  const indexSize = columnIndexByteLength(col.offsets.length - 1);
@@ -368,9 +332,7 @@ async function executeBatch(
368
332
  return { name: col.name, blob, dataStart: indexSize };
369
333
  }));
370
334
 
371
- // For each source, copy its byte runs into every output column whose plan references it. A source
372
- // that's not referenced by any column in this batch is skipped entirely — that's the point of the
373
- // batch shape: read 20GB of inputs only once for the whole pass, not once per output.
335
+ // For each source, copy its byte runs into every output column whose plan references it. A source that's not referenced by any column in this batch is skipped entirely — that's the point of the batch shape: read 20GB of inputs only once for the whole pass, not once per output.
374
336
  for (let si = 0; si < sources.length; si++) {
375
337
  let contributes = false;
376
338
  for (let fi = 0; fi < plans.length && !contributes; fi++) {
@@ -21,10 +21,7 @@ export type ReaderConfig = {
21
21
  maxTriggerThrottleMs?: number;
22
22
  };
23
23
 
24
- // Owns the current LoadedIndex and WriteOverlay and serves every read (async + sync reactive). Apply
25
- // writes/deletes through applyWrite/applyDelete so the right signals fire. The host swaps in a fresh
26
- // LoadedIndex atomically via setIndex once the new one has fully built — no lazy rebuild on the next
27
- // read, no synchronous lag spike.
24
+ // Owns the current LoadedIndex and WriteOverlay and serves every read (async + sync reactive). Apply writes/deletes through applyWrite/applyDelete so the right signals fire. The host swaps in a fresh LoadedIndex atomically via setIndex once the new one has fully built — no lazy rebuild on the next read, no synchronous lag spike.
28
25
  export class BulkDatabaseReader<T extends { key: string }> {
29
26
  constructor(private readonly cfg: ReaderConfig) { }
30
27
 
@@ -89,8 +86,7 @@ export class BulkDatabaseReader<T extends { key: string }> {
89
86
  return -Infinity;
90
87
  }
91
88
 
92
- // Counter (so concurrent / nested merges in the same instance compose correctly). Signal fires
93
- // immediately, not through the trigger throttle — UI spinners should show right away.
89
+ // Counter (so concurrent / nested merges in the same instance compose correctly). Signal fires immediately, not through the trigger throttle — UI spinners should show right away.
94
90
  private compactingCount = 0;
95
91
  beginCompaction(): void {
96
92
  this.compactingCount++;
@@ -247,9 +243,7 @@ export class BulkDatabaseReader<T extends { key: string }> {
247
243
  return false;
248
244
  }
249
245
 
250
- // ── helpers ──────────────────────────────────────────────────────────────────────────────────────
251
- // Both async reads await this. The host wires an `ensureIndex` that triggers the initial build if
252
- // no index is loaded yet — reads that race the first load all wait on the same promise.
246
+ // ── helpers ────────────────────────────────────────────────────────────────────────────────────── Both async reads await this. The host wires an `ensureIndex` that triggers the initial build if no index is loaded yet — reads that race the first load all wait on the same promise.
253
247
  setEnsureIndex(fn: () => Promise<LoadedIndex<T>>): void { this.ensureIndexFn = fn; }
254
248
  private ensureIndexFn: (() => Promise<LoadedIndex<T>>) | undefined;
255
249
  private async requireIndex(): Promise<LoadedIndex<T>> {
@@ -262,9 +256,7 @@ export class BulkDatabaseReader<T extends { key: string }> {
262
256
  return `(collection has ${blue(formatNumber(index.reader.rowCount))} rows, ${blue(formatNumber(index.reader.totalBytes))}B)`;
263
257
  }
264
258
 
265
- // Notifications are rampingly delayed under sustained changes (so a high-rate writer can't re-run
266
- // watchers per change). Underlying data is always current — only the observable notification is
267
- // batched/delayed.
259
+ // Notifications are rampingly delayed under sustained changes (so a high-rate writer can't re-run watchers per change). Underlying data is always current — only the observable notification is batched/delayed.
268
260
  private invalidateSignal(signal: string): void {
269
261
  const maxMs = this.cfg.maxTriggerThrottleMs ?? 0;
270
262
  if (maxMs <= 0) { this.cfg.deps.invalidate(signal); return; }
@@ -39,10 +39,7 @@ function nullJoin(a: string, b: string): string {
39
39
  return a + String.fromCharCode(0) + b;
40
40
  }
41
41
 
42
- // A loaded snapshot of one collection's on-disk state: the resolved reader + the per-file decoded sub-
43
- // readers it stitched, the stream file sizes/times it observed, and the sync (reactive) caches that
44
- // surface column/field reads. Built once via LoadedIndex.build — eagerly, so the swap into a fresh
45
- // LoadedIndex never blocks a read on a synchronous rebuild.
42
+ // A loaded snapshot of one collection's on-disk state: the resolved reader + the per-file decoded sub- readers it stitched, the stream file sizes/times it observed, and the sync (reactive) caches that surface column/field reads. Built once via LoadedIndex.build — eagerly, so the swap into a fresh LoadedIndex never blocks a read on a synchronous rebuild.
46
43
  export class LoadedIndex<T extends { key: string }> {
47
44
  private constructor(
48
45
  public readonly name: string,
@@ -67,8 +64,7 @@ export class LoadedIndex<T extends { key: string }> {
67
64
  private baseColumnsLoading = new Set<string>();
68
65
  private baseFields = new Map<string, { value: unknown; time: number } | undefined>();
69
66
  private baseFieldsLoading = new Set<string>();
70
- // Last-known values from the previous index, served until this index loads its own fresh value, so
71
- // a swap doesn't flash empty in sync reads. Cleared per-entry when the fresh value lands.
67
+ // Last-known values from the previous index, served until this index loads its own fresh value, so a swap doesn't flash empty in sync reads. Cleared per-entry when the fresh value lands.
72
68
  private staleBaseColumns = new Map<string, { key: string; value: unknown; time: number }[]>();
73
69
  private staleBaseFields = new Map<string, { value: unknown; time: number } | undefined>();
74
70
 
@@ -150,9 +146,7 @@ export class LoadedIndex<T extends { key: string }> {
150
146
  }
151
147
  }
152
148
 
153
- // Adopts the previous index's loaded base values as STALE on this one, so a sync read mid-swap can
154
- // serve the old value (patched with the current overlay) instead of flashing empty while the fresh
155
- // load runs. Per-entry: dropped when the fresh value lands here.
149
+ // Adopts the previous index's loaded base values as STALE on this one, so a sync read mid-swap can serve the old value (patched with the current overlay) instead of flashing empty while the fresh load runs. Per-entry: dropped when the fresh value lands here.
156
150
  inheritStaleFrom(prev: LoadedIndex<T>): void {
157
151
  for (const [k, v] of prev.baseColumns) this.staleBaseColumns.set(k, v);
158
152
  for (const [k, v] of prev.staleBaseColumns) if (!this.staleBaseColumns.has(k)) this.staleBaseColumns.set(k, v);
@@ -203,9 +197,7 @@ export class LoadedIndex<T extends { key: string }> {
203
197
  })();
204
198
  }
205
199
 
206
- // Returns the loaded value (fresh if available, else last-known stale) for the column, or undefined
207
- // if neither is present yet. The boolean indicates whether the returned value is fresh; callers may
208
- // skip caching a stale value.
200
+ // Returns the loaded value (fresh if available, else last-known stale) for the column, or undefined if neither is present yet. The boolean indicates whether the returned value is fresh; callers may skip caching a stale value.
209
201
  getBaseColumn(column: string): { entries: { key: string; value: unknown; time: number }[]; fresh: boolean } | undefined {
210
202
  const fresh = this.baseColumns.get(column);
211
203
  if (fresh) return { entries: fresh, fresh: true };
@@ -230,8 +222,7 @@ export class LoadedIndex<T extends { key: string }> {
230
222
  return this.baseFields.has(ck) || this.staleBaseFields.has(ck);
231
223
  }
232
224
 
233
- // Drops in-memory loaded values for this index, so a subsequent read reloads fresh from disk. Used
234
- // by reloadFromDisk() — a hard reset, no stale fallback survives.
225
+ // Drops in-memory loaded values for this index, so a subsequent read reloads fresh from disk. Used by reloadFromDisk() — a hard reset, no stale fallback survives.
235
226
  dropLoadedValues(): void {
236
227
  this.baseColumns.clear();
237
228
  this.baseColumnsLoading.clear();
@@ -330,10 +321,7 @@ function pruneSubCaches(bulkFiles: BulkFileInfo[], streamFiles: StreamFileInfo[]
330
321
  for (const n of subCaches.stream.keys()) if (!liveStream.has(n)) subCaches.stream.delete(n);
331
322
  }
332
323
 
333
- // A corrupt/unreadable column in ONE underlying file must not break the whole joined read. When a
334
- // source's column read throws, we drop that source for that column (falling through to older readers,
335
- // exactly as if the file never set the column) and warn once per (file, column) so the corruption is
336
- // visible without flooding the log on every rebuild.
324
+ // A corrupt/unreadable column in ONE underlying file must not break the whole joined read. When a source's column read throws, we drop that source for that column (falling through to older readers, exactly as if the file never set the column) and warn once per (file, column) so the corruption is visible without flooding the log on every rebuild.
337
325
  const warnedCorruptReads = new Set<string>();
338
326
  function warnCorruptRead(db: BaseBulkDatabaseReader, column: string, e: unknown): void {
339
327
  const path = db.name || "(unknown source)";
@@ -2,10 +2,7 @@ export const DELETED = Symbol("deleted");
2
2
 
3
3
  export type OverlayEntry = { time: number; value: Record<string, unknown> | typeof DELETED };
4
4
 
5
- // In-memory pending mutations layered on top of a LoadedIndex. Each entry carries the write's unique
6
- // timestamp so cross-tab writes can be ordered + the swap can drop entries the new index already
7
- // covers. Methods that mutate report which columns invalidate ("all" if the key's liveness flipped),
8
- // so the host can drop the right caches without scanning every column.
5
+ // In-memory pending mutations layered on top of a LoadedIndex. Each entry carries the write's unique timestamp so cross-tab writes can be ordered + the swap can drop entries the new index already covers. Methods that mutate report which columns invalidate ("all" if the key's liveness flipped), so the host can drop the right caches without scanning every column.
9
6
  export class WriteOverlay {
10
7
  private entries = new Map<string, OverlayEntry>();
11
8
 
@@ -29,16 +26,14 @@ export class WriteOverlay {
29
26
 
30
27
  clear(): void { this.entries.clear(); }
31
28
 
32
- // Drop entries the new index already reflects (its keyTime or deleteTime ≥ the entry's time). The
33
- // remaining entries are writes the index doesn't yet see — they have to keep serving reads.
29
+ // Drop entries the new index already reflects (its keyTime or deleteTime ≥ the entry's time). The remaining entries are writes the index doesn't yet see — they have to keep serving reads.
34
30
  sweepCovered(authority: (key: string) => number): void {
35
31
  for (const [key, entry] of this.entries) {
36
32
  if (entry.time <= authority(key)) this.entries.delete(key);
37
33
  }
38
34
  }
39
35
 
40
- // Apply this overlay onto the base column entries. A column-unset overlay entry leaves the base
41
- // value+time in place (a partial write only overrides the columns it set).
36
+ // Apply this overlay onto the base column entries. A column-unset overlay entry leaves the base value+time in place (a partial write only overrides the columns it set).
42
37
  patchColumn(base: { key: string; value: unknown; time: number }[], column: string): { key: string; value: unknown; time: number }[] {
43
38
  if (this.entries.size === 0) return base;
44
39
  const map = new Map(base.map(e => [e.key, { value: e.value, time: e.time }]));