sliftutils 1.4.13 → 1.4.14
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
|
@@ -1014,6 +1014,13 @@ declare module "sliftutils/storage/BulkDatabase2/BulkDatabaseBase" {
|
|
|
1014
1014
|
column: string;
|
|
1015
1015
|
byteSize: number;
|
|
1016
1016
|
}[]>;
|
|
1017
|
+
getKeyStats(): Promise<{
|
|
1018
|
+
rawKeys: number;
|
|
1019
|
+
finalKeys: number;
|
|
1020
|
+
wastedKeys: number;
|
|
1021
|
+
duplication: number;
|
|
1022
|
+
readers: number;
|
|
1023
|
+
}>;
|
|
1017
1024
|
getReaderInfo(): Promise<{
|
|
1018
1025
|
rowCount: number;
|
|
1019
1026
|
totalBytes: number;
|
package/package.json
CHANGED
|
@@ -117,6 +117,13 @@ export declare class BulkDatabaseBase<T extends {
|
|
|
117
117
|
column: string;
|
|
118
118
|
byteSize: number;
|
|
119
119
|
}[]>;
|
|
120
|
+
getKeyStats(): Promise<{
|
|
121
|
+
rawKeys: number;
|
|
122
|
+
finalKeys: number;
|
|
123
|
+
wastedKeys: number;
|
|
124
|
+
duplication: number;
|
|
125
|
+
readers: number;
|
|
126
|
+
}>;
|
|
120
127
|
getReaderInfo(): Promise<{
|
|
121
128
|
rowCount: number;
|
|
122
129
|
totalBytes: number;
|
|
@@ -637,7 +637,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
637
637
|
syncBroadcastSeal(this.name);
|
|
638
638
|
this.streamFileName = undefined;
|
|
639
639
|
const { bulkFiles, streamFiles } = await this.listFiles();
|
|
640
|
-
|
|
640
|
+
// compact() merges every file on disk, so nothing older survives outside it — tombstones for
|
|
641
|
+
// fully-deleted keys can be dropped rather than carried into a fresh carry stream.
|
|
642
|
+
if (bulkFiles.length + streamFiles.length >= 1) await this.mergeFileSet(bulkFiles, streamFiles, true);
|
|
641
643
|
} finally {
|
|
642
644
|
releaseMergeLock(this.name, writerId);
|
|
643
645
|
}
|
|
@@ -662,7 +664,9 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
662
664
|
const selStream = streamFiles.filter(f =>
|
|
663
665
|
f.timestamp <= timeHi && f.timestamp + bulkDatabase2Timing.streamSealAgeMs >= timeLo);
|
|
664
666
|
if (selBulk.length + selStream.length < 2) return;
|
|
665
|
-
|
|
667
|
+
// timeLo <= 0 reaches the start of time: every older file overlaps [timeLo, timeHi] and is in the
|
|
668
|
+
// merge, so no older set survives outside it and surviving tombstones can be dropped.
|
|
669
|
+
await this.mergeFileSet(selBulk, selStream, timeLo <= 0);
|
|
666
670
|
}
|
|
667
671
|
|
|
668
672
|
// Throws MissingFileError (not a generic error) when the file is gone, so callers can distinguish a
|
|
@@ -804,7 +808,12 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
804
808
|
// merge removes them), never a gap. A bulk file is deleted only if we actually read it; a stream file
|
|
805
809
|
// only if it's aged out (its writer has switched files) or — when cross-tab sync sealed it — its size
|
|
806
810
|
// didn't change while we read it. Returns whether it produced anything.
|
|
807
|
-
|
|
811
|
+
// `includesOldest` means this merge consumes every file at or before its time range — there is no
|
|
812
|
+
// file before it on disk. A surviving tombstone only exists to suppress an OLDER set in some file
|
|
813
|
+
// outside the merge; if nothing older exists, that older set can't exist either, so the tombstone has
|
|
814
|
+
// nothing left to suppress and we drop it instead of carrying it forward. (A full compact and a
|
|
815
|
+
// merge that reaches time 0 are the cases where this holds.)
|
|
816
|
+
private async mergeFileSet(bulkFiles: BulkFileInfo[], streamFiles: StreamFileInfo[], includesOldest = false): Promise<boolean> {
|
|
808
817
|
const storage = await this.storage();
|
|
809
818
|
const timestamp = nextFileTime();
|
|
810
819
|
const now = Date.now();
|
|
@@ -837,7 +846,10 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
837
846
|
newNames.push(name);
|
|
838
847
|
}
|
|
839
848
|
}
|
|
840
|
-
if
|
|
849
|
+
// Carry surviving tombstones forward only if older files exist outside this merge that they still
|
|
850
|
+
// need to suppress; when this merge includes the oldest data there's nothing older to suppress.
|
|
851
|
+
const carriedDeletes = includesOldest ? 0 : deletes.size;
|
|
852
|
+
if (carriedDeletes) {
|
|
841
853
|
const carryName = `stream_${Date.now()}_${Math.random().toString(36).slice(2, 10)}${STREAM_EXTENSION}`;
|
|
842
854
|
await storage.set(carryName, frameDeletes([...deletes].map(([key, time]) => ({ time, key }))));
|
|
843
855
|
}
|
|
@@ -849,7 +861,7 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
849
861
|
}
|
|
850
862
|
|
|
851
863
|
this.resetReader();
|
|
852
|
-
return newNames.length > 0 ||
|
|
864
|
+
return newNames.length > 0 || carriedDeletes > 0;
|
|
853
865
|
}
|
|
854
866
|
|
|
855
867
|
// A stream file is safe to delete iff no writer will ever append to it again: it's aged past the seal
|
|
@@ -1177,6 +1189,25 @@ export class BulkDatabaseBase<T extends { key: string }> {
|
|
|
1177
1189
|
return reader.columns;
|
|
1178
1190
|
}
|
|
1179
1191
|
|
|
1192
|
+
// Raw vs. resolved key counts: how much duplicate/stale key data is sitting on disk that a compact()
|
|
1193
|
+
// would collapse. rawKeys counts every key-slot across all loaded files — each set and each delete
|
|
1194
|
+
// tombstone (a key written into N files counts N times); finalKeys is the number of live resolved
|
|
1195
|
+
// keys (after newest-write-wins and tombstones). Both come straight from the already-loaded reader, so
|
|
1196
|
+
// this is ~free. wastedKeys = rawKeys - finalKeys; duplication = rawKeys / finalKeys (well above 1 ⇒
|
|
1197
|
+
// fragmented, compaction would shrink it).
|
|
1198
|
+
public async getKeyStats(): Promise<{ rawKeys: number; finalKeys: number; wastedKeys: number; duplication: number; readers: number }> {
|
|
1199
|
+
const reader = await this.reader();
|
|
1200
|
+
const rawKeys = reader.rawKeyCount;
|
|
1201
|
+
const finalKeys = reader.keys.length;
|
|
1202
|
+
return {
|
|
1203
|
+
rawKeys,
|
|
1204
|
+
finalKeys,
|
|
1205
|
+
wastedKeys: rawKeys - finalKeys,
|
|
1206
|
+
duplication: finalKeys ? rawKeys / finalKeys : 0,
|
|
1207
|
+
readers: reader.readerCount,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1180
1211
|
public async getReaderInfo() {
|
|
1181
1212
|
let reader = await this.reader();
|
|
1182
1213
|
return {
|
|
@@ -1212,6 +1243,11 @@ type ResolvedReader = {
|
|
|
1212
1243
|
rowCount: number;
|
|
1213
1244
|
totalBytes: number;
|
|
1214
1245
|
keys: string[];
|
|
1246
|
+
// Total key-slots across every loaded reader — every set AND every delete tombstone (a key stored in
|
|
1247
|
+
// N files counts N times) — and how many readers there are. Already in memory after the join, so
|
|
1248
|
+
// getKeyStats is ~free; rawKeyCount vs keys.length is how much duplication a compaction would collapse.
|
|
1249
|
+
rawKeyCount: number;
|
|
1250
|
+
readerCount: number;
|
|
1215
1251
|
columns: { column: string; byteSize: number }[];
|
|
1216
1252
|
getColumn: (column: string) => Promise<{ key: string; value: unknown; time: number }[]>;
|
|
1217
1253
|
getSingleField: (key: string, column: string) => Promise<{ value: unknown; time: number } | undefined>;
|
|
@@ -1256,6 +1292,8 @@ async function joinBulkDatabases(databases: BaseBulkDatabaseReader[]): Promise<R
|
|
|
1256
1292
|
totalBytes: databases.reduce((acc, db) => acc + db.totalBytes, 0),
|
|
1257
1293
|
rowCount: keys.length,
|
|
1258
1294
|
keys,
|
|
1295
|
+
rawKeyCount: databases.reduce((acc, db) => acc + db.keyTimes.size + (db.deleteTimes?.size ?? 0), 0),
|
|
1296
|
+
readerCount: databases.length,
|
|
1259
1297
|
columns,
|
|
1260
1298
|
async getColumn(column) {
|
|
1261
1299
|
const perReader = await Promise.all(databases.map(async db => {
|