sliftutils 1.4.14 → 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 +19 -0
- package/package.json +1 -1
- package/storage/BulkDatabase2/BulkDatabase2.d.ts +7 -0
- package/storage/BulkDatabase2/BulkDatabase2.ts +8 -0
- package/storage/BulkDatabase2/BulkDatabaseBase.d.ts +10 -0
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +101 -12
- package/storage/FileFolderAPI.d.ts +2 -0
- package/storage/FileFolderAPI.tsx +7 -0
- package/storage/remoteFileServer.ts +10 -1
- package/storage/remoteFileStorage.ts +1 -0
package/index.d.ts
CHANGED
|
@@ -865,6 +865,13 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabase2" {
|
|
|
865
865
|
}>;
|
|
866
866
|
/** Consolidate on-disk files. Optional to call; the database also does this in the background. */
|
|
867
867
|
compact(): Promise<void>;
|
|
868
|
+
/**
|
|
869
|
+
* Whether this collection's storage is served over the network (a remote server) rather than local
|
|
870
|
+
* disk. Apps can branch on this to adapt to the higher latency. Note: over the network the database
|
|
871
|
+
* skips automatic background compaction by default — call the static
|
|
872
|
+
* `BulkDatabase2.enableNetworkCompaction()` once to opt in.
|
|
873
|
+
*/
|
|
874
|
+
isRemote(): Promise<boolean>;
|
|
868
875
|
/**
|
|
869
876
|
* Flush buffered stream writes to disk now. Writes are coalesced and flushed on a ramping delay (to
|
|
870
877
|
* avoid the browser rewriting the whole stream file per write), so a write's promise resolving means
|
|
@@ -902,6 +909,8 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
902
909
|
mergeSpacingMs: number;
|
|
903
910
|
firstMergeTriggerFiles: number;
|
|
904
911
|
firstMergeTriggerRangeMs: number;
|
|
912
|
+
streamFoldTriggerRows: number;
|
|
913
|
+
streamFoldTriggerBytes: number;
|
|
905
914
|
writeFlushMaxDelayMs: number;
|
|
906
915
|
};
|
|
907
916
|
export interface ReactiveDeps {
|
|
@@ -924,13 +933,21 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
924
933
|
private currentFlushDelay;
|
|
925
934
|
private lastWriteTime;
|
|
926
935
|
static clearCache(): void;
|
|
936
|
+
static enableNetworkCompaction(): void;
|
|
927
937
|
storage: {
|
|
928
938
|
(): Promise<FileStorage>;
|
|
929
939
|
reset(): void;
|
|
930
940
|
set(newValue: Promise<FileStorage>): void;
|
|
931
941
|
};
|
|
942
|
+
isRemote(): Promise<boolean>;
|
|
943
|
+
private streamNeedsFold;
|
|
944
|
+
private automaticCompactionAllowed;
|
|
932
945
|
private overlay;
|
|
933
946
|
private streamTimes;
|
|
947
|
+
private columnCache;
|
|
948
|
+
private dataGen;
|
|
949
|
+
private streamRowsOnDisk;
|
|
950
|
+
private streamBytesOnDisk;
|
|
934
951
|
private streamFileName;
|
|
935
952
|
private lastMergeCheck;
|
|
936
953
|
private getStreamFileName;
|
|
@@ -1372,6 +1389,7 @@ declare module "sliftutils/storage/FileFolderAPI" {
|
|
|
1372
1389
|
readonly kind: "directory";
|
|
1373
1390
|
readonly name: string;
|
|
1374
1391
|
readonly fullPath?: string;
|
|
1392
|
+
readonly isRemote?: boolean;
|
|
1375
1393
|
removeEntry(key: string, options?: {
|
|
1376
1394
|
recursive?: boolean;
|
|
1377
1395
|
}): Promise<void>;
|
|
@@ -1459,6 +1477,7 @@ declare module "sliftutils/storage/FileFolderAPI" {
|
|
|
1459
1477
|
};
|
|
1460
1478
|
export type FileStorage = IStorageRaw & {
|
|
1461
1479
|
folder: NestedFileStorage;
|
|
1480
|
+
isRemote?: boolean;
|
|
1462
1481
|
};
|
|
1463
1482
|
export declare function wrapHandle(handle: DirectoryWrapper): FileStorage;
|
|
1464
1483
|
export declare function getRemoteFileStorageFactory(url: string, password: string, options?: RemoteOptions): (pathStr: string) => Promise<FileStorage>;
|
package/package.json
CHANGED
|
@@ -100,6 +100,13 @@ export interface IBulkDatabase2<T extends {
|
|
|
100
100
|
}>;
|
|
101
101
|
/** Consolidate on-disk files. Optional to call; the database also does this in the background. */
|
|
102
102
|
compact(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Whether this collection's storage is served over the network (a remote server) rather than local
|
|
105
|
+
* disk. Apps can branch on this to adapt to the higher latency. Note: over the network the database
|
|
106
|
+
* skips automatic background compaction by default — call the static
|
|
107
|
+
* `BulkDatabase2.enableNetworkCompaction()` once to opt in.
|
|
108
|
+
*/
|
|
109
|
+
isRemote(): Promise<boolean>;
|
|
103
110
|
/**
|
|
104
111
|
* Flush buffered stream writes to disk now. Writes are coalesced and flushed on a ramping delay (to
|
|
105
112
|
* avoid the browser rewriting the whole stream file per write), so a write's promise resolving means
|
|
@@ -79,6 +79,14 @@ export interface IBulkDatabase2<T extends { key: string }> {
|
|
|
79
79
|
/** Consolidate on-disk files. Optional to call; the database also does this in the background. */
|
|
80
80
|
compact(): Promise<void>;
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether this collection's storage is served over the network (a remote server) rather than local
|
|
84
|
+
* disk. Apps can branch on this to adapt to the higher latency. Note: over the network the database
|
|
85
|
+
* skips automatic background compaction by default — call the static
|
|
86
|
+
* `BulkDatabase2.enableNetworkCompaction()` once to opt in.
|
|
87
|
+
*/
|
|
88
|
+
isRemote(): Promise<boolean>;
|
|
89
|
+
|
|
82
90
|
/**
|
|
83
91
|
* Flush buffered stream writes to disk now. Writes are coalesced and flushed on a ramping delay (to
|
|
84
92
|
* avoid the browser rewriting the whole stream file per write), so a write's promise resolving means
|
|
@@ -5,6 +5,8 @@ export declare const bulkDatabase2Timing: {
|
|
|
5
5
|
mergeSpacingMs: number;
|
|
6
6
|
firstMergeTriggerFiles: number;
|
|
7
7
|
firstMergeTriggerRangeMs: number;
|
|
8
|
+
streamFoldTriggerRows: number;
|
|
9
|
+
streamFoldTriggerBytes: number;
|
|
8
10
|
writeFlushMaxDelayMs: number;
|
|
9
11
|
};
|
|
10
12
|
export interface ReactiveDeps {
|
|
@@ -27,13 +29,21 @@ export declare class BulkDatabaseBase<T extends {
|
|
|
27
29
|
private currentFlushDelay;
|
|
28
30
|
private lastWriteTime;
|
|
29
31
|
static clearCache(): void;
|
|
32
|
+
static enableNetworkCompaction(): void;
|
|
30
33
|
storage: {
|
|
31
34
|
(): Promise<FileStorage>;
|
|
32
35
|
reset(): void;
|
|
33
36
|
set(newValue: Promise<FileStorage>): void;
|
|
34
37
|
};
|
|
38
|
+
isRemote(): Promise<boolean>;
|
|
39
|
+
private streamNeedsFold;
|
|
40
|
+
private automaticCompactionAllowed;
|
|
35
41
|
private overlay;
|
|
36
42
|
private streamTimes;
|
|
43
|
+
private columnCache;
|
|
44
|
+
private dataGen;
|
|
45
|
+
private streamRowsOnDisk;
|
|
46
|
+
private streamBytesOnDisk;
|
|
37
47
|
private streamFileName;
|
|
38
48
|
private lastMergeCheck;
|
|
39
49
|
private getStreamFileName;
|
|
@@ -77,6 +77,11 @@ export const bulkDatabase2Timing = {
|
|
|
77
77
|
firstMergeTriggerFiles: 20,
|
|
78
78
|
// ...or span a wider write-time range than this (data trickling in slowly still gets consolidated).
|
|
79
79
|
firstMergeTriggerRangeMs: 3 * 24 * 60 * 60 * 1000,
|
|
80
|
+
// Tier-0 stream data can't be read per-cell — a read pulls the whole stream — so once the stream
|
|
81
|
+
// exceeds BOTH of these it's folded into bulk (columnar, range-readable) promptly, bypassing the merge
|
|
82
|
+
// throttle. The byte bound matters most over the network, where pulling the whole stream is expensive.
|
|
83
|
+
streamFoldTriggerRows: 100,
|
|
84
|
+
streamFoldTriggerBytes: 64 * 1024 * 1024,
|
|
80
85
|
// Max delay (per collection) before buffered stream writes are flushed to disk; the delay ramps up to
|
|
81
86
|
// this under sustained writing (see WRITE_FLUSH_FIRST_STEP_MS). 0 = flush every write immediately —
|
|
82
87
|
// the default in Node, where append is real and cheap; the browser ramps to 15s to avoid rewriting
|
|
@@ -132,6 +137,11 @@ export type StorageFactory = (path: string) => Promise<FileStorage>;
|
|
|
132
137
|
const LOAD_SIGNAL = NULL + "load";
|
|
133
138
|
const OVERLAY_SIGNAL = NULL + "overlay";
|
|
134
139
|
|
|
140
|
+
// Over network storage we skip automatic (background) compaction by default — reading and rewriting whole
|
|
141
|
+
// files over the network is expensive. An app that wants it anyway opts in once via
|
|
142
|
+
// BulkDatabaseBase.enableNetworkCompaction(). Explicit compact()/tryMergeNow() calls are unaffected.
|
|
143
|
+
let networkCompactionEnabled = false;
|
|
144
|
+
|
|
135
145
|
let fileNameCounter = 0;
|
|
136
146
|
// Random per-process id baked into file names so two processes (tabs) writing the same collection
|
|
137
147
|
// never collide on a name when they pick the same timestamp/counter in the same millisecond.
|
|
@@ -216,7 +226,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
216
226
|
// ramping delay (see WRITE_FLUSH_FIRST_STEP_MS / writeFlushMaxDelayMs). Each buffered item keeps an
|
|
217
227
|
// `apply` that re-applies its overlay mutation, so resetReader (which clears the overlay) can restore
|
|
218
228
|
// writes that aren't on disk yet. Removed from the buffer only once their append succeeds.
|
|
219
|
-
private pendingAppends: { framed: Buffer; apply: () => void }[] = [];
|
|
229
|
+
private pendingAppends: { framed: Buffer; apply: () => void; rows: number }[] = [];
|
|
220
230
|
private flushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
221
231
|
private flushChain: Promise<void> = Promise.resolve();
|
|
222
232
|
private currentFlushDelay = 0;
|
|
@@ -228,8 +238,34 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
228
238
|
blockCache.clear();
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
// Opt in to automatic compaction even when the storage is remote (off by default — see
|
|
242
|
+
// networkCompactionEnabled). Global; affects every collection. Explicit compact()/tryMergeNow() always
|
|
243
|
+
// run regardless of this.
|
|
244
|
+
public static enableNetworkCompaction() {
|
|
245
|
+
networkCompactionEnabled = true;
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
public storage = lazy(async () => this.storageFactory(`${BULK_ROOT_FOLDER}/${this.name}`));
|
|
232
249
|
|
|
250
|
+
// True when this collection's storage is served over the network (a remote server). Apps can branch on
|
|
251
|
+
// this to adjust behavior for the higher latency (e.g. show a "slower storage" hint, prefetch less).
|
|
252
|
+
public async isRemote(): Promise<boolean> {
|
|
253
|
+
return !!(await this.storage()).isRemote;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Whether the tier-0 stream is big enough to fold into bulk now (both bounds): too many rows AND too
|
|
257
|
+
// many bytes to keep reading whole. See bulkDatabase2Timing.streamFoldTrigger*.
|
|
258
|
+
private streamNeedsFold(): boolean {
|
|
259
|
+
return this.streamRowsOnDisk >= bulkDatabase2Timing.streamFoldTriggerRows && this.streamBytesOnDisk > bulkDatabase2Timing.streamFoldTriggerBytes;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Automatic (background) compaction is skipped over the network unless the app opted in. Explicit
|
|
263
|
+
// compact()/tryMergeNow() bypass this.
|
|
264
|
+
private async automaticCompactionAllowed(): Promise<boolean> {
|
|
265
|
+
if (networkCompactionEnabled) return true;
|
|
266
|
+
return !(await this.storage()).isRemote;
|
|
267
|
+
}
|
|
268
|
+
|
|
233
269
|
// In-memory overlay of pending writes/deletes. It takes priority over the loaded readers, so writes
|
|
234
270
|
// are reflected in reads without reloading. Reads observe the relevant signal; mutations invalidate it.
|
|
235
271
|
//
|
|
@@ -242,6 +278,23 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
242
278
|
// overlay to decide whether an incoming remote write is actually newer than what we have.
|
|
243
279
|
private streamTimes = new Map<string, number>();
|
|
244
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
|
+
|
|
292
|
+
// Approximate size of the tier-0 stream data on disk: set accurately from the last reader build, then
|
|
293
|
+
// kept current as each flush appends more. Drives the stream-fold trigger (see streamNeedsFold);
|
|
294
|
+
// reset on resetReader, since after a merge/reset the next reader build re-measures it.
|
|
295
|
+
private streamRowsOnDisk = 0;
|
|
296
|
+
private streamBytesOnDisk = 0;
|
|
297
|
+
|
|
245
298
|
// This instance's tier-0 stream file. Each instance (≈ each thread/tab) streams to its own file
|
|
246
299
|
// so concurrent writers never touch the same file.
|
|
247
300
|
private streamFileName: string | undefined;
|
|
@@ -262,6 +315,8 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
262
315
|
}
|
|
263
316
|
|
|
264
317
|
private invalidateOverlay(key: string) {
|
|
318
|
+
this.dataGen++;
|
|
319
|
+
this.columnCache.clear();
|
|
265
320
|
this.deps.invalidate(key);
|
|
266
321
|
this.deps.invalidate(OVERLAY_SIGNAL);
|
|
267
322
|
}
|
|
@@ -319,6 +374,10 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
319
374
|
if (streamData.missing) filesChanged = true;
|
|
320
375
|
if (filesChanged && !tolerateMissing) throw new FilesChangedError();
|
|
321
376
|
|
|
377
|
+
// Accurate stream size as of this load; subsequent flushes keep it current (see doFlush).
|
|
378
|
+
this.streamRowsOnDisk = streamData.entries.length;
|
|
379
|
+
this.streamBytesOnDisk = streamData.totalBytes;
|
|
380
|
+
|
|
322
381
|
const bulkReaders = bulkReadersRaw.filter((r): r is BaseBulkDatabaseReader => !!r);
|
|
323
382
|
// The join resolves purely by write-time, so reader order doesn't matter.
|
|
324
383
|
const readers: BaseBulkDatabaseReader[] = [];
|
|
@@ -389,6 +448,12 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
389
448
|
this.baseFields.clear();
|
|
390
449
|
this.baseFieldsLoading.clear();
|
|
391
450
|
this.overlay.clear();
|
|
451
|
+
this.dataGen++;
|
|
452
|
+
this.columnCache.clear();
|
|
453
|
+
// The next reader build re-measures the stream; clear the estimate so a just-folded stream
|
|
454
|
+
// doesn't keep looking "heavy" and re-trigger a fold before then.
|
|
455
|
+
this.streamRowsOnDisk = 0;
|
|
456
|
+
this.streamBytesOnDisk = 0;
|
|
392
457
|
for (const p of this.pendingAppends) p.apply();
|
|
393
458
|
this.deps.invalidate(LOAD_SIGNAL);
|
|
394
459
|
this.deps.invalidate(OVERLAY_SIGNAL);
|
|
@@ -422,7 +487,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
422
487
|
const apply = () => { for (const { time, row } of stamped) this.setOverlayRow(row.key as string, row, time); };
|
|
423
488
|
this.deps.batch(apply);
|
|
424
489
|
for (const { time, row } of stamped) syncBroadcast(this.name, { key: row.key as string, time, value: row });
|
|
425
|
-
await this.streamAppend(framed, apply);
|
|
490
|
+
await this.streamAppend(framed, apply, stamped.length);
|
|
426
491
|
void this.maybeMerge();
|
|
427
492
|
}
|
|
428
493
|
|
|
@@ -437,7 +502,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
437
502
|
const apply = () => { for (const { time, key } of stamped) this.setOverlayDeleted(key, time); };
|
|
438
503
|
this.deps.batch(apply);
|
|
439
504
|
for (const { time, key } of stamped) syncBroadcast(this.name, { key, time, deleted: true });
|
|
440
|
-
await this.streamAppend(frameDeletes(stamped), apply);
|
|
505
|
+
await this.streamAppend(frameDeletes(stamped), apply, stamped.length);
|
|
441
506
|
void this.maybeMerge();
|
|
442
507
|
}
|
|
443
508
|
|
|
@@ -445,8 +510,8 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
445
510
|
// `apply` re-applies this write's overlay mutation if resetReader clears the overlay before it's on
|
|
446
511
|
// disk. Awaits durability only on an immediate (idle/Node) flush, so a single action — then close — is
|
|
447
512
|
// saved at once; a burst returns fast and is flushed in the background.
|
|
448
|
-
private async streamAppend(framed: Buffer, apply: () => void): Promise<void> {
|
|
449
|
-
this.pendingAppends.push({ framed, apply });
|
|
513
|
+
private async streamAppend(framed: Buffer, apply: () => void, rows: number): Promise<void> {
|
|
514
|
+
this.pendingAppends.push({ framed, apply, rows });
|
|
450
515
|
const max = bulkDatabase2Timing.writeFlushMaxDelayMs;
|
|
451
516
|
const now = Date.now();
|
|
452
517
|
// Immediate when batching is off (Node / real append), or for the first write after a lull.
|
|
@@ -487,6 +552,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
487
552
|
await storage.append(this.getStreamFileName(), combined);
|
|
488
553
|
// New writes added during the await are after `batch`, so removing the front is exactly the flushed set.
|
|
489
554
|
this.pendingAppends.splice(0, batch.length);
|
|
555
|
+
// Keep the stream-size estimate current so the fold trigger sees write-accumulation between reads.
|
|
556
|
+
this.streamBytesOnDisk += combined.length;
|
|
557
|
+
for (const p of batch) this.streamRowsOnDisk += p.rows;
|
|
490
558
|
}
|
|
491
559
|
|
|
492
560
|
public async update(entry: Partial<T> & { key: string }): Promise<void> {
|
|
@@ -603,10 +671,13 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
603
671
|
return entries.map(e => e.entry);
|
|
604
672
|
}
|
|
605
673
|
|
|
606
|
-
// Throttled, fire-and-forget after writes: run a background merge check at most once per interval
|
|
674
|
+
// Throttled, fire-and-forget after writes: run a background merge check at most once per interval — but
|
|
675
|
+
// a tier-0 stream that's grown too big folds promptly, bypassing the throttle. Skipped entirely over
|
|
676
|
+
// the network unless the app opted in (see automaticCompactionAllowed).
|
|
607
677
|
private async maybeMerge(): Promise<void> {
|
|
678
|
+
if (!await this.automaticCompactionAllowed()) return;
|
|
608
679
|
const now = Date.now();
|
|
609
|
-
if (now - this.lastMergeCheck < bulkDatabase2Timing.mergeCheckIntervalMs) return;
|
|
680
|
+
if (!this.streamNeedsFold() && now - this.lastMergeCheck < bulkDatabase2Timing.mergeCheckIntervalMs) return;
|
|
610
681
|
this.lastMergeCheck = now;
|
|
611
682
|
try {
|
|
612
683
|
await this.tryMergeNow();
|
|
@@ -951,8 +1022,12 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
951
1022
|
if (recentBytes >= FIRST_MERGE_BYTES) break;
|
|
952
1023
|
}
|
|
953
1024
|
const span = recent.length ? recent[0].time - recent[recent.length - 1].time : 0;
|
|
954
|
-
|
|
955
|
-
|
|
1025
|
+
// Fold when there's enough fragmentation to consolidate, OR when a foldable stream has grown too
|
|
1026
|
+
// big to keep reading whole (even if it's the only recent file) — see streamNeedsFold.
|
|
1027
|
+
const heavyStreamRecent = this.streamNeedsFold() && recent.some(i => i.kind === "stream");
|
|
1028
|
+
const triggered =
|
|
1029
|
+
recent.length >= 2 && (recent.length > bulkDatabase2Timing.firstMergeTriggerFiles || span > bulkDatabase2Timing.firstMergeTriggerRangeMs)
|
|
1030
|
+
|| heavyStreamRecent && recent.length >= 1;
|
|
956
1031
|
if (triggered) {
|
|
957
1032
|
const rb = recent.filter(i => i.kind === "bulk").map(i => (i.file as BulkFileInfo));
|
|
958
1033
|
const rs = recent.filter(i => i.kind === "stream").map(i => (i.file as StreamFileInfo));
|
|
@@ -1074,14 +1149,21 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1074
1149
|
|
|
1075
1150
|
public async getColumn<Column extends keyof T>(column: Column): Promise<{ key: string; value: T[Column]; time: number }[]> {
|
|
1076
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;
|
|
1077
1156
|
let time = Date.now();
|
|
1078
1157
|
let reader = await this.reader();
|
|
1079
|
-
let base = await reader.getColumn(
|
|
1080
|
-
let result = this.patchColumn(base,
|
|
1158
|
+
let base = await reader.getColumn(col);
|
|
1159
|
+
let result = this.patchColumn(base, col) as { key: string; value: T[Column]; time: number }[];
|
|
1081
1160
|
time = Date.now() - time;
|
|
1082
1161
|
if (time > 50) {
|
|
1083
1162
|
console.log(`${blue(`${this.name}.getColumn(${JSON.stringify(column)})`)} took ${red(formatTime(time))} ${this.formatInfo(reader)}`);
|
|
1084
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);
|
|
1085
1167
|
return result;
|
|
1086
1168
|
}
|
|
1087
1169
|
|
|
@@ -1176,12 +1258,19 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1176
1258
|
// Observe the overlay-wide signal so we recompute once the base arrives or the overlay changes.
|
|
1177
1259
|
this.deps.observe(OVERLAY_SIGNAL);
|
|
1178
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 }[];
|
|
1179
1263
|
let base = this.baseColumns.get(col);
|
|
1180
1264
|
if (!base) {
|
|
1181
1265
|
this.ensureBaseColumn(col);
|
|
1182
1266
|
return undefined;
|
|
1183
1267
|
}
|
|
1184
|
-
|
|
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;
|
|
1185
1274
|
}
|
|
1186
1275
|
|
|
1187
1276
|
public async getColumnInfo() {
|
|
@@ -51,6 +51,7 @@ export type DirectoryWrapper = {
|
|
|
51
51
|
readonly kind: "directory";
|
|
52
52
|
readonly name: string;
|
|
53
53
|
readonly fullPath?: string;
|
|
54
|
+
readonly isRemote?: boolean;
|
|
54
55
|
removeEntry(key: string, options?: {
|
|
55
56
|
recursive?: boolean;
|
|
56
57
|
}): Promise<void>;
|
|
@@ -138,6 +139,7 @@ export type NestedFileStorage = {
|
|
|
138
139
|
};
|
|
139
140
|
export type FileStorage = IStorageRaw & {
|
|
140
141
|
folder: NestedFileStorage;
|
|
142
|
+
isRemote?: boolean;
|
|
141
143
|
};
|
|
142
144
|
export declare function wrapHandle(handle: DirectoryWrapper): FileStorage;
|
|
143
145
|
export declare function getRemoteFileStorageFactory(url: string, password: string, options?: RemoteOptions): (pathStr: string) => Promise<FileStorage>;
|
|
@@ -63,6 +63,9 @@ export type DirectoryWrapper = {
|
|
|
63
63
|
// Full path from the storage root, for diagnostics/logging (the native handle doesn't expose paths,
|
|
64
64
|
// so it's optional). e.g. "bulkDatabases2/myCollection".
|
|
65
65
|
readonly fullPath?: string;
|
|
66
|
+
// True when this storage is served over the network (a remote server) rather than a local disk, so
|
|
67
|
+
// callers can adapt for the higher latency/cost (local/native handles leave it undefined = false).
|
|
68
|
+
readonly isRemote?: boolean;
|
|
66
69
|
removeEntry(key: string, options?: { recursive?: boolean }): Promise<void>;
|
|
67
70
|
getFileHandle(key: string, options?: { create?: boolean }): Promise<FileWrapper>;
|
|
68
71
|
getDirectoryHandle(key: string, options?: { create?: boolean }): Promise<DirectoryWrapper>;
|
|
@@ -509,6 +512,9 @@ export type NestedFileStorage = {
|
|
|
509
512
|
|
|
510
513
|
export type FileStorage = IStorageRaw & {
|
|
511
514
|
folder: NestedFileStorage;
|
|
515
|
+
// Mirrors DirectoryWrapper.isRemote: true when this storage is served over the network. Lets callers
|
|
516
|
+
// (e.g. BulkDatabase2, which skips automatic compaction over the network by default) adapt.
|
|
517
|
+
isRemote?: boolean;
|
|
512
518
|
};
|
|
513
519
|
|
|
514
520
|
let appendQueue = cache((key: string) => {
|
|
@@ -705,6 +711,7 @@ export function wrapHandle(handle: DirectoryWrapper): FileStorage {
|
|
|
705
711
|
return {
|
|
706
712
|
...wrapHandleFiles(handle),
|
|
707
713
|
folder: wrapHandleNested(handle),
|
|
714
|
+
isRemote: handle.isRemote,
|
|
708
715
|
};
|
|
709
716
|
}
|
|
710
717
|
|
|
@@ -317,7 +317,16 @@ export function startRemoteFileServer(options: RemoteFileServerOptions): Promise
|
|
|
317
317
|
if (!accessLog.size) return;
|
|
318
318
|
const rows = [...accessLog.values()].sort((a, b) => b.bytes - a.bytes);
|
|
319
319
|
accessLog.clear();
|
|
320
|
-
|
|
320
|
+
// Fixed-width columns first so lines align, then the variable-length path last. count is the
|
|
321
|
+
// number of requests folded into this row (R = requests, not a multiplier).
|
|
322
|
+
const time = new Date().toTimeString().slice(0, 8);
|
|
323
|
+
for (const e of rows) {
|
|
324
|
+
const size = formatBytes(e.bytes).padStart(9);
|
|
325
|
+
const reqs = (e.count + "R").padStart(6);
|
|
326
|
+
const op = `[${e.op.padEnd(5)}]`;
|
|
327
|
+
const ip = e.ip.padEnd(15);
|
|
328
|
+
console.log(` ${time} ${size} ${reqs} ${op} ${ip} ${e.path}`);
|
|
329
|
+
}
|
|
321
330
|
}, 5000);
|
|
322
331
|
flushTimer.unref?.();
|
|
323
332
|
}
|
|
@@ -377,6 +377,7 @@ class RemoteDirectoryWrapper implements DirectoryWrapper {
|
|
|
377
377
|
// Mirror the native FileSystemDirectoryHandle shape (name/kind/entries) so code written against the
|
|
378
378
|
// native API — e.g. recursive walks using `handle.entries()` — works the same over the network.
|
|
379
379
|
readonly kind = "directory" as const;
|
|
380
|
+
readonly isRemote = true;
|
|
380
381
|
get name() { return baseName(this.dirPath); }
|
|
381
382
|
get fullPath() { return this.dirPath; }
|
|
382
383
|
async removeEntry(key: string): Promise<void> {
|