sliftutils 1.7.19 → 1.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -2871,7 +2871,9 @@ declare module "sliftutils/storage/remoteStorage/ArchivesRemote" {
2871
2871
  private lastDeniedLog;
2872
2872
  getDebugName(): string;
2873
2873
  isConnected(): boolean;
2874
- ping(): Promise<void>;
2874
+ ping(): Promise<{
2875
+ takeover?: string;
2876
+ }>;
2875
2877
  private authenticate;
2876
2878
  private callAuthed;
2877
2879
  waitingForAccess(): Promise<{
@@ -3185,6 +3187,7 @@ declare module "sliftutils/storage/remoteStorage/createArchives" {
3185
3187
  getDebugName(): string;
3186
3188
  private getState;
3187
3189
  private init;
3190
+ private createChainSource;
3188
3191
  private buildSources;
3189
3192
  private startConfigPoll;
3190
3193
  private configRefreshInFlight;
@@ -3295,6 +3298,10 @@ declare module "sliftutils/storage/remoteStorage/deployTakeover" {
3295
3298
  * of the deploy overlap points at the alternate port. Pure, in-memory only - the stored routing
3296
3299
  * config is never modified, and this must never be applied to data that gets persisted. */
3297
3300
  export declare function applyDeployRemap(routing: RemoteConfig): RemoteConfig;
3301
+ /** A stamp of the current remap interpretation, advertised in ping responses - so every connected
3302
+ * client learns of a takeover within one ping interval, instead of waiting for its config poll
3303
+ * or a write rejection. */
3304
+ export declare function getTakeoverStamp(): string | undefined;
3298
3305
  /** For the dying process: fast-write flush delays must never extend past this time, and after it
3299
3306
  * fast writes flush immediately - so nothing is left in memory when the write window transfers. */
3300
3307
  export declare function getFlushDeadline(): number | undefined;
@@ -3375,6 +3382,10 @@ declare module "sliftutils/storage/remoteStorage/sourceWrapper" {
3375
3382
  private pings;
3376
3383
  private pingTimer;
3377
3384
  private loggedConnected;
3385
+ private lastTakeoverStamp;
3386
+ /** Fired when the source's advertised takeover stamp changes (a deploy takeover started or
3387
+ * ended) - the chain refreshes its config, so connected clients learn within one ping. */
3388
+ onServedConfigChanged: (() => void) | undefined;
3378
3389
  /** Starts measuring this source's latency (for variable-shard target preference). Only hosted
3379
3390
  * remotes are pinged; our own local server counts as 0, everything else as Infinity. */
3380
3391
  startPinging(): void;
@@ -3428,7 +3439,9 @@ declare module "sliftutils/storage/remoteStorage/storageController" {
3428
3439
  trustedMachines?: TrustRecord[];
3429
3440
  };
3430
3441
  export declare const RemoteStorageController: import("socket-function/SocketFunctionTypes").SocketRegistered<{
3431
- ping: () => Promise<void>;
3442
+ ping: () => Promise<{
3443
+ takeover?: string;
3444
+ }>;
3432
3445
  authenticate: (token: AuthToken) => Promise<{
3433
3446
  machineId: string;
3434
3447
  ip: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.19",
3
+ "version": "1.7.21",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -57,7 +57,7 @@
57
57
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
58
58
  "preact-old-types": "^10.28.1",
59
59
  "shell-quote": "^1.8.3",
60
- "socket-function": "^1.2.24",
60
+ "socket-function": "^1.2.26",
61
61
  "typenode": "^6.6.1",
62
62
  "typesafecss": "*",
63
63
  "ws": "^8.18.3",
@@ -29,7 +29,9 @@ export declare class ArchivesRemote implements IArchives {
29
29
  private lastDeniedLog;
30
30
  getDebugName(): string;
31
31
  isConnected(): boolean;
32
- ping(): Promise<void>;
32
+ ping(): Promise<{
33
+ takeover?: string;
34
+ }>;
33
35
  private authenticate;
34
36
  private callAuthed;
35
37
  waitingForAccess(): Promise<{
@@ -77,8 +77,8 @@ export class ArchivesRemote implements IArchives {
77
77
  return SocketFunction.isNodeConnected(this.nodeId);
78
78
  }
79
79
 
80
- public async ping(): Promise<void> {
81
- await this.controller.ping();
80
+ public async ping(): Promise<{ takeover?: string }> {
81
+ return await this.controller.ping();
82
82
  }
83
83
 
84
84
  private async authenticate(): Promise<void> {
@@ -15,6 +15,7 @@ export declare class ArchivesChain implements IArchives {
15
15
  getDebugName(): string;
16
16
  private getState;
17
17
  private init;
18
+ private createChainSource;
18
19
  private buildSources;
19
20
  private startConfigPoll;
20
21
  private configRefreshInFlight;
@@ -223,6 +223,29 @@ export class ArchivesChain implements IArchives {
223
223
  }
224
224
  }
225
225
  }
226
+ // The server may be MID deploy-takeover: its getConfig returns the in-memory
227
+ // interpretation (window splits pointing at a successor's port), which is never in the
228
+ // routing file. A client connecting during the takeover must start on that
229
+ // interpretation, not discover it minutes later.
230
+ try {
231
+ let probeApi = found.probe.api;
232
+ if (probeApi) {
233
+ let servedConfig = (await probeApi.getConfig()).remoteConfig;
234
+ if (servedConfig) {
235
+ let served = normalizeRemoteConfig(servedConfig);
236
+ if (JSON.stringify(served) !== JSON.stringify(active) && getConfigVersion(served) >= getConfigVersion(active)) {
237
+ console.log(`Adopting the server's in-memory routing interpretation at init for ${this.getDebugName()} (deploy takeover in progress)`);
238
+ active = served;
239
+ for (let source of sources) {
240
+ source.dispose();
241
+ }
242
+ sources = await this.buildSources(active);
243
+ }
244
+ }
245
+ }
246
+ } catch {
247
+ // The raw routing config we already adopted still works
248
+ }
226
249
  // The routing fetches double as our first latency measurements: variable-shard picking
227
250
  // works immediately, instead of waiting for the first ping pass to land
228
251
  for (let source of sources) {
@@ -241,13 +264,23 @@ export class ArchivesChain implements IArchives {
241
264
  }
242
265
  }
243
266
 
267
+ private async createChainSource(sourceConfig: HostedConfig | BackblazeConfig): Promise<SourceWrapper> {
268
+ let source = await SourceWrapper.create(sourceConfig);
269
+ // A takeover stamp change in a ping means the server's routing interpretation changed
270
+ // (deploy takeover started/ended); refresh so we learn within one ping interval
271
+ source.onServedConfigChanged = () => {
272
+ console.log(`Storage ${source.getDebugName()} advertised a routing change (deploy takeover); refreshing config for ${this.getDebugName()}`);
273
+ void this.refreshActiveConfig().catch((e: Error) => console.error(`Config refresh failed for ${this.getDebugName()}: ${e.stack ?? e}`));
274
+ };
275
+ // Latency (for variable-shard target preference) is tracked from initialization on
276
+ source.startPinging();
277
+ return source;
278
+ }
279
+
244
280
  private async buildSources(config: RemoteConfig): Promise<SourceWrapper[]> {
245
281
  let sources: SourceWrapper[] = [];
246
282
  for (let sourceConfig of config.sources.map(normalizeSource)) {
247
- let source = await SourceWrapper.create(sourceConfig);
248
- // Latency (for variable-shard target preference) is tracked from initialization on
249
- source.startPinging();
250
- sources.push(source);
283
+ sources.push(await this.createChainSource(sourceConfig));
251
284
  }
252
285
  return sources;
253
286
  }
@@ -320,9 +353,7 @@ export class ArchivesChain implements IArchives {
320
353
  oldByConfig.delete(key);
321
354
  sources.push(old);
322
355
  } else {
323
- let source = await SourceWrapper.create(sourceConfig);
324
- source.startPinging();
325
- sources.push(source);
356
+ sources.push(await this.createChainSource(sourceConfig));
326
357
  }
327
358
  }
328
359
  // In-flight requests still hold the old wrappers and finish fine; dispose just stops any
@@ -16,6 +16,10 @@ export declare function onTakeoverEvent(listener: (event: TakeoverEvent) => void
16
16
  * of the deploy overlap points at the alternate port. Pure, in-memory only - the stored routing
17
17
  * config is never modified, and this must never be applied to data that gets persisted. */
18
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;
19
23
  /** For the dying process: fast-write flush delays must never extend past this time, and after it
20
24
  * fast writes flush immediately - so nothing is left in memory when the write window transfers. */
21
25
  export declare function getFlushDeadline(): number | undefined;
@@ -392,6 +392,15 @@ export function applyDeployRemap(routing: RemoteConfig): RemoteConfig {
392
392
  return { version: routing.version, sources };
393
393
  }
394
394
 
395
+ /** A stamp of the current remap interpretation, advertised in ping responses - so every connected
396
+ * client learns of a takeover within one ping interval, instead of waiting for its config poll
397
+ * or a write rejection. */
398
+ export function getTakeoverStamp(): string | undefined {
399
+ let remap = current.remap;
400
+ if (!remap) return undefined;
401
+ return JSON.stringify(remap);
402
+ }
403
+
395
404
  /** For the dying process: fast-write flush delays must never extend past this time, and after it
396
405
  * fast writes flush immediately - so nothing is left in memory when the write window transfers. */
397
406
  export function getFlushDeadline(): number | undefined {