querysub 0.437.0 → 0.438.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/.eslintrc.js +50 -50
- package/bin/deploy.js +0 -0
- package/bin/function.js +0 -0
- package/bin/server.js +0 -0
- package/costsBenefits.txt +115 -115
- package/deploy.ts +2 -2
- package/package.json +1 -1
- package/spec.txt +1192 -1192
- package/src/-a-archives/archives.ts +202 -202
- package/src/-a-archives/archivesDisk.ts +454 -454
- package/src/-a-auth/certs.ts +540 -540
- package/src/-a-auth/node-forge-ed25519.d.ts +16 -16
- package/src/-b-authorities/dnsAuthority.ts +138 -138
- package/src/-c-identity/IdentityController.ts +258 -258
- package/src/-d-trust/NetworkTrust2.ts +180 -180
- package/src/-e-certs/EdgeCertController.ts +252 -252
- package/src/-e-certs/certAuthority.ts +201 -201
- package/src/-f-node-discovery/NodeDiscovery.ts +640 -640
- package/src/-g-core-values/NodeCapabilities.ts +200 -200
- package/src/-h-path-value-serialize/stringSerializer.ts +175 -175
- package/src/0-path-value-core/PathValueCommitter.ts +468 -468
- package/src/0-path-value-core/PathValueController.ts +0 -2
- package/src/2-proxy/PathValueProxyWatcher.ts +2542 -2542
- package/src/2-proxy/TransactionDelayer.ts +94 -94
- package/src/2-proxy/pathDatabaseProxyBase.ts +36 -36
- package/src/2-proxy/pathValueProxy.ts +159 -159
- package/src/3-path-functions/PathFunctionRunnerMain.ts +87 -87
- package/src/3-path-functions/pathFunctionLoader.ts +516 -516
- package/src/3-path-functions/tests/rejectTest.ts +76 -76
- package/src/4-deploy/deployCheck.ts +6 -6
- package/src/4-dom/css.tsx +29 -29
- package/src/4-dom/cssTypes.d.ts +211 -211
- package/src/4-dom/qreact.tsx +2799 -2799
- package/src/4-dom/qreactTest.tsx +410 -410
- package/src/4-querysub/permissions.ts +335 -335
- package/src/4-querysub/querysubPrediction.ts +483 -483
- package/src/5-diagnostics/qreactDebug.tsx +377 -346
- package/src/TestController.ts +34 -34
- package/src/bits.ts +104 -104
- package/src/buffers.ts +69 -69
- package/src/diagnostics/ActionsHistory.ts +57 -57
- package/src/diagnostics/listenOnDebugger.ts +71 -71
- package/src/diagnostics/periodic.ts +111 -111
- package/src/diagnostics/trackResources.ts +91 -91
- package/src/diagnostics/watchdog.ts +120 -120
- package/src/errors.ts +133 -133
- package/src/forceProduction.ts +2 -2
- package/src/fs.ts +80 -80
- package/src/functional/diff.ts +857 -857
- package/src/functional/promiseCache.ts +78 -78
- package/src/functional/random.ts +8 -8
- package/src/functional/stats.ts +60 -60
- package/src/heapDumps.ts +665 -665
- package/src/https.ts +1 -1
- package/src/library-components/AspectSizedComponent.tsx +87 -87
- package/src/library-components/ButtonSelector.tsx +64 -64
- package/src/library-components/DropdownCustom.tsx +150 -150
- package/src/library-components/DropdownSelector.tsx +31 -31
- package/src/library-components/InlinePopup.tsx +66 -66
- package/src/misc/color.ts +29 -29
- package/src/misc/hash.ts +83 -83
- package/src/misc/ipPong.js +13 -13
- package/src/misc/networking.ts +1 -1
- package/src/misc/random.ts +44 -44
- package/src/misc.ts +196 -196
- package/src/path.ts +255 -255
- package/src/persistentLocalStore.ts +41 -41
- package/src/promise.ts +14 -14
- package/src/storage/fileSystemPointer.ts +71 -71
- package/src/test/heapProcess.ts +35 -35
- package/src/zip.ts +15 -15
- package/tsconfig.json +26 -26
- package/yarnSpec.txt +56 -56
|
@@ -1,2542 +1,2542 @@
|
|
|
1
|
-
import { measureCode, measureWrap, registerMeasureInfo } from "socket-function/src/profiling/measure";
|
|
2
|
-
import { SocketFunction } from "socket-function/SocketFunction";
|
|
3
|
-
import { binarySearchBasic, binarySearchBasic2, binarySearchIndex, deepCloneJSON, getKeys, insertIntoSortedList, isNode, recursiveFreeze, sort, timeInMinute, timeInSecond } 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 { cache, cacheLimited, lazy } from "socket-function/src/caching";
|
|
7
|
-
import { delay, runInSerial, 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, getPathFromProxy, getProxyPath, getProxyPathAndWatch, isValueProxy, isValueProxy2 } from "./pathValueProxy";
|
|
16
|
-
import { authorityStorage, compareTime, ReadLock, epochTime, getNextTime, MAX_ACCEPTED_CHANGE_AGE, PathValue, Time, getCreatorId, getTimeUnique } from "../0-path-value-core/pathValueCore";
|
|
17
|
-
import { runCodeWithDatabase, rawSchema } from "./pathDatabaseProxyBase";
|
|
18
|
-
import debugbreak from "debugbreak";
|
|
19
|
-
import { pathValueCommitter } from "../0-path-value-core/PathValueController";
|
|
20
|
-
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
21
|
-
import { registerPeriodic } from "../diagnostics/periodic";
|
|
22
|
-
import { remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
23
|
-
import { Schema2, Schema2Fncs } from "./schema2";
|
|
24
|
-
import { devDebugbreak, getDomain, isDynamicallyLoading, isPublic } from "../config";
|
|
25
|
-
|
|
26
|
-
import type { CallSpec } from "../3-path-functions/PathFunctionRunner";
|
|
27
|
-
import type { FunctionMetadata } from "../3-path-functions/syncSchema";
|
|
28
|
-
|
|
29
|
-
import { DEPTH_TO_DATA, MODULE_INDEX, getCurrentCall, getCurrentCallObj } from "../3-path-functions/PathFunctionRunner";
|
|
30
|
-
import { interceptCallsBase, runCall } from "../3-path-functions/PathFunctionHelpers";
|
|
31
|
-
import { deepCloneCborx } from "../misc/cloneHelpers";
|
|
32
|
-
import { formatNumber, formatPercent, formatTime } from "socket-function/src/formatting/format";
|
|
33
|
-
import { addStatPeriodic, interceptCalls, onAllPredictionsFinished, onTimeProfile } from "../-0-hooks/hooks";
|
|
34
|
-
import { onNextPaint } from "../functional/onNextPaint";
|
|
35
|
-
import { isAsyncFunction } from "../misc";
|
|
36
|
-
import { isClient } from "../config2";
|
|
37
|
-
|
|
38
|
-
// TODO: Break this into two parts:
|
|
39
|
-
// 1) Run and get accesses
|
|
40
|
-
// 2) Commit/watch/unwatch
|
|
41
|
-
// With a harness that joins the two parts in a loop (mostly powered by clientWatcher,
|
|
42
|
-
// which can run the trigger loop).
|
|
43
|
-
|
|
44
|
-
const DEFAULT_MAX_LOCKS = 1000;
|
|
45
|
-
|
|
46
|
-
// After this time we allow proxies to be reordered, even if there's flags that tell them not to be.
|
|
47
|
-
const MAX_PROXY_REORDER_BLOCK_TIME = timeInSecond * 10;
|
|
48
|
-
|
|
49
|
-
let nextSeqNum = 1;
|
|
50
|
-
let nextOrderSeqNum = 1;
|
|
51
|
-
|
|
52
|
-
export interface WatcherOptions<Result> {
|
|
53
|
-
/** NOTE: If you try to run too far in the past, an error will occur.
|
|
54
|
-
* - This both sets the read time, and write time.
|
|
55
|
-
* - setCurrentReadTime can also set the read time (but runAtTime is the only
|
|
56
|
-
* way to set the write time).
|
|
57
|
-
*/
|
|
58
|
-
runAtTime?: Time;
|
|
59
|
-
|
|
60
|
-
/** NOTE: Write values are only committed after we have all reads synced. */
|
|
61
|
-
canWrite?: boolean;
|
|
62
|
-
|
|
63
|
-
/** ONLY allowed if the writes and reads are all local (throws if any non-local accesses are attempted).
|
|
64
|
-
* - Stops lock creation. This might increase speed?
|
|
65
|
-
*/
|
|
66
|
-
noLocks?: boolean;
|
|
67
|
-
/** No locks, without the local-only restriction. A lot more unsafe. Don't use for any writes
|
|
68
|
-
* that will eventually be committed (we only use it in prediction code, and GC). */
|
|
69
|
-
unsafeNoLocks?: boolean;
|
|
70
|
-
|
|
71
|
-
/** Causes us to read without counting as reading, so we don't sync any values, or
|
|
72
|
-
* add them to our locks. Usually only set temporarily inside of a watcher,
|
|
73
|
-
* so it can read sync values in order to write non-synced side-effects, without
|
|
74
|
-
* having to rerun if those synced values change (as the non-synced side-effects won't
|
|
75
|
-
* be undone, so rerunning wouldn't matter).
|
|
76
|
-
* - qreact uses this for component traversal to set disposing flags, as disposing flags
|
|
77
|
-
* are non-synced, and just for optimization, so there's no reason to watch the component
|
|
78
|
-
* hierarchy when changing them.
|
|
79
|
-
*/
|
|
80
|
-
noSyncing?: boolean;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/** Doesn't allow reading any values, but... also causes allows the function to have
|
|
84
|
-
* no readLocks! (so it can't be rejected)
|
|
85
|
-
* - Still allows reading back your own writes though...
|
|
86
|
-
* - Doesn't write object parents, which breaks Object.getKeys(), unless you use
|
|
87
|
-
* atomicObjectWrite.
|
|
88
|
-
* - Usually set with runImmediately
|
|
89
|
-
*/
|
|
90
|
-
writeOnly?: boolean;
|
|
91
|
-
|
|
92
|
-
/** Ignore currentReadTime, and just read the latest. For use with doProxyOptions. */
|
|
93
|
-
forceReadLatest?: boolean;
|
|
94
|
-
|
|
95
|
-
/** If you are writing values you KNOW you won't reading back, set this flag to
|
|
96
|
-
* prevent caching of the writes clientside (which is a lot more efficient).
|
|
97
|
-
* - More for testing purposes, as the performance benefit is basically negligible,
|
|
98
|
-
* and the memory impact is unlikely to be an issue unless you are writing huge values.
|
|
99
|
-
*/
|
|
100
|
-
doNotStoreWritesAsPredictions?: boolean;
|
|
101
|
-
|
|
102
|
-
/** See PathValue.event */
|
|
103
|
-
eventWrite?: boolean;
|
|
104
|
-
|
|
105
|
-
/** By default we read values before we write to them, and don't write if the value
|
|
106
|
-
* would be `===`. This is usually preferred, because unneeded writes are a lot
|
|
107
|
-
* more expensive than unneeded reads. However, if you are writing a lot, and you
|
|
108
|
-
* know the values will be different, you can set this to reduce the amount of reads.
|
|
109
|
-
* This is implicitly used for atomic writes (both due to atomicObjectWrite, or atomicWrites: true),
|
|
110
|
-
* otherwise almost all atomic writes would result in an infinite loop, as usually
|
|
111
|
-
* objects aren't === (and we don't deep compare objects on writes).
|
|
112
|
-
*/
|
|
113
|
-
forceEqualWrites?: boolean;
|
|
114
|
-
|
|
115
|
-
atomicWrites?: boolean;
|
|
116
|
-
|
|
117
|
-
debugName?: string;
|
|
118
|
-
source?: string;
|
|
119
|
-
|
|
120
|
-
watchFunction: () => Result;
|
|
121
|
-
baseFunction?: Function;
|
|
122
|
-
|
|
123
|
-
// Only called after all the reads are synced
|
|
124
|
-
// - getTriggeredWatcher will function correctly in this callback
|
|
125
|
-
onResultUpdated?: (
|
|
126
|
-
result: { result: Result } | { error: string },
|
|
127
|
-
writes: PathValue[] | undefined,
|
|
128
|
-
watcher: SyncWatcher
|
|
129
|
-
) => void;
|
|
130
|
-
|
|
131
|
-
onCallCommit?: (call: CallSpec, metadata: FunctionMetadata) => void;
|
|
132
|
-
|
|
133
|
-
/** Causes us to not actually write values (we still sync new values, etc),
|
|
134
|
-
* we just don't actually commit any writes. */
|
|
135
|
-
dryRun?: boolean;
|
|
136
|
-
|
|
137
|
-
/** Runs immediately, returning immediately, not waiting for any values to synchronize.
|
|
138
|
-
* - Also disposes it immediately, as it isn't watching any values
|
|
139
|
-
* - Does not even START to synchronize any values
|
|
140
|
-
*/
|
|
141
|
-
runImmediately?: boolean;
|
|
142
|
-
|
|
143
|
-
/** By default we call pathValueCommitter.waitForValuesToCommit. This skips that check. */
|
|
144
|
-
noWaitForCommit?: boolean;
|
|
145
|
-
|
|
146
|
-
/** HACK: Used in conjuction with specific callers to prevent disposing, so the watcher
|
|
147
|
-
* can be reused for specific cases.
|
|
148
|
-
*/
|
|
149
|
-
static?: boolean;
|
|
150
|
-
|
|
151
|
-
/** Runs the inner function and calls the callback, even if there are unsynced values. */
|
|
152
|
-
allowUnsyncedReads?: boolean;
|
|
153
|
-
|
|
154
|
-
// NOTE: We create a new checker every single run
|
|
155
|
-
getPermissionsCheck?: () => PermissionsChecker;
|
|
156
|
-
|
|
157
|
-
skipPermissionsCheck?: boolean;
|
|
158
|
-
|
|
159
|
-
/** Default is after, which runs nested calls after the function runs (and only if all the
|
|
160
|
-
* data is synchronized)
|
|
161
|
-
* "inline" results in calls being run immediately, instead of queued.
|
|
162
|
-
* TODO: We can only inline calls on the same domain, as we don't have
|
|
163
|
-
* permissions for calls on other domains. In this case we might want to throw
|
|
164
|
-
* ("after" is complicated due to FunctionRunner).
|
|
165
|
-
* */
|
|
166
|
-
nestedCalls?: "throw" | "after" | "ignore" | "inline";
|
|
167
|
-
|
|
168
|
-
/** These domains are allowed to be referenced by locks. Can be overriden, so you can invalidate based on on other domains
|
|
169
|
-
* (ex, predict the server from the client).
|
|
170
|
-
* - Defaults to the current domain, which is the safest option (as invalidating based on other domains can cause
|
|
171
|
-
* security issues, reliability issues, etc).
|
|
172
|
-
*/
|
|
173
|
-
overrideAllowLockDomainsPrefixes?: string[];
|
|
174
|
-
|
|
175
|
-
/** See WatchSpec.orderGroup */
|
|
176
|
-
order?: number;
|
|
177
|
-
orderGroup?: number;
|
|
178
|
-
|
|
179
|
-
synchronousInit?: boolean;
|
|
180
|
-
|
|
181
|
-
overrides?: PathValue[];
|
|
182
|
-
|
|
183
|
-
// Triggers are also tracked if ProxyWatcher.TRACK_TRIGGERS (or ClientWatcher.DEBUG_TRIGGERS)
|
|
184
|
-
// Sometimes we need trigger tracking for delta watchers, so this isn't just for debugging
|
|
185
|
-
trackTriggers?: boolean;
|
|
186
|
-
|
|
187
|
-
// Fairly self explanatory. If there are nested createWatchers, instead of forking, we just
|
|
188
|
-
// run them immediately.
|
|
189
|
-
inlineNestedWatchers?: boolean;
|
|
190
|
-
|
|
191
|
-
// Temporary indicates after becoming synchronizes it will immediately dispose itself
|
|
192
|
-
temporary?: boolean;
|
|
193
|
-
|
|
194
|
-
// MUCH faster for large data (which otherwise requires a deep clone), but if it does return a proxy, and you access it in non-commit, it will throw.
|
|
195
|
-
allowProxyResults?: boolean;
|
|
196
|
-
|
|
197
|
-
/** Prevents us from (ever) tracking this as a wasted evaluation.
|
|
198
|
-
* (indicates that this function changes non-synced states)
|
|
199
|
-
* - Ex, qreact's mounter sets this, as it changes the DOM.
|
|
200
|
-
* - If you are watching for the purpose of logging, or writing to files, you would set this.
|
|
201
|
-
* - A lot of serverside watchers will set this (such as PathFunctionRunner)
|
|
202
|
-
*/
|
|
203
|
-
hasSideEffects?: boolean;
|
|
204
|
-
|
|
205
|
-
/** Takes any runs with the same runOncePerPaint (globally, across all watchers),
|
|
206
|
-
* and only runs them once per paint per group. Any excessive runs will be culled,
|
|
207
|
-
* with the latest call being rerun after the next paint.
|
|
208
|
-
* - If "runImmediately" or "temporary" is set, the latest call won't be rerun after the
|
|
209
|
-
* next paint (it will be dropped).
|
|
210
|
-
* - This should often be an id unique to the specific watcher. Otherwise
|
|
211
|
-
* you may have watchers left in a outdated state. However, this might be
|
|
212
|
-
* what you want, it depends on the usecase.
|
|
213
|
-
*/
|
|
214
|
-
runOncePerPaint?: string;
|
|
215
|
-
|
|
216
|
-
/** Logs time to go from unsynced to synced, and the paths that were unloaded.
|
|
217
|
-
* - In order to tell what paths to pre-load, and how much time we are waiting to load data.
|
|
218
|
-
* - Also to tell us how long and how many paths are loading on startup.
|
|
219
|
-
*/
|
|
220
|
-
logSyncTimings?: boolean;
|
|
221
|
-
|
|
222
|
-
maxLocksOverride?: number;
|
|
223
|
-
|
|
224
|
-
/** Finish in the order we start in. This is required to prevent races.
|
|
225
|
-
* - Defaults to true client side if we're using Querysub.commitAsync
|
|
226
|
-
* - If a number overrides the order, this is used when we're making predictions, so they trigger based on the order of their parent, instead of the order of when they started.
|
|
227
|
-
* - Only applies the order based on other functions that have this flag set.
|
|
228
|
-
*/
|
|
229
|
-
finishInStartOrder?: number | boolean;
|
|
230
|
-
|
|
231
|
-
/** Only to be used for logging. Is VERY useful in certain circumstances */
|
|
232
|
-
predictMetadata?: FunctionMetadata;
|
|
233
|
-
|
|
234
|
-
/** AKA, isAllSynced is always true. Useful for cases when we want to show partially loaded values. */
|
|
235
|
-
commitAllRuns?: boolean;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
/** Path values contain some information about all the paths that were written at once, which we call a transaction. We can use this to detect when we are missing part of a transaction and wait until we receive that part of the transaction.
|
|
239
|
-
- If we don't wait, our writes will get rejected, and so the system will still be valid, however, there are a few reasons we want to avoid this:
|
|
240
|
-
- If we are trying to trigger external data, such as API calls, we want to reduce raise conditions. The values still might be rejected entirely, however, at least there won't be a partial read, whereas partial reads are significantly more common than rejections.
|
|
241
|
-
- Rejections can cause view stuttering, where the view goes to a bad state before it goes to a good state. If we just wait for the transaction to complete before trying to commit a value, such as in function runner, we can remove this stutter.
|
|
242
|
-
- Waiting can be more efficient than committing it and having to reject it, especially if something else depends on our value and then it has to be rejected and rerun.
|
|
243
|
-
- Not all writers will automatically rerun on rejections. They should be able to handle rejections. However, it might cause downtime and it might have to put the system into a partial recovery state. It's better to avoid this and just wait for the full transaction.
|
|
244
|
-
- Because this is so useful and so cheap to calculate, this flag will be set by default by most commit-style templates. It should be safe to set in watch style templates, However, it's not on by default there as, technically speaking, waiting for incomplete transactions will cause higher latency to render, And by default we prefer to render fast and usually get it right rather than render slow and always render the right thing.
|
|
245
|
-
*/
|
|
246
|
-
waitForIncompleteTransactions?: boolean;
|
|
247
|
-
|
|
248
|
-
// NOTE: The reason there isn't throttle support here is very frequently when you want to throttle one component rendering, it's because you have many components. So you actually want to throttle many components and have them throttle in conjunction with each other, which results in the logic becoming complicated.
|
|
249
|
-
// - But maybe we should support the single throttle case anyways?
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export type DryRunResult = {
|
|
253
|
-
writes: PathValue[];
|
|
254
|
-
readPaths: Set<string>
|
|
255
|
-
readParentPaths: Set<string>;
|
|
256
|
-
result: unknown;
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
let harvestableReadyLoopCount = 0;
|
|
260
|
-
let harvestableWaitingLoopCount = 0;
|
|
261
|
-
let lastZombieCount = 0;
|
|
262
|
-
// NOTE: Only the % waiting for active watchers. Which... is fine
|
|
263
|
-
addStatPeriodic({
|
|
264
|
-
title: "Performance|Watcher % Waiting",
|
|
265
|
-
getValue: () => {
|
|
266
|
-
let totalLoop = harvestableReadyLoopCount + harvestableWaitingLoopCount;
|
|
267
|
-
if (totalLoop === 0) return 0;
|
|
268
|
-
let frac = harvestableWaitingLoopCount / totalLoop;
|
|
269
|
-
harvestableWaitingLoopCount = 0;
|
|
270
|
-
harvestableReadyLoopCount = 0;
|
|
271
|
-
return frac;
|
|
272
|
-
},
|
|
273
|
-
format: formatPercent,
|
|
274
|
-
});
|
|
275
|
-
addStatPeriodic({
|
|
276
|
-
title: "Performance|Stalled Watchers",
|
|
277
|
-
getValue: () => lastZombieCount,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// Used to prevent local rejections on remote values (as local functions aren't rerun)
|
|
282
|
-
// Also used for security on servers, so they can read from untrusted domains, but can't have values
|
|
283
|
-
// rejected by untrusted domains.
|
|
284
|
-
const getAllowedLockDomainsPrefixes = function getTrustedDomains(): string[] {
|
|
285
|
-
if (isNode()) {
|
|
286
|
-
return [getPathStr1(getDomain())];
|
|
287
|
-
} else {
|
|
288
|
-
// TODO: We COULD allow non-local watches on clients? This would allow something such as...
|
|
289
|
-
// clicking on a button to go the first page that no other user is on... to revert if
|
|
290
|
-
// another user clicks at the same time and we race. Or... something? Maybe there
|
|
291
|
-
// are no cases when we need it... and it would definitely slow things down, so...
|
|
292
|
-
// maybe we just don't?
|
|
293
|
-
return [LOCAL_DOMAIN_PATH];
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
export type SyncWatcher = {
|
|
298
|
-
seqNum: number;
|
|
299
|
-
options: WatcherOptions<any>;
|
|
300
|
-
|
|
301
|
-
dispose: () => void;
|
|
302
|
-
disposed?: boolean;
|
|
303
|
-
// Reset every time the function is run, so this can be subscribed to every run, but will
|
|
304
|
-
// only be called once the final commit.
|
|
305
|
-
onInnerDisposed: (() => void)[];
|
|
306
|
-
// Runs after any trigger happens (usually multiple triggers are required for a commit)
|
|
307
|
-
onAfterTriggered: (() => void)[];
|
|
308
|
-
|
|
309
|
-
// Not great, but... sometimes we just want to trigger the function
|
|
310
|
-
explicitlyTrigger: (changes?: WatchSpecData) => void;
|
|
311
|
-
|
|
312
|
-
hasAnyUnsyncedAccesses: () => boolean;
|
|
313
|
-
|
|
314
|
-
// - epochTime results in reading the latest time
|
|
315
|
-
// - time is exclusive, so if you set the time of an existing write, you won't
|
|
316
|
-
// read back it's writes.
|
|
317
|
-
setCurrentReadTime: <T>(time: Time, code: () => T) => T;
|
|
318
|
-
// NOTE: Use the setCurrentReadTime helper, to prevent accidentally
|
|
319
|
-
// leaving the read time set (or use runAtTime in the options,
|
|
320
|
-
// to set it forever).
|
|
321
|
-
currentReadTime: Time | undefined;
|
|
322
|
-
nextWriteTime: Time | undefined;
|
|
323
|
-
|
|
324
|
-
debugName: string;
|
|
325
|
-
|
|
326
|
-
// The changes which triggered the current run (or last run if we are not currently running).
|
|
327
|
-
triggeredByChanges: undefined | WatchSpecData;
|
|
328
|
-
|
|
329
|
-
pendingWrites: Map<string, unknown>;
|
|
330
|
-
pendingEventWrites: Set<string>;
|
|
331
|
-
pendingCalls: { call: CallSpec; metadata: FunctionMetadata }[];
|
|
332
|
-
|
|
333
|
-
pendingWatches: {
|
|
334
|
-
paths: Set<string>;
|
|
335
|
-
parentPaths: Set<string>;
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
// These aren't set until after a run finishes
|
|
339
|
-
lastWatches: {
|
|
340
|
-
paths: Set<string>;
|
|
341
|
-
parentPaths: Set<string>;
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
/** The key is the time we read at. This is required to create the lock.
|
|
346
|
-
* - Only utilized if we commit the write
|
|
347
|
-
*/
|
|
348
|
-
pendingAccesses: Map<Time | undefined, Map<string, {
|
|
349
|
-
pathValue: PathValue;
|
|
350
|
-
noLocks: boolean;
|
|
351
|
-
}>>;
|
|
352
|
-
pendingEpochAccesses: Map<Time | undefined, Set<string>>;
|
|
353
|
-
|
|
354
|
-
// A necessity for react rendering, as rendering unsynced data almost always looks bad,
|
|
355
|
-
// so we need to show the previously rendered data when we have unsynced data.
|
|
356
|
-
// (Although, there is a certain pleasantness to showing SOME change extremely fast,
|
|
357
|
-
// even if it's a somewhat broken state, so... we might at a mode to allow rendering
|
|
358
|
-
// with unsynced data).
|
|
359
|
-
// TODO: We might make this a function, and lazily calculate it. That way we can batch reads
|
|
360
|
-
// without having to check isSynced?
|
|
361
|
-
// NOTE: The "pendingUnsynced" must ALWAYS also set pendingWatches
|
|
362
|
-
pendingUnsyncedAccesses: Set<string>;
|
|
363
|
-
lastUnsyncedAccesses: Set<string>;
|
|
364
|
-
|
|
365
|
-
pendingUnsyncedParentAccesses: Set<string>;
|
|
366
|
-
lastUnsyncedParentAccesses: Set<string>;
|
|
367
|
-
|
|
368
|
-
specialPromiseUnsynced: boolean;
|
|
369
|
-
lastSpecialPromiseUnsynced: boolean;
|
|
370
|
-
lastSpecialPromiseUnsyncedReason?: string;
|
|
371
|
-
|
|
372
|
-
consecutiveErrors: number;
|
|
373
|
-
countSinceLastFullSync: number;
|
|
374
|
-
lastSyncTime: number;
|
|
375
|
-
syncRunCount: number;
|
|
376
|
-
startTime: number;
|
|
377
|
-
|
|
378
|
-
lastEvalTime: number;
|
|
379
|
-
inThrottle?: boolean;
|
|
380
|
-
|
|
381
|
-
permissionsChecker?: PermissionsChecker;
|
|
382
|
-
|
|
383
|
-
/** Allows us to tag the object for heap analysis */
|
|
384
|
-
tag: SyncWatcherTag;
|
|
385
|
-
|
|
386
|
-
hackHistory: { message: string; time: number }[];
|
|
387
|
-
|
|
388
|
-
createTime: number;
|
|
389
|
-
|
|
390
|
-
logSyncTimings?: {
|
|
391
|
-
startTime: number;
|
|
392
|
-
unsyncedPaths: Set<string>;
|
|
393
|
-
unsyncedStages: {
|
|
394
|
-
paths: string[];
|
|
395
|
-
time: number;
|
|
396
|
-
}[];
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
function addToHistory(watcher: SyncWatcher, message: string) {
|
|
400
|
-
watcher.hackHistory.push({ message, time: Date.now() });
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// NOTE: A lot of our proxy callback functionality is partially duplicated in PathTimeRunner
|
|
405
|
-
export let atomicObjectSymbol = Symbol.for("atomicObject");
|
|
406
|
-
export function atomicObjectWrite<T>(obj: T): T {
|
|
407
|
-
if (canHaveChildren(obj) && Object.isExtensible(obj)) {
|
|
408
|
-
(obj as any)[atomicObjectSymbol] = true;
|
|
409
|
-
}
|
|
410
|
-
return recursiveFreeze(obj);
|
|
411
|
-
}
|
|
412
|
-
export function atomicCloneWrite<T>(obj: T): T {
|
|
413
|
-
return atomicObjectWrite(deepCloneJSON(obj));
|
|
414
|
-
}
|
|
415
|
-
export function atomicObjectWriteNoFreeze<T>(obj: T): T {
|
|
416
|
-
if (canHaveChildren(obj) && Object.isExtensible(obj)) {
|
|
417
|
-
(obj as any)[atomicObjectSymbol] = true;
|
|
418
|
-
}
|
|
419
|
-
return obj;
|
|
420
|
-
}
|
|
421
|
-
const readNoProxy = Symbol.for("readNoProxy");
|
|
422
|
-
// Specifies we are reading this object, and not any children of it
|
|
423
|
-
export function atomicObjectRead<T>(obj: T): T {
|
|
424
|
-
if (!isValueProxy(obj)) return obj;
|
|
425
|
-
return (obj as any)[readNoProxy];
|
|
426
|
-
}
|
|
427
|
-
export const atomic = atomicObjectRead;
|
|
428
|
-
(globalThis as any).atomic = atomic;
|
|
429
|
-
|
|
430
|
-
export function doUnatomicWrites<T>(callback: () => T): T {
|
|
431
|
-
return doProxyOptions({ atomicWrites: false }, callback);
|
|
432
|
-
}
|
|
433
|
-
export function doAtomicWrites<T>(callback: () => T): T {
|
|
434
|
-
return doProxyOptions({ atomicWrites: true }, callback);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const rawRead = Symbol.for("rawRead");
|
|
438
|
-
/** Reads the raw value, which includes transparent values.
|
|
439
|
-
* - Basically just useful for testing if an object exists in a lookup, in which case
|
|
440
|
-
* specialObjectWriteValue will be returned for the object.
|
|
441
|
-
* - NEVER returns a proxy, always a real object (which might be undefined, or an object).
|
|
442
|
-
*/
|
|
443
|
-
export function atomicRaw<T>(obj: T): T {
|
|
444
|
-
if (!isValueProxy(obj)) return obj;
|
|
445
|
-
return (obj as any)[rawRead];
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export function doProxyOptions<T>(options: Partial<WatcherOptions<any>>, callback: () => T): T {
|
|
449
|
-
let watcher = proxyWatcher.getTriggeredWatcher();
|
|
450
|
-
let prev = watcher.options;
|
|
451
|
-
watcher.options = { ...watcher.options, ...options };
|
|
452
|
-
try {
|
|
453
|
-
return callback();
|
|
454
|
-
} finally {
|
|
455
|
-
watcher.options = prev;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// HACK: A special value which acts like undefined, EXCEPT when using Object.keys(). The reason it can't
|
|
460
|
-
// be undefined, is that undefined values are removed by garbage collection, and this has to stick around!
|
|
461
|
-
// - Is automatically set in the parents paths of object writes, so Object.keys() works.
|
|
462
|
-
// (Which makes garbage collection a lot harder, but... not impossible, and Object.keys() is so important
|
|
463
|
-
// that it is okay).
|
|
464
|
-
// - Due to permissions rejecting root writes, BUT, the ease of this value getting nito relatively root paths
|
|
465
|
-
// (although hopefully not THE root), reading this counts as a "readIsUndefined"
|
|
466
|
-
// NOTE: Yes, this does technically mean you depend on the first value to write to an object.
|
|
467
|
-
// And so, if the first write is rejected, the lock becomes rejected, even though
|
|
468
|
-
// there might be other writes. This is fine, because the first value is the oldest, so
|
|
469
|
-
// the least likely to be rejected, and if it is... FunctionRunner will just rerun
|
|
470
|
-
// the dependent call, and find the key exists again, and the only difference will
|
|
471
|
-
// be a bit of lag, and value flicker.
|
|
472
|
-
export const specialObjectWriteValue = "_specialObjectWriteValue_16c4c3bb43f24111976a2681c972f6f4";
|
|
473
|
-
export const specialObjectWriteSymbol = Symbol("specialObjectWriteSymbol");
|
|
474
|
-
// Values that don't add another proxy layer
|
|
475
|
-
export function isTransparentValue(value: unknown) {
|
|
476
|
-
return (
|
|
477
|
-
value === undefined
|
|
478
|
-
|| value === specialObjectWriteValue
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export function undeleteFromLookup<T>(lookup: { [key: string]: T }, key: string): void {
|
|
483
|
-
lookup[key] = {} as any;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const syncedSymbol = Symbol.for("syncedSymbol");
|
|
487
|
-
// HACK: This should probably be somewhere else, but... it is just so useful for PathFunctionRunner...
|
|
488
|
-
/** @deprecated this is not very accurate (it breaks for schema accesses). Only use it for low level places that can't call the more accurate Querysub.isSynced) */
|
|
489
|
-
export function isSynced(obj: unknown): boolean {
|
|
490
|
-
// If it is a primitive, then it must be synced!
|
|
491
|
-
if (!canHaveChildren(obj)) return true;
|
|
492
|
-
if (!isValueProxy(obj)) return true;
|
|
493
|
-
return !!(obj[syncedSymbol]);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
export type PermissionsChecker = {
|
|
497
|
-
checkPermissions(path: string): { permissionsPath: string; allowed: boolean; };
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
class SyncWatcherTag { }
|
|
502
|
-
|
|
503
|
-
function isRecursiveMatch(paths: Set<string>, path: string): boolean {
|
|
504
|
-
if (paths.size === 0) return false;
|
|
505
|
-
if (paths.has(path)) return true;
|
|
506
|
-
for (let otherPath of paths) {
|
|
507
|
-
if (path.includes(otherPath) || otherPath.includes(path)) {
|
|
508
|
-
return true;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
function removeMatches(paths: Set<string>, path: string): void {
|
|
514
|
-
paths.delete(path);
|
|
515
|
-
for (let otherPath of paths) {
|
|
516
|
-
if (path.includes(otherPath) || otherPath.includes(path)) {
|
|
517
|
-
paths.delete(otherPath);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
export class PathValueProxyWatcher {
|
|
523
|
-
public static BREAK_ON_READS = new Set<string>();
|
|
524
|
-
public static BREAK_ON_WRITES = new Set<string>();
|
|
525
|
-
public static SET_FUNCTION_WATCH_ON_WRITES = new Set<string>();
|
|
526
|
-
public static LOG_WRITES_INCLUDES = new Set<string>();
|
|
527
|
-
|
|
528
|
-
// getPathStr(ModuleId, FunctionId)
|
|
529
|
-
public static BREAK_ON_CALL = new Set<string>();
|
|
530
|
-
|
|
531
|
-
public static DEBUG = false;
|
|
532
|
-
/** NOTE: This results in VERY slow performance, and will log A LOT of information. */
|
|
533
|
-
public static TRACE = false;
|
|
534
|
-
public static TRACE_WRITES = false;
|
|
535
|
-
public static TRACE_WATCHERS = new Set<string>();
|
|
536
|
-
|
|
537
|
-
public static TRACK_TRIGGERS = false;
|
|
538
|
-
|
|
539
|
-
private SHOULD_TRACE(watcher: SyncWatcher) {
|
|
540
|
-
return PathValueProxyWatcher.DEBUG || PathValueProxyWatcher.TRACE || PathValueProxyWatcher.TRACE_WRITES && watcher.options.canWrite || ClientWatcher.DEBUG_TRIGGERS === "heavy" || PathValueProxyWatcher.TRACE_WATCHERS.size > 0 && Array.from(PathValueProxyWatcher.TRACE_WATCHERS).some(x => watcher.debugName.includes(x));
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
private isUnsyncCheckers = new Set<{ value: number }>();
|
|
544
|
-
|
|
545
|
-
public countUnsynced = <T>(checker: { value: number }, code: () => T): T => {
|
|
546
|
-
this.isUnsyncCheckers.add(checker);
|
|
547
|
-
try {
|
|
548
|
-
return code();
|
|
549
|
-
} finally {
|
|
550
|
-
this.isUnsyncCheckers.delete(checker);
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
public getCallbackPathValue = (pathStr: string, syncParentKeys?: "parentKeys"): PathValue | undefined => {
|
|
555
|
-
const watcher = this.runningWatcher;
|
|
556
|
-
if (!watcher) {
|
|
557
|
-
debugger;
|
|
558
|
-
throw new Error(`Tried to get path "${pathStr}" outside of a watcher function.`);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (watcher.options.noLocks && !pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
562
|
-
throw new Error(`Tried to read a non-local path in a "noLocks" watcher, ${watcher.debugName}, path ${pathStr}`);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (watcher.permissionsChecker) {
|
|
566
|
-
if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
|
|
567
|
-
if (
|
|
568
|
-
!watcher.hasAnyUnsyncedAccesses()
|
|
569
|
-
&& authorityStorage.DEBUG_hasAnyValues(pathStr)
|
|
570
|
-
// HACK: Don't show warnings for some framework paths, because they are filling up the console logs
|
|
571
|
-
// and don't really matter. We could just not request them, but at depth 3 is valid,
|
|
572
|
-
// so we kind of have to request that. And at depth 2, it would require special case code,
|
|
573
|
-
// in a bunch of annoying places.
|
|
574
|
-
&& (getPathIndex(pathStr, 1) !== "PathFunctionRunner" || getPathDepth(pathStr) > 3)
|
|
575
|
-
) {
|
|
576
|
-
console.warn(`Denied read access to path "${pathStr}"`);
|
|
577
|
-
// console.warn(`${new Date().toLocaleTimeString()} Denied read access to path "${pathStr}"`);
|
|
578
|
-
// console.warn(new Error().stack);
|
|
579
|
-
}
|
|
580
|
-
// This undefined MIGHT cause issues, but... it also makes it easier to check if
|
|
581
|
-
// we have permissions, and more clear if we are denied permissions.
|
|
582
|
-
return undefined;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (watcher.options.writeOnly) {
|
|
587
|
-
return undefined;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const currentReadTime = watcher.options.forceReadLatest ? undefined : watcher.currentReadTime;
|
|
591
|
-
|
|
592
|
-
let pathValue = authorityStorage.getValueAtTime(pathStr, currentReadTime);
|
|
593
|
-
if (!watcher.options.noSyncing) {
|
|
594
|
-
// NOTE: If we have any value, we are always synced (that's what synced means, as if we aren't syncing,
|
|
595
|
-
// we delete any values, to prevent stale values from being used).
|
|
596
|
-
if (!pathValue) {
|
|
597
|
-
// NOTE: We might not have a value simply due to reading too far back in time,
|
|
598
|
-
// so we have to call isSynced just to be sure it is actually unsynced
|
|
599
|
-
if (!authorityStorage.isSynced(pathStr)) {
|
|
600
|
-
for (let checker of this.isUnsyncCheckers) {
|
|
601
|
-
checker.value++;
|
|
602
|
-
}
|
|
603
|
-
watcher.pendingUnsyncedAccesses.add(pathStr);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (syncParentKeys) {
|
|
607
|
-
if (!authorityStorage.isParentSynced(pathStr)) {
|
|
608
|
-
watcher.pendingUnsyncedParentAccesses.add(pathStr);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
watcher.pendingWatches.paths.add(pathStr);
|
|
612
|
-
if (syncParentKeys) {
|
|
613
|
-
watcher.pendingWatches.parentPaths.add(pathStr);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (!pathValue) {
|
|
617
|
-
let pendingNullAccesses = watcher.pendingEpochAccesses.get(currentReadTime);
|
|
618
|
-
if (!pendingNullAccesses) {
|
|
619
|
-
pendingNullAccesses = new Set();
|
|
620
|
-
watcher.pendingEpochAccesses.set(currentReadTime, pendingNullAccesses);
|
|
621
|
-
}
|
|
622
|
-
pendingNullAccesses.add(pathStr);
|
|
623
|
-
return undefined;
|
|
624
|
-
} else {
|
|
625
|
-
let readValues = watcher.pendingAccesses.get(currentReadTime);
|
|
626
|
-
if (!readValues) {
|
|
627
|
-
readValues = new Map();
|
|
628
|
-
watcher.pendingAccesses.set(currentReadTime, readValues);
|
|
629
|
-
}
|
|
630
|
-
let prevObj = readValues.get(pathValue.path);
|
|
631
|
-
let noLocks = watcher.options.noLocks || watcher.options.unsafeNoLocks || false;
|
|
632
|
-
if (!prevObj) {
|
|
633
|
-
readValues.set(pathValue.path, { pathValue, noLocks });
|
|
634
|
-
} else {
|
|
635
|
-
prevObj.pathValue = pathValue;
|
|
636
|
-
// It can only be no locks if it's always no locks (prev && current)
|
|
637
|
-
prevObj.noLocks = prevObj.noLocks && noLocks;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return pathValue;
|
|
643
|
-
};
|
|
644
|
-
public getCallback = (pathStr: string, syncParentKeys?: "parentKeys", readTransparent?: "readTransparent"): { value: unknown } | undefined => {
|
|
645
|
-
if (PathValueProxyWatcher.BREAK_ON_READS.size > 0 && (proxyWatcher.isAllSynced() || this)) {
|
|
646
|
-
// NOTE: We can't do a recursive match, as the parent paths include the
|
|
647
|
-
// root, which is constantly read, but not relevant.
|
|
648
|
-
if (PathValueProxyWatcher.BREAK_ON_READS.has(pathStr)) {
|
|
649
|
-
const unwatch = () => PathValueProxyWatcher.BREAK_ON_READS.delete(pathStr);
|
|
650
|
-
debugger;
|
|
651
|
-
unwatch;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
const watcher = this.runningWatcher;
|
|
655
|
-
if (!watcher) {
|
|
656
|
-
debugger;
|
|
657
|
-
throw new Error(`Tried to get path "${pathStr}" outside of a watcher function.`);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// IMPORTANT! If we read a value we wrote, we don't need to incur a watch!
|
|
661
|
-
// NOTE: If we wrote undefined, then that DOESN'T clobber child values, as it actually
|
|
662
|
-
// deletes values, so... it doesn't count as having a value.
|
|
663
|
-
// - ALSO, writing undefined allows us to then read child values, which is odd,
|
|
664
|
-
// but is the correct behavior.
|
|
665
|
-
if (watcher.pendingWrites.has(pathStr)) {
|
|
666
|
-
let pendingValue = watcher.pendingWrites.get(pathStr);
|
|
667
|
-
let isPendingTransparent = isTransparentValue(pendingValue);
|
|
668
|
-
if (isPendingTransparent) {
|
|
669
|
-
if (readTransparent) {
|
|
670
|
-
return { value: pendingValue };
|
|
671
|
-
} else {
|
|
672
|
-
return undefined;
|
|
673
|
-
}
|
|
674
|
-
} else {
|
|
675
|
-
return { value: pendingValue };
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
let pathValue = this.getCallbackPathValue(pathStr, syncParentKeys);
|
|
680
|
-
|
|
681
|
-
if (!pathValue || !readTransparent && isTransparentValue(pathValueSerializer.getPathValue(pathValue))) {
|
|
682
|
-
let schema2 = getMatchingSchema(pathStr);
|
|
683
|
-
if (schema2) {
|
|
684
|
-
let atomicObj = Schema2Fncs.isAtomic(schema2.schema, "read", schema2.nestedPath);
|
|
685
|
-
if (atomicObj) {
|
|
686
|
-
return { value: atomicObj.defaultValue };
|
|
687
|
-
}
|
|
688
|
-
// Check if the object should be an object. Objects can be read though, if they've
|
|
689
|
-
// been initialized as an object (with specialObjectWriteValue). OTHERWISE,
|
|
690
|
-
// they require initialization (root.object = {}).
|
|
691
|
-
if (Schema2Fncs.isObject(schema2.schema, schema2.nestedPath)) {
|
|
692
|
-
// IMPORTANT! It MAY seem, like we can just add a special "deleteObjectValue_1f548f4a01cb4c35bc7b559d8dd74c42"
|
|
693
|
-
// sentinel, and use that to delete something (both in proxy, and in gc, so it really recursively deletes it),
|
|
694
|
-
// BUT, that has one major issue. Once that sentinel is truly deleted, the value will evaluate a proxy again,
|
|
695
|
-
// which will cause GC to change the state, which would be a bug. Not to mention accessing not existing
|
|
696
|
-
// keys would return a proxy, which is annoying. So... this is the best approach.
|
|
697
|
-
if (pathValueSerializer.getPathValue(pathValue) !== specialObjectWriteValue) {
|
|
698
|
-
// Means the key was deleted (`delete lookup[key]`), and so the intention is for this
|
|
699
|
-
// to terminate in undefined.
|
|
700
|
-
return { value: undefined };
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
return undefined;
|
|
705
|
-
} else {
|
|
706
|
-
let readValue = pathValueSerializer.getPathValue(pathValue);
|
|
707
|
-
// Unescape specialObjectWriteValue-like strings
|
|
708
|
-
if (typeof readValue === "string" && readValue.length > specialObjectWriteValue.length && readValue.startsWith(specialObjectWriteValue)) {
|
|
709
|
-
readValue = readValue.slice(0, -1);
|
|
710
|
-
}
|
|
711
|
-
return { value: readValue };
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
// We exclude undefined, BUT, we include "null", etc.
|
|
716
|
-
// - This differs from javascript, but... due to how deletions work, we can't/don't differentiate between
|
|
717
|
-
// a deleted value and undefined, and a lot of things break if deleting a value doesn't remove it from an "in" check!
|
|
718
|
-
private hasCallback = (pathStr: string): boolean => {
|
|
719
|
-
let value = this.getCallback(pathStr, undefined, "readTransparent");
|
|
720
|
-
return value?.value !== undefined;
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
private setCallback = (pathStr: string, value: unknown, inRecursion = false, allowSpecial = false): void => {
|
|
724
|
-
if (PathValueProxyWatcher.BREAK_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
725
|
-
if (isRecursiveMatch(PathValueProxyWatcher.BREAK_ON_WRITES, pathStr)) {
|
|
726
|
-
let unwatch = () => removeMatches(PathValueProxyWatcher.BREAK_ON_WRITES, pathStr);
|
|
727
|
-
debugger;
|
|
728
|
-
unwatch;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
733
|
-
if (isRecursiveMatch(PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES, pathStr)) {
|
|
734
|
-
let unwatch = () => removeMatches(PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES, pathStr);
|
|
735
|
-
PathValueProxyWatcher.BREAK_ON_CALL.add(
|
|
736
|
-
getPathStr2(getCurrentCall().ModuleId, getCurrentCall().FunctionId)
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (PathValueProxyWatcher.LOG_WRITES_INCLUDES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
742
|
-
if (isRecursiveMatch(PathValueProxyWatcher.LOG_WRITES_INCLUDES, pathStr)) {
|
|
743
|
-
let unwatch = () => removeMatches(PathValueProxyWatcher.LOG_WRITES_INCLUDES, pathStr);
|
|
744
|
-
console.log(`Write path "${pathStr}" = ${value}`);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
if (value === specialObjectWriteSymbol) {
|
|
748
|
-
value = specialObjectWriteValue;
|
|
749
|
-
allowSpecial = true;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Escape specialObjectWriteValue-like strings
|
|
753
|
-
// - Check for .startsWith, so we change all values. If we just added when it was ===, then
|
|
754
|
-
// anything that === the escaped value before hand, would get unescaped, even though
|
|
755
|
-
// it was never escaped!
|
|
756
|
-
if (!allowSpecial && typeof value === "string" && value.startsWith(specialObjectWriteValue)) {
|
|
757
|
-
value += " ";
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const watcher = this.runningWatcher;
|
|
761
|
-
if (!watcher) {
|
|
762
|
-
// Ignore preact paths
|
|
763
|
-
let lastPart = getLastPathPart(pathStr);
|
|
764
|
-
if (lastPart.startsWith("__") || lastPart === "type") {
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
debugger;
|
|
768
|
-
throw new Error(`Tried to set path "${pathStr}" outside of a watcher function.`);
|
|
769
|
-
}
|
|
770
|
-
if (!watcher.options.canWrite) {
|
|
771
|
-
throw new Error(`Tried to write to path "${pathStr}" in watcher (${watcher.debugName}) that has { canWrite: false }`);
|
|
772
|
-
}
|
|
773
|
-
if (watcher.options.noLocks && !pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
774
|
-
throw new Error(`Tried to write a non-local path in a "noLocks" watcher, ${watcher.debugName}, path ${pathStr}`);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (watcher.permissionsChecker) {
|
|
778
|
-
if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
|
|
779
|
-
if (value !== specialObjectWriteValue) {
|
|
780
|
-
if (!watcher.hasAnyUnsyncedAccesses()) {
|
|
781
|
-
// NOTE: Only throw once we sync all accesses, otherwise this could cause sync cascading, which is bad
|
|
782
|
-
// NOTE: We COULD do the same thing for reason... but... it should be equivalent to just NOOP reads. We
|
|
783
|
-
// could NOOP writes as well, but throwing makes development easier (by telling you why your write
|
|
784
|
-
// is doing nothing more loudly than just a console warning).
|
|
785
|
-
throw new Error(`Denied write access to path "${pathStr}"`);
|
|
786
|
-
//console.warn(red(`Denied write access to path "${pathStr}" (ignoring write and continuing)`));
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if (
|
|
794
|
-
!watcher.options.forceEqualWrites
|
|
795
|
-
&& (
|
|
796
|
-
!canHaveChildren(value)
|
|
797
|
-
|| !(atomicObjectSymbol in value || watcher.options.atomicWrites)
|
|
798
|
-
)
|
|
799
|
-
) {
|
|
800
|
-
// NOTE: This NOOPs on predicted writes as well. BUT, we also incur a lock, so, it's fine...
|
|
801
|
-
let existingValue = this.getCallback(pathStr, undefined, "readTransparent");
|
|
802
|
-
// NOTE: We don't deep compare atomicObjectSymbol, as atomicObjectSymbol is often
|
|
803
|
-
// used for exotic values. The user will have to deep compare themselves, if
|
|
804
|
-
// they want to avoid the extra write.
|
|
805
|
-
if (existingValue?.value === value) {
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Set parent specialObjectWriteValue values
|
|
811
|
-
if (
|
|
812
|
-
!watcher.options.writeOnly
|
|
813
|
-
// If we are an event write we can't set parent values, otherwise the parents will become
|
|
814
|
-
// events, which breaks Object.keys on them (which is almost certainly not intended), because
|
|
815
|
-
// event paths can't be watched after a certain time.
|
|
816
|
-
&& !watcher.options.eventWrite
|
|
817
|
-
&& !inRecursion
|
|
818
|
-
// Deletes shouldn't create parent objects!
|
|
819
|
-
&& value !== undefined
|
|
820
|
-
) {
|
|
821
|
-
if (!watcher.pendingWrites.has(pathStr)) {
|
|
822
|
-
let parentPathStr = pathStr;
|
|
823
|
-
let isLocal = pathStr.startsWith(LOCAL_DOMAIN_PATH);
|
|
824
|
-
let depthToData = isLocal ? 1 : DEPTH_TO_DATA;
|
|
825
|
-
while (true) {
|
|
826
|
-
parentPathStr = getParentPathStr(parentPathStr);
|
|
827
|
-
if (getPathDepth(parentPathStr) < depthToData) break;
|
|
828
|
-
// We don't need to check all parents, as if any value is set, all parents will
|
|
829
|
-
// likely have their specialObjectWriteValue set, due to a previous run of this function!
|
|
830
|
-
if (watcher.pendingWrites.has(parentPathStr)) break;
|
|
831
|
-
let parentValue = this.getCallback(parentPathStr, undefined, "readTransparent");
|
|
832
|
-
// If we have a value, don't set the object.
|
|
833
|
-
// NOTE: This means if you:
|
|
834
|
-
// x.y.z = 1
|
|
835
|
-
// delete x.y; // (OR, x.y = null)
|
|
836
|
-
// x.y.z = 2
|
|
837
|
-
// Object.keys(x.y) === ["z"]
|
|
838
|
-
// Which is annoying (keys show up for values you accidentally resurrected), but...
|
|
839
|
-
// it is somewhat required, as we don't know if you are trying to readd
|
|
840
|
-
// an object, or accidentally resurrected it!
|
|
841
|
-
// - Really you should use a flag for deletion, which will work 100% of the time,
|
|
842
|
-
// and make life easier for specific undoing, and also eventually... instructed garbage collection.
|
|
843
|
-
if (parentValue?.value !== specialObjectWriteValue) {
|
|
844
|
-
watcher.pendingWrites.set(parentPathStr, specialObjectWriteValue);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function isAtomicSchema(pathStr: string) {
|
|
851
|
-
let schema2 = getMatchingSchema(pathStr);
|
|
852
|
-
if (schema2) {
|
|
853
|
-
let atomicObj = Schema2Fncs.isAtomic(schema2.schema, "write", schema2.nestedPath);
|
|
854
|
-
if (atomicObj) {
|
|
855
|
-
return true;
|
|
856
|
-
// NOTE: We can't skip if the value === default, as the previous value might not be the default,
|
|
857
|
-
// (so the write might change the value).
|
|
858
|
-
// - We could optimize the = undefined case for atomic objects, where the value is already undefined
|
|
859
|
-
// (but we defaulted it), but... it's probably not worth the effort.
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Destructure the value if it is an object, UNLESS, it specifies that it is atomic.
|
|
865
|
-
if (canHaveChildren(value) && !(atomicObjectSymbol in value) && !watcher.options.atomicWrites && !isAtomicSchema(pathStr)) {
|
|
866
|
-
const addWrites = (pathStr: string, value: unknown) => {
|
|
867
|
-
if (canHaveChildren(value) && !(atomicObjectSymbol in value) && !isAtomicSchema(pathStr)) {
|
|
868
|
-
this.setCallback(pathStr, specialObjectWriteValue, true, true);
|
|
869
|
-
const keys = getKeys(value);
|
|
870
|
-
for (let key of keys) {
|
|
871
|
-
if (typeof key !== "string") {
|
|
872
|
-
throw new Error(`Cannot have non-string keys in non-atomic (atomicObjectWrite) objects. Key: "${String(key)}" at path "${pathStr}".`);
|
|
873
|
-
}
|
|
874
|
-
addWrites(appendToPathStr(pathStr, key), value[key]);
|
|
875
|
-
}
|
|
876
|
-
} else {
|
|
877
|
-
// NOTE: It is a bit inefficient in terms of computation to call setCallback, BUT, it
|
|
878
|
-
// allows us to automatically run our deduping checks, which saves a lot of unnecessary
|
|
879
|
-
// computation on other nodes (and network bandwidth, etc, etc).
|
|
880
|
-
this.setCallback(pathStr, value, true);
|
|
881
|
-
//watcher.pendingWrites.set(pathStr, value);
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
addWrites(pathStr, value);
|
|
885
|
-
} else {
|
|
886
|
-
if (!pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
887
|
-
// Copy the value, to ensure any proxy values aren't attempted to be written
|
|
888
|
-
// to the database. A bit slower, but... it should be fine. We COULD do this
|
|
889
|
-
// later on, but this makes it easier to attribute lag and errors to the original source.
|
|
890
|
-
// - If we had a proxy, we WOULD copy it eventually, but we would copy it later,
|
|
891
|
-
// which would cause an error, because the reader wouldn't be trackable.
|
|
892
|
-
value = deepCloneCborx(value);
|
|
893
|
-
}
|
|
894
|
-
watcher.pendingWrites.set(pathStr, value);
|
|
895
|
-
}
|
|
896
|
-
};
|
|
897
|
-
/** Syncs keys AND values (as we won't return a key for a value that is undefined). */
|
|
898
|
-
public getKeys = (pathStr: string): string[] => {
|
|
899
|
-
if (getPathDepth(pathStr) < MODULE_INDEX) {
|
|
900
|
-
throw new Error(`Cannot call getKeys on path "${pathStr}" because it is too shallow. Must be at least ${MODULE_INDEX} levels deep.`);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const watcher = this.runningWatcher;
|
|
904
|
-
if (!watcher) {
|
|
905
|
-
debugger;
|
|
906
|
-
throw new Error(`Tried to getKeys on path "${pathStr}" outside of a watcher function.`);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
if (watcher.permissionsChecker) {
|
|
910
|
-
if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
|
|
911
|
-
console.warn(`Denied read access to parentPath "${pathStr}"`);
|
|
912
|
-
return [];
|
|
913
|
-
}
|
|
914
|
-
let childPath = appendToPathStr(pathStr, Date.now() + "_" + Math.random());
|
|
915
|
-
if (!watcher.permissionsChecker.checkPermissions(childPath).allowed) {
|
|
916
|
-
console.warn(red(`Denied write access to children of parent path "${childPath}"`));
|
|
917
|
-
return [];
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Using the schema for keys is a MASSIVE optimization, despite the potential drawbacks.
|
|
922
|
-
// - It returns all possible keys, even optional keys, which isn't the same as Object.keys()
|
|
923
|
-
// - It ignores any keys removed from the schema, which causes JSON.stringify to behave differently
|
|
924
|
-
// HOWEVER, saving an entire serverside call, which would otherwise require a roundtrip before we could
|
|
925
|
-
// access values inside the keys... is unbelievably useful. This reduces latency, which is the hardest
|
|
926
|
-
// thing to remove. AND, "noAtomicSchema" can always be used to skip this optimization.
|
|
927
|
-
let schema2 = getMatchingSchema(pathStr);
|
|
928
|
-
if (schema2) {
|
|
929
|
-
let atomicKeys = Schema2Fncs.getKeys(schema2.schema, schema2.nestedPath);
|
|
930
|
-
if (atomicKeys) {
|
|
931
|
-
return atomicKeys;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// IMPORTANT: The this.getCallback is what triggers the parent watch!
|
|
936
|
-
// We leave it up to getCallback to add the parentPaths watch, to allow Object.keys on values
|
|
937
|
-
// that were just written with incurring a parent watch.
|
|
938
|
-
let pathValue = this.getCallback(pathStr, "parentKeys");
|
|
939
|
-
if (!isTransparentValue(pathValue?.value)) {
|
|
940
|
-
return getKeys(pathValue?.value) as string[];
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
let childPaths = authorityStorage.getPathsFromParent(pathStr);
|
|
944
|
-
|
|
945
|
-
// We need to also get keys from pendingWrites
|
|
946
|
-
{
|
|
947
|
-
let copiedChildPaths: Set<string> | undefined;
|
|
948
|
-
let pendingWrites = watcher.pendingWrites;
|
|
949
|
-
for (let childPath of pendingWrites.keys()) {
|
|
950
|
-
if (childPath.length > pathStr.length && childPath.startsWith(pathStr)) {
|
|
951
|
-
if (!copiedChildPaths) {
|
|
952
|
-
copiedChildPaths = childPaths = new Set(childPaths);
|
|
953
|
-
}
|
|
954
|
-
copiedChildPaths.add(childPath);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
let keys = new Set<string>();
|
|
960
|
-
if (childPaths) {
|
|
961
|
-
let targetDepth = getPathDepth(pathStr);
|
|
962
|
-
for (let childPath of childPaths) {
|
|
963
|
-
let key = getPathIndexAssert(childPath, targetDepth);
|
|
964
|
-
let childValue = this.getCallback(childPath, undefined, "readTransparent");
|
|
965
|
-
if (childValue?.value !== undefined) {
|
|
966
|
-
keys.add(key);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
let keysArray = Array.from(keys);
|
|
972
|
-
|
|
973
|
-
// NOTE: Because getPathsFromParent does not preserve order, we have to sort here to ensure
|
|
974
|
-
// we provide a consistent order (which might not be the order the user wants, but at least
|
|
975
|
-
// it will always fail if it does fail).
|
|
976
|
-
keysArray.sort();
|
|
977
|
-
|
|
978
|
-
return keysArray;
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
private getSymbol = (pathStr: string, symbol: symbol): { value: unknown } | undefined => {
|
|
982
|
-
if (symbol === Symbol.toPrimitive) return {
|
|
983
|
-
value: (hint: string) => {
|
|
984
|
-
if (hint === "string") return "";
|
|
985
|
-
if (hint === "number") return 0;
|
|
986
|
-
// NOTE: Returning 0 as the default makes (x++) work nicely, defaulting values to undefined.
|
|
987
|
-
// If they are expecting a string... maybe they should use `${x}`, or... just type check it.
|
|
988
|
-
return 0;
|
|
989
|
-
}
|
|
990
|
-
};
|
|
991
|
-
if (symbol === Symbol.toStringTag) return { value: () => `[proxy at ${pathStr}]` };
|
|
992
|
-
|
|
993
|
-
if (symbol === readNoProxy) {
|
|
994
|
-
let value = this.getCallback(pathStr)?.value;
|
|
995
|
-
return { value };
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
if (symbol === syncedSymbol) {
|
|
999
|
-
return { value: authorityStorage.isSynced(pathStr) };
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// Proxies should be considered atomic, at least for the purpose of other proxies!
|
|
1003
|
-
// - This allows proxies to be written to synced state easily (although this will
|
|
1004
|
-
// cause issues if it isn't local synced state, but... there's not too much
|
|
1005
|
-
// we can do about that. It should just serialize it, possibly reading values
|
|
1006
|
-
// which weren't synced. Which isn't so bad, as if they start trying to create
|
|
1007
|
-
// object graphs in synced state they are going to immediately run into issues
|
|
1008
|
-
// anyways).
|
|
1009
|
-
if (symbol === atomicObjectSymbol) return { value: true };
|
|
1010
|
-
|
|
1011
|
-
if (symbol === rawRead) {
|
|
1012
|
-
return { value: this.getCallback(pathStr, undefined, "readTransparent")?.value };
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return { value: undefined };
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
private proxy = createPathValueProxy({
|
|
1019
|
-
getCallback: this.getCallback,
|
|
1020
|
-
hasCallback: this.hasCallback,
|
|
1021
|
-
setCallback: this.setCallback,
|
|
1022
|
-
getKeys: this.getKeys,
|
|
1023
|
-
getSymbol: this.getSymbol,
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
private runningWatcher: SyncWatcher | undefined;
|
|
1027
|
-
|
|
1028
|
-
// NOTE: We can't support promises until we are able to run the function on another
|
|
1029
|
-
// thread (at which point we can ensure there is no parallel function running).
|
|
1030
|
-
public createWatcher<Result = void>(
|
|
1031
|
-
options: WatcherOptions<Result>
|
|
1032
|
-
): SyncWatcher {
|
|
1033
|
-
if (isAsyncFunction(options.watchFunction)) {
|
|
1034
|
-
throw new Error(`A watcher function cannot be async, it must be synchronous. You probably did Querysub.commitAsync(async () => {}). Just remove the async, and do Querysub.commit(() => {}). The caller will be called again whenever the data you access changes, And if you are running this to return a result, it will be rerun until all the data you want is synchronized. Watch function: ${options.watchFunction.toString()}`);
|
|
1035
|
-
}
|
|
1036
|
-
// NOTE: Setting an order is needed for rendering, so parents render before children. I believe
|
|
1037
|
-
// it is generally what we want, so event triggering is consistent, and fits with any tree based
|
|
1038
|
-
// watching system. If this causes problems we COULD remove it from here and have just qreact.tsx set it.
|
|
1039
|
-
if (options.order === undefined) {
|
|
1040
|
-
options.order = nextOrderSeqNum++;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// Unwrap nested immediate calls to be as close to a naked call as possible. The watcher never becomes
|
|
1044
|
-
// the current watcher, and is immediately disposed. We only create SyncWatcher to return it, otherwise
|
|
1045
|
-
// callers would need to handle a variable return type, which is annoying.
|
|
1046
|
-
// - This means any writes are attributed to the existing call, which... is fine, if the caller wants
|
|
1047
|
-
// to disconnect the call from the existing watcher, they should use `Promise.resolve().finally(...)`.
|
|
1048
|
-
// NOTE: We check for runImmediately as we still want to be able to create watchers in
|
|
1049
|
-
// reaction to trigger of other watchers.
|
|
1050
|
-
if (this.runningWatcher && (options.runImmediately || this.runningWatcher.options.inlineNestedWatchers)) {
|
|
1051
|
-
// TODO: We COULD change our dispose out and change it back, if we find
|
|
1052
|
-
// we often want to mutate shallow props of nested watchers.
|
|
1053
|
-
const outerWatcher = {
|
|
1054
|
-
...this.runningWatcher,
|
|
1055
|
-
// Let them use the watcher, EXCEPT, don't let them dispose it,
|
|
1056
|
-
// as that would dispose the outer watcher as well
|
|
1057
|
-
dispose: () => { },
|
|
1058
|
-
};
|
|
1059
|
-
if (!this.runningWatcher.options.canWrite && options.canWrite) {
|
|
1060
|
-
debugger;
|
|
1061
|
-
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(...)`);
|
|
1062
|
-
}
|
|
1063
|
-
// NOTE: Nested watchers are fine. We need to apply the nested options
|
|
1064
|
-
// (some of which will be ignroed), but otherwise, all of the watchers
|
|
1065
|
-
// will be collapsed to a single watcher, making it fairly efficient.
|
|
1066
|
-
let { watchFunction, ...mostOptions } = options;
|
|
1067
|
-
doProxyOptions(mostOptions, () => {
|
|
1068
|
-
try {
|
|
1069
|
-
let result = authorityStorage.temporaryOverride(options.overrides, () =>
|
|
1070
|
-
options.watchFunction()
|
|
1071
|
-
);
|
|
1072
|
-
// Clone, otherwise proxies get out of the watcher, which can result in accesses outside
|
|
1073
|
-
// of a synced watcher.
|
|
1074
|
-
if (!options.allowProxyResults) {
|
|
1075
|
-
result = deepCloneCborx(result);
|
|
1076
|
-
} else {
|
|
1077
|
-
result = atomicObjectRead(result);
|
|
1078
|
-
}
|
|
1079
|
-
options.onResultUpdated?.({ result }, undefined, outerWatcher);
|
|
1080
|
-
} catch (e: any) {
|
|
1081
|
-
options.onResultUpdated?.({ error: e.stack }, undefined, outerWatcher);
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
return outerWatcher;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const self = this;
|
|
1088
|
-
if (options.runAtTime) {
|
|
1089
|
-
// 80% of our real max range, so calls with a runAtTime close to our limit have a bit of breathing room to run
|
|
1090
|
-
let runLeeway = MAX_ACCEPTED_CHANGE_AGE * 0.8;
|
|
1091
|
-
let runCutoffTime = Date.now() - runLeeway;
|
|
1092
|
-
if (options.runAtTime && options.runAtTime.time < runCutoffTime) {
|
|
1093
|
-
let message = `MAX_CHANGE_AGE_EXCEEDED! Cannot run watcher ${options.debugName} 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.`;
|
|
1094
|
-
console.error(red(message));
|
|
1095
|
-
// NOTE: We could also adjust the to be more recent, to allow it to be commited anyway,
|
|
1096
|
-
// in a slightly different state than originally expected.
|
|
1097
|
-
throw new Error(message);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
let now = Date.now();
|
|
1102
|
-
let debugName = (
|
|
1103
|
-
options.debugName
|
|
1104
|
-
|| options.watchFunction.name
|
|
1105
|
-
|| options.watchFunction.toString().replaceAll("\n", "\\n").replaceAll(/ +/g, " ").slice(0, 50)
|
|
1106
|
-
);
|
|
1107
|
-
if (debugName === "watchFunction" && options.baseFunction) {
|
|
1108
|
-
debugName = options.baseFunction.name || options.baseFunction.toString().replaceAll("\n", "\\n").replaceAll(/ +/g, " ").slice(0, 50);
|
|
1109
|
-
}
|
|
1110
|
-
let watcher: SyncWatcher = {
|
|
1111
|
-
seqNum: nextSeqNum++,
|
|
1112
|
-
debugName,
|
|
1113
|
-
dispose: () => { },
|
|
1114
|
-
disposed: false,
|
|
1115
|
-
onInnerDisposed: [],
|
|
1116
|
-
onAfterTriggered: [],
|
|
1117
|
-
explicitlyTrigger: () => { },
|
|
1118
|
-
hasAnyUnsyncedAccesses: () => false,
|
|
1119
|
-
currentReadTime: options.runAtTime,
|
|
1120
|
-
nextWriteTime: undefined,
|
|
1121
|
-
setCurrentReadTime: null as any,
|
|
1122
|
-
pendingAccesses: new Map(),
|
|
1123
|
-
pendingEpochAccesses: new Map(),
|
|
1124
|
-
triggeredByChanges: undefined,
|
|
1125
|
-
pendingWrites: new Map(),
|
|
1126
|
-
pendingEventWrites: new Set(),
|
|
1127
|
-
pendingCalls: [],
|
|
1128
|
-
pendingWatches: {
|
|
1129
|
-
paths: new Set(),
|
|
1130
|
-
parentPaths: new Set(),
|
|
1131
|
-
},
|
|
1132
|
-
lastWatches: {
|
|
1133
|
-
paths: new Set(),
|
|
1134
|
-
parentPaths: new Set(),
|
|
1135
|
-
},
|
|
1136
|
-
|
|
1137
|
-
pendingUnsyncedAccesses: new Set(),
|
|
1138
|
-
pendingUnsyncedParentAccesses: new Set(),
|
|
1139
|
-
lastUnsyncedAccesses: new Set(),
|
|
1140
|
-
lastUnsyncedParentAccesses: new Set(),
|
|
1141
|
-
|
|
1142
|
-
specialPromiseUnsynced: false,
|
|
1143
|
-
lastSpecialPromiseUnsynced: false,
|
|
1144
|
-
|
|
1145
|
-
options,
|
|
1146
|
-
consecutiveErrors: 0,
|
|
1147
|
-
countSinceLastFullSync: 0,
|
|
1148
|
-
lastEvalTime: 0,
|
|
1149
|
-
lastSyncTime: now,
|
|
1150
|
-
startTime: now,
|
|
1151
|
-
syncRunCount: 0,
|
|
1152
|
-
tag: new SyncWatcherTag(),
|
|
1153
|
-
|
|
1154
|
-
hackHistory: [],
|
|
1155
|
-
createTime: getTimeUnique(),
|
|
1156
|
-
};
|
|
1157
|
-
addToProxyOrder(watcher);
|
|
1158
|
-
const SHOULD_TRACE = this.SHOULD_TRACE(watcher);
|
|
1159
|
-
const proxy = this.proxy;
|
|
1160
|
-
|
|
1161
|
-
if (options.overrides && !options.dryRun) {
|
|
1162
|
-
// If you override values, and want to watch values... how does that even work? And what about
|
|
1163
|
-
// writes? You can't depend on an override, so... what would we do?
|
|
1164
|
-
throw new Error(`overrides without dryRun is not presently supported`);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
watcher.dispose = dispose;
|
|
1168
|
-
this.allWatchers.add(watcher);
|
|
1169
|
-
|
|
1170
|
-
let baseFunction = options.watchFunction;
|
|
1171
|
-
baseFunction = measureWrap(baseFunction, watcher.debugName);
|
|
1172
|
-
{
|
|
1173
|
-
let base = baseFunction;
|
|
1174
|
-
baseFunction = (...args: unknown[]) => {
|
|
1175
|
-
let time = Date.now();
|
|
1176
|
-
try {
|
|
1177
|
-
return (base as any)(...args);
|
|
1178
|
-
} finally {
|
|
1179
|
-
onTimeProfile("Performance|Watcher", time);
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
watcher.setCurrentReadTime = (time, code) => {
|
|
1185
|
-
let prev = watcher.currentReadTime;
|
|
1186
|
-
watcher.currentReadTime = time;
|
|
1187
|
-
try {
|
|
1188
|
-
return code();
|
|
1189
|
-
} finally {
|
|
1190
|
-
watcher.currentReadTime = prev;
|
|
1191
|
-
}
|
|
1192
|
-
};
|
|
1193
|
-
watcher.hasAnyUnsyncedAccesses = () => {
|
|
1194
|
-
if (watcher.options.waitForIncompleteTransactions) {
|
|
1195
|
-
waitIfReceivedIncompleteTransaction();
|
|
1196
|
-
}
|
|
1197
|
-
return (
|
|
1198
|
-
watcher.pendingUnsyncedAccesses.size > 0
|
|
1199
|
-
|| watcher.pendingUnsyncedParentAccesses.size > 0
|
|
1200
|
-
|| watcher.specialPromiseUnsynced
|
|
1201
|
-
|| isProxyBlockedByOrder(watcher)
|
|
1202
|
-
);
|
|
1203
|
-
};
|
|
1204
|
-
const getReadyToCommit = () => {
|
|
1205
|
-
if (watcher.options.commitAllRuns) {
|
|
1206
|
-
return true;
|
|
1207
|
-
}
|
|
1208
|
-
return (
|
|
1209
|
-
watcher.lastUnsyncedAccesses.size === 0
|
|
1210
|
-
&& watcher.lastUnsyncedParentAccesses.size === 0
|
|
1211
|
-
&& !watcher.lastSpecialPromiseUnsynced
|
|
1212
|
-
&& !isProxyBlockedByOrder(watcher)
|
|
1213
|
-
);
|
|
1214
|
-
};
|
|
1215
|
-
function afterTrigger() {
|
|
1216
|
-
for (let i = 0; i < 10; i++) {
|
|
1217
|
-
let callbacks = watcher.onAfterTriggered;
|
|
1218
|
-
watcher.onAfterTriggered = [];
|
|
1219
|
-
for (let callback of callbacks) {
|
|
1220
|
-
logErrors(((async () => { await callback(); }))());
|
|
1221
|
-
}
|
|
1222
|
-
if (watcher.onAfterTriggered.length === 0) break;
|
|
1223
|
-
}
|
|
1224
|
-
if (watcher.onAfterTriggered.length > 0) {
|
|
1225
|
-
console.warn(`Watcher ${watcher.debugName} keeps adding after trigger callbacks during after trigger. We reached our iteration limit
|
|
1226
|
-
and will be ignoring triggers remaining triggers.`);
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
function dispose() {
|
|
1231
|
-
if (watcher.disposed) return;
|
|
1232
|
-
watcher.disposed = true;
|
|
1233
|
-
clientWatcher.unwatch(trigger);
|
|
1234
|
-
self.allWatchers.delete(watcher);
|
|
1235
|
-
|
|
1236
|
-
// NOTE: We trigger next before onInnerDisposed, as onInnerDispose often run more commits, and if we proxy block it causes proxies to run twice.
|
|
1237
|
-
// TODO: Is this guaranteed to be called all the time? What about nested proxy watchers?
|
|
1238
|
-
void finishProxyAndTriggerNext(watcher);
|
|
1239
|
-
|
|
1240
|
-
for (let i = 0; i < 10; i++) {
|
|
1241
|
-
let disposeCallbacks = watcher.onInnerDisposed;
|
|
1242
|
-
watcher.onInnerDisposed = [];
|
|
1243
|
-
for (let callback of disposeCallbacks) {
|
|
1244
|
-
logErrors(((async () => { await callback(); }))());
|
|
1245
|
-
}
|
|
1246
|
-
if (watcher.onInnerDisposed.length === 0) break;
|
|
1247
|
-
}
|
|
1248
|
-
if (watcher.onInnerDisposed.length > 0) {
|
|
1249
|
-
console.warn(`Watcher ${watcher.debugName} keeps adding inner disposed callbacks during dispose. We reached our iteration limit
|
|
1250
|
-
and will be ignoring dispose callbacks remaining dispose callbacks.`);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
self.allWatchersLookup.delete(trigger);
|
|
1254
|
-
}
|
|
1255
|
-
function runWatcher() {
|
|
1256
|
-
watcher.pendingWrites.clear();
|
|
1257
|
-
watcher.pendingEventWrites.clear();
|
|
1258
|
-
watcher.pendingCalls = [];
|
|
1259
|
-
// NOTE: We have to make new sets here, because these Sets are reused for lastWatches, which makes
|
|
1260
|
-
// it's way into clientWatcher, without changing. So if we just clear it, we can break clientWatcher.
|
|
1261
|
-
watcher.pendingWatches.paths = new Set();
|
|
1262
|
-
watcher.pendingWatches.parentPaths = new Set();
|
|
1263
|
-
watcher.pendingUnsyncedAccesses.clear();
|
|
1264
|
-
watcher.pendingUnsyncedParentAccesses.clear();
|
|
1265
|
-
|
|
1266
|
-
watcher.pendingAccesses.clear();
|
|
1267
|
-
watcher.pendingEpochAccesses.clear();
|
|
1268
|
-
// IMPORTANT! Reset onInnerDisposed, so onCommitFinished doesn't result in a callback
|
|
1269
|
-
// per time we wan the watcher!
|
|
1270
|
-
watcher.onInnerDisposed = [];
|
|
1271
|
-
watcher.specialPromiseUnsynced = false;
|
|
1272
|
-
|
|
1273
|
-
// NOTE: If runAtTime is undefined, the writeTime will be undefined, causing us to read the latest data.
|
|
1274
|
-
// When we finish running we will determine a lock time > any received times.
|
|
1275
|
-
// - 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
|
|
1276
|
-
// differences. And we can't just set it arbitrarily into the future, as then we will be writing
|
|
1277
|
-
// in the future, which would cause us to become rejected on writes that happen after our real
|
|
1278
|
-
// time (but after our given time).
|
|
1279
|
-
watcher.nextWriteTime = watcher.options.runAtTime;
|
|
1280
|
-
// Ensure nextWriteTime is a unique time every run, in case we were rerun due to a rejection (unless it is a dryRun,
|
|
1281
|
-
// in which case the writes won't be committed, so they don't need to be unique).
|
|
1282
|
-
|
|
1283
|
-
// 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),
|
|
1284
|
-
// then when we receive a value we will ignore it.
|
|
1285
|
-
if (!watcher.options.dryRun && watcher.nextWriteTime) {
|
|
1286
|
-
watcher.nextWriteTime = clientWatcher.getFreeWriteTime(watcher.nextWriteTime);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
// The read time equals the write time, so our locks use the same time as our write time, and
|
|
1290
|
-
// therefore lock the actual state we used.
|
|
1291
|
-
watcher.currentReadTime = watcher.nextWriteTime;
|
|
1292
|
-
|
|
1293
|
-
watcher.permissionsChecker = options.getPermissionsCheck?.();
|
|
1294
|
-
|
|
1295
|
-
let result: { result: Result } | { error: string };
|
|
1296
|
-
try {
|
|
1297
|
-
let rawResult: Result;
|
|
1298
|
-
const handling = watcher.options.nestedCalls;
|
|
1299
|
-
let curFunction = baseFunction;
|
|
1300
|
-
if (handling === "inline") {
|
|
1301
|
-
curFunction = () => inlineNestedCalls(baseFunction);
|
|
1302
|
-
}
|
|
1303
|
-
rawResult = interceptCalls({
|
|
1304
|
-
onCall(call, metadata) {
|
|
1305
|
-
if (PathValueProxyWatcher.BREAK_ON_CALL.size > 0 && !watcher.hasAnyUnsyncedAccesses()) {
|
|
1306
|
-
let hash = getPathStr2(call.ModuleId, call.FunctionId);
|
|
1307
|
-
if (PathValueProxyWatcher.BREAK_ON_CALL.has(hash)) {
|
|
1308
|
-
const unwatch = () => removeMatches(PathValueProxyWatcher.BREAK_ON_CALL, hash);
|
|
1309
|
-
debugger;
|
|
1310
|
-
unwatch;
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
if (!watcher.options.canWrite) {
|
|
1314
|
-
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.`);
|
|
1315
|
-
}
|
|
1316
|
-
if (handling === "throw") {
|
|
1317
|
-
// TODO: Support making inline calls, which is useful as it will check permissions. Although, it can
|
|
1318
|
-
// easily be slow, and adds a lot of complexity, so... maybe not... maybe always force the app
|
|
1319
|
-
// to do the permissions checks if they want them
|
|
1320
|
-
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.`);
|
|
1321
|
-
} else if (handling === "after" || handling === undefined) {
|
|
1322
|
-
// UPDATE: This isn't feasible. For one, it has n squared complexity. Also, I don't think it would work in the case if you make two function calls within one function? So it seems like it would only ever work if the function calls were in different functions?
|
|
1323
|
-
// UPDATE: Use `if (Querysub.syncAnyPredictionsPending()) return;` at the start of a function if you want to emulate this behavior
|
|
1324
|
-
// - I think the solution for this is that the second function itself needs to wait for all predictions to finish. It's annoying, but I think it's required...
|
|
1325
|
-
// // We need to wait for predictions to finish, otherwise we run into situations
|
|
1326
|
-
// // where we call a function which should change a parameter we want to pass
|
|
1327
|
-
// // to another function, but because the first call didn't predict, the second
|
|
1328
|
-
// // call gets a different values, causing all kinds of issues.
|
|
1329
|
-
// if (watcher.pendingCalls.length === 0) {
|
|
1330
|
-
// let waitPromise = onAllPredictionsFinished();
|
|
1331
|
-
// if (waitPromise) {
|
|
1332
|
-
// proxyWatcher.triggerOnPromiseFinish(waitPromise, {
|
|
1333
|
-
// waitReason: "Waiting for predictions to finish",
|
|
1334
|
-
// });
|
|
1335
|
-
// }
|
|
1336
|
-
// }
|
|
1337
|
-
|
|
1338
|
-
watcher.pendingCalls.push({ call, metadata });
|
|
1339
|
-
} else if (handling === "ignore") {
|
|
1340
|
-
} else if (handling === "inline") {
|
|
1341
|
-
throw new Error(`inlineNestedCalls should have prevented this call from being passed to us`);
|
|
1342
|
-
} else {
|
|
1343
|
-
let unhandled: never = handling;
|
|
1344
|
-
throw new Error(`Invalid handling type ${handling}`);
|
|
1345
|
-
}
|
|
1346
|
-
},
|
|
1347
|
-
code() {
|
|
1348
|
-
return authorityStorage.temporaryOverride(options.overrides, () =>
|
|
1349
|
-
runCodeWithDatabase(proxy, baseFunction)
|
|
1350
|
-
);
|
|
1351
|
-
},
|
|
1352
|
-
}) as Result;
|
|
1353
|
-
|
|
1354
|
-
// NOTE: Deep clone non-local paths. It's too confusing for a partial object to be returned. Any issues caused by
|
|
1355
|
-
// this (aka, returning the entire database), would also happen if we manually added deep clones before
|
|
1356
|
-
// we returned values (and both should be throttled at a framework level so we don't break the database).
|
|
1357
|
-
// - For local paths there is a risk that there are functions
|
|
1358
|
-
if (!options.allowProxyResults) {
|
|
1359
|
-
try {
|
|
1360
|
-
rawResult = deepCloneCborx(rawResult);
|
|
1361
|
-
} catch {
|
|
1362
|
-
// Unfortunately, cborx throws on functions.
|
|
1363
|
-
// TODO: Use a recursive clone technique that doesn't throw on functions
|
|
1364
|
-
rawResult = atomicObjectRead(rawResult);
|
|
1365
|
-
}
|
|
1366
|
-
} else {
|
|
1367
|
-
rawResult = atomicObjectRead(rawResult);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
result = { result: rawResult };
|
|
1371
|
-
watcher.consecutiveErrors = 0;
|
|
1372
|
-
} catch (e: any) {
|
|
1373
|
-
watcher.consecutiveErrors++;
|
|
1374
|
-
if (watcher.consecutiveErrors > 10) {
|
|
1375
|
-
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.`);
|
|
1376
|
-
console.error(e);
|
|
1377
|
-
}
|
|
1378
|
-
result = { error: e.stack };
|
|
1379
|
-
} finally {
|
|
1380
|
-
afterTrigger();
|
|
1381
|
-
// Set the checker to undefined, so we don't accidentally use it again,
|
|
1382
|
-
// as it will throw if we try to use it after any asynchronous delay.
|
|
1383
|
-
watcher.permissionsChecker = undefined;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
let anyUnsynced = watcher.hasAnyUnsyncedAccesses();
|
|
1388
|
-
|
|
1389
|
-
watcher.lastUnsyncedAccesses = watcher.pendingUnsyncedAccesses;
|
|
1390
|
-
watcher.lastUnsyncedParentAccesses = watcher.pendingUnsyncedParentAccesses;
|
|
1391
|
-
watcher.lastSpecialPromiseUnsynced = watcher.specialPromiseUnsynced;
|
|
1392
|
-
|
|
1393
|
-
// TODO: Add promise delays as well as the finish order reordering delays to the sync timings.
|
|
1394
|
-
if (watcher.options.logSyncTimings) {
|
|
1395
|
-
if (anyUnsynced) {
|
|
1396
|
-
watcher.logSyncTimings = watcher.logSyncTimings || {
|
|
1397
|
-
startTime: Date.now(),
|
|
1398
|
-
unsyncedPaths: new Set(),
|
|
1399
|
-
unsyncedStages: [],
|
|
1400
|
-
};
|
|
1401
|
-
let newPaths: string[] = [];
|
|
1402
|
-
let set = watcher.logSyncTimings.unsyncedPaths;
|
|
1403
|
-
function addPath(path: string) {
|
|
1404
|
-
if (set.has(path)) return;
|
|
1405
|
-
set.add(path);
|
|
1406
|
-
newPaths.push(path);
|
|
1407
|
-
}
|
|
1408
|
-
for (let path of watcher.lastUnsyncedAccesses) {
|
|
1409
|
-
addPath(path);
|
|
1410
|
-
}
|
|
1411
|
-
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
1412
|
-
addPath(path);
|
|
1413
|
-
}
|
|
1414
|
-
if (newPaths.length > 0) {
|
|
1415
|
-
watcher.logSyncTimings.unsyncedStages.push({
|
|
1416
|
-
paths: newPaths,
|
|
1417
|
-
time: Date.now(),
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
} else {
|
|
1421
|
-
if (watcher.logSyncTimings) {
|
|
1422
|
-
let now = Date.now();
|
|
1423
|
-
console.groupCollapsed(`${watcher.debugName} synced in ${formatTime(now - watcher.logSyncTimings.startTime)}`);
|
|
1424
|
-
// Log the stages
|
|
1425
|
-
let stages = watcher.logSyncTimings.unsyncedStages;
|
|
1426
|
-
for (let i = 0; i < stages.length; i++) {
|
|
1427
|
-
let nextTime = stages[i + 1]?.time || now;
|
|
1428
|
-
let stage = stages[i];
|
|
1429
|
-
console.groupCollapsed(`${formatTime(nextTime - stage.time)}`);
|
|
1430
|
-
for (let path of stage.paths) {
|
|
1431
|
-
console.log(path);
|
|
1432
|
-
}
|
|
1433
|
-
console.groupEnd();
|
|
1434
|
-
}
|
|
1435
|
-
console.groupEnd();
|
|
1436
|
-
watcher.logSyncTimings = undefined;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
if (!watcher.options.static) {
|
|
1442
|
-
if (!anyUnsynced) {
|
|
1443
|
-
watcher.countSinceLastFullSync = 0;
|
|
1444
|
-
watcher.lastSyncTime = Date.now();
|
|
1445
|
-
watcher.syncRunCount++;
|
|
1446
|
-
} else {
|
|
1447
|
-
if (watcher.countSinceLastFullSync === 0) {
|
|
1448
|
-
let syncRunCount = watcher.syncRunCount;
|
|
1449
|
-
setTimeout(() => {
|
|
1450
|
-
if (watcher.syncRunCount !== syncRunCount) return;
|
|
1451
|
-
if (watcher.disposed) return;
|
|
1452
|
-
let remainingUnsynced = Array.from(watcher.lastUnsyncedAccesses).filter(x => !authorityStorage.isSynced(x));
|
|
1453
|
-
let remainingUnsyncedParent = Array.from(watcher.lastUnsyncedParentAccesses).filter(x => !authorityStorage.isParentSynced(x));
|
|
1454
|
-
console.warn(red(`Did not sync ${watcher.debugName} after 30 seconds. Either we had a lot synchronous block, or we will never received the values we are waiting for.`), remainingUnsynced, remainingUnsyncedParent, watcher.options.watchFunction);
|
|
1455
|
-
}, 30 * 1000);
|
|
1456
|
-
setTimeout(() => {
|
|
1457
|
-
if (watcher.syncRunCount !== syncRunCount) return;
|
|
1458
|
-
if (watcher.disposed) return;
|
|
1459
|
-
|
|
1460
|
-
// IMPORTANT! Any of these can be also caused by high synchronous lag!
|
|
1461
|
-
// - If this becomes a serious concern, we could add a watch loop for this, which
|
|
1462
|
-
// tries to run every second, recording how far it misses the target time by,
|
|
1463
|
-
// and then uses that (plus any outstanding missed time) to determine how much lag
|
|
1464
|
-
// we have had since this watcher last synced. If it is > 50% of the time...
|
|
1465
|
-
// then synchronous lag is the issue.
|
|
1466
|
-
|
|
1467
|
-
let reallyUnsyncedAccesses = Array.from(watcher.lastUnsyncedAccesses).filter(x => !authorityStorage.isSynced(x));
|
|
1468
|
-
let reallyUnsyncedParentAccesses = Array.from(watcher.lastUnsyncedParentAccesses).filter(x => !authorityStorage.isParentSynced(x));
|
|
1469
|
-
|
|
1470
|
-
if (reallyUnsyncedAccesses.length !== 0 || reallyUnsyncedParentAccesses.length !== 0) {
|
|
1471
|
-
let notWatchingUnsynced = reallyUnsyncedAccesses.filter(x => !remoteWatcher.debugIsWatchingPath(x));
|
|
1472
|
-
let notWatchingUnsyncedParent = reallyUnsyncedParentAccesses.filter(x => !remoteWatcher.debugIsWatchingPath(x));
|
|
1473
|
-
if (notWatchingUnsynced.length !== 0 || notWatchingUnsyncedParent.length !== 0) {
|
|
1474
|
-
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);
|
|
1475
|
-
debugbreak(2);
|
|
1476
|
-
debugger;
|
|
1477
|
-
} else {
|
|
1478
|
-
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);
|
|
1479
|
-
// debugbreak(2);
|
|
1480
|
-
// debugger;
|
|
1481
|
-
}
|
|
1482
|
-
} else if (watcher.lastSpecialPromiseUnsynced) {
|
|
1483
|
-
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);
|
|
1484
|
-
debugbreak(2);
|
|
1485
|
-
debugger;
|
|
1486
|
-
} else {
|
|
1487
|
-
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);
|
|
1488
|
-
debugbreak(2);
|
|
1489
|
-
debugger;
|
|
1490
|
-
}
|
|
1491
|
-
}, 60000);
|
|
1492
|
-
}
|
|
1493
|
-
watcher.countSinceLastFullSync++;
|
|
1494
|
-
if (watcher.countSinceLastFullSync > 10) {
|
|
1495
|
-
require("debugbreak")(2);
|
|
1496
|
-
debugger;
|
|
1497
|
-
console.warn(`Watcher ${watcher.debugName} has been unsynced for ${watcher.countSinceLastFullSync} times. This is fine, but maybe optimize it. Why is it cascading?`, watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses, watcher.options.watchFunction);
|
|
1498
|
-
}
|
|
1499
|
-
if (watcher.countSinceLastFullSync > 500) {
|
|
1500
|
-
debugger;
|
|
1501
|
-
// NOTE: Using forceEqualWrites will also fix this a lot of the time, such as when
|
|
1502
|
-
// a write contains random numbers or dates.
|
|
1503
|
-
let errorMessage = `Too many attempts (${watcher.countSinceLastFullSync}) 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.`;
|
|
1504
|
-
if (watcher.lastSpecialPromiseUnsynced) {
|
|
1505
|
-
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!)`;
|
|
1506
|
-
}
|
|
1507
|
-
errorMessage += ` (${watcher.debugName}})`;
|
|
1508
|
-
console.error(red(errorMessage));
|
|
1509
|
-
logUnsynced();
|
|
1510
|
-
result = { error: new Error(errorMessage).stack || "" };
|
|
1511
|
-
// Force the watches to be equal, so we stop looping
|
|
1512
|
-
watcher.pendingWatches.paths = new Set(watcher.lastWatches.paths);
|
|
1513
|
-
watcher.pendingWatches.parentPaths = new Set(watcher.lastWatches.parentPaths);
|
|
1514
|
-
watcher.syncRunCount++;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
return result;
|
|
1520
|
-
}
|
|
1521
|
-
function updateWatchers() {
|
|
1522
|
-
watcher.lastWatches = {
|
|
1523
|
-
paths: watcher.pendingWatches.paths,
|
|
1524
|
-
parentPaths: watcher.pendingWatches.parentPaths,
|
|
1525
|
-
};
|
|
1526
|
-
|
|
1527
|
-
// NOTE: I don't see the reason for this. How many watchers do we have that aren't watching anything?
|
|
1528
|
-
// If nothing is being watched, and nothing was... then don't bother updating the watchers
|
|
1529
|
-
// if (
|
|
1530
|
-
// !initialTriggerWatch
|
|
1531
|
-
// && watcher.lastWatches.paths.size === 0 && watcher.lastWatches.parentPaths.size === 0
|
|
1532
|
-
// && watcher.pendingWatches.paths.size === 0 && watcher.pendingWatches.parentPaths.size === 0
|
|
1533
|
-
// ) return;
|
|
1534
|
-
|
|
1535
|
-
clientWatcher.setWatches({
|
|
1536
|
-
debugName: watcher.debugName,
|
|
1537
|
-
// We don't need to trigger immediately, as we already ran, and if all of our values were synced
|
|
1538
|
-
// we will have already committed any results.
|
|
1539
|
-
noInitialTrigger: true,
|
|
1540
|
-
paths: watcher.lastWatches.paths,
|
|
1541
|
-
parentPaths: watcher.lastWatches.parentPaths,
|
|
1542
|
-
order: watcher.options.order,
|
|
1543
|
-
orderGroup: watcher.options.orderGroup,
|
|
1544
|
-
callback: trigger,
|
|
1545
|
-
unwatchEventPaths(paths) {
|
|
1546
|
-
self.unwatchEventPaths(watcher, paths);
|
|
1547
|
-
},
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
function commitWrites(): PathValue[] | undefined {
|
|
1551
|
-
if (!options.canWrite) return undefined;
|
|
1552
|
-
|
|
1553
|
-
let setValues: PathValue[] | undefined;
|
|
1554
|
-
|
|
1555
|
-
let prefixes = options.overrideAllowLockDomainsPrefixes || getAllowedLockDomainsPrefixes();
|
|
1556
|
-
// This consumes our write time, as once we write, we can't reuse writeTimes!
|
|
1557
|
-
// (If we do, it breaks dependencies).
|
|
1558
|
-
let writeTime = watcher.nextWriteTime;
|
|
1559
|
-
watcher.nextWriteTime = undefined;
|
|
1560
|
-
if (!writeTime) {
|
|
1561
|
-
writeTime = getNextTime();
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
if (
|
|
1565
|
-
!watcher.options.hasSideEffects
|
|
1566
|
-
&& watcher.pendingWrites.size === 0
|
|
1567
|
-
&& watcher.pendingCalls.length === 0
|
|
1568
|
-
) {
|
|
1569
|
-
wastedEvaluations++;
|
|
1570
|
-
}
|
|
1571
|
-
evaluations++;
|
|
1572
|
-
|
|
1573
|
-
if (watcher.pendingWrites.size > 0) {
|
|
1574
|
-
let locks: ReadLock[] = [];
|
|
1575
|
-
let historyBasedReads = false;
|
|
1576
|
-
for (let time of watcher.pendingAccesses.keys()) {
|
|
1577
|
-
if (time === undefined) continue;
|
|
1578
|
-
// If we're reading from a specific time, and it isn't just our write time, then we're reading in the past.
|
|
1579
|
-
if (compareTime(time, writeTime) !== 0) {
|
|
1580
|
-
historyBasedReads = true;
|
|
1581
|
-
break;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
if (!watcher.options.noLocks && !watcher.options.unsafeNoLocks) {
|
|
1586
|
-
for (let [readTime, values] of watcher.pendingAccesses) {
|
|
1587
|
-
if (!readTime) {
|
|
1588
|
-
readTime = writeTime;
|
|
1589
|
-
}
|
|
1590
|
-
for (let valueObj of values.values()) {
|
|
1591
|
-
if (valueObj.noLocks) continue;
|
|
1592
|
-
let value = valueObj.pathValue;
|
|
1593
|
-
// If any value has no locks, AND is local, it won't be rejected, so we don't need to lock it
|
|
1594
|
-
if (value.lockCount === 0 && value.path.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
1595
|
-
continue;
|
|
1596
|
-
}
|
|
1597
|
-
if (!prefixes.some(x => value.path.startsWith(x))) continue;
|
|
1598
|
-
let newReadCheck = !!value.isTransparent;
|
|
1599
|
-
if (value.canGCValue && !value.isTransparent) {
|
|
1600
|
-
console.error(`Value is GCable, but not transparent. We likely forgot to set isTransparent when setting canGCValue. Path ${value.path}, value: ${String(pathValueSerializer.getPathValue(value))}`);
|
|
1601
|
-
debugbreak(2);
|
|
1602
|
-
debugger;
|
|
1603
|
-
}
|
|
1604
|
-
locks.push({
|
|
1605
|
-
path: value.path,
|
|
1606
|
-
startTime: value.time,
|
|
1607
|
-
endTime: readTime,
|
|
1608
|
-
readIsTransparent: newReadCheck,
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
for (let [readTime, paths] of watcher.pendingEpochAccesses) {
|
|
1613
|
-
if (!readTime) {
|
|
1614
|
-
readTime = writeTime;
|
|
1615
|
-
}
|
|
1616
|
-
for (let path of paths) {
|
|
1617
|
-
if (!prefixes.some(x => path.startsWith(x))) continue;
|
|
1618
|
-
locks.push({
|
|
1619
|
-
path,
|
|
1620
|
-
startTime: epochTime,
|
|
1621
|
-
endTime: readTime,
|
|
1622
|
-
readIsTransparent: true,
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
{
|
|
1629
|
-
// Ensure our write time isn't before any of our reads (as long as we aren't running
|
|
1630
|
-
// at a specific time).
|
|
1631
|
-
// - We might want an option to disable this behavior? Although, I'm not sure how we would
|
|
1632
|
-
// run in the present, but intentionally want to write in the past? This usually only
|
|
1633
|
-
// happens if we intentionally run in the past (in which case writeTime will be set,
|
|
1634
|
-
// so this if statement wouldn't run anyways).
|
|
1635
|
-
let fixedWriteTime = writeTime;
|
|
1636
|
-
for (let lock of locks) {
|
|
1637
|
-
if (compareTime(lock.endTime, fixedWriteTime) > 0) {
|
|
1638
|
-
fixedWriteTime = {
|
|
1639
|
-
time: addEpsilons(lock.endTime.time, 1),
|
|
1640
|
-
version: 0,
|
|
1641
|
-
creatorId: getCreatorId(),
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
if (fixedWriteTime !== writeTime) {
|
|
1646
|
-
let prevWriteTime = writeTime;
|
|
1647
|
-
writeTime = fixedWriteTime;
|
|
1648
|
-
for (let lock of locks) {
|
|
1649
|
-
if (lock.endTime === prevWriteTime) {
|
|
1650
|
-
lock.endTime = writeTime;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
let forcedUndefinedWrites = new Set<string>();
|
|
1657
|
-
for (let [path, value] of watcher.pendingWrites) {
|
|
1658
|
-
if (value === specialObjectWriteValue) {
|
|
1659
|
-
forcedUndefinedWrites.add(path);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
let maxLocks = watcher.options.maxLocksOverride || DEFAULT_MAX_LOCKS;
|
|
1664
|
-
if (locks.length > maxLocks) {
|
|
1665
|
-
throw new Error(`Too many locks for ${watcher.debugName} (${locks.length} > ${maxLocks}). Use Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. You can override max locks with maxLocksOverride (in options / functionMetadata).`);
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
// if (debugName.includes("setBookNodes")) {
|
|
1670
|
-
// debugger;
|
|
1671
|
-
// }
|
|
1672
|
-
|
|
1673
|
-
setValues = clientWatcher.setValues({
|
|
1674
|
-
values: watcher.pendingWrites,
|
|
1675
|
-
eventPaths: watcher.pendingEventWrites,
|
|
1676
|
-
writeTime: writeTime,
|
|
1677
|
-
locks,
|
|
1678
|
-
noWritePrediction: options.doNotStoreWritesAsPredictions,
|
|
1679
|
-
eventWrite: options.eventWrite,
|
|
1680
|
-
dryRun: options.dryRun,
|
|
1681
|
-
forcedUndefinedWrites,
|
|
1682
|
-
source: options.source,
|
|
1683
|
-
historyBasedReads,
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
doCallCreation(watcher, () => {
|
|
1688
|
-
// TODO: Maybe return this in some way, so dryRun can know what calls we want to call?
|
|
1689
|
-
for (let { call, metadata } of watcher.pendingCalls) {
|
|
1690
|
-
// The calls have to happen after our local writes. This is because they are likely to
|
|
1691
|
-
// influence the local writes, and we don't want our local writes to be always invalidated
|
|
1692
|
-
call.runAtTime = getNextTime();
|
|
1693
|
-
call.fromProxy = watcher.debugName;
|
|
1694
|
-
logErrors(runCall(call, metadata));
|
|
1695
|
-
watcher.options.onCallCommit?.(call, metadata);
|
|
1696
|
-
}
|
|
1697
|
-
});
|
|
1698
|
-
|
|
1699
|
-
return setValues;
|
|
1700
|
-
|
|
1701
|
-
}
|
|
1702
|
-
const hasUnsyncedBefore = measureWrap(function proxyWatchHasUnsyncedBefore() {
|
|
1703
|
-
if (watcher.options.commitAllRuns) return false;
|
|
1704
|
-
// NOTE: We COULD remove any synced values from lastUnsyncedAccesses, however... we will generally sync all values at once, so we don't really need to optimize the cascading case here. Also... deleting values requires cloning while we iterate, as well as mutating the set, which probably makes the non-cascading case slower.
|
|
1705
|
-
for (let path of watcher.lastUnsyncedAccesses) {
|
|
1706
|
-
if (!authorityStorage.isSynced(path)) {
|
|
1707
|
-
return true;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
1711
|
-
if (!authorityStorage.isParentSynced(path)) {
|
|
1712
|
-
return true;
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
// Actually, a lot of calls are going to be blocked by the proxy order, and we want to run them in parallel. So we have all the values synced, so we're ready to finish them when the next one finally finishes.
|
|
1716
|
-
//if (isProxyBlockedByOrder(watcher)) return true;
|
|
1717
|
-
// NOTE: We don't check promises as they often access non-synced code, and so we might retrigger
|
|
1718
|
-
// and not use the same promise, so it might be wrong to check them.
|
|
1719
|
-
return false;
|
|
1720
|
-
});
|
|
1721
|
-
function logUnsynced() {
|
|
1722
|
-
let anyLogged = false;
|
|
1723
|
-
for (let path of watcher.lastUnsyncedAccesses) {
|
|
1724
|
-
if (!authorityStorage.isSynced(path)) {
|
|
1725
|
-
console.log(yellow(` Waiting for ${path}`));
|
|
1726
|
-
anyLogged = true;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
1730
|
-
if (!authorityStorage.isParentSynced(path)) {
|
|
1731
|
-
console.log(yellow(` Waiting for parent ${path}`));
|
|
1732
|
-
anyLogged = true;
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
// NOTE: We don't log for promises, as they are not checked in hasUnsyncedBefore
|
|
1736
|
-
return anyLogged;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
let trigger = (changed?: WatchSpecData) => {
|
|
1741
|
-
let time = Date.now();
|
|
1742
|
-
|
|
1743
|
-
if (this.runningWatcher) {
|
|
1744
|
-
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}`);
|
|
1745
|
-
}
|
|
1746
|
-
if (watcher.disposed) {
|
|
1747
|
-
return;
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
// Merge triggers. We keep triggers if a callback fails to rerun due to unsynced values. This is essential, as it allows
|
|
1751
|
-
// callbacks to always receive every changed value, which we rely on to maintain delta based dependencies.
|
|
1752
|
-
{
|
|
1753
|
-
if (changed && (PathValueProxyWatcher.TRACK_TRIGGERS || ClientWatcher.DEBUG_TRIGGERS || options.trackTriggers)) {
|
|
1754
|
-
if (watcher.triggeredByChanges) {
|
|
1755
|
-
for (let path of changed.paths) {
|
|
1756
|
-
watcher.triggeredByChanges.paths.add(path);
|
|
1757
|
-
}
|
|
1758
|
-
for (let parentPath of changed.newParentsSynced) {
|
|
1759
|
-
watcher.triggeredByChanges.newParentsSynced.add(parentPath);
|
|
1760
|
-
}
|
|
1761
|
-
if (changed.pathSources) {
|
|
1762
|
-
if (watcher.triggeredByChanges.pathSources) {
|
|
1763
|
-
for (let value of changed.pathSources) {
|
|
1764
|
-
watcher.triggeredByChanges.pathSources.add(value);
|
|
1765
|
-
}
|
|
1766
|
-
} else {
|
|
1767
|
-
watcher.triggeredByChanges.pathSources = new Set(changed.pathSources);
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
if (changed.extraReasons) {
|
|
1771
|
-
watcher.triggeredByChanges.extraReasons = [
|
|
1772
|
-
...watcher.triggeredByChanges.extraReasons || [],
|
|
1773
|
-
...changed.extraReasons
|
|
1774
|
-
];
|
|
1775
|
-
}
|
|
1776
|
-
} else {
|
|
1777
|
-
watcher.triggeredByChanges = changed;
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
triggerCount++;
|
|
1783
|
-
let dropInsteadOfDelay = options.runImmediately || options.temporary;
|
|
1784
|
-
|
|
1785
|
-
const runOncePerPaint = options.runOncePerPaint;
|
|
1786
|
-
|
|
1787
|
-
if (runOncePerPaint) {
|
|
1788
|
-
if (pausedUntilNextPaint.has(runOncePerPaint)) {
|
|
1789
|
-
let prevValue = pausedUntilNextPaint.get(runOncePerPaint);
|
|
1790
|
-
if (dropInsteadOfDelay) {
|
|
1791
|
-
triggersDropped++;
|
|
1792
|
-
} else if (prevValue !== PAINT_NOOP_FUNCTION) {
|
|
1793
|
-
// If the previous value was a NOOP, we aren't dropping the function, we are just delaying it
|
|
1794
|
-
triggersDropped++;
|
|
1795
|
-
}
|
|
1796
|
-
pausedUntilNextPaint.set(runOncePerPaint, () => {
|
|
1797
|
-
// Use dropInsteadOfDelay as late as possible, so it is set by the latest call,
|
|
1798
|
-
// instead of the first call.
|
|
1799
|
-
if (dropInsteadOfDelay) return;
|
|
1800
|
-
watcher.explicitlyTrigger();
|
|
1801
|
-
});
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
// Set NOOP function just as a placeholder, to block future calls (until the next paint)
|
|
1806
|
-
pausedUntilNextPaint.set(runOncePerPaint, PAINT_NOOP_FUNCTION);
|
|
1807
|
-
void onNextPaint().finally(() => {
|
|
1808
|
-
let fnc = pausedUntilNextPaint.get(runOncePerPaint);
|
|
1809
|
-
if (!fnc) return;
|
|
1810
|
-
pausedUntilNextPaint.delete(runOncePerPaint);
|
|
1811
|
-
fnc();
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
if (!options.runImmediately && !options.allowUnsyncedReads && hasUnsyncedBefore()) {
|
|
1816
|
-
if (SHOULD_TRACE) {
|
|
1817
|
-
console.log(yellow(`Skipping trigger due to unsynced watchers. ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
|
|
1818
|
-
if (!logUnsynced()) {
|
|
1819
|
-
debugbreak(2);
|
|
1820
|
-
debugger;
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
// NOTE: When we sync the values, we will automatically be triggered, so we don't need to
|
|
1824
|
-
// setup a trigger here.
|
|
1825
|
-
return;
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
// NOTE: We don't log the trigger evaluation starting, as that should really be logged inside of ClientWatcher,
|
|
1829
|
-
// as a result of ClientWatcher.DEBUG_TRIGGERS (except for runImmediate).
|
|
1830
|
-
if (options.runImmediately) {
|
|
1831
|
-
if (SHOULD_TRACE) {
|
|
1832
|
-
console.log(`${green("INITIAL TRIGGER")} ${watcher.debugName}`, watcher.triggeredByChanges);
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
watcher.lastEvalTime = Date.now();
|
|
1837
|
-
|
|
1838
|
-
// Do as little as possible while runningWatcher is true. This allows callbacks, etc, to create
|
|
1839
|
-
// new watchers without having to juggle .runningWatcher.
|
|
1840
|
-
let result: { result: Result } | { error: string } | undefined;
|
|
1841
|
-
try {
|
|
1842
|
-
this.runningWatcher = watcher;
|
|
1843
|
-
result = runWatcher();
|
|
1844
|
-
} finally {
|
|
1845
|
-
this.runningWatcher = undefined;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
const nonLooping = options.runImmediately || options.allowUnsyncedReads;
|
|
1849
|
-
|
|
1850
|
-
const readyToCommit = (
|
|
1851
|
-
nonLooping || getReadyToCommit()
|
|
1852
|
-
);
|
|
1853
|
-
|
|
1854
|
-
if (!nonLooping) {
|
|
1855
|
-
if (readyToCommit) {
|
|
1856
|
-
harvestableReadyLoopCount++;
|
|
1857
|
-
} else {
|
|
1858
|
-
harvestableWaitingLoopCount++;
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
// Commit writes BEFORE we watch. This prevents us from triggering ourself, in our first watch
|
|
1863
|
-
// (on subsequent watches ClientWatcher will prevent self watches. It can't for the first
|
|
1864
|
-
// watch, as it wasn't the triggerer).
|
|
1865
|
-
let values: PathValue[] | undefined;
|
|
1866
|
-
if (readyToCommit) {
|
|
1867
|
-
try {
|
|
1868
|
-
values = commitWrites();
|
|
1869
|
-
} catch (e: any) {
|
|
1870
|
-
// setValues might throw, such as if the write time is too far into the past.
|
|
1871
|
-
result = { error: e.stack };
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
if (SHOULD_TRACE && "error" in result) {
|
|
1876
|
-
console.log(red(`Error in watcher ${watcher.debugName} at ${time}`), result.error);
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
if (!options.runImmediately) {
|
|
1880
|
-
updateWatchers();
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
if (!readyToCommit) {
|
|
1884
|
-
if (SHOULD_TRACE) {
|
|
1885
|
-
console.log(yellow(`Skipping trigger commit due to unsynced accesses. ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
|
|
1886
|
-
if (watcher.lastSpecialPromiseUnsynced) {
|
|
1887
|
-
console.log(yellow(` Waiting for promise`));
|
|
1888
|
-
}
|
|
1889
|
-
logUnsynced();
|
|
1890
|
-
}
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
if (SHOULD_TRACE) {
|
|
1895
|
-
console.log(green(`Committing ${values?.length || 0} watcher writes ${watcher.debugName} at ${time}`), watcher.triggeredByChanges, watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses);
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
// ONLY clear triggeredByChanges when we have a succesful run, otherwise delta based watchers won't work!
|
|
1899
|
-
watcher.triggeredByChanges = undefined;
|
|
1900
|
-
options.onResultUpdated?.(result, values, watcher);
|
|
1901
|
-
};
|
|
1902
|
-
trigger = measureWrap(trigger, `(watcher) ${watcher.debugName}`);
|
|
1903
|
-
watcher.explicitlyTrigger = trigger;
|
|
1904
|
-
|
|
1905
|
-
self.allWatchersLookup.set(trigger, watcher);
|
|
1906
|
-
|
|
1907
|
-
if (options.static) {
|
|
1908
|
-
// Do nothing, this will be explicitly triggered when needed
|
|
1909
|
-
} else if (options.runImmediately) {
|
|
1910
|
-
trigger(undefined);
|
|
1911
|
-
dispose();
|
|
1912
|
-
} else {
|
|
1913
|
-
// Set our watchers
|
|
1914
|
-
updateWatchers();
|
|
1915
|
-
clientWatcher.explicitlyTriggerWatcher(trigger, {
|
|
1916
|
-
synchronous: options.synchronousInit,
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
return watcher;
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
/** IMPORTANT! Values written will only show up in direct parent calls.
|
|
1923
|
-
* This means if you do `data.list[Date.now()].value = value`,
|
|
1924
|
-
* getKeys(data.list) will NOT have your new key.
|
|
1925
|
-
* Instead, do writes like this: `data.list[Date.now()] = { value }`,
|
|
1926
|
-
* or, if the objects are atomic, like this: `data.list[Date.now()] = atomicObjectWrite({ value })`,
|
|
1927
|
-
* which is move efficient (but the .value cannot be mutated in the future, only clobbered).
|
|
1928
|
-
* NOTE: For reading values (readOnly), just use `commitFunction`.
|
|
1929
|
-
*/
|
|
1930
|
-
public writeOnly<Result = void>(
|
|
1931
|
-
options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">
|
|
1932
|
-
): Result {
|
|
1933
|
-
let result: { result: Result } | { error: string } | undefined;
|
|
1934
|
-
this.createWatcher({
|
|
1935
|
-
forceEqualWrites: true,
|
|
1936
|
-
...options,
|
|
1937
|
-
writeOnly: true,
|
|
1938
|
-
runImmediately: true,
|
|
1939
|
-
allowUnsyncedReads: true,
|
|
1940
|
-
canWrite: true,
|
|
1941
|
-
temporary: true,
|
|
1942
|
-
onResultUpdated: r => {
|
|
1943
|
-
result = r;
|
|
1944
|
-
}
|
|
1945
|
-
});
|
|
1946
|
-
if (!result) {
|
|
1947
|
-
throw new Error("Expected result to be set");
|
|
1948
|
-
}
|
|
1949
|
-
if ("error" in result) {
|
|
1950
|
-
throw errorify(result.error);
|
|
1951
|
-
}
|
|
1952
|
-
return result.result as Result;
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
public runOnce<Result = void>(
|
|
1956
|
-
options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">
|
|
1957
|
-
): Result {
|
|
1958
|
-
let result: { result: Result } | { error: string } | undefined;
|
|
1959
|
-
let watcher = this.createWatcher({
|
|
1960
|
-
...options,
|
|
1961
|
-
runImmediately: true,
|
|
1962
|
-
allowUnsyncedReads: true,
|
|
1963
|
-
canWrite: true,
|
|
1964
|
-
temporary: true,
|
|
1965
|
-
onResultUpdated: r => {
|
|
1966
|
-
result = r;
|
|
1967
|
-
}
|
|
1968
|
-
});
|
|
1969
|
-
if (!result) {
|
|
1970
|
-
throw new Error("Expected result to be set");
|
|
1971
|
-
}
|
|
1972
|
-
if ("error" in result) {
|
|
1973
|
-
throw errorify(result.error);
|
|
1974
|
-
}
|
|
1975
|
-
if (watcher.lastUnsyncedAccesses.size > 0 || watcher.lastUnsyncedParentAccesses.size > 0) {
|
|
1976
|
-
//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);
|
|
1977
|
-
}
|
|
1978
|
-
return result.result as Result;
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
/** Runs the function until all reads are synced, then disposes the watcher and returns
|
|
1982
|
-
* - Can be used just with readers, but is most useful for writers (as the
|
|
1983
|
-
* absolutely soonest we can safely write is when all reads are synced)
|
|
1984
|
-
* - "writeOnly" is a dangerous version of this, which doesn't wait, and instead
|
|
1985
|
-
* just writes immediately (but in exchange, it cannot read).
|
|
1986
|
-
* TODO: Find a way to reuse watchers with different functions. For simple functions this can be
|
|
1987
|
-
* about 4X faster. It is more difficult if the function has to wait to sync values,
|
|
1988
|
-
* but we can probably still reuse watchers in some way.
|
|
1989
|
-
* - The "static" option might help with this.
|
|
1990
|
-
*/
|
|
1991
|
-
public async commitFunction<Result = void>(
|
|
1992
|
-
options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">,
|
|
1993
|
-
config?: {
|
|
1994
|
-
onWritesCommitted?: (writes: PathValue[]) => void;
|
|
1995
|
-
}
|
|
1996
|
-
): Promise<Result> {
|
|
1997
|
-
options = {
|
|
1998
|
-
canWrite: true,
|
|
1999
|
-
waitForIncompleteTransactions: true,
|
|
2000
|
-
...options,
|
|
2001
|
-
};
|
|
2002
|
-
|
|
2003
|
-
if (this.runningWatcher?.options.inlineNestedWatchers) {
|
|
2004
|
-
this.createWatcher(options);
|
|
2005
|
-
return "Nested commitFunctions cannot return values" as any;
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
let onResult!: (result: Result) => void;
|
|
2009
|
-
let onError!: (error: Error) => void;
|
|
2010
|
-
let resultPromise = new Promise<Result>((resolve, reject) => { onResult = resolve; onError = reject; });
|
|
2011
|
-
let anyWrites = false;
|
|
2012
|
-
this.createWatcher({
|
|
2013
|
-
...options,
|
|
2014
|
-
temporary: true,
|
|
2015
|
-
onResultUpdated: (result, writes, watcher) => {
|
|
2016
|
-
if (writes?.length) {
|
|
2017
|
-
anyWrites = true;
|
|
2018
|
-
}
|
|
2019
|
-
watcher.dispose();
|
|
2020
|
-
if ("error" in result) {
|
|
2021
|
-
onError(errorify(result.error));
|
|
2022
|
-
} else {
|
|
2023
|
-
onResult(result.result);
|
|
2024
|
-
}
|
|
2025
|
-
if (writes && config?.onWritesCommitted) {
|
|
2026
|
-
config.onWritesCommitted(writes);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
});
|
|
2030
|
-
let result = await resultPromise;
|
|
2031
|
-
// NOTE: Now we ALWAYS wait, to prevent services from terminating their process before their writes finish.
|
|
2032
|
-
// It is just too hard to remember to call pathValueCommitter.waitForValuesToCommit in every service.
|
|
2033
|
-
if (options.canWrite && anyWrites && !options.dryRun && !options.noWaitForCommit) {
|
|
2034
|
-
await pathValueCommitter.waitForValuesToCommit();
|
|
2035
|
-
}
|
|
2036
|
-
return result;
|
|
2037
|
-
}
|
|
2038
|
-
/** Run the same as usual, but instead of committing writes, returns them. */
|
|
2039
|
-
public async dryRun(
|
|
2040
|
-
options: Omit<WatcherOptions<any>, "onResultUpdated" | "onWriteCommitted">
|
|
2041
|
-
): Promise<PathValue[] | undefined> {
|
|
2042
|
-
let result = await this.dryRunFull(options);
|
|
2043
|
-
return result.writes;
|
|
2044
|
-
}
|
|
2045
|
-
/** Run the same as usual, but instead of committing writes, returns them. */
|
|
2046
|
-
public async dryRunFull(
|
|
2047
|
-
options: Omit<WatcherOptions<any>, "onResultUpdated" | "onWriteCommitted">
|
|
2048
|
-
): Promise<DryRunResult> {
|
|
2049
|
-
type Result = {
|
|
2050
|
-
writes: PathValue[];
|
|
2051
|
-
readPaths: Set<string>
|
|
2052
|
-
readParentPaths: Set<string>;
|
|
2053
|
-
result: unknown;
|
|
2054
|
-
};
|
|
2055
|
-
let onResult!: (result: Result) => void;
|
|
2056
|
-
let onError!: (error: Error) => void;
|
|
2057
|
-
let resultPromise = new Promise<Result>((resolve, reject) => { onResult = resolve; onError = reject; });
|
|
2058
|
-
let watcher = this.createWatcher({
|
|
2059
|
-
...options,
|
|
2060
|
-
canWrite: true,
|
|
2061
|
-
dryRun: true,
|
|
2062
|
-
temporary: true,
|
|
2063
|
-
onResultUpdated: (result, values, watcher) => {
|
|
2064
|
-
watcher.dispose();
|
|
2065
|
-
if ("error" in result) {
|
|
2066
|
-
onError(errorify(result.error));
|
|
2067
|
-
} else {
|
|
2068
|
-
onResult({
|
|
2069
|
-
writes: values ?? [],
|
|
2070
|
-
readPaths: new Set(watcher.pendingWatches.paths),
|
|
2071
|
-
readParentPaths: new Set(watcher.pendingWatches.parentPaths),
|
|
2072
|
-
result: result.result
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
});
|
|
2077
|
-
let result = await resultPromise;
|
|
2078
|
-
return result;
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
private allWatchers = registerResource("paths|proxyWatcher.allWatchers", new Set<SyncWatcher>());
|
|
2083
|
-
private allWatchersLookup = registerResource("paths|proxyWatcher.allWatchersLookup", new Map<Function, SyncWatcher>());
|
|
2084
|
-
public getWatcherForTrigger(trigger: Function): SyncWatcher | undefined {
|
|
2085
|
-
return this.allWatchersLookup.get(trigger);
|
|
2086
|
-
}
|
|
2087
|
-
public getAllWatchers(): Set<SyncWatcher> {
|
|
2088
|
-
return this.allWatchers;
|
|
2089
|
-
}
|
|
2090
|
-
public inWatcher(): boolean {
|
|
2091
|
-
return !!this.runningWatcher;
|
|
2092
|
-
}
|
|
2093
|
-
/** @deprecated try not to call getTriggeredWatcher, and instead try to call Querysub helper
|
|
2094
|
-
* functions. getTriggeredWatcher exposes too much of our interface, which we need
|
|
2095
|
-
* to abstact out when we rewrite proxyWatcher.
|
|
2096
|
-
*/
|
|
2097
|
-
public getTriggeredWatcher(): SyncWatcher {
|
|
2098
|
-
let watcher = this.runningWatcher;
|
|
2099
|
-
if (!watcher) {
|
|
2100
|
-
throw new Error("Not in a watcher");
|
|
2101
|
-
}
|
|
2102
|
-
return watcher;
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
public isAllSynced(config?: {
|
|
2106
|
-
ignoreAlwaysCommitAllRunsFlag?: boolean;
|
|
2107
|
-
}) {
|
|
2108
|
-
if (!config?.ignoreAlwaysCommitAllRunsFlag && this.runningWatcher?.options.commitAllRuns) {
|
|
2109
|
-
return true;
|
|
2110
|
-
}
|
|
2111
|
-
return !this.getTriggeredWatcherMaybeUndefined()?.hasAnyUnsyncedAccesses();
|
|
2112
|
-
}
|
|
2113
|
-
/** @deprecated try not to call getTriggeredWatcherMaybeUndefined, and instead try to call Querysub helper
|
|
2114
|
-
* functions. getTriggeredWatcherMaybeUndefined exposes too much of our interface, which we need
|
|
2115
|
-
* to abstact out when we rewrite proxyWatcher.
|
|
2116
|
-
*/
|
|
2117
|
-
public getTriggeredWatcherMaybeUndefined(): SyncWatcher | undefined {
|
|
2118
|
-
return this.runningWatcher;
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
/** Watches everything we were watching last watch. Is essential for efficient watching code,
|
|
2122
|
-
* which needs access to a lot of state, but doesn't want to process the entire state
|
|
2123
|
-
* every time any value changes.
|
|
2124
|
-
*/
|
|
2125
|
-
public reuseLastWatches() {
|
|
2126
|
-
let watcher = this.getTriggeredWatcher();
|
|
2127
|
-
for (let path of watcher.lastUnsyncedAccesses) {
|
|
2128
|
-
if (!authorityStorage.isSynced(path)) {
|
|
2129
|
-
watcher.pendingUnsyncedAccesses.add(path);
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
2133
|
-
if (!authorityStorage.isParentSynced(path)) {
|
|
2134
|
-
watcher.pendingUnsyncedParentAccesses.add(path);
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
function addToSet(set1: Set<string>, set2: Set<string>) {
|
|
2140
|
-
let base = set2;
|
|
2141
|
-
for (let path of set1) {
|
|
2142
|
-
base.add(path);
|
|
2143
|
-
}
|
|
2144
|
-
return base;
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
// NOTE: If we don't clone, then pathValueClientWatcher cannot detect changes, and we
|
|
2148
|
-
// end up staying watched on everything!
|
|
2149
|
-
// - We should really fix this though, as if we can avoid this clone, we can have really efficient
|
|
2150
|
-
// incremental watches.
|
|
2151
|
-
watcher.pendingWatches.paths = addToSet(new Set(watcher.lastWatches.paths), watcher.pendingWatches.paths);
|
|
2152
|
-
watcher.pendingWatches.parentPaths = addToSet(new Set(watcher.lastWatches.parentPaths), watcher.pendingWatches.parentPaths);
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
/** NOTE: This explicitly causes the watcher to be unsynced until function
|
|
2156
|
-
* can be evaluated without any pending promises. This makes this significantly
|
|
2157
|
-
* more powerful than waiting on localState.
|
|
2158
|
-
* NOTE: IF you don't want to cause the caller to be unsynced, use a local schema instead.
|
|
2159
|
-
* This allows callers to see what path triggered them, which is often more informative
|
|
2160
|
-
* than waitReason. It also exposes the state to any synced diagnostics tools.
|
|
2161
|
-
*/
|
|
2162
|
-
public triggerOnPromiseFinish(
|
|
2163
|
-
promise: MaybePromise<unknown>,
|
|
2164
|
-
config: {
|
|
2165
|
-
// For example, "waitForModuleToLoad"
|
|
2166
|
-
waitReason: string;
|
|
2167
|
-
// Doesn't cause us to be unsynced, instead just triggering the watcher
|
|
2168
|
-
// when the promise resolves (or rejects).
|
|
2169
|
-
noWait?: boolean;
|
|
2170
|
-
}
|
|
2171
|
-
) {
|
|
2172
|
-
// This needs to mark the watcher as unsynced
|
|
2173
|
-
if (typeof promise === "object" && promise && promise instanceof Promise) {
|
|
2174
|
-
let watcher = this.getTriggeredWatcher();
|
|
2175
|
-
if (!config.noWait) {
|
|
2176
|
-
watcher.specialPromiseUnsynced = true;
|
|
2177
|
-
}
|
|
2178
|
-
watcher.lastSpecialPromiseUnsyncedReason = config.waitReason;
|
|
2179
|
-
void promise.finally(() => {
|
|
2180
|
-
watcher.explicitlyTrigger({
|
|
2181
|
-
paths: new Set(),
|
|
2182
|
-
newParentsSynced: new Set(),
|
|
2183
|
-
pathSources: new Set(),
|
|
2184
|
-
extraReasons: [getPathStr2(`Promise`, config.waitReason || "")]
|
|
2185
|
-
});
|
|
2186
|
-
});
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
public setEventPath(getValue: () => unknown) {
|
|
2191
|
-
let path = getProxyPath(getValue);
|
|
2192
|
-
let watcher = this.getTriggeredWatcher();
|
|
2193
|
-
watcher.pendingEventWrites.add(path);
|
|
2194
|
-
}
|
|
2195
|
-
public unwatchEventPaths(watcher: SyncWatcher, paths: Set<string>) {
|
|
2196
|
-
for (let path of paths) {
|
|
2197
|
-
watcher.lastWatches.paths.delete(path);
|
|
2198
|
-
watcher.lastWatches.parentPaths.delete(path);
|
|
2199
|
-
watcher.pendingWatches.paths.delete(path);
|
|
2200
|
-
watcher.pendingWatches.parentPaths.delete(path);
|
|
2201
|
-
watcher.pendingUnsyncedAccesses.delete(path);
|
|
2202
|
-
watcher.pendingUnsyncedParentAccesses.delete(path);
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
public debug_breakOnWrite(proxy: string | unknown | (() => unknown)) {
|
|
2207
|
-
let path = this.getPathAndWatch(proxy);
|
|
2208
|
-
if (!path) {
|
|
2209
|
-
console.error(`Value is not a proxy, and so cannot get path. Either pass a proxy, or a getter for a value.`);
|
|
2210
|
-
return;
|
|
2211
|
-
}
|
|
2212
|
-
PathValueProxyWatcher.BREAK_ON_WRITES.add(path);
|
|
2213
|
-
}
|
|
2214
|
-
public debug_logOnWrite(proxy: unknown | (() => unknown)) {
|
|
2215
|
-
let path = this.getPathAndWatch(proxy);
|
|
2216
|
-
if (!path) {
|
|
2217
|
-
console.error(`Value is not a proxy, and so cannot get path. Either pass a proxy, or a getter for a value.`);
|
|
2218
|
-
return;
|
|
2219
|
-
}
|
|
2220
|
-
PathValueProxyWatcher.LOG_WRITES_INCLUDES.add(path);
|
|
2221
|
-
}
|
|
2222
|
-
public getPathNoWatch(proxy: string | unknown | (() => unknown)) {
|
|
2223
|
-
return typeof proxy === "function" ? getProxyPath(proxy as any) : getPathFromProxy(proxy);
|
|
2224
|
-
}
|
|
2225
|
-
public getPathAndWatch(proxy: string | unknown | (() => unknown)) {
|
|
2226
|
-
if (typeof proxy === "string") {
|
|
2227
|
-
return proxy;
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
// Prefer to call the function, as if the result is a proxy we want the path of that,
|
|
2231
|
-
// instead of the path the proxy is inside of!
|
|
2232
|
-
if (typeof proxy === "function") {
|
|
2233
|
-
return getPathFromProxy(proxy()) || getProxyPathAndWatch(proxy as any);
|
|
2234
|
-
}
|
|
2235
|
-
return getPathFromProxy(proxy);
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
public ignoreWatches<T>(code: () => T) {
|
|
2239
|
-
return doProxyOptions({ noSyncing: true }, code);
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
const PAINT_NOOP_FUNCTION = () => { };
|
|
2244
|
-
let pausedUntilNextPaint = new Map<string, () => void>();
|
|
2245
|
-
|
|
2246
|
-
// gitHash => domainName => moduleId => schema
|
|
2247
|
-
let schemas = new Map<string, Map<string, Map<string, Schema2>>>();
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
// NOTE: We hardcode knowledge of how module data is nested to handle schemas. This... isn't great,
|
|
2251
|
-
// but it makes the code faster, and if we change how module data is nested it will break
|
|
2252
|
-
// all existing data anyways, so... I don't think we ever will.
|
|
2253
|
-
// Path pattern is: [domain, "PathFunctionRunner", moduleId, "Data"]
|
|
2254
|
-
export const getMatchingSchema = cacheLimited(1000, function getMatchingSchema(pathStr: string): {
|
|
2255
|
-
schema: Schema2;
|
|
2256
|
-
nestedPath: string[];
|
|
2257
|
-
} | undefined {
|
|
2258
|
-
if (_noAtomicSchema) return undefined;
|
|
2259
|
-
let base = getBaseMatchingSchema(pathStr);
|
|
2260
|
-
if (base) return base;
|
|
2261
|
-
let prefix = getPrefixMatchingSchema(pathStr);
|
|
2262
|
-
if (prefix) return prefix;
|
|
2263
|
-
return undefined;
|
|
2264
|
-
});
|
|
2265
|
-
function getBaseMatchingSchema(pathStr: string): {
|
|
2266
|
-
schema: Schema2;
|
|
2267
|
-
nestedPath: string[];
|
|
2268
|
-
} | undefined {
|
|
2269
|
-
let schemaForHash = (
|
|
2270
|
-
schemas.get(getCurrentCallObj()?.fnc.gitRef || "ambient")
|
|
2271
|
-
// Default to the ambient schema for clientside calls.
|
|
2272
|
-
|| schemas.get("ambient")
|
|
2273
|
-
);
|
|
2274
|
-
|
|
2275
|
-
if (_noAtomicSchema) return undefined;
|
|
2276
|
-
if (getPathIndex(pathStr, 3) !== "Data") return undefined;
|
|
2277
|
-
let domain = getPathIndex(pathStr, 0)!;
|
|
2278
|
-
let moduleId = getPathIndex(pathStr, 2)!;
|
|
2279
|
-
let schema = schemaForHash?.get(domain)?.get(moduleId);
|
|
2280
|
-
if (!schema) return undefined;
|
|
2281
|
-
let nestedPathStr = getPathSuffix(pathStr, 4);
|
|
2282
|
-
let nestedPath = getPathFromStr(nestedPathStr);
|
|
2283
|
-
return { schema, nestedPath };
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
export function registerSchema(config: {
|
|
2287
|
-
schema: Schema2;
|
|
2288
|
-
domainName: string;
|
|
2289
|
-
moduleId: string;
|
|
2290
|
-
gitHash: string;
|
|
2291
|
-
}) {
|
|
2292
|
-
getMatchingSchema.clear();
|
|
2293
|
-
|
|
2294
|
-
const { schema, domainName, moduleId, gitHash } = config;
|
|
2295
|
-
let gitSchemas = schemas.get(gitHash);
|
|
2296
|
-
if (!gitSchemas) {
|
|
2297
|
-
schemas.set(gitHash, gitSchemas = new Map());
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
let domainSchemas = gitSchemas.get(domainName);
|
|
2301
|
-
if (!domainSchemas) {
|
|
2302
|
-
gitSchemas.set(domainName, domainSchemas = new Map());
|
|
2303
|
-
}
|
|
2304
|
-
domainSchemas.set(moduleId, schema);
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
export let schemaPrefixes: { len: number; prefixes: Map<string, Schema2> }[] = [];
|
|
2308
|
-
export function registerSchemaPrefix(config: {
|
|
2309
|
-
schema: Schema2;
|
|
2310
|
-
prefixPathStr: string;
|
|
2311
|
-
}) {
|
|
2312
|
-
let len = getPathDepth(config.prefixPathStr);
|
|
2313
|
-
let list = schemaPrefixes.find(x => x.len === len);
|
|
2314
|
-
if (!list) {
|
|
2315
|
-
schemaPrefixes.push(list = { len, prefixes: new Map() });
|
|
2316
|
-
}
|
|
2317
|
-
if (isNode() && !globalThis.isHotReloading?.() && !isDynamicallyLoading()) {
|
|
2318
|
-
if (list.prefixes.has(config.prefixPathStr)) {
|
|
2319
|
-
throw new Error(`Prefix matches only work due to only having one version of the code ever load (except for during hot reloading development clientside). If we try to render multiple versions of the same code serverside (ex, because we pre-loaded new code during a deploy), it will break, as we can't distinguish them. ADD GIT HASH TO PREFIX SCHEMAS TO FIX THIS! Tried to double load ${config.prefixPathStr}`);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
list.prefixes.set(config.prefixPathStr, config.schema);
|
|
2323
|
-
}
|
|
2324
|
-
export function unregisterSchemaPrefix(config: {
|
|
2325
|
-
schema: Schema2;
|
|
2326
|
-
prefixPathStr: string;
|
|
2327
|
-
}) {
|
|
2328
|
-
let len = getPathDepth(config.prefixPathStr);
|
|
2329
|
-
let list = schemaPrefixes.find(x => x.len === len);
|
|
2330
|
-
if (!list) return;
|
|
2331
|
-
list.prefixes.delete(config.prefixPathStr);
|
|
2332
|
-
}
|
|
2333
|
-
function getPrefixMatchingSchema(pathStr: string): {
|
|
2334
|
-
schema: Schema2;
|
|
2335
|
-
nestedPath: string[];
|
|
2336
|
-
} | undefined {
|
|
2337
|
-
let len = getPathDepth(pathStr);
|
|
2338
|
-
for (let list of schemaPrefixes) {
|
|
2339
|
-
if (len < list.len) continue;
|
|
2340
|
-
let prefix = getPathPrefix(pathStr, list.len);
|
|
2341
|
-
let schema = list.prefixes.get(prefix);
|
|
2342
|
-
if (schema) {
|
|
2343
|
-
let nestedPath = getPathFromStr(getPathSuffix(pathStr, list.len));
|
|
2344
|
-
return { schema, nestedPath };
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
return undefined;
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
let _noAtomicSchema = false;
|
|
2352
|
-
export function noAtomicSchema<T>(code: () => T) {
|
|
2353
|
-
let prev = _noAtomicSchema;
|
|
2354
|
-
_noAtomicSchema = true;
|
|
2355
|
-
try {
|
|
2356
|
-
return code();
|
|
2357
|
-
} finally {
|
|
2358
|
-
_noAtomicSchema = prev;
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
export const proxyWatcher = new PathValueProxyWatcher();
|
|
2364
|
-
if ((globalThis as any).proxyWatcher) {
|
|
2365
|
-
debugger;
|
|
2366
|
-
console.error(`Loaded PathValueProxyWatcher twice. This will break this. In ${module.filename}`);
|
|
2367
|
-
}
|
|
2368
|
-
(globalThis as any).proxyWatcher = proxyWatcher;
|
|
2369
|
-
(globalThis as any).ProxyWatcher = PathValueProxyWatcher;
|
|
2370
|
-
// Probably the most useful debug function for debugging watch based functions.
|
|
2371
|
-
(globalThis as any).getCurrentTriggers = function () {
|
|
2372
|
-
return proxyWatcher.getTriggeredWatcher().triggeredByChanges;
|
|
2373
|
-
};
|
|
2374
|
-
(globalThis as any).getAllWatchers = function () {
|
|
2375
|
-
return proxyWatcher.getAllWatchers();
|
|
2376
|
-
};
|
|
2377
|
-
|
|
2378
|
-
registerPeriodic(function checkForZombieWatches() {
|
|
2379
|
-
let now = Date.now();
|
|
2380
|
-
const threshold = 30 * 1000;
|
|
2381
|
-
function isZombieWatch(watcher: SyncWatcher) {
|
|
2382
|
-
if (watcher.options.static) return false;
|
|
2383
|
-
if (!watcher.hasAnyUnsyncedAccesses()) return false;
|
|
2384
|
-
let timeSinceLastSync = now - watcher.lastSyncTime;
|
|
2385
|
-
if (timeSinceLastSync < threshold) return false;
|
|
2386
|
-
return true;
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
let zombies = Array.from(proxyWatcher.getAllWatchers()).filter(isZombieWatch);
|
|
2390
|
-
lastZombieCount = zombies.length;
|
|
2391
|
-
if (zombies.length > 0) {
|
|
2392
|
-
let namesSet = new Set(zombies.map(x => x.debugName));
|
|
2393
|
-
console.warn(`Found ${zombies.length} zombie watchers`, { names: Array.from(namesSet) });
|
|
2394
|
-
}
|
|
2395
|
-
});
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
let evaluations = 0;
|
|
2399
|
-
let wastedEvaluations = 0;
|
|
2400
|
-
|
|
2401
|
-
registerMeasureInfo(() => {
|
|
2402
|
-
if (evaluations === 0) return undefined;
|
|
2403
|
-
let current = wastedEvaluations / evaluations;
|
|
2404
|
-
wastedEvaluations = 0;
|
|
2405
|
-
evaluations = 0;
|
|
2406
|
-
return `${formatPercent(current)} NOOP triggers`;
|
|
2407
|
-
});
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
let triggerCount = 0;
|
|
2411
|
-
let triggersDropped = 0;
|
|
2412
|
-
registerMeasureInfo(() => {
|
|
2413
|
-
if (triggerCount === 0) return undefined;
|
|
2414
|
-
let current = triggersDropped / triggerCount;
|
|
2415
|
-
triggersDropped = 0;
|
|
2416
|
-
triggerCount = 0;
|
|
2417
|
-
return `${formatPercent(current)} paint dropped triggers`;
|
|
2418
|
-
});
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
// #region Proxy Ordering
|
|
2424
|
-
let proxiesOrdered: SyncWatcher[] = [];
|
|
2425
|
-
function getProxyOrder(proxy: SyncWatcher): string | undefined {
|
|
2426
|
-
// We can't wait if we're running immediately.
|
|
2427
|
-
if (proxy.options.runImmediately) return undefined;
|
|
2428
|
-
|
|
2429
|
-
let value = proxy.options.finishInStartOrder;
|
|
2430
|
-
|
|
2431
|
-
if (value === false) return undefined;
|
|
2432
|
-
if (value === true) {
|
|
2433
|
-
value = proxy.createTime;
|
|
2434
|
-
}
|
|
2435
|
-
if (value === undefined) return undefined;
|
|
2436
|
-
return getPathStr2(value.toFixed(10).padStart(30, "0") + "", proxy.seqNum + "");
|
|
2437
|
-
}
|
|
2438
|
-
function addToProxyOrder(proxy: SyncWatcher) {
|
|
2439
|
-
let order = getProxyOrder(proxy);
|
|
2440
|
-
if (order === undefined) return;
|
|
2441
|
-
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2442
|
-
if (index >= 0) {
|
|
2443
|
-
throw new Error(`Proxy ${proxy.debugName} is already in the proxy order at index ${index}? This shouldn't be possible, as the sequence number should make it unique. `);
|
|
2444
|
-
}
|
|
2445
|
-
insertIntoSortedList(proxiesOrdered, x => getProxyOrder(x) || 0, proxy);
|
|
2446
|
-
setTimeout(() => {
|
|
2447
|
-
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2448
|
-
if (index >= 0) {
|
|
2449
|
-
if (proxiesOrdered.length > (index + 1)) {
|
|
2450
|
-
// Only warn if there's actually proxies after us.
|
|
2451
|
-
console.warn(`Allowing out-of-order proxy finishing due to long-running proxy not resolving. Not resolving: ${proxy.debugName} timed out after ${formatTime(MAX_PROXY_REORDER_BLOCK_TIME)}`);
|
|
2452
|
-
}
|
|
2453
|
-
void finishProxyAndTriggerNext(proxy, true);
|
|
2454
|
-
}
|
|
2455
|
-
}, MAX_PROXY_REORDER_BLOCK_TIME);
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
// - This should be fine though, because if we're doing this for an async call, it means they're already expecting the result back within a promise. So waiting another promise shouldn't mess up the ordering.
|
|
2459
|
-
const finishProxyAndTriggerNext = runInSerial(
|
|
2460
|
-
async function finishProxyAndTriggerNext(proxy: SyncWatcher, fromTimeout: boolean = false) {
|
|
2461
|
-
let order = getProxyOrder(proxy);
|
|
2462
|
-
if (order === undefined) return;
|
|
2463
|
-
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2464
|
-
if (index < 0) {
|
|
2465
|
-
console.warn(`Proxy ${proxy.debugName} is not in the proxy order at index ${index}? This shouldn't be possible, don't we only dispose it once? `);
|
|
2466
|
-
return;
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
// NOTE: We have to wait to the next tick, as finish order code is most often on for async functions, in which case, even though we will have called onResultUpdated, the caller's waiting on a promise, so we need to give it a tick to actually receive the value.
|
|
2470
|
-
// - This shouldn't cause any actual slowdown because people are calling the functions and usually awaiting them, so they won't notice a difference.
|
|
2471
|
-
// HACK: We wait 10 times because I guess there's a lot of change promises. I found five works, but adding more is fine. This is pretty bad, but... I don't think there's anything that can possibly be expecting an ordered function to finish synchronously. Anything that is waiting on the ordered function is going to actually be waiting with an await, so we could iterate 100 times here and it wouldn't break anything.
|
|
2472
|
-
for (let i = 0; i < 10; i++) {
|
|
2473
|
-
await Promise.resolve();
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
proxiesOrdered.splice(index, 1);
|
|
2477
|
-
let next = proxiesOrdered[index];
|
|
2478
|
-
// Only trigger if it's the first entry and therefore it won't be blocked by the previous proxy, or, of course, if it's from a timeout, In which case, it won't be blocked anyway.
|
|
2479
|
-
if (next && (index === 0 || fromTimeout)) {
|
|
2480
|
-
//console.info(`Triggering next proxy in order: ${next.debugName}`, next.options.baseFunction || next.options.watchFunction);
|
|
2481
|
-
next.explicitlyTrigger({
|
|
2482
|
-
newParentsSynced: new Set(),
|
|
2483
|
-
pathSources: new Set(),
|
|
2484
|
-
paths: new Set(),
|
|
2485
|
-
extraReasons: [`Delayed due to proxy order enforcement. Previous proxy finished, so we can now finish. Previous was ${proxy.debugName}`],
|
|
2486
|
-
});
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
);
|
|
2490
|
-
export function isProxyBlockedByOrder(proxy: SyncWatcher): boolean {
|
|
2491
|
-
let order = getProxyOrder(proxy);
|
|
2492
|
-
if (order === undefined) return false;
|
|
2493
|
-
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2494
|
-
if (index <= 0) return false;
|
|
2495
|
-
return true;
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
let currentCallCreationProxy: SyncWatcher | undefined = undefined;
|
|
2499
|
-
function doCallCreation(proxy: SyncWatcher, code: () => void) {
|
|
2500
|
-
let prev = currentCallCreationProxy;
|
|
2501
|
-
currentCallCreationProxy = proxy;
|
|
2502
|
-
try {
|
|
2503
|
-
return code();
|
|
2504
|
-
} finally {
|
|
2505
|
-
currentCallCreationProxy = prev;
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
export function debug_getQueueOrder() {
|
|
2509
|
-
return proxiesOrdered;
|
|
2510
|
-
}
|
|
2511
|
-
export function getCurrentCallCreationProxy() {
|
|
2512
|
-
return currentCallCreationProxy;
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
registerPeriodic(function logWatcherTypes() {
|
|
2517
|
-
let allWatchers = proxyWatcher.getAllWatchers();
|
|
2518
|
-
let counts = new Map<string, { count: number; }>();
|
|
2519
|
-
for (let watcher of allWatchers) {
|
|
2520
|
-
let type = watcher.debugName.split("|")[0] || "no debug name";
|
|
2521
|
-
let countObj = counts.get(type);
|
|
2522
|
-
if (!countObj) {
|
|
2523
|
-
countObj = { count: 0 };
|
|
2524
|
-
counts.set(type, countObj);
|
|
2525
|
-
}
|
|
2526
|
-
countObj.count++;
|
|
2527
|
-
}
|
|
2528
|
-
let sorted = sort(Array.from(counts.entries()), x => -x[1].count);
|
|
2529
|
-
|
|
2530
|
-
console.log(`${formatNumber(allWatchers.size)} watchers | ${formatNumber(sorted.length)} types | remember, components have 3 watchers`);
|
|
2531
|
-
// Log the top 5
|
|
2532
|
-
for (let [type, countObj] of sorted.slice(0, 5)) {
|
|
2533
|
-
let countText = `${formatPercent(countObj.count / allWatchers.size)} (${formatNumber(countObj.count)})`;
|
|
2534
|
-
console.log(` ${countText.padEnd(15, " ")} ${type}`);
|
|
2535
|
-
}
|
|
2536
|
-
});
|
|
2537
|
-
|
|
2538
|
-
// #endregion Proxy Ordering
|
|
2539
|
-
|
|
2540
|
-
import { inlineNestedCalls } from "../3-path-functions/syncSchema"; import { LOCAL_DOMAIN_PATH } from "../0-path-value-core/PathRouter";
|
|
2541
|
-
import { waitIfReceivedIncompleteTransaction } from "./TransactionDelayer";
|
|
2542
|
-
|
|
1
|
+
import { measureCode, measureWrap, registerMeasureInfo } from "socket-function/src/profiling/measure";
|
|
2
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
3
|
+
import { binarySearchBasic, binarySearchBasic2, binarySearchIndex, deepCloneJSON, getKeys, insertIntoSortedList, isNode, recursiveFreeze, sort, timeInMinute, timeInSecond } 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 { cache, cacheLimited, lazy } from "socket-function/src/caching";
|
|
7
|
+
import { delay, runInSerial, 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, getPathFromProxy, getProxyPath, getProxyPathAndWatch, isValueProxy, isValueProxy2 } from "./pathValueProxy";
|
|
16
|
+
import { authorityStorage, compareTime, ReadLock, epochTime, getNextTime, MAX_ACCEPTED_CHANGE_AGE, PathValue, Time, getCreatorId, getTimeUnique } from "../0-path-value-core/pathValueCore";
|
|
17
|
+
import { runCodeWithDatabase, rawSchema } from "./pathDatabaseProxyBase";
|
|
18
|
+
import debugbreak from "debugbreak";
|
|
19
|
+
import { pathValueCommitter } from "../0-path-value-core/PathValueController";
|
|
20
|
+
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
21
|
+
import { registerPeriodic } from "../diagnostics/periodic";
|
|
22
|
+
import { remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
23
|
+
import { Schema2, Schema2Fncs } from "./schema2";
|
|
24
|
+
import { devDebugbreak, getDomain, isDynamicallyLoading, isPublic } from "../config";
|
|
25
|
+
|
|
26
|
+
import type { CallSpec } from "../3-path-functions/PathFunctionRunner";
|
|
27
|
+
import type { FunctionMetadata } from "../3-path-functions/syncSchema";
|
|
28
|
+
|
|
29
|
+
import { DEPTH_TO_DATA, MODULE_INDEX, getCurrentCall, getCurrentCallObj } from "../3-path-functions/PathFunctionRunner";
|
|
30
|
+
import { interceptCallsBase, runCall } from "../3-path-functions/PathFunctionHelpers";
|
|
31
|
+
import { deepCloneCborx } from "../misc/cloneHelpers";
|
|
32
|
+
import { formatNumber, formatPercent, formatTime } from "socket-function/src/formatting/format";
|
|
33
|
+
import { addStatPeriodic, interceptCalls, onAllPredictionsFinished, onTimeProfile } from "../-0-hooks/hooks";
|
|
34
|
+
import { onNextPaint } from "../functional/onNextPaint";
|
|
35
|
+
import { isAsyncFunction } from "../misc";
|
|
36
|
+
import { isClient } from "../config2";
|
|
37
|
+
|
|
38
|
+
// TODO: Break this into two parts:
|
|
39
|
+
// 1) Run and get accesses
|
|
40
|
+
// 2) Commit/watch/unwatch
|
|
41
|
+
// With a harness that joins the two parts in a loop (mostly powered by clientWatcher,
|
|
42
|
+
// which can run the trigger loop).
|
|
43
|
+
|
|
44
|
+
const DEFAULT_MAX_LOCKS = 1000;
|
|
45
|
+
|
|
46
|
+
// After this time we allow proxies to be reordered, even if there's flags that tell them not to be.
|
|
47
|
+
const MAX_PROXY_REORDER_BLOCK_TIME = timeInSecond * 10;
|
|
48
|
+
|
|
49
|
+
let nextSeqNum = 1;
|
|
50
|
+
let nextOrderSeqNum = 1;
|
|
51
|
+
|
|
52
|
+
export interface WatcherOptions<Result> {
|
|
53
|
+
/** NOTE: If you try to run too far in the past, an error will occur.
|
|
54
|
+
* - This both sets the read time, and write time.
|
|
55
|
+
* - setCurrentReadTime can also set the read time (but runAtTime is the only
|
|
56
|
+
* way to set the write time).
|
|
57
|
+
*/
|
|
58
|
+
runAtTime?: Time;
|
|
59
|
+
|
|
60
|
+
/** NOTE: Write values are only committed after we have all reads synced. */
|
|
61
|
+
canWrite?: boolean;
|
|
62
|
+
|
|
63
|
+
/** ONLY allowed if the writes and reads are all local (throws if any non-local accesses are attempted).
|
|
64
|
+
* - Stops lock creation. This might increase speed?
|
|
65
|
+
*/
|
|
66
|
+
noLocks?: boolean;
|
|
67
|
+
/** No locks, without the local-only restriction. A lot more unsafe. Don't use for any writes
|
|
68
|
+
* that will eventually be committed (we only use it in prediction code, and GC). */
|
|
69
|
+
unsafeNoLocks?: boolean;
|
|
70
|
+
|
|
71
|
+
/** Causes us to read without counting as reading, so we don't sync any values, or
|
|
72
|
+
* add them to our locks. Usually only set temporarily inside of a watcher,
|
|
73
|
+
* so it can read sync values in order to write non-synced side-effects, without
|
|
74
|
+
* having to rerun if those synced values change (as the non-synced side-effects won't
|
|
75
|
+
* be undone, so rerunning wouldn't matter).
|
|
76
|
+
* - qreact uses this for component traversal to set disposing flags, as disposing flags
|
|
77
|
+
* are non-synced, and just for optimization, so there's no reason to watch the component
|
|
78
|
+
* hierarchy when changing them.
|
|
79
|
+
*/
|
|
80
|
+
noSyncing?: boolean;
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/** Doesn't allow reading any values, but... also causes allows the function to have
|
|
84
|
+
* no readLocks! (so it can't be rejected)
|
|
85
|
+
* - Still allows reading back your own writes though...
|
|
86
|
+
* - Doesn't write object parents, which breaks Object.getKeys(), unless you use
|
|
87
|
+
* atomicObjectWrite.
|
|
88
|
+
* - Usually set with runImmediately
|
|
89
|
+
*/
|
|
90
|
+
writeOnly?: boolean;
|
|
91
|
+
|
|
92
|
+
/** Ignore currentReadTime, and just read the latest. For use with doProxyOptions. */
|
|
93
|
+
forceReadLatest?: boolean;
|
|
94
|
+
|
|
95
|
+
/** If you are writing values you KNOW you won't reading back, set this flag to
|
|
96
|
+
* prevent caching of the writes clientside (which is a lot more efficient).
|
|
97
|
+
* - More for testing purposes, as the performance benefit is basically negligible,
|
|
98
|
+
* and the memory impact is unlikely to be an issue unless you are writing huge values.
|
|
99
|
+
*/
|
|
100
|
+
doNotStoreWritesAsPredictions?: boolean;
|
|
101
|
+
|
|
102
|
+
/** See PathValue.event */
|
|
103
|
+
eventWrite?: boolean;
|
|
104
|
+
|
|
105
|
+
/** By default we read values before we write to them, and don't write if the value
|
|
106
|
+
* would be `===`. This is usually preferred, because unneeded writes are a lot
|
|
107
|
+
* more expensive than unneeded reads. However, if you are writing a lot, and you
|
|
108
|
+
* know the values will be different, you can set this to reduce the amount of reads.
|
|
109
|
+
* This is implicitly used for atomic writes (both due to atomicObjectWrite, or atomicWrites: true),
|
|
110
|
+
* otherwise almost all atomic writes would result in an infinite loop, as usually
|
|
111
|
+
* objects aren't === (and we don't deep compare objects on writes).
|
|
112
|
+
*/
|
|
113
|
+
forceEqualWrites?: boolean;
|
|
114
|
+
|
|
115
|
+
atomicWrites?: boolean;
|
|
116
|
+
|
|
117
|
+
debugName?: string;
|
|
118
|
+
source?: string;
|
|
119
|
+
|
|
120
|
+
watchFunction: () => Result;
|
|
121
|
+
baseFunction?: Function;
|
|
122
|
+
|
|
123
|
+
// Only called after all the reads are synced
|
|
124
|
+
// - getTriggeredWatcher will function correctly in this callback
|
|
125
|
+
onResultUpdated?: (
|
|
126
|
+
result: { result: Result } | { error: string },
|
|
127
|
+
writes: PathValue[] | undefined,
|
|
128
|
+
watcher: SyncWatcher
|
|
129
|
+
) => void;
|
|
130
|
+
|
|
131
|
+
onCallCommit?: (call: CallSpec, metadata: FunctionMetadata) => void;
|
|
132
|
+
|
|
133
|
+
/** Causes us to not actually write values (we still sync new values, etc),
|
|
134
|
+
* we just don't actually commit any writes. */
|
|
135
|
+
dryRun?: boolean;
|
|
136
|
+
|
|
137
|
+
/** Runs immediately, returning immediately, not waiting for any values to synchronize.
|
|
138
|
+
* - Also disposes it immediately, as it isn't watching any values
|
|
139
|
+
* - Does not even START to synchronize any values
|
|
140
|
+
*/
|
|
141
|
+
runImmediately?: boolean;
|
|
142
|
+
|
|
143
|
+
/** By default we call pathValueCommitter.waitForValuesToCommit. This skips that check. */
|
|
144
|
+
noWaitForCommit?: boolean;
|
|
145
|
+
|
|
146
|
+
/** HACK: Used in conjuction with specific callers to prevent disposing, so the watcher
|
|
147
|
+
* can be reused for specific cases.
|
|
148
|
+
*/
|
|
149
|
+
static?: boolean;
|
|
150
|
+
|
|
151
|
+
/** Runs the inner function and calls the callback, even if there are unsynced values. */
|
|
152
|
+
allowUnsyncedReads?: boolean;
|
|
153
|
+
|
|
154
|
+
// NOTE: We create a new checker every single run
|
|
155
|
+
getPermissionsCheck?: () => PermissionsChecker;
|
|
156
|
+
|
|
157
|
+
skipPermissionsCheck?: boolean;
|
|
158
|
+
|
|
159
|
+
/** Default is after, which runs nested calls after the function runs (and only if all the
|
|
160
|
+
* data is synchronized)
|
|
161
|
+
* "inline" results in calls being run immediately, instead of queued.
|
|
162
|
+
* TODO: We can only inline calls on the same domain, as we don't have
|
|
163
|
+
* permissions for calls on other domains. In this case we might want to throw
|
|
164
|
+
* ("after" is complicated due to FunctionRunner).
|
|
165
|
+
* */
|
|
166
|
+
nestedCalls?: "throw" | "after" | "ignore" | "inline";
|
|
167
|
+
|
|
168
|
+
/** These domains are allowed to be referenced by locks. Can be overriden, so you can invalidate based on on other domains
|
|
169
|
+
* (ex, predict the server from the client).
|
|
170
|
+
* - Defaults to the current domain, which is the safest option (as invalidating based on other domains can cause
|
|
171
|
+
* security issues, reliability issues, etc).
|
|
172
|
+
*/
|
|
173
|
+
overrideAllowLockDomainsPrefixes?: string[];
|
|
174
|
+
|
|
175
|
+
/** See WatchSpec.orderGroup */
|
|
176
|
+
order?: number;
|
|
177
|
+
orderGroup?: number;
|
|
178
|
+
|
|
179
|
+
synchronousInit?: boolean;
|
|
180
|
+
|
|
181
|
+
overrides?: PathValue[];
|
|
182
|
+
|
|
183
|
+
// Triggers are also tracked if ProxyWatcher.TRACK_TRIGGERS (or ClientWatcher.DEBUG_TRIGGERS)
|
|
184
|
+
// Sometimes we need trigger tracking for delta watchers, so this isn't just for debugging
|
|
185
|
+
trackTriggers?: boolean;
|
|
186
|
+
|
|
187
|
+
// Fairly self explanatory. If there are nested createWatchers, instead of forking, we just
|
|
188
|
+
// run them immediately.
|
|
189
|
+
inlineNestedWatchers?: boolean;
|
|
190
|
+
|
|
191
|
+
// Temporary indicates after becoming synchronizes it will immediately dispose itself
|
|
192
|
+
temporary?: boolean;
|
|
193
|
+
|
|
194
|
+
// MUCH faster for large data (which otherwise requires a deep clone), but if it does return a proxy, and you access it in non-commit, it will throw.
|
|
195
|
+
allowProxyResults?: boolean;
|
|
196
|
+
|
|
197
|
+
/** Prevents us from (ever) tracking this as a wasted evaluation.
|
|
198
|
+
* (indicates that this function changes non-synced states)
|
|
199
|
+
* - Ex, qreact's mounter sets this, as it changes the DOM.
|
|
200
|
+
* - If you are watching for the purpose of logging, or writing to files, you would set this.
|
|
201
|
+
* - A lot of serverside watchers will set this (such as PathFunctionRunner)
|
|
202
|
+
*/
|
|
203
|
+
hasSideEffects?: boolean;
|
|
204
|
+
|
|
205
|
+
/** Takes any runs with the same runOncePerPaint (globally, across all watchers),
|
|
206
|
+
* and only runs them once per paint per group. Any excessive runs will be culled,
|
|
207
|
+
* with the latest call being rerun after the next paint.
|
|
208
|
+
* - If "runImmediately" or "temporary" is set, the latest call won't be rerun after the
|
|
209
|
+
* next paint (it will be dropped).
|
|
210
|
+
* - This should often be an id unique to the specific watcher. Otherwise
|
|
211
|
+
* you may have watchers left in a outdated state. However, this might be
|
|
212
|
+
* what you want, it depends on the usecase.
|
|
213
|
+
*/
|
|
214
|
+
runOncePerPaint?: string;
|
|
215
|
+
|
|
216
|
+
/** Logs time to go from unsynced to synced, and the paths that were unloaded.
|
|
217
|
+
* - In order to tell what paths to pre-load, and how much time we are waiting to load data.
|
|
218
|
+
* - Also to tell us how long and how many paths are loading on startup.
|
|
219
|
+
*/
|
|
220
|
+
logSyncTimings?: boolean;
|
|
221
|
+
|
|
222
|
+
maxLocksOverride?: number;
|
|
223
|
+
|
|
224
|
+
/** Finish in the order we start in. This is required to prevent races.
|
|
225
|
+
* - Defaults to true client side if we're using Querysub.commitAsync
|
|
226
|
+
* - If a number overrides the order, this is used when we're making predictions, so they trigger based on the order of their parent, instead of the order of when they started.
|
|
227
|
+
* - Only applies the order based on other functions that have this flag set.
|
|
228
|
+
*/
|
|
229
|
+
finishInStartOrder?: number | boolean;
|
|
230
|
+
|
|
231
|
+
/** Only to be used for logging. Is VERY useful in certain circumstances */
|
|
232
|
+
predictMetadata?: FunctionMetadata;
|
|
233
|
+
|
|
234
|
+
/** AKA, isAllSynced is always true. Useful for cases when we want to show partially loaded values. */
|
|
235
|
+
commitAllRuns?: boolean;
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
/** Path values contain some information about all the paths that were written at once, which we call a transaction. We can use this to detect when we are missing part of a transaction and wait until we receive that part of the transaction.
|
|
239
|
+
- If we don't wait, our writes will get rejected, and so the system will still be valid, however, there are a few reasons we want to avoid this:
|
|
240
|
+
- If we are trying to trigger external data, such as API calls, we want to reduce raise conditions. The values still might be rejected entirely, however, at least there won't be a partial read, whereas partial reads are significantly more common than rejections.
|
|
241
|
+
- Rejections can cause view stuttering, where the view goes to a bad state before it goes to a good state. If we just wait for the transaction to complete before trying to commit a value, such as in function runner, we can remove this stutter.
|
|
242
|
+
- Waiting can be more efficient than committing it and having to reject it, especially if something else depends on our value and then it has to be rejected and rerun.
|
|
243
|
+
- Not all writers will automatically rerun on rejections. They should be able to handle rejections. However, it might cause downtime and it might have to put the system into a partial recovery state. It's better to avoid this and just wait for the full transaction.
|
|
244
|
+
- Because this is so useful and so cheap to calculate, this flag will be set by default by most commit-style templates. It should be safe to set in watch style templates, However, it's not on by default there as, technically speaking, waiting for incomplete transactions will cause higher latency to render, And by default we prefer to render fast and usually get it right rather than render slow and always render the right thing.
|
|
245
|
+
*/
|
|
246
|
+
waitForIncompleteTransactions?: boolean;
|
|
247
|
+
|
|
248
|
+
// NOTE: The reason there isn't throttle support here is very frequently when you want to throttle one component rendering, it's because you have many components. So you actually want to throttle many components and have them throttle in conjunction with each other, which results in the logic becoming complicated.
|
|
249
|
+
// - But maybe we should support the single throttle case anyways?
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export type DryRunResult = {
|
|
253
|
+
writes: PathValue[];
|
|
254
|
+
readPaths: Set<string>
|
|
255
|
+
readParentPaths: Set<string>;
|
|
256
|
+
result: unknown;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
let harvestableReadyLoopCount = 0;
|
|
260
|
+
let harvestableWaitingLoopCount = 0;
|
|
261
|
+
let lastZombieCount = 0;
|
|
262
|
+
// NOTE: Only the % waiting for active watchers. Which... is fine
|
|
263
|
+
addStatPeriodic({
|
|
264
|
+
title: "Performance|Watcher % Waiting",
|
|
265
|
+
getValue: () => {
|
|
266
|
+
let totalLoop = harvestableReadyLoopCount + harvestableWaitingLoopCount;
|
|
267
|
+
if (totalLoop === 0) return 0;
|
|
268
|
+
let frac = harvestableWaitingLoopCount / totalLoop;
|
|
269
|
+
harvestableWaitingLoopCount = 0;
|
|
270
|
+
harvestableReadyLoopCount = 0;
|
|
271
|
+
return frac;
|
|
272
|
+
},
|
|
273
|
+
format: formatPercent,
|
|
274
|
+
});
|
|
275
|
+
addStatPeriodic({
|
|
276
|
+
title: "Performance|Stalled Watchers",
|
|
277
|
+
getValue: () => lastZombieCount,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
// Used to prevent local rejections on remote values (as local functions aren't rerun)
|
|
282
|
+
// Also used for security on servers, so they can read from untrusted domains, but can't have values
|
|
283
|
+
// rejected by untrusted domains.
|
|
284
|
+
const getAllowedLockDomainsPrefixes = function getTrustedDomains(): string[] {
|
|
285
|
+
if (isNode()) {
|
|
286
|
+
return [getPathStr1(getDomain())];
|
|
287
|
+
} else {
|
|
288
|
+
// TODO: We COULD allow non-local watches on clients? This would allow something such as...
|
|
289
|
+
// clicking on a button to go the first page that no other user is on... to revert if
|
|
290
|
+
// another user clicks at the same time and we race. Or... something? Maybe there
|
|
291
|
+
// are no cases when we need it... and it would definitely slow things down, so...
|
|
292
|
+
// maybe we just don't?
|
|
293
|
+
return [LOCAL_DOMAIN_PATH];
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export type SyncWatcher = {
|
|
298
|
+
seqNum: number;
|
|
299
|
+
options: WatcherOptions<any>;
|
|
300
|
+
|
|
301
|
+
dispose: () => void;
|
|
302
|
+
disposed?: boolean;
|
|
303
|
+
// Reset every time the function is run, so this can be subscribed to every run, but will
|
|
304
|
+
// only be called once the final commit.
|
|
305
|
+
onInnerDisposed: (() => void)[];
|
|
306
|
+
// Runs after any trigger happens (usually multiple triggers are required for a commit)
|
|
307
|
+
onAfterTriggered: (() => void)[];
|
|
308
|
+
|
|
309
|
+
// Not great, but... sometimes we just want to trigger the function
|
|
310
|
+
explicitlyTrigger: (changes?: WatchSpecData) => void;
|
|
311
|
+
|
|
312
|
+
hasAnyUnsyncedAccesses: () => boolean;
|
|
313
|
+
|
|
314
|
+
// - epochTime results in reading the latest time
|
|
315
|
+
// - time is exclusive, so if you set the time of an existing write, you won't
|
|
316
|
+
// read back it's writes.
|
|
317
|
+
setCurrentReadTime: <T>(time: Time, code: () => T) => T;
|
|
318
|
+
// NOTE: Use the setCurrentReadTime helper, to prevent accidentally
|
|
319
|
+
// leaving the read time set (or use runAtTime in the options,
|
|
320
|
+
// to set it forever).
|
|
321
|
+
currentReadTime: Time | undefined;
|
|
322
|
+
nextWriteTime: Time | undefined;
|
|
323
|
+
|
|
324
|
+
debugName: string;
|
|
325
|
+
|
|
326
|
+
// The changes which triggered the current run (or last run if we are not currently running).
|
|
327
|
+
triggeredByChanges: undefined | WatchSpecData;
|
|
328
|
+
|
|
329
|
+
pendingWrites: Map<string, unknown>;
|
|
330
|
+
pendingEventWrites: Set<string>;
|
|
331
|
+
pendingCalls: { call: CallSpec; metadata: FunctionMetadata }[];
|
|
332
|
+
|
|
333
|
+
pendingWatches: {
|
|
334
|
+
paths: Set<string>;
|
|
335
|
+
parentPaths: Set<string>;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// These aren't set until after a run finishes
|
|
339
|
+
lastWatches: {
|
|
340
|
+
paths: Set<string>;
|
|
341
|
+
parentPaths: Set<string>;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
/** The key is the time we read at. This is required to create the lock.
|
|
346
|
+
* - Only utilized if we commit the write
|
|
347
|
+
*/
|
|
348
|
+
pendingAccesses: Map<Time | undefined, Map<string, {
|
|
349
|
+
pathValue: PathValue;
|
|
350
|
+
noLocks: boolean;
|
|
351
|
+
}>>;
|
|
352
|
+
pendingEpochAccesses: Map<Time | undefined, Set<string>>;
|
|
353
|
+
|
|
354
|
+
// A necessity for react rendering, as rendering unsynced data almost always looks bad,
|
|
355
|
+
// so we need to show the previously rendered data when we have unsynced data.
|
|
356
|
+
// (Although, there is a certain pleasantness to showing SOME change extremely fast,
|
|
357
|
+
// even if it's a somewhat broken state, so... we might at a mode to allow rendering
|
|
358
|
+
// with unsynced data).
|
|
359
|
+
// TODO: We might make this a function, and lazily calculate it. That way we can batch reads
|
|
360
|
+
// without having to check isSynced?
|
|
361
|
+
// NOTE: The "pendingUnsynced" must ALWAYS also set pendingWatches
|
|
362
|
+
pendingUnsyncedAccesses: Set<string>;
|
|
363
|
+
lastUnsyncedAccesses: Set<string>;
|
|
364
|
+
|
|
365
|
+
pendingUnsyncedParentAccesses: Set<string>;
|
|
366
|
+
lastUnsyncedParentAccesses: Set<string>;
|
|
367
|
+
|
|
368
|
+
specialPromiseUnsynced: boolean;
|
|
369
|
+
lastSpecialPromiseUnsynced: boolean;
|
|
370
|
+
lastSpecialPromiseUnsyncedReason?: string;
|
|
371
|
+
|
|
372
|
+
consecutiveErrors: number;
|
|
373
|
+
countSinceLastFullSync: number;
|
|
374
|
+
lastSyncTime: number;
|
|
375
|
+
syncRunCount: number;
|
|
376
|
+
startTime: number;
|
|
377
|
+
|
|
378
|
+
lastEvalTime: number;
|
|
379
|
+
inThrottle?: boolean;
|
|
380
|
+
|
|
381
|
+
permissionsChecker?: PermissionsChecker;
|
|
382
|
+
|
|
383
|
+
/** Allows us to tag the object for heap analysis */
|
|
384
|
+
tag: SyncWatcherTag;
|
|
385
|
+
|
|
386
|
+
hackHistory: { message: string; time: number }[];
|
|
387
|
+
|
|
388
|
+
createTime: number;
|
|
389
|
+
|
|
390
|
+
logSyncTimings?: {
|
|
391
|
+
startTime: number;
|
|
392
|
+
unsyncedPaths: Set<string>;
|
|
393
|
+
unsyncedStages: {
|
|
394
|
+
paths: string[];
|
|
395
|
+
time: number;
|
|
396
|
+
}[];
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function addToHistory(watcher: SyncWatcher, message: string) {
|
|
400
|
+
watcher.hackHistory.push({ message, time: Date.now() });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
// NOTE: A lot of our proxy callback functionality is partially duplicated in PathTimeRunner
|
|
405
|
+
export let atomicObjectSymbol = Symbol.for("atomicObject");
|
|
406
|
+
export function atomicObjectWrite<T>(obj: T): T {
|
|
407
|
+
if (canHaveChildren(obj) && Object.isExtensible(obj)) {
|
|
408
|
+
(obj as any)[atomicObjectSymbol] = true;
|
|
409
|
+
}
|
|
410
|
+
return recursiveFreeze(obj);
|
|
411
|
+
}
|
|
412
|
+
export function atomicCloneWrite<T>(obj: T): T {
|
|
413
|
+
return atomicObjectWrite(deepCloneJSON(obj));
|
|
414
|
+
}
|
|
415
|
+
export function atomicObjectWriteNoFreeze<T>(obj: T): T {
|
|
416
|
+
if (canHaveChildren(obj) && Object.isExtensible(obj)) {
|
|
417
|
+
(obj as any)[atomicObjectSymbol] = true;
|
|
418
|
+
}
|
|
419
|
+
return obj;
|
|
420
|
+
}
|
|
421
|
+
const readNoProxy = Symbol.for("readNoProxy");
|
|
422
|
+
// Specifies we are reading this object, and not any children of it
|
|
423
|
+
export function atomicObjectRead<T>(obj: T): T {
|
|
424
|
+
if (!isValueProxy(obj)) return obj;
|
|
425
|
+
return (obj as any)[readNoProxy];
|
|
426
|
+
}
|
|
427
|
+
export const atomic = atomicObjectRead;
|
|
428
|
+
(globalThis as any).atomic = atomic;
|
|
429
|
+
|
|
430
|
+
export function doUnatomicWrites<T>(callback: () => T): T {
|
|
431
|
+
return doProxyOptions({ atomicWrites: false }, callback);
|
|
432
|
+
}
|
|
433
|
+
export function doAtomicWrites<T>(callback: () => T): T {
|
|
434
|
+
return doProxyOptions({ atomicWrites: true }, callback);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const rawRead = Symbol.for("rawRead");
|
|
438
|
+
/** Reads the raw value, which includes transparent values.
|
|
439
|
+
* - Basically just useful for testing if an object exists in a lookup, in which case
|
|
440
|
+
* specialObjectWriteValue will be returned for the object.
|
|
441
|
+
* - NEVER returns a proxy, always a real object (which might be undefined, or an object).
|
|
442
|
+
*/
|
|
443
|
+
export function atomicRaw<T>(obj: T): T {
|
|
444
|
+
if (!isValueProxy(obj)) return obj;
|
|
445
|
+
return (obj as any)[rawRead];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function doProxyOptions<T>(options: Partial<WatcherOptions<any>>, callback: () => T): T {
|
|
449
|
+
let watcher = proxyWatcher.getTriggeredWatcher();
|
|
450
|
+
let prev = watcher.options;
|
|
451
|
+
watcher.options = { ...watcher.options, ...options };
|
|
452
|
+
try {
|
|
453
|
+
return callback();
|
|
454
|
+
} finally {
|
|
455
|
+
watcher.options = prev;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// HACK: A special value which acts like undefined, EXCEPT when using Object.keys(). The reason it can't
|
|
460
|
+
// be undefined, is that undefined values are removed by garbage collection, and this has to stick around!
|
|
461
|
+
// - Is automatically set in the parents paths of object writes, so Object.keys() works.
|
|
462
|
+
// (Which makes garbage collection a lot harder, but... not impossible, and Object.keys() is so important
|
|
463
|
+
// that it is okay).
|
|
464
|
+
// - Due to permissions rejecting root writes, BUT, the ease of this value getting nito relatively root paths
|
|
465
|
+
// (although hopefully not THE root), reading this counts as a "readIsUndefined"
|
|
466
|
+
// NOTE: Yes, this does technically mean you depend on the first value to write to an object.
|
|
467
|
+
// And so, if the first write is rejected, the lock becomes rejected, even though
|
|
468
|
+
// there might be other writes. This is fine, because the first value is the oldest, so
|
|
469
|
+
// the least likely to be rejected, and if it is... FunctionRunner will just rerun
|
|
470
|
+
// the dependent call, and find the key exists again, and the only difference will
|
|
471
|
+
// be a bit of lag, and value flicker.
|
|
472
|
+
export const specialObjectWriteValue = "_specialObjectWriteValue_16c4c3bb43f24111976a2681c972f6f4";
|
|
473
|
+
export const specialObjectWriteSymbol = Symbol("specialObjectWriteSymbol");
|
|
474
|
+
// Values that don't add another proxy layer
|
|
475
|
+
export function isTransparentValue(value: unknown) {
|
|
476
|
+
return (
|
|
477
|
+
value === undefined
|
|
478
|
+
|| value === specialObjectWriteValue
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function undeleteFromLookup<T>(lookup: { [key: string]: T }, key: string): void {
|
|
483
|
+
lookup[key] = {} as any;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const syncedSymbol = Symbol.for("syncedSymbol");
|
|
487
|
+
// HACK: This should probably be somewhere else, but... it is just so useful for PathFunctionRunner...
|
|
488
|
+
/** @deprecated this is not very accurate (it breaks for schema accesses). Only use it for low level places that can't call the more accurate Querysub.isSynced) */
|
|
489
|
+
export function isSynced(obj: unknown): boolean {
|
|
490
|
+
// If it is a primitive, then it must be synced!
|
|
491
|
+
if (!canHaveChildren(obj)) return true;
|
|
492
|
+
if (!isValueProxy(obj)) return true;
|
|
493
|
+
return !!(obj[syncedSymbol]);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export type PermissionsChecker = {
|
|
497
|
+
checkPermissions(path: string): { permissionsPath: string; allowed: boolean; };
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class SyncWatcherTag { }
|
|
502
|
+
|
|
503
|
+
function isRecursiveMatch(paths: Set<string>, path: string): boolean {
|
|
504
|
+
if (paths.size === 0) return false;
|
|
505
|
+
if (paths.has(path)) return true;
|
|
506
|
+
for (let otherPath of paths) {
|
|
507
|
+
if (path.includes(otherPath) || otherPath.includes(path)) {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
function removeMatches(paths: Set<string>, path: string): void {
|
|
514
|
+
paths.delete(path);
|
|
515
|
+
for (let otherPath of paths) {
|
|
516
|
+
if (path.includes(otherPath) || otherPath.includes(path)) {
|
|
517
|
+
paths.delete(otherPath);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export class PathValueProxyWatcher {
|
|
523
|
+
public static BREAK_ON_READS = new Set<string>();
|
|
524
|
+
public static BREAK_ON_WRITES = new Set<string>();
|
|
525
|
+
public static SET_FUNCTION_WATCH_ON_WRITES = new Set<string>();
|
|
526
|
+
public static LOG_WRITES_INCLUDES = new Set<string>();
|
|
527
|
+
|
|
528
|
+
// getPathStr(ModuleId, FunctionId)
|
|
529
|
+
public static BREAK_ON_CALL = new Set<string>();
|
|
530
|
+
|
|
531
|
+
public static DEBUG = false;
|
|
532
|
+
/** NOTE: This results in VERY slow performance, and will log A LOT of information. */
|
|
533
|
+
public static TRACE = false;
|
|
534
|
+
public static TRACE_WRITES = false;
|
|
535
|
+
public static TRACE_WATCHERS = new Set<string>();
|
|
536
|
+
|
|
537
|
+
public static TRACK_TRIGGERS = false;
|
|
538
|
+
|
|
539
|
+
private SHOULD_TRACE(watcher: SyncWatcher) {
|
|
540
|
+
return PathValueProxyWatcher.DEBUG || PathValueProxyWatcher.TRACE || PathValueProxyWatcher.TRACE_WRITES && watcher.options.canWrite || ClientWatcher.DEBUG_TRIGGERS === "heavy" || PathValueProxyWatcher.TRACE_WATCHERS.size > 0 && Array.from(PathValueProxyWatcher.TRACE_WATCHERS).some(x => watcher.debugName.includes(x));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private isUnsyncCheckers = new Set<{ value: number }>();
|
|
544
|
+
|
|
545
|
+
public countUnsynced = <T>(checker: { value: number }, code: () => T): T => {
|
|
546
|
+
this.isUnsyncCheckers.add(checker);
|
|
547
|
+
try {
|
|
548
|
+
return code();
|
|
549
|
+
} finally {
|
|
550
|
+
this.isUnsyncCheckers.delete(checker);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
public getCallbackPathValue = (pathStr: string, syncParentKeys?: "parentKeys"): PathValue | undefined => {
|
|
555
|
+
const watcher = this.runningWatcher;
|
|
556
|
+
if (!watcher) {
|
|
557
|
+
debugger;
|
|
558
|
+
throw new Error(`Tried to get path "${pathStr}" outside of a watcher function.`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (watcher.options.noLocks && !pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
562
|
+
throw new Error(`Tried to read a non-local path in a "noLocks" watcher, ${watcher.debugName}, path ${pathStr}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (watcher.permissionsChecker) {
|
|
566
|
+
if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
|
|
567
|
+
if (
|
|
568
|
+
!watcher.hasAnyUnsyncedAccesses()
|
|
569
|
+
&& authorityStorage.DEBUG_hasAnyValues(pathStr)
|
|
570
|
+
// HACK: Don't show warnings for some framework paths, because they are filling up the console logs
|
|
571
|
+
// and don't really matter. We could just not request them, but at depth 3 is valid,
|
|
572
|
+
// so we kind of have to request that. And at depth 2, it would require special case code,
|
|
573
|
+
// in a bunch of annoying places.
|
|
574
|
+
&& (getPathIndex(pathStr, 1) !== "PathFunctionRunner" || getPathDepth(pathStr) > 3)
|
|
575
|
+
) {
|
|
576
|
+
console.warn(`Denied read access to path "${pathStr}"`);
|
|
577
|
+
// console.warn(`${new Date().toLocaleTimeString()} Denied read access to path "${pathStr}"`);
|
|
578
|
+
// console.warn(new Error().stack);
|
|
579
|
+
}
|
|
580
|
+
// This undefined MIGHT cause issues, but... it also makes it easier to check if
|
|
581
|
+
// we have permissions, and more clear if we are denied permissions.
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (watcher.options.writeOnly) {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const currentReadTime = watcher.options.forceReadLatest ? undefined : watcher.currentReadTime;
|
|
591
|
+
|
|
592
|
+
let pathValue = authorityStorage.getValueAtTime(pathStr, currentReadTime);
|
|
593
|
+
if (!watcher.options.noSyncing) {
|
|
594
|
+
// NOTE: If we have any value, we are always synced (that's what synced means, as if we aren't syncing,
|
|
595
|
+
// we delete any values, to prevent stale values from being used).
|
|
596
|
+
if (!pathValue) {
|
|
597
|
+
// NOTE: We might not have a value simply due to reading too far back in time,
|
|
598
|
+
// so we have to call isSynced just to be sure it is actually unsynced
|
|
599
|
+
if (!authorityStorage.isSynced(pathStr)) {
|
|
600
|
+
for (let checker of this.isUnsyncCheckers) {
|
|
601
|
+
checker.value++;
|
|
602
|
+
}
|
|
603
|
+
watcher.pendingUnsyncedAccesses.add(pathStr);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (syncParentKeys) {
|
|
607
|
+
if (!authorityStorage.isParentSynced(pathStr)) {
|
|
608
|
+
watcher.pendingUnsyncedParentAccesses.add(pathStr);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
watcher.pendingWatches.paths.add(pathStr);
|
|
612
|
+
if (syncParentKeys) {
|
|
613
|
+
watcher.pendingWatches.parentPaths.add(pathStr);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!pathValue) {
|
|
617
|
+
let pendingNullAccesses = watcher.pendingEpochAccesses.get(currentReadTime);
|
|
618
|
+
if (!pendingNullAccesses) {
|
|
619
|
+
pendingNullAccesses = new Set();
|
|
620
|
+
watcher.pendingEpochAccesses.set(currentReadTime, pendingNullAccesses);
|
|
621
|
+
}
|
|
622
|
+
pendingNullAccesses.add(pathStr);
|
|
623
|
+
return undefined;
|
|
624
|
+
} else {
|
|
625
|
+
let readValues = watcher.pendingAccesses.get(currentReadTime);
|
|
626
|
+
if (!readValues) {
|
|
627
|
+
readValues = new Map();
|
|
628
|
+
watcher.pendingAccesses.set(currentReadTime, readValues);
|
|
629
|
+
}
|
|
630
|
+
let prevObj = readValues.get(pathValue.path);
|
|
631
|
+
let noLocks = watcher.options.noLocks || watcher.options.unsafeNoLocks || false;
|
|
632
|
+
if (!prevObj) {
|
|
633
|
+
readValues.set(pathValue.path, { pathValue, noLocks });
|
|
634
|
+
} else {
|
|
635
|
+
prevObj.pathValue = pathValue;
|
|
636
|
+
// It can only be no locks if it's always no locks (prev && current)
|
|
637
|
+
prevObj.noLocks = prevObj.noLocks && noLocks;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return pathValue;
|
|
643
|
+
};
|
|
644
|
+
public getCallback = (pathStr: string, syncParentKeys?: "parentKeys", readTransparent?: "readTransparent"): { value: unknown } | undefined => {
|
|
645
|
+
if (PathValueProxyWatcher.BREAK_ON_READS.size > 0 && (proxyWatcher.isAllSynced() || this)) {
|
|
646
|
+
// NOTE: We can't do a recursive match, as the parent paths include the
|
|
647
|
+
// root, which is constantly read, but not relevant.
|
|
648
|
+
if (PathValueProxyWatcher.BREAK_ON_READS.has(pathStr)) {
|
|
649
|
+
const unwatch = () => PathValueProxyWatcher.BREAK_ON_READS.delete(pathStr);
|
|
650
|
+
debugger;
|
|
651
|
+
unwatch;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const watcher = this.runningWatcher;
|
|
655
|
+
if (!watcher) {
|
|
656
|
+
debugger;
|
|
657
|
+
throw new Error(`Tried to get path "${pathStr}" outside of a watcher function.`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// IMPORTANT! If we read a value we wrote, we don't need to incur a watch!
|
|
661
|
+
// NOTE: If we wrote undefined, then that DOESN'T clobber child values, as it actually
|
|
662
|
+
// deletes values, so... it doesn't count as having a value.
|
|
663
|
+
// - ALSO, writing undefined allows us to then read child values, which is odd,
|
|
664
|
+
// but is the correct behavior.
|
|
665
|
+
if (watcher.pendingWrites.has(pathStr)) {
|
|
666
|
+
let pendingValue = watcher.pendingWrites.get(pathStr);
|
|
667
|
+
let isPendingTransparent = isTransparentValue(pendingValue);
|
|
668
|
+
if (isPendingTransparent) {
|
|
669
|
+
if (readTransparent) {
|
|
670
|
+
return { value: pendingValue };
|
|
671
|
+
} else {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
return { value: pendingValue };
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let pathValue = this.getCallbackPathValue(pathStr, syncParentKeys);
|
|
680
|
+
|
|
681
|
+
if (!pathValue || !readTransparent && isTransparentValue(pathValueSerializer.getPathValue(pathValue))) {
|
|
682
|
+
let schema2 = getMatchingSchema(pathStr);
|
|
683
|
+
if (schema2) {
|
|
684
|
+
let atomicObj = Schema2Fncs.isAtomic(schema2.schema, "read", schema2.nestedPath);
|
|
685
|
+
if (atomicObj) {
|
|
686
|
+
return { value: atomicObj.defaultValue };
|
|
687
|
+
}
|
|
688
|
+
// Check if the object should be an object. Objects can be read though, if they've
|
|
689
|
+
// been initialized as an object (with specialObjectWriteValue). OTHERWISE,
|
|
690
|
+
// they require initialization (root.object = {}).
|
|
691
|
+
if (Schema2Fncs.isObject(schema2.schema, schema2.nestedPath)) {
|
|
692
|
+
// IMPORTANT! It MAY seem, like we can just add a special "deleteObjectValue_1f548f4a01cb4c35bc7b559d8dd74c42"
|
|
693
|
+
// sentinel, and use that to delete something (both in proxy, and in gc, so it really recursively deletes it),
|
|
694
|
+
// BUT, that has one major issue. Once that sentinel is truly deleted, the value will evaluate a proxy again,
|
|
695
|
+
// which will cause GC to change the state, which would be a bug. Not to mention accessing not existing
|
|
696
|
+
// keys would return a proxy, which is annoying. So... this is the best approach.
|
|
697
|
+
if (pathValueSerializer.getPathValue(pathValue) !== specialObjectWriteValue) {
|
|
698
|
+
// Means the key was deleted (`delete lookup[key]`), and so the intention is for this
|
|
699
|
+
// to terminate in undefined.
|
|
700
|
+
return { value: undefined };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return undefined;
|
|
705
|
+
} else {
|
|
706
|
+
let readValue = pathValueSerializer.getPathValue(pathValue);
|
|
707
|
+
// Unescape specialObjectWriteValue-like strings
|
|
708
|
+
if (typeof readValue === "string" && readValue.length > specialObjectWriteValue.length && readValue.startsWith(specialObjectWriteValue)) {
|
|
709
|
+
readValue = readValue.slice(0, -1);
|
|
710
|
+
}
|
|
711
|
+
return { value: readValue };
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// We exclude undefined, BUT, we include "null", etc.
|
|
716
|
+
// - This differs from javascript, but... due to how deletions work, we can't/don't differentiate between
|
|
717
|
+
// a deleted value and undefined, and a lot of things break if deleting a value doesn't remove it from an "in" check!
|
|
718
|
+
private hasCallback = (pathStr: string): boolean => {
|
|
719
|
+
let value = this.getCallback(pathStr, undefined, "readTransparent");
|
|
720
|
+
return value?.value !== undefined;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
private setCallback = (pathStr: string, value: unknown, inRecursion = false, allowSpecial = false): void => {
|
|
724
|
+
if (PathValueProxyWatcher.BREAK_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
725
|
+
if (isRecursiveMatch(PathValueProxyWatcher.BREAK_ON_WRITES, pathStr)) {
|
|
726
|
+
let unwatch = () => removeMatches(PathValueProxyWatcher.BREAK_ON_WRITES, pathStr);
|
|
727
|
+
debugger;
|
|
728
|
+
unwatch;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
733
|
+
if (isRecursiveMatch(PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES, pathStr)) {
|
|
734
|
+
let unwatch = () => removeMatches(PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES, pathStr);
|
|
735
|
+
PathValueProxyWatcher.BREAK_ON_CALL.add(
|
|
736
|
+
getPathStr2(getCurrentCall().ModuleId, getCurrentCall().FunctionId)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (PathValueProxyWatcher.LOG_WRITES_INCLUDES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
742
|
+
if (isRecursiveMatch(PathValueProxyWatcher.LOG_WRITES_INCLUDES, pathStr)) {
|
|
743
|
+
let unwatch = () => removeMatches(PathValueProxyWatcher.LOG_WRITES_INCLUDES, pathStr);
|
|
744
|
+
console.log(`Write path "${pathStr}" = ${value}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (value === specialObjectWriteSymbol) {
|
|
748
|
+
value = specialObjectWriteValue;
|
|
749
|
+
allowSpecial = true;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Escape specialObjectWriteValue-like strings
|
|
753
|
+
// - Check for .startsWith, so we change all values. If we just added when it was ===, then
|
|
754
|
+
// anything that === the escaped value before hand, would get unescaped, even though
|
|
755
|
+
// it was never escaped!
|
|
756
|
+
if (!allowSpecial && typeof value === "string" && value.startsWith(specialObjectWriteValue)) {
|
|
757
|
+
value += " ";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const watcher = this.runningWatcher;
|
|
761
|
+
if (!watcher) {
|
|
762
|
+
// Ignore preact paths
|
|
763
|
+
let lastPart = getLastPathPart(pathStr);
|
|
764
|
+
if (lastPart.startsWith("__") || lastPart === "type") {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
debugger;
|
|
768
|
+
throw new Error(`Tried to set path "${pathStr}" outside of a watcher function.`);
|
|
769
|
+
}
|
|
770
|
+
if (!watcher.options.canWrite) {
|
|
771
|
+
throw new Error(`Tried to write to path "${pathStr}" in watcher (${watcher.debugName}) that has { canWrite: false }`);
|
|
772
|
+
}
|
|
773
|
+
if (watcher.options.noLocks && !pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
774
|
+
throw new Error(`Tried to write a non-local path in a "noLocks" watcher, ${watcher.debugName}, path ${pathStr}`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (watcher.permissionsChecker) {
|
|
778
|
+
if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
|
|
779
|
+
if (value !== specialObjectWriteValue) {
|
|
780
|
+
if (!watcher.hasAnyUnsyncedAccesses()) {
|
|
781
|
+
// NOTE: Only throw once we sync all accesses, otherwise this could cause sync cascading, which is bad
|
|
782
|
+
// NOTE: We COULD do the same thing for reason... but... it should be equivalent to just NOOP reads. We
|
|
783
|
+
// could NOOP writes as well, but throwing makes development easier (by telling you why your write
|
|
784
|
+
// is doing nothing more loudly than just a console warning).
|
|
785
|
+
throw new Error(`Denied write access to path "${pathStr}"`);
|
|
786
|
+
//console.warn(red(`Denied write access to path "${pathStr}" (ignoring write and continuing)`));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (
|
|
794
|
+
!watcher.options.forceEqualWrites
|
|
795
|
+
&& (
|
|
796
|
+
!canHaveChildren(value)
|
|
797
|
+
|| !(atomicObjectSymbol in value || watcher.options.atomicWrites)
|
|
798
|
+
)
|
|
799
|
+
) {
|
|
800
|
+
// NOTE: This NOOPs on predicted writes as well. BUT, we also incur a lock, so, it's fine...
|
|
801
|
+
let existingValue = this.getCallback(pathStr, undefined, "readTransparent");
|
|
802
|
+
// NOTE: We don't deep compare atomicObjectSymbol, as atomicObjectSymbol is often
|
|
803
|
+
// used for exotic values. The user will have to deep compare themselves, if
|
|
804
|
+
// they want to avoid the extra write.
|
|
805
|
+
if (existingValue?.value === value) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Set parent specialObjectWriteValue values
|
|
811
|
+
if (
|
|
812
|
+
!watcher.options.writeOnly
|
|
813
|
+
// If we are an event write we can't set parent values, otherwise the parents will become
|
|
814
|
+
// events, which breaks Object.keys on them (which is almost certainly not intended), because
|
|
815
|
+
// event paths can't be watched after a certain time.
|
|
816
|
+
&& !watcher.options.eventWrite
|
|
817
|
+
&& !inRecursion
|
|
818
|
+
// Deletes shouldn't create parent objects!
|
|
819
|
+
&& value !== undefined
|
|
820
|
+
) {
|
|
821
|
+
if (!watcher.pendingWrites.has(pathStr)) {
|
|
822
|
+
let parentPathStr = pathStr;
|
|
823
|
+
let isLocal = pathStr.startsWith(LOCAL_DOMAIN_PATH);
|
|
824
|
+
let depthToData = isLocal ? 1 : DEPTH_TO_DATA;
|
|
825
|
+
while (true) {
|
|
826
|
+
parentPathStr = getParentPathStr(parentPathStr);
|
|
827
|
+
if (getPathDepth(parentPathStr) < depthToData) break;
|
|
828
|
+
// We don't need to check all parents, as if any value is set, all parents will
|
|
829
|
+
// likely have their specialObjectWriteValue set, due to a previous run of this function!
|
|
830
|
+
if (watcher.pendingWrites.has(parentPathStr)) break;
|
|
831
|
+
let parentValue = this.getCallback(parentPathStr, undefined, "readTransparent");
|
|
832
|
+
// If we have a value, don't set the object.
|
|
833
|
+
// NOTE: This means if you:
|
|
834
|
+
// x.y.z = 1
|
|
835
|
+
// delete x.y; // (OR, x.y = null)
|
|
836
|
+
// x.y.z = 2
|
|
837
|
+
// Object.keys(x.y) === ["z"]
|
|
838
|
+
// Which is annoying (keys show up for values you accidentally resurrected), but...
|
|
839
|
+
// it is somewhat required, as we don't know if you are trying to readd
|
|
840
|
+
// an object, or accidentally resurrected it!
|
|
841
|
+
// - Really you should use a flag for deletion, which will work 100% of the time,
|
|
842
|
+
// and make life easier for specific undoing, and also eventually... instructed garbage collection.
|
|
843
|
+
if (parentValue?.value !== specialObjectWriteValue) {
|
|
844
|
+
watcher.pendingWrites.set(parentPathStr, specialObjectWriteValue);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function isAtomicSchema(pathStr: string) {
|
|
851
|
+
let schema2 = getMatchingSchema(pathStr);
|
|
852
|
+
if (schema2) {
|
|
853
|
+
let atomicObj = Schema2Fncs.isAtomic(schema2.schema, "write", schema2.nestedPath);
|
|
854
|
+
if (atomicObj) {
|
|
855
|
+
return true;
|
|
856
|
+
// NOTE: We can't skip if the value === default, as the previous value might not be the default,
|
|
857
|
+
// (so the write might change the value).
|
|
858
|
+
// - We could optimize the = undefined case for atomic objects, where the value is already undefined
|
|
859
|
+
// (but we defaulted it), but... it's probably not worth the effort.
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Destructure the value if it is an object, UNLESS, it specifies that it is atomic.
|
|
865
|
+
if (canHaveChildren(value) && !(atomicObjectSymbol in value) && !watcher.options.atomicWrites && !isAtomicSchema(pathStr)) {
|
|
866
|
+
const addWrites = (pathStr: string, value: unknown) => {
|
|
867
|
+
if (canHaveChildren(value) && !(atomicObjectSymbol in value) && !isAtomicSchema(pathStr)) {
|
|
868
|
+
this.setCallback(pathStr, specialObjectWriteValue, true, true);
|
|
869
|
+
const keys = getKeys(value);
|
|
870
|
+
for (let key of keys) {
|
|
871
|
+
if (typeof key !== "string") {
|
|
872
|
+
throw new Error(`Cannot have non-string keys in non-atomic (atomicObjectWrite) objects. Key: "${String(key)}" at path "${pathStr}".`);
|
|
873
|
+
}
|
|
874
|
+
addWrites(appendToPathStr(pathStr, key), value[key]);
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
// NOTE: It is a bit inefficient in terms of computation to call setCallback, BUT, it
|
|
878
|
+
// allows us to automatically run our deduping checks, which saves a lot of unnecessary
|
|
879
|
+
// computation on other nodes (and network bandwidth, etc, etc).
|
|
880
|
+
this.setCallback(pathStr, value, true);
|
|
881
|
+
//watcher.pendingWrites.set(pathStr, value);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
addWrites(pathStr, value);
|
|
885
|
+
} else {
|
|
886
|
+
if (!pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
887
|
+
// Copy the value, to ensure any proxy values aren't attempted to be written
|
|
888
|
+
// to the database. A bit slower, but... it should be fine. We COULD do this
|
|
889
|
+
// later on, but this makes it easier to attribute lag and errors to the original source.
|
|
890
|
+
// - If we had a proxy, we WOULD copy it eventually, but we would copy it later,
|
|
891
|
+
// which would cause an error, because the reader wouldn't be trackable.
|
|
892
|
+
value = deepCloneCborx(value);
|
|
893
|
+
}
|
|
894
|
+
watcher.pendingWrites.set(pathStr, value);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
/** Syncs keys AND values (as we won't return a key for a value that is undefined). */
|
|
898
|
+
public getKeys = (pathStr: string): string[] => {
|
|
899
|
+
if (getPathDepth(pathStr) < MODULE_INDEX) {
|
|
900
|
+
throw new Error(`Cannot call getKeys on path "${pathStr}" because it is too shallow. Must be at least ${MODULE_INDEX} levels deep.`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const watcher = this.runningWatcher;
|
|
904
|
+
if (!watcher) {
|
|
905
|
+
debugger;
|
|
906
|
+
throw new Error(`Tried to getKeys on path "${pathStr}" outside of a watcher function.`);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (watcher.permissionsChecker) {
|
|
910
|
+
if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
|
|
911
|
+
console.warn(`Denied read access to parentPath "${pathStr}"`);
|
|
912
|
+
return [];
|
|
913
|
+
}
|
|
914
|
+
let childPath = appendToPathStr(pathStr, Date.now() + "_" + Math.random());
|
|
915
|
+
if (!watcher.permissionsChecker.checkPermissions(childPath).allowed) {
|
|
916
|
+
console.warn(red(`Denied write access to children of parent path "${childPath}"`));
|
|
917
|
+
return [];
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Using the schema for keys is a MASSIVE optimization, despite the potential drawbacks.
|
|
922
|
+
// - It returns all possible keys, even optional keys, which isn't the same as Object.keys()
|
|
923
|
+
// - It ignores any keys removed from the schema, which causes JSON.stringify to behave differently
|
|
924
|
+
// HOWEVER, saving an entire serverside call, which would otherwise require a roundtrip before we could
|
|
925
|
+
// access values inside the keys... is unbelievably useful. This reduces latency, which is the hardest
|
|
926
|
+
// thing to remove. AND, "noAtomicSchema" can always be used to skip this optimization.
|
|
927
|
+
let schema2 = getMatchingSchema(pathStr);
|
|
928
|
+
if (schema2) {
|
|
929
|
+
let atomicKeys = Schema2Fncs.getKeys(schema2.schema, schema2.nestedPath);
|
|
930
|
+
if (atomicKeys) {
|
|
931
|
+
return atomicKeys;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// IMPORTANT: The this.getCallback is what triggers the parent watch!
|
|
936
|
+
// We leave it up to getCallback to add the parentPaths watch, to allow Object.keys on values
|
|
937
|
+
// that were just written with incurring a parent watch.
|
|
938
|
+
let pathValue = this.getCallback(pathStr, "parentKeys");
|
|
939
|
+
if (!isTransparentValue(pathValue?.value)) {
|
|
940
|
+
return getKeys(pathValue?.value) as string[];
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
let childPaths = authorityStorage.getPathsFromParent(pathStr);
|
|
944
|
+
|
|
945
|
+
// We need to also get keys from pendingWrites
|
|
946
|
+
{
|
|
947
|
+
let copiedChildPaths: Set<string> | undefined;
|
|
948
|
+
let pendingWrites = watcher.pendingWrites;
|
|
949
|
+
for (let childPath of pendingWrites.keys()) {
|
|
950
|
+
if (childPath.length > pathStr.length && childPath.startsWith(pathStr)) {
|
|
951
|
+
if (!copiedChildPaths) {
|
|
952
|
+
copiedChildPaths = childPaths = new Set(childPaths);
|
|
953
|
+
}
|
|
954
|
+
copiedChildPaths.add(childPath);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
let keys = new Set<string>();
|
|
960
|
+
if (childPaths) {
|
|
961
|
+
let targetDepth = getPathDepth(pathStr);
|
|
962
|
+
for (let childPath of childPaths) {
|
|
963
|
+
let key = getPathIndexAssert(childPath, targetDepth);
|
|
964
|
+
let childValue = this.getCallback(childPath, undefined, "readTransparent");
|
|
965
|
+
if (childValue?.value !== undefined) {
|
|
966
|
+
keys.add(key);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
let keysArray = Array.from(keys);
|
|
972
|
+
|
|
973
|
+
// NOTE: Because getPathsFromParent does not preserve order, we have to sort here to ensure
|
|
974
|
+
// we provide a consistent order (which might not be the order the user wants, but at least
|
|
975
|
+
// it will always fail if it does fail).
|
|
976
|
+
keysArray.sort();
|
|
977
|
+
|
|
978
|
+
return keysArray;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
private getSymbol = (pathStr: string, symbol: symbol): { value: unknown } | undefined => {
|
|
982
|
+
if (symbol === Symbol.toPrimitive) return {
|
|
983
|
+
value: (hint: string) => {
|
|
984
|
+
if (hint === "string") return "";
|
|
985
|
+
if (hint === "number") return 0;
|
|
986
|
+
// NOTE: Returning 0 as the default makes (x++) work nicely, defaulting values to undefined.
|
|
987
|
+
// If they are expecting a string... maybe they should use `${x}`, or... just type check it.
|
|
988
|
+
return 0;
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
if (symbol === Symbol.toStringTag) return { value: () => `[proxy at ${pathStr}]` };
|
|
992
|
+
|
|
993
|
+
if (symbol === readNoProxy) {
|
|
994
|
+
let value = this.getCallback(pathStr)?.value;
|
|
995
|
+
return { value };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (symbol === syncedSymbol) {
|
|
999
|
+
return { value: authorityStorage.isSynced(pathStr) };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Proxies should be considered atomic, at least for the purpose of other proxies!
|
|
1003
|
+
// - This allows proxies to be written to synced state easily (although this will
|
|
1004
|
+
// cause issues if it isn't local synced state, but... there's not too much
|
|
1005
|
+
// we can do about that. It should just serialize it, possibly reading values
|
|
1006
|
+
// which weren't synced. Which isn't so bad, as if they start trying to create
|
|
1007
|
+
// object graphs in synced state they are going to immediately run into issues
|
|
1008
|
+
// anyways).
|
|
1009
|
+
if (symbol === atomicObjectSymbol) return { value: true };
|
|
1010
|
+
|
|
1011
|
+
if (symbol === rawRead) {
|
|
1012
|
+
return { value: this.getCallback(pathStr, undefined, "readTransparent")?.value };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return { value: undefined };
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
private proxy = createPathValueProxy({
|
|
1019
|
+
getCallback: this.getCallback,
|
|
1020
|
+
hasCallback: this.hasCallback,
|
|
1021
|
+
setCallback: this.setCallback,
|
|
1022
|
+
getKeys: this.getKeys,
|
|
1023
|
+
getSymbol: this.getSymbol,
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
private runningWatcher: SyncWatcher | undefined;
|
|
1027
|
+
|
|
1028
|
+
// NOTE: We can't support promises until we are able to run the function on another
|
|
1029
|
+
// thread (at which point we can ensure there is no parallel function running).
|
|
1030
|
+
public createWatcher<Result = void>(
|
|
1031
|
+
options: WatcherOptions<Result>
|
|
1032
|
+
): SyncWatcher {
|
|
1033
|
+
if (isAsyncFunction(options.watchFunction)) {
|
|
1034
|
+
throw new Error(`A watcher function cannot be async, it must be synchronous. You probably did Querysub.commitAsync(async () => {}). Just remove the async, and do Querysub.commit(() => {}). The caller will be called again whenever the data you access changes, And if you are running this to return a result, it will be rerun until all the data you want is synchronized. Watch function: ${options.watchFunction.toString()}`);
|
|
1035
|
+
}
|
|
1036
|
+
// NOTE: Setting an order is needed for rendering, so parents render before children. I believe
|
|
1037
|
+
// it is generally what we want, so event triggering is consistent, and fits with any tree based
|
|
1038
|
+
// watching system. If this causes problems we COULD remove it from here and have just qreact.tsx set it.
|
|
1039
|
+
if (options.order === undefined) {
|
|
1040
|
+
options.order = nextOrderSeqNum++;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Unwrap nested immediate calls to be as close to a naked call as possible. The watcher never becomes
|
|
1044
|
+
// the current watcher, and is immediately disposed. We only create SyncWatcher to return it, otherwise
|
|
1045
|
+
// callers would need to handle a variable return type, which is annoying.
|
|
1046
|
+
// - This means any writes are attributed to the existing call, which... is fine, if the caller wants
|
|
1047
|
+
// to disconnect the call from the existing watcher, they should use `Promise.resolve().finally(...)`.
|
|
1048
|
+
// NOTE: We check for runImmediately as we still want to be able to create watchers in
|
|
1049
|
+
// reaction to trigger of other watchers.
|
|
1050
|
+
if (this.runningWatcher && (options.runImmediately || this.runningWatcher.options.inlineNestedWatchers)) {
|
|
1051
|
+
// TODO: We COULD change our dispose out and change it back, if we find
|
|
1052
|
+
// we often want to mutate shallow props of nested watchers.
|
|
1053
|
+
const outerWatcher = {
|
|
1054
|
+
...this.runningWatcher,
|
|
1055
|
+
// Let them use the watcher, EXCEPT, don't let them dispose it,
|
|
1056
|
+
// as that would dispose the outer watcher as well
|
|
1057
|
+
dispose: () => { },
|
|
1058
|
+
};
|
|
1059
|
+
if (!this.runningWatcher.options.canWrite && options.canWrite) {
|
|
1060
|
+
debugger;
|
|
1061
|
+
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(...)`);
|
|
1062
|
+
}
|
|
1063
|
+
// NOTE: Nested watchers are fine. We need to apply the nested options
|
|
1064
|
+
// (some of which will be ignroed), but otherwise, all of the watchers
|
|
1065
|
+
// will be collapsed to a single watcher, making it fairly efficient.
|
|
1066
|
+
let { watchFunction, ...mostOptions } = options;
|
|
1067
|
+
doProxyOptions(mostOptions, () => {
|
|
1068
|
+
try {
|
|
1069
|
+
let result = authorityStorage.temporaryOverride(options.overrides, () =>
|
|
1070
|
+
options.watchFunction()
|
|
1071
|
+
);
|
|
1072
|
+
// Clone, otherwise proxies get out of the watcher, which can result in accesses outside
|
|
1073
|
+
// of a synced watcher.
|
|
1074
|
+
if (!options.allowProxyResults) {
|
|
1075
|
+
result = deepCloneCborx(result);
|
|
1076
|
+
} else {
|
|
1077
|
+
result = atomicObjectRead(result);
|
|
1078
|
+
}
|
|
1079
|
+
options.onResultUpdated?.({ result }, undefined, outerWatcher);
|
|
1080
|
+
} catch (e: any) {
|
|
1081
|
+
options.onResultUpdated?.({ error: e.stack }, undefined, outerWatcher);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
return outerWatcher;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const self = this;
|
|
1088
|
+
if (options.runAtTime) {
|
|
1089
|
+
// 80% of our real max range, so calls with a runAtTime close to our limit have a bit of breathing room to run
|
|
1090
|
+
let runLeeway = MAX_ACCEPTED_CHANGE_AGE * 0.8;
|
|
1091
|
+
let runCutoffTime = Date.now() - runLeeway;
|
|
1092
|
+
if (options.runAtTime && options.runAtTime.time < runCutoffTime) {
|
|
1093
|
+
let message = `MAX_CHANGE_AGE_EXCEEDED! Cannot run watcher ${options.debugName} 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.`;
|
|
1094
|
+
console.error(red(message));
|
|
1095
|
+
// NOTE: We could also adjust the to be more recent, to allow it to be commited anyway,
|
|
1096
|
+
// in a slightly different state than originally expected.
|
|
1097
|
+
throw new Error(message);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
let now = Date.now();
|
|
1102
|
+
let debugName = (
|
|
1103
|
+
options.debugName
|
|
1104
|
+
|| options.watchFunction.name
|
|
1105
|
+
|| options.watchFunction.toString().replaceAll("\n", "\\n").replaceAll(/ +/g, " ").slice(0, 50)
|
|
1106
|
+
);
|
|
1107
|
+
if (debugName === "watchFunction" && options.baseFunction) {
|
|
1108
|
+
debugName = options.baseFunction.name || options.baseFunction.toString().replaceAll("\n", "\\n").replaceAll(/ +/g, " ").slice(0, 50);
|
|
1109
|
+
}
|
|
1110
|
+
let watcher: SyncWatcher = {
|
|
1111
|
+
seqNum: nextSeqNum++,
|
|
1112
|
+
debugName,
|
|
1113
|
+
dispose: () => { },
|
|
1114
|
+
disposed: false,
|
|
1115
|
+
onInnerDisposed: [],
|
|
1116
|
+
onAfterTriggered: [],
|
|
1117
|
+
explicitlyTrigger: () => { },
|
|
1118
|
+
hasAnyUnsyncedAccesses: () => false,
|
|
1119
|
+
currentReadTime: options.runAtTime,
|
|
1120
|
+
nextWriteTime: undefined,
|
|
1121
|
+
setCurrentReadTime: null as any,
|
|
1122
|
+
pendingAccesses: new Map(),
|
|
1123
|
+
pendingEpochAccesses: new Map(),
|
|
1124
|
+
triggeredByChanges: undefined,
|
|
1125
|
+
pendingWrites: new Map(),
|
|
1126
|
+
pendingEventWrites: new Set(),
|
|
1127
|
+
pendingCalls: [],
|
|
1128
|
+
pendingWatches: {
|
|
1129
|
+
paths: new Set(),
|
|
1130
|
+
parentPaths: new Set(),
|
|
1131
|
+
},
|
|
1132
|
+
lastWatches: {
|
|
1133
|
+
paths: new Set(),
|
|
1134
|
+
parentPaths: new Set(),
|
|
1135
|
+
},
|
|
1136
|
+
|
|
1137
|
+
pendingUnsyncedAccesses: new Set(),
|
|
1138
|
+
pendingUnsyncedParentAccesses: new Set(),
|
|
1139
|
+
lastUnsyncedAccesses: new Set(),
|
|
1140
|
+
lastUnsyncedParentAccesses: new Set(),
|
|
1141
|
+
|
|
1142
|
+
specialPromiseUnsynced: false,
|
|
1143
|
+
lastSpecialPromiseUnsynced: false,
|
|
1144
|
+
|
|
1145
|
+
options,
|
|
1146
|
+
consecutiveErrors: 0,
|
|
1147
|
+
countSinceLastFullSync: 0,
|
|
1148
|
+
lastEvalTime: 0,
|
|
1149
|
+
lastSyncTime: now,
|
|
1150
|
+
startTime: now,
|
|
1151
|
+
syncRunCount: 0,
|
|
1152
|
+
tag: new SyncWatcherTag(),
|
|
1153
|
+
|
|
1154
|
+
hackHistory: [],
|
|
1155
|
+
createTime: getTimeUnique(),
|
|
1156
|
+
};
|
|
1157
|
+
addToProxyOrder(watcher);
|
|
1158
|
+
const SHOULD_TRACE = this.SHOULD_TRACE(watcher);
|
|
1159
|
+
const proxy = this.proxy;
|
|
1160
|
+
|
|
1161
|
+
if (options.overrides && !options.dryRun) {
|
|
1162
|
+
// If you override values, and want to watch values... how does that even work? And what about
|
|
1163
|
+
// writes? You can't depend on an override, so... what would we do?
|
|
1164
|
+
throw new Error(`overrides without dryRun is not presently supported`);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
watcher.dispose = dispose;
|
|
1168
|
+
this.allWatchers.add(watcher);
|
|
1169
|
+
|
|
1170
|
+
let baseFunction = options.watchFunction;
|
|
1171
|
+
baseFunction = measureWrap(baseFunction, watcher.debugName);
|
|
1172
|
+
{
|
|
1173
|
+
let base = baseFunction;
|
|
1174
|
+
baseFunction = (...args: unknown[]) => {
|
|
1175
|
+
let time = Date.now();
|
|
1176
|
+
try {
|
|
1177
|
+
return (base as any)(...args);
|
|
1178
|
+
} finally {
|
|
1179
|
+
onTimeProfile("Performance|Watcher", time);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
watcher.setCurrentReadTime = (time, code) => {
|
|
1185
|
+
let prev = watcher.currentReadTime;
|
|
1186
|
+
watcher.currentReadTime = time;
|
|
1187
|
+
try {
|
|
1188
|
+
return code();
|
|
1189
|
+
} finally {
|
|
1190
|
+
watcher.currentReadTime = prev;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
watcher.hasAnyUnsyncedAccesses = () => {
|
|
1194
|
+
if (watcher.options.waitForIncompleteTransactions) {
|
|
1195
|
+
waitIfReceivedIncompleteTransaction();
|
|
1196
|
+
}
|
|
1197
|
+
return (
|
|
1198
|
+
watcher.pendingUnsyncedAccesses.size > 0
|
|
1199
|
+
|| watcher.pendingUnsyncedParentAccesses.size > 0
|
|
1200
|
+
|| watcher.specialPromiseUnsynced
|
|
1201
|
+
|| isProxyBlockedByOrder(watcher)
|
|
1202
|
+
);
|
|
1203
|
+
};
|
|
1204
|
+
const getReadyToCommit = () => {
|
|
1205
|
+
if (watcher.options.commitAllRuns) {
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
return (
|
|
1209
|
+
watcher.lastUnsyncedAccesses.size === 0
|
|
1210
|
+
&& watcher.lastUnsyncedParentAccesses.size === 0
|
|
1211
|
+
&& !watcher.lastSpecialPromiseUnsynced
|
|
1212
|
+
&& !isProxyBlockedByOrder(watcher)
|
|
1213
|
+
);
|
|
1214
|
+
};
|
|
1215
|
+
function afterTrigger() {
|
|
1216
|
+
for (let i = 0; i < 10; i++) {
|
|
1217
|
+
let callbacks = watcher.onAfterTriggered;
|
|
1218
|
+
watcher.onAfterTriggered = [];
|
|
1219
|
+
for (let callback of callbacks) {
|
|
1220
|
+
logErrors(((async () => { await callback(); }))());
|
|
1221
|
+
}
|
|
1222
|
+
if (watcher.onAfterTriggered.length === 0) break;
|
|
1223
|
+
}
|
|
1224
|
+
if (watcher.onAfterTriggered.length > 0) {
|
|
1225
|
+
console.warn(`Watcher ${watcher.debugName} keeps adding after trigger callbacks during after trigger. We reached our iteration limit
|
|
1226
|
+
and will be ignoring triggers remaining triggers.`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function dispose() {
|
|
1231
|
+
if (watcher.disposed) return;
|
|
1232
|
+
watcher.disposed = true;
|
|
1233
|
+
clientWatcher.unwatch(trigger);
|
|
1234
|
+
self.allWatchers.delete(watcher);
|
|
1235
|
+
|
|
1236
|
+
// NOTE: We trigger next before onInnerDisposed, as onInnerDispose often run more commits, and if we proxy block it causes proxies to run twice.
|
|
1237
|
+
// TODO: Is this guaranteed to be called all the time? What about nested proxy watchers?
|
|
1238
|
+
void finishProxyAndTriggerNext(watcher);
|
|
1239
|
+
|
|
1240
|
+
for (let i = 0; i < 10; i++) {
|
|
1241
|
+
let disposeCallbacks = watcher.onInnerDisposed;
|
|
1242
|
+
watcher.onInnerDisposed = [];
|
|
1243
|
+
for (let callback of disposeCallbacks) {
|
|
1244
|
+
logErrors(((async () => { await callback(); }))());
|
|
1245
|
+
}
|
|
1246
|
+
if (watcher.onInnerDisposed.length === 0) break;
|
|
1247
|
+
}
|
|
1248
|
+
if (watcher.onInnerDisposed.length > 0) {
|
|
1249
|
+
console.warn(`Watcher ${watcher.debugName} keeps adding inner disposed callbacks during dispose. We reached our iteration limit
|
|
1250
|
+
and will be ignoring dispose callbacks remaining dispose callbacks.`);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
self.allWatchersLookup.delete(trigger);
|
|
1254
|
+
}
|
|
1255
|
+
function runWatcher() {
|
|
1256
|
+
watcher.pendingWrites.clear();
|
|
1257
|
+
watcher.pendingEventWrites.clear();
|
|
1258
|
+
watcher.pendingCalls = [];
|
|
1259
|
+
// NOTE: We have to make new sets here, because these Sets are reused for lastWatches, which makes
|
|
1260
|
+
// it's way into clientWatcher, without changing. So if we just clear it, we can break clientWatcher.
|
|
1261
|
+
watcher.pendingWatches.paths = new Set();
|
|
1262
|
+
watcher.pendingWatches.parentPaths = new Set();
|
|
1263
|
+
watcher.pendingUnsyncedAccesses.clear();
|
|
1264
|
+
watcher.pendingUnsyncedParentAccesses.clear();
|
|
1265
|
+
|
|
1266
|
+
watcher.pendingAccesses.clear();
|
|
1267
|
+
watcher.pendingEpochAccesses.clear();
|
|
1268
|
+
// IMPORTANT! Reset onInnerDisposed, so onCommitFinished doesn't result in a callback
|
|
1269
|
+
// per time we wan the watcher!
|
|
1270
|
+
watcher.onInnerDisposed = [];
|
|
1271
|
+
watcher.specialPromiseUnsynced = false;
|
|
1272
|
+
|
|
1273
|
+
// NOTE: If runAtTime is undefined, the writeTime will be undefined, causing us to read the latest data.
|
|
1274
|
+
// When we finish running we will determine a lock time > any received times.
|
|
1275
|
+
// - 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
|
|
1276
|
+
// differences. And we can't just set it arbitrarily into the future, as then we will be writing
|
|
1277
|
+
// in the future, which would cause us to become rejected on writes that happen after our real
|
|
1278
|
+
// time (but after our given time).
|
|
1279
|
+
watcher.nextWriteTime = watcher.options.runAtTime;
|
|
1280
|
+
// Ensure nextWriteTime is a unique time every run, in case we were rerun due to a rejection (unless it is a dryRun,
|
|
1281
|
+
// in which case the writes won't be committed, so they don't need to be unique).
|
|
1282
|
+
|
|
1283
|
+
// 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),
|
|
1284
|
+
// then when we receive a value we will ignore it.
|
|
1285
|
+
if (!watcher.options.dryRun && watcher.nextWriteTime) {
|
|
1286
|
+
watcher.nextWriteTime = clientWatcher.getFreeWriteTime(watcher.nextWriteTime);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// The read time equals the write time, so our locks use the same time as our write time, and
|
|
1290
|
+
// therefore lock the actual state we used.
|
|
1291
|
+
watcher.currentReadTime = watcher.nextWriteTime;
|
|
1292
|
+
|
|
1293
|
+
watcher.permissionsChecker = options.getPermissionsCheck?.();
|
|
1294
|
+
|
|
1295
|
+
let result: { result: Result } | { error: string };
|
|
1296
|
+
try {
|
|
1297
|
+
let rawResult: Result;
|
|
1298
|
+
const handling = watcher.options.nestedCalls;
|
|
1299
|
+
let curFunction = baseFunction;
|
|
1300
|
+
if (handling === "inline") {
|
|
1301
|
+
curFunction = () => inlineNestedCalls(baseFunction);
|
|
1302
|
+
}
|
|
1303
|
+
rawResult = interceptCalls({
|
|
1304
|
+
onCall(call, metadata) {
|
|
1305
|
+
if (PathValueProxyWatcher.BREAK_ON_CALL.size > 0 && !watcher.hasAnyUnsyncedAccesses()) {
|
|
1306
|
+
let hash = getPathStr2(call.ModuleId, call.FunctionId);
|
|
1307
|
+
if (PathValueProxyWatcher.BREAK_ON_CALL.has(hash)) {
|
|
1308
|
+
const unwatch = () => removeMatches(PathValueProxyWatcher.BREAK_ON_CALL, hash);
|
|
1309
|
+
debugger;
|
|
1310
|
+
unwatch;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (!watcher.options.canWrite) {
|
|
1314
|
+
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.`);
|
|
1315
|
+
}
|
|
1316
|
+
if (handling === "throw") {
|
|
1317
|
+
// TODO: Support making inline calls, which is useful as it will check permissions. Although, it can
|
|
1318
|
+
// easily be slow, and adds a lot of complexity, so... maybe not... maybe always force the app
|
|
1319
|
+
// to do the permissions checks if they want them
|
|
1320
|
+
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.`);
|
|
1321
|
+
} else if (handling === "after" || handling === undefined) {
|
|
1322
|
+
// UPDATE: This isn't feasible. For one, it has n squared complexity. Also, I don't think it would work in the case if you make two function calls within one function? So it seems like it would only ever work if the function calls were in different functions?
|
|
1323
|
+
// UPDATE: Use `if (Querysub.syncAnyPredictionsPending()) return;` at the start of a function if you want to emulate this behavior
|
|
1324
|
+
// - I think the solution for this is that the second function itself needs to wait for all predictions to finish. It's annoying, but I think it's required...
|
|
1325
|
+
// // We need to wait for predictions to finish, otherwise we run into situations
|
|
1326
|
+
// // where we call a function which should change a parameter we want to pass
|
|
1327
|
+
// // to another function, but because the first call didn't predict, the second
|
|
1328
|
+
// // call gets a different values, causing all kinds of issues.
|
|
1329
|
+
// if (watcher.pendingCalls.length === 0) {
|
|
1330
|
+
// let waitPromise = onAllPredictionsFinished();
|
|
1331
|
+
// if (waitPromise) {
|
|
1332
|
+
// proxyWatcher.triggerOnPromiseFinish(waitPromise, {
|
|
1333
|
+
// waitReason: "Waiting for predictions to finish",
|
|
1334
|
+
// });
|
|
1335
|
+
// }
|
|
1336
|
+
// }
|
|
1337
|
+
|
|
1338
|
+
watcher.pendingCalls.push({ call, metadata });
|
|
1339
|
+
} else if (handling === "ignore") {
|
|
1340
|
+
} else if (handling === "inline") {
|
|
1341
|
+
throw new Error(`inlineNestedCalls should have prevented this call from being passed to us`);
|
|
1342
|
+
} else {
|
|
1343
|
+
let unhandled: never = handling;
|
|
1344
|
+
throw new Error(`Invalid handling type ${handling}`);
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
code() {
|
|
1348
|
+
return authorityStorage.temporaryOverride(options.overrides, () =>
|
|
1349
|
+
runCodeWithDatabase(proxy, baseFunction)
|
|
1350
|
+
);
|
|
1351
|
+
},
|
|
1352
|
+
}) as Result;
|
|
1353
|
+
|
|
1354
|
+
// NOTE: Deep clone non-local paths. It's too confusing for a partial object to be returned. Any issues caused by
|
|
1355
|
+
// this (aka, returning the entire database), would also happen if we manually added deep clones before
|
|
1356
|
+
// we returned values (and both should be throttled at a framework level so we don't break the database).
|
|
1357
|
+
// - For local paths there is a risk that there are functions
|
|
1358
|
+
if (!options.allowProxyResults) {
|
|
1359
|
+
try {
|
|
1360
|
+
rawResult = deepCloneCborx(rawResult);
|
|
1361
|
+
} catch {
|
|
1362
|
+
// Unfortunately, cborx throws on functions.
|
|
1363
|
+
// TODO: Use a recursive clone technique that doesn't throw on functions
|
|
1364
|
+
rawResult = atomicObjectRead(rawResult);
|
|
1365
|
+
}
|
|
1366
|
+
} else {
|
|
1367
|
+
rawResult = atomicObjectRead(rawResult);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
result = { result: rawResult };
|
|
1371
|
+
watcher.consecutiveErrors = 0;
|
|
1372
|
+
} catch (e: any) {
|
|
1373
|
+
watcher.consecutiveErrors++;
|
|
1374
|
+
if (watcher.consecutiveErrors > 10) {
|
|
1375
|
+
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.`);
|
|
1376
|
+
console.error(e);
|
|
1377
|
+
}
|
|
1378
|
+
result = { error: e.stack };
|
|
1379
|
+
} finally {
|
|
1380
|
+
afterTrigger();
|
|
1381
|
+
// Set the checker to undefined, so we don't accidentally use it again,
|
|
1382
|
+
// as it will throw if we try to use it after any asynchronous delay.
|
|
1383
|
+
watcher.permissionsChecker = undefined;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
let anyUnsynced = watcher.hasAnyUnsyncedAccesses();
|
|
1388
|
+
|
|
1389
|
+
watcher.lastUnsyncedAccesses = watcher.pendingUnsyncedAccesses;
|
|
1390
|
+
watcher.lastUnsyncedParentAccesses = watcher.pendingUnsyncedParentAccesses;
|
|
1391
|
+
watcher.lastSpecialPromiseUnsynced = watcher.specialPromiseUnsynced;
|
|
1392
|
+
|
|
1393
|
+
// TODO: Add promise delays as well as the finish order reordering delays to the sync timings.
|
|
1394
|
+
if (watcher.options.logSyncTimings) {
|
|
1395
|
+
if (anyUnsynced) {
|
|
1396
|
+
watcher.logSyncTimings = watcher.logSyncTimings || {
|
|
1397
|
+
startTime: Date.now(),
|
|
1398
|
+
unsyncedPaths: new Set(),
|
|
1399
|
+
unsyncedStages: [],
|
|
1400
|
+
};
|
|
1401
|
+
let newPaths: string[] = [];
|
|
1402
|
+
let set = watcher.logSyncTimings.unsyncedPaths;
|
|
1403
|
+
function addPath(path: string) {
|
|
1404
|
+
if (set.has(path)) return;
|
|
1405
|
+
set.add(path);
|
|
1406
|
+
newPaths.push(path);
|
|
1407
|
+
}
|
|
1408
|
+
for (let path of watcher.lastUnsyncedAccesses) {
|
|
1409
|
+
addPath(path);
|
|
1410
|
+
}
|
|
1411
|
+
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
1412
|
+
addPath(path);
|
|
1413
|
+
}
|
|
1414
|
+
if (newPaths.length > 0) {
|
|
1415
|
+
watcher.logSyncTimings.unsyncedStages.push({
|
|
1416
|
+
paths: newPaths,
|
|
1417
|
+
time: Date.now(),
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
} else {
|
|
1421
|
+
if (watcher.logSyncTimings) {
|
|
1422
|
+
let now = Date.now();
|
|
1423
|
+
console.groupCollapsed(`${watcher.debugName} synced in ${formatTime(now - watcher.logSyncTimings.startTime)}`);
|
|
1424
|
+
// Log the stages
|
|
1425
|
+
let stages = watcher.logSyncTimings.unsyncedStages;
|
|
1426
|
+
for (let i = 0; i < stages.length; i++) {
|
|
1427
|
+
let nextTime = stages[i + 1]?.time || now;
|
|
1428
|
+
let stage = stages[i];
|
|
1429
|
+
console.groupCollapsed(`${formatTime(nextTime - stage.time)}`);
|
|
1430
|
+
for (let path of stage.paths) {
|
|
1431
|
+
console.log(path);
|
|
1432
|
+
}
|
|
1433
|
+
console.groupEnd();
|
|
1434
|
+
}
|
|
1435
|
+
console.groupEnd();
|
|
1436
|
+
watcher.logSyncTimings = undefined;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (!watcher.options.static) {
|
|
1442
|
+
if (!anyUnsynced) {
|
|
1443
|
+
watcher.countSinceLastFullSync = 0;
|
|
1444
|
+
watcher.lastSyncTime = Date.now();
|
|
1445
|
+
watcher.syncRunCount++;
|
|
1446
|
+
} else {
|
|
1447
|
+
if (watcher.countSinceLastFullSync === 0) {
|
|
1448
|
+
let syncRunCount = watcher.syncRunCount;
|
|
1449
|
+
setTimeout(() => {
|
|
1450
|
+
if (watcher.syncRunCount !== syncRunCount) return;
|
|
1451
|
+
if (watcher.disposed) return;
|
|
1452
|
+
let remainingUnsynced = Array.from(watcher.lastUnsyncedAccesses).filter(x => !authorityStorage.isSynced(x));
|
|
1453
|
+
let remainingUnsyncedParent = Array.from(watcher.lastUnsyncedParentAccesses).filter(x => !authorityStorage.isParentSynced(x));
|
|
1454
|
+
console.warn(red(`Did not sync ${watcher.debugName} after 30 seconds. Either we had a lot synchronous block, or we will never received the values we are waiting for.`), remainingUnsynced, remainingUnsyncedParent, watcher.options.watchFunction);
|
|
1455
|
+
}, 30 * 1000);
|
|
1456
|
+
setTimeout(() => {
|
|
1457
|
+
if (watcher.syncRunCount !== syncRunCount) return;
|
|
1458
|
+
if (watcher.disposed) return;
|
|
1459
|
+
|
|
1460
|
+
// IMPORTANT! Any of these can be also caused by high synchronous lag!
|
|
1461
|
+
// - If this becomes a serious concern, we could add a watch loop for this, which
|
|
1462
|
+
// tries to run every second, recording how far it misses the target time by,
|
|
1463
|
+
// and then uses that (plus any outstanding missed time) to determine how much lag
|
|
1464
|
+
// we have had since this watcher last synced. If it is > 50% of the time...
|
|
1465
|
+
// then synchronous lag is the issue.
|
|
1466
|
+
|
|
1467
|
+
let reallyUnsyncedAccesses = Array.from(watcher.lastUnsyncedAccesses).filter(x => !authorityStorage.isSynced(x));
|
|
1468
|
+
let reallyUnsyncedParentAccesses = Array.from(watcher.lastUnsyncedParentAccesses).filter(x => !authorityStorage.isParentSynced(x));
|
|
1469
|
+
|
|
1470
|
+
if (reallyUnsyncedAccesses.length !== 0 || reallyUnsyncedParentAccesses.length !== 0) {
|
|
1471
|
+
let notWatchingUnsynced = reallyUnsyncedAccesses.filter(x => !remoteWatcher.debugIsWatchingPath(x));
|
|
1472
|
+
let notWatchingUnsyncedParent = reallyUnsyncedParentAccesses.filter(x => !remoteWatcher.debugIsWatchingPath(x));
|
|
1473
|
+
if (notWatchingUnsynced.length !== 0 || notWatchingUnsyncedParent.length !== 0) {
|
|
1474
|
+
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);
|
|
1475
|
+
debugbreak(2);
|
|
1476
|
+
debugger;
|
|
1477
|
+
} else {
|
|
1478
|
+
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);
|
|
1479
|
+
// debugbreak(2);
|
|
1480
|
+
// debugger;
|
|
1481
|
+
}
|
|
1482
|
+
} else if (watcher.lastSpecialPromiseUnsynced) {
|
|
1483
|
+
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);
|
|
1484
|
+
debugbreak(2);
|
|
1485
|
+
debugger;
|
|
1486
|
+
} else {
|
|
1487
|
+
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);
|
|
1488
|
+
debugbreak(2);
|
|
1489
|
+
debugger;
|
|
1490
|
+
}
|
|
1491
|
+
}, 60000);
|
|
1492
|
+
}
|
|
1493
|
+
watcher.countSinceLastFullSync++;
|
|
1494
|
+
if (watcher.countSinceLastFullSync > 10) {
|
|
1495
|
+
require("debugbreak")(2);
|
|
1496
|
+
debugger;
|
|
1497
|
+
console.warn(`Watcher ${watcher.debugName} has been unsynced for ${watcher.countSinceLastFullSync} times. This is fine, but maybe optimize it. Why is it cascading?`, watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses, watcher.options.watchFunction);
|
|
1498
|
+
}
|
|
1499
|
+
if (watcher.countSinceLastFullSync > 500) {
|
|
1500
|
+
debugger;
|
|
1501
|
+
// NOTE: Using forceEqualWrites will also fix this a lot of the time, such as when
|
|
1502
|
+
// a write contains random numbers or dates.
|
|
1503
|
+
let errorMessage = `Too many attempts (${watcher.countSinceLastFullSync}) 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.`;
|
|
1504
|
+
if (watcher.lastSpecialPromiseUnsynced) {
|
|
1505
|
+
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!)`;
|
|
1506
|
+
}
|
|
1507
|
+
errorMessage += ` (${watcher.debugName}})`;
|
|
1508
|
+
console.error(red(errorMessage));
|
|
1509
|
+
logUnsynced();
|
|
1510
|
+
result = { error: new Error(errorMessage).stack || "" };
|
|
1511
|
+
// Force the watches to be equal, so we stop looping
|
|
1512
|
+
watcher.pendingWatches.paths = new Set(watcher.lastWatches.paths);
|
|
1513
|
+
watcher.pendingWatches.parentPaths = new Set(watcher.lastWatches.parentPaths);
|
|
1514
|
+
watcher.syncRunCount++;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
return result;
|
|
1520
|
+
}
|
|
1521
|
+
function updateWatchers() {
|
|
1522
|
+
watcher.lastWatches = {
|
|
1523
|
+
paths: watcher.pendingWatches.paths,
|
|
1524
|
+
parentPaths: watcher.pendingWatches.parentPaths,
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
// NOTE: I don't see the reason for this. How many watchers do we have that aren't watching anything?
|
|
1528
|
+
// If nothing is being watched, and nothing was... then don't bother updating the watchers
|
|
1529
|
+
// if (
|
|
1530
|
+
// !initialTriggerWatch
|
|
1531
|
+
// && watcher.lastWatches.paths.size === 0 && watcher.lastWatches.parentPaths.size === 0
|
|
1532
|
+
// && watcher.pendingWatches.paths.size === 0 && watcher.pendingWatches.parentPaths.size === 0
|
|
1533
|
+
// ) return;
|
|
1534
|
+
|
|
1535
|
+
clientWatcher.setWatches({
|
|
1536
|
+
debugName: watcher.debugName,
|
|
1537
|
+
// We don't need to trigger immediately, as we already ran, and if all of our values were synced
|
|
1538
|
+
// we will have already committed any results.
|
|
1539
|
+
noInitialTrigger: true,
|
|
1540
|
+
paths: watcher.lastWatches.paths,
|
|
1541
|
+
parentPaths: watcher.lastWatches.parentPaths,
|
|
1542
|
+
order: watcher.options.order,
|
|
1543
|
+
orderGroup: watcher.options.orderGroup,
|
|
1544
|
+
callback: trigger,
|
|
1545
|
+
unwatchEventPaths(paths) {
|
|
1546
|
+
self.unwatchEventPaths(watcher, paths);
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
function commitWrites(): PathValue[] | undefined {
|
|
1551
|
+
if (!options.canWrite) return undefined;
|
|
1552
|
+
|
|
1553
|
+
let setValues: PathValue[] | undefined;
|
|
1554
|
+
|
|
1555
|
+
let prefixes = options.overrideAllowLockDomainsPrefixes || getAllowedLockDomainsPrefixes();
|
|
1556
|
+
// This consumes our write time, as once we write, we can't reuse writeTimes!
|
|
1557
|
+
// (If we do, it breaks dependencies).
|
|
1558
|
+
let writeTime = watcher.nextWriteTime;
|
|
1559
|
+
watcher.nextWriteTime = undefined;
|
|
1560
|
+
if (!writeTime) {
|
|
1561
|
+
writeTime = getNextTime();
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (
|
|
1565
|
+
!watcher.options.hasSideEffects
|
|
1566
|
+
&& watcher.pendingWrites.size === 0
|
|
1567
|
+
&& watcher.pendingCalls.length === 0
|
|
1568
|
+
) {
|
|
1569
|
+
wastedEvaluations++;
|
|
1570
|
+
}
|
|
1571
|
+
evaluations++;
|
|
1572
|
+
|
|
1573
|
+
if (watcher.pendingWrites.size > 0) {
|
|
1574
|
+
let locks: ReadLock[] = [];
|
|
1575
|
+
let historyBasedReads = false;
|
|
1576
|
+
for (let time of watcher.pendingAccesses.keys()) {
|
|
1577
|
+
if (time === undefined) continue;
|
|
1578
|
+
// If we're reading from a specific time, and it isn't just our write time, then we're reading in the past.
|
|
1579
|
+
if (compareTime(time, writeTime) !== 0) {
|
|
1580
|
+
historyBasedReads = true;
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (!watcher.options.noLocks && !watcher.options.unsafeNoLocks) {
|
|
1586
|
+
for (let [readTime, values] of watcher.pendingAccesses) {
|
|
1587
|
+
if (!readTime) {
|
|
1588
|
+
readTime = writeTime;
|
|
1589
|
+
}
|
|
1590
|
+
for (let valueObj of values.values()) {
|
|
1591
|
+
if (valueObj.noLocks) continue;
|
|
1592
|
+
let value = valueObj.pathValue;
|
|
1593
|
+
// If any value has no locks, AND is local, it won't be rejected, so we don't need to lock it
|
|
1594
|
+
if (value.lockCount === 0 && value.path.startsWith(LOCAL_DOMAIN_PATH)) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
if (!prefixes.some(x => value.path.startsWith(x))) continue;
|
|
1598
|
+
let newReadCheck = !!value.isTransparent;
|
|
1599
|
+
if (value.canGCValue && !value.isTransparent) {
|
|
1600
|
+
console.error(`Value is GCable, but not transparent. We likely forgot to set isTransparent when setting canGCValue. Path ${value.path}, value: ${String(pathValueSerializer.getPathValue(value))}`);
|
|
1601
|
+
debugbreak(2);
|
|
1602
|
+
debugger;
|
|
1603
|
+
}
|
|
1604
|
+
locks.push({
|
|
1605
|
+
path: value.path,
|
|
1606
|
+
startTime: value.time,
|
|
1607
|
+
endTime: readTime,
|
|
1608
|
+
readIsTransparent: newReadCheck,
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
for (let [readTime, paths] of watcher.pendingEpochAccesses) {
|
|
1613
|
+
if (!readTime) {
|
|
1614
|
+
readTime = writeTime;
|
|
1615
|
+
}
|
|
1616
|
+
for (let path of paths) {
|
|
1617
|
+
if (!prefixes.some(x => path.startsWith(x))) continue;
|
|
1618
|
+
locks.push({
|
|
1619
|
+
path,
|
|
1620
|
+
startTime: epochTime,
|
|
1621
|
+
endTime: readTime,
|
|
1622
|
+
readIsTransparent: true,
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
{
|
|
1629
|
+
// Ensure our write time isn't before any of our reads (as long as we aren't running
|
|
1630
|
+
// at a specific time).
|
|
1631
|
+
// - We might want an option to disable this behavior? Although, I'm not sure how we would
|
|
1632
|
+
// run in the present, but intentionally want to write in the past? This usually only
|
|
1633
|
+
// happens if we intentionally run in the past (in which case writeTime will be set,
|
|
1634
|
+
// so this if statement wouldn't run anyways).
|
|
1635
|
+
let fixedWriteTime = writeTime;
|
|
1636
|
+
for (let lock of locks) {
|
|
1637
|
+
if (compareTime(lock.endTime, fixedWriteTime) > 0) {
|
|
1638
|
+
fixedWriteTime = {
|
|
1639
|
+
time: addEpsilons(lock.endTime.time, 1),
|
|
1640
|
+
version: 0,
|
|
1641
|
+
creatorId: getCreatorId(),
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (fixedWriteTime !== writeTime) {
|
|
1646
|
+
let prevWriteTime = writeTime;
|
|
1647
|
+
writeTime = fixedWriteTime;
|
|
1648
|
+
for (let lock of locks) {
|
|
1649
|
+
if (lock.endTime === prevWriteTime) {
|
|
1650
|
+
lock.endTime = writeTime;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
let forcedUndefinedWrites = new Set<string>();
|
|
1657
|
+
for (let [path, value] of watcher.pendingWrites) {
|
|
1658
|
+
if (value === specialObjectWriteValue) {
|
|
1659
|
+
forcedUndefinedWrites.add(path);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
let maxLocks = watcher.options.maxLocksOverride || DEFAULT_MAX_LOCKS;
|
|
1664
|
+
if (locks.length > maxLocks) {
|
|
1665
|
+
throw new Error(`Too many locks for ${watcher.debugName} (${locks.length} > ${maxLocks}). Use Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. You can override max locks with maxLocksOverride (in options / functionMetadata).`);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
// if (debugName.includes("setBookNodes")) {
|
|
1670
|
+
// debugger;
|
|
1671
|
+
// }
|
|
1672
|
+
|
|
1673
|
+
setValues = clientWatcher.setValues({
|
|
1674
|
+
values: watcher.pendingWrites,
|
|
1675
|
+
eventPaths: watcher.pendingEventWrites,
|
|
1676
|
+
writeTime: writeTime,
|
|
1677
|
+
locks,
|
|
1678
|
+
noWritePrediction: options.doNotStoreWritesAsPredictions,
|
|
1679
|
+
eventWrite: options.eventWrite,
|
|
1680
|
+
dryRun: options.dryRun,
|
|
1681
|
+
forcedUndefinedWrites,
|
|
1682
|
+
source: options.source,
|
|
1683
|
+
historyBasedReads,
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
doCallCreation(watcher, () => {
|
|
1688
|
+
// TODO: Maybe return this in some way, so dryRun can know what calls we want to call?
|
|
1689
|
+
for (let { call, metadata } of watcher.pendingCalls) {
|
|
1690
|
+
// The calls have to happen after our local writes. This is because they are likely to
|
|
1691
|
+
// influence the local writes, and we don't want our local writes to be always invalidated
|
|
1692
|
+
call.runAtTime = getNextTime();
|
|
1693
|
+
call.fromProxy = watcher.debugName;
|
|
1694
|
+
logErrors(runCall(call, metadata));
|
|
1695
|
+
watcher.options.onCallCommit?.(call, metadata);
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
return setValues;
|
|
1700
|
+
|
|
1701
|
+
}
|
|
1702
|
+
const hasUnsyncedBefore = measureWrap(function proxyWatchHasUnsyncedBefore() {
|
|
1703
|
+
if (watcher.options.commitAllRuns) return false;
|
|
1704
|
+
// NOTE: We COULD remove any synced values from lastUnsyncedAccesses, however... we will generally sync all values at once, so we don't really need to optimize the cascading case here. Also... deleting values requires cloning while we iterate, as well as mutating the set, which probably makes the non-cascading case slower.
|
|
1705
|
+
for (let path of watcher.lastUnsyncedAccesses) {
|
|
1706
|
+
if (!authorityStorage.isSynced(path)) {
|
|
1707
|
+
return true;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
1711
|
+
if (!authorityStorage.isParentSynced(path)) {
|
|
1712
|
+
return true;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
// Actually, a lot of calls are going to be blocked by the proxy order, and we want to run them in parallel. So we have all the values synced, so we're ready to finish them when the next one finally finishes.
|
|
1716
|
+
//if (isProxyBlockedByOrder(watcher)) return true;
|
|
1717
|
+
// NOTE: We don't check promises as they often access non-synced code, and so we might retrigger
|
|
1718
|
+
// and not use the same promise, so it might be wrong to check them.
|
|
1719
|
+
return false;
|
|
1720
|
+
});
|
|
1721
|
+
function logUnsynced() {
|
|
1722
|
+
let anyLogged = false;
|
|
1723
|
+
for (let path of watcher.lastUnsyncedAccesses) {
|
|
1724
|
+
if (!authorityStorage.isSynced(path)) {
|
|
1725
|
+
console.log(yellow(` Waiting for ${path}`));
|
|
1726
|
+
anyLogged = true;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
1730
|
+
if (!authorityStorage.isParentSynced(path)) {
|
|
1731
|
+
console.log(yellow(` Waiting for parent ${path}`));
|
|
1732
|
+
anyLogged = true;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
// NOTE: We don't log for promises, as they are not checked in hasUnsyncedBefore
|
|
1736
|
+
return anyLogged;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
let trigger = (changed?: WatchSpecData) => {
|
|
1741
|
+
let time = Date.now();
|
|
1742
|
+
|
|
1743
|
+
if (this.runningWatcher) {
|
|
1744
|
+
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}`);
|
|
1745
|
+
}
|
|
1746
|
+
if (watcher.disposed) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Merge triggers. We keep triggers if a callback fails to rerun due to unsynced values. This is essential, as it allows
|
|
1751
|
+
// callbacks to always receive every changed value, which we rely on to maintain delta based dependencies.
|
|
1752
|
+
{
|
|
1753
|
+
if (changed && (PathValueProxyWatcher.TRACK_TRIGGERS || ClientWatcher.DEBUG_TRIGGERS || options.trackTriggers)) {
|
|
1754
|
+
if (watcher.triggeredByChanges) {
|
|
1755
|
+
for (let path of changed.paths) {
|
|
1756
|
+
watcher.triggeredByChanges.paths.add(path);
|
|
1757
|
+
}
|
|
1758
|
+
for (let parentPath of changed.newParentsSynced) {
|
|
1759
|
+
watcher.triggeredByChanges.newParentsSynced.add(parentPath);
|
|
1760
|
+
}
|
|
1761
|
+
if (changed.pathSources) {
|
|
1762
|
+
if (watcher.triggeredByChanges.pathSources) {
|
|
1763
|
+
for (let value of changed.pathSources) {
|
|
1764
|
+
watcher.triggeredByChanges.pathSources.add(value);
|
|
1765
|
+
}
|
|
1766
|
+
} else {
|
|
1767
|
+
watcher.triggeredByChanges.pathSources = new Set(changed.pathSources);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (changed.extraReasons) {
|
|
1771
|
+
watcher.triggeredByChanges.extraReasons = [
|
|
1772
|
+
...watcher.triggeredByChanges.extraReasons || [],
|
|
1773
|
+
...changed.extraReasons
|
|
1774
|
+
];
|
|
1775
|
+
}
|
|
1776
|
+
} else {
|
|
1777
|
+
watcher.triggeredByChanges = changed;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
triggerCount++;
|
|
1783
|
+
let dropInsteadOfDelay = options.runImmediately || options.temporary;
|
|
1784
|
+
|
|
1785
|
+
const runOncePerPaint = options.runOncePerPaint;
|
|
1786
|
+
|
|
1787
|
+
if (runOncePerPaint) {
|
|
1788
|
+
if (pausedUntilNextPaint.has(runOncePerPaint)) {
|
|
1789
|
+
let prevValue = pausedUntilNextPaint.get(runOncePerPaint);
|
|
1790
|
+
if (dropInsteadOfDelay) {
|
|
1791
|
+
triggersDropped++;
|
|
1792
|
+
} else if (prevValue !== PAINT_NOOP_FUNCTION) {
|
|
1793
|
+
// If the previous value was a NOOP, we aren't dropping the function, we are just delaying it
|
|
1794
|
+
triggersDropped++;
|
|
1795
|
+
}
|
|
1796
|
+
pausedUntilNextPaint.set(runOncePerPaint, () => {
|
|
1797
|
+
// Use dropInsteadOfDelay as late as possible, so it is set by the latest call,
|
|
1798
|
+
// instead of the first call.
|
|
1799
|
+
if (dropInsteadOfDelay) return;
|
|
1800
|
+
watcher.explicitlyTrigger();
|
|
1801
|
+
});
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// Set NOOP function just as a placeholder, to block future calls (until the next paint)
|
|
1806
|
+
pausedUntilNextPaint.set(runOncePerPaint, PAINT_NOOP_FUNCTION);
|
|
1807
|
+
void onNextPaint().finally(() => {
|
|
1808
|
+
let fnc = pausedUntilNextPaint.get(runOncePerPaint);
|
|
1809
|
+
if (!fnc) return;
|
|
1810
|
+
pausedUntilNextPaint.delete(runOncePerPaint);
|
|
1811
|
+
fnc();
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
if (!options.runImmediately && !options.allowUnsyncedReads && hasUnsyncedBefore()) {
|
|
1816
|
+
if (SHOULD_TRACE) {
|
|
1817
|
+
console.log(yellow(`Skipping trigger due to unsynced watchers. ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
|
|
1818
|
+
if (!logUnsynced()) {
|
|
1819
|
+
debugbreak(2);
|
|
1820
|
+
debugger;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
// NOTE: When we sync the values, we will automatically be triggered, so we don't need to
|
|
1824
|
+
// setup a trigger here.
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// NOTE: We don't log the trigger evaluation starting, as that should really be logged inside of ClientWatcher,
|
|
1829
|
+
// as a result of ClientWatcher.DEBUG_TRIGGERS (except for runImmediate).
|
|
1830
|
+
if (options.runImmediately) {
|
|
1831
|
+
if (SHOULD_TRACE) {
|
|
1832
|
+
console.log(`${green("INITIAL TRIGGER")} ${watcher.debugName}`, watcher.triggeredByChanges);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
watcher.lastEvalTime = Date.now();
|
|
1837
|
+
|
|
1838
|
+
// Do as little as possible while runningWatcher is true. This allows callbacks, etc, to create
|
|
1839
|
+
// new watchers without having to juggle .runningWatcher.
|
|
1840
|
+
let result: { result: Result } | { error: string } | undefined;
|
|
1841
|
+
try {
|
|
1842
|
+
this.runningWatcher = watcher;
|
|
1843
|
+
result = runWatcher();
|
|
1844
|
+
} finally {
|
|
1845
|
+
this.runningWatcher = undefined;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
const nonLooping = options.runImmediately || options.allowUnsyncedReads;
|
|
1849
|
+
|
|
1850
|
+
const readyToCommit = (
|
|
1851
|
+
nonLooping || getReadyToCommit()
|
|
1852
|
+
);
|
|
1853
|
+
|
|
1854
|
+
if (!nonLooping) {
|
|
1855
|
+
if (readyToCommit) {
|
|
1856
|
+
harvestableReadyLoopCount++;
|
|
1857
|
+
} else {
|
|
1858
|
+
harvestableWaitingLoopCount++;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Commit writes BEFORE we watch. This prevents us from triggering ourself, in our first watch
|
|
1863
|
+
// (on subsequent watches ClientWatcher will prevent self watches. It can't for the first
|
|
1864
|
+
// watch, as it wasn't the triggerer).
|
|
1865
|
+
let values: PathValue[] | undefined;
|
|
1866
|
+
if (readyToCommit) {
|
|
1867
|
+
try {
|
|
1868
|
+
values = commitWrites();
|
|
1869
|
+
} catch (e: any) {
|
|
1870
|
+
// setValues might throw, such as if the write time is too far into the past.
|
|
1871
|
+
result = { error: e.stack };
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
if (SHOULD_TRACE && "error" in result) {
|
|
1876
|
+
console.log(red(`Error in watcher ${watcher.debugName} at ${time}`), result.error);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
if (!options.runImmediately) {
|
|
1880
|
+
updateWatchers();
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
if (!readyToCommit) {
|
|
1884
|
+
if (SHOULD_TRACE) {
|
|
1885
|
+
console.log(yellow(`Skipping trigger commit due to unsynced accesses. ${watcher.debugName} at ${time}`), watcher.triggeredByChanges);
|
|
1886
|
+
if (watcher.lastSpecialPromiseUnsynced) {
|
|
1887
|
+
console.log(yellow(` Waiting for promise`));
|
|
1888
|
+
}
|
|
1889
|
+
logUnsynced();
|
|
1890
|
+
}
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (SHOULD_TRACE) {
|
|
1895
|
+
console.log(green(`Committing ${values?.length || 0} watcher writes ${watcher.debugName} at ${time}`), watcher.triggeredByChanges, watcher.lastUnsyncedAccesses, watcher.lastUnsyncedParentAccesses);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// ONLY clear triggeredByChanges when we have a succesful run, otherwise delta based watchers won't work!
|
|
1899
|
+
watcher.triggeredByChanges = undefined;
|
|
1900
|
+
options.onResultUpdated?.(result, values, watcher);
|
|
1901
|
+
};
|
|
1902
|
+
trigger = measureWrap(trigger, `(watcher) ${watcher.debugName}`);
|
|
1903
|
+
watcher.explicitlyTrigger = trigger;
|
|
1904
|
+
|
|
1905
|
+
self.allWatchersLookup.set(trigger, watcher);
|
|
1906
|
+
|
|
1907
|
+
if (options.static) {
|
|
1908
|
+
// Do nothing, this will be explicitly triggered when needed
|
|
1909
|
+
} else if (options.runImmediately) {
|
|
1910
|
+
trigger(undefined);
|
|
1911
|
+
dispose();
|
|
1912
|
+
} else {
|
|
1913
|
+
// Set our watchers
|
|
1914
|
+
updateWatchers();
|
|
1915
|
+
clientWatcher.explicitlyTriggerWatcher(trigger, {
|
|
1916
|
+
synchronous: options.synchronousInit,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
return watcher;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/** IMPORTANT! Values written will only show up in direct parent calls.
|
|
1923
|
+
* This means if you do `data.list[Date.now()].value = value`,
|
|
1924
|
+
* getKeys(data.list) will NOT have your new key.
|
|
1925
|
+
* Instead, do writes like this: `data.list[Date.now()] = { value }`,
|
|
1926
|
+
* or, if the objects are atomic, like this: `data.list[Date.now()] = atomicObjectWrite({ value })`,
|
|
1927
|
+
* which is move efficient (but the .value cannot be mutated in the future, only clobbered).
|
|
1928
|
+
* NOTE: For reading values (readOnly), just use `commitFunction`.
|
|
1929
|
+
*/
|
|
1930
|
+
public writeOnly<Result = void>(
|
|
1931
|
+
options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">
|
|
1932
|
+
): Result {
|
|
1933
|
+
let result: { result: Result } | { error: string } | undefined;
|
|
1934
|
+
this.createWatcher({
|
|
1935
|
+
forceEqualWrites: true,
|
|
1936
|
+
...options,
|
|
1937
|
+
writeOnly: true,
|
|
1938
|
+
runImmediately: true,
|
|
1939
|
+
allowUnsyncedReads: true,
|
|
1940
|
+
canWrite: true,
|
|
1941
|
+
temporary: true,
|
|
1942
|
+
onResultUpdated: r => {
|
|
1943
|
+
result = r;
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
if (!result) {
|
|
1947
|
+
throw new Error("Expected result to be set");
|
|
1948
|
+
}
|
|
1949
|
+
if ("error" in result) {
|
|
1950
|
+
throw errorify(result.error);
|
|
1951
|
+
}
|
|
1952
|
+
return result.result as Result;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
public runOnce<Result = void>(
|
|
1956
|
+
options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">
|
|
1957
|
+
): Result {
|
|
1958
|
+
let result: { result: Result } | { error: string } | undefined;
|
|
1959
|
+
let watcher = this.createWatcher({
|
|
1960
|
+
...options,
|
|
1961
|
+
runImmediately: true,
|
|
1962
|
+
allowUnsyncedReads: true,
|
|
1963
|
+
canWrite: true,
|
|
1964
|
+
temporary: true,
|
|
1965
|
+
onResultUpdated: r => {
|
|
1966
|
+
result = r;
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
if (!result) {
|
|
1970
|
+
throw new Error("Expected result to be set");
|
|
1971
|
+
}
|
|
1972
|
+
if ("error" in result) {
|
|
1973
|
+
throw errorify(result.error);
|
|
1974
|
+
}
|
|
1975
|
+
if (watcher.lastUnsyncedAccesses.size > 0 || watcher.lastUnsyncedParentAccesses.size > 0) {
|
|
1976
|
+
//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);
|
|
1977
|
+
}
|
|
1978
|
+
return result.result as Result;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/** Runs the function until all reads are synced, then disposes the watcher and returns
|
|
1982
|
+
* - Can be used just with readers, but is most useful for writers (as the
|
|
1983
|
+
* absolutely soonest we can safely write is when all reads are synced)
|
|
1984
|
+
* - "writeOnly" is a dangerous version of this, which doesn't wait, and instead
|
|
1985
|
+
* just writes immediately (but in exchange, it cannot read).
|
|
1986
|
+
* TODO: Find a way to reuse watchers with different functions. For simple functions this can be
|
|
1987
|
+
* about 4X faster. It is more difficult if the function has to wait to sync values,
|
|
1988
|
+
* but we can probably still reuse watchers in some way.
|
|
1989
|
+
* - The "static" option might help with this.
|
|
1990
|
+
*/
|
|
1991
|
+
public async commitFunction<Result = void>(
|
|
1992
|
+
options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">,
|
|
1993
|
+
config?: {
|
|
1994
|
+
onWritesCommitted?: (writes: PathValue[]) => void;
|
|
1995
|
+
}
|
|
1996
|
+
): Promise<Result> {
|
|
1997
|
+
options = {
|
|
1998
|
+
canWrite: true,
|
|
1999
|
+
waitForIncompleteTransactions: true,
|
|
2000
|
+
...options,
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
if (this.runningWatcher?.options.inlineNestedWatchers) {
|
|
2004
|
+
this.createWatcher(options);
|
|
2005
|
+
return "Nested commitFunctions cannot return values" as any;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
let onResult!: (result: Result) => void;
|
|
2009
|
+
let onError!: (error: Error) => void;
|
|
2010
|
+
let resultPromise = new Promise<Result>((resolve, reject) => { onResult = resolve; onError = reject; });
|
|
2011
|
+
let anyWrites = false;
|
|
2012
|
+
this.createWatcher({
|
|
2013
|
+
...options,
|
|
2014
|
+
temporary: true,
|
|
2015
|
+
onResultUpdated: (result, writes, watcher) => {
|
|
2016
|
+
if (writes?.length) {
|
|
2017
|
+
anyWrites = true;
|
|
2018
|
+
}
|
|
2019
|
+
watcher.dispose();
|
|
2020
|
+
if ("error" in result) {
|
|
2021
|
+
onError(errorify(result.error));
|
|
2022
|
+
} else {
|
|
2023
|
+
onResult(result.result);
|
|
2024
|
+
}
|
|
2025
|
+
if (writes && config?.onWritesCommitted) {
|
|
2026
|
+
config.onWritesCommitted(writes);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
let result = await resultPromise;
|
|
2031
|
+
// NOTE: Now we ALWAYS wait, to prevent services from terminating their process before their writes finish.
|
|
2032
|
+
// It is just too hard to remember to call pathValueCommitter.waitForValuesToCommit in every service.
|
|
2033
|
+
if (options.canWrite && anyWrites && !options.dryRun && !options.noWaitForCommit) {
|
|
2034
|
+
await pathValueCommitter.waitForValuesToCommit();
|
|
2035
|
+
}
|
|
2036
|
+
return result;
|
|
2037
|
+
}
|
|
2038
|
+
/** Run the same as usual, but instead of committing writes, returns them. */
|
|
2039
|
+
public async dryRun(
|
|
2040
|
+
options: Omit<WatcherOptions<any>, "onResultUpdated" | "onWriteCommitted">
|
|
2041
|
+
): Promise<PathValue[] | undefined> {
|
|
2042
|
+
let result = await this.dryRunFull(options);
|
|
2043
|
+
return result.writes;
|
|
2044
|
+
}
|
|
2045
|
+
/** Run the same as usual, but instead of committing writes, returns them. */
|
|
2046
|
+
public async dryRunFull(
|
|
2047
|
+
options: Omit<WatcherOptions<any>, "onResultUpdated" | "onWriteCommitted">
|
|
2048
|
+
): Promise<DryRunResult> {
|
|
2049
|
+
type Result = {
|
|
2050
|
+
writes: PathValue[];
|
|
2051
|
+
readPaths: Set<string>
|
|
2052
|
+
readParentPaths: Set<string>;
|
|
2053
|
+
result: unknown;
|
|
2054
|
+
};
|
|
2055
|
+
let onResult!: (result: Result) => void;
|
|
2056
|
+
let onError!: (error: Error) => void;
|
|
2057
|
+
let resultPromise = new Promise<Result>((resolve, reject) => { onResult = resolve; onError = reject; });
|
|
2058
|
+
let watcher = this.createWatcher({
|
|
2059
|
+
...options,
|
|
2060
|
+
canWrite: true,
|
|
2061
|
+
dryRun: true,
|
|
2062
|
+
temporary: true,
|
|
2063
|
+
onResultUpdated: (result, values, watcher) => {
|
|
2064
|
+
watcher.dispose();
|
|
2065
|
+
if ("error" in result) {
|
|
2066
|
+
onError(errorify(result.error));
|
|
2067
|
+
} else {
|
|
2068
|
+
onResult({
|
|
2069
|
+
writes: values ?? [],
|
|
2070
|
+
readPaths: new Set(watcher.pendingWatches.paths),
|
|
2071
|
+
readParentPaths: new Set(watcher.pendingWatches.parentPaths),
|
|
2072
|
+
result: result.result
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
let result = await resultPromise;
|
|
2078
|
+
return result;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
|
|
2082
|
+
private allWatchers = registerResource("paths|proxyWatcher.allWatchers", new Set<SyncWatcher>());
|
|
2083
|
+
private allWatchersLookup = registerResource("paths|proxyWatcher.allWatchersLookup", new Map<Function, SyncWatcher>());
|
|
2084
|
+
public getWatcherForTrigger(trigger: Function): SyncWatcher | undefined {
|
|
2085
|
+
return this.allWatchersLookup.get(trigger);
|
|
2086
|
+
}
|
|
2087
|
+
public getAllWatchers(): Set<SyncWatcher> {
|
|
2088
|
+
return this.allWatchers;
|
|
2089
|
+
}
|
|
2090
|
+
public inWatcher(): boolean {
|
|
2091
|
+
return !!this.runningWatcher;
|
|
2092
|
+
}
|
|
2093
|
+
/** @deprecated try not to call getTriggeredWatcher, and instead try to call Querysub helper
|
|
2094
|
+
* functions. getTriggeredWatcher exposes too much of our interface, which we need
|
|
2095
|
+
* to abstact out when we rewrite proxyWatcher.
|
|
2096
|
+
*/
|
|
2097
|
+
public getTriggeredWatcher(): SyncWatcher {
|
|
2098
|
+
let watcher = this.runningWatcher;
|
|
2099
|
+
if (!watcher) {
|
|
2100
|
+
throw new Error("Not in a watcher");
|
|
2101
|
+
}
|
|
2102
|
+
return watcher;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
public isAllSynced(config?: {
|
|
2106
|
+
ignoreAlwaysCommitAllRunsFlag?: boolean;
|
|
2107
|
+
}) {
|
|
2108
|
+
if (!config?.ignoreAlwaysCommitAllRunsFlag && this.runningWatcher?.options.commitAllRuns) {
|
|
2109
|
+
return true;
|
|
2110
|
+
}
|
|
2111
|
+
return !this.getTriggeredWatcherMaybeUndefined()?.hasAnyUnsyncedAccesses();
|
|
2112
|
+
}
|
|
2113
|
+
/** @deprecated try not to call getTriggeredWatcherMaybeUndefined, and instead try to call Querysub helper
|
|
2114
|
+
* functions. getTriggeredWatcherMaybeUndefined exposes too much of our interface, which we need
|
|
2115
|
+
* to abstact out when we rewrite proxyWatcher.
|
|
2116
|
+
*/
|
|
2117
|
+
public getTriggeredWatcherMaybeUndefined(): SyncWatcher | undefined {
|
|
2118
|
+
return this.runningWatcher;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/** Watches everything we were watching last watch. Is essential for efficient watching code,
|
|
2122
|
+
* which needs access to a lot of state, but doesn't want to process the entire state
|
|
2123
|
+
* every time any value changes.
|
|
2124
|
+
*/
|
|
2125
|
+
public reuseLastWatches() {
|
|
2126
|
+
let watcher = this.getTriggeredWatcher();
|
|
2127
|
+
for (let path of watcher.lastUnsyncedAccesses) {
|
|
2128
|
+
if (!authorityStorage.isSynced(path)) {
|
|
2129
|
+
watcher.pendingUnsyncedAccesses.add(path);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
for (let path of watcher.lastUnsyncedParentAccesses) {
|
|
2133
|
+
if (!authorityStorage.isParentSynced(path)) {
|
|
2134
|
+
watcher.pendingUnsyncedParentAccesses.add(path);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
|
|
2139
|
+
function addToSet(set1: Set<string>, set2: Set<string>) {
|
|
2140
|
+
let base = set2;
|
|
2141
|
+
for (let path of set1) {
|
|
2142
|
+
base.add(path);
|
|
2143
|
+
}
|
|
2144
|
+
return base;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// NOTE: If we don't clone, then pathValueClientWatcher cannot detect changes, and we
|
|
2148
|
+
// end up staying watched on everything!
|
|
2149
|
+
// - We should really fix this though, as if we can avoid this clone, we can have really efficient
|
|
2150
|
+
// incremental watches.
|
|
2151
|
+
watcher.pendingWatches.paths = addToSet(new Set(watcher.lastWatches.paths), watcher.pendingWatches.paths);
|
|
2152
|
+
watcher.pendingWatches.parentPaths = addToSet(new Set(watcher.lastWatches.parentPaths), watcher.pendingWatches.parentPaths);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
/** NOTE: This explicitly causes the watcher to be unsynced until function
|
|
2156
|
+
* can be evaluated without any pending promises. This makes this significantly
|
|
2157
|
+
* more powerful than waiting on localState.
|
|
2158
|
+
* NOTE: IF you don't want to cause the caller to be unsynced, use a local schema instead.
|
|
2159
|
+
* This allows callers to see what path triggered them, which is often more informative
|
|
2160
|
+
* than waitReason. It also exposes the state to any synced diagnostics tools.
|
|
2161
|
+
*/
|
|
2162
|
+
public triggerOnPromiseFinish(
|
|
2163
|
+
promise: MaybePromise<unknown>,
|
|
2164
|
+
config: {
|
|
2165
|
+
// For example, "waitForModuleToLoad"
|
|
2166
|
+
waitReason: string;
|
|
2167
|
+
// Doesn't cause us to be unsynced, instead just triggering the watcher
|
|
2168
|
+
// when the promise resolves (or rejects).
|
|
2169
|
+
noWait?: boolean;
|
|
2170
|
+
}
|
|
2171
|
+
) {
|
|
2172
|
+
// This needs to mark the watcher as unsynced
|
|
2173
|
+
if (typeof promise === "object" && promise && promise instanceof Promise) {
|
|
2174
|
+
let watcher = this.getTriggeredWatcher();
|
|
2175
|
+
if (!config.noWait) {
|
|
2176
|
+
watcher.specialPromiseUnsynced = true;
|
|
2177
|
+
}
|
|
2178
|
+
watcher.lastSpecialPromiseUnsyncedReason = config.waitReason;
|
|
2179
|
+
void promise.finally(() => {
|
|
2180
|
+
watcher.explicitlyTrigger({
|
|
2181
|
+
paths: new Set(),
|
|
2182
|
+
newParentsSynced: new Set(),
|
|
2183
|
+
pathSources: new Set(),
|
|
2184
|
+
extraReasons: [getPathStr2(`Promise`, config.waitReason || "")]
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
public setEventPath(getValue: () => unknown) {
|
|
2191
|
+
let path = getProxyPath(getValue);
|
|
2192
|
+
let watcher = this.getTriggeredWatcher();
|
|
2193
|
+
watcher.pendingEventWrites.add(path);
|
|
2194
|
+
}
|
|
2195
|
+
public unwatchEventPaths(watcher: SyncWatcher, paths: Set<string>) {
|
|
2196
|
+
for (let path of paths) {
|
|
2197
|
+
watcher.lastWatches.paths.delete(path);
|
|
2198
|
+
watcher.lastWatches.parentPaths.delete(path);
|
|
2199
|
+
watcher.pendingWatches.paths.delete(path);
|
|
2200
|
+
watcher.pendingWatches.parentPaths.delete(path);
|
|
2201
|
+
watcher.pendingUnsyncedAccesses.delete(path);
|
|
2202
|
+
watcher.pendingUnsyncedParentAccesses.delete(path);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
public debug_breakOnWrite(proxy: string | unknown | (() => unknown)) {
|
|
2207
|
+
let path = this.getPathAndWatch(proxy);
|
|
2208
|
+
if (!path) {
|
|
2209
|
+
console.error(`Value is not a proxy, and so cannot get path. Either pass a proxy, or a getter for a value.`);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
PathValueProxyWatcher.BREAK_ON_WRITES.add(path);
|
|
2213
|
+
}
|
|
2214
|
+
public debug_logOnWrite(proxy: unknown | (() => unknown)) {
|
|
2215
|
+
let path = this.getPathAndWatch(proxy);
|
|
2216
|
+
if (!path) {
|
|
2217
|
+
console.error(`Value is not a proxy, and so cannot get path. Either pass a proxy, or a getter for a value.`);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
PathValueProxyWatcher.LOG_WRITES_INCLUDES.add(path);
|
|
2221
|
+
}
|
|
2222
|
+
public getPathNoWatch(proxy: string | unknown | (() => unknown)) {
|
|
2223
|
+
return typeof proxy === "function" ? getProxyPath(proxy as any) : getPathFromProxy(proxy);
|
|
2224
|
+
}
|
|
2225
|
+
public getPathAndWatch(proxy: string | unknown | (() => unknown)) {
|
|
2226
|
+
if (typeof proxy === "string") {
|
|
2227
|
+
return proxy;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// Prefer to call the function, as if the result is a proxy we want the path of that,
|
|
2231
|
+
// instead of the path the proxy is inside of!
|
|
2232
|
+
if (typeof proxy === "function") {
|
|
2233
|
+
return getPathFromProxy(proxy()) || getProxyPathAndWatch(proxy as any);
|
|
2234
|
+
}
|
|
2235
|
+
return getPathFromProxy(proxy);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
public ignoreWatches<T>(code: () => T) {
|
|
2239
|
+
return doProxyOptions({ noSyncing: true }, code);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const PAINT_NOOP_FUNCTION = () => { };
|
|
2244
|
+
let pausedUntilNextPaint = new Map<string, () => void>();
|
|
2245
|
+
|
|
2246
|
+
// gitHash => domainName => moduleId => schema
|
|
2247
|
+
let schemas = new Map<string, Map<string, Map<string, Schema2>>>();
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
// NOTE: We hardcode knowledge of how module data is nested to handle schemas. This... isn't great,
|
|
2251
|
+
// but it makes the code faster, and if we change how module data is nested it will break
|
|
2252
|
+
// all existing data anyways, so... I don't think we ever will.
|
|
2253
|
+
// Path pattern is: [domain, "PathFunctionRunner", moduleId, "Data"]
|
|
2254
|
+
export const getMatchingSchema = cacheLimited(1000, function getMatchingSchema(pathStr: string): {
|
|
2255
|
+
schema: Schema2;
|
|
2256
|
+
nestedPath: string[];
|
|
2257
|
+
} | undefined {
|
|
2258
|
+
if (_noAtomicSchema) return undefined;
|
|
2259
|
+
let base = getBaseMatchingSchema(pathStr);
|
|
2260
|
+
if (base) return base;
|
|
2261
|
+
let prefix = getPrefixMatchingSchema(pathStr);
|
|
2262
|
+
if (prefix) return prefix;
|
|
2263
|
+
return undefined;
|
|
2264
|
+
});
|
|
2265
|
+
function getBaseMatchingSchema(pathStr: string): {
|
|
2266
|
+
schema: Schema2;
|
|
2267
|
+
nestedPath: string[];
|
|
2268
|
+
} | undefined {
|
|
2269
|
+
let schemaForHash = (
|
|
2270
|
+
schemas.get(getCurrentCallObj()?.fnc.gitRef || "ambient")
|
|
2271
|
+
// Default to the ambient schema for clientside calls.
|
|
2272
|
+
|| schemas.get("ambient")
|
|
2273
|
+
);
|
|
2274
|
+
|
|
2275
|
+
if (_noAtomicSchema) return undefined;
|
|
2276
|
+
if (getPathIndex(pathStr, 3) !== "Data") return undefined;
|
|
2277
|
+
let domain = getPathIndex(pathStr, 0)!;
|
|
2278
|
+
let moduleId = getPathIndex(pathStr, 2)!;
|
|
2279
|
+
let schema = schemaForHash?.get(domain)?.get(moduleId);
|
|
2280
|
+
if (!schema) return undefined;
|
|
2281
|
+
let nestedPathStr = getPathSuffix(pathStr, 4);
|
|
2282
|
+
let nestedPath = getPathFromStr(nestedPathStr);
|
|
2283
|
+
return { schema, nestedPath };
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
export function registerSchema(config: {
|
|
2287
|
+
schema: Schema2;
|
|
2288
|
+
domainName: string;
|
|
2289
|
+
moduleId: string;
|
|
2290
|
+
gitHash: string;
|
|
2291
|
+
}) {
|
|
2292
|
+
getMatchingSchema.clear();
|
|
2293
|
+
|
|
2294
|
+
const { schema, domainName, moduleId, gitHash } = config;
|
|
2295
|
+
let gitSchemas = schemas.get(gitHash);
|
|
2296
|
+
if (!gitSchemas) {
|
|
2297
|
+
schemas.set(gitHash, gitSchemas = new Map());
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
let domainSchemas = gitSchemas.get(domainName);
|
|
2301
|
+
if (!domainSchemas) {
|
|
2302
|
+
gitSchemas.set(domainName, domainSchemas = new Map());
|
|
2303
|
+
}
|
|
2304
|
+
domainSchemas.set(moduleId, schema);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
export let schemaPrefixes: { len: number; prefixes: Map<string, Schema2> }[] = [];
|
|
2308
|
+
export function registerSchemaPrefix(config: {
|
|
2309
|
+
schema: Schema2;
|
|
2310
|
+
prefixPathStr: string;
|
|
2311
|
+
}) {
|
|
2312
|
+
let len = getPathDepth(config.prefixPathStr);
|
|
2313
|
+
let list = schemaPrefixes.find(x => x.len === len);
|
|
2314
|
+
if (!list) {
|
|
2315
|
+
schemaPrefixes.push(list = { len, prefixes: new Map() });
|
|
2316
|
+
}
|
|
2317
|
+
if (isNode() && !globalThis.isHotReloading?.() && !isDynamicallyLoading()) {
|
|
2318
|
+
if (list.prefixes.has(config.prefixPathStr)) {
|
|
2319
|
+
throw new Error(`Prefix matches only work due to only having one version of the code ever load (except for during hot reloading development clientside). If we try to render multiple versions of the same code serverside (ex, because we pre-loaded new code during a deploy), it will break, as we can't distinguish them. ADD GIT HASH TO PREFIX SCHEMAS TO FIX THIS! Tried to double load ${config.prefixPathStr}`);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
list.prefixes.set(config.prefixPathStr, config.schema);
|
|
2323
|
+
}
|
|
2324
|
+
export function unregisterSchemaPrefix(config: {
|
|
2325
|
+
schema: Schema2;
|
|
2326
|
+
prefixPathStr: string;
|
|
2327
|
+
}) {
|
|
2328
|
+
let len = getPathDepth(config.prefixPathStr);
|
|
2329
|
+
let list = schemaPrefixes.find(x => x.len === len);
|
|
2330
|
+
if (!list) return;
|
|
2331
|
+
list.prefixes.delete(config.prefixPathStr);
|
|
2332
|
+
}
|
|
2333
|
+
function getPrefixMatchingSchema(pathStr: string): {
|
|
2334
|
+
schema: Schema2;
|
|
2335
|
+
nestedPath: string[];
|
|
2336
|
+
} | undefined {
|
|
2337
|
+
let len = getPathDepth(pathStr);
|
|
2338
|
+
for (let list of schemaPrefixes) {
|
|
2339
|
+
if (len < list.len) continue;
|
|
2340
|
+
let prefix = getPathPrefix(pathStr, list.len);
|
|
2341
|
+
let schema = list.prefixes.get(prefix);
|
|
2342
|
+
if (schema) {
|
|
2343
|
+
let nestedPath = getPathFromStr(getPathSuffix(pathStr, list.len));
|
|
2344
|
+
return { schema, nestedPath };
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return undefined;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
|
|
2351
|
+
let _noAtomicSchema = false;
|
|
2352
|
+
export function noAtomicSchema<T>(code: () => T) {
|
|
2353
|
+
let prev = _noAtomicSchema;
|
|
2354
|
+
_noAtomicSchema = true;
|
|
2355
|
+
try {
|
|
2356
|
+
return code();
|
|
2357
|
+
} finally {
|
|
2358
|
+
_noAtomicSchema = prev;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
export const proxyWatcher = new PathValueProxyWatcher();
|
|
2364
|
+
if ((globalThis as any).proxyWatcher) {
|
|
2365
|
+
debugger;
|
|
2366
|
+
console.error(`Loaded PathValueProxyWatcher twice. This will break this. In ${module.filename}`);
|
|
2367
|
+
}
|
|
2368
|
+
(globalThis as any).proxyWatcher = proxyWatcher;
|
|
2369
|
+
(globalThis as any).ProxyWatcher = PathValueProxyWatcher;
|
|
2370
|
+
// Probably the most useful debug function for debugging watch based functions.
|
|
2371
|
+
(globalThis as any).getCurrentTriggers = function () {
|
|
2372
|
+
return proxyWatcher.getTriggeredWatcher().triggeredByChanges;
|
|
2373
|
+
};
|
|
2374
|
+
(globalThis as any).getAllWatchers = function () {
|
|
2375
|
+
return proxyWatcher.getAllWatchers();
|
|
2376
|
+
};
|
|
2377
|
+
|
|
2378
|
+
registerPeriodic(function checkForZombieWatches() {
|
|
2379
|
+
let now = Date.now();
|
|
2380
|
+
const threshold = 30 * 1000;
|
|
2381
|
+
function isZombieWatch(watcher: SyncWatcher) {
|
|
2382
|
+
if (watcher.options.static) return false;
|
|
2383
|
+
if (!watcher.hasAnyUnsyncedAccesses()) return false;
|
|
2384
|
+
let timeSinceLastSync = now - watcher.lastSyncTime;
|
|
2385
|
+
if (timeSinceLastSync < threshold) return false;
|
|
2386
|
+
return true;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
let zombies = Array.from(proxyWatcher.getAllWatchers()).filter(isZombieWatch);
|
|
2390
|
+
lastZombieCount = zombies.length;
|
|
2391
|
+
if (zombies.length > 0) {
|
|
2392
|
+
let namesSet = new Set(zombies.map(x => x.debugName));
|
|
2393
|
+
console.warn(`Found ${zombies.length} zombie watchers`, { names: Array.from(namesSet) });
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
|
|
2398
|
+
let evaluations = 0;
|
|
2399
|
+
let wastedEvaluations = 0;
|
|
2400
|
+
|
|
2401
|
+
registerMeasureInfo(() => {
|
|
2402
|
+
if (evaluations === 0) return undefined;
|
|
2403
|
+
let current = wastedEvaluations / evaluations;
|
|
2404
|
+
wastedEvaluations = 0;
|
|
2405
|
+
evaluations = 0;
|
|
2406
|
+
return `${formatPercent(current)} NOOP triggers`;
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
|
|
2410
|
+
let triggerCount = 0;
|
|
2411
|
+
let triggersDropped = 0;
|
|
2412
|
+
registerMeasureInfo(() => {
|
|
2413
|
+
if (triggerCount === 0) return undefined;
|
|
2414
|
+
let current = triggersDropped / triggerCount;
|
|
2415
|
+
triggersDropped = 0;
|
|
2416
|
+
triggerCount = 0;
|
|
2417
|
+
return `${formatPercent(current)} paint dropped triggers`;
|
|
2418
|
+
});
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
// #region Proxy Ordering
|
|
2424
|
+
let proxiesOrdered: SyncWatcher[] = [];
|
|
2425
|
+
function getProxyOrder(proxy: SyncWatcher): string | undefined {
|
|
2426
|
+
// We can't wait if we're running immediately.
|
|
2427
|
+
if (proxy.options.runImmediately) return undefined;
|
|
2428
|
+
|
|
2429
|
+
let value = proxy.options.finishInStartOrder;
|
|
2430
|
+
|
|
2431
|
+
if (value === false) return undefined;
|
|
2432
|
+
if (value === true) {
|
|
2433
|
+
value = proxy.createTime;
|
|
2434
|
+
}
|
|
2435
|
+
if (value === undefined) return undefined;
|
|
2436
|
+
return getPathStr2(value.toFixed(10).padStart(30, "0") + "", proxy.seqNum + "");
|
|
2437
|
+
}
|
|
2438
|
+
function addToProxyOrder(proxy: SyncWatcher) {
|
|
2439
|
+
let order = getProxyOrder(proxy);
|
|
2440
|
+
if (order === undefined) return;
|
|
2441
|
+
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2442
|
+
if (index >= 0) {
|
|
2443
|
+
throw new Error(`Proxy ${proxy.debugName} is already in the proxy order at index ${index}? This shouldn't be possible, as the sequence number should make it unique. `);
|
|
2444
|
+
}
|
|
2445
|
+
insertIntoSortedList(proxiesOrdered, x => getProxyOrder(x) || 0, proxy);
|
|
2446
|
+
setTimeout(() => {
|
|
2447
|
+
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2448
|
+
if (index >= 0) {
|
|
2449
|
+
if (proxiesOrdered.length > (index + 1)) {
|
|
2450
|
+
// Only warn if there's actually proxies after us.
|
|
2451
|
+
console.warn(`Allowing out-of-order proxy finishing due to long-running proxy not resolving. Not resolving: ${proxy.debugName} timed out after ${formatTime(MAX_PROXY_REORDER_BLOCK_TIME)}`);
|
|
2452
|
+
}
|
|
2453
|
+
void finishProxyAndTriggerNext(proxy, true);
|
|
2454
|
+
}
|
|
2455
|
+
}, MAX_PROXY_REORDER_BLOCK_TIME);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// - This should be fine though, because if we're doing this for an async call, it means they're already expecting the result back within a promise. So waiting another promise shouldn't mess up the ordering.
|
|
2459
|
+
const finishProxyAndTriggerNext = runInSerial(
|
|
2460
|
+
async function finishProxyAndTriggerNext(proxy: SyncWatcher, fromTimeout: boolean = false) {
|
|
2461
|
+
let order = getProxyOrder(proxy);
|
|
2462
|
+
if (order === undefined) return;
|
|
2463
|
+
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2464
|
+
if (index < 0) {
|
|
2465
|
+
console.warn(`Proxy ${proxy.debugName} is not in the proxy order at index ${index}? This shouldn't be possible, don't we only dispose it once? `);
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// NOTE: We have to wait to the next tick, as finish order code is most often on for async functions, in which case, even though we will have called onResultUpdated, the caller's waiting on a promise, so we need to give it a tick to actually receive the value.
|
|
2470
|
+
// - This shouldn't cause any actual slowdown because people are calling the functions and usually awaiting them, so they won't notice a difference.
|
|
2471
|
+
// HACK: We wait 10 times because I guess there's a lot of change promises. I found five works, but adding more is fine. This is pretty bad, but... I don't think there's anything that can possibly be expecting an ordered function to finish synchronously. Anything that is waiting on the ordered function is going to actually be waiting with an await, so we could iterate 100 times here and it wouldn't break anything.
|
|
2472
|
+
for (let i = 0; i < 10; i++) {
|
|
2473
|
+
await Promise.resolve();
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
proxiesOrdered.splice(index, 1);
|
|
2477
|
+
let next = proxiesOrdered[index];
|
|
2478
|
+
// Only trigger if it's the first entry and therefore it won't be blocked by the previous proxy, or, of course, if it's from a timeout, In which case, it won't be blocked anyway.
|
|
2479
|
+
if (next && (index === 0 || fromTimeout)) {
|
|
2480
|
+
//console.info(`Triggering next proxy in order: ${next.debugName}`, next.options.baseFunction || next.options.watchFunction);
|
|
2481
|
+
next.explicitlyTrigger({
|
|
2482
|
+
newParentsSynced: new Set(),
|
|
2483
|
+
pathSources: new Set(),
|
|
2484
|
+
paths: new Set(),
|
|
2485
|
+
extraReasons: [`Delayed due to proxy order enforcement. Previous proxy finished, so we can now finish. Previous was ${proxy.debugName}`],
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
);
|
|
2490
|
+
export function isProxyBlockedByOrder(proxy: SyncWatcher): boolean {
|
|
2491
|
+
let order = getProxyOrder(proxy);
|
|
2492
|
+
if (order === undefined) return false;
|
|
2493
|
+
let index = binarySearchBasic(proxiesOrdered, x => getProxyOrder(x) || 0, order);
|
|
2494
|
+
if (index <= 0) return false;
|
|
2495
|
+
return true;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
let currentCallCreationProxy: SyncWatcher | undefined = undefined;
|
|
2499
|
+
function doCallCreation(proxy: SyncWatcher, code: () => void) {
|
|
2500
|
+
let prev = currentCallCreationProxy;
|
|
2501
|
+
currentCallCreationProxy = proxy;
|
|
2502
|
+
try {
|
|
2503
|
+
return code();
|
|
2504
|
+
} finally {
|
|
2505
|
+
currentCallCreationProxy = prev;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
export function debug_getQueueOrder() {
|
|
2509
|
+
return proxiesOrdered;
|
|
2510
|
+
}
|
|
2511
|
+
export function getCurrentCallCreationProxy() {
|
|
2512
|
+
return currentCallCreationProxy;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
|
|
2516
|
+
registerPeriodic(function logWatcherTypes() {
|
|
2517
|
+
let allWatchers = proxyWatcher.getAllWatchers();
|
|
2518
|
+
let counts = new Map<string, { count: number; }>();
|
|
2519
|
+
for (let watcher of allWatchers) {
|
|
2520
|
+
let type = watcher.debugName.split("|")[0] || "no debug name";
|
|
2521
|
+
let countObj = counts.get(type);
|
|
2522
|
+
if (!countObj) {
|
|
2523
|
+
countObj = { count: 0 };
|
|
2524
|
+
counts.set(type, countObj);
|
|
2525
|
+
}
|
|
2526
|
+
countObj.count++;
|
|
2527
|
+
}
|
|
2528
|
+
let sorted = sort(Array.from(counts.entries()), x => -x[1].count);
|
|
2529
|
+
|
|
2530
|
+
console.log(`${formatNumber(allWatchers.size)} watchers | ${formatNumber(sorted.length)} types | remember, components have 3 watchers`);
|
|
2531
|
+
// Log the top 5
|
|
2532
|
+
for (let [type, countObj] of sorted.slice(0, 5)) {
|
|
2533
|
+
let countText = `${formatPercent(countObj.count / allWatchers.size)} (${formatNumber(countObj.count)})`;
|
|
2534
|
+
console.log(` ${countText.padEnd(15, " ")} ${type}`);
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
// #endregion Proxy Ordering
|
|
2539
|
+
|
|
2540
|
+
import { inlineNestedCalls } from "../3-path-functions/syncSchema"; import { LOCAL_DOMAIN_PATH } from "../0-path-value-core/PathRouter";
|
|
2541
|
+
import { waitIfReceivedIncompleteTransaction } from "./TransactionDelayer";
|
|
2542
|
+
|