sliftutils 1.4.15 → 1.4.17
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
|
@@ -763,9 +763,9 @@ declare module "sliftutils/render-utils/observer" {
|
|
|
763
763
|
}
|
|
764
764
|
|
|
765
765
|
declare module "sliftutils/storage/BulkDatabase2/BulkDatabase2" {
|
|
766
|
-
import { BulkDatabaseBase } from "./BulkDatabaseBase";
|
|
766
|
+
import { BulkDatabaseBase, BulkDatabase2Config } from "./BulkDatabaseBase";
|
|
767
767
|
export { BulkDatabaseBase, noopReactiveDeps, bulkDatabase2Timing } from "./BulkDatabaseBase";
|
|
768
|
-
export type { ReactiveDeps, StorageFactory } from "./BulkDatabaseBase";
|
|
768
|
+
export type { ReactiveDeps, StorageFactory, BulkDatabase2Config } from "./BulkDatabaseBase";
|
|
769
769
|
/** Per-column on-disk size info, as reported by getColumnInfo/getReaderInfo. */
|
|
770
770
|
export type BulkColumnInfo = {
|
|
771
771
|
column: string;
|
|
@@ -896,7 +896,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabase2" {
|
|
|
896
896
|
export declare class BulkDatabase2<T extends {
|
|
897
897
|
key: string;
|
|
898
898
|
}> extends BulkDatabaseBase<T> implements IBulkDatabase2<T> {
|
|
899
|
-
constructor(name: string);
|
|
899
|
+
constructor(name: string, config?: BulkDatabase2Config);
|
|
900
900
|
}
|
|
901
901
|
|
|
902
902
|
}
|
|
@@ -920,13 +920,17 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
920
920
|
}
|
|
921
921
|
export declare const noopReactiveDeps: ReactiveDeps;
|
|
922
922
|
export type StorageFactory = (path: string) => Promise<FileStorage>;
|
|
923
|
+
export type BulkDatabase2Config = {
|
|
924
|
+
triggerThrottleMs?: number;
|
|
925
|
+
};
|
|
923
926
|
export declare class BulkDatabaseBase<T extends {
|
|
924
927
|
key: string;
|
|
925
928
|
}> {
|
|
926
929
|
readonly name: string;
|
|
927
930
|
protected deps: ReactiveDeps;
|
|
928
931
|
private storageFactory;
|
|
929
|
-
|
|
932
|
+
private config;
|
|
933
|
+
constructor(name: string, deps: ReactiveDeps, storageFactory: StorageFactory, config?: BulkDatabase2Config);
|
|
930
934
|
private pendingAppends;
|
|
931
935
|
private flushTimer;
|
|
932
936
|
private flushChain;
|
|
@@ -944,12 +948,22 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
944
948
|
private automaticCompactionAllowed;
|
|
945
949
|
private overlay;
|
|
946
950
|
private streamTimes;
|
|
951
|
+
private columnCache;
|
|
952
|
+
private readerKeys;
|
|
953
|
+
private dataGen;
|
|
954
|
+
private pendingSignals;
|
|
955
|
+
private triggerTimer;
|
|
956
|
+
private currentTriggerDelay;
|
|
957
|
+
private lastTriggerTime;
|
|
947
958
|
private streamRowsOnDisk;
|
|
948
959
|
private streamBytesOnDisk;
|
|
949
960
|
private streamFileName;
|
|
950
961
|
private lastMergeCheck;
|
|
951
962
|
private getStreamFileName;
|
|
952
963
|
private invalidateOverlay;
|
|
964
|
+
private isLiveNow;
|
|
965
|
+
private invalidateSignal;
|
|
966
|
+
private flushSignals;
|
|
953
967
|
private setOverlayRow;
|
|
954
968
|
private setOverlayDeleted;
|
|
955
969
|
private reader;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BulkDatabaseBase } from "./BulkDatabaseBase";
|
|
1
|
+
import { BulkDatabaseBase, BulkDatabase2Config } from "./BulkDatabaseBase";
|
|
2
2
|
export { BulkDatabaseBase, noopReactiveDeps, bulkDatabase2Timing } from "./BulkDatabaseBase";
|
|
3
|
-
export type { ReactiveDeps, StorageFactory } from "./BulkDatabaseBase";
|
|
3
|
+
export type { ReactiveDeps, StorageFactory, BulkDatabase2Config } from "./BulkDatabaseBase";
|
|
4
4
|
/** Per-column on-disk size info, as reported by getColumnInfo/getReaderInfo. */
|
|
5
5
|
export type BulkColumnInfo = {
|
|
6
6
|
column: string;
|
|
@@ -131,5 +131,5 @@ export interface IBulkDatabase2<T extends {
|
|
|
131
131
|
export declare class BulkDatabase2<T extends {
|
|
132
132
|
key: string;
|
|
133
133
|
}> extends BulkDatabaseBase<T> implements IBulkDatabase2<T> {
|
|
134
|
-
constructor(name: string);
|
|
134
|
+
constructor(name: string, config?: BulkDatabase2Config);
|
|
135
135
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getFileStorageNested2 } from "../FileFolderAPI";
|
|
2
2
|
import { observable, runInAction } from "../../render-utils/mobxTyped";
|
|
3
|
-
import { BulkDatabaseBase, ReactiveDeps } from "./BulkDatabaseBase";
|
|
3
|
+
import { BulkDatabaseBase, ReactiveDeps, BulkDatabase2Config } from "./BulkDatabaseBase";
|
|
4
4
|
|
|
5
5
|
export { BulkDatabaseBase, noopReactiveDeps, bulkDatabase2Timing } from "./BulkDatabaseBase";
|
|
6
|
-
export type { ReactiveDeps, StorageFactory } from "./BulkDatabaseBase";
|
|
6
|
+
export type { ReactiveDeps, StorageFactory, BulkDatabase2Config } from "./BulkDatabaseBase";
|
|
7
7
|
|
|
8
8
|
/** Per-column on-disk size info, as reported by getColumnInfo/getReaderInfo. */
|
|
9
9
|
export type BulkColumnInfo = { column: string; byteSize: number };
|
|
@@ -138,7 +138,7 @@ class MobxReactiveDeps implements ReactiveDeps {
|
|
|
138
138
|
// this just supplies mobx reactivity and the default (getFileStorageNested2) storage backend, so the
|
|
139
139
|
// sync reads (getSingleFieldSync / getColumnSync) stay observable for mobx components.
|
|
140
140
|
export class BulkDatabase2<T extends { key: string }> extends BulkDatabaseBase<T> implements IBulkDatabase2<T> {
|
|
141
|
-
constructor(name: string) {
|
|
142
|
-
super(name, new MobxReactiveDeps(), getFileStorageNested2);
|
|
141
|
+
constructor(name: string, config?: BulkDatabase2Config) {
|
|
142
|
+
super(name, new MobxReactiveDeps(), getFileStorageNested2, config);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -16,13 +16,17 @@ export interface ReactiveDeps {
|
|
|
16
16
|
}
|
|
17
17
|
export declare const noopReactiveDeps: ReactiveDeps;
|
|
18
18
|
export type StorageFactory = (path: string) => Promise<FileStorage>;
|
|
19
|
+
export type BulkDatabase2Config = {
|
|
20
|
+
triggerThrottleMs?: number;
|
|
21
|
+
};
|
|
19
22
|
export declare class BulkDatabaseBase<T extends {
|
|
20
23
|
key: string;
|
|
21
24
|
}> {
|
|
22
25
|
readonly name: string;
|
|
23
26
|
protected deps: ReactiveDeps;
|
|
24
27
|
private storageFactory;
|
|
25
|
-
|
|
28
|
+
private config;
|
|
29
|
+
constructor(name: string, deps: ReactiveDeps, storageFactory: StorageFactory, config?: BulkDatabase2Config);
|
|
26
30
|
private pendingAppends;
|
|
27
31
|
private flushTimer;
|
|
28
32
|
private flushChain;
|
|
@@ -40,12 +44,22 @@ export declare class BulkDatabaseBase<T extends {
|
|
|
40
44
|
private automaticCompactionAllowed;
|
|
41
45
|
private overlay;
|
|
42
46
|
private streamTimes;
|
|
47
|
+
private columnCache;
|
|
48
|
+
private readerKeys;
|
|
49
|
+
private dataGen;
|
|
50
|
+
private pendingSignals;
|
|
51
|
+
private triggerTimer;
|
|
52
|
+
private currentTriggerDelay;
|
|
53
|
+
private lastTriggerTime;
|
|
43
54
|
private streamRowsOnDisk;
|
|
44
55
|
private streamBytesOnDisk;
|
|
45
56
|
private streamFileName;
|
|
46
57
|
private lastMergeCheck;
|
|
47
58
|
private getStreamFileName;
|
|
48
59
|
private invalidateOverlay;
|
|
60
|
+
private isLiveNow;
|
|
61
|
+
private invalidateSignal;
|
|
62
|
+
private flushSignals;
|
|
49
63
|
private setOverlayRow;
|
|
50
64
|
private setOverlayDeleted;
|
|
51
65
|
private reader;
|
|
@@ -129,6 +129,22 @@ export const noopReactiveDeps: ReactiveDeps = {
|
|
|
129
129
|
// file needn't know about getFileStorageNested2 / the browser-vs-node storage details).
|
|
130
130
|
export type StorageFactory = (path: string) => Promise<FileStorage>;
|
|
131
131
|
|
|
132
|
+
// Optional per-collection configuration.
|
|
133
|
+
export type BulkDatabase2Config = {
|
|
134
|
+
// When set (> 0), the reactive change notifications writes/loads emit are throttled and BATCHED
|
|
135
|
+
// globally (across all keys), so a high-frequency write source doesn't re-run watchers on every single
|
|
136
|
+
// change. The throttle RAMPS like the write-flush one: the first change after a lull notifies
|
|
137
|
+
// immediately, then under sustained changes the delay doubles up to triggerThrottleMs, coalescing the
|
|
138
|
+
// burst into one notification. In-memory/async reads are always current — only the OBSERVABLE
|
|
139
|
+
// notification (the mobx re-render trigger) is delayed and merged.
|
|
140
|
+
triggerThrottleMs?: number;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Trigger-throttle ramp: the first deferred notification waits this long, then the delay doubles on each
|
|
144
|
+
// further change up to BulkDatabase2Config.triggerThrottleMs. ~16ms ≈ one animation frame, so an isolated
|
|
145
|
+
// burst still notifies within a frame.
|
|
146
|
+
const TRIGGER_THROTTLE_FIRST_STEP_MS = 16;
|
|
147
|
+
|
|
132
148
|
// The load/reset lifecycle shares one signal; every sync read observes it so it re-renders when the
|
|
133
149
|
// reader resets or a base column/field finishes loading. The overlay's per-key signal is the key
|
|
134
150
|
// itself (a point read observes just its key), plus one overlay-wide signal that whole-column reads
|
|
@@ -205,6 +221,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
205
221
|
public readonly name: string,
|
|
206
222
|
protected deps: ReactiveDeps,
|
|
207
223
|
private storageFactory: StorageFactory,
|
|
224
|
+
private config: BulkDatabase2Config = {},
|
|
208
225
|
) {
|
|
209
226
|
// Best-effort: persist buffered stream writes when the tab is hidden/closing. visibilitychange →
|
|
210
227
|
// hidden fires early enough that the (async) flush usually completes; pagehide is the backstop.
|
|
@@ -278,6 +295,29 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
278
295
|
// overlay to decide whether an incoming remote write is actually newer than what we have.
|
|
279
296
|
private streamTimes = new Map<string, number>();
|
|
280
297
|
|
|
298
|
+
// Cache of fully-resolved (overlay-patched) column results, keyed by column name. The result only
|
|
299
|
+
// changes when the overlay mutates or the reader resets, so we keep it until then — repeat whole-column
|
|
300
|
+
// reads (common in a UI re-render) are then free, however large the column. Each cached array is frozen
|
|
301
|
+
// SHALLOWLY: Object.freeze locks the array itself (callers can't mutate a shared result) but never the
|
|
302
|
+
// element objects or their values, so typed-array column values keep their fast representation.
|
|
303
|
+
private columnCache = new Map<string, { key: string; value: unknown; time: number }[]>();
|
|
304
|
+
// Live keys of the currently-loaded reader (disk only, no overlay), so a write can tell a partial
|
|
305
|
+
// update of an existing key (only the written columns change) from adding/removing a key (which appears
|
|
306
|
+
// in / disappears from EVERY column). Set on reader build, cleared on reset.
|
|
307
|
+
private readerKeys: Set<string> | undefined;
|
|
308
|
+
// Bumped on every overlay mutation / reader reset. An async column build captures it before its awaits
|
|
309
|
+
// and only caches its result if it hasn't changed since — so a write or reset mid-build (which clears
|
|
310
|
+
// the cache and may swap the reader) can never leave a stale entry behind.
|
|
311
|
+
private dataGen = 0;
|
|
312
|
+
|
|
313
|
+
// ---- trigger throttle (see BulkDatabase2Config.triggerThrottleMs) ----
|
|
314
|
+
// Signals whose notification is deferred, the pending flush timer, and the ramping delay. Data state is
|
|
315
|
+
// already updated synchronously; only these observable notifications are batched/delayed.
|
|
316
|
+
private pendingSignals = new Set<string>();
|
|
317
|
+
private triggerTimer: ReturnType<typeof setTimeout> | undefined;
|
|
318
|
+
private currentTriggerDelay = 0;
|
|
319
|
+
private lastTriggerTime = 0;
|
|
320
|
+
|
|
281
321
|
// Approximate size of the tier-0 stream data on disk: set accurately from the last reader build, then
|
|
282
322
|
// kept current as each flush appends more. Drives the stream-fold trigger (see streamNeedsFold);
|
|
283
323
|
// reset on resetReader, since after a merge/reset the next reader build re-measures it.
|
|
@@ -303,24 +343,71 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
303
343
|
return this.streamFileName;
|
|
304
344
|
}
|
|
305
345
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
346
|
+
// Invalidate after an overlay change. `columns` is the set of columns whose resolved result actually
|
|
347
|
+
// changed — only those cached columns are dropped — or "all" when the key set itself changed (a key
|
|
348
|
+
// added or removed appears in / disappears from every column).
|
|
349
|
+
private invalidateOverlay(key: string, columns: Iterable<string> | "all") {
|
|
350
|
+
this.dataGen++;
|
|
351
|
+
if (columns === "all") this.columnCache.clear();
|
|
352
|
+
else for (const c of columns) this.columnCache.delete(c);
|
|
353
|
+
this.invalidateSignal(key);
|
|
354
|
+
this.invalidateSignal(OVERLAY_SIGNAL);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Whether `key` is currently a live key in the resolved view (overlay wins over disk). A write to a key
|
|
358
|
+
// that's already live only changes the columns it sets; a write to an absent key (or one currently
|
|
359
|
+
// deleted in the overlay) makes it appear in every column, so every column's cache must drop.
|
|
360
|
+
private isLiveNow(key: string): boolean {
|
|
361
|
+
const e = this.overlay.get(key);
|
|
362
|
+
if (e) return e.value !== DELETED;
|
|
363
|
+
return this.readerKeys?.has(key) ?? false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Notify observers of `signal`. With triggerThrottleMs set, notifications are batched and delayed on a
|
|
367
|
+
// ramping schedule so a high-frequency source can't re-run watchers on every change: a change after a
|
|
368
|
+
// lull notifies on the next tick (no real delay, but all of one change's signals batch together);
|
|
369
|
+
// under sustained changes the delay doubles up to the max, coalescing the burst into one notification.
|
|
370
|
+
// Only the OBSERVABLE notification is delayed — the underlying data was already updated, so a read in
|
|
371
|
+
// the meantime still sees current values.
|
|
372
|
+
private invalidateSignal(signal: string) {
|
|
373
|
+
const maxMs = this.config.triggerThrottleMs ?? 0;
|
|
374
|
+
if (maxMs <= 0) { this.deps.invalidate(signal); return; }
|
|
375
|
+
this.pendingSignals.add(signal);
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const lull = now - this.lastTriggerTime > maxMs;
|
|
378
|
+
this.lastTriggerTime = now;
|
|
379
|
+
if (this.triggerTimer !== undefined) return; // a flush is already scheduled; it will pick this up
|
|
380
|
+
// Lull resets the ramp (notify next tick); an active burst ramps the delay toward the max.
|
|
381
|
+
this.currentTriggerDelay = lull ? 0 : Math.min(maxMs, Math.max(TRIGGER_THROTTLE_FIRST_STEP_MS, this.currentTriggerDelay * 2));
|
|
382
|
+
this.triggerTimer = setTimeout(() => { this.triggerTimer = undefined; this.flushSignals(); }, this.currentTriggerDelay);
|
|
383
|
+
(this.triggerTimer as { unref?: () => void }).unref?.();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private flushSignals() {
|
|
387
|
+
if (this.pendingSignals.size === 0) return;
|
|
388
|
+
const signals = [...this.pendingSignals];
|
|
389
|
+
this.pendingSignals.clear();
|
|
390
|
+
this.deps.batch(() => { for (const s of signals) this.deps.invalidate(s); });
|
|
309
391
|
}
|
|
310
392
|
|
|
311
393
|
// Merges a (possibly partial) row onto the key's current overlay value, so a partial write/update
|
|
312
394
|
// only changes the columns it includes — the rest fall through to disk on read. A prior delete is
|
|
313
395
|
// reset (the key is being re-created).
|
|
314
396
|
private setOverlayRow(key: string, row: Record<string, unknown>, time: number) {
|
|
397
|
+
// Decide BEFORE mutating: if the key is already live, only the columns this write sets change;
|
|
398
|
+
// otherwise it's a new/re-created key that joins every column.
|
|
399
|
+
const wasLive = this.isLiveNow(key);
|
|
315
400
|
const existing = this.overlay.get(key);
|
|
316
401
|
const value = existing && existing.value !== DELETED ? { ...existing.value, ...row } : { ...row };
|
|
317
402
|
this.overlay.set(key, { time, value });
|
|
318
|
-
this.invalidateOverlay(key);
|
|
403
|
+
this.invalidateOverlay(key, wasLive ? Object.keys(row) : "all");
|
|
319
404
|
}
|
|
320
405
|
|
|
321
406
|
private setOverlayDeleted(key: string, time: number) {
|
|
407
|
+
// Deleting a live key removes it from every column; deleting an absent key changes no column.
|
|
408
|
+
const columns = this.isLiveNow(key) ? "all" : [];
|
|
322
409
|
this.overlay.set(key, { time, value: DELETED });
|
|
323
|
-
this.invalidateOverlay(key);
|
|
410
|
+
this.invalidateOverlay(key, columns);
|
|
324
411
|
}
|
|
325
412
|
|
|
326
413
|
private reader = lazy(async (): Promise<ResolvedReader> => {
|
|
@@ -378,6 +465,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
378
465
|
}
|
|
379
466
|
readers.push(...bulkReaders);
|
|
380
467
|
const joined = await joinBulkDatabases(readers);
|
|
468
|
+
// Live keys of this reader, so per-column cache invalidation can tell an update of an existing key
|
|
469
|
+
// (only its set columns change) from a key add/remove (which touches every column).
|
|
470
|
+
this.readerKeys = new Set(joined.keys);
|
|
381
471
|
|
|
382
472
|
let time = Date.now() - start;
|
|
383
473
|
if (time > 50) {
|
|
@@ -435,13 +525,16 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
435
525
|
this.baseFields.clear();
|
|
436
526
|
this.baseFieldsLoading.clear();
|
|
437
527
|
this.overlay.clear();
|
|
528
|
+
this.dataGen++;
|
|
529
|
+
this.columnCache.clear();
|
|
530
|
+
this.readerKeys = undefined; // the next reader build repopulates it
|
|
438
531
|
// The next reader build re-measures the stream; clear the estimate so a just-folded stream
|
|
439
532
|
// doesn't keep looking "heavy" and re-trigger a fold before then.
|
|
440
533
|
this.streamRowsOnDisk = 0;
|
|
441
534
|
this.streamBytesOnDisk = 0;
|
|
442
535
|
for (const p of this.pendingAppends) p.apply();
|
|
443
|
-
this.
|
|
444
|
-
this.
|
|
536
|
+
this.invalidateSignal(LOAD_SIGNAL);
|
|
537
|
+
this.invalidateSignal(OVERLAY_SIGNAL);
|
|
445
538
|
});
|
|
446
539
|
}
|
|
447
540
|
|
|
@@ -1134,14 +1227,21 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1134
1227
|
|
|
1135
1228
|
public async getColumn<Column extends keyof T>(column: Column): Promise<{ key: string; value: T[Column]; time: number }[]> {
|
|
1136
1229
|
void this.syncSetup();
|
|
1230
|
+
const col = String(column);
|
|
1231
|
+
const cached = this.columnCache.get(col);
|
|
1232
|
+
if (cached) return cached as { key: string; value: T[Column]; time: number }[];
|
|
1233
|
+
const gen = this.dataGen;
|
|
1137
1234
|
let time = Date.now();
|
|
1138
1235
|
let reader = await this.reader();
|
|
1139
|
-
let base = await reader.getColumn(
|
|
1140
|
-
let result = this.patchColumn(base,
|
|
1236
|
+
let base = await reader.getColumn(col);
|
|
1237
|
+
let result = this.patchColumn(base, col) as { key: string; value: T[Column]; time: number }[];
|
|
1141
1238
|
time = Date.now() - time;
|
|
1142
1239
|
if (time > 50) {
|
|
1143
1240
|
console.log(`${blue(`${this.name}.getColumn(${JSON.stringify(column)})`)} took ${red(formatTime(time))} ${this.formatInfo(reader)}`);
|
|
1144
1241
|
}
|
|
1242
|
+
Object.freeze(result);
|
|
1243
|
+
// Only cache if no write/reset happened during the awaits above (else this result may be stale).
|
|
1244
|
+
if (this.dataGen === gen) this.columnCache.set(col, result);
|
|
1145
1245
|
return result;
|
|
1146
1246
|
}
|
|
1147
1247
|
|
|
@@ -1179,7 +1279,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1179
1279
|
this.deps.batch(() => {
|
|
1180
1280
|
this.baseColumns.set(column, base);
|
|
1181
1281
|
this.baseColumnsLoading.delete(column);
|
|
1182
|
-
this.
|
|
1282
|
+
this.invalidateSignal(LOAD_SIGNAL);
|
|
1183
1283
|
});
|
|
1184
1284
|
})();
|
|
1185
1285
|
}
|
|
@@ -1194,7 +1294,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1194
1294
|
this.deps.batch(() => {
|
|
1195
1295
|
this.baseFields.set(cacheKey, resolved);
|
|
1196
1296
|
this.baseFieldsLoading.delete(cacheKey);
|
|
1197
|
-
this.
|
|
1297
|
+
this.invalidateSignal(LOAD_SIGNAL);
|
|
1198
1298
|
});
|
|
1199
1299
|
})();
|
|
1200
1300
|
}
|
|
@@ -1236,12 +1336,19 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1236
1336
|
// Observe the overlay-wide signal so we recompute once the base arrives or the overlay changes.
|
|
1237
1337
|
this.deps.observe(OVERLAY_SIGNAL);
|
|
1238
1338
|
let col = String(column);
|
|
1339
|
+
const cached = this.columnCache.get(col);
|
|
1340
|
+
if (cached) return cached as { key: string; value: T[Column]; time: number }[];
|
|
1239
1341
|
let base = this.baseColumns.get(col);
|
|
1240
1342
|
if (!base) {
|
|
1241
1343
|
this.ensureBaseColumn(col);
|
|
1242
1344
|
return undefined;
|
|
1243
1345
|
}
|
|
1244
|
-
|
|
1346
|
+
// Synchronous (no awaits) → reads the current overlay and caches it atomically; an observer re-runs
|
|
1347
|
+
// (and the cache is cleared) on any later change, so the frozen result is safe to share.
|
|
1348
|
+
let result = this.patchColumn(base, col) as { key: string; value: T[Column]; time: number }[];
|
|
1349
|
+
Object.freeze(result);
|
|
1350
|
+
this.columnCache.set(col, result);
|
|
1351
|
+
return result;
|
|
1245
1352
|
}
|
|
1246
1353
|
|
|
1247
1354
|
public async getColumnInfo() {
|