querysub 0.508.0 → 0.509.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 (48) hide show
  1. package/bin/function-public.js +2 -0
  2. package/bin/server.js +0 -2
  3. package/package.json +3 -4
  4. package/spec.txt +5 -0
  5. package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
  6. package/src/-g-core-values/NodeCapabilities.ts +4 -0
  7. package/src/-g-core-values/scheduledShutdown.ts +48 -0
  8. package/src/0-path-value-core/AuthorityLookup.ts +50 -9
  9. package/src/0-path-value-core/PathRouter.ts +10 -31
  10. package/src/0-path-value-core/PathRouterRouteOverride.ts +14 -98
  11. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +1 -4
  12. package/src/0-path-value-core/PathValueCommitter.ts +1 -6
  13. package/src/0-path-value-core/PathValueController.ts +5 -7
  14. package/src/0-path-value-core/PathWatcher.ts +8 -11
  15. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +8 -22
  16. package/src/0-path-value-core/pathValueCore.ts +4 -5
  17. package/src/1-path-client/RemoteWatcher.ts +47 -34
  18. package/src/2-proxy/archiveMoveHarness.ts +2 -1
  19. package/src/3-path-functions/PathFunctionHelpers.ts +11 -10
  20. package/src/3-path-functions/PathFunctionRunner.ts +165 -24
  21. package/src/3-path-functions/functionReplay.ts +2 -0
  22. package/src/3-path-functions/syncSchema.ts +0 -2
  23. package/src/4-deploy/edgeBootstrap.ts +10 -0
  24. package/src/4-deploy/edgeClientWatcher.tsx +4 -6
  25. package/src/4-deploy/edgeNodes.ts +52 -12
  26. package/src/4-querysub/FunctionRunnerTracking.ts +290 -0
  27. package/src/4-querysub/Querysub.ts +11 -16
  28. package/src/4-querysub/QuerysubController.ts +13 -11
  29. package/src/4-querysub/permissions.ts +3 -0
  30. package/src/4-querysub/querysubPrediction.ts +2 -2
  31. package/src/config.ts +1 -3
  32. package/src/deployManager/components/ServiceDetailPage.tsx +127 -73
  33. package/src/deployManager/components/ServicesListPage.tsx +3 -3
  34. package/src/deployManager/machineApplyMainCode.ts +139 -153
  35. package/src/deployManager/machineController.ts +12 -61
  36. package/src/deployManager/machineSchema.ts +84 -3
  37. package/src/deployManager/setupMachineMain.ts +2 -1
  38. package/src/diagnostics/grossStats/GrossStatsController.ts +3 -2
  39. package/src/diagnostics/logs/errorTickets/autoFixer.ts +3 -2
  40. package/src/diagnostics/logs/logGitHashes.ts +1 -1
  41. package/src/diagnostics/managementPages.tsx +2 -0
  42. package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +36 -28
  43. package/src/diagnostics/misc-pages/FunctionCaptureAnalyzePage.tsx +1 -1
  44. package/src/library-components/NetworkSelector.tsx +77 -0
  45. package/src/misc.ts +19 -0
  46. package/src/user-implementation/userData.ts +1 -1
  47. package/src/zipThreaded.ts +3 -2
  48. package/bin/server-dev.js +0 -11
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig } from "../machineSchema";
3
+ import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/Querysub";
@@ -12,7 +12,7 @@ import { deepCloneJSON, nextId, sort } from "socket-function/src/misc";
12
12
  import { InputLabel } from "../../library-components/InputLabel";
13
13
  import { Button } from "../../library-components/Button";
14
14
  import { isDefined } from "../../misc";
15
- import { watchScreenOutput, stopWatchingScreenOutput, MachineController } from "../machineController";
15
+ import { watchScreenOutput, stopWatchingScreenOutput } from "../machineController";
16
16
  import { getPathStr2 } from "../../path";
17
17
  import { ATag, Anchor } from "../../library-components/ATag";
18
18
  import { ScrollOnMount } from "../../library-components/ScrollOnMount";
@@ -42,7 +42,10 @@ export class ServiceDetailPage extends qreact.Component {
42
42
  data: t.type(""),
43
43
  callbackId: t.type(""),
44
44
  }),
45
- isKillingRolling: t.type(false),
45
+ // Milliseconds; 0 means no scheduled time picked yet (use Deploy Now instead)
46
+ switchTime: t.number(0),
47
+ // Milliseconds; 0 means use the service's configured overlapTime
48
+ overlapOverride: t.number(0),
46
49
  });
47
50
 
48
51
 
@@ -109,37 +112,7 @@ export class ServiceDetailPage extends qreact.Component {
109
112
  });
110
113
  }
111
114
 
112
- private async killAllRollingServices(config: ServiceConfig) {
113
- Querysub.commit(() => {
114
- this.state.isKillingRolling = true;
115
- });
116
-
117
- Querysub.onCommitFinished(async () => {
118
- try {
119
- let uniqueMachineIds = Array.from(new Set(config.machineIds));
120
-
121
- for (let machineId of uniqueMachineIds) {
122
- let machineInfo = await MachineServiceController(SocketFunction.browserNodeId()).getMachineInfo.promise(machineId);
123
- if (!machineInfo) continue;
124
-
125
- let applyNodeId = machineInfo.applyNodeId;
126
- await MachineController(SocketFunction.browserNodeId()).killRollingServicesFromBrowser.promise({
127
- machineNodeId: applyNodeId,
128
- serviceKey: config.parameters.key,
129
- });
130
- }
131
- } catch (e: any) {
132
- Querysub.localCommit(() => {
133
- this.state.saveError = `Error killing rolling services: ${e.message}`;
134
- });
135
- } finally {
136
- Querysub.localCommit(() => {
137
- this.state.isKillingRolling = false;
138
- });
139
- }
140
- });
141
- }
142
-
115
+ // Saves to the PENDING config, which only takes effect once a switchover is deployed / scheduled.
143
116
  private async updateConfig(updatedConfig: ServiceConfig): Promise<void> {
144
117
  const selectedServiceId = selectedServiceIdParam.value;
145
118
  if (!selectedServiceId) return;
@@ -153,7 +126,7 @@ export class ServiceDetailPage extends qreact.Component {
153
126
 
154
127
  Querysub.onCommitFinished(async () => {
155
128
  try {
156
- await MachineServiceController(SocketFunction.browserNodeId()).setServiceConfigs.promise([updatedConfig]);
129
+ await MachineServiceController(SocketFunction.browserNodeId()).setPendingServiceConfigs.promise([updatedConfig]);
157
130
  } catch (error) {
158
131
  Querysub.localCommit(() => {
159
132
  this.state.saveError = error instanceof Error ? error.message : String(error);
@@ -168,6 +141,18 @@ export class ServiceDetailPage extends qreact.Component {
168
141
  });
169
142
  }
170
143
 
144
+ private runControllerAction(action: () => Promise<void>) {
145
+ Querysub.onCommitFinished(async () => {
146
+ try {
147
+ await action();
148
+ } catch (error) {
149
+ Querysub.localCommit(() => {
150
+ this.state.saveError = error instanceof Error ? error.message : String(error);
151
+ });
152
+ }
153
+ });
154
+ }
155
+
171
156
  render() {
172
157
  const selectedServiceId = selectedServiceIdParam.value;
173
158
  if (!selectedServiceId) return <div>No service selected</div>;
@@ -175,40 +160,21 @@ export class ServiceDetailPage extends qreact.Component {
175
160
  const gitInfo = MachineServiceController(SocketFunction.browserNodeId()).getGitInfo();
176
161
 
177
162
  let controller = MachineServiceController(SocketFunction.browserNodeId());
178
- let originalConfig = controller.getServiceConfig(selectedServiceId);
163
+ const originalConfig = controller.getServiceConfig(selectedServiceId);
179
164
  let serviceConfigType = controller.getServiceConfigType();
165
+ let pendingConfig = controller.getPendingServiceConfig(selectedServiceId);
166
+ let switchover = controller.getServiceSwitchover(selectedServiceId);
180
167
 
181
168
  if (!originalConfig || !serviceConfigType) {
182
169
  if (controller.isAnyLoading()) return <div>Loading service...</div>;
183
170
  return <div>Service not found</div>;
184
171
  }
185
172
 
186
- // Use unsaved changes if available, otherwise use original config
187
- const config = this.state.unsavedChanges ? this.state.unsavedChanges : originalConfig;
173
+ // The editor shows unsaved edits, else the saved-but-not-deployed pending config, else the live config
174
+ const config = this.state.unsavedChanges || pendingConfig || originalConfig;
188
175
  const configT = config;
189
176
  const hasUnsavedChanges = !!this.state.unsavedChanges;
190
-
191
- let rollingServiceCount = 0;
192
- if (config.parameters.rollingWindow) {
193
- let uniqueMachineIds = Array.from(new Set(config.machineIds));
194
- for (let machineId of uniqueMachineIds) {
195
- try {
196
- let machineInfo = controller.getMachineInfo(machineId);
197
- if (!machineInfo) continue;
198
-
199
- let applyNodeId = machineInfo.applyNodeId;
200
- let rollingInfo = MachineController(SocketFunction.browserNodeId()).getRollingServiceInfoFromBrowser({
201
- machineNodeId: applyNodeId,
202
- serviceKey: config.parameters.key,
203
- });
204
- if (rollingInfo) {
205
- rollingServiceCount += rollingInfo.length;
206
- }
207
- } catch (e: any) {
208
- console.error(`Error getting rolling service info for machine ${machineId}: ${e.message}`);
209
- }
210
- }
211
- }
177
+ const hasPendingConfig = !!pendingConfig;
212
178
 
213
179
  // Sort machines by status and heartbeat
214
180
  let nextIndexes = new Map<string, number>();
@@ -358,6 +324,11 @@ export class ServiceDetailPage extends qreact.Component {
358
324
  <div title={formatVeryNiceDateTime(machineInfo.heartbeat)}>
359
325
  Heartbeat: {formatTime(now - machineInfo.heartbeat)}
360
326
  </div>
327
+ {switchover && (
328
+ <div className={css.colorhsl(35, 80, 40).fontWeight("bold")} title={formatVeryNiceDateTime(switchover.switchTime)}>
329
+ ⏻ Shuts down in {formatTime(switchover.switchTime - now)}
330
+ </div>
331
+ )}
361
332
  {serviceInfo && (
362
333
  <>
363
334
  {!!serviceInfo.lastLaunchedTime && <div title={formatVeryNiceDateTime(serviceInfo.lastLaunchedTime)}>
@@ -482,22 +453,14 @@ export class ServiceDetailPage extends qreact.Component {
482
453
 
483
454
  <button className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 80)}
484
455
  onClick={() => {
485
- configT.parameters.poke = (configT.parameters.poke || 0) + 1;
486
- void this.updateConfigSynced(configT);
456
+ // Poke bypasses the pending flow, as it exists to force an immediate restart of the LIVE config
457
+ let live = deepCloneJSON(originalConfig);
458
+ live.parameters.poke = (live.parameters.poke || 0) + 1;
459
+ this.runControllerAction(() => controller.setServiceConfigs.promise([live]));
487
460
  }}>
488
461
  Poke
489
462
  </button>
490
463
 
491
- {!!configT.parameters.rollingWindow && rollingServiceCount > 0 && <button
492
- className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 70, 90)}
493
- onClick={() => {
494
- void this.killAllRollingServices(config);
495
- }}
496
- disabled={this.state.isKillingRolling}
497
- >
498
- {this.state.isKillingRolling ? "Killing..." : `💀 Kill ${rollingServiceCount} Rolling Service${rollingServiceCount === 1 ? "" : "s"}`}
499
- </button>}
500
-
501
464
  {hasUnsavedChanges && <>
502
465
  <Button
503
466
  flavor="noui"
@@ -511,7 +474,7 @@ export class ServiceDetailPage extends qreact.Component {
511
474
  this.updateConfigSynced(this.state.unsavedChanges);
512
475
  }
513
476
  }>
514
- {this.state.isSaving ? "Saving..." : "Save"}
477
+ {this.state.isSaving ? "Saving..." : "Save Pending"}
515
478
  </Button>
516
479
  <button
517
480
  className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(50, 80, 50)}
@@ -604,6 +567,97 @@ export class ServiceDetailPage extends qreact.Component {
604
567
  </button>
605
568
  </div>
606
569
 
570
+ {(hasPendingConfig || switchover) && (() => {
571
+ let overlap = this.state.overlapOverride || config.parameters.overlapTime || DEFAULT_OVERLAP_TIME;
572
+ let scheduledTime = this.state.switchTime;
573
+ let notEnoughTime = !!scheduledTime && scheduledTime - now < overlap;
574
+ return <div className={css.vbox(10).pad2(12).fillWidth.bord2(210, 60, 50).hsl(210, 40, 95)}>
575
+ <div className={css.boldStyle.fontSize(15)}>
576
+ {switchover && "🕐 Switchover scheduled" || "📝 Pending changes saved (not deployed)"}
577
+ </div>
578
+ {switchover && <div className={css.vbox(4)}>
579
+ <div>
580
+ Old instances shut down at <b>{formatVeryNiceDateTime(switchover.switchTime)}</b> (in {formatTime(switchover.switchTime - now)}), overlap {formatTime(switchover.overlapTime)}
581
+ </div>
582
+ <div>
583
+ New instances start at <b>{formatVeryNiceDateTime(switchover.switchTime - switchover.overlapTime)}</b> {switchover.switchTime - switchover.overlapTime <= now && "(already running)" || `(in ${formatTime(switchover.switchTime - switchover.overlapTime - now)})`}
584
+ </div>
585
+ <div className={css.hbox(8)}>
586
+ <button
587
+ className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 70, 90)}
588
+ onClick={() => {
589
+ this.runControllerAction(() => controller.cancelSwitchover.promise(selectedServiceId));
590
+ }}>
591
+ Cancel Switchover
592
+ </button>
593
+ </div>
594
+ </div>}
595
+ {hasPendingConfig && !switchover && <div className={css.vbox(8)}>
596
+ <div className={css.hbox(12).wrap}>
597
+ <InputLabel
598
+ label={`Overlap (ms, default ${formatTime(config.parameters.overlapTime || DEFAULT_OVERLAP_TIME)})`}
599
+ number
600
+ value={this.state.overlapOverride || ""}
601
+ onChangeValue={value => {
602
+ this.state.overlapOverride = Number(value) || 0;
603
+ }}
604
+ />
605
+ <span>Effective overlap: {formatTime(overlap)}</span>
606
+ </div>
607
+ <div className={css.hbox(12).wrap}>
608
+ <button
609
+ className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(120, 70, 85).fontWeight("bold")}
610
+ onClick={() => {
611
+ let switchTime = Date.now() + overlap;
612
+ this.runControllerAction(() => controller.scheduleSwitchover.promise({
613
+ serviceId: selectedServiceId,
614
+ switchTime,
615
+ overlapTime: overlap,
616
+ }));
617
+ }}>
618
+ 🚀 Deploy Now (switch in {formatTime(overlap)})
619
+ </button>
620
+ <InputLabel
621
+ label="Switch at"
622
+ isDatetime
623
+ value={this.state.switchTime}
624
+ onChangeValue={value => {
625
+ this.state.switchTime = Number(value) || 0;
626
+ }}
627
+ />
628
+ {!!scheduledTime && <span>
629
+ {scheduledTime > now && `in ${formatTime(scheduledTime - now)}` || "in the past!"}
630
+ </span>}
631
+ {notEnoughTime && <span className={css.colorhsl(35, 85, 40).fontWeight("bold")}>
632
+ ⚠️ Not enough time for the full overlap ({formatTime(overlap)}) — the new instances won't be fully started before the old ones shut down
633
+ </span>}
634
+ <button
635
+ className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(120, 70, 90)}
636
+ disabled={!scheduledTime}
637
+ onClick={() => {
638
+ if (!scheduledTime) return;
639
+ this.runControllerAction(() => controller.scheduleSwitchover.promise({
640
+ serviceId: selectedServiceId,
641
+ switchTime: scheduledTime,
642
+ overlapTime: overlap,
643
+ }));
644
+ }}>
645
+ 🕐 Schedule Switchover
646
+ </button>
647
+ </div>
648
+ <div className={css.hbox(8)}>
649
+ <button
650
+ className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(50, 80, 60)}
651
+ onClick={() => {
652
+ this.runControllerAction(() => controller.discardPendingServiceConfig.promise(selectedServiceId));
653
+ }}>
654
+ Discard Pending Config
655
+ </button>
656
+ </div>
657
+ </div>}
658
+ </div>;
659
+ })()}
660
+
607
661
  {this.state.saveError && (
608
662
  <div className={css.pad2(12).bord2(0, 80, 50).hsl(0, 80, 95).colorhsl(0, 80, 30)}>
609
663
  ⚠️ Error: {this.state.saveError}
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MachineServiceController, ServiceConfig } from "../machineSchema";
3
+ import { DEFAULT_OVERLAP_TIME, MachineServiceController, ServiceConfig } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { t } from "../../2-proxy/schema2";
6
6
  import { Querysub } from "../../4-querysub/Querysub";
@@ -55,7 +55,7 @@ export class ServicesListPage extends qreact.Component {
55
55
  gitRef: gitInfo.latestRef,
56
56
  command: "ping 1.1.1.1",
57
57
  deploy: false,
58
- rollingWindow: 0,
58
+ overlapTime: DEFAULT_OVERLAP_TIME,
59
59
  },
60
60
  info: {
61
61
  title: `New Service created at ${formatNiceDateTime(Date.now())}`,
@@ -138,7 +138,7 @@ export class ServicesListPage extends qreact.Component {
138
138
  </div>
139
139
  <div className={css.vbox(4)}>
140
140
  <div>Updated {formatDateJSX(config.info.lastUpdatedTime)} AGO</div>
141
- {config.parameters.rollingWindow && <div className={css.colorhsl(0, 50, 50).fontSize(14).fontWeight("bold")}>Rolling window: {formatTime(config.parameters.rollingWindow)}</div> || undefined}
141
+ {config.parameters.overlapTime && <div className={css.colorhsl(210, 50, 50).fontSize(14).fontWeight("bold")}>Switchover overlap: {formatTime(config.parameters.overlapTime)}</div> || undefined}
142
142
  </div>
143
143
  </div>
144
144
  </Anchor>