sliftutils 1.7.29 → 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 +106 -99
  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 +54 -186
  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 +32 -0
  103. package/storage/remoteStorage/dist/storageController.ts.cache +30 -64
  104. package/storage/remoteStorage/dist/storageServerState.ts.cache +269 -186
  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 +5 -3
  113. package/storage/remoteStorage/storageController.ts +12 -83
  114. package/storage/remoteStorage/storageServer.ts +20 -34
  115. package/storage/remoteStorage/storageServerState.d.ts +28 -6
  116. package/storage/remoteStorage/storageServerState.ts +275 -221
@@ -9,9 +9,10 @@ import {
9
9
  } from "../IArchives";
10
10
  import {
11
11
  ROUTING_FILE, getConfigVersion, parseHostedUrl, parseBackblazeUrl,
12
- normalizeRemoteConfig, normalizeSource, parseRoutingData, serializeRemoteConfig,
12
+ normalizeRemoteConfig, normalizeSource, serializeRemoteConfig,
13
13
  getRoute, routeContains, parseVariableRoute,
14
14
  } from "./remoteConfig";
15
+ import { resolveIntermediateSources } from "./intermediateSources";
15
16
  import { SocketFunction } from "socket-function/SocketFunction";
16
17
  import { ArchivesRemote, parseStorageUrl, authenticateStorage } from "./ArchivesRemote";
17
18
  import { onServerRoutingChanged } from "./storageClientController";
@@ -20,31 +21,15 @@ import { getStorageServerConfigOptional, getLocalArchives, ServerBucketInfo } fr
20
21
  import { RemoteStorageController, STORAGE_NOT_AUTHENTICATED } from "./storageController";
21
22
  import { SourceWrapper, RETRY_START_DELAY, RETRY_MAX_DELAY, RETRY_GROWTH } from "./sourceWrapper";
22
23
 
23
- // Turns a RemoteConfig into a usable IArchives (createArchives). Initialization is lazy: on the
24
- // first call we walk the sources IN ORDER and the first one that answers is authoritative (sources
25
- // are synchronized copies of the same bucket, so we never consult the rest). Its stored routing
26
- // config wins over the in-memory one - unless ours has a strictly newer version, in which case we
27
- // write ours (creating the bucket when no routing exists at all). A failed init (all sources down,
28
- // or the routing write was rejected) stays failed for callers but retries itself in the background
29
- // with a ramping delay. Once running, we re-read the routing config every CONFIG_POLL_INTERVAL and
30
- // rebuild the source list when it changed (reusing wrappers for unchanged sources).
31
-
32
24
  const CONFIG_POLL_INTERVAL = 5 * 60 * 1000;
33
- // Wrong-valid-window rejections within this distance of a known window boundary are just us racing
34
- // the boundary itself - waiting fixes them, no new config needed
35
25
  const WRONG_TARGET_BOUNDARY_WINDOW = 30 * 1000;
36
26
  const WRONG_TARGET_BOUNDARY_RETRY_DELAY = 15 * 1000;
37
- // Otherwise our config is stale; re-fetch it from the server, at most this often (when throttled,
38
- // retry with what we have)
39
27
  const CONFIG_REFRESH_THROTTLE = 30 * 1000;
40
- // When every source looks down, all of them are re-contacted (routing re-read + connection
41
- // re-attempt) before giving up - at most this often
42
28
  const AVAILABILITY_RECHECK_THROTTLE = 5 * 1000;
43
29
 
44
- // The direct API IArchives for one source, with no URL-form fallback and no chaining. Used by the
45
- // storage server for its synchronization sources. Denied calls throw immediately (registering the
46
- // access request in the background): the sync loops retry-and-log until access is granted, while
47
- // explicit writes surface the denial (with grant instructions) to the caller instead of hanging.
30
+ /** 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). */
31
+ export { parseHostedUrl, parseBackblazeUrl, getBucketBaseUrl } from "./remoteConfig";
32
+
48
33
  export function createApiArchives(source: HostedConfig | BackblazeConfig): IArchives {
49
34
  if (source.type === "backblaze") {
50
35
  return new ArchivesBackblaze({ bucketName: parseBackblazeUrl(source.url).bucketName, public: source.public, immutable: source.immutable, allowedOrigins: source.allowedOrigins });
@@ -52,11 +37,9 @@ export function createApiArchives(source: HostedConfig | BackblazeConfig): IArch
52
37
  let parsed = parseHostedUrl(source.url);
53
38
  let server = isNode() && getStorageServerConfigOptional() || undefined;
54
39
  if (server && parsed.address === server.domain && parsed.port === server.port) {
55
- // This source is a bucket hosted by our own process - use it directly instead of calling
56
- // ourselves over HTTPS
57
40
  return getLocalArchives(parsed.account, parsed.bucketName);
58
41
  }
59
- return new ArchivesRemote({ url: source.url, accountName: source.accountName, waitForAccess: false });
42
+ return new ArchivesRemote({ url: source.url, waitForAccess: false });
60
43
  }
61
44
 
62
45
  type ChainState = {
@@ -64,24 +47,18 @@ type ChainState = {
64
47
  sources: SourceWrapper[];
65
48
  };
66
49
 
67
- // The window must CONTAIN now (half-open, so a boundary resolves unambiguously). No grace in
68
- // either direction: a boundary is a hard handoff, and a write racing it is simply rejected by the
69
- // server and retried against the newly-valid source.
70
50
  function configWindowCurrent(config: HostedConfig | BackblazeConfig): boolean {
71
51
  let now = Date.now();
72
52
  let [start, end] = config.validWindow;
73
53
  return start <= now && now < end;
74
54
  }
75
55
 
76
- // Client writes target only the CURRENTLY valid source: a future-window source receives its data
77
- // through server-side downstream propagation/backfill, never directly from clients
78
56
  function configAcceptsWrites(config: HostedConfig | BackblazeConfig): boolean {
79
57
  return configWindowCurrent(config);
80
58
  }
81
59
 
82
60
  export class ArchivesChain implements IArchives {
83
61
  private configured: RemoteConfig;
84
- // The config we actually run on (the authoritative stored one after init)
85
62
  private activeConfig: RemoteConfig;
86
63
  private statePromise: Promise<ChainState> | undefined;
87
64
  private initRetryDelay = RETRY_START_DELAY;
@@ -93,10 +70,7 @@ export class ArchivesChain implements IArchives {
93
70
  constructor(config: RemoteConfig | RemoteConfigBase) {
94
71
  this.configured = normalizeRemoteConfig(config);
95
72
  this.activeConfig = this.configured;
96
- // Servers broadcast to their connected clients the moment a routing config changes; the
97
- // push runs the exact same refresh the poll does, just immediately
98
73
  this.unsubscribeRoutingPush = onServerRoutingChanged(() => {
99
- console.log(`A storage server broadcast a routing config change; refreshing config for ${this.getDebugName()}`);
100
74
  void this.refreshActiveConfig().catch((e: Error) => console.error(`Config refresh failed for ${this.getDebugName()}: ${e.stack ?? e}`));
101
75
  });
102
76
  }
@@ -106,9 +80,6 @@ export class ArchivesChain implements IArchives {
106
80
  return `chain ${urls.join(", ")}`;
107
81
  }
108
82
 
109
- // Lazy init that rethrows its error to every caller, while a background timer resets and
110
- // retries it with a ramping delay - so a chain that couldn't initialize fixes itself once a
111
- // source comes back, without any caller having to drive it.
112
83
  private getState(): Promise<ChainState> {
113
84
  if (this.disposed) {
114
85
  return Promise.reject(new Error(`ArchivesChain ${this.getDebugName()} has been disposed`));
@@ -138,16 +109,12 @@ export class ArchivesChain implements IArchives {
138
109
 
139
110
  private async init(): Promise<ChainState> {
140
111
  let configs = this.configured.sources.map(normalizeSource);
141
- // EVERY source is contacted for its picture of the routing config (per the spec: the config
142
- // is duplicated into all of them) - which also establishes our connection to every node up
143
- // front. Contact is parallel; ADOPTION is deterministic by config order among the sources
144
- // that answered (the first up source is authoritative).
145
112
  let fetches = await Promise.all(configs.map(async sourceConfig => {
146
113
  let probe = await SourceWrapper.create(sourceConfig, { background: false });
147
114
  let start = Date.now();
148
115
  try {
149
- let data = await probe.read(archives => archives.get(ROUTING_FILE));
150
- return { probe, sourceConfig, responded: true, latency: Date.now() - start, existing: data && parseRoutingData(data) || undefined, error: "" };
116
+ let existing = await probe.readRoutingConfig();
117
+ return { probe, sourceConfig, responded: true, latency: Date.now() - start, existing, error: "" };
151
118
  } catch (e) {
152
119
  return { probe, sourceConfig, responded: false, latency: 0, existing: undefined, error: `${sourceConfig.url}: ${(e as Error).stack ?? e}` };
153
120
  }
@@ -168,13 +135,6 @@ export class ArchivesChain implements IArchives {
168
135
  needsWrite = false;
169
136
  }
170
137
  if (needsWrite) {
171
- // We decided to write (ours is newer than the authoritative first-up source's, or
172
- // no routing exists yet). The write goes to all sources, so it is refused unless
173
- // our version is strictly greater than EVERY source's stored version (all already
174
- // fetched above) - a source at (or past) our version means this version number is
175
- // taken, and writing anyway would leave sources at the same version with different
176
- // content, each rejecting the other's pushes forever. Down sources are still
177
- // protected by the server-side version guard when the write eventually reaches them.
178
138
  let best: RemoteConfig | undefined;
179
139
  let conflictUrl: string | undefined;
180
140
  for (let fetch of fetches) {
@@ -197,10 +157,6 @@ export class ArchivesChain implements IArchives {
197
157
  }
198
158
  let sources = await this.buildSources(active);
199
159
  if (needsWrite) {
200
- // The update only happens when EVERY source would accept it - a partial update
201
- // would leave the sources out of sync, and a client without access retrying the
202
- // write forever would never initialize. Without full access we run on the stored
203
- // config (when there is one) and log the problem instead.
204
160
  let missing: string[] = [];
205
161
  for (let source of sources) {
206
162
  try {
@@ -222,12 +178,6 @@ export class ArchivesChain implements IArchives {
222
178
  }
223
179
  } else {
224
180
  try {
225
- // A rejected write fails init, which retries from scratch - re-reading the
226
- // routing, so losing a create race to another client just adopts their
227
- // config on the next attempt.
228
- // The routing file is NEVER synchronized between storage nodes, so it is
229
- // written directly to EVERY node, with one shared write time (the latest
230
- // write time wins on each node independently)
231
181
  let routingData = serializeRemoteConfig(this.configured);
232
182
  let routingWriteTime = Date.now();
233
183
  let writtenUrls = new Set<string>();
@@ -251,31 +201,6 @@ export class ArchivesChain implements IArchives {
251
201
  }
252
202
  }
253
203
  }
254
- // The server may be MID deploy-takeover: its getConfig returns the in-memory
255
- // interpretation (window splits pointing at a successor's port), which is never in the
256
- // routing file. A client connecting during the takeover must start on that
257
- // interpretation, not discover it minutes later.
258
- try {
259
- let probeApi = found.probe.api;
260
- if (probeApi) {
261
- let servedConfig = (await probeApi.getConfig()).remoteConfig;
262
- if (servedConfig) {
263
- let served = normalizeRemoteConfig(servedConfig);
264
- if (JSON.stringify(served) !== JSON.stringify(active) && getConfigVersion(served) >= getConfigVersion(active)) {
265
- console.log(`Adopting the server's in-memory routing interpretation at init for ${this.getDebugName()} (deploy takeover in progress)`);
266
- active = served;
267
- for (let source of sources) {
268
- source.dispose();
269
- }
270
- sources = await this.buildSources(active);
271
- }
272
- }
273
- }
274
- } catch {
275
- // The raw routing config we already adopted still works
276
- }
277
- // The routing fetches double as our first latency measurements: variable-shard picking
278
- // works immediately, instead of waiting for the first ping pass to land
279
204
  for (let source of sources) {
280
205
  let fetch = fetches.find(x => x.responded && x.sourceConfig.url === source.config.url);
281
206
  if (fetch) {
@@ -294,13 +219,6 @@ export class ArchivesChain implements IArchives {
294
219
 
295
220
  private async createChainSource(sourceConfig: HostedConfig | BackblazeConfig): Promise<SourceWrapper> {
296
221
  let source = await SourceWrapper.create(sourceConfig);
297
- // A takeover stamp change in a ping means the server's routing interpretation changed
298
- // (deploy takeover started/ended); refresh so we learn within one ping interval
299
- source.onServedConfigChanged = () => {
300
- console.log(`Storage ${source.getDebugName()} advertised a routing change (deploy takeover); refreshing config for ${this.getDebugName()}`);
301
- void this.refreshActiveConfig().catch((e: Error) => console.error(`Config refresh failed for ${this.getDebugName()}: ${e.stack ?? e}`));
302
- };
303
- // Latency (for variable-shard target preference) is tracked from initialization on
304
222
  source.startPinging();
305
223
  return source;
306
224
  }
@@ -335,18 +253,20 @@ export class ArchivesChain implements IArchives {
335
253
  }
336
254
 
337
255
  // The latest config, as the server INTERPRETS it: getConfig carries in-memory overlays (deploy
338
- // takeover remaps) that are deliberately never written into the routing file, so it's
339
- // preferred over reading the raw file. URL-only chains fall back to the raw file.
340
256
  private async fetchLatestConfig(state: ChainState): Promise<RemoteConfig | undefined> {
341
- try {
342
- let config = await this.run(state, { apiOnly: true }, archives => archives.getConfig());
343
- if (config.remoteConfig) {
344
- return normalizeRemoteConfig(config.remoteConfig);
257
+ let errors: string[] = [];
258
+ for (let source of state.sources) {
259
+ try {
260
+ let latest = await source.readRoutingConfig();
261
+ if (latest) return normalizeRemoteConfig(latest);
262
+ } catch (e) {
263
+ errors.push(`${source.config.url}: ${(e as Error).stack ?? e}`);
345
264
  }
346
- } catch { }
347
- let data = await this.run(state, {}, archives => archives.get(ROUTING_FILE));
348
- if (!data) return undefined;
349
- return parseRoutingData(data);
265
+ }
266
+ if (errors.length === state.sources.length) {
267
+ throw new Error(`No storage source could give us the routing config for ${this.getDebugName()}: ${errors.join(" | ")}`);
268
+ }
269
+ return undefined;
350
270
  }
351
271
 
352
272
  private async checkForNewConfig(): Promise<void> {
@@ -355,7 +275,6 @@ export class ArchivesChain implements IArchives {
355
275
  try {
356
276
  state = await this.statePromise;
357
277
  } catch {
358
- // Init is failing; its own retry loop handles that
359
278
  return;
360
279
  }
361
280
  let latest = await this.fetchLatestConfig(state);
@@ -365,18 +284,13 @@ export class ArchivesChain implements IArchives {
365
284
 
366
285
  private async adoptNewConfig(state: ChainState, latest: RemoteConfig): Promise<void> {
367
286
  if (JSON.stringify(latest) === JSON.stringify(state.config)) return;
368
- // Same version but different content is a deploy switchover remap (an in-memory window
369
- // split pointing at a successor's port), not an actual configuration change
370
- if (getConfigVersion(latest) === getConfigVersion(state.config)) {
371
- console.log(`Storage routing reinterpreted at version ${getConfigVersion(latest)} for ${this.getDebugName()} (deploy switchover in progress), rebuilding sources. Sources: ${JSON.stringify(latest.sources)}`);
287
+ let received = new Date().toISOString();
288
+ let onlyIntermediatesChanged = JSON.stringify(resolveIntermediateSources(latest)) === JSON.stringify(resolveIntermediateSources(state.config));
289
+ if (onlyIntermediatesChanged) {
290
+ console.log(`Storage routing switchover windows changed (version ${getConfigVersion(state.config)} -> ${getConfigVersion(latest)}), received ${received}, rebuilding sources. New config: ${JSON.stringify(latest)}`);
372
291
  } else {
373
- console.log(`Storage routing config changed for ${this.getDebugName()} (version ${getConfigVersion(state.config)} -> ${getConfigVersion(latest)}), rebuilding sources`);
292
+ console.log(`Storage routing config changed (version ${getConfigVersion(state.config)} -> ${getConfigVersion(latest)}), received ${received}, rebuilding sources. New config: ${JSON.stringify(latest)}`);
374
293
  }
375
- // Sources are matched IGNORING the valid window: config updates routinely just move
376
- // windows (reduce the forever-window, append a new entry after it), and a window-only
377
- // change must reuse the existing wrapper (connection, pings, latency history) instead of
378
- // dispose-and-reconnect. The same URL can appear several times differing only by window
379
- // (deploy window splits), so equal keys pair off in order.
380
294
  let strippedKey = (config: HostedConfig | BackblazeConfig) => JSON.stringify({ ...config, validWindow: undefined });
381
295
  let oldByConfig = new Map<string, SourceWrapper[]>();
382
296
  for (let source of state.sources) {
@@ -398,8 +312,6 @@ export class ArchivesChain implements IArchives {
398
312
  sources.push(await this.createChainSource(sourceConfig));
399
313
  }
400
314
  }
401
- // In-flight requests still hold the old wrappers and finish fine; dispose just stops any
402
- // background reconnect/ping loops
403
315
  for (let leftovers of oldByConfig.values()) {
404
316
  for (let leftover of leftovers) {
405
317
  leftover.dispose();
@@ -409,9 +321,6 @@ export class ArchivesChain implements IArchives {
409
321
  this.statePromise = Promise.resolve({ config: latest, sources });
410
322
  }
411
323
 
412
- // When every source looks down, the routing config is re-read from EVERY source - which both
413
- // re-attempts their connections (our liveness re-check) and discovers a routing update we may
414
- // have missed (adopting it re-initializes the source list). Concurrent callers share one pass.
415
324
  private lastAvailabilityRecheck = 0;
416
325
  private availabilityRecheckInFlight: Promise<void> | undefined;
417
326
  private recheckAvailability(): Promise<void> {
@@ -429,14 +338,12 @@ export class ArchivesChain implements IArchives {
429
338
  try {
430
339
  state = await this.statePromise;
431
340
  } catch {
432
- // Init is failing; its own retry loop handles that
433
341
  return;
434
342
  }
435
343
  console.log(`Every storage source failed for ${this.getDebugName()}; re-contacting all ${state.sources.length} sources (routing re-read + connection re-attempt)`);
436
344
  let results = await Promise.all(state.sources.map(async source => {
437
345
  try {
438
- let data = await source.read(archives => archives.get(ROUTING_FILE));
439
- return data && parseRoutingData(data) || undefined;
346
+ return await source.readRoutingConfig();
440
347
  } catch {
441
348
  return undefined;
442
349
  }
@@ -446,10 +353,6 @@ export class ArchivesChain implements IArchives {
446
353
  await this.adoptNewConfig(state, latest);
447
354
  }
448
355
 
449
- // Runs a request against the first available source (that covers the key's route, when one is
450
- // given), moving to the next one ONLY when the source's WebSocket is down (checked directly -
451
- // never by inspecting the error, which is arbitrary data). An error from a connected source is
452
- // an application error and throws as-is. A down source gets its background reconnect kicked.
453
356
  private async run<T>(state: ChainState, config: { apiOnly?: boolean; write?: boolean; route?: number }, run: (archives: IArchives) => Promise<T>): Promise<T> {
454
357
  if (config.write) {
455
358
  return await this.runWrite(config.route, run);
@@ -476,8 +379,6 @@ export class ArchivesChain implements IArchives {
476
379
  errors.push(String((e as Error).stack ?? e));
477
380
  }
478
381
  }
479
- // Every source failed: re-contact everything (routing re-read + connection re-attempt,
480
- // possibly adopting a routing update) and try once more before giving up
481
382
  if (!recheckedAvailability) {
482
383
  recheckedAvailability = true;
483
384
  await this.recheckAvailability();
@@ -488,11 +389,6 @@ export class ArchivesChain implements IArchives {
488
389
  }
489
390
  }
490
391
 
491
- // Writes are consistent: they go to the first currently-valid source covering the key's route
492
- // and NEVER fail over to another node - a client wrongly deciding nodes are down must not
493
- // scatter its writes across the chain. (Reads fail over freely above: sources are synchronized
494
- // copies, so reading from any of them is safe.) The one retry is for a window boundary passing
495
- // (or a route disagreement) mid-write, which re-resolves the target rather than falling back.
496
392
  private async runWrite<T>(route: number | undefined, run: (archives: IArchives) => Promise<T>): Promise<T> {
497
393
  let retriedWrongWindow = false;
498
394
  let retriedWrongRoute = false;
@@ -501,8 +397,6 @@ export class ArchivesChain implements IArchives {
501
397
  let state = await this.getState();
502
398
  let target = state.sources.find(x => configAcceptsWrites(x.config) && (route === undefined || routeContains(x.config.route, route)));
503
399
  if (!target) {
504
- // Before giving up, re-contact every source once (routing re-read + connection
505
- // re-attempt, possibly adopting a routing update that fixes the target)
506
400
  if (!recheckedAvailability) {
507
401
  recheckedAvailability = true;
508
402
  await this.recheckAvailability();
@@ -524,8 +418,6 @@ export class ArchivesChain implements IArchives {
524
418
  await this.prepareWrongTargetRetry(state, "route");
525
419
  continue;
526
420
  }
527
- // The connection died during (or before) the request. One full re-contact pass,
528
- // then one retry - still the same consistent target, never a different node.
529
421
  if (!target.isConnected()) {
530
422
  target.noteFailure();
531
423
  if (!recheckedAvailability) {
@@ -540,8 +432,6 @@ export class ArchivesChain implements IArchives {
540
432
  }
541
433
 
542
434
  private lastConfigRefresh = 0;
543
- // A wrong-target rejection means either we raced a window boundary (time fixes it) or our
544
- // config is stale - e.g. a deploy-takeover remap that only exists in the server's memory
545
435
  private async prepareWrongTargetRetry(state: ChainState, kind: "window" | "route"): Promise<void> {
546
436
  if (kind === "window") {
547
437
  let now = Date.now();
@@ -563,9 +453,6 @@ export class ArchivesChain implements IArchives {
563
453
  return await this.run(state, config, run);
564
454
  }
565
455
 
566
- // The access-page link (plus our machineId/ip, for the approver to match the request) for the
567
- // first hosted source that hasn't granted us access. undefined when we have access everywhere
568
- // we can have it. Also registers the access request server-side.
569
456
  public async waitingForAccess(): Promise<{ link: string; machineId: string; ip: string } | undefined> {
570
457
  let state = await this.getState();
571
458
  for (let source of state.sources) {
@@ -588,9 +475,6 @@ export class ArchivesChain implements IArchives {
588
475
  return await this.request({ route: getRoute(fileName) }, archives => archives.getInfo(fileName));
589
476
  }
590
477
 
591
- // A minimal set of connected, currently-valid API sources whose routes cover [0, 1) - listing
592
- // operations must merge every shard. Greedy sweep: repeatedly take the source that extends
593
- // coverage the furthest. In practice this is one source (unsharded), or one per shard.
594
478
  private selectCoveringSources(state: ChainState, excluded: Set<SourceWrapper>): SourceWrapper[] {
595
479
  let candidates = state.sources.filter(x => configWindowCurrent(x.config) && x.api && !excluded.has(x));
596
480
  let chosen: SourceWrapper[] = [];
@@ -615,9 +499,6 @@ export class ArchivesChain implements IArchives {
615
499
  return chosen;
616
500
  }
617
501
 
618
- // Fans one call out over a covering set of shards. Sources are never pre-filtered by
619
- // connectivity - the request is attempted, and a failure with the socket down afterwards
620
- // excludes that source and re-selects the covering set (an error from a live source throws).
621
502
  private async runOnCovering<T>(run: (archives: IArchives) => Promise<T>): Promise<T[]> {
622
503
  let state = await this.getState();
623
504
  let excluded = new Set<SourceWrapper>();
@@ -648,7 +529,6 @@ export class ArchivesChain implements IArchives {
648
529
  }
649
530
  public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
650
531
  let results = await this.runOnCovering(archives => archives.findInfo(prefix, config));
651
- // Overlapping shards can both report a path; the newest wins
652
532
  let byPath = new Map<string, ArchiveFileInfo>();
653
533
  for (let list of results) {
654
534
  for (let file of list) {
@@ -701,8 +581,6 @@ export class ArchivesChain implements IArchives {
701
581
  let config = await this.run(state, { apiOnly: true }, archives => archives.getConfig());
702
582
  return { ...config, remoteConfig: state.config };
703
583
  }
704
- /** True only when EVERY write-receiving source would accept our writes (partial write access
705
- * desynchronizes sources, so it counts as no access). */
706
584
  public async hasWriteAccess(): Promise<boolean> {
707
585
  let state = await this.getState();
708
586
  for (let source of state.sources) {
@@ -712,9 +590,6 @@ export class ArchivesChain implements IArchives {
712
590
  return true;
713
591
  }
714
592
 
715
- /** Returns the full key written. Plain keys come back unchanged; keys containing VARIABLE_SHARD
716
- * are automatically materialized (a shard value is picked and embedded, see setVariableShard)
717
- * and the caller needs the returned key to ever read the value back. */
718
593
  public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<string> {
719
594
  if (fileName === ROUTING_FILE) {
720
595
  return await this.setRoutingConfig(data, config);
@@ -726,27 +601,20 @@ export class ArchivesChain implements IArchives {
726
601
  return fileName;
727
602
  }
728
603
 
729
- // The routing config is NEVER synchronized between nodes, so a chain-level write of it goes
730
- // directly to EVERY node - first-valid-source routing would leave every other node on the old
731
- // config forever. One shared write time, so each node resolves latest-write-time-wins
732
- // identically. Afterwards we immediately run the same refresh the config poll uses: the
733
- // update must apply to us now, not whenever the poll would have noticed.
734
604
  private async setRoutingConfig(data: Buffer, config?: { lastModified?: number }): Promise<string> {
735
605
  let state = await this.getState();
736
- // Parse first, so a malformed config fails before any node stores it
737
- let incoming = parseRoutingData(data);
738
606
  let writeTime = config?.lastModified || Date.now();
739
607
  let written: string[] = [];
740
608
  let errors: string[] = [];
741
609
  let seen = new Set<string>();
742
- console.log(`Writing storage routing config version ${getConfigVersion(incoming)} for ${this.getDebugName()} to every node (write time ${new Date(writeTime).toISOString()})`);
610
+ console.log(`Writing storage routing config for ${this.getDebugName()} to every node (write time ${new Date(writeTime).toISOString()}): ${data.toString("utf8").slice(0, 2000)}`);
743
611
  for (let source of state.sources) {
744
612
  if (seen.has(source.config.url)) continue;
745
613
  seen.add(source.config.url);
746
614
  try {
747
615
  await source.write(archives => archives.set(ROUTING_FILE, data, { lastModified: writeTime }));
748
616
  written.push(source.config.url);
749
- console.log(`Wrote storage routing config version ${getConfigVersion(incoming)} to ${source.config.url}`);
617
+ console.log(`Wrote the storage routing config to ${source.config.url}`);
750
618
  } catch (e) {
751
619
  errors.push(`${source.config.url}: ${(e as Error).stack ?? e}`);
752
620
  }
@@ -761,17 +629,10 @@ export class ArchivesChain implements IArchives {
761
629
  await this.request({ write: true, route: getRoute(fileName) }, archives => archives.del(fileName));
762
630
  }
763
631
 
764
- // Writes a bare variable-shard key: picks the lowest-latency up write shard, materializes the
765
- // key with a random value inside that shard's route, writes it, and returns the FULL key
766
- // actually written. Unlike normal writes this CAN move to another shard when the preferred one
767
- // is down (error + socket down, same rule as reads) - each shard receives a different key, so
768
- // write consistency is preserved.
769
632
  private async setVariableShard(key: string, data: Buffer, config?: { lastModified?: number }): Promise<string> {
770
633
  let recheckedAvailability = false;
771
634
  while (true) {
772
635
  let state = await this.getState();
773
- // Per-shard write consistency still holds: within a route, only the FIRST source may
774
- // take writes - latency only picks WHICH shard the key materializes into
775
636
  let targetsByRoute = new Map<string, SourceWrapper>();
776
637
  for (let source of state.sources) {
777
638
  if (!configAcceptsWrites(source.config)) continue;
@@ -790,14 +651,12 @@ export class ArchivesChain implements IArchives {
790
651
  await target.write(archives => archives.set(fullKey, data, config));
791
652
  return fullKey;
792
653
  } catch (e) {
793
- // NOTE: If we run into cases when transport level errors happen, but we are still connected, then we might want to add additional checking here, additional errors in which we will retry the other targets. However, ideally, checking if the WebSocket connection is still connected will handle all those cases. Distinguishing between application errors (which we can't retry) and transport errors.
794
654
  if (target.isConnected()) throw e;
795
655
  target.noteFailure();
796
656
  console.log(`Variable-shard write target ${target.getDebugName()} is down; moving to the next-lowest-latency shard`);
797
657
  errors.push(String((e as Error).stack ?? e));
798
658
  }
799
659
  }
800
- // Every shard failed: re-contact everything and try once more before giving up
801
660
  if (!recheckedAvailability) {
802
661
  recheckedAvailability = true;
803
662
  await this.recheckAvailability();
@@ -815,7 +674,6 @@ export class ArchivesChain implements IArchives {
815
674
  let recheckedAvailability = false;
816
675
  while (true) {
817
676
  let state = await this.getState();
818
- // Same consistency rule as runWrite: the first currently-valid source for the route, or nothing
819
677
  let target = state.sources.find(x => configAcceptsWrites(x.config) && routeContains(x.config.route, route));
820
678
  if (!target) {
821
679
  if (!recheckedAvailability) {
@@ -829,7 +687,6 @@ export class ArchivesChain implements IArchives {
829
687
  await target.write(archives => archives.setLargeFile(config));
830
688
  return;
831
689
  } catch (e) {
832
- // The data stream cannot be rewound, so a mid-upload disconnect cannot be retried
833
690
  if (!target.isConnected()) {
834
691
  target.noteFailure();
835
692
  }
@@ -870,35 +727,34 @@ export class ArchivesChain implements IArchives {
870
727
  }
871
728
  }
872
729
 
873
- // The IArchives for a RemoteConfig (or a single source - a routing URL string works). Fully lazy:
874
- // nothing is contacted until the first call. Call dispose() when done with it, so its background
875
- // retry/poll loops stop.
876
730
  export function createArchives(config: RemoteConfig | RemoteConfigBase): ArchivesChain {
877
731
  return new ArchivesChain(config);
878
732
  }
879
733
 
880
- /** Every bucket an account has on one storage server - active and inactive - with each bucket's
881
- * configuration. One authenticated call (the normal trust system applies): no ArchivesChain, no
882
- * synchronization, and inactive buckets on the server stay inactive. Any URL addressing the
883
- * server works (a bucket routing URL, or just https://host:port). */
884
- export async function listServerBuckets(config: { url: string; account: string }): Promise<ServerBucketInfo[]> {
734
+ async function callServer<T>(url: string, run: (controller: typeof RemoteStorageController.nodes[string]) => Promise<T>): Promise<T> {
885
735
  SocketFunction.ENABLE_CLIENT_MODE = true;
886
- let parsed = parseStorageUrl(config.url);
736
+ let parsed = parseStorageUrl(url);
887
737
  let nodeId = SocketFunction.connect({ address: parsed.address, port: parsed.port });
888
738
  let controller = RemoteStorageController.nodes[nodeId];
889
739
  try {
890
- return await controller.listBuckets(config.account);
740
+ return await run(controller);
891
741
  } catch (e) {
892
742
  if (!String((e as Error).stack ?? e).includes(STORAGE_NOT_AUTHENTICATED)) throw e;
893
743
  await authenticateStorage({ address: parsed.address, port: parsed.port, nodeId });
894
- return await controller.listBuckets(config.account);
744
+ return await run(controller);
895
745
  }
896
746
  }
897
747
 
898
- /** Live info for one bucket given its routing URL (getConfig: routing config, index totals, disk
899
- * limit, in-progress synchronization). One authenticated call to that server - a light, safe
900
- * alternative to instantiating an ArchivesChain, which would start synchronization machinery. */
901
- export async function getBucketInfo(config: { url: string; accountName?: string }): Promise<ArchivesConfig> {
902
- let remote = new ArchivesRemote({ url: config.url, accountName: config.accountName, waitForAccess: false });
748
+ export async function listServerBuckets(config: { url: string; account: string }): Promise<ServerBucketInfo[]> {
749
+ return await callServer(config.url, controller => controller.listBuckets(config.account));
750
+ }
751
+
752
+ /** Zeroes the write statistics listServerBuckets reports, for every bucket in the account. */
753
+ export async function clearServerWriteStats(config: { url: string; account: string }): Promise<{ clearedBuckets: number }> {
754
+ return await callServer(config.url, controller => controller.clearWriteStats(config.account));
755
+ }
756
+
757
+ export async function getBucketInfo(config: { url: string }): Promise<ArchivesConfig> {
758
+ let remote = new ArchivesRemote({ url: config.url, waitForAccess: false });
903
759
  return await remote.getConfig();
904
760
  }
@@ -1,33 +1,19 @@
1
- import { RemoteConfig } from "../IArchives";
2
- export type TakeoverEvent = "remapChanged";
3
- /** Starts the takeover machinery. Port fallback (alternate port + registry + acquisition polling)
4
- * works regardless; without a deploy timeline folder the switchover-specific parts (the remap,
5
- * the tighter acquisition pacing) simply stay inert. */
6
- export declare function initDeployTakeover(config: {
7
- domain: string;
8
- mainPort: number;
9
- storageFolder: string;
10
- }): Promise<void>;
11
- /** Called when we had to listen on an alternate port (the main port was still held by our
12
- * predecessor): registers it so the predecessor can route the middle overlap window to us. */
13
- export declare function registerAltPort(port: number): Promise<void>;
14
- export declare function onTakeoverEvent(listener: (event: TakeoverEvent) => void): void;
15
- /** The interpretation overlay: splits every source pointing at our domain+main port so the middle
16
- * of the deploy overlap points at the alternate port. Pure, in-memory only - the stored routing
17
- * config is never modified, and this must never be applied to data that gets persisted. */
18
- export declare function applyDeployRemap(routing: RemoteConfig): RemoteConfig;
19
- /** A stamp of the current remap interpretation, advertised in ping responses - so every connected
20
- * client learns of a takeover within one ping interval, instead of waiting for its config poll
21
- * or a write rejection. */
22
- export declare function getTakeoverStamp(): string | undefined;
23
- /** The middle-window alternate port of an active remap. The OTHER process of the takeover lives on
24
- * this port on OUR machine (same disk!), so sources pointing at it are self, never sync targets. */
25
- export declare function getTakeoverAltPort(): number | undefined;
26
- /** For the dying process of a takeover: our own process's data ends at the write handoff - fast
27
- * writes must be on disk by then. Undefined for the successor and in normal operation. Both
28
- * processes share the config identity (all self windows look like "ours" to both), so this is
29
- * the only way the dying side knows the post-handoff windows belong to the other process. */
30
- export declare function getOwnWindowEndClip(): number | undefined;
31
- /** How long to wait between main-port acquisition attempts: tight around the predecessor's
32
- * scheduled death (when the port actually frees), relaxed otherwise. */
1
+ type DeployTakeover = {
2
+ releaseTime: number;
3
+ overlapTime: number;
4
+ altPort?: number;
5
+ };
6
+ /** Called when the main port is already in use, which on a healthy machine only happens while our predecessor is still running a deploy overlap. Confirms that against the deploy timeline; if no deploy is in progress we are in a bad state (someone else holds our port) and the process must not keep running. */
7
+ export declare function detectDeployTakeover(): Promise<DeployTakeover>;
8
+ export declare function setAltPort(port: number): void;
9
+ /** The window in which writes belong to our alternate port: from the release until our predecessor is killed and we take the main port. */
10
+ export declare function getTakeoverIntermediate(): {
11
+ start: number;
12
+ end: number;
13
+ altPort: number;
14
+ } | undefined;
15
+ /** We never stop listening on the alternate port while its window is still valid, and hold it well past that for clients that have not caught up yet. */
16
+ export declare function getAltPortListenEnd(): number;
17
+ /** How long to wait between main-port acquisition attempts: tight around our predecessor's scheduled death (when the port actually frees), relaxed otherwise. */
33
18
  export declare function getMainPortAcquireDelay(): number;
19
+ export {};