sliftutils 1.7.32 → 1.7.33

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.
Files changed (116) hide show
  1. package/builders/extensionBuild.ts +1 -1
  2. package/bundler/bundleEntry.ts +1 -3
  3. package/bundler/bundler.ts +2 -4
  4. package/bundler/sourceMaps.ts +1 -2
  5. package/examplestorage/browser.tsx +1 -2
  6. package/examplestorage/exampleserver.ts +1 -2
  7. package/index.d.ts +105 -100
  8. package/misc/dist/environment.ts.cache +2 -2
  9. package/misc/dist/getSecret.ts.cache +2 -2
  10. package/misc/dist/strings.ts.cache +2 -2
  11. package/misc/dist/zip.ts.cache +2 -2
  12. package/misc/https/dist/certs.ts.cache +2 -2
  13. package/misc/https/dist/dns.ts.cache +7 -16
  14. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  15. package/misc/https/dns.d.ts +1 -4
  16. package/misc/https/dns.ts +5 -14
  17. package/misc/https/hostServer.d.ts +2 -0
  18. package/misc/https/hostServer.ts +6 -7
  19. package/misc/https/httpsCerts.ts +4 -10
  20. package/misc/openrouter.ts +1 -2
  21. package/package.json +1 -1
  22. package/render-utils/FullscreenModal.tsx +1 -4
  23. package/render-utils/GenericFormat.tsx +2 -8
  24. package/render-utils/Input.tsx +5 -13
  25. package/render-utils/InputLabel.tsx +4 -6
  26. package/render-utils/InputPicker.tsx +1 -2
  27. package/render-utils/URLParam.ts +2 -3
  28. package/render-utils/dist/observer.tsx.cache +2 -2
  29. package/render-utils/mobxTyped.ts +2 -3
  30. package/render-utils/niceStringify.ts +2 -4
  31. package/storage/ArchivesDisk.ts +4 -14
  32. package/storage/BulkDatabase2/BulkDatabase2.d.ts +1 -2
  33. package/storage/BulkDatabase2/BulkDatabase2.ts +4 -11
  34. package/storage/BulkDatabase2/BulkDatabaseBase.ts +42 -114
  35. package/storage/BulkDatabase2/BulkDatabaseFormat.ts +31 -91
  36. package/storage/BulkDatabase2/BulkDatabaseMerge.ts +19 -57
  37. package/storage/BulkDatabase2/BulkDatabaseReader.ts +4 -12
  38. package/storage/BulkDatabase2/LoadedIndex.ts +6 -18
  39. package/storage/BulkDatabase2/WriteOverlay.ts +3 -8
  40. package/storage/BulkDatabase2/blockCache.ts +12 -28
  41. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +40 -103
  42. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +19 -50
  43. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +18 -50
  44. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +7 -15
  45. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +9 -21
  46. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +6 -11
  47. package/storage/BulkDatabase2/dist/blockCache.ts.cache +15 -31
  48. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +10 -29
  49. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +10 -18
  50. package/storage/BulkDatabase2/dist/streamLog.ts.cache +17 -43
  51. package/storage/BulkDatabase2/dist/syncClient.ts.cache +11 -27
  52. package/storage/BulkDatabase2/mergeLock.ts +7 -26
  53. package/storage/BulkDatabase2/mergeMarkers.ts +7 -15
  54. package/storage/BulkDatabase2/streamLog.ts +14 -40
  55. package/storage/BulkDatabase2/syncClient.ts +8 -24
  56. package/storage/FileFolderAPI.tsx +28 -75
  57. package/storage/IArchives.d.ts +3 -3
  58. package/storage/IArchives.ts +21 -50
  59. package/storage/IStorage.ts +2 -4
  60. package/storage/PendingManager.tsx +2 -3
  61. package/storage/TransactionStorage.ts +11 -24
  62. package/storage/backblaze.ts +12 -43
  63. package/storage/dist/ArchivesDisk.ts.cache +7 -17
  64. package/storage/dist/FileFolderAPI.tsx.cache +25 -63
  65. package/storage/dist/IArchives.ts.cache +10 -20
  66. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
  67. package/storage/dist/JSONStorage.ts.cache +2 -2
  68. package/storage/dist/PendingManager.tsx.cache +5 -6
  69. package/storage/dist/TransactionStorage.ts.cache +14 -27
  70. package/storage/dist/backblaze.ts.cache +15 -46
  71. package/storage/dist/embeddingFormats.ts.cache +11 -24
  72. package/storage/dist/fileSystemPointer.ts.cache +4 -7
  73. package/storage/dist/remoteFileStorage.ts.cache +17 -36
  74. package/storage/embeddingFormats.ts +11 -27
  75. package/storage/fileSystemPointer.ts +2 -7
  76. package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +8 -18
  77. package/storage/proxydatabase/dist/transactionSet.ts.cache +4 -7
  78. package/storage/proxydatabase/inMemoryDatabase.ts +1 -3
  79. package/storage/proxydatabase/ivfEmbeddingDatabase.ts +8 -23
  80. package/storage/proxydatabase/transactionSet.ts +2 -5
  81. package/storage/remoteFileServer.ts +20 -48
  82. package/storage/remoteFileStorage.ts +14 -33
  83. package/storage/remoteStorage/ArchivesRemote.d.ts +1 -4
  84. package/storage/remoteStorage/ArchivesRemote.ts +11 -26
  85. package/storage/remoteStorage/ArchivesUrl.ts +2 -4
  86. package/storage/remoteStorage/accessPage.tsx +1 -5
  87. package/storage/remoteStorage/blobStore.d.ts +5 -13
  88. package/storage/remoteStorage/blobStore.ts +52 -130
  89. package/storage/remoteStorage/createArchives.d.ts +9 -13
  90. package/storage/remoteStorage/createArchives.ts +43 -187
  91. package/storage/remoteStorage/deployTakeover.d.ts +18 -32
  92. package/storage/remoteStorage/deployTakeover.ts +70 -384
  93. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +12 -24
  94. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +5 -7
  95. package/storage/remoteStorage/dist/blobStore.ts.cache +47 -117
  96. package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
  97. package/storage/remoteStorage/dist/createArchives.ts.cache +48 -189
  98. package/storage/remoteStorage/dist/deployTakeover.ts.cache +81 -387
  99. package/storage/remoteStorage/dist/intermediateSources.ts.cache +119 -0
  100. package/storage/remoteStorage/dist/remoteConfig.ts.cache +36 -24
  101. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +20 -40
  102. package/storage/remoteStorage/dist/storageClientController.ts.cache +4 -6
  103. package/storage/remoteStorage/dist/storageController.ts.cache +11 -73
  104. package/storage/remoteStorage/dist/storageServerState.ts.cache +265 -178
  105. package/storage/remoteStorage/grantAccessCli.ts +1 -3
  106. package/storage/remoteStorage/intermediateSources.d.ts +21 -0
  107. package/storage/remoteStorage/intermediateSources.ts +117 -0
  108. package/storage/remoteStorage/remoteConfig.ts +33 -20
  109. package/storage/remoteStorage/sourceWrapper.d.ts +8 -19
  110. package/storage/remoteStorage/sourceWrapper.ts +19 -41
  111. package/storage/remoteStorage/storageClientController.ts +1 -3
  112. package/storage/remoteStorage/storageController.d.ts +4 -5
  113. package/storage/remoteStorage/storageController.ts +10 -81
  114. package/storage/remoteStorage/storageServer.ts +20 -34
  115. package/storage/remoteStorage/storageServerState.d.ts +28 -5
  116. package/storage/remoteStorage/storageServerState.ts +271 -207
@@ -13,18 +13,11 @@ import { ROUTING_FILE, getRoute, routeContains } from "./remoteConfig";
13
13
  import { BulkDatabaseBase, noopReactiveDeps } from "../BulkDatabase2/BulkDatabaseBase";
14
14
  import { wrapHandle, NodeJSDirectoryHandleWrapper, DirectoryWrapper } from "../FileFolderAPI";
15
15
 
16
- // The storage engine of the remote storage server. Data lives in synchronization sources (at
17
- // minimum an ArchivesDisk, the local disk); BlobStore keeps an index of every file (path, last
18
- // modified time, size, and which source currently holds the data) in a BulkDatabase2, and
19
- // synchronizes the index from all sources (see ArchivesSource in IArchives.ts).
20
- // Every startup fully rescans each source's metadata, so the index self-heals; the file with the
21
- // highest write time wins across all sources, so multiple sources need no stacking order.
16
+ // The storage engine of the remote storage server. Data lives in synchronization sources (at minimum an ArchivesDisk, the local disk); BlobStore keeps an index of every file (path, last modified time, size, and which source currently holds the data) in a BulkDatabase2, and synchronizes the index from all sources (see ArchivesSource in IArchives.ts). Every startup fully rescans each source's metadata, so the index self-heals; the file with the highest write time wins across all sources, so multiple sources need no stacking order.
22
17
 
23
18
  export const DEFAULT_FAST_WRITE_DELAY = timeInMinute * 5;
24
19
  const FAST_FLUSH_POLL = 1000 * 15;
25
- // Fast writes are never delayed past our own valid window, and within this margin of the window's
26
- // end they write through immediately - so when the next window's source takes over, the writes are
27
- // already on disk
20
+ // Fast writes are never delayed past our own valid window, and within this margin of the window's end they write through immediately - so when the next window's source takes over, the writes are already on disk
28
21
  export const WINDOW_END_FLUSH_MARGIN = timeInMinute * 5;
29
22
  // Index changes are buffered in memory and written to the BulkDatabase2 in batches
30
23
  const INDEX_FLUSH_INTERVAL = 1000 * 30;
@@ -32,38 +25,31 @@ const INDEX_FLUSH_INTERVAL = 1000 * 30;
32
25
  const CHANGES_POLL_INTERVAL = 1000 * 60;
33
26
  // Sources that don't support getChangesAfter get a full metadata rescan this often
34
27
  const FULL_RESCAN_INTERVAL = 1000 * 60 * 60;
35
- // On a request for a file the index doesn't know, changes-after sources are re-polled, at most
36
- // this often
28
+ // On a request for a file the index doesn't know, changes-after sources are re-polled, at most this often
37
29
  const MISS_CHECK_INTERVAL = 1000 * 5;
38
30
  // Change polls re-request this much overlap, so clock skew between us and a source can't drop changes
39
31
  const CHANGES_POLL_OVERLAP = timeInMinute;
40
32
  const SCAN_RETRY_DELAY = 1000 * 30;
41
- // Deletes are tombstones (an empty file IS a missing file): the size-0 index entry is what lets a
42
- // deletion propagate/reconcile like any other write, and it expires after this long
33
+ // Deletes are tombstones (an empty file IS a missing file): the size-0 index entry is what lets a deletion propagate/reconcile like any other write, and it expires after this long
43
34
  const TOMBSTONE_EXPIRY = 1000 * 60 * 60 * 24 * 7;
44
35
  const TOMBSTONE_CLEANUP_INTERVAL = 1000 * 60 * 60;
45
36
  // While a metadata scan or full sync is running, its progress is logged this often
46
37
  const SYNC_PROGRESS_LOG_INTERVAL = 1000 * 60;
47
38
  const DISK_LIMIT_CHECK_INTERVAL = 1000 * 60;
48
- // Full syncs download this many files concurrently (high-latency sources like backblaze would
49
- // otherwise crawl one round-trip at a time)
39
+ // Full syncs download this many files concurrently (high-latency sources like backblaze would otherwise crawl one round-trip at a time)
50
40
  const FULL_SYNC_PARALLEL = 8;
51
- // A full sync running longer than this is console.errored (and again every interval after), so a
52
- // sync that will take days is loud instead of a quiet console.log every minute
41
+ // A full sync running longer than this is console.errored (and again every interval after), so a sync that will take days is loud instead of a quiet console.log every minute
53
42
  const FULL_SYNC_SLOW_ERROR_INTERVAL = 1000 * 60 * 60;
54
43
 
55
44
  export type WriteConfig = {
56
- // Resolve once the write is in memory; flush to the sources after writeDelay, coalescing
57
- // writes to the same key (only the latest is written). Data is lost if the process crashes first.
45
+ // Resolve once the write is in memory; flush to the sources after writeDelay, coalescing writes to the same key (only the latest is written). Data is lost if the process crashes first.
58
46
  fast?: boolean;
59
47
  writeDelay?: number;
60
- // Stamps the write with this last-write time instead of now. Older than the current file's
61
- // time no-ops; more than 15 minutes in the future throws. See IArchives.set.
48
+ // Stamps the write with this last-write time instead of now. Older than the current file's time no-ops; more than 15 minutes in the future throws. See IArchives.set.
62
49
  lastModified?: number;
63
50
  };
64
51
 
65
- // What the storage server needs from a bucket's store. BlobStore implements it fully; ArchivesDisk
66
- // also satisfies it (used directly for rawDisk buckets), minus the optional index-backed methods.
52
+ // What the storage server needs from a bucket's store. BlobStore implements it fully; ArchivesDisk also satisfies it (used directly for rawDisk buckets), minus the optional index-backed methods.
67
53
  export type IBucketStore = {
68
54
  get(fileName: string, config?: { range?: { start: number; end: number } }): Promise<Buffer | undefined>;
69
55
  get2(fileName: string, config?: { range?: { start: number; end: number } }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined>;
@@ -105,11 +91,9 @@ type IndexEntry = {
105
91
  writeTime: number;
106
92
  size: number;
107
93
  source: number;
108
- // When WE last changed this entry (not the file's write time) — what getChangesAfter filters
109
- // on, so late-arriving files with old write times are still reported as changes
94
+ // When WE last changed this entry (not the file's write time) — what getChangesAfter filters on, so late-arriving files with old write times are still reported as changes
110
95
  changedAt: number;
111
- // In-memory only (not persisted): when the file was last served, for readerDiskLimit's LRU
112
- // eviction. Starts as changedAt on load.
96
+ // In-memory only (not persisted): when the file was last served, for readerDiskLimit's LRU eviction. Starts as changedAt on load.
113
97
  lastAccess: number;
114
98
  };
115
99
 
@@ -124,15 +108,13 @@ type SourceState = {
124
108
  lastMissCheck: number;
125
109
  // Per-slot stop token: a removed source's loops stop without touching the rest of the store
126
110
  stopped: { stop: boolean };
127
- // Source slots are stable forever (index entries reference sources by slot number), so a
128
- // removed source's slot stays in the arrays, marked dead - never scanned, written, or read
111
+ // Source slots are stable forever (index entries reference sources by slot number), so a removed source's slot stays in the arrays, marked dead - never scanned, written, or read
129
112
  dead?: boolean;
130
113
  };
131
114
 
132
115
  // One source of a live source-list update; see BlobStore.updateSources
133
116
  export type BlobSourceSpec = {
134
- // Matched against ArchivesSource.identity ("disk" for the base slot); equal identities pair
135
- // off in order
117
+ // Matched against ArchivesSource.identity ("disk" for the base slot); equal identities pair off in order
136
118
  identity: string;
137
119
  validWindow: [number, number];
138
120
  route?: [number, number];
@@ -169,11 +151,12 @@ export class BlobStore implements IBucketStore {
169
151
  private folder: string,
170
152
  private sources: ArchivesSource[],
171
153
  private config?: {
172
- // Called whenever a key's index entry changes (our own writes AND files pulled in via
173
- // synchronization) — how the storage server notices routing config updates.
154
+ // Called whenever a key's index entry changes (our own writes AND files pulled in via synchronization) — how the storage server notices routing config updates.
174
155
  onIndexChanged?: (key: string) => void;
175
156
  // LRU-bound the disk (base source) to this many bytes; see CommonConfig.readerDiskLimit
176
157
  readerDiskLimit?: number;
158
+ // Every accepted write ("original") and every write that actually reached the sources ("flushed"). Fast writes coalesce, so the two counts differ.
159
+ onWriteCounted?: (kind: "original" | "flushed", bytes: number) => void;
177
160
  }
178
161
  ) { }
179
162
 
@@ -187,18 +170,15 @@ export class BlobStore implements IBucketStore {
187
170
  }
188
171
  return wrapHandle(base);
189
172
  });
190
- // The in-memory copy of the index. All reads are served from it; changes are buffered in
191
- // dirty and flushed to the BulkDatabase2 in batches (undefined = delete).
173
+ // The in-memory copy of the index. All reads are served from it; changes are buffered in dirty and flushed to the BulkDatabase2 in batches (undefined = delete).
192
174
  private mem = new Map<string, IndexEntry>();
193
- // Live totals over mem (tombstones excluded), adjusted on every mutation and recomputed on
194
- // load - so any drift heals on restart. computeIndexTotals gives the walk-the-index truth.
175
+ // Live totals over mem (tombstones excluded), adjusted on every mutation and recomputed on load - so any drift heals on restart. computeIndexTotals gives the walk-the-index truth.
195
176
  private indexFileCount = 0;
196
177
  private indexByteCount = 0;
197
178
  // The same totals per holding source (index 0 = our disk, which readerDiskLimit bounds)
198
179
  private sourceFileCounts = this.sources.map(() => 0);
199
180
  private sourceByteCounts = this.sources.map(() => 0);
200
- // Background scans / full syncs currently in progress (a Set - one source can have a change
201
- // poll's full sync and a rescan overlapping)
181
+ // Background scans / full syncs currently in progress (a Set - one source can have a change poll's full sync and a rescan overlapping)
202
182
  private syncActivities = new Set<SyncActivity>();
203
183
  private dirty = new Map<string, IndexEntry | undefined>();
204
184
  private overlay = new Map<string, OverlayEntry>();
@@ -224,9 +204,7 @@ export class BlobStore implements IBucketStore {
224
204
  }
225
205
  });
226
206
 
227
- // Stops all synchronization scans/polls and flushes pending writes. Only used when the store
228
- // genuinely cannot continue (rawDisk flip, process shutdown) - routine config changes go
229
- // through updateSources instead, which the store survives.
207
+ // Stops all synchronization scans/polls and flushes pending writes. Only used when the store genuinely cannot continue (rawDisk flip, process shutdown) - routine config changes go through updateSources instead, which the store survives.
230
208
  public async dispose(): Promise<void> {
231
209
  this.stopped.stop = true;
232
210
  for (let state of this.sourceStates) {
@@ -249,8 +227,7 @@ export class BlobStore implements IBucketStore {
249
227
  let source = sourceMap.get(entry.key);
250
228
  // Explicit checks, as 0 is a valid size and a valid source number
251
229
  if (size === undefined || source === undefined) continue;
252
- // The routing config is only ever read off our own disk (see applyScanned), and a
253
- // loaded bucket always has it there - a persisted entry pointing elsewhere is stale
230
+ // The routing config is only ever read off our own disk (see applyScanned), and a loaded bucket always has it there - a persisted entry pointing elsewhere is stale
254
231
  if (entry.key === ROUTING_FILE) {
255
232
  source = 0;
256
233
  }
@@ -287,12 +264,7 @@ export class BlobStore implements IBucketStore {
287
264
  this.dirty.set(key, undefined);
288
265
  }
289
266
 
290
- /** Applies a config change to the RUNNING store: windows/routes update in place, new sources
291
- * are added (their sync starts immediately), and removed sources' slots go dead (their scans
292
- * stop, their index entries drop). The store survives every routine config evolution - it is
293
- * never destroyed for a source-list change, only for structural flips it cannot express
294
- * (rawDisk). Pending fast writes are re-capped to the new flush deadline (flushing
295
- * immediately when it has already passed). */
267
+ /** Applies a config change to the RUNNING store: windows/routes update in place, new sources are added (their sync starts immediately), and removed sources' slots go dead (their scans stop, their index entries drop). The store survives every routine config evolution - it is never destroyed for a source-list change, only for structural flips it cannot express (rawDisk). Pending fast writes are re-capped to the new flush deadline (flushing immediately when it has already passed). */
296
268
  public updateSources(specs: BlobSourceSpec[]): void {
297
269
  if (!specs.length || specs[0].identity !== "disk") {
298
270
  throw new Error(`updateSources expects the disk source first (identity "disk"), got ${JSON.stringify(specs.map(x => x.identity))} (store ${this.folder})`);
@@ -304,8 +276,7 @@ export class BlobStore implements IBucketStore {
304
276
  this.sources[i].validWindow = window;
305
277
  };
306
278
  setWindow(0, specs[0].validWindow);
307
- // Live slots pair with specs by identity, in order (the same endpoint can appear several
308
- // times, e.g. one entry per window)
279
+ // Live slots pair with specs by identity, in order (the same endpoint can appear several times, e.g. one entry per window)
309
280
  let liveByIdentity = new Map<string, number[]>();
310
281
  let originalLength = this.sources.length;
311
282
  for (let i = 1; i < originalLength; i++) {
@@ -362,9 +333,7 @@ export class BlobStore implements IBucketStore {
362
333
  }
363
334
  }
364
335
 
365
- // The slot stays in the arrays forever (index entries reference sources by slot number); it
366
- // just goes dead - loops stop, and its index entries drop (other sources' scans re-find any
367
- // copy that's still reachable through the new config)
336
+ // The slot stays in the arrays forever (index entries reference sources by slot number); it just goes dead - loops stop, and its index entries drop (other sources' scans re-find any copy that's still reachable through the new config)
368
337
  private removeSource(sourceIndex: number): void {
369
338
  let state = this.sourceStates[sourceIndex];
370
339
  let source = this.sources[sourceIndex].source;
@@ -381,16 +350,13 @@ export class BlobStore implements IBucketStore {
381
350
  console.log(`Removed sync source ${source.getDebugName()} (store ${this.folder}): its scans are stopped and ${dropped} index entries it held were dropped`);
382
351
  }
383
352
 
384
- /** Rescans our own disk's metadata into the index - used around valid window handoffs, where
385
- * another process wrote files to the shared folder that our index hasn't seen. */
353
+ /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
386
354
  public async rescanBase(): Promise<void> {
387
355
  await this.init();
388
356
  await this.scanSource(0);
389
357
  }
390
358
 
391
- /** A boundary scan of the node that owned (part of) our route in the valid window before ours,
392
- * when that node is different storage (a disk rescan can't see its writes): just its changes
393
- * since the boundary neighborhood, with matching values pulled onto our own disk. */
359
+ /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
394
360
  public async boundaryScanRemote(source: IArchives, config: { since: number; route?: [number, number] }): Promise<void> {
395
361
  await this.init();
396
362
  let scanStart = Date.now();
@@ -457,8 +423,7 @@ export class BlobStore implements IBucketStore {
457
423
  };
458
424
  }
459
425
 
460
- /** Walks the whole index for exact totals - more expensive than getSyncProgress, but immune to
461
- * any drift in the maintained counters (and loads the index first, so it's never cold zeros). */
426
+ /** Walks the whole index for exact totals - more expensive than getSyncProgress, but immune to any drift in the maintained counters (and loads the index first, so it's never cold zeros). */
462
427
  public async computeIndexTotals(): Promise<{
463
428
  fileCount: number;
464
429
  byteCount: number;
@@ -535,8 +500,7 @@ export class BlobStore implements IBucketStore {
535
500
  await this.pollChanges(sourceIndex);
536
501
  if (!noFullSync()) await this.copySourceFiles(sourceIndex);
537
502
  }, state.stopped);
538
- // Change polls only show what the source HAS, never what it's missing, so pushes run on
539
- // the full-rescan cadence (findInfo on an index-backed source is cheap)
503
+ // Change polls only show what the source HAS, never what it's missing, so pushes run on the full-rescan cadence (findInfo on an index-backed source is cheap)
540
504
  runInfinitePoll(FULL_RESCAN_INTERVAL, async () => {
541
505
  let files = await source.findInfo("");
542
506
  await this.reconcileSource(sourceIndex, new Map(files.map(x => [x.path, x.createTime])));
@@ -550,8 +514,7 @@ export class BlobStore implements IBucketStore {
550
514
  }
551
515
  }
552
516
 
553
- // Full metadata scan (size, writeTime, path) of one source, applied to the index. Returns the
554
- // source's listing (path -> write time), which reconcileSource uses for the push direction.
517
+ // Full metadata scan (size, writeTime, path) of one source, applied to the index. Returns the source's listing (path -> write time), which reconcileSource uses for the push direction.
555
518
  private async scanSource(sourceIndex: number): Promise<Map<string, number>> {
556
519
  let { source, validWindow, route } = this.sources[sourceIndex];
557
520
  let state = this.sourceStates[sourceIndex];
@@ -584,10 +547,7 @@ export class BlobStore implements IBucketStore {
584
547
  tally[this.applyScanned(sourceIndex, file)]++;
585
548
  }
586
549
  state.scannedCount = files.length;
587
- // Index entries this source was the holder of, but that vanished from it (e.g. deleted
588
- // while we were offline), come out of the index. Entries changed after the scan started
589
- // are kept — the scan listing may simply predate them. Tombstones have no physical file
590
- // for a listing to vouch for, so they're exempt (cleanupTombstones expires them instead).
550
+ // Index entries this source was the holder of, but that vanished from it (e.g. deleted while we were offline), come out of the index. Entries changed after the scan started are kept — the scan listing may simply predate them. Tombstones have no physical file for a listing to vouch for, so they're exempt (cleanupTombstones expires them instead).
591
551
  let removedFromIndex = 0;
592
552
  let missingOnSource = 0;
593
553
  for (let [key, entry] of this.mem) {
@@ -597,15 +557,13 @@ export class BlobStore implements IBucketStore {
597
557
  removedFromIndex++;
598
558
  continue;
599
559
  }
600
- // Counted only when the source SHOULD hold the entry (its window/route match) - these
601
- // are what the reconcile pass pushes to it
560
+ // Counted only when the source SHOULD hold the entry (its window/route match) - these are what the reconcile pass pushes to it
602
561
  if (entry.size === 0 || key === ROUTING_FILE) continue;
603
562
  if (entry.writeTime < validWindow[0] || entry.writeTime > validWindow[1]) continue;
604
563
  if (!routeContains(route, getRoute(key))) continue;
605
564
  missingOnSource++;
606
565
  }
607
- // Percentages are of the union of both sides (our index + their listing), so every count
608
- // has a stable denominator
566
+ // Percentages are of the union of both sides (our index + their listing), so every count has a stable denominator
609
567
  let union = indexSizeBefore + newPaths;
610
568
  let pct = (n: number) => `${Math.round(n / Math.max(union, 1) * 1000) / 10}%`;
611
569
  console.log(`Metadata scan of ${source.getDebugName()} finished in ${Math.round((Date.now() - scanStart) / 1000)}s (store ${this.folder}): ${files.length} listed vs ${indexSizeBefore} indexed (union ${union}): ${formatScanTally(tally, union)}, ${missingOnSource} in index but missing on source (${pct(missingOnSource)}), ${removedFromIndex} removed from index (${pct(removedFromIndex)})`);
@@ -613,10 +571,7 @@ export class BlobStore implements IBucketStore {
613
571
  return seen;
614
572
  }
615
573
 
616
- // The push direction of synchronization: everything we know that the source is missing (or
617
- // holds an older copy of) is written to it — including deletions, as tombstone writes. This is
618
- // what heals a source whose background writes failed (e.g. it was down): the next scan sees
619
- // what's missing and re-sends it.
574
+ // The push direction of synchronization: everything we know that the source is missing (or holds an older copy of) is written to it — including deletions, as tombstone writes. This is what heals a source whose background writes failed (e.g. it was down): the next scan sees what's missing and re-sends it.
620
575
  private async reconcileSource(sourceIndex: number, listing: Map<string, number>): Promise<void> {
621
576
  let { source, validWindow, route } = this.sources[sourceIndex];
622
577
  let state = this.sourceStates[sourceIndex];
@@ -625,8 +580,7 @@ export class BlobStore implements IBucketStore {
625
580
  for (let [key, entry] of this.mem) {
626
581
  if (this.stopped.stop || state.stopped.stop) return;
627
582
  if (entry.source === sourceIndex) continue;
628
- // The routing file is NEVER synchronized between storage nodes - it is only ever
629
- // written directly to each node, and only ever read off our own disk
583
+ // The routing file is NEVER synchronized between storage nodes - it is only ever written directly to each node, and only ever read off our own disk
630
584
  if (key === ROUTING_FILE) continue;
631
585
  if (!acceptsWrites) continue;
632
586
  if (!routeContains(route, getRoute(key))) continue;
@@ -652,11 +606,7 @@ export class BlobStore implements IBucketStore {
652
606
  // An in-flight scan can outlive its source's removal; its results are dead
653
607
  if (!this.isLive(sourceIndex)) return "filtered";
654
608
  if (file.path === ROUTING_FILE) {
655
- // The routing config is NEVER pulled from other sources - it only ever arrives as an
656
- // explicit, version-validated write, and is only ever read off our own disk. Route and
657
- // valid-window filters can't possibly apply to it either: it is the file DEFINING
658
- // them, so filtering it would mean certain sources could never have their routing
659
- // config updated, ever.
609
+ // The routing config is NEVER pulled from other sources - it only ever arrives as an explicit, version-validated write, and is only ever read off our own disk. Route and valid-window filters can't possibly apply to it either: it is the file DEFINING them, so filtering it would mean certain sources could never have their routing config updated, ever.
660
610
  if (sourceIndex !== 0) return "filtered";
661
611
  } else {
662
612
  let { validWindow, route } = this.sources[sourceIndex];
@@ -692,9 +642,7 @@ export class BlobStore implements IBucketStore {
692
642
  state.changesAfterTime = pollStart - CHANGES_POLL_OVERLAP;
693
643
  }
694
644
 
695
- // Downloads the files a source currently holds onto our own base source (the local disk),
696
- // preserving their modified times — so a newer local write always wins. Skipped for noFullSync
697
- // sources (fronting a large database without copying it); reads still down-cache lazily.
645
+ // Downloads the files a source currently holds onto our own base source (the local disk), preserving their modified times — so a newer local write always wins. Skipped for noFullSync sources (fronting a large database without copying it); reads still down-cache lazily.
698
646
  private async copySourceFiles(sourceIndex: number): Promise<void> {
699
647
  if (sourceIndex === 0) return;
700
648
  let { source } = this.sources[sourceIndex];
@@ -779,16 +727,12 @@ export class BlobStore implements IBucketStore {
779
727
  }
780
728
  }
781
729
 
782
- // findInfo and getChangesAfter list from the index, so they must wait for our own base
783
- // source's initial scan (which might lag minutes) before the listing is trustworthy. The base
784
- // (local disk) is implicitly required - remote sources are not, they come and go.
730
+ // findInfo and getChangesAfter list from the index, so they must wait for our own base source's initial scan (which might lag minutes) before the listing is trustworthy. The base (local disk) is implicitly required - remote sources are not, they come and go.
785
731
  private async waitForRequiredScans(): Promise<void> {
786
732
  await this.sourceStates[0].initialScan.promise;
787
733
  }
788
734
 
789
- // A requested file isn't in the index: our own base source (implicitly required) is checked
790
- // directly if its initial scan hasn't finished, and changes-after sources are re-polled (at
791
- // most every 5 seconds)
735
+ // A requested file isn't in the index: our own base source (implicitly required) is checked directly if its initial scan hasn't finished, and changes-after sources are re-polled (at most every 5 seconds)
792
736
  private async checkMissingKey(key: string): Promise<void> {
793
737
  for (let i = 0; i < this.sources.length; i++) {
794
738
  if (!this.isLive(i)) continue;
@@ -859,14 +803,12 @@ export class BlobStore implements IBucketStore {
859
803
  }
860
804
  return result;
861
805
  }
862
- // The routing file is only ever read off our own disk - falling back to another source's
863
- // copy would synchronize it between nodes through the read path, which it never is
806
+ // The routing file is only ever read off our own disk - falling back to another source's copy would synchronize it between nodes through the read path, which it never is
864
807
  if (key === ROUTING_FILE) {
865
808
  if (holderError) throw holderError;
866
809
  return undefined;
867
810
  }
868
- // The holder is down or lost the file. ANY other source's copy beats no value - even an
869
- // OLDER one - and it's copied onto our disk so the next read doesn't depend on luck.
811
+ // The holder is down or lost the file. ANY other source's copy beats no value - even an OLDER one - and it's copied onto our disk so the next read doesn't depend on luck.
870
812
  for (let i = 0; i < this.sources.length; i++) {
871
813
  if (i === holder || !this.isLive(i)) continue;
872
814
  let fallback: { data: Buffer; writeTime: number; size: number } | undefined;
@@ -889,8 +831,7 @@ export class BlobStore implements IBucketStore {
889
831
  return undefined;
890
832
  }
891
833
 
892
- // The read's bytes came from a remote source, so write them onto our own base source (the
893
- // local disk), which becomes the entry's new holder - reads only pay the remote fetch once
834
+ // The read's bytes came from a remote source, so write them onto our own base source (the local disk), which becomes the entry's new holder - reads only pay the remote fetch once
894
835
  private async cacheRead(key: string, result: { data: Buffer; writeTime: number }): Promise<void> {
895
836
  await this.sources[0].source.set(key, result.data, { lastModified: result.writeTime });
896
837
  this.setIndexEntry(key, { writeTime: result.writeTime, size: result.data.length, source: 0 });
@@ -898,6 +839,7 @@ export class BlobStore implements IBucketStore {
898
839
 
899
840
  public async set(key: string, data: Buffer, config?: WriteConfig): Promise<string> {
900
841
  await this.init();
842
+ this.config?.onWriteCounted?.("original", data.length);
901
843
  let lastModified = config?.lastModified;
902
844
  if (lastModified) {
903
845
  assertValidLastModified(lastModified);
@@ -909,16 +851,12 @@ export class BlobStore implements IBucketStore {
909
851
  }
910
852
  let writeTime = lastModified || Date.now();
911
853
  if (config?.fast) {
912
- // A writeDelay of zero is a real choice (no delay at all), so only an omitted delay
913
- // gets the default
854
+ // A writeDelay of zero is a real choice (no delay at all), so only an omitted delay gets the default
914
855
  let writeDelay = config.writeDelay;
915
856
  if (writeDelay === undefined) {
916
857
  writeDelay = DEFAULT_FAST_WRITE_DELAY;
917
858
  }
918
- // The delay never extends past our own valid window's end (minus the margin, so the
919
- // writes are on disk before the next window's source takes over - a deploy switchover
920
- // is just this too, since its remap ends our window). Past that point fast writes
921
- // write through immediately.
859
+ // The delay never extends past our own valid window's end (minus the margin, so the writes are on disk before the next window's source takes over - a deploy switchover is just this too, since its remap ends our window). Past that point fast writes write through immediately.
922
860
  let deadline = this.sources[0].validWindow[1] - WINDOW_END_FLUSH_MARGIN;
923
861
  if (writeDelay > 0 && Date.now() < deadline) {
924
862
  let flushAt = Math.min(Date.now() + writeDelay, deadline);
@@ -935,9 +873,7 @@ export class BlobStore implements IBucketStore {
935
873
  }
936
874
 
937
875
  public async del(key: string, config?: WriteConfig): Promise<void> {
938
- // Deletes are tombstone writes (an empty file IS a missing file): the size-0 index entry is
939
- // ordered by write time like any other write, propagates through synchronization, and
940
- // expires after TOMBSTONE_EXPIRY
876
+ // Deletes are tombstone writes (an empty file IS a missing file): the size-0 index entry is ordered by write time like any other write, propagates through synchronization, and expires after TOMBSTONE_EXPIRY
941
877
  await this.set(key, Buffer.alloc(0), config);
942
878
  }
943
879
 
@@ -952,19 +888,15 @@ export class BlobStore implements IBucketStore {
952
888
  }
953
889
 
954
890
  private async writeToSources(key: string, data: Buffer, writeTime: number): Promise<void> {
955
- // The routing file is NEVER synchronized between storage nodes: the writer writes it
956
- // directly to each node, so we store it on our own disk only (no valid-window filter -
957
- // routing/valid windows can't possibly apply to the file defining them) and never forward
958
- // it to other sources.
891
+ // The routing file is NEVER synchronized between storage nodes: the writer writes it directly to each node, so we store it on our own disk only (no valid-window filter - routing/valid windows can't possibly apply to the file defining them) and never forward it to other sources.
892
+ this.config?.onWriteCounted?.("flushed", data.length);
959
893
  let isRouting = key === ROUTING_FILE;
960
894
  let writable = this.getWritableSources({ ignoreWindow: isRouting });
961
895
  let first = writable.shift();
962
896
  if (first === undefined) {
963
897
  throw new Error(`No source accepts writes (every source's valid window is in the past), so writes cannot be stored (store ${this.folder})`);
964
898
  }
965
- // Only our own (first) source blocks the write. Downstream sources are written in the
966
- // background: a down downstream source must not fail or stall writes, and reconcileSource
967
- // re-sends anything they missed once they come back.
899
+ // Only our own (first) source blocks the write. Downstream sources are written in the background: a down downstream source must not fail or stall writes, and reconcileSource re-sends anything they missed once they come back.
968
900
  if (data.length === 0) {
969
901
  // A tombstone stores nothing on our own source - the index entry alone records it
970
902
  await this.sources[first].source.del(key);
@@ -976,8 +908,7 @@ export class BlobStore implements IBucketStore {
976
908
  let route = getRoute(key);
977
909
  for (let i of writable) {
978
910
  if (!routeContains(this.sources[i].route, route)) continue;
979
- // Downstream sources receive tombstones as actual empty writes, so their listings show
980
- // the deletion (size 0) and other stores scan it in as a tombstone
911
+ // Downstream sources receive tombstones as actual empty writes, so their listings show the deletion (size 0) and other stores scan it in as a tombstone
981
912
  void this.sources[i].source.set(key, data, { lastModified: writeTime }).catch((e: Error) => {
982
913
  console.error(`Background write of ${key} to sync source ${this.sources[i].source.getDebugName()} failed: ${e.stack ?? e}`);
983
914
  });
@@ -1018,10 +949,7 @@ export class BlobStore implements IBucketStore {
1018
949
  return files;
1019
950
  }
1020
951
 
1021
- // All files changed after the given time — fast, straight from the in-memory index. Filters on
1022
- // when WE learned of the change (changedAt), so files synchronized late (with old write times)
1023
- // are still reported. Deletions ARE reported, as size-0 tombstone entries — that's how they
1024
- // propagate to stores syncing from us.
952
+ // All files changed after the given time — fast, straight from the in-memory index. Filters on when WE learned of the change (changedAt), so files synchronized late (with old write times) are still reported. Deletions ARE reported, as size-0 tombstone entries — that's how they propagate to stores syncing from us.
1025
953
  public async getChangesAfter(time: number): Promise<ArchiveFileInfo[]> {
1026
954
  await this.init();
1027
955
  await this.waitForRequiredScans();
@@ -1098,10 +1026,7 @@ export class BlobStore implements IBucketStore {
1098
1026
  }
1099
1027
  }
1100
1028
 
1101
- // readerDiskLimit: the disk is only a bounded read cache, so once it exceeds the limit, the
1102
- // least recently used files are deleted from it - but ONLY when another source verifiably
1103
- // holds a same-or-newer copy (the only copy of a file is never deleted), and the index entry
1104
- // repoints to that source so reads keep working (re-caching on the next read).
1029
+ // readerDiskLimit: the disk is only a bounded read cache, so once it exceeds the limit, the least recently used files are deleted from it - but ONLY when another source verifiably holds a same-or-newer copy (the only copy of a file is never deleted), and the index entry repoints to that source so reads keep working (re-caching on the next read).
1105
1030
  private evicting = false;
1106
1031
  private async enforceDiskLimit(): Promise<void> {
1107
1032
  let limit = this.config?.readerDiskLimit;
@@ -1148,10 +1073,7 @@ export class BlobStore implements IBucketStore {
1148
1073
  }
1149
1074
  }
1150
1075
 
1151
- // Tombstones only need to exist long enough for every store to learn of the deletion; expired
1152
- // ones come out of the index. The physical empty file is removed only on backblaze sources:
1153
- // remote stores expire their own tombstones (a del there would just mint a fresh one), and our
1154
- // own disk never stored anything for it.
1076
+ // Tombstones only need to exist long enough for every store to learn of the deletion; expired ones come out of the index. The physical empty file is removed only on backblaze sources: remote stores expire their own tombstones (a del there would just mint a fresh one), and our own disk never stored anything for it.
1155
1077
  private async cleanupTombstones(): Promise<void> {
1156
1078
  let cutoff = Date.now() - TOMBSTONE_EXPIRY;
1157
1079
  for (let [key, entry] of this.mem) {
@@ -2,6 +2,8 @@
2
2
  /// <reference types="node" />
3
3
  import { IArchives, RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
4
4
  import { ServerBucketInfo } from "./storageServerState";
5
+ /** The address, port, account, and bucket name a bucket routing URL addresses. Throws when the URL isn't a hosted bucket routing URL (https://host:port/file/<account>/<bucketName>/storage/storagerouting.json). */
6
+ export { parseHostedUrl, parseBackblazeUrl, getBucketBaseUrl } from "./remoteConfig";
5
7
  export declare function createApiArchives(source: HostedConfig | BackblazeConfig): IArchives;
6
8
  export declare class ArchivesChain implements IArchives {
7
9
  private configured;
@@ -71,12 +73,7 @@ export declare class ArchivesChain implements IArchives {
71
73
  getChangesAfter(time: number): Promise<ArchiveFileInfo[]>;
72
74
  getSyncStatus(): Promise<ArchivesSyncStatus>;
73
75
  getConfig(): Promise<ArchivesConfig>;
74
- /** True only when EVERY write-receiving source would accept our writes (partial write access
75
- * desynchronizes sources, so it counts as no access). */
76
76
  hasWriteAccess(): Promise<boolean>;
77
- /** Returns the full key written. Plain keys come back unchanged; keys containing VARIABLE_SHARD
78
- * are automatically materialized (a shard value is picked and embedded, see setVariableShard)
79
- * and the caller needs the returned key to ever read the value back. */
80
77
  set(fileName: string, data: Buffer, config?: {
81
78
  lastModified?: number;
82
79
  }): Promise<string>;
@@ -91,18 +88,17 @@ export declare class ArchivesChain implements IArchives {
91
88
  dispose(): void;
92
89
  }
93
90
  export declare function createArchives(config: RemoteConfig | RemoteConfigBase): ArchivesChain;
94
- /** Every bucket an account has on one storage server - active and inactive - with each bucket's
95
- * configuration. One authenticated call (the normal trust system applies): no ArchivesChain, no
96
- * synchronization, and inactive buckets on the server stay inactive. Any URL addressing the
97
- * server works (a bucket routing URL, or just https://host:port). */
98
91
  export declare function listServerBuckets(config: {
99
92
  url: string;
100
93
  account: string;
101
94
  }): Promise<ServerBucketInfo[]>;
102
- /** Live info for one bucket given its routing URL (getConfig: routing config, index totals, disk
103
- * limit, in-progress synchronization). One authenticated call to that server - a light, safe
104
- * alternative to instantiating an ArchivesChain, which would start synchronization machinery. */
95
+ /** Zeroes the write statistics listServerBuckets reports, for every bucket in the account. */
96
+ export declare function clearServerWriteStats(config: {
97
+ url: string;
98
+ account: string;
99
+ }): Promise<{
100
+ clearedBuckets: number;
101
+ }>;
105
102
  export declare function getBucketInfo(config: {
106
103
  url: string;
107
- accountName?: string;
108
104
  }): Promise<ArchivesConfig>;