querysub 0.259.0 → 0.261.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 (31) hide show
  1. package/.cursorrules +2 -0
  2. package/package.json +1 -1
  3. package/src/-e-certs/EdgeCertController.ts +1 -1
  4. package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
  5. package/src/3-path-functions/PathFunctionHelpers.ts +5 -6
  6. package/src/3-path-functions/PathFunctionRunner.ts +10 -3
  7. package/src/3-path-functions/PathFunctionRunnerMain.ts +2 -4
  8. package/src/4-deploy/deployFunctions.ts +82 -0
  9. package/src/4-deploy/deployGetFunctionsInner.ts +94 -0
  10. package/src/4-deploy/deployMain.ts +9 -109
  11. package/src/4-deploy/edgeBootstrap.ts +3 -0
  12. package/src/4-deploy/edgeClientWatcher.tsx +4 -0
  13. package/src/4-deploy/edgeNodes.ts +7 -1
  14. package/src/4-querysub/Querysub.ts +45 -6
  15. package/src/deployManager/MachinesPage.tsx +3 -0
  16. package/src/deployManager/components/DeployPage.tsx +385 -0
  17. package/src/deployManager/components/DeployProgressView.tsx +135 -0
  18. package/src/deployManager/components/MachinesListPage.tsx +10 -9
  19. package/src/deployManager/components/ServiceDetailPage.tsx +2 -1
  20. package/src/deployManager/components/ServicesListPage.tsx +2 -2
  21. package/src/deployManager/components/deployButtons.tsx +3 -3
  22. package/src/deployManager/machineApplyMainCode.ts +1 -0
  23. package/src/deployManager/machineController.ts +1 -0
  24. package/src/deployManager/machineSchema.ts +77 -3
  25. package/src/deployManager/spec.txt +22 -17
  26. package/src/deployManager/urlParams.ts +1 -1
  27. package/src/diagnostics/NodeViewer.tsx +17 -2
  28. package/src/library-components/ATag.tsx +14 -9
  29. package/src/misc/formatJSX.tsx +1 -1
  30. package/src/misc.ts +8 -0
  31. package/src/server.ts +10 -10
package/.cursorrules CHANGED
@@ -49,6 +49,8 @@ Don't use switch statements. Use if statements instead.
49
49
 
50
50
  Don't use ! when accessing a value from a map. Use the get / if undefined initialize and set, and then use style. It's faster, and more type safe.
51
51
 
52
+ NEVER use the non-null assertion operator. Null check correctly, using const if required to preserve the assertion.
53
+
52
54
  Sort with this function, which takes a single function to map each object to a sortable value
53
55
  import { sort } from "socket-function/src/misc";
54
56
  export function sort<T>(arr: T[], sortKey: (obj: T) => unknown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.259.0",
3
+ "version": "0.261.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",
@@ -31,7 +31,7 @@ import { shutdown } from "../diagnostics/periodic";
31
31
 
32
32
  let publicPort = -1;
33
33
 
34
- const getHostedIP = lazy(async () => {
34
+ export const getHostedIP = lazy(async () => {
35
35
  if (!isPublic()) {
36
36
  return "127.0.0.1";
37
37
  }
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { getArchives } from "../-a-archives/archives";
3
- import { getDomain, isDevDebugbreak, isNoNetwork } from "../config";
3
+ import { getDomain, isDevDebugbreak, isNoNetwork, isPublic } from "../config";
4
4
  import { measureBlock } from "socket-function/src/profiling/measure";
5
5
  import { isNode, sha256Hash, throttleFunction, timeInMinute, timeInSecond } from "socket-function/src/misc";
6
6
  import { errorToUndefinedSilent, ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../errors";
@@ -178,8 +178,8 @@ function setNodeIds(nodeIds: string[]) {
178
178
  nodeIds = nodeIds.filter(x => x !== SPECIAL_NODE_ID_FOR_UNMOUNTED_NODE);
179
179
 
180
180
  diskLog("setNodeIds", { nodeIds });
181
- // Also try all localhost ports, if we are in dev mode
182
- if (isNode() && isDevDebugbreak()) {
181
+ // Also try all localhost ports, if we are developing and not in public mode
182
+ if (isNode() && !isPublic() && isDevDebugbreak()) {
183
183
  let ports = new Set(nodeIds.map(nodeId => decodeNodeId(nodeId)?.port).filter(isDefined));
184
184
  for (let port of ports) {
185
185
  let localNodeId = getNodeId("127-0-0-1." + getDomain(), port);
@@ -20,7 +20,6 @@ import { interceptCalls } from "../-0-hooks/hooks";
20
20
  // NOTE: We could deploy single functions, but... we will almost always be updating all functions at
21
21
  // once, because keeping everything on the same git hash reduces a lot of potential bugs.
22
22
  export async function replaceFunctions(config: {
23
- domainName: string;
24
23
  functions: FunctionSpec[];
25
24
  }) {
26
25
  await proxyWatcher.commitFunction({
@@ -30,12 +29,12 @@ export async function replaceFunctions(config: {
30
29
  return `${func.DomainName}:${func.FilePath}:${func.ModuleId}:${func.FunctionId}`;
31
30
  }
32
31
 
33
- let { domainName, functions } = config;
34
- for (let fnc of functions) {
35
- if (fnc.DomainName !== domainName) {
36
- throw new Error(`Tried to deploy function ${fnc.FunctionId} is not in domain ${domainName}, was in ${fnc.DomainName}`);
37
- }
32
+ let { functions } = config;
33
+ let domainsNames = new Set(functions.map(x => x.DomainName));
34
+ if (domainsNames.size !== 1) {
35
+ throw new Error(`Tried to deploy functions with multiple domains, ${JSON.stringify(domainsNames)}`);
38
36
  }
37
+ let domainName = Array.from(domainsNames)[0];
39
38
 
40
39
  let base = functionSchema()[domainName].PathFunctionRunner;
41
40
  let previousFunctions = Object.values(base).flatMap(x => Object.values(x.Sources)).filter(isDefined);
@@ -24,6 +24,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
24
24
  import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
25
25
  import { getDomain, isLocal } from "../config";
26
26
  import { getGitRefSync, getGitURLSync } from "../4-deploy/git";
27
+ import { DeployProgress } from "../4-deploy/deployFunctions";
27
28
 
28
29
  export const functionSchema = rawSchema<{
29
30
  [domainName: string]: {
@@ -683,12 +684,18 @@ export class PathFunctionRunner {
683
684
  }
684
685
  }
685
686
 
686
- export async function preloadFunctions(specs: FunctionSpec[]) {
687
+ export async function preloadFunctions(specs: FunctionSpec[], progress?: DeployProgress) {
688
+ progress?.({ section: "Finding FunctionPreloadControllers", progress: 0 });
687
689
  let nodeIds = await getControllerNodeIdList(FunctionPreloadController);
688
- await Promise.allSettled(nodeIds.map(async nodeId => {
689
- let controller = FunctionPreloadController.nodes[nodeId.nodeId];
690
+ progress?.({ section: "Finding FunctionPreloadControllers", progress: 1 });
691
+ await Promise.allSettled(nodeIds.map(async nodeObj => {
692
+ let nodeId = nodeObj.nodeId;
693
+ let controller = FunctionPreloadController.nodes[nodeId];
694
+ let section = `${nodeObj.entryPoint}:${nodeId}|Preloading Functions`;
695
+ progress?.({ section, progress: 0 });
690
696
  console.log(blue(`Preloading functions on ${String(nodeId)}`));
691
697
  await errorToUndefined(controller.preloadFunctions(specs));
698
+ progress?.({ section, progress: 1 });
692
699
  console.log(blue(`Finished preloading functions on ${String(nodeId)}`));
693
700
  }));
694
701
  }
@@ -19,10 +19,10 @@ import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
19
19
  import { PermissionsCheck } from "../4-querysub/permissions";
20
20
  import { timeInMinute } from "socket-function/src/misc";
21
21
  import { getDomain, isLocal, isPublic } from "../config";
22
- import { publishMachineARecords } from "../-e-certs/EdgeCertController";
23
22
  import { green, magenta } from "socket-function/src/formatting/logColors";
24
23
  import { parseFilterSelector } from "../misc/filterable";
25
24
  import path from "path";
25
+ import { Querysub } from "../4-querysub/QuerysubController";
26
26
 
27
27
  async function main() {
28
28
  Error.stackTraceLimit = 20;
@@ -37,9 +37,7 @@ async function main() {
37
37
  PathFunctionRunner.DEBUG_CALLS = true;
38
38
  // debugCoreMode();
39
39
 
40
- let keyPair = await getThreadKeyCert();
41
- await SocketFunction.mount({ port: 0, ...keyPair, public: isPublic() });
42
- await publishMachineARecords();
40
+ await Querysub.hostService("PathFunctionRunnerMain");
43
41
 
44
42
  // Use a fairly high stick time (the default is 10s), because having wait to sync data is very slow,
45
43
  // and the function runner SHOULD have more memory than the clients, and much faster network speeds
@@ -0,0 +1,82 @@
1
+ import { magenta } from "socket-function/src/formatting/logColors";
2
+ import { FunctionSpec, preloadFunctions } from "../3-path-functions/PathFunctionRunner";
3
+ import { Querysub } from "../4-querysub/QuerysubController";
4
+ import { errorify } from "../errors";
5
+ import { runPromise } from "../functional/runCommand";
6
+ import path from "path";
7
+ import { preloadUI } from "./edgeNodes";
8
+ import { proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
9
+ import { replaceFunctions } from "../3-path-functions/PathFunctionHelpers";
10
+ import { setLiveDeployedHash } from "./deploySchema";
11
+
12
+ export async function deployGetFunctions(): Promise<FunctionSpec[]> {
13
+ let innerPath = path.resolve(__dirname + "/deployGetFunctionsInner.ts").replaceAll("\\", "/");
14
+ let result = await runPromise(`yarn typenode ${innerPath}`);
15
+ let functionSpecs: FunctionSpec[] = [];
16
+ for (let line of result.split("\n")) {
17
+ if (line.startsWith("ERROR:")) {
18
+ throw errorify(line.slice("ERROR:".length));
19
+ }
20
+ if (line.startsWith("FUNCTION_SPEC:")) {
21
+ let spec = JSON.parse(line.slice("FUNCTION_SPEC:".length));
22
+ functionSpecs.push(spec);
23
+ }
24
+ }
25
+ return functionSpecs;
26
+ }
27
+
28
+ //todonext;
29
+ // Ugh... some kind of throttling? Nah...
30
+ // So... progress sections, just... start and stop?
31
+ export type DeployProgress = {
32
+ (config: { section: string; progress: number; }): void;
33
+ };
34
+
35
+ export async function deployFunctions(config: {
36
+ functionSpecs: FunctionSpec[];
37
+ notifyRefreshDelay: number;
38
+ deployOnlyCode?: boolean;
39
+ deployOnlyUI?: boolean;
40
+ progress?: DeployProgress;
41
+ }) {
42
+ const { functionSpecs, notifyRefreshDelay, deployOnlyCode, deployOnlyUI } = config;
43
+ let gitRefs = new Set(functionSpecs.map(x => x.gitRef));
44
+ if (gitRefs.size !== 1) {
45
+ throw new Error(`Tried to deploy functions with multiple git refs, ${JSON.stringify(gitRefs)}`);
46
+ }
47
+ let gitRef = Array.from(gitRefs)[0];
48
+ const currentFunctions = functionSpecs;
49
+
50
+ console.log();
51
+ console.log();
52
+ console.log(magenta(`Preloading ${currentFunctions.length} functions & UI`));
53
+ await Promise.all([
54
+ preloadFunctions(currentFunctions, config.progress),
55
+ preloadUI(gitRef, config.progress),
56
+ ]);
57
+
58
+ console.log();
59
+ console.log();
60
+ console.log(magenta(`Deploying ${currentFunctions.length} functions`));
61
+
62
+ let refreshThresholdTime = Date.now() + notifyRefreshDelay;
63
+
64
+ config.progress?.({ section: "Deploying Functions", progress: 0 });
65
+ await proxyWatcher.commitFunction({
66
+ debugName: "setLiveDeployedHash",
67
+ inlineNestedWatchers: true,
68
+ watchFunction: () => {
69
+ if (deployOnlyUI) {
70
+ void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
71
+ } else if (deployOnlyCode) {
72
+ void replaceFunctions({ functions: currentFunctions, });
73
+ } else {
74
+ void replaceFunctions({ functions: currentFunctions, });
75
+ void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
76
+ }
77
+ },
78
+ });
79
+ config.progress?.({ section: "Deploying Functions", progress: 1 });
80
+
81
+ console.log(magenta(`FINISHED DEPLOY (${gitRef})`));
82
+ }
@@ -0,0 +1,94 @@
1
+ import path from "path";
2
+ import { deployBlock } from "./deployBlock";
3
+ import { SocketFunction } from "socket-function/SocketFunction";
4
+ import { getThreadKeyCert } from "../-a-auth/certs";
5
+ import { getDomain, isPublic } from "../config";
6
+ import { LOCAL_DOMAIN } from "../0-path-value-core/PathController";
7
+ import { getSchemaObject, getModuleRelativePath, PERMISSIONS_FUNCTION_ID, getExportPath } from "../3-path-functions/syncSchema";
8
+ import { getPathStr2 } from "../path";
9
+ import { FunctionSpec } from "../3-path-functions/PathFunctionRunner";
10
+ import fs from "fs";
11
+ import { getGitRefLive, getGitURLLive } from "./git";
12
+
13
+ async function main() {
14
+ let domainName = getDomain();
15
+ if (!domainName) {
16
+ throw new Error(`No domain found, set in querysub.json { domain: "example.com" }, or in --domain "example.com"`);
17
+ }
18
+
19
+ await deployBlock();
20
+
21
+ // Mount, so we can write directly to the database
22
+ await SocketFunction.mount({ port: 0, ...await getThreadKeyCert(), public: isPublic() });
23
+
24
+ let folderRoot = path.resolve(".").replaceAll("\\", "/");
25
+ const deployPath = path.resolve("./deploy.ts");
26
+ await import(deployPath);
27
+
28
+ const querysubRoot = path.resolve(__dirname + "/../").replaceAll("\\", "/");
29
+
30
+ let gitDir = folderRoot;
31
+ if (!fs.existsSync(gitDir + "/.git")) {
32
+ throw new Error(`No .git folder found at ${JSON.stringify(gitDir + "/.git")}`);
33
+ }
34
+ let gitURL = await getGitURLLive(gitDir);
35
+ if (!gitURL) {
36
+ throw new Error(`Git repo has no remote url, at ${JSON.stringify(gitDir + "/.git")}`);
37
+ }
38
+ let gitRef = await getGitRefLive(gitDir);
39
+
40
+ let usedModules = new Map<string, string>();
41
+ for (let module of Object.values(require.cache)) {
42
+ if (!module) continue;
43
+
44
+ // NOTE: We don't want dependencies to deploy schemas. This would result in using an
45
+ // API also deploying it.
46
+ // - Also, this add more security, by preventing 3rd party libraries from secretly adding apis
47
+ // (maybe even just innocently, for diagnostics), which might have vulnerabilities.
48
+ let isAllowedToDeploy = (
49
+ !module.filename.includes("node_modules")
50
+ && module.filename.startsWith(folderRoot)
51
+ );
52
+
53
+ // We DO allow querysub to deploy, so we can deploy diagnostic functions, such as
54
+ // for the managment interfaces. It only works if the user opts in (by calling
55
+ // registerManagementPages2), so it is actually quite safe to do this.
56
+ if (module.filename.startsWith(querysubRoot)) {
57
+ isAllowedToDeploy = true;
58
+ }
59
+
60
+ if (!isAllowedToDeploy) continue;
61
+ let schema = getSchemaObject(module);
62
+ if (!schema) continue;
63
+ // If it's local, we don't need to deploy it.
64
+ // (And shouldn't?, as we don't want to run local functions by request?)
65
+ if (schema.domainName === LOCAL_DOMAIN) continue;
66
+
67
+ let filePath = getModuleRelativePath(module);
68
+
69
+ let moduleId = getPathStr2(domainName, filePath);
70
+ let otherUsed = usedModules.get(moduleId);
71
+ if (otherUsed) {
72
+ throw new Error(`Overlapping module ids for ${domainName}.${filePath}, at ${otherUsed} and ${filePath}`);
73
+ }
74
+ usedModules.set(moduleId, filePath);
75
+
76
+ const functionIds = Object.keys(schema.rawFunctions);
77
+ functionIds.push(PERMISSIONS_FUNCTION_ID);
78
+ for (let functionId of functionIds) {
79
+ const exportPathStr = getExportPath(functionId);
80
+ let spec: FunctionSpec = {
81
+ DomainName: domainName,
82
+ ModuleId: schema.moduleId,
83
+ FilePath: filePath,
84
+ FunctionId: functionId,
85
+ exportPathStr,
86
+ gitURL,
87
+ gitRef,
88
+ };
89
+ console.log(`FUNCTION_SPEC:${JSON.stringify(spec)}`);
90
+ }
91
+ }
92
+ }
93
+
94
+ main().catch(e => console.error("ERROR:" + String(e?.stack || e).replaceAll("\n", "\\n"))).finally(() => process.exit(0));
@@ -25,6 +25,8 @@ import { preloadUI } from "./edgeNodes";
25
25
  import { shutdown } from "../diagnostics/periodic";
26
26
  import { delay } from "socket-function/src/batching";
27
27
  import { waitForImportBlockers } from "../3-path-functions/pathFunctionLoader";
28
+ import { deployFunctions, deployGetFunctions } from "./deployFunctions";
29
+ import { Querysub } from "../4-querysub/QuerysubController";
28
30
 
29
31
  let yargObj = yargs(process.argv)
30
32
  .option("deployonlycode", { type: "boolean", desc: "Only deploy code, not ui" })
@@ -41,88 +43,10 @@ export async function deployMain() {
41
43
  //quietCoreMode();
42
44
  //ClientWatcher.DEBUG_READS = true;
43
45
  //ClientWatcher.DEBUG_WRITES = true;
44
- let domainName = getDomain();
45
- if (!domainName) {
46
- throw new Error(`No domain found, set in querysub.json { domain: "example.com" }, or in --domain "example.com"`);
47
- }
48
- await deployBlock();
49
-
50
- // Mount, so we can write directly to the database
51
- await SocketFunction.mount({ port: 0, ...await getThreadKeyCert(), public: isPublic() });
52
-
53
- let folderRoot = path.resolve(".").replaceAll("\\", "/");
54
- const deployPath = path.resolve("./deploy.ts");
55
- require(deployPath);
56
-
57
- // Wait for Promise.resolve imports to import
58
- await waitForImportBlockers();
59
-
60
- const srcRoot = path.resolve(__dirname + "/../").replaceAll("\\", "/");
61
-
62
- let currentFunctions: FunctionSpec[] = [];
63
-
64
- let gitDir = folderRoot;
65
- if (!fs.existsSync(gitDir + "/.git")) {
66
- throw new Error(`No .git folder found at ${JSON.stringify(gitDir + "/.git")}`);
67
- }
68
- let gitURL = await getGitURLLive(gitDir);
69
- if (!gitURL) {
70
- throw new Error(`Git repo has no remote url, at ${JSON.stringify(gitDir + "/.git")}`);
71
- }
72
- let gitRef = await getGitRefLive(gitDir);
73
-
74
- let usedModules = new Map<string, string>();
75
46
 
76
- for (let module of Object.values(require.cache)) {
77
- if (!module) continue;
47
+ await Querysub.hostService("deployMain");
78
48
 
79
- // NOTE: We don't want dependencies to deploy schemas. This would result in using an
80
- // API also deploying it.
81
- // - Also, this add more security, by preventing 3rd party libraries from adding apis
82
- // (maybe even just innocent, for diagnostics), which might have vulnerabilities.
83
- let isAllowedToDeploy = (
84
- !module.filename.includes("node_modules")
85
- && module.filename.startsWith(folderRoot)
86
- );
87
-
88
- // We DO allow shard to deploy, so we can deploy diagnostic functions, such as
89
- // for the managment interfaces. It only works if the user opts in (by calling
90
- // registerManagementPages2), so it is actually quite safe to do this.
91
- if (module.filename.startsWith(srcRoot)) {
92
- isAllowedToDeploy = true;
93
- }
94
-
95
- if (!isAllowedToDeploy) continue;
96
- let schema = getSchemaObject(module);
97
- if (!schema) continue;
98
- // If it's local, we don't need to deploy it.
99
- // (And shouldn't?, as we don't want to run local functions by request?)
100
- if (schema.domainName === LOCAL_DOMAIN) continue;
101
-
102
- let filePath = getModuleRelativePath(module);
103
-
104
- let moduleId = getPathStr2(domainName, filePath);
105
- let otherUsed = usedModules.get(moduleId);
106
- if (otherUsed) {
107
- throw new Error(`Overlapping module ids for ${domainName}.${filePath}, at ${otherUsed} and ${filePath}`);
108
- }
109
- usedModules.set(moduleId, filePath);
110
-
111
- const functionIds = Object.keys(schema.rawFunctions);
112
- functionIds.push(PERMISSIONS_FUNCTION_ID);
113
- for (let functionId of functionIds) {
114
- const exportPathStr = getExportPath(functionId);
115
- currentFunctions.push({
116
- DomainName: domainName,
117
- ModuleId: schema.moduleId,
118
- FilePath: filePath,
119
- FunctionId: functionId,
120
- exportPathStr,
121
- gitURL,
122
- gitRef,
123
- });
124
- }
125
- }
49
+ const currentFunctions = await deployGetFunctions();
126
50
  for (let fnc of currentFunctions) {
127
51
  console.log(blue(`${fnc.DomainName}.${fnc.ModuleId}.${fnc.FunctionId}`));
128
52
  }
@@ -131,37 +55,13 @@ export async function deployMain() {
131
55
  return;
132
56
  }
133
57
 
134
- console.log();
135
- console.log();
136
- console.log(magenta(`Preloading ${currentFunctions.length} functions & UI`));
137
- await Promise.all([
138
- preloadFunctions(currentFunctions),
139
- preloadUI(gitRef),
140
- ]);
141
-
142
- console.log();
143
- console.log();
144
- console.log(magenta(`Deploying ${currentFunctions.length} functions`));
145
-
146
- let refreshThresholdTime = Date.now() + yargObj.notifyrefreshdelay;
147
-
148
- await proxyWatcher.commitFunction({
149
- debugName: "setLiveDeployedHash",
150
- inlineNestedWatchers: true,
151
- watchFunction: () => {
152
- if (yargObj.deployonlycode) {
153
- void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
154
- } else if (yargObj.deployonlyui) {
155
- void replaceFunctions({ domainName, functions: currentFunctions, });
156
- } else {
157
- void replaceFunctions({ domainName, functions: currentFunctions, });
158
- void setLiveDeployedHash({ hash: gitRef, refreshThresholdTime, });
159
- }
160
- },
58
+ await deployFunctions({
59
+ functionSpecs: currentFunctions,
60
+ notifyRefreshDelay: yargObj.notifyrefreshdelay,
61
+ deployOnlyCode: yargObj.deployonlycode,
62
+ deployOnlyUI: yargObj.deployonlyui,
161
63
  });
162
64
 
163
- console.log(magenta(`FINISHED DEPLOY (${gitRef})`));
164
-
165
65
  await shutdown();
166
66
  }
167
67
 
@@ -429,6 +429,9 @@ async function edgeNodeFunction(config: {
429
429
  let host = edgeNodeConfig.host;
430
430
  if (!host.endsWith("/")) host += "/";
431
431
  for (let entryPath of edgeNodeConfig.entryPaths) {
432
+ if (entryPath.startsWith("/")) {
433
+ entryPath = entryPath.slice("/".length);
434
+ }
432
435
  await require("https://" + host + entryPath);
433
436
  }
434
437
  }
@@ -24,6 +24,9 @@ export function startEdgeNotifier() {
24
24
  if (liveHash === lastHashServer) return;
25
25
  let refreshThresholdTime = atomicObjectRead(deploySchema()[getDomain()].deploy.live.refreshThresholdTime) || timeInMinute;
26
26
  lastHashServer = liveHash;
27
+ if (!curHash) {
28
+ curHash = liveHash;
29
+ }
27
30
  void notifyClients(liveHash, refreshThresholdTime);
28
31
  });
29
32
  }
@@ -52,6 +55,7 @@ function onLiveHashChange(liveHash: string, refreshThresholdTime: number) {
52
55
  let prevHash = curHash;
53
56
  let notifyIntervals = [0, 0.1, 0.5, 1];
54
57
  console.log(blue(`Client liveHash changed ${liveHash}, prev hash: ${prevHash}`));
58
+ // If we are replacing an already existing notification, don't show immediately
55
59
  let skipFirst = false;
56
60
  if (currentNotification) {
57
61
  currentNotification.close();
@@ -25,6 +25,7 @@ import { Querysub } from "../4-querysub/QuerysubController";
25
25
  import { onEdgeNodesChanged } from "./edgeBootstrap";
26
26
  import { startEdgeNotifier } from "./edgeClientWatcher";
27
27
  import { getGitRefLive, getGitURLLive } from "./git";
28
+ import { DeployProgress } from "./deployFunctions";
28
29
 
29
30
  const UPDATE_POLL_INTERVAL = timeInMinute * 15;
30
31
  const DEAD_NODE_COUNT_THRESHOLD = 15;
@@ -259,12 +260,17 @@ async function updateEdgeNodesFile() {
259
260
  await edgeNodeStorage.set(edgeNodeIndexFile, Buffer.from(JSON.stringify(newEdgeNodeIndex)));
260
261
  }
261
262
 
262
- export async function preloadUI(hash: string) {
263
+ export async function preloadUI(hash: string, progress?: DeployProgress) {
264
+ progress?.({ section: "Finding EdgeNodeControllers", progress: 0 });
263
265
  let nodeIds = await getControllerNodeIdList(EdgeNodeController);
266
+ progress?.({ section: "Finding EdgeNodeControllers", progress: 1 });
264
267
  await Promise.allSettled(nodeIds.map(async nodeId => {
265
268
  let controller = EdgeNodeController.nodes[nodeId.nodeId];
269
+ let section = `${nodeId.entryPoint}:${nodeId.nodeId}|Preloading UI`;
270
+ progress?.({ section, progress: 0 });
266
271
  console.log(blue(`Preloading UI on ${String(nodeId)}, hash: ${hash}`));
267
272
  await errorToUndefined(controller.preloadUI({ hash }));
273
+ progress?.({ section, progress: 1 });
268
274
  console.log(blue(`Finished preloading UI on ${String(nodeId)}`));
269
275
  }));
270
276
  }
@@ -6,14 +6,14 @@ import "../inject";
6
6
  import { shimDateNow } from "socket-function/time/trueTimeShim";
7
7
  shimDateNow();
8
8
 
9
- import { isNode, isNodeTrue, timeInMinute } from "socket-function/src/misc";
9
+ import { isNode, isNodeTrue, timeInMinute, timeInSecond, timeoutToUndefined } from "socket-function/src/misc";
10
10
 
11
11
  import { SocketFunction } from "socket-function/SocketFunction";
12
12
  import { isHotReloading, watchFilesAndTriggerHotReloading } from "socket-function/hot/HotReloadController";
13
13
  import { RequireController, setRequireBootRequire } from "socket-function/require/RequireController";
14
14
  import { cache, cacheLimited, lazy } from "socket-function/src/caching";
15
15
  import { getOwnMachineId, getThreadKeyCert, verifyMachineIdForPublicKey } from "../-a-auth/certs";
16
- import { getSNICerts, publishMachineARecords } from "../-e-certs/EdgeCertController";
16
+ import { getHostedIP, getSNICerts, publishMachineARecords } from "../-e-certs/EdgeCertController";
17
17
  import { LOCAL_DOMAIN, nodePathAuthority } from "../0-path-value-core/NodePathAuthorities";
18
18
  import { debugCoreMode, registerGetCompressNetwork, encodeParentFilter, registerGetCompressDisk, authorityStorage } from "../0-path-value-core/pathValueCore";
19
19
  import { clientWatcher, ClientWatcher } from "../1-path-client/pathValueClientWatcher";
@@ -67,6 +67,8 @@ let yargObj = parseArgsFactory()
67
67
  .option("verbosenetwork", { type: "boolean", desc: "Log all network activity" })
68
68
  .option("verboseframework", { type: "boolean", desc: "Log internal SocketFunction framework" })
69
69
  .option("nodelay", { type: "boolean", desc: "Don't delay committing functions, even ones that are marked to be delayed." })
70
+ // 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?
71
+ .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." })
70
72
  .argv
71
73
  ;
72
74
  setImmediate(() => {
@@ -765,6 +767,13 @@ export class Querysub {
765
767
  allowHostnames.push(domain);
766
768
  }
767
769
  allowHostnames.push("127-0-0-1." + getDomain());
770
+
771
+ if (yargObj.bootstraponly && isPublic()) {
772
+ if (config.port !== 443) {
773
+ throw new Error(`--bootstraponly requires you to set port 443. There can only be one bootstrap node per server.`);
774
+ }
775
+ }
776
+
768
777
  await SocketFunction.mount({
769
778
  public: isPublic(),
770
779
  port: config.port,
@@ -780,10 +789,38 @@ export class Querysub {
780
789
 
781
790
  let { ip, ipDomain } = await publishMachineARecords();
782
791
 
783
- await registerEdgeNode({
784
- host: ipDomain + ":" + config.port,
785
- entryPaths,
786
- });
792
+ if (!yargObj.bootstraponly) {
793
+ await registerEdgeNode({
794
+ host: ipDomain + ":" + config.port,
795
+ entryPaths,
796
+ });
797
+ } else {
798
+ // bootstraponly mode. Setup cloudflare proxy. If they are developing (localhost), we can't proxy, so don't (but still setup domain). Using the cloudflare proxy should prevent the site from entirely breaking if the bootstrapper goes down.
799
+ let ip = await getHostedIP();
800
+ let existingRecords = await getRecords("A", getDomain());
801
+ if (ip !== "127.0.0.1") {
802
+ let validRecords: string[] = [];
803
+ await Promise.all(existingRecords.map(async (record) => {
804
+ let isListening = await timeoutToUndefined(timeInSecond * 10, testTCPIsListening(record, 443));
805
+ if (isListening) {
806
+ validRecords.push(record);
807
+ }
808
+ }));
809
+ // It's hard to manage multiple bootstrappers, so... just don't.
810
+ if (validRecords.length > 0) {
811
+ console.error(`Found existing bootstrapper at ${JSON.stringify(validRecords)}, so why are we even running? Terminating shortly`);
812
+ // Give logs time to write
813
+ await shutdown();
814
+ }
815
+ await setRecord("A", getDomain(), ip, "proxied");
816
+ } else {
817
+ if (existingRecords.length === 0) {
818
+ await setRecord("A", ip, getDomain());
819
+ } else {
820
+ console.log(`Not clobbering existing A record for ${getDomain()} of ${JSON.stringify(existingRecords)}`);
821
+ }
822
+ }
823
+ }
787
824
  }
788
825
  private static async addSourceMapCheck(config: {
789
826
  sourceCheck?: MachineSourceCheck;
@@ -1279,4 +1316,6 @@ import { getCountPerPaint } from "../functional/onNextPaint";
1279
1316
  import { addEpsilons } from "../bits";
1280
1317
  import { blue } from "socket-function/src/formatting/logColors";
1281
1318
  import { MachineController } from "../deployManager/machineController";
1319
+ import { getRecords, setRecord } from "../-b-authorities/dnsAuthority";
1320
+ import { testTCPIsListening } from "socket-function/src/networking";
1282
1321
 
@@ -8,6 +8,7 @@ import { MachinesListPage } from "./components/MachinesListPage";
8
8
  import { ServiceDetailPage } from "./components/ServiceDetailPage";
9
9
  import { MachineDetailPage } from "./components/MachineDetailPage";
10
10
  import { Anchor } from "../library-components/ATag";
11
+ import { DeployPage } from "./components/DeployPage";
11
12
 
12
13
  export class MachinesPage extends qreact.Component {
13
14
  private renderTabs() {
@@ -18,6 +19,7 @@ export class MachinesPage extends qreact.Component {
18
19
  {[
19
20
  { key: "machines", label: "Machines", otherKeys: ["machine-detail"] },
20
21
  { key: "services", label: "Services", otherKeys: ["service-detail"] },
22
+ { key: "deploy", label: "Deploy", otherKeys: [] },
21
23
  ].map(tab => {
22
24
  let isActive = currentViewParam.value === tab.key || tab.otherKeys.includes(currentViewParam.value);
23
25
  return <Anchor noStyles key={tab.key}
@@ -44,6 +46,7 @@ export class MachinesPage extends qreact.Component {
44
46
  {currentViewParam.value === "machines" && <MachinesListPage />}
45
47
  {currentViewParam.value === "service-detail" && <ServiceDetailPage />}
46
48
  {currentViewParam.value === "machine-detail" && <MachineDetailPage />}
49
+ {currentViewParam.value === "deploy" && <DeployPage />}
47
50
  </div>
48
51
  </div>;
49
52
  }