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.
- package/index.d.ts +4 -1
- package/package.json +1 -1
- package/storage/IArchives.ts +2 -1
- package/storage/backblaze.ts +16 -10
- package/storage/dist/IArchives.ts.cache +4 -3
- package/storage/dist/backblaze.ts.cache +19 -11
- package/storage/remoteStorage/blobStore.ts +5 -8
- package/storage/remoteStorage/createArchives.d.ts +4 -1
- package/storage/remoteStorage/createArchives.ts +31 -17
- package/storage/remoteStorage/dist/blobStore.ts.cache +8 -13
- package/storage/remoteStorage/dist/createArchives.ts.cache +33 -23
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageController.ts.cache +3 -2
- package/storage/remoteStorage/dist/storageServerState.ts.cache +2 -2
- package/storage/remoteStorage/storageController.ts +1 -0
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
-
|
|
715
|
-
let
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
let
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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 {
|