querysub 0.303.0 → 0.305.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.303.0",
3
+ "version": "0.305.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",
@@ -22,7 +22,7 @@
22
22
  "js-sha512": "^0.9.0",
23
23
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
24
24
  "pako": "^2.1.0",
25
- "socket-function": "^0.134.0",
25
+ "socket-function": "^0.135.0",
26
26
  "terser": "^5.31.0",
27
27
  "typesafecss": "^0.22.0",
28
28
  "yaml": "^2.5.0",
@@ -19,6 +19,7 @@ import { decodeNodeId, decodeNodeIdAssert } from "../-a-auth/certs";
19
19
  import { isDefined } from "../misc";
20
20
  import { diskLog } from "../diagnostics/logs/diskLogger";
21
21
  import { getBootedEdgeNode } from "../-0-hooks/hooks";
22
+ import { EdgeNodeConfig } from "../4-deploy/edgeNodes";
22
23
 
23
24
  let HEARTBEAT_INTERVAL = timeInMinute * 15;
24
25
  // Interval which we check other heartbeats
@@ -220,6 +221,47 @@ export function configRootDiscoveryLocation(config: {
220
221
  }) {
221
222
  rootDiscoveryNodeId = getNodeId(config.domain, config.port);
222
223
  }
224
+
225
+ export function updateRootDiscoveryLocation(edgeNode: EdgeNodeConfig) {
226
+ let prevRootId = getBrowserUrlNode();
227
+ if (!isClient()) {
228
+ throw new Error(`updateRootDiscoveryLocation should only be called in the browser. If not a client, you should be able to discovery new nodes automatically`);
229
+ }
230
+ let nodeId = edgeNode.host;
231
+ let nodeCache = Object.entries(require.cache);
232
+ for (let [path, module] of nodeCache) {
233
+ if (!module) continue;
234
+ if (!path.includes(prevRootId)) continue;
235
+ let newPath = path.replace(prevRootId, nodeId);
236
+ // Remove the old module
237
+ delete require.cache[path];
238
+ // Add the new module
239
+ require.cache[newPath] = module;
240
+ module.filename = module.filename.replace(prevRootId, nodeId);
241
+ module.id = module.id.replace(prevRootId, nodeId);
242
+ if (module.original) {
243
+ module.original.filename = module.original.filename.replace(prevRootId, nodeId);
244
+ module.original.originalId = module.original.originalId.replace(prevRootId, nodeId);
245
+ for (let [key, value] of Object.entries(module.original.requests)) {
246
+ if (key.includes(prevRootId)) {
247
+ let newKey = key.replace(prevRootId, nodeId);
248
+ delete module.original.requests[key];
249
+ module.original.requests[newKey] = value;
250
+ key = newKey;
251
+ }
252
+ if (value.includes(prevRootId)) {
253
+ value = value.replace(prevRootId, nodeId);
254
+ module.original.requests[key] = value;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ globalThis.BOOTED_EDGE_NODE = edgeNode;
260
+ rootDiscoveryNodeId = nodeId;
261
+ addNodeId(nodeId);
262
+ }
263
+ (globalThis as any).updateRootDiscoveryLocation = updateRootDiscoveryLocation;
264
+
223
265
  /** NOTE: Can also be called serverside, if configRootDiscoveryLocation is called (otherwise can always be called clientside). */
224
266
  export function getBrowserUrlNode() {
225
267
  if (!isClient()) throw new Error(`getBrowserUrlNode can only be called when isClient()`);
@@ -472,6 +514,7 @@ if (isServer()) {
472
514
  }
473
515
  }
474
516
 
517
+
475
518
  export async function forceRemoveNode(nodeId: string) {
476
519
  await archives().del(nodeId);
477
520
  void tellEveryoneNodesChanges();
@@ -488,6 +531,7 @@ export async function nodeDiscoveryShutdown() {
488
531
  void tellEveryoneNodesChanges();
489
532
  }
490
533
  const tellEveryoneNodesChanges = throttleFunction(1000, function tellEveryoneNodesChanges() {
534
+ if (isClient()) return;
491
535
  for (let nodeId of allNodeIds2) {
492
536
  if (isOwnNodeId(nodeId)) continue;
493
537
  ignoreErrors(NodeDiscoveryController.nodes[nodeId].resyncNodes());
@@ -19,6 +19,8 @@ import { hackDevtoolsWebsocketForward } from "./oneTimeForward";
19
19
 
20
20
  let loadTime = Date.now();
21
21
 
22
+ let controllerNodeIdCache = new Map<string, string | Promise<string | undefined>>();
23
+
22
24
  // NOTE: If this becomes slow (because we are just trying all servers), we could start to store capabilities
23
25
  // in PersistedStorage. However, I don't think it will be a problem for quite some time.
24
26
  export async function getControllerNodeId(
@@ -27,26 +29,42 @@ export async function getControllerNodeId(
27
29
  retryCount = 1,
28
30
  initialTime = Date.now(),
29
31
  ): Promise<string | undefined> {
30
- let nodeIdsToTest = await getAllNodeIds();
31
- // Shuffle, so we aren't always using the same node!
32
- nodeIdsToTest = shuffle(nodeIdsToTest, Date.now());
33
- for (let nodeId of nodeIdsToTest) {
34
- if (await doesNodeExposeController(nodeId, controller)) {
35
- if (!quiet) {
36
- let duration = Date.now() - initialTime;
37
- console.log(green(`Resolved capability ${controller._classGuid} with node ${nodeId}, in ${formatTime(duration)}`));
38
- }
39
- return nodeId;
40
- }
32
+ let cached = controllerNodeIdCache.get(controller._classGuid);
33
+ if (cached && typeof cached !== "string") {
34
+ cached = await cached;
41
35
  }
42
- if (retryCount > 0) {
43
- await delay(2000);
44
- return getControllerNodeId(controller, quiet, retryCount - 1, initialTime);
36
+ if (cached && !SocketFunction.isNodeConnected(cached)) {
37
+ controllerNodeIdCache.delete(controller._classGuid);
38
+ cached = undefined;
45
39
  }
46
- if (!quiet) {
47
- console.warn(yellow(`Could not find a node that exposes controller ${controller._classGuid}, tried: ${JSON.stringify(nodeIdsToTest)}`));
40
+ if (cached) return cached;
41
+ let promise = getInternal();
42
+ controllerNodeIdCache.set(controller._classGuid, promise);
43
+ return promise;
44
+
45
+ async function getInternal() {
46
+
47
+ let nodeIdsToTest = await getAllNodeIds();
48
+ // Shuffle, so we aren't always using the same node!
49
+ nodeIdsToTest = shuffle(nodeIdsToTest, Date.now());
50
+ for (let nodeId of nodeIdsToTest) {
51
+ if (await doesNodeExposeController(nodeId, controller)) {
52
+ if (!quiet) {
53
+ let duration = Date.now() - initialTime;
54
+ console.log(green(`Resolved capability ${controller._classGuid} with node ${nodeId}, in ${formatTime(duration)}`));
55
+ }
56
+ return nodeId;
57
+ }
58
+ }
59
+ if (retryCount > 0) {
60
+ await delay(2000);
61
+ return getControllerNodeId(controller, quiet, retryCount - 1, initialTime);
62
+ }
63
+ if (!quiet) {
64
+ console.warn(yellow(`Could not find a node that exposes controller ${controller._classGuid}, tried: ${JSON.stringify(nodeIdsToTest)}`));
65
+ }
66
+ return undefined;
48
67
  }
49
- return undefined;
50
68
  }
51
69
 
52
70
  export async function getControllerNodeIdList(
@@ -20,7 +20,7 @@ import { formatNumber, formatTime } from "socket-function/src/formatting/format"
20
20
  import { cache, cacheLimited } from "socket-function/src/caching";
21
21
  import { IdentityController_getCurrentReconnectNodeIdAssert, IdentityController_getReconnectNodeIdAssert } from "../-c-identity/IdentityController";
22
22
  import { getBufferFraction, getBufferInt, getShortNumber } from "../bits";
23
- import { devDebugbreak, isDevDebugbreak } from "../config";
23
+ import { devDebugbreak, getDomain, isDevDebugbreak } from "../config";
24
24
  import { diskLog } from "../diagnostics/logs/diskLogger";
25
25
  import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
26
26
 
@@ -106,17 +106,18 @@ class NodePathAuthorities {
106
106
  // NOTE: WHILE calling this function at the module level (because we this is a singleton) is bad,
107
107
  // it SHOULD be fine. "getOwnNodeId" is part of level "-f-node-discovery", and we are level "0-path-value-core".
108
108
  // It should be ready before us, and it should never be allowed to depend on us.
109
- let originalOwnId = getOwnNodeId();
109
+
110
110
  let selfOnReady = new PromiseObj();
111
- this.authorities.set(getOwnNodeId(), {
112
- nodeId: getOwnNodeId(),
113
- authorityPaths: [],
114
- isReadReady: 0,
115
- createTime: this.createTime,
116
- self: true,
117
- onReady: selfOnReady,
118
- });
119
111
  if (isServer()) {
112
+ let originalOwnId = getOwnNodeId();
113
+ this.authorities.set(getOwnNodeId(), {
114
+ nodeId: getOwnNodeId(),
115
+ authorityPaths: [],
116
+ isReadReady: 0,
117
+ createTime: this.createTime,
118
+ self: true,
119
+ onReady: selfOnReady,
120
+ });
120
121
  void SocketFunction.mountPromise.finally(() => {
121
122
  // Move over to actual nodeId (which only exists after we mount)
122
123
  let newOwnId = getOwnNodeId();
@@ -234,16 +235,17 @@ class NodePathAuthorities {
234
235
  nodeIds = nodeIds.filter(nodeId => !isOwnNodeId(nodeId));
235
236
  let newNodes = nodeIds.filter(nodeId => !this.authorities.has(nodeId));
236
237
 
237
- let timeout = first ? POLL_RATE : 0;
238
+ let doInitialCheck = 0;
239
+ if (first || this.authorities.size === 0) {
240
+ doInitialCheck = POLL_RATE;
241
+ }
238
242
  let isCurrentFirst = first;
239
243
  first = false;
240
244
 
241
245
  let promise = Promise.allSettled(newNodes.map(async nodeId => {
242
246
  if (this.authorities.has(nodeId)) return;
243
247
 
244
- let loc = getNodeIdLocation(nodeId);
245
- let ourLoc = getNodeIdLocation(getOwnNodeId());
246
- if (loc && loc.address.startsWith("127-0-0-1") && loc.port === ourLoc?.port) {
248
+ if (isOwnNodeId(nodeId)) {
247
249
  // Ignore it if it's just us, using the special localhost address
248
250
  return;
249
251
  }
@@ -266,7 +268,7 @@ class NodePathAuthorities {
266
268
  console.log(blue(`Identifying ${nodeId} as a path authority. ping latency = ${formatTime(time)}`));
267
269
  this.authorities.set(nodeId, {
268
270
  nodeId,
269
- self: nodeId.startsWith("127-0-0-1."),
271
+ self: isOwnNodeId(nodeId),
270
272
  authorityPaths: [],
271
273
  isReadReady: 0,
272
274
  createTime,
@@ -279,14 +281,14 @@ class NodePathAuthorities {
279
281
  });
280
282
  let finished = false;
281
283
  try {
282
- if (timeout) {
284
+ if (doInitialCheck) {
283
285
  let timeoutObj = new PromiseObj();
284
286
  setTimeout(() => {
285
287
  timeoutObj.resolve();
286
- if (!finished && timeout) {
287
- console.warn(yellow(`Timeout after ${formatTime(timeout)} while identifying ${nodeId}`));
288
+ if (!finished && doInitialCheck) {
289
+ console.warn(yellow(`Timeout after ${formatTime(doInitialCheck)} while identifying ${nodeId}`));
288
290
  }
289
- }, timeout);
291
+ }, doInitialCheck);
290
292
  await Promise.race([
291
293
  timeoutObj.promise,
292
294
  errorToUndefined(this.forceCheckForReadReady(nodeId)),
@@ -384,7 +386,15 @@ class NodePathAuthorities {
384
386
  obj.isReadReady = isReadReady;
385
387
  obj.onReady.resolve();
386
388
 
387
- obj.authorityPaths = await PathController.nodes[nodeId].getAuthorityPaths();
389
+ if (isClient()) {
390
+ // If we are in the browser, any node that we can talk to is an edge node, and so presumably owns all paths!
391
+ // - Also clients, as... we shouldn't add certs when isClient, even if we are isNode.
392
+ obj.authorityPaths = [{
393
+ pathPrefix: getPathStr1(getDomain()),
394
+ }];
395
+ } else {
396
+ obj.authorityPaths = await PathController.nodes[nodeId].getAuthorityPaths();
397
+ }
388
398
  if (obj.authorityPaths.length > 0) {
389
399
  console.log(blue(`Identified ${isReadReady ? "(ready)" : yellow("(loading)")} ${nodeId} as a path authority for:`));
390
400
  for (let authorityPath of obj.authorityPaths) {
@@ -570,9 +580,6 @@ class NodePathAuthorities {
570
580
  );
571
581
  }
572
582
  public getReadNodes(path: string): string[] {
573
- if (isClient()) {
574
- return [getBrowserUrlNode()];
575
- }
576
583
  if (!this.waited) throw new Error(`waitUntilRoutingIsReady must be called before getReadNodes`);
577
584
  return this.getReadObjs(path).map(x => x.nodeId);
578
585
  }
@@ -631,9 +638,6 @@ class NodePathAuthorities {
631
638
  if (this.isSelfAuthority(path)) {
632
639
  return getOwnNodeId();
633
640
  }
634
- if (isClient()) {
635
- return getBrowserUrlNode();
636
- }
637
641
  if (!this.waited) throw new Error(`waitUntilRoutingIsReady must be called before getSingleReadNode`);
638
642
  let objs = this.getReadObjs(path);
639
643
  return this.pickAuthorityNode(objs)?.nodeId;
@@ -654,9 +658,6 @@ class NodePathAuthorities {
654
658
  if (this.isSelfAuthority(path)) {
655
659
  return { nodes: [{ nodeId: getOwnNodeId(), range: { start: 0, end: 1 } }] };
656
660
  }
657
- if (isClient()) {
658
- return { nodes: [{ nodeId: getBrowserUrlNode(), range: { start: 0, end: 1 } }] };
659
- }
660
661
 
661
662
  let wildcardPath = appendToPathStr(path, "");
662
663
 
@@ -749,9 +750,6 @@ class NodePathAuthorities {
749
750
  // IMPORTANT! Even if we are the authority, this doesn't mean other nodes aren't also the authority,
750
751
  // so we have to let it propagate to all nodes.
751
752
  //if (this.isSelfAuthority(path)) return [getOwnNodeId()];
752
- if (isClient()) {
753
- return [getBrowserUrlNode()];
754
- }
755
753
  await this.waitUntilRoutingIsReady();
756
754
 
757
755
  let result = Array.from(this.authorities.values())
@@ -5,7 +5,7 @@ import { cache } from "socket-function/src/caching";
5
5
  import { isNode, nextId } from "socket-function/src/misc";
6
6
  import { measureFnc, measureBlock } from "socket-function/src/profiling/measure";
7
7
  import { isOwnNodeId, getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
8
- import { nodePathAuthority, pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
8
+ import { LOCAL_DOMAIN_PATH, nodePathAuthority, pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
9
9
  import { PathValueController, pathValueCommitter } from "../0-path-value-core/PathValueController";
10
10
  import { WatchConfig, authorityStorage, decodeParentFilter, matchesParentRangeFilter, pathWatcher } from "../0-path-value-core/pathValueCore";
11
11
  import { ActionsHistory } from "../diagnostics/ActionsHistory";
@@ -151,9 +151,9 @@ export class RemoteWatcher {
151
151
  return watchObj?.nodesId;
152
152
  }
153
153
 
154
- private watchUnwatchSerial = runInSerial((name: string, fnc: () => Promise<unknown>) =>
155
- fnc()
156
- );
154
+ private watchUnwatchSerial = runInSerial(async (name: string, fnc: () => Promise<unknown>) => {
155
+ return await fnc();
156
+ });
157
157
 
158
158
  /** NOTE: We dedupe duplicate watches in watchLatest. */
159
159
  private async watchLatestBase(config: WatchConfig & { debugName?: string }) {
@@ -183,6 +183,9 @@ export class RemoteWatcher {
183
183
  let totalMissingPaths = 0;
184
184
  measureBlock(() => {
185
185
  for (let path of config.paths) {
186
+ // NOTE: We weren't ignoring local paths here for a while, but... I'm pretty sure we should, to save time.
187
+ if (path.startsWith(LOCAL_DOMAIN_PATH)) continue;
188
+
186
189
  // IMPORTANT! We have to await, otherwise we might see the paths are watched, skip them
187
190
  // find another path which has an authority, then go to watch that... which causes
188
191
  // us to circumvent earlier watchers, causing us to watch out of order, causing reads
@@ -229,6 +232,8 @@ export class RemoteWatcher {
229
232
  }
230
233
 
231
234
  for (let path of config.parentPaths) {
235
+ // NOTE: We weren't ignoring local paths here for a while, but... I'm pretty sure we should, to save time.
236
+ if (path.startsWith(LOCAL_DOMAIN_PATH)) continue;
232
237
  let watchObj = this.remoteWatchParents.get(path);
233
238
  if (
234
239
  watchObj
@@ -42,6 +42,8 @@ import { onNextPaint } from "../functional/onNextPaint";
42
42
  // With a harness that joins the two parts in a loop (mostly powered by clientWatcher,
43
43
  // which can run the trigger loop).
44
44
 
45
+ const DEFAULT_MAX_LOCKS = 1000;
46
+
45
47
  let nextOrderSeqNum = 1;
46
48
 
47
49
  export interface WatcherOptions<Result> {
@@ -208,6 +210,8 @@ export interface WatcherOptions<Result> {
208
210
  * - Also to tell us how long and how many paths are loading on startup.
209
211
  */
210
212
  logSyncTimings?: boolean;
213
+
214
+ maxLocksOverride?: number;
211
215
  }
212
216
 
213
217
  let harvestableReadyLoopCount = 0;
@@ -1539,6 +1543,11 @@ export class PathValueProxyWatcher {
1539
1543
  }
1540
1544
  }
1541
1545
 
1546
+ let maxLocks = watcher.options.maxLocksOverride || DEFAULT_MAX_LOCKS;
1547
+ if (locks.length > maxLocks) {
1548
+ throw new Error(`Too many locks for ${watcher.debugName} (${locks.length} > ${maxLocks}). You can override max locks with maxLocksOverride (in options / functionMetadata).`);
1549
+ }
1550
+
1542
1551
  setValues = clientWatcher.setValues({
1543
1552
  values: watcher.pendingWrites,
1544
1553
  eventPaths: watcher.pendingEventWrites,
@@ -2229,6 +2238,10 @@ export function noAtomicSchema<T>(code: () => T) {
2229
2238
 
2230
2239
 
2231
2240
  export const proxyWatcher = new PathValueProxyWatcher();
2241
+ if ((globalThis as any).proxyWatcher) {
2242
+ debugger;
2243
+ console.error(`Loaded PathValueProxyWatcher twice. This will break this. In ${module.filename}`);
2244
+ }
2232
2245
  (globalThis as any).proxyWatcher = proxyWatcher;
2233
2246
  (globalThis as any).ProxyWatcher = PathValueProxyWatcher;
2234
2247
  // Probably the most useful debug function for debugging watch based functions.
@@ -76,6 +76,8 @@ export type FunctionSpec = {
76
76
  gitURL: string;
77
77
  /** Ex, the hash */
78
78
  gitRef: string;
79
+
80
+ maxLocksOverride?: number;
79
81
  };
80
82
 
81
83
  export interface CallSpec {
@@ -513,6 +515,7 @@ export class PathFunctionRunner {
513
515
  getPermissionsCheck: PermissionsChecker && (() => new PermissionsChecker(callPath)),
514
516
  nestedCalls: "inline",
515
517
  temporary: true,
518
+ maxLocksOverride: functionSpec.maxLocksOverride,
516
519
  watchFunction: function runCallWatcher() {
517
520
  runCount++;
518
521
  if (PathFunctionRunner.DEBUG_CALLS) {
@@ -560,6 +563,7 @@ export class PathFunctionRunner {
560
563
  || syncedSpec.FilePath !== functionSpec.FilePath
561
564
  || syncedSpec.gitRef !== functionSpec.gitRef
562
565
  || syncedSpec.gitURL !== functionSpec.gitURL
566
+ || syncedSpec.maxLocksOverride !== functionSpec.maxLocksOverride
563
567
  )
564
568
  ) {
565
569
  isFunctionSpecOutdated = true;
@@ -20,6 +20,7 @@ import path from "path";
20
20
  import { isEmpty } from "../misc";
21
21
  import { getSpecFromModule } from "./pathFunctionLoader";
22
22
  import { isNode } from "typesafecss";
23
+ import { Querysub } from "../4-querysub/QuerysubController";
23
24
 
24
25
  // This is the the function id which should be used when creating the FunctionSpec (in order to load the module),
25
26
  // to access the permissions in the schema.
@@ -45,6 +46,9 @@ export type FunctionMetadata<F = unknown> = {
45
46
  * before writing to B.
46
47
  */
47
48
  delayCommit?: boolean;
49
+
50
+ /** Too many locks can lag the server, and eventually cause crashes. Consider using Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. However, if absolutely required, you can override max locks to allow as many locks to be created as you want until the server crashses... */
51
+ maxLocksOverride?: number;
48
52
  };
49
53
 
50
54
  export interface SchemaObject<Schema = any, Functions = any> {
@@ -25,9 +25,6 @@ export async function deployGetFunctions(): Promise<FunctionSpec[]> {
25
25
  return functionSpecs;
26
26
  }
27
27
 
28
- //todonext;
29
- // Ugh... some kind of throttling? Nah...
30
- // So... progress sections, just... start and stop?
31
28
  export type DeployProgress = {
32
29
  (config: { section: string; progress: number; }): void;
33
30
  };
@@ -76,6 +76,7 @@ async function main() {
76
76
  const functionIds = Object.keys(schema.rawFunctions);
77
77
  functionIds.push(PERMISSIONS_FUNCTION_ID);
78
78
  for (let functionId of functionIds) {
79
+ let metadata = schema.functionMetadata?.[functionId];
79
80
  const exportPathStr = getExportPath(functionId);
80
81
  let spec: FunctionSpec = {
81
82
  DomainName: domainName,
@@ -85,6 +86,7 @@ async function main() {
85
86
  exportPathStr,
86
87
  gitURL,
87
88
  gitRef,
89
+ maxLocksOverride: metadata?.maxLocksOverride,
88
90
  };
89
91
  console.log(`FUNCTION_SPEC:${JSON.stringify(spec)}`);
90
92
  }
@@ -14,6 +14,9 @@ export const liveHashOverrideURL = new URLParam(liveHashOverrideParam, undefined
14
14
  time: number;
15
15
  });
16
16
 
17
+ declare global {
18
+ var getEdgeNodeConfig: undefined | (() => Promise<EdgeNodeConfig>);
19
+ }
17
20
 
18
21
  let getCachedConfig = cache(async (url: string): Promise<EdgeNodesIndex | undefined> => {
19
22
  setTimeout(() => {
@@ -218,6 +221,8 @@ async function edgeNodeFunction(config: {
218
221
  };
219
222
  }
220
223
 
224
+ globalThis.getEdgeNodeConfig = getEdgeNodeConfig;
225
+
221
226
  let cachedConfig = config.cachedConfig;
222
227
 
223
228
  let liveHashOverride = "";
@@ -2,10 +2,10 @@ import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { Querysub } from "../4-querysub/QuerysubController";
3
3
  import { deploySchema } from "./deploySchema";
4
4
  import { getDomain, noSyncing } from "../config";
5
- import { throttleFunction, timeInMinute } from "socket-function/src/misc";
5
+ import { throttleFunction, timeInMinute, timeInSecond } from "socket-function/src/misc";
6
6
  import { isNode } from "typesafecss";
7
7
  import { logErrors, timeoutToError } from "../errors";
8
- import { blue, red } from "socket-function/src/formatting/logColors";
8
+ import { blue, red, yellow } from "socket-function/src/formatting/logColors";
9
9
  import { showModal } from "../5-diagnostics/Modal";
10
10
  import { qreact } from "../4-dom/qreact";
11
11
  import { liveHashOverrideURL } from "./edgeBootstrap";
@@ -13,6 +13,11 @@ import { css } from "typesafecss";
13
13
  import { formatNiceDateTime, formatTime } from "socket-function/src/formatting/format";
14
14
  import { delay } from "socket-function/src/batching";
15
15
  import { atomicObjectRead } from "../2-proxy/PathValueProxyWatcher";
16
+ import { lazy } from "socket-function/src/caching";
17
+ import { Button } from "../library-components/Button";
18
+ import { updateRootDiscoveryLocation } from "../-f-node-discovery/NodeDiscovery";
19
+
20
+ const SWITCH_SERVER_TIMEOUT = timeInSecond * 15;
16
21
 
17
22
  let lastHashServer = "";
18
23
  export function startEdgeNotifier() {
@@ -29,7 +34,7 @@ export function startEdgeNotifier() {
29
34
  }
30
35
  void notifyClients(liveHash, refreshThresholdTime);
31
36
  });
32
- (async () => {
37
+ void (async () => {
33
38
  await delay(1);
34
39
  let { watchOnRollingUpdate } = await import("../deployManager/machineController");
35
40
  watchOnRollingUpdate({
@@ -119,7 +124,7 @@ function onLiveHashChange(liveHash: string, refreshThresholdTime: number) {
119
124
  >
120
125
  <div className={css.vbox(10).maxWidth(250)}>
121
126
  <h3 className={css.margin(0)}>Server Update Available</h3>
122
- <div>The server has been updated. Please refresh the page to ensure you don't experience incompatibility issues.</div>
127
+ <div>The server has been updated. Please refresh the page to ensure you don't experience compatibility issues.</div>
123
128
  {i !== notifyIntervals.length - 2 && <div>This notification will be shown again at {formatNiceDateTime(Date.now() + waitDuration)}</div>}
124
129
  {i === notifyIntervals.length - 2 && <div>The page will automatically refresh at {formatNiceDateTime(Date.now() + waitDuration)}</div>}
125
130
  </div>
@@ -168,6 +173,7 @@ function onLiveHashChange(liveHash: string, refreshThresholdTime: number) {
168
173
  })();
169
174
  }
170
175
 
176
+
171
177
  const EdgeNotifierClientController = SocketFunction.register(
172
178
  "EdgeNotifierClientController-ed122e41-6ad2-4161-9492-a3f8414bd4f0",
173
179
  new class EdgeNotifierClient {
@@ -188,10 +194,37 @@ if (!isNode()) {
188
194
  setImmediate(() => {
189
195
  void Querysub.optionalStartupWait().finally(async () => {
190
196
  curHash = await EdgeNotifierController.nodes[SocketFunction.browserNodeId()].watchUpdates();
197
+ void runRefreshLoop();
191
198
  });
192
199
  SocketFunction.expose(EdgeNotifierClientController);
193
200
  });
194
201
  }
202
+ const runRefreshLoop = lazy(async () => {
203
+ let getEdgeNodeConfig = globalThis.getEdgeNodeConfig;
204
+ if (!getEdgeNodeConfig) throw new Error(`Missing getEdgeNodeConfig. This means we can't reconnect if the server disconnects. Did we not inject the edge node detection code into the bootstrapper?`);
205
+ let lastAliveTime = Date.now();
206
+ let doNotRefresh = false;
207
+ while (true) {
208
+ await delay(3000);
209
+ if (doNotRefresh) break;
210
+ let timeSinceLastAlive = Date.now() - lastAliveTime;
211
+ if (SocketFunction.isNodeConnected(SocketFunction.browserNodeId())) {
212
+ lastAliveTime = Date.now();
213
+ } else if (timeSinceLastAlive > SWITCH_SERVER_TIMEOUT) {
214
+ lastAliveTime = Date.now();
215
+ console.log(yellow(`Server timed out, finding new server`));
216
+ try {
217
+ let config = await getEdgeNodeConfig();
218
+ updateRootDiscoveryLocation(config);
219
+ } catch (e: any) {
220
+ console.warn(`Failed to find new edge node, trying again in 15 seconds: ${e.message}`);
221
+ await delay(15000);
222
+ }
223
+ } else {
224
+ console.log(yellow(`No edge node connected, trying again in 3 seconds`));
225
+ }
226
+ }
227
+ });
195
228
 
196
229
  class EdgeNotifierControllerBase {
197
230
  public async watchUpdates() {
@@ -17,7 +17,7 @@ import { getHostedIP, getSNICerts, publishMachineARecords } from "../-e-certs/Ed
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";
20
- import { SyncWatcher, proxyWatcher, specialObjectWriteValue, isSynced, PathValueProxyWatcher, atomic, doAtomicWrites, noAtomicSchema, undeleteFromLookup, registerSchemaPrefix, WatcherOptions } from "../2-proxy/PathValueProxyWatcher";
20
+ import { SyncWatcher, proxyWatcher, specialObjectWriteValue, isSynced, PathValueProxyWatcher, atomic, doAtomicWrites, noAtomicSchema, undeleteFromLookup, registerSchemaPrefix, WatcherOptions, doProxyOptions } from "../2-proxy/PathValueProxyWatcher";
21
21
  import { isInProxyDatabase, rawSchema } from "../2-proxy/pathDatabaseProxyBase";
22
22
  import { isValueProxy2, getProxyPath } from "../2-proxy/pathValueProxy";
23
23
  import { getCurrentCallAllowUndefined, getCurrentCall, CallSpec, PathFunctionRunner } from "../3-path-functions/PathFunctionRunner";
@@ -438,6 +438,10 @@ export class Querysub {
438
438
  onNestedSynced(callback);
439
439
  }
440
440
 
441
+ public static noLocks = (callback: () => void) => {
442
+ doProxyOptions({ noLocks: true }, callback);
443
+ };
444
+
441
445
  /** A more powerful version of omCommitFinished, which even waits for call predictions (or tries to).
442
446
  * - Also see afterPredictionsSynced, which runs the callback in a write.
443
447
  * NOTE: IDEALLY, you would just call a series of synced functions. They will be run in order,
@@ -34,7 +34,7 @@ import { PromiseObj } from "../promise";
34
34
  import { LoggingClient } from "../0-path-value-core/LoggingClient";
35
35
  import * as prediction from "./querysubPrediction";
36
36
  import { getCallResultPath } from "./querysubPrediction";
37
- import { pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
37
+ import { nodePathAuthority, pathValueAuthority2 } from "../0-path-value-core/NodePathAuthorities";
38
38
  import { diskLog } from "../diagnostics/logs/diskLogger";
39
39
  import { assertIsManagementUser } from "../diagnostics/managementPages";
40
40
  import { getBrowserUrlNode } from "../-f-node-discovery/NodeDiscovery";
@@ -43,7 +43,7 @@ setFlag(require, "preact", "allowclient", true);
43
43
  import yargs from "yargs";
44
44
  import { mergeFilterables, parseFilterable, serializeFilterable } from "../misc/filterable";
45
45
  import { isManagementUser, onAllPredictionsFinished } from "../-0-hooks/hooks";
46
- import { isBootstrapOnly, isLocal } from "../config";
46
+ import { getDomain, isBootstrapOnly, isLocal } from "../config";
47
47
 
48
48
  let yargObj = isNodeTrue() && yargs(process.argv)
49
49
  .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.` })
@@ -55,40 +55,29 @@ const getFncFilter = lazy(() => parseFilterable(yargObj.fncfilter));
55
55
  export { Querysub, id };
56
56
 
57
57
 
58
- // NOTE: Eventually this will be split by domain, because the underlying getAllNodeIds
59
- // will be split by domain.
60
- // NOTE: We don't need to shard querysub nodes within a domain, because... they don't
61
- // store data, and don't run functions, and only really check permissions (which should be
62
- // very lightweight, hopefully...)
63
- // - I guess we track our watches, but even that... should be fairly light, and... there
64
- // is little benefit to sharding that?
65
- export const querysubNodeId = lazy(() => {
66
- if (isNode()) {
67
- return getControllerNodeId(QuerysubController);
58
+ // TODO: Eventually this will be split by domain, which required nodeIds to be split by domain, so that getControllerNodeId can check nodeIds on a specific domain.
59
+ export async function querysubNodeId() {
60
+ if (isClient()) {
61
+ return nodePathAuthority.getSingleReadNodePromise(getPathStr1(getDomain()));
68
62
  } else {
69
- // NOTE: We won't be able to directly connect to some querysub nodes (because they are from other
70
- // developers), so... just use the browser url.
71
- return getBrowserUrlNode();
63
+ return getControllerNodeId(QuerysubController);
72
64
  }
73
- });
65
+ }
66
+
74
67
 
75
68
  setImmediate(() => {
76
- async function querysubWatchLatest(config: WatchConfig) {
77
- let nodeId = await querysubNodeId();
78
- if (!nodeId) throw new Error("No querysub node found");
79
- await QuerysubController.nodes[nodeId].watch(config);
69
+ async function querysubWatchLatest(config: WatchConfig, authorityId: string) {
70
+ await QuerysubController.nodes[authorityId].watch(config);
80
71
  }
81
- async function querysubUnwatchLatest(config: WatchConfig) {
82
- let nodeId = await querysubNodeId();
83
- if (!nodeId) throw new Error("No querysub node found");
84
- await QuerysubController.nodes[nodeId].unwatch(config);
72
+ async function querysubUnwatchLatest(config: WatchConfig, authorityId: string) {
73
+ await QuerysubController.nodes[authorityId].unwatch(config);
85
74
  }
86
75
 
87
76
  let baseRemoteWatchFunction = RemoteWatcher.REMOTE_WATCH_FUNCTION;
88
77
  RemoteWatcher.REMOTE_WATCH_FUNCTION = async (config, authorityId) => {
89
78
  let weTrusted = isServer() && await errorToUndefined(isTrustedByNode(authorityId));
90
79
  if (!weTrusted) {
91
- await querysubWatchLatest(config);
80
+ await querysubWatchLatest(config, authorityId);
92
81
  } else {
93
82
  await baseRemoteWatchFunction(config, authorityId);
94
83
  }
@@ -98,7 +87,7 @@ setImmediate(() => {
98
87
  RemoteWatcher.REMOTE_UNWATCH_FUNCTION = async (config, authorityIdBase) => {
99
88
  let weTrusted = isServer() && await isTrustedByNode(authorityIdBase);
100
89
  if (!weTrusted) {
101
- logErrors(querysubUnwatchLatest(config));
90
+ logErrors(querysubUnwatchLatest(config, authorityIdBase));
102
91
  } else {
103
92
  logErrors(baseRemoteUnwatchFunction(config, authorityIdBase));
104
93
  }
@@ -313,7 +313,7 @@ async function auditSpecificPathsBase(config: {
313
313
  }
314
314
  sort(logObjs, x => x.log.time);
315
315
  let now = Date.now();
316
- for (let { source, log } of logObjs) {
316
+ for (let { source, log } of logObjs.slice(-100)) {
317
317
  console.log(`${source.padEnd(8, " ")} ${log.type.padEnd(20, " ")}`, now - log.time, log.values, log.time);
318
318
  }
319
319
  debugbreak(2);
@@ -106,7 +106,7 @@ async function registerNodeForMachineCleanup(nodeId: string) {
106
106
  console.log(green(`Registering node for machine cleanup at ${nodeIdPath}`));
107
107
  await fs.promises.writeFile(nodeIdPath, nodeId);
108
108
  } else {
109
- console.log(red(`Not registering node for machine cleanup because we are not in the service folder: ${currentPath}`));
109
+ console.log(`Not registering node for machine cleanup because we are not in the service folder: ${currentPath}`);
110
110
  }
111
111
  }
112
112
 
@@ -1,16 +1,11 @@
1
- TODO: It seems like every other time it fails to reload?
1
+ // updateRootDiscoveryLocation("127-0-0-1.querysubtest.com:7008")
2
+
3
+ Get updateRootDiscoveryLocation called when we disconnect, and don't reconnect?
2
4
 
3
- Uh... we get a failure in commitWrites, and then we never receive the path values we need?
4
- - Although we only don't receive the values we need once, so... maybe the failed check is okay?
5
5
 
6
- 1) Verify clientside notifications work when we rolling update cyoa
7
- 2) Verify edge nodes are removed when we rolling update cyoa
8
- - Eh... it removes most of the time?
9
- 3) Update cyoa rolling time back to 4 hours
10
6
 
11
- 1) Auto restart if the connect to the cyoa server is down, after 30 seconds, instead of just infinitely erroring out.
12
- - Give a notification after 15 seconds warning them, with the ability to not restart if they click "do not restart"
13
- - In edgeClientWatcher
7
+ 1) Update everything to latest
8
+ 3) Update cyoa rolling time back to 4 hours
14
9
 
15
10
 
16
11
  9) In machine info, support an object which can show current, and max, so we can show it as a bar and the max amount for:
@@ -25,5 +20,4 @@ Uh... we get a failure in commitWrites, and then we never receive the path value
25
20
  (Yes, it overlaps with node metrics, but that's okay, sometimes we want to look at machines, other times services, and the types of errors that show up in either changes).
26
21
 
27
22
 
28
- 11) Restart the server and verify everything starts up nicely on reboot!
29
- - Or... screw it, and move onto audio apis...
23
+ 11) A spinning animation on the FPS display, so we can visually see when the browser locks up (because the animation will stop?)
@@ -344,7 +344,8 @@ export class ErrorLogControllerBase {
344
344
  if (ErrorLogControllerBase.logIssueNotifyEquals(prev, notify)) {
345
345
  return;
346
346
  }
347
- console.log(magenta(`Received log notify from ${caller.nodeId} for ${notify?.oldestUngrouped?.message}`));
347
+ // Don't log when we receive it, otherwise errors show up on all servers which is very confusing and annoying!
348
+ //console.log(magenta(`Received log notify from ${caller.nodeId} for ${notify?.oldestUngrouped?.message}`));
348
349
  ErrorLogControllerBase.liveLogsIssueNotify.set(caller.nodeId, notify);
349
350
  logErrors(ErrorLogControllerBase.recalcAllNotify());
350
351
  }
@@ -89,9 +89,14 @@ export class ObjectDisplay extends qreact.Component<{
89
89
  </>;
90
90
  };
91
91
 
92
+ function log() {
93
+ (globalThis as any).debugValue = value;
94
+ console.log("debugValue=", value);
95
+ }
96
+
92
97
  if (!canHaveChildren(value)) {
93
98
  return <ShowMore maxHeight={maxHeight} className={css.maxWidth("50vw")}>
94
- <span className={css.maxHeight("70vh").overflowAuto}>
99
+ <span className={css.maxHeight("70vh").overflowAuto} onClick={log}>
95
100
  <div class={css.relative}>
96
101
  {renderValue(value, [])}
97
102
  <MeasureHeightCSS />
@@ -103,7 +108,7 @@ export class ObjectDisplay extends qreact.Component<{
103
108
  css.hbox(6).alignItems("start")
104
109
  + (!expanded && css.maxHeight(maxHeight).overflowHidden)
105
110
  + (expanded && css.maxHeight("70vh").overflowAuto)
106
- }>
111
+ } onClick={log}>
107
112
  {canHaveChildren(value) && <Button
108
113
  className={
109
114
  css.hsl(270, 50, 50).background("hsl(270, 50%, 70%)", "hover")
@@ -135,7 +140,7 @@ export class PrimitiveDisplay extends qreact.Component<{
135
140
  return <span className={css.boldStyle.hslcolor(265, 70, 60)}>{value}</span>;
136
141
  }
137
142
  if (typeof value === "boolean") {
138
- return <span className={css.boldStyle.hslcolor(207, 70, 60)}>{value}</span>;
143
+ return <span className={css.boldStyle.hslcolor(207, 70, 60)}>{JSON.stringify(value)}</span>;
139
144
  }
140
145
 
141
146
  if (typeof value === "string") {
@@ -318,7 +318,7 @@ class ManagementRoot extends qreact.Component {
318
318
  }
319
319
  `}
320
320
  </style>
321
- <div class={css.fillWidth.hbox(30).wrap.hsl(245, 25, 60).pad2(10).pointerEvents("all")}>
321
+ <div class={css.fillWidth.hbox(30, 10).wrap.hsl(245, 25, 60).pad2(10).pointerEvents("all")}>
322
322
  <LogNotify />
323
323
  {pages.map(page =>
324
324
  <ATag values={[{ param: managementPageURL, value: page.componentName }]}>{page.title}</ATag>