sliftutils 1.7.32 → 1.7.34

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 +134 -102
  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 +22 -14
  90. package/storage/remoteStorage/createArchives.ts +54 -188
  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 +58 -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 +37 -74
  104. package/storage/remoteStorage/dist/storageServerState.ts.cache +345 -188
  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 +7 -6
  113. package/storage/remoteStorage/storageController.ts +36 -82
  114. package/storage/remoteStorage/storageServer.ts +20 -34
  115. package/storage/remoteStorage/storageServerState.d.ts +41 -5
  116. package/storage/remoteStorage/storageServerState.ts +355 -216
@@ -3,9 +3,7 @@ import { RemoteStorageController } from "./storageController";
3
3
  import { authenticateStorage, parseStorageUrl } from "./ArchivesRemote";
4
4
  import { getArg } from "./cliArgs";
5
5
 
6
- // The grantAccess CLI. Invoked via the sibling grantAccess.js bootstrap (which loads typenode and
7
- // then requires this file). Grants a specific access request by its requestId; must be run on the
8
- // storage machine itself (its certs.ts identity is what the server trusts as admin).
6
+ // The grantAccess CLI. Invoked via the sibling grantAccess.js bootstrap (which loads typenode and then requires this file). Grants a specific access request by its requestId; must be run on the storage machine itself (its certs.ts identity is what the server trusts as admin).
9
7
 
10
8
  async function main() {
11
9
  let url = getArg("url");
@@ -0,0 +1,21 @@
1
+ import { RemoteConfig, HostedConfig, BackblazeConfig } from "../IArchives";
2
+ export declare const INTERMEDIATE_EXPIRE_GRACE: number;
3
+ /** Adding or removing intermediates is a real config update, so it takes a real version increment - but a proportional one, so it stays far below whatever the author's next version would be (whether they count 1, 2, 3 or use timestamps), and a million of them still fit under it. */
4
+ export declare function nextIntermediateVersion(version: number): number;
5
+ type ObjectSource = HostedConfig | BackblazeConfig;
6
+ export declare function getIntermediateSources(config: RemoteConfig): ObjectSource[];
7
+ export declare function hasIntermediateSources(config: RemoteConfig): boolean;
8
+ /** Removes every intermediate entry and rejoins the windows it split, giving back the underlying configuration. Two configs that resolve equal differ only by intermediates. */
9
+ export declare function resolveIntermediateSources(config: RemoteConfig): RemoteConfig;
10
+ /** Splits every source at splitUrl covering [start, end) so that middle window points at intermediateUrl instead, flagged as intermediate. Idempotent: a config that already contains the exact intermediate comes back unchanged. */
11
+ export declare function injectIntermediateSource(config: RemoteConfig, inject: {
12
+ splitUrl: string;
13
+ intermediateUrl: string;
14
+ start: number;
15
+ end: number;
16
+ }): RemoteConfig;
17
+ /** Intermediates whose window ended more than INTERMEDIATE_EXPIRE_GRACE ago are removed, and the windows they split are rejoined. */
18
+ export declare function expireIntermediateSources(config: RemoteConfig, now: number): RemoteConfig;
19
+ /** The url of the entry an intermediate was split out of - the neighbour it touches. */
20
+ export declare function findSplitUrl(config: RemoteConfig, intermediate: ObjectSource): string | undefined;
21
+ export {};
@@ -0,0 +1,117 @@
1
+ import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } from "../IArchives";
2
+
3
+ export const INTERMEDIATE_EXPIRE_GRACE = 15 * 60 * 1000;
4
+ const INTERMEDIATE_VERSION_FRACTION = 1_000_000;
5
+
6
+ /** Adding or removing intermediates is a real config update, so it takes a real version increment - but a proportional one, so it stays far below whatever the author's next version would be (whether they count 1, 2, 3 or use timestamps), and a million of them still fit under it. */
7
+ export function nextIntermediateVersion(version: number): number {
8
+ let step = Math.abs(version) / INTERMEDIATE_VERSION_FRACTION;
9
+ if (!(step > 0)) {
10
+ step = 1 / INTERMEDIATE_VERSION_FRACTION;
11
+ }
12
+ return version + step;
13
+ }
14
+
15
+ type ObjectSource = HostedConfig | BackblazeConfig;
16
+
17
+ function isObjectSource(source: RemoteConfigBase): source is ObjectSource {
18
+ return typeof source !== "string";
19
+ }
20
+
21
+ function cloneSource(source: RemoteConfigBase): RemoteConfigBase {
22
+ if (!isObjectSource(source)) return source;
23
+ return { ...source, validWindow: [source.validWindow[0], source.validWindow[1]] };
24
+ }
25
+
26
+ function joinKey(source: ObjectSource): string {
27
+ return JSON.stringify({ ...source, validWindow: undefined });
28
+ }
29
+
30
+ export function getIntermediateSources(config: RemoteConfig): ObjectSource[] {
31
+ return config.sources.filter(x => isObjectSource(x) && x.intermediate) as ObjectSource[];
32
+ }
33
+
34
+ export function hasIntermediateSources(config: RemoteConfig): boolean {
35
+ return config.sources.some(x => isObjectSource(x) && x.intermediate);
36
+ }
37
+
38
+ /** Removes every intermediate entry and rejoins the windows it split, giving back the underlying configuration. Two configs that resolve equal differ only by intermediates. */
39
+ export function resolveIntermediateSources(config: RemoteConfig): RemoteConfig {
40
+ if (!hasIntermediateSources(config)) return config;
41
+ let removed = getIntermediateSources(config);
42
+ let sources = config.sources.filter(x => !isObjectSource(x) || !x.intermediate).map(cloneSource);
43
+ for (let gap of removed) {
44
+ let [gapStart, gapEnd] = gap.validWindow;
45
+ let before = sources.find(x => isObjectSource(x) && x.validWindow[1] === gapStart) as ObjectSource | undefined;
46
+ let after = sources.find(x => isObjectSource(x) && x.validWindow[0] === gapEnd) as ObjectSource | undefined;
47
+ if (before && after && joinKey(before) === joinKey(after)) {
48
+ before.validWindow = [before.validWindow[0], after.validWindow[1]];
49
+ sources.splice(sources.indexOf(after), 1);
50
+ continue;
51
+ }
52
+ if (before) {
53
+ before.validWindow = [before.validWindow[0], gapEnd];
54
+ continue;
55
+ }
56
+ if (after) {
57
+ after.validWindow = [gapStart, after.validWindow[1]];
58
+ }
59
+ }
60
+ return { version: config.version, sources };
61
+ }
62
+
63
+ /** Splits every source at splitUrl covering [start, end) so that middle window points at intermediateUrl instead, flagged as intermediate. Idempotent: a config that already contains the exact intermediate comes back unchanged. */
64
+ export function injectIntermediateSource(config: RemoteConfig, inject: { splitUrl: string; intermediateUrl: string; start: number; end: number }): RemoteConfig {
65
+ let { splitUrl, intermediateUrl, start, end } = inject;
66
+ if (start >= end) return config;
67
+ if (config.sources.some(x => isObjectSource(x) && x.intermediate && x.url === intermediateUrl && x.validWindow[0] === start && x.validWindow[1] === end)) {
68
+ return config;
69
+ }
70
+ let sources: RemoteConfigBase[] = [];
71
+ for (let source of config.sources) {
72
+ if (!isObjectSource(source) || source.url !== splitUrl || source.intermediate) {
73
+ sources.push(cloneSource(source));
74
+ continue;
75
+ }
76
+ let [windowStart, windowEnd] = source.validWindow;
77
+ if (windowEnd <= start || windowStart >= end) {
78
+ sources.push(cloneSource(source));
79
+ continue;
80
+ }
81
+ let middleStart = Math.max(windowStart, start);
82
+ let middleEnd = Math.min(windowEnd, end);
83
+ if (middleStart > windowStart) {
84
+ sources.push({ ...source, validWindow: [windowStart, middleStart] });
85
+ }
86
+ sources.push({ ...source, url: intermediateUrl, validWindow: [middleStart, middleEnd], intermediate: true });
87
+ if (windowEnd > middleEnd) {
88
+ sources.push({ ...source, validWindow: [middleEnd, windowEnd] });
89
+ }
90
+ }
91
+ return { version: config.version, sources };
92
+ }
93
+
94
+ /** Intermediates whose window ended more than INTERMEDIATE_EXPIRE_GRACE ago are removed, and the windows they split are rejoined. */
95
+ export function expireIntermediateSources(config: RemoteConfig, now: number): RemoteConfig {
96
+ let expired = getIntermediateSources(config).filter(x => x.validWindow[1] + INTERMEDIATE_EXPIRE_GRACE < now);
97
+ if (!expired.length) return config;
98
+ let keptIntermediates = getIntermediateSources(config).filter(x => !expired.includes(x));
99
+ let resolved = resolveIntermediateSources(config);
100
+ let result = resolved;
101
+ for (let keep of keptIntermediates) {
102
+ result = injectIntermediateSource(result, {
103
+ splitUrl: findSplitUrl(config, keep) || keep.url,
104
+ intermediateUrl: keep.url,
105
+ start: keep.validWindow[0],
106
+ end: keep.validWindow[1],
107
+ });
108
+ }
109
+ return result;
110
+ }
111
+
112
+ /** The url of the entry an intermediate was split out of - the neighbour it touches. */
113
+ export function findSplitUrl(config: RemoteConfig, intermediate: ObjectSource): string | undefined {
114
+ let [start, end] = intermediate.validWindow;
115
+ let neighbour = config.sources.find(x => isObjectSource(x) && !x.intermediate && (x.validWindow[1] === start || x.validWindow[0] === end)) as ObjectSource | undefined;
116
+ return neighbour?.url;
117
+ }
@@ -4,10 +4,7 @@ import { sort, sha256HashBuffer } from "socket-function/src/misc";
4
4
  import { getBufferInt } from "socket-function/src/bits";
5
5
  import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, FULL_VALID_WINDOW, FULL_ROUTE, VARIABLE_SHARD } from "../IArchives";
6
6
 
7
- // Parsing / normalization of RemoteConfig (see IArchives.ts). Every bucket stores its own
8
- // configuration (a RemoteConfig) inside itself, at ROUTING_FILE. Writing that file creates the
9
- // bucket / reconfigures it (see storageServerState.ts); clients reconcile it by version (see
10
- // createArchives.ts).
7
+ // Parsing / normalization of RemoteConfig (see IArchives.ts). Every bucket stores its own configuration (a RemoteConfig) inside itself, at ROUTING_FILE. Writing that file creates the bucket / reconfigures it (see storageServerState.ts); clients reconcile it by version (see createArchives.ts).
11
8
 
12
9
  export const ROUTING_FILE = "storage/storagerouting.json";
13
10
  const ROUTING_SUFFIX = "/" + ROUTING_FILE;
@@ -125,8 +122,7 @@ export function normalizeSource(source: RemoteConfigBase): HostedConfig | Backbl
125
122
  }
126
123
  let hostname = new URL(source).hostname;
127
124
  if (hostname.endsWith(".backblazeb2.com")) {
128
- // Validates the URL (throws on malformed) before it's stored; the bucket name is read back
129
- // out of the URL at use sites, never stored on the config.
125
+ // Validates the URL (throws on malformed) before it's stored; the bucket name is read back out of the URL at use sites, never stored on the config.
130
126
  parseBackblazeUrl(source);
131
127
  return { type: "backblaze", url: source, validWindow: FULL_VALID_WINDOW };
132
128
  }
@@ -134,6 +130,18 @@ export function normalizeSource(source: RemoteConfigBase): HostedConfig | Backbl
134
130
  return { type: "remote", url: source, validWindow: FULL_VALID_WINDOW };
135
131
  }
136
132
 
133
+ /** How far up from 0 the sources' routes reach without a gap (1 means the whole key space). */
134
+ function getRouteCoverage(sources: (HostedConfig | BackblazeConfig)[]): number {
135
+ let routes = sources.map(x => x.route || FULL_ROUTE);
136
+ sort(routes, x => x[0]);
137
+ let covered = 0;
138
+ for (let route of routes) {
139
+ if (route[0] > covered) break;
140
+ covered = Math.max(covered, route[1]);
141
+ }
142
+ return covered;
143
+ }
144
+
137
145
  export function normalizeRemoteConfig(config: RemoteConfig | RemoteConfigBase): RemoteConfig {
138
146
  let result: RemoteConfig;
139
147
  if (typeof config !== "string" && "sources" in config) {
@@ -141,11 +149,7 @@ export function normalizeRemoteConfig(config: RemoteConfig | RemoteConfigBase):
141
149
  } else {
142
150
  result = { sources: [normalizeSource(config)] };
143
151
  }
144
- // Mixed immutability makes no sense AMONG sources valid at the same time: a mutable source
145
- // would accept overwrites that its immutable peers refuse to synchronize, forking their
146
- // contents. Sources are grouped by transitively overlapping valid windows (absorb everything
147
- // overlapping the group, extend the group's end, repeat) - a soft check that is correct as
148
- // long as windows have clean breaks (a group ends exactly where the next begins).
152
+ // Mixed immutability makes no sense AMONG sources valid at the same time: a mutable source would accept overwrites that its immutable peers refuse to synchronize, forking their contents. Sources are grouped by transitively overlapping valid windows (absorb everything overlapping the group, extend the group's end, repeat) - a soft check that is correct as long as windows have clean breaks (a group ends exactly where the next begins).
149
153
  let sources = result.sources.map(normalizeSource);
150
154
  let sorted = [...sources];
151
155
  sort(sorted, x => x.validWindow[0]);
@@ -156,18 +160,27 @@ export function normalizeRemoteConfig(config: RemoteConfig | RemoteConfigBase):
156
160
  if (immutableCount && immutableCount !== group.length) {
157
161
  throw new Error(`Sources with overlapping valid windows must agree on immutability: ${immutableCount} of ${group.length} are immutable. Sources: ${JSON.stringify(group.map(x => ({ url: x.url, validWindow: x.validWindow, immutable: !!x.immutable })))}`);
158
162
  }
159
- // The sources valid at any instant must cover the whole key space, or keys routing into a
160
- // gap could never be read
161
- let routes = group.map(x => x.route || FULL_ROUTE);
162
- sort(routes, x => x[0]);
163
- let covered = 0;
164
- for (let route of routes) {
165
- if (route[0] > covered) break;
166
- covered = Math.max(covered, route[1]);
167
- }
163
+ // The sources valid at any instant must cover the whole key space, or keys routing into a gap could never be read
164
+ let covered = getRouteCoverage(group);
168
165
  if (covered < 1) {
169
166
  throw new Error(`Sources with overlapping valid windows must cover the full route space [0, 1); coverage stops at ${covered}. Sources: ${JSON.stringify(group.map(x => ({ url: x.url, validWindow: x.validWindow, route: x.route || FULL_ROUTE })))}`);
170
167
  }
168
+ // Every bucket must ALSO cover the full route space across its own entries, because all of a bucket's entries share one disk. A bucket that only ever synchronizes part of the key space never learns about deletions in the rest of it - and tombstones expire, so files it still holds from a route it no longer serves eventually look like live files again, and get resurrected into the chain by its next scan. An entry with no route (implicitly the full space) satisfies this on its own, which is why the usual configuration lists each bucket once per shard AND once unsharded.
169
+ let byUrl = new Map<string, typeof group>();
170
+ for (let source of group) {
171
+ let entries = byUrl.get(source.url);
172
+ if (!entries) {
173
+ entries = [];
174
+ byUrl.set(source.url, entries);
175
+ }
176
+ entries.push(source);
177
+ }
178
+ for (let [url, entries] of byUrl) {
179
+ let urlCovered = getRouteCoverage(entries);
180
+ if (urlCovered < 1) {
181
+ throw new Error(`Every bucket must cover the full route space [0, 1) across its own entries with overlapping valid windows, but ${url} only covers up to ${urlCovered} (its routes: ${JSON.stringify(entries.map(x => x.route || FULL_ROUTE))}). Add an entry for it with no route (or entries whose routes span the rest), alongside its sharded entries. This is enforced because a bucket's entries all share one disk: if it only ever synchronizes part of the key space, it never sees deletions in the rest, and once those tombstones expire the files it still holds are resurrected into the chain by its next scan.`);
182
+ }
183
+ }
171
184
  }
172
185
  for (let source of sorted) {
173
186
  if (group.length && source.validWindow[0] >= groupEnd) {
@@ -1,4 +1,4 @@
1
- import { IArchives, HostedConfig, BackblazeConfig } from "../IArchives";
1
+ import { IArchives, HostedConfig, BackblazeConfig, RemoteConfig } from "../IArchives";
2
2
  import { ArchivesUrl } from "./ArchivesUrl";
3
3
  export declare const RETRY_START_DELAY: number;
4
4
  export declare const RETRY_MAX_DELAY: number;
@@ -14,41 +14,30 @@ export declare class SourceWrapper {
14
14
  private reconnectRunning;
15
15
  private accessCache?;
16
16
  private constructor();
17
- /** Config updates routinely just move a source's valid window (the last window extends
18
- * forever, then gets reduced when a new entry is appended). The wrapper survives that: only
19
- * the window changes, keeping the connection, pings, and latency history. */
17
+ /** Config updates routinely just move a source's valid window (the last window extends forever, then gets reduced when a new entry is appended). The wrapper survives that: only the window changes, keeping the connection, pings, and latency history. */
20
18
  updateValidWindow(validWindow: [number, number]): void;
21
19
  static create(config: HostedConfig | BackblazeConfig, options?: {
22
20
  background?: boolean;
23
21
  }): Promise<SourceWrapper>;
24
22
  getDebugName(): string;
25
23
  isConnected(): boolean;
26
- /** Call after a request failed while isConnected() was false: starts (if not already running)
27
- * the background reconnect loop. Never blocks - the failed request still throws. */
24
+ /** Call after a request failed while isConnected() was false: starts (if not already running) the background reconnect loop. Never blocks - the failed request still throws. */
28
25
  noteFailure(): void;
29
26
  private reconnectLoop;
30
27
  private checkAccess;
31
28
  read<T>(run: (archives: IArchives) => Promise<T>): Promise<T>;
29
+ readRoutingConfig(): Promise<RemoteConfig | undefined>;
32
30
  hasWriteAccess(): Promise<boolean>;
33
31
  private pings;
34
32
  private pingTimer;
35
33
  private loggedConnected;
36
- private lastTakeoverStamp;
37
- /** Fired when the source's advertised takeover stamp changes (a deploy takeover started or
38
- * ended) - the chain refreshes its config, so connected clients learn within one ping. */
39
- onServedConfigChanged: (() => void) | undefined;
40
- /** Starts measuring this source's latency (for variable-shard target preference). Only hosted
41
- * remotes are pinged; our own local server counts as 0, everything else as Infinity. */
34
+ /** Starts measuring this source's latency (for variable-shard target preference). Only hosted remotes are pinged; our own local server counts as 0, everything else as Infinity. */
42
35
  startPinging(): void;
43
- /** Seeds the latency estimate before the first ping lands (e.g. from the initial routing
44
- * fetch), so variable-shard picking has something immediately. Real pings take over from the
45
- * first measurement on. */
36
+ /** Seeds the latency estimate before the first ping lands (e.g. from the initial routing fetch), so variable-shard picking has something immediately. Real pings take over from the first measurement on. */
46
37
  seedLatency(ms: number): void;
47
- /** Median of the recent pings. Sources that can't be pinged sort last (Infinity), except our
48
- * own in-process server, which is the best possible target (0). */
38
+ /** Median of the recent pings. Sources that can't be pinged sort last (Infinity), except our own in-process server, which is the best possible target (0). */
49
39
  getLatency(): number;
50
- /** Writes always go through the API, so a permission error throws to the caller on every write
51
- * (and access granted in the meantime is picked up automatically). */
40
+ /** Writes always go through the API, so a permission error throws to the caller on every write (and access granted in the meantime is picked up automatically). */
52
41
  write<T>(run: (archives: IArchives) => Promise<T>): Promise<T>;
53
42
  dispose(): void;
54
43
  }
@@ -3,8 +3,8 @@ module.allowclient = true;
3
3
  import { isNode, sort } from "socket-function/src/misc";
4
4
  import { delay } from "socket-function/src/batching";
5
5
  import { getSecret } from "../../misc/getSecret";
6
- import { IArchives, HostedConfig, BackblazeConfig } from "../IArchives";
7
- import { ROUTING_FILE, getBucketBaseUrl, parseHostedUrl, parseBackblazeUrl } from "./remoteConfig";
6
+ import { IArchives, HostedConfig, BackblazeConfig, RemoteConfig } from "../IArchives";
7
+ import { ROUTING_FILE, getBucketBaseUrl, parseHostedUrl, parseBackblazeUrl, parseRoutingData } from "./remoteConfig";
8
8
  import { ArchivesRemote } from "./ArchivesRemote";
9
9
  import { ArchivesUrl } from "./ArchivesUrl";
10
10
  import { ArchivesBackblaze } from "../backblaze";
@@ -28,12 +28,7 @@ async function hasBackblazeCreds(): Promise<boolean> {
28
28
  }
29
29
  }
30
30
 
31
- // One source of a RemoteConfig, as a usable IArchives. Reads fall back to the public-URL form when
32
- // we don't have API access; writes always attempt the API (so access granted later is picked up on
33
- // the next write). When a call fails while the WebSocket is down, noteFailure starts a background
34
- // reconnect loop with a ramping delay - it never blocks callers, it just keeps trying so the
35
- // connection eventually comes back. Sources without a WebSocket (backblaze, URL-form, our own
36
- // local server) report always-connected: they're last-resort sources where throwing is fine.
31
+ // One source of a RemoteConfig, as a usable IArchives. Reads fall back to the public-URL form when we don't have API access; writes always attempt the API (so access granted later is picked up on the next write). When a call fails while the WebSocket is down, noteFailure starts a background reconnect loop with a ramping delay - it never blocks callers, it just keeps trying so the connection eventually comes back. Sources without a WebSocket (backblaze, URL-form, our own local server) report always-connected: they're last-resort sources where throwing is fine.
37
32
  export class SourceWrapper {
38
33
  public api?: IArchives;
39
34
  public url?: ArchivesUrl;
@@ -49,9 +44,7 @@ export class SourceWrapper {
49
44
  private background: boolean,
50
45
  ) { }
51
46
 
52
- /** Config updates routinely just move a source's valid window (the last window extends
53
- * forever, then gets reduced when a new entry is appended). The wrapper survives that: only
54
- * the window changes, keeping the connection, pings, and latency history. */
47
+ /** Config updates routinely just move a source's valid window (the last window extends forever, then gets reduced when a new entry is appended). The wrapper survives that: only the window changes, keeping the connection, pings, and latency history. */
55
48
  public updateValidWindow(validWindow: [number, number]): void {
56
49
  let old = this.config.validWindow;
57
50
  if (old[0] === validWindow[0] && old[1] === validWindow[1]) return;
@@ -81,7 +74,7 @@ export class SourceWrapper {
81
74
  wrapper.api = getLocalArchives(parsed.account, parsed.bucketName);
82
75
  return wrapper;
83
76
  }
84
- wrapper.remote = new ArchivesRemote({ url: config.url, accountName: config.accountName, waitForAccess: false });
77
+ wrapper.remote = new ArchivesRemote({ url: config.url, waitForAccess: false });
85
78
  wrapper.api = wrapper.remote;
86
79
  if (config.public !== false) {
87
80
  wrapper.url = new ArchivesUrl(getBucketBaseUrl(config.url));
@@ -90,8 +83,7 @@ export class SourceWrapper {
90
83
  }
91
84
 
92
85
  public getDebugName(): string {
93
- // The same URL can appear as multiple sources (different routes / valid windows), so the
94
- // distinguishing slice is part of the name
86
+ // The same URL can appear as multiple sources (different routes / valid windows), so the distinguishing slice is part of the name
95
87
  let parts: string[] = [];
96
88
  let route = this.config.route;
97
89
  if (route) {
@@ -111,8 +103,7 @@ export class SourceWrapper {
111
103
  return this.remote.isConnected();
112
104
  }
113
105
 
114
- /** Call after a request failed while isConnected() was false: starts (if not already running)
115
- * the background reconnect loop. Never blocks - the failed request still throws. */
106
+ /** Call after a request failed while isConnected() was false: starts (if not already running) the background reconnect loop. Never blocks - the failed request still throws. */
116
107
  public noteFailure(): void {
117
108
  if (!this.background || this.disposed || this.reconnectRunning) return;
118
109
  if (this.isConnected()) return;
@@ -145,9 +136,7 @@ export class SourceWrapper {
145
136
  console.log(`Reconnected to storage ${this.getDebugName()}`);
146
137
  }
147
138
 
148
- // For hosted sources: cached access check, so reads know whether to use the API or the public
149
- // URL form. Access, once seen, is assumed to stick; no-access is re-checked periodically (the
150
- // check also registers our access request server-side, which logs the grant link).
139
+ // For hosted sources: cached access check, so reads know whether to use the API or the public URL form. Access, once seen, is assumed to stick; no-access is re-checked periodically (the check also registers our access request server-side, which logs the grant link).
151
140
  private async checkAccess(): Promise<boolean> {
152
141
  let remote = this.remote;
153
142
  if (!remote) return !!this.api;
@@ -171,13 +160,17 @@ export class SourceWrapper {
171
160
  return await run(this.url);
172
161
  }
173
162
  if (this.api) {
174
- // No public URL form - let the API call throw its own error (which includes the access
175
- // instructions for hosted sources)
163
+ // No public URL form - let the API call throw its own error (which includes the access instructions for hosted sources)
176
164
  return await run(this.api);
177
165
  }
178
166
  throw new Error(`Source ${this.config.url} has no API access and no public URL form (public: false)`);
179
167
  }
180
168
 
169
+ public async readRoutingConfig(): Promise<RemoteConfig | undefined> {
170
+ let data = await this.read(archives => archives.get(ROUTING_FILE));
171
+ return data && parseRoutingData(data) || undefined;
172
+ }
173
+
181
174
  public async hasWriteAccess(): Promise<boolean> {
182
175
  if (this.writeBlocked || !this.api) return false;
183
176
  return await this.api.hasWriteAccess();
@@ -186,21 +179,15 @@ export class SourceWrapper {
186
179
  private pings: number[] = [];
187
180
  private pingTimer: ReturnType<typeof setInterval> | undefined;
188
181
  private loggedConnected = false;
189
- private lastTakeoverStamp: string | undefined;
190
- /** Fired when the source's advertised takeover stamp changes (a deploy takeover started or
191
- * ended) - the chain refreshes its config, so connected clients learn within one ping. */
192
- public onServedConfigChanged: (() => void) | undefined;
193
182
 
194
- /** Starts measuring this source's latency (for variable-shard target preference). Only hosted
195
- * remotes are pinged; our own local server counts as 0, everything else as Infinity. */
183
+ /** Starts measuring this source's latency (for variable-shard target preference). Only hosted remotes are pinged; our own local server counts as 0, everything else as Infinity. */
196
184
  public startPinging(): void {
197
185
  const remote = this.remote;
198
186
  if (!remote || this.pingTimer || this.disposed) return;
199
187
  let measure = async () => {
200
188
  let start = Date.now();
201
- let result: { takeover?: string } | undefined;
202
189
  try {
203
- result = await remote.ping();
190
+ await remote.ping();
204
191
  } catch {
205
192
  // A failed ping is also our earliest down-detection
206
193
  this.noteFailure();
@@ -214,11 +201,6 @@ export class SourceWrapper {
214
201
  if (this.pings.length > PING_HISTORY) {
215
202
  this.pings.shift();
216
203
  }
217
- let stamp = result?.takeover;
218
- if (stamp !== this.lastTakeoverStamp) {
219
- this.lastTakeoverStamp = stamp;
220
- this.onServedConfigChanged?.();
221
- }
222
204
  };
223
205
  void measure();
224
206
  this.pingTimer = setInterval(() => {
@@ -227,16 +209,13 @@ export class SourceWrapper {
227
209
  (this.pingTimer as { unref?: () => void }).unref?.();
228
210
  }
229
211
 
230
- /** Seeds the latency estimate before the first ping lands (e.g. from the initial routing
231
- * fetch), so variable-shard picking has something immediately. Real pings take over from the
232
- * first measurement on. */
212
+ /** Seeds the latency estimate before the first ping lands (e.g. from the initial routing fetch), so variable-shard picking has something immediately. Real pings take over from the first measurement on. */
233
213
  public seedLatency(ms: number): void {
234
214
  if (this.pings.length) return;
235
215
  this.pings.push(ms);
236
216
  }
237
217
 
238
- /** Median of the recent pings. Sources that can't be pinged sort last (Infinity), except our
239
- * own in-process server, which is the best possible target (0). */
218
+ /** Median of the recent pings. Sources that can't be pinged sort last (Infinity), except our own in-process server, which is the best possible target (0). */
240
219
  public getLatency(): number {
241
220
  if (!this.remote) {
242
221
  if (this.config.type === "remote" && this.api) return 0;
@@ -248,8 +227,7 @@ export class SourceWrapper {
248
227
  return sorted[Math.floor(sorted.length / 2)];
249
228
  }
250
229
 
251
- /** Writes always go through the API, so a permission error throws to the caller on every write
252
- * (and access granted in the meantime is picked up automatically). */
230
+ /** Writes always go through the API, so a permission error throws to the caller on every write (and access granted in the meantime is picked up automatically). */
253
231
  public async write<T>(run: (archives: IArchives) => Promise<T>): Promise<T> {
254
232
  if (this.writeBlocked) {
255
233
  throw new Error(this.writeBlocked);
@@ -1,8 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
 
3
- // The client half of routing-change broadcasts: the storage server tracks its connected clients
4
- // (see trackCaller in storageController) and calls routingConfigChanged on every one of them the
5
- // moment a routing config changes - clients react immediately instead of waiting for a poll.
3
+ // The client half of routing-change broadcasts: the storage server tracks its connected clients (see trackCaller in storageController) and calls routingConfigChanged on every one of them the moment a routing config changes - clients react immediately instead of waiting for a poll.
6
4
 
7
5
  const listeners = new Set<() => void>();
8
6
 
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
3
  import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
4
- import { ServerBucketInfo } from "./storageServerState";
4
+ import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
5
5
  export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
6
6
  export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
7
7
  export declare const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
@@ -36,13 +36,9 @@ export type AccessState = {
36
36
  grantAccessCommand?: string;
37
37
  trustedMachines?: TrustRecord[];
38
38
  };
39
- /** Called by storageServerState the moment any routing config is applied - clients must never
40
- * have to wait for a poll to learn the topology changed. */
41
39
  export declare function broadcastRoutingChanged(): void;
42
40
  export declare const RemoteStorageController: import("socket-function/SocketFunctionTypes").SocketRegistered<{
43
- ping: () => Promise<{
44
- takeover?: string;
45
- }>;
41
+ ping: () => Promise<{}>;
46
42
  authenticate: (token: AuthToken) => Promise<{
47
43
  machineId: string;
48
44
  ip: string;
@@ -83,6 +79,11 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
83
79
  getChangesAfter: (account: string, bucketName: string, time: number) => Promise<ArchiveFileInfo[]>;
84
80
  getArchivesConfig: (account: string, bucketName: string) => Promise<ArchivesConfig>;
85
81
  listBuckets: (account: string) => Promise<ServerBucketInfo[]>;
82
+ getActiveBucket: (account: string, bucketName: string) => Promise<ActiveBucketInfo | string>;
83
+ activateBucket: (account: string, bucketName: string) => Promise<ActiveBucketInfo | string>;
84
+ clearWriteStats: (account: string) => Promise<{
85
+ clearedBuckets: number;
86
+ }>;
86
87
  getIndexInfo: (account: string, bucketName: string) => Promise<{
87
88
  fileCount: number;
88
89
  byteCount: number;