querysub 0.518.0 → 0.520.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.
@@ -0,0 +1,324 @@
1
+ module.allowclient = true;
2
+
3
+ import { qreact } from "../../4-dom/qreact";
4
+ import { css } from "typesafecss";
5
+ import { SocketFunction } from "socket-function/SocketFunction";
6
+ import { getAllNodeIds, getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
7
+ import { LatencyController } from "../../-f-node-discovery/LatencyTracking";
8
+ import { TrafficController, TrafficStats } from "../../-f-node-discovery/TrafficTracking";
9
+ import { getDomain } from "../../config";
10
+ import { decodeNodeId } from "sliftutils/misc/https/certs";
11
+ import { getSyncedController } from "../../library-components/SyncedController";
12
+ import { assertIsManagementUser } from "../managementPages";
13
+ import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
14
+ import { timeoutToUndefinedSilent } from "../../errors";
15
+ import { sort } from "socket-function/src/misc";
16
+ import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
17
+ import { getFunctionRunnerIndex, FunctionRunnerNodeInfo } from "../../4-querysub/FunctionRunnerTracking";
18
+ import { formatTime, formatNumber } from "socket-function/src/formatting/format";
19
+ import { LatencyGraph, LatencyGraphNode, LatencyGraphLink, LatencyGraphLabelLine } from "../../library-components/LatencyGraph";
20
+
21
+ const ID_CHARS = 8;
22
+ // Green means querysub, blue means path value, purple means function runner.
23
+ const FUNCTION_COLOR = "hsl(280, 65%, 72%)";
24
+ const PATHVALUE_COLOR = "hsl(210, 75%, 68%)";
25
+ const QUERYSUB_COLOR = "hsl(140, 60%, 60%)";
26
+
27
+ const PROBE_TIMEOUT_MS = 5000;
28
+ const RANGE_BAR_WIDTH_PX = 360;
29
+ const RANGE_BAR_HEIGHT_PX = 14;
30
+ const LATENCY_GRAPH_HEIGHT_PX = 720;
31
+
32
+ type NodeAuthorityInfo = {
33
+ nodeId: string;
34
+ entryPoint?: string;
35
+ spec?: AuthoritySpec;
36
+ };
37
+
38
+ class RoutingTablePageControllerBase {
39
+ public async getAllNodeAuthoritySpecs(): Promise<NodeAuthorityInfo[]> {
40
+ let nodes = await getAllNodeIds();
41
+ return Promise.all(nodes.map(async (nodeId): Promise<NodeAuthorityInfo> => {
42
+ let metadata = await timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, NodeCapabilitiesController.nodes[nodeId].getMetadata());
43
+ return { nodeId, entryPoint: metadata?.entryPoint, spec: metadata?.authoritySpec };
44
+ }));
45
+ }
46
+ // Proxies a single node's own latency map (browser can't talk to nodes directly). One call per node, so the
47
+ // graph can render each node's latencies as they arrive, and just skip nodes that are slow or unreachable.
48
+ public async getNodeLatencies(nodeId: string): Promise<{ [nodeId: string]: number } | undefined> {
49
+ return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, LatencyController.nodes[nodeId].getLatencies());
50
+ }
51
+ // Renamed (was getNodeTraffic) because the returned shape changed — a new name busts any stale synced-call cache.
52
+ public async getNodeTrafficStats(nodeId: string): Promise<TrafficStats | undefined> {
53
+ return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, TrafficController.nodes[nodeId].getTrafficStats());
54
+ }
55
+ }
56
+
57
+ export const RoutingTablePageController = SocketFunction.register(
58
+ "AuthoritySpecPageController-7b8c9d0e-1f2a-3b4c-5d6e-7f8090a1b2c3",
59
+ new RoutingTablePageControllerBase(),
60
+ () => ({
61
+ getAllNodeAuthoritySpecs: {},
62
+ getNodeLatencies: {},
63
+ getNodeTrafficStats: {},
64
+ }),
65
+ () => ({
66
+ hooks: [assertIsManagementUser],
67
+ }),
68
+ {
69
+ noAutoExpose: true,
70
+ }
71
+ );
72
+
73
+ const RoutingTableSynced = getSyncedController(RoutingTablePageController, {
74
+ reads: { getAllNodeAuthoritySpecs: ["nodeAuthoritySpecs"] },
75
+ writes: {},
76
+ });
77
+
78
+ class AuthorityRangeBar extends qreact.Component<{ start: number; end: number }> {
79
+ render() {
80
+ let startPct = this.props.start * 100;
81
+ let widthPct = (this.props.end - this.props.start) * 100;
82
+ return <div className={css.relative.size(RANGE_BAR_WIDTH_PX, RANGE_BAR_HEIGHT_PX).flexShrink0.hsl(0, 0, 92).bord2(0, 0, 75)}>
83
+ <div className={css.absolute.top(0).left(`${startPct}%`).size(`${widthPct}%`, "100%").hsl(210, 65, 50)} />
84
+ </div>;
85
+ }
86
+ }
87
+
88
+ class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
89
+ state = {
90
+ expanded: false,
91
+ };
92
+ render() {
93
+ let info = this.props.info;
94
+ let spec = info.spec!;
95
+ let expanded = this.state.expanded;
96
+ return <div
97
+ className={css.button.vbox(6).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}
98
+ onClick={() => this.state.expanded = !expanded}
99
+ >
100
+ <div className={css.hbox(10).fillWidth}>
101
+ <span>{expanded ? "▼" : "▶"}</span>
102
+ <span className={css.boldStyle}>{info.nodeId}</span>
103
+ <span>{spec.routeStart.toFixed(4)} - {spec.routeEnd.toFixed(4)}</span>
104
+ <AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />
105
+ <span className={css.colorhsl(0, 0, 50)}>width {(spec.routeEnd - spec.routeStart).toFixed(4)}</span>
106
+ {spec.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
107
+ <span className={css.colorhsl(0, 0, 40).ellipsis.flexFillWidth}>{info.entryPoint || "(no entry point)"}</span>
108
+ </div>
109
+ {expanded &&
110
+ <div className={css.vbox(2)}>
111
+ <div className={css.boldStyle}>Prefixes ({spec.prefixes.length}):</div>
112
+ {spec.prefixes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(none)</div>}
113
+ {spec.prefixes.map(p =>
114
+ <div key={p.originalPrefix} className={css.colorhsl(0, 0, 25)}>{p.originalPrefix}</div>
115
+ )}
116
+ </div>
117
+ }
118
+ </div>;
119
+ }
120
+ }
121
+
122
+ class FunctionRunnersSection extends qreact.Component {
123
+ render() {
124
+ let index = getFunctionRunnerIndex();
125
+ let nodes = index?.nodes || [];
126
+ return <div className={css.vbox(8).fillWidth}>
127
+ <h2>Function Runners ({nodes.length})</h2>
128
+ {nodes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(no function runners found yet)</div>}
129
+ {nodes.map(node =>
130
+ <div className={css.vbox(4).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}>
131
+ <div className={css.hbox(10).fillWidth}>
132
+ <span className={css.boldStyle}>{node.nodeId}</span>
133
+ <span className={css.colorhsl(210, 60, 40)}>networks: {node.networks.join(", ")}</span>
134
+ {!node.isPublic && <span className={css.colorhsl(0, 70, 35)}>(non-public)</span>}
135
+ <span>latency {formatTime(node.averageLatency)}</span>
136
+ <span>up for {formatTime(Date.now() - node.startupTime)}</span>
137
+ <span className={css.colorhsl(0, 0, 40).ellipsis}>{node.entryPoint}</span>
138
+ </div>
139
+ {node.shards.map(shard =>
140
+ <div className={css.hbox(10).fillWidth}>
141
+ <span>{shard.shardRange.startFraction.toFixed(4)} - {shard.shardRange.endFraction.toFixed(4)}</span>
142
+ <AuthorityRangeBar start={shard.shardRange.startFraction} end={shard.shardRange.endFraction} />
143
+ {shard.secondaryShardRange && <span className={css.colorhsl(0, 0, 50)}>secondary {shard.secondaryShardRange.startFraction.toFixed(4)} - {shard.secondaryShardRange.endFraction.toFixed(4)}</span>}
144
+ </div>
145
+ )}
146
+ </div>
147
+ )}
148
+ </div>;
149
+ }
150
+ }
151
+
152
+ function nodeLabelLines(config: {
153
+ nodeId: string;
154
+ runner: FunctionRunnerNodeInfo | undefined;
155
+ info: NodeAuthorityInfo | undefined;
156
+ traffic: TrafficStats | undefined;
157
+ }): LatencyGraphLabelLine[] {
158
+ let { nodeId, runner, info, traffic } = config;
159
+ let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
160
+ let thread = (parts?.threadId || "?").slice(0, ID_CHARS);
161
+ let machine = (parts?.machineId || nodeId).slice(0, ID_CHARS);
162
+ let lines: LatencyGraphLabelLine[] = [{ text: `${thread}:${parts?.port ?? "?"}` }];
163
+ // The last segment of the entry-point path is a good human name; shown on one line with the server (machine) id.
164
+ let entryPoint = info?.entryPoint || runner?.entryPoint;
165
+ let name = entryPoint ? entryPoint.split(/[\\/]/).filter(Boolean).at(-1) : undefined;
166
+ lines.push({ text: name ? `${name} ${machine}` : machine });
167
+ // Total data this node sent / received, and the percent of it that went outside our known node list.
168
+ if (traffic) {
169
+ let totals = nodeTotalTraffic(traffic);
170
+ let sum = totals.sent + totals.received;
171
+ if (sum > 0) {
172
+ let outsidePct = Math.round((totals.outside / sum) * 100);
173
+ lines.push({ text: `↑${formatNumber(totals.sent)}B ↓${formatNumber(totals.received)}B ${outsidePct}%⊘` });
174
+ }
175
+ }
176
+ // All function-runner info on one purple line: networks, shard ranges, calls, private.
177
+ if (runner) {
178
+ let fnParts: string[] = [];
179
+ if (runner.networks.length) fnParts.push(runner.networks.join(" "));
180
+ for (let shard of runner.shards) {
181
+ fnParts.push(`fn ${shard.shardRange.startFraction.toFixed(3)}-${shard.shardRange.endFraction.toFixed(3)}`);
182
+ }
183
+ fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)} calls`);
184
+ if (!runner.isPublic) fnParts.push("PRIVATE");
185
+ lines.push({ text: fnParts.join(" "), color: FUNCTION_COLOR });
186
+ }
187
+ // All path-value info on one blue line: shard range (only if real) and values sent.
188
+ let spec = info?.spec;
189
+ let pvParts: string[] = [];
190
+ if (spec && spec.routeStart >= 0 && spec.routeEnd >= 0) {
191
+ pvParts.push(`pv ${spec.routeStart.toFixed(3)}-${spec.routeEnd.toFixed(3)}`);
192
+ }
193
+ if (traffic && (traffic.pathValuesSent || traffic.pathValuesReceived)) {
194
+ pvParts.push(`↑${formatNumber(traffic.pathValuesSent)} ↓${formatNumber(traffic.pathValuesReceived)} values`);
195
+ }
196
+ if (pvParts.length) {
197
+ lines.push({ text: pvParts.join(" "), color: PATHVALUE_COLOR });
198
+ }
199
+ // Querysub activity: how many function calls were submitted to this node's QuerysubController.
200
+ if (traffic?.querysubCalls) {
201
+ lines.push({ text: `${formatNumber(traffic.querysubCalls)} addCalls`, color: QUERYSUB_COLOR });
202
+ }
203
+ return lines;
204
+ }
205
+
206
+ function nodeTotalTraffic(traffic: TrafficStats): { sent: number; received: number; outside: number; } {
207
+ let sent = traffic.outside.sent;
208
+ let received = traffic.outside.received;
209
+ for (let d of Object.values(traffic.perNode)) {
210
+ sent += d.sent;
211
+ received += d.received;
212
+ }
213
+ return { sent, received, outside: traffic.outside.sent + traffic.outside.received };
214
+ }
215
+
216
+ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: NodeAuthorityInfo[] }> {
217
+ render() {
218
+ let nodeIds = this.props.nodeIds;
219
+ let synced = RoutingTableSynced(getBrowserUrlNode());
220
+
221
+ let index = getFunctionRunnerIndex();
222
+ let runnerByNode = new Map<string, FunctionRunnerNodeInfo>();
223
+ for (let node of index?.nodes ?? []) {
224
+ runnerByNode.set(node.nodeId, node);
225
+ }
226
+ let infoByNode = new Map(this.props.infos.map(info => [info.nodeId, info]));
227
+
228
+ // One synced call per node for each data source; each resolves independently so the graph fills in progressively.
229
+ let latencyMaps = new Map<string, { [nodeId: string]: number }>();
230
+ let trafficMaps = new Map<string, TrafficStats>();
231
+ for (let nodeId of nodeIds) {
232
+ let latencies = synced.getNodeLatencies(nodeId);
233
+ if (latencies) latencyMaps.set(nodeId, latencies);
234
+ let traffic = synced.getNodeTrafficStats(nodeId);
235
+ if (traffic) trafficMaps.set(nodeId, traffic);
236
+ }
237
+ // Only render nodes that reported their latencies — ones that never respond probably don't exist.
238
+ let respondedSet = new Set(latencyMaps.keys());
239
+
240
+ // Total bytes on each node-pair link (both directions). Both endpoints report the same link total, so we take
241
+ // the max instead of summing. Circle size = a node's total traffic.
242
+ let pairKey = (a: string, b: string) => a < b ? `${a}|${b}` : `${b}|${a}`;
243
+ let pairTraffic = new Map<string, number>();
244
+ for (let [reporter, traffic] of trafficMaps) {
245
+ for (let [peer, data] of Object.entries(traffic.perNode)) {
246
+ if (reporter === peer || !respondedSet.has(peer)) continue;
247
+ let key = pairKey(reporter, peer);
248
+ pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, data.sent + data.received));
249
+ }
250
+ }
251
+
252
+ let nodes: LatencyGraphNode[] = [...respondedSet].map(nodeId => {
253
+ let traffic = trafficMaps.get(nodeId);
254
+ let totals = traffic ? nodeTotalTraffic(traffic) : undefined;
255
+ return {
256
+ id: nodeId,
257
+ labelLines: nodeLabelLines({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic }),
258
+ weight: totals ? totals.sent + totals.received : 0,
259
+ };
260
+ });
261
+ let links: LatencyGraphLink[] = [];
262
+ for (let [sourceId, latencies] of latencyMaps) {
263
+ for (let [destId, latencyMs] of Object.entries(latencies)) {
264
+ if (!respondedSet.has(destId)) continue;
265
+ links.push({ source: sourceId, destination: destId, latencyMs, weight: pairTraffic.get(pairKey(sourceId, destId)) || 0 });
266
+ }
267
+ }
268
+
269
+ // Per-cluster summary: sum the same per-node metrics across the cluster's members.
270
+ let clusterSummary = (memberIds: string[]): LatencyGraphLabelLine[] => {
271
+ let sent = 0;
272
+ let received = 0;
273
+ let calls = 0;
274
+ let addCalls = 0;
275
+ let dataSent = 0;
276
+ let dataReceived = 0;
277
+ for (let id of memberIds) {
278
+ let traffic = trafficMaps.get(id);
279
+ if (!traffic) continue;
280
+ sent += traffic.pathValuesSent;
281
+ received += traffic.pathValuesReceived;
282
+ calls += traffic.functionsExecuted;
283
+ addCalls += traffic.querysubCalls;
284
+ let totals = nodeTotalTraffic(traffic);
285
+ dataSent += totals.sent;
286
+ dataReceived += totals.received;
287
+ }
288
+ let summary: LatencyGraphLabelLine[] = [];
289
+ summary.push({ text: `↑${formatNumber(dataSent)}B ↓${formatNumber(dataReceived)}B` });
290
+ summary.push({ text: `↑${formatNumber(sent)} ↓${formatNumber(received)} values`, color: PATHVALUE_COLOR });
291
+ summary.push({ text: `${formatNumber(calls)} calls`, color: FUNCTION_COLOR });
292
+ summary.push({ text: `${formatNumber(addCalls)} addCalls`, color: QUERYSUB_COLOR });
293
+ return summary;
294
+ };
295
+
296
+ return <div className={css.vbox(8).fillWidth}>
297
+ <h2 className={css.margin(0)}>Latency Graph ({respondedSet.size}/{nodeIds.length} nodes reported)</h2>
298
+ <div className={css.relative.fillWidth.height(LATENCY_GRAPH_HEIGHT_PX).bord2(0, 0, 85)}>
299
+ <LatencyGraph nodes={nodes} links={links} formatWeight={weight => formatNumber(weight) + "B"} clusterSummary={clusterSummary} />
300
+ </div>
301
+ </div>;
302
+ }
303
+ }
304
+
305
+ export class RoutingTablePage extends qreact.Component {
306
+ render() {
307
+ let infos = RoutingTableSynced(getBrowserUrlNode()).getAllNodeAuthoritySpecs();
308
+ if (!infos) {
309
+ return <div className={css.pad2(16)}>Loading routing table...</div>;
310
+ }
311
+ let allNodeIds = infos.map(x => x.nodeId);
312
+ let routableInfos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
313
+ sort(routableInfos, x => x.spec!.routeStart);
314
+
315
+ return <div className={css.vbox(12).pad2(16).fillWidth}>
316
+ <LatencyGraphSection nodeIds={allNodeIds} infos={infos} />
317
+ <h2>Routing Table ({routableInfos.length})</h2>
318
+ <div className={css.vbox(8).fillWidth}>
319
+ {routableInfos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
320
+ </div>
321
+ <FunctionRunnersSection />
322
+ </div>;
323
+ }
324
+ }
@@ -308,7 +308,8 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
308
308
  let valuesToSend: PathValue[] = [];
309
309
  let pathsToForceSync = new Set<string>();
310
310
 
311
- for (let response of responses) {
311
+ for (let [requestIndex, response] of responses.entries()) {
312
+ let request = requests[requestIndex];
312
313
  let originalValue = originalValues.get(response.path);
313
314
  let ourValue = authorityStorage.getValueAtOrBeforeTime(response.path) || createMissingEpochValue(response.path);
314
315
 
@@ -328,6 +329,64 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
328
329
 
329
330
  if (ourValue.isTransparent && response.isTransparent) continue;
330
331
 
332
+ if (request.time) {
333
+ let ourExact = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
334
+ if (!ourExact || !ourExact.valid) continue;
335
+ if (compareTime(ourExact.time, epochTime) === 0) continue;
336
+ // The exact value = server does not have
337
+ // - Send it our value
338
+ if (!response.time) {
339
+ valuesToSend.push(ourExact);
340
+ trackSyncAge({
341
+ path: request.path,
342
+ ourTimeId: ourExact.time.time,
343
+ remoteTimeId: undefined,
344
+ ourValid: ourExact.valid,
345
+ remoteValid: response.valid,
346
+ remoteNodeId: nodeId,
347
+ reason: "Remote is missing our value, sending it to them",
348
+ });
349
+ continue;
350
+ }
351
+ // The exact value = server has, but says it's not valid
352
+ // - If time < MAX_CHANGE_AGE old, record this, and wait until MAX_CHANGE_AGE (put it in some other kind of queue), and then check again
353
+ // - If time >= MAX_CHANGE_AGE old, then tell it to change it's valid state to ours. Also, tell all other authorities that use this path.
354
+ // NOTE: We skew towards making values valid instead of invalid. So if there's disagreement, we always take the valid value instead of the invalid value.
355
+ // NOTE: The opposite case, if the remote is valid and ours is invalid, is handled by our latest value being older than the remote value, as we're never going to ask about an invalid value. It'll just implicitly change what the latest valid value is.
356
+ if (response.valid === false) {
357
+ let age = now - ourExact.time.time;
358
+ if (age >= MAX_CHANGE_AGE) {
359
+ pathsToForceSync.add(request.path);
360
+ trackSyncAge({
361
+ path: request.path,
362
+ ourTimeId: ourExact.time.time,
363
+ remoteTimeId: response.time.time,
364
+ ourValid: ourExact.valid,
365
+ remoteValid: response.valid,
366
+ remoteNodeId: nodeId,
367
+ reason: "Remote says our value is invalid, but we think it's valid. Telling all nodes about this value to ensure it's in sync everywhere.",
368
+ });
369
+ } else {
370
+ if (!pendingValidityCheckPaths.has(request.path)) {
371
+ let newCheck: PendingValidityCheck = {
372
+ path: request.path,
373
+ ourTimeId: ourExact.time.time,
374
+ remoteTimeId: response.time.time,
375
+ ourValid: ourExact.valid,
376
+ remoteValid: response.valid,
377
+ remoteNodeId: nodeId,
378
+ discoveredAt: now,
379
+ };
380
+ let insertIndex = binarySearchBasic2(pendingValidityChecks, check => check.discoveredAt, newCheck);
381
+ if (insertIndex < 0) insertIndex = ~insertIndex;
382
+ pendingValidityChecks.splice(insertIndex, 0, newCheck);
383
+ pendingValidityCheckPaths.add(request.path);
384
+ }
385
+ }
386
+ }
387
+ continue;
388
+ }
389
+
331
390
  // it's latest valid is newer than ours
332
391
  // - Ask it for the value of the latest
333
392
  if (response.valid && response.time && compareTime(response.time, ourValue.time) > 0) {
@@ -357,42 +416,6 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
357
416
  reason: "Remote is missing our value, sending it to them",
358
417
  });
359
418
  }
360
- // Our latest valid = server has, but says it's not valid
361
- // - If time < MAX_CHANGE_AGE old, record this, and wait until MAX_CHANGE_AGE (put it in some other kind of queue), and then check again
362
- // - If time >= MAX_CHANGE_AGE old, then tell it to change it's valid state to ours. Also, tell all other authorities that use this path.
363
- // NOTE: We skew towards making values valid instead of invalid. So if there's disagreement, we always take the valid value instead of the invalid value.
364
- // NOTE: The opposite case, if the remote is valid and ours is invalid, is handled by our latest value being older than the remote value, as we're never going to ask about an invalid value. It'll just implicitly change what the latest valid value is.
365
- else if (!response.valid && ourValue.valid && ourValue.time !== epochTime) {
366
- let age = now - ourValue.time.time;
367
- if (age >= MAX_CHANGE_AGE) {
368
- pathsToForceSync.add(response.path);
369
- trackSyncAge({
370
- path: response.path,
371
- ourTimeId: ourValue.time.time,
372
- remoteTimeId: response.time?.time,
373
- ourValid: ourValue.valid,
374
- remoteValid: response.valid,
375
- remoteNodeId: nodeId,
376
- reason: "Remote says our value is invalid, but we think it's valid. Telling all nodes about this value to ensure it's in sync everywhere.",
377
- });
378
- } else {
379
- if (!pendingValidityCheckPaths.has(response.path)) {
380
- let newCheck: PendingValidityCheck = {
381
- path: response.path,
382
- ourTimeId: ourValue.time.time,
383
- remoteTimeId: response.time?.time,
384
- ourValid: ourValue.valid ?? false,
385
- remoteValid: response.valid,
386
- remoteNodeId: nodeId,
387
- discoveredAt: now,
388
- };
389
- let insertIndex = binarySearchBasic2(pendingValidityChecks, check => check.discoveredAt, newCheck);
390
- if (insertIndex < 0) insertIndex = ~insertIndex;
391
- pendingValidityChecks.splice(insertIndex, 0, newCheck);
392
- pendingValidityCheckPaths.add(response.path);
393
- }
394
- }
395
- }
396
419
  }
397
420
 
398
421
  if (valuesToRequest.length > 0) {
@@ -404,6 +427,7 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
404
427
  pathValues: receivedValues,
405
428
  parentSyncs: [],
406
429
  initialTriggers: { values: new Set(), parentPaths: new Set() },
430
+ doNotArchive: true,
407
431
  });
408
432
  }
409
433
  }
@@ -447,7 +471,12 @@ class PathAuditerService {
447
471
  public async getValidStates(requests: PathTimeRequest[]): Promise<ValidStateResponse[]> {
448
472
  let results: ValidStateResponse[] = [];
449
473
  for (let request of requests) {
450
- let value = authorityStorage.getValueAtOrBeforeTime(request.path, request.time);
474
+ let value: PathValue | undefined;
475
+ if (request.time) {
476
+ value = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
477
+ } else {
478
+ value = authorityStorage.getValueAtOrBeforeTime(request.path);
479
+ }
451
480
  results.push({
452
481
  path: request.path,
453
482
  time: value?.time,
@@ -462,8 +491,13 @@ class PathAuditerService {
462
491
  public async getPathValues(requests: PathTimeRequest[]): Promise<Buffer[]> {
463
492
  let results: PathValue[] = [];
464
493
  for (let request of requests) {
465
- let value = authorityStorage.getValueAtOrBeforeTime(request.path, request.time);
466
- if (value) {
494
+ let value: PathValue | undefined;
495
+ if (request.time) {
496
+ value = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
497
+ } else {
498
+ value = authorityStorage.getValueAtOrBeforeTime(request.path);
499
+ }
500
+ if (value && value.valid) {
467
501
  results.push(value);
468
502
  }
469
503
  }
@@ -477,6 +511,7 @@ class PathAuditerService {
477
511
  pathValues: values,
478
512
  parentSyncs: [],
479
513
  initialTriggers: { values: new Set(), parentPaths: new Set() },
514
+ doNotArchive: true,
480
515
  });
481
516
  }
482
517