querysub 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,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
|
+
});
|