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.
|
|
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.
|
|
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
|
-
|
|
511
|
+
await setupPipePane({ screenName, folder, pipeName: config.pipeName || "pipe" });
|
|
512
|
+
});
|
|
509
513
|
|
|
510
|
-
|
|
511
|
-
|
|
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 {
|