querysub 0.2.0
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/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { measureFnc, measureWrap } from "socket-function/src/profiling/measure";
|
|
3
|
+
import { errorToUndefined, errorToUndefinedSilent, ignoreErrors, logErrors, timeoutToUndefined, timeoutToUndefinedSilent } from "../errors";
|
|
4
|
+
import { PromiseObj } from "../promise";
|
|
5
|
+
import { getAllNodeIds, getBrowserUrlNode, getOwnNodeId, isNodeDiscoveryLogging, isOwnNodeId, onNodeDiscoveryReady, watchDeltaNodeIds, watchNodeIds } from "../-f-node-discovery/NodeDiscovery";
|
|
6
|
+
import { PathValueController } from "./PathValueController";
|
|
7
|
+
import { MAX_ACCEPTED_AUTHORITY_STARTUP_TIME, PathValueSnapshot, STARTUP_CUTOFF_TIME, authorityStorage, matchesParentRangeFilterPart } from "./pathValueCore";
|
|
8
|
+
import { pathValueArchives } from "./pathValueArchives";
|
|
9
|
+
import { deepCloneJSON, isNode, sha256Hash, sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
10
|
+
import { delay, runInfinitePoll } from "socket-function/src/batching";
|
|
11
|
+
import { blue, green, magenta, red, yellow } from "socket-function/src/formatting/logColors";
|
|
12
|
+
import debugbreak from "debugbreak";
|
|
13
|
+
import { getNodeIdFromLocation, getNodeIdIP, getNodeIdLocation } from "socket-function/src/nodeCache";
|
|
14
|
+
import { appendToPathStr, getPathDepth, getPathFromStr, getPathIndex, getPathStr, getPathStr1, getPathStr2 } from "../path";
|
|
15
|
+
import { MaybePromise } from "socket-function/src/types";
|
|
16
|
+
import { isClient, isServer } from "../config2";
|
|
17
|
+
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
18
|
+
import { sha256 } from "js-sha256";
|
|
19
|
+
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
20
|
+
import { cache, cacheLimited } from "socket-function/src/caching";
|
|
21
|
+
import { IdentityController_getCurrentReconnectNodeIdAssert, IdentityController_getReconnectNodeIdAssert } from "../-c-identity/IdentityController";
|
|
22
|
+
import { getBufferFraction, getBufferInt, getShortNumber } from "../bits";
|
|
23
|
+
import { devDebugbreak, isDevDebugbreak } from "../config";
|
|
24
|
+
import { diskLog } from "../diagnostics/logs/diskLogger";
|
|
25
|
+
|
|
26
|
+
export const LOCAL_DOMAIN = "LOCAL";
|
|
27
|
+
export const LOCAL_DOMAIN_PATH = getPathStr1(LOCAL_DOMAIN);
|
|
28
|
+
|
|
29
|
+
const POLL_RATE = 5000;
|
|
30
|
+
const MAX_RECONNECT_TIME = timeInMinute * 15;
|
|
31
|
+
const RECONNECT_POLL_INTERVAL = 10000;
|
|
32
|
+
|
|
33
|
+
// Poll nodes that appear dead. Without this, if the internet goes down, we might forever ignore nodes.
|
|
34
|
+
const RECOVERY_POLL_INTERVAL = timeInMinute;
|
|
35
|
+
|
|
36
|
+
// NOTE: We don't implicitly include parents, as when we add multiple specific paths it is easier
|
|
37
|
+
// to do it off of a tree of AuthorityPathPart2s, instead of trying to store a whole tree's worth
|
|
38
|
+
// of into in a single AuthorityPathPart2.
|
|
39
|
+
export type AuthorityPath = {
|
|
40
|
+
// NOTE: Object.keys() accesses "" might sometimes be considered a wildcard.
|
|
41
|
+
|
|
42
|
+
// MATCHES path.startsWith(pathPrefix)
|
|
43
|
+
pathPrefix: string;
|
|
44
|
+
// AND MATCHES !path.startsWith(excludedChildren[i]) || path === excludedChildren[i]
|
|
45
|
+
// (As in, this only excludes descendants, not the exact child matches)
|
|
46
|
+
excludedChildren?: string[];
|
|
47
|
+
// See matchesParentRangeFilterPart
|
|
48
|
+
hash?: {
|
|
49
|
+
// Depth MUST equal getPathDepth(pathPrefix), and is only stored for faster matching
|
|
50
|
+
depth: number;
|
|
51
|
+
start: number;
|
|
52
|
+
end: number;
|
|
53
|
+
};
|
|
54
|
+
/* Causes any path parts that are "" to be considered wildcards, and allow matching any
|
|
55
|
+
path (both in pathPrefix and excludedChildren).
|
|
56
|
+
NOTE: The only reason to NOT set this is because wild card checks are slow.
|
|
57
|
+
*/
|
|
58
|
+
emptyIsWildcard?: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type AuthorityObj = {
|
|
62
|
+
nodeId: string;
|
|
63
|
+
authorityPaths: AuthorityPath[];
|
|
64
|
+
isReadReady: number;
|
|
65
|
+
createTime: number;
|
|
66
|
+
self?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type WatchedAuthorityObj = AuthorityObj & {
|
|
70
|
+
// Waits until ready OR throws, if MAX_ACCEPTED_AUTHORITY_STARTUP_TIME is exceeded
|
|
71
|
+
onReady: PromiseObj<void>;
|
|
72
|
+
};
|
|
73
|
+
class NodePathAuthorities {
|
|
74
|
+
// nodeId =>
|
|
75
|
+
private authorities = new Map<string, WatchedAuthorityObj>();
|
|
76
|
+
private disconnectedAuthorities = new Map<string, WatchedAuthorityObj>();
|
|
77
|
+
private disconnectCounts = new WeakMap<WatchedAuthorityObj, number>();
|
|
78
|
+
|
|
79
|
+
private selfAuthorities: AuthorityPath[] = [];
|
|
80
|
+
|
|
81
|
+
public debug_getAuthority(nodeId: string) {
|
|
82
|
+
return this.authorities.get(nodeId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public getSelfAuthorities(): AuthorityPath[] {
|
|
86
|
+
return this.selfAuthorities;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public getAuthorityPaths(nodeId: string) {
|
|
90
|
+
return this.authorities.get(nodeId)?.authorityPaths;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public readonly createTime = Date.now();
|
|
94
|
+
constructor() {
|
|
95
|
+
// NOTE: WHILE calling this function at the module level (because we this is a singleton) is bad,
|
|
96
|
+
// it SHOULD be fine. "getOwnNodeId" is part of level "-f-node-discovery", and we are level "0-path-value-core".
|
|
97
|
+
// It should be ready before us, and it should never be allowed to depend on us.
|
|
98
|
+
let originalOwnId = getOwnNodeId();
|
|
99
|
+
let selfOnReady = new PromiseObj();
|
|
100
|
+
this.authorities.set(getOwnNodeId(), {
|
|
101
|
+
nodeId: getOwnNodeId(),
|
|
102
|
+
authorityPaths: [],
|
|
103
|
+
isReadReady: 0,
|
|
104
|
+
createTime: this.createTime,
|
|
105
|
+
self: true,
|
|
106
|
+
onReady: selfOnReady,
|
|
107
|
+
});
|
|
108
|
+
if (isServer()) {
|
|
109
|
+
void SocketFunction.mountPromise.finally(() => {
|
|
110
|
+
// Move over to actual nodeId (which only exists after we mount)
|
|
111
|
+
let newOwnId = getOwnNodeId();
|
|
112
|
+
if (newOwnId !== originalOwnId) {
|
|
113
|
+
let obj = this.authorities.get(originalOwnId);
|
|
114
|
+
if (obj) {
|
|
115
|
+
this.authorities.delete(originalOwnId);
|
|
116
|
+
this.authorities.set(newOwnId, obj);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let promise = this.baseBecomeAuthority(this.createTime);
|
|
120
|
+
promise.catch(this.routingReady.reject);
|
|
121
|
+
selfOnReady.resolve(promise);
|
|
122
|
+
logErrors(promise);
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
selfOnReady.resolve();
|
|
126
|
+
let promise = this.watchAuthorityPaths();
|
|
127
|
+
promise.catch(this.routingReady.reject);
|
|
128
|
+
logErrors(promise);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private reconnectLoop = runInfinitePoll(RECONNECT_POLL_INTERVAL, async () => {
|
|
133
|
+
if (!isNode()) return;
|
|
134
|
+
let disconnectedAuthorities = Array.from(this.disconnectedAuthorities.values());
|
|
135
|
+
await Promise.allSettled(
|
|
136
|
+
disconnectedAuthorities.map(async x => {
|
|
137
|
+
let isAliveAgain = await timeoutToUndefinedSilent(5000, PathController.nodes[x.nodeId].isReadReady());
|
|
138
|
+
if (isAliveAgain) {
|
|
139
|
+
this.disconnectedAuthorities.delete(x.nodeId);
|
|
140
|
+
// NOTE: We don't wait for reconnections if we fail to find an authority or anything, because...
|
|
141
|
+
// it is VERY unlikely for nodes to reconnect. HOWEVER, RemoteWatcher polls forever
|
|
142
|
+
// for orphaned watches, so reconnecting here will result in the node being reused
|
|
143
|
+
// somewhat quickly.
|
|
144
|
+
this.authorities.set(x.nodeId, x);
|
|
145
|
+
this.disconnectCounts.delete(x);
|
|
146
|
+
console.log(green(`Reconnected to ${x.nodeId}`));
|
|
147
|
+
} else {
|
|
148
|
+
let prevCount = this.disconnectCounts.get(x) || 0;
|
|
149
|
+
prevCount++;
|
|
150
|
+
// If it takes too long, give up, and stop polling.
|
|
151
|
+
if (prevCount > (MAX_RECONNECT_TIME / RECONNECT_POLL_INTERVAL)) {
|
|
152
|
+
this.disconnectedAuthorities.delete(x.nodeId);
|
|
153
|
+
this.disconnectCounts.delete(x);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.disconnectCounts.set(x, prevCount);
|
|
157
|
+
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// NOTE: Must be called BEFORE mounting
|
|
164
|
+
public becomePathAuthority(authority: AuthorityPath) {
|
|
165
|
+
if (SocketFunction.isMounted()) throw new Error(`Cannot call becomePathAuthority after mounting`);
|
|
166
|
+
authority = deepCloneJSON(authority);
|
|
167
|
+
if (authority.hash) {
|
|
168
|
+
authority.hash.depth = getPathDepth(authority.pathPrefix);
|
|
169
|
+
}
|
|
170
|
+
if (!this.selfAuthorities.includes(authority)) {
|
|
171
|
+
this.selfAuthorities.push(authority);
|
|
172
|
+
// Longest first is more efficient
|
|
173
|
+
sort(this.selfAuthorities, x => -x.pathPrefix.length);
|
|
174
|
+
}
|
|
175
|
+
let authorityObj = this.authorities.get(getOwnNodeId());
|
|
176
|
+
if (!authorityObj) {
|
|
177
|
+
throw new Error(`Authority object not found for self. Should be impossible, but maybe our own id changed. Own id is ${getOwnNodeId()}, we have authorities for: ${Object.keys(this.authorities).join(", ")}`);
|
|
178
|
+
}
|
|
179
|
+
let hash = JSON.stringify(authority);
|
|
180
|
+
if (!authorityObj.authorityPaths.some(x => JSON.stringify(x) === hash)) {
|
|
181
|
+
authorityObj.authorityPaths.push(authority);
|
|
182
|
+
sort(authorityObj.authorityPaths, x => -x.pathPrefix.length);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(blue(`Becoming an authority for ${getArchiveDirectory(authority)}`));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private isReadReady = 0;
|
|
189
|
+
public isSelfReadReady(): number {
|
|
190
|
+
return this.isReadReady;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@measureFnc
|
|
194
|
+
private async watchAuthorityPaths() {
|
|
195
|
+
await onNodeDiscoveryReady();
|
|
196
|
+
|
|
197
|
+
onReadyReady = (nodeId, time) => {
|
|
198
|
+
let obj = this.authorities.get(nodeId);
|
|
199
|
+
if (!obj) {
|
|
200
|
+
// HACK: Sometimes when a node is first added we can't identify it yet. I THINK this is because
|
|
201
|
+
// we haven't learned to trust the key, or... something? Hmm...
|
|
202
|
+
// ingestNewNodeIds([nodeId], []);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
obj.isReadReady = time;
|
|
206
|
+
obj.onReady.resolve();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
let first = true;
|
|
210
|
+
let firstPromise = new PromiseObj<unknown>();
|
|
211
|
+
|
|
212
|
+
const ingestNewNodeIds = (newNodeIds: string[], removedNodeIds: string[]) => {
|
|
213
|
+
for (let nodeId of removedNodeIds) {
|
|
214
|
+
this.authorities.delete(nodeId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let nodeIds = newNodeIds;
|
|
218
|
+
nodeIds = nodeIds.filter(nodeId => !isOwnNodeId(nodeId));
|
|
219
|
+
let newNodes = nodeIds.filter(nodeId => !this.authorities.has(nodeId));
|
|
220
|
+
let timeout = first ? POLL_RATE : 0;
|
|
221
|
+
first = false;
|
|
222
|
+
|
|
223
|
+
let promise = Promise.allSettled(newNodes.map(async nodeId => {
|
|
224
|
+
if (this.authorities.has(nodeId)) return;
|
|
225
|
+
|
|
226
|
+
let loc = getNodeIdLocation(nodeId);
|
|
227
|
+
let ourLoc = getNodeIdLocation(getOwnNodeId());
|
|
228
|
+
if (loc && loc.address.startsWith("127-0-0-1") && loc.port === ourLoc?.port) {
|
|
229
|
+
// Ignore it if it's just us, using the special localhost address
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let createTime = await errorToUndefinedSilent(PathController.nodes[nodeId].getCreateTime());
|
|
234
|
+
if (createTime === undefined) {
|
|
235
|
+
// Don't log for 127-0-0-1, as it usually fails, and is mostly a development optimization
|
|
236
|
+
if (!nodeId.includes("127-0-0-1")) {
|
|
237
|
+
diskLog(`Node didn't respond to getCreateTime`, { nodeId });
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
console.log(blue(`Identifying ${nodeId} as a path authority`));
|
|
242
|
+
this.authorities.set(nodeId, {
|
|
243
|
+
nodeId,
|
|
244
|
+
self: nodeId.startsWith("127-0-0-1."),
|
|
245
|
+
authorityPaths: [],
|
|
246
|
+
isReadReady: 0,
|
|
247
|
+
createTime,
|
|
248
|
+
onReady: new PromiseObj(),
|
|
249
|
+
});
|
|
250
|
+
let obj = this.authorities.get(nodeId)!;
|
|
251
|
+
SocketFunction.onNextDisconnect(nodeId, () => {
|
|
252
|
+
this.authorities.delete(nodeId);
|
|
253
|
+
this.disconnectedAuthorities.set(nodeId, obj);
|
|
254
|
+
});
|
|
255
|
+
let finished = false;
|
|
256
|
+
try {
|
|
257
|
+
if (timeout) {
|
|
258
|
+
let timeoutObj = new PromiseObj();
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
timeoutObj.resolve();
|
|
261
|
+
if (!finished && timeout) {
|
|
262
|
+
console.warn(yellow(`Timeout after ${formatTime(timeout)} while identifying ${nodeId}`));
|
|
263
|
+
}
|
|
264
|
+
}, timeout);
|
|
265
|
+
await Promise.race([
|
|
266
|
+
timeoutObj.promise,
|
|
267
|
+
errorToUndefined(this.forceCheckForReadReady(nodeId)),
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
// In case we miss the broadcast, poll to see if it becomes ready
|
|
271
|
+
if (!obj.isReadReady) {
|
|
272
|
+
ignoreErrors((async () => {
|
|
273
|
+
while (!obj.isReadReady && this.authorities.has(nodeId)) {
|
|
274
|
+
await delay(timeInSecond * 10);
|
|
275
|
+
if (obj.isReadReady) break;
|
|
276
|
+
await errorToUndefinedSilent(this.forceCheckForReadReady(nodeId));
|
|
277
|
+
}
|
|
278
|
+
})());
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
finished = true;
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
logErrors(firstPromise);
|
|
285
|
+
firstPromise.resolve(promise);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
let time = Date.now();
|
|
289
|
+
watchDeltaNodeIds(obj => ingestNewNodeIds(obj.newNodeIds, obj.removedNodeIds));
|
|
290
|
+
await firstPromise.promise;
|
|
291
|
+
|
|
292
|
+
runInfinitePoll(RECOVERY_POLL_INTERVAL, async () => {
|
|
293
|
+
let nodes = await getAllNodeIds();
|
|
294
|
+
let nonExistentNodes: string[] = [];
|
|
295
|
+
for (let node of nodes) {
|
|
296
|
+
if (this.authorities.has(node)) continue;
|
|
297
|
+
if (this.disconnectedAuthorities.has(node)) continue;
|
|
298
|
+
nonExistentNodes.push(node);
|
|
299
|
+
}
|
|
300
|
+
// Pretend all dead nodes are new
|
|
301
|
+
ingestNewNodeIds(nonExistentNodes, []);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
let readyCount = 0;
|
|
305
|
+
for (let authority of this.authorities.values()) {
|
|
306
|
+
if (!authority.isReadReady) continue;
|
|
307
|
+
if (authority.authorityPaths.length === 0) continue;
|
|
308
|
+
readyCount++;
|
|
309
|
+
console.log(` ${green(`(ready)`)} ${authority.nodeId}`);
|
|
310
|
+
}
|
|
311
|
+
console.log(green(`Finished loading all ${this.authorities.size} path authorities in ${formatTime(Date.now() - time)}. Ready authorities: ${readyCount}`));
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
this.routingReady.resolve();
|
|
315
|
+
this.waitUntilRoutingIsReadyBase = () => undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private async forceCheckForReadReady(nodeId: string) {
|
|
319
|
+
let obj = this.authorities.get(nodeId);
|
|
320
|
+
if (!obj) return;
|
|
321
|
+
if (obj.isReadReady) return;
|
|
322
|
+
if (isNodeDiscoveryLogging()) {
|
|
323
|
+
console.log(blue(`Starting to identify ${nodeId} as a path authority`));
|
|
324
|
+
}
|
|
325
|
+
// now BEFORE we poll, so skew towards lower times, as higher times results in us discarding the node
|
|
326
|
+
// as being out of date.
|
|
327
|
+
let now = Date.now();
|
|
328
|
+
let isReadReady = await errorToUndefinedSilent(PathController.nodes[nodeId].isReadReady());
|
|
329
|
+
// If the node is gone, delete it from authorities
|
|
330
|
+
if (isReadReady === undefined) {
|
|
331
|
+
console.error(yellow(`Node errored out, removing from authorities ${nodeId}`));
|
|
332
|
+
this.authorities.delete(nodeId);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!isReadReady && obj.createTime + MAX_ACCEPTED_AUTHORITY_STARTUP_TIME < now) {
|
|
336
|
+
let errorMessage = `Node took too long to become ready. Removing from authorities. ${nodeId}`;
|
|
337
|
+
obj.onReady.reject(new Error(errorMessage));
|
|
338
|
+
console.error(red(errorMessage));
|
|
339
|
+
this.authorities.delete(nodeId);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
obj.isReadReady = isReadReady;
|
|
344
|
+
obj.onReady.resolve();
|
|
345
|
+
|
|
346
|
+
obj.authorityPaths = await PathController.nodes[nodeId].getAuthorityPaths();
|
|
347
|
+
if (obj.authorityPaths.length > 0) {
|
|
348
|
+
console.log(blue(`Identified ${isReadReady ? "(ready)" : yellow("(loading)")} ${nodeId} as a path authority for:`));
|
|
349
|
+
for (let authorityPath of obj.authorityPaths) {
|
|
350
|
+
console.log(` ${getArchiveDirectory(authorityPath)}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@measureFnc
|
|
356
|
+
private async baseBecomeAuthority(createTime: number) {
|
|
357
|
+
await this.watchAuthorityPaths();
|
|
358
|
+
|
|
359
|
+
// NOTE: This technically races, as even if all NEW values will be sent to us, there may be on the
|
|
360
|
+
// write that we can't read in any way (disk or memory reads), which we just have to wait to flush.
|
|
361
|
+
// BUT... this is very uncommon, as virtually all of the time all network writes will finish
|
|
362
|
+
// before we can flush to backblaze...
|
|
363
|
+
|
|
364
|
+
let authoritySources: {
|
|
365
|
+
authorityPath: AuthorityPath;
|
|
366
|
+
authorities: AuthorityObj[];
|
|
367
|
+
}[] = [];
|
|
368
|
+
|
|
369
|
+
// Find sources, waiting for all to be ready. Memory cleanup requires
|
|
370
|
+
while (true) {
|
|
371
|
+
authoritySources = [];
|
|
372
|
+
for (let authorityPath of this.getSelfAuthorities()) {
|
|
373
|
+
let sources: AuthorityObj[] = [];
|
|
374
|
+
for (let otherAuthObj of this.authorities.values()) {
|
|
375
|
+
if (otherAuthObj.self) continue;
|
|
376
|
+
// Only load from authorites newer than us
|
|
377
|
+
if (otherAuthObj.createTime > createTime) continue;
|
|
378
|
+
let matchingAuth = Object.values(otherAuthObj.authorityPaths).find(x => authoritiesMightOverlap(x, authorityPath));
|
|
379
|
+
if (!matchingAuth) continue;
|
|
380
|
+
sources.push(otherAuthObj);
|
|
381
|
+
}
|
|
382
|
+
authoritySources.push({ authorityPath, authorities: sources });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let waitForAuthorities = authoritySources.flatMap(x => x.authorities).filter(x => !x.isReadReady);
|
|
386
|
+
if (waitForAuthorities.length === 0) break;
|
|
387
|
+
console.log(yellow(`Waiting for ${waitForAuthorities.length} authorities to be ready:`));
|
|
388
|
+
// Log the paths we are waiting for
|
|
389
|
+
for (let source of authoritySources) {
|
|
390
|
+
if (source.authorities.every(x => x.isReadReady)) continue;
|
|
391
|
+
console.log(yellow(` ${getArchiveDirectory(source.authorityPath)}`));
|
|
392
|
+
}
|
|
393
|
+
await delay(POLL_RATE);
|
|
394
|
+
// NOTE: forceCheckForReadReady will remove dead authorities.
|
|
395
|
+
await Promise.allSettled(waitForAuthorities.map(x => this.forceCheckForReadReady(x.nodeId)));
|
|
396
|
+
// If any authorities are not ready (and have been matched, wait, and then check them again).
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Load all values from memory
|
|
400
|
+
let allSnapshots: PathValueSnapshot[] = [];
|
|
401
|
+
for (let source of authoritySources) {
|
|
402
|
+
console.log(blue(`Loading authority path ${getArchiveDirectory(source.authorityPath)}`));
|
|
403
|
+
for (let otherAuthObj of source.authorities) {
|
|
404
|
+
let nodeId = otherAuthObj.nodeId;
|
|
405
|
+
console.log(blue(` Loading snapshot from ${nodeId}`));
|
|
406
|
+
// NOTE: getSnapshot automatically filters to those asked for on authorityPath
|
|
407
|
+
let snapshotBuffers = await PathValueController.nodes[nodeId].getSnapshot({ authorityPath: source.authorityPath });
|
|
408
|
+
let totalSize = snapshotBuffers.reduce((a, b) => a + b.length, 0);
|
|
409
|
+
console.log(green(` Loaded ${formatNumber(snapshotBuffers.length)} buffers, total ${formatNumber(totalSize)}B`));
|
|
410
|
+
let pathValues = await pathValueSerializer.deserialize(snapshotBuffers);
|
|
411
|
+
console.log(green(` Loaded ${formatNumber(pathValues.length)} values`));
|
|
412
|
+
let snapshot: PathValueSnapshot = {
|
|
413
|
+
values: {},
|
|
414
|
+
};
|
|
415
|
+
for (let pathValue of pathValues) {
|
|
416
|
+
let path = pathValue.path;
|
|
417
|
+
let values = snapshot.values[path];
|
|
418
|
+
if (!values) {
|
|
419
|
+
snapshot.values[path] = values = [];
|
|
420
|
+
}
|
|
421
|
+
values.push(pathValue);
|
|
422
|
+
}
|
|
423
|
+
allSnapshots.push(snapshot);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let requireArchivesLoad = !source.authorities.some(x => x.authorityPaths.some(y =>
|
|
427
|
+
// TODO: Deal with subsets, instead of only using an exact comparison
|
|
428
|
+
JSON.stringify(y) === JSON.stringify(source.authorityPath)
|
|
429
|
+
));
|
|
430
|
+
if (requireArchivesLoad) {
|
|
431
|
+
if (source.authorities.length > 0) {
|
|
432
|
+
console.log(yellow(`Loading from disk for ${getArchiveDirectory(source.authorityPath)}, even though there are existing authorities. This is likely technically unnecessary, but presently done because we haven't implemented AuthorityPath subset code. If this takes an excessive amount of time, we should implement the correct AuthorityPath subset code.`));
|
|
433
|
+
} else {
|
|
434
|
+
console.log(blue(`Loading from disk for ${getArchiveDirectory(source.authorityPath)}`));
|
|
435
|
+
}
|
|
436
|
+
let snapshot = await pathValueArchives.loadValues(source.authorityPath);
|
|
437
|
+
allSnapshots.push(snapshot);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (let snapshot of allSnapshots) {
|
|
442
|
+
authorityStorage.ingestSnapshot(snapshot);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let readyTime = Date.now();
|
|
446
|
+
let timeToReady = readyTime - this.createTime;
|
|
447
|
+
if (timeToReady > MAX_ACCEPTED_AUTHORITY_STARTUP_TIME) {
|
|
448
|
+
console.error(red(`Took too long to become an authority (${formatTime(timeToReady)}), other nodes will not accept us as an authority, killing process now`));
|
|
449
|
+
process.exit();
|
|
450
|
+
}
|
|
451
|
+
this.isReadReady = readyTime;
|
|
452
|
+
for (let nodeId of await getAllNodeIds()) {
|
|
453
|
+
ignoreErrors(PathController.nodes[nodeId].broadcastReadReady(readyTime));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (this.getSelfAuthorities().length > 0) {
|
|
457
|
+
console.log(green(`Became an authority for:`));
|
|
458
|
+
for (let authorityPath of this.getSelfAuthorities()) {
|
|
459
|
+
console.log(green(` ${getArchiveDirectory(authorityPath)}`));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private waited = false;
|
|
465
|
+
private routingReady = new PromiseObj();
|
|
466
|
+
@measureFnc
|
|
467
|
+
public waitUntilRoutingIsReady() {
|
|
468
|
+
this.waited = true;
|
|
469
|
+
return this.waitUntilRoutingIsReadyBase();
|
|
470
|
+
}
|
|
471
|
+
// NOTE: For some reason, having to wait on the promise every time is really slow (~1ms). So... once it is done,
|
|
472
|
+
// we make it return undefined, avoiding this issue. It probably isn't REALLY slow, and is probably just a measurement
|
|
473
|
+
// error, but... oh well...
|
|
474
|
+
private waitUntilRoutingIsReadyBase: () => MaybePromise<void> = async () => {
|
|
475
|
+
return this.routingReady.promise;
|
|
476
|
+
};
|
|
477
|
+
public assertRoutingIsReady() {
|
|
478
|
+
let waitPromise = this.waitUntilRoutingIsReadyBase();
|
|
479
|
+
if (waitPromise !== undefined) {
|
|
480
|
+
throw new Error(`waitUntilRoutingIsReady was not called`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
public isSelfAuthority(path: string): boolean {
|
|
485
|
+
if (this.isLocalPath(path)) return true;
|
|
486
|
+
return this.selfAuthorities.some(x => isInAuthority(x, path));
|
|
487
|
+
}
|
|
488
|
+
public isInAuthority(authority: AuthorityPath, path: string): boolean {
|
|
489
|
+
return isInAuthority(authority, path);
|
|
490
|
+
}
|
|
491
|
+
public isLocalPath(path: string): boolean {
|
|
492
|
+
return path.startsWith(LOCAL_DOMAIN_PATH);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
/** Not a full path, it is expected to be nested under the main PathValue archive
|
|
497
|
+
* (or wherever you want to store files).
|
|
498
|
+
*/
|
|
499
|
+
public getArchiveDirectory(authorityPath: AuthorityPath): string {
|
|
500
|
+
return getArchiveDirectory(authorityPath);
|
|
501
|
+
}
|
|
502
|
+
public mightMatchArchiveDirectory(authorityPath: AuthorityPath, directory: string): boolean {
|
|
503
|
+
return mightMatchArchiveDirectory(authorityPath, directory);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
public getSelfArchiveAuthority(path: string): AuthorityPath | undefined {
|
|
507
|
+
for (let authorityPath of this.getSelfAuthorities()) {
|
|
508
|
+
if (isInAuthority(authorityPath, path)) {
|
|
509
|
+
return authorityPath;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@measureFnc
|
|
516
|
+
private getReadObjs(path: string, config?: { allowUnready?: boolean }) {
|
|
517
|
+
return Array.from(this.authorities.values())
|
|
518
|
+
// If it will never be ready... just ignore it
|
|
519
|
+
.filter(x => !x.onReady.rejected)
|
|
520
|
+
.filter(x => config?.allowUnready || x.isReadReady)
|
|
521
|
+
.filter(x =>
|
|
522
|
+
Object.values(x.authorityPaths).some(authority =>
|
|
523
|
+
isInAuthority(authority, path)
|
|
524
|
+
)
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
public getReadNodes(path: string): string[] {
|
|
528
|
+
if (isClient()) {
|
|
529
|
+
return [getBrowserUrlNode()];
|
|
530
|
+
}
|
|
531
|
+
if (!this.waited) throw new Error(`waitUntilRoutingIsReady must be called before getReadNodes`);
|
|
532
|
+
return this.getReadObjs(path).map(x => x.nodeId);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private pickAuthorityNode(objs: AuthorityObj[] | undefined): AuthorityObj | undefined {
|
|
536
|
+
if (!objs) return undefined;
|
|
537
|
+
// Prefer local nodes if they exist, and we are developing
|
|
538
|
+
if (isDevDebugbreak()) {
|
|
539
|
+
let local = objs.find(x => x.nodeId.startsWith("127-0-0-1."));
|
|
540
|
+
if (local) {
|
|
541
|
+
return local;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return objs[Math.floor(Math.random() * objs.length)];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private logWaitingForNode = cache((nodeId: string) => {
|
|
548
|
+
console.log(yellow(`Waiting for ${nodeId} to be ready`));
|
|
549
|
+
});
|
|
550
|
+
public async getSingleReadNodePromise(path: string): Promise<string> {
|
|
551
|
+
await this.waitUntilRoutingIsReady();
|
|
552
|
+
let nodeId = this.getSingleReadNodeSync(path);
|
|
553
|
+
// 1) See if there IS a node, but maybe it is just not ready yet
|
|
554
|
+
if (!nodeId) {
|
|
555
|
+
let objs = this.getReadObjs(path, { allowUnready: true });
|
|
556
|
+
nodeId = this.pickAuthorityNode(objs)?.nodeId;
|
|
557
|
+
if (nodeId) {
|
|
558
|
+
let obj = this.authorities.get(nodeId)!;
|
|
559
|
+
this.logWaitingForNode(nodeId);
|
|
560
|
+
|
|
561
|
+
// Only wait a few seconds, and then try again. This solves the issue of a node loading,
|
|
562
|
+
// and then taking forever, while other nodes have already become ready since we
|
|
563
|
+
// started waiting
|
|
564
|
+
await Promise.race([
|
|
565
|
+
// Ignore errors, we will log it somewhere else, and we shouldn't fail the call
|
|
566
|
+
// just because one node errored out
|
|
567
|
+
obj.onReady.promise.catch(() => { }),
|
|
568
|
+
delay(5000),
|
|
569
|
+
]);
|
|
570
|
+
// Call again, as either the node is ready, or we should wait
|
|
571
|
+
// for another node to be ready (or no nodes are even loading!)
|
|
572
|
+
return this.getSingleReadNodePromise(path);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (!nodeId && Date.now() < STARTUP_CUTOFF_TIME) {
|
|
576
|
+
// Wait a bit, then try again
|
|
577
|
+
await delay(5000);
|
|
578
|
+
return this.getSingleReadNodePromise(path);
|
|
579
|
+
}
|
|
580
|
+
if (!nodeId) {
|
|
581
|
+
throw new Error(`No node found for path ${path}`);
|
|
582
|
+
}
|
|
583
|
+
return nodeId;
|
|
584
|
+
}
|
|
585
|
+
public getSingleReadNodeSync(path: string): string | undefined {
|
|
586
|
+
if (this.isSelfAuthority(path)) {
|
|
587
|
+
return getOwnNodeId();
|
|
588
|
+
}
|
|
589
|
+
if (isClient()) {
|
|
590
|
+
return getBrowserUrlNode();
|
|
591
|
+
}
|
|
592
|
+
if (!this.waited) throw new Error(`waitUntilRoutingIsReady must be called before getSingleReadNode`);
|
|
593
|
+
let objs = this.getReadObjs(path);
|
|
594
|
+
return this.pickAuthorityNode(objs)?.nodeId;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
public getChildReadNodes(path: string, config?: {
|
|
598
|
+
preferredNodeIds?: Map<string, unknown>;
|
|
599
|
+
}): {
|
|
600
|
+
// NOTE: If at all possible, we will cover all ranges. Node of the returned nodes will be redundant.
|
|
601
|
+
// - Sorted by range.start
|
|
602
|
+
nodes: {
|
|
603
|
+
nodeId: string;
|
|
604
|
+
// The range of hashes this node owns, for the child keys of path
|
|
605
|
+
// (If the node doesn't restrict the range, it will just be { start: 0, end: 1 })
|
|
606
|
+
range: { start: number; end: number };
|
|
607
|
+
}[];
|
|
608
|
+
} {
|
|
609
|
+
if (this.isSelfAuthority(path)) {
|
|
610
|
+
return { nodes: [{ nodeId: getOwnNodeId(), range: { start: 0, end: 1 } }] };
|
|
611
|
+
}
|
|
612
|
+
if (isClient()) {
|
|
613
|
+
return { nodes: [{ nodeId: getBrowserUrlNode(), range: { start: 0, end: 1 } }] };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
let wildcardPath = appendToPathStr(path, "");
|
|
617
|
+
|
|
618
|
+
let pathDepth = getPathDepth(path);
|
|
619
|
+
|
|
620
|
+
// Sweep over the nodes, sorted by start, adding each node with an end > the previous end.
|
|
621
|
+
// - Whenever we have duplicates with the same start, pick one according to our pick algorithm.
|
|
622
|
+
// ALSO, if one of the options is in preferredNodeIds, pick it automatically
|
|
623
|
+
|
|
624
|
+
let allNodes: {
|
|
625
|
+
nodeId: AuthorityObj;
|
|
626
|
+
range: { start: number; end: number };
|
|
627
|
+
}[] = [];
|
|
628
|
+
for (let authorityObj of this.authorities.values()) {
|
|
629
|
+
if (!authorityObj.isReadReady) continue;
|
|
630
|
+
for (let path of Object.values(authorityObj.authorityPaths)) {
|
|
631
|
+
// NOTE: The only reason we don't ALWAYS check wildcards is for speed, so forcing true here is fine...
|
|
632
|
+
if (!isInAuthority({ ...path, emptyIsWildcard: true }, wildcardPath)) continue;
|
|
633
|
+
let range = { start: 0, end: 1 };
|
|
634
|
+
if (path.hash?.depth === pathDepth) {
|
|
635
|
+
range = path.hash;
|
|
636
|
+
}
|
|
637
|
+
allNodes.push({ nodeId: authorityObj, range });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let pickedNodes: {
|
|
642
|
+
nodeId: string;
|
|
643
|
+
range: { start: number; end: number };
|
|
644
|
+
}[] = [];
|
|
645
|
+
|
|
646
|
+
sort(allNodes, x => x.range.start);
|
|
647
|
+
let allNodesByStart: {
|
|
648
|
+
start: number;
|
|
649
|
+
nodes: {
|
|
650
|
+
nodeId: AuthorityObj;
|
|
651
|
+
range: { start: number; end: number };
|
|
652
|
+
}[];
|
|
653
|
+
}[] = [];
|
|
654
|
+
|
|
655
|
+
for (let node of allNodes) {
|
|
656
|
+
let last = allNodesByStart[allNodesByStart.length - 1];
|
|
657
|
+
if (!last || last.start !== node.range.start) {
|
|
658
|
+
last = { start: node.range.start, nodes: [] };
|
|
659
|
+
allNodesByStart.push(last);
|
|
660
|
+
}
|
|
661
|
+
last.nodes.push(node);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let lastEnd = -1;
|
|
665
|
+
for (let { start, nodes } of allNodesByStart) {
|
|
666
|
+
nodes = nodes.filter(x => x.range.end > lastEnd);
|
|
667
|
+
if (nodes.length === 0) continue;
|
|
668
|
+
let preferredNodes = nodes.filter(x => config?.preferredNodeIds?.has(x.nodeId.nodeId));
|
|
669
|
+
if (preferredNodes.length > 0) {
|
|
670
|
+
nodes = preferredNodes;
|
|
671
|
+
}
|
|
672
|
+
let node = this.pickAuthorityNode(nodes.map(x => x.nodeId));
|
|
673
|
+
if (node) {
|
|
674
|
+
let obj = nodes.find(x => x.nodeId.nodeId === node?.nodeId)!;
|
|
675
|
+
pickedNodes.push({ nodeId: node.nodeId, range: obj.range });
|
|
676
|
+
lastEnd = obj.range.end;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// At the end do a check for purely redundant nodes. This can happen if we have tiling overlaps. We can determine
|
|
681
|
+
// this by comparing to the previous and next nodes.
|
|
682
|
+
for (let i = 1; i < pickedNodes.length - 1; i++) {
|
|
683
|
+
let prev = pickedNodes[i - 1];
|
|
684
|
+
let next = pickedNodes[i + 1];
|
|
685
|
+
// If our neighbors touch, then we are not needed
|
|
686
|
+
if (prev.range.end >= next.range.start) {
|
|
687
|
+
pickedNodes.splice(i, 1);
|
|
688
|
+
// Try i again, as the next node might itself be redundant, using the same prev
|
|
689
|
+
i--;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
sort(pickedNodes, x => x.range.start);
|
|
694
|
+
return { nodes: pickedNodes };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// NOTE: Nodes become writeable before they become readable, so they have to be guaranteed to have
|
|
698
|
+
// all values to be readable, but need to be writeable immediately (otherwise they will never have all values)
|
|
699
|
+
// - However, we might eventually create read only nodes, which are just replicas of a write nodes,
|
|
700
|
+
// so we can reduce our redundant backblaze / storage throughput (it might also
|
|
701
|
+
// be faster, or allow us to run a less powerful node which only reads?)
|
|
702
|
+
public async getWriteNodes(path: string): Promise<string[]> {
|
|
703
|
+
if (this.isLocalPath(path)) return [getOwnNodeId()];
|
|
704
|
+
// IMPORTANT! Even if we are the authority, this doesn't mean other nodes aren't also the authority,
|
|
705
|
+
// so we have to let it propagate to all nodes.
|
|
706
|
+
//if (this.isSelfAuthority(path)) return [getOwnNodeId()];
|
|
707
|
+
if (isClient()) {
|
|
708
|
+
return [getBrowserUrlNode()];
|
|
709
|
+
}
|
|
710
|
+
await this.waitUntilRoutingIsReady();
|
|
711
|
+
|
|
712
|
+
let result = Array.from(this.authorities.values())
|
|
713
|
+
.filter(x => !x.onReady.rejected)
|
|
714
|
+
.filter(x => Object.values(x.authorityPaths).some(authorityPath => isInAuthority(authorityPath, path)))
|
|
715
|
+
.map(x => x.nodeId);
|
|
716
|
+
if (result.length === 0) {
|
|
717
|
+
await this.getSingleReadNodePromise(path);
|
|
718
|
+
}
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
// Length prefixes to escape parts
|
|
725
|
+
function encodeNiceArray(arr: string[], delimit = "."): string {
|
|
726
|
+
return arr.map(value => {
|
|
727
|
+
if (arr.includes(delimit) || !value) {
|
|
728
|
+
return delimit + value.length + delimit + value;
|
|
729
|
+
}
|
|
730
|
+
return value;
|
|
731
|
+
}).join(delimit);
|
|
732
|
+
}
|
|
733
|
+
function decodeNiceArray(str: string, delimit = "."): string[] {
|
|
734
|
+
let output: string[] = [];
|
|
735
|
+
let i = 0;
|
|
736
|
+
function readUntilDelimit() {
|
|
737
|
+
let value = "";
|
|
738
|
+
while (i < str.length && str[i] !== delimit) {
|
|
739
|
+
value += str[i];
|
|
740
|
+
i++;
|
|
741
|
+
}
|
|
742
|
+
if (str[i] === delimit) i++;
|
|
743
|
+
return value;
|
|
744
|
+
}
|
|
745
|
+
function readNChars(n: number) {
|
|
746
|
+
let value = str.slice(i, i + n);
|
|
747
|
+
i += n;
|
|
748
|
+
return value;
|
|
749
|
+
}
|
|
750
|
+
while (i < str.length) {
|
|
751
|
+
let part = readUntilDelimit();
|
|
752
|
+
if (!part) {
|
|
753
|
+
let length = Number(readUntilDelimit());
|
|
754
|
+
if (Number.isNaN(length) || length < 0 || length > str.length) throw new Error(`Invalid length in nice array ${length}, array ${str}`);
|
|
755
|
+
let value = readNChars(length);
|
|
756
|
+
output.push(value);
|
|
757
|
+
} else {
|
|
758
|
+
output.push(part);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return output;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Just checks the pathPrefix, applying wildcard logic */
|
|
765
|
+
function startsWithWildcard(authority: AuthorityPath, rootPath: string, childPath: string, onlyMatchChildren = false): boolean {
|
|
766
|
+
if (onlyMatchChildren && rootPath === childPath) return false;
|
|
767
|
+
if (!authority.emptyIsWildcard) return childPath.startsWith(rootPath);
|
|
768
|
+
// Eh... we might get lucky and they might both be wildcards?
|
|
769
|
+
if (childPath.startsWith(rootPath)) {
|
|
770
|
+
if (onlyMatchChildren) {
|
|
771
|
+
let rootParts = getPathFromStr(rootPath);
|
|
772
|
+
let childParts = getPathFromStr(childPath);
|
|
773
|
+
if (rootParts.length === childParts.length) return false;
|
|
774
|
+
}
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
// Because our wildcard is the shortest possible length, any match must be >= length,
|
|
778
|
+
// so if childPath is shorter... it can't match.
|
|
779
|
+
if (childPath.length < rootPath.length) return false;
|
|
780
|
+
|
|
781
|
+
let rootParts = getPathFromStr(rootPath);
|
|
782
|
+
let childParts = getPathFromStr(childPath);
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
// If we are only matching children, and it isn't a child, don't mathc
|
|
786
|
+
if (onlyMatchChildren && rootParts.length === childParts.length) return false;
|
|
787
|
+
|
|
788
|
+
for (let i = 0; i < rootParts.length; i++) {
|
|
789
|
+
if (rootParts[i] === "") continue;
|
|
790
|
+
if (rootParts[i] !== childParts[i]) return false;
|
|
791
|
+
}
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// TOOD: The routing hash (per depth) could be cached for free if we explicitly ran this in a loop. However...
|
|
796
|
+
// that would make per authority caching harder, so... I don't know how to take advantage of this.
|
|
797
|
+
const isInAuthorityBase = measureWrap(function isInAuthorityBase(authority: AuthorityPath, path: string): boolean {
|
|
798
|
+
if (!startsWithWildcard(authority, authority.pathPrefix, path)) return false;
|
|
799
|
+
if (authority.excludedChildren) {
|
|
800
|
+
if (authority.excludedChildren.some(x => startsWithWildcard(authority, x, path, true))) return false;
|
|
801
|
+
}
|
|
802
|
+
if (authority.hash) {
|
|
803
|
+
let part = getPathIndex(path, authority.hash.depth);
|
|
804
|
+
if (part === undefined) return false;
|
|
805
|
+
// If we match a wildcard, then we definitely match
|
|
806
|
+
if (part !== "") {
|
|
807
|
+
if (!(matchesParentRangeFilterPart({ part, start: authority.hash.start, end: authority.hash.end }))) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return true;
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
const isInAuthorityCache = cacheLimited(300, (authority: AuthorityPath) => {
|
|
816
|
+
return cacheLimited(10_000, (path: string) => isInAuthorityBase(authority, path));
|
|
817
|
+
});
|
|
818
|
+
export function isInAuthority(authority: AuthorityPath, path: string): boolean {
|
|
819
|
+
return isInAuthorityCache(authority)(path);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function getArchiveDirectory(authority: AuthorityPath): string {
|
|
823
|
+
let parts: string[] = [];
|
|
824
|
+
parts.push(authority.emptyIsWildcard ? "*" : "=");
|
|
825
|
+
parts.push(authority.pathPrefix);
|
|
826
|
+
if (authority.excludedChildren?.length || authority.hash) {
|
|
827
|
+
parts.push(String(authority.excludedChildren?.length ?? 0));
|
|
828
|
+
if (authority.excludedChildren) {
|
|
829
|
+
parts.push(...authority.excludedChildren);
|
|
830
|
+
}
|
|
831
|
+
if (authority.hash) {
|
|
832
|
+
parts.push(String(authority.hash.start));
|
|
833
|
+
parts.push(String(authority.hash.end));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return encodeNiceArray(parts, "-") + "/";
|
|
837
|
+
}
|
|
838
|
+
function decodeArchiveDirectory(directory: string): AuthorityPath {
|
|
839
|
+
if (!directory.includes("-")) {
|
|
840
|
+
return { pathPrefix: directory };
|
|
841
|
+
}
|
|
842
|
+
let parts = decodeNiceArray(directory, "-");
|
|
843
|
+
if (parts.length === 2) {
|
|
844
|
+
return { emptyIsWildcard: parts[0] === "*", pathPrefix: parts[1] };
|
|
845
|
+
}
|
|
846
|
+
if (parts.length < 3) throw new Error(`Invalid authority directory ${directory}, not enough parts. Expected at least 2, got ${parts.length}`);
|
|
847
|
+
let authority: AuthorityPath = {
|
|
848
|
+
emptyIsWildcard: parts.shift() === "*",
|
|
849
|
+
pathPrefix: parts.shift()!,
|
|
850
|
+
};
|
|
851
|
+
let excludedPrefixCount = Number(parts.shift());
|
|
852
|
+
if (excludedPrefixCount > 0) {
|
|
853
|
+
authority.excludedChildren = parts.splice(0, excludedPrefixCount);
|
|
854
|
+
}
|
|
855
|
+
if (parts.length > 0) {
|
|
856
|
+
authority.hash = {
|
|
857
|
+
depth: getPathDepth(authority.pathPrefix) + 1,
|
|
858
|
+
start: Number(parts.shift()),
|
|
859
|
+
end: Number(parts.shift()),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (parts.length > 0) {
|
|
863
|
+
throw new Error(`Invalid authority directory ${directory}, too many parts. Have ${parts.length} too many.`);
|
|
864
|
+
}
|
|
865
|
+
return authority;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function gcd(a: number, b: number): number {
|
|
869
|
+
if (b === 0) return a;
|
|
870
|
+
return gcd(b, a % b);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function modsOverlap(a: { mod: number, value: number }, b: { mod: number, value: number }) {
|
|
874
|
+
// Basically... if a and b are unrelated, the mod becomes 1, and so the values are equal
|
|
875
|
+
// (so they always overlap).
|
|
876
|
+
// Otherwise, we might be able to tell overlap vs not overlap. Ex, for 6 and 10. Despite not being
|
|
877
|
+
// factors of each other, they are both even, and so if we know if the mod is even the original number
|
|
878
|
+
// was even, etc. So if they differ in their value mod 2, then they don't overlap.
|
|
879
|
+
let baseMod = gcd(a.mod, b.mod);
|
|
880
|
+
let aValue = a.value % baseMod;
|
|
881
|
+
let bValue = b.value % baseMod;
|
|
882
|
+
return aValue === bValue;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Due to file max lengths the directory might not really match. It might also only partially overlap, due
|
|
886
|
+
* to range encodings.
|
|
887
|
+
* - We don't match ancestors, as AuthorityPaths never never match ancestors.
|
|
888
|
+
*/
|
|
889
|
+
function mightMatchArchiveDirectory(authority: AuthorityPath, directory: string): boolean {
|
|
890
|
+
let dirAuthority: AuthorityPath;
|
|
891
|
+
try {
|
|
892
|
+
dirAuthority = decodeArchiveDirectory(directory);
|
|
893
|
+
} catch {
|
|
894
|
+
// Hack, to support legacy databases (one we merge archives files we should be able to
|
|
895
|
+
// run merge on all to update the paths, and remove this try/catch)
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
return authoritiesMightOverlap(authority, dirAuthority);
|
|
899
|
+
}
|
|
900
|
+
function authoritiesMightOverlap(other: AuthorityPath, current: AuthorityPath): boolean {
|
|
901
|
+
|
|
902
|
+
let otherNoHash = { ...other, hashes: undefined };
|
|
903
|
+
let currentNoHash = { ...current, hashes: undefined };
|
|
904
|
+
let matchesWithoutHash = isInAuthority(otherNoHash, currentNoHash.pathPrefix) || isInAuthority(currentNoHash, otherNoHash.pathPrefix);
|
|
905
|
+
// If we ignore hash and we don't match then we definitely don't match
|
|
906
|
+
if (!matchesWithoutHash) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
// If neither have hashes, then we match (as we discarded the not match case above)
|
|
910
|
+
if (!other.hash && !current.hash) return true;
|
|
911
|
+
|
|
912
|
+
let parent = other.pathPrefix.length > current.pathPrefix.length ? current : other;
|
|
913
|
+
let child = other.pathPrefix.length > current.pathPrefix.length ? other : current;
|
|
914
|
+
// NOTE: As we match in one direction without the hash, it means our path prefixes DEFINITELY match,
|
|
915
|
+
// so we don't need to check them again.
|
|
916
|
+
if (!parent.hash && child.hash) {
|
|
917
|
+
return true;
|
|
918
|
+
} else if (parent.hash && !child.hash) {
|
|
919
|
+
return getPathDepth(child.pathPrefix) === parent.hash.depth;
|
|
920
|
+
} else {
|
|
921
|
+
// Both have hashes
|
|
922
|
+
|
|
923
|
+
// If the prefix doesn't match, we don't match
|
|
924
|
+
if (otherNoHash.pathPrefix !== currentNoHash.pathPrefix) return false;
|
|
925
|
+
|
|
926
|
+
if (otherNoHash.hashes && currentNoHash.hashes) {
|
|
927
|
+
return modsOverlap(otherNoHash.hashes, currentNoHash.hashes);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
let onReadyReady = (nodeId: string, time: number) => { };
|
|
938
|
+
class PathControllerBase {
|
|
939
|
+
public async getAuthorityPaths() {
|
|
940
|
+
return pathValueAuthority2.getSelfAuthorities();
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
public async getCreateTime() {
|
|
944
|
+
return pathValueAuthority2.createTime;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
public async isReadReady() {
|
|
948
|
+
return pathValueAuthority2.isSelfReadReady();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
public async broadcastReadReady(time: number) {
|
|
952
|
+
onReadyReady(IdentityController_getCurrentReconnectNodeIdAssert(), time);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
const PathController = SocketFunction.register(
|
|
956
|
+
"PathController-64dae2b5-c719-423a-b547-64555b26f942",
|
|
957
|
+
new PathControllerBase(),
|
|
958
|
+
() => ({
|
|
959
|
+
getAuthorityPaths: {},
|
|
960
|
+
getCreateTime: {},
|
|
961
|
+
isReadReady: {
|
|
962
|
+
// NOTE: Avoid using client hooks, so we don't change our identity, which
|
|
963
|
+
// can be overly slow when the node is likely disconnected anyways.
|
|
964
|
+
noClientHooks: true,
|
|
965
|
+
},
|
|
966
|
+
broadcastReadReady: {},
|
|
967
|
+
})
|
|
968
|
+
);
|
|
969
|
+
export async function debug_isReadReady(nodeId: string) {
|
|
970
|
+
return PathController.nodes[nodeId].isReadReady();
|
|
971
|
+
}
|
|
972
|
+
export async function debug_getAuthorityPaths(nodeId: string) {
|
|
973
|
+
return PathController.nodes[nodeId].getAuthorityPaths();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
export const pathValueAuthority2 = new NodePathAuthorities();
|
|
978
|
+
export const nodePathAuthority = pathValueAuthority2;
|