querysub 0.403.0 → 0.405.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 (108) hide show
  1. package/.cursorrules +2 -0
  2. package/bin/audit-imports.js +4 -0
  3. package/bin/join.js +1 -1
  4. package/package.json +7 -4
  5. package/spec.txt +77 -0
  6. package/src/-a-archives/archiveCache.ts +9 -4
  7. package/src/-a-archives/archivesBackBlaze.ts +1039 -1039
  8. package/src/-a-auth/certs.ts +0 -12
  9. package/src/-c-identity/IdentityController.ts +12 -3
  10. package/src/-f-node-discovery/NodeDiscovery.ts +32 -26
  11. package/src/-g-core-values/NodeCapabilities.ts +12 -2
  12. package/src/0-path-value-core/AuthorityLookup.ts +239 -0
  13. package/src/0-path-value-core/LockWatcher2.ts +150 -0
  14. package/src/0-path-value-core/PathRouter.ts +543 -0
  15. package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
  16. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +73 -0
  17. package/src/0-path-value-core/PathValueCommitter.ts +222 -488
  18. package/src/0-path-value-core/PathValueController.ts +277 -239
  19. package/src/0-path-value-core/PathWatcher.ts +534 -0
  20. package/src/0-path-value-core/ShardPrefixes.ts +31 -0
  21. package/src/0-path-value-core/ValidStateComputer.ts +303 -0
  22. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +1 -1
  23. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +80 -44
  24. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +13 -16
  25. package/src/0-path-value-core/auditLogs.ts +2 -0
  26. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +97 -0
  27. package/src/0-path-value-core/pathValueArchives.ts +491 -492
  28. package/src/0-path-value-core/pathValueCore.ts +195 -1496
  29. package/src/0-path-value-core/startupAuthority.ts +74 -0
  30. package/src/1-path-client/RemoteWatcher.ts +90 -82
  31. package/src/1-path-client/pathValueClientWatcher.ts +808 -815
  32. package/src/2-proxy/PathValueProxyWatcher.ts +10 -8
  33. package/src/2-proxy/archiveMoveHarness.ts +182 -214
  34. package/src/2-proxy/garbageCollection.ts +9 -8
  35. package/src/2-proxy/schema2.ts +21 -1
  36. package/src/3-path-functions/PathFunctionHelpers.ts +206 -180
  37. package/src/3-path-functions/PathFunctionRunner.ts +943 -766
  38. package/src/3-path-functions/PathFunctionRunnerMain.ts +5 -3
  39. package/src/3-path-functions/pathFunctionLoader.ts +2 -2
  40. package/src/3-path-functions/syncSchema.ts +596 -521
  41. package/src/4-deploy/deployFunctions.ts +19 -4
  42. package/src/4-deploy/deployGetFunctionsInner.ts +8 -2
  43. package/src/4-deploy/deployMain.ts +51 -68
  44. package/src/4-deploy/edgeClientWatcher.tsx +6 -1
  45. package/src/4-deploy/edgeNodes.ts +2 -2
  46. package/src/4-dom/qreact.tsx +2 -4
  47. package/src/4-dom/qreactTest.tsx +7 -13
  48. package/src/4-querysub/Querysub.ts +21 -8
  49. package/src/4-querysub/QuerysubController.ts +45 -29
  50. package/src/4-querysub/permissions.ts +2 -2
  51. package/src/4-querysub/querysubPrediction.ts +80 -70
  52. package/src/4-querysub/schemaHelpers.ts +5 -1
  53. package/src/5-diagnostics/GenericFormat.tsx +14 -9
  54. package/src/archiveapps/archiveGCEntry.tsx +9 -2
  55. package/src/archiveapps/archiveJoinEntry.ts +96 -84
  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 +65 -0
  102. package/test.ts +298 -97
  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/archiveMergeEntry.tsx +0 -48
  108. package/src/archiveapps/lockTest.ts +0 -127
@@ -88,18 +88,6 @@ export function createX509(
88
88
 
89
89
  let localHostDomain = "127-0-0-1." + domain.split(".").slice(-2).join(".");
90
90
 
91
- //todonext
92
- // Wait, why is one of our alt names not the machine URL that includes the machine hash? Oh, I guess it can't be because we don't know the hash until we create the cert, but we kind of do. Or we can at least.
93
- // I guess the real question is why was this not an issue before? Were we adding it before and then we stopped adding it? Were we adding the thread certificate before to trust?
94
- // I think it's fine to change this behavior, although it is kind of annoying to get the public key here (I think we have to derive it from the private key. What a nightmare.
95
- // Maybe we had just an issue where we weren't correctly verifying the certificate? No, but that doesn't make any sense because Node.js is verifying the certificate.
96
- //todonext
97
- // Our trust store hasn't changed, it's just our machine ID that's changed. And even if it has changed, that's fine. But anyway, we should check our trust store to see what the old certificate looked like.
98
- //todonext;
99
- // Huh, the certificates did used to have the machine ID in them. And actually the domain I'm looking at, I'm pretty sure we were adding, that doesn't make any sense, the thread IDs? I don't know, maybe it was just totally borked before, maybe explain some of the issues we were having. It could be why sometimes startup failed. Maybe what happened was it only works if... When a node starts, it tells all other nodes that it changed the trust cache. And in that way, if they start in order, even though they keep breaking the trust cache by clobbering the machine CA certificate, We end up with all the certificates.
100
- // I do really like the Node.js error. It's extremely specific. It even gives us the full cert alt names. Very good error. Without this error it would be extremely difficult to debug this.
101
-
102
-
103
91
  extensions.push(...[
104
92
  { name: "keyUsage", keyCertSign: isCA, digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true },
105
93
  { name: "subjectKeyIdentifier" },
@@ -9,7 +9,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
9
9
  import { CallerContext } from "socket-function/SocketFunctionTypes";
10
10
  import { cache, cacheWeak, lazy } from "socket-function/src/caching";
11
11
  import { getClientNodeId, getNodeId, getNodeIdDomain, getNodeIdIP, getNodeIdLocation, isClientNodeId } from "socket-function/src/nodeCache";
12
- import { decodeNodeId, getCommonName, getIdentityCA, getMachineId, getPublicIdentifier, getThreadKeyCert, parseCert, sign, validateCertificate, verify } from "../-a-auth/certs";
12
+ import { decodeNodeId, getCommonName, getIdentityCA, getMachineId, getOwnMachineId, getPublicIdentifier, getThreadKeyCert, parseCert, sign, validateCertificate, verify } from "../-a-auth/certs";
13
13
  import { getShortNumber } from "../bits";
14
14
  import { measureBlock, measureFnc, measureWrap } from "socket-function/src/profiling/measure";
15
15
  import { timeoutToError } from "../errors";
@@ -18,7 +18,7 @@ import { formatTime } from "socket-function/src/formatting/format";
18
18
  import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
19
19
  import { red } from "socket-function/src/formatting/logColors";
20
20
  import { isNode } from "typesafecss";
21
- import { getOwnThreadId } from "../-f-node-discovery/NodeDiscovery";
21
+ import { areNodeIdsEqual, getOwnThreadId } from "../-f-node-discovery/NodeDiscovery";
22
22
 
23
23
  let callerInfo = new Map<CallerContext, {
24
24
  reconnectNodeId: string | undefined;
@@ -137,9 +137,18 @@ class IdentityControllerBase {
137
137
  // (We don't have to worry about other servers on the same domain, as all servers
138
138
  // on the same domain should be the same!)
139
139
  let localNodeId = caller.localNodeId;
140
- if (payload.serverId !== localNodeId) {
140
+ if (!areNodeIdsEqual(payload.serverId, localNodeId)) {
141
141
  throw new Error(`Identity is for another server! The connection is calling us ${localNodeId}, but signature is for ${payload.serverId}`);
142
142
  }
143
+ let calledMachineId = getMachineId(payload.serverId);
144
+ if (calledMachineId !== "127-0-0-1" && calledMachineId !== getOwnMachineId()) {
145
+ throw new Error(`Tried to call a different machine. We are ${getOwnMachineId()}, they called ${calledMachineId}`);
146
+ }
147
+ let calledThreadId = decodeNodeId(payload.serverId)?.threadId;
148
+ if (calledThreadId && calledThreadId !== "127-0-0-1" && calledThreadId !== getOwnThreadId()) {
149
+ throw new Error(`Tried to call a different thread. We are ${getOwnThreadId()}, they called ${calledThreadId}`);
150
+ }
151
+
143
152
 
144
153
  // Verify the caller can sign as the cert
145
154
  verify(payload.cert, signature, payload);
@@ -6,7 +6,7 @@ import { isNode, sha256Hash, throttleFunction, timeInMinute, timeInSecond } from
6
6
  import { errorToUndefinedSilent, ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../errors";
7
7
  import { ensureWeAreTrusted, requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
8
8
  import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
9
- import { getNodeId, getNodeIdFromLocation } from "socket-function/src/nodeCache";
9
+ import { getNodeId, getNodeIdFromLocation, getNodeIdLocation } from "socket-function/src/nodeCache";
10
10
  import { lazy } from "socket-function/src/caching";
11
11
  import { shuffle } from "../misc/random";
12
12
  import { blue, green, magenta, red, yellow } from "socket-function/src/formatting/logColors";
@@ -21,6 +21,7 @@ import { getBootedEdgeNode } from "../-0-hooks/hooks";
21
21
  import { EdgeNodeConfig } from "../4-deploy/edgeNodes";
22
22
  import * as certs from "../-a-auth/certs";
23
23
  import { logDisk } from "../diagnostics/logs/diskLogger";
24
+ import { MaybePromise } from "socket-function/src/types";
24
25
 
25
26
  let HEARTBEAT_INTERVAL = timeInMinute * 15;
26
27
  // Interval which we check other heartbeats
@@ -64,6 +65,21 @@ export function isNodeDiscoveryLogging() {
64
65
  return logging;
65
66
  }
66
67
 
68
+
69
+ function getAlternateNodeIds(nodeId: string): MaybePromise<string[] | undefined> {
70
+ let machineId = certs.getMachineId(nodeId);
71
+ if (machineId === getOwnMachineId()) {
72
+ let decoded = decodeNodeId(nodeId);
73
+ if (decoded) {
74
+ return [
75
+ "127-0-0-1." + decoded.domain + ":" + decoded.port
76
+ ];
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+ SocketFunction.GET_ALTERNATE_NODE_IDS = getAlternateNodeIds;
82
+
67
83
  export const getOurNodeId = getOwnNodeId;
68
84
  export const getOurNodeIdAssert = getOwnNodeIdAssert;
69
85
 
@@ -110,14 +126,12 @@ export function isOwnNodeId(nodeId: string): boolean {
110
126
  }
111
127
 
112
128
  export function isNodeIdOnOwnMachineId(nodeId: string): boolean {
113
- return certs.getMachineId(nodeId) === getOwnMachineId() || nodeId.startsWith("127-0-0-1.");
129
+ return certs.getMachineId(nodeId) === getOwnMachineId() || decodeNodeId(nodeId)?.machineId === "127-0-0-1";
114
130
  }
115
131
 
116
- /** Often, when getting all nodes, you're going to receive duplicates of all local nodes with the duplicate containing this prefix. Using the local ID is faster as it will actually use the local IP.
117
- TODO: We need a robust way to give a deduplicated set that prefers the local ids. At the moment we just filter out the id locals if we can't have duplicates.
118
- */
119
- export function isNodeIdLocal(nodeId: string): boolean {
120
- return nodeId.startsWith("127-0-0-1.");
132
+ export function areNodeIdsEqual(lhs: string, rhs: string): boolean {
133
+ if (lhs === rhs) return true;
134
+ return isNodeIdOnOwnMachineId(lhs) && isNodeIdOnOwnMachineId(rhs) && getNodeIdLocation(rhs)?.port === getNodeIdLocation(lhs)?.port;
121
135
  }
122
136
 
123
137
  let nodeOverrides: string[] | undefined;
@@ -173,13 +187,6 @@ function getAllNodesHash() {
173
187
  }
174
188
  function addNodeId(nodeId: string) {
175
189
  addNodeIdBase(nodeId);
176
- if (isNode() && isDevDebugbreak()) {
177
- let obj = decodeNodeId(nodeId);
178
- if (obj) {
179
- let localNodeId = getNodeId("127-0-0-1." + getDomain(), obj.port);
180
- addNodeIdBase(localNodeId);
181
- }
182
- }
183
190
  }
184
191
  function addNodeIdBase(nodeId: string) {
185
192
  if (allNodeIds2.has(nodeId)) return;
@@ -193,15 +200,6 @@ function setNodeIds(nodeIds: string[]) {
193
200
  nodeIds = nodeIds.filter(x => x !== SPECIAL_NODE_ID_FOR_UNMOUNTED_NODE);
194
201
 
195
202
  console.info("setNodeIds", { nodeIds });
196
- // Also try all localhost ports, if we are developing and not in public mode
197
- if (isNode() && !isPublic() && isDevDebugbreak()) {
198
- let ports = new Set(nodeIds.map(nodeId => decodeNodeId(nodeId)?.port).filter(isDefined));
199
- for (let port of ports) {
200
- let localNodeId = getNodeId("127-0-0-1." + getDomain(), port);
201
- nodeIds.push(localNodeId);
202
- }
203
- nodeIds = Array.from(new Set(nodeIds));
204
- }
205
203
  let newNodeIds = nodeIds.filter(nodeId => !allNodeIds2.has(nodeId));
206
204
  let newIds = new Set(nodeIds);
207
205
  let removedNodeIds = Array.from(allNodeIds2).filter(nodeId => !newIds.has(nodeId));
@@ -423,7 +421,7 @@ async function writeHeartbeat() {
423
421
  await archives().set(nodeId, Buffer.from(now + ""));
424
422
  }
425
423
 
426
- async function runMainSyncLoops(discoveryReady: PromiseObj<void>) {
424
+ async function runMainSyncLoops() {
427
425
  await syncArchives();
428
426
 
429
427
  discoveryReady.resolve();
@@ -446,6 +444,8 @@ async function runMainSyncLoops(discoveryReady: PromiseObj<void>) {
446
444
  await timeoutToUndefinedSilent(timeInSecond * 5, errorToUndefinedSilent(NodeDiscoveryController.nodes[nodeId].addNode(getOwnNodeId())));
447
445
  }));
448
446
 
447
+ nodeBroadcasted.resolve();
448
+
449
449
  console.log(magenta(`Node discovery is loaded`));
450
450
 
451
451
  await runInfinitePollCallAtStart(HEARTBEAT_INTERVAL, async function nodeDiscoverHeartbeat() {
@@ -477,6 +477,7 @@ async function runMainSyncLoops(discoveryReady: PromiseObj<void>) {
477
477
  }
478
478
 
479
479
  let discoveryReady = new PromiseObj<void>();
480
+ let nodeBroadcasted = new PromiseObj<void>();
480
481
  beforeGetNodeAllId = async () => {
481
482
  await discoveryReady.promise;
482
483
  };
@@ -484,6 +485,9 @@ export async function onNodeDiscoveryReady() {
484
485
  await getAllNodeIds();
485
486
  }
486
487
 
488
+ export async function onNodeBroadcasted() {
489
+ await nodeBroadcasted.promise;
490
+ }
487
491
  if (isServer()) {
488
492
  setImmediate(async () => {
489
493
 
@@ -491,7 +495,7 @@ if (isServer()) {
491
495
  logErrors(runMemoryAuditLoop());
492
496
  // NOTE: We used to wait until we mounted, but... we should be able to find nodes
493
497
  // before we mount, right? (And what if we never mount?)
494
- runMainSyncLoops(discoveryReady).catch(e => {
498
+ runMainSyncLoops().catch(e => {
495
499
  discoveryReady.reject(e);
496
500
  logErrors(Promise.reject(e));
497
501
  });
@@ -500,6 +504,7 @@ if (isServer()) {
500
504
 
501
505
  if (isNode()) {
502
506
  discoveryReady.resolve();
507
+ nodeBroadcasted.resolve();
503
508
  // Just get the archives, syncing again if we haven't synced in a while
504
509
  let lastGetTime = 0;
505
510
  beforeGetNodeAllId = async () => {
@@ -518,6 +523,7 @@ if (isServer()) {
518
523
  let nodes = [edgeNode.host];
519
524
  allNodeIds2 = new Set(nodes);
520
525
  discoveryReady.resolve();
526
+ nodeBroadcasted.resolve();
521
527
 
522
528
  // NOTE: We run into TLS issues (as in, our servers use self signed certs), if we try to talk to just
523
529
  // any node, so... we better just talk to the edge node
@@ -570,7 +576,7 @@ class NodeDiscoveryControllerBase {
570
576
  }
571
577
 
572
578
  public async getAllNodeIds(): Promise<string[]> {
573
- return Array.from(allNodeIds2).filter(x => !x.startsWith("127-0-0-1."));
579
+ return Array.from(allNodeIds2);
574
580
  }
575
581
  public async getNodeId() {
576
582
  return SocketFunction.mountedNodeId;
@@ -5,7 +5,7 @@
5
5
  import { SocketFunction } from "socket-function/SocketFunction";
6
6
  import { SocketRegistered } from "socket-function/SocketFunctionTypes";
7
7
  import { errorToUndefined, errorToUndefinedSilent, timeoutToUndefinedSilent } from "../errors";
8
- import { getAllNodeIds } from "../-f-node-discovery/NodeDiscovery";
8
+ import { getAllNodeIds, isNodeIdOnOwnMachineId } from "../-f-node-discovery/NodeDiscovery";
9
9
  import { green, red, yellow } from "socket-function/src/formatting/logColors";
10
10
  import { shuffle } from "../misc/random";
11
11
  import { delay } from "socket-function/src/batching";
@@ -20,6 +20,7 @@ import { getOwnMachineId, decodeNodeId, decodeNodeIdAssert, getMachineId } from
20
20
  import { sort } from "socket-function/src/misc";
21
21
  import { getPathStr2 } from "../path";
22
22
  import { PromiseObj } from "../promise";
23
+ import { getTrueTimeOffset } from "socket-function/time/trueTimeShim";
23
24
  setImmediate(() => {
24
25
  import("../diagnostics/MachineThreadInfo");
25
26
  });
@@ -103,7 +104,7 @@ export async function getControllerNodeIdList(
103
104
 
104
105
  let results = Array.from(passedNodeIds.entries());
105
106
  // Prefer localhost connections as they're faster.
106
- sort(results, (x) => x[0].startsWith("127-0-0-1.") ? 0 : 1);
107
+ sort(results, (x) => isNodeIdOnOwnMachineId(x[0]) ? 0 : 1);
107
108
  let lookup = new Map<string, { nodeId: string; entryPoint: string }>();
108
109
  for (let x of results) {
109
110
  let key = getPathStr2(x[1].machineId, decodeNodeIdAssert(x[0]).port.toString());
@@ -142,6 +143,10 @@ class NodeCapabilitiesControllerBase {
142
143
  return getFunctionRunnerShards();
143
144
  }
144
145
 
146
+ public async getTrueTimeOffset() {
147
+ return getTrueTimeOffset();
148
+ }
149
+
145
150
  public async getInspectURL() {
146
151
  return await getDebuggerUrl();
147
152
  }
@@ -185,7 +190,12 @@ export const NodeCapabilitiesController = SocketFunction.register(
185
190
  getStartupTime: {},
186
191
  getMemoryUsage: {},
187
192
  getFunctionRunnerShards: {},
193
+ getTrueTimeOffset: {},
188
194
  getInspectURL: { hooks: [requiresNetworkTrustHook] },
189
195
  exposeExternalDebugPortOnce: { hooks: [requiresNetworkTrustHook] },
196
+ }),
197
+ () => ({
198
+ // I think assert is management user didn't exist when we wrote these functions, and so we just made them public. But now that it does exist, we might as well use it. Servers will count as management users anyway, and I don't see any reason a non-management user or a non-server needs to access any of these functions.
199
+ hooks: [(require("../diagnostics/managementPages") as typeof import("../diagnostics/managementPages")).assertIsManagementUser],
190
200
  })
191
201
  );
@@ -0,0 +1,239 @@
1
+ import { timeInMinute, timeInSecond } from "socket-function/src/misc";
2
+ import { nestArchives } from "../-a-archives/archives";
3
+ import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
4
+ import { archiveJSONT } from "../-a-archives/archivesJSONT";
5
+ import { getDomain, isPublic } from "../config";
6
+ import { cache, lazy } from "socket-function/src/caching";
7
+ import { SocketFunction } from "socket-function/SocketFunction";
8
+ import { runInSerial, runInfinitePollCallAtStart } from "socket-function/src/batching";
9
+ import { getAllNodeIds, getOwnNodeId, isOwnNodeId, onNodeBroadcasted, syncNodesNow, watchDeltaNodeIds, watchNodeIds } from "../-f-node-discovery/NodeDiscovery";
10
+ import { IdentityController_getCurrentReconnectNodeIdAssert } from "../-c-identity/IdentityController";
11
+ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
12
+ import { isClient } from "../config2";
13
+ import { getPathStr1 } from "../path";
14
+ import { timeoutToError } from "../errors";
15
+ import { AuthoritySpec } from "./PathRouter";
16
+ import { formatTime } from "socket-function/src/formatting/format";
17
+ import { getAllAuthoritySpec, getEmptyAuthoritySpec } from "./PathRouterServerAuthoritySpec";
18
+
19
+
20
+ let NETWORK_POLL_INTERVAL = timeInMinute * 5;
21
+ let CALL_TIMEOUT = isPublic() ? timeInSecond * 20 : timeInSecond * 3;
22
+
23
+ export type AuthorityEntry = {
24
+ nodeId: string;
25
+ authoritySpec: AuthoritySpec;
26
+ entryReceivedTime: number;
27
+ isReady: boolean;
28
+ };
29
+ type AuthorityTopology = {
30
+ // nodeId =>
31
+ nodes: Map<string, AuthorityEntry>;
32
+ };
33
+
34
+ class AuthorityLookup {
35
+ private topology: AuthorityTopology = {
36
+ nodes: new Map(),
37
+ };
38
+ private ourSpec: AuthoritySpec = getEmptyAuthoritySpec();
39
+ private ourIsReady = false;
40
+ private didInitialSync = false;
41
+
42
+
43
+ public async getTopology() {
44
+ await this.startSyncing();
45
+ return Array.from(this.topology.nodes.values());
46
+ }
47
+
48
+ public getTopologySync() {
49
+ if (!this.didInitialSync) throw new Error("Cannot call getTopologySync without calling syncAllNow at some point first.");
50
+ return Array.from(this.topology.nodes.values()).filter(x => x.isReady);
51
+ }
52
+ public getAuthoritySpecForNodeId(nodeId: string): AuthoritySpec | undefined {
53
+ if (!this.didInitialSync) throw new Error("Cannot call getAuthoritySpecForNodeId without calling syncAllNow at some point first.");
54
+ return this.topology.nodes.get(nodeId)?.authoritySpec;
55
+ }
56
+
57
+ public getOurSpec() {
58
+ this.ourSpec.nodeId = getOwnNodeId();
59
+ return this.ourSpec;
60
+ }
61
+
62
+ private setSpec = false;
63
+ public async setOurSpec(spec: AuthoritySpec) {
64
+ if (!SocketFunction.isMounted()) throw new Error("Cannot call setOurPaths without mounting first (use Querysub.hostService).");
65
+ spec.nodeId = getOwnNodeId();
66
+
67
+
68
+ if (this.setSpec) {
69
+ throw new Error("You cannot redefine what authority paths you have. If you remove any authority paths, then you will no longer receive new value rights, but your watchers won't know, and so they will become broken. IF You are only adding paths, this might allowed, and we need to update this code to check for that (you will still need to make sure you synchronize all the values first, etc).");
70
+ }
71
+ this.setSpec = true;
72
+
73
+ this.ourSpec = spec;
74
+ this.updatePaths(getOwnNodeId(), spec, false);
75
+
76
+ await this.syncAllNow();
77
+ }
78
+ public async setIsReady() {
79
+ if (!this.setSpec) throw new Error("Cannot call setIsReady without calling setOurPaths first.");
80
+ this.ourIsReady = true;
81
+ this.updatePaths(getOwnNodeId(), this.ourSpec, true);
82
+ await this.syncAllNow();
83
+ }
84
+
85
+ public async syncAllNow() {
86
+ if (!this.didInitialSync) {
87
+ await this.startSyncing();
88
+ return;
89
+ }
90
+ let time = Date.now();
91
+ console.log(`Syncing all nodes now`);
92
+ // Sync again. There are some important points when there could be races, and so syncing everything now should help eliminate that.
93
+ await syncNodesNow();
94
+ let allNodeIds = await getAllNodeIds();
95
+ await Promise.all(allNodeIds.map(async nodeId => {
96
+ if (isOwnNodeId(nodeId)) return;
97
+ await this.syncNodeSerial(nodeId)();
98
+ }));
99
+ console.log(`Finished syncing all nodes now, took ${formatTime(Date.now() - time)}`);
100
+ }
101
+
102
+
103
+ public startSyncing = lazy(async () => {
104
+ await onNodeBroadcasted();
105
+ let firstSync = true;
106
+ let promises: Promise<void>[] = [];
107
+ watchDeltaNodeIds(({ newNodeIds, removedNodeIds }) => {
108
+ for (let nodeId of newNodeIds) {
109
+ try {
110
+ let { promise } = this.connectTo(nodeId);
111
+ if (firstSync) {
112
+ promises.push(promise);
113
+ }
114
+ } catch { }
115
+ }
116
+ firstSync = false;
117
+ for (let nodeId of removedNodeIds) {
118
+ this.connectTo(nodeId).onNodeRemoved();
119
+ }
120
+ });
121
+ await Promise.all(promises);
122
+ this.didInitialSync = true;
123
+ });
124
+
125
+
126
+ private updatePaths(nodeId: string, spec: AuthoritySpec, isReady: boolean) {
127
+ let entry: AuthorityEntry = {
128
+ nodeId,
129
+ authoritySpec: spec,
130
+ entryReceivedTime: Date.now(),
131
+ isReady,
132
+ };
133
+ let prevEntry = this.topology.nodes.get(nodeId);
134
+ // Try to preserve === property where possible, so we can use it as a cache key
135
+ if (prevEntry && JSON.stringify(prevEntry.authoritySpec) === JSON.stringify(spec)) {
136
+ entry.authoritySpec = prevEntry.authoritySpec;
137
+ }
138
+ this.topology.nodes.set(nodeId, entry);
139
+ }
140
+
141
+ public async networkSyncPaths(otherSpec: AuthoritySpec, isReady: boolean): Promise<{
142
+ spec: AuthoritySpec;
143
+ isReady: boolean;
144
+ }> {
145
+ let nodeId = IdentityController_getCurrentReconnectNodeIdAssert();
146
+ this.updatePaths(nodeId, otherSpec, isReady);
147
+ console.info(`Received network sync, returning our data`, {
148
+ nodeId,
149
+ otherSpec,
150
+ isReady,
151
+ ourSpec: this.ourSpec,
152
+ ourIsReady: this.ourIsReady,
153
+ });
154
+ return {
155
+ spec: this.ourSpec,
156
+ isReady: this.ourIsReady,
157
+ };
158
+ }
159
+
160
+
161
+
162
+
163
+ private connectTo = cache((nodeId: string) => {
164
+ let stopObj = { stop: false };
165
+ let promise = runInfinitePollCallAtStart(NETWORK_POLL_INTERVAL, async () => {
166
+ await this.syncNodeSerial(nodeId)();
167
+ }, stopObj);
168
+
169
+ const onNodeRemoved = () => {
170
+ this.topology.nodes.delete(nodeId);
171
+ stopObj.stop = true;
172
+ this.connectTo.clear(nodeId);
173
+ this.disconnectWatch.clear(nodeId);
174
+ };
175
+ return { onNodeRemoved, promise };
176
+ });
177
+
178
+
179
+ private syncNodeSerial = cache((nodeId: string) => {
180
+ return runInSerial(async () => {
181
+ await this.syncNodeNow(nodeId);
182
+ });
183
+ });
184
+ private disconnectWatch = cache((nodeId: string) => {
185
+ let onDisconnect = () => {
186
+ SocketFunction.onNextDisconnect(nodeId, onDisconnect);
187
+ this.topology.nodes.delete(nodeId);
188
+ };
189
+ SocketFunction.onNextDisconnect(nodeId, onDisconnect);
190
+ });
191
+ private async syncNodeNow(nodeId: string) {
192
+ try {
193
+ await this.syncNodeNowBase(nodeId);
194
+ } catch {
195
+ this.topology.nodes.delete(nodeId);
196
+ }
197
+ }
198
+
199
+
200
+ private async syncNodeNowBase(nodeId: string) {
201
+ if (isClient()) {
202
+ // Doesn't matter what the node ID is, we should really only be connecting to the browser node ID, Which will have all the data for the current domain.
203
+ // - Get all node IDs should restrict our nodes to just the browser node ID. If we ever change this, then either it's redundant nodes and they all have all the same data, or we need to figure out what data they have, And as their proxies, it probably won't be their actual authority data. So that will require new API functions, etc.
204
+ this.updatePaths(nodeId, {
205
+ ...getAllAuthoritySpec(),
206
+ nodeId: nodeId,
207
+ }, true);
208
+ return;
209
+ }
210
+
211
+ let otherPaths = await timeoutToError(
212
+ CALL_TIMEOUT,
213
+ AuthorityLookupController.nodes[nodeId].networkSyncPaths(this.ourSpec, this.ourIsReady),
214
+ () => new Error(`Timeout calling networkSyncPaths for ${nodeId}`)
215
+ );
216
+ console.info(`Finished network sync`, {
217
+ nodeId,
218
+ otherSpec: otherPaths.spec,
219
+ isReady: otherPaths.isReady,
220
+ ourSpec: this.ourSpec,
221
+ ourIsReady: this.ourIsReady,
222
+ });
223
+ this.updatePaths(nodeId, otherPaths.spec, otherPaths.isReady);
224
+ this.disconnectWatch(nodeId);
225
+ }
226
+ }
227
+ export const authorityLookup = new AuthorityLookup();
228
+ (globalThis as any).authorityLookup = authorityLookup;
229
+
230
+ const AuthorityLookupController = SocketFunction.register(
231
+ "AuthorityLookup-019d1bbd-e7b9-719b-8fd1-fe06c7e5b115",
232
+ authorityLookup,
233
+ () => ({
234
+ networkSyncPaths: {},
235
+ }),
236
+ () => ({
237
+ hooks: [requiresNetworkTrustHook],
238
+ })
239
+ );
@@ -0,0 +1,150 @@
1
+ import { runInfinitePoll } from "socket-function/src/batching";
2
+ import { lazy } from "socket-function/src/caching";
3
+ import { binarySearchIndex } from "socket-function/src/misc";
4
+ import { registerResource } from "../diagnostics/trackResources";
5
+ import { MAX_CHANGE_AGE, Time, PathValue, byLockGroup, compareTime } from "./pathValueCore";
6
+ import { PathRouter } from "./PathRouter";
7
+ import { getInternalValueWatchPrefix, pathWatcher } from "./PathWatcher";
8
+
9
+ function isGoldenLock(time: number, now: number): boolean {
10
+ return time < now - MAX_CHANGE_AGE * 2;
11
+ }
12
+
13
+ function getLockWatcher2NodeId() {
14
+ return getInternalValueWatchPrefix() + "_LockWatcher2";
15
+ }
16
+
17
+ /**
18
+ * Anything over a certain age gets removed as we assume at that point the valid state won't change, and so we don't need to keep track of the watchers.
19
+ */
20
+ class LockWatcher2 {
21
+
22
+ private remoteWatches = registerResource("paths|remoteWatches", new Map<string, {
23
+ newestTime: number;
24
+ }>());
25
+
26
+ // path => sorted by endTime
27
+ private validRangeWatchers = registerResource("paths|validRangeWatchers", new Map<string, {
28
+ startTime: Time;
29
+ endTime: Time;
30
+ watchers: PathValue[];
31
+ }[]>());
32
+
33
+
34
+
35
+ private watchRemotePath(path: string, newestTime: number) {
36
+ let watch = this.remoteWatches.get(path);
37
+ if (!watch) {
38
+ watch = {
39
+ newestTime: 0,
40
+ };
41
+ this.remoteWatches.set(path, watch);
42
+ // So do we do deduping on the watchers? I think we do. We have to do our own little watch unwatch thing.
43
+ pathWatcher.watchPath({
44
+ nodeId: getLockWatcher2NodeId(),
45
+ paths: [path],
46
+ parentPaths: [],
47
+ // NOTE: Receiving the full history is inefficient, however, it doesn't matter. The history gets compressed fairly aggressively, So, there won't be many values. Also, this makes our keeping track of what we're watching simpler. So, it might be actually more efficient overall.
48
+ fullHistory: true,
49
+ noInitialTrigger: true,
50
+ });
51
+ }
52
+ if (newestTime > watch.newestTime) {
53
+ watch.newestTime = newestTime;
54
+ }
55
+ }
56
+
57
+
58
+ /** Tracks a lookup so that you can find if any path value will change the valid state of another path value.
59
+ * - ALSO, for any value which is held remotely, watches its locks.
60
+ */
61
+ public watchValueLocks(values: PathValue[], now: number) {
62
+ values = values.filter(x => PathRouter.isSelfAuthority(x.path));
63
+ if (values.length === 0) return;
64
+ this.ensureGarbageCollectLoop();
65
+ let lockGroups = byLockGroup(values);
66
+ for (let [locks, valueGroup] of lockGroups) {
67
+ locks = locks.filter(x => !isGoldenLock(x.endTime.time, now));
68
+ if (locks.length === 0) continue;
69
+ for (let lock of locks) {
70
+ if (!PathRouter.isSelfAuthority(lock.path)) {
71
+ this.watchRemotePath(lock.path, lock.endTime.time);
72
+ }
73
+ let rangeWatchers = this.validRangeWatchers.get(lock.path);
74
+ if (!rangeWatchers) {
75
+ rangeWatchers = [];
76
+ this.validRangeWatchers.set(lock.path, rangeWatchers);
77
+ }
78
+ let list = rangeWatchers;
79
+ let index = binarySearchIndex(list.length, i => compareTime(list[i].endTime, lock.endTime));
80
+ if (index < 0) index = ~index;
81
+ list.splice(index, 0, {
82
+ startTime: lock.startTime,
83
+ endTime: lock.endTime,
84
+ watchers: valueGroup,
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ /** Returns all the path values that might have their valid state changed by this change. If this change is an accepted value, then it's anything that's within the locking region for any of the read locks, or the read lock itself (presumably, your reads exist before you, so if they change, it probably isn't them being initially created, it's probably them being rejected, which will cause you to be rejected.)
91
+ */
92
+ public getValuePathWatchers(config: {
93
+ path: string;
94
+ time: Time;
95
+ }): PathValue[] {
96
+ let result: PathValue[][] = [];
97
+
98
+ let rangeWatchers = this.validRangeWatchers.get(config.path);
99
+ if (rangeWatchers && rangeWatchers.length > 0) {
100
+ let list = rangeWatchers;
101
+ let index = binarySearchIndex(list.length, i => compareTime(list[i].endTime, config.time));
102
+ if (index < 0) index = ~index;
103
+ // Anything with an endTime < our time definitely won't overlap us.
104
+ for (let i = index; i < rangeWatchers.length; i++) {
105
+ let watcher = rangeWatchers[i];
106
+ if (compareTime(watcher.startTime, config.time) <= 0) {
107
+ result.push(watcher.watchers);
108
+ }
109
+ }
110
+ }
111
+
112
+ return result.flat();
113
+ }
114
+
115
+ private ensureGarbageCollectLoop = lazy(() => {
116
+ runInfinitePoll(MAX_CHANGE_AGE, () => this.garbageCollectOldWatchers());
117
+ });
118
+ private garbageCollectOldWatchers() {
119
+ let threshold = Date.now() - MAX_CHANGE_AGE * 2;
120
+
121
+ // 1) Look through remote watches, and any which are too old, we can actually unwatch their paths with Pathwatcher.
122
+ let pathsToUnwatch: string[] = [];
123
+ for (let [path, watch] of this.remoteWatches) {
124
+ if (watch.newestTime < threshold) {
125
+ pathsToUnwatch.push(path);
126
+ this.remoteWatches.delete(path);
127
+ }
128
+ }
129
+ if (pathsToUnwatch.length > 0) {
130
+ pathWatcher.unwatchPath({
131
+ paths: pathsToUnwatch,
132
+ parentPaths: [],
133
+ callback: getLockWatcher2NodeId(),
134
+ });
135
+ }
136
+
137
+ // 2) Look through validRangeWatchers, and remove any with an endTime that are too old
138
+ for (let [path, ranges] of this.validRangeWatchers) {
139
+ let index = binarySearchIndex(ranges.length, i => ranges[i].endTime.time - threshold);
140
+ if (index < 0) index = ~index;
141
+ if (index > 0) {
142
+ ranges.splice(0, index);
143
+ }
144
+ if (ranges.length === 0) {
145
+ this.validRangeWatchers.delete(path);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ export const lockWatcher2 = new LockWatcher2();