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.
- package/builders/extensionBuild.ts +1 -1
- package/bundler/bundleEntry.ts +1 -3
- package/bundler/bundler.ts +2 -4
- package/bundler/sourceMaps.ts +1 -2
- package/examplestorage/browser.tsx +1 -2
- package/examplestorage/exampleserver.ts +1 -2
- package/index.d.ts +105 -100
- package/misc/dist/environment.ts.cache +2 -2
- package/misc/dist/getSecret.ts.cache +2 -2
- package/misc/dist/strings.ts.cache +2 -2
- package/misc/dist/zip.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +2 -2
- package/misc/https/dist/dns.ts.cache +7 -16
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/misc/https/dns.d.ts +1 -4
- package/misc/https/dns.ts +5 -14
- package/misc/https/hostServer.d.ts +2 -0
- package/misc/https/hostServer.ts +6 -7
- package/misc/https/httpsCerts.ts +4 -10
- package/misc/openrouter.ts +1 -2
- package/package.json +1 -1
- package/render-utils/FullscreenModal.tsx +1 -4
- package/render-utils/GenericFormat.tsx +2 -8
- package/render-utils/Input.tsx +5 -13
- package/render-utils/InputLabel.tsx +4 -6
- package/render-utils/InputPicker.tsx +1 -2
- package/render-utils/URLParam.ts +2 -3
- package/render-utils/dist/observer.tsx.cache +2 -2
- package/render-utils/mobxTyped.ts +2 -3
- package/render-utils/niceStringify.ts +2 -4
- package/storage/ArchivesDisk.ts +4 -14
- package/storage/BulkDatabase2/BulkDatabase2.d.ts +1 -2
- package/storage/BulkDatabase2/BulkDatabase2.ts +4 -11
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +42 -114
- package/storage/BulkDatabase2/BulkDatabaseFormat.ts +31 -91
- package/storage/BulkDatabase2/BulkDatabaseMerge.ts +19 -57
- package/storage/BulkDatabase2/BulkDatabaseReader.ts +4 -12
- package/storage/BulkDatabase2/LoadedIndex.ts +6 -18
- package/storage/BulkDatabase2/WriteOverlay.ts +3 -8
- package/storage/BulkDatabase2/blockCache.ts +12 -28
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +40 -103
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +19 -50
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +18 -50
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +7 -15
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +9 -21
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +6 -11
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +15 -31
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +10 -29
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +10 -18
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +17 -43
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +11 -27
- package/storage/BulkDatabase2/mergeLock.ts +7 -26
- package/storage/BulkDatabase2/mergeMarkers.ts +7 -15
- package/storage/BulkDatabase2/streamLog.ts +14 -40
- package/storage/BulkDatabase2/syncClient.ts +8 -24
- package/storage/FileFolderAPI.tsx +28 -75
- package/storage/IArchives.d.ts +3 -3
- package/storage/IArchives.ts +21 -50
- package/storage/IStorage.ts +2 -4
- package/storage/PendingManager.tsx +2 -3
- package/storage/TransactionStorage.ts +11 -24
- package/storage/backblaze.ts +12 -43
- package/storage/dist/ArchivesDisk.ts.cache +7 -17
- package/storage/dist/FileFolderAPI.tsx.cache +25 -63
- package/storage/dist/IArchives.ts.cache +10 -20
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
- package/storage/dist/JSONStorage.ts.cache +2 -2
- package/storage/dist/PendingManager.tsx.cache +5 -6
- package/storage/dist/TransactionStorage.ts.cache +14 -27
- package/storage/dist/backblaze.ts.cache +15 -46
- package/storage/dist/embeddingFormats.ts.cache +11 -24
- package/storage/dist/fileSystemPointer.ts.cache +4 -7
- package/storage/dist/remoteFileStorage.ts.cache +17 -36
- package/storage/embeddingFormats.ts +11 -27
- package/storage/fileSystemPointer.ts +2 -7
- package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +8 -18
- package/storage/proxydatabase/dist/transactionSet.ts.cache +4 -7
- package/storage/proxydatabase/inMemoryDatabase.ts +1 -3
- package/storage/proxydatabase/ivfEmbeddingDatabase.ts +8 -23
- package/storage/proxydatabase/transactionSet.ts +2 -5
- package/storage/remoteFileServer.ts +20 -48
- package/storage/remoteFileStorage.ts +14 -33
- package/storage/remoteStorage/ArchivesRemote.d.ts +1 -4
- package/storage/remoteStorage/ArchivesRemote.ts +11 -26
- package/storage/remoteStorage/ArchivesUrl.ts +2 -4
- package/storage/remoteStorage/accessPage.tsx +1 -5
- package/storage/remoteStorage/blobStore.d.ts +5 -13
- package/storage/remoteStorage/blobStore.ts +52 -130
- package/storage/remoteStorage/createArchives.d.ts +9 -13
- package/storage/remoteStorage/createArchives.ts +43 -187
- package/storage/remoteStorage/deployTakeover.d.ts +18 -32
- package/storage/remoteStorage/deployTakeover.ts +70 -384
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +12 -24
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +5 -7
- package/storage/remoteStorage/dist/blobStore.ts.cache +47 -117
- package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +48 -189
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +81 -387
- package/storage/remoteStorage/dist/intermediateSources.ts.cache +119 -0
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +36 -24
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +20 -40
- package/storage/remoteStorage/dist/storageClientController.ts.cache +4 -6
- package/storage/remoteStorage/dist/storageController.ts.cache +11 -73
- package/storage/remoteStorage/dist/storageServerState.ts.cache +265 -178
- package/storage/remoteStorage/grantAccessCli.ts +1 -3
- package/storage/remoteStorage/intermediateSources.d.ts +21 -0
- package/storage/remoteStorage/intermediateSources.ts +117 -0
- package/storage/remoteStorage/remoteConfig.ts +33 -20
- package/storage/remoteStorage/sourceWrapper.d.ts +8 -19
- package/storage/remoteStorage/sourceWrapper.ts +19 -41
- package/storage/remoteStorage/storageClientController.ts +1 -3
- package/storage/remoteStorage/storageController.d.ts +4 -5
- package/storage/remoteStorage/storageController.ts +10 -81
- package/storage/remoteStorage/storageServer.ts +20 -34
- package/storage/remoteStorage/storageServerState.d.ts +28 -5
- package/storage/remoteStorage/storageServerState.ts +271 -207
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
} from "../IArchives";
|
|
10
10
|
import {
|
|
11
11
|
ROUTING_FILE, getConfigVersion, parseHostedUrl, parseBackblazeUrl,
|
|
12
|
-
normalizeRemoteConfig, normalizeSource,
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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,
|
|
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
|
|
150
|
-
return { probe, sourceConfig, responded: true, latency: Date.now() - start, existing
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
if (
|
|
371
|
-
console.log(`Storage routing
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
744
|
+
return await run(controller);
|
|
895
745
|
}
|
|
896
746
|
}
|
|
897
747
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
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 {};
|