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.
Files changed (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. 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
+