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
@@ -0,0 +1,290 @@
1
+ /*
2
+ Tracks the FunctionRunners in the cluster: which networks they listen on, their shard ranges, whether they are public, how long they have been up, and the latency of talking to them. Also tracks call durations (overall, and per network), in 5 minute buckets, for the last hour.
3
+
4
+ This is all PER QUERYSUB SERVER, in memory (the latency and call times depend on the node making the calls) — it is NEVER written to the database. Clients get their querysub server's index from the edge node config at startup (so it is available immediately, with no calls), and then refresh it by polling QuerysubController.getFunctionRunnerIndex, using it to automatically select the best network for their function calls.
5
+ */
6
+
7
+ import { SocketFunction } from "socket-function/SocketFunction";
8
+ import { timeInMinute, timeInSecond, sort } from "socket-function/src/misc";
9
+ import { lazy } from "socket-function/src/caching";
10
+ import { runInfinitePollCallAtStart } from "socket-function/src/batching";
11
+ import { isClient } from "../config2";
12
+ import { t } from "../2-proxy/schema2";
13
+ import { createLocalSchema } from "./schemaHelpers";
14
+ import { Querysub } from "./Querysub";
15
+ import { getAllNodeIds, getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
16
+ import { FunctionRunnerInfoController, FunctionStatsSummary } from "../3-path-functions/PathFunctionRunner";
17
+ import { URLParam } from "../library-components/URLParam";
18
+ import { timeoutToUndefinedSilent } from "../errors";
19
+ import { spreadCallsOverTime } from "../misc";
20
+ import { formatTime } from "socket-function/src/formatting/format";
21
+ import type { DebugFunctionShardInfo } from "../3-path-functions/PathFunctionRunner";
22
+ import type { EdgeNodeConfig } from "../4-deploy/edgeNodes";
23
+ import { isPublic } from "../config";
24
+
25
+ const POLL_INTERVAL = timeInMinute;
26
+ const INDEX_POLL_INTERVAL = timeInMinute * 5;
27
+ // Forget runners we haven't been able to reach for this long
28
+ const NODE_EXPIRY_TIME = POLL_INTERVAL * 5;
29
+ // Rolling latency average size. Small, so latency changes show up quickly.
30
+ const LATENCY_SAMPLE_LIMIT = 20;
31
+ // Only prefer runners that have been up at least this long
32
+ const MIN_UP_TIME = timeInMinute * 5;
33
+ // Only weight by call time once a network has at least this much total recorded call time
34
+ const MIN_TOTAL_CALL_TIME_FOR_WEIGHTING = timeInSecond * 60;
35
+ // Added to the average call time when weighting, so small call time differences don't give near infinite priority (2s vs 0s becomes 3 / 1 priority, instead of infinite)
36
+ const CALL_TIME_WEIGHT_BASE = 1000;
37
+ // If every runner on a network will be shut down within this window, the network is dying and we switch off of it.
38
+ const NETWORK_DYING_WINDOW = timeInMinute * 2;
39
+
40
+ // The forced network is a URL parameter, so it survives reloads and is shareable / obvious in the URL.
41
+ export const forcedNetworkURL = new URLParam("network", "");
42
+
43
+ export type FunctionRunnerNodeInfo = {
44
+ nodeId: string;
45
+ entryPoint: string;
46
+ startupTime: number;
47
+ lastSeen: number;
48
+ isPublic: boolean;
49
+ networks: string[];
50
+ shards: DebugFunctionShardInfo[];
51
+ averageLatency: number;
52
+ latencySampleCount: number;
53
+ // Set once the runner has been told it will shut down (scheduled switchover)
54
+ scheduledShutdownTime?: number;
55
+ };
56
+ export type FunctionRunnerIndex = {
57
+ nodes: FunctionRunnerNodeInfo[];
58
+ // Stats over ALL function calls in the last hour
59
+ callStats: FunctionStatsSummary;
60
+ // network => stats over the last hour
61
+ networkStats: { [network: string]: FunctionStatsSummary };
62
+ };
63
+ export type NetworkSelectionInfo = {
64
+ network: string;
65
+ nodeCount: number;
66
+ isPublic: boolean;
67
+ upLongEnough: boolean;
68
+ // Every runner on this network is scheduled to shut down within NETWORK_DYING_WINDOW
69
+ dying: boolean;
70
+ averageLatency: number;
71
+ averageCallTime: number;
72
+ score: number;
73
+ };
74
+
75
+ let nodeInfos = new Map<string, FunctionRunnerNodeInfo>();
76
+ // runner nodeId => that runner's stats over our own calls (network => summary, already summarized by the runner)
77
+ let nodeTimings = new Map<string, { [network: string]: FunctionStatsSummary }>();
78
+ // Clients don't poll the runners directly, they poll their querysub server's index. Local state (not a plain variable), so the UI updates when a poll updates it.
79
+ const polledIndexSchema = createLocalSchema("functionRunnerPolledIndex", {
80
+ index: t.atomic<FunctionRunnerIndex>(),
81
+ });
82
+
83
+ export function getFunctionRunnerIndex(): FunctionRunnerIndex | undefined {
84
+ void startFunctionRunnerTracking();
85
+ if (isClient()) {
86
+ // Until our first index poll finishes we use the index our edge node registered with (available immediately at startup, with no calls)
87
+ let bootedEdgeNode = (globalThis as any).BOOTED_EDGE_NODE as EdgeNodeConfig | undefined;
88
+ return Querysub.localRead(() => polledIndexSchema().index) || bootedEdgeNode?.functionRunnerIndex;
89
+ }
90
+ return getFunctionRunnerIndexServer();
91
+ }
92
+ export function getFunctionRunnerIndexServer(): FunctionRunnerIndex {
93
+ void startFunctionRunnerTracking();
94
+ let nodes = Array.from(nodeInfos.values());
95
+ sort(nodes, x => x.nodeId);
96
+ let callStats: FunctionStatsSummary = { count: 0, totalTime: 0, minTime: 0, maxTime: 0 };
97
+ let networkStatsObj: FunctionRunnerIndex["networkStats"] = {};
98
+ for (let timings of nodeTimings.values()) {
99
+ for (let [network, summary] of Object.entries(timings)) {
100
+ let networkSummary = networkStatsObj[network];
101
+ if (!networkSummary) {
102
+ networkSummary = { count: 0, totalTime: 0, minTime: 0, maxTime: 0 };
103
+ networkStatsObj[network] = networkSummary;
104
+ }
105
+ mergeSummary(networkSummary, summary);
106
+ mergeSummary(callStats, summary);
107
+ }
108
+ }
109
+ return {
110
+ nodes,
111
+ callStats,
112
+ networkStats: networkStatsObj,
113
+ };
114
+
115
+ function mergeSummary(into: FunctionStatsSummary, from: FunctionStatsSummary) {
116
+ if (from.count === 0) return;
117
+ if (into.count === 0) {
118
+ into.minTime = from.minTime;
119
+ }
120
+ into.count += from.count;
121
+ into.totalTime += from.totalTime;
122
+ into.minTime = Math.min(into.minTime, from.minTime);
123
+ into.maxTime = Math.max(into.maxTime, from.maxTime);
124
+ }
125
+ }
126
+
127
+ setImmediate(() => {
128
+ void import("./QuerysubController");
129
+ });
130
+ // Awaits getting the initial function runner information, so it is populated before anything makes calls (and so servers can include the initial index in their edge node config).
131
+ export const startFunctionRunnerTracking = lazy(async () => {
132
+ if (isClient()) {
133
+ await runInfinitePollCallAtStart(INDEX_POLL_INTERVAL, async function pollFunctionRunnerIndex() {
134
+ const { QuerysubController, querysubNodeId } = await import("./QuerysubController");
135
+ let nodeId = await querysubNodeId();
136
+ if (!nodeId) return;
137
+ let index = await QuerysubController.nodes[nodeId].getFunctionRunnerIndex();
138
+ Querysub.localCommit(() => {
139
+ polledIndexSchema().index = index;
140
+ });
141
+ });
142
+ return;
143
+ }
144
+ console.log(`Starting function runner tracking (polling every ${formatTime(POLL_INTERVAL)})`);
145
+ await runInfinitePollCallAtStart(POLL_INTERVAL, pollFunctionRunners);
146
+ finishedStartup = true;
147
+ });
148
+
149
+ let finishedStartup = false;
150
+ async function pollFunctionRunners() {
151
+ let now = Date.now();
152
+ let nodeIds = await getAllNodeIds();
153
+ await spreadCallsOverTime(nodeIds, finishedStartup ? POLL_INTERVAL : 0, async nodeId => {
154
+ let start = Date.now();
155
+ // Only function runners expose this, so a failed call just means the node isn't a runner
156
+ let runnerInfo = await timeoutToUndefinedSilent(POLL_INTERVAL, FunctionRunnerInfoController.nodes[nodeId].getRunnerInfo({
157
+ writeNodeId: getOwnNodeId(),
158
+ }));
159
+ if (!runnerInfo) return;
160
+ let latency = Date.now() - start;
161
+ if (runnerInfo.shards.length === 0) return;
162
+
163
+ let networks = new Set<string>();
164
+ for (let shard of runnerInfo.shards) {
165
+ for (let network of shard.networks) {
166
+ networks.add(network);
167
+ }
168
+ }
169
+
170
+ let prev = nodeInfos.get(nodeId);
171
+ let sampleCount = Math.min(prev?.latencySampleCount || 0, LATENCY_SAMPLE_LIMIT);
172
+ nodeInfos.set(nodeId, {
173
+ nodeId,
174
+ entryPoint: runnerInfo.entryPoint,
175
+ startupTime: runnerInfo.startupTime,
176
+ lastSeen: now,
177
+ isPublic: runnerInfo.shards.some(x => x.isPublic),
178
+ networks: Array.from(networks),
179
+ shards: runnerInfo.shards,
180
+ averageLatency: ((prev?.averageLatency || 0) * sampleCount + latency) / (sampleCount + 1),
181
+ latencySampleCount: sampleCount + 1,
182
+ scheduledShutdownTime: runnerInfo.scheduledShutdownTime,
183
+ });
184
+ nodeTimings.set(nodeId, runnerInfo.timings);
185
+ watchRunnerDisconnect(nodeId);
186
+ });
187
+
188
+ for (let [nodeId, info] of Array.from(nodeInfos)) {
189
+ if (info.lastSeen < now - NODE_EXPIRY_TIME) {
190
+ nodeInfos.delete(nodeId);
191
+ nodeTimings.delete(nodeId);
192
+ }
193
+ }
194
+
195
+ if (nodeInfos.size === 0) {
196
+ console.warn(`No function runner networks discovered at ${new Date().toLocaleString()}. Function calls cannot be given a network until one is found (we poll again in ${formatTime(POLL_INTERVAL)}).`);
197
+ }
198
+ }
199
+
200
+ // Remove disconnected runners immediately, otherwise we would keep recommending their networks for up to NODE_EXPIRY_TIME after every runner on them is gone. If the runner comes back, the poll re-adds it (and re-registers this watch).
201
+ let disconnectWatchedNodes = new Set<string>();
202
+ function watchRunnerDisconnect(nodeId: string) {
203
+ if (disconnectWatchedNodes.has(nodeId)) return;
204
+ disconnectWatchedNodes.add(nodeId);
205
+ SocketFunction.onNextDisconnect(nodeId, () => {
206
+ disconnectWatchedNodes.delete(nodeId);
207
+ if (nodeInfos.delete(nodeId)) {
208
+ nodeTimings.delete(nodeId);
209
+ console.log(`Function runner ${nodeId} disconnected, removing it from the runner index immediately`);
210
+ }
211
+ }, "iKnowThatServerNodeIdsMayReconnect_andIHandleReconnections");
212
+ }
213
+
214
+ /** Whether any known runner on this network is public. Unknown networks count as non-public. */
215
+ export function isNetworkPublic(network: string): boolean {
216
+ for (let info of nodeInfos.values()) {
217
+ if (info.networks.includes(network) && info.isPublic) return true;
218
+ }
219
+ return false;
220
+ }
221
+
222
+ /** Everything the selection algorithm knows about each network, so the UI can show why a network was (or wasn't) selected. */
223
+ export function getNetworkSelectionInfos(): NetworkSelectionInfo[] {
224
+ let index = getFunctionRunnerIndex();
225
+ if (!index) return [];
226
+ let now = Date.now();
227
+
228
+ let perNetwork = new Map<string, FunctionRunnerNodeInfo[]>();
229
+ for (let node of index.nodes) {
230
+ for (let network of node.networks) {
231
+ let list = perNetwork.get(network);
232
+ if (!list) {
233
+ list = [];
234
+ perNetwork.set(network, list);
235
+ }
236
+ list.push(node);
237
+ }
238
+ }
239
+
240
+ let infos: NetworkSelectionInfo[] = [];
241
+ for (let [network, nodes] of perNetwork) {
242
+ let averageLatency = nodes.reduce((sum, x) => sum + x.averageLatency, 0) / nodes.length;
243
+ let stats = index.networkStats[network];
244
+ let averageCallTime = 0;
245
+ if (stats && stats.totalTime >= MIN_TOTAL_CALL_TIME_FOR_WEIGHTING && stats.count > 0) {
246
+ averageCallTime = stats.totalTime / stats.count;
247
+ }
248
+ let dying = nodes.every(x => x.scheduledShutdownTime !== undefined && x.scheduledShutdownTime - now < NETWORK_DYING_WINDOW);
249
+ infos.push({
250
+ network,
251
+ nodeCount: nodes.length,
252
+ isPublic: nodes.some(x => x.isPublic),
253
+ upLongEnough: nodes.some(x => now - x.startupTime >= MIN_UP_TIME),
254
+ dying,
255
+ averageLatency,
256
+ averageCallTime,
257
+ score: averageLatency * (averageCallTime + CALL_TIME_WEIGHT_BASE),
258
+ });
259
+ }
260
+ sort(infos, x => x.score);
261
+ return infos;
262
+ }
263
+
264
+ export function getAutoSelectedNetwork(): string | undefined {
265
+ let infos = getNetworkSelectionInfos();
266
+ // Networks where every runner is non-public must never be selected by default. Networks that will have no runners soon are forcefully switched off of.
267
+ let candidates = infos.filter(x => !x.dying);
268
+ if (candidates.length === 0) {
269
+ candidates = infos;
270
+ }
271
+ if (isPublic()) {
272
+ candidates = candidates.filter(x => x.isPublic);
273
+ }
274
+ let upLongEnough = candidates.filter(x => x.upLongEnough);
275
+ if (upLongEnough.length > 0) {
276
+ candidates = upLongEnough;
277
+ }
278
+ return candidates[0]?.network;
279
+ }
280
+
281
+ /** Safe to call from any context. Returns the network new function calls should be put on. Throws if no network is available (calls without a network can never run), unless noThrow is set. */
282
+ export function getSelectedNetwork(): string;
283
+ export function getSelectedNetwork(config: { noThrow: boolean }): string | undefined;
284
+ export function getSelectedNetwork(config?: { noThrow?: boolean }): string | undefined {
285
+ let network = forcedNetworkURL.value || getAutoSelectedNetwork();
286
+ if (!network && !config?.noThrow) {
287
+ throw new Error(`No function runner discovered during discovery, so this call cannot be given a network and would never run (we keep polling for function runners, every ${formatTime(POLL_INTERVAL)} when tracking, every ${formatTime(INDEX_POLL_INTERVAL)} for the client index)`);
288
+ }
289
+ return network;
290
+ }
@@ -290,7 +290,7 @@ export class Querysub {
290
290
  * isn't presently necessary in most serverside scripts.
291
291
  */
292
292
  @measureFnc
293
- public static async optionalStartupWait(addTime?: (name: string) => void) {
293
+ public static async startupWait(addTime?: (name: string) => void) {
294
294
  // Used by authorityLookup, pulled out here so timing is more clear.
295
295
  await onNodeDiscoveryReady();
296
296
  addTime?.("onNodeDiscoveryReady");
@@ -300,6 +300,10 @@ export class Querysub {
300
300
  await waitForFirstTimeSync();
301
301
  }, "waitForFirstTimeSync");
302
302
  addTime?.("waitForFirstTimeSync");
303
+ // Populate the function runner information (networks, timings), so calls can select a network immediately.
304
+ const { startFunctionRunnerTracking } = await import("./FunctionRunnerTracking");
305
+ await startFunctionRunnerTracking();
306
+ addTime?.("pollFunctionRunners");
303
307
  }
304
308
 
305
309
  public static createWatcher(watcher: (obj: SyncWatcher) => void, options?: Partial<WatcherOptions<unknown>>): {
@@ -860,6 +864,9 @@ export class Querysub {
860
864
  host: ipDomain + ":" + port,
861
865
  entryPaths,
862
866
  });
867
+ // Watch the FunctionRunners in the cluster (networks, latency, call times), building the in-memory index clients pull (initially via the edge node config, then by polling QuerysubController) to select a network for their calls.
868
+ const { startFunctionRunnerTracking } = await import("./FunctionRunnerTracking");
869
+ void startFunctionRunnerTracking();
863
870
  }
864
871
  }
865
872
  private static async addSourceMapCheck(config: {
@@ -1080,7 +1087,7 @@ export class Querysub {
1080
1087
  await Promise.all([mountPromise, publishPromise]);
1081
1088
  addTime("mount & publish a records");
1082
1089
 
1083
- await Querysub.optionalStartupWait(addTime);
1090
+ await Querysub.startupWait(addTime);
1084
1091
 
1085
1092
  console.log(magenta(`Started hosting service ${name}`) + ` | ${times.map(t => `${blue(t.name)}: ${green(formatTime(t.duration))}`).join(" | ")}`);
1086
1093
  }
@@ -1096,21 +1103,15 @@ export class Querysub {
1096
1103
  * BOTH start and end must be provided, otherwise if only one is provided we will ignore it.
1097
1104
  */
1098
1105
  endFraction?: number;
1099
- /** Only listen to keys on this network. If not specified, only keys on the default network are returned. */
1100
- network?: string;
1101
1106
  }): (keyof T)[] {
1102
1107
 
1103
- let { startFraction, endFraction, network } = config || {};
1108
+ let { startFraction, endFraction } = config || {};
1104
1109
  if ((startFraction === undefined) !== (endFraction === undefined)) {
1105
1110
  throw new Error(`startFraction and endFraction must both be provided, or both be undefined. If you want to get all keys, don't provide startFraction and endFraction.`);
1106
1111
  }
1107
1112
  if (!isNode() && startFraction !== undefined && endFraction !== undefined) {
1108
1113
  console.warn(`keys() with a range restriction is not supported clientside. It's too complicated for the proxy to handle it because the hashing depends on the authority server. You can synchronize all the keys client side, but you can't synchronize a restriction of the keys.`);
1109
1114
  }
1110
- if (network && startFraction === undefined) {
1111
- startFraction = 0;
1112
- endFraction = 1;
1113
- }
1114
1115
  if (startFraction === undefined || endFraction === undefined) {
1115
1116
  return Object.keys(obj);
1116
1117
  }
@@ -1118,15 +1119,10 @@ export class Querysub {
1118
1119
  if (!path) {
1119
1120
  return Object.keys(obj);
1120
1121
  }
1121
- let packedPath = encodeParentFilter({ path, startFraction, endFraction, network });
1122
+ let packedPath = encodeParentFilter({ path, startFraction, endFraction });
1122
1123
  return proxyWatcher.getKeys(packedPath);
1123
1124
  }
1124
1125
 
1125
- /** Creates a key that lives on the given network. Reads and writes using the returned key route to authorities on that network (instead of the default network), and it will only be enumerated by Querysub.keys calls that ask for that network. */
1126
- public static createNetworkKey(config: { key: string; network: string }): string {
1127
- return createNetworkKey({ originalKey: config.key, network: config.network });
1128
- }
1129
-
1130
1126
  // TODO: Maybe expose checkPermissions(getValue: () => unknown)?
1131
1127
  // - It would be easy, if we every need to explicitly check if we have permissions. Although, it seems
1132
1128
  // like just relying on the automatic checking is better?
@@ -1377,6 +1373,5 @@ import { onAllPredictionsFinished } from "../-0-hooks/hooks";
1377
1373
  import { LOCAL_DOMAIN } from "../0-path-value-core/PathRouter";
1378
1374
  import { authorityLookup } from "../0-path-value-core/AuthorityLookup";
1379
1375
  import { encodeParentFilter } from "../0-path-value-core/hackedPackedPathParentFiltering";
1380
- import { createNetworkKey } from "../0-path-value-core/PathRouterRouteOverride";
1381
1376
  import { AliveChecker, registerAliveChecker } from "../2-proxy/garbageCollection";
1382
1377
  import { QuerysubController, anyPredictionsPending, flushDelayedFunctions, onCallPredict, waitUntilAllPredictionsFinish } from "./QuerysubController";
@@ -34,9 +34,10 @@ import * as prediction from "./querysubPrediction";
34
34
  setFlag(require, "preact", "allowclient", true);
35
35
 
36
36
  import yargs from "yargs";
37
- import { setRoutingOverrideKeyNetwork } from "../0-path-value-core/PathRouterRouteOverride";
37
+ import { getFunctionRunnerIndexServer, isNetworkPublic } from "./FunctionRunnerTracking";
38
+ import type { FunctionRunnerIndex } from "./FunctionRunnerTracking";
38
39
  import { isManagementUser, onAllPredictionsFinished } from "../-0-hooks/hooks";
39
- import { getDomain, getPrimaryNetwork, isBootstrapOnly, DEFAULT_NETWORK } from "../config";
40
+ import { getDomain, isBootstrapOnly } from "../config";
40
41
  import { flushPredictionQueueBase, runInPredictionQueue, syncHasPendingPredictionsBase } from "./predictionQueue";
41
42
  import { PathRouter } from "../0-path-value-core/PathRouter";
42
43
  import { authorityLookup } from "../0-path-value-core/AuthorityLookup";
@@ -551,16 +552,12 @@ export class QuerysubControllerBase {
551
552
  let callerCreatorId = IdentityController_getPubKeyShort(caller);
552
553
  call.callerIP = IdentityController_getSecureIP(caller);
553
554
 
554
- if (call.network && call.network !== DEFAULT_NETWORK && !await isManagementUser()) {
555
- throw new Error(`Caller is not a management user, and so does not have permissions to set the network on calls. Call ${debugCallSpec(call)}, network was "${call.network}"`);
556
- }
557
- // Clients don't know their network, so we decide it for them, rewriting the callId so the call routes to (and is picked up by) the right network.
558
555
  if (!call.network) {
559
- let network = getPrimaryNetwork();
560
- if (network !== DEFAULT_NETWORK) {
561
- call.network = network;
562
- call.CallId = setRoutingOverrideKeyNetwork(call.CallId, network);
563
- }
556
+ throw new Error(`Call has no network, so it would never run (no FunctionRunner would pick it up). Call ${debugCallSpec(call)}`);
557
+ }
558
+ // Networks where every function runner is non-public are only usable by super users (they were never meant to receive public traffic, ex, a dev network).
559
+ if (!isNetworkPublic(call.network) && !await isManagementUser()) {
560
+ throw new Error(`Caller is not a super user, and so cannot add calls on the non-public network "${call.network}". Call ${debugCallSpec(call)}`);
564
561
  }
565
562
 
566
563
  if (Querysub.SIMULATE_LAG) {
@@ -628,6 +625,10 @@ export class QuerysubControllerBase {
628
625
  // except the server being down, in which case, the client will gracefully timeout when it doesn't receive the confirmation
629
626
  }
630
627
 
628
+ public async getFunctionRunnerIndex(): Promise<FunctionRunnerIndex> {
629
+ return getFunctionRunnerIndexServer();
630
+ }
631
+
631
632
  public async getModulePath(config: {
632
633
  functionSpec: {
633
634
  DomainName: string;
@@ -720,6 +721,7 @@ export const QuerysubController = SocketFunction.register(
720
721
  watch: { compress: true, },
721
722
  unwatch: { compress: true, },
722
723
  addCall: { compress: true, },
724
+ getFunctionRunnerIndex: { compress: true, },
723
725
  // NOTE: Most of these debug functions are pretty innocuous. A lot of it is actually already exposed, and other parts of it is fine. It's fine for the user to know what node a path is on. It's fine for them to know how many value paths there are.
724
726
  debugGetPathNodeIds: {},
725
727
  debugGetNodeSpecs: {},
@@ -163,6 +163,8 @@ export class PermissionsCheck {
163
163
  if (rootKey === "Calls" || rootKey === "Results") {
164
164
  // Don't allow "" call to be read, as this would allow running Object.values() to read all calls.
165
165
  if (path === pathParts.emptyKeyPath) return { permissionsPath: path, allowed: false };
166
+ // Calls/Results are nested one deeper by network, so we also have to block the empty key at both the network level and the callId level (otherwise Object.values() on a network would read all of its calls).
167
+ if (getPathIndex(path, DEPTH_TO_DATA) === "" || getPathIndex(path, DEPTH_TO_DATA + 1) === "") return { permissionsPath: path, allowed: false };
166
168
  // NOTE: A lot of the time, anyone will technically be able to read calls, as we don't inspect the call.
167
169
  // However... they would need to know the id, which will be very hard to guess.
168
170
  // - Also inspecting the call adds a lot of call overhead, so we would prefer not to do that.
@@ -208,6 +210,7 @@ class PermissionsCheckSchema {
208
210
  ModuleId: "",
209
211
  runAtTime: epochTime,
210
212
  argsEncoded: "",
213
+ network: "",
211
214
  };
212
215
 
213
216
  /** Converts a specific path to a more general path. All paths which map to this general path
@@ -74,10 +74,10 @@ const getDevFunctionSpecFromCall = cacheJSONArgsEqual(async (call: {
74
74
 
75
75
 
76
76
  export function getCallResultPath(call: CallSpec) {
77
- return getProxyPath(() => functionSchema()[call.DomainName].PathFunctionRunner[call.ModuleId].Results[call.CallId]);
77
+ return getProxyPath(() => functionSchema()[call.DomainName].PathFunctionRunner[call.ModuleId].Results[call.network][call.CallId]);
78
78
  }
79
79
  export function getCallResult(call: CallSpec) {
80
- return functionSchema()[call.DomainName].PathFunctionRunner[call.ModuleId].Results[call.CallId];
80
+ return functionSchema()[call.DomainName].PathFunctionRunner[call.ModuleId].Results[call.network][call.CallId];
81
81
  }
82
82
 
83
83
  /** Force predictions to run in the trigger order, so they can resolve an be added before
package/src/config.ts CHANGED
@@ -63,6 +63,7 @@ let networkFileNetworks = lazy((): string[] => {
63
63
  return fs.readFileSync(path, "utf8").split("\n").map(x => x.trim()).filter(x => x);
64
64
  });
65
65
 
66
+ /** The networks a function runner listens on, from the command line (--network / --networkfile). */
66
67
  export function getNetworks(): string[] {
67
68
  let networks = yargObj.network;
68
69
  if (!networks) {
@@ -78,9 +79,6 @@ export function getNetworks(): string[] {
78
79
  if (result.length === 0) return [DEFAULT_NETWORK];
79
80
  return result;
80
81
  }
81
- export function getPrimaryNetwork(): string {
82
- return getNetworks()[0];
83
- }
84
82
 
85
83
  type QuerysubConfig = {
86
84
  domain?: string;