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.
Files changed (65) hide show
  1. package/package.json +6 -6
  2. package/src/-b-authorities/cloudflareHelpers.ts +11 -2
  3. package/src/3-path-functions/PathFunctionRunner.ts +168 -97
  4. package/src/3-path-functions/PathFunctionRunnerMain.ts +8 -2
  5. package/src/3-path-functions/pathFunctionLoader.ts +11 -6
  6. package/src/3-path-functions/syncSchema.ts +10 -1
  7. package/src/4-deploy/edgeBootstrap.ts +10 -1
  8. package/src/4-querysub/Querysub.ts +77 -3
  9. package/src/4-querysub/QuerysubController.ts +22 -2
  10. package/src/4-querysub/permissions.ts +33 -2
  11. package/src/4-querysub/querysubPrediction.ts +52 -18
  12. package/src/archiveapps/archiveGCEntry.tsx +38 -0
  13. package/src/archiveapps/archiveJoinEntry.ts +121 -0
  14. package/src/archiveapps/archiveMergeEntry.tsx +47 -0
  15. package/src/archiveapps/compressTest.tsx +59 -0
  16. package/src/archiveapps/lockTest.ts +127 -0
  17. package/src/config.ts +5 -0
  18. package/src/diagnostics/managementPages.tsx +55 -0
  19. package/src/diagnostics/misc-pages/ArchiveInspect.tsx +325 -0
  20. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +781 -0
  21. package/src/diagnostics/misc-pages/ArchiveViewerTable.tsx +156 -0
  22. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +573 -0
  23. package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +129 -0
  24. package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +431 -0
  25. package/src/diagnostics/misc-pages/RequireAuditPage.tsx +218 -0
  26. package/src/diagnostics/misc-pages/SnapshotViewer.tsx +206 -0
  27. package/src/diagnostics/misc-pages/TimeRangeView.tsx +648 -0
  28. package/src/diagnostics/misc-pages/archiveViewerFilter.tsx +221 -0
  29. package/src/diagnostics/misc-pages/archiveViewerShared.tsx +76 -0
  30. package/src/email/postmark.tsx +40 -0
  31. package/src/email/sendgrid.tsx +44 -0
  32. package/src/functional/UndoWatch.tsx +133 -0
  33. package/src/functional/diff.ts +858 -0
  34. package/src/functional/promiseCache.ts +67 -0
  35. package/src/functional/random.ts +9 -0
  36. package/src/functional/runCommand.ts +42 -0
  37. package/src/functional/runOnce.ts +7 -0
  38. package/src/functional/stats.ts +61 -0
  39. package/src/functional/throttleRerender.tsx +80 -0
  40. package/src/library-components/AspectSizedComponent.tsx +88 -0
  41. package/src/library-components/Histogram.tsx +338 -0
  42. package/src/library-components/InlinePopup.tsx +67 -0
  43. package/src/library-components/Notifications.tsx +153 -0
  44. package/src/library-components/RenderIfVisible.tsx +80 -0
  45. package/src/library-components/SimpleNotification.tsx +133 -0
  46. package/src/library-components/TabbedUI.tsx +39 -0
  47. package/src/library-components/animateAnyElement.tsx +65 -0
  48. package/src/library-components/errorNotifications.tsx +81 -0
  49. package/src/library-components/placeholder.ts +18 -0
  50. package/src/misc/format2.ts +48 -0
  51. package/src/misc.ts +33 -0
  52. package/src/misc2.ts +5 -0
  53. package/src/server.ts +2 -1
  54. package/src/storage/diskCache.ts +227 -0
  55. package/src/storage/diskCache2.ts +122 -0
  56. package/src/storage/fileSystemPointer.ts +72 -0
  57. package/src/user-implementation/LoginPage.tsx +78 -0
  58. package/src/user-implementation/RequireAuditPage.tsx +219 -0
  59. package/src/user-implementation/SecurityPage.tsx +212 -0
  60. package/src/user-implementation/UserPage.tsx +320 -0
  61. package/src/user-implementation/addSuperUser.ts +21 -0
  62. package/src/user-implementation/canSeeSource.ts +41 -0
  63. package/src/user-implementation/loginEmail.tsx +159 -0
  64. package/src/user-implementation/setEmailKey.ts +20 -0
  65. package/src/user-implementation/userData.ts +974 -0
@@ -0,0 +1,218 @@
1
+ import preact from "preact"; import { qreact } from "../../4-dom/qreact";
2
+ import { Querysub } from "../../4-querysub/Querysub";
3
+ import { css } from "../../4-dom/css";
4
+ import { isDefined } from "../../misc";
5
+ import { formatNumber, formatTime } from "socket-function/src/formatting/format";
6
+ import { getPathStr1 } from "../../path";
7
+ import { URLParam, createURLSync } from "../../library-components/URLParam";
8
+ import { Input } from "../../library-components/Input";
9
+ import { ButtonSelector } from "../../library-components/ButtonSelector";
10
+
11
+ type ModuleFolderEntry = {
12
+ path: string;
13
+ children: ModuleTreeEntry[];
14
+ sum: number;
15
+ count: number;
16
+ };
17
+ type ModuleTreeEntry = {
18
+ module: NodeJS.Module;
19
+ } | ModuleFolderEntry;
20
+
21
+ const filterURL = createURLSync<string>("filter", "");
22
+ const expandedURL = createURLSync<string>("expanded", "");
23
+
24
+ const valueType = new URLParam("requireauditvalue", "size" as "size" | "evaltime");
25
+ export class RequireAuditPage extends qreact.Component {
26
+ getModuleValue(module: NodeJS.Module) {
27
+ if (valueType.value === "evaltime") return (module.evalEndTime ?? 0) - (module.evalStartTime ?? 0);
28
+ return module.size || 0;
29
+ }
30
+ formatValue(value: number) {
31
+ if (valueType.value === "evaltime") return formatTime(value);
32
+ return formatNumber(value) + "B";
33
+ }
34
+
35
+ getFontSize(size: number, total: number) {
36
+ if (size / total > 0.20) return 30;
37
+ if (size / total > 0.02) return 20;
38
+ return 12;
39
+ return (
40
+ Math.max((size / total) ** 0.5 * 50, 10)
41
+ );
42
+ }
43
+ renderModule(module: NodeJS.Module, depth: number) {
44
+ return (
45
+ <>
46
+ <div>{module.filename}</div>
47
+ </>
48
+ );
49
+ }
50
+
51
+ renderTreeLevel(entry: ModuleTreeEntry[], root = true, depth = 0, config: {
52
+ totalSum: number;
53
+ maxSum: number;
54
+ expanded: string;
55
+ }): preact.ComponentChild {
56
+ const renderLine = (entry: ModuleTreeEntry, contents: preact.ComponentChild) => {
57
+ let size = "module" in entry ? this.getModuleValue(entry.module) : entry.sum;
58
+ let path = "module" in entry ? entry.module.filename : entry.path;
59
+ let expanded = config.expanded.includes(getPathStr1(path));
60
+ const isTreeNode = !("module" in entry);
61
+ return (
62
+ <div class={
63
+ css.hbox(10).marginLeft(depth * 20).relative.fillWidth
64
+ .fontSize(this.getFontSize(size, config.totalSum))
65
+ + (isTreeNode && css.button)
66
+ }
67
+ onClick={(e) => {
68
+ if (!isTreeNode) return;
69
+ if (expanded) {
70
+ expandedURL.value = config.expanded.replace(getPathStr1(entry.path), "");
71
+ } else {
72
+ expandedURL.value += getPathStr1(entry.path);
73
+ }
74
+ }}
75
+ >
76
+ <div
77
+ class={
78
+ css.absolute.pos(0, 2).height("calc(100% - 4px)" as "100%").width((size / config.maxSum) * 100 + "%" as "100%")
79
+ .hsl(isTreeNode ? 180 : 210, 75, 50).opacity(0.5)
80
+ .borderRadius(2)
81
+ .zIndex(-1)
82
+ }
83
+ />
84
+ {contents}
85
+ <span class={css.opacity(0.4)}>
86
+ {
87
+ "module" in entry
88
+ ? <>({this.formatValue(this.getModuleValue(entry.module))}) / {(size / config.totalSum * 100).toFixed(1)}%</>
89
+ : <>({formatNumber(entry.count)} / {this.formatValue(entry.sum)}) / {(size / config.totalSum * 100).toFixed(1)}%</>
90
+ }
91
+ </span>
92
+ </div>
93
+ );
94
+ };
95
+ if (entry.length === 1) {
96
+ let singleEntry = entry[0];
97
+ if ("module" in singleEntry) {
98
+ return renderLine(singleEntry, this.renderModule(singleEntry.module, depth));
99
+ } else {
100
+ return this.renderTreeLevel(singleEntry.children, root, depth, config);
101
+ }
102
+ }
103
+ return (
104
+ <div class={css.vbox(0).fillWidth}>
105
+ {entry.map(entry => {
106
+ if ("module" in entry) return renderLine(entry, this.renderModule(entry.module, depth));
107
+ let expanded = config.expanded.includes(getPathStr1(entry.path));
108
+ return (
109
+ <div
110
+ className={css.vbox(0).fillWidth
111
+ + (expanded && css.paddingBottom(10))
112
+ }
113
+ >
114
+ {renderLine(entry,
115
+ <span>{entry.path}</span>
116
+ )}
117
+ {expanded && this.renderTreeLevel(entry.children, false, depth + 1, config)}
118
+ </div>
119
+ );
120
+ })}
121
+ </div>
122
+ );
123
+ }
124
+ render() {
125
+ const self = this;
126
+
127
+ // NOTE: We might need to re-render periodically, in order to catch new requires that are added.
128
+ // However... we want to avoid this, as rendering when the user is typing can cause issues.
129
+ // Querysub.timeDelayed(5000);
130
+
131
+ let rootModules: ModuleFolderEntry[] = [];
132
+ let modulesList = Object.values(require.cache).filter(isDefined);
133
+ let filter = filterURL.value.toLowerCase();
134
+ if (filter) {
135
+ modulesList = modulesList.filter(module => module.filename.toLowerCase().includes(filter));
136
+ }
137
+
138
+ let entryByPath = new Map<string, ModuleFolderEntry>();
139
+ function getParentPath(path: string) {
140
+ return path.split("/").slice(0, -1).join("/");
141
+ }
142
+ function getTreeEntry(path: string) {
143
+ let entry = entryByPath.get(path);
144
+ if (!entry) {
145
+ entry = { children: [], path, sum: 0, count: 0, };
146
+ entryByPath.set(path, entry);
147
+ let parent = getParentPath(path);
148
+ if (parent) {
149
+ getTreeEntry(parent).children.push(entry);
150
+ } else {
151
+ rootModules.push(entry);
152
+ }
153
+ }
154
+ return entry;
155
+ }
156
+ for (let module of modulesList) {
157
+ getTreeEntry(getParentPath(module.filename)).children.push({ module });
158
+ }
159
+
160
+ function calculateSizes(entry: ModuleFolderEntry) {
161
+ entry.sum = 0;
162
+ entry.count = 0;
163
+ for (let child of entry.children) {
164
+ if ("module" in child) {
165
+ entry.sum += self.getModuleValue(child.module);
166
+ entry.count++;
167
+ } else {
168
+ calculateSizes(child);
169
+ entry.sum += child.sum;
170
+ entry.count += child.count;
171
+ }
172
+ }
173
+ }
174
+ for (let rootModule of rootModules) {
175
+ calculateSizes(rootModule);
176
+ }
177
+
178
+ while (rootModules.length === 1) {
179
+ rootModules = rootModules[0].children as ModuleFolderEntry[] || [];
180
+ }
181
+
182
+ let totalCount = rootModules.reduce((sum, entry) => sum + entry.count, 0);
183
+ let totalSum = rootModules.reduce((sum, entry) => sum + entry.sum, 0);
184
+ let maxSum = Math.max(...rootModules.map(entry => entry.sum));
185
+
186
+ return (
187
+ <div class={css.pad(20).vbox(20).fillWidth.overflowY("auto").overflowX("hidden")}>
188
+ <div class={css.hbox(40)}>
189
+ <h1>
190
+ {totalCount} / {this.formatValue(totalSum)}
191
+ </h1>
192
+ <ButtonSelector
193
+ options={[
194
+ { value: "size", title: "Size" },
195
+ { value: "evaltime", title: "Eval Time (Includes Child Time)" },
196
+ ]}
197
+ value={valueType.value}
198
+ onChange={value => valueType.value = value as any}
199
+ />
200
+ </div>
201
+ <Input
202
+ flavor="large"
203
+ focusOnMount
204
+ placeholder={"Filter"}
205
+ className={css.width("60vw")}
206
+ value={filterURL.value}
207
+ hot
208
+ onChange={e => filterURL.value = e.currentTarget.value}
209
+ />
210
+ {this.renderTreeLevel(rootModules, undefined, undefined, {
211
+ totalSum,
212
+ maxSum,
213
+ expanded: expandedURL.value,
214
+ })}
215
+ </div>
216
+ );
217
+ }
218
+ }
@@ -0,0 +1,206 @@
1
+ module.allowclient = true;
2
+
3
+ import { ArchiveSnapshotOverview, ArchiveSnapshotRead, getSnapshot, getSnapshotList, loadSnapshot, saveSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
4
+ import { qreact } from "../../4-dom/qreact";
5
+ import { SocketFunction } from "socket-function/SocketFunction";
6
+ import { ValueAuditController } from "../../5-diagnostics/memoryValueAudit";
7
+ import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
8
+ import { Table } from "../../5-diagnostics/Table";
9
+ import { css } from "typesafecss";
10
+ import { t } from "../../4-querysub/Querysub";
11
+ import { ATag, Anchor } from "../../library-components/ATag";
12
+ import { URLParam } from "../../library-components/URLParam";
13
+ import { formatDateTime, formatNumber } from "socket-function/src/formatting/format";
14
+ import { Button } from "../../library-components/Button";
15
+ import { showModal } from "../../5-diagnostics/Modal";
16
+ import { FullscreenModal } from "../../5-diagnostics/FullscreenModal";
17
+ import { InputLabel } from "../../library-components/InputLabel";
18
+ import { green, magenta, red } from "socket-function/src/formatting/logColors";
19
+ import { archiveViewerHistoryPaths } from "./ArchiveViewer";
20
+ import { assertIsManagementUser, managementPageURL } from "../../diagnostics/managementPages";
21
+ import { getSyncedController } from "../../library-components/SyncedController";
22
+
23
+ // TODO: Create a transaction viewer, so we can view transactions in the recycle bin and data,
24
+ // to see why various files were moved.
25
+ // - This can help debug bad archives, who accidentally delete the entire database, create new files, etc.
26
+ // - Which apparently... we have? Although maybe we fixed it?
27
+
28
+ const selectedSnapshot = new URLParam("selectedSnapshot", "");
29
+ export class SnapshotViewer extends qreact.Component {
30
+ state = t.state({
31
+ // file => true
32
+ expanded: t.lookup(t.boolean)
33
+ });
34
+ render() {
35
+ let controller = SnapshotViewerSynced(getBrowserUrlNode());
36
+ let snapshotList = controller.getSnapshotList();
37
+ if (!snapshotList) return <div>Loading...</div>;
38
+ let file = selectedSnapshot.value;
39
+ if (!file) {
40
+ return (
41
+ <div class={css.pad2(10)}>
42
+ <Table
43
+ rows={snapshotList}
44
+ columns={{
45
+ time: {},
46
+ fileCount: {},
47
+ valueCount: {},
48
+ byteCount: {},
49
+ file: {
50
+ title: "Actions",
51
+ formatter: (file, context) => {
52
+ return <div class={css.hbox(10)}>
53
+ <ATag values={[{ param: selectedSnapshot, value: file }]}>
54
+ View
55
+ </ATag>
56
+ {context?.row?.file === "live" &&
57
+ <Button onClick={async () => {
58
+ await SnapshotViewerController.nodes[getBrowserUrlNode()].saveLiveSnapshot();
59
+ console.log("Saved live snapshot.");
60
+ }}>
61
+ Save
62
+ </Button>
63
+ }
64
+ </div>;
65
+ }
66
+ }
67
+ }}
68
+ />
69
+ </div>
70
+ );
71
+ }
72
+
73
+
74
+ const entry = snapshotList.find(x => x.file === file);
75
+ let snapshotInfo = controller.getSnapshot(file);
76
+ if (!entry) return <div>Snapshot not found, {file}</div>;
77
+ return (
78
+ <div class={css.pad2(10).vbox(10)}>
79
+ <div class={css.hbox(10)}>
80
+ <Anchor values={[{ param: selectedSnapshot, value: "" }]}>
81
+ Back
82
+ </Anchor>
83
+ <h2>Snapshot: {file}</h2>
84
+ </div>
85
+ {entry &&
86
+ <div class={css.vbox(2)}>
87
+ <div>Time: {formatDateTime(entry.time)}</div>
88
+ <div>File Count: {formatNumber(entry.fileCount)}</div>
89
+ <div>Value Count: {formatNumber(entry.valueCount)}</div>
90
+ <div>Byte Count: {formatNumber(entry.byteCount)}</div>
91
+ </div>
92
+ }
93
+ <Button onClick={async () => {
94
+ let didConfirm = await seriousConfirm({
95
+ message: `Restoring will break all servers. This won't break your data, but you will have to restart all servers before the system will be stable again. Before then data will be random, and invalid writes might be allowed. It is recommend to run this with only 1 querysub server running, on a developers machine. Consider writing a database iterate script instead of restoring (similar to how gc and join work).`,
96
+ });
97
+ if (!didConfirm) {
98
+ console.log(red("Aborted restore"));
99
+ return;
100
+ }
101
+ console.log(magenta("Restoring snapshot..."));
102
+ await SnapshotViewerController.nodes[getBrowserUrlNode()].loadSnapshot({
103
+ overview: entry
104
+ });
105
+ console.log(green("Restored snapshot"));
106
+ window.location.reload();
107
+ }}>
108
+ Restore
109
+ </Button>
110
+
111
+ {snapshotInfo && <Anchor values={[
112
+ { param: archiveViewerHistoryPaths, value: snapshotInfo.files.map(x => x.file) },
113
+ { param: managementPageURL, value: "ArchiveViewer" },
114
+ ]}>
115
+ View in Archive Viewer
116
+ </Anchor>}
117
+ {snapshotInfo &&
118
+ <Table
119
+ rows={snapshotInfo.files}
120
+ columns={{
121
+ //file: {},
122
+ state: {},
123
+ time: {},
124
+ valueCount: {},
125
+ byteCount: {},
126
+ source: {},
127
+ }}
128
+ />
129
+ }
130
+ </div>
131
+ );
132
+ }
133
+ }
134
+
135
+ async function seriousConfirm(config: {
136
+ message: string;
137
+ }): Promise<boolean> {
138
+ return await new Promise<boolean>((resolve, reject) => {
139
+ let modal = showModal({
140
+ content: <FullscreenModal onCancel={() => {
141
+ modal.close();
142
+ resolve(false);
143
+ }}>
144
+ <div>{config.message}</div>
145
+ <InputLabel
146
+ label={<pre>
147
+ Kill the PathValueServer first.
148
+ Type "confirm" and press enter to continue.
149
+ Then watch querysub logs and wait until "finished loading snapshot" is logged.
150
+ Then kill querysub, and start all the processes normally.
151
+ </pre>}
152
+ onChangeValue={value => {
153
+ if (value === "confirm") {
154
+ modal.close();
155
+ resolve(true);
156
+ }
157
+ }}
158
+ />
159
+ </FullscreenModal>
160
+ });
161
+ });
162
+ }
163
+
164
+ class SnapshotViewerControllerBase {
165
+ public async getSnapshotList(): Promise<ArchiveSnapshotOverview[]> {
166
+ return await getSnapshotList();
167
+ }
168
+
169
+ public async loadSnapshot(config: {
170
+ overview: ArchiveSnapshotOverview,
171
+ }): Promise<void> {
172
+ await loadSnapshot({
173
+ overview: config.overview,
174
+ async audit(nodeId) {
175
+ await ValueAuditController.nodes[nodeId].diskAuditNow();
176
+ },
177
+ });
178
+ }
179
+
180
+ public async getSnapshot(snapshotFile: string | "live"): Promise<ArchiveSnapshotRead> {
181
+ return await getSnapshot(snapshotFile);
182
+ }
183
+
184
+ public async saveLiveSnapshot() {
185
+ let files = await getSnapshot("live");
186
+ await saveSnapshot({ files: files.files.map(x => x.file) });
187
+ }
188
+ }
189
+
190
+ export const SnapshotViewerController = SocketFunction.register(
191
+ "SnapshotViewerController-a78ae3cf-5024-49da-b71a-f064a5d4b5f4",
192
+ new SnapshotViewerControllerBase(),
193
+ () => ({
194
+ getSnapshotList: {},
195
+ loadSnapshot: {},
196
+ getSnapshot: {},
197
+ saveLiveSnapshot: {},
198
+ }),
199
+ () => ({
200
+ hooks: [assertIsManagementUser],
201
+ }),
202
+ {
203
+ noAutoExpose: true,
204
+ }
205
+ );
206
+ const SnapshotViewerSynced = getSyncedController(SnapshotViewerController);