sliftutils 1.7.56 → 1.7.58

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.
@@ -140,7 +140,7 @@ function newScanTally(): ScanTally {
140
140
  }
141
141
  function formatScanTally(tally: ScanTally, total: number): string {
142
142
  let pct = (n: number) => `${Math.round(n / Math.max(total, 1) * 1000) / 10}%`;
143
- return `${tally.new} new paths (${pct(tally.new)}), ${tally.updated} newer writes (${pct(tally.updated)}), ${tally.tombstone} deletions (${pct(tally.tombstone)}), ${tally.unchanged} unchanged (${pct(tally.unchanged)}), ${tally.filtered} outside window/route (${pct(tally.filtered)})`;
143
+ return `${tally.new} new paths (${pct(tally.new)}), ${tally.updated} newer writes (${pct(tally.updated)}), ${tally.tombstone} deletions (${pct(tally.tombstone)}), ${tally.unchanged} unchanged (${pct(tally.unchanged)}), ${tally.filtered} outside route (${pct(tally.filtered)})`;
144
144
  }
145
145
 
146
146
  function newSourceState(): SourceState {
@@ -593,7 +593,7 @@ export class BlobStore implements IBucketStore {
593
593
 
594
594
  // Full metadata scan (size, writeTime, path) of one source, applied to the index. Returns the source's listing (path -> write time), which reconcileSource uses for the push direction.
595
595
  private async scanSource(sourceIndex: number): Promise<Map<string, number>> {
596
- let { source, validWindow, route } = this.sources[sourceIndex];
596
+ let { source, route } = this.sources[sourceIndex];
597
597
  let state = this.sourceStates[sourceIndex];
598
598
  let scanStart = Date.now();
599
599
  let activity: SyncActivity = { type: "metadataScan", sourceDebugName: source.getDebugName(), startTime: scanStart };
@@ -636,9 +636,8 @@ export class BlobStore implements IBucketStore {
636
636
  removedFromIndex++;
637
637
  continue;
638
638
  }
639
- // Counted only when the source SHOULD hold the entry (its window/route match) - these are what the reconcile pass pushes to it
639
+ // Counted only when the source SHOULD hold the entry (its route matches) - these are what the reconcile pass pushes to it (which also ignores the valid window: synchronization moves existing values, the window only routes fresh writes)
640
640
  if (entry.size === 0 || key === ROUTING_FILE) continue;
641
- if (entry.writeTime < validWindow[0] || entry.writeTime > validWindow[1]) continue;
642
641
  if (!routeContains(route, getRoute(key))) continue;
643
642
  missingOnSource++;
644
643
  }
@@ -711,10 +710,8 @@ export class BlobStore implements IBucketStore {
711
710
  // The routing config is NEVER pulled from other sources - it only ever arrives as an explicit, version-validated write, and is only ever read off our own disk. Route and valid-window filters can't possibly apply to it either: it is the file DEFINING them, so filtering it would mean certain sources could never have their routing config updated, ever.
712
711
  if (sourceIndex !== 0) return "filtered";
713
712
  } else {
714
- let { validWindow, route } = this.sources[sourceIndex];
715
- let [windowStart, windowEnd] = validWindow;
716
- if (file.createTime < windowStart || file.createTime > windowEnd) return "filtered";
717
- // A partially-overlapping shard's listing includes keys outside our route; ignore them
713
+ // The valid window is deliberately NOT applied here: it decides where WRITES route, but a scan is us asking a source what it already holds - existing values synchronize regardless of the window (the same reasoning that lets synchronization ignore the immutable flag). Only the route filters: a partially-overlapping shard's listing legitimately includes keys that aren't ours.
714
+ let { route } = this.sources[sourceIndex];
718
715
  if (!routeContains(route, getRoute(file.path))) return "filtered";
719
716
  }
720
717
  let existing = this.mem.get(file.path);
@@ -14,6 +14,7 @@ export declare class ArchivesChain implements IArchives {
14
14
  private configured;
15
15
  private activeConfig;
16
16
  private statePromise;
17
+ private latestState;
17
18
  private initRetryDelay;
18
19
  private initRetryTimer;
19
20
  private pollTimer;
@@ -86,8 +87,10 @@ export declare class ArchivesChain implements IArchives {
86
87
  getNextData(): Promise<Buffer | undefined>;
87
88
  }): Promise<void>;
88
89
  getURL(path: string): Promise<string>;
89
- /** Every URL that could serve this path, in source order: public sources matching both the path's route and the current valid window. Empty when none qualify. */
90
+ /** Every URL that could serve this path: public sources matching both the path's route and the current valid window. The first is the write node's (first matching source in config order, see runWrite - the one guaranteed current); the rest are ranked fastest-first by measured latency. Empty when none qualify. */
90
91
  getURLs(path: string): Promise<string[]>;
92
+ /** getURLs, but after the one await (initialization) the returned function is synchronous: everything underneath - route hashing, window checks, latencies, URL building - is synchronous, and the closure always reads the newest adopted config, so it stays correct across config refreshes. */
93
+ getGetURLs(): Promise<(path: string) => string[]>;
91
94
  dispose(): void;
92
95
  }
93
96
  export declare function createArchives(config: RemoteConfig | RemoteConfigBase, options?: ArchivesChainOptions): ArchivesChain;
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  ROUTING_FILE, getConfigVersion, parseHostedUrl, parseBackblazeUrl,
10
10
  normalizeRemoteConfig, normalizeSource, serializeRemoteConfig,
11
- getRoute, routeContains, parseVariableRoute, getBucketBaseUrl,
11
+ getRoute, routeContains, parseVariableRoute, getBucketBaseUrl, buildFileUrl,
12
12
  } from "./remoteConfig";
13
13
  import { ArchivesUrl } from "./ArchivesUrl";
14
14
  import { resolveIntermediateSources } from "./intermediateSources";
@@ -100,6 +100,8 @@ export class ArchivesChain implements IArchives {
100
100
  private configured: RemoteConfig;
101
101
  private activeConfig: RemoteConfig;
102
102
  private statePromise: Promise<ChainState> | undefined;
103
+ // The resolved state, for synchronous access (getGetURLs) - always the newest adopted config, where statePromise may briefly lag during a rebuild
104
+ private latestState: ChainState | undefined;
103
105
  private initRetryDelay = RETRY_START_DELAY;
104
106
  private initRetryTimer: ReturnType<typeof setTimeout> | undefined;
105
107
  private pollTimer: ReturnType<typeof setInterval> | undefined;
@@ -249,7 +251,9 @@ export class ArchivesChain implements IArchives {
249
251
  }
250
252
  this.activeConfig = active;
251
253
  this.startConfigPoll();
252
- return { config: active, sources };
254
+ let state: ChainState = { config: active, sources };
255
+ this.latestState = state;
256
+ return state;
253
257
  } finally {
254
258
  for (let fetch of fetches) {
255
259
  fetch.probe.dispose();
@@ -366,7 +370,9 @@ export class ArchivesChain implements IArchives {
366
370
  }
367
371
  }
368
372
  this.activeConfig = latest;
369
- this.statePromise = Promise.resolve({ config: latest, sources });
373
+ let newState: ChainState = { config: latest, sources };
374
+ this.latestState = newState;
375
+ this.statePromise = Promise.resolve(newState);
370
376
  }
371
377
 
372
378
  private lastAvailabilityRecheck = 0;
@@ -802,21 +808,29 @@ export class ArchivesChain implements IArchives {
802
808
  return urls[0];
803
809
  }
804
810
 
805
- /** Every URL that could serve this path, in source order: public sources matching both the path's route and the current valid window. Empty when none qualify. */
811
+ /** Every URL that could serve this path: public sources matching both the path's route and the current valid window. The first is the write node's (first matching source in config order, see runWrite - the one guaranteed current); the rest are ranked fastest-first by measured latency. Empty when none qualify. */
806
812
  public async getURLs(path: string): Promise<string[]> {
807
- let state = await this.getState();
808
- let route = getRoute(path);
809
- let providers: IArchives[] = [];
810
- for (let source of state.sources) {
811
- if (!(source.config.public ?? true)) continue;
812
- if (!routeContains(source.config.route, route)) continue;
813
- if (!configWindowCurrent(source.config)) continue;
814
- let provider = source.url || source.api;
815
- if (!provider) continue;
816
- providers.push(provider);
817
- }
818
- let urls = await Promise.all(providers.map(provider => provider.getURL(path)));
819
- return [...new Set(urls)];
813
+ return (await this.getGetURLs())(path);
814
+ }
815
+
816
+ /** getURLs, but after the one await (initialization) the returned function is synchronous: everything underneath - route hashing, window checks, latencies, URL building - is synchronous, and the closure always reads the newest adopted config, so it stays correct across config refreshes. */
817
+ public async getGetURLs(): Promise<(path: string) => string[]> {
818
+ let initialState = await this.getState();
819
+ return (path: string) => {
820
+ let state = this.latestState || initialState;
821
+ let route = getRoute(path);
822
+ let sources: SourceWrapper[] = [];
823
+ for (let source of state.sources) {
824
+ if (!(source.config.public ?? true)) continue;
825
+ if (!routeContains(source.config.route, route)) continue;
826
+ if (!configWindowCurrent(source.config)) continue;
827
+ sources.push(source);
828
+ }
829
+ let rest = sources.slice(1);
830
+ sort(rest, x => x.getLatency());
831
+ let urls = [...sources.slice(0, 1), ...rest].map(x => buildFileUrl(getBucketBaseUrl(x.config.url), path));
832
+ return [...new Set(urls)];
833
+ };
820
834
  }
821
835
 
822
836
  public dispose(): void {