querysub 0.136.0 → 0.138.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.136.0",
3
+ "version": "0.138.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",
@@ -11,7 +11,7 @@ import { errorToUndefined, logErrors, timeoutToUndefined } from "../errors";
11
11
  import { getPathFromStr } from "../path";
12
12
  import { nodePathAuthority, pathValueAuthority2 } from "./NodePathAuthorities";
13
13
  import { PathValueController } from "./PathValueController";
14
- import { PathValue, MAX_ACCEPTED_CHANGE_AGE, lockWatcher, authorityStorage, writeValidStorage, WriteState, lockToCallback, pathWatcher, MAX_CHANGE_AGE, debugPathValue, debugPathValuePath } from "./pathValueCore";
14
+ import { PathValue, MAX_ACCEPTED_CHANGE_AGE, lockWatcher, authorityStorage, writeValidStorage, WriteState, lockToCallback, pathWatcher, MAX_CHANGE_AGE, debugPathValue, debugPathValuePath, compareTime, epochTime } from "./pathValueCore";
15
15
  import debugbreak from "debugbreak";
16
16
  import { red } from "socket-function/src/formatting/logColors";
17
17
  import { registerDynamicResource } from "../diagnostics/trackResources";
@@ -20,7 +20,7 @@ import { formatStats } from "socket-function/src/profiling/statsFormat";
20
20
  import { formatNumber, formatTime } from "socket-function/src/formatting/format";
21
21
  import { isClient } from "../config2";
22
22
  import { remoteWatcher } from "../1-path-client/RemoteWatcher";
23
- import { debugLog, isDebugLogEnabled } from "./debugLogs";
23
+ import { auditLog, isDebugLogEnabled } from "./auditLogs";
24
24
  import { diskLog } from "../diagnostics/logs/diskLogger";
25
25
 
26
26
  /*
@@ -130,7 +130,7 @@ class PathValueCommitter {
130
130
  let valuesPerOtherAuthority = new Map<string, PathValue[]>();
131
131
  for (let pathValue of values) {
132
132
  if (isDebugLogEnabled()) {
133
- debugLog("CREATE VALUE", { path: pathValue.path, time: pathValue.time.time });
133
+ auditLog("CREATE VALUE", { path: pathValue.path, time: pathValue.time.time });
134
134
  }
135
135
 
136
136
  let otherAuthorities = await pathValueAuthority2.getWriteNodes(pathValue.path);
@@ -233,13 +233,13 @@ class PathValueCommitter {
233
233
  sourceNodeId: string;
234
234
  initialTrigger?: "initialTrigger";
235
235
  }[]) => {
236
- let initialTriggers = { values: new Set<PathValue>(), parentPaths: new Set<string>() };
236
+ let initialTriggers = { values: new Set<string>(), parentPaths: new Set<string>() };
237
237
  // Check received values to make sure the authority for them is the same as the sourceNodeId
238
238
  // (OR that WE are the authority). If not... it means we received values after changing the watcher,
239
239
  // which requires us to ignore the other values
240
240
  // (Clients get most of their values from the same server, so they shouldn't require this check)
241
+ const self = this;
241
242
  if (!isClient()) {
242
- const self = this;
243
243
  measureBlock(function ignoreUnrequestedValues() {
244
244
  for (let batch of batched) {
245
245
  batch.pathValues = batch.pathValues.filter(value => {
@@ -255,8 +255,16 @@ class PathValueCommitter {
255
255
  // When we start watching, those values will get clobbered.
256
256
  if (watchingAuthorityId === undefined) return true;
257
257
  if (watchingAuthorityId === batch.sourceNodeId) return true;
258
+ // epochTimes are just indicators that the value has no value, and so are safe to sync
259
+ // from any source. They won't cause future conflicts, because any other value overrides them.
260
+ // - This might not be required?
261
+ if (compareTime(value.time, epochTime) === 0) {
262
+ return true;
263
+ }
264
+
258
265
  // Also warn, because... if we get a lot of these, there might be a bug.
259
266
  // A few when we change watches is possible, but it should be rare.
267
+ auditLog("IGNORING VALUE FROM DIFFERENT AUTHORITY", { path: value.path, watchingAuthorityId, receivedFromAuthority: batch.sourceNodeId });
260
268
  console.warn(`Ignoring a value that was received from a different authority than the one we are watching. Path: ${value.path}, Watching Authority (should be source): ${watchingAuthorityId}, Actual Source Authority: ${batch.sourceNodeId}`);
261
269
  return false;
262
270
  });
@@ -264,29 +272,84 @@ class PathValueCommitter {
264
272
  });
265
273
  }
266
274
 
267
- // The first time we receive value for a watch from a specific node, we need to clobber our history
268
- // (unless we are the authority). Otherwise bad rejected values can stick around,
275
+ let specialInitialParentCausedRejections: WriteState[] = [];
276
+
277
+ // The first time we receive value for a watch from a specific node, we need to reject future
278
+ // missing values (unless we are the authority). Otherwise bad rejected values can stick around,
269
279
  // because they are newer, and our new source doesn't know we have them.
270
280
  measureBlock(function resetOnInitialTrigger() {
271
281
  for (let batch of batched) {
272
282
  if (!batch.initialTrigger) continue;
273
283
  for (let pathValue of batch.pathValues) {
274
- initialTriggers.values.add(pathValue);
275
- authorityStorage.resetForInitialTrigger(pathValue.path);
284
+ initialTriggers.values.add(pathValue.path);
285
+ auditLog("INITIAL SYNC", { path: pathValue.path, time: pathValue.time.time, sourceNodeId: batch.sourceNodeId });
286
+ // We shouldn't receive the initialTrigger flag if we are the authority anyways
287
+ if (nodePathAuthority.isSelfAuthority(pathValue.path)) continue;
288
+ let history = authorityStorage.getValuePlusHistory(pathValue.path);
289
+ for (let value of history) {
290
+ if (compareTime(value.time, pathValue.time) > 0) {
291
+ specialInitialParentCausedRejections.push({
292
+ path: value.path,
293
+ time: value.time,
294
+ isValid: false,
295
+ isTransparent: !!value.isTransparent,
296
+ reason: "future missing (initial trigger)",
297
+ });
298
+ }
299
+ }
276
300
  }
277
- // Reset child paths as well, otherwise extra keys that have been deleted
278
- // don't get properly removed.
301
+ // IF the parent is synced, but we don't receive a path value, then invalidate the
302
+ // old PathValue (as this means it is invalid, but the remote doesn't know to
303
+ // tell us it is invalid, as we only watched the parent, not the specific path).
304
+ // - If the values become unrejected in the future, we will receive them again, which
305
+ // will cause their valid state to be changed to accepted.
306
+ // - Also, if another batch sets the value, it will override our valid state write,
307
+ // as we are writing this in a special array will is always handled first.
279
308
  if (batch.parentsSynced) {
280
309
  for (let parentPath of batch.parentsSynced) {
281
- initialTriggers.parentPaths.add(parentPath);
310
+ auditLog("INITIAL PARENT SYNC", { path: parentPath, sourceNodeId: batch.sourceNodeId });
282
311
  let allChildPaths = authorityStorage.getPathsFromParent(parentPath) || [];
283
312
  let childChecker = remoteWatcher.getChildPatchWatchChecker({
284
313
  parentPath,
285
314
  nodeId: batch.sourceNodeId
286
315
  });
287
316
  for (let childPath of allChildPaths) {
288
- if (childChecker.isWatchedByNodeId(childPath)) {
289
- authorityStorage.resetForInitialTrigger(childPath);
317
+ if (!childChecker.isWatchedByNodeId(childPath)) continue;
318
+ // We shouldn't receive the initialTrigger flag if we are the authority anyways.
319
+ if (nodePathAuthority.isSelfAuthority(childPath)) continue;
320
+ initialTriggers.parentPaths.add(childPath);
321
+
322
+ // NOTE: On initial trigger, we send all child paths, so they should be
323
+ // in the values list, if they exist. AND if we receive them after,
324
+ // they will clobber this rejection
325
+ // - See PathWatcher.watchPath, where when !noInitialTrigger (aka, initialTrigger),
326
+ // we get all paths via authorityStorage.getPathsFromParent, and send their values.
327
+ if (initialTriggers.values.has(childPath)) continue;
328
+
329
+ let values = authorityStorage.getValuePlusHistory(childPath);
330
+ if (values.length === 0) continue;
331
+
332
+ values = values.filter(value => {
333
+ // DO NOT reject epoch values. For missing values, we still need a signal, and the parent
334
+ // sync won't send this, so we need to NOT reject them.
335
+ if (compareTime(value.time, epochTime) === 0) {
336
+ return false;
337
+ }
338
+ return true;
339
+ });
340
+ if (values.length === 0) continue;
341
+
342
+ debugbreak(2);
343
+ debugger;
344
+ console.log(self.getExistingWatchRemoteNodeId(childPath));
345
+ for (let value of values) {
346
+ specialInitialParentCausedRejections.push({
347
+ path: value.path,
348
+ time: value.time,
349
+ isValid: false,
350
+ isTransparent: !!value.isTransparent,
351
+ reason: "child missing (initial trigger)",
352
+ });
290
353
  }
291
354
  }
292
355
  }
@@ -326,10 +389,12 @@ class PathValueCommitter {
326
389
  pathValues = pathValues.filter(value => {
327
390
  if (!nodePathAuthority.isSelfAuthority(value.path)) return true;
328
391
  if (value.time.time < maxAge) {
392
+ auditLog("DISCARD INVALID GENESIS VALUE", { path: value.path, time: value.time.time });
329
393
  console.error(red(`Ignoring a value that is ${formatTime(Date.now() - value.time.time)} pass change time, which would break assumptions made by archives / locks about change stability. ${debugPathValuePath(value)}`));
330
394
  return false;
331
395
  }
332
396
  if (value.time.time > maxFutureAge) {
397
+ auditLog("DISCARD INVALID FUTURE VALUE", { path: value.path, time: value.time.time });
333
398
  console.error(red(`Ignoring a value that is ${formatTime(value.time.time - Date.now())} in the future, which would break writes to that path until that time passes. ${debugPathValuePath(value)}`));
334
399
  return false;
335
400
  }
@@ -338,7 +403,7 @@ class PathValueCommitter {
338
403
  });
339
404
 
340
405
  // NOTE: Values MUST be synchronously ingested, otherwise our "initialTrigger" resetting will break things.
341
- this.ingestValues(pathValues, parentsSynced, parentSyncedSources, initialTriggers);
406
+ this.ingestValues(pathValues, parentsSynced, parentSyncedSources, initialTriggers, specialInitialParentCausedRejections);
342
407
  // Wait a microtick to allow triggered watchers to finish.
343
408
  await Promise.resolve();
344
409
  if (!isNode()) {
@@ -355,7 +420,8 @@ class PathValueCommitter {
355
420
  pathValues: PathValue[],
356
421
  parentsSynced: string[] | undefined,
357
422
  parentSyncedSources: Map<string, string[]> | undefined,
358
- initialTriggers?: { values: Set<PathValue>; parentPaths: Set<string> }
423
+ initialTriggers?: { values: Set<string>; parentPaths: Set<string> },
424
+ specialInitialParentCausedRejections?: WriteState[],
359
425
  ): void {
360
426
  if (pathValues.length === 0 && !parentsSynced?.length) return;
361
427
 
@@ -370,7 +436,7 @@ class PathValueCommitter {
370
436
  }
371
437
 
372
438
  // NOTE: Also triggers rejections of watched paths
373
- this.ingestValidStates([], parentsSynced, undefined, pathValues, initialTriggers);
439
+ this.ingestValidStates(specialInitialParentCausedRejections ?? [], parentsSynced, undefined, pathValues, initialTriggers);
374
440
  }
375
441
 
376
442
  // NOTE: Technically, because we trigger all remotes any time a value changes, even
@@ -385,14 +451,10 @@ class PathValueCommitter {
385
451
  parentsSynced?: string[],
386
452
  recomputeValidState?: "recomputeValidState",
387
453
  alsoPathValues?: PathValue[],
388
- initialTriggers?: { values: Set<PathValue>; parentPaths: Set<string> }
454
+ initialTriggers?: { values: Set<string>; parentPaths: Set<string> }
389
455
  ) {
390
456
  if (remoteLocksChanged.length === 0 && !parentsSynced?.length && !alsoPathValues?.length) return;
391
457
 
392
- if (alsoPathValues?.some(x => x.path.includes("#BREAK"))) {
393
- debugger;
394
- }
395
-
396
458
  let now = Date.now();
397
459
 
398
460
  let valuesChanged = new Set<PathValue>();
@@ -14,7 +14,7 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
14
14
  import { sha256 } from "js-sha256";
15
15
  import debugbreak from "debugbreak";
16
16
  import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
17
- import { debugLog, isDebugLogEnabled } from "./debugLogs";
17
+ import { auditLog, isDebugLogEnabled } from "./auditLogs";
18
18
  import { debugNodeId } from "../-c-identity/IdentityController";
19
19
  import { diskLog } from "../diagnostics/logs/diskLogger";
20
20
  export { pathValueCommitter };
@@ -83,7 +83,7 @@ class PathValueControllerBase {
83
83
  if (isDebugLogEnabled()) {
84
84
  let sourceNodeId = debugNodeId(callerId);
85
85
  for (let value of values) {
86
- debugLog("RECEIVE VALUE", { path: value.path, time: value.time.time, sourceNodeId });
86
+ auditLog("RECEIVE VALUE", { path: value.path, time: value.time.time, sourceNodeId });
87
87
  }
88
88
  }
89
89
  diskLog(`Received PathValues via forwardWrites`, { valueCount: values.length, callerId, });
@@ -178,10 +178,10 @@ class PathValueControllerBase {
178
178
  if (isDebugLogEnabled()) {
179
179
  let sourceNodeId = debugNodeId(callerId);
180
180
  for (let value of config.paths) {
181
- debugLog("WATCH PATH", { path: value, sourceNodeId });
181
+ auditLog("WATCH PATH", { path: value, sourceNodeId });
182
182
  }
183
183
  for (let value of config.parentPaths) {
184
- debugLog("WATCH PARENT PATH", { path: value, sourceNodeId });
184
+ auditLog("WATCH PARENT PATH", { path: value, sourceNodeId });
185
185
  }
186
186
  }
187
187
  pathWatcher.watchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId, initialTrigger: true });
@@ -191,10 +191,10 @@ class PathValueControllerBase {
191
191
  if (isDebugLogEnabled()) {
192
192
  let sourceNodeId = debugNodeId(SocketFunction.getCaller().nodeId);
193
193
  for (let value of config.paths) {
194
- debugLog("UNWATCH PATH", { path: value, sourceNodeId });
194
+ auditLog("UNWATCH PATH", { path: value, sourceNodeId });
195
195
  }
196
196
  for (let value of config.parentPaths) {
197
- debugLog("UNWATCH PARENT PATH", { path: value, sourceNodeId });
197
+ auditLog("UNWATCH PARENT PATH", { path: value, sourceNodeId });
198
198
  }
199
199
  }
200
200
  pathWatcher.unwatchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId });
@@ -4,18 +4,20 @@ import { isDevDebugbreak } from "../config";
4
4
  import { measureWrap } from "socket-function/src/profiling/measure";
5
5
  import { QueueLimited } from "socket-function/src/misc";
6
6
 
7
- export interface DebugLog {
7
+ export type DebugLog = {
8
8
  type: string;
9
9
  time: number;
10
10
  values: { [key: string]: unknown };
11
- }
11
+ };
12
12
 
13
- let ENABLED_LOGGING = isDevDebugbreak();
14
- export function enableDebugLogging() {
13
+ // NOTE: For now, this is actually fairly light, so we'll just turn it on by default. It is very useful
14
+ // in debugging synchronization issues.
15
+ let ENABLED_LOGGING = true;
16
+ export function enableAuditLogging() {
15
17
  ENABLED_LOGGING = true;
16
18
  debugLogFnc = debugLogBase;
17
19
  }
18
- export function disableDebugLogging() {
20
+ export function disableAuditLogging() {
19
21
  ENABLED_LOGGING = false;
20
22
  debugLogFnc = NOOP;
21
23
  }
@@ -32,20 +34,22 @@ export function getLogHistoryIncludes(includes: string) {
32
34
  return Object.values(x.values).some(y => String(y).includes(includes));
33
35
  });
34
36
  }
37
+ (globalThis as any).getLogHistoryEquals = getLogHistoryEquals;
38
+ (globalThis as any).getPathAuditLogs = getLogHistoryEquals;
35
39
  export function getLogHistoryEquals(value: string) {
36
40
  return logHistory.getAllUnordered().filter(x => {
37
41
  return Object.values(x.values).some(y => y === value);
38
42
  });
39
43
  }
40
44
 
41
- export function debugLog(type: string, values: { [key: string]: unknown }) {
45
+ export function auditLog(type: string, values: { [key: string]: unknown }) {
42
46
  debugLogFnc(type, values);
43
47
  }
44
48
  let debugLogFnc = debugLogBase;
45
49
  let NOOP = () => { };
46
50
  function debugLogBase(type: string, values: { [key: string]: unknown }) {
47
51
  if (!ENABLED_LOGGING) {
48
- disableDebugLogging();
52
+ disableAuditLogging();
49
53
  return;
50
54
  }
51
55
  let newEntry: DebugLog = { type, time: Date.now(), values };
@@ -28,7 +28,7 @@ import yargs from "yargs";
28
28
  import { sha256 } from "js-sha256";
29
29
  import { PromiseObj } from "../promise";
30
30
  import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
31
- import { debugLog, isDebugLogEnabled } from "./debugLogs";
31
+ import { auditLog, isDebugLogEnabled } from "./auditLogs";
32
32
  import { diskLog } from "../diagnostics/logs/diskLogger";
33
33
 
34
34
  let yargObj = isNodeTrue() && yargs(process.argv)
@@ -207,6 +207,7 @@ export type WriteState = {
207
207
  // - HOWEVER, writers should just be smart, as it causes a lot of thrashing and work
208
208
  // on all the clients per write, even if nothing changes!
209
209
  isTransparent: boolean;
210
+ reason?: string;
210
211
  };
211
212
 
212
213
  // NOTE: Object values ARE allowed here. They are effectively immutable though, so it is recommended
@@ -679,7 +680,7 @@ class AuthorityPathValueStorage {
679
680
 
680
681
  public resetForInitialTrigger(path: string) {
681
682
  if (isDebugLogEnabled()) {
682
- debugLog("REMOVE FOR INITIAL SYNC", { path });
683
+ auditLog("REMOVE FOR INITIAL SYNC", { path });
683
684
  }
684
685
  this.removePathFromStorage(path, "reset for initial trigger");
685
686
  }
@@ -720,12 +721,20 @@ class AuthorityPathValueStorage {
720
721
  this.getMultiNodesForParent = fnc;
721
722
  }
722
723
  @measureFnc
723
- public ingestValues(newValues: PathValue[], parentsSynced: string[] | undefined, parentSyncedSources: Map<string, string[]> | undefined) {
724
+ public ingestValues(
725
+ newValues: PathValue[],
726
+ parentsSynced: string[] | undefined,
727
+ parentSyncedSources: Map<string, string[]> | undefined,
728
+ // Skips archiving (used if we received the values from another node, in which case
729
+ // not only should the values already be archives, they are likely too old to be archives
730
+ // again!)
731
+ skipArchive?: "skipArchive"
732
+ ) {
724
733
  let now = Date.now();
725
734
 
726
735
  if (isDebugLogEnabled()) {
727
736
  for (let value of newValues) {
728
- debugLog("INGEST VALUE", { path: value.path, time: value.time.time });
737
+ auditLog("INGEST VALUE", { path: value.path, time: value.time.time });
729
738
  }
730
739
  }
731
740
 
@@ -762,7 +771,7 @@ class AuthorityPathValueStorage {
762
771
  this.possiblyUndefinedPaths().value.add(value.path);
763
772
  }
764
773
 
765
- if (!value.event && !value.path.startsWith(LOCAL_DOMAIN_PATH)) {
774
+ if (!skipArchive && !value.event && !value.path.startsWith(LOCAL_DOMAIN_PATH)) {
766
775
  let authorityPath = pathValueAuthority2.getSelfArchiveAuthority(value.path);
767
776
 
768
777
  // Store in pending archive lookup
@@ -983,7 +992,7 @@ class AuthorityPathValueStorage {
983
992
  this.values.delete(path);
984
993
 
985
994
  if (isDebugLogEnabled()) {
986
- debugLog("REMOVE PATH", { path, reason });
995
+ auditLog("REMOVE PATH", { path, reason });
987
996
  }
988
997
 
989
998
  // NOTE: parentPathLookup is important because clients often use Object.keys() to get paths,
@@ -1348,7 +1357,7 @@ class PathWatcher {
1348
1357
  }
1349
1358
  }
1350
1359
 
1351
- public triggerValuesChanged(valuesChanged: Set<PathValue>, parentsSynced?: string[], initialTriggers?: { values: Set<PathValue>; parentPaths: Set<string> }) {
1360
+ public triggerValuesChanged(valuesChanged: Set<PathValue>, parentsSynced?: string[], initialTriggers?: { values: Set<string>; parentPaths: Set<string> }) {
1352
1361
  let changedPerCallbacks = this.getWatchers(valuesChanged, { parentsSynced, pathsAreSynced: true });
1353
1362
  for (let [watch, changes] of changedPerCallbacks) {
1354
1363
  if (initialTriggers) {
@@ -1357,7 +1366,7 @@ class PathWatcher {
1357
1366
  let valuesOther = new Set<PathValue>();
1358
1367
  let parentsOther = new Set<string>();
1359
1368
  for (let value of changes) {
1360
- if (initialTriggers.values.has(value)) {
1369
+ if (initialTriggers.values.has(value.path)) {
1361
1370
  valuesFromInitialTrigger.add(value);
1362
1371
  } else {
1363
1372
  valuesOther.add(value);
@@ -1486,7 +1495,7 @@ class PathWatcher {
1486
1495
  if (isDebugLogEnabled()) {
1487
1496
  for (let pathValue of changes) {
1488
1497
 
1489
- debugLog("SEND VALUE", { path: pathValue.path, time: pathValue.time.time, nodeId: debugNodeId(watcher), transparent: pathValue.isTransparent, canGC: pathValue.canGCValue });
1498
+ auditLog("SEND VALUE", { path: pathValue.path, time: pathValue.time.time, nodeId: debugNodeId(watcher), transparent: pathValue.isTransparent, canGC: pathValue.canGCValue });
1490
1499
  }
1491
1500
  }
1492
1501
  await PathValueController.nodes[watcher].forwardWrites(
@@ -1677,6 +1686,11 @@ class WriteValidStorage {
1677
1686
  });
1678
1687
  }
1679
1688
  public setWriteValidState(write: WriteState, lockless?: boolean): boolean {
1689
+ if (write.isValid) {
1690
+ auditLog(write.reason || "ACCEPTING VALUE", { path: write.path, time: write.time.time });
1691
+ } else {
1692
+ auditLog(write.reason || "REJECTING VALUE", { path: write.path, time: write.time.time });
1693
+ }
1680
1694
  this.ensureGarbageCollectOldState();
1681
1695
 
1682
1696
  // If lockless then it starts valid, and always stays valid.
@@ -17,7 +17,66 @@ import { appendToPathStr, getParentPathStr, getPathDepth, getPathIndexAssert, ge
17
17
  import { isEmpty } from "../misc";
18
18
  import debugbreak from "debugbreak";
19
19
  import { isDevDebugbreak } from "../config";
20
- import { debugLog } from "../0-path-value-core/debugLogs";
20
+ import { auditLog } from "../0-path-value-core/auditLogs";
21
+
22
+ //todonext
23
+ // BOTH PathValueServers crashed due to trying to archive a value that is too far in the past.
24
+ // - Ah, that would happen if we did an audit, so... let's not archive auditted values.
25
+
26
+ //todonext
27
+ // Ugh... now all paths are failing to sync?
28
+ // ".,querysub._com.,PathFunctionRunner.,book3_0.,Data.,data.,sliftist@gmail._com.,books.,bb1c01e5fb3be5828._querysub._com_1712516071167._0005.,pages.,1728369494296._706_0._30634508051801834.,items.,"
29
+
30
+ //todonext
31
+ // OH! Right, if the value doesn't exist...
32
+
33
+
34
+ // This path was received, but ignored, because apparently t
35
+
36
+ //todonext
37
+ // Alright, failing to sync paths is easy
38
+ // - Let's first see if it's on the client
39
+ // - We do receive the value, so... it's a clientside issue
40
+ // ".,querysub._com.,PathFunctionRunner.,book3_0.,Data.,data.,sliftist@gmail._com.,books.,bb1c01e5fb3be5828._querysub._com_1712516071167._0005.,pages.,1728369494296._706_0._30634508051801834.,items.,hasOwnProperty.,"
41
+
42
+
43
+ //todonext
44
+ // BUG! Every time on load (after 15 seconds), we fail to sync UndoWatch. This shouldn't be happening...
45
+ // - It looks like we are trying to load a lot of nested values, which should be atomic? So... why is our schema
46
+ // not working, and even if it isn't... why don't we receive the values eventually?
47
+ // - https://127-0-0-1.querysub.com:1111/?managementpage=AuditLogPage&selected=%221728769700117.415_0.13356892448729063%22&book=bb1c01e5fb3be5828.querysub.com_1712516071167.0005&path=.%2Cquerysub._com.%2CPathFunctionRunner.%2Cbook3_0.%2CData.%2Cdata.%2Csliftist%40gmail._com.%2Cbooks.%2Cbb1c01e5fb3be5828._querysub._com_1712516073485._0002.%2Cpages.%2Cb5da74489986c2f47._querysub._com_1708134597635._0002.%2Citems.%2C0.%2C
48
+
49
+
50
+ //todonext
51
+ // BUG! Our predictions seem to be rejected for a frame before we get the new value. Our predictions
52
+ // can't break like this for such a simple thing...
53
+
54
+
55
+ //todonext
56
+ // Yep, found the bug. We were missing child values in the sync due to having them split between two authorities,
57
+ // but the parent sent both, so it's got lost, and then it deleted the other paths because it figured
58
+ // they are erroneous (as in, bad predictions).
59
+ // - The fix was basically just updating the code that gets the remote node id to use the parent syncer instead
60
+
61
+ //todonext
62
+ // Ugh... we failed to load a value, after starting on the main book page, and then drilling down. And then
63
+ // we loaded it on server restart. UGH... we might be ignoring values we shouldn't...
64
+
65
+
66
+
67
+ //todonext
68
+ // Enable this flag, and verify it stops double sending of values
69
+ // Ex, of this path:
70
+ // https://127-0-0-1.querysub.com:1111/?managementpage=AuditLogPage&selected=%221728769700117.415_0.13356892448729063%22&path=.%2Cquerysub._com.%2CPathFunctionRunner.%2Cbook3_0.%2CData.%2Cdata.%2Csliftist%40gmail._com.%2Cbooks.%2Cbb1c01e5fb3be5828._querysub._com_1712516073485._0002.%2Cpages.%2Cb5da74489986c2f47._querysub._com_1708134597635._0002.%2Citems.%2C0.%2C
71
+ //todonext
72
+ // Add a breakpoint at "IGNORING VALUE..." when we turn stop double sends on. Presently, this happens
73
+ // a lot, due to double syncing. BUT, once we stop double syncing... this should mostly go away.
74
+ const STOP_KEYS_DOUBLE_SENDS = false;
75
+
76
+ //todonext;
77
+ // DEPLOY (do-update AND yarn deploy), and then test some more!
78
+ // - Also get archive.org to recache our site, as we changed our bootstrapper
79
+
21
80
 
22
81
  export class RemoteWatcher {
23
82
  public static DEBUG = false;
@@ -33,6 +92,9 @@ export class RemoteWatcher {
33
92
  start: number;
34
93
  end: number;
35
94
  }>;
95
+ // Watches which we suppressed, due to this parent sync. If this parent sync stops,
96
+ // we will need to re-send the watches.
97
+ suppressedWatches: Set<string>;
36
98
  fullNodeCount: number;
37
99
  }>());
38
100
 
@@ -154,6 +216,8 @@ export class RemoteWatcher {
154
216
  private internalWatchLatest(config: WatchConfig & {
155
217
  debugName?: string;
156
218
  tryReconnect?: boolean;
219
+ // Watch, even if we were already watching
220
+ forceWatch?: boolean;
157
221
  }) {
158
222
  let watchesPerAuthority = new Map<string, { paths: string[]; parentPaths: string[] }>();
159
223
 
@@ -173,6 +237,14 @@ export class RemoteWatcher {
173
237
  // to come back out of order, causing watching functions to trigger out of order!
174
238
  let remoteAuthorityId = this.remoteWatchPaths.get(path);
175
239
  if (remoteAuthorityId !== undefined) {
240
+ if (config.forceWatch) {
241
+ let watchesPer = watchesPerAuthority.get(remoteAuthorityId);
242
+ if (!watchesPer) {
243
+ watchesPer = { paths: [], parentPaths: [] };
244
+ watchesPerAuthority.set(remoteAuthorityId, watchesPer);
245
+ }
246
+ watchesPer.paths.push(path);
247
+ }
176
248
  this.disconnectedPaths.delete(path);
177
249
  continue;
178
250
  }
@@ -187,9 +259,11 @@ export class RemoteWatcher {
187
259
  if (!this.disconnectedPaths.has(path)) {
188
260
  newDisconnectPaths++;
189
261
  this.disconnectedPaths.add(path);
262
+ auditLog("NO AUTHORITY FOR PATH", { path });
190
263
  }
191
264
  continue;
192
265
  }
266
+ auditLog("FOUND AUTHORITY FOR PATH", { path, authorityId });
193
267
  this.disconnectedPaths.delete(path);
194
268
  foundPaths++;
195
269
 
@@ -281,7 +355,7 @@ export class RemoteWatcher {
281
355
  }
282
356
 
283
357
  if (!watchObj) {
284
- watchObj = { nodesId: new Map(), fullNodeCount: 0 };
358
+ watchObj = { nodesId: new Map(), fullNodeCount: 0, suppressedWatches: new Set() };
285
359
  this.remoteWatchParents.set(path, watchObj);
286
360
  }
287
361
  watchObj.fullNodeCount = nodes.length;
@@ -335,13 +409,23 @@ export class RemoteWatcher {
335
409
 
336
410
  if (isDevDebugbreak()) {
337
411
  for (let path of paths) {
338
- debugLog("remoteWatcher outer WATCH", { path, remoteNodeId: authorityId });
412
+ auditLog("remoteWatcher outer WATCH", { path, remoteNodeId: authorityId });
339
413
  }
340
414
  for (let path of parentPaths) {
341
- debugLog("remoteWatcher outer WATCH PARENT", { path, remoteNodeId: authorityId });
415
+ auditLog("remoteWatcher outer WATCH PARENT", { path, remoteNodeId: authorityId });
342
416
  }
343
417
  }
344
418
 
419
+ if (STOP_KEYS_DOUBLE_SENDS) {
420
+ paths = paths.filter(path => {
421
+ let parentPath = getParentPathStr(path);
422
+ let parentObj = this.remoteWatchParents.get(parentPath);
423
+ if (!parentObj) return true;
424
+ parentObj.suppressedWatches.add(path);
425
+ return false;
426
+ });
427
+ }
428
+
345
429
  this.reconnectCache(authorityId);
346
430
  // Don't throw, as the reconnectCache will eventually resolve errors and watch successfully
347
431
  logErrors(this.watchLatestRemote({
@@ -407,10 +491,10 @@ export class RemoteWatcher {
407
491
  for (let [authorityId, { paths, parentPaths }] of byAuthority) {
408
492
  if (isDevDebugbreak()) {
409
493
  for (let path of paths) {
410
- debugLog("remoteWatcher inner WATCH", { path, remoteNodeId: authorityId });
494
+ auditLog("remoteWatcher inner WATCH", { path, remoteNodeId: authorityId });
411
495
  }
412
496
  for (let path of parentPaths) {
413
- debugLog("remoteWatcher inner WATCH PARENT", { path, remoteNodeId: authorityId });
497
+ auditLog("remoteWatcher inner WATCH PARENT", { path, remoteNodeId: authorityId });
414
498
  }
415
499
  }
416
500
  // NOTE: We log watches here because we want to log batched watches
@@ -460,7 +544,6 @@ export class RemoteWatcher {
460
544
  };
461
545
  const innerParentPathRemove = (path: string, remoteAuthorityId: string | undefined) => {
462
546
  if (remoteAuthorityId === undefined) return;
463
- this.remoteWatchParents.delete(path);
464
547
  this.disconnectedParents.delete(path);
465
548
  let paths = unwatchesPerAuthority.get(remoteAuthorityId);
466
549
  if (!paths) {
@@ -474,21 +557,36 @@ export class RemoteWatcher {
474
557
  let remoteAuthorityId = this.remoteWatchPaths.get(path);
475
558
  innerPathRemove(path, remoteAuthorityId);
476
559
  }
560
+ let suppressedRewatches = new Set<string>();
477
561
  for (let path of config.parentPaths) {
478
562
  let watchObj = this.remoteWatchParents.get(path);
479
563
  if (!watchObj) continue;
564
+ // Rewatch anything that we suppressed watching (which happens when STOP_KEYS_DOUBLE_SENDS is set)
565
+ for (let suppressedWatch of watchObj.suppressedWatches) {
566
+ // Only if we are still watching it (which most of the time, we won't be)
567
+ if (!this.remoteWatchPaths.has(suppressedWatch)) continue;
568
+ suppressedRewatches.add(suppressedWatch);
569
+ }
480
570
  for (let authorityId of watchObj.nodesId.keys()) {
481
571
  innerParentPathRemove(path, authorityId);
482
572
  }
483
573
  this.remoteWatchParents.delete(path);
484
574
  }
575
+ if (suppressedRewatches.size > 0) {
576
+ this.internalWatchLatest({
577
+ paths: Array.from(suppressedRewatches),
578
+ parentPaths: [],
579
+ debugName: "watchSuppressedWatches",
580
+ forceWatch: true,
581
+ });
582
+ }
485
583
  for (let [authorityIdBase, { paths, parentPaths }] of unwatchesPerAuthority.entries()) {
486
584
  if (isDevDebugbreak()) {
487
585
  for (let path of paths) {
488
- debugLog("remoteWatcher inner UNWATCH", { path, remoteNodeId: authorityIdBase });
586
+ auditLog("remoteWatcher inner UNWATCH", { path, remoteNodeId: authorityIdBase });
489
587
  }
490
588
  for (let path of parentPaths) {
491
- debugLog("remoteWatcher inner UNWATCH PARENT", { path, remoteNodeId: authorityIdBase });
589
+ auditLog("remoteWatcher inner UNWATCH PARENT", { path, remoteNodeId: authorityIdBase });
492
590
  }
493
591
  }
494
592
  if (!isOwnNodeId(authorityIdBase)) {
@@ -505,7 +603,24 @@ export class RemoteWatcher {
505
603
  this.unwatchLatest(config);
506
604
  }
507
605
 
508
- public getExistingWatchRemoteNodeId(path: string) {
606
+ @measureFnc
607
+ public getExistingWatchRemoteNodeId(path: string): string | undefined {
608
+ let parentPath = getParentPathStr(path);
609
+ let parentWatchObj = this.remoteWatchParents.get(parentPath);
610
+ if (parentWatchObj) {
611
+ for (let [nodeId, obj] of parentWatchObj.nodesId) {
612
+ if (matchesParentRangeFilter({
613
+ parentPath,
614
+ fullPath: path,
615
+ start: obj.start,
616
+ end: obj.end,
617
+ })) {
618
+ return nodeId;
619
+ }
620
+ }
621
+ // NOTE: I think it is a bug if we hit here. There shouldn't be a parent, but have us not match it?
622
+ // But... I also don't see the harm in checking remoteWatchPaths?
623
+ }
509
624
  return this.remoteWatchPaths.get(path);
510
625
  }
511
626
  public getAllRemoteWatchedNodeIds() {
@@ -36,7 +36,7 @@ import { remoteWatcher } from "./RemoteWatcher";
36
36
  import { heapTagObj } from "../diagnostics/heapTag";
37
37
  import { Querysub } from "../4-querysub/QuerysubController";
38
38
  import { isDevDebugbreak } from "../config";
39
- import { debugLog } from "../0-path-value-core/debugLogs";
39
+ import { auditLog } from "../0-path-value-core/auditLogs";
40
40
 
41
41
  const pathValueCtor = heapTagObj("PathValue");
42
42
 
@@ -433,10 +433,10 @@ export class ClientWatcher {
433
433
  let expiryTime = Date.now() + ClientWatcher.WATCH_STICK_TIME;
434
434
  if (isDevDebugbreak()) {
435
435
  for (let path of fullyUnwatchedPaths) {
436
- debugLog("clientWatcher UNWATCH", { path });
436
+ auditLog("clientWatcher UNWATCH", { path });
437
437
  }
438
438
  for (let path of fullyUnwatchedParents) {
439
- debugLog("clientWatcher UNWATCH PARENT", { path });
439
+ auditLog("clientWatcher UNWATCH PARENT", { path });
440
440
  }
441
441
  }
442
442
  for (let path of fullyUnwatchedPaths) {
@@ -533,10 +533,10 @@ export class ClientWatcher {
533
533
 
534
534
  if (isDevDebugbreak()) {
535
535
  for (let path of pathsArray) {
536
- debugLog("clientWatcher WATCH", { path });
536
+ auditLog("clientWatcher WATCH", { path });
537
537
  }
538
538
  for (let path of parentPathsArray) {
539
- debugLog("clientWatcher WATCH PARENT", { path });
539
+ auditLog("clientWatcher WATCH PARENT", { path });
540
540
  }
541
541
  }
542
542
 
@@ -422,9 +422,10 @@ export class PathValueProxyWatcher {
422
422
  /** NOTE: This results in VERY slow performance, and will log A LOT of information. */
423
423
  public static TRACE = false;
424
424
  public static TRACE_WRITES = false;
425
+ public static TRACE_WATCHERS = new Set<string>();
425
426
 
426
427
  private SHOULD_TRACE(watcher: SyncWatcher) {
427
- return PathValueProxyWatcher.DEBUG || PathValueProxyWatcher.TRACE || PathValueProxyWatcher.TRACE_WRITES && watcher.options.canWrite || ClientWatcher.DEBUG_TRIGGERS === "heavy";
428
+ return PathValueProxyWatcher.DEBUG || PathValueProxyWatcher.TRACE || PathValueProxyWatcher.TRACE_WRITES && watcher.options.canWrite || ClientWatcher.DEBUG_TRIGGERS === "heavy" || PathValueProxyWatcher.TRACE_WATCHERS.size > 0 && Array.from(PathValueProxyWatcher.TRACE_WATCHERS).some(x => watcher.debugName.includes(x));
428
429
  }
429
430
 
430
431
  private isUnsyncCheckers = new Set<{ value: number }>();
@@ -917,6 +918,9 @@ export class PathValueProxyWatcher {
917
918
  let result = authorityStorage.temporaryOverride(options.overrides, () =>
918
919
  options.watchFunction()
919
920
  );
921
+ // Clone, otherwise proxies get out of the watcher, which can result in accesses outside
922
+ // of a synced watcher.
923
+ result = deepCloneCborx(result);
920
924
  options.onResultUpdated?.({ result }, undefined, outerWatcher);
921
925
  } catch (e: any) {
922
926
  options.onResultUpdated?.({ error: e.stack }, undefined, outerWatcher);
@@ -37,17 +37,6 @@ let yargObj = yargs(process.argv)
37
37
  setIsDeploy();
38
38
 
39
39
  export async function deployMain() {
40
- if (true as boolean) {
41
- // Mount, so we can write directly to the database
42
- await SocketFunction.mount({ port: 0, ...await getThreadKeyCert(), public: isPublic() });
43
-
44
- await setLiveDeployedHash({ hash: "7b94606dc1de8d03acb8b23952ff5d49e716a45e", refreshThresholdTime: 0, });
45
- //await setLiveDeployedHash({ hash: "4cba43649682967bf1ff919d941820bea6719ef7", refreshThresholdTime: 0, });
46
- console.log(magenta(`SHUTTING DOWN`));
47
- await shutdown();
48
- return;
49
- }
50
-
51
40
  //SocketFunction.logMessages = true;
52
41
  //quietCoreMode();
53
42
  //ClientWatcher.DEBUG_READS = true;
@@ -236,6 +236,7 @@ async function edgeNodeFunction(config: {
236
236
 
237
237
  let progressUI = createProgressUI();
238
238
  progressUI.setMessage("Finding available server...");
239
+ let retryCount = 0;
239
240
  while (true) {
240
241
  try {
241
242
  let booted = await tryToBoot();
@@ -243,7 +244,8 @@ async function edgeNodeFunction(config: {
243
244
  } catch (e) {
244
245
  console.error(e);
245
246
  }
246
- progressUI.setMessage("No available servers, retrying soon");
247
+ retryCount++;
248
+ progressUI.setMessage(`No available servers, retried ${retryCount++} times`);
247
249
  let time = 1000 * 15;
248
250
  if (Date.now() < liveHashOverrideExpiryTime) {
249
251
  time = 1000 * 3;
@@ -8,7 +8,7 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
8
8
  import { green } from "socket-function/src/formatting/logColors";
9
9
  import debugbreak from "debugbreak";
10
10
  import { isDevDebugbreak } from "../config";
11
- import { DebugLog, getLogHistoryEquals } from "../0-path-value-core/debugLogs";
11
+ import { DebugLog, getLogHistoryEquals } from "../0-path-value-core/auditLogs";
12
12
  import { getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
13
13
  import { sort } from "socket-function/src/misc";
14
14
 
@@ -200,7 +200,7 @@ async function checkAuthority(authority: AuthorityPath, threshold: number) {
200
200
  authorityStorage.resetForInitialTrigger(valuePath.path);
201
201
  }
202
202
  }
203
- authorityStorage.ingestValues(Array.from(allValues), undefined, undefined);
203
+ authorityStorage.ingestValues(Array.from(allValues), undefined, undefined, "skipArchive");
204
204
 
205
205
  if (allValues.size > 0) {
206
206
  // NOTE: By triggering all of these as initial syncs... it SHOULD result in us clobbering the old
@@ -209,7 +209,7 @@ async function checkAuthority(authority: AuthorityPath, threshold: number) {
209
209
  // - Except predictions might be clobbered... but that's fine, they don't really matter.
210
210
  pathWatcher.triggerValuesChanged(allValues, undefined, {
211
211
  parentPaths: new Set(),
212
- values: allValues,
212
+ values: new Set(Array.from(allValues).map(x => x.path)),
213
213
  });
214
214
  }
215
215
  }
@@ -14,7 +14,7 @@ import { remoteWatcher } from "../1-path-client/RemoteWatcher";
14
14
  import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
15
15
  import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
16
16
  import debugbreak from "debugbreak";
17
- import { DebugLog, DebugLogController, getLogHistoryEquals, getLogHistoryIncludes } from "../0-path-value-core/debugLogs";
17
+ import { DebugLog, DebugLogController, getLogHistoryEquals, getLogHistoryIncludes } from "../0-path-value-core/auditLogs";
18
18
  import { getParentPathStr } from "../path";
19
19
  import { diskAuditNow } from "./diskValueAudit";
20
20
  import { yellow, green } from "socket-function/src/formatting/logColors";
@@ -247,7 +247,7 @@ async function auditSpecificPathsBase(config: {
247
247
  continue;
248
248
  }
249
249
 
250
- if (!pathValueSerializer.compareValuePaths(value, ourValue)) continue;
250
+ if (!pathValueSerializer.compareValuePaths(value, ourValue) || !!value?.valid !== !!ourValue?.valid) continue;
251
251
 
252
252
  let ourTime = ourValue ? ourValue.time.time : 0;
253
253
  let remoteTime = value ? value.time.time : 0;
@@ -262,7 +262,7 @@ async function auditSpecificPathsBase(config: {
262
262
  ourValue = authorityStorage.getValueAtTime(path);
263
263
  value = (await ValueAuditController.nodes[authorityNodeId].getPathValue(path)).value;
264
264
 
265
- if (!pathValueSerializer.compareValuePaths(value, ourValue)) continue;
265
+ if (!pathValueSerializer.compareValuePaths(value, ourValue) || !!value?.valid !== !!ourValue?.valid) continue;
266
266
  let THRESHOLD2 = Date.now() - MAX_ASSUMED_PROPAGATION_DELAY;
267
267
  let ourTime2 = authorityStorage.getLastChangedTime(path)?.time || 0;
268
268
  let oursIsNew = ourTime2 > THRESHOLD2;
@@ -297,8 +297,8 @@ async function auditSpecificPathsBase(config: {
297
297
  }
298
298
  let atTime = Date.now();
299
299
  console.error(`Audit mismatch for ${path} from ${authorityNodeId} detected at ${atTime}. Watching because: ${reason}`);
300
- console.log(`Value Remote:`, pathValueSerializer.getPathValue(value));
301
- console.log(`Value Local:`, pathValueSerializer.getPathValue(ourValue));
300
+ console.log(`Value Remote (${value?.valid ? "valid" : "invalid"}):`, pathValueSerializer.getPathValue(value));
301
+ console.log(`Value Local (${ourValue?.valid ? "valid" : "invalid"}):`, pathValueSerializer.getPathValue(ourValue));
302
302
  console.log(`Time Remote:`, value?.time.time);
303
303
  console.log(`Time Local:`, ourValue?.time.time);
304
304
  console.log(`Our node id:`, ourNodeId);
@@ -0,0 +1,148 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { qreact } from "../4-dom/qreact";
3
+ import { assertIsManagementUser } from "./managementPages";
4
+ import { css } from "typesafecss";
5
+ import { getSyncedController } from "../library-components/SyncedController";
6
+ import { DebugLog, DebugLogController, getLogHistoryEquals, getLogHistoryIncludes } from "../0-path-value-core/auditLogs";
7
+ import { remoteWatcher } from "../1-path-client/RemoteWatcher";
8
+ import { pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
9
+ import { getParentPathStr } from "../path";
10
+ import { getBrowserUrlNode, getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
11
+ import { nextId, sort } from "socket-function/src/misc";
12
+ import { t } from "../2-proxy/schema2";
13
+ import { InputLabelURL } from "../library-components/InputLabel";
14
+ import { URLParam } from "../library-components/URLParam";
15
+ import { Table } from "../5-diagnostics/Table";
16
+ import { ObjectDisplay } from "./logs/ObjectDisplay";
17
+
18
+ /**
19
+ TODO:
20
+ 1) Support watching multiple paths at once
21
+ 2) Show extra metadata for the paths
22
+ - Current value (raw, as in, don't sync it)
23
+ - If it is synced
24
+ - The authority of it
25
+ - An expander which shows a table of the full history of values, with valid state, etc
26
+ */
27
+
28
+ type DebugLogWithSource = DebugLog & {
29
+ sourceType: "local" | "remote";
30
+ sourceNodeId: string;
31
+ }
32
+
33
+ let path = new URLParam("path", "");
34
+ let includeDescendants = new URLParam("includeDescendants", false);
35
+ export class AuditLogPage extends qreact.Component {
36
+ loadId = nextId();
37
+ state = t.state({
38
+ loadedId: t.string
39
+ });
40
+ render() {
41
+ let logs: DebugLogWithSource[] = [];
42
+ let loaded = this.loadId === this.state.loadedId;
43
+ if (loaded) {
44
+ let obj = auditLogController(getBrowserUrlNode()).getPathHistory({
45
+ path: path.value,
46
+ includeDescendants: includeDescendants.value,
47
+ });
48
+ if (obj) {
49
+ logs = obj.fullLogs;
50
+ }
51
+ }
52
+ return (
53
+ <div class={css.pad2(10).vbox(10).fillWidth}>
54
+ <InputLabelURL
55
+ label="Path"
56
+ url={path}
57
+ fillWidth
58
+ onChange={() => this.loadId = nextId()}
59
+ />
60
+ <InputLabelURL
61
+ label="Include Descendants"
62
+ url={includeDescendants}
63
+ checkbox
64
+ onChange={() => this.loadId = nextId()}
65
+ />
66
+ {!loaded && <button onClick={() => this.state.loadedId = this.loadId}>Load</button>}
67
+ <Table
68
+ rows={logs}
69
+ columns={{
70
+ type: {},
71
+ time: {},
72
+ sourceType: {},
73
+ sourceNodeId: {},
74
+ values: {
75
+ formatter(value) {
76
+ return <ObjectDisplay value={value} />;
77
+ },
78
+ },
79
+ }}
80
+ />
81
+ </div>
82
+ );
83
+ }
84
+ }
85
+
86
+
87
+ class AuditLogControllerBase {
88
+ public async getPathHistory(config: {
89
+ path: string;
90
+ includeDescendants?: boolean;
91
+ }) {
92
+ let { path, includeDescendants } = config;
93
+ let authorityNodeId = remoteWatcher.getExistingWatchRemoteNodeId(path);
94
+ if (!authorityNodeId) {
95
+ authorityNodeId = await pathValueAuthority2.getSingleReadNodeSync(path);
96
+ }
97
+ let logs: DebugLog[] = [];
98
+ let remoteLogs: DebugLog[] = [];
99
+ if (includeDescendants) {
100
+ // Also look for the parent, so we can capture keys() syncs as well
101
+ // (this should give us child paths too, because that's how includes works)
102
+ logs = getLogHistoryIncludes(path);
103
+ if (authorityNodeId) {
104
+ remoteLogs = await DebugLogController.nodes[authorityNodeId].getLogHistoryEquals(path);
105
+ }
106
+ } else {
107
+ logs = getLogHistoryEquals(path);
108
+ if (authorityNodeId) {
109
+ remoteLogs = await DebugLogController.nodes[authorityNodeId].getLogHistoryEquals(path);
110
+ }
111
+ }
112
+
113
+ let fullLogs: DebugLogWithSource[] = [];
114
+ let ownNodeId = getOwnNodeId();
115
+ for (let log of logs) {
116
+ fullLogs.push({
117
+ ...log,
118
+ sourceType: "local",
119
+ sourceNodeId: ownNodeId,
120
+ });
121
+ }
122
+ if (authorityNodeId) {
123
+ for (let log of remoteLogs) {
124
+ fullLogs.push({
125
+ ...log,
126
+ sourceType: "remote",
127
+ sourceNodeId: authorityNodeId,
128
+ });
129
+ }
130
+ }
131
+ sort(fullLogs, x => x.time);
132
+ return {
133
+ fullLogs,
134
+ };
135
+ }
136
+ }
137
+
138
+ export const AuditLogController = SocketFunction.register(
139
+ "AuditLogController-bf798faf-c803-4add-ac2c-99d0f44e753a",
140
+ new AuditLogControllerBase(),
141
+ () => ({
142
+ getPathHistory: {},
143
+ }),
144
+ () => ({
145
+ hooks: [assertIsManagementUser],
146
+ })
147
+ );
148
+ export const auditLogController = getSyncedController(AuditLogController);
@@ -64,7 +64,7 @@ export async function registerManagementPages2(config: {
64
64
  throw new Error(`registerManagementPages2 called with a different module than previously registered. This will break isManagementUser, and so is not allowed.`);
65
65
  }
66
66
  registeredModule = config.module;
67
- let inputPages = config.pages.slice();
67
+ let inputPages: typeof config.pages = [];
68
68
 
69
69
  inputPages.push({
70
70
  title: "Nodes",
@@ -89,12 +89,19 @@ export async function registerManagementPages2(config: {
89
89
  componentName: "LogClassifiers",
90
90
  getModule: () => import("./errorLogs/LogClassifiers"),
91
91
  });
92
+ inputPages.push({
93
+ title: "Audit Paths",
94
+ componentName: "AuditLogPage",
95
+ controllerName: "AuditLogController",
96
+ getModule: () => import("./AuditLogPage"),
97
+ });
92
98
  inputPages.push({
93
99
  title: "Time",
94
100
  componentName: "TimeDebug",
95
101
  controllerName: "",
96
102
  getModule: () => import("./TimeDebug"),
97
103
  });
104
+ inputPages.push(...config.pages);
98
105
 
99
106
  // NOTE: We don't store the UI in the database (here, or anywhere else, at least
100
107
  // not yet), but we do want to store the permission function call, as we may