querysub 0.438.0 → 0.439.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 +2 -2
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +37 -1
- package/src/0-path-value-core/pathValueCore.ts +12 -0
- package/src/3-path-functions/PathFunctionRunner.ts +24 -13
- package/src/5-diagnostics/qreactDebug.tsx +26 -3
- package/src/diagnostics/PathDistributionInfo.tsx +9 -1
- package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +1 -1
- package/src/diagnostics/logs/diskLogger.ts +6 -0
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +78 -1
- package/src/library-components/uncaughtToast.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.439.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",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"pako": "^2.1.0",
|
|
63
63
|
"peggy": "^5.0.6",
|
|
64
64
|
"querysub": "^0.357.0",
|
|
65
|
-
"socket-function": "^1.1.
|
|
65
|
+
"socket-function": "^1.1.21",
|
|
66
66
|
"terser": "^5.31.0",
|
|
67
67
|
"typesafecss": "^0.28.0",
|
|
68
68
|
"yaml": "^2.5.0",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { sha256 } from "js-sha256";
|
|
2
2
|
import { getArchives } from "../../-a-archives/archives";
|
|
3
3
|
import { getAllNodeIds, getOwnThreadId } from "../../-f-node-discovery/NodeDiscovery";
|
|
4
|
-
import { pathValueArchives } from "../pathValueArchives";
|
|
4
|
+
import { archives, pathValueArchives } from "../pathValueArchives";
|
|
5
5
|
import { ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../../errors";
|
|
6
6
|
import { green, magenta } from "socket-function/src/formatting/logColors";
|
|
7
7
|
import { devDebugbreak, isPublic } from "../../config";
|
|
8
8
|
import { sort } from "socket-function/src/misc";
|
|
9
9
|
import { lazy } from "socket-function/src/caching";
|
|
10
|
+
import { decodeCborx, encodeCborx } from "../../misc/cloneHelpers";
|
|
11
|
+
import { runInParallel } from "socket-function/src/batching";
|
|
12
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
10
13
|
|
|
11
14
|
const snapshots = lazy(() => getArchives("snapshots"));
|
|
12
15
|
|
|
@@ -140,6 +143,39 @@ async function getSnapshotBase(snapshotFile: string | "live") {
|
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
export interface ArchiveSnapshotPayloadFile {
|
|
147
|
+
path: string;
|
|
148
|
+
data: Buffer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function downloadSnapshot(snapshotFile: string | "live"): Promise<Buffer> {
|
|
152
|
+
let { files } = await getSnapshotBase(snapshotFile);
|
|
153
|
+
let buffers = await pathValueArchives.getRawValueFiles(files, { includeRecycleBin: true });
|
|
154
|
+
let payload: ArchiveSnapshotPayloadFile[] = [];
|
|
155
|
+
for (let i = 0; i < files.length; i++) {
|
|
156
|
+
let data = buffers[i];
|
|
157
|
+
if (!data) continue;
|
|
158
|
+
payload.push({ path: files[i], data });
|
|
159
|
+
}
|
|
160
|
+
return encodeCborx(payload);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function uploadSnapshot(buffer: Buffer): Promise<ArchiveSnapshotOverview> {
|
|
164
|
+
let payload = decodeCborx<ArchiveSnapshotPayloadFile[]>(buffer);
|
|
165
|
+
let liveArchives = archives();
|
|
166
|
+
async function uploadFile(path: string, data: Buffer) {
|
|
167
|
+
let existing = await liveArchives.getInfo(path);
|
|
168
|
+
if (existing) return;
|
|
169
|
+
console.log(`Creating missing file (${formatNumber(data.length)}B): ${path}`);
|
|
170
|
+
await liveArchives.set(path, Buffer.from(data));
|
|
171
|
+
}
|
|
172
|
+
let parallel = runInParallel({
|
|
173
|
+
parallelCount: 32,
|
|
174
|
+
}, uploadFile);
|
|
175
|
+
await Promise.all(payload.map(file => parallel(file.path, file.data)));
|
|
176
|
+
return await saveSnapshot({ files: payload.map(x => x.path) });
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
// EXTREME DANGEROUS! Should only be done when all servers are down (except one, which
|
|
144
180
|
// is run on a developers machine). Worse case scenario, where everything is broken,
|
|
145
181
|
// and you need to just go back to a previous state.
|
|
@@ -394,6 +394,18 @@ export function debugPathValue(pathValue: PathValue, write?: boolean): string {
|
|
|
394
394
|
export function debugTime(time: Time): string {
|
|
395
395
|
return `${time.time}[${time.version}]@${time.creatorId.toString().slice(4, 12)}`;
|
|
396
396
|
}
|
|
397
|
+
export function parseDebugTime(debugTime: string): {
|
|
398
|
+
time: number;
|
|
399
|
+
version: number;
|
|
400
|
+
creatorIdPart: string;
|
|
401
|
+
} {
|
|
402
|
+
let parts = debugTime.split(/[\[\]\@]/g);
|
|
403
|
+
return {
|
|
404
|
+
time: +(parts[0]),
|
|
405
|
+
version: +(parts[1]),
|
|
406
|
+
creatorIdPart: parts[2],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
397
409
|
|
|
398
410
|
|
|
399
411
|
let getCompressNetworkBase = () => false;
|
|
@@ -9,7 +9,7 @@ import { getPathFromStr, getPathIndex } from "../path";
|
|
|
9
9
|
import { rawSchema } from "../2-proxy/pathDatabaseProxyBase";
|
|
10
10
|
import { getProxyPath } from "../2-proxy/pathValueProxy";
|
|
11
11
|
import { atomicObjectRead, atomicObjectWrite, doProxyOptions, isProxyBlockedByOrder, isSynced, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
|
|
12
|
-
import { authorityStorage, compareTime, debugTime, MAX_ACCEPTED_CHANGE_AGE, PathValue, Time } from "../0-path-value-core/pathValueCore";
|
|
12
|
+
import { authorityStorage, compareTime, debugTime, MAX_ACCEPTED_CHANGE_AGE, parseDebugTime, PathValue, Time } from "../0-path-value-core/pathValueCore";
|
|
13
13
|
import { getModuleFromSpec } from "./pathFunctionLoader";
|
|
14
14
|
import debugbreak from "debugbreak";
|
|
15
15
|
import { parseArgs } from "./PathFunctionHelpers";
|
|
@@ -164,6 +164,15 @@ function getDebugName(call: CallSpec, functionSpec: FunctionSpec | undefined, co
|
|
|
164
164
|
return `${mainPart}`;
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
+
export function parseDebugName(debugName: string) {
|
|
168
|
+
let parts = debugName.split("|");
|
|
169
|
+
if (parts.length !== 3) return undefined;
|
|
170
|
+
return {
|
|
171
|
+
file: parts[0],
|
|
172
|
+
functionId: parts[1],
|
|
173
|
+
runAtTime: parseDebugTime(parts[2]),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
167
176
|
|
|
168
177
|
type PermissionsCheckType = {
|
|
169
178
|
new(callerMachineId: { callerMachineId: string; callerIP: string; }): {
|
|
@@ -531,7 +540,9 @@ export class PathFunctionRunner {
|
|
|
531
540
|
return;
|
|
532
541
|
}
|
|
533
542
|
|
|
534
|
-
|
|
543
|
+
let debugNameColored = getDebugName(callSpec, functionSpec, true);
|
|
544
|
+
let debugName = getDebugName(callSpec, functionSpec);
|
|
545
|
+
console.info(`New function call: ${debugNameColored}`, {
|
|
535
546
|
callId: callSpec.CallId,
|
|
536
547
|
timeId: callSpec.runAtTime.time,
|
|
537
548
|
functionId: callSpec.FunctionId,
|
|
@@ -564,13 +575,13 @@ export class PathFunctionRunner {
|
|
|
564
575
|
}
|
|
565
576
|
if (PathFunctionRunner.DEBUG_CALLS) {
|
|
566
577
|
let fraction = getRoutingOverridePart(callSpec.CallId)?.route;
|
|
567
|
-
console.log(`${yellow("Function run fallback")} (primary servery failed to run function) after ${formatTime(secondaryDelay)}. Fraction ${fraction?.toFixed(3)}. Primary ${this.config.shardRange.startFraction.toFixed(3)} to ${this.config.shardRange.endFraction.toFixed(3)}. Secondary ${this.config.secondaryShardRange?.startFraction.toFixed(3)} to ${this.config.secondaryShardRange?.endFraction.toFixed(3)}. for ${
|
|
578
|
+
console.log(`${yellow("Function run fallback")} (primary servery failed to run function) after ${formatTime(secondaryDelay)}. Fraction ${fraction?.toFixed(3)}. Primary ${this.config.shardRange.startFraction.toFixed(3)} to ${this.config.shardRange.endFraction.toFixed(3)}. Secondary ${this.config.secondaryShardRange?.startFraction.toFixed(3)} to ${this.config.secondaryShardRange?.endFraction.toFixed(3)}. for ${debugNameColored}`);
|
|
568
579
|
}
|
|
569
580
|
}
|
|
570
581
|
|
|
571
582
|
PathFunctionRunner.RUN_START_COUNT++;
|
|
572
583
|
if (PathFunctionRunner.DEBUG_CALLS) {
|
|
573
|
-
console.log(`STARTING ${
|
|
584
|
+
console.log(`STARTING ${debugNameColored}`);
|
|
574
585
|
}
|
|
575
586
|
let startTime = Date.now();
|
|
576
587
|
let runCount = 0;
|
|
@@ -618,29 +629,29 @@ export class PathFunctionRunner {
|
|
|
618
629
|
devFunctionMetadata = schemaObj?.functionMetadata?.[functionSpec.FunctionId];
|
|
619
630
|
}
|
|
620
631
|
|
|
621
|
-
console.info(`Running function: ${
|
|
632
|
+
console.info(`Running function: ${debugNameColored}`, {
|
|
622
633
|
callId: callSpec.CallId,
|
|
623
634
|
outerLoop: retries,
|
|
624
635
|
});
|
|
625
636
|
|
|
626
637
|
await proxyWatcher.commitFunction({
|
|
627
638
|
canWrite: true,
|
|
628
|
-
debugName:
|
|
639
|
+
debugName: debugName,
|
|
640
|
+
source: debugName,
|
|
629
641
|
runAtTime: callSpec.runAtTime,
|
|
630
642
|
getPermissionsCheck: PermissionsChecker && (() => new PermissionsChecker(callSpec)),
|
|
631
643
|
nestedCalls: "inline",
|
|
632
644
|
temporary: true,
|
|
633
645
|
maxLocksOverride: devFunctionMetadata ? devFunctionMetadata.maxLocksOverride : functionSpec.maxLocksOverride,
|
|
634
|
-
source: callSpec.CallId,
|
|
635
646
|
watchFunction: function runCallWatcher() {
|
|
636
647
|
runCount++;
|
|
637
648
|
stats.lastInternalLoopCount = runCount;
|
|
638
649
|
stats.totalInternalLoopCount++;
|
|
639
650
|
if (PathFunctionRunner.DEBUG_CALLS) {
|
|
640
|
-
console.log(`Evaluating (try count ${runCount}) ${
|
|
651
|
+
console.log(`Evaluating (try count ${runCount}) ${debugNameColored}`);
|
|
641
652
|
}
|
|
642
653
|
if (runCount > PathFunctionRunner.MAX_WATCH_LOOPS) {
|
|
643
|
-
let errorMessage = `MAX_WATCH_LOOPS exceeded for ${
|
|
654
|
+
let errorMessage = `MAX_WATCH_LOOPS exceeded for ${debugNameColored}. All accesses have to be consistent. So Querysub.time() instead of Date.now() and Querysub.nextId() instead of nextId() / Math.random(). If you need multiple random numbers, keep track of an index, and pass it to Querysub.nextId() for the nth random number.`;
|
|
644
655
|
console.error(errorMessage, {
|
|
645
656
|
callId: callSpec.CallId,
|
|
646
657
|
runCount,
|
|
@@ -662,7 +673,7 @@ export class PathFunctionRunner {
|
|
|
662
673
|
atomicObjectRead(syncedModule.Sources[callSpec.FunctionId])
|
|
663
674
|
);
|
|
664
675
|
if (!syncedSpec) {
|
|
665
|
-
throw new Error(`Function spec not found for ${
|
|
676
|
+
throw new Error(`Function spec not found for ${debugNameColored}`);
|
|
666
677
|
}
|
|
667
678
|
|
|
668
679
|
// (We also need to depend on the RIGHT function spec).
|
|
@@ -775,7 +786,7 @@ export class PathFunctionRunner {
|
|
|
775
786
|
// }
|
|
776
787
|
//if (PathFunctionRunner.DEBUG_CALLS)
|
|
777
788
|
{
|
|
778
|
-
console.log(`Function run error ${
|
|
789
|
+
console.log(`Function run error ${debugNameColored}`, {
|
|
779
790
|
callId: callSpec.CallId,
|
|
780
791
|
error: e.stack,
|
|
781
792
|
});
|
|
@@ -799,7 +810,7 @@ export class PathFunctionRunner {
|
|
|
799
810
|
}
|
|
800
811
|
|
|
801
812
|
if (PathFunctionRunner.DEBUG_CALLS) {
|
|
802
|
-
console.log(`FINISHED${nooped ? " (skipped)" : ""} ${
|
|
813
|
+
console.log(`FINISHED${nooped ? " (skipped)" : ""} ${debugNameColored}, writes: ${finalWrites?.length}, took ${blue(formatTime(Date.now() - startTime))}`);
|
|
803
814
|
}
|
|
804
815
|
|
|
805
816
|
let wallTime = Date.now() - startTime;
|
|
@@ -814,7 +825,7 @@ export class PathFunctionRunner {
|
|
|
814
825
|
});
|
|
815
826
|
}
|
|
816
827
|
|
|
817
|
-
console.info(`Finished function evaluation: ${
|
|
828
|
+
console.info(`Finished function evaluation: ${debugNameColored}`, {
|
|
818
829
|
callId: callSpec.CallId,
|
|
819
830
|
outerLoop: retries,
|
|
820
831
|
finalWrites: finalWrites?.length,
|
|
@@ -17,6 +17,12 @@ import { InputLabel, InputLabelURL } from "../library-components/InputLabel";
|
|
|
17
17
|
import { URLParam } from "../library-components/URLParam";
|
|
18
18
|
import { hotReloadingGuard, isHotReloading, onHotReload } from "socket-function/hot/HotReloadController";
|
|
19
19
|
import { isCallerDynamicModule, isDynamicModule } from "../3-path-functions/pathFunctionLoader";
|
|
20
|
+
import { ATag } from "../library-components/ATag";
|
|
21
|
+
import { managementPageURL, showingManagementURL } from "../diagnostics/managementPages";
|
|
22
|
+
import { endTimeParam, startTimeParam } from "../diagnostics/logs/TimeRangeSelector";
|
|
23
|
+
import { timeInHour } from "socket-function/src/misc";
|
|
24
|
+
import { additionalSearchURL, lifecycleIdURL } from "../diagnostics/logs/lifeCycleAnalysis/LifeCyclePage";
|
|
25
|
+
import { parseDebugName } from "../3-path-functions/PathFunctionRunner";
|
|
20
26
|
|
|
21
27
|
// Map, so hot reloading doesn't break things
|
|
22
28
|
let componentButtons = new Map<string, { title: string, callback: (component: ExternalRenderClass) => void }>();
|
|
@@ -290,9 +296,6 @@ class WatchModal extends qreact.Component<{
|
|
|
290
296
|
return undefined;
|
|
291
297
|
}
|
|
292
298
|
let valueObj = authorityStorage.getValueAtTime(path);
|
|
293
|
-
// if (!path.includes("LOCAL") && valueObj?.source) {
|
|
294
|
-
// debugger;
|
|
295
|
-
// }
|
|
296
299
|
let value = pathValueSerializer.getPathValue(valueObj);
|
|
297
300
|
let valueStr = String(value);
|
|
298
301
|
try {
|
|
@@ -311,6 +314,9 @@ class WatchModal extends qreact.Component<{
|
|
|
311
314
|
let logOnRead = PathValueProxyWatcher.LOG_WRITES_INCLUDES.has(path);
|
|
312
315
|
let fncBreak = PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES.has(path);
|
|
313
316
|
let selectedButton = css.hsl(120, 75, 75);
|
|
317
|
+
|
|
318
|
+
let sourceObj = parseDebugName(valueObj?.source || "");
|
|
319
|
+
|
|
314
320
|
return (
|
|
315
321
|
<div class={css.hbox(10, 0).fillWidth.wrap}>
|
|
316
322
|
<button class={breakOnWrites && selectedButton || ""} onClick={() => {
|
|
@@ -353,6 +359,23 @@ class WatchModal extends qreact.Component<{
|
|
|
353
359
|
}}>
|
|
354
360
|
Caller*
|
|
355
361
|
</button>
|
|
362
|
+
{
|
|
363
|
+
valueObj && sourceObj && <ATag values={[
|
|
364
|
+
showingManagementURL.getOverride(true),
|
|
365
|
+
managementPageURL.getOverride("LifeCyclePage"),
|
|
366
|
+
startTimeParam.getOverride(valueObj.time.time - timeInHour),
|
|
367
|
+
endTimeParam.getOverride(valueObj.time.time + timeInHour),
|
|
368
|
+
lifecycleIdURL.getOverride("1772917451506.018_0.441679673600166"),
|
|
369
|
+
additionalSearchURL.getOverride(`${sourceObj.functionId} & ${sourceObj.runAtTime.time}`),
|
|
370
|
+
]}
|
|
371
|
+
title={valueObj.source || ""}
|
|
372
|
+
>
|
|
373
|
+
View Call
|
|
374
|
+
</ATag>
|
|
375
|
+
|| <div title={valueObj?.source || ""}>
|
|
376
|
+
Source
|
|
377
|
+
</div>
|
|
378
|
+
}
|
|
356
379
|
<div class={css.hbox(4).wrap.button} onClick={() => {
|
|
357
380
|
console.log(path);
|
|
358
381
|
return navigator.clipboard.writeText(path);
|
|
@@ -47,7 +47,7 @@ function getNodeForPath(path: string): string | "missing" | undefined {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export class PathDistributionInfo extends qreact.Component {
|
|
50
|
-
|
|
50
|
+
renderBase() {
|
|
51
51
|
if (!isCurrentUserSuperUser()) return undefined;
|
|
52
52
|
|
|
53
53
|
let nodeSpecs = querysubController(SocketFunction.browserNodeId()).debugGetNodeSpecs();
|
|
@@ -99,4 +99,12 @@ export class PathDistributionInfo extends qreact.Component {
|
|
|
99
99
|
{parts.length > 0 && parts || "..."}
|
|
100
100
|
</div>;
|
|
101
101
|
}
|
|
102
|
+
render() {
|
|
103
|
+
try {
|
|
104
|
+
return this.renderBase();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error("Error in rendering PathDistributionInfo:", error);
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
102
110
|
}
|
|
@@ -276,7 +276,7 @@ export class BufferUnitIndex {
|
|
|
276
276
|
// Only inaccurate if unique units approaches or exceeds 65k
|
|
277
277
|
return uniqueCount * 2;
|
|
278
278
|
}
|
|
279
|
-
}, `
|
|
279
|
+
}, `estimatedUniqueUnits`);
|
|
280
280
|
|
|
281
281
|
// Step 3: Calculate hash table size and mask
|
|
282
282
|
const { hashTableCapacity, mask } = measureBlock(() => {
|
|
@@ -21,6 +21,11 @@ if (isNode()) {
|
|
|
21
21
|
}));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
let loggingToDiskDisabled = false;
|
|
25
|
+
export function disableLoggingToDisk() {
|
|
26
|
+
loggingToDiskDisabled = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
// NOTE: When logging we spread objects. If we encounter strings, we set the field `param${index}`
|
|
25
30
|
export type LogDatum = Record<string, unknown> & {
|
|
26
31
|
time: number;
|
|
@@ -109,6 +114,7 @@ const logDiskDontShim = logDisk;
|
|
|
109
114
|
/** @deprecated, Don't call this directly, call console info instead, which our shim will prevent from logging to the console, but it will still call logDisk. */
|
|
110
115
|
export function logDisk(type: "log" | "warn" | "info" | "error", ...args: unknown[]) {
|
|
111
116
|
if (!isNode()) return;
|
|
117
|
+
if (loggingToDiskDisabled) return;
|
|
112
118
|
try {
|
|
113
119
|
if (args.length === 0) return;
|
|
114
120
|
let logType = args.find(x => typeof x === "string") as string | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module.allowclient = true;
|
|
2
2
|
|
|
3
|
-
import { ArchiveSnapshotOverview, ArchiveSnapshotRead, getSnapshot, getSnapshotList, loadSnapshot, saveSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
|
|
3
|
+
import { ArchiveSnapshotOverview, ArchiveSnapshotRead, downloadSnapshot, getSnapshot, getSnapshotList, loadSnapshot, saveSnapshot, uploadSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
|
|
4
4
|
import { qreact } from "../../4-dom/qreact";
|
|
5
5
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
6
|
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
@@ -30,6 +30,8 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
30
30
|
// file => true
|
|
31
31
|
expanded: t.lookup(t.boolean),
|
|
32
32
|
saving: t.boolean(false),
|
|
33
|
+
uploading: t.boolean(false),
|
|
34
|
+
downloading: t.lookup(t.boolean),
|
|
33
35
|
});
|
|
34
36
|
render() {
|
|
35
37
|
let controller = SnapshotViewerSynced(getBrowserUrlNode());
|
|
@@ -40,6 +42,11 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
40
42
|
return (
|
|
41
43
|
<div class={css.pad2(10).vbox(10)}>
|
|
42
44
|
<h1>Snapshots are taken before every change. "zombie" means the file exists, but it has no confirm (so it will be ignored).</h1>
|
|
45
|
+
<div class={css.hbox(10)}>
|
|
46
|
+
<Button onClick={() => triggerSnapshotUpload(this)}>
|
|
47
|
+
{this.state.uploading && "Uploading..." || "Upload Snapshot"}
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
43
50
|
<Table
|
|
44
51
|
rows={snapshotList}
|
|
45
52
|
columns={{
|
|
@@ -68,6 +75,9 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
68
75
|
{this.state.saving ? "Saving..." : "Save"}
|
|
69
76
|
</Button>
|
|
70
77
|
}
|
|
78
|
+
<Button onClick={() => downloadSnapshotFile(this, file)}>
|
|
79
|
+
{this.state.downloading[file] && "Downloading..." || "Download"}
|
|
80
|
+
</Button>
|
|
71
81
|
</div>;
|
|
72
82
|
}
|
|
73
83
|
}
|
|
@@ -142,6 +152,61 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
async function downloadSnapshotFile(component: SnapshotViewer, file: string) {
|
|
156
|
+
if (component.state.downloading[file]) return;
|
|
157
|
+
Querysub.localCommit(() => {
|
|
158
|
+
component.state.downloading[file] = true;
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
let buffer = await SnapshotViewerController.nodes[getBrowserUrlNode()].downloadSnapshot(file);
|
|
162
|
+
let blob = new Blob([buffer]);
|
|
163
|
+
let url = URL.createObjectURL(blob);
|
|
164
|
+
let anchor = document.createElement("a");
|
|
165
|
+
anchor.href = url;
|
|
166
|
+
anchor.download = file + ".cborx";
|
|
167
|
+
if (file === "live") {
|
|
168
|
+
anchor.download = `${formatDateTime(Date.now())}.cborx`;
|
|
169
|
+
}
|
|
170
|
+
document.body.appendChild(anchor);
|
|
171
|
+
anchor.click();
|
|
172
|
+
document.body.removeChild(anchor);
|
|
173
|
+
URL.revokeObjectURL(url);
|
|
174
|
+
} catch (e: any) {
|
|
175
|
+
console.error(red(`Failed to download snapshot ${file}: ${e.stack}`));
|
|
176
|
+
} finally {
|
|
177
|
+
Querysub.localCommit(() => {
|
|
178
|
+
delete component.state.downloading[file];
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function triggerSnapshotUpload(component: SnapshotViewer) {
|
|
184
|
+
if (component.state.uploading) return;
|
|
185
|
+
let input = document.createElement("input");
|
|
186
|
+
input.type = "file";
|
|
187
|
+
input.accept = ".cborx";
|
|
188
|
+
input.onchange = async () => {
|
|
189
|
+
let selected = input.files?.[0];
|
|
190
|
+
if (!selected) return;
|
|
191
|
+
Querysub.localCommit(() => {
|
|
192
|
+
component.state.uploading = true;
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
let arrayBuffer = await selected.arrayBuffer();
|
|
196
|
+
let buffer = Buffer.from(arrayBuffer);
|
|
197
|
+
await SnapshotViewerController.nodes[getBrowserUrlNode()].uploadSnapshot(buffer);
|
|
198
|
+
console.log(green("Uploaded snapshot"));
|
|
199
|
+
} catch (e: any) {
|
|
200
|
+
console.error(red(`Failed to upload snapshot: ${e.stack}`));
|
|
201
|
+
} finally {
|
|
202
|
+
Querysub.localCommit(() => {
|
|
203
|
+
component.state.uploading = false;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
input.click();
|
|
208
|
+
}
|
|
209
|
+
|
|
145
210
|
async function seriousConfirm(config: {
|
|
146
211
|
message: string;
|
|
147
212
|
}): Promise<boolean> {
|
|
@@ -192,6 +257,14 @@ class SnapshotViewerControllerBase {
|
|
|
192
257
|
let files = await getSnapshot("live");
|
|
193
258
|
await saveSnapshot({ files: files.files.map(x => x.file) });
|
|
194
259
|
}
|
|
260
|
+
|
|
261
|
+
public async downloadSnapshot(snapshotFile: string | "live"): Promise<Buffer> {
|
|
262
|
+
return await downloadSnapshot(snapshotFile);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public async uploadSnapshot(buffer: Buffer): Promise<ArchiveSnapshotOverview> {
|
|
266
|
+
return await uploadSnapshot(buffer);
|
|
267
|
+
}
|
|
195
268
|
}
|
|
196
269
|
|
|
197
270
|
export const SnapshotViewerController = SocketFunction.register(
|
|
@@ -202,6 +275,9 @@ export const SnapshotViewerController = SocketFunction.register(
|
|
|
202
275
|
loadSnapshot: {},
|
|
203
276
|
getSnapshot: {},
|
|
204
277
|
saveLiveSnapshot: {},
|
|
278
|
+
downloadSnapshot: {},
|
|
279
|
+
uploadSnapshot: {},
|
|
280
|
+
example: {}
|
|
205
281
|
}),
|
|
206
282
|
() => ({
|
|
207
283
|
hooks: [assertIsManagementUser],
|
|
@@ -217,5 +293,6 @@ const SnapshotViewerSynced = getSyncedController(SnapshotViewerController, {
|
|
|
217
293
|
},
|
|
218
294
|
writes: {
|
|
219
295
|
saveLiveSnapshot: ["snapshots"],
|
|
296
|
+
uploadSnapshot: ["snapshots"],
|
|
220
297
|
}
|
|
221
298
|
});
|
|
@@ -13,6 +13,8 @@ function onUncaught(...args: unknown[]) {
|
|
|
13
13
|
// Ignore ResizeObserver errors, they are spurious
|
|
14
14
|
// - https://github.com/vercel/next.js/discussions/51551
|
|
15
15
|
if (error.message.startsWith("ResizeObserver loop")) return;
|
|
16
|
+
// We should really do this better. Basically, if we're disposing or canceling, it's not actually an error, just ignore it.
|
|
17
|
+
if (error.message.startsWith("Dispose")) return;
|
|
16
18
|
|
|
17
19
|
onMessage({
|
|
18
20
|
type: "error",
|