querysub 0.357.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.
package/.cursorrules CHANGED
@@ -26,6 +26,7 @@ NEVER EVER pass state to qreact.Component as a template parameter. It should ALS
26
26
 
27
27
  t.lookup objects cannot be set with `this.state.someLookup = {}`. You can add keys or remove keys, and keys are added implicitly (`this.state.someLookup["someKey"] = { x: 1, y: 1 }`, always works, the key is automatically added if it doesn't exist). Removal uses delete `delete this.state.someLookup["someKey"]`.
28
28
 
29
+ Follow the rule of minimum scoping. If something can be a local variable, it should be a local variable, it shouldn't be a class variable. If it can be a class variable, it should be a class variable, not a state variable. Etc, etc. Keep it simple, reference things closer, and write less code.
29
30
 
30
31
  Try not to use "null", and instead always use "undefined".
31
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.357.0",
3
+ "version": "0.358.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",
@@ -56,6 +56,7 @@
56
56
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
57
57
  "pako": "^2.1.0",
58
58
  "peggy": "^5.0.6",
59
+ "querysub": "^0.357.0",
59
60
  "socket-function": "^1.1.2",
60
61
  "terser": "^5.31.0",
61
62
  "typesafecss": "^0.28.0",
@@ -75,7 +75,7 @@ class ArchivesDisk {
75
75
  }
76
76
  }
77
77
 
78
- @measureFnc
78
+ // We don't time the file system writes as it's almost never blocking, it's not stopping our processing. And if it is stopping our processing, then we actually want to know the parent function which is calling it, and then we want it to just void it instead of awaiting it (And the only way to know the parent caller is to not time these functions).
79
79
  public async set(fileName: string, data: Buffer): Promise<void> {
80
80
  await this.init();
81
81
 
@@ -88,7 +88,6 @@ class ArchivesDisk {
88
88
 
89
89
  await fs.promises.writeFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
90
90
  }
91
- @measureFnc
92
91
  public async append(fileName: string, data: Buffer): Promise<void> {
93
92
  await this.init();
94
93
 
@@ -101,7 +100,6 @@ class ArchivesDisk {
101
100
 
102
101
  await fs.promises.appendFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
103
102
  }
104
- @measureFnc
105
103
  public async del(fileName: string): Promise<void> {
106
104
  await this.init();
107
105
  this.log(blue(`Deleting file ${fileName}`));
@@ -112,8 +110,16 @@ class ArchivesDisk {
112
110
  try {
113
111
  await fs.promises.rm(this.LOCAL_ARCHIVE_FOLDER + fileName, { recursive: true });
114
112
  } catch { }
115
- let dir = fileName.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
116
- await this.gcDir(dir);
113
+ try {
114
+ let dir = fileName;
115
+ while (dir.length > 0) {
116
+ dir = dir.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
117
+ if (dir.endsWith(":")) break;
118
+ let files = await fs.promises.readdir(this.LOCAL_ARCHIVE_FOLDER + dir);
119
+ if (files.length > 0) break;
120
+ await fs.promises.rmdir(this.LOCAL_ARCHIVE_FOLDER + dir);
121
+ }
122
+ } catch { }
117
123
  }
118
124
 
119
125
  public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
@@ -180,7 +186,8 @@ class ArchivesDisk {
180
186
  * it all at once).
181
187
  * @fsErrorRetryCount Retries count for file system error (not user errors). Ex, too many open files, etc.
182
188
  */
183
- @measureFnc
189
+ // NOTE: I've commented out the gate measuring so we can determine what is taking the time. We might want to remove all the timing from this file, as if we find any timing this file is slow, it doesn't help us optimize at all, because we then need to figure out what is calling it.
190
+ //@measureFnc
184
191
  public async get(fileNameInput: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
185
192
  await this.init();
186
193
  this.log(blue(`Start read file ${fileNameInput}`));
@@ -36,13 +36,14 @@ export function createArchivesMemoryCache(
36
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
37
  fullyImmutable?: boolean;
38
38
  stats?: ArchivesMemoryCacheStats;
39
+ sizeCache?: Map<string, number>;
39
40
  }
40
41
  ): Archives {
41
42
  let {
42
43
  maxSize = 1024 * 1024 * 1024 * 4,
43
44
  maxCount = 1000 * 1000,
44
45
  fullyImmutable = false,
45
- extraReadSize = 1024
46
+ extraReadSize = 1024 * 1024
46
47
  } = config ?? {};
47
48
 
48
49
  // Cache structure: Map from path to sorted array of ranges
@@ -199,13 +200,34 @@ export function createArchivesMemoryCache(
199
200
  }
200
201
  });
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
+
202
222
  async function cachedGet(path: string, getConfig?: {
203
223
  range?: { start: number; end: number; };
204
224
  retryCount?: number;
205
225
  fastRead?: boolean;
206
226
  }): Promise<Buffer | undefined> {
207
227
  if (cacheDisabled) {
208
- return await archives.get(path, getConfig);
228
+ let result = await archives.get(path, getConfig);
229
+ updateStats(false, result?.length);
230
+ return result;
209
231
  }
210
232
 
211
233
  let range = getConfig?.range;
@@ -214,12 +236,15 @@ export function createArchivesMemoryCache(
214
236
  if (!range) {
215
237
  // If not fully immutable, don't cache entire file reads (might have appends)
216
238
  if (!fullyImmutable) {
217
- return await archives.get(path, getConfig);
239
+ let result = await archives.get(path, getConfig);
240
+ updateStats(false, result?.length);
241
+ return result;
218
242
  }
219
243
 
220
244
  // Get file info to determine size, then treat as range read
221
245
  let info = await archives.getInfo(path);
222
246
  if (!info) {
247
+ updateStats(false, undefined);
223
248
  return undefined;
224
249
  }
225
250
 
@@ -234,12 +259,20 @@ export function createArchivesMemoryCache(
234
259
  {
235
260
  for (let entry of cacheByPath.get(path) || []) {
236
261
  if (entry.start <= start && entry.end >= end) {
237
- return entry.data.slice(start - entry.start, end - entry.start);
262
+ let result = entry.data.slice(start - entry.start, end - entry.start);
263
+ updateStats(true, result.length);
264
+ return result;
238
265
  }
239
266
  }
240
267
  }
241
268
 
242
- let size = await archives.getInfo(path);
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;
243
276
  if (!size) {
244
277
  return undefined;
245
278
  }
@@ -250,7 +283,7 @@ export function createArchivesMemoryCache(
250
283
  let aligned = alignRange(start, end);
251
284
 
252
285
  let readStart = aligned.start;
253
- let readEnd = Math.min(aligned.end, size.size);
286
+ let readEnd = Math.min(aligned.end, size);
254
287
 
255
288
  let overlapping = findOverlappingEntries(path, start, end);
256
289
 
@@ -283,6 +316,7 @@ export function createArchivesMemoryCache(
283
316
  });
284
317
 
285
318
  if (!data) {
319
+ updateStats(false, undefined);
286
320
  return undefined;
287
321
  }
288
322
 
@@ -329,17 +363,7 @@ export function createArchivesMemoryCache(
329
363
  let result = coveringEntry.data.slice(offsetStart, offsetEnd);
330
364
 
331
365
  // Update statistics at the end when we know the final result
332
- if (config?.stats) {
333
- if (didRead) {
334
- config.stats.uncachedReads++;
335
- config.stats.uncachedReadSize += result.length;
336
- } else {
337
- config.stats.cachedReads++;
338
- config.stats.cachedReadSize += result.length;
339
- }
340
- config.stats.totalCacheSize = totalSize;
341
- config.stats.totalCacheCount = lruArray.length;
342
- }
366
+ updateStats(!didRead, result.length);
343
367
 
344
368
  return result;
345
369
  }
@@ -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();
@@ -93,8 +93,13 @@ export type ServiceConfig = {
93
93
  lastUpdatedTime: number;
94
94
  };
95
95
  };
96
+ export type MachineConfig = {
97
+ machineId: string;
98
+ disabled: boolean;
99
+ };
96
100
  export const machineInfos = archiveJSONT<MachineInfo>(() => nestArchives("machines/machine-heartbeats/", getArchivesBackblaze(getDomain())));
97
101
  export const serviceConfigs = archiveJSONT<ServiceConfig>(() => nestArchives("machines/service-configs/", getArchivesBackblaze(getDomain())));
102
+ export const machineConfigs = archiveJSONT<MachineConfig>(() => nestArchives("machines/machine-configs/", getArchivesBackblaze(getDomain())));
98
103
 
99
104
  export function doRegisterNodeForMachineCleanup() {
100
105
  if (isNode()) {
@@ -153,6 +158,16 @@ export class MachineServiceControllerBase {
153
158
  return await serviceConfigs.get(serviceId);
154
159
  }
155
160
 
161
+ public async getMachineConfigList() {
162
+ return await machineConfigs.values();
163
+ }
164
+ public async getMachineConfig(machineId: string): Promise<MachineConfig | undefined> {
165
+ return await machineConfigs.get(machineId);
166
+ }
167
+ public async setMachineConfig(machineId: string, config: MachineConfig) {
168
+ await machineConfigs.set(machineId, config);
169
+ }
170
+
156
171
  private async notifyMachines(newMachineIds: string[], oldMachineIds: string[]) {
157
172
  let addedNodeIds = new Set<string>();
158
173
  let removedNodeIds = new Set<string>();
@@ -358,6 +373,24 @@ export class MachineServiceControllerBase {
358
373
  });
359
374
  }
360
375
  }
376
+
377
+ export async function getEffectiveServiceConfigs(): Promise<ServiceConfig[]> {
378
+ let configs = await serviceConfigs.values();
379
+ let allMachineConfigs = await machineConfigs.values();
380
+
381
+ let disabledMachineIds = new Set<string>();
382
+ for (let machineConfig of allMachineConfigs) {
383
+ if (machineConfig.disabled) {
384
+ disabledMachineIds.add(machineConfig.machineId);
385
+ }
386
+ }
387
+
388
+ return configs.map(config => ({
389
+ ...config,
390
+ machineIds: config.machineIds.filter(machineId => !disabledMachineIds.has(machineId)),
391
+ }));
392
+ }
393
+
361
394
  let deployWatchers = new Set<DeployProgress>();
362
395
  class DeployProgressControllerBase {
363
396
  async onDeployProgress(config: { section: string; progress: number; }) {
@@ -401,6 +434,9 @@ export const MachineServiceController = getSyncedController(
401
434
  setMachineInfo: {},
402
435
  getServiceList: {},
403
436
  getServiceConfig: {},
437
+ getMachineConfigList: {},
438
+ getMachineConfig: {},
439
+ setMachineConfig: {},
404
440
  addServiceConfig: {},
405
441
  setServiceConfigs: {},
406
442
  getServiceConfigType: {},
@@ -422,6 +458,7 @@ export const MachineServiceController = getSyncedController(
422
458
  deleteMachineIds: ["MachineInfo", "MachineInfoList"],
423
459
  addMachineInfo: ["MachineInfo", "MachineInfoList"],
424
460
  setMachineInfo: ["MachineInfo"],
461
+ setMachineConfig: ["MachineConfig", "MachineConfigList"],
425
462
  addServiceConfig: ["ServiceConfig", "ServiceConfigList"],
426
463
  setServiceConfigs: ["ServiceConfig"],
427
464
  deleteServiceConfig: ["ServiceConfig", "ServiceConfigList"],
@@ -432,6 +469,8 @@ export const MachineServiceController = getSyncedController(
432
469
  reads: {
433
470
  getMachineList: ["MachineInfoList"],
434
471
  getMachineInfo: ["MachineInfo"],
472
+ getMachineConfigList: ["MachineConfigList"],
473
+ getMachineConfig: ["MachineConfig"],
435
474
  getServiceList: ["ServiceConfigList"],
436
475
  getServiceConfig: ["ServiceConfig"],
437
476
  getGitInfo: ["gitInfo"],
@@ -80,6 +80,7 @@ export class NodeViewer extends qreact.Component {
80
80
  Querysub.commit(() => this.state.nodeIds = nodeIds);
81
81
 
82
82
  let ourIP = await controller.getCallerIP();
83
+ let ourExternalIP = await getExternalIP();
83
84
 
84
85
  await Promise.allSettled(nodeIds.map(async nodeId => {
85
86
  let time = Date.now();
@@ -97,7 +98,7 @@ export class NodeViewer extends qreact.Component {
97
98
  data.live_entryPoint = await controller.live_getEntryPoint(nodeId);
98
99
  data.live_authorityPaths = await controller.live_getAuthorityPaths(nodeId);
99
100
  data.ip = await controller.getNodeIP(nodeId);
100
- if (data.ip === ourIP || data.ip === "127.0.0.1") {
101
+ if (data.ip === ourExternalIP || data.ip === ourIP) {
101
102
  data.devToolsURL = await controller.getInspectURL(nodeId);
102
103
  if (data.devToolsURL) {
103
104
  data.devToolsURL = data.devToolsURL + "<inspect>";