querysub 0.356.0 → 0.358.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 (76) hide show
  1. package/.cursorrules +9 -0
  2. package/bin/movelogs.js +4 -0
  3. package/package.json +13 -6
  4. package/scripts/postinstall.js +23 -0
  5. package/src/-a-archives/archiveCache.ts +10 -12
  6. package/src/-a-archives/archives.ts +29 -0
  7. package/src/-a-archives/archivesBackBlaze.ts +60 -12
  8. package/src/-a-archives/archivesDisk.ts +39 -13
  9. package/src/-a-archives/archivesLimitedCache.ts +21 -0
  10. package/src/-a-archives/archivesMemoryCache.ts +374 -0
  11. package/src/-a-archives/archivesPrivateFileSystem.ts +22 -0
  12. package/src/-g-core-values/NodeCapabilities.ts +3 -0
  13. package/src/0-path-value-core/auditLogs.ts +5 -1
  14. package/src/0-path-value-core/pathValueCore.ts +7 -7
  15. package/src/4-dom/qreact.tsx +1 -0
  16. package/src/4-querysub/Querysub.ts +1 -5
  17. package/src/config.ts +5 -0
  18. package/src/deployManager/components/MachineDetailPage.tsx +43 -2
  19. package/src/deployManager/components/MachinesListPage.tsx +10 -2
  20. package/src/deployManager/machineApplyMainCode.ts +3 -3
  21. package/src/deployManager/machineSchema.ts +39 -0
  22. package/src/diagnostics/MachineThreadInfo.tsx +235 -0
  23. package/src/diagnostics/NodeViewer.tsx +5 -3
  24. package/src/diagnostics/logs/FastArchiveAppendable.ts +79 -42
  25. package/src/diagnostics/logs/FastArchiveController.ts +102 -63
  26. package/src/diagnostics/logs/FastArchiveViewer.tsx +36 -8
  27. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +462 -0
  28. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.cpp +327 -0
  29. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.d.ts +18 -0
  30. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.js +1 -0
  31. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +222 -0
  32. package/src/diagnostics/logs/IndexedLogs/BufferIndexLogsOptimizationConstants.ts +22 -0
  33. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat +1145 -0
  34. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat.d.ts +178 -0
  35. package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +208 -0
  36. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +716 -0
  37. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +146 -0
  38. package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +569 -0
  39. package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +45 -0
  40. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +685 -0
  41. package/src/diagnostics/logs/IndexedLogs/LogStreamer.ts +47 -0
  42. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +901 -0
  43. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +236 -0
  44. package/src/diagnostics/logs/IndexedLogs/binding.gyp +23 -0
  45. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +251 -0
  46. package/src/diagnostics/logs/IndexedLogs/moveLogsEntry.ts +10 -0
  47. package/src/diagnostics/logs/LogViewer2.tsx +120 -55
  48. package/src/diagnostics/logs/TimeRangeSelector.tsx +5 -2
  49. package/src/diagnostics/logs/diskLogger.ts +32 -48
  50. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +3 -2
  51. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +1 -0
  52. package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +0 -0
  53. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +150 -0
  54. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +150 -15
  55. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +0 -0
  56. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +106 -0
  57. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +2 -0
  58. package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +5 -0
  59. package/src/diagnostics/logs/logViewerExtractField.ts +2 -3
  60. package/src/diagnostics/managementPages.tsx +10 -0
  61. package/src/diagnostics/trackResources.ts +1 -1
  62. package/src/functional/limitProcessing.ts +39 -0
  63. package/src/misc/lz4_wasm_nodejs.d.ts +34 -0
  64. package/src/misc/lz4_wasm_nodejs.js +178 -0
  65. package/src/misc/lz4_wasm_nodejs_bg.js +94 -0
  66. package/src/misc/lz4_wasm_nodejs_bg.wasm +0 -0
  67. package/src/misc/lz4_wasm_nodejs_bg.wasm.d.ts +15 -0
  68. package/src/storage/CompressedStream.ts +13 -0
  69. package/src/storage/LZ4.ts +32 -0
  70. package/src/storage/ZSTD.ts +10 -0
  71. package/src/wat/watCompiler.ts +1716 -0
  72. package/src/wat/watGrammar.pegjs +93 -0
  73. package/src/wat/watHandler.ts +179 -0
  74. package/src/wat/watInstructions.txt +707 -0
  75. package/src/zip.ts +3 -89
  76. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +0 -125
@@ -0,0 +1,374 @@
1
+ import { measureWrap } from "socket-function/src/profiling/measure";
2
+ import { Archives, createArchivesOverride } from "./archives";
3
+
4
+ interface CacheEntry {
5
+ path: string;
6
+ start: number;
7
+ end: number;
8
+ data: Buffer;
9
+ size: number;
10
+ lastAccess: number;
11
+ }
12
+
13
+ export type ArchivesMemoryCacheStats = {
14
+ cachedReads: number;
15
+ uncachedReads: number;
16
+ cachedReadSize: number;
17
+ uncachedReadSize: number;
18
+ totalCacheSize: number;
19
+ totalCacheCount: number;
20
+ };
21
+
22
+ let cacheDisabled = false;
23
+ export function testDisableCache() {
24
+ cacheDisabled = true;
25
+ }
26
+
27
+ /** Creates an in-memory cache for the archives. We expect that the values will only have values appended to them or be deleted. Also, if a value is deleted, we won't erase our in-memory cache, and so we will still allow reading from deleted values. */
28
+ export function createArchivesMemoryCache(
29
+ archives: Archives,
30
+ config?: {
31
+ extraReadSize?: number;
32
+ maxSize?: number;
33
+ maxCount?: number;
34
+ // If the files are guaranteed to be immutable, then all reads, including reading entire file and reading any range, can be cached.
35
+ // - Otherwise, we can't cache when the entire file is read, as there might be further appends after it.
36
+ // - Also, it means if there's a range read that happens, if that range read doesn't return enough bytes, for example, we try to read from 0 to 100, but we only get 50 bytes back, we can only cache the 0 to 50, and then if they read from 0 to 100 again, we can use the cache, but we have to also read 50 to 100 from the source, because the file might have gotten larger.
37
+ fullyImmutable?: boolean;
38
+ stats?: ArchivesMemoryCacheStats;
39
+ sizeCache?: Map<string, number>;
40
+ }
41
+ ): Archives {
42
+ let {
43
+ maxSize = 1024 * 1024 * 1024 * 4,
44
+ maxCount = 1000 * 1000,
45
+ fullyImmutable = false,
46
+ extraReadSize = 1024 * 1024
47
+ } = config ?? {};
48
+
49
+ // Cache structure: Map from path to sorted array of ranges
50
+ let cacheByPath = new Map<string, CacheEntry[]>();
51
+ // LRU tracking: all entries sorted by last access time
52
+ let lruArray: CacheEntry[] = [];
53
+ let totalSize = 0;
54
+
55
+ const alignRange = measureWrap(function alignRange(start: number, end: number): { start: number; end: number; } {
56
+ if (extraReadSize <= 1) {
57
+ return { start, end };
58
+ }
59
+ return {
60
+ start: Math.floor(start / extraReadSize) * extraReadSize,
61
+ end: Math.ceil(end / extraReadSize) * extraReadSize
62
+ };
63
+ });
64
+
65
+ const updateAccess = measureWrap(function updateAccess(entry: CacheEntry) {
66
+ entry.lastAccess = Date.now();
67
+ // Move to end of LRU array (most recent)
68
+ let index = lruArray.indexOf(entry);
69
+ if (index !== -1) {
70
+ lruArray.splice(index, 1);
71
+ lruArray.push(entry);
72
+ }
73
+ });
74
+
75
+ const evictOldest = measureWrap(function evictOldest() {
76
+ while ((totalSize > maxSize || lruArray.length > maxCount) && lruArray.length > 0) {
77
+ let oldest = lruArray.shift();
78
+ if (oldest) {
79
+ totalSize -= oldest.size;
80
+ // Remove from path map
81
+ let pathEntries = cacheByPath.get(oldest.path);
82
+ if (pathEntries) {
83
+ let index = pathEntries.indexOf(oldest);
84
+ if (index !== -1) {
85
+ pathEntries.splice(index, 1);
86
+ }
87
+ if (pathEntries.length === 0) {
88
+ cacheByPath.delete(oldest.path);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ });
94
+
95
+ const removeEntry = measureWrap(function removeEntry(entry: CacheEntry) {
96
+ totalSize -= entry.size;
97
+
98
+ // Remove from path map
99
+ let pathEntries = cacheByPath.get(entry.path);
100
+ if (pathEntries) {
101
+ let index = pathEntries.indexOf(entry);
102
+ if (index !== -1) {
103
+ pathEntries.splice(index, 1);
104
+ }
105
+ }
106
+
107
+ // Remove from LRU array
108
+ let lruIndex = lruArray.indexOf(entry);
109
+ if (lruIndex !== -1) {
110
+ lruArray.splice(lruIndex, 1);
111
+ }
112
+ });
113
+
114
+ const addEntry = measureWrap(function addEntry(path: string, start: number, end: number, data: Buffer) {
115
+ let entry: CacheEntry = {
116
+ path,
117
+ start,
118
+ end,
119
+ data,
120
+ size: data.length,
121
+ lastAccess: Date.now()
122
+ };
123
+
124
+ // Add to path map (keeping sorted by start)
125
+ let pathEntries = cacheByPath.get(path);
126
+ if (!pathEntries) {
127
+ pathEntries = [];
128
+ cacheByPath.set(path, pathEntries);
129
+ }
130
+
131
+ // Insert sorted by start position
132
+ let insertIndex = 0;
133
+ while (insertIndex < pathEntries.length && pathEntries[insertIndex].start < start) {
134
+ insertIndex++;
135
+ }
136
+ pathEntries.splice(insertIndex, 0, entry);
137
+
138
+ // Add to LRU array
139
+ lruArray.push(entry);
140
+ totalSize += entry.size;
141
+
142
+ evictOldest();
143
+ });
144
+
145
+ const findOverlappingEntries = measureWrap(function findOverlappingEntries(path: string, start: number, end: number): CacheEntry[] {
146
+ let pathEntries = cacheByPath.get(path);
147
+ if (!pathEntries) {
148
+ return [];
149
+ }
150
+
151
+ let overlapping: CacheEntry[] = [];
152
+ for (let entry of pathEntries) {
153
+ // Check if ranges overlap: entry overlaps if it ends after start and starts before end
154
+ if (entry.end > start && entry.start < end) {
155
+ overlapping.push(entry);
156
+ }
157
+ }
158
+ return overlapping;
159
+ });
160
+
161
+ const merge = measureWrap(function merge(path: string) {
162
+ let pathEntries = cacheByPath.get(path);
163
+ if (!pathEntries || pathEntries.length <= 1) {
164
+ return;
165
+ }
166
+
167
+ // Entries are already sorted by start
168
+ // Merge any overlapping or adjacent entries
169
+ for (let i = 0; i < pathEntries.length - 1; i++) {
170
+ let current = pathEntries[i];
171
+ let next = pathEntries[i + 1];
172
+
173
+ // Check if current and next are overlapping or adjacent
174
+ if (current.end >= next.start) {
175
+ // Calculate how much data from next extends beyond current
176
+ let newEnd = Math.max(current.end, next.end);
177
+ let bytesToCopy = newEnd - current.end;
178
+
179
+ if (bytesToCopy > 0) {
180
+ // Extend current's data with data from next
181
+ let newData = Buffer.alloc(newEnd - current.start);
182
+ current.data.copy(newData, 0);
183
+
184
+ // Copy the part of next that extends beyond current
185
+ let nextOffset = current.end - next.start;
186
+ next.data.copy(newData, current.end - current.start, nextOffset, nextOffset + bytesToCopy);
187
+
188
+ current.data = newData;
189
+ current.end = newEnd;
190
+ current.size = newData.length;
191
+ totalSize += bytesToCopy;
192
+ }
193
+
194
+ // Remove next entry
195
+ removeEntry(next);
196
+
197
+ // Check current against the new next
198
+ i--;
199
+ }
200
+ }
201
+ });
202
+
203
+ function updateStats(cached: boolean, size: number | undefined) {
204
+ if (!config?.stats) {
205
+ return;
206
+ }
207
+ if (cached) {
208
+ config.stats.cachedReads++;
209
+ if (size !== undefined) {
210
+ config.stats.cachedReadSize += size;
211
+ }
212
+ } else {
213
+ config.stats.uncachedReads++;
214
+ if (size !== undefined) {
215
+ config.stats.uncachedReadSize += size;
216
+ }
217
+ }
218
+ config.stats.totalCacheSize = totalSize;
219
+ config.stats.totalCacheCount = lruArray.length;
220
+ }
221
+
222
+ async function cachedGet(path: string, getConfig?: {
223
+ range?: { start: number; end: number; };
224
+ retryCount?: number;
225
+ fastRead?: boolean;
226
+ }): Promise<Buffer | undefined> {
227
+ if (cacheDisabled) {
228
+ let result = await archives.get(path, getConfig);
229
+ updateStats(false, result?.length);
230
+ return result;
231
+ }
232
+
233
+ let range = getConfig?.range;
234
+
235
+ // If no range specified, read entire file
236
+ if (!range) {
237
+ // If not fully immutable, don't cache entire file reads (might have appends)
238
+ if (!fullyImmutable) {
239
+ let result = await archives.get(path, getConfig);
240
+ updateStats(false, result?.length);
241
+ return result;
242
+ }
243
+
244
+ // Get file info to determine size, then treat as range read
245
+ let info = await archives.getInfo(path);
246
+ if (!info) {
247
+ updateStats(false, undefined);
248
+ return undefined;
249
+ }
250
+
251
+ // Convert to range read from 0 to file size
252
+ range = { start: 0, end: info.size };
253
+ }
254
+
255
+ // Handle range reads
256
+ let { start, end } = range;
257
+
258
+ // If we have something which satisfies the range request, we should use this first. Otherwise, every single request requires doing an API call, which is extremely slow.
259
+ {
260
+ for (let entry of cacheByPath.get(path) || []) {
261
+ if (entry.start <= start && entry.end >= end) {
262
+ let result = entry.data.slice(start - entry.start, end - entry.start);
263
+ updateStats(true, result.length);
264
+ return result;
265
+ }
266
+ }
267
+ }
268
+
269
+ let cachedSize = config?.sizeCache?.get(path);
270
+ if (cachedSize === undefined) {
271
+ cachedSize = ((await archives.getInfo(path))?.size || 0);
272
+ config?.sizeCache?.set(path, cachedSize);
273
+ updateStats(false, undefined);
274
+ }
275
+ let size = cachedSize;
276
+ if (!size) {
277
+ return undefined;
278
+ }
279
+
280
+ // Track if we had to perform any read from the underlying archives
281
+ let didRead = false;
282
+ {
283
+ let aligned = alignRange(start, end);
284
+
285
+ let readStart = aligned.start;
286
+ let readEnd = Math.min(aligned.end, size);
287
+
288
+ let overlapping = findOverlappingEntries(path, start, end);
289
+
290
+ // Check if we can trim the beginning
291
+ for (let entry of overlapping) {
292
+ if (entry.start <= readStart && entry.end > readStart) {
293
+ // We have cached data covering the beginning
294
+ readStart = Math.max(readStart, entry.end);
295
+ }
296
+ }
297
+
298
+ let haveEnd = false;
299
+
300
+ // Check if we can trim the end
301
+ for (let entry of overlapping) {
302
+ if (entry.start < readEnd && entry.end >= readEnd) {
303
+ // We have cached data covering the end
304
+ readEnd = Math.min(readEnd, entry.start);
305
+ haveEnd = true;
306
+ }
307
+ }
308
+
309
+
310
+ // If after trimming we have nothing to read, we have complete coverage
311
+ if (readStart < readEnd) {
312
+ // Read the single contiguous range
313
+ let data = await archives.get(path, {
314
+ ...getConfig,
315
+ range: { start: readStart, end: readEnd }
316
+ });
317
+
318
+ if (!data) {
319
+ updateStats(false, undefined);
320
+ return undefined;
321
+ }
322
+
323
+ didRead = true;
324
+
325
+ // Cache the data we read (it's immutable even if the file can have future appends)
326
+ readEnd = readStart + data.length;
327
+ // If they tried to read too far then we reduce the end value, otherwise we'll be looking for something which we didn't satisfy and we'll get an error.
328
+ if (!haveEnd) {
329
+ end = Math.min(end, readEnd);
330
+ }
331
+ addEntry(path, readStart, readEnd, data);
332
+ }
333
+ }
334
+
335
+ // Always merge after potentially adding new data
336
+ merge(path);
337
+
338
+ // After merging, find the single entry that covers our aligned range
339
+ let pathEntries = cacheByPath.get(path);
340
+ if (!pathEntries) {
341
+ throw new Error(`Expected cached entries for path ${path} after merge`);
342
+ }
343
+
344
+ let coveringEntry: CacheEntry | undefined = undefined;
345
+ for (let entry of pathEntries) {
346
+ if (entry.start <= start && entry.end >= end) {
347
+ if (coveringEntry) {
348
+ throw new Error(`Multiple entries cover range ${start}-${end} for path ${path}`);
349
+ }
350
+ coveringEntry = entry;
351
+ }
352
+ }
353
+
354
+ if (!coveringEntry) {
355
+ throw new Error(`No entry covers range ${start}-${end} for path ${path}`);
356
+ }
357
+
358
+ updateAccess(coveringEntry);
359
+
360
+ // Extract the requested range
361
+ let offsetStart = start - coveringEntry.start;
362
+ let offsetEnd = offsetStart + (end - start);
363
+ let result = coveringEntry.data.slice(offsetStart, offsetEnd);
364
+
365
+ // Update statistics at the end when we know the final result
366
+ updateStats(!didRead, result.length);
367
+
368
+ return result;
369
+ }
370
+
371
+ return createArchivesOverride(archives, {
372
+ get: cachedGet
373
+ });
374
+ }
@@ -75,6 +75,28 @@ class ArchivesPrivateFileSystem {
75
75
  await writable.close();
76
76
  }
77
77
 
78
+ @measureFnc
79
+ public async append(fileName: string, data: Buffer): Promise<void> {
80
+ this.log(blue(`Appending to file ${fileName} += ${data.length} bytes`));
81
+
82
+ await this.ensureDirsExist(fileName);
83
+
84
+ const pathParts = fileName.split("/");
85
+ const filename = pathParts[pathParts.length - 1];
86
+ const dirPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
87
+
88
+ const directory = await this.getOrCreateDirectory(dirPath);
89
+ const fileHandle = await directory.getFileHandle(filename, { create: true });
90
+
91
+ // Get current file to find the size for appending at the end
92
+ const file = await fileHandle.getFile();
93
+ const currentSize = file.size;
94
+
95
+ const writable = await fileHandle.createWritable({ keepExistingData: true });
96
+ await writable.write({ type: "write", position: currentSize, data });
97
+ await writable.close();
98
+ }
99
+
78
100
  @measureFnc
79
101
  public async del(fileName: string): Promise<void> {
80
102
  this.log(blue(`Deleting file ${fileName}`));
@@ -19,6 +19,9 @@ import { hackDevtoolsWebsocketForward } from "./oneTimeForward";
19
19
  import { getOwnMachineId, decodeNodeId, decodeNodeIdAssert } from "../-a-auth/certs";
20
20
  import { sort } from "socket-function/src/misc";
21
21
  import { getPathStr2 } from "../path";
22
+ setImmediate(() => {
23
+ import("../diagnostics/MachineThreadInfo");
24
+ });
22
25
 
23
26
  let loadTime = Date.now();
24
27
 
@@ -1,9 +1,10 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
3
- import { isDevDebugbreak } from "../config";
3
+ import { isDevDebugbreak, isDiskAudit } from "../config";
4
4
  import { measureWrap } from "socket-function/src/profiling/measure";
5
5
  import { QueueLimited } from "socket-function/src/misc";
6
6
  import { isNode } from "typesafecss";
7
+ import { logDisk } from "../diagnostics/logs/diskLogger";
7
8
 
8
9
  export type DebugLog = {
9
10
  type: string;
@@ -56,6 +57,9 @@ function debugLogBase(type: string, values: { [key: string]: unknown }) {
56
57
  disableAuditLogging();
57
58
  return;
58
59
  }
60
+ if (isDiskAudit()) {
61
+ logDisk("log", type, values, { "diskAudit": true });
62
+ }
59
63
  let newEntry: DebugLog = { type, time: Date.now(), values };
60
64
  logHistory.push(newEntry);
61
65
  };
@@ -17,7 +17,6 @@ import { binarySearchIndex, isNode, isNodeTrue, last, promiseObj, sort, timeInHo
17
17
  import { isNodeTrusted, isTrusted, isTrustedByNode } from "../-d-trust/NetworkTrust2";
18
18
  import { AuthorityPath, LOCAL_DOMAIN, LOCAL_DOMAIN_PATH, pathValueAuthority2 } from "./NodePathAuthorities";
19
19
  import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
20
- import { isNoNetwork } from "../config";
21
20
  import { formatTime } from "socket-function/src/formatting/format";
22
21
  import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
23
22
  import { getNodeIdDomain, getNodeIdDomainMaybeUndefined, getNodeIdIP } from "socket-function/src/nodeCache";
@@ -406,12 +405,6 @@ export function getCompressNetwork() {
406
405
  return getCompressNetworkBase();
407
406
  }
408
407
 
409
- let getCompressDiskBase = () => false;
410
- export const registerGetCompressDisk = (fnc: () => boolean) => { getCompressDiskBase = fnc; };
411
- export function getCompressDisk() {
412
- return getCompressDiskBase();
413
- }
414
-
415
408
 
416
409
  const filterChildPathsBase = measureWrap(
417
410
  function filterChildPathsBase(parentPath: string, packedSuffix: string, paths: Set<string>): Set<string> {
@@ -1260,6 +1253,13 @@ class PathWatcher {
1260
1253
  for (let path of newParentsWatched) {
1261
1254
  auditLog("new local WATCH PARENT", { path });
1262
1255
  }
1256
+ } else {
1257
+ for (let path of newPathsWatched) {
1258
+ auditLog("new non-local WATCH", { path, watcher: config.callback });
1259
+ }
1260
+ for (let path of newParentsWatched) {
1261
+ auditLog("new non-local WATCH PARENT", { path, watcher: config.callback });
1262
+ }
1263
1263
  }
1264
1264
  logDisk("log", `New PathValue watches`, {
1265
1265
  newPathsWatched: newPathsWatched.size,
@@ -1223,6 +1223,7 @@ class QRenderClass {
1223
1223
  // break our render function.
1224
1224
  void Promise.resolve().finally(() => {
1225
1225
  logErrors(proxyWatcher.commitFunction({
1226
+ debugName: getDebugName("ref"),
1226
1227
  canWrite: true,
1227
1228
  baseFunction: ref,
1228
1229
  watchFunction() {
@@ -19,7 +19,7 @@ import { cache, cacheLimited, lazy } from "socket-function/src/caching";
19
19
  import { getOwnMachineId, getOwnThreadId, getThreadKeyCert, verifyMachineIdForPublicKey } from "../-a-auth/certs";
20
20
  import { getHostedIP, getSNICerts, publishMachineARecords } from "../-e-certs/EdgeCertController";
21
21
  import { LOCAL_DOMAIN, nodePathAuthority } from "../0-path-value-core/NodePathAuthorities";
22
- import { debugCoreMode, registerGetCompressNetwork, encodeParentFilter, registerGetCompressDisk, authorityStorage } from "../0-path-value-core/pathValueCore";
22
+ import { debugCoreMode, registerGetCompressNetwork, encodeParentFilter, authorityStorage } from "../0-path-value-core/pathValueCore";
23
23
  import { clientWatcher, ClientWatcher } from "../1-path-client/pathValueClientWatcher";
24
24
  import { SyncWatcher, proxyWatcher, specialObjectWriteValue, isSynced, PathValueProxyWatcher, atomic, doAtomicWrites, noAtomicSchema, undeleteFromLookup, registerSchemaPrefix, WatcherOptions, doProxyOptions } from "../2-proxy/PathValueProxyWatcher";
25
25
  import { isInProxyDatabase, rawSchema } from "../2-proxy/pathDatabaseProxyBase";
@@ -202,9 +202,6 @@ export class Querysub {
202
202
  public static MAX_FUTURE_CALL_TIME = 10_000;
203
203
 
204
204
 
205
- /** Compression makes serialization about 2X slower, but reduces the size by about 2X (more or less if your
206
- * data size is dominated by value size instead of key size). */
207
- public static COMPRESS_DISK = false;
208
205
  public static COMPRESS_NETWORK = true;
209
206
 
210
207
  public static now = getSyncedTime;
@@ -1297,7 +1294,6 @@ setImmediate(async () => {
1297
1294
  });
1298
1295
 
1299
1296
  registerGetCompressNetwork(() => Querysub.COMPRESS_NETWORK);
1300
- registerGetCompressDisk(() => Querysub.COMPRESS_DISK);
1301
1297
 
1302
1298
  (globalThis as any).Querysub = Querysub;
1303
1299
 
package/src/config.ts CHANGED
@@ -22,6 +22,7 @@ let yargObj = parseArgsFactory()
22
22
  // TODO: The bootstrapper is a single file. Maybe we shouldn't run the entire service just for that. Although... maybe it's fine, as services are light?
23
23
  .option("bootstraponly", { type: "boolean", desc: "Don't register as an edge node, so we serve the bootstrap files, but we don't need up to date code because we are not used for endpoints or the UI." })
24
24
  .option("notifyemails", { type: "array", desc: "The emails to notify when errors occur." })
25
+ .option("diskaudit", { type: "boolean", desc: "Track all audit logs to disk. This might end up writing A LOT of data." })
25
26
  .argv
26
27
  ;
27
28
  type QuerysubConfig = {
@@ -74,6 +75,10 @@ export function isRecovery() {
74
75
  return yargObj.recovery;
75
76
  }
76
77
 
78
+ export function isDiskAudit() {
79
+ return !!yargObj.diskaudit;
80
+ }
81
+
77
82
  export function devDebugbreak() {
78
83
  if (!isNode()) {
79
84
  debugger;
@@ -9,6 +9,8 @@ import { ATag, Anchor } from "../../library-components/ATag";
9
9
  import { ShowMore } from "../../library-components/ShowMore";
10
10
  import { filterParam } from "../../diagnostics/logs/FastArchiveViewer";
11
11
  import { managementPageURL } from "../../diagnostics/managementPages";
12
+ import { t } from "../../2-proxy/schema2";
13
+ import { Querysub } from "../../4-querysub/QuerysubController";
12
14
 
13
15
  export class MachineDetailPage extends qreact.Component {
14
16
  render() {
@@ -18,12 +20,14 @@ export class MachineDetailPage extends qreact.Component {
18
20
  let controller = MachineServiceController(SocketFunction.browserNodeId());
19
21
  let machineInfo = controller.getMachineInfo(selectedMachineId);
20
22
  let serviceList = controller.getServiceList();
23
+ let machineConfig = controller.getMachineConfig(selectedMachineId);
21
24
 
22
25
  if (controller.isAnyLoading()) return <div>Loading machine info...</div>;
23
26
  if (!machineInfo) return <div>Machine not found</div>;
24
27
  if (!serviceList) return <div>Service list not found</div>;
25
28
 
26
- const machine = machineInfo; // Create const reference for type safety
29
+ const machine = machineInfo;
30
+ const isDisabled = machineConfig?.disabled || false;
27
31
 
28
32
  // Get all service configs that target this machine
29
33
  let relevantServiceConfigs = new Map<string, ServiceConfig>();
@@ -47,12 +51,49 @@ export class MachineDetailPage extends qreact.Component {
47
51
 
48
52
  const isMachineDead = Date.now() - machine.heartbeat > (MACHINE_RESYNC_INTERVAL * 4);
49
53
 
54
+ let failingServices = Object.keys(machine.services).filter(serviceId => {
55
+ return machine.services[serviceId].errorFromLastRun;
56
+ });
57
+
58
+ let backgroundColor = (
59
+ isDisabled && css.hsl(0, 0, 50)
60
+ || failingServices.length > 0 && css.hsl(0, 50, 60)
61
+ || isMachineDead && css.hsl(45, 80, 80)
62
+ || css.hsl(0, 0, 100)
63
+ );
64
+
50
65
  return <div className={css.vbox(16)}>
51
- <div className={css.hbox(12)}>
66
+ <div className={css.hbox(12).pad2(16).bord2(0, 0, 20) + backgroundColor}>
52
67
  <h2 className={css.flexGrow(1)}>{selectedMachineId}</h2>
53
68
  {isMachineDead && <div className={css.colorhsl(0, 80, 60)}>
54
69
  ⚠️ Machine is likely dead
55
70
  </div>}
71
+ {isDisabled && <div className={css.colorhsl(30, 80, 60).pad2(8, 4).bord2(30, 80, 60).hsl(30, 80, 95)}>
72
+ ⚠️ Machine is disabled
73
+ </div>}
74
+ </div>
75
+
76
+ <div className={css.hbox(12).pad2(16).bord2(0, 0, 20).hsl(0, 0, 95)}>
77
+ <div className={css.flexGrow(1).vbox(4)}>
78
+ <div className={css.fontSize(16).colorhsl(0, 0, 20)}>
79
+ <b>Machine Status</b>
80
+ </div>
81
+ <div>
82
+ {isDisabled && "When disabled, services will not be deployed to this machine. Re-enabling will restore all service assignments." || "This machine is enabled and available for service deployments."}
83
+ </div>
84
+ </div>
85
+ <button
86
+ className={css.pad2(12, 8).button.bord2(0, 0, 20) + (isDisabled ? css.hsl(120, 60, 90) : css.hsl(30, 80, 90))}
87
+ onClick={() => {
88
+ Querysub.onCommitFinished(async () => {
89
+ await controller.setMachineConfig.promise(selectedMachineId, {
90
+ machineId: selectedMachineId,
91
+ disabled: !isDisabled
92
+ });
93
+ });
94
+ }}>
95
+ {isDisabled && "Enable Machine" || "Disable Machine"}
96
+ </button>
56
97
  </div>
57
98
 
58
99
  <div className={css.vbox(12)}>
@@ -28,6 +28,8 @@ export class MachinesListPage extends qreact.Component {
28
28
  let machines = machineList.map(machineId => [machineId, controller.getMachineInfo(machineId)] as const).filter(x => x[1]);
29
29
  sort(machines, x => -(x[1]?.heartbeat || 0));
30
30
 
31
+ let machineConfigs = controller.getMachineConfigList();
32
+
31
33
  const selectedMachineIds = Object.keys(this.state.selectedForDeletion);
32
34
  const hasSelectedMachines = selectedMachineIds.length > 0;
33
35
  const DEAD_MACHINE_THRESHOLD = MACHINE_RESYNC_INTERVAL * 4;
@@ -121,6 +123,8 @@ export class MachinesListPage extends qreact.Component {
121
123
  const serviceCount = Object.keys(machineInfo.services).length;
122
124
  const isMachineDead = Date.now() - machineInfo.heartbeat > (MACHINE_RESYNC_INTERVAL * 4);
123
125
  const isSelected = this.state.selectedForDeletion[machineId];
126
+ const machineConfig = machineConfigs?.find(x => x.machineId === machineId);
127
+ const isDisabled = machineConfig?.disabled || false;
124
128
 
125
129
  let failingServices = Object.keys(machineInfo.services).filter(serviceId => {
126
130
  return machineInfo.services[serviceId].errorFromLastRun;
@@ -136,8 +140,9 @@ export class MachinesListPage extends qreact.Component {
136
140
  className={
137
141
  css.pad2(12).bord2(0, 0, 20).button
138
142
  + (
139
- failingServices.length > 0 && css.hsl(0, 50, 60)
140
- || isMachineDead && css.hsl(0, 0, 50)
143
+ isDisabled && css.hsl(0, 0, 50)
144
+ || failingServices.length > 0 && css.hsl(0, 50, 60)
145
+ || isMachineDead && css.hsl(45, 80, 80)
141
146
  || css.hsl(0, 0, 100)
142
147
  )
143
148
  + (this.state.isDeleteMode && isSelected && css.bord2(200, 80, 60, 2))
@@ -165,6 +170,9 @@ export class MachinesListPage extends qreact.Component {
165
170
  />
166
171
  </div>
167
172
  )}
173
+ {isDisabled && <div className={css.colorhsl(30, 80, 60).pad2(4, 2).bord2(30, 80, 60).hsl(30, 80, 95)}>
174
+ 🚫 Disabled
175
+ </div>}
168
176
  {isMachineDead && <div className={css.colorhsl(0, 80, 50)}>
169
177
  ⚠️ Machine is likely dead
170
178
  </div>}
@@ -4,7 +4,7 @@ import { measureWrap } from "socket-function/src/profiling/measure";
4
4
  import { getOwnMachineId } from "../-a-auth/certs";
5
5
  import { forceRemoveNode, getOurNodeId, getOurNodeIdAssert } from "../-f-node-discovery/NodeDiscovery";
6
6
  import { Querysub } from "../4-querysub/QuerysubController";
7
- import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, serviceConfigs, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME } from "./machineSchema";
7
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, serviceConfigs, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs } from "./machineSchema";
8
8
  import { runPromise } from "../functional/runCommand";
9
9
  import { getExternalIP } from "socket-function/src/networking";
10
10
  import { errorToUndefined, errorToUndefinedSilent } from "../errors";
@@ -579,7 +579,7 @@ async function quickIsOutdated() {
579
579
  return true;
580
580
  }
581
581
  }
582
- let configs = await serviceConfigs.values();
582
+ let configs = await getEffectiveServiceConfigs();
583
583
  let relevantConfigs = configs.filter(config => config.machineIds.includes(machineId)).filter(x => x.parameters.deploy);
584
584
  let screens = await getScreenState();
585
585
  let root = os.homedir() + "/" + SERVICE_FOLDER;
@@ -608,7 +608,7 @@ async function quickIsOutdated() {
608
608
  const resyncServicesBase = runInSerial(measureWrap(async function resyncServices() {
609
609
  console.log(magenta("Resyncing services"));
610
610
  let machineId = getOwnMachineId();
611
- let configs = await serviceConfigs.values();
611
+ let configs = await getEffectiveServiceConfigs();
612
612
  let relevantConfigs = configs.filter(config => config.machineIds.includes(machineId)).filter(x => x.parameters.deploy);
613
613
 
614
614
  let machineInfo = await getLiveMachineInfo();