sliftutils 0.58.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
@@ -140,6 +140,20 @@ declare module "sliftutils/misc/zip" {
140
140
 
141
141
  }
142
142
 
143
+ declare module "sliftutils/render-utils/Anchor" {
144
+ import preact from "preact";
145
+ import { URLParam } from "./URLParam";
146
+ export declare class Anchor extends preact.Component<{
147
+ className?: string;
148
+ params: ([URLParam, unknown] | [string, string])[];
149
+ button?: boolean;
150
+ } & Omit<preact.JSX.HTMLAttributes<HTMLAnchorElement>, "href">> {
151
+ render(): preact.JSX.Element;
152
+ }
153
+ export declare function createLinkRaw(params: ([URLParam, unknown])[]): string;
154
+
155
+ }
156
+
143
157
  declare module "sliftutils/render-utils/DropdownCustom" {
144
158
  import preact from "preact";
145
159
  import { LengthOrPercentage } from "typesafecss/cssTypes";
@@ -772,6 +786,7 @@ declare module "sliftutils/storage/IStorage" {
772
786
  lastModified: number;
773
787
  }>;
774
788
  reset(): Promise<void>;
789
+ watchResync?: (callback: () => void) => void;
775
790
  };
776
791
  export type IStorageRaw = {
777
792
  get(key: string): Promise<Buffer | undefined>;
@@ -890,6 +905,9 @@ declare module "sliftutils/storage/StorageObservable" {
890
905
  synced: {
891
906
  keySeqNum: number;
892
907
  };
908
+ resynced: {
909
+ seqNum: number;
910
+ };
893
911
  constructor(storage: IStorage<T>);
894
912
  get(key: string): T | undefined;
895
913
  set(key: string, value: T): void;
@@ -928,11 +946,14 @@ declare module "sliftutils/storage/TransactionStorage" {
928
946
  private debugName;
929
947
  private writeDelay;
930
948
  cache: Map<string, TransactionEntry>;
949
+ private diskFiles;
931
950
  private currentChunk;
932
951
  private entryCount;
933
952
  private static allStorage;
934
953
  constructor(rawStorage: IStorageRaw, debugName: string, writeDelay?: number);
935
954
  static compressAll(): Promise<void>;
955
+ private resyncCallbacks;
956
+ watchResync(callback: () => void): void;
936
957
  private init;
937
958
  private getCurrentChunk;
938
959
  private onAddToChunk;
@@ -949,6 +970,7 @@ declare module "sliftutils/storage/TransactionStorage" {
949
970
  pushAppend(entry: TransactionEntry): Promise<void>;
950
971
  private updatePendingAppends;
951
972
  getKeys(): Promise<string[]>;
973
+ checkDisk(): Promise<void>;
952
974
  private loadAllTransactions;
953
975
  private parseTransactionFile;
954
976
  private applyTransactionEntries;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "0.58.0",
3
+ "version": "0.60.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -0,0 +1,10 @@
1
+ import preact from "preact";
2
+ import { URLParam } from "./URLParam";
3
+ export declare class Anchor extends preact.Component<{
4
+ className?: string;
5
+ params: ([URLParam, unknown] | [string, string])[];
6
+ button?: boolean;
7
+ } & Omit<preact.JSX.HTMLAttributes<HTMLAnchorElement>, "href">> {
8
+ render(): preact.JSX.Element;
9
+ }
10
+ export declare function createLinkRaw(params: ([URLParam, unknown])[]): string;
@@ -1,10 +1,6 @@
1
1
  import preact from "preact";
2
2
  import { URLParam, batchURLParamUpdate, getResolvedParam } from "./URLParam";
3
- import { css, isNode } from "typesafecss";
4
-
5
- export const AnchorClass = (
6
- css.textDecoration("none").color("hsl(210, 75%, 65%)").opacity(0.8, "hover")
7
- );
3
+ import { CSSType, css, isNode } from "typesafecss";
8
4
 
9
5
  export class Anchor extends preact.Component<{
10
6
  className?: string;
@@ -11,4 +11,9 @@ export const errorMessage = css.hsl(0, 75, 50).color("white")
11
11
  export const warnMessage = css.hsl(50, 75, 50).color("hsl(0, 0%, 7%)", "important", "soft")
12
12
  .padding("4px 6px", "soft")
13
13
  .whiteSpace("pre-wrap").display("inline-block", "soft")
14
- ;
14
+ ;
15
+
16
+
17
+ export const AnchorClass = (
18
+ css.textDecoration("none").color("hsl(210, 75%, 65%)").opacity(0.8, "hover")
19
+ );
@@ -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
  });
@@ -3,8 +3,8 @@
3
3
  "compilerOptions": {
4
4
  "declaration": true,
5
5
  "emitDeclarationOnly": true,
6
- "outDir": "./render-utils",
7
- "rootDir": "./render-utils"
6
+ "outDir": "./",
7
+ "rootDir": "./"
8
8
  },
9
9
  "include": [
10
10
  "render-utils/**/*.ts",