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,1857 @@
1
+ import { measureCode, measureWrap } from "socket-function/src/profiling/measure";
2
+ import { SocketFunction } from "socket-function/SocketFunction";
3
+ import { deepCloneJSON, getKeys, isNode, recursiveFreeze, timeInMinute } from "socket-function/src/misc";
4
+ import { canHaveChildren, MaybePromise } from "socket-function/src/types";
5
+ import { blue, green, magenta, red, yellow } from "socket-function/src/formatting/logColors";
6
+ import { lazy } from "socket-function/src/caching";
7
+ import { delay, runInfinitePoll } from "socket-function/src/batching";
8
+ import { errorify, logErrors } from "../errors";
9
+ import { appendToPathStr, getLastPathPart, getParentPathStr, getPathDepth, getPathFromStr, getPathIndex, getPathIndexAssert, getPathPrefix, getPathStr1, getPathStr2, getPathSuffix, slicePathStrToDepth } from "../path";
10
+ import { addEpsilons } from "../bits";
11
+ import { getThreadKeyCert } from "../-a-auth/certs";
12
+ import { ActionsHistory } from "../diagnostics/ActionsHistory";
13
+ import { registerResource } from "../diagnostics/trackResources";
14
+ import { ClientWatcher, WatchSpecData, clientWatcher } from "../1-path-client/pathValueClientWatcher";
15
+ import { createPathValueProxy, getProxyPath, isValueProxy, isValueProxy2 } from "./pathValueProxy";
16
+ import { authorityStorage, compareTime, ReadLock, epochTime, getNextTime, MAX_ACCEPTED_CHANGE_AGE, PathValue, Time, getCreatorId } from "../0-path-value-core/pathValueCore";
17
+ import { runCodeWithDatabase, rawSchema } from "./pathDatabaseProxyBase";
18
+ import { CallSpec, DEPTH_TO_DATA, MODULE_INDEX, getCurrentCall } from "../3-path-functions/PathFunctionRunner";
19
+ import { interceptCalls, runCall } from "../3-path-functions/PathFunctionHelpers";
20
+ import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
21
+ import debugbreak from "debugbreak";
22
+ import { FunctionMetadata, getSchemaObject, inlineNestedCalls } from "../3-path-functions/syncSchema";
23
+ import { pathValueCommitter } from "../0-path-value-core/PathValueController";
24
+ import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
25
+ import { isClient } from "../config2";
26
+ import { isDeploy } from "../3-path-functions/deployCheck";
27
+ import { LOCAL_DOMAIN_PATH } from "../0-path-value-core/NodePathAuthorities";
28
+ import { PermissionsCheck } from "../4-querysub/permissions";
29
+ import { registerPeriodic } from "../diagnostics/periodic";
30
+ import { remoteWatcher } from "../1-path-client/RemoteWatcher";
31
+ import { Schema2, Schema2Fncs } from "./schema2";
32
+ import { getDomain } from "../config";
33
+ import { Querysub } from "../4-querysub/QuerysubController";
34
+
35
+ // TODO: Break this into two parts:
36
+ // 1) Run and get accesses
37
+ // 2) Commit/watch/unwatch
38
+ // With a harness that joins the two parts in a loop (mostly powered by clientWatcher,,
39
+ // which can run the trigger loop).
40
+
41
+ let nextOrderSeqNum = 1;
42
+
43
+ interface WatcherOptions<Result> {
44
+ /** NOTE: If you try to run too far in the past, an error will occur.
45
+ * - This both sets the read time, and write time.
46
+ * - setCurrentReadTime can also set the read time (but runAtTime is the only
47
+ * way to set the write time).
48
+ */
49
+ runAtTime?: Time;
50
+
51
+ /** NOTE: Write values are only committed after we have all reads synced. */
52
+ canWrite?: boolean;
53
+
54
+ /** ONLY allowed if the writes and reads are all local (throws if any non-local accesses are attempted).
55
+ * - Stops lock creation. This might increase speed?
56
+ */
57
+ noLocks?: boolean;
58
+ /** No locks, without the local-only restriction. A lot more unsafe. Don't use for any writes
59
+ * that will eventually be committed (we only use it in prediction code, and GC). */
60
+ unsafeNoLocks?: boolean;
61
+
62
+ /** Doesn't allow reading any values, but... also causes allows the function to have
63
+ * no readLocks! (so it can't be rejected)
64
+ * - Still allows reading back your own writes though...
65
+ * - Doesn't write object parents, which breaks Object.getKeys(), unless you use
66
+ * atomicObjectWrite.
67
+ * - Usually set with runImmediately
68
+ */
69
+ writeOnly?: boolean;
70
+
71
+ /** Ignore currentReadTime, and just read the latest. For use with doProxyOptions. */
72
+ forceReadLatest?: boolean;
73
+
74
+ /** If you are writing values you KNOW you won't reading back, set this flag to
75
+ * prevent caching of the writes clientside (which is a lot more efficient).
76
+ * - More for testing purposes, as the performance benefit is basically negligible,
77
+ * and the memory impact is unlikely to be an issue unless you are writing huge values.
78
+ */
79
+ doNotStoreWritesAsPredictions?: boolean;
80
+
81
+ /** See PathValue.event */
82
+ eventWrite?: boolean;
83
+
84
+ /** By default we read values before we write to them, and don't write if the value
85
+ * would be `===`. This is usually preferred, because unneeded writes are a lot
86
+ * more expensive than unneeded reads. However, if you are writing a lot, and you
87
+ * know the values will be different, you can set this to reduce the amount of reads.
88
+ * This is implicitly used for atomic writes (both due to atomicObjectWrite, or atomicWrites: true),
89
+ * otherwise almost all atomic writes would result in an infinite loop, as usually
90
+ * objects aren't === (and we don't deep compare objects on writes).
91
+ */
92
+ forceEqualWrites?: boolean;
93
+
94
+ atomicWrites?: boolean;
95
+
96
+ debugName?: string;
97
+
98
+ watchFunction: () => Result;
99
+
100
+ // Only called after all the reads are synced
101
+ // - getTriggeredWatcher will function correctly in this callback
102
+ onResultUpdated?: (
103
+ result: { result: Result } | { error: string },
104
+ writes: PathValue[] | undefined,
105
+ watcher: SyncWatcher
106
+ ) => void;
107
+
108
+ /** Causes us to not actually write values (we still sync new values, etc),
109
+ * we just don't actually commit any writes. */
110
+ dryRun?: boolean;
111
+
112
+ /** Runs immediately, returning immediately, not waiting for any values to synchronize.
113
+ * - Also disposes it immediately, as it isn't watching any values
114
+ * - Does not even START to synchronize any values
115
+ */
116
+ runImmediately?: boolean;
117
+
118
+ /** HACK: Used in conjuction with specific callers to prevent disposing, so the watcher
119
+ * can be reused for specific cases.
120
+ */
121
+ static?: boolean;
122
+
123
+ /** Runs the inner function and calls the callback, even if there are unsynced values. */
124
+ allowUnsyncedReads?: boolean;
125
+
126
+ // NOTE: We create a new checker every single run
127
+ getPermissionsCheck?: () => PermissionsChecker;
128
+
129
+ skipPermissionsCheck?: boolean;
130
+
131
+ /** Default is after, which runs nested calls after the function runs (and only if all the
132
+ * data is synchronized)
133
+ * "inline" results in calls being run immediately, instead of queued.
134
+ * TODO: We can only inline calls on the same domain, as we don't have
135
+ * permissions for calls on other domains. In this case we might want to throw
136
+ * ("after" is complicated due to FunctionRunner).
137
+ * */
138
+ nestedCalls?: "throw" | "after" | "ignore" | "inline";
139
+
140
+ /** These domains are allowed to be referenced by locks. Can be overriden, so you can invalidate based on on other domains
141
+ * (ex, predict the server from the client).
142
+ * - Defaults to the current domain, which is the safest option (as invalidating based on other domains can cause
143
+ * security issues, reliability issues, etc).
144
+ */
145
+ overrideAllowLockDomainsPrefixes?: string[];
146
+
147
+ /** See WatchSpec.orderGroup */
148
+ order?: number;
149
+ orderGroup?: number;
150
+
151
+ synchronousInit?: boolean;
152
+
153
+ overrides?: PathValue[];
154
+
155
+ // Triggers are also tracked if ClientWatcher.DEBUG_SOURCES
156
+ // Sometimes we need trigger tracking for delta watchers, so this isn't just for debugging
157
+ trackTriggers?: boolean;
158
+ }
159
+
160
+ const startTime = Date.now();
161
+
162
+ // Used to prevent local rejections on remote values (as local functions aren't rerun)
163
+ // Also used for security on servers, so they can read from untrusted domains, but can't have values
164
+ // rejected by untrusted domains.
165
+ const getAllowedLockDomainsPrefixes = function getTrustedDomains(): string[] {
166
+ if (isNode()) {
167
+ return [getPathStr1(getDomain())];
168
+ } else {
169
+ // TODO: We COULD allow non-local watches on clients? This would allow something such as...
170
+ // clicking on a button to go the first page that no other user is on... to revert if
171
+ // another user clicks at the same time and we race. Or... something? Maybe there
172
+ // are no cases when we need it... and it would definitely slow things down, so...
173
+ // maybe we just don't?
174
+ return [getPathStr1(LOCAL_DOMAIN)];
175
+ }
176
+ };
177
+
178
+ export type SyncWatcher = {
179
+ options: WatcherOptions<any>;
180
+
181
+ dispose: () => void;
182
+ disposed?: boolean;
183
+ // Reset every time the inner function is run, so that inner callbacks can know when the dispose
184
+ // happens, without having to dedupe their callback function.
185
+ onInnerDisposed: (() => void)[];
186
+
187
+ // Not great, but... sometimes we just want to trigger the function
188
+ explicitlyTrigger: (changes?: WatchSpecData) => void;
189
+
190
+ hasAnyUnsyncedAccesses: () => boolean;
191
+
192
+ // - epochTime results in reading the latest time
193
+ // - time is exclusive, so if you set the time of an existing write, you won't
194
+ // read back it's writes.
195
+ setCurrentReadTime: <T>(time: Time, code: () => T) => T;
196
+ // NOTE: Use the setCurrentReadTime helper, to prevent accidentally
197
+ // leaving the read time set (or use runAtTime in the options,
198
+ // to set it forever).
199
+ currentReadTime: Time | undefined;
200
+ nextWriteTime: Time | undefined;
201
+
202
+ debugName: string;
203
+
204
+ // The changes which triggered the current run (or last run if we are not currently running).
205
+ triggeredByChanges: undefined | WatchSpecData;
206
+
207
+ pendingWrites: Map<string, unknown>;
208
+ pendingEventWrites: Set<string>;
209
+ pendingCalls: { call: CallSpec; metadata: FunctionMetadata }[];
210
+
211
+ pendingWatches: {
212
+ paths: Set<string>;
213
+ parentPaths: Set<string>;
214
+ };
215
+
216
+ // These aren't set until after a run finishes
217
+ lastWatches: {
218
+ paths: Set<string>;
219
+ parentPaths: Set<string>;
220
+ };
221
+
222
+
223
+ /** The key is the time we read at. This is required to create the lock.
224
+ * - Only utilized if we commit the write
225
+ */
226
+ pendingAccesses: Map<Time | undefined, Map<string, PathValue>>;
227
+ pendingEpochAccesses: Map<Time | undefined, Set<string>>;
228
+
229
+ // A necessity for react rendering, as rendering unsynced data almost always looks bad,
230
+ // so we need to show the previously rendered data when we have unsynced data.
231
+ // (Although, there is a certain pleasantness to showing SOME change extremely fast,
232
+ // even if it's a somewhat broken state, so... we might at a mode to allow rendering
233
+ // with unsynced data).
234
+ // TODO: We might make this a function, and lazily calculate it. That way we can batch reads
235
+ // without having to check isSynced?
236
+ // NOTE: The "pendingUnsynced" must ALWAYS also set pendingWatches
237
+ pendingUnsyncedAccesses: Set<string>;
238
+ lastUnsyncedAccesses: Set<string>;
239
+
240
+ pendingUnsyncedParentAccesses: Set<string>;
241
+ lastUnsyncedParentAccesses: Set<string>;
242
+
243
+ specialPromiseUnsynced: boolean;
244
+ lastSpecialPromiseUnsynced: boolean;
245
+ lastSpecialPromiseUnsyncedReason?: string;
246
+
247
+ consecutiveErrors: number;
248
+ countSinceLastFullSync: number;
249
+ lastSyncTime: number;
250
+ syncRunCount: number;
251
+ startTime: number;
252
+
253
+ lastEvalTime: number;
254
+ inThrottle?: boolean;
255
+
256
+ permissionsChecker?: PermissionsChecker;
257
+
258
+ /** Allows us to tag the object for heap analysis */
259
+ tag: SyncWatcherTag;
260
+
261
+ hackHistory: { message: string; time: number }[];
262
+ }
263
+ function addToHistory(watcher: SyncWatcher, message: string) {
264
+ watcher.hackHistory.push({ message, time: Date.now() });
265
+ }
266
+
267
+
268
+ // NOTE: A lot of our proxy callback functionality is partially duplicated in PathTimeRunner
269
+ export let atomicObjectSymbol = Symbol.for("atomicObject");
270
+ export function atomicObjectWrite<T>(obj: T): T {
271
+ if (canHaveChildren(obj) && Object.isExtensible(obj)) {
272
+ (obj as any)[atomicObjectSymbol] = true;
273
+ }
274
+ return recursiveFreeze(obj);
275
+ }
276
+ export function atomicCloneWrite<T>(obj: T): T {
277
+ return atomicObjectWrite(deepCloneJSON(obj));
278
+ }
279
+ export function atomicObjectWriteNoFreeze<T>(obj: T): T {
280
+ if (canHaveChildren(obj) && Object.isExtensible(obj)) {
281
+ (obj as any)[atomicObjectSymbol] = true;
282
+ }
283
+ return obj;
284
+ }
285
+ const readNoProxy = Symbol.for("readNoProxy");
286
+ // Specifies we are reading this object, and not any children of it
287
+ export function atomicObjectRead<T>(obj: T): T {
288
+ if (!isValueProxy(obj)) return obj;
289
+ return (obj as any)[readNoProxy];
290
+ }
291
+ export const atomic = atomicObjectRead;
292
+ (global as any).atomic = atomic;
293
+
294
+ export function doUnatomicWrites<T>(callback: () => T): T {
295
+ return doProxyOptions({ atomicWrites: false }, callback);
296
+ }
297
+ export function doAtomicWrites<T>(callback: () => T): T {
298
+ return doProxyOptions({ atomicWrites: true }, callback);
299
+ }
300
+
301
+ const rawRead = Symbol.for("rawRead");
302
+ /** Reads the raw value, which includes transparent values.
303
+ * - Basically just useful for testing if an object exists in a lookup, in which case
304
+ * specialObjectWriteValue will be returned for the object.
305
+ * - NEVER returns a proxy, always a real object (which might be undefined, or an object).
306
+ */
307
+ export function atomicRaw<T>(obj: T): T {
308
+ if (!isValueProxy(obj)) return obj;
309
+ return (obj as any)[rawRead];
310
+ }
311
+
312
+ export function doProxyOptions<T>(options: Partial<WatcherOptions<any>>, callback: () => T): T {
313
+ let watcher = proxyWatcher.getTriggeredWatcher();
314
+ let prev = watcher.options;
315
+ watcher.options = { ...watcher.options, ...options };
316
+ try {
317
+ return callback();
318
+ } finally {
319
+ watcher.options = prev;
320
+ }
321
+ }
322
+
323
+ // HACK: A special value which acts like undefined, EXCEPT when using Object.keys(). The reason it can't
324
+ // be undefined, is that undefined values are removed by garbage collection, and this has to stick around!
325
+ // - Is automatically set in the parents paths of object writes, so Object.keys() works.
326
+ // (Which makes garbage collection a lot harder, but... not impossible, and Object.keys() is so important
327
+ // that it is okay).
328
+ // - Due to permissions rejecting root writes, BUT, the ease of this value getting nito relatively root paths
329
+ // (although hopefully not THE root), reading this counts as a "readIsUndefined"
330
+ // NOTE: Yes, this does technically mean you depend on the first value to write to an object.
331
+ // And so, if the first write is rejected, the lock becomes rejected, even though
332
+ // there might be other writes. This is fine, because the first value is the oldest, so
333
+ // the least likely to be rejected, and if it is... FunctionRunner will just rerun
334
+ // the dependent call, and find the key exists again, and the only difference will
335
+ // be a bit of lag, and value flicker.
336
+ export const specialObjectWriteValue = "_specialObjectWriteValue_16c4c3bb43f24111976a2681c972f6f4";
337
+ // Values that don't add another proxy layer
338
+ export function isTransparentValue(value: unknown) {
339
+ return (
340
+ value === undefined
341
+ || value === specialObjectWriteValue
342
+ );
343
+ }
344
+
345
+ export function undeleteFromLookup<T>(lookup: { [key: string]: T }, key: string): void {
346
+ lookup[key] = {} as any;
347
+ }
348
+
349
+ const syncedSymbol = Symbol.for("syncedSymbol");
350
+ // HACK: This should probably be somewhere else, but... it is just so useful for PathFunctionRunner...
351
+ export function isSynced(obj: unknown): boolean {
352
+ // If it is a primitive, then it must be synced!
353
+ if (!canHaveChildren(obj)) return true;
354
+ if (!isValueProxy(obj)) return true;
355
+ return !!(obj[syncedSymbol]);
356
+ }
357
+
358
+ export type PermissionsChecker = {
359
+ checkPermissions(path: string): { permissionsPath: string; allowed: boolean; };
360
+ };
361
+
362
+
363
+ const specialObjectWriteDepth = lazy(() => {
364
+ if (isDeploy()) return MODULE_INDEX;
365
+ return DEPTH_TO_DATA;
366
+ });
367
+
368
+ class SyncWatcherTag { }
369
+
370
+ export class PathValueProxyWatcher {
371
+ public static BREAK_ON_READS = new Set<string>();
372
+ public static BREAK_ON_WRITES = new Set<string>();
373
+ public static SET_FUNCTION_WATCH_ON_WRITES = new Set<string>();
374
+ public static LOG_WRITES_INCLUDES = new Set<string>();
375
+
376
+ // getPathStr(ModuleId, FunctionId)
377
+ public static BREAK_ON_CALL = new Set<string>();
378
+
379
+ public static DEBUG = false;
380
+ /** NOTE: This results in VERY slow performance, and will log A LOT of information. */
381
+ public static TRACE = false;
382
+ public static TRACE_WRITES = false;
383
+
384
+ private SHOULD_TRACE(watcher: SyncWatcher) {
385
+ return PathValueProxyWatcher.DEBUG || PathValueProxyWatcher.TRACE || PathValueProxyWatcher.TRACE_WRITES && watcher.options.canWrite || ClientWatcher.DEBUG_TRIGGERS === "heavy";
386
+ }
387
+
388
+ private isUnsyncCheckers = new Set<{ value: number }>();
389
+
390
+ public countUnsynced = <T>(checker: { value: number }, code: () => T): T => {
391
+ this.isUnsyncCheckers.add(checker);
392
+ try {
393
+ return code();
394
+ } finally {
395
+ this.isUnsyncCheckers.delete(checker);
396
+ }
397
+ };
398
+
399
+ public getCallbackPathValue = (pathStr: string, syncParentKeys?: "parentKeys"): PathValue | undefined => {
400
+ const watcher = this.runningWatcher;
401
+ if (!watcher) {
402
+ throw new Error(`Tried to get path "${pathStr}" outside of a watcher function.`);
403
+ }
404
+
405
+ if (watcher.options.noLocks && !pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
406
+ throw new Error(`Tried to read a non-local path in a "noLocks" watcher, ${watcher.debugName}, path ${pathStr}`);
407
+ }
408
+
409
+ if (watcher.permissionsChecker && !PermissionsCheck.skippingChecks) {
410
+ if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
411
+ if (
412
+ !watcher.hasAnyUnsyncedAccesses()
413
+ && authorityStorage.DEBUG_hasAnyValues(pathStr)
414
+ // HACK: Don't show warnings for some framework paths, because they are filling up the console logs
415
+ // and don't really matter. We could just not request them, but at depth 3 is valid,
416
+ // so we kind of have to request that. And at depth 2, it would require special case code,
417
+ // in a bunch of annoying places.
418
+ && (getPathIndex(pathStr, 1) !== "PathFunctionRunner" || getPathDepth(pathStr) > 3)
419
+ ) {
420
+ console.warn(`Denied read access to path "${pathStr}"`);
421
+ // console.warn(`${new Date().toLocaleTimeString()} Denied read access to path "${pathStr}"`);
422
+ // console.warn(new Error().stack);
423
+ }
424
+ // This undefined MIGHT cause issues, but... it also makes it easier to check if
425
+ // we have permissions, and more clear if we are denied permissions.
426
+ return undefined;
427
+ }
428
+ }
429
+
430
+ if (watcher.options.writeOnly) {
431
+ return undefined;
432
+ }
433
+
434
+ const currentReadTime = watcher.options.forceReadLatest ? undefined : watcher.currentReadTime;
435
+
436
+ let pathValue = authorityStorage.getValueAtTime(pathStr, currentReadTime);
437
+ // NOTE: If we have any value, we are always synced (that's what synced means, as if we aren't syncing,
438
+ // we delete any values, to prevent stale values from being used).
439
+ if (!pathValue) {
440
+ // NOTE: We might not have a value simply due to reading too far back in time,
441
+ // so we have to call isSynced just to be sure it is actually unsynced
442
+ if (!authorityStorage.isSynced(pathStr)) {
443
+ for (let checker of this.isUnsyncCheckers) {
444
+ checker.value++;
445
+ }
446
+ watcher.pendingUnsyncedAccesses.add(pathStr);
447
+ }
448
+ }
449
+ if (syncParentKeys) {
450
+ if (!authorityStorage.isParentSynced(pathStr)) {
451
+ watcher.pendingUnsyncedParentAccesses.add(pathStr);
452
+ }
453
+ }
454
+ watcher.pendingWatches.paths.add(pathStr);
455
+ if (syncParentKeys) {
456
+ watcher.pendingWatches.parentPaths.add(pathStr);
457
+ }
458
+
459
+ if (!pathValue) {
460
+ let pendingNullAccesses = watcher.pendingEpochAccesses.get(currentReadTime);
461
+ if (!pendingNullAccesses) {
462
+ pendingNullAccesses = new Set();
463
+ watcher.pendingEpochAccesses.set(currentReadTime, pendingNullAccesses);
464
+ }
465
+ pendingNullAccesses.add(pathStr);
466
+ return undefined;
467
+ } else {
468
+ let readValues = watcher.pendingAccesses.get(currentReadTime);
469
+ if (!readValues) {
470
+ readValues = new Map();
471
+ watcher.pendingAccesses.set(currentReadTime, readValues);
472
+ }
473
+ readValues.set(pathValue.path, pathValue);
474
+ }
475
+
476
+ return pathValue;
477
+ };
478
+ public getCallback = (pathStr: string, syncParentKeys?: "parentKeys", readTransparent?: "readTransparent"): { value: unknown } | undefined => {
479
+ if (PathValueProxyWatcher.BREAK_ON_READS.size > 0 && proxyWatcher.isAllSynced()) {
480
+ if (PathValueProxyWatcher.BREAK_ON_READS.has(pathStr)) {
481
+ debugger;
482
+ }
483
+ }
484
+ const watcher = this.runningWatcher;
485
+ if (!watcher) {
486
+ throw new Error(`Tried to get path "${pathStr}" outside of a watcher function.`);
487
+ }
488
+
489
+ // IMPORTANT! If we read a value we wrote, we don't need to incur a watch!
490
+ // NOTE: If we wrote undefined, then that DOESN'T clobber child values, as it actually
491
+ // deletes values, so... it doesn't count as having a value.
492
+ // - ALSO, writing undefined allows us to then read child values, which is odd,
493
+ // but is the correct behavior.
494
+ if (watcher.pendingWrites.has(pathStr)) {
495
+ let pendingValue = watcher.pendingWrites.get(pathStr);
496
+ let isPendingTransparent = isTransparentValue(pendingValue);
497
+ if (isPendingTransparent) {
498
+ if (readTransparent) {
499
+ return { value: pendingValue };
500
+ } else {
501
+ return undefined;
502
+ }
503
+ } else {
504
+ return { value: pendingValue };
505
+ }
506
+ }
507
+
508
+ let pathValue = this.getCallbackPathValue(pathStr, syncParentKeys);
509
+
510
+ if (!pathValue || !readTransparent && isTransparentValue(pathValueSerializer.getPathValue(pathValue))) {
511
+ let schema2 = getMatchingSchema(pathStr);
512
+ if (schema2) {
513
+ let atomicObj = Schema2Fncs.isAtomic(schema2.schema, "read", schema2.nestedPath);
514
+ if (atomicObj) {
515
+ return { value: atomicObj.defaultValue };
516
+ }
517
+ // Check if the object should be an object. Objects can be read though, if they've
518
+ // been initialized as an object (with specialObjectWriteValue). OTHERWISE,
519
+ // they require initialization (root.object = {}).
520
+ if (Schema2Fncs.isObject(schema2.schema, schema2.nestedPath)) {
521
+ // IMPORTANT! It MAY seem, like we can just add a special "deleteObjectValue_1f548f4a01cb4c35bc7b559d8dd74c42"
522
+ // sentinel, and use that to delete something (both in proxy, and in gc, so it really recursively deletes it),
523
+ // BUT, that has one major issue. Once that sentinel is truly deleted, the value will evaluate a proxy again,
524
+ // which will cause GC to change the state, which would be a bug. Not to mention accessing not existing
525
+ // keys would return a proxy, which is annoying. So... this is the best approach.
526
+ if (pathValueSerializer.getPathValue(pathValue) !== specialObjectWriteValue) {
527
+ // Means the key was deleted (`delete lookup[key]`), and so the intention is for this
528
+ // to terminate in undefined.
529
+ return { value: undefined };
530
+ }
531
+ }
532
+ }
533
+ return undefined;
534
+ } else {
535
+ let readValue = pathValueSerializer.getPathValue(pathValue);
536
+ // Unescape specialObjectWriteValue-like strings
537
+ if (typeof readValue === "string" && readValue.length > specialObjectWriteValue.length && readValue.startsWith(specialObjectWriteValue)) {
538
+ readValue = readValue.slice(0, -1);
539
+ }
540
+ return { value: readValue };
541
+ }
542
+ };
543
+
544
+ // We exclude undefined, BUT, we include "null", etc.
545
+ // - This differs from javascript, but... due to how deletions work, we can't/don't differentiate between
546
+ // a deleted value and undefined, and a lot of things break if deleting a value doesn't remove it from an "in" check!
547
+ private hasCallback = (pathStr: string): boolean => {
548
+ let value = this.getCallback(pathStr, undefined, "readTransparent");
549
+ return value?.value !== undefined;
550
+ };
551
+
552
+ private setCallback = (pathStr: string, value: unknown, inRecursion = false, allowSpecial = false): void => {
553
+ if (PathValueProxyWatcher.BREAK_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
554
+ if (PathValueProxyWatcher.BREAK_ON_WRITES.has(pathStr)) {
555
+ debugger;
556
+ }
557
+ }
558
+
559
+ if (PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
560
+ if (PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES.has(pathStr)) {
561
+ PathValueProxyWatcher.BREAK_ON_CALL.add(
562
+ getPathStr2(getCurrentCall().ModuleId, getCurrentCall().FunctionId)
563
+ );
564
+ }
565
+ }
566
+
567
+ if (PathValueProxyWatcher.LOG_WRITES_INCLUDES.size > 0 && proxyWatcher.isAllSynced()) {
568
+ for (let log of PathValueProxyWatcher.LOG_WRITES_INCLUDES) {
569
+ if (pathStr.includes(log)) {
570
+ console.log(`Write path "${pathStr}" = ${value}`);
571
+ }
572
+ }
573
+ }
574
+
575
+ // Escape specialObjectWriteValue-like strings
576
+ // - Check for .startsWith, so we change all values. If we just added when it was ===, then
577
+ // anything that === the escaped value before hand, would get unescaped, even though
578
+ // it was never escaped!
579
+ if (!allowSpecial && typeof value === "string" && value.startsWith(specialObjectWriteValue)) {
580
+ value += " ";
581
+ }
582
+
583
+ const watcher = this.runningWatcher;
584
+ if (!watcher) {
585
+ // Ignore preact paths
586
+ let lastPart = getLastPathPart(pathStr);
587
+ if (lastPart.startsWith("__") || lastPart === "type") {
588
+ return;
589
+ }
590
+ throw new Error(`Tried to set path "${pathStr}" outside of a watcher function.`);
591
+ }
592
+ if (!watcher.options.canWrite) {
593
+ throw new Error(`Tried to write to path "${pathStr}" in watcher (${watcher.debugName}) that has { canWrite: false }`);
594
+ }
595
+ if (watcher.options.noLocks && !pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
596
+ throw new Error(`Tried to write a non-local path in a "noLocks" watcher, ${watcher.debugName}, path ${pathStr}`);
597
+ }
598
+
599
+ if (watcher.permissionsChecker) {
600
+ if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
601
+ if (value !== specialObjectWriteValue) {
602
+ if (!watcher.hasAnyUnsyncedAccesses()) {
603
+ // NOTE: Only throw once we sync all accesses, otherwise this could cause sync cascading, which is bad
604
+ // NOTE: We COULD do the same thing for reason... but... it should be equivalent to just NOOP reads. We
605
+ // could NOOP writes as well, but throwing makes development easier (by telling you why your write
606
+ // is doing nothing more loudly than just a console warning).
607
+ throw new Error(`Denied write access to path "${pathStr}"`);
608
+ //console.warn(red(`Denied write access to path "${pathStr}" (ignoring write and continuing)`));
609
+ }
610
+ }
611
+ return;
612
+ }
613
+ }
614
+
615
+ if (
616
+ !watcher.options.forceEqualWrites
617
+ && (
618
+ !canHaveChildren(value)
619
+ || !(atomicObjectSymbol in value || watcher.options.atomicWrites)
620
+ )
621
+ ) {
622
+ // NOTE: This NOOPs on predicted writes as well. BUT, we also incur a lock, so, it's fine...
623
+ let existingValue = this.getCallback(pathStr, undefined, "readTransparent");
624
+ // NOTE: We don't deep compare atomicObjectSymbol, as atomicObjectSymbol is often
625
+ // used for exotic values. The user will have to deep compare themselves, if
626
+ // they want to avoid the extra write.
627
+ if (existingValue?.value === value) {
628
+ return;
629
+ }
630
+ }
631
+
632
+ // Set parent specialObjectWriteValue values
633
+ if (
634
+ !watcher.options.writeOnly
635
+ // If we are an event write we can't set parent values, otherwise the parents will become
636
+ // events, which breaks Object.keys on them (which is almost certainly not intended), because
637
+ // event paths can't be watched after a certain time.
638
+ && !watcher.options.eventWrite
639
+ && !inRecursion
640
+ // Deletes shouldn't create parent objects!
641
+ && value !== undefined
642
+ ) {
643
+ if (!watcher.pendingWrites.has(pathStr)) {
644
+ let parentPathStr = pathStr;
645
+ while (true) {
646
+ parentPathStr = getParentPathStr(parentPathStr);
647
+ if (getPathDepth(parentPathStr) < specialObjectWriteDepth()) break;
648
+ // We don't need to check all parents, as if any value is set, all parents will
649
+ // likely have their specialObjectWriteValue set, due to a previous run of this function!
650
+ if (watcher.pendingWrites.has(parentPathStr)) break;
651
+ let parentValue = this.getCallback(parentPathStr, undefined, "readTransparent");
652
+ // If we have a value, don't set the object.
653
+ // NOTE: This means if you:
654
+ // x.y.z = 1
655
+ // delete x.y; // (OR, x.y = null)
656
+ // x.y.z = 2
657
+ // Object.keys(x.y) === ["z"]
658
+ // Which is annoying (keys show up for values you accidentally resurrected), but...
659
+ // it is somewhat required, as we don't know if you are trying to readd
660
+ // an object, or accidentally resurrected it!
661
+ // - Really you should use a flag for deletion, which will work 100% of the time,
662
+ // and make life easier for specific undoing, and also eventually... instructed garbage collection.
663
+ if (parentValue?.value !== specialObjectWriteValue) {
664
+ watcher.pendingWrites.set(parentPathStr, specialObjectWriteValue);
665
+ }
666
+ }
667
+ }
668
+ }
669
+
670
+ function isAtomicSchema(pathStr: string) {
671
+ let schema2 = getMatchingSchema(pathStr);
672
+ if (schema2) {
673
+ let atomicObj = Schema2Fncs.isAtomic(schema2.schema, "write", schema2.nestedPath);
674
+ if (atomicObj) {
675
+ return true;
676
+ // NOTE: We can't skip if the value === default, as the previous value might not be the default,
677
+ // (so the write might change the value).
678
+ // - We could optimize the = undefined case for atomic objects, where the value is already undefined
679
+ // (but we defaulted it), but... it's probably not worth the effort.
680
+ }
681
+ }
682
+ }
683
+
684
+ // Destructure the value if it is an object, UNLESS, it specifies that it is atomic.
685
+ if (canHaveChildren(value) && !(atomicObjectSymbol in value) && !watcher.options.atomicWrites && !isAtomicSchema(pathStr)) {
686
+ const addWrites = (pathStr: string, value: unknown) => {
687
+ if (canHaveChildren(value) && !(atomicObjectSymbol in value) && !isAtomicSchema(pathStr)) {
688
+ this.setCallback(pathStr, specialObjectWriteValue, true, true);
689
+ const keys = getKeys(value);
690
+ for (let key of keys) {
691
+ if (typeof key !== "string") {
692
+ throw new Error(`Cannot have non-string keys in non-atomic (atomicObjectWrite) objects. Key: "${String(key)}" at path "${pathStr}".`);
693
+ }
694
+ addWrites(appendToPathStr(pathStr, key), value[key]);
695
+ }
696
+ } else {
697
+ // NOTE: It is a bit inefficient in terms of computation to call setCallback, BUT, it
698
+ // allows us to automatically run our deduping checks, which saves a lot of unnecessary
699
+ // computation on other nodes (and network bandwidth, etc, etc).
700
+ this.setCallback(pathStr, value, true);
701
+ //watcher.pendingWrites.set(pathStr, value);
702
+ }
703
+ };
704
+ addWrites(pathStr, value);
705
+ } else {
706
+ watcher.pendingWrites.set(pathStr, value);
707
+ }
708
+ };
709
+ public getKeys = (pathStr: string): string[] => {
710
+ if (getPathDepth(pathStr) < MODULE_INDEX) {
711
+ throw new Error(`Cannot call getKeys on path "${pathStr}" because it is too shallow. Must be at least ${MODULE_INDEX} levels deep.`);
712
+ }
713
+
714
+ const watcher = this.runningWatcher;
715
+ if (!watcher) {
716
+ throw new Error(`Tried to getKeys on path "${pathStr}" outside of a watcher function.`);
717
+ }
718
+
719
+ if (watcher.permissionsChecker) {
720
+ if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
721
+ console.warn(`Denied read access to parentPath "${pathStr}"`);
722
+ return [];
723
+ }
724
+ let childPath = appendToPathStr(pathStr, Date.now() + "_" + Math.random());
725
+ if (!watcher.permissionsChecker.checkPermissions(childPath).allowed) {
726
+ console.warn(red(`Denied write access to children of parent path "${childPath}"`));
727
+ return [];
728
+ }
729
+ }
730
+
731
+ // Using the schema for keys is a MASSIVE optimization, despite the potential drawbacks.
732
+ // - It returns all possible keys, even optional keys, which isn't the same as Object.keys()
733
+ // - It ignores any keys removed from the schema, which causes JSON.stringify to behave differently
734
+ // HOWEVER, saving an entire serverside call, which would otherwise require a roundtrip before we could
735
+ // access values inside the keys... is unbelievably useful. This reduces latency, which is the hardest
736
+ // thing to remove. AND, "noAtomicSchema" can always be used to skip this optimization.
737
+ let schema2 = getMatchingSchema(pathStr);
738
+ if (schema2) {
739
+ let atomicKeys = Schema2Fncs.getKeys(schema2.schema, schema2.nestedPath);
740
+ if (atomicKeys) {
741
+ return atomicKeys;
742
+ }
743
+ }
744
+
745
+ // IMPORTANT: The this.getCallback is what triggers the parent watch!
746
+ // We leave it up to getCallback to add the parentPaths watch, to allow Object.keys on values
747
+ // that were just written with incurring a parent watch.
748
+ let pathValue = this.getCallback(pathStr, "parentKeys");
749
+ if (!isTransparentValue(pathValue?.value)) {
750
+ return getKeys(pathValue?.value) as string[];
751
+ }
752
+
753
+ let keys: string[] = [];
754
+ let childPaths = authorityStorage.getPathsFromParent(pathStr);
755
+ if (childPaths) {
756
+ let targetDepth = getPathDepth(pathStr);
757
+ for (let childPath of childPaths) {
758
+ let key = getPathIndexAssert(childPath, targetDepth);
759
+ let childValue = this.getCallback(childPath, undefined, "readTransparent");
760
+ if (childValue?.value !== undefined) {
761
+ keys.push(key);
762
+ }
763
+ }
764
+ }
765
+
766
+ // NOTE: Because getPathsFromParent does not preserve order, we have to sort here to ensure
767
+ // we provide a consistent order (which might not be the order the user wants, but at least
768
+ // it will always fail if it does fail).
769
+ keys.sort();
770
+
771
+ return keys;
772
+ };
773
+
774
+ private getSymbol = (pathStr: string, symbol: symbol): { value: unknown } | undefined => {
775
+ if (symbol === Symbol.toPrimitive) return {
776
+ value: (hint: string) => {
777
+ if (hint === "string") return "";
778
+ if (hint === "number") return 0;
779
+ // NOTE: Returning 0 as the default makes (x++) work nicely, defaulting values to undefined.
780
+ // If they are expecting a string... maybe they should use `${x}`, or... just type check it.
781
+ return 0;
782
+ }
783
+ };
784
+ if (symbol === Symbol.toStringTag) return { value: () => `[proxy at ${pathStr}]` };
785
+
786
+ if (symbol === readNoProxy) {
787
+ let value = this.getCallback(pathStr)?.value;
788
+ return { value };
789
+ }
790
+
791
+ if (symbol === syncedSymbol) {
792
+ return { value: authorityStorage.isSynced(pathStr) };
793
+ }
794
+
795
+ // Proxies should be considered atomic, at least for the purpose of other proxies!
796
+ // - This allows proxies to be written to synced state easily (although this will
797
+ // cause issues if it isn't local synced state, but... there's not too much
798
+ // we can do about that. It should just serialize it, possibly reading values
799
+ // which weren't synced. Which isn't so bad, as if they start trying to create
800
+ // object graphs in synced state they are going to immediately run into issues
801
+ // anyways).
802
+ if (symbol === atomicObjectSymbol) return { value: true };
803
+
804
+ if (symbol === rawRead) {
805
+ return { value: this.getCallback(pathStr, undefined, "readTransparent")?.value };
806
+ }
807
+
808
+ return { value: undefined };
809
+ };
810
+
811
+ private proxy = createPathValueProxy({
812
+ getCallback: this.getCallback,
813
+ hasCallback: this.hasCallback,
814
+ setCallback: this.setCallback,
815
+ getKeys: this.getKeys,
816
+ getSymbol: this.getSymbol,
817
+ });
818
+
819
+ private runningWatcher: SyncWatcher | undefined;
820
+
821
+ // NOTE: We can't support promises until we are able to run the function on another
822
+ // thread (at which point we can ensure there is no parallel function running).
823
+ public createWatcher<Result = void>(
824
+ options: WatcherOptions<Result>
825
+ ): SyncWatcher {
826
+ // NOTE: Setting an order is needed for rendering, so parents render before children. I believe
827
+ // it is generally what we want, so event triggering is consistent, and fits with any tree based
828
+ // watching system. If this causes problems we COULD remove it from here and have just qreact.tsx set it.
829
+ if (options.order === undefined) {
830
+ options.order = nextOrderSeqNum++;
831
+ }
832
+
833
+ // Unwrap nested immediate calls to be as close to a naked call as possible. The watcher never becomes
834
+ // the current watcher, and is immediately disposed. We only create SyncWatcher to return it, otherwise
835
+ // callers would need to handle a variable return type, which is annoying.
836
+ // - This means any writes are attributed to the existing call, which... is fine, if the caller wants
837
+ // to disconnect the call from the existing watcher, they should use `Promise.resolve().finally(...)`.
838
+ if (options.runImmediately && this.runningWatcher) {
839
+ // TODO: We COULD change our dispose out and change it back, if we find
840
+ // we often want to mutate shallow props of nested watchers.
841
+ const outerWatcher = {
842
+ ...this.runningWatcher,
843
+ // Let them use the watcher, EXCEPT, don't let them dispose it,
844
+ // as that would dispose the outer watcher as well
845
+ dispose: () => { },
846
+ };
847
+ if (!this.runningWatcher.options.canWrite && options.canWrite) {
848
+ debugger;
849
+ throw new Error(`Cannot run a canWrite nested watcher inside of a watcher that can't. Either explicitly specify canWrite = true in outer watcher, canWrite = false in the inner watcher, or disconnect it with Promise.resolve().finally(...)`);
850
+ }
851
+ // NOTE: Nested watchers are fine. We need to apply the nested options
852
+ // (some of which will be ignroed), but otherwise, all of the watchers
853
+ // will be collapsed to a single watcher, making it fairly efficient.
854
+ let { watchFunction, ...mostOptions } = options;
855
+ doProxyOptions(mostOptions, () => {
856
+ try {
857
+ let result = authorityStorage.temporaryOverride(options.overrides, () =>
858
+ options.watchFunction()
859
+ );
860
+ options.onResultUpdated?.({ result }, undefined, outerWatcher);
861
+ } catch (e: any) {
862
+ options.onResultUpdated?.({ error: e.stack }, undefined, outerWatcher);
863
+ }
864
+ });
865
+ return outerWatcher;
866
+ }
867
+
868
+ const self = this;
869
+ if (options.runAtTime) {
870
+ // 80% of our real max range, so calls with a runAtTime close to our limit have a bit of breathing room to run
871
+ let runLeeway = MAX_ACCEPTED_CHANGE_AGE * 0.8;
872
+ let runCutoffTime = Date.now() - runLeeway;
873
+ if (options.runAtTime && options.runAtTime.time < runCutoffTime) {
874
+ // let timeSinceStart = Date.now() - startTime;
875
+ // // If we JUST started, we are just clearing out old function calls, so don't break on the error
876
+ // // (Run leeway time + 60s)
877
+ // if (timeSinceStart > 1000 * 60 + runLeeway) {
878
+ // debugbreak(2);
879
+ // debugger;
880
+ // }
881
+ let message = `MAX_CHANGE_AGE_EXCEEDED! Cannot run watcher at time ${options.runAtTime.time} because it is older than the cutOff time of ${runCutoffTime}. Writing this far in the past would break things, and might be rejected by other authorities due to being too old.`;
882
+ console.error(red(message));
883
+ // NOTE: We could also adjust the to be more recent, to allow it to be commited anyway,
884
+ // in a slightly different state than originally expected.
885
+ throw new Error(message);
886
+ }
887
+ }
888
+
889
+ let now = Date.now();
890
+ let watcher: SyncWatcher = {
891
+ debugName: options.debugName || options.watchFunction.name || options.watchFunction.toString().replaceAll("\n", "\\n").replaceAll(/ +/g, " ").slice(0, 50),
892
+ dispose: () => { },
893
+ disposed: false,
894
+ onInnerDisposed: [],
895
+ explicitlyTrigger: () => { },
896
+ hasAnyUnsyncedAccesses: () => false,
897
+ currentReadTime: options.runAtTime,
898
+ nextWriteTime: undefined,
899
+ setCurrentReadTime: null as any,
900
+ pendingAccesses: new Map(),
901
+ pendingEpochAccesses: new Map(),
902
+ triggeredByChanges: undefined,
903
+ pendingWrites: new Map(),
904
+ pendingEventWrites: new Set(),
905
+ pendingCalls: [],
906
+ pendingWatches: {
907
+ paths: new Set(),
908
+ parentPaths: new Set(),
909
+ },
910
+ lastWatches: {
911
+ paths: new Set(),
912
+ parentPaths: new Set(),
913
+ },
914
+
915
+ pendingUnsyncedAccesses: new Set(),
916
+ pendingUnsyncedParentAccesses: new Set(),
917
+ lastUnsyncedAccesses: new Set(),
918
+ lastUnsyncedParentAccesses: new Set(),
919
+
920
+ specialPromiseUnsynced: false,
921
+ lastSpecialPromiseUnsynced: false,
922
+
923
+ options,
924
+ consecutiveErrors: 0,
925
+ countSinceLastFullSync: 0,
926
+ lastEvalTime: 0,
927
+ lastSyncTime: now,
928
+ startTime: now,
929
+ syncRunCount: 0,
930
+ tag: new SyncWatcherTag(),
931
+
932
+ hackHistory: [],
933
+ };
934
+ const SHOULD_TRACE = this.SHOULD_TRACE(watcher);
935
+ const proxy = this.proxy;
936
+
937
+ if (options.overrides && !options.dryRun) {
938
+ // If you override values, and want to watch values... how does that even work? And what about
939
+ // writes? You can't depend on an override, so... what would we do?
940
+ throw new Error(`overrides without dryRun is not presently supported`);
941
+ }
942
+
943
+ watcher.dispose = dispose;
944
+ this.allWatchers.add(watcher);
945
+
946
+ const baseFunction = measureWrap(options.watchFunction, watcher.debugName);
947
+
948
+ watcher.setCurrentReadTime = (time, code) => {
949
+ let prev = watcher.currentReadTime;
950
+ watcher.currentReadTime = time;
951
+ try {
952
+ return code();
953
+ } finally {
954
+ watcher.currentReadTime = prev;
955
+ }
956
+ };
957
+ watcher.hasAnyUnsyncedAccesses = () => {
958
+ return watcher.pendingUnsyncedAccesses.size > 0 || watcher.pendingUnsyncedParentAccesses.size > 0 || watcher.specialPromiseUnsynced;
959
+ };
960
+
961
+ function dispose() {
962
+ watcher.disposed = true;
963
+ clientWatcher.unwatch(trigger);
964
+ self.allWatchers.delete(watcher);
965
+ let disposeCallbacks = watcher.onInnerDisposed;
966
+ watcher.onInnerDisposed = [];
967
+ for (let callback of disposeCallbacks) {
968
+ logErrors(((async () => { await callback(); }))());
969
+ }
970
+ }
971
+ function runWatcher() {
972
+ watcher.pendingWrites.clear();
973
+ watcher.pendingEventWrites.clear();
974
+ watcher.pendingCalls = [];
975
+ watcher.pendingWatches.paths.clear();
976
+ watcher.pendingWatches.parentPaths.clear();
977
+ watcher.pendingUnsyncedAccesses.clear();
978
+ watcher.pendingUnsyncedParentAccesses.clear();
979
+
980
+ watcher.pendingAccesses.clear();
981
+ watcher.pendingEpochAccesses.clear();
982
+ watcher.onInnerDisposed = [];
983
+
984
+ // NOTE: If runAtTime is undefined, the writeTime will be undefined, causing us to read the latest data.
985
+ // When we finish running we will determine a lock time > any received times.
986
+ // - Even using Date.now() is not far enough in the future, as we might be given data a bit before Date.now() due to clock
987
+ // differences. And we can't just set it arbitrarily into the future, as then we will be writing
988
+ // in the future, which would cause us to become rejected on writes that happen after our real
989
+ // time (but after our given time).
990
+ watcher.nextWriteTime = watcher.options.runAtTime;
991
+ // Ensure nextWriteTime is a unique time every run, in case we were rerun due to a rejection (unless it is a dryRun,
992
+ // in which case the writes won't be committed, so they don't need to be unique).
993
+
994
+ // NOTE: We prefer to not set the time, as if our clock is at all in the past (or other clocks are in the future),
995
+ // then when we receive a value we will ignore it.
996
+ if (!watcher.options.dryRun && watcher.nextWriteTime) {
997
+ watcher.nextWriteTime = clientWatcher.getFreeWriteTime(watcher.nextWriteTime);
998
+ }
999
+
1000
+ // The read time equals the write time, so our locks use the same time as our write time, and
1001
+ // therefore lock the actual state we used.
1002
+ watcher.currentReadTime = watcher.nextWriteTime;
1003
+
1004
+ watcher.permissionsChecker = options.getPermissionsCheck?.();
1005
+
1006
+ let result: { result: Result } | { error: string };
1007
+ try {
1008
+ let rawResult: Result;
1009
+ const handling = watcher.options.nestedCalls;
1010
+ if (handling === "inline") {
1011
+ rawResult = inlineNestedCalls(() => {
1012
+ return authorityStorage.temporaryOverride(options.overrides, () =>
1013
+ runCodeWithDatabase(proxy, baseFunction)
1014
+ );
1015
+ });
1016
+ } else {
1017
+ rawResult = interceptCalls({
1018
+ onCall(call, metadata) {
1019
+ if (PathValueProxyWatcher.BREAK_ON_CALL.size > 0 && !watcher.hasAnyUnsyncedAccesses()) {
1020
+ let hash = getPathStr2(call.ModuleId, call.FunctionId);
1021
+ if (PathValueProxyWatcher.BREAK_ON_CALL.has(hash)) {
1022
+ debugger;
1023
+ }
1024
+ }
1025
+ if (!watcher.options.canWrite) {
1026
+ throw new Error(`Cannot call a synced function in a watcher which does not have write permissions. If you want to call a function, you must also have write permissions.`);
1027
+ }
1028
+ if (handling === "throw") {
1029
+ // TODO: Support making inline calls, which is useful as it will check permissions. Although, it can
1030
+ // easily be slow, and adds a lot of complexity, so... maybe not... maybe always force the app
1031
+ // to do the permissions checks if they want them
1032
+ throw new Error(`Nested synced function calls are not allowed. Call the function directly, or use Querysub.onCommitFinished to wait for the function to finish.`);
1033
+ } else if (handling === "after" || handling === undefined) {
1034
+ watcher.pendingCalls.push({ call, metadata });
1035
+ } else if (handling === "ignore") {
1036
+ } else {
1037
+ let unhandled: never = handling;
1038
+ throw new Error(`Invalid handling type ${handling}`);
1039
+ }
1040
+ },
1041
+ code() {
1042
+ return authorityStorage.temporaryOverride(options.overrides, () =>
1043
+ runCodeWithDatabase(proxy, baseFunction)
1044
+ );
1045
+ },
1046
+ });
1047
+ }
1048
+
1049
+ // We use atomic object read, as our callers don't want a proxy.
1050
+ // If they want to get a full object, they will have to destructure it themselves
1051
+ // (as for us to destructure it would require further synchronization, which...
1052
+ // is just annoying, and so we won't implement that unless it becomes very useful).
1053
+ // (Also, if the result is a proxy is confuses promises into thinking it is another promise,
1054
+ // which is annoying).
1055
+ rawResult = atomicObjectRead(rawResult);
1056
+
1057
+ result = { result: rawResult };
1058
+ watcher.consecutiveErrors = 0;
1059
+ } catch (e: any) {
1060
+ watcher.consecutiveErrors++;
1061
+ if (watcher.consecutiveErrors > 10) {
1062
+ console.error(`Watch callback is failing a lot ${watcher.debugName} (${watcher.consecutiveErrors} times). It is possible you are throwing in a loop on unsynced values. Instead for all the values you want to check, then loop over them, otherwise syncing could take forever.`);
1063
+ console.error(e);
1064
+ }
1065
+ result = { error: e.stack };
1066
+ } finally {
1067
+ // Set the checker to undefined, so we don't accidentally use it again,
1068
+ // as it will throw if we try to use it after any asynchronous delay.
1069
+ watcher.permissionsChecker = undefined;
1070
+ }
1071
+
1072
+ watcher.lastUnsyncedAccesses = watcher.pendingUnsyncedAccesses;
1073
+ watcher.lastUnsyncedParentAccesses = watcher.pendingUnsyncedParentAccesses;
1074
+ watcher.lastSpecialPromiseUnsynced = watcher.specialPromiseUnsynced;
1075
+ const specialPromiseUnsynced = watcher.specialPromiseUnsynced;
1076
+ watcher.specialPromiseUnsynced = false;
1077
+
1078
+ if (!watcher.options.static) {
1079
+ if (!watcher.hasAnyUnsyncedAccesses()) {
1080
+ watcher.countSinceLastFullSync = 0;
1081
+ watcher.lastSyncTime = Date.now();
1082
+ watcher.syncRunCount++;
1083
+ } else {
1084
+ if (watcher.countSinceLastFullSync === 0) {
1085
+ let syncRunCount = watcher.syncRunCount;
1086
+ setTimeout(() => {
1087
+ if (watcher.syncRunCount !== syncRunCount) return;
1088
+ if (watcher.disposed) return;
1089
+ console.warn(red(`Did not sync ${watcher.debugName} after 15 seconds. Either we had a lot synchronous block, or we will never received the values we are waiting for.`), watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses, watcher.options.watchFunction);
1090
+ }, 15000);
1091
+ setTimeout(() => {
1092
+ if (watcher.syncRunCount !== syncRunCount) return;
1093
+ if (watcher.disposed) return;
1094
+
1095
+ // IMPORTANT! Any of these can be also caused by high synchronous lag!
1096
+ // - If this becomes a serious concern, we could add a watch loop for this, which
1097
+ // tries to run every second, recording how far it misses the target time by,
1098
+ // and then uses that (plus any outstanding missed time) to determine how much lag
1099
+ // we have had since this watcher last synced. If it is > 50% of the time...
1100
+ // then synchronous lag is the issue.
1101
+
1102
+ let reallyUnsyncedAccesses = Array.from(watcher.lastUnsyncedAccesses).filter(x => !authorityStorage.isSynced(x));
1103
+ let reallyUnsyncedParentAccesses = Array.from(watcher.lastUnsyncedParentAccesses).filter(x => !authorityStorage.isParentSynced(x));
1104
+
1105
+ if (reallyUnsyncedAccesses.length !== 0 || reallyUnsyncedParentAccesses.length !== 0) {
1106
+ let notWatchingUnsynced = reallyUnsyncedAccesses.filter(x => !remoteWatcher.debugIsWatchingPath(x));
1107
+ let notWatchingUnsyncedParent = reallyUnsyncedParentAccesses.filter(x => !remoteWatcher.debugIsWatchingPath(x));
1108
+ if (notWatchingUnsynced.length !== 0 || notWatchingUnsyncedParent.length !== 0) {
1109
+ console.error((`${red("WATCHER FAILED TO SYNC")} ${watcher.debugName} ${magenta("NOT REMOTE WATCHING REQUIRED PATHS")}. This means our sync or unsync (likely unsync) logic is broken, in remoteWatcher/clientWatcher. OR, there were no read nodes when we tried to sync (we don't handle missing read nodes correctly at the moment)`), notWatchingUnsynced, notWatchingUnsyncedParent, watcher.options.watchFunction);
1110
+ debugbreak(2);
1111
+ debugger;
1112
+ } else {
1113
+ console.error((`${red("WATCHER FAILED TO SYNC")} ${watcher.debugName} ${magenta("DID NOT RECEIVE PATH VALUES")}. This means PathValueServer is not responding to watches, either to specific paths, or for all paths`), reallyUnsyncedAccesses, reallyUnsyncedParentAccesses, watcher.options.watchFunction);
1114
+ // debugbreak(2);
1115
+ // debugger;
1116
+ }
1117
+ } else if (watcher.lastSpecialPromiseUnsynced) {
1118
+ console.warn((`${yellow("WATCHER SLOW TO SYNC")} ${watcher.debugName} ${magenta("DEPENDENT PROMISE NEVER RESOLVED")}. This promise might resolve, but it probably won't. Slow promises should be detached from the watcher system and use multiple watchers/writes, instead of blocking on a promise.`), watcher.lastSpecialPromiseUnsyncedReason, watcher.options.watchFunction);
1119
+ debugbreak(2);
1120
+ debugger;
1121
+ } else {
1122
+ console.error((`${red("WATCHER FAILED TO SYNC")} ${watcher.debugName} ${magenta("DID NOT TRIGGER WATCHER")}. This means either ProxyWatcher is broken (and isn't triggering when it should, or isn't watching when it should), or ClientWatcher/PathWatcher are broken and are not properly informing callers of watchers.`), watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses, watcher.options.watchFunction);
1123
+ debugbreak(2);
1124
+ debugger;
1125
+ }
1126
+ }, 60000);
1127
+ }
1128
+ watcher.countSinceLastFullSync++;
1129
+ if (watcher.countSinceLastFullSync > 500) {
1130
+ debugger;
1131
+ // NOTE: Using forceEqualWrites will also fix this a lot of the time, such as when
1132
+ // a write contains random numbers or dates.
1133
+ let errorMessage = `Too many attempts to sync with different values. If you are reading in a loop, make sure to read all the values, instead of aborting the loop if a value is not synced. ALSO, make sure you don't access paths with Math.random() or Date.now(). This will prevent the sync loop from ever stabilizing.`;
1134
+ if (specialPromiseUnsynced) {
1135
+ errorMessage += ` A promise is being accessed, so it is possible triggerOnPromiseFinish is being used on a new promise every loop, which cannot work (you MUST cache MaybePromise and replace the value with a non-promise, otherwise it will never be available synchronously!)`;
1136
+ }
1137
+ console.error(red(errorMessage));
1138
+ result = { error: new Error(errorMessage).stack || "" };
1139
+ // Force the watches to be equal, so we stop looping
1140
+ watcher.pendingWatches = watcher.lastWatches;
1141
+ watcher.syncRunCount++;
1142
+ }
1143
+ }
1144
+ }
1145
+
1146
+ return result;
1147
+ }
1148
+ function updateWatchers(initialTriggerWatch?: boolean) {
1149
+ // If nothing is being watched, and nothing was... then don't bother updating the watchers
1150
+ if (
1151
+ !initialTriggerWatch
1152
+ && watcher.lastWatches.paths.size === 0 && watcher.lastWatches.parentPaths.size === 0
1153
+ && watcher.pendingWatches.paths.size === 0 && watcher.pendingWatches.parentPaths.size === 0
1154
+ ) return;
1155
+
1156
+ watcher.lastWatches = watcher.pendingWatches;
1157
+ clientWatcher.setWatches({
1158
+ debugName: watcher.debugName,
1159
+ // We don't need to trigger immediately, as we already ran, and if all of our values were synced
1160
+ // we will have already committed any results.
1161
+ noInitialTrigger: true,
1162
+ paths: watcher.lastWatches.paths,
1163
+ parentPaths: watcher.lastWatches.parentPaths,
1164
+ order: watcher.options.order,
1165
+ orderGroup: watcher.options.orderGroup,
1166
+ callback: trigger,
1167
+ unwatchEventPaths(paths) {
1168
+ self.unwatchEventPaths(watcher, paths);
1169
+ },
1170
+ });
1171
+ }
1172
+ function commitWrites(): PathValue[] | undefined {
1173
+ if (!options.canWrite) return undefined;
1174
+
1175
+ let setValues: PathValue[] | undefined;
1176
+
1177
+ let prefixes = options.overrideAllowLockDomainsPrefixes || getAllowedLockDomainsPrefixes();
1178
+ // This consumes our write time, as once we write, we can't reuse writeTimes!
1179
+ // (If we do, it breaks dependencies).
1180
+ let writeTime = watcher.nextWriteTime;
1181
+ watcher.nextWriteTime = undefined;
1182
+ if (!writeTime) {
1183
+ writeTime = getNextTime();
1184
+ }
1185
+ if (watcher.pendingWrites.size > 0) {
1186
+ let locks: ReadLock[] = [];
1187
+
1188
+ if (!watcher.options.noLocks && !watcher.options.unsafeNoLocks) {
1189
+ for (let [readTime, values] of watcher.pendingAccesses) {
1190
+ if (!readTime) {
1191
+ readTime = writeTime;
1192
+ }
1193
+ for (let value of values.values()) {
1194
+ // If any value has no locks, AND is local, it won't be rejected, so we don't need to lock it
1195
+ if (value.lockCount === 0 && value.path.startsWith(LOCAL_DOMAIN_PATH)) {
1196
+ continue;
1197
+ }
1198
+ if (!prefixes.some(x => value.path.startsWith(x))) continue;
1199
+ let oldReadCheck = !!(
1200
+ value.canGCValue
1201
+ || pathValueSerializer.getPathValue(value) === specialObjectWriteValue
1202
+ );
1203
+ let newReadCheck = !!value.isTransparent;
1204
+ if (oldReadCheck !== newReadCheck) {
1205
+ // IMPORTANT! IF WE HIT THIS ASSERT, DEBUG IT RIGHT NOW!
1206
+ // - I updated the read check to use isTransparent. BUT... I'm not sure
1207
+ // this is correct. This WILL break if we add any new isTransparent values,
1208
+ // so this update is requried, but... this code is so old, that I'm not
1209
+ // sure updating it like this is safe.
1210
+ // - Double check the the value at `pathValueSerializer.getPathValue(value)`
1211
+ // and value, and see if it should act as a tranparent value or not.
1212
+ debugbreak(2);
1213
+ debugger;
1214
+ }
1215
+ locks.push({
1216
+ path: value.path,
1217
+ startTime: value.time,
1218
+ endTime: readTime,
1219
+ readIsTranparent: newReadCheck,
1220
+ });
1221
+ }
1222
+ }
1223
+ for (let [readTime, paths] of watcher.pendingEpochAccesses) {
1224
+ if (!readTime) {
1225
+ readTime = writeTime;
1226
+ }
1227
+ for (let path of paths) {
1228
+ if (!prefixes.some(x => path.startsWith(x))) continue;
1229
+ locks.push({
1230
+ path,
1231
+ startTime: epochTime,
1232
+ endTime: readTime,
1233
+ readIsTranparent: true,
1234
+ });
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ {
1240
+ // Ensure our write time isn't before any of our reads (as long as we aren't running
1241
+ // at a specific time).
1242
+ // - We might want an option to disable this behavior? Although, I'm not sure how we would
1243
+ // run in the present, but intentionally want to write in the past? This usually only
1244
+ // happens if we intentionally run in the past (in which case writeTime will be set,
1245
+ // so this if statement wouldn't run anyways).
1246
+ let fixedWriteTime = writeTime;
1247
+ for (let lock of locks) {
1248
+ if (compareTime(lock.endTime, fixedWriteTime) > 0) {
1249
+ fixedWriteTime = {
1250
+ time: addEpsilons(lock.endTime.time, 1),
1251
+ version: 0,
1252
+ creatorId: getCreatorId(),
1253
+ };
1254
+ }
1255
+ }
1256
+ if (fixedWriteTime !== writeTime) {
1257
+ let prevWriteTime = writeTime;
1258
+ writeTime = fixedWriteTime;
1259
+ for (let lock of locks) {
1260
+ if (lock.endTime === prevWriteTime) {
1261
+ lock.endTime = writeTime;
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+
1267
+ let forcedUndefinedWrites = new Set<string>();
1268
+ for (let [path, value] of watcher.pendingWrites) {
1269
+ if (value === specialObjectWriteValue) {
1270
+ forcedUndefinedWrites.add(path);
1271
+ }
1272
+ }
1273
+
1274
+ setValues = clientWatcher.setValues({
1275
+ values: watcher.pendingWrites,
1276
+ eventPaths: watcher.pendingEventWrites,
1277
+ writeTime: writeTime,
1278
+ locks,
1279
+ noWritePrediction: options.doNotStoreWritesAsPredictions,
1280
+ eventWrite: options.eventWrite,
1281
+ dryRun: options.dryRun,
1282
+ forcedUndefinedWrites,
1283
+ });
1284
+ }
1285
+
1286
+ // TODO: Maybe return this in some way, so dryRun can know what calls we want to call?
1287
+ for (let { call, metadata } of watcher.pendingCalls) {
1288
+ // The calls have to happen after our local writes. This is because they are likely to
1289
+ // influence the local writes, and we don't want our local writes to be always invalidated
1290
+ call.runAtTime = getNextTime();
1291
+ logErrors(runCall(call, metadata));
1292
+ }
1293
+
1294
+ return setValues;
1295
+
1296
+ }
1297
+ const hasUnsyncedBefore = measureWrap(function proxyWatchHasUnsyncedBefore() {
1298
+ // NOTE: We COULD remove any synced values, however... we will generally sync all values at once,
1299
+ // so we don't really need to optimize the cascading case here. Also... deleting values
1300
+ // requires cloning while we iterate, as well as mutating the set, which probably makes the
1301
+ // non-cascading case slower.
1302
+ let anyUnsynced = false;
1303
+ for (let path of watcher.lastUnsyncedAccesses) {
1304
+ if (!authorityStorage.isSynced(path)) {
1305
+ anyUnsynced = true;
1306
+ }
1307
+ }
1308
+ for (let path of watcher.lastUnsyncedParentAccesses) {
1309
+ if (!authorityStorage.isParentSynced(path)) {
1310
+ anyUnsynced = true;
1311
+ }
1312
+ }
1313
+ // NOTE: We don't check promises as they often access non-synced code, and so we might retrigger
1314
+ // and not use the same promise, so it might be wrong to check them.
1315
+ return anyUnsynced;
1316
+ });
1317
+ function logUnsynced() {
1318
+ let anyLogged = false;
1319
+ for (let path of watcher.lastUnsyncedAccesses) {
1320
+ if (!authorityStorage.isSynced(path)) {
1321
+ console.log(yellow(` Waiting for ${path}`));
1322
+ anyLogged = true;
1323
+ }
1324
+ }
1325
+ for (let path of watcher.lastUnsyncedParentAccesses) {
1326
+ if (!authorityStorage.isParentSynced(path)) {
1327
+ console.log(yellow(` Waiting for parent ${path}`));
1328
+ anyLogged = true;
1329
+ }
1330
+ }
1331
+ // NOTE: We don't log for promises, as they are not checked in hasUnsyncedBefore
1332
+ return anyLogged;
1333
+ }
1334
+
1335
+
1336
+ let trigger = (changed?: WatchSpecData) => {
1337
+ let time = Date.now();
1338
+
1339
+ if (this.runningWatcher) {
1340
+ throw new Error(`Tried to trigger a nested watcher. This likely means explicitlyTrigger was called inside a callback. Instead, use a watched synced sequenceNumber, accessed in the first line of the target watcher, which is incremented when you want to re-evaluate it. Attempted nested watcher ${watcher.debugName}, triggered by ${this.runningWatcher.debugName}`);
1341
+ }
1342
+ if (watcher.disposed) {
1343
+ return;
1344
+ }
1345
+
1346
+ // Merge triggers. We keep triggers if a callback fails to rerun due to unsynced values. This is essential, as it allows
1347
+ // callbacks to always receive every changed value, which we rely on to maintain delta based dependencies.
1348
+ {
1349
+ if (changed && (ClientWatcher.DEBUG_TRIGGERS || options.trackTriggers)) {
1350
+ if (watcher.triggeredByChanges) {
1351
+ for (let path of changed.paths) {
1352
+ watcher.triggeredByChanges.paths.add(path);
1353
+ }
1354
+ for (let parentPath of changed.newParentsSynced) {
1355
+ watcher.triggeredByChanges.newParentsSynced.add(parentPath);
1356
+ }
1357
+ if (changed.pathSources) {
1358
+ if (watcher.triggeredByChanges.pathSources) {
1359
+ for (let value of changed.pathSources) {
1360
+ watcher.triggeredByChanges.pathSources.add(value);
1361
+ }
1362
+ } else {
1363
+ watcher.triggeredByChanges.pathSources = new Set(changed.pathSources);
1364
+ }
1365
+ }
1366
+ if (changed.extraReasons) {
1367
+ watcher.triggeredByChanges.extraReasons = [
1368
+ ...watcher.triggeredByChanges.extraReasons || [],
1369
+ ...changed.extraReasons
1370
+ ];
1371
+ }
1372
+ } else {
1373
+ watcher.triggeredByChanges = changed;
1374
+ }
1375
+ }
1376
+ }
1377
+
1378
+ if (!options.runImmediately && !options.allowUnsyncedReads && hasUnsyncedBefore()) {
1379
+ if (SHOULD_TRACE) {
1380
+ console.log(yellow(`Skipping trigger due to unsynced watchers. ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
1381
+ if (!logUnsynced()) {
1382
+ debugbreak(2);
1383
+ debugger;
1384
+ }
1385
+ }
1386
+ return;
1387
+ }
1388
+
1389
+
1390
+ // NOTE: We don't log the trigger evaluation starting, as that should really be logged inside of ClientWatcher,
1391
+ // as a result of ClientWatcher.DEBUG_TRIGGERS (except for runImmediate).
1392
+ if (options.runImmediately) {
1393
+ if (SHOULD_TRACE) {
1394
+ console.log(`${green("INITIAL TRIGGER")} ${watcher.debugName}`, watcher.triggeredByChanges);
1395
+ }
1396
+ }
1397
+
1398
+ watcher.lastEvalTime = Date.now();
1399
+
1400
+ // Do as little as possible while runningWatcher is true. This allows callbacks, etc, to create
1401
+ // new watchers without having to juggle .runningWatcher.
1402
+ let result: { result: Result } | { error: string } | undefined;
1403
+ try {
1404
+ this.runningWatcher = watcher;
1405
+ result = runWatcher();
1406
+ } finally {
1407
+ this.runningWatcher = undefined;
1408
+ }
1409
+
1410
+ const readyToCommit = (
1411
+ options.runImmediately || options.allowUnsyncedReads || (
1412
+ watcher.lastUnsyncedAccesses.size === 0
1413
+ && watcher.lastUnsyncedParentAccesses.size === 0
1414
+ && !watcher.lastSpecialPromiseUnsynced
1415
+ )
1416
+ );
1417
+ // Commit writes BEFORE we watch. This prevents us from triggering ourself, in our first watch
1418
+ // (on subsequent watches ClientWatcher will prevent self watches. It can't for the first
1419
+ // watch, as it wasn't the triggerer).
1420
+ let values: PathValue[] | undefined;
1421
+ if (readyToCommit) {
1422
+ try {
1423
+ values = commitWrites();
1424
+ } catch (e: any) {
1425
+ // setValues might throw, such as if the write time is too far into the past.
1426
+ result = { error: e.stack };
1427
+ }
1428
+ }
1429
+
1430
+ if (SHOULD_TRACE && "error" in result) {
1431
+ console.log(red(`Error in watcher ${watcher.debugName} at ${time}`), result.error);
1432
+ }
1433
+
1434
+ if (!options.runImmediately) {
1435
+ updateWatchers();
1436
+ }
1437
+
1438
+ if (!readyToCommit) {
1439
+ if (SHOULD_TRACE) {
1440
+ console.log(yellow(`Skipping trigger commit due to unsynced accesses. ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
1441
+ if (watcher.lastSpecialPromiseUnsynced) {
1442
+ console.log(yellow(` Waiting for promise`));
1443
+ }
1444
+ logUnsynced();
1445
+ }
1446
+ return;
1447
+ }
1448
+
1449
+ if (SHOULD_TRACE) {
1450
+ console.log(green(`Committing ${values?.length || 0} watcher writes ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
1451
+ }
1452
+
1453
+ // ONLY clear triggeredByChanges when we have a succesful run, otherwise delta based
1454
+ // watchers won't work!
1455
+ watcher.triggeredByChanges = undefined;
1456
+ options.onResultUpdated?.(result, values, watcher);
1457
+ };
1458
+ trigger = measureWrap(trigger, `(watcher) ${watcher.debugName}`);
1459
+ watcher.explicitlyTrigger = trigger;
1460
+
1461
+ if (options.static) {
1462
+ // Do nothing, this will be explicitly triggered when needed
1463
+ } else if (options.runImmediately) {
1464
+ trigger(undefined);
1465
+ dispose();
1466
+ } else {
1467
+ // Set our watchers
1468
+ updateWatchers(true);
1469
+ clientWatcher.explicitlyTriggerWatcher(trigger, {
1470
+ synchronous: options.synchronousInit,
1471
+ });
1472
+ }
1473
+ return watcher;
1474
+ }
1475
+
1476
+ /** IMPORTANT! Values written will only show up in direct parent calls.
1477
+ * This means if you do `data.list[Date.now()].value = value`,
1478
+ * getKeys(data.list) will NOT have your new key.
1479
+ * Instead, do writes like this: `data.list[Date.now()] = { value }`,
1480
+ * or, if the objects are atomic, like this: `data.list[Date.now()] = atomicObjectWrite({ value })`,
1481
+ * which is move efficient (but the .value cannot be mutated in the future, only clobbered).
1482
+ * NOTE: For reading values (readOnly), just use `commitFunction`.
1483
+ */
1484
+ public writeOnly<Result = void>(
1485
+ options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">
1486
+ ): Result {
1487
+ let result: { result: Result } | { error: string } | undefined;
1488
+ this.createWatcher({
1489
+ forceEqualWrites: true,
1490
+ ...options,
1491
+ writeOnly: true,
1492
+ runImmediately: true,
1493
+ allowUnsyncedReads: true,
1494
+ canWrite: true,
1495
+ onResultUpdated: r => {
1496
+ result = r;
1497
+ }
1498
+ });
1499
+ if (!result) {
1500
+ throw new Error("Expected result to be set");
1501
+ }
1502
+ if ("error" in result) {
1503
+ throw errorify(result.error);
1504
+ }
1505
+ return result.result as Result;
1506
+ }
1507
+
1508
+ public runOnce<Result = void>(
1509
+ options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">
1510
+ ): Result {
1511
+ let result: { result: Result } | { error: string } | undefined;
1512
+ let watcher = this.createWatcher({
1513
+ ...options,
1514
+ runImmediately: true,
1515
+ allowUnsyncedReads: true,
1516
+ canWrite: true,
1517
+ onResultUpdated: r => {
1518
+ result = r;
1519
+ }
1520
+ });
1521
+ if (!result) {
1522
+ throw new Error("Expected result to be set");
1523
+ }
1524
+ if ("error" in result) {
1525
+ throw errorify(result.error);
1526
+ }
1527
+ if (watcher.lastUnsyncedAccesses.size > 0 || watcher.lastUnsyncedParentAccesses.size > 0) {
1528
+ //console.warn(`One time watcher run had unsynced accesses after running once. The result will be incomplete. Run with syncing (Querysub.commit) to actually synchronize new values, ${watcher.debugName}`, watcher.options.watchFunction, watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses);
1529
+ }
1530
+ return result.result as Result;
1531
+ }
1532
+
1533
+ /** Runs the function until all reads are synced, then disposes the watcher and returns
1534
+ * - Can be used just with readers, but is most useful for writers (as the
1535
+ * absolutely soonest we can safely write is when all reads are synced)
1536
+ * - "writeOnly" is a dangerous version of this, which doesn't wait, and instead
1537
+ * just writes immediately (but in exchange, it cannot read).
1538
+ * TODO: Find a way to reuse watchers with different functions. For simple functions this can be
1539
+ * about 4X faster. It is more difficult if the function has to wait to sync values,
1540
+ * but we can probably still reuse watchers in some way.
1541
+ * - The "static" option might help with this.
1542
+ */
1543
+ public async commitFunction<Result = void>(
1544
+ options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">,
1545
+ config?: {
1546
+ onWritesCommitted?: (writes: PathValue[]) => void;
1547
+ }
1548
+ ): Promise<Result> {
1549
+ let onResult!: (result: Result) => void;
1550
+ let onError!: (error: Error) => void;
1551
+ let resultPromise = new Promise<Result>((resolve, reject) => { onResult = resolve; onError = reject; });
1552
+ let anyWrites = false;
1553
+ this.createWatcher({
1554
+ canWrite: true,
1555
+ ...options,
1556
+ onResultUpdated: (result, writes, watcher) => {
1557
+ if (writes?.length) {
1558
+ anyWrites = true;
1559
+ }
1560
+ watcher.dispose();
1561
+ if ("error" in result) {
1562
+ onError(errorify(result.error));
1563
+ } else {
1564
+ onResult(result.result);
1565
+ }
1566
+ if (writes && config?.onWritesCommitted) {
1567
+ config.onWritesCommitted(writes);
1568
+ }
1569
+ }
1570
+ });
1571
+ let result = await resultPromise;
1572
+ // NOTE: Now we ALWAYS wait, to prevent services from terminating their process before their writes finish.
1573
+ // It is just too hard to remember to call pathValueCommitter.waitForValuesToCommit in every service.
1574
+ if (options.canWrite && anyWrites && !options.dryRun) {
1575
+ await pathValueCommitter.waitForValuesToCommit();
1576
+ }
1577
+ return result;
1578
+ }
1579
+ /** Run the same as usual, but instead of committing writes, returns them. */
1580
+ public async dryRun(
1581
+ options: Omit<WatcherOptions<any>, "onResultUpdated" | "onWriteCommitted">
1582
+ ): Promise<PathValue[] | undefined> {
1583
+ let result = await this.dryRunFull(options);
1584
+ return result.writes;
1585
+ }
1586
+ /** Run the same as usual, but instead of committing writes, returns them. */
1587
+ public async dryRunFull(
1588
+ options: Omit<WatcherOptions<any>, "onResultUpdated" | "onWriteCommitted">
1589
+ ): Promise<{
1590
+ writes: PathValue[];
1591
+ readPaths: Set<string>
1592
+ readParentPaths: Set<string>;
1593
+ result: unknown;
1594
+ }> {
1595
+ type Result = {
1596
+ writes: PathValue[];
1597
+ readPaths: Set<string>
1598
+ readParentPaths: Set<string>;
1599
+ result: unknown;
1600
+ };
1601
+ let onResult!: (result: Result) => void;
1602
+ let onError!: (error: Error) => void;
1603
+ let resultPromise = new Promise<Result>((resolve, reject) => { onResult = resolve; onError = reject; });
1604
+ this.createWatcher({
1605
+ ...options,
1606
+ canWrite: true,
1607
+ dryRun: true,
1608
+ onResultUpdated: (result, values, watcher) => {
1609
+ watcher.dispose();
1610
+ if ("error" in result) {
1611
+ onError(errorify(result.error));
1612
+ } else {
1613
+ onResult({
1614
+ writes: values ?? [],
1615
+ readPaths: new Set(watcher.pendingWatches.paths),
1616
+ readParentPaths: new Set(watcher.pendingWatches.parentPaths),
1617
+ result: result.result
1618
+ });
1619
+ }
1620
+ }
1621
+ });
1622
+ return await resultPromise;
1623
+ }
1624
+
1625
+
1626
+ private allWatchers = registerResource("paths|proxyWatcher.allWatchers", new Set<SyncWatcher>());
1627
+ public getAllWatchers(): Set<SyncWatcher> {
1628
+ return this.allWatchers;
1629
+ }
1630
+ public inWatcher(): boolean {
1631
+ return !!this.runningWatcher;
1632
+ }
1633
+ /** @deprecated try not to call getTriggeredWatcher, and instead try to call Querysub helper
1634
+ * functions. getTriggeredWatcher exposes too much of our interface, which we need
1635
+ * to abstact out when we rewrite proxyWatcher.
1636
+ */
1637
+ public getTriggeredWatcher(): SyncWatcher {
1638
+ let watcher = this.runningWatcher;
1639
+ if (!watcher) {
1640
+ throw new Error("Not in a watcher");
1641
+ }
1642
+ return watcher;
1643
+ }
1644
+ public isAllSynced() {
1645
+ return !this.getTriggeredWatcher().hasAnyUnsyncedAccesses();
1646
+ }
1647
+ /** @deprecated try not to call getTriggeredWatcherMaybeUndefined, and instead try to call Querysub helper
1648
+ * functions. getTriggeredWatcherMaybeUndefined exposes too much of our interface, which we need
1649
+ * to abstact out when we rewrite proxyWatcher.
1650
+ */
1651
+ public getTriggeredWatcherMaybeUndefined(): SyncWatcher | undefined {
1652
+ return this.runningWatcher;
1653
+ }
1654
+
1655
+ /** NOTE: This explicitly causes the watcher to be unsynced until function
1656
+ * can be evaluated without any pending promises. This makes this significantly
1657
+ * more powerful than waiting on localState.
1658
+ * NOTE: IF you don't want to cause the caller to be unsynced, use a local schema instead.
1659
+ * This allows callers to see what path triggered them, which is often more informative
1660
+ * than waitReason. It also exposes the state to any synced diagnostics tools.
1661
+ */
1662
+ public triggerOnPromiseFinish(
1663
+ promise: MaybePromise<unknown>,
1664
+ config: {
1665
+ // For example, "waitForModuleToLoad"
1666
+ waitReason: string;
1667
+ // Doesn't cause us to be unsynced, instead just triggering the watcher
1668
+ // when the promise resolves (or rejects).
1669
+ noWait?: boolean;
1670
+ }
1671
+ ) {
1672
+ // This needs to mark the watcher as unsynced
1673
+ if (typeof promise === "object" && promise && promise instanceof Promise) {
1674
+ let watcher = this.getTriggeredWatcher();
1675
+ if (!config.noWait) {
1676
+ watcher.specialPromiseUnsynced = true;
1677
+ }
1678
+ watcher.lastSpecialPromiseUnsyncedReason = config.waitReason;
1679
+ void promise.finally(() => {
1680
+ watcher.explicitlyTrigger({
1681
+ paths: new Set(),
1682
+ newParentsSynced: new Set(),
1683
+ pathSources: new Set(),
1684
+ extraReasons: [getPathStr2(`Promise`, config.waitReason || "")]
1685
+ });
1686
+ });
1687
+ }
1688
+ }
1689
+
1690
+ public setEventPath(getValue: () => unknown) {
1691
+ let path = getProxyPath(getValue);
1692
+ let watcher = this.getTriggeredWatcher();
1693
+ watcher.pendingEventWrites.add(path);
1694
+ }
1695
+ public unwatchEventPaths(watcher: SyncWatcher, paths: Set<string>) {
1696
+ for (let path of paths) {
1697
+ watcher.lastWatches.paths.delete(path);
1698
+ watcher.lastWatches.parentPaths.delete(path);
1699
+ watcher.pendingWatches.paths.delete(path);
1700
+ watcher.pendingWatches.parentPaths.delete(path);
1701
+ watcher.pendingUnsyncedAccesses.delete(path);
1702
+ watcher.pendingUnsyncedParentAccesses.delete(path);
1703
+ }
1704
+ }
1705
+ }
1706
+
1707
+ // domainName => moduleId => schema
1708
+ let schemas = new Map<string, Map<string, Schema2>>();
1709
+
1710
+ // NOTE: We hardcode knowledge of how module data is nested to handle schemas. This... isn't great,
1711
+ // but it makes the code faster, and if we change how module data is nested it will break
1712
+ // all existing data anyways, so... I don't think we ever will.
1713
+ // Path pattern is: [domain, "PathFunctionRunner", moduleId, "Data"]
1714
+ function getMatchingSchema(pathStr: string): {
1715
+ schema: Schema2;
1716
+ nestedPath: string[];
1717
+ } | undefined {
1718
+ if (_noAtomicSchema) return undefined;
1719
+ let base = getBaseMatchingSchema(pathStr);
1720
+ if (base) return base;
1721
+ let prefix = getPrefixMatchingSchema(pathStr);
1722
+ if (prefix) return prefix;
1723
+ return undefined;
1724
+
1725
+ }
1726
+ function getBaseMatchingSchema(pathStr: string): {
1727
+ schema: Schema2;
1728
+ nestedPath: string[];
1729
+ } | undefined {
1730
+ //todonext
1731
+ // So... the current call, need to register the hash used for the call. This needs to be
1732
+ // used to prefer that schema from that same hash.
1733
+ // - And if we ever do cross repo calls... we need to store multiple hashes in the call
1734
+ // (or cascade hashes, etc, but we need to know all the hashes, and then use them here!)
1735
+ //todonext;
1736
+ // Ah, it's not quite so easy. So... pathFunctionLoader knows the hash on load (or that it's local),
1737
+ // but this information is not maintained when we are making the call (writeFunctionCall). BUT,
1738
+ // when we are in the call (unless it's a test call), we just called getModuleFromConfig
1739
+ // so we aren't too far away from knowing the hash, and passing it on.
1740
+ // - I think... we should return the hash, and then request it in overrideCurrentCall?
1741
+ //todonext;
1742
+ // Permissions checks are annoying, but... they DO call getModuleFromConfig at some time.
1743
+ // ONLY managementPages.tsx does not actually have a module.
1744
+ //todonext;
1745
+ // Oh, it looks like managementPages.tsx can't operate the way it does. Hmm...
1746
+
1747
+ if (_noAtomicSchema) return undefined;
1748
+ if (getPathIndex(pathStr, 3) !== "Data") return undefined;
1749
+ let domain = getPathIndex(pathStr, 0)!;
1750
+ let moduleId = getPathIndex(pathStr, 2)!;
1751
+ let schema = schemas.get(domain)?.get(moduleId);
1752
+ if (!schema) return undefined;
1753
+ let nestedPathStr = getPathSuffix(pathStr, 4);
1754
+ let nestedPath = getPathFromStr(nestedPathStr);
1755
+ return { schema, nestedPath };
1756
+ }
1757
+
1758
+ export function registerSchema(config: {
1759
+ schema: Schema2;
1760
+ domainName: string;
1761
+ moduleId: string;
1762
+ }) {
1763
+ //todonext
1764
+ // Require passing the module information here
1765
+
1766
+ let domainSchemas = schemas.get(config.domainName);
1767
+ if (!domainSchemas) {
1768
+ schemas.set(config.domainName, domainSchemas = new Map());
1769
+ }
1770
+ domainSchemas.set(config.moduleId, config.schema);
1771
+ }
1772
+
1773
+ export let schemaPrefixes: { len: number; prefixes: Map<string, Schema2> }[] = [];
1774
+ export function registerSchemaPrefix(config: {
1775
+ schema: Schema2;
1776
+ prefixPathStr: string;
1777
+ }) {
1778
+ let len = getPathDepth(config.prefixPathStr);
1779
+ let list = schemaPrefixes.find(x => x.len === len);
1780
+ if (!list) {
1781
+ schemaPrefixes.push(list = { len, prefixes: new Map() });
1782
+ }
1783
+ list.prefixes.set(config.prefixPathStr, config.schema);
1784
+ }
1785
+ export function unregisterSchemaPrefix(config: {
1786
+ schema: Schema2;
1787
+ prefixPathStr: string;
1788
+ }) {
1789
+ let len = getPathDepth(config.prefixPathStr);
1790
+ let list = schemaPrefixes.find(x => x.len === len);
1791
+ if (!list) return;
1792
+ list.prefixes.delete(config.prefixPathStr);
1793
+ }
1794
+ function getPrefixMatchingSchema(pathStr: string): {
1795
+ schema: Schema2;
1796
+ nestedPath: string[];
1797
+ } | undefined {
1798
+ let len = getPathDepth(pathStr);
1799
+ for (let list of schemaPrefixes) {
1800
+ if (len < list.len) continue;
1801
+ let prefix = getPathPrefix(pathStr, list.len);
1802
+ let schema = list.prefixes.get(prefix);
1803
+ if (schema) {
1804
+ let nestedPath = getPathFromStr(getPathSuffix(pathStr, list.len));
1805
+ return { schema, nestedPath };
1806
+ }
1807
+ }
1808
+ return undefined;
1809
+ }
1810
+
1811
+
1812
+ let _noAtomicSchema = false;
1813
+ export function noAtomicSchema<T>(code: () => T) {
1814
+ let prev = _noAtomicSchema;
1815
+ _noAtomicSchema = true;
1816
+ try {
1817
+ return code();
1818
+ } finally {
1819
+ _noAtomicSchema = prev;
1820
+ }
1821
+ }
1822
+
1823
+
1824
+ export const proxyWatcher = new PathValueProxyWatcher();
1825
+ (global as any).proxyWatcher = proxyWatcher;
1826
+ // Probably the most useful debug function for debugging watch based functions.
1827
+ (global as any).getCurrentTriggers = function () {
1828
+ return proxyWatcher.getTriggeredWatcher().triggeredByChanges;
1829
+ };
1830
+ (global as any).getAllWatchers = function () {
1831
+ return proxyWatcher.getAllWatchers();
1832
+ };
1833
+
1834
+ registerPeriodic(function checkForZombieWatches() {
1835
+ let now = Date.now();
1836
+ const threshold = 30 * 1000;
1837
+ function isZombieWatch(watcher: SyncWatcher) {
1838
+ if (watcher.options.static) return false;
1839
+ if (!watcher.hasAnyUnsyncedAccesses()) return false;
1840
+ let timeSinceLastSync = now - watcher.lastSyncTime;
1841
+ if (timeSinceLastSync < threshold) return false;
1842
+ return true;
1843
+ }
1844
+
1845
+ let zombies = Array.from(proxyWatcher.getAllWatchers()).filter(isZombieWatch);
1846
+ if (zombies.length > 0) {
1847
+ console.warn(red(`Found ${zombies.length} zombie watchers`));
1848
+ let namesSet = new Set(zombies.map(x => x.debugName));
1849
+ let topNames = Array.from(namesSet).slice(0, 5);
1850
+ for (let name of topNames) {
1851
+ console.warn(red(` ${name}`));
1852
+ }
1853
+ if (namesSet.size > topNames.length) {
1854
+ console.warn(red(` ... and ${namesSet.size - topNames.length} more`));
1855
+ }
1856
+ }
1857
+ });