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