sliftutils 1.7.70 → 1.7.72
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/dist/treeSummary.ts.cache +2 -2
- package/index.d.ts +505 -257
- 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/hostServer.ts.cache +5 -2
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/package.json +1 -1
- package/render-utils/dist/observer.tsx.cache +2 -2
- package/spec.txt +10 -0
- package/storage/ArchivesDisk.d.ts +4 -13
- package/storage/ArchivesDisk.ts +6 -4
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
- package/storage/IArchives.d.ts +14 -10
- package/storage/IArchives.ts +16 -4
- package/storage/backblaze.d.ts +3 -9
- package/storage/backblaze.ts +22 -19
- package/storage/dist/ArchivesDisk.ts.cache +6 -3
- package/storage/dist/FileFolderAPI.tsx.cache +2 -2
- package/storage/dist/IArchives.ts.cache +18 -4
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
- package/storage/dist/JSONStorage.ts.cache +2 -2
- package/storage/dist/PendingManager.tsx.cache +2 -2
- package/storage/dist/TransactionStorage.ts.cache +2 -2
- package/storage/dist/backblaze.ts.cache +36 -27
- package/storage/dist/fileSystemPointer.ts.cache +2 -2
- package/storage/dist/remoteFileStorage.ts.cache +2 -2
- package/storage/remoteStorage/ArchivesRemote.d.ts +5 -9
- package/storage/remoteStorage/ArchivesRemote.ts +25 -20
- package/storage/remoteStorage/ArchivesUrl.d.ts +3 -9
- package/storage/remoteStorage/ArchivesUrl.ts +7 -5
- package/storage/remoteStorage/accessPage.tsx +4 -4
- package/storage/remoteStorage/accessStats.d.ts +2 -0
- package/storage/remoteStorage/accessStats.ts +32 -0
- package/storage/remoteStorage/blobStore.d.ts +193 -88
- package/storage/remoteStorage/blobStore.ts +535 -400
- package/storage/remoteStorage/bucketDisk.d.ts +18 -0
- package/storage/remoteStorage/bucketDisk.ts +66 -0
- package/storage/remoteStorage/createArchives.d.ts +4 -10
- package/storage/remoteStorage/createArchives.ts +102 -63
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +29 -20
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +8 -5
- package/storage/remoteStorage/dist/accessPage.tsx.cache +6 -6
- package/storage/remoteStorage/dist/accessStats.ts.cache +39 -4
- package/storage/remoteStorage/dist/blobStore.ts.cache +517 -375
- package/storage/remoteStorage/dist/bucketDisk.ts.cache +72 -0
- package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +112 -58
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +2 -2
- package/storage/remoteStorage/dist/intermediateSources.ts.cache +13 -13
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +19 -3
- package/storage/remoteStorage/dist/serverConfig.ts.cache +116 -0
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +7 -6
- package/storage/remoteStorage/dist/sourcesList.ts.cache +6 -3
- package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageController.ts.cache +224 -191
- package/storage/remoteStorage/dist/storageServer.ts.cache +9 -9
- package/storage/remoteStorage/dist/storageServerState.ts.cache +330 -567
- package/storage/remoteStorage/dist/storePlan.ts.cache +149 -0
- package/storage/remoteStorage/dist/validation.ts.cache +40 -0
- package/storage/remoteStorage/grantAccessCli.ts +1 -1
- package/storage/remoteStorage/intermediateSources.d.ts +3 -5
- package/storage/remoteStorage/intermediateSources.ts +14 -16
- package/storage/remoteStorage/remoteConfig.d.ts +2 -2
- package/storage/remoteStorage/remoteConfig.ts +19 -3
- package/storage/remoteStorage/serverConfig.d.ts +27 -0
- package/storage/remoteStorage/serverConfig.ts +109 -0
- package/storage/remoteStorage/sourceWrapper.d.ts +3 -3
- package/storage/remoteStorage/sourceWrapper.ts +7 -6
- package/storage/remoteStorage/sourcesList.ts +3 -0
- package/storage/remoteStorage/storageController.d.ts +118 -36
- package/storage/remoteStorage/storageController.ts +166 -168
- package/storage/remoteStorage/storageServer.ts +1 -1
- package/storage/remoteStorage/storageServerState.d.ts +52 -63
- package/storage/remoteStorage/storageServerState.ts +340 -584
- package/storage/remoteStorage/storePlan.d.ts +34 -0
- package/storage/remoteStorage/storePlan.ts +166 -0
- package/storage/remoteStorage/validation.d.ts +4 -0
- package/storage/remoteStorage/validation.ts +31 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RemoteConfig, HostedConfig, SourceConfig } from "../IArchives";
|
|
2
|
+
export declare function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[];
|
|
3
|
+
export declare function selectEntryAt(entries: HostedConfig[], time: number, route?: number): HostedConfig | undefined;
|
|
4
|
+
/** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
|
|
5
|
+
export type SelfSummary = {
|
|
6
|
+
/** The union of the current entries' routes, with overlapping/adjacent ranges combined - which commonly collapses to a single full range, making matching trivial. */
|
|
7
|
+
routes: [number, number][];
|
|
8
|
+
public: boolean;
|
|
9
|
+
immutable: boolean;
|
|
10
|
+
noFullSync: boolean;
|
|
11
|
+
rawDisk: boolean;
|
|
12
|
+
readerDiskLimit?: number;
|
|
13
|
+
};
|
|
14
|
+
export type StoreSourceSpec = {
|
|
15
|
+
sourceConfig?: SourceConfig;
|
|
16
|
+
validWindow: [number, number];
|
|
17
|
+
route?: [number, number];
|
|
18
|
+
noFullSync?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type StorePlanStore = {
|
|
21
|
+
routeKey: string;
|
|
22
|
+
route?: [number, number];
|
|
23
|
+
entries: HostedConfig[];
|
|
24
|
+
rawDisk: boolean;
|
|
25
|
+
readerDiskLimit?: number;
|
|
26
|
+
sourceSpecs: StoreSourceSpec[];
|
|
27
|
+
};
|
|
28
|
+
export type StorePlan = {
|
|
29
|
+
selfEntries: HostedConfig[];
|
|
30
|
+
self: SelfSummary | undefined;
|
|
31
|
+
stores: StorePlanStore[];
|
|
32
|
+
structureKey: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function computeStorePlan(account: string, bucketName: string, routing: RemoteConfig): StorePlan;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { sort } from "socket-function/src/misc";
|
|
2
|
+
import { RemoteConfig, HostedConfig, SourceConfig, FULL_ROUTE } from "../IArchives";
|
|
3
|
+
import { parseHostedUrl, routeContains, routeIntersection } from "./remoteConfig";
|
|
4
|
+
import { isOwnAddress } from "./serverConfig";
|
|
5
|
+
|
|
6
|
+
// Turns a bucket's routing config into this server's store plan: which of the entries are us, what stores (one per route) we run, and which peers each store synchronizes from.
|
|
7
|
+
|
|
8
|
+
export function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[] {
|
|
9
|
+
let indexes: number[] = [];
|
|
10
|
+
for (let i = 0; i < routing.sources.length; i++) {
|
|
11
|
+
let source = routing.sources[i];
|
|
12
|
+
if (typeof source === "string" || source.type !== "remote") continue;
|
|
13
|
+
let parsed = parseHostedUrl(source.url);
|
|
14
|
+
if (parsed.account !== account || parsed.bucketName !== bucketName) continue;
|
|
15
|
+
if (isOwnAddress(parsed.address, parsed.port)) {
|
|
16
|
+
indexes.push(i);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return indexes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function selectEntryAt(entries: HostedConfig[], time: number, route?: number): HostedConfig | undefined {
|
|
23
|
+
if (route !== undefined) {
|
|
24
|
+
let covering = entries.filter(x => routeContains(x.route, route));
|
|
25
|
+
if (covering.length) {
|
|
26
|
+
entries = covering;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
let containing = entries.find(x => x.validWindow[0] <= time && time < x.validWindow[1]);
|
|
30
|
+
if (containing) return containing;
|
|
31
|
+
let best: HostedConfig | undefined;
|
|
32
|
+
let bestDistance = Infinity;
|
|
33
|
+
for (let entry of entries) {
|
|
34
|
+
let distance = Math.min(Math.abs(time - entry.validWindow[0]), Math.abs(time - entry.validWindow[1]));
|
|
35
|
+
if (distance < bestDistance) {
|
|
36
|
+
bestDistance = distance;
|
|
37
|
+
best = entry;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return best;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
|
|
44
|
+
export type SelfSummary = {
|
|
45
|
+
/** The union of the current entries' routes, with overlapping/adjacent ranges combined - which commonly collapses to a single full range, making matching trivial. */
|
|
46
|
+
routes: [number, number][];
|
|
47
|
+
public: boolean;
|
|
48
|
+
immutable: boolean;
|
|
49
|
+
noFullSync: boolean;
|
|
50
|
+
rawDisk: boolean;
|
|
51
|
+
readerDiskLimit?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function mergeRoutes(routes: ([number, number] | undefined)[]): [number, number][] {
|
|
55
|
+
let list = routes.map(x => x || FULL_ROUTE).map(x => [x[0], x[1]] as [number, number]);
|
|
56
|
+
sort(list, x => x[0]);
|
|
57
|
+
let merged: [number, number][] = [];
|
|
58
|
+
for (let route of list) {
|
|
59
|
+
let last = merged[merged.length - 1];
|
|
60
|
+
if (last && route[0] <= last[1]) {
|
|
61
|
+
last[1] = Math.max(last[1], route[1]);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
merged.push(route);
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function summarizeSelf(selfEntries: HostedConfig[], now: number): SelfSummary | undefined {
|
|
70
|
+
let current = selfEntries.filter(x => x.validWindow[0] <= now && now < x.validWindow[1]);
|
|
71
|
+
if (!current.length) {
|
|
72
|
+
let nearest = selectEntryAt(selfEntries, now);
|
|
73
|
+
current = nearest && [nearest] || [];
|
|
74
|
+
}
|
|
75
|
+
if (!current.length) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
routes: mergeRoutes(current.map(x => x.route)),
|
|
80
|
+
public: current.some(x => x.public),
|
|
81
|
+
immutable: current.some(x => x.immutable),
|
|
82
|
+
noFullSync: current.some(x => x.noFullSync),
|
|
83
|
+
rawDisk: current.some(x => x.rawDisk),
|
|
84
|
+
readerDiskLimit: current.find(x => x.readerDiskLimit !== undefined)?.readerDiskLimit,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type StoreSourceSpec = { sourceConfig?: SourceConfig; validWindow: [number, number]; route?: [number, number]; noFullSync?: boolean };
|
|
89
|
+
export type StorePlanStore = {
|
|
90
|
+
routeKey: string;
|
|
91
|
+
route?: [number, number];
|
|
92
|
+
entries: HostedConfig[];
|
|
93
|
+
rawDisk: boolean;
|
|
94
|
+
readerDiskLimit?: number;
|
|
95
|
+
sourceSpecs: StoreSourceSpec[];
|
|
96
|
+
};
|
|
97
|
+
export type StorePlan = {
|
|
98
|
+
selfEntries: HostedConfig[];
|
|
99
|
+
self: SelfSummary | undefined;
|
|
100
|
+
stores: StorePlanStore[];
|
|
101
|
+
structureKey: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function computeStorePlan(account: string, bucketName: string, routing: RemoteConfig): StorePlan {
|
|
105
|
+
let selfIndexes = findSelfIndexes(routing, account, bucketName);
|
|
106
|
+
let selfEntries = selfIndexes.map(i => routing.sources[i] as HostedConfig);
|
|
107
|
+
let self = summarizeSelf(selfEntries, Date.now());
|
|
108
|
+
let ownIndexes = new Set(selfIndexes);
|
|
109
|
+
// One store per distinct route among ALL our entries - past and future windows included, so historical route folders keep serving their data and upcoming routes sync ahead of their window
|
|
110
|
+
let groups = new Map<string, { route?: [number, number]; entries: HostedConfig[]; firstIndex: number }>();
|
|
111
|
+
for (let i of selfIndexes) {
|
|
112
|
+
let entry = routing.sources[i] as HostedConfig;
|
|
113
|
+
let routeKey = JSON.stringify(entry.route || FULL_ROUTE);
|
|
114
|
+
let group = groups.get(routeKey);
|
|
115
|
+
if (!group) {
|
|
116
|
+
group = { route: entry.route, entries: [], firstIndex: i };
|
|
117
|
+
groups.set(routeKey, group);
|
|
118
|
+
}
|
|
119
|
+
group.entries.push(entry);
|
|
120
|
+
}
|
|
121
|
+
let stores: StorePlanStore[] = [];
|
|
122
|
+
for (let [routeKey, group] of groups) {
|
|
123
|
+
let anchor = selectEntryAt(group.entries, Date.now());
|
|
124
|
+
let diskWindow: [number, number] = [0, 0];
|
|
125
|
+
if (anchor) {
|
|
126
|
+
let [start, end] = anchor.validWindow;
|
|
127
|
+
let merged = true;
|
|
128
|
+
while (merged) {
|
|
129
|
+
merged = false;
|
|
130
|
+
for (let entry of group.entries) {
|
|
131
|
+
let [entryStart, entryEnd] = entry.validWindow;
|
|
132
|
+
if (entryStart > end || entryEnd < start) continue;
|
|
133
|
+
if (entryStart < start || entryEnd > end) {
|
|
134
|
+
start = Math.min(start, entryStart);
|
|
135
|
+
end = Math.max(end, entryEnd);
|
|
136
|
+
merged = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
diskWindow = [start, end];
|
|
141
|
+
}
|
|
142
|
+
let sourceSpecs: StoreSourceSpec[] = [{ validWindow: diskWindow }];
|
|
143
|
+
let noFullSync = group.entries.some(x => x.noFullSync);
|
|
144
|
+
for (let i = group.firstIndex + 1; i < routing.sources.length; i++) {
|
|
145
|
+
let source = routing.sources[i];
|
|
146
|
+
if (typeof source === "string" || ownIndexes.has(i)) continue;
|
|
147
|
+
let sharedRoute = routeIntersection(group.route, source.route);
|
|
148
|
+
if (!sharedRoute) continue;
|
|
149
|
+
sourceSpecs.push({ sourceConfig: source, validWindow: source.validWindow, route: sharedRoute, noFullSync: source.noFullSync || noFullSync });
|
|
150
|
+
}
|
|
151
|
+
stores.push({
|
|
152
|
+
routeKey,
|
|
153
|
+
route: group.route,
|
|
154
|
+
entries: group.entries,
|
|
155
|
+
rawDisk: group.entries.some(x => x.rawDisk),
|
|
156
|
+
readerDiskLimit: group.entries.find(x => x.readerDiskLimit !== undefined)?.readerDiskLimit,
|
|
157
|
+
sourceSpecs,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (!stores.length) {
|
|
161
|
+
// Not in the config at all: still serve whatever the plain folder holds, through one inert full-route store
|
|
162
|
+
stores.push({ routeKey: JSON.stringify(FULL_ROUTE), route: undefined, entries: [], rawDisk: false, readerDiskLimit: undefined, sourceSpecs: [{ validWindow: [0, 0] }] });
|
|
163
|
+
}
|
|
164
|
+
let structureKey = JSON.stringify(stores.map(s => ({ routeKey: s.routeKey, rawDisk: s.rawDisk, readerDiskLimit: s.readerDiskLimit })));
|
|
165
|
+
return { selfEntries, self, stores, structureKey };
|
|
166
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function assertValidName(value: string, kind: string): void;
|
|
2
|
+
export declare function assertValidPath(path: string): void;
|
|
3
|
+
/** Method decorator: validates the well-known fields of the method's single config-object argument - account/bucketName as names, path as a path - before the method runs. Fields the config doesn't have are skipped, so it applies to every API method uniformly. prefix is deliberately NOT validated: prefixes may be empty or end with "/", both invalid for paths. */
|
|
4
|
+
export declare function assertValidArgs(target: unknown, key: string, descriptor: PropertyDescriptor): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function assertValidName(value: string, kind: string): void {
|
|
2
|
+
if (!/^[\w-]{1,64}$/.test(value)) {
|
|
3
|
+
throw new Error(`Invalid ${kind} ${JSON.stringify(value)}, expected 1-64 characters of letters/numbers/underscore/dash`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function assertValidPath(path: string): void {
|
|
8
|
+
if (Buffer.from(path, "utf8").length > 1000) {
|
|
9
|
+
throw new Error(`Path too long: ${path.length} characters > 1000. Path: ${path.slice(0, 200)}`);
|
|
10
|
+
}
|
|
11
|
+
if (!path || path.startsWith("/") || path.endsWith("/") || path.includes("//") || path.includes("\\") || path.includes("\x00")) {
|
|
12
|
+
throw new Error(`Invalid path ${JSON.stringify(path.slice(0, 200))}, paths cannot be empty, start or end with /, or contain //, backslashes, or null characters`);
|
|
13
|
+
}
|
|
14
|
+
if (path.split("/").some(part => part === "." || part === "..")) {
|
|
15
|
+
throw new Error(`Invalid path ${JSON.stringify(path.slice(0, 200))}, paths cannot contain . or .. segments`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Method decorator: validates the well-known fields of the method's single config-object argument - account/bucketName as names, path as a path - before the method runs. Fields the config doesn't have are skipped, so it applies to every API method uniformly. prefix is deliberately NOT validated: prefixes may be empty or end with "/", both invalid for paths. */
|
|
20
|
+
export function assertValidArgs(target: unknown, key: string, descriptor: PropertyDescriptor): void {
|
|
21
|
+
let original = descriptor.value as (...args: unknown[]) => unknown;
|
|
22
|
+
descriptor.value = function (...args: unknown[]): unknown {
|
|
23
|
+
let config = args[0] as { account?: string; bucketName?: string; path?: string } | undefined;
|
|
24
|
+
if (config && typeof config === "object") {
|
|
25
|
+
if ("account" in config) assertValidName(config.account as string, "account");
|
|
26
|
+
if ("bucketName" in config) assertValidName(config.bucketName as string, "bucket name");
|
|
27
|
+
if ("path" in config) assertValidPath(config.path as string);
|
|
28
|
+
}
|
|
29
|
+
return original.apply(this, args);
|
|
30
|
+
};
|
|
31
|
+
}
|