sliftutils 1.4.15 → 1.4.16

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
@@ -944,6 +944,8 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
944
944
  private automaticCompactionAllowed;
945
945
  private overlay;
946
946
  private streamTimes;
947
+ private columnCache;
948
+ private dataGen;
947
949
  private streamRowsOnDisk;
948
950
  private streamBytesOnDisk;
949
951
  private streamFileName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.4.15",
3
+ "version": "1.4.16",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -40,6 +40,8 @@ export declare class BulkDatabaseBase<T extends {
40
40
  private automaticCompactionAllowed;
41
41
  private overlay;
42
42
  private streamTimes;
43
+ private columnCache;
44
+ private dataGen;
43
45
  private streamRowsOnDisk;
44
46
  private streamBytesOnDisk;
45
47
  private streamFileName;
@@ -278,6 +278,17 @@ export class BulkDatabaseBase<T extends { key: string }> {
278
278
  // overlay to decide whether an incoming remote write is actually newer than what we have.
279
279
  private streamTimes = new Map<string, number>();
280
280
 
281
+ // Cache of fully-resolved (overlay-patched) column results, keyed by column name. The result only
282
+ // changes when the overlay mutates or the reader resets, so we keep it until then — repeat whole-column
283
+ // reads (common in a UI re-render) are then free, however large the column. Each cached array is frozen
284
+ // SHALLOWLY: Object.freeze locks the array itself (callers can't mutate a shared result) but never the
285
+ // element objects or their values, so typed-array column values keep their fast representation.
286
+ private columnCache = new Map<string, { key: string; value: unknown; time: number }[]>();
287
+ // Bumped on every overlay mutation / reader reset. An async column build captures it before its awaits
288
+ // and only caches its result if it hasn't changed since — so a write or reset mid-build (which clears
289
+ // the cache and may swap the reader) can never leave a stale entry behind.
290
+ private dataGen = 0;
291
+
281
292
  // Approximate size of the tier-0 stream data on disk: set accurately from the last reader build, then
282
293
  // kept current as each flush appends more. Drives the stream-fold trigger (see streamNeedsFold);
283
294
  // reset on resetReader, since after a merge/reset the next reader build re-measures it.
@@ -304,6 +315,8 @@ export class BulkDatabaseBase<T extends { key: string }> {
304
315
  }
305
316
 
306
317
  private invalidateOverlay(key: string) {
318
+ this.dataGen++;
319
+ this.columnCache.clear();
307
320
  this.deps.invalidate(key);
308
321
  this.deps.invalidate(OVERLAY_SIGNAL);
309
322
  }
@@ -435,6 +448,8 @@ export class BulkDatabaseBase<T extends { key: string }> {
435
448
  this.baseFields.clear();
436
449
  this.baseFieldsLoading.clear();
437
450
  this.overlay.clear();
451
+ this.dataGen++;
452
+ this.columnCache.clear();
438
453
  // The next reader build re-measures the stream; clear the estimate so a just-folded stream
439
454
  // doesn't keep looking "heavy" and re-trigger a fold before then.
440
455
  this.streamRowsOnDisk = 0;
@@ -1134,14 +1149,21 @@ export class BulkDatabaseBase<T extends { key: string }> {
1134
1149
 
1135
1150
  public async getColumn<Column extends keyof T>(column: Column): Promise<{ key: string; value: T[Column]; time: number }[]> {
1136
1151
  void this.syncSetup();
1152
+ const col = String(column);
1153
+ const cached = this.columnCache.get(col);
1154
+ if (cached) return cached as { key: string; value: T[Column]; time: number }[];
1155
+ const gen = this.dataGen;
1137
1156
  let time = Date.now();
1138
1157
  let reader = await this.reader();
1139
- let base = await reader.getColumn(String(column));
1140
- let result = this.patchColumn(base, String(column)) as { key: string; value: T[Column]; time: number }[];
1158
+ let base = await reader.getColumn(col);
1159
+ let result = this.patchColumn(base, col) as { key: string; value: T[Column]; time: number }[];
1141
1160
  time = Date.now() - time;
1142
1161
  if (time > 50) {
1143
1162
  console.log(`${blue(`${this.name}.getColumn(${JSON.stringify(column)})`)} took ${red(formatTime(time))} ${this.formatInfo(reader)}`);
1144
1163
  }
1164
+ Object.freeze(result);
1165
+ // Only cache if no write/reset happened during the awaits above (else this result may be stale).
1166
+ if (this.dataGen === gen) this.columnCache.set(col, result);
1145
1167
  return result;
1146
1168
  }
1147
1169
 
@@ -1236,12 +1258,19 @@ export class BulkDatabaseBase<T extends { key: string }> {
1236
1258
  // Observe the overlay-wide signal so we recompute once the base arrives or the overlay changes.
1237
1259
  this.deps.observe(OVERLAY_SIGNAL);
1238
1260
  let col = String(column);
1261
+ const cached = this.columnCache.get(col);
1262
+ if (cached) return cached as { key: string; value: T[Column]; time: number }[];
1239
1263
  let base = this.baseColumns.get(col);
1240
1264
  if (!base) {
1241
1265
  this.ensureBaseColumn(col);
1242
1266
  return undefined;
1243
1267
  }
1244
- return this.patchColumn(base, col) as { key: string; value: T[Column]; time: number }[];
1268
+ // Synchronous (no awaits) reads the current overlay and caches it atomically; an observer re-runs
1269
+ // (and the cache is cleared) on any later change, so the frozen result is safe to share.
1270
+ let result = this.patchColumn(base, col) as { key: string; value: T[Column]; time: number }[];
1271
+ Object.freeze(result);
1272
+ this.columnCache.set(col, result);
1273
+ return result;
1245
1274
  }
1246
1275
 
1247
1276
  public async getColumnInfo() {