sliftutils 1.4.65 → 1.4.67
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 +14 -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 +3 -0
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +45 -9
- package/storage/BulkDatabase2/BulkDatabaseReader.d.ts +4 -0
- package/storage/BulkDatabase2/BulkDatabaseReader.ts +17 -0
package/index.d.ts
CHANGED
|
@@ -853,6 +853,13 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabase2" {
|
|
|
853
853
|
isFieldLoadedSync<Column extends keyof T>(key: string, column: Column): boolean;
|
|
854
854
|
/** Reactive: whether a whole column is loaded yet (see isFieldLoadedSync). */
|
|
855
855
|
isColumnLoadedSync<Column extends keyof T>(column: Column): boolean;
|
|
856
|
+
/**
|
|
857
|
+
* Reactive: true while a merge is rewriting this collection's files (background `maybeMerge` or
|
|
858
|
+
* an explicit `compact`/`merge`/`tryMergeNow`). Becomes false as soon as the new index is swapped
|
|
859
|
+
* in — the deferred-delete cleanup window is NOT counted. Use this in a UI to show a per-database
|
|
860
|
+
* "compacting…" indicator.
|
|
861
|
+
*/
|
|
862
|
+
isCompactingSync(): boolean;
|
|
856
863
|
/**
|
|
857
864
|
* Whether a row (key) is currently being watched by some reactive observer (getSingleFieldObjSync /
|
|
858
865
|
* getSingleFieldSync). Lets callers skip per-row work when nothing's watching. Non-reactive query;
|
|
@@ -936,6 +943,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
936
943
|
streamFoldHardLimitBytes: number;
|
|
937
944
|
writeFlushMaxDelayMs: number;
|
|
938
945
|
fileSetPollIntervalMs: number;
|
|
946
|
+
deleteDeferMs: number;
|
|
939
947
|
memoryFlushHeapBytes: number;
|
|
940
948
|
memoryFlushMinCollectionBytes: number;
|
|
941
949
|
memoryFlushThrottleMs: number;
|
|
@@ -1029,6 +1037,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
1029
1037
|
private fileLogicalSize;
|
|
1030
1038
|
private handleUnreadableFile;
|
|
1031
1039
|
private mergeFileSet;
|
|
1040
|
+
private mergeFileSetInner;
|
|
1032
1041
|
private canDeleteStream;
|
|
1033
1042
|
private mergeSpacingDelay;
|
|
1034
1043
|
private testMerge;
|
|
@@ -1058,6 +1067,7 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
1058
1067
|
}[] | undefined;
|
|
1059
1068
|
isFieldLoadedSync<C extends keyof T>(key: string, column: C): boolean;
|
|
1060
1069
|
isColumnLoadedSync<C extends keyof T>(column: C): boolean;
|
|
1070
|
+
isCompactingSync(): boolean;
|
|
1061
1071
|
getColumnInfo(): Promise<{
|
|
1062
1072
|
column: string;
|
|
1063
1073
|
byteSize: number;
|
|
@@ -1278,6 +1288,10 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseReader" {
|
|
|
1278
1288
|
isKeyWatched(key: string): boolean;
|
|
1279
1289
|
isLiveNow(key: string): boolean;
|
|
1280
1290
|
localTime(key: string): number;
|
|
1291
|
+
private compactingCount;
|
|
1292
|
+
beginCompaction(): void;
|
|
1293
|
+
endCompaction(): void;
|
|
1294
|
+
isCompactingSync(): boolean;
|
|
1281
1295
|
private notifyOverlayMutation;
|
|
1282
1296
|
getKeys(): Promise<string[]>;
|
|
1283
1297
|
getColumn<C extends keyof T>(column: C): Promise<{
|
package/package.json
CHANGED
|
@@ -88,6 +88,13 @@ export interface IBulkDatabase2<T extends {
|
|
|
88
88
|
isFieldLoadedSync<Column extends keyof T>(key: string, column: Column): boolean;
|
|
89
89
|
/** Reactive: whether a whole column is loaded yet (see isFieldLoadedSync). */
|
|
90
90
|
isColumnLoadedSync<Column extends keyof T>(column: Column): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Reactive: true while a merge is rewriting this collection's files (background `maybeMerge` or
|
|
93
|
+
* an explicit `compact`/`merge`/`tryMergeNow`). Becomes false as soon as the new index is swapped
|
|
94
|
+
* in — the deferred-delete cleanup window is NOT counted. Use this in a UI to show a per-database
|
|
95
|
+
* "compacting…" indicator.
|
|
96
|
+
*/
|
|
97
|
+
isCompactingSync(): boolean;
|
|
91
98
|
/**
|
|
92
99
|
* Whether a row (key) is currently being watched by some reactive observer (getSingleFieldObjSync /
|
|
93
100
|
* getSingleFieldSync). Lets callers skip per-row work when nothing's watching. Non-reactive query;
|
|
@@ -74,6 +74,14 @@ export interface IBulkDatabase2<T extends { key: string }> {
|
|
|
74
74
|
/** Reactive: whether a whole column is loaded yet (see isFieldLoadedSync). */
|
|
75
75
|
isColumnLoadedSync<Column extends keyof T>(column: Column): boolean;
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Reactive: true while a merge is rewriting this collection's files (background `maybeMerge` or
|
|
79
|
+
* an explicit `compact`/`merge`/`tryMergeNow`). Becomes false as soon as the new index is swapped
|
|
80
|
+
* in — the deferred-delete cleanup window is NOT counted. Use this in a UI to show a per-database
|
|
81
|
+
* "compacting…" indicator.
|
|
82
|
+
*/
|
|
83
|
+
isCompactingSync(): boolean;
|
|
84
|
+
|
|
77
85
|
/**
|
|
78
86
|
* Whether a row (key) is currently being watched by some reactive observer (getSingleFieldObjSync /
|
|
79
87
|
* getSingleFieldSync). Lets callers skip per-row work when nothing's watching. Non-reactive query;
|
|
@@ -12,6 +12,7 @@ export declare const bulkDatabase2Timing: {
|
|
|
12
12
|
streamFoldHardLimitBytes: number;
|
|
13
13
|
writeFlushMaxDelayMs: number;
|
|
14
14
|
fileSetPollIntervalMs: number;
|
|
15
|
+
deleteDeferMs: number;
|
|
15
16
|
memoryFlushHeapBytes: number;
|
|
16
17
|
memoryFlushMinCollectionBytes: number;
|
|
17
18
|
memoryFlushThrottleMs: number;
|
|
@@ -105,6 +106,7 @@ export declare class BulkDatabaseBase<T extends {
|
|
|
105
106
|
private fileLogicalSize;
|
|
106
107
|
private handleUnreadableFile;
|
|
107
108
|
private mergeFileSet;
|
|
109
|
+
private mergeFileSetInner;
|
|
108
110
|
private canDeleteStream;
|
|
109
111
|
private mergeSpacingDelay;
|
|
110
112
|
private testMerge;
|
|
@@ -134,6 +136,7 @@ export declare class BulkDatabaseBase<T extends {
|
|
|
134
136
|
}[] | undefined;
|
|
135
137
|
isFieldLoadedSync<C extends keyof T>(key: string, column: C): boolean;
|
|
136
138
|
isColumnLoadedSync<C extends keyof T>(column: C): boolean;
|
|
139
|
+
isCompactingSync(): boolean;
|
|
137
140
|
getColumnInfo(): Promise<{
|
|
138
141
|
column: string;
|
|
139
142
|
byteSize: number;
|
|
@@ -57,6 +57,12 @@ export const bulkDatabase2Timing = {
|
|
|
57
57
|
// the whole stream file per write.
|
|
58
58
|
writeFlushMaxDelayMs: isNode() ? 0 : 15 * 1000,
|
|
59
59
|
fileSetPollIntervalMs: 30 * 60 * 1000,
|
|
60
|
+
// Defer deletion of a merge's consumed inputs by this long. The OS may not durably flush the new
|
|
61
|
+
// outputs immediately, so if we deleted the inputs and crashed before the writes settled we'd lose
|
|
62
|
+
// data. Within the window the inputs sit as duplicates of the outputs; if we never crash, the timer
|
|
63
|
+
// fires and removes them. If we DO crash before the timer, the duplicates stay until the next
|
|
64
|
+
// key-stratify pass (high duplicate-key fraction → it merges them again, dedup completes).
|
|
65
|
+
deleteDeferMs: 15 * 60 * 1000,
|
|
60
66
|
memoryFlushHeapBytes: 1500 * 1024 * 1024,
|
|
61
67
|
memoryFlushMinCollectionBytes: 100 * 1024 * 1024,
|
|
62
68
|
memoryFlushThrottleMs: 15 * 60 * 1000,
|
|
@@ -626,9 +632,17 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
626
632
|
// we trigger an index rebuild + atomic swap; once swap completes, the consumed files' block-cache
|
|
627
633
|
// entries are evicted (no consumer can ask for them now).
|
|
628
634
|
private async mergeFileSet(bulkFiles: BulkFileInfo[], streamFiles: StreamFileInfo[], includesOldest = false, forceDeleteStreams = false): Promise<boolean> {
|
|
635
|
+
this.reader.beginCompaction();
|
|
636
|
+
try {
|
|
637
|
+
return await this.mergeFileSetInner(bulkFiles, streamFiles, includesOldest, forceDeleteStreams);
|
|
638
|
+
} finally {
|
|
639
|
+
this.reader.endCompaction();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private async mergeFileSetInner(bulkFiles: BulkFileInfo[], streamFiles: StreamFileInfo[], includesOldest: boolean, forceDeleteStreams: boolean): Promise<boolean> {
|
|
629
644
|
const storage = await this.storage();
|
|
630
645
|
const timestamp = nextFileTime();
|
|
631
|
-
const now = Date.now();
|
|
632
646
|
|
|
633
647
|
const consumedBulk: BulkFileInfo[] = [];
|
|
634
648
|
const bulkReaders: BaseBulkDatabaseReader[] = [];
|
|
@@ -702,15 +716,30 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
702
716
|
for (const line of steps) console.log(line);
|
|
703
717
|
console.groupEnd();
|
|
704
718
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
for
|
|
708
|
-
if (await this.canDeleteStream(f, now, streamData.sizes, forceDeleteStreams)) await remove(f.fileName);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// File set changed — rebuild + swap. After the swap, consumed files' block-cache entries are
|
|
712
|
-
// evicted (no reader will request them now).
|
|
719
|
+
// Rebuild + swap NOW so the index sees the new outputs; the old inputs are still on disk until
|
|
720
|
+
// the deferred-delete timer fires (the index briefly serves both, resolved by time so reads are
|
|
721
|
+
// correct). Block-cache eviction for inputs happens on the SECOND rebuild after deletion.
|
|
713
722
|
await this.triggerRebuild();
|
|
723
|
+
|
|
724
|
+
// Deferred delete: give the FS time to durably flush new writes before removing the inputs.
|
|
725
|
+
// If we crash before the timer fires, the inputs survive as duplicates and a later key-stratify
|
|
726
|
+
// pass dedupes them. We DON'T await this — the merge returns "done" as soon as the new outputs
|
|
727
|
+
// are written and the index is swapped in.
|
|
728
|
+
const remove = async (name: string) => { try { await storage.remove(name); } catch { /* already gone */ } };
|
|
729
|
+
const timer = setTimeout(() => {
|
|
730
|
+
void (async () => {
|
|
731
|
+
try {
|
|
732
|
+
for (const f of consumedBulk) await remove(f.fileName);
|
|
733
|
+
for (const f of streamFiles) {
|
|
734
|
+
if (await this.canDeleteStream(f, Date.now(), streamData.sizes, forceDeleteStreams)) await remove(f.fileName);
|
|
735
|
+
}
|
|
736
|
+
await this.triggerRebuild();
|
|
737
|
+
} catch (e) {
|
|
738
|
+
console.warn(`${this.name}: deferred delete of consumed merge inputs failed: ${(e as Error).message}`);
|
|
739
|
+
}
|
|
740
|
+
})();
|
|
741
|
+
}, bulkDatabase2Timing.deleteDeferMs);
|
|
742
|
+
(timer as { unref?: () => void }).unref?.();
|
|
714
743
|
return newNames.length > 0 || carriedDeletes > 0;
|
|
715
744
|
}
|
|
716
745
|
|
|
@@ -912,6 +941,13 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
912
941
|
return this.reader.isColumnLoadedSync(column);
|
|
913
942
|
}
|
|
914
943
|
|
|
944
|
+
// Reactive: true while a merge is rewriting this collection's files. Use this in UI to show a
|
|
945
|
+
// "compacting…" indicator. Counts both background merges (maybeMerge) and explicit compact/merge
|
|
946
|
+
// calls. Becomes false once the new index is swapped in (the deferred-delete window is NOT counted).
|
|
947
|
+
public isCompactingSync(): boolean {
|
|
948
|
+
return this.reader.isCompactingSync();
|
|
949
|
+
}
|
|
950
|
+
|
|
915
951
|
public async getColumnInfo() {
|
|
916
952
|
const index = await this.ensureIndex();
|
|
917
953
|
return index.reader.columns;
|
|
@@ -31,6 +31,10 @@ export declare class BulkDatabaseReader<T extends {
|
|
|
31
31
|
isKeyWatched(key: string): boolean;
|
|
32
32
|
isLiveNow(key: string): boolean;
|
|
33
33
|
localTime(key: string): number;
|
|
34
|
+
private compactingCount;
|
|
35
|
+
beginCompaction(): void;
|
|
36
|
+
endCompaction(): void;
|
|
37
|
+
isCompactingSync(): boolean;
|
|
34
38
|
private notifyOverlayMutation;
|
|
35
39
|
getKeys(): Promise<string[]>;
|
|
36
40
|
getColumn<C extends keyof T>(column: C): Promise<{
|
|
@@ -7,6 +7,7 @@ import { blue, red } from "socket-function/src/formatting/logColors";
|
|
|
7
7
|
const NULL = String.fromCharCode(0);
|
|
8
8
|
const LOAD_SIGNAL = NULL + "load";
|
|
9
9
|
const OVERLAY_SIGNAL = NULL + "overlay";
|
|
10
|
+
const COMPACTING_SIGNAL = NULL + "compacting";
|
|
10
11
|
const TRIGGER_THROTTLE_FIRST_STEP_MS = 16;
|
|
11
12
|
|
|
12
13
|
function nullJoin(a: string, b: string): string {
|
|
@@ -88,6 +89,22 @@ export class BulkDatabaseReader<T extends { key: string }> {
|
|
|
88
89
|
return -Infinity;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// Counter (so concurrent / nested merges in the same instance compose correctly). Signal fires
|
|
93
|
+
// immediately, not through the trigger throttle — UI spinners should show right away.
|
|
94
|
+
private compactingCount = 0;
|
|
95
|
+
beginCompaction(): void {
|
|
96
|
+
this.compactingCount++;
|
|
97
|
+
if (this.compactingCount === 1) this.cfg.deps.invalidate(COMPACTING_SIGNAL);
|
|
98
|
+
}
|
|
99
|
+
endCompaction(): void {
|
|
100
|
+
this.compactingCount--;
|
|
101
|
+
if (this.compactingCount === 0) this.cfg.deps.invalidate(COMPACTING_SIGNAL);
|
|
102
|
+
}
|
|
103
|
+
isCompactingSync(): boolean {
|
|
104
|
+
this.cfg.deps.observe(COMPACTING_SIGNAL);
|
|
105
|
+
return this.compactingCount > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
private notifyOverlayMutation(key: string, columns: Iterable<string> | "all"): void {
|
|
92
109
|
this.dataGen++;
|
|
93
110
|
if (columns === "all") this.columnCache.clear();
|