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
@@ -0,0 +1,534 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { cache } from "socket-function/src/caching";
3
+ import { QueueLimited, keyByArray } from "socket-function/src/misc";
4
+ import { measureFnc } from "socket-function/src/profiling/measure";
5
+ import { debugNodeThread } from "../-c-identity/IdentityController";
6
+ import { isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
7
+ import { registerResource } from "../diagnostics/trackResources";
8
+ import { ignoreErrors } from "../errors";
9
+ import { hack_stripPackedPath, getParentPathStr } from "../path";
10
+ import { PathValueControllerBase } from "./PathValueController";
11
+ import { PathRouter } from "./PathRouter";
12
+ import { auditLog } from "./auditLogs";
13
+ import { WatchConfig, authorityStorage, PathValue, NodeId, compareTime, isCoreQuiet, MAX_CHANGE_AGE, createMissingEpochValue } from "./pathValueCore";
14
+ import { decodeParentFilter, matchesParentRangeFilter } from "./hackedPackedPathParentFiltering";
15
+
16
+
17
+ // These are used so we can have our internal trigger lookup without watching them on remote values.
18
+ export function getInternalValueWatchPrefix() {
19
+ return "INTERNAL_VALUE_WATCH_PREFIX_";
20
+ }
21
+
22
+ // WATCH CASES
23
+ // 1) getOwnNodeId() is used to trigger pathValueClientWatcher (no one else should be using it)
24
+ // 2) Use other nodes to immediately proxy to the other nodes
25
+ // 3) Use any string starting with the INTERNAL_VALUE_WATCH_PREFIX variable to watch them (remotely if required), but not trigger any callback. This works because the values are provided to the valid state computer before we receive them, so it doesn't require us to call them back (but it does require us to watch them remotely).
26
+ // - Valid state system uses this to watch values for locks
27
+ type PathWatcherCallback = string;
28
+ /*
29
+ PathWatcher is used if you want to either proxy watches to another server or if you just need the values, but don't need a callback triggered.
30
+ - If you want to watch with the callback, you should be using pathValueClientWatcher.
31
+ */
32
+ class PathWatcher {
33
+ private watchers = registerResource("paths|PathWatcher.watchers", new Map<string, {
34
+ watchers: Set<PathWatcherCallback>;
35
+ requestedTime: number;
36
+ // When we last received an initial sync message, this isn't necessarily the first time we've ever synced it, just means this is when we started our current syncing.
37
+ syncReceivedTime: number;
38
+ }>());
39
+ // If we don't limit the length, then this will memory leak. And if we only track
40
+ // the values in watchers, then rolling key syncs will be lost.
41
+ // DOES NOT track local values, as they are used internally, but we aren't waiting for them to be synced.
42
+ private syncHistoryForHarvest = new QueueLimited<{ start: number; end: number; path: string; }>(1_000_000);
43
+ private syncHistoryForDebug = new QueueLimited<{ start: number; end: number; path: string; }>(1_000_000);
44
+
45
+ //private watchers = registerResource("paths|PathWatcher.watchers", new Map<string, Set<PathWatcherCallback>>());
46
+
47
+ // realPath => packedPath => watcher => { depth, start, end }[]
48
+ private parentWatchers = registerResource("paths|parentWatchers", new Map<string,
49
+ // packedPath
50
+ Map<string, {
51
+ watchers: Set<PathWatcherCallback>;
52
+ }>
53
+ >());
54
+ private watchersToPaths = registerResource("paths|watchersToPaths", new Map<PathWatcherCallback, {
55
+ paths: Set<string>;
56
+ parents: Set<string>;
57
+ fullHistory?: boolean;
58
+ }>());
59
+
60
+ /** Automatically watches the remote if it's a remote path.
61
+ * Automatically unwatches the remote when the paths no longer have any callbacks.
62
+ * - Only stores callbacks in a Set, as strings, so unique ones are deduped, but the first unwatch will unwatch them all.
63
+ * */
64
+ @measureFnc
65
+ public watchPath(config: WatchConfig & {
66
+ nodeId: PathWatcherCallback;
67
+
68
+ debugName?: string;
69
+ noInitialTrigger?: boolean;
70
+ /** Force trigger, even if we are already watching the values. */
71
+ initialTrigger?: boolean;
72
+ }) {
73
+ this.ensureUnwatchingOnDisconnect(config.nodeId);
74
+ if (config.nodeId.startsWith(getInternalValueWatchPrefix()) && (!config.noInitialTrigger || config.initialTrigger)) {
75
+ require("debugbreak")(2);
76
+ debugger;
77
+ // No initial trigger because if it's a remote value, then we won't have it. If it's a local value, then we've already processed it in valid state computers, there's no reason to trigger it again.
78
+ throw new Error("Internal value watches (INTERNAL_VALUE_WATCH_PREFIX) cannot have initial triggers");
79
+ }
80
+
81
+ let newPathsWatched = new Set<string>();
82
+ let newParentsWatched = new Set<string>();
83
+
84
+ let time = Date.now();
85
+
86
+ for (let path of config.paths) {
87
+ let watchers = this.watchers.get(path);
88
+ if (!watchers) {
89
+ watchers = {
90
+ watchers: new Set(),
91
+ requestedTime: time,
92
+ syncReceivedTime: 0,
93
+ };
94
+ this.watchers.set(path, watchers);
95
+ }
96
+ if (watchers.watchers.has(config.nodeId)) continue;
97
+ newPathsWatched.add(path);
98
+ watchers.watchers.add(config.nodeId);
99
+
100
+ let watchObj = this.watchersToPaths.get(config.nodeId);
101
+ if (!watchObj) {
102
+ watchObj = { paths: new Set(), parents: new Set() };
103
+ this.watchersToPaths.set(config.nodeId, watchObj);
104
+ }
105
+ watchObj.paths.add(path);
106
+ if (config.fullHistory) {
107
+ watchObj.fullHistory = true;
108
+ }
109
+ }
110
+ for (let path of config.parentPaths) {
111
+ let basePath = hack_stripPackedPath(path);
112
+ let watchersObj = this.parentWatchers.get(basePath);
113
+ if (!watchersObj) {
114
+ watchersObj = new Map();
115
+ this.parentWatchers.set(basePath, watchersObj);
116
+ }
117
+ let obj = watchersObj.get(path);
118
+ if (!obj) {
119
+ obj = { watchers: new Set() };
120
+ watchersObj.set(path, obj);
121
+ }
122
+ if (obj.watchers.has(config.nodeId)) continue;
123
+ obj.watchers.add(config.nodeId);
124
+
125
+ newParentsWatched.add(path);
126
+
127
+ let watchObj = this.watchersToPaths.get(config.nodeId);
128
+ if (!watchObj) {
129
+ watchObj = { paths: new Set(), parents: new Set() };
130
+ this.watchersToPaths.set(config.nodeId, watchObj);
131
+ }
132
+ watchObj.parents.add(path);
133
+ }
134
+
135
+ if (newPathsWatched.size > 0 || newParentsWatched.size > 0) {
136
+ if (isOwnNodeId(config.nodeId)) {
137
+ for (let path of newPathsWatched) {
138
+ auditLog("new local WATCH VALUE", { path });
139
+ }
140
+ for (let path of newParentsWatched) {
141
+ auditLog("new local WATCH PARENT", { path });
142
+ }
143
+ } else {
144
+ for (let path of newPathsWatched) {
145
+ auditLog("new non-local WATCH VALUE", {
146
+ path,
147
+ watcher: config.nodeId,
148
+ sourceNodeThreadId: debugNodeThread(config.nodeId),
149
+ });
150
+ }
151
+ for (let path of newParentsWatched) {
152
+ auditLog("new non-local WATCH PARENT", { path, watcher: config.nodeId });
153
+ }
154
+ }
155
+ console.info(`New PathValue watches`, {
156
+ newPathsWatched: newPathsWatched.size,
157
+ newParentsWatched: newParentsWatched.size,
158
+ });
159
+ }
160
+ if (newPathsWatched.size > 0 || newParentsWatched.size > 0) {
161
+ if (!this.remoteWatchCallback) throw new Error("remoteWatchCallback not set by the pathWatcher");
162
+ let newRemotePaths = Array.from(newPathsWatched).filter(x => !PathRouter.isSelfAuthority(x));
163
+ let newRemoteParentPaths = Array.from(newParentsWatched).filter(x => !PathRouter.isSelfAuthority(x));
164
+ if (newRemotePaths.length > 0 || newRemoteParentPaths.length > 0) {
165
+ this.remoteWatchCallback({
166
+ paths: newRemotePaths,
167
+ parentPaths: newRemoteParentPaths,
168
+ });
169
+ }
170
+ }
171
+
172
+ // Treat everything as a new watch
173
+ if (config.initialTrigger) {
174
+ newPathsWatched = new Set(config.paths);
175
+ newParentsWatched = new Set(config.parentPaths);
176
+ }
177
+
178
+ if (!config.noInitialTrigger) {
179
+ this.triggerValuesChanged({
180
+ valuesChanged: new Set(),
181
+ initialTriggers: { values: newPathsWatched, parentPaths: newParentsWatched },
182
+ });
183
+ }
184
+ }
185
+ // NOTE: Automatically unwatches from remoteWatcher on all paths that are fully unwatched
186
+ @measureFnc
187
+ public unwatchPath(config: WatchConfig & { callback: PathWatcherCallback; }) {
188
+ const { callback } = config;
189
+
190
+ let fullyUnwatched: WatchConfig = {
191
+ paths: [],
192
+ parentPaths: [],
193
+ };
194
+
195
+ let pathsWatched = this.watchersToPaths.get(callback);
196
+
197
+ for (let path of config.parentPaths) {
198
+ if (pathsWatched) {
199
+ pathsWatched.parents.delete(path);
200
+ }
201
+
202
+ let unpackedPath = hack_stripPackedPath(path);
203
+
204
+ let watchersObj = this.parentWatchers.get(unpackedPath);
205
+ if (!watchersObj) continue;
206
+ let obj = watchersObj.get(path);
207
+ if (!obj) continue;
208
+ obj.watchers.delete(callback);
209
+
210
+ if (isOwnNodeId(callback)) {
211
+ auditLog("local UNWATCH PARENT", { path });
212
+ } else {
213
+ auditLog("non-local UNWATCH PARENT", { path, watcher: callback });
214
+ }
215
+
216
+ if (obj.watchers.size === 0) {
217
+ watchersObj.delete(path);
218
+ this.parentWatchers.delete(path);
219
+ fullyUnwatched.parentPaths.push(path);
220
+ authorityStorage.markParentPathAsUnwatched(path);
221
+
222
+ if (watchersObj.size === 0) {
223
+ this.parentWatchers.delete(unpackedPath);
224
+
225
+ let childPaths = authorityStorage.getPathsFromParent(unpackedPath);
226
+ // Destroy values that now have no watchers (no value watchers, and now no parent watcher
227
+ if (childPaths) {
228
+ for (let childPath of childPaths) {
229
+ if (!this.watchers.has(childPath)) {
230
+ authorityStorage.destroyPath(childPath);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ // NOTE: Unwatch parents, then values, as parent paths might keep alive value path watches.
239
+ for (let path of config.paths) {
240
+ if (pathsWatched) {
241
+ pathsWatched.paths.delete(path);
242
+ }
243
+
244
+ let watchers = this.watchers.get(path);
245
+ if (!watchers) continue;
246
+ watchers.watchers.delete(callback);
247
+
248
+ if (isOwnNodeId(callback)) {
249
+ auditLog("local UNWATCH VALUE", { path });
250
+ } else {
251
+ auditLog("non-local UNWATCH VALUE", { path, watcher: callback });
252
+ }
253
+
254
+ if (watchers.watchers.size === 0) {
255
+ this.watchers.delete(path);
256
+
257
+ let parentPath = getParentPathStr(path);
258
+
259
+ fullyUnwatched.paths.push(path);
260
+ // NOTE: If the parent is being watched, don't destroy the value.
261
+ // - The fact that we only do the check here does mean that if the path is unwatched
262
+ // first and then later the parent path is unwatched we will fail to properly destroy
263
+ // the path. However, in practice, either both are unwatched at the same time, or more
264
+ // likely, the value watch only goes away because it's made redundant by the parent watch.
265
+ if (!this.parentWatchers.has(parentPath)) {
266
+ authorityStorage.destroyPath(path);
267
+ }
268
+ }
269
+ }
270
+
271
+ if (fullyUnwatched.paths.length > 0 || fullyUnwatched.parentPaths.length > 0) {
272
+ console.info(`Unwatched PathValue watches`, {
273
+ unwatchedPaths: fullyUnwatched.paths.length,
274
+ unwatchedParents: fullyUnwatched.parentPaths.length,
275
+ });
276
+ if (!this.unwatchedCallbacks) throw new Error("unwatchedCallbacks not set by the RemoteWatcher");
277
+ this.unwatchedCallbacks(fullyUnwatched);
278
+ }
279
+ }
280
+
281
+ public unwatchEventPaths(paths: Set<string>) {
282
+ for (let path of paths) {
283
+ let watchers = this.watchers.get(path);
284
+ if (watchers) {
285
+ this.watchers.delete(path);
286
+ for (let watcher of watchers.watchers) {
287
+ let watchObj = this.watchersToPaths.get(watcher);
288
+ if (watchObj) {
289
+ watchObj.paths.delete(path);
290
+ }
291
+ }
292
+ }
293
+ let parentWatchers = this.parentWatchers.get(path);
294
+ if (parentWatchers) {
295
+ this.parentWatchers.delete(path);
296
+ for (let { watchers } of parentWatchers.values()) {
297
+ for (let watcher of watchers) {
298
+ let watchObj = this.watchersToPaths.get(watcher);
299
+ if (watchObj) {
300
+ watchObj.parents.delete(path);
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ /** You can provide any values that you want us to trigger. You can also provide initial triggers, which can send values by themselves, depending on what type of watcher it is. */
309
+ @measureFnc
310
+ public triggerValuesChanged(config: {
311
+ // IMPORTANT! The place that triggers this likely can only be our committing code as it knows what is valid or not, and explicitly calculates what's valid or not. If the valid states on these path values are wrong, we'll broadcast them and everyone will have the wrong valid states.
312
+ // ALSO, These path values need to be ingested first.
313
+ valuesChanged: Set<PathValue>;
314
+
315
+ // We mutate initialTriggers
316
+ initialTriggers: { values: Set<string>; parentPaths: Set<string> };
317
+
318
+ onlyTriggerNodeId?: string;
319
+ }) {
320
+ let { valuesChanged, initialTriggers, onlyTriggerNodeId } = config;
321
+
322
+ let changedPerCallbacks: Map<PathWatcherCallback, {
323
+ values: Set<PathValue>;
324
+ initialTriggers: { values: Set<string>; parentPaths: Set<string> };
325
+ }> = new Map();
326
+ let time = Date.now();
327
+
328
+ let triggerPaths = new Set<string>();
329
+ for (let value of valuesChanged) {
330
+ triggerPaths.add(value.path);
331
+ }
332
+ for (let path of initialTriggers?.values ?? []) {
333
+ triggerPaths.add(path);
334
+ }
335
+
336
+ let valuePerPath = keyByArray(Array.from(valuesChanged), x => x.path);
337
+
338
+ // initialTriggers.parentPaths => triggerPaths
339
+ for (let parentPath of initialTriggers.parentPaths) {
340
+ let valuePaths = authorityStorage.getPathsFromParent(parentPath);
341
+ for (let valuePath of valuePaths || []) {
342
+ triggerPaths.add(valuePath);
343
+ config.initialTriggers.values.add(valuePath);
344
+ }
345
+
346
+ let latestParentWatches = this.parentWatchers.get(hack_stripPackedPath(parentPath));
347
+ if (!latestParentWatches) continue;
348
+ for (let { watchers } of latestParentWatches.values()) {
349
+ for (let watcher of watchers) {
350
+ let changes = changedPerCallbacks.get(watcher);
351
+ if (!changes) {
352
+ changes = { values: new Set(), initialTriggers: { values: new Set(), parentPaths: new Set() } };
353
+ changedPerCallbacks.set(watcher, changes);
354
+ }
355
+ changes.initialTriggers.parentPaths.add(parentPath);
356
+ }
357
+ }
358
+ }
359
+
360
+ // triggerPaths => changedPerCallbacks + value histories of initial triggered values
361
+ for (let path of triggerPaths) {
362
+ let values = valuePerPath.get(path) ?? [];
363
+ let latestWatches = this.watchers.get(path);
364
+ if (latestWatches && !latestWatches.syncReceivedTime && !PathRouter.isLocalPath(path) && initialTriggers?.values.has(path)) {
365
+ latestWatches.syncReceivedTime = time;
366
+ let obj = { start: latestWatches.requestedTime, end: time, path };
367
+ this.syncHistoryForHarvest.push(obj);
368
+ this.syncHistoryForDebug.push(obj);
369
+ }
370
+
371
+ // Important. We can't do the initial trigger if we aren't synced. If we're the authority, this doesn't matter. This is only important for proxy watchers.
372
+ let isInitialTrigger = !!initialTriggers?.values.has(path) && authorityStorage.isSynced(path);
373
+ const triggerNodeChanged = (watcher: NodeId) => {
374
+ if (onlyTriggerNodeId && watcher !== onlyTriggerNodeId) return;
375
+ let changes = changedPerCallbacks.get(watcher);
376
+ if (!changes) {
377
+ changes = { values: new Set(), initialTriggers: { values: new Set(), parentPaths: new Set() } };
378
+ changedPerCallbacks.set(watcher, changes);
379
+ }
380
+ // Always send the explicit triggers (new values)
381
+ // - This will result in some redundant values being sent if these aren't the latest values (and fullHistory is off). However, it shouldn't make a difference in speed as it's very unusual for the values to be out of order. And certain things, such as the function runner, wants all the values, and there's debugging cases when having all the values will be useful as well.
382
+ for (let value of values) {
383
+ changes.values.add(value);
384
+ }
385
+ if (isInitialTrigger) {
386
+ let watcherObj = this.watchersToPaths.get(watcher);
387
+ let fullHistory = watcherObj?.fullHistory;
388
+ if (fullHistory) {
389
+ let history = authorityStorage.getValuePlusHistory(path);
390
+ for (let historicalValue of history) {
391
+ changes.values.add(historicalValue);
392
+ }
393
+ if (history.length === 0) {
394
+ changes.values.add(createMissingEpochValue(path));
395
+ }
396
+ } else {
397
+ // NOTE: We won't be given a value if it isn't synced, which will be never because we check if we're synced above. However, the type system doesn't know that.
398
+ let value = authorityStorage.getValueAtTime(path);
399
+ if (value) {
400
+ changes.values.add(value);
401
+ }
402
+ }
403
+ }
404
+ };
405
+
406
+ if (latestWatches) {
407
+ for (let watch of latestWatches.watchers) {
408
+ triggerNodeChanged(watch);
409
+ }
410
+ }
411
+
412
+ let parentPath = getParentPathStr(path);
413
+ let latestParentWatches = this.parentWatchers.get(parentPath);
414
+ if (latestParentWatches) {
415
+ for (let [packedPath, { watchers }] of latestParentWatches) {
416
+ if (!matchesParentRangeFilter({ parentPath, fullPath: path, packedPath })) continue;
417
+ for (let watch of watchers) {
418
+ triggerNodeChanged(watch);
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+
425
+ for (let [watch, { values, initialTriggers }] of changedPerCallbacks) {
426
+ this.triggerLatestWatcher(watch, Array.from(values), initialTriggers);
427
+ }
428
+ }
429
+
430
+ public isWatching(path: string): boolean {
431
+ return this.watchers.has(path) || this.parentWatchers.has(getParentPathStr(path));
432
+ }
433
+
434
+ @measureFnc
435
+ private triggerLatestWatcher(
436
+ watcher: PathWatcherCallback,
437
+ changes: PathValue[],
438
+ initialTriggers?: { values: Set<string>; parentPaths: Set<string> },
439
+ ) {
440
+ // NOTE: The receivers will sort these anyway, however it makes the logs cleaner if we send them in the correct order.
441
+ changes.sort((a, b) => compareTime(a.time, b.time));
442
+ if (isOwnNodeId(watcher)) {
443
+ if (!this.localTriggerCallback) throw new Error("localTriggerCallback not set by the pathValueClientWatcher");
444
+ this.localTriggerCallback(changes, initialTriggers?.parentPaths ?? new Set());
445
+ } else if (watcher.startsWith(getInternalValueWatchPrefix())) {
446
+ // The valid state computer will get the value before we even get it, so it doesn't need to be told to trigger. It triggers in all values.
447
+ } else {
448
+ if (!isCoreQuiet) {
449
+ console.log(`(${Date.now()}) Sending values to client: ${changes.length} (${watcher})`);
450
+ }
451
+ this.ensureUnwatchingOnDisconnect(watcher);
452
+ ignoreErrors((async () => {
453
+ await PathValueControllerBase.sendValues({
454
+ nodeId: watcher,
455
+ pathValues: changes,
456
+ initialTriggers: initialTriggers,
457
+ });
458
+ })());
459
+ }
460
+ }
461
+
462
+ private ensureUnwatchingOnDisconnect = cache((nodeId: string) => {
463
+ if (isOwnNodeId(nodeId)) return;
464
+ if (nodeId.startsWith(getInternalValueWatchPrefix())) return;
465
+ SocketFunction.onNextDisconnect(nodeId, async () => {
466
+ this.ensureUnwatchingOnDisconnect.clear(nodeId);
467
+
468
+ // Wait while, so Querysub doesn't thrash when clients connect and disconnect
469
+ // TODO: We should be smarter about our wait time, waiting less if we have a lot of resource
470
+ // pressure, and more (potentially forever), if we don't have much resource pressure
471
+ // (memory, cpu, and network).
472
+ setTimeout(() => {
473
+ let watches = this.watchersToPaths.get(nodeId);
474
+ this.watchersToPaths.delete(nodeId);
475
+ if (watches) {
476
+ this.unwatchPath({ paths: Array.from(watches.paths), parentPaths: Array.from(watches.parents), callback: nodeId });
477
+ }
478
+ }, MAX_CHANGE_AGE);
479
+ });
480
+ });
481
+
482
+ private localTriggerCallback: ((changes: PathValue[], parentPaths: Set<string>) => void) | undefined;
483
+ public setLocalTriggerCallback(callback: (changes: PathValue[], parentPaths: Set<string>) => void) {
484
+ if (this.localTriggerCallback) throw new Error("localTriggerCallback already set. It should only be set once, by the pathValueClientWatcher");
485
+ this.localTriggerCallback = callback;
486
+ }
487
+
488
+ private unwatchedCallbacks: ((config: WatchConfig) => void) | undefined;
489
+ public setUnwatchCallback(callback: (config: WatchConfig) => void) {
490
+ if (this.unwatchedCallbacks) throw new Error("unwatchedCallbacks already set. It should only be set once, by the RemoteWatcher");
491
+ this.unwatchedCallbacks = callback;
492
+ }
493
+
494
+ private remoteWatchCallback: ((config: WatchConfig) => void) | undefined;
495
+ public setWatchRemoteCallback(callback: (config: WatchConfig) => void) {
496
+ if (this.remoteWatchCallback) throw new Error("remoteWatchCallback already set. It should only be set once, by the RemoteWatcher");
497
+ this.remoteWatchCallback = callback;
498
+ }
499
+
500
+
501
+ public getAllParentWatches() {
502
+ return Array.from(this.parentWatchers.keys());
503
+ }
504
+
505
+ public debug_harvestSyncTimes(): { start: number; end: number; path: string; }[] {
506
+ let times = this.syncHistoryForHarvest.getAllUnordered();
507
+ this.syncHistoryForHarvest.reset();
508
+ return times;
509
+ }
510
+
511
+ public debug_getSyncHistory(): { start: number; end: number; path: string; }[] {
512
+ return this.syncHistoryForDebug.getAllUnordered();
513
+ }
514
+
515
+ public getWatcherStats() {
516
+ let localPathCount = 0;
517
+ let remotePathCount = 0;
518
+
519
+ for (let [path] of this.watchers) {
520
+ if (PathRouter.isLocalPath(path)) {
521
+ localPathCount++;
522
+ } else {
523
+ remotePathCount++;
524
+ }
525
+ }
526
+
527
+ return {
528
+ localPathCount,
529
+ remotePathCount,
530
+ };
531
+ }
532
+ }
533
+ export const pathWatcher = new PathWatcher();
534
+ (globalThis as any).pathWatcher = pathWatcher;
@@ -0,0 +1,31 @@
1
+
2
+ /*
3
+ By default, sharding works by hashing the hash to get the route. This technically works, however, it means if you have an object with ten properties, you could easily end up talking to 10 different servers.
4
+
5
+ And so we have a list of prefixes, and if we match a prefix, then we hash less of the path (We accidentally hashed the child key under the prefix, but that's for a different reason related to making sure parallel lookups use the same server).
6
+
7
+ The prefix needs to be detailed enough so that everything doesn't use the same server.
8
+
9
+ After a PathServerValue starts, it can't change its prefixes, as if the routing changes it makes it too difficult for clients to reason about who they should be synchronizing with.
10
+ */
11
+
12
+ import { nestArchives } from "../-a-archives/archives";
13
+ import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
14
+ import { archiveJSONT } from "../-a-archives/archivesJSONT";
15
+ import { getDomain } from "../config";
16
+
17
+ type PrefixObj = {
18
+ prefixes: string[];
19
+ };
20
+
21
+ let prefixes = archiveJSONT<PrefixObj>(() => nestArchives("shard-prefixes/", getArchivesBackblaze(getDomain())));
22
+ // We don't actually need a collection, we only need a single value. However, the collection interface is a lot more convenient than manually dealing with backplays files.
23
+ const key = "all";
24
+
25
+ export async function getShardPrefixes(): Promise<string[]> {
26
+ return (await prefixes.get(key))?.prefixes ?? [];
27
+ }
28
+
29
+ export async function setShardPrefixes(_prefixes: string[]) {
30
+ await prefixes.set(key, { prefixes: _prefixes });
31
+ }