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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -23,6 +23,7 @@ export type IStorage<T> = {
23
23
  lastModified: number;
24
24
  }>;
25
25
  reset(): Promise<void>;
26
+ watchResync?: (callback: () => void) => void;
26
27
  };
27
28
  export type IStorageRaw = {
28
29
  get(key: string): Promise<Buffer | undefined>;
@@ -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.
@@ -10,6 +10,9 @@ export declare class StorageSync<T> implements IStorageSync<T> {
10
10
  synced: {
11
11
  keySeqNum: number;
12
12
  };
13
+ resynced: {
14
+ seqNum: number;
15
+ };
13
16
  constructor(storage: IStorage<T>);
14
17
  get(key: string): T | undefined;
15
18
  set(key: string, value: T): void;
@@ -11,9 +11,16 @@ export class StorageSync<T> implements IStorageSync<T> {
11
11
  keySeqNum: 0,
12
12
  }, undefined, { deep: false });
13
13
 
14
- constructor(public storage: IStorage<T>) { }
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
- sort(entries, x => x.time);
335
- for (let entry of entries) {
336
- let time = entry.time;
337
- let prevTime = pendingWriteTimes.get(entry.key);
338
- if (prevTime && prevTime > time) {
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
- let prev = this.cache.get(entry.key);
346
- if (prev && (prev.time > entry.time)) {
347
- continue;
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
  });