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.
Files changed (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. 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;