querysub 0.406.0 → 0.408.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 (55) hide show
  1. package/bin/audit-disk-values.js +7 -0
  2. package/bin/deploy-prefixes.js +7 -0
  3. package/package.json +5 -3
  4. package/src/-a-archives/archiveCache.ts +12 -9
  5. package/src/-a-auth/certs.ts +1 -1
  6. package/src/-c-identity/IdentityController.ts +9 -1
  7. package/src/-f-node-discovery/NodeDiscovery.ts +63 -10
  8. package/src/0-path-value-core/AuthorityLookup.ts +14 -4
  9. package/src/0-path-value-core/PathRouter.ts +247 -117
  10. package/src/0-path-value-core/PathRouterRouteOverride.ts +1 -1
  11. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +4 -2
  12. package/src/0-path-value-core/PathValueCommitter.ts +68 -31
  13. package/src/0-path-value-core/PathValueController.ts +77 -8
  14. package/src/0-path-value-core/PathWatcher.ts +46 -4
  15. package/src/0-path-value-core/ShardPrefixes.ts +6 -0
  16. package/src/0-path-value-core/ValidStateComputer.ts +20 -8
  17. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +18 -55
  18. package/src/0-path-value-core/pathValueArchives.ts +19 -8
  19. package/src/0-path-value-core/pathValueCore.ts +75 -27
  20. package/src/0-path-value-core/startupAuthority.ts +9 -9
  21. package/src/1-path-client/RemoteWatcher.ts +217 -178
  22. package/src/1-path-client/pathValueClientWatcher.ts +6 -11
  23. package/src/2-proxy/pathValueProxy.ts +2 -3
  24. package/src/3-path-functions/PathFunctionRunner.ts +3 -1
  25. package/src/3-path-functions/syncSchema.ts +6 -2
  26. package/src/4-deploy/deployGetFunctionsInner.ts +1 -1
  27. package/src/4-deploy/deployPrefixes.ts +14 -0
  28. package/src/4-deploy/edgeNodes.ts +1 -1
  29. package/src/4-querysub/Querysub.ts +17 -5
  30. package/src/4-querysub/QuerysubController.ts +21 -10
  31. package/src/4-querysub/predictionQueue.tsx +3 -0
  32. package/src/4-querysub/querysubPrediction.ts +27 -20
  33. package/src/5-diagnostics/nodeMetadata.ts +17 -0
  34. package/src/diagnostics/NodeConnectionsPage.tsx +167 -0
  35. package/src/diagnostics/NodeViewer.tsx +11 -15
  36. package/src/diagnostics/PathDistributionInfo.tsx +102 -0
  37. package/src/diagnostics/SyncTestPage.tsx +19 -8
  38. package/src/diagnostics/auditDiskValues.ts +221 -0
  39. package/src/diagnostics/auditDiskValuesEntry.ts +43 -0
  40. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
  41. package/src/diagnostics/logs/TimeRangeSelector.tsx +3 -3
  42. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +2 -0
  43. package/src/diagnostics/managementPages.tsx +10 -1
  44. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +3 -2
  45. package/src/diagnostics/pathAuditer.ts +21 -0
  46. package/src/path.ts +9 -2
  47. package/src/rangeMath.ts +41 -0
  48. package/tempnotes.txt +5 -58
  49. package/test.ts +13 -295
  50. package/src/diagnostics/benchmark.ts +0 -139
  51. package/src/diagnostics/runSaturationTest.ts +0 -416
  52. package/src/diagnostics/satSchema.ts +0 -64
  53. package/src/test/mongoSatTest.tsx +0 -55
  54. package/src/test/satTest.ts +0 -193
  55. package/src/test/test.tsx +0 -552
@@ -0,0 +1,102 @@
1
+ import { qreact } from "../4-dom/qreact";
2
+ import { css } from "../4-dom/css";
3
+ import { sort } from "socket-function/src/misc";
4
+ import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
5
+ import { pathWatcher } from "../0-path-value-core/PathWatcher";
6
+ import { PathRouter } from "../0-path-value-core/PathRouter";
7
+ import { QuerysubController, querysubNodeId } from "../4-querysub/QuerysubController";
8
+ import { getSyncedController } from "../library-components/SyncedController";
9
+ import { SocketFunction } from "socket-function/SocketFunction";
10
+ import { isCurrentUserSuperUser } from "../user-implementation/userData";
11
+ import { delay } from "socket-function/src/batching";
12
+ import { remoteWatcher } from "../1-path-client/RemoteWatcher";
13
+
14
+ module.hotreload = true;
15
+
16
+ let querysubController = getSyncedController(QuerysubController);
17
+
18
+ let pathNodeCache = new Map<string, string | "missing">();
19
+ let pendingPaths = new Set<string>();
20
+ let flushScheduled = false;
21
+
22
+ function scheduleFlush() {
23
+ if (flushScheduled) return;
24
+ flushScheduled = true;
25
+ let promise = Promise.resolve().then(async () => {
26
+ flushScheduled = false;
27
+ if (pendingPaths.size === 0) return;
28
+ let paths = Array.from(pendingPaths);
29
+ pendingPaths.clear();
30
+ let nodeId = await querysubNodeId();
31
+ await remoteWatcher.flushWatchers();
32
+ if (!nodeId) return;
33
+ let result = await QuerysubController.nodes[nodeId].debugGetPathNodeIds(paths);
34
+ for (let path of paths) {
35
+ pathNodeCache.set(path, result.get(path) ?? "missing");
36
+ }
37
+ });
38
+ proxyWatcher.triggerOnPromiseFinish(promise, { waitReason: "pathDistribution" });
39
+ }
40
+
41
+ function getNodeForPath(path: string): string | "missing" | undefined {
42
+ let cached = pathNodeCache.get(path);
43
+ if (cached !== undefined) return cached;
44
+ pendingPaths.add(path);
45
+ scheduleFlush();
46
+ return undefined;
47
+ }
48
+
49
+ export class PathDistributionInfo extends qreact.Component {
50
+ render() {
51
+ if (!isCurrentUserSuperUser()) return undefined;
52
+
53
+ let nodeSpecs = querysubController(SocketFunction.browserNodeId()).debugGetNodeSpecs();
54
+
55
+ let paths = pathWatcher.getAllWatchedPaths().filter(p => !PathRouter.isLocalPath(p));
56
+ let distribution = new Map<string, string[]>();
57
+ let missingPaths: string[] = [];
58
+ for (let path of paths) {
59
+ let nodeId = getNodeForPath(path);
60
+ if (nodeId === undefined) continue;
61
+ if (nodeId === "missing") {
62
+ missingPaths.push(path);
63
+ continue;
64
+ }
65
+ let nodePaths = distribution.get(nodeId);
66
+ if (!nodePaths) {
67
+ nodePaths = [];
68
+ distribution.set(nodeId, nodePaths);
69
+ }
70
+ nodePaths.push(path);
71
+ }
72
+
73
+ let entries = Array.from(distribution.entries());
74
+ sort(entries, ([nodeId, p]) => nodeId);
75
+ sort(entries, ([nodeId, p]) => (nodeSpecs?.get(nodeId)?.routeStart ?? 0));
76
+
77
+ let titleParts = entries.map(([nodeId, p]) => {
78
+ let spec = nodeSpecs?.get(nodeId);
79
+ let range = spec ? ` (${spec.routeStart}-${spec.routeEnd})` : "";
80
+ return `${nodeId}${range}: ${p.length}`;
81
+ });
82
+ if (missingPaths.length > 0) titleParts.push(`missing: ${missingPaths.length}`);
83
+ let title = titleParts.join("\n");
84
+
85
+ let parts: preact.ComponentChild[] = [];
86
+ for (let i = 0; i < entries.length; i++) {
87
+ let [nodeId, nodePaths] = entries[i];
88
+ if (i > 0) parts.push(" | ");
89
+ let spec = nodeSpecs?.get(nodeId);
90
+ parts.push(<span className={css.button} onClick={() => console.log(`Paths for ${nodeId}:`, nodePaths, spec && { range: `${spec.routeStart}-${spec.routeEnd}` })}>{nodePaths.length}</span>);
91
+ }
92
+ if (missingPaths.length > 0) {
93
+ if (parts.length > 0) parts.push(" | ");
94
+ parts.push(<span className={css.button.boldStyle.hslcolor(0, 100, 60)} onClick={() => console.log("Missing paths:", missingPaths)}>{missingPaths.length}</span>);
95
+ }
96
+
97
+ return <div className={css.hbox(4)} title={title}>
98
+ <span>🌐</span>
99
+ {parts.length > 0 && parts || "..."}
100
+ </div>;
101
+ }
102
+ }
@@ -17,11 +17,9 @@ import { PathAuditerController } from "./pathAuditer";
17
17
  import { t } from "../2-proxy/schema2";
18
18
 
19
19
 
20
- let { data, functions } = Querysub.createSchema<{
21
- values: {
22
- [key: string]: number;
23
- };
24
- }>()({
20
+ let { data, functions } = Querysub.createSchema({
21
+ values: t.lookup(t.number)
22
+ })({
25
23
  functions: {
26
24
  setValue(key: string, value: number) {
27
25
  data().values[key] = value;
@@ -90,6 +88,14 @@ export class SyncTestPage extends qreact.Component {
90
88
  allThreads = allThreads.slice();
91
89
  sort(allThreads, x => x.entrypoint);
92
90
 
91
+ function refreshDelayed() {
92
+ Querysub.onCommitFinished(() => {
93
+ setTimeout(() => {
94
+ syncTestController.refreshAll();
95
+ }, 500);
96
+ });
97
+ }
98
+
93
99
  return (
94
100
  <div className={css.pad2(20).vbox(40)}>
95
101
  <h1>Sync Test Page</h1>
@@ -125,8 +131,11 @@ export class SyncTestPage extends qreact.Component {
125
131
  <div className={css.hbox(10)}>
126
132
  <span class={css.boldStyle}>{key}</span>
127
133
  <span>{value}</span>
128
- <Button onClick={() => functions.incrementValue(key)}>Increment</Button>
129
- <InputLabel value={value} number onChangeValue={value => functions.setValue(key, +value)} />
134
+ <Button onClick={() => { functions.incrementValue(key); refreshDelayed(); }}>Increment</Button>
135
+ <InputLabel value={value} number onChangeValue={value => {
136
+ functions.setValue(key, +value);
137
+ refreshDelayed();
138
+ }} />
130
139
  </div>
131
140
  <div className={css.hbox(10, 2).wrap.marginLeft(20)}>
132
141
  {allThreads.map(thread => {
@@ -149,6 +158,7 @@ export class SyncTestPage extends qreact.Component {
149
158
  path: path,
150
159
  nodeId: thread.nodeId
151
160
  });
161
+ refreshDelayed();
152
162
  }}>
153
163
  Delete Value
154
164
  </Button>
@@ -168,6 +178,7 @@ export class SyncTestPage extends qreact.Component {
168
178
  label="Insert new key"
169
179
  onChangeValue={value => {
170
180
  functions.setValue(value, 0);
181
+ refreshDelayed();
171
182
  }}
172
183
  />
173
184
  <Button onClick={() => {
@@ -205,7 +216,7 @@ class SyncTestControllerBase {
205
216
  }
206
217
 
207
218
  public async getValueAndTime(path: string) {
208
- let pathValue = authorityStorage.getValueAtTime(path);
219
+ let pathValue = authorityStorage.getValueAtTime(path, undefined, false, "noAudit");
209
220
  let value = pathValueSerializer.getPathValue(pathValue);
210
221
  let time = pathValue?.time.time;
211
222
  return { value, time };
@@ -0,0 +1,221 @@
1
+ import { getControllerNodeIdList } from "../-g-core-values/NodeCapabilities";
2
+ import { AuthoritySpec, PathRouter } from "../0-path-value-core/PathRouter";
3
+ import { PathValueController, PathValueControllerBase, AuditSnapshotEntry } from "../0-path-value-core/PathValueController";
4
+ import { MAX_CHANGE_AGE, MAX_TIME_UNTIL_DISK_FLUSH, pathValueArchives, compareTime, PathValue, Time } from "../0-path-value-core/pathValueCore";
5
+ import { binarySearchIndex, compare } from "socket-function/src/misc";
6
+ import { formatNumber, formatTime } from "socket-function/src/formatting/format";
7
+ import { magenta, green, yellow, red } from "socket-function/src/formatting/logColors";
8
+
9
+ function logDiscrepancy(message: string, diskEntry: { path: string; time: Time } | undefined, serverEntry: AuditSnapshotEntry | undefined, authorityNodeId: string) {
10
+ let details: Record<string, unknown> = { authorityNodeId };
11
+
12
+ if (diskEntry) {
13
+ details.path = diskEntry.path;
14
+ details.diskTime = diskEntry.time.time;
15
+ details.diskTimeVersion = diskEntry.time.version;
16
+ details.diskCreatorId = diskEntry.time.creatorId;
17
+ }
18
+
19
+ if (serverEntry) {
20
+ details.path = serverEntry.path;
21
+ details.serverTime = serverEntry.time.time;
22
+ details.serverTimeVersion = serverEntry.time.version;
23
+ details.serverCreatorId = serverEntry.time.creatorId;
24
+ }
25
+
26
+ console.error(`Disk audit: ${message}`, details);
27
+ }
28
+
29
+ export async function auditDiskValues(spec: AuthoritySpec) {
30
+ let startTime = Date.now();
31
+ console.log(magenta("=== Starting Disk Audit ==="));
32
+
33
+ let timeStartWait = Date.now();
34
+ await PathRouter.waitUntilReady();
35
+ let timeWaitReady = Date.now() - timeStartWait;
36
+
37
+ let checkCutOff = Date.now() - MAX_TIME_UNTIL_DISK_FLUSH;
38
+ let authoritiesToCheck = PathRouter.getAllOverlappingAuthorities(spec);
39
+
40
+ console.log(magenta(`Checking ${green(formatNumber(authoritiesToCheck.length))} authorities`));
41
+
42
+ let timeStartLoadDisk = Date.now();
43
+ let snapshot = await pathValueArchives.loadValues(spec);
44
+ let diskValues = Object.values(snapshot.values).flat();
45
+ let timeLoadDisk = Date.now() - timeStartLoadDisk;
46
+
47
+ console.log(green(`Loaded ${magenta(formatNumber(diskValues.length))} values from disk in ${formatTime(timeLoadDisk)}`));
48
+
49
+ let diskValuesByPath = new Map<string, PathValue>();
50
+ for (let value of diskValues) {
51
+ if (!value.valid) continue;
52
+ if (value.canGCValue) continue;
53
+
54
+ let existing = diskValuesByPath.get(value.path);
55
+ if (!existing || compareTime(value.time, existing.time) > 0) {
56
+ diskValuesByPath.set(value.path, value);
57
+ }
58
+ }
59
+
60
+ let serverSnapshotsByAuthority = new Map<string, { spec: AuthoritySpec; serverMap: Map<string, AuditSnapshotEntry> }>();
61
+
62
+ let timeStartLoadAuthorities = Date.now();
63
+ let authoritiesLoaded = 0;
64
+ await Promise.all(authoritiesToCheck.map(async authority => {
65
+ try {
66
+ let serverSnapshot = await PathValueControllerBase.getAuditSnapshot({
67
+ nodeId: authority.nodeId,
68
+ spec,
69
+ });
70
+
71
+ let serverMap = new Map<string, AuditSnapshotEntry>();
72
+ for (let entry of serverSnapshot) {
73
+ serverMap.set(entry.path, entry);
74
+ }
75
+ serverSnapshotsByAuthority.set(authority.nodeId, { spec: authority, serverMap });
76
+ authoritiesLoaded++;
77
+ console.log(magenta(`Loaded authority ${magenta(String(authoritiesLoaded))}/${green(formatNumber(authoritiesToCheck.length))}: ${authority.nodeId} (${magenta(formatNumber(serverSnapshot.length))} entries)`));
78
+ } catch (error) {
79
+ console.error(`Failed to get audit snapshot from authority ${authority.nodeId}`, error);
80
+ }
81
+ }));
82
+ let timeLoadAuthorities = Date.now() - timeStartLoadAuthorities;
83
+
84
+ let totalServerValues = 0;
85
+ for (let [, { serverMap }] of serverSnapshotsByAuthority) {
86
+ totalServerValues += serverMap.size;
87
+ }
88
+
89
+ console.log(yellow(`Starting comparison: ${green(formatNumber(diskValuesByPath.size))} disk values vs ${green(formatNumber(totalServerValues))} server values`));
90
+
91
+ let timeStartComparison = Date.now();
92
+ let issuesOnDiskMissingFromServer = 0;
93
+ let issuesTimeMismatch = 0;
94
+ let issuesOnServerMissingFromDisk = 0;
95
+ let valuesToSync: PathValue[] = [];
96
+ let serverEntriesToFetch = new Map<string, AuditSnapshotEntry[]>();
97
+
98
+ function compareValues(a: PathValue, b: PathValue): number {
99
+ let pathComparison = compare(a.path, b.path);
100
+ if (pathComparison !== 0) return pathComparison;
101
+ return compareTime(a.time, b.time);
102
+ }
103
+
104
+ function addValueToSync(value: PathValue) {
105
+ let index = binarySearchIndex(valuesToSync.length, i => compareValues(valuesToSync[i], value));
106
+ if (index >= 0) return;
107
+
108
+ index = ~index;
109
+ valuesToSync.splice(index, 0, value);
110
+ }
111
+
112
+ for (let [authorityNodeId, { spec: authoritySpec, serverMap }] of serverSnapshotsByAuthority) {
113
+ for (let [path, diskValue] of diskValuesByPath) {
114
+ if (diskValue.time.time > checkCutOff) continue;
115
+ if (diskValue.canGCValue) continue;
116
+
117
+ if (!PathRouter.matchesAuthoritySpec(authoritySpec, path)) continue;
118
+
119
+ let diskEntry = { path: diskValue.path, time: diskValue.time };
120
+ let serverEntry = serverMap.get(path);
121
+ if (!serverEntry) {
122
+ logDiscrepancy("Value on disk but missing from server", diskEntry, undefined, authorityNodeId);
123
+ issuesOnDiskMissingFromServer++;
124
+ addValueToSync(diskValue);
125
+ } else {
126
+ if (serverEntry.time.time > checkCutOff) continue;
127
+
128
+ let comparison = compareTime(diskValue.time, serverEntry.time);
129
+ if (comparison !== 0) {
130
+ logDiscrepancy("Time mismatch between disk and server", diskEntry, serverEntry, authorityNodeId);
131
+ issuesTimeMismatch++;
132
+
133
+ if (comparison > 0) {
134
+ addValueToSync(diskValue);
135
+ } else {
136
+ let entries = serverEntriesToFetch.get(authorityNodeId);
137
+ if (!entries) {
138
+ entries = [];
139
+ serverEntriesToFetch.set(authorityNodeId, entries);
140
+ }
141
+ entries.push(serverEntry);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ for (let [path, serverEntry] of serverMap) {
148
+ if (serverEntry.time.time > checkCutOff) continue;
149
+
150
+ if (!PathRouter.matchesAuthoritySpec(authoritySpec, path)) continue;
151
+
152
+ let diskValue = diskValuesByPath.get(path);
153
+ if (!diskValue) {
154
+ logDiscrepancy("Value on server but missing from disk", undefined, serverEntry, authorityNodeId);
155
+ issuesOnServerMissingFromDisk++;
156
+ let entries = serverEntriesToFetch.get(authorityNodeId);
157
+ if (!entries) {
158
+ entries = [];
159
+ serverEntriesToFetch.set(authorityNodeId, entries);
160
+ }
161
+ entries.push(serverEntry);
162
+ }
163
+ }
164
+ }
165
+ let timeComparison = Date.now() - timeStartComparison;
166
+
167
+ let timeStartFetch = Date.now();
168
+ for (let [authorityNodeId, entries] of serverEntriesToFetch) {
169
+ try {
170
+ let values = await PathValueControllerBase.getValuesByPathAndTime({
171
+ nodeId: authorityNodeId,
172
+ entries,
173
+ });
174
+ for (let value of values) {
175
+ addValueToSync(value);
176
+ }
177
+ } catch (error) {
178
+ console.error(`Failed to fetch values from authority ${authorityNodeId}`, error);
179
+ }
180
+ }
181
+ let timeFetch = Date.now() - timeStartFetch;
182
+
183
+ let timeStartSync = Date.now();
184
+ if (valuesToSync.length > 0) {
185
+ let valuesPerAuthority = PathRouter.getAllAuthoritiesForValues(valuesToSync);
186
+ for (let [authorityNodeId, values] of valuesPerAuthority) {
187
+ await PathValueControllerBase.sendValues({ nodeId: authorityNodeId, pathValues: values });
188
+ }
189
+ }
190
+ let timeSync = Date.now() - timeStartSync;
191
+
192
+ let totalTime = Date.now() - startTime;
193
+
194
+ let totalIssues = issuesOnDiskMissingFromServer + issuesTimeMismatch + issuesOnServerMissingFromDisk;
195
+
196
+ console.log(magenta("=== Disk Audit Summary ==="));
197
+ if (totalIssues === 0) {
198
+ console.log(green(`✓ No issues found`));
199
+ } else {
200
+ console.log(yellow(`Found ${red(formatNumber(totalIssues))} total issues:`));
201
+ if (issuesOnDiskMissingFromServer > 0) {
202
+ console.log(yellow(` - ${red(formatNumber(issuesOnDiskMissingFromServer))} values on disk but missing from server`));
203
+ }
204
+ if (issuesTimeMismatch > 0) {
205
+ console.log(yellow(` - ${red(formatNumber(issuesTimeMismatch))} time mismatches between disk and server`));
206
+ }
207
+ if (issuesOnServerMissingFromDisk > 0) {
208
+ console.log(yellow(` - ${red(formatNumber(issuesOnServerMissingFromDisk))} values on server but missing from disk`));
209
+ }
210
+ console.log(yellow(`Synced ${magenta(formatNumber(valuesToSync.length))} values`));
211
+ }
212
+
213
+ console.log(magenta("=== Timing Breakdown ==="));
214
+ console.log(magenta(`Wait for ready: ${formatTime(timeWaitReady)}`));
215
+ console.log(magenta(`Load disk: ${formatTime(timeLoadDisk)}`));
216
+ console.log(magenta(`Load server: ${formatTime(timeLoadAuthorities)}`));
217
+ console.log(magenta(`Comparison: ${formatTime(timeComparison)}`));
218
+ console.log(magenta(`Fetch values: ${formatTime(timeFetch)}`));
219
+ console.log(magenta(`Sync values: ${formatTime(timeSync)}`));
220
+ console.log(green(`Total time: ${formatTime(totalTime)}`));
221
+ }
@@ -0,0 +1,43 @@
1
+ import { isNodeTrue, timeInHour } from "socket-function/src/misc";
2
+ import { AuthoritySpec } from "../0-path-value-core/PathRouter";
3
+ import { getAllAuthoritySpec } from "../0-path-value-core/PathRouterServerAuthoritySpec";
4
+ import { auditDiskValues } from "./auditDiskValues";
5
+ import yargs from "yargs";
6
+ import { runInfinitePollCallAtStart } from "socket-function/src/batching";
7
+ import { Querysub } from "../4-querysub/Querysub";
8
+
9
+ let yargObj = isNodeTrue() && yargs(process.argv)
10
+ .option("watch", { type: "boolean", desc: "Audit in a loop (otherwise just audit once)" })
11
+ .argv || {}
12
+ ;
13
+
14
+
15
+ const SPLIT_COUNT = 10;
16
+ async function auditDiskValuesNow() {
17
+ let baseSpec = await getAllAuthoritySpec();
18
+ let splitSpecs: AuthoritySpec[] = [];
19
+ for (let i = 0; i < SPLIT_COUNT; i++) {
20
+ splitSpecs.push({
21
+ ...baseSpec,
22
+ routeStart: i * baseSpec.routeEnd / SPLIT_COUNT,
23
+ routeEnd: (i + 1) * baseSpec.routeEnd / SPLIT_COUNT,
24
+ });
25
+ }
26
+ for (let spec of splitSpecs) {
27
+ await auditDiskValues(spec);
28
+ }
29
+ }
30
+ async function main() {
31
+ await Querysub.hostService("audit-disk-values");
32
+ if (yargObj.watch) {
33
+ await runInfinitePollCallAtStart(timeInHour, auditDiskValuesNow);
34
+ } else {
35
+ try {
36
+ // Force, as they are running this manually, and so they probably want to see something happen...
37
+ await auditDiskValuesNow();
38
+ } finally {
39
+ process.exit();
40
+ }
41
+ }
42
+ }
43
+ main().catch(console.error);
@@ -304,7 +304,11 @@ export class LogViewer3 extends qreact.Component {
304
304
  },
305
305
  onResult: (match: LogDatum) => {
306
306
  results.push(match);
307
- sort(results, x => -x.time);
307
+ if (range.searchFromStart) {
308
+ sort(results, x => x.time);
309
+ } else {
310
+ sort(results, x => -x.time);
311
+ }
308
312
  void updateResults();
309
313
  },
310
314
  onResults: (loggerStats: IndexedLogResults) => {
@@ -80,9 +80,9 @@ export class TimeRangeSelector extends qreact.Component {
80
80
  <InputLabel
81
81
  label="Search from Start"
82
82
  checkbox
83
- value={searchFromStartParam.value ? "true" : ""}
84
- onChangeValue={value => {
85
- searchFromStartParam.value = !!value;
83
+ checked={searchFromStartParam.value}
84
+ onChange={e => {
85
+ searchFromStartParam.value = e.currentTarget.checked;
86
86
  }}
87
87
  />
88
88
  <Button
@@ -209,6 +209,8 @@ export class LifeCycleInstanceRenderer extends qreact.Component<{
209
209
  }
210
210
  title={statusTitle}
211
211
  onClick={(e) => {
212
+ // If it's already been removed from the DOM, then don't trigger the toggle
213
+ if (!(e.target as HTMLElement).parentElement) return;
212
214
  if ((e.target as HTMLElement).closest(".LifeCycleRenderer-contents")) {
213
215
  return;
214
216
  }
@@ -28,6 +28,8 @@ import { closeAllModals } from "../5-diagnostics/Modal";
28
28
  import { delay } from "socket-function/src/batching";
29
29
  import { currentViewParam, selectedServiceIdParam } from "../deployManager/urlParams";
30
30
  import { FunctionCallInfo } from "./FunctionCallInfo";
31
+ import { PathDistributionInfo } from "./PathDistributionInfo";
32
+ import { isCurrentUserSuperUser } from "../user-implementation/userData";
31
33
 
32
34
  export const managementPageURL = new URLParam("managementpage", "");
33
35
  export const showingManagementURL = new URLParam("showingmanagement", false);
@@ -114,6 +116,12 @@ export async function registerManagementPages2(config: {
114
116
  controllerName: "AuditLogController",
115
117
  getModule: () => import("./AuditLogPage"),
116
118
  });
119
+ inputPages.push({
120
+ title: "Node Connections",
121
+ componentName: "NodeConnectionsPage",
122
+ controllerName: "NodeConnectionsController",
123
+ getModule: () => import("./NodeConnectionsPage"),
124
+ });
117
125
  inputPages.push({
118
126
  title: "Time",
119
127
  componentName: "TimeDebug",
@@ -357,7 +365,8 @@ class ManagementRoot extends qreact.Component {
357
365
  {pages.map(page =>
358
366
  <ATag values={[{ param: managementPageURL, value: page.componentName }]}>{page.title}</ATag>
359
367
  )}
360
- <FunctionCallInfo />
368
+ {isCurrentUserSuperUser() && <FunctionCallInfo />}
369
+ {isCurrentUserSuperUser() && <PathDistributionInfo />}
361
370
  </div>
362
371
  {currentPage &&
363
372
  <div
@@ -101,7 +101,7 @@ export class ArchiveViewer extends qreact.Component {
101
101
  return;
102
102
  }
103
103
  try {
104
- let newValuePaths = await controller.getValuePaths(getAllAuthoritySpec());
104
+ let newValuePaths = await controller.getValuePaths(await getAllAuthoritySpec());
105
105
  Querysub.localCommit(() => this.state.valuePaths = newValuePaths);
106
106
  } catch (e: any) {
107
107
  Querysub.localCommit(() => {
@@ -715,7 +715,8 @@ class ProcessProgress extends qreact.Component<{
715
715
  const EMPTY_BUFFER = Buffer.alloc(0);
716
716
  class ArchiveViewerControllerBase {
717
717
  public async getValuePaths(authority: AuthoritySpec): Promise<string[]> {
718
- return pathValueArchives.getValuePaths(authority);
718
+ let { pickedPaths } = await pathValueArchives.getValuePaths(authority);
719
+ return pickedPaths;
719
720
  }
720
721
  public async getValuePathSizes(files: string[]): Promise<number[]> {
721
722
  return pathValueArchives.getValuePathSizes(files);
@@ -19,6 +19,7 @@ import { isNode } from "typesafecss";
19
19
  import { isClient } from "../config2";
20
20
  import { isLocal } from "../config";
21
21
  import { pathWatcher } from "../0-path-value-core/PathWatcher";
22
+ import { debugNodeId } from "../-c-identity/IdentityController";
22
23
 
23
24
  if (!isClient()) {
24
25
  // Comment this line out to disable our functionality
@@ -272,10 +273,12 @@ function trackSyncAge(config: {
272
273
 
273
274
  async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[], now: number) {
274
275
  let requests: PathTimeRequest[] = [];
276
+ let originalValues = new Map<string, PathValue | undefined>();
275
277
 
276
278
  for (let pathObj of pathsToAudit) {
277
279
  let path = pathObj.path;
278
280
  let ourLatest = authorityStorage.getValueAtTime(path);
281
+ originalValues.set(path, ourLatest);
279
282
 
280
283
  // Ask for their latest valid (time: undefined means latest)
281
284
  requests.push({ path, time: undefined });
@@ -293,14 +296,32 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
293
296
  }
294
297
  }
295
298
 
299
+ // Wait. That way, we basically can't have race conditions, as there can't be anything which we have seen both before and after the request, which the remote server hasn't seen, as we're giving it three seconds to receive anything we haven't seen.
300
+ await new Promise(resolve => setTimeout(resolve, 3000));
296
301
  let responses = await PathAuditerController.nodes[nodeId].getValidStates(requests);
302
+ // Wait again, also for race conditions. That way, if the server saw something, if we wait three seconds, we'll probably have seen it by the time this timeout is up.
303
+ await new Promise(resolve => setTimeout(resolve, 3000));
297
304
 
298
305
  let valuesToRequest: PathTimeRequest[] = [];
299
306
  let valuesToSend: PathValue[] = [];
300
307
  let pathsToForceSync = new Set<string>();
301
308
 
302
309
  for (let response of responses) {
310
+ let originalValue = originalValues.get(response.path);
303
311
  let ourValue = authorityStorage.getValueAtTime(response.path) || createMissingEpochValue(response.path);
312
+
313
+ let localValueChanged = false;
314
+ if (originalValue === undefined && ourValue.time !== epochTime) {
315
+ localValueChanged = true;
316
+ } else if (originalValue !== undefined && (originalValue.time !== ourValue.time || originalValue.valid !== ourValue.valid)) {
317
+ localValueChanged = true;
318
+ }
319
+
320
+ if (localValueChanged) {
321
+ onPathInteracted(response.path, 2);
322
+ continue;
323
+ }
324
+
304
325
  if (ourValue.isTransparent && response.isTransparent) continue;
305
326
 
306
327
  // it's latest valid is newer than ours
package/src/path.ts CHANGED
@@ -171,9 +171,16 @@ export function getParentPathStr(pathStr: string) {
171
171
  return pathStr.slice(0, getStartOfLastPart(pathStr));
172
172
  }
173
173
 
174
- /** === getPathFromStr(pathStr).slice(-1)[0] */
174
+ /** === getPathFromStr(pathStr).slice(-1)[0] || "" */
175
175
  export function getLastPathPart(pathStr: string) {
176
- return unescapePathPart(pathStr.slice(getStartOfLastPart(pathStr) - pathDelimitEscaped.length, -pathDelimitEscaped.length));
176
+ let lastPartIndex = pathStr.lastIndexOf(pathDelimitEscaped);
177
+ if (lastPartIndex < 0) return "";
178
+ return unescapePathPart(
179
+ pathStr.slice(
180
+ getStartOfLastPart(pathStr),
181
+ lastPartIndex,
182
+ )
183
+ );
177
184
  }
178
185
 
179
186
  /** === getPathStr(getPathFromStr(pathStr).slice(0, -1)) */
@@ -0,0 +1,41 @@
1
+ export type Range = {
2
+ start: number;
3
+ end: number;
4
+ };
5
+
6
+ export type Ranges = Range[];
7
+
8
+ export function rangesOverlap(a: Range, b: Range): boolean {
9
+ return a.start < b.end && a.end > b.start;
10
+ }
11
+
12
+ export function removeRange(ranges: Ranges, range: Range): {
13
+ removedRanges: Range[];
14
+ } {
15
+ let s = range.start;
16
+ let e = range.end;
17
+ let removedRanges: Range[] = [];
18
+ for (let i = ranges.length - 1; i >= 0; i--) {
19
+ let rangeInRanges = ranges[i];
20
+ if (s >= rangeInRanges.end || e <= rangeInRanges.start) continue;
21
+ let startTaken = Math.max(rangeInRanges.start, s);
22
+ let endTaken = Math.min(rangeInRanges.end, e);
23
+ removedRanges.push({ start: startTaken, end: endTaken });
24
+ ranges.splice(i, 1);
25
+ // Add back the parts we didn't overlap
26
+ if (rangeInRanges.start < s) {
27
+ ranges.push({
28
+ start: rangeInRanges.start,
29
+ end: s,
30
+ });
31
+ }
32
+ if (rangeInRanges.end > e) {
33
+ ranges.push({
34
+ start: e,
35
+ end: rangeInRanges.end,
36
+ });
37
+ }
38
+
39
+ }
40
+ return { removedRanges };
41
+ }