querysub 0.402.0 → 0.404.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.cursorrules +2 -0
  2. package/bin/audit-imports.js +4 -0
  3. package/package.json +7 -4
  4. package/spec.txt +77 -0
  5. package/src/-a-archives/archiveCache.ts +9 -4
  6. package/src/-a-archives/archivesBackBlaze.ts +1039 -1039
  7. package/src/-a-auth/certs.ts +0 -12
  8. package/src/-c-identity/IdentityController.ts +12 -3
  9. package/src/-f-node-discovery/NodeDiscovery.ts +32 -26
  10. package/src/-g-core-values/NodeCapabilities.ts +12 -2
  11. package/src/0-path-value-core/AuthorityLookup.ts +239 -0
  12. package/src/0-path-value-core/LockWatcher2.ts +150 -0
  13. package/src/0-path-value-core/PathRouter.ts +535 -0
  14. package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
  15. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +65 -0
  16. package/src/0-path-value-core/PathValueCommitter.ts +222 -488
  17. package/src/0-path-value-core/PathValueController.ts +277 -239
  18. package/src/0-path-value-core/PathWatcher.ts +534 -0
  19. package/src/0-path-value-core/ShardPrefixes.ts +31 -0
  20. package/src/0-path-value-core/ValidStateComputer.ts +303 -0
  21. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +1 -1
  22. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +80 -44
  23. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +13 -16
  24. package/src/0-path-value-core/auditLogs.ts +2 -0
  25. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +97 -0
  26. package/src/0-path-value-core/pathValueArchives.ts +490 -492
  27. package/src/0-path-value-core/pathValueCore.ts +195 -1492
  28. package/src/0-path-value-core/startupAuthority.ts +74 -0
  29. package/src/1-path-client/RemoteWatcher.ts +100 -83
  30. package/src/1-path-client/pathValueClientWatcher.ts +808 -815
  31. package/src/2-proxy/PathValueProxyWatcher.ts +10 -8
  32. package/src/2-proxy/archiveMoveHarness.ts +182 -214
  33. package/src/2-proxy/garbageCollection.ts +9 -8
  34. package/src/2-proxy/schema2.ts +21 -1
  35. package/src/3-path-functions/PathFunctionHelpers.ts +206 -180
  36. package/src/3-path-functions/PathFunctionRunner.ts +943 -766
  37. package/src/3-path-functions/PathFunctionRunnerMain.ts +5 -3
  38. package/src/3-path-functions/pathFunctionLoader.ts +2 -2
  39. package/src/3-path-functions/syncSchema.ts +592 -521
  40. package/src/4-deploy/deployFunctions.ts +19 -4
  41. package/src/4-deploy/deployGetFunctionsInner.ts +8 -2
  42. package/src/4-deploy/deployMain.ts +51 -68
  43. package/src/4-deploy/edgeClientWatcher.tsx +1 -1
  44. package/src/4-deploy/edgeNodes.ts +2 -2
  45. package/src/4-dom/qreact.tsx +2 -4
  46. package/src/4-dom/qreactTest.tsx +7 -13
  47. package/src/4-querysub/Querysub.ts +21 -8
  48. package/src/4-querysub/QuerysubController.ts +45 -29
  49. package/src/4-querysub/permissions.ts +2 -2
  50. package/src/4-querysub/querysubPrediction.ts +80 -70
  51. package/src/4-querysub/schemaHelpers.ts +5 -1
  52. package/src/5-diagnostics/GenericFormat.tsx +14 -9
  53. package/src/archiveapps/archiveGCEntry.tsx +9 -2
  54. package/src/archiveapps/archiveJoinEntry.ts +87 -84
  55. package/src/archiveapps/archiveMergeEntry.tsx +2 -0
  56. package/src/bits.ts +19 -0
  57. package/src/config.ts +21 -3
  58. package/src/config2.ts +23 -48
  59. package/src/deployManager/components/DeployPage.tsx +7 -3
  60. package/src/deployManager/machineSchema.ts +4 -1
  61. package/src/diagnostics/ActionsHistory.ts +3 -8
  62. package/src/diagnostics/AuditLogPage.tsx +2 -3
  63. package/src/diagnostics/FunctionCallInfo.tsx +141 -0
  64. package/src/diagnostics/FunctionCallInfoState.ts +162 -0
  65. package/src/diagnostics/MachineThreadInfo.tsx +1 -1
  66. package/src/diagnostics/NodeViewer.tsx +37 -48
  67. package/src/diagnostics/SyncTestPage.tsx +241 -0
  68. package/src/diagnostics/auditImportViolations.ts +185 -0
  69. package/src/diagnostics/listenOnDebugger.ts +3 -3
  70. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +10 -4
  71. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
  72. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +24 -22
  73. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
  74. package/src/diagnostics/logs/diskLogGlobalContext.ts +1 -0
  75. package/src/diagnostics/logs/errorNotifications2/logWatcher.ts +1 -3
  76. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +39 -17
  77. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +4 -6
  78. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleInstanceTableView.tsx +36 -5
  79. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +19 -5
  80. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +15 -7
  81. package/src/diagnostics/logs/lifeCycleAnalysis/NestedLifeCycleInfo.tsx +28 -106
  82. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +2 -0
  83. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMisc.ts +0 -0
  84. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +18 -7
  85. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +3 -0
  86. package/src/diagnostics/managementPages.tsx +10 -3
  87. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +20 -26
  88. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +6 -4
  89. package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +2 -2
  90. package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +7 -9
  91. package/src/diagnostics/misc-pages/SnapshotViewer.tsx +23 -12
  92. package/src/diagnostics/misc-pages/archiveViewerShared.tsx +1 -1
  93. package/src/diagnostics/pathAuditer.ts +486 -0
  94. package/src/diagnostics/pathAuditerCallback.ts +20 -0
  95. package/src/diagnostics/watchdog.ts +8 -1
  96. package/src/library-components/URLParam.ts +1 -1
  97. package/src/misc/hash.ts +1 -0
  98. package/src/path.ts +21 -7
  99. package/src/server.ts +54 -47
  100. package/src/user-implementation/loginEmail.tsx +1 -1
  101. package/tempnotes.txt +67 -0
  102. package/test.ts +288 -95
  103. package/src/0-path-value-core/NodePathAuthorities.ts +0 -1057
  104. package/src/0-path-value-core/PathController.ts +0 -1
  105. package/src/5-diagnostics/diskValueAudit.ts +0 -218
  106. package/src/5-diagnostics/memoryValueAudit.ts +0 -438
  107. package/src/archiveapps/lockTest.ts +0 -127
@@ -1,815 +1,808 @@
1
- /**
2
- Does everything needed to synchronize values with PathValueController, including
3
- storing the values. While this can result in redundant storage, it helps keep
4
- our strictly synchronized values, with values we just get from another node.
5
- - We also don't consider rejections at all, and just assume there is no
6
- contention in this file.
7
- - Keys based on baseTime, so our values can be correctly clobbered by the remote.
8
-
9
- setValues
10
- - For writing values
11
- getValue
12
- - For getting values
13
- setWatches
14
- - For knowing when values change
15
- */
16
-
17
- import { SocketFunction } from "socket-function/SocketFunction";
18
- import { cache, lazy } from "socket-function/src/caching";
19
- import { binarySearchIndex, isNode, recursiveFreeze, sort } from "socket-function/src/misc";
20
- import { logErrors } from "../errors";
21
- import { getParentPathStr, getPathFromStr, hack_stripPackedPath } from "../path";
22
- import { measureBlock, measureFnc } from "socket-function/src/profiling/measure";
23
- import { pathValueCommitter, PathValueController } from "../0-path-value-core/PathValueController";
24
- import { PathValue, Value, getNextTime, Time, ReadLock, pathWatcher, MAX_CHANGE_AGE, authorityStorage, getCreatorId, WatchConfig, decodeParentFilter, matchesParentRangeFilter } from "../0-path-value-core/pathValueCore";
25
- import { blue, green, red } from "socket-function/src/formatting/logColors";
26
- import { MaybePromise } from "socket-function/src/types";
27
- import { batchFunction, batchFunctionNone, runInfinitePoll } from "socket-function/src/batching";
28
- import { registerResource } from "../diagnostics/trackResources";
29
- import debugbreak from "debugbreak";
30
- import { ActionsHistory } from "../diagnostics/ActionsHistory";
31
- import { pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
32
- import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
33
- import { PromiseObj } from "../promise";
34
- import { getOwnMachineId } from "../-a-auth/certs";
35
- import { remoteWatcher } from "./RemoteWatcher";
36
- import { heapTagObj } from "../diagnostics/heapTag";
37
- import { Querysub } from "../4-querysub/QuerysubController";
38
- import { isDevDebugbreak } from "../config";
39
- import { auditLog } from "../0-path-value-core/auditLogs";
40
-
41
- const pathValueCtor = heapTagObj("PathValue");
42
-
43
- export interface WatchSpecData {
44
- paths: Set<string>;
45
- pathSources: Set<PathValue> | undefined;
46
- newParentsSynced: Set<string>;
47
- extraReasons?: string[];
48
- }
49
-
50
- export interface WatchSpec {
51
- debugName?: string;
52
-
53
- // Run order is determined by the lowest [orderGroup, order]. It is HIGHLY recommended
54
- // to use orderGroup, with low values (0 is the default, so use 1 to run later,
55
- // and -1 to run earlier).
56
- // - Using a large number of unique orderGroups (for example, a unique group per callback),
57
- // will slow down callbacks.
58
- // - "order" is defaulted to a good default (the construct order), and so setting it will often
59
- // result in callbacks being given too high of a priority.
60
- order?: number;
61
- orderGroup?: number;
62
-
63
- // callback is the key used to update watches, if the same callback is used
64
- // it won't start a new watch, but will instead update the paths used.
65
- callback: (changed: WatchSpecData) => void;
66
- unwatchEventPaths?: (paths: Set<string>) => void;
67
- paths: Set<string>;
68
- parentPaths: Set<string>;
69
- /** For PathValueProxyWatcher. BUT, not in the Querysub routing service, or most backend middle-man type services. */
70
- noInitialTrigger?: boolean;
71
-
72
- // IMPORTANT! Any variables that should be persisted, must be copied over in updateUnwatches.
73
- // Otherwise we don't keep them. We do this for GC reasons (I think?).
74
- // - We might want to try reusing the same object... it should greatly reduce our GC overhead...
75
- breakOnNextTrigger?: boolean;
76
- triggerCount?: number;
77
- }
78
-
79
- let nextWatchSpecOrder = 1;
80
- export class ClientWatcher {
81
- public static DEBUG_READS = false;
82
- public static DEBUG_WRITES = false;
83
- public static DEBUG_READS_EXPANDED = false;
84
- public static DEBUG_WRITES_EXPANDED = false;
85
- public static DEBUG_TRIGGERS?: "light" | "heavy";
86
- public static DEBUG_SOURCES = false;
87
-
88
- /** How long we keep watching, despite a value no longer being needed. */
89
- public static WATCH_STICK_TIME = MAX_CHANGE_AGE * 2;
90
- public static MAX_TRIGGER_TIME_BROWSER = 1000 * 5;
91
- public static MAX_TRIGGER_TIME_NODEJS = 1000 * 300;
92
-
93
-
94
- private valueFunctionWatchers = registerResource("paths|valueFunctionWatchers", new Map<string, Map<WatchSpec["callback"], WatchSpec>>());
95
- // hack_stripPackedPath(path) =>
96
- private parentValueFunctionWatchers = registerResource("paths|parentValueFunctionWatchers", new Map<string,
97
- // path =>
98
- Map<string, {
99
- start: number;
100
- end: number;
101
- lookup: Map<WatchSpec["callback"], WatchSpec>
102
- }>
103
- >());
104
- private allWatchers = registerResource("paths|clientWatcher.allWatchers", new Map<WatchSpec["callback"], WatchSpec>());
105
-
106
- // path => expiryTime
107
- private pendingUnwatches = registerResource("paths|pendingUnwatches", new Map<string, number>());
108
- private pendingParentUnwatches = registerResource("paths|pendingParentUnwatches", new Map<string, number>());
109
-
110
-
111
- private inLoop = false;
112
- private isLoopSynchronous = false;
113
- private pendingTriggered: Map<WatchSpec, WatchSpecData> | undefined;
114
-
115
- private preTrigger(spec: WatchSpec) {
116
- spec.triggerCount = (spec.triggerCount ?? 0) + 1;
117
- if (spec.breakOnNextTrigger) {
118
- spec.breakOnNextTrigger = false;
119
- debugger;
120
- }
121
- }
122
-
123
- private triggerWatcher(spec: WatchSpec, config?: { synchronous?: boolean }) {
124
- this.preTrigger(spec);
125
- let watchedTriggers = this.pendingTriggered = this.pendingTriggered || (new Map() as never);
126
- let trigger = watchedTriggers.get(spec);
127
- if (!trigger) {
128
- trigger = {
129
- pathSources: ClientWatcher.DEBUG_SOURCES ? new Set() : undefined,
130
- newParentsSynced: new Set(),
131
- paths: new Set(),
132
- };
133
- watchedTriggers.set(spec, trigger);
134
- }
135
- this.triggerWatchLoop(config);
136
- return trigger;
137
- }
138
- private beforeTriggerDone: Promise<void> | undefined;
139
- private onTriggerDone: Promise<void> | undefined;
140
- public activeWatchSpec: WatchSpec | undefined;
141
- private ignoreWatch: ((spec: WatchSpec, source: PathValue | string) => boolean) | undefined;
142
- // Needed for typesafecss. Same as onTriggerDone, but fires first.
143
- public waitForBeforeTriggerFinished() { return this.beforeTriggerDone; }
144
- public waitForTriggerFinished() { return this.onTriggerDone; }
145
- public isCurrentLoopSynchronous() { return this.isLoopSynchronous; }
146
-
147
- @measureFnc
148
- public localOnValueCallback(values: PathValue[], parentsSynced: string[]) {
149
- if (values.length === 0 && parentsSynced.length === 0) return;
150
- const onTrigger = (spec: WatchSpec, path: string, source?: PathValue) => {
151
- if (
152
- ClientWatcher.DEBUG_TRIGGERS === "heavy"
153
- && !this.pendingTriggered?.has(spec)
154
- ) {
155
- let sourceName = "";
156
- if (this.activeWatchSpec) {
157
- sourceName = ` (inside ${this.activeWatchSpec.debugName})`;
158
- } else if (source?.source) {
159
- sourceName = ` (remote ${source.source.split("|").at(-1)})`;
160
- }
161
- console.log(`${blue("QUEUEING TRIGGER")} ${spec.debugName} ${sourceName}`);
162
- console.log(` DUE TO WRITE ${getPathFromStr(path).join(".")}`);
163
- }
164
- };
165
-
166
- const triggerWatcher = (spec: WatchSpec, source: PathValue) => {
167
- // Ignore self writes, otherwise local writes as simple as `x++`, will trigger an infinite loop.
168
- if (this.ignoreWatch?.(spec, source)) return;
169
- let trigger = this.triggerWatcher(spec);
170
- if (trigger.pathSources) {
171
- trigger.pathSources.add(source);
172
- }
173
- trigger.paths.add(source.path);
174
- onTrigger(spec, source.path, source);
175
- };
176
- const triggerWatcherParent = (spec: WatchSpec, parent: string) => {
177
- if (this.ignoreWatch?.(spec, parent)) return;
178
- let trigger = this.triggerWatcher(spec);
179
- trigger.newParentsSynced.add(parent);
180
- onTrigger(spec, parent);
181
- };
182
- for (let value of values) {
183
- let watchers = this.valueFunctionWatchers.get(value.path);
184
- if (watchers) {
185
- for (let watcher of watchers.values()) {
186
- triggerWatcher(watcher, value);
187
- }
188
- }
189
- let parentPath = getParentPathStr(value.path);
190
- let parentWatchers = this.parentValueFunctionWatchers.get(parentPath);
191
- if (parentWatchers) {
192
- // NOTE: We don't filter by path shard here
193
- for (let { start, end, lookup } of parentWatchers.values()) {
194
- if (!matchesParentRangeFilter({ parentPath, fullPath: value.path, start, end })) {
195
- continue;
196
- }
197
-
198
- for (let watcher of lookup.values()) {
199
- triggerWatcher(watcher, value);
200
- triggerWatcherParent(watcher, parentPath);
201
- }
202
- }
203
- }
204
- }
205
- for (let path of parentsSynced) {
206
- let basePath = hack_stripPackedPath(path);
207
- let watchers = this.parentValueFunctionWatchers.get(basePath);
208
- if (watchers) {
209
- let realObj = watchers.get(path);
210
- if (realObj) {
211
- for (let watcher of realObj.lookup.values()) {
212
- triggerWatcherParent(watcher, path);
213
- }
214
- }
215
- }
216
- }
217
- // Trigger all children of parents synced
218
- // NOTE: This is the counterpointer to RemoteWatcher's removal of extra requests when we are already syncing the parent path. If it's not going to request them, we're not going to get the paths, so we need to trigger based on parents.
219
- // - We ignore hack paths here (that path in range requests in parent paths), because RemoteWatcher also ignores them (so hacked parent paths won't stop value path syncs there), so they will be received normally.
220
- // NOTE: This will probably be slow, but we shouldn't be receiving parent syncs very frequently. Or maybe we will, and maybe this will just be incredibly slow.
221
- if (parentsSynced.length > 0) {
222
- measureBlock(() => {
223
- for (let parentPath of parentsSynced) {
224
- for (let [path, lookup] of this.valueFunctionWatchers.entries()) {
225
- if (!path.startsWith(parentPath)) continue;
226
- if (getParentPathStr(path) !== parentPath) continue;
227
- for (let watch of lookup.values()) {
228
- let trigger = this.triggerWatcher(watch);
229
- trigger.paths.add(path);
230
- onTrigger(watch, path);
231
- }
232
- }
233
- }
234
- }, "ClientWatcher|triggerChildrenOfParentsSynced");
235
- }
236
- }
237
-
238
- public debugBreakOnNextTrigger() {
239
- if (!this.activeWatchSpec) throw new Error(`Break on next trigger must be called inside of a clientWatcher trigger`);
240
- this.activeWatchSpec.breakOnNextTrigger = true;
241
- }
242
-
243
- public explicitlyTriggerWatcher(callback: WatchSpec["callback"], config?: { synchronous?: boolean }) {
244
- let watcher = this.allWatchers.get(callback);
245
- if (!watcher) throw new Error(`No watcher found for callback ${callback.toString()}`);
246
- this.triggerWatcher(watcher, config);
247
- }
248
-
249
- private triggerWatchLoop(config?: { synchronous?: boolean }) {
250
- // A fairly standard trigger loop. Batch synchronous triggers for efficiency's sake
251
- if (this.inLoop) return;
252
- this.inLoop = true;
253
- let beforeTriggerDone = new PromiseObj();
254
- let triggerPromiseObj = new PromiseObj();
255
- this.beforeTriggerDone = beforeTriggerDone.promise;
256
- this.onTriggerDone = triggerPromiseObj.promise;
257
- const doLoop = () => {
258
- try {
259
- this.innerTriggerLoop();
260
- } finally {
261
- this.inLoop = false;
262
- this.isLoopSynchronous = false;
263
- beforeTriggerDone.resolve();
264
- triggerPromiseObj.resolve();
265
- this.beforeTriggerDone = undefined;
266
- this.onTriggerDone = undefined;
267
- }
268
- };
269
- if (config?.synchronous) {
270
- this.isLoopSynchronous = true;
271
- doLoop();
272
- } else {
273
- void Promise.resolve().finally(doLoop);
274
- }
275
- }
276
- @measureFnc
277
- private innerTriggerLoop() {
278
- let loopCount = 0;
279
- let triggerCount = 0;
280
- let groupStart = (
281
- ClientWatcher.DEBUG_TRIGGERS === "light" && console.groupCollapsed
282
- || ClientWatcher.DEBUG_TRIGGERS === "heavy" && console.group
283
- || undefined
284
- );
285
-
286
- for (let callback of this.loopStartCallbacks) {
287
- try {
288
- callback();
289
- } catch (e) {
290
- logErrors(e);
291
- }
292
- }
293
- let MAX_TRIGGER_TIME = isNode() ? ClientWatcher.MAX_TRIGGER_TIME_NODEJS : ClientWatcher.MAX_TRIGGER_TIME_BROWSER;
294
-
295
- let time = Date.now();
296
- while (true) {
297
- let currentTriggered = this.pendingTriggered;
298
- this.pendingTriggered = undefined;
299
- if (!currentTriggered?.size) break;
300
-
301
- if (ClientWatcher.DEBUG_TRIGGERS) {
302
- console.log(" ");
303
- }
304
- groupStart?.(`${blue("TRIGGERS LOOP ITERATION")} ${loopCount++}`);
305
-
306
- let sorted = Array.from(currentTriggered);
307
- sorted.sort((a, b) => {
308
- let orderDiff = (a[0].orderGroup ?? 0) - (b[0].orderGroup ?? 0);
309
- if (orderDiff !== 0) return orderDiff;
310
- return (a[0].order ?? 0) - (b[0].order ?? 0);
311
- });
312
- if (ClientWatcher.DEBUG_TRIGGERS) {
313
- for (let [trigger] of sorted) {
314
- console.log(" " + trigger.debugName + " " + (trigger.orderGroup ?? 0) + "|" + (trigger.order ?? 0));
315
- }
316
- console.groupEnd();
317
- }
318
- let curIndex = 0;
319
- this.ignoreWatch = (spec, source) => {
320
- this.preTrigger(spec);
321
- // Ignore it if we are handling it (self triggers are almost always just due to `x++`, and are virtually
322
- // never intended to actually self trigger. And if they are, then they can just detach with
323
- // Promise.resolve.finally(...))
324
- // ALSO ignore if we are going to handle it. There's no reason to queue another watch for something
325
- // we are about to handle.
326
- for (let checkIndex = curIndex; checkIndex < sorted.length; checkIndex++) {
327
- if (sorted[checkIndex][0] === spec) {
328
- let watchSpecData = sorted[checkIndex][1];
329
- if (typeof source === "string") {
330
- watchSpecData.newParentsSynced.add(source);
331
- } else {
332
- if (watchSpecData.pathSources) {
333
- watchSpecData.pathSources.add(source);
334
- }
335
- watchSpecData.paths.add(source.path);
336
- }
337
- return true;
338
- }
339
- }
340
-
341
- // If they care about the order, and it is higher than the current order, then insert it into the current
342
- if (spec.order !== undefined && (
343
- (spec.order ?? 0) > (sorted[curIndex][0].order ?? 0)
344
- && (spec.orderGroup ?? 0) === (sorted[curIndex][0].orderGroup ?? 0)
345
- || (spec.orderGroup ?? 0) > (sorted[curIndex][0].orderGroup ?? 0)
346
- )) {
347
- if (ClientWatcher.DEBUG_TRIGGERS === "heavy") {
348
- console.log(" (insert new trigger)");
349
- console.log(" " + spec.debugName + " " + (spec.orderGroup ?? 0) + "|" + (spec.order ?? 0));
350
- }
351
- // Find the last insert >= the current order (check for equals so we can insert at the end
352
- // duplicate order values).
353
- let insertIndex = sorted.findLastIndex(x =>
354
- (spec.order ?? 0) >= (x[0].order ?? 0)
355
- && (spec.orderGroup ?? 0) === (x[0].orderGroup ?? 0)
356
- || (spec.orderGroup ?? 0) > (x[0].orderGroup ?? 0)
357
- );
358
- if (insertIndex === -1) {
359
- insertIndex = sorted.length - 1;
360
- }
361
- insertIndex++;
362
- let watchSpecData: WatchSpecData = {
363
- paths: new Set(),
364
- pathSources: ClientWatcher.DEBUG_SOURCES ? new Set() : undefined,
365
- newParentsSynced: new Set(),
366
- };
367
- if (typeof source === "string") {
368
- watchSpecData.newParentsSynced.add(source);
369
- } else {
370
- if (watchSpecData.pathSources) {
371
- watchSpecData.pathSources.add(source);
372
- }
373
- watchSpecData.paths.add(source.path);
374
- }
375
- sorted.splice(insertIndex, 0, [spec, watchSpecData]);
376
- return true;
377
- }
378
- return false;
379
- };
380
-
381
- let stoppedEarly = false;
382
- for (; curIndex < sorted.length; curIndex++) {
383
- let [watchSpec, data] = sorted[curIndex];
384
- triggerCount++;
385
- if (ClientWatcher.DEBUG_TRIGGERS === "heavy") {
386
- console.log(`${green("RUNNING TRIGGER")} ${watchSpec.debugName}`);
387
- }
388
-
389
- try {
390
- this.activeWatchSpec = watchSpec;
391
- watchSpec.callback(data);
392
- } catch (e) {
393
- void Promise.resolve().finally(() => { throw e; });
394
- } finally {
395
- this.activeWatchSpec = undefined;
396
- }
397
- if (
398
- Date.now() - time > MAX_TRIGGER_TIME
399
- && !isDevDebugbreak()
400
- ) {
401
- stoppedEarly = true;
402
- break;
403
- }
404
- }
405
- this.ignoreWatch = undefined;
406
-
407
- if (ClientWatcher.DEBUG_TRIGGERS) {
408
- console.log(" ");
409
- }
410
-
411
- if (stoppedEarly) {
412
- const history = 10;
413
- let remaining = (
414
- sorted
415
- .slice(curIndex, curIndex + history)
416
- .map(x => `${x[0].debugName} (${Array.from(x[1].paths).map(x => getPathFromStr(x).join(".")).join(" | ")})`)
417
- .join(", ")
418
- );
419
- console.error(red(`Too much time spent in trigger loop, aborting loop. Triggered ${triggerCount} triggers. Remaining triggers: ${remaining}`), triggerCount);
420
- break;
421
- }
422
- }
423
-
424
- let finalStats = { loopCount, triggerCount };
425
- for (let callback of this.loopEndCallbacks) {
426
- try {
427
- callback(finalStats);
428
- } catch (e) {
429
- logErrors(e);
430
- }
431
- }
432
-
433
- time = Date.now() - time;
434
- if (ClientWatcher.DEBUG_TRIGGERS) {
435
- console.log(`${green("TRIGGERS LOOP FINISHED")} ${loopCount} loops, ${triggerCount} triggers, in ${time}ms`);
436
- console.log(" ");
437
- console.log(" ");
438
- }
439
- }
440
-
441
- private loopStartCallbacks = new Set<() => void>();
442
- private loopEndCallbacks = new Set<(info: { loopCount: number; triggerCount: number; }) => void>();
443
- public onLoopStart(callback: () => void) {
444
- this.loopStartCallbacks.add(callback);
445
- }
446
- public onLoopEnd(
447
- callback: (info: {
448
- loopCount: number;
449
- triggerCount: number;
450
- }) => void
451
- ) {
452
- this.loopEndCallbacks.add(callback);
453
- }
454
-
455
- public unwatch(callback: WatchSpec["callback"]) {
456
- this.updateUnwatches({
457
- callback,
458
- paths: new Set(),
459
- parentPaths: new Set(),
460
- });
461
- this.allWatchers.delete(callback);
462
- }
463
-
464
- @measureFnc
465
- private updateUnwatches(watchSpec: WatchSpec) {
466
- const { callback, paths, parentPaths } = watchSpec;
467
- let prevSpec = this.allWatchers.get(callback);
468
- if (!prevSpec) return;
469
- if (prevSpec) {
470
- watchSpec.order = prevSpec.order;
471
- watchSpec.triggerCount = prevSpec.triggerCount;
472
- watchSpec.breakOnNextTrigger = prevSpec.breakOnNextTrigger;
473
- }
474
-
475
- let fullyUnwatchedPaths = new Set<string>();
476
- let fullyUnwatchedParents = new Set<string>();
477
-
478
- // Remove removed paths
479
- for (let path of prevSpec.paths) {
480
- if (paths.has(path)) continue;
481
- let watchers = this.valueFunctionWatchers.get(path);
482
- if (!watchers) continue;
483
- watchers.delete(callback);
484
- if (watchers.size === 0) {
485
- fullyUnwatchedPaths.add(path);
486
- this.valueFunctionWatchers.delete(path);
487
- }
488
- }
489
-
490
- // Remove removed parents
491
- for (let path of prevSpec.parentPaths) {
492
- if (parentPaths.has(path)) continue;
493
- let basePath = hack_stripPackedPath(path);
494
- let watchersBase = this.parentValueFunctionWatchers.get(basePath);
495
- if (!watchersBase) continue;
496
- let watchers = watchersBase.get(path);
497
- if (!watchers) continue;
498
- watchers.lookup.delete(callback);
499
- if (watchers.lookup.size === 0) {
500
- watchersBase.delete(path);
501
- fullyUnwatchedParents.add(path);
502
- }
503
- if (watchersBase.size === 0) {
504
- this.parentValueFunctionWatchers.delete(basePath);
505
- }
506
- }
507
-
508
- prevSpec.paths = paths;
509
- prevSpec.parentPaths = parentPaths;
510
- watchSpec = prevSpec;
511
-
512
- let expiryTime = Date.now() + ClientWatcher.WATCH_STICK_TIME;
513
- // Commented out, as this is showing up as a bit slow in profiling
514
- // if (isDevDebugbreak()) {
515
- // for (let path of fullyUnwatchedPaths) {
516
- // auditLog("clientWatcher UNWATCH", { path });
517
- // }
518
- // for (let path of fullyUnwatchedParents) {
519
- // auditLog("clientWatcher UNWATCH PARENT", { path });
520
- // }
521
- // }
522
- for (let path of fullyUnwatchedPaths) {
523
- this.pendingUnwatches.set(path, expiryTime);
524
- }
525
- for (let path of fullyUnwatchedParents) {
526
- this.pendingParentUnwatches.set(path, expiryTime);
527
- }
528
- this.ensureUnwatching();
529
- }
530
- private ensureUnwatching = lazy(() => {
531
- let waitTime = Math.max(1000 * 5, ClientWatcher.WATCH_STICK_TIME - 1000);
532
- const self = this;
533
- runInfinitePoll(waitTime, async function ensureUnwatching() {
534
- let now = Date.now();
535
- let pathsToUnwatch = new Set<string>();
536
- let parentPathsToUnwatch = new Set<string>();
537
- for (let [path, expiryTime] of self.pendingUnwatches) {
538
- if (expiryTime < now) {
539
- self.pendingUnwatches.delete(path);
540
- pathsToUnwatch.add(path);
541
- }
542
- }
543
- for (let [path, expiryTime] of self.pendingParentUnwatches) {
544
- if (expiryTime < now) {
545
- self.pendingParentUnwatches.delete(path);
546
- parentPathsToUnwatch.add(path);
547
- }
548
- }
549
- if (pathsToUnwatch.size > 0 || parentPathsToUnwatch.size > 0) {
550
- //console.log(red("Unwatching paths"), pathsToUnwatch, parentPathsToUnwatch, Date.now());
551
- let unwatchConfig: WatchConfig = { paths: Array.from(pathsToUnwatch), parentPaths: Array.from(parentPathsToUnwatch), };
552
- // pathWatcher unwatches remotely as well
553
- pathWatcher.unwatchPath({ callback: getOwnNodeId(), ...unwatchConfig });
554
- }
555
- });
556
- });
557
-
558
-
559
- // TODO: EVENTUALLY! We should accept a delta on the watches, which should allow for more efficient updates.
560
-
561
- // NOTE: The callback is only called when synced values change (so if they are already synced, and don't change, the callback won't be called).
562
- // NOTE: Doesn't call the callback until all requested values have finished their initial sync, to prevent too many callback callbacks.
563
- // - You can just fragment your writes into different components to isolate any slow loading parts, so partial
564
- // loading of data really isn't needed.
565
- // NOTE: Takes ownership of paths and parentPaths, so... don't mutate them after calling this!
566
- @measureFnc
567
- public setWatches(watchSpec: WatchSpec) {
568
- // NOTE: Yes, setWatches shows up as being slow in the profiler. I added measureBlock to every single section, and none of them showed up as being slow.
569
- // - My best guess is that after we clobber values in various lookups, but BEFORE we return, the destructor for those old resources runs. So their destruct time gets put in our profiler?
570
- // - Or maybe hack_stripPackedPath, decodeParentFilter, or other functions with heavy temporary allocations.
571
- // - measureBlock also has overhead (which is why I commented them all out), but... It's not nearly enough to account for this.
572
-
573
-
574
- //measureBlock(() => {
575
- watchSpec.order = watchSpec.order ?? nextWatchSpecOrder++;
576
- //}, "ClientWatcher()|watchSpec.order = ...");
577
-
578
- //measureBlock(() => {
579
- this.updateUnwatches(watchSpec);
580
- //}, "ClientWatcher()|updateUnwatches wrapper?");
581
-
582
- //measureBlock(() => {
583
- this.allWatchers.set(watchSpec.callback, watchSpec);
584
- //}, "ClientWatcher()|setWatches|this.allWatcher.set");
585
- //measureBlock(() => {
586
- for (let path of watchSpec.paths) {
587
- let watchers = this.valueFunctionWatchers.get(path);
588
- if (!watchers) {
589
- watchers = new Map();
590
- this.valueFunctionWatchers.set(path, watchers);
591
- }
592
- watchers.set(watchSpec.callback, watchSpec);
593
- this.pendingUnwatches.delete(path);
594
- }
595
- //}, "ClientWatcher()|setWatches|paths");
596
- //measureBlock(() => {
597
- for (let path of watchSpec.parentPaths) {
598
- let basePath = hack_stripPackedPath(path);
599
- let watchersBase = this.parentValueFunctionWatchers.get(basePath);
600
- if (!watchersBase) {
601
- watchersBase = new Map();
602
- this.parentValueFunctionWatchers.set(basePath, watchersBase);
603
- }
604
- let watchers = watchersBase.get(path);
605
- if (!watchers) {
606
- let range = decodeParentFilter(path) || { start: 0, end: 1 };
607
- watchers = {
608
- start: range.start,
609
- end: range.end,
610
- lookup: new Map()
611
- };
612
- watchersBase.set(path, watchers);
613
- }
614
- watchers.lookup.set(watchSpec.callback, watchSpec);
615
- this.pendingParentUnwatches.delete(path);
616
- }
617
- //}, "ClientWatcher()|setWatches|parentPaths");
618
-
619
- if (watchSpec.paths.size === 0 && watchSpec.parentPaths.size === 0) {
620
- return;
621
- }
622
-
623
- //measureBlock(() => {
624
- let pathsArray = Array.from(watchSpec.paths);
625
- let parentPathsArray = Array.from(watchSpec.parentPaths);
626
- let debugName = watchSpec.callback.name;
627
- // Watch it locally as well, so when we get the value we know to trigger the client callback
628
- pathWatcher.watchPath({
629
- callback: getOwnNodeId(),
630
- paths: pathsArray,
631
- parentPaths: parentPathsArray,
632
- debugName,
633
- noInitialTrigger: watchSpec.noInitialTrigger,
634
- });
635
- remoteWatcher.watchLatest({
636
- paths: pathsArray,
637
- parentPaths: parentPathsArray,
638
- debugName,
639
- });
640
- //}, "ClientWatcher()|setWatches|watchCalls");
641
- }
642
-
643
- private lastVersions = registerResource("paths|lastVersion", new Map<number, number>());
644
- private getFreeVersionBase(time: number): number {
645
- this.ensureCleaningUpVersions();
646
- let lastVersion = this.lastVersions.get(time);
647
- let nextVersion = lastVersion === undefined ? 0 : lastVersion + 1;
648
- this.lastVersions.set(time, nextVersion);
649
- return nextVersion;
650
- }
651
- private ensureCleaningUpVersions = lazy(() => {
652
- const self = this;
653
- const THRESHOLD = MAX_CHANGE_AGE * 2;
654
- runInfinitePoll(THRESHOLD, function cleanUpVersions() {
655
- let curThreshold = Date.now() - THRESHOLD;
656
- for (let time of self.lastVersions.keys()) {
657
- if (time < curThreshold) {
658
- self.lastVersions.delete(time);
659
- }
660
- }
661
- });
662
- });
663
-
664
- /** Gets a free write time by incrementing the version, and then ensuring that version isn't reused for the given time. */
665
- public getFreeWriteTime(time: Time | undefined): Time {
666
- if (!time?.time) return getNextTime();
667
- let version = this.getFreeVersionBase(time.time);
668
- return { time: time.time, version, creatorId: getCreatorId() };
669
- }
670
-
671
- // IMPORTANT! The entire path from setValues => localOnValueCallback MUST be synchronous. If it is asynchronous we CANNOT
672
- // correctly prevent watchers from triggering themselves, which can result in either 2X the number of needed evaluations,
673
- // OR infinite evaluations.
674
- // NOTE: This should be called RIGHT after you read the values, otherwise the lock that is created
675
- // may not depend on what was actually read.
676
- // NOTE: If a server goes down, these writes may fail to commit (they will depend on each other, so
677
- // it will be either all or nothing).
678
- // NOTE: This synchronously predicts the values, so you don't need to wait. When the promise resolves
679
- // the values will be committed and they won't belost (although it is still possible for them to become
680
- // rejected).
681
- @measureFnc
682
- public setValues(
683
- config: {
684
- values: Map<string, Value>;
685
- forcedUndefinedWrites?: Set<string>;
686
- eventPaths?: Set<string>;
687
- locks: ReadLock[];
688
- // NOTE: Use clientWatcher.getFreeWriteTime() to ensure this writeTime is valid
689
- // (likely BEFORE you run the function creating these, so you can use it as actual read times).
690
- writeTime?: Time;
691
- /** If you are writing values you KNOW you won't reading back, set this flag to
692
- * prevent caching of the writes clientside (which is a lot more efficient).
693
- */
694
- noWritePrediction?: boolean;
695
- /** See PathValue.event */
696
- eventWrite?: boolean;
697
- /** Causes us to NOT ACTUALLY write values, but just return what we would write, if !dryRun */
698
- dryRun?: boolean;
699
- }
700
- ): PathValue[] {
701
- const { values, locks, eventPaths } = config;
702
-
703
- // We trust our caller to use getFreeWriteTime, as they need to know the exact time
704
- // they are using so they can use it for their readTimes
705
- let writeTime = config.writeTime;
706
- if (!writeTime || writeTime.time === 0) {
707
- writeTime = getNextTime();
708
- }
709
-
710
- let event = !!config.eventWrite;
711
-
712
- let debugName = getOwnMachineId().slice(0, 8);
713
- if (this.activeWatchSpec?.debugName) {
714
- debugName += "|" + this.activeWatchSpec.debugName;
715
- }
716
-
717
- let pathValues: PathValue[] = [];
718
- for (let [path, value] of values) {
719
- let valueIsEvent = event;
720
- if (eventPaths && eventPaths.has(path)) {
721
- valueIsEvent = true;
722
- }
723
- let isTransparent = value === undefined || config.forcedUndefinedWrites?.has(path);
724
- let pathValue: PathValue = pathValueCtor({
725
- path,
726
- valid: true,
727
- value,
728
- canGCValue: value === undefined,
729
- time: writeTime,
730
- locks: locks,
731
- lockCount: locks.length,
732
- event: valueIsEvent,
733
- isTransparent,
734
- source: debugName,
735
- });
736
- pathValues.push(pathValue);
737
- }
738
-
739
- if (!config.dryRun) {
740
- pathValueCommitter.commitValues(pathValues, config.noWritePrediction ? undefined : "predictWrites");
741
- }
742
-
743
- return pathValues;
744
- }
745
-
746
- public unwatchEventPaths(paths: Set<string>) {
747
- let relevantWatches = new Set<WatchSpec>();
748
- for (let path of paths) {
749
- let watchers = this.valueFunctionWatchers.get(path);
750
- if (watchers) {
751
- this.valueFunctionWatchers.delete(path);
752
- for (let watcher of watchers.values()) {
753
- watcher.paths.delete(path);
754
- relevantWatches.add(watcher);
755
- }
756
- }
757
- let basePath = hack_stripPackedPath(path);
758
- let parentWatcher = this.parentValueFunctionWatchers.get(basePath);
759
- if (parentWatcher) {
760
- let watcherObj = parentWatcher.get(path);
761
- if (watcherObj) {
762
- parentWatcher.delete(path);
763
- if (parentWatcher.size === 0) {
764
- this.parentValueFunctionWatchers.delete(basePath);
765
- }
766
- for (let watcher of watcherObj.lookup.values()) {
767
- watcher.parentPaths.delete(path);
768
- relevantWatches.add(watcher);
769
- }
770
- }
771
- }
772
- }
773
- for (let watcher of relevantWatches) {
774
- watcher.unwatchEventPaths?.(paths);
775
- }
776
- remoteWatcher.unwatchEventPaths({ paths: Array.from(paths), parentPaths: Array.from(paths), });
777
- }
778
-
779
- public pathHasAnyWatchers(path: string) {
780
- let parentPath = getParentPathStr(path);
781
- return !!this.valueFunctionWatchers.get(path)?.size || !!this.parentValueFunctionWatchers.get(parentPath)?.size;
782
- }
783
- public getWatchersForPath(path: string) {
784
- let matched = new Set<WatchSpec>();
785
- let parentPath = getParentPathStr(path);
786
- let watchers = this.valueFunctionWatchers.get(path);
787
- if (watchers) {
788
- for (let watcher of watchers.values()) {
789
- matched.add(watcher);
790
- }
791
- }
792
- let parentWatchers = this.parentValueFunctionWatchers.get(parentPath);
793
- if (parentWatchers) {
794
- for (let [_, watcherObj] of parentWatchers) {
795
- for (let watcher of watcherObj.lookup.values()) {
796
- if (!matchesParentRangeFilter({ parentPath, fullPath: path, start: watcherObj.start, end: watcherObj.end })) {
797
- continue;
798
- }
799
- matched.add(watcher);
800
- }
801
- }
802
- }
803
- return matched;
804
- }
805
- }
806
- export const clientWatcher = new ClientWatcher();
807
- (globalThis as any).clientWatcher = clientWatcher;
808
- (globalThis as any).ClientWatcher = ClientWatcher;
809
-
810
- void Promise.resolve().finally(() => {
811
- authorityStorage.watchEventRemovals(x => clientWatcher.unwatchEventPaths(x));
812
- pathWatcher.watchAllLocalTriggers((x, y) => clientWatcher.localOnValueCallback(x, y));
813
- });
814
-
815
-
1
+ /**
2
+ Does everything needed to synchronize values with PathValueController, including
3
+ storing the values. While this can result in redundant storage, it helps keep
4
+ our strictly synchronized values, with values we just get from another node.
5
+ - We also don't consider rejections at all, and just assume there is no
6
+ contention in this file.
7
+ - Keys based on baseTime, so our values can be correctly clobbered by the remote.
8
+
9
+ setValues
10
+ - For writing values
11
+ getValue
12
+ - For getting values
13
+ setWatches
14
+ - For knowing when values change
15
+ */
16
+
17
+ import { SocketFunction } from "socket-function/SocketFunction";
18
+ import { cache, lazy } from "socket-function/src/caching";
19
+ import { binarySearchIndex, isNode, recursiveFreeze, sort } from "socket-function/src/misc";
20
+ import { logErrors } from "../errors";
21
+ import { getParentPathStr, getPathFromStr, hack_stripPackedPath } from "../path";
22
+ import { measureBlock, measureFnc } from "socket-function/src/profiling/measure";
23
+ import { pathValueCommitter, PathValueController } from "../0-path-value-core/PathValueController";
24
+ import { PathValue, Value, getNextTime, Time, ReadLock, MAX_CHANGE_AGE, authorityStorage, getCreatorId, WatchConfig } from "../0-path-value-core/pathValueCore";
25
+ import { pathWatcher } from "../0-path-value-core/PathWatcher";
26
+ import { blue, green, red } from "socket-function/src/formatting/logColors";
27
+ import { MaybePromise } from "socket-function/src/types";
28
+ import { batchFunction, batchFunctionNone, runInfinitePoll } from "socket-function/src/batching";
29
+ import { registerResource } from "../diagnostics/trackResources";
30
+ import debugbreak from "debugbreak";
31
+ import { ActionsHistory } from "../diagnostics/ActionsHistory";
32
+ import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
33
+ import { PromiseObj } from "../promise";
34
+ import { getOwnMachineId } from "../-a-auth/certs";
35
+ import { remoteWatcher } from "./RemoteWatcher";
36
+ import { heapTagObj } from "../diagnostics/heapTag";
37
+ import { isDevDebugbreak } from "../config";
38
+ import { auditLog } from "../0-path-value-core/auditLogs";
39
+ import { decodeParentFilter, matchesParentRangeFilter } from "../0-path-value-core/hackedPackedPathParentFiltering";
40
+
41
+ const pathValueCtor = heapTagObj("PathValue");
42
+
43
+ export interface WatchSpecData {
44
+ paths: Set<string>;
45
+ pathSources: Set<PathValue> | undefined;
46
+ newParentsSynced: Set<string>;
47
+ extraReasons?: string[];
48
+ }
49
+
50
+ export interface WatchSpec {
51
+ debugName?: string;
52
+
53
+ // Run order is determined by the lowest [orderGroup, order]. It is HIGHLY recommended
54
+ // to use orderGroup, with low values (0 is the default, so use 1 to run later,
55
+ // and -1 to run earlier).
56
+ // - Using a large number of unique orderGroups (for example, a unique group per callback),
57
+ // will slow down callbacks.
58
+ // - "order" is defaulted to a good default (the construct order), and so setting it will often
59
+ // result in callbacks being given too high of a priority.
60
+ order?: number;
61
+ orderGroup?: number;
62
+
63
+ // callback is the key used to update watches, if the same callback is used
64
+ // it won't start a new watch, but will instead update the paths used.
65
+ callback: (changed: WatchSpecData) => void;
66
+ unwatchEventPaths?: (paths: Set<string>) => void;
67
+ paths: Set<string>;
68
+ parentPaths: Set<string>;
69
+ /** For PathValueProxyWatcher. BUT, not in the Querysub routing service, or most backend middle-man type services. */
70
+ noInitialTrigger?: boolean;
71
+
72
+ // IMPORTANT! Any variables that should be persisted, must be copied over in updateUnwatches.
73
+ // Otherwise we don't keep them. We do this for GC reasons (I think?).
74
+ // - We might want to try reusing the same object... it should greatly reduce our GC overhead...
75
+ breakOnNextTrigger?: boolean;
76
+ triggerCount?: number;
77
+ }
78
+
79
+ let nextWatchSpecOrder = 1;
80
+ export class ClientWatcher {
81
+ public static DEBUG_READS = false;
82
+ public static DEBUG_WRITES = false;
83
+ public static DEBUG_READS_EXPANDED = false;
84
+ public static DEBUG_WRITES_EXPANDED = false;
85
+ public static DEBUG_TRIGGERS?: "light" | "heavy";
86
+ public static DEBUG_SOURCES = false;
87
+
88
+ /** How long we keep watching, despite a value no longer being needed. */
89
+ public static WATCH_STICK_TIME = MAX_CHANGE_AGE * 2;
90
+ public static MAX_TRIGGER_TIME_BROWSER = 1000 * 5;
91
+ public static MAX_TRIGGER_TIME_NODEJS = 1000 * 300;
92
+
93
+
94
+ private valueFunctionWatchers = registerResource("paths|valueFunctionWatchers", new Map<string, Map<WatchSpec["callback"], WatchSpec>>());
95
+ // hack_stripPackedPath(path) =>
96
+ private parentValueFunctionWatchers = registerResource("paths|parentValueFunctionWatchers", new Map<string,
97
+ // path =>
98
+ Map<string, {
99
+ // start: number;
100
+ // end: number;
101
+ lookup: Map<WatchSpec["callback"], WatchSpec>
102
+ }>
103
+ >());
104
+ private allWatchers = registerResource("paths|clientWatcher.allWatchers", new Map<WatchSpec["callback"], WatchSpec>());
105
+
106
+ // path => expiryTime
107
+ private pendingUnwatches = registerResource("paths|pendingUnwatches", new Map<string, number>());
108
+ private pendingParentUnwatches = registerResource("paths|pendingParentUnwatches", new Map<string, number>());
109
+
110
+
111
+ private inLoop = false;
112
+ private isLoopSynchronous = false;
113
+ private pendingTriggered: Map<WatchSpec, WatchSpecData> | undefined;
114
+
115
+ private preTrigger(spec: WatchSpec) {
116
+ spec.triggerCount = (spec.triggerCount ?? 0) + 1;
117
+ if (spec.breakOnNextTrigger) {
118
+ spec.breakOnNextTrigger = false;
119
+ debugger;
120
+ }
121
+ }
122
+
123
+ private triggerWatcher(spec: WatchSpec, config?: { synchronous?: boolean }) {
124
+ this.preTrigger(spec);
125
+ let watchedTriggers = this.pendingTriggered = this.pendingTriggered || (new Map() as never);
126
+ let trigger = watchedTriggers.get(spec);
127
+ if (!trigger) {
128
+ trigger = {
129
+ pathSources: ClientWatcher.DEBUG_SOURCES ? new Set() : undefined,
130
+ newParentsSynced: new Set(),
131
+ paths: new Set(),
132
+ };
133
+ watchedTriggers.set(spec, trigger);
134
+ }
135
+ this.triggerWatchLoop(config);
136
+ return trigger;
137
+ }
138
+ private beforeTriggerDone: Promise<void> | undefined;
139
+ private onTriggerDone: Promise<void> | undefined;
140
+ public activeWatchSpec: WatchSpec | undefined;
141
+ private ignoreWatch: ((spec: WatchSpec, source: PathValue | string) => boolean) | undefined;
142
+ // Needed for typesafecss. Same as onTriggerDone, but fires first.
143
+ public waitForBeforeTriggerFinished() { return this.beforeTriggerDone; }
144
+ public waitForTriggerFinished() { return this.onTriggerDone; }
145
+ public isCurrentLoopSynchronous() { return this.isLoopSynchronous; }
146
+
147
+ @measureFnc
148
+ public localOnValueCallback(values: PathValue[], parentsSynced: Set<string>) {
149
+ if (values.length === 0 && parentsSynced.size === 0) return;
150
+ const onTrigger = (spec: WatchSpec, path: string, source?: PathValue) => {
151
+ if (
152
+ ClientWatcher.DEBUG_TRIGGERS === "heavy"
153
+ && !this.pendingTriggered?.has(spec)
154
+ ) {
155
+ let sourceName = "";
156
+ if (this.activeWatchSpec) {
157
+ sourceName = ` (inside ${this.activeWatchSpec.debugName})`;
158
+ } else if (source?.source) {
159
+ sourceName = ` (remote ${source.source.split("|").at(-1)})`;
160
+ }
161
+ console.log(`${blue("QUEUEING TRIGGER")} ${spec.debugName} ${sourceName}`);
162
+ console.log(` DUE TO WRITE ${getPathFromStr(path).join(".")}`);
163
+ }
164
+ };
165
+
166
+ const triggerWatcher = (spec: WatchSpec, source: PathValue) => {
167
+ // Ignore self writes, otherwise local writes as simple as `x++`, will trigger an infinite loop.
168
+ if (this.ignoreWatch?.(spec, source)) return;
169
+ let trigger = this.triggerWatcher(spec);
170
+ if (trigger.pathSources) {
171
+ trigger.pathSources.add(source);
172
+ }
173
+ trigger.paths.add(source.path);
174
+ onTrigger(spec, source.path, source);
175
+ };
176
+ const triggerWatcherParent = (spec: WatchSpec, parent: string) => {
177
+ if (this.ignoreWatch?.(spec, parent)) return;
178
+ let trigger = this.triggerWatcher(spec);
179
+ trigger.newParentsSynced.add(parent);
180
+ onTrigger(spec, parent);
181
+ };
182
+ for (let value of values) {
183
+ let watchers = this.valueFunctionWatchers.get(value.path);
184
+ if (watchers) {
185
+ for (let watcher of watchers.values()) {
186
+ triggerWatcher(watcher, value);
187
+ }
188
+ }
189
+ let parentPath = getParentPathStr(value.path);
190
+ let parentWatchers = this.parentValueFunctionWatchers.get(parentPath);
191
+ if (parentWatchers) {
192
+ // NOTE: We don't filter by path shard here
193
+ for (let [packedPath, { lookup }] of parentWatchers) {
194
+ if (!matchesParentRangeFilter({ parentPath, fullPath: value.path, packedPath })) continue;
195
+ for (let watcher of lookup.values()) {
196
+ triggerWatcher(watcher, value);
197
+ triggerWatcherParent(watcher, parentPath);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ // NOTE: We need to keep track of the parents being synced independently of the values, as you might request for all the child values, and there could be none. And so, this will be the only pathway which can actually trigger the watcher to rerun. And when it reruns, a previous call should have ingested the fact that the parent was synced to the authority storage. And so it should now decide that it's synced and stop waiting for the parent to sync values and commit (finish, render, etc, whatever it wants to do when it's fully synced).
203
+ for (let path of parentsSynced) {
204
+ let basePath = hack_stripPackedPath(path);
205
+ let watchers = this.parentValueFunctionWatchers.get(basePath);
206
+ if (watchers) {
207
+ let realObj = watchers.get(path);
208
+ if (realObj) {
209
+ for (let watcher of realObj.lookup.values()) {
210
+ triggerWatcherParent(watcher, path);
211
+ }
212
+ }
213
+ }
214
+ }
215
+ // Trigger all children of parents synced
216
+ // NOTE: This is the counterpoint to RemoteWatcher's removal of extra requests when we are already syncing the parent path. If it's not going to request them, we're not going to get the paths, so we need to trigger based on parents.
217
+ // - We ignore hack paths here (that path in range requests in parent paths), because RemoteWatcher also ignores them (so hacked parent paths won't stop value path syncs there), so they will be received normally.
218
+ // NOTE: This will probably be slow, but we shouldn't be receiving parent syncs very frequently. Or maybe we will, and maybe this will just be incredibly slow.
219
+ if (parentsSynced.size > 0) {
220
+ measureBlock(() => {
221
+ for (let parentPath of parentsSynced) {
222
+ for (let [path, lookup] of this.valueFunctionWatchers.entries()) {
223
+ if (!path.startsWith(parentPath)) continue;
224
+ if (getParentPathStr(path) !== parentPath) continue;
225
+ for (let watch of lookup.values()) {
226
+ let trigger = this.triggerWatcher(watch);
227
+ trigger.paths.add(path);
228
+ onTrigger(watch, path);
229
+ }
230
+ }
231
+ }
232
+ }, "ClientWatcher|triggerChildrenOfParentsSynced");
233
+ }
234
+ }
235
+
236
+ public debugBreakOnNextTrigger() {
237
+ if (!this.activeWatchSpec) throw new Error(`Break on next trigger must be called inside of a clientWatcher trigger`);
238
+ this.activeWatchSpec.breakOnNextTrigger = true;
239
+ }
240
+
241
+ public explicitlyTriggerWatcher(callback: WatchSpec["callback"], config?: { synchronous?: boolean }) {
242
+ let watcher = this.allWatchers.get(callback);
243
+ if (!watcher) throw new Error(`No watcher found for callback ${callback.toString()}`);
244
+ this.triggerWatcher(watcher, config);
245
+ }
246
+
247
+ private triggerWatchLoop(config?: { synchronous?: boolean }) {
248
+ // A fairly standard trigger loop. Batch synchronous triggers for efficiency's sake
249
+ if (this.inLoop) return;
250
+ this.inLoop = true;
251
+ let beforeTriggerDone = new PromiseObj();
252
+ let triggerPromiseObj = new PromiseObj();
253
+ this.beforeTriggerDone = beforeTriggerDone.promise;
254
+ this.onTriggerDone = triggerPromiseObj.promise;
255
+ const doLoop = () => {
256
+ try {
257
+ this.innerTriggerLoop();
258
+ } finally {
259
+ this.inLoop = false;
260
+ this.isLoopSynchronous = false;
261
+ beforeTriggerDone.resolve();
262
+ triggerPromiseObj.resolve();
263
+ this.beforeTriggerDone = undefined;
264
+ this.onTriggerDone = undefined;
265
+ }
266
+ };
267
+ if (config?.synchronous) {
268
+ this.isLoopSynchronous = true;
269
+ doLoop();
270
+ } else {
271
+ void Promise.resolve().finally(doLoop);
272
+ }
273
+ }
274
+ @measureFnc
275
+ private innerTriggerLoop() {
276
+ let loopCount = 0;
277
+ let triggerCount = 0;
278
+ let groupStart = (
279
+ ClientWatcher.DEBUG_TRIGGERS === "light" && console.groupCollapsed
280
+ || ClientWatcher.DEBUG_TRIGGERS === "heavy" && console.group
281
+ || undefined
282
+ );
283
+
284
+ for (let callback of this.loopStartCallbacks) {
285
+ try {
286
+ callback();
287
+ } catch (e) {
288
+ logErrors(e);
289
+ }
290
+ }
291
+ let MAX_TRIGGER_TIME = isNode() ? ClientWatcher.MAX_TRIGGER_TIME_NODEJS : ClientWatcher.MAX_TRIGGER_TIME_BROWSER;
292
+
293
+ let time = Date.now();
294
+ while (true) {
295
+ let currentTriggered = this.pendingTriggered;
296
+ this.pendingTriggered = undefined;
297
+ if (!currentTriggered?.size) break;
298
+
299
+ if (ClientWatcher.DEBUG_TRIGGERS) {
300
+ console.log(" ");
301
+ }
302
+ groupStart?.(`${blue("TRIGGERS LOOP ITERATION")} ${loopCount++}`);
303
+
304
+ let sorted = Array.from(currentTriggered);
305
+ sorted.sort((a, b) => {
306
+ let orderDiff = (a[0].orderGroup ?? 0) - (b[0].orderGroup ?? 0);
307
+ if (orderDiff !== 0) return orderDiff;
308
+ return (a[0].order ?? 0) - (b[0].order ?? 0);
309
+ });
310
+ if (ClientWatcher.DEBUG_TRIGGERS) {
311
+ for (let [trigger] of sorted) {
312
+ console.log(" " + trigger.debugName + " " + (trigger.orderGroup ?? 0) + "|" + (trigger.order ?? 0));
313
+ }
314
+ console.groupEnd();
315
+ }
316
+ let curIndex = 0;
317
+ this.ignoreWatch = (spec, source) => {
318
+ this.preTrigger(spec);
319
+ // Ignore it if we are handling it (self triggers are almost always just due to `x++`, and are virtually
320
+ // never intended to actually self trigger. And if they are, then they can just detach with
321
+ // Promise.resolve.finally(...))
322
+ // ALSO ignore if we are going to handle it. There's no reason to queue another watch for something
323
+ // we are about to handle.
324
+ for (let checkIndex = curIndex; checkIndex < sorted.length; checkIndex++) {
325
+ if (sorted[checkIndex][0] === spec) {
326
+ let watchSpecData = sorted[checkIndex][1];
327
+ if (typeof source === "string") {
328
+ watchSpecData.newParentsSynced.add(source);
329
+ } else {
330
+ if (watchSpecData.pathSources) {
331
+ watchSpecData.pathSources.add(source);
332
+ }
333
+ watchSpecData.paths.add(source.path);
334
+ }
335
+ return true;
336
+ }
337
+ }
338
+
339
+ // If they care about the order, and it is higher than the current order, then insert it into the current
340
+ if (spec.order !== undefined && (
341
+ (spec.order ?? 0) > (sorted[curIndex][0].order ?? 0)
342
+ && (spec.orderGroup ?? 0) === (sorted[curIndex][0].orderGroup ?? 0)
343
+ || (spec.orderGroup ?? 0) > (sorted[curIndex][0].orderGroup ?? 0)
344
+ )) {
345
+ if (ClientWatcher.DEBUG_TRIGGERS === "heavy") {
346
+ console.log(" (insert new trigger)");
347
+ console.log(" " + spec.debugName + " " + (spec.orderGroup ?? 0) + "|" + (spec.order ?? 0));
348
+ }
349
+ // Find the last insert >= the current order (check for equals so we can insert at the end
350
+ // duplicate order values).
351
+ let insertIndex = sorted.findLastIndex(x =>
352
+ (spec.order ?? 0) >= (x[0].order ?? 0)
353
+ && (spec.orderGroup ?? 0) === (x[0].orderGroup ?? 0)
354
+ || (spec.orderGroup ?? 0) > (x[0].orderGroup ?? 0)
355
+ );
356
+ if (insertIndex === -1) {
357
+ insertIndex = sorted.length - 1;
358
+ }
359
+ insertIndex++;
360
+ let watchSpecData: WatchSpecData = {
361
+ paths: new Set(),
362
+ pathSources: ClientWatcher.DEBUG_SOURCES ? new Set() : undefined,
363
+ newParentsSynced: new Set(),
364
+ };
365
+ if (typeof source === "string") {
366
+ watchSpecData.newParentsSynced.add(source);
367
+ } else {
368
+ if (watchSpecData.pathSources) {
369
+ watchSpecData.pathSources.add(source);
370
+ }
371
+ watchSpecData.paths.add(source.path);
372
+ }
373
+ sorted.splice(insertIndex, 0, [spec, watchSpecData]);
374
+ return true;
375
+ }
376
+ return false;
377
+ };
378
+
379
+ let stoppedEarly = false;
380
+ for (; curIndex < sorted.length; curIndex++) {
381
+ let [watchSpec, data] = sorted[curIndex];
382
+ triggerCount++;
383
+ if (ClientWatcher.DEBUG_TRIGGERS === "heavy") {
384
+ console.log(`${green("RUNNING TRIGGER")} ${watchSpec.debugName}`);
385
+ }
386
+
387
+ try {
388
+ this.activeWatchSpec = watchSpec;
389
+ watchSpec.callback(data);
390
+ } catch (e) {
391
+ void Promise.resolve().finally(() => { throw e; });
392
+ } finally {
393
+ this.activeWatchSpec = undefined;
394
+ }
395
+ if (
396
+ Date.now() - time > MAX_TRIGGER_TIME
397
+ && !isDevDebugbreak()
398
+ ) {
399
+ stoppedEarly = true;
400
+ break;
401
+ }
402
+ }
403
+ this.ignoreWatch = undefined;
404
+
405
+ if (ClientWatcher.DEBUG_TRIGGERS) {
406
+ console.log(" ");
407
+ }
408
+
409
+ if (stoppedEarly) {
410
+ const history = 10;
411
+ let remaining = (
412
+ sorted
413
+ .slice(curIndex, curIndex + history)
414
+ .map(x => `${x[0].debugName} (${Array.from(x[1].paths).map(x => getPathFromStr(x).join(".")).join(" | ")})`)
415
+ .join(", ")
416
+ );
417
+ console.error(red(`Too much time spent in trigger loop, aborting loop. Triggered ${triggerCount} triggers. Remaining triggers: ${remaining}`), triggerCount);
418
+ break;
419
+ }
420
+ }
421
+
422
+ let finalStats = { loopCount, triggerCount };
423
+ for (let callback of this.loopEndCallbacks) {
424
+ try {
425
+ callback(finalStats);
426
+ } catch (e) {
427
+ logErrors(e);
428
+ }
429
+ }
430
+
431
+ time = Date.now() - time;
432
+ if (ClientWatcher.DEBUG_TRIGGERS) {
433
+ console.log(`${green("TRIGGERS LOOP FINISHED")} ${loopCount} loops, ${triggerCount} triggers, in ${time}ms`);
434
+ console.log(" ");
435
+ console.log(" ");
436
+ }
437
+ }
438
+
439
+ private loopStartCallbacks = new Set<() => void>();
440
+ private loopEndCallbacks = new Set<(info: { loopCount: number; triggerCount: number; }) => void>();
441
+ public onLoopStart(callback: () => void) {
442
+ this.loopStartCallbacks.add(callback);
443
+ }
444
+ public onLoopEnd(
445
+ callback: (info: {
446
+ loopCount: number;
447
+ triggerCount: number;
448
+ }) => void
449
+ ) {
450
+ this.loopEndCallbacks.add(callback);
451
+ }
452
+
453
+ public unwatch(callback: WatchSpec["callback"]) {
454
+ this.updateUnwatches({
455
+ callback,
456
+ paths: new Set(),
457
+ parentPaths: new Set(),
458
+ });
459
+ this.allWatchers.delete(callback);
460
+ }
461
+
462
+ @measureFnc
463
+ private updateUnwatches(watchSpec: WatchSpec) {
464
+ const { callback, paths, parentPaths } = watchSpec;
465
+ let prevSpec = this.allWatchers.get(callback);
466
+ if (!prevSpec) return;
467
+ if (prevSpec) {
468
+ watchSpec.order = prevSpec.order;
469
+ watchSpec.triggerCount = prevSpec.triggerCount;
470
+ watchSpec.breakOnNextTrigger = prevSpec.breakOnNextTrigger;
471
+ }
472
+
473
+ let fullyUnwatchedPaths = new Set<string>();
474
+ let fullyUnwatchedParents = new Set<string>();
475
+
476
+ // Remove removed paths
477
+ for (let path of prevSpec.paths) {
478
+ if (paths.has(path)) continue;
479
+ let watchers = this.valueFunctionWatchers.get(path);
480
+ if (!watchers) continue;
481
+ watchers.delete(callback);
482
+ if (watchers.size === 0) {
483
+ fullyUnwatchedPaths.add(path);
484
+ this.valueFunctionWatchers.delete(path);
485
+ }
486
+ }
487
+
488
+ // Remove removed parents
489
+ for (let path of prevSpec.parentPaths) {
490
+ if (parentPaths.has(path)) continue;
491
+ let basePath = hack_stripPackedPath(path);
492
+ let watchersBase = this.parentValueFunctionWatchers.get(basePath);
493
+ if (!watchersBase) continue;
494
+ let watchers = watchersBase.get(path);
495
+ if (!watchers) continue;
496
+ watchers.lookup.delete(callback);
497
+ if (watchers.lookup.size === 0) {
498
+ watchersBase.delete(path);
499
+ fullyUnwatchedParents.add(path);
500
+ }
501
+ if (watchersBase.size === 0) {
502
+ this.parentValueFunctionWatchers.delete(basePath);
503
+ }
504
+ }
505
+
506
+ prevSpec.paths = paths;
507
+ prevSpec.parentPaths = parentPaths;
508
+ watchSpec = prevSpec;
509
+
510
+ let expiryTime = Date.now() + ClientWatcher.WATCH_STICK_TIME;
511
+ // Commented out, as this is showing up as a bit slow in profiling
512
+ // if (isDevDebugbreak()) {
513
+ // for (let path of fullyUnwatchedPaths) {
514
+ // auditLog("clientWatcher UNWATCH", { path });
515
+ // }
516
+ // for (let path of fullyUnwatchedParents) {
517
+ // auditLog("clientWatcher UNWATCH PARENT", { path });
518
+ // }
519
+ // }
520
+ for (let path of fullyUnwatchedPaths) {
521
+ this.pendingUnwatches.set(path, expiryTime);
522
+ }
523
+ for (let path of fullyUnwatchedParents) {
524
+ this.pendingParentUnwatches.set(path, expiryTime);
525
+ }
526
+ this.ensureUnwatching();
527
+ }
528
+ private ensureUnwatching = lazy(() => {
529
+ let waitTime = Math.max(1000 * 5, ClientWatcher.WATCH_STICK_TIME - 1000);
530
+ const self = this;
531
+ runInfinitePoll(waitTime, async function ensureUnwatching() {
532
+ let now = Date.now();
533
+ let pathsToUnwatch = new Set<string>();
534
+ let parentPathsToUnwatch = new Set<string>();
535
+ for (let [path, expiryTime] of self.pendingUnwatches) {
536
+ if (expiryTime < now) {
537
+ self.pendingUnwatches.delete(path);
538
+ pathsToUnwatch.add(path);
539
+ }
540
+ }
541
+ for (let [path, expiryTime] of self.pendingParentUnwatches) {
542
+ if (expiryTime < now) {
543
+ self.pendingParentUnwatches.delete(path);
544
+ parentPathsToUnwatch.add(path);
545
+ }
546
+ }
547
+ if (pathsToUnwatch.size > 0 || parentPathsToUnwatch.size > 0) {
548
+ //console.log(red("Unwatching paths"), pathsToUnwatch, parentPathsToUnwatch, Date.now());
549
+ let unwatchConfig: WatchConfig = { paths: Array.from(pathsToUnwatch), parentPaths: Array.from(parentPathsToUnwatch), };
550
+ // pathWatcher unwatches remotely as well
551
+ pathWatcher.unwatchPath({ callback: getOwnNodeId(), ...unwatchConfig });
552
+ }
553
+ });
554
+ });
555
+
556
+
557
+ // TODO: EVENTUALLY! We should accept a delta on the watches, which should allow for more efficient updates.
558
+
559
+ // NOTE: The callback is only called when synced values change (so if they are already synced, and don't change, the callback won't be called).
560
+ // NOTE: Doesn't call the callback until all requested values have finished their initial sync, to prevent too many callback callbacks.
561
+ // - You can just fragment your writes into different components to isolate any slow loading parts, so partial
562
+ // loading of data really isn't needed.
563
+ // NOTE: Takes ownership of paths and parentPaths, so... don't mutate them after calling this!
564
+ @measureFnc
565
+ public setWatches(watchSpec: WatchSpec) {
566
+ // NOTE: Yes, setWatches shows up as being slow in the profiler. I added measureBlock to every single section, and none of them showed up as being slow.
567
+ // - My best guess is that after we clobber values in various lookups, but BEFORE we return, the destructor for those old resources runs. So their destruct time gets put in our profiler?
568
+ // - Or maybe hack_stripPackedPath, decodeParentFilter, or other functions with heavy temporary allocations.
569
+ // - measureBlock also has overhead (which is why I commented them all out), but... It's not nearly enough to account for this.
570
+
571
+
572
+ //measureBlock(() => {
573
+ watchSpec.order = watchSpec.order ?? nextWatchSpecOrder++;
574
+ //}, "ClientWatcher()|watchSpec.order = ...");
575
+
576
+ //measureBlock(() => {
577
+ this.updateUnwatches(watchSpec);
578
+ //}, "ClientWatcher()|updateUnwatches wrapper?");
579
+
580
+ //measureBlock(() => {
581
+ this.allWatchers.set(watchSpec.callback, watchSpec);
582
+ //}, "ClientWatcher()|setWatches|this.allWatcher.set");
583
+ //measureBlock(() => {
584
+ for (let path of watchSpec.paths) {
585
+ let watchers = this.valueFunctionWatchers.get(path);
586
+ if (!watchers) {
587
+ watchers = new Map();
588
+ this.valueFunctionWatchers.set(path, watchers);
589
+ }
590
+ watchers.set(watchSpec.callback, watchSpec);
591
+ this.pendingUnwatches.delete(path);
592
+ }
593
+ //}, "ClientWatcher()|setWatches|paths");
594
+ //measureBlock(() => {
595
+ for (let path of watchSpec.parentPaths) {
596
+ let basePath = hack_stripPackedPath(path);
597
+ let watchersBase = this.parentValueFunctionWatchers.get(basePath);
598
+ if (!watchersBase) {
599
+ watchersBase = new Map();
600
+ this.parentValueFunctionWatchers.set(basePath, watchersBase);
601
+ }
602
+ let watchers = watchersBase.get(path);
603
+ if (!watchers) {
604
+ watchers = {
605
+ lookup: new Map()
606
+ };
607
+ watchersBase.set(path, watchers);
608
+ }
609
+ watchers.lookup.set(watchSpec.callback, watchSpec);
610
+ this.pendingParentUnwatches.delete(path);
611
+ }
612
+ //}, "ClientWatcher()|setWatches|parentPaths");
613
+
614
+ if (watchSpec.paths.size === 0 && watchSpec.parentPaths.size === 0) {
615
+ return;
616
+ }
617
+
618
+ //measureBlock(() => {
619
+ let pathsArray = Array.from(watchSpec.paths);
620
+ let parentPathsArray = Array.from(watchSpec.parentPaths);
621
+ let debugName = watchSpec.callback.name;
622
+ pathWatcher.watchPath({
623
+ nodeId: getOwnNodeId(),
624
+ paths: pathsArray,
625
+ parentPaths: parentPathsArray,
626
+ debugName,
627
+ noInitialTrigger: watchSpec.noInitialTrigger,
628
+ });
629
+ //}, "ClientWatcher()|setWatches|watchCalls");
630
+ }
631
+
632
+ private lastVersions = registerResource("paths|lastVersion", new Map<number, number>());
633
+ private getFreeVersionBase(time: number): number {
634
+ this.ensureCleaningUpVersions();
635
+ let lastVersion = this.lastVersions.get(time);
636
+ let nextVersion = lastVersion === undefined ? 0 : lastVersion + 1;
637
+ this.lastVersions.set(time, nextVersion);
638
+ return nextVersion;
639
+ }
640
+ private ensureCleaningUpVersions = lazy(() => {
641
+ const self = this;
642
+ const THRESHOLD = MAX_CHANGE_AGE * 2;
643
+ runInfinitePoll(THRESHOLD, function cleanUpVersions() {
644
+ let curThreshold = Date.now() - THRESHOLD;
645
+ for (let time of self.lastVersions.keys()) {
646
+ if (time < curThreshold) {
647
+ self.lastVersions.delete(time);
648
+ }
649
+ }
650
+ });
651
+ });
652
+
653
+ /** Gets a free write time by incrementing the version, and then ensuring that version isn't reused for the given time. */
654
+ public getFreeWriteTime(time: Time | undefined): Time {
655
+ if (!time?.time) return getNextTime();
656
+ let version = this.getFreeVersionBase(time.time);
657
+ return { time: time.time, version, creatorId: getCreatorId() };
658
+ }
659
+
660
+ // IMPORTANT! The entire path from setValues => localOnValueCallback MUST be synchronous. If it is asynchronous we CANNOT
661
+ // correctly prevent watchers from triggering themselves, which can result in either 2X the number of needed evaluations,
662
+ // OR infinite evaluations.
663
+ // NOTE: This should be called RIGHT after you read the values, otherwise the lock that is created
664
+ // may not depend on what was actually read.
665
+ // NOTE: If a server goes down, these writes may fail to commit (they will depend on each other, so
666
+ // it will be either all or nothing).
667
+ // NOTE: This synchronously predicts the values, so you don't need to wait. When the promise resolves
668
+ // the values will be committed and they won't belost (although it is still possible for them to become
669
+ // rejected).
670
+ @measureFnc
671
+ public setValues(
672
+ config: {
673
+ values: Map<string, Value>;
674
+ forcedUndefinedWrites?: Set<string>;
675
+ eventPaths?: Set<string>;
676
+ locks: ReadLock[];
677
+ // NOTE: Use clientWatcher.getFreeWriteTime() to ensure this writeTime is valid
678
+ // (likely BEFORE you run the function creating these, so you can use it as actual read times).
679
+ writeTime?: Time;
680
+ /** If you are writing values you KNOW you won't reading back, set this flag to
681
+ * prevent caching of the writes clientside (which is a lot more efficient).
682
+ */
683
+ noWritePrediction?: boolean;
684
+ /** See PathValue.event */
685
+ eventWrite?: boolean;
686
+ /** Causes us to NOT ACTUALLY write values, but just return what we would write, if !dryRun */
687
+ dryRun?: boolean;
688
+ source?: string;
689
+ }
690
+ ): PathValue[] {
691
+ const { values, locks, eventPaths } = config;
692
+
693
+ // We trust our caller to use getFreeWriteTime, as they need to know the exact time
694
+ // they are using so they can use it for their readTimes
695
+ let writeTime = config.writeTime;
696
+ if (!writeTime || writeTime.time === 0) {
697
+ writeTime = getNextTime();
698
+ }
699
+
700
+ let event = !!config.eventWrite;
701
+
702
+ let source = config.source;
703
+
704
+ if (!source) {
705
+ let debugName = getOwnMachineId().slice(0, 8);
706
+ if (this.activeWatchSpec?.debugName) {
707
+ debugName += "|" + this.activeWatchSpec.debugName;
708
+ }
709
+ source = debugName;
710
+ }
711
+
712
+ let pathValues: PathValue[] = [];
713
+ for (let [path, value] of values) {
714
+ let valueIsEvent = event;
715
+ if (eventPaths && eventPaths.has(path)) {
716
+ valueIsEvent = true;
717
+ }
718
+ let isTransparent = value === undefined || config.forcedUndefinedWrites?.has(path);
719
+ let pathValue: PathValue = pathValueCtor({
720
+ path,
721
+ valid: true,
722
+ value,
723
+ canGCValue: value === undefined,
724
+ time: writeTime,
725
+ locks: locks,
726
+ lockCount: locks.length,
727
+ event: valueIsEvent,
728
+ isTransparent,
729
+ source,
730
+ });
731
+ pathValues.push(pathValue);
732
+ }
733
+
734
+ if (!config.dryRun) {
735
+ pathValueCommitter.commitValues(pathValues, config.noWritePrediction ? undefined : "predictWrites");
736
+ }
737
+
738
+ return pathValues;
739
+ }
740
+
741
+ public unwatchEventPaths(paths: Set<string>) {
742
+ let relevantWatches = new Set<WatchSpec>();
743
+ for (let path of paths) {
744
+ let watchers = this.valueFunctionWatchers.get(path);
745
+ if (watchers) {
746
+ this.valueFunctionWatchers.delete(path);
747
+ for (let watcher of watchers.values()) {
748
+ watcher.paths.delete(path);
749
+ relevantWatches.add(watcher);
750
+ }
751
+ }
752
+ let basePath = hack_stripPackedPath(path);
753
+ let parentWatcher = this.parentValueFunctionWatchers.get(basePath);
754
+ if (parentWatcher) {
755
+ let watcherObj = parentWatcher.get(path);
756
+ if (watcherObj) {
757
+ parentWatcher.delete(path);
758
+ if (parentWatcher.size === 0) {
759
+ this.parentValueFunctionWatchers.delete(basePath);
760
+ }
761
+ for (let watcher of watcherObj.lookup.values()) {
762
+ watcher.parentPaths.delete(path);
763
+ relevantWatches.add(watcher);
764
+ }
765
+ }
766
+ }
767
+ }
768
+ for (let watcher of relevantWatches) {
769
+ watcher.unwatchEventPaths?.(paths);
770
+ }
771
+ remoteWatcher.unwatchEventPaths({ paths: Array.from(paths), parentPaths: Array.from(paths), });
772
+ }
773
+
774
+ public pathHasAnyWatchers(path: string) {
775
+ let parentPath = getParentPathStr(path);
776
+ return !!this.valueFunctionWatchers.get(path)?.size || !!this.parentValueFunctionWatchers.get(parentPath)?.size;
777
+ }
778
+ public getWatchersForPath(path: string) {
779
+ let matched = new Set<WatchSpec>();
780
+ let parentPath = getParentPathStr(path);
781
+ let watchers = this.valueFunctionWatchers.get(path);
782
+ if (watchers) {
783
+ for (let watcher of watchers.values()) {
784
+ matched.add(watcher);
785
+ }
786
+ }
787
+ let parentWatchers = this.parentValueFunctionWatchers.get(parentPath);
788
+ if (parentWatchers) {
789
+ for (let [packedPath, watcherObj] of parentWatchers) {
790
+ for (let watcher of watcherObj.lookup.values()) {
791
+ if (!matchesParentRangeFilter({ parentPath, fullPath: path, packedPath })) continue;
792
+ matched.add(watcher);
793
+ }
794
+ }
795
+ }
796
+ return matched;
797
+ }
798
+ }
799
+ export const clientWatcher = new ClientWatcher();
800
+ (globalThis as any).clientWatcher = clientWatcher;
801
+ (globalThis as any).ClientWatcher = ClientWatcher;
802
+
803
+ void Promise.resolve().finally(() => {
804
+ authorityStorage.watchEventRemovals(x => clientWatcher.unwatchEventPaths(x));
805
+ pathWatcher.setLocalTriggerCallback((x, y) => clientWatcher.localOnValueCallback(x, y));
806
+ });
807
+
808
+