querysub 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,702 @@
|
|
|
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 { debugLog } from "../0-path-value-core/debugLogs";
|
|
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
|
+
|
|
73
|
+
let nextWatchSpecOrder = 1;
|
|
74
|
+
export class ClientWatcher {
|
|
75
|
+
public static DEBUG_READS = false;
|
|
76
|
+
public static DEBUG_WRITES = false;
|
|
77
|
+
public static DEBUG_READS_EXPANDED = false;
|
|
78
|
+
public static DEBUG_WRITES_EXPANDED = false;
|
|
79
|
+
public static DEBUG_TRIGGERS?: "light" | "heavy";
|
|
80
|
+
public static DEBUG_SOURCES = false;
|
|
81
|
+
|
|
82
|
+
/** How long we keep watching, despite a value no longer being needed. */
|
|
83
|
+
public static WATCH_STICK_TIME = MAX_CHANGE_AGE * 2;
|
|
84
|
+
public static MAX_TRIGGER_TIME = 1000 * 5;
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
private valueFunctionWatchers = registerResource("paths|valueFunctionWatchers", new Map<string, Map<WatchSpec["callback"], WatchSpec>>());
|
|
88
|
+
// hack_stripPackedPath(path) =>
|
|
89
|
+
private parentValueFunctionWatchers = registerResource("paths|parentValueFunctionWatchers", new Map<string,
|
|
90
|
+
// path =>
|
|
91
|
+
Map<string, {
|
|
92
|
+
start: number;
|
|
93
|
+
end: number;
|
|
94
|
+
lookup: Map<WatchSpec["callback"], WatchSpec>
|
|
95
|
+
}>
|
|
96
|
+
>());
|
|
97
|
+
private allWatchers = registerResource("paths|clientWatcher.allWatchers", new Map<WatchSpec["callback"], WatchSpec>());
|
|
98
|
+
|
|
99
|
+
// path => expiryTime
|
|
100
|
+
private pendingUnwatches = registerResource("paths|pendingUnwatches", new Map<string, number>());
|
|
101
|
+
private pendingParentUnwatches = registerResource("paths|pendingParentUnwatches", new Map<string, number>());
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
private inLoop = false;
|
|
105
|
+
private isLoopSynchronous = false;
|
|
106
|
+
private pendingTriggered: Map<WatchSpec, WatchSpecData> | undefined;
|
|
107
|
+
private triggerWatcher(spec: WatchSpec, config?: { synchronous?: boolean }) {
|
|
108
|
+
let watchedTriggers = this.pendingTriggered = this.pendingTriggered || (new Map() as never);
|
|
109
|
+
let trigger = watchedTriggers.get(spec);
|
|
110
|
+
if (!trigger) {
|
|
111
|
+
trigger = { pathSources: ClientWatcher.DEBUG_SOURCES ? new Set() : undefined, newParentsSynced: new Set(), paths: new Set() };
|
|
112
|
+
watchedTriggers.set(spec, trigger);
|
|
113
|
+
}
|
|
114
|
+
this.triggerWatchLoop(config);
|
|
115
|
+
return trigger;
|
|
116
|
+
}
|
|
117
|
+
private onTriggerDone: Promise<void> | undefined;
|
|
118
|
+
private activeWatchSpec: WatchSpec | undefined;
|
|
119
|
+
private ignoreWatch: ((spec: WatchSpec) => boolean) | undefined;
|
|
120
|
+
public waitForTriggerFinished() { return this.onTriggerDone; }
|
|
121
|
+
public isCurrentLoopSynchronous() { return this.isLoopSynchronous; }
|
|
122
|
+
|
|
123
|
+
@measureFnc
|
|
124
|
+
public localOnValueCallback(values: PathValue[], parentsSynced: string[]) {
|
|
125
|
+
if (values.length === 0 && parentsSynced.length === 0) return;
|
|
126
|
+
const onTrigger = (spec: WatchSpec, path: string, source?: PathValue) => {
|
|
127
|
+
if (
|
|
128
|
+
ClientWatcher.DEBUG_TRIGGERS === "heavy"
|
|
129
|
+
&& !this.pendingTriggered?.has(spec)
|
|
130
|
+
) {
|
|
131
|
+
let sourceName = "";
|
|
132
|
+
if (this.activeWatchSpec) {
|
|
133
|
+
sourceName = ` (inside ${this.activeWatchSpec.debugName})`;
|
|
134
|
+
} else if (source?.source) {
|
|
135
|
+
sourceName = ` (remote ${source.source.split("|").at(-1)})`;
|
|
136
|
+
}
|
|
137
|
+
console.log(`${blue("QUEUEING TRIGGER")} ${spec.debugName} ${sourceName}`);
|
|
138
|
+
console.log(` DUE TO WRITE ${getPathFromStr(path).join(".")}`);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const triggerWatcher = (spec: WatchSpec, source: PathValue) => {
|
|
143
|
+
// Ignore self writes, otherwise local writes as simple as `x++`, will trigger an infinite loop.
|
|
144
|
+
if (this.ignoreWatch?.(spec)) return;
|
|
145
|
+
let trigger = this.triggerWatcher(spec);
|
|
146
|
+
if (trigger.pathSources) {
|
|
147
|
+
trigger.pathSources.add(source);
|
|
148
|
+
}
|
|
149
|
+
trigger.paths.add(source.path);
|
|
150
|
+
onTrigger(spec, source.path, source);
|
|
151
|
+
};
|
|
152
|
+
const triggerWatcherParent = (spec: WatchSpec, parent: string) => {
|
|
153
|
+
if (this.ignoreWatch?.(spec)) return;
|
|
154
|
+
onTrigger(spec, parent);
|
|
155
|
+
let trigger = this.triggerWatcher(spec);
|
|
156
|
+
trigger.newParentsSynced.add(parent);
|
|
157
|
+
};
|
|
158
|
+
for (let value of values) {
|
|
159
|
+
let watchers = this.valueFunctionWatchers.get(value.path);
|
|
160
|
+
if (watchers) {
|
|
161
|
+
for (let watcher of watchers.values()) {
|
|
162
|
+
triggerWatcher(watcher, value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
let parentPath = getParentPathStr(value.path);
|
|
166
|
+
let parentWatchers = this.parentValueFunctionWatchers.get(parentPath);
|
|
167
|
+
if (parentWatchers) {
|
|
168
|
+
// NOTE: We don't filter by path shard here
|
|
169
|
+
for (let { start, end, lookup } of parentWatchers.values()) {
|
|
170
|
+
if (!matchesParentRangeFilter({ parentPath, fullPath: value.path, start, end })) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (let watcher of lookup.values()) {
|
|
175
|
+
triggerWatcher(watcher, value);
|
|
176
|
+
triggerWatcherParent(watcher, parentPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (let path of parentsSynced) {
|
|
182
|
+
let basePath = hack_stripPackedPath(path);
|
|
183
|
+
let watchers = this.parentValueFunctionWatchers.get(basePath);
|
|
184
|
+
if (watchers) {
|
|
185
|
+
let realObj = watchers.get(path);
|
|
186
|
+
if (realObj) {
|
|
187
|
+
for (let watcher of realObj.lookup.values()) {
|
|
188
|
+
triggerWatcherParent(watcher, path);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public explicitlyTriggerWatcher(callback: WatchSpec["callback"], config?: { synchronous?: boolean }) {
|
|
196
|
+
let watcher = this.allWatchers.get(callback);
|
|
197
|
+
if (!watcher) throw new Error(`No watcher found for callback ${callback.toString()}`);
|
|
198
|
+
this.triggerWatcher(watcher, config);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private triggerWatchLoop(config?: { synchronous?: boolean }) {
|
|
202
|
+
// A fairly standard trigger loop. Batch synchronous triggers for efficiency's sake
|
|
203
|
+
if (this.inLoop) return;
|
|
204
|
+
this.inLoop = true;
|
|
205
|
+
let triggerPromiseObj = new PromiseObj();
|
|
206
|
+
this.onTriggerDone = triggerPromiseObj.promise;
|
|
207
|
+
const doLoop = () => {
|
|
208
|
+
try {
|
|
209
|
+
this.innerTriggerLoop();
|
|
210
|
+
} finally {
|
|
211
|
+
this.inLoop = false;
|
|
212
|
+
this.isLoopSynchronous = false;
|
|
213
|
+
triggerPromiseObj.resolve();
|
|
214
|
+
this.onTriggerDone = undefined;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
if (config?.synchronous) {
|
|
218
|
+
this.isLoopSynchronous = true;
|
|
219
|
+
doLoop();
|
|
220
|
+
} else {
|
|
221
|
+
void Promise.resolve().finally(doLoop);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
@measureFnc
|
|
225
|
+
private innerTriggerLoop() {
|
|
226
|
+
let loopCount = 0;
|
|
227
|
+
let triggerCount = 0;
|
|
228
|
+
let groupStart = (
|
|
229
|
+
ClientWatcher.DEBUG_TRIGGERS === "light" && console.groupCollapsed
|
|
230
|
+
|| ClientWatcher.DEBUG_TRIGGERS === "heavy" && console.group
|
|
231
|
+
|| undefined
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
for (let callback of this.loopStartCallbacks) {
|
|
235
|
+
try {
|
|
236
|
+
callback();
|
|
237
|
+
} catch (e) {
|
|
238
|
+
logErrors(e);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let time = Date.now();
|
|
243
|
+
while (true) {
|
|
244
|
+
let currentTriggered = this.pendingTriggered;
|
|
245
|
+
this.pendingTriggered = undefined;
|
|
246
|
+
if (!currentTriggered?.size) break;
|
|
247
|
+
|
|
248
|
+
if (ClientWatcher.DEBUG_TRIGGERS) {
|
|
249
|
+
console.log(" ");
|
|
250
|
+
}
|
|
251
|
+
groupStart?.(`${blue("TRIGGERS LOOP ITERATION")} ${loopCount++}`);
|
|
252
|
+
|
|
253
|
+
let sorted = Array.from(currentTriggered);
|
|
254
|
+
sorted.sort((a, b) => {
|
|
255
|
+
let orderDiff = (a[0].orderGroup ?? 0) - (b[0].orderGroup ?? 0);
|
|
256
|
+
if (orderDiff !== 0) return orderDiff;
|
|
257
|
+
return (a[0].order ?? 0) - (b[0].order ?? 0);
|
|
258
|
+
});
|
|
259
|
+
if (ClientWatcher.DEBUG_TRIGGERS) {
|
|
260
|
+
for (let [trigger] of sorted) {
|
|
261
|
+
console.log(" " + trigger.debugName + " " + (trigger.orderGroup ?? 0) + "|" + (trigger.order ?? 0));
|
|
262
|
+
}
|
|
263
|
+
console.groupEnd();
|
|
264
|
+
}
|
|
265
|
+
let curIndex = 0;
|
|
266
|
+
this.ignoreWatch = spec => {
|
|
267
|
+
// Ignore it if we are handling it (self triggers are almost always just due to `x++`, and are virtually
|
|
268
|
+
// never intended to actually self trigger. And if they are, then they can just detach with
|
|
269
|
+
// Promise.resolve.finally(...))
|
|
270
|
+
// ALSO ignore if we are going to handle it. There's no reason to queue another watch for something
|
|
271
|
+
// we are about to handle.
|
|
272
|
+
for (let checkIndex = curIndex; checkIndex < sorted.length; checkIndex++) {
|
|
273
|
+
if (sorted[checkIndex][0] === spec) return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// If they care about the order, and it is higher than the current order, then insert it into the current
|
|
277
|
+
if (spec.order !== undefined && (
|
|
278
|
+
(spec.order ?? 0) > (sorted[curIndex][0].order ?? 0)
|
|
279
|
+
&& (spec.orderGroup ?? 0) === (sorted[curIndex][0].orderGroup ?? 0)
|
|
280
|
+
|| (spec.orderGroup ?? 0) > (sorted[curIndex][0].orderGroup ?? 0)
|
|
281
|
+
)) {
|
|
282
|
+
if (ClientWatcher.DEBUG_TRIGGERS) {
|
|
283
|
+
console.log(" (insert new trigger)");
|
|
284
|
+
console.log(" " + spec.debugName + " " + (spec.orderGroup ?? 0) + "|" + (spec.order ?? 0));
|
|
285
|
+
}
|
|
286
|
+
// Find the last insert >= the current order (check for equals so we can insert at the end
|
|
287
|
+
// duplicate order values).
|
|
288
|
+
let insertIndex = sorted.findLastIndex(x =>
|
|
289
|
+
(spec.order ?? 0) >= (x[0].order ?? 0)
|
|
290
|
+
&& (spec.orderGroup ?? 0) === (x[0].orderGroup ?? 0)
|
|
291
|
+
|| (spec.orderGroup ?? 0) > (x[0].orderGroup ?? 0)
|
|
292
|
+
);
|
|
293
|
+
if (insertIndex === -1) {
|
|
294
|
+
insertIndex = sorted.length - 1;
|
|
295
|
+
}
|
|
296
|
+
insertIndex++;
|
|
297
|
+
sorted.splice(insertIndex, 0, [spec, {
|
|
298
|
+
paths: new Set(),
|
|
299
|
+
pathSources: ClientWatcher.DEBUG_SOURCES ? new Set() : undefined,
|
|
300
|
+
newParentsSynced: new Set(),
|
|
301
|
+
}]);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
let stoppedEarly = false;
|
|
308
|
+
for (; curIndex < sorted.length; curIndex++) {
|
|
309
|
+
let [watchSpec, data] = sorted[curIndex];
|
|
310
|
+
triggerCount++;
|
|
311
|
+
this.activeWatchSpec = watchSpec;
|
|
312
|
+
if (ClientWatcher.DEBUG_TRIGGERS === "heavy") {
|
|
313
|
+
console.log(`${green("RUNNING TRIGGER")} ${watchSpec.debugName}`);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
watchSpec.callback(data);
|
|
317
|
+
} catch (e) {
|
|
318
|
+
void Promise.resolve().finally(() => { throw e; });
|
|
319
|
+
}
|
|
320
|
+
this.activeWatchSpec = undefined;
|
|
321
|
+
if (Date.now() - time > ClientWatcher.MAX_TRIGGER_TIME) {
|
|
322
|
+
stoppedEarly = true;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this.ignoreWatch = undefined;
|
|
327
|
+
|
|
328
|
+
if (ClientWatcher.DEBUG_TRIGGERS) {
|
|
329
|
+
console.log(" ");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (stoppedEarly) {
|
|
333
|
+
const history = 10;
|
|
334
|
+
let remaining = (
|
|
335
|
+
sorted
|
|
336
|
+
.slice(curIndex, curIndex + history)
|
|
337
|
+
.map(x => `${x[0].debugName} (${Array.from(x[1].paths).map(x => getPathFromStr(x).join(".")).join(" | ")})`)
|
|
338
|
+
.join(", ")
|
|
339
|
+
);
|
|
340
|
+
console.error(red(`Too much time spent in trigger loop, aborting loop. Triggered ${triggerCount} triggers. Remaining triggers: ${remaining}`), triggerCount);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let finalStats = { loopCount, triggerCount };
|
|
346
|
+
for (let callback of this.loopEndCallbacks) {
|
|
347
|
+
try {
|
|
348
|
+
callback(finalStats);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
logErrors(e);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
time = Date.now() - time;
|
|
355
|
+
if (ClientWatcher.DEBUG_TRIGGERS) {
|
|
356
|
+
console.log(`${green("TRIGGERS LOOP FINISHED")} ${loopCount} loops, ${triggerCount} triggers, in ${time}ms`);
|
|
357
|
+
console.log(" ");
|
|
358
|
+
console.log(" ");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private loopStartCallbacks = new Set<() => void>();
|
|
363
|
+
private loopEndCallbacks = new Set<(info: { loopCount: number; triggerCount: number; }) => void>();
|
|
364
|
+
public onLoopStart(callback: () => void) {
|
|
365
|
+
this.loopStartCallbacks.add(callback);
|
|
366
|
+
}
|
|
367
|
+
public onLoopEnd(
|
|
368
|
+
callback: (info: {
|
|
369
|
+
loopCount: number;
|
|
370
|
+
triggerCount: number;
|
|
371
|
+
}) => void
|
|
372
|
+
) {
|
|
373
|
+
this.loopEndCallbacks.add(callback);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public unwatch(callback: WatchSpec["callback"]) {
|
|
377
|
+
this.updateUnwatches({
|
|
378
|
+
callback,
|
|
379
|
+
paths: new Set(),
|
|
380
|
+
parentPaths: new Set(),
|
|
381
|
+
});
|
|
382
|
+
this.allWatchers.delete(callback);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private updateUnwatches(watchSpec: WatchSpec) {
|
|
386
|
+
const { callback, paths, parentPaths } = watchSpec;
|
|
387
|
+
let prevSpec = this.allWatchers.get(callback);
|
|
388
|
+
if (!prevSpec) return;
|
|
389
|
+
if (prevSpec) {
|
|
390
|
+
watchSpec.order = prevSpec.order;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let fullyUnwatchedPaths = new Set<string>();
|
|
394
|
+
let fullyUnwatchedParents = new Set<string>();
|
|
395
|
+
|
|
396
|
+
// Remove removed paths
|
|
397
|
+
for (let path of prevSpec.paths) {
|
|
398
|
+
if (paths.has(path)) continue;
|
|
399
|
+
let watchers = this.valueFunctionWatchers.get(path);
|
|
400
|
+
if (!watchers) continue;
|
|
401
|
+
watchers.delete(callback);
|
|
402
|
+
if (watchers.size === 0) {
|
|
403
|
+
fullyUnwatchedPaths.add(path);
|
|
404
|
+
this.valueFunctionWatchers.delete(path);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Remove removed parents
|
|
409
|
+
for (let path of prevSpec.parentPaths) {
|
|
410
|
+
if (parentPaths.has(path)) continue;
|
|
411
|
+
let basePath = hack_stripPackedPath(path);
|
|
412
|
+
let watchersBase = this.parentValueFunctionWatchers.get(basePath);
|
|
413
|
+
if (!watchersBase) continue;
|
|
414
|
+
let watchers = watchersBase.get(path);
|
|
415
|
+
if (!watchers) continue;
|
|
416
|
+
watchers.lookup.delete(callback);
|
|
417
|
+
if (watchers.lookup.size === 0) {
|
|
418
|
+
watchersBase.delete(path);
|
|
419
|
+
fullyUnwatchedParents.add(path);
|
|
420
|
+
}
|
|
421
|
+
if (watchersBase.size === 0) {
|
|
422
|
+
this.parentValueFunctionWatchers.delete(basePath);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
prevSpec.paths = paths;
|
|
427
|
+
prevSpec.parentPaths = parentPaths;
|
|
428
|
+
watchSpec = prevSpec;
|
|
429
|
+
|
|
430
|
+
let expiryTime = Date.now() + ClientWatcher.WATCH_STICK_TIME;
|
|
431
|
+
if (isDevDebugbreak()) {
|
|
432
|
+
for (let path of fullyUnwatchedPaths) {
|
|
433
|
+
debugLog("clientWatcher UNWATCH", { path });
|
|
434
|
+
}
|
|
435
|
+
for (let path of fullyUnwatchedParents) {
|
|
436
|
+
debugLog("clientWatcher UNWATCH PARENT", { path });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (let path of fullyUnwatchedPaths) {
|
|
440
|
+
this.pendingUnwatches.set(path, expiryTime);
|
|
441
|
+
}
|
|
442
|
+
for (let path of fullyUnwatchedParents) {
|
|
443
|
+
this.pendingParentUnwatches.set(path, expiryTime);
|
|
444
|
+
}
|
|
445
|
+
this.ensureUnwatching();
|
|
446
|
+
}
|
|
447
|
+
private ensureUnwatching = lazy(() => {
|
|
448
|
+
let waitTime = Math.max(1000 * 5, ClientWatcher.WATCH_STICK_TIME - 1000);
|
|
449
|
+
const self = this;
|
|
450
|
+
runInfinitePoll(waitTime, async function ensureUnwatching() {
|
|
451
|
+
let now = Date.now();
|
|
452
|
+
let pathsToUnwatch = new Set<string>();
|
|
453
|
+
let parentPathsToUnwatch = new Set<string>();
|
|
454
|
+
for (let [path, expiryTime] of self.pendingUnwatches) {
|
|
455
|
+
if (expiryTime < now) {
|
|
456
|
+
self.pendingUnwatches.delete(path);
|
|
457
|
+
pathsToUnwatch.add(path);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (let [path, expiryTime] of self.pendingParentUnwatches) {
|
|
461
|
+
if (expiryTime < now) {
|
|
462
|
+
self.pendingParentUnwatches.delete(path);
|
|
463
|
+
parentPathsToUnwatch.add(path);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (pathsToUnwatch.size > 0 || parentPathsToUnwatch.size > 0) {
|
|
467
|
+
//console.log(red("Unwatching paths"), pathsToUnwatch, parentPathsToUnwatch, Date.now());
|
|
468
|
+
let unwatchConfig: WatchConfig = { paths: Array.from(pathsToUnwatch), parentPaths: Array.from(parentPathsToUnwatch), };
|
|
469
|
+
// pathWatcher unwatches remotely as well
|
|
470
|
+
pathWatcher.unwatchPath({ callback: getOwnNodeId(), ...unwatchConfig });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// NOTE: The promise USUALLY doesn't resolve until all the watches have synced (except in certain error cases).
|
|
476
|
+
// NOTE: The callback is only called when synced values change (so if they are already synced, and don't change,
|
|
477
|
+
// the callback won't be called).
|
|
478
|
+
// NOTE: Doesn't call the callback until all requested values have finished their initial sync, to prevent
|
|
479
|
+
// too many callback callbacks.
|
|
480
|
+
// - You can just fragment your writes into different components to isolate any slow loading parts, so partial
|
|
481
|
+
// loading of data really isn't needed.
|
|
482
|
+
// NOTE: Takes ownership of paths and parentPaths, so... don't mutate them after calling this!
|
|
483
|
+
@measureFnc
|
|
484
|
+
public setWatches(watchSpec: WatchSpec) {
|
|
485
|
+
let callback = watchSpec.callback;
|
|
486
|
+
let paths = watchSpec.paths;
|
|
487
|
+
let parentPaths = watchSpec.parentPaths;
|
|
488
|
+
watchSpec.order = watchSpec.order ?? nextWatchSpecOrder++;
|
|
489
|
+
|
|
490
|
+
this.updateUnwatches(watchSpec);
|
|
491
|
+
|
|
492
|
+
this.allWatchers.set(callback, watchSpec);
|
|
493
|
+
for (let path of paths) {
|
|
494
|
+
let watchers = this.valueFunctionWatchers.get(path);
|
|
495
|
+
if (!watchers) {
|
|
496
|
+
watchers = new Map();
|
|
497
|
+
this.valueFunctionWatchers.set(path, watchers);
|
|
498
|
+
}
|
|
499
|
+
watchers.set(callback, watchSpec);
|
|
500
|
+
this.pendingUnwatches.delete(path);
|
|
501
|
+
}
|
|
502
|
+
for (let path of parentPaths) {
|
|
503
|
+
let basePath = hack_stripPackedPath(path);
|
|
504
|
+
let watchersBase = this.parentValueFunctionWatchers.get(basePath);
|
|
505
|
+
if (!watchersBase) {
|
|
506
|
+
watchersBase = new Map();
|
|
507
|
+
this.parentValueFunctionWatchers.set(basePath, watchersBase);
|
|
508
|
+
}
|
|
509
|
+
let watchers = watchersBase.get(path);
|
|
510
|
+
if (!watchers) {
|
|
511
|
+
let range = decodeParentFilter(path) || { start: 0, end: 1 };
|
|
512
|
+
watchers = {
|
|
513
|
+
start: range.start,
|
|
514
|
+
end: range.end,
|
|
515
|
+
lookup: new Map()
|
|
516
|
+
};
|
|
517
|
+
watchersBase.set(path, watchers);
|
|
518
|
+
}
|
|
519
|
+
watchers.lookup.set(callback, watchSpec);
|
|
520
|
+
this.pendingParentUnwatches.delete(path);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let pathsArray = Array.from(paths);
|
|
524
|
+
let parentPathsArray = Array.from(parentPaths);
|
|
525
|
+
let debugName = watchSpec.callback.name;
|
|
526
|
+
|
|
527
|
+
if (pathsArray.length === 0 && parentPathsArray.length === 0) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (isDevDebugbreak()) {
|
|
532
|
+
for (let path of pathsArray) {
|
|
533
|
+
debugLog("clientWatcher WATCH", { path });
|
|
534
|
+
}
|
|
535
|
+
for (let path of parentPathsArray) {
|
|
536
|
+
debugLog("clientWatcher WATCH PARENT", { path });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Watch it locally as well, so when we get the value we know to trigger the client callback
|
|
541
|
+
pathWatcher.watchPath({
|
|
542
|
+
callback: getOwnNodeId(),
|
|
543
|
+
paths: pathsArray,
|
|
544
|
+
parentPaths: parentPathsArray,
|
|
545
|
+
debugName,
|
|
546
|
+
noInitialTrigger: watchSpec.noInitialTrigger,
|
|
547
|
+
});
|
|
548
|
+
remoteWatcher.watchLatest({
|
|
549
|
+
paths: pathsArray,
|
|
550
|
+
parentPaths: parentPathsArray,
|
|
551
|
+
debugName,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private lastVersions = registerResource("paths|lastVersion", new Map<number, number>());
|
|
556
|
+
private getFreeVersionBase(time: number): number {
|
|
557
|
+
this.ensureCleaningUpVersions();
|
|
558
|
+
let lastVersion = this.lastVersions.get(time);
|
|
559
|
+
let nextVersion = lastVersion === undefined ? 0 : lastVersion + 1;
|
|
560
|
+
this.lastVersions.set(time, nextVersion);
|
|
561
|
+
return nextVersion;
|
|
562
|
+
}
|
|
563
|
+
private ensureCleaningUpVersions = lazy(() => {
|
|
564
|
+
const self = this;
|
|
565
|
+
const THRESHOLD = MAX_CHANGE_AGE * 2;
|
|
566
|
+
runInfinitePoll(THRESHOLD, function cleanUpVersions() {
|
|
567
|
+
let curThreshold = Date.now() - THRESHOLD;
|
|
568
|
+
for (let time of self.lastVersions.keys()) {
|
|
569
|
+
if (time < curThreshold) {
|
|
570
|
+
self.lastVersions.delete(time);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
/** Gets a free write time by incrementing the version, and then ensuring that version isn't reused for the given time. */
|
|
577
|
+
public getFreeWriteTime(time: Time | undefined): Time {
|
|
578
|
+
if (!time?.time) return getNextTime();
|
|
579
|
+
let version = this.getFreeVersionBase(time.time);
|
|
580
|
+
return { time: time.time, version, creatorId: getCreatorId() };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// IMPORTANT! The entire path from setValues => localOnValueCallback MUST be synchronous. If it is asynchronous we CANNOT
|
|
584
|
+
// correctly prevent watchers from triggering themselves, which can result in either 2X the number of needed evaluations,
|
|
585
|
+
// OR infinite evaluations.
|
|
586
|
+
// NOTE: This should be called RIGHT after you read the values, otherwise the lock that is created
|
|
587
|
+
// may not depend on what was actually read.
|
|
588
|
+
// NOTE: If a server goes down, these writes may fail to commit (they will depend on each other, so
|
|
589
|
+
// it will be either all or nothing).
|
|
590
|
+
// NOTE: This synchronously predicts the values, so you don't need to wait. When the promise resolves
|
|
591
|
+
// the values will be committed and they won't belost (although it is still possible for them to become
|
|
592
|
+
// rejected).
|
|
593
|
+
@measureFnc
|
|
594
|
+
public setValues(
|
|
595
|
+
config: {
|
|
596
|
+
values: Map<string, Value>;
|
|
597
|
+
forcedUndefinedWrites?: Set<string>;
|
|
598
|
+
eventPaths?: Set<string>;
|
|
599
|
+
locks: ReadLock[];
|
|
600
|
+
// NOTE: Use clientWatcher.getFreeWriteTime() to ensure this writeTime is valid
|
|
601
|
+
// (likely BEFORE you run the function creating these, so you can use it as actual read times).
|
|
602
|
+
writeTime?: Time;
|
|
603
|
+
/** If you are writing values you KNOW you won't reading back, set this flag to
|
|
604
|
+
* prevent caching of the writes clientside (which is a lot more efficient).
|
|
605
|
+
*/
|
|
606
|
+
noWritePrediction?: boolean;
|
|
607
|
+
/** See PathValue.event */
|
|
608
|
+
eventWrite?: boolean;
|
|
609
|
+
/** Causes us to NOT ACTUALLY write values, but just return what we would write, if !dryRun */
|
|
610
|
+
dryRun?: boolean;
|
|
611
|
+
}
|
|
612
|
+
): PathValue[] {
|
|
613
|
+
const { values, locks, eventPaths } = config;
|
|
614
|
+
|
|
615
|
+
// We trust our caller to use getFreeWriteTime, as they need to know the exact time
|
|
616
|
+
// they are using so they can use it for their readTimes
|
|
617
|
+
let writeTime = config.writeTime;
|
|
618
|
+
if (!writeTime || writeTime.time === 0) {
|
|
619
|
+
writeTime = getNextTime();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let event = !!config.eventWrite;
|
|
623
|
+
|
|
624
|
+
let debugName = getOwnMachineId().slice(0, 8);
|
|
625
|
+
if (this.activeWatchSpec?.debugName) {
|
|
626
|
+
debugName += "|" + this.activeWatchSpec.debugName;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let pathValues: PathValue[] = [];
|
|
630
|
+
for (let [path, value] of values) {
|
|
631
|
+
let valueIsEvent = event;
|
|
632
|
+
if (eventPaths && eventPaths.has(path)) {
|
|
633
|
+
valueIsEvent = true;
|
|
634
|
+
}
|
|
635
|
+
let isTransparent = value === undefined || config.forcedUndefinedWrites?.has(path);
|
|
636
|
+
let pathValue: PathValue = pathValueCtor({
|
|
637
|
+
path,
|
|
638
|
+
valid: true,
|
|
639
|
+
value,
|
|
640
|
+
canGCValue: value === undefined,
|
|
641
|
+
time: writeTime,
|
|
642
|
+
locks: locks,
|
|
643
|
+
lockCount: locks.length,
|
|
644
|
+
event: valueIsEvent,
|
|
645
|
+
isTransparent,
|
|
646
|
+
source: debugName,
|
|
647
|
+
});
|
|
648
|
+
pathValues.push(pathValue);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!config.dryRun) {
|
|
652
|
+
pathValueCommitter.commitValues(pathValues, config.noWritePrediction ? undefined : "predictWrites");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return pathValues;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
public unwatchEventPaths(paths: Set<string>) {
|
|
659
|
+
let relevantWatches = new Set<WatchSpec>();
|
|
660
|
+
for (let path of paths) {
|
|
661
|
+
let watchers = this.valueFunctionWatchers.get(path);
|
|
662
|
+
if (watchers) {
|
|
663
|
+
this.valueFunctionWatchers.delete(path);
|
|
664
|
+
for (let watcher of watchers.values()) {
|
|
665
|
+
watcher.paths.delete(path);
|
|
666
|
+
relevantWatches.add(watcher);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
let basePath = hack_stripPackedPath(path);
|
|
670
|
+
let parentWatcher = this.parentValueFunctionWatchers.get(basePath);
|
|
671
|
+
if (parentWatcher) {
|
|
672
|
+
let watcherObj = parentWatcher.get(path);
|
|
673
|
+
if (watcherObj) {
|
|
674
|
+
parentWatcher.delete(path);
|
|
675
|
+
if (parentWatcher.size === 0) {
|
|
676
|
+
this.parentValueFunctionWatchers.delete(basePath);
|
|
677
|
+
}
|
|
678
|
+
for (let watcher of watcherObj.lookup.values()) {
|
|
679
|
+
watcher.parentPaths.delete(path);
|
|
680
|
+
relevantWatches.add(watcher);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (let watcher of relevantWatches) {
|
|
686
|
+
watcher.unwatchEventPaths?.(paths);
|
|
687
|
+
}
|
|
688
|
+
remoteWatcher.unwatchEventPaths({ paths: Array.from(paths), parentPaths: Array.from(paths), });
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
public pathHasAnyWatchers(path: string) {
|
|
692
|
+
return !!this.valueFunctionWatchers.get(path)?.size;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
export const clientWatcher = new ClientWatcher();
|
|
696
|
+
(globalThis as any).clientWatcher = clientWatcher;
|
|
697
|
+
void Promise.resolve().finally(() => {
|
|
698
|
+
authorityStorage.watchEventRemovals(x => clientWatcher.unwatchEventPaths(x));
|
|
699
|
+
pathWatcher.watchAllLocalTriggers((x, y) => clientWatcher.localOnValueCallback(x, y));
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
|