querysub 0.403.0 → 0.404.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/.cursorrules +2 -0
- package/bin/audit-imports.js +4 -0
- package/package.json +7 -4
- package/spec.txt +77 -0
- package/src/-a-archives/archiveCache.ts +9 -4
- package/src/-a-archives/archivesBackBlaze.ts +1039 -1039
- package/src/-a-auth/certs.ts +0 -12
- package/src/-c-identity/IdentityController.ts +12 -3
- package/src/-f-node-discovery/NodeDiscovery.ts +32 -26
- package/src/-g-core-values/NodeCapabilities.ts +12 -2
- package/src/0-path-value-core/AuthorityLookup.ts +239 -0
- package/src/0-path-value-core/LockWatcher2.ts +150 -0
- package/src/0-path-value-core/PathRouter.ts +535 -0
- package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +65 -0
- package/src/0-path-value-core/PathValueCommitter.ts +222 -488
- package/src/0-path-value-core/PathValueController.ts +277 -239
- package/src/0-path-value-core/PathWatcher.ts +534 -0
- package/src/0-path-value-core/ShardPrefixes.ts +31 -0
- package/src/0-path-value-core/ValidStateComputer.ts +303 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +1 -1
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +80 -44
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +13 -16
- package/src/0-path-value-core/auditLogs.ts +2 -0
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +97 -0
- package/src/0-path-value-core/pathValueArchives.ts +490 -492
- package/src/0-path-value-core/pathValueCore.ts +195 -1496
- package/src/0-path-value-core/startupAuthority.ts +74 -0
- package/src/1-path-client/RemoteWatcher.ts +90 -82
- package/src/1-path-client/pathValueClientWatcher.ts +808 -815
- package/src/2-proxy/PathValueProxyWatcher.ts +10 -8
- package/src/2-proxy/archiveMoveHarness.ts +182 -214
- package/src/2-proxy/garbageCollection.ts +9 -8
- package/src/2-proxy/schema2.ts +21 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +206 -180
- package/src/3-path-functions/PathFunctionRunner.ts +943 -766
- package/src/3-path-functions/PathFunctionRunnerMain.ts +5 -3
- package/src/3-path-functions/pathFunctionLoader.ts +2 -2
- package/src/3-path-functions/syncSchema.ts +592 -521
- package/src/4-deploy/deployFunctions.ts +19 -4
- package/src/4-deploy/deployGetFunctionsInner.ts +8 -2
- package/src/4-deploy/deployMain.ts +51 -68
- package/src/4-deploy/edgeClientWatcher.tsx +1 -1
- package/src/4-deploy/edgeNodes.ts +2 -2
- package/src/4-dom/qreact.tsx +2 -4
- package/src/4-dom/qreactTest.tsx +7 -13
- package/src/4-querysub/Querysub.ts +21 -8
- package/src/4-querysub/QuerysubController.ts +45 -29
- package/src/4-querysub/permissions.ts +2 -2
- package/src/4-querysub/querysubPrediction.ts +80 -70
- package/src/4-querysub/schemaHelpers.ts +5 -1
- package/src/5-diagnostics/GenericFormat.tsx +14 -9
- package/src/archiveapps/archiveGCEntry.tsx +9 -2
- package/src/archiveapps/archiveJoinEntry.ts +87 -84
- package/src/archiveapps/archiveMergeEntry.tsx +2 -0
- package/src/bits.ts +19 -0
- package/src/config.ts +21 -3
- package/src/config2.ts +23 -48
- package/src/deployManager/components/DeployPage.tsx +7 -3
- package/src/deployManager/machineSchema.ts +4 -1
- package/src/diagnostics/ActionsHistory.ts +3 -8
- package/src/diagnostics/AuditLogPage.tsx +2 -3
- package/src/diagnostics/FunctionCallInfo.tsx +141 -0
- package/src/diagnostics/FunctionCallInfoState.ts +162 -0
- package/src/diagnostics/MachineThreadInfo.tsx +1 -1
- package/src/diagnostics/NodeViewer.tsx +37 -48
- package/src/diagnostics/SyncTestPage.tsx +241 -0
- package/src/diagnostics/auditImportViolations.ts +185 -0
- package/src/diagnostics/listenOnDebugger.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +10 -4
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +24 -22
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
- package/src/diagnostics/logs/diskLogGlobalContext.ts +1 -0
- package/src/diagnostics/logs/errorNotifications2/logWatcher.ts +1 -3
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +34 -16
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +4 -6
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleInstanceTableView.tsx +36 -5
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +19 -5
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +15 -7
- package/src/diagnostics/logs/lifeCycleAnalysis/NestedLifeCycleInfo.tsx +28 -106
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +2 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMisc.ts +0 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +18 -7
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +3 -0
- package/src/diagnostics/managementPages.tsx +10 -3
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +20 -26
- package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +6 -4
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +2 -2
- package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +7 -9
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +23 -12
- package/src/diagnostics/misc-pages/archiveViewerShared.tsx +1 -1
- package/src/diagnostics/pathAuditer.ts +486 -0
- package/src/diagnostics/pathAuditerCallback.ts +20 -0
- package/src/diagnostics/watchdog.ts +8 -1
- package/src/library-components/URLParam.ts +1 -1
- package/src/misc/hash.ts +1 -0
- package/src/path.ts +21 -7
- package/src/server.ts +54 -47
- package/src/user-implementation/loginEmail.tsx +1 -1
- package/tempnotes.txt +67 -0
- package/test.ts +288 -95
- package/src/0-path-value-core/NodePathAuthorities.ts +0 -1057
- package/src/0-path-value-core/PathController.ts +0 -1
- package/src/5-diagnostics/diskValueAudit.ts +0 -218
- package/src/5-diagnostics/memoryValueAudit.ts +0 -438
- package/src/archiveapps/lockTest.ts +0 -127
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { delay, batchFunction } from "socket-function/src/batching";
|
|
3
|
-
import { markArrayAsSplitable } from "socket-function/src/fixLargeNetworkCalls";
|
|
4
3
|
import { isNode, timeInSecond } from "socket-function/src/misc";
|
|
5
|
-
import { measureBlock, measureFnc
|
|
4
|
+
import { measureBlock, measureFnc } from "socket-function/src/profiling/measure";
|
|
6
5
|
import { isTrustedByNode } from "../-d-trust/NetworkTrust2";
|
|
7
|
-
import { isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
8
|
-
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
6
|
+
import { areNodeIdsEqual, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
9
7
|
import { ActionsHistory } from "../diagnostics/ActionsHistory";
|
|
10
8
|
import { errorToUndefined, logErrors, timeoutToUndefined } from "../errors";
|
|
11
9
|
import { getPathFromStr } from "../path";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { PathValue, MAX_ACCEPTED_CHANGE_AGE,
|
|
10
|
+
import { PathValueControllerBase } from "./PathValueController";
|
|
11
|
+
import { PathRouter } from "./PathRouter";
|
|
12
|
+
import { PathValue, MAX_ACCEPTED_CHANGE_AGE, WriteState, debugPathValuePath, compareTime, epochTime } from "./pathValueCore";
|
|
13
|
+
import { validStateComputer } from "./ValidStateComputer";
|
|
15
14
|
import debugbreak from "debugbreak";
|
|
16
15
|
import { red } from "socket-function/src/formatting/logColors";
|
|
17
|
-
import { registerDynamicResource } from "../diagnostics/trackResources";
|
|
18
|
-
import { StatsValue, addToStats, addToStatsValue, createStatsValue, getStatsTop } from "socket-function/src/profiling/stats";
|
|
19
|
-
import { formatStats } from "socket-function/src/profiling/statsFormat";
|
|
20
|
-
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
21
16
|
import { isClient } from "../config2";
|
|
22
|
-
import { remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
23
17
|
import { auditLog, isDebugLogEnabled } from "./auditLogs";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
import { authorityLookup } from "./AuthorityLookup";
|
|
19
|
+
setImmediate(() => import("../1-path-client/RemoteWatcher"));
|
|
20
|
+
setImmediate(() => import("../4-querysub/Querysub"));
|
|
21
|
+
|
|
22
|
+
const MAX_SEND_TRY_COUNT = 3;
|
|
23
|
+
|
|
24
|
+
export type BatchValues = {
|
|
25
|
+
pathValues: PathValue[],
|
|
26
|
+
parentsSynced?: string[];
|
|
27
|
+
sourceNodeId: string;
|
|
28
|
+
initialTrigger?: "initialTrigger";
|
|
29
|
+
authoritySync?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type RemoteValueAndValidState = {
|
|
33
|
+
sourceNodeId: string;
|
|
34
|
+
pathValues: PathValue[];
|
|
35
|
+
validStates: WriteState[];
|
|
36
|
+
initialTriggers: { values: Set<string>; parentPaths: Set<string> };
|
|
37
|
+
// Means it's from an authority path sync
|
|
38
|
+
authoritySyncPaths?: Set<string>;
|
|
39
|
+
};
|
|
39
40
|
|
|
40
41
|
class PathValueCommitter {
|
|
41
42
|
private pendingCommits = new Set<Promise<unknown>>();
|
|
@@ -95,7 +96,7 @@ class PathValueCommitter {
|
|
|
95
96
|
if (predictWrites) {
|
|
96
97
|
pathValuesToIngest = values;
|
|
97
98
|
} else {
|
|
98
|
-
pathValuesToIngest = values.filter(pathValue =>
|
|
99
|
+
pathValuesToIngest = values.filter(pathValue => PathRouter.isSelfAuthority(pathValue.path));
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
// NOTE: Error out early on the client, so we get better errors AND so we can avoid even ingesting the values
|
|
@@ -107,54 +108,110 @@ class PathValueCommitter {
|
|
|
107
108
|
// (It could even populate on the first domain, so it would always populate on the first write
|
|
108
109
|
// on the current domain).
|
|
109
110
|
if (!isNode()) {
|
|
110
|
-
let remoteWrites = values.filter(pathValue => !
|
|
111
|
+
let remoteWrites = values.filter(pathValue => !PathRouter.isSelfAuthority(pathValue.path));
|
|
111
112
|
if (remoteWrites.length > 0) {
|
|
112
113
|
throw new Error(`Cannot commit writes to paths that are not local to this node. Paths: ${remoteWrites.map(x => x.path).join(", ")}`);
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
// NOTE: This function will just ignore writes we aren't an authority on, so we can just give it everything.
|
|
117
|
-
|
|
118
|
+
validStateComputer.ingestValuesAndValidStates({
|
|
119
|
+
pathValues: pathValuesToIngest,
|
|
120
|
+
parentSyncs: [],
|
|
121
|
+
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let remoteValues = values.filter(x => !PathRouter.isSelfAuthority(x.path));
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
if (remoteValues.length > 0) {
|
|
127
|
+
this.addCommitPromise(this.broadcastValues({ values: new Set(remoteValues) }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If we're the authority on it, we should still share it with the other authorities.
|
|
131
|
+
let stillShareValues = values.filter(x => !PathRouter.isLocalPath(x.path) && PathRouter.isSelfAuthority(x.path));
|
|
132
|
+
if (stillShareValues.length > 0) {
|
|
133
|
+
this.addCommitPromise(PathValueControllerBase.authorityShareValues({ pathValues: stillShareValues }));
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
private broadcastValues = batchFunction(
|
|
126
138
|
{ delay: 10, throttleWindow: 500, noMeasure: true },
|
|
127
|
-
async function internal_forwardWrites(valuesBatched:
|
|
128
|
-
|
|
139
|
+
async function internal_forwardWrites(valuesBatched: {
|
|
140
|
+
values: Set<PathValue>;
|
|
141
|
+
tryCount?: number;
|
|
142
|
+
}[]) {
|
|
143
|
+
let values = new Set(valuesBatched.flatMap(x => Array.from(x.values)));
|
|
144
|
+
let tryCountPerValue = new Map<PathValue, number>();
|
|
145
|
+
for (let list of valuesBatched) {
|
|
146
|
+
for (let value of list.values) {
|
|
147
|
+
if (!list.tryCount) continue;
|
|
148
|
+
tryCountPerValue.set(value, list.tryCount);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (tryCountPerValue.size > 0) {
|
|
152
|
+
console.info(`Syncing all authorities to ensure we have the latest values, due to failed writes.`);
|
|
153
|
+
await authorityLookup.syncAllNow();
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
|
|
130
157
|
let valuesPerOtherAuthority = new Map<string, PathValue[]>();
|
|
131
158
|
for (let pathValue of values) {
|
|
132
159
|
if (isDebugLogEnabled()) {
|
|
133
|
-
|
|
160
|
+
// let valueStr: string | undefined;
|
|
161
|
+
// if (typeof pathValue.value === "boolean" || typeof pathValue.value === "number") {
|
|
162
|
+
// valueStr = pathValue.value.toString();
|
|
163
|
+
// } else if (typeof pathValue.value === "string") {
|
|
164
|
+
// valueStr = pathValue.value.slice(0, 100);
|
|
165
|
+
// if (valueStr.length < pathValue.value.length) {
|
|
166
|
+
// valueStr += `[+${pathValue.value.length - valueStr.length}]`;
|
|
167
|
+
// }
|
|
168
|
+
// } else if (Buffer.isBuffer(pathValue.value)) {
|
|
169
|
+
// valueStr = `Buffer(${pathValue.value.length})`;
|
|
170
|
+
// } else if (pathValue.value === null) {
|
|
171
|
+
// valueStr = "{null}";
|
|
172
|
+
// } else {
|
|
173
|
+
// valueStr = `{${typeof pathValue.value}}`;
|
|
174
|
+
// }
|
|
175
|
+
auditLog("CREATE VALUE", {
|
|
176
|
+
path: pathValue.path,
|
|
177
|
+
timeId: pathValue.time.time,
|
|
178
|
+
source: pathValue.source,
|
|
179
|
+
transparent: pathValue.isTransparent,
|
|
180
|
+
// value: valueStr,
|
|
181
|
+
event: pathValue.event,
|
|
182
|
+
// NOTE: I think it'd be too expensive to log all the locks. There's one thing to log every time we read a value or create a value, but to log all of the dependencies for each write path would be O(locksPerWrite * 2 * writes), which is just too much, and could easily result in ten thousand logs per write. I think it will be fine because if there's a rejection, then that will tell us the time ID that the rejected value was reading, Which we can infer to know at least one of the locks, as in the most important lock that that right had.
|
|
183
|
+
});
|
|
134
184
|
}
|
|
135
185
|
|
|
136
|
-
let otherAuthorities =
|
|
137
|
-
otherAuthorities = otherAuthorities.filter(x => !isOwnNodeId(x));
|
|
138
|
-
if (otherAuthorities.length === 0
|
|
139
|
-
|
|
186
|
+
let otherAuthorities = PathRouter.getAllAuthorities(pathValue.path);
|
|
187
|
+
otherAuthorities = otherAuthorities.filter(x => !isOwnNodeId(x.nodeId));
|
|
188
|
+
if (otherAuthorities.length === 0) {
|
|
189
|
+
validStateComputer.ingestValuesAndValidStates({
|
|
190
|
+
pathValues: [{
|
|
191
|
+
...pathValue,
|
|
192
|
+
valid: false,
|
|
193
|
+
}],
|
|
194
|
+
parentSyncs: [],
|
|
195
|
+
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
196
|
+
});
|
|
197
|
+
PathRouter.getAllAuthorities(pathValue.path);
|
|
198
|
+
console.error(`There are no authorities for path ${pathValue.path}. The write will be lost.`, {
|
|
199
|
+
path: pathValue.path,
|
|
200
|
+
timeId: pathValue.time.time,
|
|
201
|
+
source: pathValue.source,
|
|
202
|
+
otherAuthorities,
|
|
203
|
+
});
|
|
140
204
|
}
|
|
141
205
|
for (let otherAuthority of otherAuthorities) {
|
|
142
|
-
let values = valuesPerOtherAuthority.get(otherAuthority);
|
|
206
|
+
let values = valuesPerOtherAuthority.get(otherAuthority.nodeId);
|
|
143
207
|
if (!values) {
|
|
144
208
|
values = [];
|
|
145
|
-
valuesPerOtherAuthority.set(otherAuthority, values);
|
|
209
|
+
valuesPerOtherAuthority.set(otherAuthority.nodeId, values);
|
|
146
210
|
}
|
|
147
211
|
values.push(pathValue);
|
|
148
212
|
}
|
|
149
213
|
}
|
|
150
214
|
|
|
151
|
-
// for (let [authorityId, values] of valuesPerOtherAuthority) {
|
|
152
|
-
// console.log(`Sending ${values.length} values to ${authorityId} (${pathValueAuthority2.getAuthorityPaths(authorityId)?.map(x => pathValueAuthority2.getArchiveDirectory(x)).join(" | ")})`);
|
|
153
|
-
// // for (let value of values) {
|
|
154
|
-
// // console.log(" " + value.path);
|
|
155
|
-
// // }
|
|
156
|
-
// }
|
|
157
|
-
|
|
158
215
|
// Don't send to bad nodes for 60 seconds
|
|
159
216
|
const nodeIgnoreTime = Date.now() - 1000 * 60;
|
|
160
217
|
let promises = Array.from(valuesPerOtherAuthority.entries()).map(async ([otherAuthority, values]) => {
|
|
@@ -174,39 +231,58 @@ class PathValueCommitter {
|
|
|
174
231
|
// to be on a trusted node (which should always be a server), so... either our network went
|
|
175
232
|
// down, or all nodes went down. Either way, it likely won't fix itself quickly, and so these
|
|
176
233
|
// writes are going to too old and therefore rejected by the time a server is up anyways.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
serializedValues,
|
|
183
|
-
undefined,
|
|
184
|
-
"watchLocks"
|
|
185
|
-
);
|
|
186
|
-
pathValueCommitter.addCommitPromise(forwardPromise);
|
|
234
|
+
|
|
235
|
+
let forwardPromise = PathValueControllerBase.createValues({
|
|
236
|
+
nodeId: otherAuthority,
|
|
237
|
+
pathValues: values,
|
|
238
|
+
});
|
|
187
239
|
logErrors(forwardPromise);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// for the same path, which can result in thrashing. But... this probably won't happen...
|
|
208
|
-
lockWatcher.watchValueLocks(values);
|
|
240
|
+
void forwardPromise.then(x => {
|
|
241
|
+
if (x === "refused") {
|
|
242
|
+
values = values.map(value => {
|
|
243
|
+
console.info(`Rejecting past value due to initial sync: ${debugPathValuePath(value)}`, {
|
|
244
|
+
path: value.path,
|
|
245
|
+
timeId: value.time.time,
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
...value,
|
|
249
|
+
valid: false,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
validStateComputer.ingestValuesAndValidStates({
|
|
254
|
+
pathValues: values,
|
|
255
|
+
parentSyncs: [],
|
|
256
|
+
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
209
259
|
});
|
|
260
|
+
void forwardPromise.catch(async error => {
|
|
261
|
+
let byTryCount = new Map<number, PathValue[]>();
|
|
262
|
+
for (let value of values) {
|
|
263
|
+
let tryCount = tryCountPerValue.get(value) ?? 0;
|
|
264
|
+
let arr = byTryCount.get(tryCount) ?? [];
|
|
265
|
+
arr.push(value);
|
|
266
|
+
byTryCount.set(tryCount, arr);
|
|
267
|
+
}
|
|
268
|
+
for (let [tryCount, values] of byTryCount.entries()) {
|
|
269
|
+
tryCount++;
|
|
270
|
+
if (tryCount > MAX_SEND_TRY_COUNT) {
|
|
271
|
+
console.error(`Failed to send values after ${MAX_SEND_TRY_COUNT} tries. Giving up.`, {
|
|
272
|
+
error: error.message,
|
|
273
|
+
otherAuthority,
|
|
274
|
+
count: values.length,
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
void pathValueCommitter.broadcastValues({
|
|
279
|
+
values: new Set(values),
|
|
280
|
+
tryCount: tryCount,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
pathValueCommitter.addCommitPromise(forwardPromise);
|
|
210
286
|
});
|
|
211
287
|
// await, so "waitForValuesToCommit" works correctly
|
|
212
288
|
await Promise.all(promises.map(x => timeoutToUndefined(timeInSecond * 30, errorToUndefined(x))));
|
|
@@ -214,47 +290,33 @@ class PathValueCommitter {
|
|
|
214
290
|
);
|
|
215
291
|
|
|
216
292
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
293
|
+
public ingestRemoteValuesAndValidStates = batchFunction(
|
|
294
|
+
{ delay: 16, throttleWindow: 1000, name: "ingestRemoteValuesAndValidStates", noMeasure: true },
|
|
295
|
+
async (batched: RemoteValueAndValidState[]) => {
|
|
296
|
+
const { remoteWatcher } = await import("../1-path-client/RemoteWatcher");
|
|
221
297
|
|
|
222
|
-
|
|
223
|
-
* clientside evaluations. The network will already add a lot of delay, so this shouldn't be noticeable.
|
|
224
|
-
* - AND, with the throttleWindow, we generally won't even be waiting anyways.
|
|
225
|
-
* - The delay is 16ms, as we won't render to the screen faster than that anyways, so handling updates
|
|
226
|
-
* at a rate faster than that will just pointlessly add lag.
|
|
227
|
-
*/
|
|
228
|
-
public ingestRemoteValues = batchFunction(
|
|
229
|
-
{ delay: 16, throttleWindow: 1000, name: "ingestRemoteValues", noMeasure: true },
|
|
230
|
-
async (batched: {
|
|
231
|
-
pathValues: PathValue[],
|
|
232
|
-
parentsSynced?: string[];
|
|
233
|
-
sourceNodeId: string;
|
|
234
|
-
initialTrigger?: "initialTrigger";
|
|
235
|
-
}[]) => {
|
|
236
|
-
let initialTriggers = { values: new Set<string>(), parentPaths: new Set<string>() };
|
|
237
|
-
// Check received values to make sure the authority for them is the same as the sourceNodeId
|
|
238
|
-
// (OR that WE are the authority). If not... it means we received values after changing the watcher,
|
|
239
|
-
// which requires us to ignore the other values
|
|
240
|
-
// (Clients get most of their values from the same server, so they shouldn't require this check)
|
|
241
|
-
const self = this;
|
|
298
|
+
// NOTE: We need to ignore values if they're not who we're watching. That way, if we change the watcher, it's smooth and we don't partial data that might clobber with the data from the authority we are really using.
|
|
242
299
|
if (!isClient()) {
|
|
243
300
|
measureBlock(function ignoreUnrequestedValues() {
|
|
244
301
|
for (let batch of batched) {
|
|
302
|
+
function isWrongAuthority(path: string) {
|
|
303
|
+
let watchingAuthorityId = remoteWatcher.getExistingWatchRemoteNodeId(path);
|
|
304
|
+
// If we AREN'T watching it... it's actually fine, we can receive any values.
|
|
305
|
+
// When we start watching, those values will get clobbered.
|
|
306
|
+
if (watchingAuthorityId === undefined) return false;
|
|
307
|
+
return !areNodeIdsEqual(watchingAuthorityId, batch.sourceNodeId);
|
|
308
|
+
}
|
|
245
309
|
batch.pathValues = batch.pathValues.filter(value => {
|
|
310
|
+
// Authorities watch other authorities using path watches, And so one path can come from many authorities, so we just have to accept it no matter what, if it's from that type of source.
|
|
311
|
+
if (batch.authoritySyncPaths?.has(value.path)) return true;
|
|
312
|
+
// NOTE: See the definition for lock count for why this check isn't checking all the possible cases. Essentially, locks is often empty, and that's intentional. However, the reverse should never be true, locks should never have values when lockCount is 0.
|
|
246
313
|
if (value.lockCount === 0 && value.locks.length > 0) {
|
|
247
314
|
console.error(red(`Ignoring value with invalid lockCount. Was ${value.lockCount}, but we have ${value.locks.length} locks. locks are optional, but lockCount isn't. We should never have locks without having lockCount set. ${debugPathValuePath(value)}`));
|
|
248
315
|
return false;
|
|
249
316
|
}
|
|
250
317
|
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
let watchingAuthorityId = self.getExistingWatchRemoteNodeId(value.path);
|
|
254
|
-
// If we AREN'T watching it... it's actually fine, we can receive any values.
|
|
255
|
-
// When we start watching, those values will get clobbered.
|
|
256
|
-
if (watchingAuthorityId === undefined) return true;
|
|
257
|
-
if (watchingAuthorityId === batch.sourceNodeId) return true;
|
|
318
|
+
if (PathRouter.isSelfAuthority(value.path)) return true;
|
|
319
|
+
if (isWrongAuthority(value.path)) return false;
|
|
258
320
|
// epochTimes are just indicators that the value has no value, and so are safe to sync
|
|
259
321
|
// from any source. They won't cause future conflicts, because any other value overrides them.
|
|
260
322
|
// - This might not be required?
|
|
@@ -264,402 +326,74 @@ class PathValueCommitter {
|
|
|
264
326
|
|
|
265
327
|
// Also warn, because... if we get a lot of these, there might be a bug.
|
|
266
328
|
// A few when we change watches is possible, but it should be rare.
|
|
329
|
+
let watchingAuthorityId = remoteWatcher.getExistingWatchRemoteNodeId(value.path);
|
|
267
330
|
auditLog("IGNORING VALUE FROM DIFFERENT AUTHORITY", { path: value.path, watchingAuthorityId, receivedFromAuthority: batch.sourceNodeId });
|
|
268
|
-
console.warn(`Ignoring a value that was received from a different authority than the one we are watching. Path: ${value.path}, Watching Authority (should be source): ${watchingAuthorityId}, Actual Source Authority: ${batch.sourceNodeId}`);
|
|
269
331
|
return false;
|
|
270
332
|
});
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
let specialInitialParentCausedRejections: WriteState[] = [];
|
|
276
|
-
|
|
277
|
-
// The first time we receive value for a watch from a specific node, we need to reject future
|
|
278
|
-
// missing values (unless we are the authority). Otherwise bad rejected values can stick around,
|
|
279
|
-
// because they are newer, and our new source doesn't know we have them.
|
|
280
|
-
measureBlock(function resetOnInitialTrigger() {
|
|
281
|
-
let receivedValues = new Map<string, Set<string>>();
|
|
282
|
-
for (let batch of batched) {
|
|
283
|
-
for (let pathValue of batch.pathValues) {
|
|
284
|
-
let path = pathValue.path;
|
|
285
|
-
let hash = timeHash(pathValue.time);
|
|
286
|
-
let paths = receivedValues.get(path);
|
|
287
|
-
if (!paths) {
|
|
288
|
-
paths = new Set();
|
|
289
|
-
receivedValues.set(path, paths);
|
|
290
|
-
}
|
|
291
|
-
paths.add(hash);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
333
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (!batch.initialTrigger) continue;
|
|
300
|
-
for (let pathValue of batch.pathValues) {
|
|
301
|
-
initialTriggers.values.add(pathValue.path);
|
|
302
|
-
auditLog("INITIAL SYNC", { path: pathValue.path, time: pathValue.time.time, sourceNodeId: batch.sourceNodeId });
|
|
303
|
-
// We shouldn't receive the initialTrigger flag if we are the authority anyways
|
|
304
|
-
if (nodePathAuthority.isSelfAuthority(pathValue.path)) continue;
|
|
305
|
-
let history = authorityStorage.getValuePlusHistory(pathValue.path);
|
|
306
|
-
for (let value of history) {
|
|
307
|
-
// Definitely don't reset our predictions. We will reset those ourselves
|
|
308
|
-
if (isOurPrediction(value)) continue;
|
|
309
|
-
// Only reset values after us, otherwise we might clear valid history that
|
|
310
|
-
// we JUST received.
|
|
311
|
-
if (compareTime(value.time, pathValue.time) <= 0) continue;
|
|
312
|
-
|
|
313
|
-
// If we just received it, accept it.
|
|
314
|
-
// - Otherwise we might reject a valid older value, and then when the newer value is rejected
|
|
315
|
-
// the server won't tell us about the older value again, and so we will think no values
|
|
316
|
-
// are accepted, when the older value IS accepted.
|
|
317
|
-
// BUG: In theory couldn't we receive an outdated value, then reconnect, then receive the correct
|
|
318
|
-
// value, and have them all batched together? In which case, we shouldn't accept the value.
|
|
319
|
-
// I'm not really sure how to fix this though. We kind of need to just handle adding the values
|
|
320
|
-
// and the rejections in order, instead of batching them, but that is much slower, and the race
|
|
321
|
-
// condition will likely be extremely rare (and only happen on disconnections anyways).
|
|
322
|
-
if (receivedValues.get(pathValue.path)?.has(timeHash(pathValue.time))) continue;
|
|
323
|
-
|
|
324
|
-
specialInitialParentCausedRejections.push({
|
|
325
|
-
path: value.path,
|
|
326
|
-
time: value.time,
|
|
327
|
-
isValid: false,
|
|
328
|
-
isTransparent: !!value.isTransparent,
|
|
329
|
-
reason: "future missing on resync (initial trigger)",
|
|
330
|
-
});
|
|
334
|
+
for (let value of batch.initialTriggers.values) {
|
|
335
|
+
if (isWrongAuthority(value)) {
|
|
336
|
+
batch.initialTriggers.values.delete(value);
|
|
337
|
+
}
|
|
331
338
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// tell us it is invalid, as we only watched the parent, not the specific path).
|
|
336
|
-
// - If the values become unrejected in the future, we will receive them again, which
|
|
337
|
-
// will cause their valid state to be changed to accepted.
|
|
338
|
-
// - Also, if another batch sets the value, it will override our valid state write,
|
|
339
|
-
// as we are writing this in a special array will is always handled first.
|
|
340
|
-
if (batch.parentsSynced) {
|
|
341
|
-
for (let parentPath of batch.parentsSynced) {
|
|
342
|
-
auditLog("INITIAL PARENT SYNC", { path: parentPath, sourceNodeId: batch.sourceNodeId });
|
|
343
|
-
let allChildPaths = authorityStorage.getPathsFromParent(parentPath) || [];
|
|
344
|
-
let childChecker = remoteWatcher.getChildPatchWatchChecker({
|
|
345
|
-
parentPath,
|
|
346
|
-
nodeId: batch.sourceNodeId
|
|
347
|
-
});
|
|
348
|
-
for (let childPath of allChildPaths) {
|
|
349
|
-
if (!childChecker.isWatchedByNodeId(childPath)) continue;
|
|
350
|
-
// We shouldn't receive the initialTrigger flag if we are the authority anyways.
|
|
351
|
-
if (nodePathAuthority.isSelfAuthority(childPath)) continue;
|
|
352
|
-
initialTriggers.parentPaths.add(childPath);
|
|
353
|
-
|
|
354
|
-
// NOTE: On initial trigger, we send all child paths, so they should be
|
|
355
|
-
// in the values list, if they exist. AND if we receive them after,
|
|
356
|
-
// they will clobber this rejection
|
|
357
|
-
// - See PathWatcher.watchPath, where when !noInitialTrigger (aka, initialTrigger),
|
|
358
|
-
// we get all paths via authorityStorage.getPathsFromParent, and send their values.
|
|
359
|
-
if (initialTriggers.values.has(childPath)) continue;
|
|
360
|
-
|
|
361
|
-
let values = authorityStorage.getValuePlusHistory(childPath);
|
|
362
|
-
if (values.length === 0) continue;
|
|
363
|
-
|
|
364
|
-
values = values.filter(value => {
|
|
365
|
-
// DO NOT reject epoch values. For missing values, we still need a signal, and the parent
|
|
366
|
-
// sync won't send this, so we need to NOT reject them.
|
|
367
|
-
if (compareTime(value.time, epochTime) === 0) {
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
return true;
|
|
371
|
-
});
|
|
372
|
-
if (values.length === 0) continue;
|
|
373
|
-
|
|
374
|
-
console.log(self.getExistingWatchRemoteNodeId(childPath));
|
|
375
|
-
for (let value of values) {
|
|
376
|
-
specialInitialParentCausedRejections.push({
|
|
377
|
-
path: value.path,
|
|
378
|
-
time: value.time,
|
|
379
|
-
isValid: false,
|
|
380
|
-
isTransparent: !!value.isTransparent,
|
|
381
|
-
reason: "child missing (initial trigger)",
|
|
382
|
-
});
|
|
383
|
-
}
|
|
339
|
+
for (let parentPath of batch.initialTriggers.parentPaths) {
|
|
340
|
+
if (isWrongAuthority(parentPath)) {
|
|
341
|
+
batch.initialTriggers.parentPaths.delete(parentPath);
|
|
384
342
|
}
|
|
385
343
|
}
|
|
386
344
|
}
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
let pathValues = batched.map(x => x.pathValues).flat();
|
|
391
|
-
let parentsSynced = batched.map(x => x.parentsSynced ?? []).flat();
|
|
392
|
-
let parentSyncedSources = new Map<string, string[]>();
|
|
393
|
-
for (let batch of batched) {
|
|
394
|
-
if (!batch.parentsSynced) continue;
|
|
395
|
-
for (let parentSynced of batch.parentsSynced) {
|
|
396
|
-
let sources = parentSyncedSources.get(parentSynced);
|
|
397
|
-
if (!sources) {
|
|
398
|
-
sources = [];
|
|
399
|
-
parentSyncedSources.set(parentSynced, sources);
|
|
400
|
-
}
|
|
401
|
-
sources.push(batch.sourceNodeId);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// Ignore outdated PathValues if we are the authority.
|
|
407
|
-
// - This CAN leave our servers in a bad state. However...
|
|
408
|
-
// 1) We will resync with the disk eventually (in a few hours), fixing the state
|
|
409
|
-
// (IF some servers accepted the values, and other ignored them).
|
|
410
|
-
// 1.1) We should also audit between PathValueServers, which should fix the state
|
|
411
|
-
// MUCH faster.
|
|
412
|
-
// 2) If we DON'T ignore these values, we might end up rejecting an archived value,
|
|
413
|
-
// which breaks a lot of values, and which cannot be automatically recovered from.
|
|
414
|
-
measureBlock(function discardInvalidGenesisValues() {
|
|
415
|
-
let maxAge = Date.now() - MAX_ACCEPTED_CHANGE_AGE;
|
|
416
|
-
// NOTE: We could add less to future age here, but it's fine. This is the time
|
|
417
|
-
// a services (not a client, as services validate clients) can write in the future,
|
|
418
|
-
// which will prevent writes on that path until after that time has passed.
|
|
419
|
-
let maxFutureAge = Date.now() + MAX_ACCEPTED_CHANGE_AGE;
|
|
420
|
-
pathValues = pathValues.filter(value => {
|
|
421
|
-
if (!nodePathAuthority.isSelfAuthority(value.path)) return true;
|
|
422
|
-
if (value.time.time < maxAge) {
|
|
423
|
-
auditLog("DISCARD INVALID GENESIS VALUE", { path: value.path, time: value.time.time });
|
|
424
|
-
console.error(red(`Ignoring a value that is ${formatTime(Date.now() - value.time.time)} pass change time, which would break assumptions made by archives / locks about change stability. ${debugPathValuePath(value)}`));
|
|
425
|
-
return false;
|
|
426
|
-
}
|
|
427
|
-
if (value.time.time > maxFutureAge) {
|
|
428
|
-
auditLog("DISCARD INVALID FUTURE VALUE", { path: value.path, time: value.time.time });
|
|
429
|
-
console.error(red(`Ignoring a value that is ${formatTime(value.time.time - Date.now())} in the future, which would break writes to that path until that time passes. ${debugPathValuePath(value)}`));
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
return true;
|
|
433
345
|
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// NOTE: Values MUST be synchronously ingested, otherwise our "initialTrigger" resetting will break things.
|
|
437
|
-
this.ingestValues(pathValues, parentsSynced, parentSyncedSources, initialTriggers, specialInitialParentCausedRejections);
|
|
438
|
-
// Wait a microtick to allow triggered watchers to finish.
|
|
439
|
-
await Promise.resolve();
|
|
440
|
-
if (!isNode()) {
|
|
441
|
-
// Wait a frame, to allow painting to happen. (requestAnimationFrame waits until before paint,
|
|
442
|
-
// so we have to wait twice).
|
|
443
|
-
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
444
|
-
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
445
346
|
}
|
|
446
|
-
}
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
@measureFnc
|
|
450
|
-
public ingestValues(
|
|
451
|
-
pathValues: PathValue[],
|
|
452
|
-
parentsSynced: string[] | undefined,
|
|
453
|
-
parentSyncedSources: Map<string, string[]> | undefined,
|
|
454
|
-
initialTriggers?: { values: Set<string>; parentPaths: Set<string> },
|
|
455
|
-
specialInitialParentCausedRejections?: WriteState[],
|
|
456
|
-
): void {
|
|
457
|
-
if (pathValues.length === 0 && !parentsSynced?.length) return;
|
|
458
|
-
|
|
459
|
-
let now = Date.now();
|
|
460
347
|
|
|
461
|
-
// NOTE: specialInitialParentCausedRejections is a bit dangerous, because it can reject golden values.
|
|
462
|
-
// This means we have to ingest it immediately, otherwise authorityStorage might gc path values
|
|
463
|
-
// (that are before a golden value), when rejections might remove that golden value.
|
|
464
|
-
if (specialInitialParentCausedRejections?.length) {
|
|
465
|
-
this.ingestValidStates(specialInitialParentCausedRejections);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
authorityStorage.ingestValues(pathValues, parentsSynced, parentSyncedSources);
|
|
469
|
-
|
|
470
|
-
// NOTE: This doesn't seem to be where we lag, so we aren't tracking it anymore.
|
|
471
|
-
for (let value of pathValues) {
|
|
472
|
-
let lag = now - value.time.time;
|
|
473
|
-
trackLag(now, lag);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// NOTE: Also triggers rejections of watched paths
|
|
477
|
-
this.ingestValidStates([], parentsSynced, undefined, pathValues, initialTriggers);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// NOTE: Technically, because we trigger all remotes any time a value changes, even
|
|
481
|
-
// if it was changed by a remote, this could result in excessive notifications. However,
|
|
482
|
-
// the only watchers are ourself, and remotes that consider us an authority.
|
|
483
|
-
// AND, if we are an authority, we aren't asking any remotes for valid states. So...
|
|
484
|
-
// it works out, with authorities receiving few/none external triggerWatchersForValidStateChange calls,
|
|
485
|
-
// and non-authorities having few/no remote watchers.
|
|
486
|
-
@measureFnc
|
|
487
|
-
public ingestValidStates(
|
|
488
|
-
remoteLocksChanged: WriteState[],
|
|
489
|
-
parentsSynced?: string[],
|
|
490
|
-
recomputeValidState?: "recomputeValidState",
|
|
491
|
-
alsoPathValues?: PathValue[],
|
|
492
|
-
initialTriggers?: { values: Set<string>; parentPaths: Set<string> }
|
|
493
|
-
) {
|
|
494
|
-
if (remoteLocksChanged.length === 0 && !parentsSynced?.length && !alsoPathValues?.length) return;
|
|
495
|
-
|
|
496
|
-
let now = Date.now();
|
|
497
|
-
|
|
498
|
-
let valuesChanged = new Set<PathValue>();
|
|
499
|
-
|
|
500
|
-
if (alsoPathValues) {
|
|
501
|
-
let complexNewValues: PathValue[] = [];
|
|
502
|
-
// TODO: We can avoid values being updated in the updateValidStatesFromCache call,
|
|
503
|
-
// and then also being used in setWriteValidState. This might save some time.
|
|
504
|
-
// It is rare though, as valid states are mostly only for function predictions,
|
|
505
|
-
// and usually arrive after the ValuePaths? Maybe...
|
|
506
|
-
writeValidStorage.updateValidStatesFromCache(alsoPathValues);
|
|
507
|
-
for (let pathValue of alsoPathValues) {
|
|
508
|
-
// All the values are assumed to have changed
|
|
509
|
-
valuesChanged.add(pathValue);
|
|
510
|
-
if (pathValue.locks.length > 0) {
|
|
511
|
-
complexNewValues.push(pathValue);
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
let testValidState: WriteState = {
|
|
515
|
-
path: pathValue.path,
|
|
516
|
-
isValid: !!pathValue.valid,
|
|
517
|
-
time: pathValue.time,
|
|
518
|
-
isTransparent: pathValue.isTransparent || pathValue.canGCValue || false,
|
|
519
|
-
};
|
|
520
|
-
let callbacks = lockToCallback.getValidStateChangedTriggers(testValidState);
|
|
521
|
-
if (callbacks.some(x => typeof x !== "string")) {
|
|
522
|
-
complexNewValues.push(pathValue);
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
// If we get here, it is a simple value, with nothing watching it, and no possibility of
|
|
526
|
-
// being rejected, so just mark it as valid.
|
|
527
|
-
writeValidStorage.setWriteValidState(testValidState);
|
|
528
|
-
}
|
|
529
|
-
if (complexNewValues.length > 0) {
|
|
530
|
-
let newValidStates = writeValidStorage.computeValidStates(complexNewValues, now);
|
|
531
|
-
for (let change of newValidStates) {
|
|
532
|
-
remoteLocksChanged.push(change);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
348
|
|
|
537
|
-
|
|
538
|
-
let
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
349
|
+
// path => sourceNodeId
|
|
350
|
+
let parentSyncs = new Map<string, string>();
|
|
351
|
+
|
|
352
|
+
// We need to do a bit of work to properly clear path values that are from old initial triggers. As if we receive two initial triggers, they need to clobber each other. And if we collapse it, we lose that information. So we have to do that here.
|
|
353
|
+
let finalResults = new Map<string, {
|
|
354
|
+
path: string;
|
|
355
|
+
pathValues: PathValue[];
|
|
356
|
+
initialTrigger: boolean;
|
|
357
|
+
batchIndex: number;
|
|
358
|
+
}>();
|
|
359
|
+
|
|
360
|
+
for (let batchIndex = 0; batchIndex < batched.length; batchIndex++) {
|
|
361
|
+
let batch = batched[batchIndex];
|
|
362
|
+
for (let pathValue of batch.pathValues) {
|
|
363
|
+
let initialTrigger = batch.initialTriggers.values.has(pathValue.path);
|
|
364
|
+
let results = finalResults.get(pathValue.path);
|
|
365
|
+
if (!results) {
|
|
366
|
+
results = {
|
|
367
|
+
path: pathValue.path,
|
|
368
|
+
pathValues: [],
|
|
369
|
+
initialTrigger: false,
|
|
370
|
+
batchIndex: -1,
|
|
371
|
+
};
|
|
372
|
+
finalResults.set(pathValue.path, results);
|
|
556
373
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
// - We can tell via looking to see if it is a valid state watcher, but... that happens in a different
|
|
563
|
-
// function, and... optimizing the reject path shouldn't matter, as rejections should rarely
|
|
564
|
-
// happen anyways...
|
|
565
|
-
let valueChanged = authorityStorage.getValueAtTime(changedWrite.path, changedWrite.time);
|
|
566
|
-
if (valueChanged) {
|
|
567
|
-
valuesChanged.add(valueChanged);
|
|
568
|
-
}
|
|
569
|
-
// Have to send the previous value, as the caller might not have it (as we might start
|
|
570
|
-
// rejecting everything, result in the latest valid state being WAY in the past, to the point
|
|
571
|
-
// where it is impossible for any caller to have the value synced!
|
|
572
|
-
if (!changedWrite.isValid) {
|
|
573
|
-
let prevValue = authorityStorage.getValueBeforeTime(changedWrite.path, changedWrite.time);
|
|
574
|
-
if (prevValue) {
|
|
575
|
-
valuesChanged.add(prevValue);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
allWrites.add(changedWrite);
|
|
581
|
-
|
|
582
|
-
let callbacks = lockToCallback.getValidStateChangedTriggers(changedWrite);
|
|
583
|
-
for (let valuesWatching of callbacks) {
|
|
584
|
-
if (typeof valuesWatching !== "string") {
|
|
585
|
-
triggeredSelfChanges.add(valuesWatching);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// TODO: I'm not sure if we need to sort these before updating them?
|
|
591
|
-
// If we experience rejections and then undoing of rejections... we might need to...
|
|
592
|
-
let flatTriggeredChanges = Array.from(new Set(Array.from(triggeredSelfChanges).flat()));
|
|
593
|
-
let changes = writeValidStorage.computeValidStates(flatTriggeredChanges, now, "alreadyWatching");
|
|
594
|
-
for (let change of changes) {
|
|
595
|
-
pendingWriteChanges.add(change);
|
|
596
|
-
}
|
|
597
|
-
// Anything triggered needs to be recomputed
|
|
598
|
-
recomputeValidState = "recomputeValidState";
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Handle valid state watches (mostly other authorities)
|
|
602
|
-
let validChangesPerNode = new Map<string, WriteState[]>();
|
|
603
|
-
for (let write of allWrites) {
|
|
604
|
-
let callbacks = lockToCallback.getValidStateChangedTriggers(write);
|
|
605
|
-
for (let valuesWatching of callbacks) {
|
|
606
|
-
if (typeof valuesWatching !== "string") continue;
|
|
607
|
-
let nodeId = valuesWatching;
|
|
608
|
-
let changes = validChangesPerNode.get(nodeId);
|
|
609
|
-
if (!changes) {
|
|
610
|
-
changes = [];
|
|
611
|
-
validChangesPerNode.set(nodeId, changes);
|
|
374
|
+
let isContinuedBatch = results.batchIndex === batchIndex;
|
|
375
|
+
results.batchIndex = batchIndex;
|
|
376
|
+
if (initialTrigger && !isContinuedBatch) {
|
|
377
|
+
results.pathValues = [];
|
|
378
|
+
results.initialTrigger = true;
|
|
612
379
|
}
|
|
613
|
-
|
|
380
|
+
results.pathValues.push(pathValue);
|
|
614
381
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
// want our values, and cannot properly ingest them (as they wouldn't be able to recursively resolve
|
|
618
|
-
// them, they would be too large, etc, etc).
|
|
619
|
-
for (let [nodeId, validStates] of validChangesPerNode) {
|
|
620
|
-
if (validStates.length > 0) {
|
|
621
|
-
logErrors(PathValueController.nodes[nodeId].onValidChange(validStates));
|
|
382
|
+
for (let parentPath of batch.initialTriggers.parentPaths) {
|
|
383
|
+
parentSyncs.set(parentPath, batch.sourceNodeId);
|
|
622
384
|
}
|
|
623
385
|
}
|
|
624
|
-
}
|
|
625
386
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
pathWatcher.triggerValuesChanged(valuesChanged, parentsSynced, initialTriggers);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
387
|
+
let parentPaths = new Set(parentSyncs.keys());
|
|
388
|
+
let initialValues = new Set(Array.from(finalResults.values()).filter(x => x.initialTrigger).map(x => x.path));
|
|
631
389
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
startTime: Date.now(),
|
|
640
|
-
};
|
|
641
|
-
export function getReceiveLag() {
|
|
642
|
-
let stats = lastLagInfo.value;
|
|
643
|
-
if (stats.sum === 0) return undefined;
|
|
644
|
-
let top = getStatsTop(stats);
|
|
645
|
-
if (!top.topHeavy) {
|
|
646
|
-
return `${formatTime(stats.sum / stats.count)} TCP LAG`;
|
|
647
|
-
}
|
|
648
|
-
let bottomValue = stats.sum - top.value;
|
|
649
|
-
let bottomCount = stats.count - top.count;
|
|
650
|
-
return `${formatTime(top.value / top.count)} * ${formatNumber(top.count)} + ${formatTime(bottomValue / bottomCount)} * ${formatNumber(bottomCount)} LAG`;
|
|
651
|
-
}
|
|
652
|
-
registerMeasureInfo(getReceiveLag);
|
|
653
|
-
function trackLag(now: number, lag: number) {
|
|
654
|
-
if (now - currentLagInfo.startTime > lagWindowSize) {
|
|
655
|
-
lastLagInfo = currentLagInfo;
|
|
656
|
-
currentLagInfo = {
|
|
657
|
-
value: createStatsValue(),
|
|
658
|
-
startTime: now,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
addToStatsValue(currentLagInfo.value, lag);
|
|
390
|
+
validStateComputer.ingestValuesAndValidStates({
|
|
391
|
+
pathValues: Array.from(finalResults.values()).map(x => x.pathValues).flat(),
|
|
392
|
+
parentSyncs: Array.from(parentSyncs.entries()).map(([parentPath, sourceNodeId]) => ({ parentPath, sourceNodeId })),
|
|
393
|
+
initialTriggers: { values: initialValues, parentPaths: parentPaths },
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
);
|
|
662
397
|
}
|
|
663
398
|
|
|
664
|
-
|
|
665
399
|
export const pathValueCommitter = new PathValueCommitter();
|