sliftutils 1.4.47 → 1.4.48

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/index.d.ts CHANGED
@@ -1141,7 +1141,13 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseFormat" {
1141
1141
  export declare const EMPTY_BUFFER: Buffer;
1142
1142
  export declare const ABSENT: unique symbol;
1143
1143
  export declare const TARGET_FILE_BYTES: number;
1144
- export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): Buffer[];
1144
+ export interface BuiltFile {
1145
+ buffer: Buffer;
1146
+ minKey: string;
1147
+ maxKey: string;
1148
+ rowCount: number;
1149
+ }
1150
+ export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
1145
1151
  export type BaseBulkDatabaseReader = {
1146
1152
  rowCount: number;
1147
1153
  totalBytes: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.4.47",
3
+ "version": "1.4.48",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -929,9 +929,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
929
929
  const timestamp = nextFileTime();
930
930
  const now = Date.now();
931
931
  const times = rows.map(() => now);
932
- for (const buffer of buildFileBuffer(rows, times)) {
932
+ for (const built of buildFileBuffer(rows, times)) {
933
933
  const name = newFileName(timestamp);
934
- await storage.set(name, encodeCompressedBlocks(buffer));
934
+ await storage.set(name, encodeCompressedBlocks(built.buffer));
935
935
  }
936
936
  this.resetReader();
937
937
  void this.maybeMerge();
@@ -1270,10 +1270,19 @@ export class BulkDatabaseBase<T extends { key: string }> {
1270
1270
  // Write all outputs BEFORE deleting any input, so a throw mid-write just leaves duplicates.
1271
1271
  const newNames: string[] = [];
1272
1272
  if (rows.length) {
1273
- for (const buffer of buildFileBuffer(rows, times)) {
1273
+ const built = buildFileBuffer(rows, times);
1274
+ // When the result is big enough to split across several files, log each sub-write (its key
1275
+ // range + start/finish), since each storage.set can be large/slow and we want to see progress.
1276
+ const split = built.length > 1;
1277
+ if (split) console.log(`${blue(this.name)} merge: output split into ${built.length} files (> ${fmtBytes(TARGET_FILE_BYTES)} each)`);
1278
+ for (let i = 0; i < built.length; i++) {
1279
+ const part = built[i];
1274
1280
  const name = newFileName(timestamp);
1275
- await storage.set(name, encodeCompressedBlocks(buffer));
1281
+ const subStart = Date.now();
1282
+ if (split) console.log(` [${i + 1}/${built.length}] writing ${formatNumber(part.rowCount)} rows [${part.minKey} .. ${part.maxKey}] → ${name} at ${new Date(subStart).toISOString()}`);
1283
+ await storage.set(name, encodeCompressedBlocks(part.buffer));
1276
1284
  newNames.push(name);
1285
+ if (split) console.log(` [${i + 1}/${built.length}] wrote ${name} (${fmtBytes((await storage.getInfo(name).catch(() => undefined))?.size ?? 0)}) in ${formatTime(Date.now() - subStart)}`);
1277
1286
  }
1278
1287
  }
1279
1288
  // Carry surviving tombstones forward only if older files exist outside this merge that they still
@@ -4,7 +4,13 @@ export declare const KEY_COLUMN = "key";
4
4
  export declare const EMPTY_BUFFER: Buffer;
5
5
  export declare const ABSENT: unique symbol;
6
6
  export declare const TARGET_FILE_BYTES: number;
7
- export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): Buffer[];
7
+ export interface BuiltFile {
8
+ buffer: Buffer;
9
+ minKey: string;
10
+ maxKey: string;
11
+ rowCount: number;
12
+ }
13
+ export declare function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes?: number): BuiltFile[];
8
14
  export type BaseBulkDatabaseReader = {
9
15
  rowCount: number;
10
16
  totalBytes: number;
@@ -226,14 +226,25 @@ function buildOneFile(rows: Record<string, unknown>[], times: number[]): Buffer
226
226
  return Buffer.concat([lengthPrefix, headerBuf, ...blobs]);
227
227
  }
228
228
 
229
- // Returns one complete, independent file buffer per chunk of rows. Rows are first sorted by key, then
229
+ // One complete, independent file: the encoded buffer plus its key range + row count (the caller logs the
230
+ // range when a merge splits across several files).
231
+ export interface BuiltFile { buffer: Buffer; minKey: string; maxKey: string; rowCount: number; }
232
+
233
+ // Returns one complete, independent file per chunk of rows. Rows are first sorted by key, then
230
234
  // partitioned into key-contiguous chunks of ~targetBytes each — so every returned file is key-sorted
231
235
  // (tight minKey/maxKey for the read-skip + merge planner) and stays near the target size, and no
232
236
  // single column blob / Buffer.concat approaches the ~2GB limit. The chunks have disjoint key ranges,
233
- // so the caller just writes each as its own file. A normal-sized write returns a single buffer.
237
+ // so the caller just writes each as its own file. A normal-sized write returns a single file.
234
238
  // `times[i]` is row i's write-time, stored per row so reads resolve a key to its latest value by time.
235
- export function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes = TARGET_FILE_BYTES): Buffer[] {
236
- if (rows.length === 0) return [buildOneFile([], [])];
239
+ export function buildFileBuffer(rows: Record<string, unknown>[], times: number[], targetBytes = TARGET_FILE_BYTES): BuiltFile[] {
240
+ // A chunk is already key-sorted, so its first/last row are its min/max key.
241
+ const make = (rs: Record<string, unknown>[], ts: number[]): BuiltFile => ({
242
+ buffer: buildOneFile(rs, ts),
243
+ minKey: rs.length ? (rs[0][KEY_COLUMN] as string) : "",
244
+ maxKey: rs.length ? (rs[rs.length - 1][KEY_COLUMN] as string) : "",
245
+ rowCount: rs.length,
246
+ });
247
+ if (rows.length === 0) return [make([], [])];
237
248
  // Sort rows + their times together by key so each output file is key-contiguous.
238
249
  const order = rows.map((_, i) => i).sort((a, b) => {
239
250
  const ka = rows[a][KEY_COLUMN] as string;
@@ -242,19 +253,19 @@ export function buildFileBuffer(rows: Record<string, unknown>[], times: number[]
242
253
  });
243
254
  const sortedRows = order.map(i => rows[i]);
244
255
  const sortedTimes = order.map(i => times[i]);
245
- const result: Buffer[] = [];
256
+ const result: BuiltFile[] = [];
246
257
  let chunkStart = 0;
247
258
  let chunkBytes = 0;
248
259
  for (let i = 0; i < sortedRows.length; i++) {
249
260
  const rowBytes = estimateRowBytes(sortedRows[i]);
250
261
  if (i > chunkStart && chunkBytes + rowBytes > targetBytes) {
251
- result.push(buildOneFile(sortedRows.slice(chunkStart, i), sortedTimes.slice(chunkStart, i)));
262
+ result.push(make(sortedRows.slice(chunkStart, i), sortedTimes.slice(chunkStart, i)));
252
263
  chunkStart = i;
253
264
  chunkBytes = 0;
254
265
  }
255
266
  chunkBytes += rowBytes;
256
267
  }
257
- result.push(buildOneFile(sortedRows.slice(chunkStart), sortedTimes.slice(chunkStart)));
268
+ result.push(make(sortedRows.slice(chunkStart), sortedTimes.slice(chunkStart)));
258
269
  return result;
259
270
  }
260
271