querysub 0.459.0 → 0.461.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 (35) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/package.json +2 -2
  3. package/src/-b-authorities/dnsAuthority.ts +23 -15
  4. package/src/-g-core-values/NodeCapabilities.ts +3 -0
  5. package/src/-h-path-value-serialize/PathValueSerializer.ts +11 -3
  6. package/src/0-path-value-core/PathRouter.ts +6 -0
  7. package/src/0-path-value-core/PathWatcher.ts +1 -1
  8. package/src/0-path-value-core/pathValueCore.ts +4 -7
  9. package/src/1-path-client/RemoteWatcher.ts +8 -3
  10. package/src/1-path-client/pathValueClientWatcher.ts +3 -0
  11. package/src/2-proxy/PathValueProxyWatcher.ts +1 -1
  12. package/src/2-proxy/TransactionDelayer.ts +1 -1
  13. package/src/3-path-functions/PathFunctionHelpers.ts +13 -8
  14. package/src/3-path-functions/PathFunctionRunner.ts +2 -0
  15. package/src/4-querysub/Querysub.ts +0 -1
  16. package/src/4-querysub/QuerysubController.ts +1 -7
  17. package/src/config.ts +9 -0
  18. package/src/config2.ts +7 -1
  19. package/src/deployManager/components/MachinePicker.tsx +40 -0
  20. package/src/deployManager/components/ServiceDetailPage.tsx +2 -5
  21. package/src/deployManager/components/ServicesListPage.tsx +2 -0
  22. package/src/deployManager/components/Tools.tsx +165 -0
  23. package/src/deployManager/setupMachineMain.ts +74 -23
  24. package/src/diagnostics/charts/Chart.tsx +240 -0
  25. package/src/diagnostics/grossStats/GrossStatsPage.tsx +48 -83
  26. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +22 -35
  27. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +39 -47
  28. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +3 -3
  29. package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +18 -3
  30. package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +1 -0
  31. package/src/diagnostics/managementPages.tsx +58 -58
  32. package/src/diagnostics/misc-pages/DNSPage.tsx +344 -0
  33. package/test.ts +46 -70
  34. package/src/diagnostics/AuditLogPage.tsx +0 -147
  35. package/src/diagnostics/NodeConnectionsPage.tsx +0 -167
@@ -23,7 +23,8 @@
23
23
  "mcp__node-debugger__resume",
24
24
  "mcp__node-debugger__listBreakpoints",
25
25
  "mcp__node-debugger__removeBreakpoint",
26
- "mcp__hottest__runTest"
26
+ "mcp__hottest__runTest",
27
+ "Bash(yarn test *)"
27
28
  ]
28
29
  }
29
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.459.0",
3
+ "version": "0.461.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",
@@ -66,7 +66,7 @@
66
66
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
67
67
  "pako": "^2.1.0",
68
68
  "peggy": "^5.0.6",
69
- "socket-function": "^1.1.33",
69
+ "socket-function": "^1.1.34",
70
70
  "terser": "^5.31.0",
71
71
  "typesafecss": "^0.29.0",
72
72
  "yaml": "^2.5.0",
@@ -1,7 +1,7 @@
1
1
  import os from "os";
2
2
  import * as fs from "fs";
3
3
  import { cache, lazy } from "socket-function/src/caching";
4
- import { isNode, isNodeTrue } from "socket-function/src/misc";
4
+ import { isNode, isNodeTrue, timeInDay } from "socket-function/src/misc";
5
5
  import { httpsRequest } from "../https";
6
6
  import { getStorageDir } from "../fs";
7
7
  import { SocketFunction } from "socket-function/SocketFunction";
@@ -17,6 +17,8 @@ const DNS_TTLSeconds = {
17
17
  "A": 60,
18
18
  };
19
19
 
20
+ const DNS_REFRESH_STALE_AFTER = timeInDay;
21
+
20
22
  export const hasDNSWritePermissions = lazy(async () => {
21
23
  if (!isNode()) return false;
22
24
  if (isClient()) return false;
@@ -53,6 +55,7 @@ export async function getRecordsRaw(type: string, key: string) {
53
55
  name: string;
54
56
  content: string;
55
57
  proxied: boolean;
58
+ modified_on: string;
56
59
  }[]>(`/zones/${zoneId}/dns_records`);
57
60
  return results.filter(x => x.type === type && x.name === key);
58
61
  }
@@ -86,7 +89,8 @@ export async function setRecord(type: string, key: string, value: string, proxie
86
89
  let prevValues = await getRecordsRaw(type, key);
87
90
  // NOTE: Apparently if we try to update by just changing proxied, cloudflare complains and
88
91
  // says "an identical record already exists", even though it doesn't, we changed the proxied value...
89
- if (prevValues.some(x => x.content === value)) return;
92
+ let alreadyExisted = prevValues.some(x => x.content === value);
93
+ if (alreadyExisted && Date.now() - new Date(prevValues.find(x => x.content === value)!.modified_on).getTime() < DNS_REFRESH_STALE_AFTER) return;
90
94
 
91
95
  console.log(magenta(`Removing previous records of ${type} for ${key} ${JSON.stringify(prevValues.map(x => x.content))}`));
92
96
  let didDeletions = false;
@@ -104,14 +108,15 @@ export async function setRecord(type: string, key: string, value: string, proxie
104
108
  ttl,
105
109
  proxied: proxied === "proxied",
106
110
  });
107
- // NOTE: Apparently... even if the record didn't exist, we still have to wait...
108
- console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
109
- for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
110
- await delay(1000);
111
- console.log(`${ttlLeft} seconds left...`);
111
+ if (!alreadyExisted) {
112
+ // NOTE: Apparently... even if the record didn't exist, we still have to wait...
113
+ console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
114
+ for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
115
+ await delay(1000);
116
+ console.log(`${ttlLeft} seconds left...`);
117
+ }
118
+ console.log(`Done waiting for DNS to update.`);
112
119
  }
113
- console.log(`Done waiting for DNS to update.`);
114
-
115
120
  }
116
121
  /** Keeps existing records */
117
122
  export async function addRecord(type: string, key: string, value: string, proxied?: "proxied") {
@@ -120,7 +125,8 @@ export async function addRecord(type: string, key: string, value: string, proxie
120
125
  let prevValues = await getRecordsRaw(type, key);
121
126
  // NOTE: Apparently if we try to update by just changing proxied, cloudflare complains and
122
127
  // says "an identical record already exists", even though it doesn't, we changed the proxied value...
123
- if (prevValues.some(x => x.content === value)) return;
128
+ let alreadyExisted = prevValues.some(x => x.content === value);
129
+ if (alreadyExisted && Date.now() - new Date(prevValues.find(x => x.content === value)!.modified_on).getTime() < DNS_REFRESH_STALE_AFTER) return;
124
130
  console.log(`Adding ${type} record for ${key} to ${value} (previously had ${JSON.stringify(prevValues.map(x => x.content))})`);
125
131
  const ttl = DNS_TTLSeconds[type as "A"] || 60;
126
132
  await cloudflarePOSTCall(`/zones/${zoneId}/dns_records`, {
@@ -130,10 +136,12 @@ export async function addRecord(type: string, key: string, value: string, proxie
130
136
  ttl,
131
137
  proxied: proxied === "proxied",
132
138
  });
133
- console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
134
- for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
135
- await delay(1000);
136
- console.log(`${ttlLeft} seconds left...`);
139
+ if (!alreadyExisted) {
140
+ console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
141
+ for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
142
+ await delay(1000);
143
+ console.log(`${ttlLeft} seconds left...`);
144
+ }
145
+ console.log(`Done waiting for DNS to update.`);
137
146
  }
138
- console.log(`Done waiting for DNS to update.`);
139
147
  }
@@ -22,6 +22,7 @@ import { sort } from "socket-function/src/misc";
22
22
  import { getPathStr2 } from "../path";
23
23
  import { PromiseObj } from "../promise";
24
24
  import { getTrueTimeOffset } from "socket-function/time/trueTimeShim";
25
+ import { getGitRefLive } from "../4-deploy/git";
25
26
  setImmediate(() => {
26
27
  import("../diagnostics/MachineThreadInfo");
27
28
  });
@@ -136,6 +137,7 @@ export type NodeMetadata = {
136
137
  authoritySpec: AuthoritySpec;
137
138
  exposedControllers: string[];
138
139
  trueTimeOffset: number;
140
+ gitRef: string;
139
141
  };
140
142
 
141
143
  class NodeCapabilitiesControllerBase {
@@ -149,6 +151,7 @@ class NodeCapabilitiesControllerBase {
149
151
  authoritySpec: authorityLookup.getOurSpec(),
150
152
  exposedControllers: Array.from(SocketFunction.exposedClasses),
151
153
  trueTimeOffset: getTrueTimeOffset(),
154
+ gitRef: await getGitRefLive(),
152
155
  };
153
156
  }
154
157
 
@@ -999,7 +999,9 @@ class PathValueSerializer {
999
999
  if (pathValue.isValueLazy) {
1000
1000
  let buffer = this.lazyValues.get(pathValue.value as {});
1001
1001
  if (!buffer) {
1002
- throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1002
+ debugbreak(2);
1003
+ debugger;
1004
+ throw new Error(`(getPathValue) Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1003
1005
  }
1004
1006
  let newValue = recursiveFreeze(cbor.decode(buffer));
1005
1007
  if (!noMutate && !pathValue.isValueLazy) {
@@ -1024,10 +1026,12 @@ class PathValueSerializer {
1024
1026
  if (pathValue.isValueLazy) {
1025
1027
  let buffer = this.lazyValues.get(pathValue.value as {});
1026
1028
  if (!buffer) {
1029
+ debugbreak(2);
1030
+ debugger;
1027
1031
  // NOTE: Did you pass a raw PathValue and then try to use PathValueSerializer with it?
1028
1032
  // - Instead you should pass a buffer serialized with pathValueSerializer.serialize and
1029
1033
  // deserialized with pathValueSerializer.deserialize.
1030
- throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1034
+ throw new Error(`(getPathValueOrBuffer) Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1031
1035
  }
1032
1036
  return buffer;
1033
1037
  }
@@ -1037,7 +1041,11 @@ class PathValueSerializer {
1037
1041
  private getBuffer(pathValue: PathValue): Buffer {
1038
1042
  if (pathValue.isValueLazy) {
1039
1043
  let buffer = this.lazyValues.get(pathValue.value as {});
1040
- if (!buffer) throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1044
+ if (!buffer) {
1045
+ debugbreak(2);
1046
+ debugger;
1047
+ throw new Error(`(getBuffer) Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1048
+ }
1041
1049
  return buffer;
1042
1050
  }
1043
1051
  return cborEncoder().encode(pathValue.value);
@@ -421,6 +421,12 @@ export class PathRouter {
421
421
  return this.matchesAuthoritySpec(ourSpec, path);
422
422
  }
423
423
 
424
+ public static isSelfParentAuthority(path: string): boolean {
425
+ let nodes = PathRouter.getChildReadNodes(path, { onlyOwnNodes: true });
426
+ // NOTE: I think we can just do the node's length is greater than or equal to zero. However, the every check isn't gonna hurt...
427
+ return nodes.nodes.length > 0 && nodes.nodes.every(x => isOwnNodeId(x.nodeId));
428
+ }
429
+
424
430
 
425
431
 
426
432
  // NOTE: This might return overlapping specs. Presently, this is only used when we're loading our initial data, so it's fine. However, if we use this for another purpose in the future, it might cause problems. So we might need to implement a different function for that theoretical future purpose.
@@ -192,7 +192,7 @@ class PathWatcher {
192
192
  if (newPathsWatched.size > 0 || newParentsWatched.size > 0) {
193
193
  if (!this.remoteWatchCallback) throw new Error("remoteWatchCallback not set by the pathWatcher");
194
194
  let newRemotePaths = Array.from(newPathsWatched).filter(x => !PathRouter.isSelfAuthority(x));
195
- let newRemoteParentPaths = Array.from(newParentsWatched).filter(x => !PathRouter.isSelfAuthority(x));
195
+ let newRemoteParentPaths = Array.from(newParentsWatched).filter(x => !PathRouter.isSelfParentAuthority(x));
196
196
  if (newRemotePaths.length > 0 || newRemoteParentPaths.length > 0) {
197
197
  this.remoteWatchCallback({
198
198
  paths: newRemotePaths,
@@ -667,13 +667,10 @@ class AuthorityPathValueStorage {
667
667
  }
668
668
 
669
669
  // Self authority check
670
- {
671
- let nodes = PathRouter.getChildReadNodes(originalPath, { onlyOwnNodes: true });
672
- if (nodes.nodes.length > 0 && nodes.nodes.every(x => isOwnNodeId(x.nodeId))) {
673
- // If we're the self authority of it, it's not going to change, so we can just cache it safely.
674
- this.parentsSynced.set(originalPath, true);
675
- return true;
676
- }
670
+ if (PathRouter.isSelfParentAuthority(originalPath)) {
671
+ // If we're the self authority of it, it's not going to change, so we can just cache it safely.
672
+ this.parentsSynced.set(originalPath, true);
673
+ return true;
677
674
  }
678
675
 
679
676
  return false;
@@ -4,7 +4,7 @@ import { batchFunction, runInSerial, runInfinitePoll } from "socket-function/src
4
4
  import { cache } from "socket-function/src/caching";
5
5
  import { isNode, sort } from "socket-function/src/misc";
6
6
  import { measureFnc, measureBlock } from "socket-function/src/profiling/measure";
7
- import { isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
7
+ import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
8
8
  import { PathValueController } from "../0-path-value-core/PathValueController";
9
9
  import { AuthoritySpec, PathRouter } from "../0-path-value-core/PathRouter";
10
10
  import { WatchConfig, authorityStorage } from "../0-path-value-core/pathValueCore";
@@ -164,6 +164,9 @@ export class RemoteWatcher {
164
164
  // and the code calling this function isn't looking for that specific issue.
165
165
  return this.remoteWatchPaths.has(path) || !PathRouter.getReadyAuthority(path);
166
166
  }
167
+ public hasAnyDirectPathWatches(path: string) {
168
+ return this.remoteWatchPaths.has(path);
169
+ }
167
170
 
168
171
  /** @deprecated If you want to watch something, call pathWatcher.watchPath instead. */
169
172
  public watchLatest(config: WatchConfig & { debugName?: string }) {
@@ -687,8 +690,10 @@ registerGetSpecForChildPath(path => {
687
690
  let remoteWatchRange = remoteWatcher.getRemoteWatchParentRange(path);
688
691
  if (remoteWatchRange) return remoteWatchRange.authoritySpec;
689
692
 
690
- // NOTE: This fall through usually means that we received a value before we started watching it, which is weird. That really shouldn't happen often. It'll probably break things. We will probably ignore this value thinking it has a different hash than it does.
691
- console.warn(`We do not own a path, but also aren't watching it on a remote, but... receive the value? This is weird, and will probably break things (we will likely ignore this value / not trigger parent watches for it).`, { path });
693
+ if (!remoteWatcher.hasAnyDirectPathWatches(path)) {
694
+ // NOTE: This fall through usually means that we received a value before we started watching it, which is weird. That really shouldn't happen often. It'll probably break things. We will probably ignore this value thinking it has a different hash than it does.
695
+ console.warn(`We do not own a path, but also aren't watching it on a remote, but... receive the value? This is weird, and will probably break things (we will likely ignore this value / not trigger parent watches for it).`, { path });
696
+ }
692
697
  }
693
698
  return authorityLookup.getOurSpec();
694
699
  });
@@ -739,6 +739,9 @@ export class ClientWatcher {
739
739
  }
740
740
 
741
741
  if (!config.dryRun) {
742
+ for (let pv of pathValues) {
743
+ (globalThis as any).TEMP_TEST_LOG?.(pv.path, { kind: "setValues", writeTime, value: pv.value, locks: pv.locks });
744
+ }
742
745
  pathValueCommitter.commitValues(pathValues, config.noWritePrediction ? undefined : "predictWrites");
743
746
  }
744
747
 
@@ -1491,7 +1491,7 @@ export class PathValueProxyWatcher {
1491
1491
  }, 60000);
1492
1492
  }
1493
1493
  watcher.countSinceLastFullSync++;
1494
- if (watcher.countSinceLastFullSync > 10) {
1494
+ if (watcher.countSinceLastFullSync > 100) {
1495
1495
  console.warn(`Watcher ${watcher.debugName} has been unsynced for ${watcher.countSinceLastFullSync} times. This is fine, but maybe optimize it. Why is it cascading?`, { lastUnsyncedAccesses: watcher.lastUnsyncedAccesses, lastUnsyncedParentAccesses: watcher.lastUnsyncedParentAccesses }, watcher.options.watchFunction);
1496
1496
  }
1497
1497
  if (watcher.countSinceLastFullSync > 500) {
@@ -88,5 +88,5 @@ export function waitIfReceivedIncompleteTransaction(watcher: SyncWatcher) {
88
88
  // This really nicely both blocks it from finishing because of the waiting for the promise and also triggers it automatically when the delay finishes. HOWEVER, In practice, the promise should never be required to trigger it. It should trigger when we receive the missing parts of the transaction, which we will be, of course, watching as they're in the path values that we're accessing. The timeout is just in case something goes wrong and we incorrectly think we should receive the values, but we won't. That way, eventually, we do commit the value.
89
89
  // ALSO! Plus the hash I think only stores like 48 bits. So the chance of collision is somewhat high, especially if we're accessing thousands of paths. So sometimes this will trigger even though we're not missing any part just because we had a collision between the path hashes.
90
90
  proxyWatcher.triggerOnPromiseFinish(promise, { waitReason: "Missing transaction part" });
91
- console.warn(`(NOT an error, convert this to a warning after we finish testing). Waiting for missing transaction part ${newestTime.path} which was written at time ${newestTime.time} (now is ${now}). We have parts of this transaction, but we are missing this specific path.`);
91
+ console.warn(`Waiting for missing transaction part ${newestTime.path} which was written at time ${newestTime.time} (now is ${now}). We have parts of this transaction, but we are missing this specific path.`);
92
92
  }
@@ -19,7 +19,8 @@ import { interceptCalls } from "../-0-hooks/hooks";
19
19
  import { createRoutingOverrideKey } from "../0-path-value-core/PathRouterRouteOverride";
20
20
  import { PathRouter } from "../0-path-value-core/PathRouter";
21
21
  import { getPathFromProxy } from "../2-proxy/pathValueProxy";
22
- import { getDomain } from "../config";
22
+ import { getDomain, isPublic } from "../config";
23
+ import { getFncFilter, isServer } from "../config2";
23
24
 
24
25
  // NOTE: We could deploy single functions, but... we will almost always be updating all functions at
25
26
  // once, because keeping everything on the same git hash reduces a lot of potential bugs.
@@ -180,13 +181,17 @@ export function writeFunctionCall(config: {
180
181
  // this is fine).
181
182
  callerIP: "127.0.0.1",
182
183
  };
183
- if (!isNode()) {
184
- // Get the "setfncfilter" querystring parameter
185
- let url = new URL(window.location.href);
186
- let setfncfilter = url.searchParams.get("setfncfilter");
187
- if (setfncfilter) {
188
- callSpec.filterable = parseFilterable(setfncfilter);
189
- }
184
+ // NOTE: I don't know when this would ever really work. I guess to isolate function calls, but if we're running locally, isn't that easy?
185
+ // if (!isNode()) {
186
+ // // Get the "setfncfilter" querystring parameter
187
+ // let url = new URL(window.location.href);
188
+ // let setfncfilter = url.searchParams.get("setfncfilter");
189
+ // if (setfncfilter) {
190
+ // callSpec.filterable = parseFilterable(setfncfilter);
191
+ // }
192
+ // }
193
+ if (isNode()) {
194
+ callSpec.filterable = getFncFilter();
190
195
  }
191
196
 
192
197
  if (curInterceptor) {
@@ -385,6 +385,8 @@ export class PathFunctionRunner {
385
385
  }
386
386
 
387
387
  if (PathFunctionRunner.DEBUG_CALLS) {
388
+ // NOTE: If this start, it means that the filter didn't match, which either means it was a dev function and so not relevant for us, or we are dev and it's a production function, so not relevant for us.
389
+ // - The filter is set by fncfilter when running serverside, or, clientside, by fncfilter set on the command line of the querysub server.
388
390
  console.log(`QUEUING ${getDebugName(callData, functionSpec, true)}`);
389
391
  let resultsPath = getProxyPath(() => moduleData.Results[callId]);
390
392
  let history = authorityStorage.getValuePlusHistory(resultsPath);
@@ -1333,7 +1333,6 @@ setImmediate(async () => {
1333
1333
  // Import, so it registers addStatPeriodic
1334
1334
  await import("../5-diagnostics/nodeMetadata");
1335
1335
  await import("../diagnostics/pathAuditer");
1336
- await import("../diagnostics/NodeConnectionsPage");
1337
1336
  });
1338
1337
 
1339
1338
  setImmediate(async () => {
@@ -27,7 +27,7 @@ import { CallerContextBase } from "socket-function/SocketFunctionTypes";
27
27
  import { isTrustedByNode } from "../-d-trust/NetworkTrust2";
28
28
  import { Querysub, id } from "./Querysub";
29
29
  import { isDefined } from "../misc";
30
- import { isClient, isServer } from "../config2";
30
+ import { getFncFilter, isClient, isServer } from "../config2";
31
31
  import { PromiseObj } from "../promise";
32
32
  import { LoggingClient } from "../0-path-value-core/LoggingClient";
33
33
  import * as prediction from "./querysubPrediction";
@@ -42,12 +42,6 @@ import { PathRouter } from "../0-path-value-core/PathRouter";
42
42
  import { authorityLookup } from "../0-path-value-core/AuthorityLookup";
43
43
  import { PathValueArchives } from "../0-path-value-core/pathValueArchives";
44
44
 
45
- let yargObj = isNodeTrue() && yargs(process.argv)
46
- .option("fncfilter", { type: "string", default: "", desc: `Sets the filterable state for function calls, causing them to target specific FunctionRunners. If no FunctionRunner matches, all functions will fail to run. For example: "devtestserver" will match a FunctionRunner that uses the "devtestserver" filter. Merges with the existing filterable state if a client sets it explicitly.` })
47
- .argv
48
- ;
49
-
50
- const getFncFilter = lazy(() => parseFilterable(yargObj.fncfilter));
51
45
 
52
46
  export { Querysub, id };
53
47
 
package/src/config.ts CHANGED
@@ -31,8 +31,14 @@ let yargObj = parseArgsFactory()
31
31
  })
32
32
  .option("logbackblaze", { type: "boolean", desc: "Log all backblaze activity to disk." })
33
33
  .option("slowdown", { type: "number", desc: "Delay all input data values by this amount of time, pretending like we didn't even receive it until this time is up." })
34
+ .option("fncfilter", { type: "string", default: "", desc: `Sets the filterable state for function calls, causing them to target specific FunctionRunners. If no FunctionRunner matches, all functions will fail to run. For example: "devtestserver" will match a FunctionRunner that uses the "devtestserver" filter. Merges with the existing filterable state if a client sets it explicitly.` })
34
35
  .argv
35
36
  ;
37
+
38
+ export function getRawFncFilter() {
39
+ return yargObj.fncfilter;
40
+ }
41
+
36
42
  type QuerysubConfig = {
37
43
  domain?: string;
38
44
  emaildomain?: string;
@@ -134,6 +140,9 @@ export function isPublic() {
134
140
  }
135
141
  return !!yargObj.public;
136
142
  }
143
+ export function setIsPublic(value: boolean) {
144
+ yargObj.public = value;
145
+ }
137
146
 
138
147
  /** @deprecated Use !isPublic() instead */
139
148
  export function isLocal() {
package/src/config2.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { deepCloneJSON, isNode } from "socket-function/src/misc";
2
2
  import { hasArchivesPermissions } from "./-a-archives/archives";
3
- import { baseIsClient, getDomain } from "./config";
3
+ import { baseIsClient, getDomain, getRawFncFilter } from "./config";
4
4
  import { JSONLACKS } from "socket-function/src/JSONLACKS/JSONLACKS";
5
5
  import { rootPathStr, prependToPathStr, getPathDepth } from "./path";
6
6
  import fs from "fs";
7
7
  import { AuthoritySpec } from "./0-path-value-core/PathRouter";
8
+ import { parseFilterable } from "./misc/filterable";
9
+ import { lazy } from "socket-function/src/caching";
8
10
 
9
11
  let isInCall = false;
10
12
  export function isClient() {
@@ -21,3 +23,7 @@ export function isServer() {
21
23
  }
22
24
 
23
25
  // NOTE: getOurAuthorities moved to PathRouterHashOverrides, and renamed to getOurAuthoritySpec
26
+
27
+
28
+
29
+ export const getFncFilter = lazy(() => parseFilterable(getRawFncFilter() || ""));
@@ -0,0 +1,40 @@
1
+ import preact from "preact";
2
+ import { SocketFunction } from "socket-function/SocketFunction";
3
+ import { qreact } from "../../4-dom/qreact";
4
+ import { sort } from "socket-function/src/misc";
5
+ import { isDefined } from "../../misc";
6
+ import { formatVeryNiceDateTime } from "socket-function/src/formatting/format";
7
+ import { InputPicker } from "../../library-components/InputPicker";
8
+ import { MachineServiceController } from "../machineSchema";
9
+
10
+ module.hotreload = true;
11
+
12
+ export class MachinePicker extends qreact.Component<{
13
+ label?: preact.ComponentChild;
14
+ picked: string[];
15
+ addPicked: (machineId: string) => void;
16
+ removePicked: (machineId: string) => void;
17
+ singleOption?: boolean;
18
+ fillWidth?: boolean;
19
+ }> {
20
+ render() {
21
+ let controller = MachineServiceController(SocketFunction.browserNodeId());
22
+ let machines = (controller.getMachineList() || []).map(x => controller.getMachineInfo(x)).filter(isDefined);
23
+ sort(machines, x => -x.heartbeat);
24
+
25
+ let options = machines.map(machineObj => ({
26
+ value: machineObj.machineId,
27
+ label: `${machineObj.machineId} ${machineObj.info["getExternalIP"]} (${Object.keys(machineObj.services || {}).length} services, last heartbeat ${formatVeryNiceDateTime(machineObj.heartbeat)})`,
28
+ }));
29
+
30
+ return <InputPicker<string>
31
+ label={this.props.label}
32
+ picked={this.props.picked}
33
+ options={options}
34
+ addPicked={this.props.addPicked}
35
+ removePicked={this.props.removePicked}
36
+ singleOption={this.props.singleOption}
37
+ fillWidth={this.props.fillWidth}
38
+ />;
39
+ }
40
+ }
@@ -7,6 +7,7 @@ import { Querysub } from "../../4-querysub/QuerysubController";
7
7
  import { currentViewParam, selectedServiceIdParam, selectedMachineIdParam } from "../urlParams";
8
8
  import { formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
9
9
  import { InputPicker } from "../../library-components/InputPicker";
10
+ import { MachinePicker } from "./MachinePicker";
10
11
  import { deepCloneJSON, nextId, sort } from "socket-function/src/misc";
11
12
  import { InputLabel } from "../../library-components/InputLabel";
12
13
  import { Button } from "../../library-components/Button";
@@ -291,13 +292,9 @@ export class ServiceDetailPage extends qreact.Component {
291
292
  />
292
293
  </div>
293
294
 
294
- <InputPicker
295
+ <MachinePicker
295
296
  label="Machines"
296
297
  picked={config.machineIds}
297
- options={machines.map(machineObj => ({
298
- value: machineObj.machineId,
299
- label: `${machineObj.machineId} ${machineObj.info["getExternalIP"]} (${Object.keys(machineObj.services || {}).length} services, last heartbeat ${formatVeryNiceDateTime(machineObj.heartbeat)})`
300
- }))}
301
298
  addPicked={machineId => {
302
299
  configT.machineIds.push(machineId);
303
300
  this.updateUnsavedChanges(configT);
@@ -13,6 +13,7 @@ import { isPublic } from "../../config";
13
13
  import { UpdateButtons, UpdateServiceButtons } from "./deployButtons";
14
14
  import { isDefined } from "../../misc";
15
15
  import { formatDateJSX } from "../../misc/formatJSX";
16
+ import { Tools } from "./Tools";
16
17
 
17
18
  module.hotreload = true;
18
19
 
@@ -74,6 +75,7 @@ export class ServicesListPage extends qreact.Component {
74
75
  </button>
75
76
  <UpdateButtons services={services.map(x => x[1]).filter(isDefined)} />
76
77
  </div>
78
+ <Tools />
77
79
  <div className={css.vbox(8)}>
78
80
  {services.map(([serviceId, config]) => {
79
81
  if (!config) return <div key={serviceId}>Config is broken? Missing value for service? Is the file corrupted?</div>;