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.
- package/.cursorrules +2 -0
- package/bin/audit-imports.js +4 -0
- package/package.json +7 -4
- package/spec.txt +77 -0
- package/src/-a-archives/archiveCache.ts +9 -4
- package/src/-a-archives/archivesBackBlaze.ts +1039 -1039
- package/src/-a-auth/certs.ts +0 -12
- package/src/-c-identity/IdentityController.ts +12 -3
- package/src/-f-node-discovery/NodeDiscovery.ts +32 -26
- package/src/-g-core-values/NodeCapabilities.ts +12 -2
- package/src/0-path-value-core/AuthorityLookup.ts +239 -0
- package/src/0-path-value-core/LockWatcher2.ts +150 -0
- package/src/0-path-value-core/PathRouter.ts +535 -0
- package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +65 -0
- package/src/0-path-value-core/PathValueCommitter.ts +222 -488
- package/src/0-path-value-core/PathValueController.ts +277 -239
- package/src/0-path-value-core/PathWatcher.ts +534 -0
- package/src/0-path-value-core/ShardPrefixes.ts +31 -0
- package/src/0-path-value-core/ValidStateComputer.ts +303 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +1 -1
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +80 -44
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +13 -16
- package/src/0-path-value-core/auditLogs.ts +2 -0
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +97 -0
- package/src/0-path-value-core/pathValueArchives.ts +490 -492
- package/src/0-path-value-core/pathValueCore.ts +195 -1492
- package/src/0-path-value-core/startupAuthority.ts +74 -0
- package/src/1-path-client/RemoteWatcher.ts +100 -83
- package/src/1-path-client/pathValueClientWatcher.ts +808 -815
- package/src/2-proxy/PathValueProxyWatcher.ts +10 -8
- package/src/2-proxy/archiveMoveHarness.ts +182 -214
- package/src/2-proxy/garbageCollection.ts +9 -8
- package/src/2-proxy/schema2.ts +21 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +206 -180
- package/src/3-path-functions/PathFunctionRunner.ts +943 -766
- package/src/3-path-functions/PathFunctionRunnerMain.ts +5 -3
- package/src/3-path-functions/pathFunctionLoader.ts +2 -2
- package/src/3-path-functions/syncSchema.ts +592 -521
- package/src/4-deploy/deployFunctions.ts +19 -4
- package/src/4-deploy/deployGetFunctionsInner.ts +8 -2
- package/src/4-deploy/deployMain.ts +51 -68
- package/src/4-deploy/edgeClientWatcher.tsx +1 -1
- package/src/4-deploy/edgeNodes.ts +2 -2
- package/src/4-dom/qreact.tsx +2 -4
- package/src/4-dom/qreactTest.tsx +7 -13
- package/src/4-querysub/Querysub.ts +21 -8
- package/src/4-querysub/QuerysubController.ts +45 -29
- package/src/4-querysub/permissions.ts +2 -2
- package/src/4-querysub/querysubPrediction.ts +80 -70
- package/src/4-querysub/schemaHelpers.ts +5 -1
- package/src/5-diagnostics/GenericFormat.tsx +14 -9
- package/src/archiveapps/archiveGCEntry.tsx +9 -2
- package/src/archiveapps/archiveJoinEntry.ts +87 -84
- package/src/archiveapps/archiveMergeEntry.tsx +2 -0
- package/src/bits.ts +19 -0
- package/src/config.ts +21 -3
- package/src/config2.ts +23 -48
- package/src/deployManager/components/DeployPage.tsx +7 -3
- package/src/deployManager/machineSchema.ts +4 -1
- package/src/diagnostics/ActionsHistory.ts +3 -8
- package/src/diagnostics/AuditLogPage.tsx +2 -3
- package/src/diagnostics/FunctionCallInfo.tsx +141 -0
- package/src/diagnostics/FunctionCallInfoState.ts +162 -0
- package/src/diagnostics/MachineThreadInfo.tsx +1 -1
- package/src/diagnostics/NodeViewer.tsx +37 -48
- package/src/diagnostics/SyncTestPage.tsx +241 -0
- package/src/diagnostics/auditImportViolations.ts +185 -0
- package/src/diagnostics/listenOnDebugger.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +10 -4
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +24 -22
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
- package/src/diagnostics/logs/diskLogGlobalContext.ts +1 -0
- package/src/diagnostics/logs/errorNotifications2/logWatcher.ts +1 -3
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +39 -17
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +4 -6
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleInstanceTableView.tsx +36 -5
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +19 -5
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +15 -7
- package/src/diagnostics/logs/lifeCycleAnalysis/NestedLifeCycleInfo.tsx +28 -106
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +2 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMisc.ts +0 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +18 -7
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +3 -0
- package/src/diagnostics/managementPages.tsx +10 -3
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +20 -26
- package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +6 -4
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +2 -2
- package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +7 -9
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +23 -12
- package/src/diagnostics/misc-pages/archiveViewerShared.tsx +1 -1
- package/src/diagnostics/pathAuditer.ts +486 -0
- package/src/diagnostics/pathAuditerCallback.ts +20 -0
- package/src/diagnostics/watchdog.ts +8 -1
- package/src/library-components/URLParam.ts +1 -1
- package/src/misc/hash.ts +1 -0
- package/src/path.ts +21 -7
- package/src/server.ts +54 -47
- package/src/user-implementation/loginEmail.tsx +1 -1
- package/tempnotes.txt +67 -0
- package/test.ts +288 -95
- package/src/0-path-value-core/NodePathAuthorities.ts +0 -1057
- package/src/0-path-value-core/PathController.ts +0 -1
- package/src/5-diagnostics/diskValueAudit.ts +0 -218
- package/src/5-diagnostics/memoryValueAudit.ts +0 -438
- 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,
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
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 {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
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.
|
|
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
|
|
194
|
-
if (!matchesParentRangeFilter({ parentPath, fullPath: value.path,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
// NOTE: This
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
this.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
beforeTriggerDone
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
let
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
let
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
watchSpecData.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
(spec.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
(spec.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
watchSpecData.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
console.log(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
watchSpec.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
let
|
|
495
|
-
if (!
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
let
|
|
535
|
-
let
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
//
|
|
569
|
-
// -
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
watchers
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
let
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
let
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
let parentPath = getParentPathStr(path);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
+
|