querysub 0.153.0 → 0.154.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 +6 -6
- package/src/-b-authorities/cloudflareHelpers.ts +11 -2
- package/src/3-path-functions/PathFunctionRunner.ts +168 -97
- package/src/3-path-functions/PathFunctionRunnerMain.ts +8 -2
- package/src/3-path-functions/pathFunctionLoader.ts +11 -6
- package/src/3-path-functions/syncSchema.ts +10 -1
- package/src/4-deploy/edgeBootstrap.ts +10 -1
- package/src/4-querysub/Querysub.ts +77 -3
- package/src/4-querysub/QuerysubController.ts +22 -2
- package/src/4-querysub/permissions.ts +33 -2
- package/src/4-querysub/querysubPrediction.ts +52 -18
- package/src/archiveapps/archiveGCEntry.tsx +38 -0
- package/src/archiveapps/archiveJoinEntry.ts +121 -0
- package/src/archiveapps/archiveMergeEntry.tsx +47 -0
- package/src/archiveapps/compressTest.tsx +59 -0
- package/src/archiveapps/lockTest.ts +127 -0
- package/src/config.ts +5 -0
- package/src/diagnostics/managementPages.tsx +55 -0
- package/src/diagnostics/misc-pages/ArchiveInspect.tsx +325 -0
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +781 -0
- package/src/diagnostics/misc-pages/ArchiveViewerTable.tsx +156 -0
- package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +573 -0
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +129 -0
- package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +431 -0
- package/src/diagnostics/misc-pages/RequireAuditPage.tsx +218 -0
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +206 -0
- package/src/diagnostics/misc-pages/TimeRangeView.tsx +648 -0
- package/src/diagnostics/misc-pages/archiveViewerFilter.tsx +221 -0
- package/src/diagnostics/misc-pages/archiveViewerShared.tsx +76 -0
- package/src/email/postmark.tsx +40 -0
- package/src/email/sendgrid.tsx +44 -0
- package/src/functional/UndoWatch.tsx +133 -0
- package/src/functional/diff.ts +858 -0
- package/src/functional/promiseCache.ts +67 -0
- package/src/functional/random.ts +9 -0
- package/src/functional/runCommand.ts +42 -0
- package/src/functional/runOnce.ts +7 -0
- package/src/functional/stats.ts +61 -0
- package/src/functional/throttleRerender.tsx +80 -0
- package/src/library-components/AspectSizedComponent.tsx +88 -0
- package/src/library-components/Histogram.tsx +338 -0
- package/src/library-components/InlinePopup.tsx +67 -0
- package/src/library-components/Notifications.tsx +153 -0
- package/src/library-components/RenderIfVisible.tsx +80 -0
- package/src/library-components/SimpleNotification.tsx +133 -0
- package/src/library-components/TabbedUI.tsx +39 -0
- package/src/library-components/animateAnyElement.tsx +65 -0
- package/src/library-components/errorNotifications.tsx +81 -0
- package/src/library-components/placeholder.ts +18 -0
- package/src/misc/format2.ts +48 -0
- package/src/misc.ts +33 -0
- package/src/misc2.ts +5 -0
- package/src/server.ts +2 -1
- package/src/storage/diskCache.ts +227 -0
- package/src/storage/diskCache2.ts +122 -0
- package/src/storage/fileSystemPointer.ts +72 -0
- package/src/user-implementation/LoginPage.tsx +78 -0
- package/src/user-implementation/RequireAuditPage.tsx +219 -0
- package/src/user-implementation/SecurityPage.tsx +212 -0
- package/src/user-implementation/UserPage.tsx +320 -0
- package/src/user-implementation/addSuperUser.ts +21 -0
- package/src/user-implementation/canSeeSource.ts +41 -0
- package/src/user-implementation/loginEmail.tsx +159 -0
- package/src/user-implementation/setEmailKey.ts +20 -0
- package/src/user-implementation/userData.ts +974 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { css } from "../4-dom/css";
|
|
3
|
+
import { Icon } from "./icons";
|
|
4
|
+
import { qreact } from "../4-dom/qreact";
|
|
5
|
+
import { closeCurrentNotification, createCloseCurrentNotification } from "./Notifications";
|
|
6
|
+
import { nextId } from "socket-function/src/misc";
|
|
7
|
+
import { Querysub } from "../4-querysub/Querysub";
|
|
8
|
+
|
|
9
|
+
let nextSeqNum = 1;
|
|
10
|
+
export class SimpleNotification extends qreact.Component<{
|
|
11
|
+
type: "info" | "warn" | "error" | "success";
|
|
12
|
+
title: string;
|
|
13
|
+
usesClickEvents?: boolean;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
tooltip?: string;
|
|
16
|
+
}> {
|
|
17
|
+
componentDidMount(): void {
|
|
18
|
+
let timeout = this.timeout();
|
|
19
|
+
if (timeout > 0) {
|
|
20
|
+
let closeNotification = createCloseCurrentNotification();
|
|
21
|
+
if (Querysub.isAllSynced()) {
|
|
22
|
+
setTimeout(() => closeNotification?.(), timeout);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
timeout() {
|
|
27
|
+
return this.props.timeout ?? 10_000;
|
|
28
|
+
}
|
|
29
|
+
seqNum = nextSeqNum++;
|
|
30
|
+
render() {
|
|
31
|
+
let { type, title, children, usesClickEvents } = this.props;
|
|
32
|
+
let color = (
|
|
33
|
+
type === "info" && { h: 188, s: 52 }
|
|
34
|
+
|| type === "warn" && { h: 50, s: 100 }
|
|
35
|
+
|| type === "error" && { h: 3, s: 62 }
|
|
36
|
+
|| type === "success" && { h: 155, s: 66 }
|
|
37
|
+
|| { h: 0, s: 50 }
|
|
38
|
+
);
|
|
39
|
+
let animClassName = `SimpleNotification-${this.seqNum}`;
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
class={
|
|
43
|
+
css.color("white").pad(2)
|
|
44
|
+
.hsla(color.h, color.s, 50, 0.9)
|
|
45
|
+
.bord(2, `hsl(${color.h}, ${color.s}%, 60%)`)
|
|
46
|
+
.hbox(10)
|
|
47
|
+
.maxWidth("40vw")
|
|
48
|
+
.relative
|
|
49
|
+
.overflowHidden
|
|
50
|
+
+ (!usesClickEvents ? css.button : "")
|
|
51
|
+
+ " " + animClassName
|
|
52
|
+
}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
void navigator.clipboard.writeText(title);
|
|
55
|
+
if (!usesClickEvents) {
|
|
56
|
+
closeCurrentNotification();
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
title={this.props.tooltip}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
class={
|
|
63
|
+
css
|
|
64
|
+
.absolute.zIndex(-1).pos(0, 0).fillHeight
|
|
65
|
+
.hsla(color.h, color.s, 50, 0.9)
|
|
66
|
+
+ " " + animClassName + "-bg"
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
<div class={css.pad(2, 4).vbox(2).fillWidth}>
|
|
70
|
+
<div class={css.ellipsis.fillWidth.fontWeight("bold")}>
|
|
71
|
+
{title}
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class={css.margin("auto")} />
|
|
78
|
+
<div class={css.flexShrink0.alignSelf("start").button} onClick={() => {
|
|
79
|
+
if (usesClickEvents) {
|
|
80
|
+
closeCurrentNotification();
|
|
81
|
+
}
|
|
82
|
+
}}>
|
|
83
|
+
{Icon.close({ size: 20, stroke: "white", fill: "hsl(0, 0%, 70%)" })}
|
|
84
|
+
</div>
|
|
85
|
+
<style>
|
|
86
|
+
{`
|
|
87
|
+
@keyframes ${animClassName}-anim {
|
|
88
|
+
0% { transform: translateY(-100%); }
|
|
89
|
+
100% { transform: translateY(0); }
|
|
90
|
+
}
|
|
91
|
+
.${animClassName} {
|
|
92
|
+
animation: ${animClassName}-anim 0.3s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes ${animClassName}-bg-anim {
|
|
96
|
+
0% { width: 100% }
|
|
97
|
+
100% { width: 0% }
|
|
98
|
+
}
|
|
99
|
+
.${animClassName}-bg {
|
|
100
|
+
animation: ${animClassName}-bg-anim ${this.timeout()}ms linear;
|
|
101
|
+
}
|
|
102
|
+
`}
|
|
103
|
+
</style>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/*
|
|
109
|
+
primary-50: hsla(188, 52%, 84%, 1);
|
|
110
|
+
primary-300: hsla(188, 46%, 69%, 1);
|
|
111
|
+
primary-500: hsla(188, 45%, 54%, 1);
|
|
112
|
+
primary-700: hsla(192, 89%, 29%, 1);
|
|
113
|
+
primary-900: hsla(192, 100%, 20%, 1);
|
|
114
|
+
|
|
115
|
+
success-50: hsla(155, 66%, 86%, 1);
|
|
116
|
+
success-300: hsla(155, 50%, 70%, 1);
|
|
117
|
+
success-500: hsla(155, 45%, 54%, 1);
|
|
118
|
+
success-700: hsla(155, 90%, 29%, 1);
|
|
119
|
+
success-900: hsla(155, 100%, 20%, 1);
|
|
120
|
+
|
|
121
|
+
warning-50: hsla(50, 100%, 88%, 1);
|
|
122
|
+
warning-75: hsla(50, 100%, 83%, 1);
|
|
123
|
+
warning-300: hsla(50, 79%, 76%, 1);
|
|
124
|
+
warning-500: hsla(50, 81%, 67%, 1);
|
|
125
|
+
warning-700: hsla(50, 100%, 61%, 1);
|
|
126
|
+
warning-900: hsla(50, 100%, 43%, 1);
|
|
127
|
+
|
|
128
|
+
error-50: hsla(3, 62%, 89%, 1);
|
|
129
|
+
error-300: hsla(3, 100%, 83%, 1);
|
|
130
|
+
error-500: hsla(3, 86%, 70%, 1);
|
|
131
|
+
error-700: hsla(3, 91%, 64%, 1);
|
|
132
|
+
error-900: hsla(3, 67%, 52%, 1);
|
|
133
|
+
*/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { qreact } from "../4-dom/qreact";
|
|
3
|
+
import { css } from "../4-dom/css";
|
|
4
|
+
import { ATag } from "../library-components/ATag";
|
|
5
|
+
import { URLParam, createURLSync } from "../library-components/URLParam";
|
|
6
|
+
|
|
7
|
+
const tabURL = createURLSync<string>("tab", "");
|
|
8
|
+
|
|
9
|
+
export class TabbedUI extends qreact.Component<{
|
|
10
|
+
// Defaults Params.tab
|
|
11
|
+
tab?: URLParam;
|
|
12
|
+
/** The first tab is the default */
|
|
13
|
+
tabs: { value: string; title: preact.ComponentChild; contents: preact.ComponentChild; }[];
|
|
14
|
+
}> {
|
|
15
|
+
render() {
|
|
16
|
+
let tabs = this.props.tabs;
|
|
17
|
+
let tabPointer = this.props.tab ?? tabURL;
|
|
18
|
+
let currentTab = tabs.find(t => t.value === tabPointer.value) ?? tabs[0];
|
|
19
|
+
return (
|
|
20
|
+
<div class={css.vbox(0)}>
|
|
21
|
+
<div class={css.hbox(0).fontSize(16)}>
|
|
22
|
+
{tabs.map(tab => (
|
|
23
|
+
<ATag
|
|
24
|
+
values={[{ param: tabPointer, value: tab.value }]}
|
|
25
|
+
class={css.flex.pad(10).bord(0, { h: 210, s: 0, l: 25 }).borderLeftWidth(1)
|
|
26
|
+
+ (tab.value === currentTab.value && css.hsl(210, 50, 40) || css.hsl(210, 0, 40))
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
{tab.title}
|
|
30
|
+
</ATag>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
<div class={css.bord(2, { h: 210, s: 0, l: 25 }).alignSelf("stretch").pad(10).vbox(10)}>
|
|
34
|
+
{currentTab.contents}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { delay } from "socket-function/src/batching";
|
|
2
|
+
import { cacheJSONArgsEqual } from "socket-function/src/caching";
|
|
3
|
+
import { isNode } from "typesafecss";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Animates the element as it moves, even if it is just part of a regular layout,
|
|
7
|
+
which works no matter where it moves. A unique id must be passed in, to
|
|
8
|
+
ensure the right position is animated. It is encouraged to use a key,
|
|
9
|
+
to ensure your element isn't reused with unrelated elements.
|
|
10
|
+
- Sets transition and transform on the element, and so is incompatible
|
|
11
|
+
with elements that use those.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
For example:
|
|
15
|
+
<div key={key} ref={createRefAnimation(key)}>
|
|
16
|
+
Test
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
*/
|
|
20
|
+
export const createRefAnimation = cacheJSONArgsEqual((id: string, transitionTime = "200ms", transitionType = "ease-in-out") => {
|
|
21
|
+
let prevRect: DOMRect | undefined;
|
|
22
|
+
let prevScrollOffsetTop = 0;
|
|
23
|
+
let prevScrollOffsetLeft = 0;
|
|
24
|
+
return async (elem: HTMLElement | null) => {
|
|
25
|
+
if (!elem) return;
|
|
26
|
+
let newRect = elem.getBoundingClientRect();
|
|
27
|
+
// If we haven't even started the transition, skip
|
|
28
|
+
if (elem.style.transform) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// If we are still animating, skip. Stopping it causes issues, and it is smoother to just
|
|
32
|
+
// let it finish.
|
|
33
|
+
if (elem.getAnimations().length > 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let curPrevRect = prevRect;
|
|
37
|
+
prevRect = newRect;
|
|
38
|
+
|
|
39
|
+
let newScrollOffsetObj = getScrollOffsetSum(elem);
|
|
40
|
+
let scrollChangeTop = newScrollOffsetObj.top - prevScrollOffsetTop;
|
|
41
|
+
prevScrollOffsetTop = newScrollOffsetObj.top;
|
|
42
|
+
let scrollChangeLeft = newScrollOffsetObj.left - prevScrollOffsetLeft;
|
|
43
|
+
prevScrollOffsetLeft = newScrollOffsetObj.left;
|
|
44
|
+
|
|
45
|
+
if (!curPrevRect?.width || !newRect.width || curPrevRect.left === newRect.left) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
elem.style.transform = `translate(${curPrevRect.left - newRect.left - scrollChangeLeft}px, ${curPrevRect.top - newRect.top - scrollChangeTop}px)`;
|
|
49
|
+
elem.style.transition = `transform 0s ${transitionType}`;
|
|
50
|
+
await delay("paintLoop");
|
|
51
|
+
elem.style.transform = "";
|
|
52
|
+
elem.style.transition = `transform ${transitionTime} ${transitionType}`;
|
|
53
|
+
};
|
|
54
|
+
}, 10000);
|
|
55
|
+
|
|
56
|
+
function getScrollOffsetSum(elem: HTMLElement | null) {
|
|
57
|
+
let top = 0;
|
|
58
|
+
let left = 0;
|
|
59
|
+
while (elem) {
|
|
60
|
+
top += elem.scrollTop;
|
|
61
|
+
left += elem.scrollLeft;
|
|
62
|
+
elem = elem.parentElement;
|
|
63
|
+
}
|
|
64
|
+
return { top, left };
|
|
65
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isNode } from "socket-function/src/misc";
|
|
2
|
+
import { showNotification } from "./Notifications";
|
|
3
|
+
import { SimpleNotification } from "./SimpleNotification";
|
|
4
|
+
import { qreact } from "../4-dom/qreact";
|
|
5
|
+
import { Querysub } from "../4-querysub/Querysub";
|
|
6
|
+
import { css } from "../4-dom/css";
|
|
7
|
+
|
|
8
|
+
function onUncaught(...args: unknown[]) {
|
|
9
|
+
let error = args[4] as Error;
|
|
10
|
+
if (!error) {
|
|
11
|
+
error = (args[0] as any).reason || { message: args[0] };
|
|
12
|
+
}
|
|
13
|
+
// Ignore ResizeObserver errors, they are spurious
|
|
14
|
+
// - https://github.com/vercel/next.js/discussions/51551
|
|
15
|
+
if (error.message.startsWith("ResizeObserver loop")) return;
|
|
16
|
+
|
|
17
|
+
onMessage({
|
|
18
|
+
type: "error",
|
|
19
|
+
message: error.message,
|
|
20
|
+
stack: error.stack || error.message,
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (!isNode()) {
|
|
25
|
+
window.onerror = window.onunhandledrejection = onUncaught;
|
|
26
|
+
function shimLog(type: "warn" | "error") {
|
|
27
|
+
let base = console[type];
|
|
28
|
+
console[type] = function (message: string, ...args: unknown[]) {
|
|
29
|
+
onMessage({ type, message, stack: message + "\n" + args.join("\n") });
|
|
30
|
+
base.call(console, message, ...args);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
shimLog("error");
|
|
35
|
+
// NOTE: We MIGHT turn warning back on, if we can add some kind of dev switch.
|
|
36
|
+
// But for normal users... warnings are useless, and caused by all sorts of things
|
|
37
|
+
// which are completely fine and require no action.
|
|
38
|
+
//shimLog("warn");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let lastUserWantsInfo = false;
|
|
42
|
+
export function enableErrorNotifications() {
|
|
43
|
+
lastUserWantsInfo = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onMessage(config: {
|
|
47
|
+
type: "error" | "warn";
|
|
48
|
+
message: string;
|
|
49
|
+
stack: string;
|
|
50
|
+
}) {
|
|
51
|
+
if (!lastUserWantsInfo && config.type !== "error") return;
|
|
52
|
+
|
|
53
|
+
let title = stripConsoleRenderedColors(config.message).split("\n")[0];
|
|
54
|
+
if (title.startsWith("Error: ")) {
|
|
55
|
+
title = title.slice("Error: ".length);
|
|
56
|
+
}
|
|
57
|
+
showNotification({
|
|
58
|
+
pos: "topRight",
|
|
59
|
+
id: title,
|
|
60
|
+
notification: <SimpleNotification
|
|
61
|
+
type={config.type}
|
|
62
|
+
title={title}
|
|
63
|
+
tooltip={config.stack}
|
|
64
|
+
>
|
|
65
|
+
{/* NOTE: I've stopping showing this info. It's annoying, as they can just look in the console
|
|
66
|
+
(as most of these come from console.error anyways). */}
|
|
67
|
+
{/* {lastUserWantsInfo && <div class={config.stack.trim().includes("\n") && css.whiteSpace("pre-wrap") || ""}>
|
|
68
|
+
{stripConsoleRenderedColors(config.stack)}
|
|
69
|
+
</div>} */}
|
|
70
|
+
</SimpleNotification>
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// \033[XXXm
|
|
75
|
+
// \033[31;42m
|
|
76
|
+
// \033[38;5;206m
|
|
77
|
+
// \033[38;2;<r>;<g>;<b>m
|
|
78
|
+
|
|
79
|
+
function stripConsoleRenderedColors(text: string) {
|
|
80
|
+
return String(text).replaceAll(/\033\[\d+(;\d+)*m/g, "");
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cacheArgsEqual } from "socket-function/src/caching";
|
|
2
|
+
|
|
3
|
+
export const getPlaceholder = cacheArgsEqual((width, height) => {
|
|
4
|
+
let canvas = document.createElement("canvas");
|
|
5
|
+
canvas.width = width;
|
|
6
|
+
canvas.height = height;
|
|
7
|
+
let ctx = canvas.getContext("2d")!;
|
|
8
|
+
ctx.fillStyle = "hsl(0, 0%, 80%)";
|
|
9
|
+
ctx.fillRect(0, 0, width, height);
|
|
10
|
+
// Draw width x height in the middle of the canvas
|
|
11
|
+
ctx.fillStyle = "hsl(0, 0%, 60%)";
|
|
12
|
+
// Bold
|
|
13
|
+
ctx.font = "bold 40px sans-serif";
|
|
14
|
+
ctx.textAlign = "center";
|
|
15
|
+
ctx.textBaseline = "middle";
|
|
16
|
+
ctx.fillText(`${width} x ${height}`, width / 2, height / 2);
|
|
17
|
+
return canvas.toDataURL();
|
|
18
|
+
}, 10);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function parseNice(text: string): unknown {
|
|
2
|
+
if (text === "undefined") return undefined;
|
|
3
|
+
try {
|
|
4
|
+
return JSON.parse(text);
|
|
5
|
+
} catch { }
|
|
6
|
+
return text;
|
|
7
|
+
}
|
|
8
|
+
export function stringifyNice(value: unknown): string {
|
|
9
|
+
if (value === undefined) return "undefined";
|
|
10
|
+
if (value === "") return JSON.stringify("");
|
|
11
|
+
let testStringParse = parseNice(String(value));
|
|
12
|
+
if (testStringParse === value) {
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function niceNumberStringify(valueIn: number) {
|
|
19
|
+
if (Math.abs(valueIn) < 0.0000000001) {
|
|
20
|
+
return "0";
|
|
21
|
+
}
|
|
22
|
+
let value = valueIn.toString();
|
|
23
|
+
// TODO: Do this MUCH better...
|
|
24
|
+
if (value.slice(0, -1).endsWith("00000000000")) {
|
|
25
|
+
value = value.slice(0, -1);
|
|
26
|
+
while (value.endsWith("0")) {
|
|
27
|
+
value = value.slice(0, -1);
|
|
28
|
+
}
|
|
29
|
+
if (value.endsWith(".")) {
|
|
30
|
+
value = value.slice(0, -1);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (value.slice(0, -1).endsWith("9999999999")) {
|
|
35
|
+
value = value.slice(0, -1);
|
|
36
|
+
while (value.endsWith("9")) {
|
|
37
|
+
value = value.slice(0, -1);
|
|
38
|
+
}
|
|
39
|
+
if (value.endsWith(".")) {
|
|
40
|
+
value = value.slice(0, -1);
|
|
41
|
+
}
|
|
42
|
+
// NOTE: Interestingly enough... because we remove all trailing 9s, it means if the last number is not 9,
|
|
43
|
+
// so... we can do this hack to round up
|
|
44
|
+
value = value.slice(0, -1) + (parseInt(value.slice(-1)) + 1);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
package/src/misc.ts
CHANGED
|
@@ -16,4 +16,37 @@ export function isEmpty(obj: unknown): boolean {
|
|
|
16
16
|
|
|
17
17
|
export function isDefined<T>(value: T | undefined | null): value is T {
|
|
18
18
|
return value !== undefined && value !== null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ObjectAssign<T extends { [key: string]: unknown }>(obj: T, values: Partial<T>) {
|
|
22
|
+
return Object.assign(obj, values);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ellipsis(text: string, max: number) {
|
|
26
|
+
if (text.length <= max) return text;
|
|
27
|
+
return text.slice(0, max - 3) + "...";
|
|
28
|
+
}
|
|
29
|
+
export function ellipsisMiddle(text: string, max: number) {
|
|
30
|
+
if (text.length <= max) return text;
|
|
31
|
+
let half = Math.floor((max - 3) / 2);
|
|
32
|
+
return text.slice(0, half) + "..." + text.slice(-half);
|
|
33
|
+
}
|
|
34
|
+
export function ellipsisStart(text: string, max: number) {
|
|
35
|
+
if (text.length <= max) return text;
|
|
36
|
+
return "..." + text.slice(-max + 3);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function joinVNodes(vNodes: preact.ComponentChild[], delimitter: preact.ComponentChild) {
|
|
40
|
+
let output: preact.ComponentChild[] = [];
|
|
41
|
+
for (let i = 0; i < vNodes.length; i++) {
|
|
42
|
+
if (i !== 0) {
|
|
43
|
+
output.push(delimitter);
|
|
44
|
+
}
|
|
45
|
+
output.push(vNodes[i]);
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function clamp(value: number, min: number, max: number) {
|
|
51
|
+
return Math.max(min, Math.min(max, value));
|
|
19
52
|
}
|
package/src/misc2.ts
ADDED
package/src/server.ts
CHANGED
|
@@ -24,10 +24,11 @@ import { formatTime } from "socket-function/src/formatting/format";
|
|
|
24
24
|
import "./4-querysub/Querysub";
|
|
25
25
|
import { getOwnThreadId } from "./-f-node-discovery/NodeDiscovery";
|
|
26
26
|
import { getOurAuthorities } from "./config2";
|
|
27
|
-
import { isPublic } from "./config";
|
|
27
|
+
import { devDebugbreak, isPublic } from "./config";
|
|
28
28
|
|
|
29
29
|
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
30
30
|
.option("authority", { type: "string", desc: `Defines the base paths we are an authority on (the domain is prepended to them). Either a file path to a JSON(AuthorityPath[]), or a base64 representation of the JSON(AuthorityPath[]).` })
|
|
31
|
+
.option("verbose", { type: "boolean", desc: "Log all writes and reads" })
|
|
31
32
|
.argv || {}
|
|
32
33
|
;
|
|
33
34
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { asBuffer } from "../buffers";
|
|
2
|
+
import { assert, assertValue, logErrors } from "../errors";
|
|
3
|
+
import { batchFunction } from "socket-function/src/batching";
|
|
4
|
+
import { lazy } from "socket-function/src/caching";
|
|
5
|
+
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
6
|
+
import { isNode } from "socket-function/src/misc";
|
|
7
|
+
import { measureBlock, measureWrap } from "socket-function/src/profiling/measure";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
let persist = lazy(() => {
|
|
12
|
+
if (!isNode()) {
|
|
13
|
+
logErrors(navigator.storage?.persist());
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let existingDiskCaches = new Set<string>();
|
|
18
|
+
/** Faster at reading then DiskBackedLookup (up to 3X, depending on the case), BUT...
|
|
19
|
+
* synchronously blocks for up to 30ms per write, and gives errors on large transactions/writes.
|
|
20
|
+
*/
|
|
21
|
+
export function diskCache(
|
|
22
|
+
config: {
|
|
23
|
+
uniqueName: string;
|
|
24
|
+
/** Default = 64MB */
|
|
25
|
+
inMemoryLimit?: number;
|
|
26
|
+
}
|
|
27
|
+
): {
|
|
28
|
+
get(key: string[]): Promise<(Buffer | undefined)[]>;
|
|
29
|
+
getSingle(key: string): Promise<Buffer | undefined>;
|
|
30
|
+
set(key: string, value: Buffer): void;
|
|
31
|
+
} {
|
|
32
|
+
let { uniqueName } = config;
|
|
33
|
+
const inMemoryLimit = config.inMemoryLimit ?? 64 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
if (existingDiskCaches.has(uniqueName)) {
|
|
36
|
+
throw new Error(`Duplicate diskCache ${uniqueName}`);
|
|
37
|
+
}
|
|
38
|
+
existingDiskCaches.add(uniqueName);
|
|
39
|
+
uniqueName = "diskCache3_" + uniqueName;
|
|
40
|
+
|
|
41
|
+
if (isNode() || !navigator.storage) {
|
|
42
|
+
return {
|
|
43
|
+
async get(keys: string[]) {
|
|
44
|
+
return keys.map(x => undefined);
|
|
45
|
+
},
|
|
46
|
+
async getSingle() {
|
|
47
|
+
return undefined;
|
|
48
|
+
},
|
|
49
|
+
set() { }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const objectStoreName = "buffers";
|
|
54
|
+
let getDB = lazy((async () => {
|
|
55
|
+
let db = indexedDB.open(uniqueName, 1);
|
|
56
|
+
db.addEventListener("upgradeneeded", () => {
|
|
57
|
+
db.result.createObjectStore(objectStoreName, {});
|
|
58
|
+
});
|
|
59
|
+
await new Promise(resolve => db.addEventListener("success", resolve));
|
|
60
|
+
return db.result;
|
|
61
|
+
}));
|
|
62
|
+
async function getTransaction() {
|
|
63
|
+
let database = await getDB();
|
|
64
|
+
return database.transaction(objectStoreName, "readwrite").objectStore(objectStoreName);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let totalBufferCacheSize = 0;
|
|
68
|
+
let bufferCache = new Map<string, Buffer>();
|
|
69
|
+
|
|
70
|
+
// TODO: Batch this better, as each request is taking ~10ms on my laptop (even if only 0B is loaded).
|
|
71
|
+
async function getBufferCachedBase(bufferIds: string[]): Promise<(Buffer | undefined | "retry")[]> {
|
|
72
|
+
let transaction = await getTransaction();
|
|
73
|
+
|
|
74
|
+
let time = Date.now();
|
|
75
|
+
|
|
76
|
+
let bufferPromises: Map<string, Promise<Buffer | undefined | "retry">> = new Map();
|
|
77
|
+
for (let bufferId of new Set(bufferIds)) {
|
|
78
|
+
let memoryCache = bufferCache.get(bufferId);
|
|
79
|
+
if (memoryCache) {
|
|
80
|
+
bufferPromises.set(bufferId, Promise.resolve(memoryCache));
|
|
81
|
+
} else {
|
|
82
|
+
let result = transaction.get(bufferId);
|
|
83
|
+
let dataPromise = new Promise<Buffer | undefined | "retry">((resolve, reject) => {
|
|
84
|
+
result.addEventListener("success", () => {
|
|
85
|
+
let data = result.result;
|
|
86
|
+
if (!data) {
|
|
87
|
+
resolve(undefined);
|
|
88
|
+
} else {
|
|
89
|
+
resolve(asBuffer(data));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
result.addEventListener("error", e => {
|
|
93
|
+
let message = (e as any).target.error.message;
|
|
94
|
+
if (message.includes("The transaction was aborted")) {
|
|
95
|
+
resolve("retry");
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`IndexedDB error, skipping file ${bufferId}`, message, e);
|
|
98
|
+
resolve(undefined);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
bufferPromises.set(bufferId, dataPromise);
|
|
103
|
+
logErrors(dataPromise.then(data => {
|
|
104
|
+
if (data === "retry") return;
|
|
105
|
+
if (!data) return;
|
|
106
|
+
let prev = bufferCache.get(bufferId);
|
|
107
|
+
if (prev) {
|
|
108
|
+
totalBufferCacheSize -= prev.byteLength;
|
|
109
|
+
}
|
|
110
|
+
bufferCache.set(bufferId, data);
|
|
111
|
+
totalBufferCacheSize += data.byteLength;
|
|
112
|
+
while (totalBufferCacheSize > inMemoryLimit) {
|
|
113
|
+
let first = bufferCache.keys().next().value;
|
|
114
|
+
let firstBuffer = bufferCache.get(first);
|
|
115
|
+
if (!firstBuffer) {
|
|
116
|
+
throw new Error(`Expected buffer to exist for ${first}`);
|
|
117
|
+
}
|
|
118
|
+
bufferCache.delete(first);
|
|
119
|
+
totalBufferCacheSize -= firstBuffer.byteLength;
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
return await Promise.all(bufferIds.map(x => bufferPromises.get(x)));
|
|
127
|
+
}
|
|
128
|
+
const batchGetBuffer = batchFunction<{
|
|
129
|
+
bufferId: string;
|
|
130
|
+
callback: (result: Buffer | undefined) => void;
|
|
131
|
+
}>({ delay: 50 }, async function getDiskCacheBuffersInner(values) {
|
|
132
|
+
// Break buffers into separate transactions, as apparently large transactions can fail
|
|
133
|
+
// (or certain files can get into a bad state, making the entire transaction fail).
|
|
134
|
+
const BATCH_SIZE = 1000;
|
|
135
|
+
let maxRetries = 100;
|
|
136
|
+
while (maxRetries-- > 0 && values.length > 0) {
|
|
137
|
+
let retry: { bufferId: string; callback: (result: Buffer | undefined) => void }[] = [];
|
|
138
|
+
for (let i = 0; i < values.length; i += BATCH_SIZE) {
|
|
139
|
+
let results = await getBufferCachedBase(values.map(x => x.bufferId).slice(i, i + BATCH_SIZE));
|
|
140
|
+
for (let j = 0; j < results.length; j++) {
|
|
141
|
+
let result = results[j];
|
|
142
|
+
if (result === "retry") {
|
|
143
|
+
retry.push(values[i + j]);
|
|
144
|
+
} else {
|
|
145
|
+
values[i + j].callback(result);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
values = retry;
|
|
150
|
+
if (values.length > 0) {
|
|
151
|
+
console.log(`Retrying ${values.length} buffers`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (let { bufferId, callback } of values) {
|
|
156
|
+
callback(undefined);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const getDiskCacheBuffersOuter = measureWrap(async function getBufferCached(bufferIds: string[]): Promise<(Buffer | undefined)[]> {
|
|
162
|
+
let time = Date.now();
|
|
163
|
+
console.log(`Reading ${bufferIds.length} buffers from disk cache`);
|
|
164
|
+
let promises = bufferIds.map(x => new Promise<Buffer | undefined>((r, e) =>
|
|
165
|
+
batchGetBuffer({ bufferId: x, callback: r })
|
|
166
|
+
));
|
|
167
|
+
let results = await Promise.all(promises);
|
|
168
|
+
time = Date.now() - time;
|
|
169
|
+
let size = results.map(x => x?.byteLength ?? 0).reduce((a, b) => a + b, 0);
|
|
170
|
+
let count = results.filter(x => x).length;
|
|
171
|
+
console.log(`Read from disk cache ${formatNumber(count)} /// ${formatNumber(size)}B in ${formatTime(time)}`);
|
|
172
|
+
return results;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
function addBufferToCache(bufferId: string, buffer: Buffer) {
|
|
176
|
+
persist();
|
|
177
|
+
bufferCache.set(bufferId, buffer);
|
|
178
|
+
logErrors(batchedAddBufferToCache({ bufferId, buffer }));
|
|
179
|
+
}
|
|
180
|
+
const batchedAddBufferToCache = batchFunction<{
|
|
181
|
+
bufferId: string;
|
|
182
|
+
buffer: Buffer
|
|
183
|
+
}>(
|
|
184
|
+
{ delay: 1000 },
|
|
185
|
+
async values => {
|
|
186
|
+
let promises: Promise<unknown>[] = [];
|
|
187
|
+
// De-duplicate buffers
|
|
188
|
+
{
|
|
189
|
+
let finalBuffer = new Map<string, Buffer>();
|
|
190
|
+
for (let { buffer, bufferId } of values) {
|
|
191
|
+
finalBuffer.set(bufferId, buffer);
|
|
192
|
+
}
|
|
193
|
+
values = Array.from(finalBuffer).map(([bufferId, buffer]) => ({ bufferId, buffer }));
|
|
194
|
+
}
|
|
195
|
+
const BATCH_SIZE = 100;
|
|
196
|
+
for (let i = 0; i < values.length; i += BATCH_SIZE) {
|
|
197
|
+
let batch = values.slice(i, i + BATCH_SIZE);
|
|
198
|
+
let time = Date.now();
|
|
199
|
+
await measureBlock(async function batchedAddBufferToCache() {
|
|
200
|
+
for (let { bufferId, buffer } of batch) {
|
|
201
|
+
|
|
202
|
+
let transaction = await getTransaction();
|
|
203
|
+
let result = transaction.put(buffer, bufferId);
|
|
204
|
+
// NOTE: Wait for it to complete, so our timing can be more accurate.
|
|
205
|
+
promises.push(new Promise((resolve, reject) => {
|
|
206
|
+
result.addEventListener("success", resolve);
|
|
207
|
+
result.addEventListener("error", reject);
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
let total = batch.map(x => x.buffer.byteLength).reduce((a, b) => a + b, 0);
|
|
212
|
+
console.log(`Wrote ${formatNumber(total)} to disk cache in ${formatTime(Date.now() - time)}`);
|
|
213
|
+
}
|
|
214
|
+
debugger;
|
|
215
|
+
await Promise.all(promises);
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
get: getDiskCacheBuffersOuter,
|
|
221
|
+
async getSingle(bufferId: string) {
|
|
222
|
+
let result = await getDiskCacheBuffersOuter([bufferId]);
|
|
223
|
+
return result[0];
|
|
224
|
+
},
|
|
225
|
+
set: addBufferToCache,
|
|
226
|
+
};
|
|
227
|
+
}
|