sliftutils 1.7.29 → 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 +106 -99
- 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 +54 -186
- 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 +32 -0
- package/storage/remoteStorage/dist/storageController.ts.cache +30 -64
- package/storage/remoteStorage/dist/storageServerState.ts.cache +269 -186
- 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 +5 -3
- package/storage/remoteStorage/storageController.ts +12 -83
- package/storage/remoteStorage/storageServer.ts +20 -34
- package/storage/remoteStorage/storageServerState.d.ts +28 -6
- package/storage/remoteStorage/storageServerState.ts +275 -221
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
|
@@ -36,10 +36,9 @@ export type AccessState = {
|
|
|
36
36
|
grantAccessCommand?: string;
|
|
37
37
|
trustedMachines?: TrustRecord[];
|
|
38
38
|
};
|
|
39
|
+
export declare function broadcastRoutingChanged(): void;
|
|
39
40
|
export declare const RemoteStorageController: import("socket-function/SocketFunctionTypes").SocketRegistered<{
|
|
40
|
-
ping: () => Promise<{
|
|
41
|
-
takeover?: string;
|
|
42
|
-
}>;
|
|
41
|
+
ping: () => Promise<{}>;
|
|
43
42
|
authenticate: (token: AuthToken) => Promise<{
|
|
44
43
|
machineId: string;
|
|
45
44
|
ip: string;
|
|
@@ -80,6 +79,9 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
80
79
|
getChangesAfter: (account: string, bucketName: string, time: number) => Promise<ArchiveFileInfo[]>;
|
|
81
80
|
getArchivesConfig: (account: string, bucketName: string) => Promise<ArchivesConfig>;
|
|
82
81
|
listBuckets: (account: string) => Promise<ServerBucketInfo[]>;
|
|
82
|
+
clearWriteStats: (account: string) => Promise<{
|
|
83
|
+
clearedBuckets: number;
|
|
84
|
+
}>;
|
|
83
85
|
getIndexInfo: (account: string, bucketName: string) => Promise<{
|
|
84
86
|
fileCount: number;
|
|
85
87
|
byteCount: number;
|