querysub 0.473.0 → 0.475.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.
@@ -24,7 +24,8 @@
24
24
  "mcp__node-debugger__listBreakpoints",
25
25
  "mcp__node-debugger__removeBreakpoint",
26
26
  "mcp__hottest__runTest",
27
- "Bash(yarn test *)"
27
+ "Bash(yarn test *)",
28
+ "Bash(yarn ssh-a-claude *)"
28
29
  ]
29
30
  }
30
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.473.0",
3
+ "version": "0.475.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",
@@ -23,7 +23,8 @@
23
23
  "mcp": "yarn typenode ./src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts",
24
24
  "mc": "yarn typenode ./src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts --cwd D:/repos/qs-cyoa/",
25
25
  "mcp2": "yarn typenode ./src/diagnostics/debugger/mcp-server.ts",
26
- "mc2": "yarn typenode ./src/diagnostics/debugger/mcp-server.ts --cwd D:/repos/qs-cyoa/"
26
+ "mc2": "yarn typenode ./src/diagnostics/debugger/mcp-server.ts --cwd D:/repos/qs-cyoa/",
27
+ "ssh-a-claude": "ssh root@a.querysubtest.com"
27
28
  },
28
29
  "bin": {
29
30
  "deploy": "./bin/deploy.js",
@@ -66,7 +67,7 @@
66
67
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
67
68
  "pako": "^2.1.0",
68
69
  "peggy": "^5.0.6",
69
- "socket-function": "^1.1.35",
70
+ "socket-function": "^1.1.36",
70
71
  "terser": "^5.31.0",
71
72
  "typesafecss": "^0.29.0",
72
73
  "yaml": "^2.5.0",
@@ -505,6 +505,8 @@ export class ArchivesBackblaze {
505
505
  return api;
506
506
  });
507
507
 
508
+ private currentReset: Promise<void> | undefined;
509
+
508
510
  // Keep track of when we last reset because of a 503
509
511
  private last503Reset = 0;
510
512
  // IMPORTANT! We must always CATCH AROUND the apiRetryLogic, NEVER inside of fnc. Otherwise we won't
@@ -532,14 +534,17 @@ export class ArchivesBackblaze {
532
534
  ) && Date.now() - this.last503Reset > 60 * 1000) {
533
535
  console.error(`[${context}] 503 error, waiting and resetting: ${err.message}`);
534
536
  this.log(`[${context}] 503 error, waiting and resetting: ${err.message}`);
535
- await delay(10 * 1000);
536
- // We check again in case, and in the very likely case that this is being run in parallel, we only want to reset once.
537
- if (Date.now() - this.last503Reset > 60 * 1000) {
538
- this.log(`[${context}] Resetting getAPI and getBucketAPI: ${err.message}`);
539
- this.last503Reset = Date.now();
540
- getAPI.reset();
541
- this.getBucketAPI.reset();
542
- }
537
+ this.currentReset = this.currentReset || (async () => {
538
+ await delay(10 * 1000);
539
+ // We check again in case, and in the very likely case that this is being run in parallel, we only want to reset once.
540
+ if (Date.now() - this.last503Reset > 60 * 1000) {
541
+ this.log(`[${context}] Resetting getAPI and getBucketAPI: ${err.message}`);
542
+ this.last503Reset = Date.now();
543
+ getAPI.reset();
544
+ this.getBucketAPI.reset();
545
+ }
546
+ })().finally(() => this.currentReset = undefined);
547
+ await this.currentReset;
543
548
  return this.apiRetryLogic(context, fnc, retries - 1);
544
549
  }
545
550
 
@@ -20,6 +20,7 @@ import { red } from "socket-function/src/formatting/logColors";
20
20
  import { isNode } from "typesafecss";
21
21
  import { areNodeIdsEqual, getOwnNodeId, getOwnThreadId } from "../-f-node-discovery/NodeDiscovery";
22
22
  import { timeInMinute } from "socket-function/src/misc";
23
+ import { isClient, isServer } from "../config2";
23
24
 
24
25
  // NOTE: This used to be small, but we cache this, so it would mean a node on startup would time out, and then we would refuse to talk to it ever again. So... this can't be small
25
26
  const MAX_CHANGE_IDENTITY_TIMEOUT = timeInMinute * 5;
@@ -239,8 +240,8 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
239
240
  cert: threadKeyCert.cert.toString(),
240
241
  certIssuer: issuer.cert.toString(),
241
242
  mountedPort: getNodeIdLocation(SocketFunction.mountedNodeId)?.port,
242
- debugEntryPoint: isNode() ? process.argv[1] : "browser",
243
- clientIsNode: isNode(),
243
+ debugEntryPoint: isServer() ? process.argv[1] : "browser",
244
+ clientIsNode: isServer(),
244
245
  };
245
246
  let signature = sign(threadKeyCert, payload);
246
247
  await timeoutToError(
@@ -229,6 +229,7 @@ function onNodesChanged() {
229
229
  }
230
230
 
231
231
  let rootDiscoveryNodeId = "";
232
+ /** IMPORTANT! This domain import is the querysub server. Usually this is going to be a local development server. However, you can use Node Discovery and use whatever Node you want. You don't need to know the node's machine ID. You just need to know its address. */
232
233
  export function configRootDiscoveryLocation(config: {
233
234
  domain: string;
234
235
  port: number;
@@ -337,11 +338,13 @@ async function syncArchives() {
337
338
  console.info(`Synced node ids from archives`, { nodeIds });
338
339
  await setNodeIds(nodeIds);
339
340
  } else {
340
- if (isNoNetwork() || !isNode()) {
341
+ if (isNoNetwork() || !isNode() || rootDiscoveryNodeId) {
341
342
  // NOTE: If no network, our trust source might be different, so we can't talk to regular nodes,
342
343
  // and instead have to only talk to HTTP nodes
343
344
  await setNodeIds([getBrowserUrlNode()]);
344
345
  } else {
346
+ // NOTE: I don't think this pathway works. But in theory it should it should be possible for us in nodejs to automatically choose the server we want to talk to. In practice, it probably doesn't matter right now, as we're mostly just using this for development, in which case we want to force it to use the local server anyway, which will set root discovery node ID.
347
+
345
348
  // If on the network, NetworkTrust2 should sync the trusted machines from backblaze, so we should be
346
349
  // able to talk to any nodes.
347
350
  // - If they user is using --client they only want to talk to querysub nodes. There might be multiple,
@@ -575,17 +578,33 @@ if (isServer()) {
575
578
  } else {
576
579
 
577
580
  if (isNode()) {
578
- discoveryReady.resolve();
579
- nodeBroadcasted.resolve();
580
- // Just get the archives, syncing again if we haven't synced in a while
581
- let lastGetTime = 0;
582
- beforeGetNodeAllId = async () => {
583
- let lastGetThreshold = lastGetTime + CLIENTSIDE_POLL_RATE;
584
- if (Date.now() > lastGetThreshold) {
585
- lastGetTime = Date.now();
586
- await syncArchives();
587
- }
588
- };
581
+ if (rootDiscoveryNodeId) {
582
+ let nodes = [rootDiscoveryNodeId];
583
+ allNodeIds2 = new Set(nodes);
584
+ discoveryReady.resolve();
585
+ nodeBroadcasted.resolve();
586
+
587
+ // NOTE: We run into TLS issues (as in, our servers use self signed certs), if we try to talk to just
588
+ // any node, so... we better just talk to the edge node
589
+ // - We COULD probably just use some special domain (maybe JUST the machine domain?), with limited wildcard
590
+ // certs (I think we can only wildcard a single depth anyways), and A records for the machines too...
591
+ // but... having all traffic route through an edge node is probably better anyways...
592
+ nodeOverrides = nodes;
593
+
594
+ } else {
595
+ // NOTE: I don't think this pathway works. But in theory it should it should be possible for us in nodejs to automatically choose the server we want to talk to. In practice, it probably doesn't matter right now, as we're mostly just using this for development, in which case we want to force it to use the local server anyway, which will set root discovery node ID.
596
+ discoveryReady.resolve();
597
+ nodeBroadcasted.resolve();
598
+ // Just get the archives, syncing again if we haven't synced in a while
599
+ let lastGetTime = 0;
600
+ beforeGetNodeAllId = async () => {
601
+ let lastGetThreshold = lastGetTime + CLIENTSIDE_POLL_RATE;
602
+ if (Date.now() > lastGetThreshold) {
603
+ lastGetTime = Date.now();
604
+ await syncArchives();
605
+ }
606
+ };
607
+ }
589
608
  } else {
590
609
  setImmediate(() => {
591
610
  let edgeNode = getBootedEdgeNode();
@@ -111,7 +111,7 @@ export class RemoteWatcher {
111
111
  // NOTE: We keep around the old authorities in routing (for a few minutes), so if a new node doesn't start,
112
112
  // we will match the disconnected node again. This means we have a few minutes to start another server.
113
113
  // TODO: Retry instead of throwing
114
- console.log(yellow(`Trying to find new authority for disconnected watches ${paths.length} paths and ${parentPaths.length} parent paths`));
114
+ console.log(yellow(`Trying to find new authority for disconnected watches ${paths.length} paths and ${parentPaths.length} parent paths`), { authorityId });
115
115
  logErrors(this.tryToReconnectNow());
116
116
  })());
117
117
  // Reconnection is our loop running again, maybe matching the same nodeId, and then using it again.
@@ -1957,6 +1957,10 @@ export class PathValueProxyWatcher {
1957
1957
  public runOnce<Result = void>(
1958
1958
  options: Omit<WatcherOptions<Result>, "onResultUpdated" | "onWriteCommitted">
1959
1959
  ): Result {
1960
+ // TODO: This is a recent change. We maybe should apply the user pass and options. However, we definitely don't want to call create watcher as that will apply our additional options, making it temporary, where this might actually be run inside of a non-temporary watcher like a render function. Which finally will break on commit finish because it'll think it's in a temporary function and subscribe to the wrong callback.
1961
+ if (this.inWatcher()) {
1962
+ return options.watchFunction();
1963
+ }
1960
1964
  let result: { result: Result } | { error: string } | undefined;
1961
1965
  let watcher = this.createWatcher({
1962
1966
  ...options,
@@ -126,18 +126,52 @@ export const setGitRef = measureWrap(async function setGitRef(config: {
126
126
  gitRef: string;
127
127
  }) {
128
128
  await fs.promises.mkdir(config.gitFolder, { recursive: true });
129
- let hostKey = await runPromise(`ssh-keyscan -t rsa bitbucket.org`);
130
- hostKey = hostKey.split("\n").filter(x => !x.startsWith("#")).join("\n");
131
- let knownHostsPath = os.homedir() + "/.ssh/known_hosts";
132
- if (!fs.existsSync(knownHostsPath) || !fs.readFileSync(knownHostsPath).toString().includes(hostKey)) {
133
- fs.appendFileSync(knownHostsPath, "\n" + hostKey + "\n");
129
+ // ssh-keyscan is a network call; a transient failure here must not abort the sync. The host key is normally already in known_hosts from machine setup, and callers only escalate to clobbering the checkout on *repo* failures — not on a momentary inability to refresh a host key.
130
+ try {
131
+ let hostKey = await runPromise(`ssh-keyscan -t rsa bitbucket.org`);
132
+ hostKey = hostKey.split("\n").filter(x => !x.startsWith("#")).join("\n");
133
+ let knownHostsPath = os.homedir() + "/.ssh/known_hosts";
134
+ if (hostKey && (!fs.existsSync(knownHostsPath) || !fs.readFileSync(knownHostsPath).toString().includes(hostKey))) {
135
+ fs.appendFileSync(knownHostsPath, "\n" + hostKey + "\n");
136
+ }
137
+ } catch (e: any) {
138
+ console.warn(`ssh-keyscan for bitbucket.org failed, continuing: ${e.stack ?? e}`);
134
139
  }
135
140
 
136
141
  await runPromise(`git remote update`, { cwd: config.gitFolder });
137
142
  await runPromise(`git add --all`, { cwd: config.gitFolder });
138
- await runPromise(`git stash`, { cwd: config.gitFolder });
143
+ // nothrow: on a freshly re-initialized repo (rebuildGitFolder) there is no initial commit yet, so `git stash` errors. That's harmless — the `git reset --hard` below is what actually forces the working tree to the target ref.
144
+ await runPromise(`git stash`, { cwd: config.gitFolder, nothrow: true });
139
145
  await runPromise(`git fetch --all`, { cwd: config.gitFolder });
140
146
  await runPromise(`git reset --hard ${config.gitRef}`, { cwd: config.gitFolder });
141
147
  // Allows us to remove deleted objects from storage, ex, if we accidentally commit a 1GB, this deletes it, so we don't have to fix each server individually. Also I think the repo breaks if we go too long without pruning it?
142
148
  await runPromise(`git prune`, { cwd: config.gitFolder });
149
+ });
150
+
151
+ /** Recovers a broken repo WITHOUT disturbing the working-tree files. We delete only .git, re-init, re-add the remote, then let setGitRef's `git reset --hard` reconcile the tracked files to the ref. Untracked / gitignored files (notably node_modules, which is expensive to reinstall and may be open in a running service) are left exactly as they are. When the directory is empty this produces the same result as a fresh clone. */
152
+ export const rebuildGitFolder = measureWrap(async function rebuildGitFolder(config: {
153
+ gitFolder: string;
154
+ repoUrl: string;
155
+ gitRef: string;
156
+ }) {
157
+ await fs.promises.mkdir(config.gitFolder, { recursive: true });
158
+ await fs.promises.rm(config.gitFolder + ".git", { recursive: true, force: true });
159
+ await runPromise(`git init`, { cwd: config.gitFolder });
160
+ await runPromise(`git remote add origin ${config.repoUrl}`, { cwd: config.gitFolder });
161
+ await setGitRef({ gitFolder: config.gitFolder, gitRef: config.gitRef });
162
+ });
163
+
164
+ /** Last-resort recovery for a working tree git can't reconcile (rebuildGitFolder failed). Clones fresh into a sibling temp directory and swaps it in only after the clone and ref-set succeed, so a failed/interrupted clone can never leave the live folder empty — the failure mode that previously stranded running services with a deleted node_modules. This necessarily discards node_modules; the caller reinstalls it. */
165
+ export const nuclearReclone = measureWrap(async function nuclearReclone(config: {
166
+ gitFolder: string;
167
+ repoUrl: string;
168
+ gitRef: string;
169
+ }) {
170
+ let target = config.gitFolder.replace(/[\/\\]+$/, "");
171
+ let tempDir = target + ".reclone-" + Date.now();
172
+ await fs.promises.rm(tempDir, { recursive: true, force: true });
173
+ await runPromise(`git clone ${config.repoUrl} ${tempDir}`);
174
+ await setGitRef({ gitFolder: tempDir + "/", gitRef: config.gitRef });
175
+ await fs.promises.rm(target, { recursive: true, force: true });
176
+ await fs.promises.rename(tempDir, target);
143
177
  });
@@ -1053,7 +1053,7 @@ export class Querysub {
1053
1053
 
1054
1054
  public static async hostService(name: string, port = 0) {
1055
1055
  if (isClient()) {
1056
- throw new Error(`--client processes cannot host a service. Either stop passing --client and keep the process on the network and trusted, or stop calling hostServer and call Querysub.configRootDiscoveryLocation instead.`);
1056
+ throw new Error(`--client processes cannot host a service. Either stop passing --client and keep the process on the network and trusted, or stop calling hostServer and call Querysub.configRootDiscoveryLocation instead. You MUST provide configRootDiscoveryLocation a valid nodeId. Which means either you do server selection manually, or if you are developing, just point it to "127-0-0-1.${getDomain()}:your local port here"`);
1057
1057
  }
1058
1058
  let times: {
1059
1059
  name: string;
@@ -403,6 +403,7 @@ function getFunctionSpec(call: CallSpec): FunctionSpec | undefined {
403
403
  ModuleId: call.ModuleId,
404
404
  FunctionId: call.FunctionId,
405
405
  });
406
+ if (!obj && !Querysub.isAllSynced()) return undefined;
406
407
  if (!obj) throw new Error(`Function not referenced in deploy.ts ${call.DomainName}.${call.ModuleId}.${call.FunctionId}`);
407
408
  setGitURLMapping({ spec: obj.functionSpec, resolvedPath: obj.modulePath });
408
409
  return obj.functionSpec;
package/src/config.ts CHANGED
@@ -5,6 +5,7 @@ import { MaybePromise } from "socket-function/src/types";
5
5
  import { parseArgsFactory } from "./misc/rawParams";
6
6
  import { lazy } from "socket-function/src/caching";
7
7
  import fs from "fs";
8
+ import { SocketFunction } from "socket-function/SocketFunction";
8
9
 
9
10
  export const serverPort = 11748;
10
11
 
@@ -35,6 +36,12 @@ let yargObj = parseArgsFactory()
35
36
  .argv
36
37
  ;
37
38
 
39
+ if (isNode()) {
40
+ if (yargObj.client) {
41
+ SocketFunction.ENABLE_CLIENT_MODE = true;
42
+ }
43
+ }
44
+
38
45
  export function getRawFncFilter() {
39
46
  return yargObj.fncfilter;
40
47
  }
@@ -13,12 +13,12 @@ import { formatTime } from "socket-function/src/formatting/format";
13
13
  import { sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
14
14
  import { isDefined } from "../misc";
15
15
  import { logLoadTime } from "../logModuleLoadTimes";
16
- import { delay, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
16
+ import { delay, retryFunctional, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
17
17
  import os from "os";
18
18
  import fs from "fs";
19
19
  import { spawn, ChildProcess } from "child_process";
20
20
  import { lazy } from "socket-function/src/caching";
21
- import { getGitRefLive, getGitURLLive, setGitRef } from "../4-deploy/git";
21
+ import { getGitRefLive, getGitURLLive, setGitRef, rebuildGitFolder, nuclearReclone } from "../4-deploy/git";
22
22
  import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
23
23
  import { shutdown } from "../diagnostics/periodic";
24
24
  import { onServiceConfigChange, triggerRollingUpdate } from "./machineController";
@@ -607,24 +607,53 @@ const killScreen = measureWrap(async function killScreen(config: {
607
607
  await runPromise(`${prefix}tmux kill-session -t ${config.screenName}`);
608
608
  await removeOldNodeId(config.screenName);
609
609
  });
610
+ // When a present repo fails to sync, retry a few times with backoff before deciding it is actually broken — most failures are transient network blips (ssh-keyscan / fetch), and reacting to those by clobbering the checkout is what stranded running services (they kept running but crashed on the next lazy `require("ws")` once node_modules was gone).
611
+ const GIT_SYNC_MAX_RETRIES = 4;
612
+ const GIT_SYNC_RETRY_MIN_DELAY = timeInSecond * 2;
613
+ const GIT_SYNC_RETRY_MAX_DELAY = timeInSecond * 30;
614
+
610
615
  const ensureGitSynced = measureWrap(async function ensureGitSynced(config: {
611
- // Ensures existingFolder is the repo, as in, cwd to it, and git clone ., if needed. Reset the hash, in whatever way we need to, to force it to gitRef (git add --all and stash, etc, etc).
612
- // - Maybe ask the AI the best way to forcefully switch a git repo.
616
+ // Forces the checkout at gitFolder to gitRef. Escalates through increasingly destructive recoveries, but only ever destroys the working tree as an absolute last resort a network blip must never wipe node_modules out from under a running service.
613
617
  gitFolder: string;
614
618
  repoUrl: string;
615
619
  gitRef: string;
616
620
  }) {
617
- try {
618
- if (!await fsExistsAsync(config.gitFolder + ".git")) {
619
- await runPromise(`git clone ${config.repoUrl} ${config.gitFolder}`);
621
+ let hasGit = await fsExistsAsync(config.gitFolder + ".git");
622
+
623
+ // Repo present: sync to the ref, retrying transient failures with backoff. We do NOT clobber the checkout just because a network call blipped.
624
+ if (hasGit) {
625
+ try {
626
+ await retryFunctional(() => setGitRef(config), {
627
+ maxRetries: GIT_SYNC_MAX_RETRIES,
628
+ minDelay: GIT_SYNC_RETRY_MIN_DELAY,
629
+ maxDelay: GIT_SYNC_RETRY_MAX_DELAY,
630
+ })();
631
+ return;
632
+ } catch (e: any) {
633
+ console.error(`Failed to sync git ref after ${GIT_SYNC_MAX_RETRIES} retries; rebuilding .git in place next (working tree, incl. node_modules, is preserved).`, {
634
+ gitFolder: config.gitFolder,
635
+ repoUrl: config.repoUrl,
636
+ gitRef: config.gitRef,
637
+ error: e?.stack ?? String(e),
638
+ });
620
639
  }
621
- await setGitRef(config);
640
+ }
641
+
642
+ // Repo missing, or present-but-unsyncable: rebuild just the .git metadata and let `git reset --hard` reconcile the tracked files. The working-tree files (node_modules) are left untouched. Equivalent to a clone when the folder is empty.
643
+ try {
644
+ await rebuildGitFolder(config);
645
+ return;
622
646
  } catch (e: any) {
623
- console.warn(`Failed to set git ref, trying to delete and clone again (${config.gitFolder}): ${e.stack}`);
624
- await fs.promises.rm(config.gitFolder, { recursive: true });
625
- await runPromise(`git clone ${config.repoUrl} ${config.gitFolder}`);
626
- await setGitRef(config);
647
+ console.error(`Rebuilding .git in place failed; falling back to a full delete + clone (this wipes node_modules, which the caller reinstalls).`, {
648
+ gitFolder: config.gitFolder,
649
+ repoUrl: config.repoUrl,
650
+ gitRef: config.gitRef,
651
+ error: e?.stack ?? String(e),
652
+ });
627
653
  }
654
+
655
+ // Nuclear: the working tree itself is unusable. Clone fresh (into a temp dir, swapped in only on success).
656
+ await nuclearReclone(config);
628
657
  });
629
658
 
630
659
 
@@ -710,8 +739,10 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
710
739
  gitRef: config.parameters.gitRef,
711
740
  });
712
741
  let afterGitRef = await getGitRefLive(gitFolder);
713
- if (afterGitRef !== prevGitRef) {
714
- console.log(green(`Yarn installing because git ref changed from ${prevGitRef} to ${afterGitRef} for ${magenta(screenName)}`));
742
+ // Reinstall when the ref changed OR node_modules is missing. The latter is the real fix for the "Cannot find module 'ws'" crash: a recovery re-clone can land on the same commit, so a ref-only check would skip the install and leave the service with no node_modules. Restoring node_modules also self-heals an already-running process, since a failed `require` is never cached and the next reconnect re-resolves it.
743
+ let nodeModulesMissing = !await fsExistsAsync(gitFolder + "node_modules");
744
+ if (afterGitRef !== prevGitRef || nodeModulesMissing) {
745
+ console.log(green(`Yarn installing for ${magenta(screenName)} (ref ${prevGitRef} -> ${afterGitRef}, nodeModulesMissing=${nodeModulesMissing})`));
715
746
  await runPromise(`yarn install`, { cwd: gitFolder });
716
747
  }
717
748
  }
@@ -1,4 +1,4 @@
1
- import { atomic, atomicObjectWrite, atomicObjectWriteNoFreeze, doAtomicWrites, isTransparentValue, proxyWatcher, specialObjectWriteValue } from "../../src/2-proxy/PathValueProxyWatcher";
1
+ import { atomic, atomicObjectWrite, atomicObjectWriteNoFreeze, doAtomicWrites, doProxyOptions, isTransparentValue, proxyWatcher, specialObjectWriteValue } from "../../src/2-proxy/PathValueProxyWatcher";
2
2
  import { Querysub } from "../../src/4-querysub/Querysub";
3
3
  import { SocketFunction } from "socket-function/SocketFunction";
4
4
  import { FullCallType, SocketRegistered } from "socket-function/SocketFunctionTypes";
@@ -91,6 +91,25 @@ const writeWatchers = new Map<string, {
91
91
  fncName: string;
92
92
  }[]>();
93
93
 
94
+ export function asyncCache<T>(fnc: () => Promise<T>): (...args: any[]) => T | undefined {
95
+ return getSyncedController({
96
+ nodes: {
97
+ "": {
98
+ fnc,
99
+ },
100
+ },
101
+ })("").fnc;
102
+ }
103
+ export function asyncCacheObj<T>(fncs: T): {
104
+ [key in keyof T]: T[key] extends (...args: any[]) => Promise<infer Return> ? (...args: any[]) => Return | undefined : never;
105
+ } {
106
+ return getSyncedController({
107
+ nodes: {
108
+ "": fncs as any,
109
+ },
110
+ })("") as any;
111
+ }
112
+
94
113
  export function getSyncedController<T extends {
95
114
  nodes: {
96
115
  [key: string]: {
@@ -274,12 +293,15 @@ export function getSyncedController<T extends {
274
293
  });
275
294
  // We have to wait until we actually commit before making the call. Otherwise, if this commit is rejected because we need to do synchronized values, we'll call the function twice.
276
295
  let fnc = controller.nodes[nodeId][fncName] as any;
296
+ console.log(`Triggering call ${fncName} with args ${JSON.stringify(args)}`);
277
297
  Querysub.onCommitFinished(() => {
278
- if (Querysub.isAllSynced()) {
279
- void Promise.resolve().then(() => {
280
- doPromiseCall();
281
- });
282
- }
298
+ console.log(`Commit finished for call ${fncName} with args ${JSON.stringify(args)}`);
299
+ // Doesn't on commit finished also implicitly mean we're all synced? Pretty sure that isAll synced is making things break.
300
+ //if (Querysub.isAllSynced()) {
301
+ void Promise.resolve().then(() => {
302
+ doPromiseCall();
303
+ });
304
+ //}
283
305
  });
284
306
  function doPromiseCall() {
285
307
  let promise = fnc(...args) as Promise<unknown>;
@@ -347,7 +369,10 @@ export function getSyncedController<T extends {
347
369
  }
348
370
  }
349
371
  }
372
+ // Force it to commit, so even if we aren't synced, it runs.
373
+ //doProxyOptions({ commitAllRuns: true }, () => {
350
374
  call(...args);
375
+ //});
351
376
  // Assign to itself, to preset the type assumptions typescript makes (otherwise we get an error below)
352
377
  obj = obj as any;
353
378
  if (config?.cachePromiseCalls) {
@@ -70,7 +70,7 @@ export class UserPage extends qreact.Component {
70
70
  `}
71
71
  </style>
72
72
  <div class={css.vbox(20)}>
73
- <div class={css.fontSize(28)}>User Configuration {isViewingOther && <span>({userId})</span>}</div>
73
+ <div class={css.fontSize(28)}>User Configuration {<span>({userId})</span>}</div>
74
74
  {userObj.userId !== userId && <div class={css.fontSize(16)}>(Used does not exist)</div>}
75
75
  <div class={css.vbox(4)}>
76
76
  <div><b>Email</b> {userObj.email}</div>
@@ -532,7 +532,7 @@ export function getCurrentUser(config?: { ignoreImpersonate?: boolean; }): strin
532
532
  let impersonate = !config?.ignoreImpersonate && impersonateURL.value;
533
533
  if (impersonate) return impersonate;
534
534
  }
535
- if (isNode() && Querysub.getCallerIP() === "127.0.0.1" && isRecovery()) {
535
+ if (isNode() && Querysub.getCallerIPAllowUndefined() === "127.0.0.1" && isRecovery()) {
536
536
  return "local";
537
537
  }
538
538
  const machineId = Querysub.getCallerMachineId();
@@ -1121,13 +1121,12 @@ function unbanUser(config: { userId: string }) {
1121
1121
  }
1122
1122
 
1123
1123
  /** Adds the current machine to the shared "service" account, which otherwise is impossible to login as. */
1124
- export async function registerServicePermissions() {
1124
+ export async function nodeJSLoginToUser(userId: string) {
1125
1125
  const machineId = Querysub.getOwnMachineId();
1126
1126
  const ips = [await getExternalIP(), "127.0.0.1", "10.0.0.1"];
1127
1127
  let now = Date.now();
1128
1128
 
1129
1129
  await Querysub.commitSynced(() => {
1130
- const userId = "service";
1131
1130
  if (!(userId in data().users)) {
1132
1131
  data().users[userId] = {
1133
1132
  userId,