querysub 0.538.0 → 0.540.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.538.0",
3
+ "version": "0.540.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",
@@ -71,7 +71,7 @@
71
71
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
72
72
  "pako": "^2.1.0",
73
73
  "peggy": "^5.0.6",
74
- "sliftutils": "^1.7.21",
74
+ "sliftutils": "^1.7.23",
75
75
  "socket-function": "^1.2.26",
76
76
  "terser": "^5.31.0",
77
77
  "typenode": "^6.6.1",
@@ -0,0 +1,109 @@
1
+ import { IArchives, RemoteConfigBase } from "sliftutils/storage/IArchives";
2
+ import { createArchives } from "sliftutils/storage/remoteStorage/createArchives";
3
+ import { cache, cacheJSONArgsEqual } from "socket-function/src/caching";
4
+ import { formatDateTime } from "socket-function/src/formatting/format";
5
+ import { getDomain } from "../config";
6
+
7
+ function createSourceWindows(
8
+ overrides: Partial<RemoteConfigBase>,
9
+ sourceWindows: {
10
+ startTime: number;
11
+ sources: Omit<Exclude<RemoteConfigBase, string>, "validWindow">[];
12
+ }[]
13
+ ) {
14
+ let sources: RemoteConfigBase[] = [];
15
+
16
+ let timeRanges: [number, number][] = [];
17
+ for (let windowIndex = 0; windowIndex < sourceWindows.length; windowIndex++) {
18
+ let startTime = sourceWindows[windowIndex].startTime;
19
+ let endTime = sourceWindows[windowIndex + 1]?.startTime ?? Number.MAX_SAFE_INTEGER;
20
+ if (startTime > endTime) throw new Error(`Source window ${formatDateTime(startTime)} starts after end of ${formatDateTime(endTime)}`);
21
+ timeRanges.push([startTime, endTime]);
22
+ }
23
+
24
+ // NOTE: We scan from everything after us, so I don't believe we need any interlacing here
25
+ for (let windowIndex = 0; windowIndex < sourceWindows.length; windowIndex++) {
26
+ for (let win of sourceWindows[windowIndex].sources) {
27
+ let fullWindow = { ...win, ...overrides as {}, validWindow: timeRanges[windowIndex] };
28
+ sources.push(fullWindow);
29
+ }
30
+ }
31
+ return sources;
32
+ }
33
+
34
+ export function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
35
+ const HETZNER = `https://65-109-93-113.querysubtest.com:5233/file/root/${bucket}/storage/storagerouting.json`;
36
+ const ONTARIO = `https://99-250-124-91.querysubtest.com:5234/file/root/${bucket}/storage/storagerouting.json`;
37
+ const BACKBLAZE = `https://f002.backblazeb2.com/file/querysub-storagetest/${bucket}/storage/storagerouting.json`;
38
+ let sources = createSourceWindows(overrides, [
39
+ {
40
+ startTime: 0,
41
+ sources: [
42
+ { type: "remote", url: HETZNER, route: [0.5, 1], },
43
+ { type: "remote", url: ONTARIO, route: [0, 0.5], },
44
+ { type: "remote", url: HETZNER, },
45
+ { type: "remote", url: ONTARIO, },
46
+ { type: "backblaze", url: BACKBLAZE, },
47
+ ]
48
+ },
49
+ // {
50
+ // startTime: +new Date("2026-07-20 12:00:00-04:00"),
51
+ // sources: [
52
+ // { type: "remote", url: HETZNER, route: [0.5, 1], },
53
+ // { type: "remote", url: ONTARIO, route: [0, 0.5], },
54
+ // { type: "remote", url: HETZNER, },
55
+ // { type: "remote", url: ONTARIO, },
56
+ // { type: "backblaze", url: BACKBLAZE, },
57
+ // ]
58
+ // }
59
+ ]);
60
+
61
+ return createArchives({
62
+ version: 7,
63
+ sources,
64
+ });
65
+ }
66
+
67
+ // TODO: We'll turn on full sync eventually. That way we can be backblaze agnostic. For now we'll leave full sync off so we don't have to download hundreds of gigabytes and potentially change our mind about how we want to store it.
68
+
69
+ const archivesBuilderCache = cacheJSONArgsEqual((domain: string, overrides: Partial<RemoteConfigBase>) => {
70
+ return archiveBuilder(domain, overrides);
71
+ });
72
+ function nestBucket(bucket: string, archives: IArchives): IArchives {
73
+ if (!bucket) return archives;
74
+ if (!bucket.endsWith("/")) {
75
+ bucket = bucket + "/";
76
+ }
77
+ return {
78
+ get: (path: string) => archives.get(bucket + path),
79
+ set: (path: string, data: Buffer) => archives.set(bucket + path, data),
80
+ del: (path: string) => archives.del(bucket + path),
81
+ find: (prefix: string) => archives.find(bucket + prefix),
82
+ findInfo: (prefix: string) => archives.findInfo(bucket + prefix),
83
+ get2: (path: string) => archives.get2(bucket + path),
84
+ setLargeFile: (config: { path: string; getNextData(): Promise<Buffer | undefined>; }) => archives.setLargeFile({ path: bucket + config.path, getNextData: config.getNextData }),
85
+ getInfo: (path: string) => archives.getInfo(bucket + path),
86
+ getDebugName: () => archives.getDebugName() + "/" + bucket,
87
+ hasWriteAccess: () => archives.hasWriteAccess(),
88
+ getConfig: () => archives.getConfig(),
89
+ getURL: (path: string) => archives.getURL(bucket + path),
90
+ };
91
+ }
92
+
93
+ export function getArchives2(bucket: string) {
94
+ return nestBucket(bucket, archivesBuilderCache(getDomain(), { noFullSync: true, }));
95
+ }
96
+ export function getArchives2PrivateImmutable(bucket: string) {
97
+ return nestBucket(bucket, archivesBuilderCache(getDomain() + "-private-immutable", { noFullSync: true, immutable: true }));
98
+ }
99
+ function getAllowedOrigins(domain: string) {
100
+ return [`https://${domain}`, `https://127-0-0-1.${domain}:7007`];
101
+ }
102
+ export function getArchives2PublicImmutable(bucket: string) {
103
+ let domain = getDomain();
104
+ return nestBucket(bucket, archivesBuilderCache(domain + "-public-immutable", { noFullSync: true, immutable: true, public: true, allowedOrigins: getAllowedOrigins(domain) }));
105
+ }
106
+ export function getArchives2Public(bucket: string) {
107
+ let domain = getDomain();
108
+ return nestBucket(bucket, archivesBuilderCache(domain + "-public", { noFullSync: true, public: true, allowedOrigins: getAllowedOrigins(domain) }));
109
+ }
@@ -455,6 +455,8 @@ const runScreenCommand = measureWrap(async function runScreenCommand(config: {
455
455
  command: string;
456
456
  // Defaults to the folder derived from screenName; a future screen passes its canonical screen's folder
457
457
  folder?: string;
458
+ // Defaults to "pipe"; a future screen uses its own pipe file so its output never interleaves with the old screen's
459
+ pipeName?: string;
458
460
  }) {
459
461
  let prefix = getTmuxPrefix();
460
462
  let screenName = config.screenName;
@@ -491,6 +493,7 @@ const runScreenCommand = measureWrap(async function runScreenCommand(config: {
491
493
  screenName,
492
494
  command: config.command,
493
495
  folder: config.folder,
496
+ pipeName: config.pipeName,
494
497
  });
495
498
  return;
496
499
  }
@@ -505,10 +508,14 @@ ${config.command}
505
508
  await fs.promises.writeFile(folder + "command.sh", command);
506
509
  await runPromise(`${prefix}tmux send-keys -t ${screenName} 'bash ../command.sh' Enter`);
507
510
 
508
- // Setup pipe-pane as well
511
+ await setupPipePane({ screenName, folder, pipeName: config.pipeName || "pipe" });
512
+ });
509
513
 
510
- let pipeFile = path.resolve(folder + "pipe.txt");
511
- let pipeScript = path.resolve(folder + "pipe.sh");
514
+ // Writes the rolling-buffer pipe script and (re)attaches the screen's pipe-pane to it. pipeName distinguishes the future screen's log from the canonical one in the shared folder — two screens' outputs must never interleave into one file.
515
+ async function setupPipePane(config: { screenName: string; folder: string; pipeName: string }) {
516
+ let prefix = getTmuxPrefix();
517
+ let pipeFile = path.resolve(config.folder + config.pipeName + ".txt");
518
+ let pipeScript = path.resolve(config.folder + config.pipeName + ".sh");
512
519
  await fs.promises.writeFile(pipeScript, `#!/bin/bash
513
520
  line_count=0
514
521
  while IFS= read -r line; do
@@ -532,8 +539,8 @@ while IFS= read -r line; do
532
539
  fi
533
540
  done`);
534
541
  await runPromise(`chmod +x ${pipeScript}`);
535
- await runPromise(`${prefix}tmux pipe-pane -t ${screenName} 'bash ${pipeScript}'`);
536
- });
542
+ await runPromise(`${prefix}tmux pipe-pane -t ${config.screenName} 'bash ${pipeScript}'`);
543
+ }
537
544
  const killScreen = measureWrap(async function killScreen(config: {
538
545
  screenName: string;
539
546
  // During a takeover the folder's nodeId file already belongs to the NEW process, so the old screen's kill must not remove it
@@ -704,7 +711,7 @@ async function ensureFutureStarted(config: {
704
711
  takeoverOldNodeIds.set(canonicalScreenName, await fs.promises.readFile(nodeIdFile, "utf8"));
705
712
  }
706
713
  console.log(green(`Starting future instance ${magenta(futureScreenName)} (release time reached)`));
707
- await runScreenCommand({ screenName: futureScreenName, folder, command: next.command });
714
+ await runScreenCommand({ screenName: futureScreenName, folder, command: next.command, pipeName: "pipe-future" });
708
715
  }
709
716
 
710
717
  // Kills the old canonical screen (if any) and renames the future screen to the canonical name. The rename leaves the new process completely untouched, so the takeover is seamless. No-op when there is no future screen.
@@ -733,6 +740,15 @@ async function takeoverFutureScreen(config: {
733
740
  await runPromise(`${prefix}tmux rename-session -t ${futureScreenName} ${canonicalScreenName}`);
734
741
  screenStateMap.set(canonicalScreenName, { ...future, screenName: canonicalScreenName });
735
742
  screenStateMap.delete(futureScreenName);
743
+ // The screen changed, so its log is replaced wholesale: the future screen's output becomes the canonical log (never interleaved with the old process's output), and its pipe-pane is re-attached to the canonical pipe file for everything after the rename
744
+ let folder = os.homedir() + "/" + SERVICE_FOLDER + canonicalScreenName + "/";
745
+ try {
746
+ await fs.promises.rename(folder + "pipe-future.txt", folder + "pipe.txt");
747
+ } catch {
748
+ // The future screen produced no output yet; still clear the old screen's log
749
+ await fs.promises.writeFile(folder + "pipe.txt", "");
750
+ }
751
+ await setupPipePane({ screenName: canonicalScreenName, folder, pipeName: "pipe" });
736
752
  }
737
753
 
738
754
  function sameRestartParameters(prevParametersJSON: string, next: ServiceParameters): boolean {