querysub 0.2.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 (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. package/yarnSpec.txt +56 -0
@@ -0,0 +1,139 @@
1
+ import { addToStats, addToStatsValue, createStatsValue, getStatsTop } from "socket-function/src/profiling/stats";
2
+ import type { PathValue } from "../0-path-value-core/pathValueCore";
3
+ import { registerPeriodic } from "./periodic";
4
+ import { formatTime } from "socket-function/src/formatting/format";
5
+ import { formatStats } from "socket-function/src/profiling/statsFormat";
6
+ import { getPathStr1 } from "../path";
7
+ import { registerResource } from "./trackResources";
8
+ import type { CallSpec } from "../3-path-functions/PathFunctionRunner";
9
+ import { PromiseObj } from "../promise";
10
+ import { lazy } from "socket-function/src/caching";
11
+ import { isNode } from "socket-function/src/misc";
12
+
13
+ const getCallResultPathFnc = lazy(() => import("../4-querysub/querysubPrediction").then((m) => m.getCallResultPath));
14
+
15
+ // NOTE: There's no point to track non-predicted calls, because if we don't predict them we won't be watching
16
+ // them, so we won't know when they finish!
17
+
18
+ class OverlapRateTracker {
19
+ public openCount = 0;
20
+ private openTime = 0;
21
+ public originalOpenTime = Date.now();
22
+ private openCloseCount = 0;
23
+
24
+ public finishedTime = 0;
25
+ private finishedCount = 0;
26
+
27
+ public openValue() {
28
+ if (this.openCount === 0) {
29
+ this.openTime = Date.now();
30
+ }
31
+ this.openCount++;
32
+ this.openCloseCount++;
33
+ }
34
+ public closeValue() {
35
+ this.openCount--;
36
+ let now = Date.now();
37
+ if (this.openCount === 0 || now - this.openTime > 5000) {
38
+ this.finishedTime += now - this.openTime;
39
+ this.openTime = now;
40
+ this.finishedCount += this.openCloseCount;
41
+ this.openCloseCount = 0;
42
+ }
43
+ }
44
+ public viewRate() {
45
+ if (this.finishedTime === 0) return 0;
46
+ return this.finishedCount / this.finishedTime * 1000;
47
+ }
48
+ public saturatedFraction() {
49
+ return this.finishedTime / (Date.now() - this.originalOpenTime);
50
+ }
51
+ public harvestRate() {
52
+ let rate = this.viewRate();
53
+ this.finishedTime = 0;
54
+ this.finishedCount = 0;
55
+ this.originalOpenTime = Date.now();
56
+ return rate;
57
+ }
58
+ }
59
+
60
+ export class Benchmark {
61
+
62
+ public static predictCount = 0;
63
+ public static receivedCount = 0;
64
+
65
+ private static finishedCalls = createStatsValue();
66
+ public static overlapRate = new OverlapRateTracker();
67
+
68
+ private static predictedCalls = registerResource("Benchmark|predictedCalls", new Map<string, number>());
69
+
70
+ private static waitingForCalls = registerResource("Benchmark|waitingForCalls", new Map<string, (() => void)[]>());
71
+
72
+ public static onStartPredictCall(path: string) {
73
+ this.predictedCalls.set(path, Date.now());
74
+ Benchmark.predictCount++;
75
+ this.overlapRate.openValue();
76
+ }
77
+ public static onReceivedValues(values: PathValue[]) {
78
+ for (let value of values) {
79
+ if (value.canGCValue) continue;
80
+ if (!value.valid) continue;
81
+ let list = this.waitingForCalls.get(value.path);
82
+ if (!list) continue;
83
+ this.waitingForCalls.delete(value.path);
84
+ for (let resolve of list) {
85
+ resolve();
86
+ }
87
+ }
88
+
89
+ let now = Date.now();
90
+ for (let value of values) {
91
+ if (!value.path.includes(getPathStr1("Results"))) {
92
+ continue;
93
+ }
94
+ if (value.canGCValue) continue;
95
+ if (!value.valid) continue;
96
+ let predictedTime = this.predictedCalls.get(value.path);
97
+ if (!predictedTime) {
98
+ continue;
99
+ }
100
+ this.overlapRate.closeValue();
101
+ this.predictedCalls.delete(value.path);
102
+ this.receivedCount++;
103
+ addToStatsValue(this.finishedCalls, now - predictedTime);;
104
+ }
105
+ }
106
+
107
+ public static async waitForCallToFinish(call: CallSpec) {
108
+ let getCallResultPath = await getCallResultPathFnc();
109
+ let path = getCallResultPath(call);
110
+ let list = this.waitingForCalls.get(path);
111
+ if (!list) {
112
+ list = [];
113
+ this.waitingForCalls.set(path, list);
114
+ }
115
+ let promiseObj = new PromiseObj();
116
+ list.push(promiseObj.resolve);
117
+ await promiseObj.promise;
118
+ }
119
+
120
+ public static logCallsNow() {
121
+ let stats = this.finishedCalls;
122
+ this.finishedCalls = createStatsValue();
123
+ console.log(`Calls ${formatStats(stats)}, rate ${this.overlapRate.harvestRate()}`);
124
+ }
125
+
126
+ public static getLiveCallStats() {
127
+ return this.finishedCalls;
128
+ }
129
+ public static getLiveRate() {
130
+ return this.overlapRate.viewRate();
131
+ }
132
+ public static saturatedFraction() {
133
+ return this.overlapRate.saturatedFraction();
134
+ }
135
+ }
136
+
137
+ if (!isNode()) {
138
+ registerPeriodic(() => Benchmark.logCallsNow());
139
+ }
@@ -0,0 +1,515 @@
1
+ import { lazy } from "socket-function/src/caching";
2
+ import { LogClass, LOG_FLUSH_INTERVAL, LogBlock, LogBlockInfo, LogRaw, LOG_ARCHIVES, LOG_CLASSES_PATH, logClassesFromString, getLogClassCategorizer, logBlockFromBuffer, logBlockToBuffer, logClassesToString, LOG_BLOCK_EXTENSION, getLogBlockInfo, getLogBlockFileName, addToLogBlock, LogType, isUngroupedClass } from "./ErrorLogCore";
3
+ import { list, throttleFunction, timeInDay, timeInMinute } from "socket-function/src/misc";
4
+ import { getMachineId, getOwnMachineId, getThreadKeyCert } from "../../-a-auth/certs";
5
+ import { getAllNodeIds, getBrowserUrlNode, getOwnNodeId, getOwnThreadId, watchDeltaNodeIds, watchNodeIds } from "../../-f-node-discovery/NodeDiscovery";
6
+ import { getArchives } from "../../-a-archives/archives";
7
+ import { isNode } from "typesafecss";
8
+ import { SocketFunction } from "socket-function/SocketFunction";
9
+ import { requiresNetworkTrustHook } from "../../-d-trust/NetworkTrust2";
10
+ import { ignoreErrors, logErrors, timeoutToError, timeoutToUndefinedSilent } from "../../errors";
11
+ import debugbreak from "debugbreak";
12
+ import { magenta } from "socket-function/src/formatting/logColors";
13
+ import { formatNumber } from "socket-function/src/formatting/format";
14
+ import { registerShutdownHandler } from "../periodic";
15
+ import { isNoNetwork } from "../../config";
16
+ import { ExternalRenderClass, qreact } from "../../4-dom/qreact";
17
+ import { Querysub } from "../../4-querysub/QuerysubController";
18
+ import { atomic, doAtomicWrites } from "../../2-proxy/PathValueProxyWatcher";
19
+ import { isClient } from "../../config2";
20
+ import { ControllerPick, SocketRegistered } from "socket-function/SocketFunctionTypes";
21
+
22
+
23
+ const NOTIFY_HISTORY = timeInDay * 7;
24
+ // Only error / fatal are tracked, for now... we might also add warns?
25
+ const logIssueNotifyTypes: LogType[] = ["error", "fatal"];
26
+
27
+ // We don't log on internal errors, otherwise we might infinitely loop
28
+ function internalError(err: unknown | Error | Promise<unknown>) {
29
+ if (err && err instanceof Error) {
30
+ if (!isNode()) {
31
+ debugger;
32
+ return;
33
+ }
34
+ process.stderr.write((err.stack || err.message) + "\n");
35
+ } else if (err && err instanceof Promise) {
36
+ err.catch(internalError);
37
+ } else {
38
+ if (!isNode()) {
39
+ debugger;
40
+ return;
41
+ }
42
+ process.stderr.write("Unknown error: " + String(err) + "\n");
43
+ }
44
+ }
45
+
46
+ export function logSilent(message: string, type: LogType) {
47
+ addLocalLog({ message, time: Date.now() }, type);
48
+ }
49
+ export function logSilentError(message: string) {
50
+ logSilent(message, "error");
51
+ }
52
+ export function logSilentWarning(message: string) {
53
+ logSilent(message, "warn");
54
+ }
55
+
56
+ let nextBlockSeqNum = 1;
57
+ let pendingBlock: LogBlock | undefined;
58
+ export function addLocalLog(log: LogRaw, typeHint: LogType) {
59
+ // NOTE: Eventually... we should probably ignore logs for non-public machines?
60
+ //if (isNoNetwork()) return;
61
+ try {
62
+ if (!pendingBlock) {
63
+ ensureBlocksFlushing();
64
+ let now = Date.now();
65
+ pendingBlock = {
66
+ classes: {},
67
+ startTime: now,
68
+ endTime: now,
69
+
70
+ threadId: "",
71
+ machineId: "",
72
+
73
+ seqNum: nextBlockSeqNum++,
74
+ count: 0,
75
+ fatalCount: 0,
76
+ errorCount: 0,
77
+ warnCount: 0,
78
+ infoCount: 0,
79
+ };
80
+ }
81
+
82
+ if (!logClassifier) {
83
+ ignoreErrors(initLogClassifier());
84
+ pendingLogs.push({ log, type: typeHint });
85
+ return;
86
+ }
87
+ let classifier = logClassifier(log, typeHint);
88
+ pendingBlock.threadId = pendingBlock.threadId || getOwnThreadId();
89
+ pendingBlock.machineId = pendingBlock.machineId || getOwnMachineId();
90
+ addToLogBlock(pendingBlock, classifier, log);
91
+ ErrorLogControllerBase.onLocalLog(log, classifier);
92
+ } catch (e) {
93
+ internalError(e);
94
+ }
95
+ }
96
+ registerShutdownHandler(flushNow);
97
+ async function flushNow() {
98
+ let newBlock = pendingBlock;
99
+ pendingBlock = undefined;
100
+ if (!newBlock) return;
101
+ newBlock.endTime = Date.now();
102
+ //console.log(magenta(`Flushing log block ${formatNumber(newBlock.count)} count, seq num ${newBlock.seqNum}, at ${Date.now()}`));
103
+ if (newBlock.count > 0) {
104
+ let fileInfo = await logBlockToBuffer(newBlock);
105
+ await LOG_ARCHIVES().set(fileInfo.fileName, fileInfo.buffer);
106
+ }
107
+ }
108
+
109
+ let ensureBlocksFlushing = lazy(() => {
110
+ setInterval(async () => {
111
+ await internalError(flushNow());
112
+ }, LOG_FLUSH_INTERVAL);
113
+
114
+ // Read log classifiers every hour, in case we miss an update
115
+ setInterval(() => {
116
+ initLogClassifier.reset();
117
+ logErrors(initLogClassifier());
118
+ }, timeInMinute * 60);
119
+ });
120
+
121
+ let pendingLogs: { log: LogRaw; type: LogType }[] = [];
122
+ let logClassifier: ((log: LogRaw, typeHint: LogType) => LogClass) | undefined;
123
+ let initLogClassifier = lazy(async () => {
124
+ await timeoutToError(60 * 1000, SocketFunction.mountPromise, () => new Error("Logs lost because service never mounted"));
125
+ let logClasses = await new ErrorLogControllerBase().getClasses();
126
+ logClassifier = getLogClassCategorizer(logClasses);
127
+ if (pendingLogs.length > 0) {
128
+ let logs = pendingLogs;
129
+ pendingLogs = [];
130
+ for (let log of logs) {
131
+ addLocalLog(log.log, log.type);
132
+ }
133
+ }
134
+ return {
135
+ classify: logClassifier,
136
+ logClasses: new Map(logClasses.map(a => [a.id, a])),
137
+ };
138
+ });
139
+ async function onLogClassesUpdated() {
140
+ initLogClassifier.reset();
141
+ await initLogClassifier();
142
+ await ErrorLogControllerBase.recalculateNotifyState();
143
+ }
144
+
145
+ export class ErrorLogControllerBase {
146
+ public async scanBlocks(): Promise<LogBlockInfo[]> {
147
+ let files = await LOG_ARCHIVES().find("", { shallow: true, type: "files" });
148
+ files = files.filter(a => a.endsWith(LOG_BLOCK_EXTENSION));
149
+ let infos = files.map(getLogBlockInfo);
150
+ if (pendingBlock) {
151
+ infos.push({
152
+ startTime: pendingBlock.startTime,
153
+ endTime: pendingBlock.endTime,
154
+ seqNum: pendingBlock.seqNum,
155
+ count: pendingBlock.count,
156
+ fatalCount: pendingBlock.fatalCount,
157
+ errorCount: pendingBlock.errorCount,
158
+ warnCount: pendingBlock.warnCount,
159
+ infoCount: pendingBlock.infoCount,
160
+ threadId: pendingBlock.threadId,
161
+ machineId: pendingBlock.machineId,
162
+ fileName: "",
163
+ });
164
+ }
165
+ return infos;
166
+ }
167
+
168
+ public async readBlocks(config: {
169
+ files: string[];
170
+ }): Promise<Buffer[]> {
171
+ let fileNames = config.files.filter(a =>
172
+ a.endsWith(LOG_BLOCK_EXTENSION) && !(a.includes("/") || a.includes("\\") || a.includes(".."))
173
+ || !a
174
+ );
175
+ let buffers: Buffer[] = [];
176
+ async function runThread() {
177
+ while (fileNames.length > 0) {
178
+ let fileName = fileNames.pop()!;
179
+ if (!fileName) {
180
+ if (pendingBlock) {
181
+ buffers.push((await logBlockToBuffer(pendingBlock)).buffer);
182
+ }
183
+ continue;
184
+ }
185
+ let buffer = await LOG_ARCHIVES().get(fileName);
186
+ if (!buffer) continue;
187
+ buffers.push(buffer);
188
+ }
189
+ }
190
+ await Promise.all(list(16).map(runThread));
191
+ return buffers;
192
+ }
193
+ public async readAllLiveBlocks(): Promise<Buffer[]> {
194
+ let nodeIds = await getAllNodeIds();
195
+ let blockBuffers: Buffer[] = [];
196
+ await Promise.allSettled(nodeIds.map(async nodeId => {
197
+ let controller = ErrorLogController.nodes[nodeId];
198
+ let buffers = await timeoutToUndefinedSilent(5000, controller.readBlocks({ files: [""] }));
199
+ if (!buffers) return;
200
+ console.log(magenta(`Read live data from ${nodeId}`));
201
+ blockBuffers.push(...buffers);
202
+ }));
203
+ return blockBuffers;
204
+ }
205
+
206
+ public async getClasses(): Promise<LogClass[]> {
207
+ let logClassesBuffer = await LOG_ARCHIVES().get(LOG_CLASSES_PATH);
208
+ let logClasses = logClassesFromString(logClassesBuffer?.toString() || "");
209
+ return logClasses;
210
+ }
211
+
212
+ public async updateClass(logClass: LogClass | { id: string; action: "delete" }) {
213
+ let logClasses = await this.getClasses();
214
+ let existingIndex = logClasses.findIndex(a => a.id === logClass.id);
215
+ if ("action" in logClass) {
216
+ if (logClass.action === "delete") {
217
+ if (existingIndex !== -1) {
218
+ logClasses.splice(existingIndex, 1);
219
+ }
220
+ }
221
+ } else {
222
+ if (existingIndex !== -1) {
223
+ logClasses[existingIndex] = logClass;
224
+ } else {
225
+ logClasses.push(logClass);
226
+ }
227
+ }
228
+ let buffer = Buffer.from(logClassesToString(logClasses));
229
+ await LOG_ARCHIVES().set(LOG_CLASSES_PATH, buffer);
230
+ logErrors((async () => {
231
+ await onLogClassesUpdated();
232
+ // Notify ALL other nodes
233
+ let nodeIdsToTest = await getAllNodeIds();
234
+ for (let nodeId of nodeIdsToTest) {
235
+ ignoreErrors(ErrorLogController.nodes[nodeId].onClassesUpdated());
236
+ }
237
+ })());
238
+ }
239
+
240
+ public async onClassesUpdated() {
241
+ ignoreErrors(onLogClassesUpdated());
242
+ }
243
+
244
+ private static joinNotifies(notify: (LogIssueNotify | undefined)[]): LogIssueNotify {
245
+ // If we don't need to merge, don't so callers can check for ===
246
+ let finalNotify: LogIssueNotify | undefined = undefined;
247
+ for (let next of notify) {
248
+ if (!next) continue;
249
+ if (!finalNotify) {
250
+ finalNotify = next;
251
+ continue;
252
+ }
253
+ if (next.oldestUnsuppressed && (!finalNotify.oldestUnsuppressed || next.oldestUnsuppressed.time < finalNotify.oldestUnsuppressed.time)) {
254
+ if (!finalNotify.oldestUngrouped) {
255
+ finalNotify = next;
256
+ } else {
257
+ finalNotify = { ...finalNotify, oldestUnsuppressed: next.oldestUnsuppressed };
258
+ }
259
+ }
260
+ if (next.oldestUngrouped && (!finalNotify.oldestUngrouped || next.oldestUngrouped.time < finalNotify.oldestUngrouped.time)) {
261
+ if (!finalNotify.oldestUnsuppressed) {
262
+ finalNotify = next;
263
+ } else {
264
+ finalNotify = { ...finalNotify, oldestUngrouped: next.oldestUngrouped };
265
+ }
266
+ }
267
+ }
268
+ return finalNotify || undefined;
269
+ }
270
+
271
+ private static logIssueNotifyEquals(lhs: LogIssueNotify | undefined, rhs: LogIssueNotify | undefined) {
272
+ return (
273
+ lhs?.oldestUngrouped?.message === rhs?.oldestUngrouped?.message
274
+ && lhs?.oldestUnsuppressed?.message === rhs?.oldestUnsuppressed?.message
275
+ && lhs?.oldestUngrouped?.time === rhs?.oldestUngrouped?.time
276
+ && lhs?.oldestUnsuppressed?.time === rhs?.oldestUnsuppressed?.time
277
+ );
278
+ }
279
+
280
+ private static inMemoryNotify: LogIssueNotify | undefined;
281
+ private static updateInMemoryNotify(notify: LogIssueNotify | undefined) {
282
+ if (ErrorLogControllerBase.logIssueNotifyEquals(this.inMemoryNotify, notify)) {
283
+ return;
284
+ }
285
+ this.inMemoryNotify = notify;
286
+ for (let watcher of this.inMemoryNotifyWatchers) {
287
+ ErrorLogController.nodes[watcher]
288
+ .liveLogsIssueNotifyCallback(notify)
289
+ .catch(() => {
290
+ this.inMemoryNotifyWatchers.delete(watcher);
291
+ });
292
+ }
293
+ logErrors(ErrorLogControllerBase.recalcAllNotify());
294
+ }
295
+ private static inMemoryNotifyWatchers = new Set<string>();
296
+ public static onLocalLog(log: LogRaw, classObj: LogClass) {
297
+ if (!logIssueNotifyTypes.includes(classObj.type)) return;
298
+ let notify: LogIssueNotify | undefined;
299
+ if (isUngroupedClass(classObj)) {
300
+ notify = {
301
+ oldestUnsuppressed: undefined,
302
+ oldestUngrouped: log,
303
+ };
304
+ } else if (log.time > classObj.suppressUntil) {
305
+ notify = {
306
+ oldestUnsuppressed: log,
307
+ oldestUngrouped: undefined,
308
+ };
309
+ }
310
+ if (notify) {
311
+ this.updateInMemoryNotify(this.joinNotifies([this.inMemoryNotify, notify]));
312
+ }
313
+ }
314
+ public async internalWatchLiveLogs() {
315
+ let watcher = SocketFunction.getCaller().nodeId;
316
+ ErrorLogControllerBase.inMemoryNotifyWatchers.add(watcher);
317
+ // Await, so the client gets it's own errors
318
+ await ErrorLogController.nodes[watcher].liveLogsIssueNotifyCallback(ErrorLogControllerBase.inMemoryNotify);
319
+ }
320
+
321
+ private static liveLogsIssueNotify = new Map<string, LogIssueNotify | undefined>();
322
+ public async liveLogsIssueNotifyCallback(notify: LogIssueNotify | undefined) {
323
+ let caller = SocketFunction.getCaller();
324
+ let prev = ErrorLogControllerBase.liveLogsIssueNotify.get(caller.nodeId);
325
+ if (ErrorLogControllerBase.logIssueNotifyEquals(prev, notify)) {
326
+ return;
327
+ }
328
+ console.log(magenta(`Received log notify from ${caller.nodeId} for ${notify?.oldestUngrouped?.message}`));
329
+ ErrorLogControllerBase.liveLogsIssueNotify.set(caller.nodeId, notify);
330
+ logErrors(ErrorLogControllerBase.recalcAllNotify());
331
+ }
332
+
333
+
334
+ private static allNotify: LogIssueNotify | undefined;
335
+ private static allWatchers = new Set<string>();
336
+ private static diskNotify: LogIssueNotify | undefined;
337
+ private static async recalcAllNotify() {
338
+ if (!ErrorLogControllerBase.watchingLogs) return;
339
+ let notifies = [
340
+ ...Array.from(this.liveLogsIssueNotify.values()),
341
+ this.inMemoryNotify,
342
+ this.diskNotify
343
+ ];
344
+ let { classify } = await initLogClassifier();
345
+ // Reclassify based on the most recent classifier
346
+ // - This only removes classifications, it doesn't add them back. Each node should be watching
347
+ // classifiers and rerunning on their data to add classifications back.
348
+ notifies = notifies.map(x => {
349
+ if (x?.oldestUngrouped && !isUngroupedClass(classify(x.oldestUngrouped, "error"))) {
350
+ x = { ...x, oldestUngrouped: undefined };
351
+ }
352
+ if (x?.oldestUnsuppressed && x.oldestUnsuppressed.time < classify(x.oldestUnsuppressed, "error").suppressUntil) {
353
+ x = { ...x, oldestUnsuppressed: undefined };
354
+ }
355
+ return x;
356
+ });
357
+ let newNotify = this.joinNotifies(notifies);
358
+ if (this.logIssueNotifyEquals(newNotify, this.allNotify)) {
359
+ return;
360
+ }
361
+ this.allNotify = newNotify;
362
+ for (let watcher of this.allWatchers) {
363
+ ErrorLogController.nodes[watcher].logsIssueNotifyCallback(newNotify)
364
+ .catch(() => {
365
+ this.allWatchers.delete(watcher);
366
+ });
367
+ }
368
+ }
369
+
370
+ private static watchForNewNodes = lazy(() => {
371
+ watchDeltaNodeIds(async ({ newNodeIds }) => {
372
+ for (let nodeId of newNodeIds) {
373
+ if (nodeId === getOwnNodeId()) continue;
374
+ if (!ErrorLogControllerBase.liveLogsIssueNotify.has(nodeId)) {
375
+ ErrorLogControllerBase.liveLogsIssueNotify.set(nodeId, undefined);
376
+ }
377
+ ignoreErrors(ErrorLogController.nodes[nodeId].internalWatchLiveLogs());
378
+ SocketFunction.onNextDisconnect(nodeId, () => {
379
+ // HACK: Remove it, to prevent notifications from errors which we never written to disk.
380
+ // - The only other alternative is to get this error in the UI... somehow, and onto disk,
381
+ // but not if the other node DOES write it to disk, which... gets complicated.
382
+ ErrorLogControllerBase.liveLogsIssueNotify.delete(nodeId);
383
+ });
384
+ }
385
+ });
386
+ });
387
+
388
+ private static watchingLogs = false;
389
+ public static async recalculateNotifyState() {
390
+ ErrorLogControllerBase.watchForNewNodes();
391
+
392
+ // Watch all nodes again, in case they disappear
393
+ for (let node of await getAllNodeIds()) {
394
+ if (node === getOwnNodeId()) continue;
395
+ ignoreErrors(ErrorLogController.nodes[node].internalWatchLiveLogs());
396
+ }
397
+
398
+ let threshold = Date.now() - NOTIFY_HISTORY;
399
+
400
+ let { classify, logClasses } = await initLogClassifier();
401
+ function summarizeBlock(block: LogBlock | undefined) {
402
+ if (!block) return undefined;
403
+ let notify: LogIssueNotify | undefined = undefined;
404
+ for (let classSummary of Object.values(block.classes)) {
405
+ for (let logRaw of classSummary.examples) {
406
+ if (logRaw.time < threshold) continue;
407
+ let classObj = classify(logRaw, classSummary.logClassType || logClasses.get(classSummary.logClassId)?.type || "error");
408
+ if (!logIssueNotifyTypes.includes(classObj.type)) continue;
409
+ if (isUngroupedClass(classObj)) {
410
+ if (!notify || !notify.oldestUngrouped || classSummary.firstTime < notify.oldestUngrouped.time) {
411
+ if (!notify) notify = { oldestUnsuppressed: undefined, oldestUngrouped: undefined };
412
+ if (!notify.oldestUngrouped || notify.oldestUngrouped.time > classSummary.firstTime) {
413
+ notify.oldestUngrouped = classSummary.examples[0];
414
+ }
415
+ }
416
+ } else if (logRaw.time > classObj.suppressUntil) {
417
+ if (!notify || !notify.oldestUnsuppressed || classSummary.firstTime < notify.oldestUnsuppressed.time) {
418
+ if (!notify) notify = { oldestUnsuppressed: undefined, oldestUngrouped: undefined };
419
+ if (!notify.oldestUnsuppressed || notify.oldestUnsuppressed.time > classSummary.firstTime) {
420
+ notify.oldestUnsuppressed = classSummary.examples[0];
421
+ }
422
+ }
423
+ }
424
+ }
425
+ }
426
+ return notify;
427
+ }
428
+
429
+ // Recalculate in memory state
430
+ this.updateInMemoryNotify(summarizeBlock(pendingBlock));
431
+
432
+ if (ErrorLogControllerBase.watchingLogs) {
433
+ let blockInfos = await new ErrorLogControllerBase().scanBlocks();
434
+ blockInfos = blockInfos.filter(a => a.startTime > threshold);
435
+ let blocks = await new ErrorLogControllerBase().readBlocks({ files: blockInfos.map(a => a.fileName) });
436
+ let blockObjs = await Promise.all(blocks.map(x => logBlockFromBuffer(x)));
437
+ let notify = this.joinNotifies(blockObjs.map(summarizeBlock));
438
+ this.diskNotify = notify;
439
+ }
440
+
441
+ await this.recalcAllNotify();
442
+ }
443
+
444
+ // NOTE: We never unwatch. Watching for LogIssueNotify should be fairly cheap, so this is fine
445
+ public static startLogWatch = lazy(async () => {
446
+ ErrorLogControllerBase.watchingLogs = true;
447
+ await this.recalculateNotifyState();
448
+ });
449
+
450
+ public async watchLogs(callbackClass?: ControllerPick<typeof ErrorLogController, "logsIssueNotifyCallback">) {
451
+ let self = ErrorLogControllerBase;
452
+ self.watchingLogs = true;
453
+
454
+ let caller = SocketFunction.getCaller();
455
+ self.allWatchers.add(caller.nodeId);
456
+
457
+ await self.startLogWatch();
458
+
459
+ let client = callbackClass ? SocketFunction.rehydrateSocketCaller(callbackClass) : ErrorLogController;
460
+ await client.nodes[caller.nodeId].logsIssueNotifyCallback(self.allNotify);
461
+ }
462
+
463
+ public static logIssueNotifyWatchers = new Set<(notify: LogIssueNotify | undefined) => void>();
464
+ public async logsIssueNotifyCallback(notify: LogIssueNotify | undefined) {
465
+ onLogIssueNotify(notify);
466
+ }
467
+ public async unwatchLogs() {
468
+ let self = ErrorLogControllerBase;
469
+ let caller = SocketFunction.getCaller();
470
+ self.allWatchers.delete(caller.nodeId);
471
+ }
472
+ }
473
+
474
+ function onLogIssueNotify(notify: LogIssueNotify | undefined) {
475
+ for (let watcher of ErrorLogControllerBase.logIssueNotifyWatchers) {
476
+ try {
477
+ watcher(notify);
478
+ } catch (e) {
479
+ logErrors(e);
480
+ }
481
+ }
482
+ }
483
+ export function watchLogIssueNotify(callback: (notify: LogIssueNotify | undefined) => void) {
484
+ ErrorLogControllerBase.logIssueNotifyWatchers.add(callback);
485
+ }
486
+
487
+ export type LogIssueNotify = undefined | {
488
+ // NOTE: Storing the oldest requires a lot fewer notifications, and is probably what we want
489
+ // anyways (it tells us when the problem started).
490
+ oldestUnsuppressed: LogRaw | undefined;
491
+ oldestUngrouped: LogRaw | undefined;
492
+ // NOTE: Count is too hard to count accurately, due to live values being written to disk, etc.
493
+ }
494
+
495
+ const ErrorLogController = SocketFunction.register(
496
+ "ErrorLogController-219a7c2e-2da6-4753-9e0e-1f42dc5f6970",
497
+ new ErrorLogControllerBase(),
498
+ () => ({
499
+ scanBlocks: {},
500
+ readBlocks: {},
501
+ readAllLiveBlocks: {},
502
+ getClasses: {},
503
+ updateClass: {},
504
+ onClassesUpdated: {},
505
+
506
+ watchLogs: {},
507
+ unwatchLogs: {},
508
+ liveLogsIssueNotifyCallback: {},
509
+ logsIssueNotifyCallback: {},
510
+ internalWatchLiveLogs: {},
511
+ }),
512
+ () => ({
513
+ hooks: [requiresNetworkTrustHook],
514
+ }),
515
+ );