querysub 0.461.0 → 0.463.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/package.json +1 -1
  2. package/src/-d-trust/NetworkTrust2.ts +1 -1
  3. package/src/0-path-value-core/LockWatcher2.ts +2 -5
  4. package/src/0-path-value-core/PathRouter.ts +2 -3
  5. package/src/0-path-value-core/PathRouterConstants.ts +4 -0
  6. package/src/0-path-value-core/PathValueCommitter.ts +0 -1
  7. package/src/0-path-value-core/PathValueController.ts +1 -1
  8. package/src/0-path-value-core/PathWatcher.ts +14 -5
  9. package/src/0-path-value-core/ValidStateComputer.ts +234 -86
  10. package/src/0-path-value-core/pathValueCore.ts +57 -78
  11. package/src/1-path-client/RemoteWatcher.ts +4 -3
  12. package/src/1-path-client/pathValueClientWatcher.ts +28 -2
  13. package/src/2-proxy/PathValueProxyWatcher.ts +29 -24
  14. package/src/2-proxy/TransactionDelayer.ts +44 -22
  15. package/src/2-proxy/archiveMoveHarness.ts +2 -2
  16. package/src/3-path-functions/PathFunctionRunner.ts +30 -22
  17. package/src/3-path-functions/PathFunctionRunnerMain.ts +1 -3
  18. package/src/4-deploy/deployFunctions.ts +1 -1
  19. package/src/4-deploy/deployMain.ts +1 -1
  20. package/src/4-deploy/edgeClientWatcher.tsx +1 -1
  21. package/src/4-deploy/edgeNodes.ts +1 -1
  22. package/src/4-dom/qreactTest.tsx +1 -1
  23. package/src/4-querysub/Querysub.ts +8 -9
  24. package/src/4-querysub/QuerysubController.ts +19 -1
  25. package/src/4-querysub/permissions.ts +1 -1
  26. package/src/4-querysub/predictionQueue.tsx +1 -1
  27. package/src/4-querysub/querysubPrediction.ts +25 -12
  28. package/src/5-diagnostics/GenericFormat.tsx +1 -1
  29. package/src/5-diagnostics/qreactDebug.tsx +2 -2
  30. package/src/archiveapps/archiveGCEntry.tsx +1 -1
  31. package/src/archiveapps/archiveJoinEntry.ts +3 -3
  32. package/src/config.ts +5 -1
  33. package/src/config2.ts +9 -7
  34. package/src/deployManager/components/CommitModal.tsx +1 -1
  35. package/src/deployManager/components/DeployProgressView.tsx +1 -1
  36. package/src/deployManager/components/MachineDetailPage.tsx +1 -1
  37. package/src/deployManager/components/MachinesListPage.tsx +1 -1
  38. package/src/deployManager/components/ServiceDetailPage.tsx +1 -1
  39. package/src/deployManager/components/ServicesListPage.tsx +1 -1
  40. package/src/deployManager/components/Tools.tsx +1 -1
  41. package/src/deployManager/machineApplyMainCode.ts +3 -3
  42. package/src/deployManager/machineController.ts +1 -1
  43. package/src/deployManager/machineSchema.ts +1 -1
  44. package/src/deployManager/setupMachineMain.ts +1 -1
  45. package/src/diagnostics/FunctionCallInfoState.ts +6 -3
  46. package/src/diagnostics/MachineThreadInfo.tsx +1 -1
  47. package/src/diagnostics/NodeViewer.tsx +2 -1
  48. package/src/diagnostics/StatWarning.tsx +56 -0
  49. package/src/diagnostics/StatsHeader.tsx +248 -0
  50. package/src/diagnostics/StatsOverrides.ts +50 -0
  51. package/src/diagnostics/SyncTestPage.tsx +3 -3
  52. package/src/diagnostics/TimeDebug.tsx +1 -1
  53. package/src/diagnostics/debugger/mcp-server.ts +1 -1
  54. package/src/diagnostics/grossStats/GrossStatsPage.tsx +1 -1
  55. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +1 -1
  56. package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +1 -1
  57. package/src/diagnostics/logs/TimeRangeSelector.tsx +1 -1
  58. package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -1
  59. package/src/diagnostics/logs/errorNotifications2/ErrorWarning.tsx +0 -1
  60. package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +1 -1
  61. package/src/diagnostics/logs/errorNotifications2/errorWatchEntry.ts +1 -1
  62. package/src/diagnostics/logs/errorNotifications2/errorWatcher.ts +1 -1
  63. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +1 -1
  64. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +1 -1
  65. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +1 -1
  66. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +1 -1
  67. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +1 -1
  68. package/src/diagnostics/managementPages.tsx +4 -9
  69. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +1 -1
  70. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +1 -1
  71. package/src/diagnostics/misc-pages/DNSPage.tsx +1 -1
  72. package/src/diagnostics/pathAuditer.ts +6 -6
  73. package/src/diagnostics/statsDefinitions.tsx +253 -0
  74. package/src/functional/throttleRender.ts +1 -1
  75. package/src/library-components/AspectSizedComponent.tsx +1 -1
  76. package/src/library-components/Button.tsx +1 -1
  77. package/src/library-components/ButtonSelector.tsx +1 -1
  78. package/src/library-components/DropdownCustom.tsx +1 -1
  79. package/src/library-components/DropdownSelector.tsx +1 -1
  80. package/src/library-components/Histogram.tsx +1 -1
  81. package/src/library-components/InlinePopup.tsx +1 -1
  82. package/src/library-components/LazyComponent.tsx +5 -1
  83. package/src/library-components/StickyBottomScroll.tsx +1 -1
  84. package/src/library-components/TypedConfigEditor.tsx +1 -1
  85. package/src/library-components/URLParam.ts +5 -9
  86. package/src/library-components/drag.ts +1 -1
  87. package/src/misc/formatJSX.tsx +1 -1
  88. package/src/user-implementation/userData.ts +1 -1
  89. package/test2.ts +1 -1
  90. package/testEntry2.ts +1 -1
  91. package/valid.md +205 -0
  92. package/src/deployManager/LaunchTrackingHeader.tsx +0 -65
  93. package/src/diagnostics/FunctionCallInfo.tsx +0 -141
  94. package/src/diagnostics/PathDistributionInfo.tsx +0 -110
  95. package/src/diagnostics/ValuePathWarning.tsx +0 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.461.0",
3
+ "version": "0.463.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -13,7 +13,7 @@ import debugbreak from "debugbreak";
13
13
  import { devDebugbreak, getDomain, isDevDebugbreak, isPublic, isRecovery } from "../config";
14
14
  import { formatTime } from "socket-function/src/formatting/format";
15
15
  import { runInSerial } from "socket-function/src/batching";
16
- import { Querysub } from "../4-querysub/QuerysubController";
16
+ import { Querysub } from "../4-querysub/Querysub";
17
17
  import { magenta } from "socket-function/src/formatting/logColors";
18
18
 
19
19
  // Cache the untrust list, to prevent bugs from causing too many backend reads (while also allowing
@@ -6,10 +6,6 @@ import { MAX_CHANGE_AGE, Time, PathValue, byLockGroup, compareTime } from "./pat
6
6
  import { PathRouter } from "./PathRouter";
7
7
  import { getInternalValueWatchPrefix, pathWatcher } from "./PathWatcher";
8
8
 
9
- function isGoldenLock(time: number, now: number): boolean {
10
- return time < now - MAX_CHANGE_AGE * 2;
11
- }
12
-
13
9
  function getLockWatcher2NodeId() {
14
10
  return getInternalValueWatchPrefix() + "_LockWatcher2";
15
11
  }
@@ -63,8 +59,9 @@ class LockWatcher2 {
63
59
  if (values.length === 0) return;
64
60
  this.ensureGarbageCollectLoop();
65
61
  let lockGroups = byLockGroup(values);
62
+ let threshold = now - MAX_CHANGE_AGE;
66
63
  for (let [locks, valueGroup] of lockGroups) {
67
- locks = locks.filter(x => !isGoldenLock(x.endTime.time, now));
64
+ locks = locks.filter(x => x.endTime.time > threshold);
68
65
  if (locks.length === 0) continue;
69
66
  for (let lock of locks) {
70
67
  if (!PathRouter.isSelfAuthority(lock.path)) {
@@ -13,6 +13,8 @@ import { rangesOverlap, removeRange } from "../rangeMath";
13
13
  import { decodeParentFilter } from "./hackedPackedPathParentFiltering";
14
14
  import { getBufferInt } from "../bits";
15
15
 
16
+ import { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH } from "./PathRouterConstants";
17
+ export { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH };
16
18
 
17
19
  // Cases
18
20
  // 1) Whole path hash
@@ -22,9 +24,6 @@ import { getBufferInt } from "../bits";
22
24
 
23
25
 
24
26
 
25
- export const LOCAL_DOMAIN = "LOCAL";
26
- export const LOCAL_DOMAIN_PATH = getPathStr1(LOCAL_DOMAIN);
27
-
28
27
  // NOTE: The goal is for a user on the site to require talking to as few authorities as possible. Because most things are nested in lookups, if we have a prefix per root lookup, this should mean if you're on a page, most of the data should be talking to the same authority, as most of the data should have the exact same hash.
29
28
  // - We also support the exclude default case, which allows you to only handle certain prefixes, so you can make a function only run on certain servers.
30
29
  export type AuthoritySpec = {
@@ -0,0 +1,4 @@
1
+ import { getPathStr1 } from "../path";
2
+
3
+ export const LOCAL_DOMAIN = "LOCAL";
4
+ export const LOCAL_DOMAIN_PATH = getPathStr1(LOCAL_DOMAIN);
@@ -123,7 +123,6 @@ class PathValueCommitter {
123
123
  }
124
124
  }
125
125
 
126
- // NOTE: This function will just ignore writes we aren't an authority on, so we can just give it everything.
127
126
  validStateComputer.ingestValuesAndValidStates({
128
127
  pathValues: pathValuesToIngest,
129
128
  parentSyncs: [],
@@ -323,7 +323,7 @@ export class PathValueControllerBase {
323
323
  public async getValuesByPathAndTime(entries: AuditSnapshotEntry[]): Promise<Buffer[]> {
324
324
  let values: PathValue[] = [];
325
325
  for (let entry of entries) {
326
- let value = authorityStorage.getValueExact(entry.path, entry.time);
326
+ let value = authorityStorage.getValueExactMaybeRejected(entry.path, entry.time);
327
327
  if (!value) continue;
328
328
  if (!value.valid) continue;
329
329
  if (value.isTransparent) continue;
@@ -87,6 +87,9 @@ class PathWatcher {
87
87
  parents: Set<string>;
88
88
  fullHistory?: boolean;
89
89
  }>());
90
+ public debugGetWatcherPaths(nodeId: string) {
91
+ return this.watchersToPaths.get(nodeId);
92
+ }
90
93
 
91
94
  /** Automatically watches the remote if it's a remote path.
92
95
  * Automatically unwatches the remote when the paths no longer have any callbacks.
@@ -346,7 +349,7 @@ class PathWatcher {
346
349
  valuesChanged: Set<PathValue>;
347
350
 
348
351
  // Mutates initialTriggers.values
349
- initialTriggers: { values: Set<string>; parentPaths: Set<string> };
352
+ initialTriggers: { values: Set<string>; parentPaths: Set<string>; initialTriggerNonHistoryWatchers?: Set<string> };
350
353
 
351
354
  onlyTriggerNodeId?: string;
352
355
  }) {
@@ -365,6 +368,9 @@ class PathWatcher {
365
368
  for (let path of initialTriggers?.values ?? []) {
366
369
  triggerPaths.add(path);
367
370
  }
371
+ for (let path of initialTriggers?.initialTriggerNonHistoryWatchers ?? []) {
372
+ triggerPaths.add(path);
373
+ }
368
374
 
369
375
  let valuePerPath = keyByArray(Array.from(valuesChanged), x => x.path);
370
376
 
@@ -409,6 +415,7 @@ class PathWatcher {
409
415
 
410
416
  // Important. We can't do the initial trigger if we aren't synced. If we're the authority, this doesn't matter. This is only important for proxy watchers.
411
417
  let isInitialTrigger = !!initialTriggers?.values.has(path) && authorityStorage.isSynced(path);
418
+ let isInitialTriggerNonHistory = !!initialTriggers?.initialTriggerNonHistoryWatchers?.has(path);
412
419
  const triggerNodeChanged = (watcher: NodeId) => {
413
420
  if (onlyTriggerNodeId && watcher !== onlyTriggerNodeId) return;
414
421
  let changes = changedPerCallbacks.get(watcher);
@@ -421,12 +428,14 @@ class PathWatcher {
421
428
  for (let value of values) {
422
429
  changes.values.add(value);
423
430
  }
424
- if (isInitialTrigger) {
431
+ let watcherObj = this.watchersToPaths.get(watcher);
432
+ let fullHistory = watcherObj?.fullHistory;
433
+ if (isInitialTrigger || isInitialTriggerNonHistory && !fullHistory) {
425
434
  changes.initialTriggers.values.add(path);
426
- let watcherObj = this.watchersToPaths.get(watcher);
427
- let fullHistory = watcherObj?.fullHistory;
428
435
  if (fullHistory) {
429
436
  let history = authorityStorage.getValuePlusHistory(path);
437
+ // There's no point in sending them invalid values. This is going to clobber any existing values. So if they had a value that was valid and it's now invalid, if we just don't send it, they'll remove it (and removal literally means setting the valid state to false, so it's identical).
438
+ history = history.filter(x => x.valid);
430
439
  for (let historicalValue of history) {
431
440
  changes.values.add(historicalValue);
432
441
  }
@@ -435,7 +444,7 @@ class PathWatcher {
435
444
  }
436
445
  } else {
437
446
  // NOTE: We won't be given a value if it isn't synced, which will be never because we check if we're synced above. However, the type system doesn't know that.
438
- let value = authorityStorage.getValueAtTime(path);
447
+ let value = authorityStorage.getValueAtOrBeforeTime(path);
439
448
  if (value) {
440
449
  changes.values.add(value);
441
450
  }
@@ -5,31 +5,25 @@ import { isDiskAudit } from "../config";
5
5
  import { getParentPathStr, getPathFromStr } from "../path";
6
6
  import { auditLog } from "./auditLogs";
7
7
  import { PathRouter } from "./PathRouter";
8
- import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue, MAX_CHANGE_AGE } from "./pathValueCore";
8
+ import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue, MAX_CHANGE_AGE, createMissingEpochValue, Time, MISSING_TRANSACTION_PART_TIMEOUT, isOurPrediction, DEFER_LOCK_WINDOW } from "./pathValueCore";
9
9
  import { pathWatcher } from "./PathWatcher";
10
10
  import { lockWatcher2 } from "./LockWatcher2";
11
11
  import { onPathInteracted } from "../diagnostics/pathAuditerCallback";
12
+ import { isMissingTransactionPart } from "../2-proxy/TransactionDelayer";
13
+ import { formatTime } from "socket-function/src/formatting/format";
14
+ import { isDefined } from "../misc";
12
15
 
13
16
  class ValidStateComputer {
14
17
 
15
- private capturePrevValidStates(valuePaths: PathValue[]): Map<PathValue, boolean | undefined> {
16
- let prevValidStates = new Map<PathValue, boolean | undefined>();
17
- for (let value of valuePaths) {
18
- let prevValidState = authorityStorage.getValueAtTime(value.path, value.time)?.valid;
19
- prevValidStates.set(value, prevValidState);
20
- }
21
- return prevValidStates;
22
- }
23
-
24
18
  @measureFnc
25
19
  public ingestValuesAndValidStates(config: {
26
20
  pathValues: PathValue[];
27
21
  parentSyncs: { parentPath: string; sourceNodeId: string }[];
28
22
  initialTriggers: { values: Set<string>; parentPaths: Set<string> };
29
23
  doNotArchive?: boolean;
30
-
31
24
  }) {
32
- let { pathValues, parentSyncs, initialTriggers, doNotArchive } = config;
25
+ let { pathValues, parentSyncs, doNotArchive } = config;
26
+ let initialTriggers = { ...config.initialTriggers, initialTriggerNonHistoryWatchers: new Set<string>() };
33
27
 
34
28
  // TODO: We might want to add back optimizations for "no watches and no locks"?
35
29
  // TODO: If we find it is a serious performance problem, we could just send the valid state instead of sending the full path value and values are rejected (this is what we used to do).
@@ -38,15 +32,47 @@ class ValidStateComputer {
38
32
 
39
33
  authorityStorage.addParentSyncs(parentSyncs);
40
34
 
35
+ // Dedup by (path, time): if multiple values for the same (path, time) arrive in this batch,
36
+ // keep only the last one. Otherwise the storage-vs-incoming dedup below can drop the wrong one
37
+ // (e.g. a correction whose valid state happens to match stale storage gets filtered, while the
38
+ // earlier incorrect value in the same batch survives and clobbers storage).
39
+ {
40
+ let byPath = new Map<string, PathValue[]>();
41
+ for (let value of pathValues) {
42
+ let arr = byPath.get(value.path);
43
+ if (!arr) {
44
+ arr = [];
45
+ byPath.set(value.path, arr);
46
+ }
47
+ let index = binarySearchIndex(arr.length, i => compareTime(arr![i].time, value.time));
48
+ if (index >= 0) {
49
+ arr[index] = value;
50
+ } else {
51
+ arr.splice(~index, 0, value);
52
+ }
53
+ }
54
+ let deduped: PathValue[] = [];
55
+ for (let arr of byPath.values()) {
56
+ for (let v of arr) {
57
+ deduped.push(v);
58
+ }
59
+ }
60
+ deduped.sort((a, b) => compareTime(a.time, b.time));
61
+ pathValues = deduped;
62
+ }
63
+
41
64
  let ourValues: PathValue[] = [];
42
65
  let notOurValues: PathValue[] = [];
66
+ // Split into our values and not our values
67
+ // - Also Ignore values we already received with no valid state change. Required to prevent authorities from infinitely looping while exchanging values.
43
68
  pathValues = pathValues.filter(value => {
44
69
  // NOTE: I think if it's initial trigger we need to process it anyways?
45
70
  if (!config.initialTriggers.values.has(value.path)) {
46
71
  // Ignore values we already have. We receive duplicates, often due to values being broadcast multiple times in order to prevent data loss.
47
- let existingValue = authorityStorage.getValueExact(value.path, value.time);
72
+ let existingValue = authorityStorage.getValueExactMaybeRejected(value.path, value.time);
48
73
  // If we already have the value and the valid state is the exact same, then remove it from path values and don't even add it to our values. Pretend like we never received it.
49
- if (existingValue && !!existingValue.valid === !!value.valid) {
74
+ // NOTE: We don't do any coercion here because we want the undefined state to not equal the false state.
75
+ if (existingValue && existingValue.valid === value.valid) {
50
76
  let parentPath = getParentPathStr(value.path);
51
77
  // We check for the parent trigger late because getting the parent path is a little bit expensive
52
78
  if (!config.initialTriggers.parentPaths.has(parentPath)) {
@@ -70,12 +96,37 @@ class ValidStateComputer {
70
96
  }
71
97
  return true;
72
98
  });
73
- authorityStorage.ingestValues(notOurValues, { doNotArchive });
99
+
100
+ // NOTE: Watch value locks, will internally watch remote locks if we aren't the authority on the values
101
+ lockWatcher2.watchValueLocks(ourValues, now);
102
+
103
+ // ALL of the direct values are assumed changed
104
+ let valuesChanged = new Set<PathValue>(pathValues);
105
+ // ONLY Put values that we are the owner of in this. As for the other ones, we just ingest the updates, but we don't calculate their valid state.
106
+ let dependenciesChanged = new Set<PathValue>(ourValues);
107
+ // AND, Even if they're not our values, we may still be watching them. But then we need to trigger the actual watcher, not the not our value. All watchers are owned by us, but the things that are watched, as in the observed values, might not be owned by us.
108
+ // - AND, Inside of the loop, after we update a value, we also find all the watchers for it. So we're essentially doing one step of the loop early here. That way we don't have to compute the valid state values for ourselves. We short short-circuit that because we're not the owner. But for values that we do own, we do compute the valid state, and then we find the watcher.
109
+ for (let value of notOurValues) {
110
+ let watchers = lockWatcher2.getValuePathWatchers(value);
111
+ for (let watcher of watchers) {
112
+ dependenciesChanged.add(watcher);
113
+ }
114
+
115
+ if (!value.valid) {
116
+ initialTriggers.initialTriggerNonHistoryWatchers.add(value.path);
117
+ }
118
+ }
74
119
 
75
120
 
76
- // Use initial triggers, at least for all the values that we aren't the authority of, to clobber the old values, creating new shallow copies of the old values with a valid state false if we don't have the corresponding value in our new values, including for all parent paths.
121
+ // NOTE: The code should still work if we have duplicate PathValues (same path, same time, but !==). It can be less efficient, but it should still work because this happens in certain cases.
122
+ let initialPrevValidStates = new Map<PathValue, boolean | undefined>();
123
+ for (let value of dependenciesChanged) {
124
+ let prevValidState = authorityStorage.getValueExactMaybeRejected(value.path, value.time)?.valid;
125
+ initialPrevValidStates.set(value, prevValidState);
126
+ }
127
+
128
+ // Create valid state false for old values on initial sync paths (ONLY for notOurValues)
77
129
  {
78
- // TODO: This code is similar to the code in trigger values change, but it's also dealing with watchers and other stuff, so I think it makes sense to have it also here.
79
130
  let fullInitialPaths = new Set<string>();
80
131
  for (let path of initialTriggers?.values ?? []) {
81
132
  fullInitialPaths.add(path);
@@ -92,89 +143,101 @@ class ValidStateComputer {
92
143
  let fullInitialPathsArr = Array.from(fullInitialPaths);
93
144
  fullInitialPathsArr = fullInitialPathsArr.filter(x => !PathRouter.isSelfAuthority(x));
94
145
 
146
+ // Remove all other values (except for predictions)
147
+ // NOTE: We do this before we ingest our values. And the caller makes sure that the values that we have are actually came after this initial sync on the wire or are part of the initial sync, or are the latest, regardless. So, we don't need to worry about issues there, such as not clobbering values that we're about to ingest. That's fine.
148
+
149
+
95
150
  let valueByPath = keyByArray(notOurValues, x => x.path);
151
+
152
+
96
153
  for (let path of fullInitialPathsArr) {
154
+ if (PathRouter.isSelfAuthority(path)) {
155
+ console.error(`Try to apply an initial sync of a value that we're the authority on. This should have been filtered out much earlier. Path: ${path}`);
156
+ continue;
157
+ }
97
158
  let newValues = valueByPath.get(path) ?? [];
98
- let prevValues = authorityStorage.getValuePlusHistory(path);
99
-
100
- newValues.sort((a, b) => compareTime(a.time, b.time));
101
159
 
160
+ let prevValues = authorityStorage.getValuePlusHistory(path);
102
161
  for (let value of prevValues) {
103
- let newIndex = binarySearchIndex(newValues.length, i => compareTime(newValues[i].time, value.time));
104
- if (newIndex < 0) {
105
- let newValue = newValues.at(-1);
106
- console.info(`Rejecting past value due to initial sync: ${debugPathValuePath(value)}`, {
107
- path: value.path,
108
- timeId: value.time.time,
109
- timeIdFull: value.time,
110
- newTimeId: newValue?.time.time,
111
- newTimeIdFull: newValue?.time,
112
- });
113
- ourValues.push({ ...value, valid: false, });
114
- }
162
+ if (!value.valid) continue;
163
+ // Don't remove predictions. They'll remove themselves anyway. They always get clobbered by a server, write, and if they don't get clobbered, they have a timeout. If we remove predictions, then cases where we're constantly writing to new paths are really annoying and start to glitch out on the client, where we predict it, we remove it, and then we add it back.
164
+ // NOTE: If not removing predictions breaks things, we could probably remove predictions, at least when isServer().
165
+ if (isOurPrediction(value)) continue;
166
+
167
+ // NOTE: This isn't required for correctness, it just makes our logs a lot easier to read.
168
+ if (newValues.some(x => compareTime(x.time, value.time) === 0)) continue;
169
+
170
+ // NOTE: If it's one of the new values, ingest values will change the valid state anyway, so it's fine to set it here.
171
+ console.info(`Rejecting past value due to initial sync (might be immediately set again): ${debugPathValuePath(value)}`, {
172
+ path: value.path,
173
+ timeId: value.time.time,
174
+ timeIdFull: value.time,
175
+ });
176
+ initialPrevValidStates.set(value, true);
177
+ // @ts-expect-error
178
+ value.valid = false;
179
+ valuesChanged.add(value);
115
180
  }
116
181
  }
117
182
  }
118
183
 
119
- let initialPrevValidStates: Map<PathValue, boolean | undefined> | undefined = this.capturePrevValidStates(ourValues);
184
+ authorityStorage.ingestValues(notOurValues, { doNotArchive });
120
185
  authorityStorage.ingestValues(ourValues, { doNotArchive });
121
186
 
122
- // NOTE: Watch value locks, will internally watch remote locks if necessary.
123
- lockWatcher2.watchValueLocks(ourValues, now);
124
-
125
- // NOTE: Initial values always get put into computeValidStates, which then ingests, but does not compute values we are not the authority for.
126
- let valuesChanged = new Set<PathValue>(pathValues);
127
- let dependenciesChanged = new Set<PathValue>(ourValues);
128
- let alreadyTriggered = new Set<PathValue>(dependenciesChanged);
129
- // NOTE: For our own values, we don't care what the input valid state is, compute valid states will calculate if it's changed.
130
- for (let value of notOurValues) {
131
- // NOTE: We could be smarter and determine if the value we're receiving has a valid state change. However, it's probably fine.
132
- // NOTE: Any watchers will get their valid states computed. I think that's really the only reason to watch something is if we own it and want to compute its valid state.
133
- let watchers = lockWatcher2.getValuePathWatchers(value);
134
- for (let watcher of watchers) {
135
- if (alreadyTriggered.has(watcher)) continue;
136
- dependenciesChanged.add(watcher);
137
- alreadyTriggered.add(watcher);
138
- }
139
- }
140
187
 
141
188
  let loopCount = 0;
189
+ let deferredAll = new Set<PathValue>();
142
190
 
143
191
  while (dependenciesChanged.size > 0) {
144
192
  loopCount++;
145
193
  if (loopCount > 3000) {
194
+ require("debugbreak")(2);
195
+ debugger;
146
196
  console.error("Too many loops in valid state loop. aborting.", {
147
197
  loopCount,
148
198
  changedCount: dependenciesChanged.size,
149
- totalTriggered: alreadyTriggered.size,
150
199
  exampleChanged: Array.from(dependenciesChanged)[0].path,
151
200
  });
152
201
  break;
153
202
  }
154
203
  // Just because the dependencies change doesn't mean the valid state has changed. We actually have to compute to see if it has changed. Of course, if we're not the authority, we won't compute the valid site at all. It'll just ignore it, which is good. If we're not the authority, we don't want to be doing recursive computation. Because we prepopulate it, the values change with all of the input values, we'll still get a trigger. We just won't get a nested trigger.
155
- let validStateChanged = validStateComputer.computeValidStates(
204
+ let result = validStateComputer.computeValidStates(
156
205
  Array.from(dependenciesChanged),
157
206
  now,
158
207
  initialPrevValidStates,
159
- ).changed;
160
- initialPrevValidStates = undefined;
208
+ );
209
+ let validStateChanged = result.changed;
210
+ for (let d of result.deferred) {
211
+ deferredAll.add(d);
212
+ }
213
+ for (let change of validStateChanged) {
214
+ initialPrevValidStates.set(change, change.valid);
215
+ deferredAll.delete(change);
216
+ }
161
217
  dependenciesChanged.clear();
162
218
 
163
219
  for (let validStateChange of validStateChanged) {
164
220
  valuesChanged.add(validStateChange);
165
- // IMPORTANT! The consequence of this is that if someone predicts a value, sends it to us, it's not valid, we will see it's not valid, and we will tell all of the nodes that are watching that path about all of the values on that path. This is inefficient. However, rejected values should be quite rare, so it shouldn't be too inefficient.
221
+
222
+ // NOTE: This approach should be faster than doing a full initial sync, but we'll see if it breaks anything.
166
223
  if (!validStateChange.valid) {
167
- // If it's not valid, we need to send the next available valid value. But we haven't done all the fullness computation yet, so the current next available valid value might be recomputed, it might be invalid. So we'll just do the initial trigger. It's not going to be that expensive as the GCing should mean that most paths have a small history, and rejections should be quite rare.
168
- initialTriggers.values.add(validStateChange.path);
224
+ initialTriggers.initialTriggerNonHistoryWatchers.add(validStateChange.path);
169
225
  }
226
+
227
+ // // NOTE: As of right now, I think the only reason to force an initial trigger in this case is so that we can bust all the values through the query sub http server to the clients. Because if we just tell the HTTP server that one value has been rejected, it can go tell its clients, but that won't give them the latest value. Which isn't great. We should really have something where we can tell them. HMM...
228
+ // // IMPORTANT! The consequence of this is that if someone predicts a value, sends it to us, it's not valid, we will see it's not valid, and we will tell all of the nodes that are watching that path about all of the values on that path. This is inefficient. However, rejected values should be quite rare, so it shouldn't be too inefficient.
229
+ // if (!validStateChange.valid) {
230
+ // // NOTE: TECHNICALLY, We could restrict this change in the case when a node just created the value, so we haven't told anyone about it. However, also, this is replacing the case where we send that update to all the other nodes. And so, if we do the math, let's say we have a history window of 100 seconds, and 1% of the writes get rejected, and we're writing once per second. Then this would result in sending twice as many values in total, which isn't great, but also a 1% rejection rate is pretty high.
231
+ // initialTriggers.values.add(validStateChange.path);
232
+ // }
170
233
  }
171
234
  // And then for everything that had a valid state change, go check if it has a dependency that was watching it.
235
+ // NOTE: We used to make sure we don't re-trigger something that triggered before, which prevents always running computeValidStates twice on new values. BUT... it causes issues if you have out of order evaluation. We try to fix out of order evaluation in computeValidStates, but I think locks could be setup so it's not possible. So instead... we just allow evaluating values multiple times, which is twice as slow, but... it's probably not a bottleneck.
236
+ // IF we change this, test with a simple x++ update, where the function has no shard hints, to verify it doesn't break (queue about 100 events at once to test)
172
237
  for (let value of validStateChanged) {
173
238
  let watchers = lockWatcher2.getValuePathWatchers(value);
174
239
  for (let watcher of watchers) {
175
- if (alreadyTriggered.has(watcher)) continue;
176
240
  dependenciesChanged.add(watcher);
177
- alreadyTriggered.add(watcher);
178
241
  }
179
242
  }
180
243
  }
@@ -183,13 +246,40 @@ class ValidStateComputer {
183
246
  valuesChanged,
184
247
  initialTriggers
185
248
  });
249
+
250
+ // If any lockGroups deferred decision (waiting on a value from a foreign authority that
251
+ // may still be in flight), schedule a re-evaluation after DEFER_LOCK_DELAY. By then the
252
+ // age check in isLockValid will treat any still-missing locks as definitively rejected.
253
+ if (deferredAll.size > 0) {
254
+ let deferredArr = Array.from(deferredAll);
255
+ let deferredPaths = new Set<string>();
256
+ for (let v of deferredArr) {
257
+ deferredPaths.add(v.path);
258
+ }
259
+ setTimeout(() => {
260
+ this.ingestValuesAndValidStates({
261
+ pathValues: deferredArr,
262
+ parentSyncs: [],
263
+ // Add deferred paths to initialTriggers.values to bypass the storage-vs-incoming
264
+ // dedup (which would otherwise filter these out since storage already has them).
265
+ initialTriggers: { values: deferredPaths, parentPaths: new Set() },
266
+ });
267
+ }, DEFER_LOCK_WINDOW);
268
+ }
186
269
  }
187
270
 
188
- private isLockValid(lock: ReadLock): boolean {
189
- let value = authorityStorage.getValueAtTime(lock.path, lock.endTime);
271
+ private isLockValid(lock: ReadLock, now: number): true | false | "defer" {
272
+ if (lock.readIsTransparent) return true;
273
+ let value = authorityStorage.getValueExactMaybeRejected(lock.path, lock.startTime);
190
274
  if (!value) {
191
275
  // If not synced, we don't want to reject it yet
192
- return !authorityStorage.isSynced(lock.path);
276
+ if (!authorityStorage.isSynced(lock.path)) return true;
277
+ // Value missing on a foreign-authority path. It may still be in flight; defer the decision
278
+ // until DEFER_LOCK_DELAY has elapsed since the lock's startTime.
279
+ if (!PathRouter.isSelfAuthority(lock.path) && now - lock.startTime.time < DEFER_LOCK_WINDOW) {
280
+ return "defer";
281
+ }
282
+ return false;
193
283
  }
194
284
  return !!value.valid;
195
285
  }
@@ -219,55 +309,115 @@ class ValidStateComputer {
219
309
  public computeValidStates(
220
310
  valuePaths: PathValue[],
221
311
  now: number,
222
- prevValidStates = this.capturePrevValidStates(valuePaths)
223
- ): { changed: PathValue[] } {
312
+ prevValidStates: Map<PathValue, boolean | undefined>,
313
+ ): { changed: PathValue[]; deferred: PathValue[] } {
224
314
  let changed: PathValue[] = [];
315
+ let deferred: PathValue[] = [];
225
316
 
226
317
  let lockGroups = byLockGroup(valuePaths);
227
318
 
319
+ // Sort by the lock time to remove unneeded cycles (although we still always loop at least once).
320
+ let entries = Array.from(lockGroups);
321
+ entries = entries.filter(x => x[0].length > 0);
322
+ entries = entries.sort((a, b) => compareTime(a[0][0].endTime, b[0][0].endTime));
323
+ lockGroups = new Map(entries);
324
+
228
325
  for (let [locks, valueGroup] of lockGroups) {
229
326
  if (valueGroup.length === 0) continue;
327
+ if (locks.length === 0) continue;
230
328
 
231
- let valid = true;
232
- for (let lock of locks) {
233
- let age = now - lock.endTime.time;
234
- // If it's old enough, then it's always valid, we can't change the valid save something after its max change age, even if it's invalid.
235
- if (age > MAX_CHANGE_AGE) continue;
236
329
 
237
- if (!this.isLockValid(lock)) {
238
- onPathInteracted(lock.path, 2);
330
+ let valid: true | false | "defer" = true;
331
+
332
+ let lockTime = locks[0].endTime;
333
+ for (let lock of locks) {
334
+ if (compareTime(lock.endTime, lockTime) !== 0) {
335
+ require("debugbreak")(2);
336
+ debugger;
337
+ console.error(`Lock has inconsistent end times. ${debugTime(lock.endTime)} !== ${debugTime(lockTime)}. Path: ${lock.path}`);
239
338
  valid = false;
240
- this.logLockInvalid(valueGroup, lock, now);
241
- break;
242
339
  }
243
- let contention = this.isLockContentionFree(lock);
244
- if (contention) {
245
- onPathInteracted(lock.path, 2);
340
+ }
341
+
342
+ for (let value of valueGroup) {
343
+ if (!PathRouter.isSelfAuthority(value.path)) {
344
+ require("debugbreak")(2);
345
+ debugger;
346
+ console.error(`Somehow we're attempting to compute the valid state of a value we don't own. This should be impossible. ${debugPathValue(value)}`);
246
347
  valid = false;
247
- this.logLockContention(valueGroup, lock, contention, now);
248
- break;
249
348
  }
250
349
  }
350
+
351
+ let invalidReason: string | undefined;
352
+ let age = now - lockTime.time;
353
+ // If it's old enough, then it's always valid, we can't change the valid save something after its max change age, even if it's invalid.
354
+ if (age < MAX_CHANGE_AGE && valid === true) {
355
+ for (let lock of locks) {
356
+ let lockResult = this.isLockValid(lock, now);
357
+ if (lockResult === "defer") {
358
+ valid = "defer";
359
+ // Don't break — a later lock might be definitively false, which beats defer.
360
+ continue;
361
+ }
362
+ if (!lockResult) {
363
+ onPathInteracted(lock.path, 2);
364
+ valid = false;
365
+ let lockValue = authorityStorage.getValueExactMaybeRejected(lock.path, lock.startTime);
366
+ invalidReason = `lock-invalid on ${lock.path} at ${debugTime(lock.startTime)} (value=${lockValue ? debugPathValue(lockValue) : "missing"}, synced=${authorityStorage.isSynced(lock.path)}, transparent=${lock.readIsTransparent})`;
367
+ this.logLockInvalid(valueGroup, lock, now);
368
+ break;
369
+ }
370
+ let contention = this.isLockContentionFree(lock);
371
+ if (contention) {
372
+ onPathInteracted(lock.path, 2);
373
+ valid = false;
374
+ invalidReason = `lock-contention on ${lock.path} range ${debugTime(lock.startTime)}..${debugTime(lock.endTime)} conflicts=[${contention.map(x => debugTime(x.time)).join(", ")}]`;
375
+ this.logLockContention(valueGroup, lock, contention, now);
376
+ break;
377
+ }
378
+ }
379
+
380
+ // NOTE: It's pretty easy for us to look at all the valid path values for the locks and then see if there's an inconsistent transaction. However, I don't think this is a common case. I think the common case is were depending on just a single write, which happened to land on a different server than us.
381
+ }
382
+
383
+ if (valid === "defer") {
384
+ // Don't decide; leave pathValue.valid and storage alone. Schedule retry at outer level.
385
+ for (let pathValue of valueGroup) {
386
+ deferred.push(pathValue);
387
+ }
388
+ continue;
389
+ }
390
+
251
391
  for (let pathValue of valueGroup) {
252
392
  let prevValidState = prevValidStates.get(pathValue);
393
+
253
394
  // IMPORTANT! This means if it didn't previously exist and it's presently rejected, we count that as a change, as it this is going from undefined to false. This is actually very useful, as a lot of places want to know if a write is rejected, so they can display it in the UI for the developer.
254
395
  if (valid === prevValidState) continue;
255
396
 
256
397
  changed.push(pathValue);
257
398
 
258
- // NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
259
- (pathValue as any).valid = valid;
399
+ // NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
400
+ // @ts-expect-error
401
+ pathValue.valid = valid;
402
+
403
+ // HACK: There are times when we might be evaluating the same path value multiple times at once (ex, multiple initial syncs at once). In which case we might not equal the stored path value, so we need to forcefully update the valid state of it.
404
+ let storedPathValue = authorityStorage.getValueExactMaybeRejected(pathValue.path, pathValue.time);
405
+ // I think this is fine. I think it happens if we receive it multiple times due to sharding. It's a bit inefficient, but... SHOULD be fine.
406
+ if (storedPathValue && pathValue !== storedPathValue) {
407
+ // @ts-expect-error
408
+ storedPathValue.valid = valid;
409
+ }
260
410
  }
261
411
  }
262
412
 
263
- return { changed };
413
+ return { changed, deferred };
264
414
  }
265
415
 
266
416
  private logLockInvalid(valueGroup: PathValue[], lock: ReadLock, now: number) {
267
417
  // This is good... unless it happens A LOT. Then the app needs to change (unless it
268
418
  // is a game, or something with realtime competition, in which case this is probably unavoidable).
269
419
  if (valueGroup.length > 0 && lock.endTime.version !== Number.MAX_SAFE_INTEGER && lock.startTime.version !== -2) {
270
- if ((!isCoreQuiet || !isNode()) && debugRejections) {
420
+ if (true) {
271
421
  let timeToReject = now - valueGroup[0].time.time;
272
422
  let message = `!!! (VALUE REJECTED) VALUE REJECTED DUE TO USING MISSING / REJECTED READ!!!(rejected after ${timeToReject}ms at ${Date.now()})`;
273
423
  message += `\n${debugTime(valueGroup[0].time)} (write)`;
@@ -307,9 +457,7 @@ class ValidStateComputer {
307
457
 
308
458
  let changed = valueGroup.filter(x => x.valid);
309
459
  if (changed.length > 0) {
310
- if ((!isCoreQuiet || !isNode())
311
- || debugRejections
312
- ) {
460
+ if (true) {
313
461
  let timeToReject = now - changed[0].time.time;
314
462
  let redMessage = `!!! (VALUE REJECTED) LOCK CONTENTION FIXED VIA REJECTION OF VALUE!!!(rejected after ${timeToReject}ms)`;
315
463
  for (let pathValue of changed) {