sliftutils 1.4.16 → 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;
|
|
@@ -945,13 +949,21 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
945
949
|
private overlay;
|
|
946
950
|
private streamTimes;
|
|
947
951
|
private columnCache;
|
|
952
|
+
private readerKeys;
|
|
948
953
|
private dataGen;
|
|
954
|
+
private pendingSignals;
|
|
955
|
+
private triggerTimer;
|
|
956
|
+
private currentTriggerDelay;
|
|
957
|
+
private lastTriggerTime;
|
|
949
958
|
private streamRowsOnDisk;
|
|
950
959
|
private streamBytesOnDisk;
|
|
951
960
|
private streamFileName;
|
|
952
961
|
private lastMergeCheck;
|
|
953
962
|
private getStreamFileName;
|
|
954
963
|
private invalidateOverlay;
|
|
964
|
+
private isLiveNow;
|
|
965
|
+
private invalidateSignal;
|
|
966
|
+
private flushSignals;
|
|
955
967
|
private setOverlayRow;
|
|
956
968
|
private setOverlayDeleted;
|
|
957
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;
|
|
@@ -41,13 +45,21 @@ export declare class BulkDatabaseBase<T extends {
|
|
|
41
45
|
private overlay;
|
|
42
46
|
private streamTimes;
|
|
43
47
|
private columnCache;
|
|
48
|
+
private readerKeys;
|
|
44
49
|
private dataGen;
|
|
50
|
+
private pendingSignals;
|
|
51
|
+
private triggerTimer;
|
|
52
|
+
private currentTriggerDelay;
|
|
53
|
+
private lastTriggerTime;
|
|
45
54
|
private streamRowsOnDisk;
|
|
46
55
|
private streamBytesOnDisk;
|
|
47
56
|
private streamFileName;
|
|
48
57
|
private lastMergeCheck;
|
|
49
58
|
private getStreamFileName;
|
|
50
59
|
private invalidateOverlay;
|
|
60
|
+
private isLiveNow;
|
|
61
|
+
private invalidateSignal;
|
|
62
|
+
private flushSignals;
|
|
51
63
|
private setOverlayRow;
|
|
52
64
|
private setOverlayDeleted;
|
|
53
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.
|
|
@@ -284,11 +301,23 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
284
301
|
// SHALLOWLY: Object.freeze locks the array itself (callers can't mutate a shared result) but never the
|
|
285
302
|
// element objects or their values, so typed-array column values keep their fast representation.
|
|
286
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;
|
|
287
308
|
// Bumped on every overlay mutation / reader reset. An async column build captures it before its awaits
|
|
288
309
|
// and only caches its result if it hasn't changed since — so a write or reset mid-build (which clears
|
|
289
310
|
// the cache and may swap the reader) can never leave a stale entry behind.
|
|
290
311
|
private dataGen = 0;
|
|
291
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
|
+
|
|
292
321
|
// Approximate size of the tier-0 stream data on disk: set accurately from the last reader build, then
|
|
293
322
|
// kept current as each flush appends more. Drives the stream-fold trigger (see streamNeedsFold);
|
|
294
323
|
// reset on resetReader, since after a merge/reset the next reader build re-measures it.
|
|
@@ -314,26 +343,71 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
314
343
|
return this.streamFileName;
|
|
315
344
|
}
|
|
316
345
|
|
|
317
|
-
|
|
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") {
|
|
318
350
|
this.dataGen++;
|
|
319
|
-
this.columnCache.clear();
|
|
320
|
-
this.
|
|
321
|
-
this.
|
|
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); });
|
|
322
391
|
}
|
|
323
392
|
|
|
324
393
|
// Merges a (possibly partial) row onto the key's current overlay value, so a partial write/update
|
|
325
394
|
// only changes the columns it includes — the rest fall through to disk on read. A prior delete is
|
|
326
395
|
// reset (the key is being re-created).
|
|
327
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);
|
|
328
400
|
const existing = this.overlay.get(key);
|
|
329
401
|
const value = existing && existing.value !== DELETED ? { ...existing.value, ...row } : { ...row };
|
|
330
402
|
this.overlay.set(key, { time, value });
|
|
331
|
-
this.invalidateOverlay(key);
|
|
403
|
+
this.invalidateOverlay(key, wasLive ? Object.keys(row) : "all");
|
|
332
404
|
}
|
|
333
405
|
|
|
334
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" : [];
|
|
335
409
|
this.overlay.set(key, { time, value: DELETED });
|
|
336
|
-
this.invalidateOverlay(key);
|
|
410
|
+
this.invalidateOverlay(key, columns);
|
|
337
411
|
}
|
|
338
412
|
|
|
339
413
|
private reader = lazy(async (): Promise<ResolvedReader> => {
|
|
@@ -391,6 +465,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
391
465
|
}
|
|
392
466
|
readers.push(...bulkReaders);
|
|
393
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);
|
|
394
471
|
|
|
395
472
|
let time = Date.now() - start;
|
|
396
473
|
if (time > 50) {
|
|
@@ -450,13 +527,14 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
450
527
|
this.overlay.clear();
|
|
451
528
|
this.dataGen++;
|
|
452
529
|
this.columnCache.clear();
|
|
530
|
+
this.readerKeys = undefined; // the next reader build repopulates it
|
|
453
531
|
// The next reader build re-measures the stream; clear the estimate so a just-folded stream
|
|
454
532
|
// doesn't keep looking "heavy" and re-trigger a fold before then.
|
|
455
533
|
this.streamRowsOnDisk = 0;
|
|
456
534
|
this.streamBytesOnDisk = 0;
|
|
457
535
|
for (const p of this.pendingAppends) p.apply();
|
|
458
|
-
this.
|
|
459
|
-
this.
|
|
536
|
+
this.invalidateSignal(LOAD_SIGNAL);
|
|
537
|
+
this.invalidateSignal(OVERLAY_SIGNAL);
|
|
460
538
|
});
|
|
461
539
|
}
|
|
462
540
|
|
|
@@ -1201,7 +1279,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1201
1279
|
this.deps.batch(() => {
|
|
1202
1280
|
this.baseColumns.set(column, base);
|
|
1203
1281
|
this.baseColumnsLoading.delete(column);
|
|
1204
|
-
this.
|
|
1282
|
+
this.invalidateSignal(LOAD_SIGNAL);
|
|
1205
1283
|
});
|
|
1206
1284
|
})();
|
|
1207
1285
|
}
|
|
@@ -1216,7 +1294,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1216
1294
|
this.deps.batch(() => {
|
|
1217
1295
|
this.baseFields.set(cacheKey, resolved);
|
|
1218
1296
|
this.baseFieldsLoading.delete(cacheKey);
|
|
1219
|
-
this.
|
|
1297
|
+
this.invalidateSignal(LOAD_SIGNAL);
|
|
1220
1298
|
});
|
|
1221
1299
|
})();
|
|
1222
1300
|
}
|