sliftutils 0.59.0 → 0.60.0
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 +8 -0
- package/package.json +1 -1
- package/storage/IStorage.d.ts +1 -0
- package/storage/IStorage.ts +2 -0
- package/storage/StorageObservable.d.ts +3 -0
- package/storage/StorageObservable.ts +10 -1
- package/storage/TransactionStorage.d.ts +4 -0
- package/storage/TransactionStorage.ts +80 -9
package/index.d.ts
CHANGED
|
@@ -786,6 +786,7 @@ declare module "sliftutils/storage/IStorage" {
|
|
|
786
786
|
lastModified: number;
|
|
787
787
|
}>;
|
|
788
788
|
reset(): Promise<void>;
|
|
789
|
+
watchResync?: (callback: () => void) => void;
|
|
789
790
|
};
|
|
790
791
|
export type IStorageRaw = {
|
|
791
792
|
get(key: string): Promise<Buffer | undefined>;
|
|
@@ -904,6 +905,9 @@ declare module "sliftutils/storage/StorageObservable" {
|
|
|
904
905
|
synced: {
|
|
905
906
|
keySeqNum: number;
|
|
906
907
|
};
|
|
908
|
+
resynced: {
|
|
909
|
+
seqNum: number;
|
|
910
|
+
};
|
|
907
911
|
constructor(storage: IStorage<T>);
|
|
908
912
|
get(key: string): T | undefined;
|
|
909
913
|
set(key: string, value: T): void;
|
|
@@ -942,11 +946,14 @@ declare module "sliftutils/storage/TransactionStorage" {
|
|
|
942
946
|
private debugName;
|
|
943
947
|
private writeDelay;
|
|
944
948
|
cache: Map<string, TransactionEntry>;
|
|
949
|
+
private diskFiles;
|
|
945
950
|
private currentChunk;
|
|
946
951
|
private entryCount;
|
|
947
952
|
private static allStorage;
|
|
948
953
|
constructor(rawStorage: IStorageRaw, debugName: string, writeDelay?: number);
|
|
949
954
|
static compressAll(): Promise<void>;
|
|
955
|
+
private resyncCallbacks;
|
|
956
|
+
watchResync(callback: () => void): void;
|
|
950
957
|
private init;
|
|
951
958
|
private getCurrentChunk;
|
|
952
959
|
private onAddToChunk;
|
|
@@ -963,6 +970,7 @@ declare module "sliftutils/storage/TransactionStorage" {
|
|
|
963
970
|
pushAppend(entry: TransactionEntry): Promise<void>;
|
|
964
971
|
private updatePendingAppends;
|
|
965
972
|
getKeys(): Promise<string[]>;
|
|
973
|
+
checkDisk(): Promise<void>;
|
|
966
974
|
private loadAllTransactions;
|
|
967
975
|
private parseTransactionFile;
|
|
968
976
|
private applyTransactionEntries;
|
package/package.json
CHANGED
package/storage/IStorage.d.ts
CHANGED
package/storage/IStorage.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type IStorage<T> = {
|
|
|
22
22
|
lastModified: number;
|
|
23
23
|
}>;
|
|
24
24
|
reset(): Promise<void>;
|
|
25
|
+
// Allows watching for when the storage detects and underlying changes, and resyncs all of it's data (which might
|
|
26
|
+
watchResync?: (callback: () => void) => void;
|
|
25
27
|
};
|
|
26
28
|
// NOTE: In the file system some characters are disallowed, and some characters do special things
|
|
27
29
|
// (/ makes a folder). And there are even more rules, such as lengths per folder, etc, etc.
|
|
@@ -11,9 +11,16 @@ export class StorageSync<T> implements IStorageSync<T> {
|
|
|
11
11
|
keySeqNum: 0,
|
|
12
12
|
}, undefined, { deep: false });
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
resynced = observable({ seqNum: 0 });
|
|
15
|
+
|
|
16
|
+
constructor(public storage: IStorage<T>) {
|
|
17
|
+
storage.watchResync?.(() => {
|
|
18
|
+
this.resynced.seqNum++;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
public get(key: string): T | undefined {
|
|
23
|
+
this.resynced.seqNum;
|
|
17
24
|
if (!this.cached.has(key)) {
|
|
18
25
|
this.cached.set(key, undefined);
|
|
19
26
|
void this.getPromise(key);
|
|
@@ -41,12 +48,14 @@ export class StorageSync<T> implements IStorageSync<T> {
|
|
|
41
48
|
}
|
|
42
49
|
private loadedKeys = false;
|
|
43
50
|
public getKeys(): string[] {
|
|
51
|
+
this.resynced.seqNum;
|
|
44
52
|
void this.getKeysPromise();
|
|
45
53
|
this.synced.keySeqNum;
|
|
46
54
|
return Array.from(this.keys);
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
public getInfo(key: string): { size: number; lastModified: number } | undefined {
|
|
58
|
+
this.resynced.seqNum;
|
|
50
59
|
if (!this.infoCached.has(key)) {
|
|
51
60
|
this.infoCached.set(key, { size: 0, lastModified: 0 });
|
|
52
61
|
void this.storage.getInfo(key).then(info => {
|
|
@@ -12,11 +12,14 @@ export declare class TransactionStorage implements IStorage<Buffer> {
|
|
|
12
12
|
private debugName;
|
|
13
13
|
private writeDelay;
|
|
14
14
|
cache: Map<string, TransactionEntry>;
|
|
15
|
+
private diskFiles;
|
|
15
16
|
private currentChunk;
|
|
16
17
|
private entryCount;
|
|
17
18
|
private static allStorage;
|
|
18
19
|
constructor(rawStorage: IStorageRaw, debugName: string, writeDelay?: number);
|
|
19
20
|
static compressAll(): Promise<void>;
|
|
21
|
+
private resyncCallbacks;
|
|
22
|
+
watchResync(callback: () => void): void;
|
|
20
23
|
private init;
|
|
21
24
|
private getCurrentChunk;
|
|
22
25
|
private onAddToChunk;
|
|
@@ -33,6 +36,7 @@ export declare class TransactionStorage implements IStorage<Buffer> {
|
|
|
33
36
|
pushAppend(entry: TransactionEntry): Promise<void>;
|
|
34
37
|
private updatePendingAppends;
|
|
35
38
|
getKeys(): Promise<string[]>;
|
|
39
|
+
checkDisk(): Promise<void>;
|
|
36
40
|
private loadAllTransactions;
|
|
37
41
|
private parseTransactionFile;
|
|
38
42
|
private applyTransactionEntries;
|
|
@@ -6,6 +6,7 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
|
|
|
6
6
|
import { setPending } from "./PendingManager";
|
|
7
7
|
import { isInBuild } from "../misc/environment";
|
|
8
8
|
import { isNode } from "typesafecss";
|
|
9
|
+
import { runInfinitePoll } from "socket-function/src/batching";
|
|
9
10
|
|
|
10
11
|
/*
|
|
11
12
|
// Spec:
|
|
@@ -39,6 +40,7 @@ UPDATE now we use chunks, because append is too slow.
|
|
|
39
40
|
IMPORTANT! If there are multiple writers, we clobber writes from other writers when we compress
|
|
40
41
|
*/
|
|
41
42
|
|
|
43
|
+
const DISK_CHECK_INTERVAL = timeInMinute * 5;
|
|
42
44
|
|
|
43
45
|
const FILE_CHUNK_SIZE = 1024 * 1024;
|
|
44
46
|
const FILE_MAX_LIFETIME = timeInMinute * 30;
|
|
@@ -76,6 +78,7 @@ function getNextChunkPath(): string {
|
|
|
76
78
|
|
|
77
79
|
export class TransactionStorage implements IStorage<Buffer> {
|
|
78
80
|
public cache: Map<string, TransactionEntry> = new Map();
|
|
81
|
+
private diskFiles: Set<string> = new Set();
|
|
79
82
|
private currentChunk: {
|
|
80
83
|
path: string;
|
|
81
84
|
size: number;
|
|
@@ -91,6 +94,8 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
91
94
|
private writeDelay = WRITE_DELAY
|
|
92
95
|
) {
|
|
93
96
|
TransactionStorage.allStorage.push(this);
|
|
97
|
+
// VERY useful for debugging.
|
|
98
|
+
(globalThis as any)[`transactionStorage-${this.debugName}`] = this;
|
|
94
99
|
}
|
|
95
100
|
// Helps get rid of parse errors which constantly log. Also, uses less space
|
|
96
101
|
public static async compressAll() {
|
|
@@ -101,6 +106,11 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
101
106
|
});
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
private resyncCallbacks: (() => void)[] = [];
|
|
110
|
+
public watchResync(callback: () => void): void {
|
|
111
|
+
this.resyncCallbacks.push(callback);
|
|
112
|
+
}
|
|
113
|
+
|
|
104
114
|
private init: Promise<unknown> | undefined = this.loadAllTransactions();
|
|
105
115
|
|
|
106
116
|
private getCurrentChunk(): string {
|
|
@@ -113,6 +123,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
113
123
|
size: 0,
|
|
114
124
|
timeCreated: Date.now()
|
|
115
125
|
};
|
|
126
|
+
this.diskFiles.add(this.currentChunk.path);
|
|
116
127
|
}
|
|
117
128
|
return this.currentChunk.path;
|
|
118
129
|
}
|
|
@@ -239,14 +250,35 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
239
250
|
return Array.from(this.cache.keys());
|
|
240
251
|
}
|
|
241
252
|
|
|
253
|
+
public async checkDisk(): Promise<void> {
|
|
254
|
+
if (this.init) await this.init;
|
|
255
|
+
const anyChanges = async () => {
|
|
256
|
+
let keys = await this.rawStorage.getKeys();
|
|
257
|
+
let diskFiles = keys.filter(key => key.endsWith(CHUNK_EXT));
|
|
258
|
+
let hasNew = diskFiles.some(file => !this.diskFiles.has(file));
|
|
259
|
+
return hasNew;
|
|
260
|
+
};
|
|
261
|
+
if (!await anyChanges()) return;
|
|
262
|
+
|
|
263
|
+
await fileLockSection(async () => {
|
|
264
|
+
if (!await anyChanges()) return;
|
|
265
|
+
await this.loadAllTransactions();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
242
269
|
|
|
243
270
|
// NOTE: This is either called in init (which blocks all other calls), or inside of the global file lock, so it is safe to load.
|
|
244
271
|
private async loadAllTransactions(): Promise<string[]> {
|
|
245
272
|
if (isInBuild()) return [];
|
|
246
273
|
|
|
274
|
+
if (this.init) {
|
|
275
|
+
runInfinitePoll(DISK_CHECK_INTERVAL, () => this.checkDisk());
|
|
276
|
+
}
|
|
277
|
+
|
|
247
278
|
let time = Date.now();
|
|
248
279
|
const keys = await this.rawStorage.getKeys();
|
|
249
280
|
const transactionFiles = keys.filter(key => key.endsWith(CHUNK_EXT));
|
|
281
|
+
this.diskFiles = new Set(transactionFiles);
|
|
250
282
|
|
|
251
283
|
let entryList: TransactionEntry[][] = [];
|
|
252
284
|
for (let file of transactionFiles) {
|
|
@@ -330,25 +362,62 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
330
362
|
}
|
|
331
363
|
pendingWriteTimes.set(entry.key, entry.time);
|
|
332
364
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
365
|
+
let latest = new Map<string, TransactionEntry>();
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
let pendingTime = pendingWriteTimes.get(entry.key);
|
|
368
|
+
if (pendingTime && pendingTime > entry.time) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
let prev = latest.get(entry.key);
|
|
372
|
+
if (prev && prev.time > entry.time) {
|
|
339
373
|
continue;
|
|
340
374
|
}
|
|
375
|
+
latest.set(entry.key, entry);
|
|
376
|
+
}
|
|
341
377
|
|
|
378
|
+
let anyChanged = false;
|
|
379
|
+
for (let entry of latest.values()) {
|
|
342
380
|
if (entry.value === undefined) {
|
|
381
|
+
if (!this.cache.has(entry.key)) continue;
|
|
382
|
+
anyChanged = true;
|
|
343
383
|
this.cache.delete(entry.key);
|
|
344
384
|
} else {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
385
|
+
if (!anyChanged) {
|
|
386
|
+
let prev = this.cache.get(entry.key);
|
|
387
|
+
if (!prev || prev.isZipped !== entry.isZipped) {
|
|
388
|
+
anyChanged = true;
|
|
389
|
+
} else {
|
|
390
|
+
if (!prev.value) {
|
|
391
|
+
if (entry.value) {
|
|
392
|
+
anyChanged = true;
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
if (!entry.value) {
|
|
396
|
+
anyChanged = true;
|
|
397
|
+
} else {
|
|
398
|
+
// Both values, so... it might not have changed
|
|
399
|
+
if (!prev.value.equals(entry.value)) {
|
|
400
|
+
anyChanged = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
348
405
|
}
|
|
349
406
|
this.cache.set(entry.key, entry);
|
|
350
407
|
}
|
|
351
408
|
}
|
|
409
|
+
|
|
410
|
+
if (anyChanged) {
|
|
411
|
+
for (const callback of this.resyncCallbacks) {
|
|
412
|
+
try {
|
|
413
|
+
callback();
|
|
414
|
+
} catch (e) {
|
|
415
|
+
setImmediate(() => {
|
|
416
|
+
throw e;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
352
421
|
}
|
|
353
422
|
|
|
354
423
|
private readTransactionEntry(buffer: Buffer, offset: number): {
|
|
@@ -511,6 +580,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
511
580
|
// the other generations, which is annoying).
|
|
512
581
|
for (const file of existingDiskEntries) {
|
|
513
582
|
await this.rawStorage.remove(file);
|
|
583
|
+
this.diskFiles.delete(file);
|
|
514
584
|
}
|
|
515
585
|
} finally {
|
|
516
586
|
this.compressing = false;
|
|
@@ -528,6 +598,7 @@ export class TransactionStorage implements IStorage<Buffer> {
|
|
|
528
598
|
|
|
529
599
|
this.pendingAppends = [];
|
|
530
600
|
this.cache.clear();
|
|
601
|
+
this.diskFiles.clear();
|
|
531
602
|
this.currentChunk = undefined;
|
|
532
603
|
this.entryCount = 0;
|
|
533
604
|
});
|