querysub 0.2.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/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { css } from "typesafecss";
|
|
2
|
+
import { qreact } from "../../4-dom/qreact";
|
|
3
|
+
import { canHaveChildren } from "socket-function/src/types";
|
|
4
|
+
import { parseAnsiColors, rgbToHsl } from "./ansiFormat";
|
|
5
|
+
import { ShowMore } from "../../library-components/ShowMore";
|
|
6
|
+
import { formatValue } from "../../5-diagnostics/GenericFormat";
|
|
7
|
+
import { Button } from "../../library-components/Button";
|
|
8
|
+
import { MeasureHeightCSS } from "../../library-components/MeasureHeightCSS";
|
|
9
|
+
|
|
10
|
+
module.hotreload = true;
|
|
11
|
+
module.hotreload = true;
|
|
12
|
+
|
|
13
|
+
export class ObjectDisplay extends qreact.Component<{
|
|
14
|
+
value: unknown;
|
|
15
|
+
excludedFields?: string[];
|
|
16
|
+
onClickKey?: (path: string[]) => void;
|
|
17
|
+
onClickValue?: (path: string[], value: unknown) => void;
|
|
18
|
+
maxHeight?: number;
|
|
19
|
+
}> {
|
|
20
|
+
state = {
|
|
21
|
+
expanded: false,
|
|
22
|
+
};
|
|
23
|
+
render() {
|
|
24
|
+
const maxHeight = this.props.maxHeight ?? 60;
|
|
25
|
+
let value = this.props.value;
|
|
26
|
+
let exclude = this.props.excludedFields;
|
|
27
|
+
let expanded = this.state.expanded;
|
|
28
|
+
let { onClickKey, onClickValue } = this.props;
|
|
29
|
+
|
|
30
|
+
const renderValue = (value: unknown, path: string[]): preact.ComponentChild => {
|
|
31
|
+
if (path.length === 0 && (typeof value === "string" && !value.includes("\x1b") || typeof value === "number" || typeof value === "boolean")) {
|
|
32
|
+
return <span
|
|
33
|
+
className={css.pad2(4, 2)
|
|
34
|
+
+ (onClickValue && css.pointer.background("hsla(210, 70%, 90%, 0.5)", "hover"))
|
|
35
|
+
}
|
|
36
|
+
onClick={() => onClickValue?.([], value)}
|
|
37
|
+
>
|
|
38
|
+
{formatValue(value)}
|
|
39
|
+
</span>;
|
|
40
|
+
}
|
|
41
|
+
if (!canHaveChildren(value)) return <PrimitiveDisplay value={value} />;
|
|
42
|
+
|
|
43
|
+
let isArray = Array.isArray(value);
|
|
44
|
+
let startEnd = isArray ? "[]" : "{}";
|
|
45
|
+
let spacingCss = css.marginRight(0);
|
|
46
|
+
let entries = Object.entries(value);
|
|
47
|
+
entries = entries.filter(([key]) => {
|
|
48
|
+
let nestedPath = [...path, key];
|
|
49
|
+
let pathFlat = nestedPath.join(".");
|
|
50
|
+
return !exclude?.includes(pathFlat);
|
|
51
|
+
});
|
|
52
|
+
return <>
|
|
53
|
+
<span className={spacingCss}>{startEnd[0]}</span>
|
|
54
|
+
{entries.map(([key, value], index, list) => {
|
|
55
|
+
let nestedPath = [...path, key];
|
|
56
|
+
return <div className={
|
|
57
|
+
(
|
|
58
|
+
expanded && css.marginLeft(20)
|
|
59
|
+
|| css.display("inline")
|
|
60
|
+
)
|
|
61
|
+
+ spacingCss
|
|
62
|
+
}>
|
|
63
|
+
{!isArray && <>
|
|
64
|
+
<span
|
|
65
|
+
className={
|
|
66
|
+
css.boldStyle.hslcolor(210, 70, 50)
|
|
67
|
+
.pad2(4, 2)
|
|
68
|
+
+ (onClickKey && css.pointer.background("hsla(210, 70%, 90%, 0.5)", "hover"))
|
|
69
|
+
}
|
|
70
|
+
onClick={() => onClickKey?.(nestedPath)}
|
|
71
|
+
>
|
|
72
|
+
{key}
|
|
73
|
+
</span>
|
|
74
|
+
<span className={spacingCss}>:</span>
|
|
75
|
+
</>}
|
|
76
|
+
<span
|
|
77
|
+
className={
|
|
78
|
+
css.pad2(4, 2) +
|
|
79
|
+
(onClickValue && css.pointer.background("hsla(210, 70%, 90%, 0.5)", "hover"))
|
|
80
|
+
}
|
|
81
|
+
onClick={() => onClickValue?.(nestedPath, value)}
|
|
82
|
+
>
|
|
83
|
+
{renderValue(value, nestedPath)}
|
|
84
|
+
</span>
|
|
85
|
+
{index !== list.length - 1 && <span>,</span>}
|
|
86
|
+
</div>;
|
|
87
|
+
})}
|
|
88
|
+
<span className={""}>{startEnd[1]}</span>
|
|
89
|
+
</>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (!canHaveChildren(value)) {
|
|
93
|
+
return <ShowMore maxHeight={maxHeight} className={css.maxWidth("50vw")}>
|
|
94
|
+
<span className={css.maxHeight("70vh").overflowAuto}>
|
|
95
|
+
<div class={css.relative}>
|
|
96
|
+
{renderValue(value, [])}
|
|
97
|
+
<MeasureHeightCSS />
|
|
98
|
+
</div>
|
|
99
|
+
</span>
|
|
100
|
+
</ShowMore>;
|
|
101
|
+
}
|
|
102
|
+
return <div className={
|
|
103
|
+
css.hbox(6).alignItems("start")
|
|
104
|
+
+ (!expanded && css.maxHeight(maxHeight).overflowHidden)
|
|
105
|
+
+ (expanded && css.maxHeight("70vh").overflowAuto)
|
|
106
|
+
}>
|
|
107
|
+
{canHaveChildren(value) && <Button
|
|
108
|
+
className={
|
|
109
|
+
css.hsl(270, 50, 50).background("hsl(270, 50%, 70%)", "hover")
|
|
110
|
+
.width(70)
|
|
111
|
+
.height(maxHeight)
|
|
112
|
+
.flexShrink0
|
|
113
|
+
.center
|
|
114
|
+
}
|
|
115
|
+
onClick={() => this.state.expanded = !this.state.expanded}
|
|
116
|
+
>
|
|
117
|
+
{expanded ? "Collapse" : "Expand"}
|
|
118
|
+
</Button>}
|
|
119
|
+
<span className={css.maxWidth("50vw").maxHeight("50vh").pad2(2).overflowAuto}>
|
|
120
|
+
<div className={css.relative}>
|
|
121
|
+
{renderValue(value, [])}
|
|
122
|
+
<MeasureHeightCSS />
|
|
123
|
+
</div>
|
|
124
|
+
</span>
|
|
125
|
+
</div>;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class PrimitiveDisplay extends qreact.Component<{
|
|
130
|
+
value: unknown;
|
|
131
|
+
}> {
|
|
132
|
+
render() {
|
|
133
|
+
let value = this.props.value;
|
|
134
|
+
if (typeof value === "number") {
|
|
135
|
+
return <span className={css.boldStyle.hslcolor(265, 70, 60)}>{value}</span>;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "boolean") {
|
|
138
|
+
return <span className={css.boldStyle.hslcolor(207, 70, 60)}>{value}</span>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof value === "string") {
|
|
142
|
+
let colors = parseAnsiColors(value);
|
|
143
|
+
if (colors.length === 1 && !colors[0].color) {
|
|
144
|
+
return <span className={css.boldStyle.hslcolor(30, 70, 50)}>"{value}"</span>;
|
|
145
|
+
}
|
|
146
|
+
return (
|
|
147
|
+
<span className={css.display("inline-flex").rowGap(4).columnGap(1).wrap}>
|
|
148
|
+
{colors.map(({ color, text }) => {
|
|
149
|
+
let hue = color ? rgbToHsl(color).h : undefined;
|
|
150
|
+
return <span
|
|
151
|
+
className={
|
|
152
|
+
(hue !== undefined && css.boldStyle.pad2(4, 1).hsla(hue, 70, 50, 0.5) || "")
|
|
153
|
+
+ (hue === undefined && css.boldStyle.hslcolor(30, 70, 50))
|
|
154
|
+
}
|
|
155
|
+
>{text}</span>;
|
|
156
|
+
})}
|
|
157
|
+
</span>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<span className={css.boldStyle.hslcolor(0, 70, 70)}>{value}</span>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module.allowclient = true;
|
|
2
|
+
export interface ColorGroup {
|
|
3
|
+
text: string;
|
|
4
|
+
color?: {
|
|
5
|
+
r: number;
|
|
6
|
+
g: number;
|
|
7
|
+
b: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const basicToRGB: Record<number, { r: number; g: number; b: number }> = {
|
|
12
|
+
30: { r: 0, g: 0, b: 0 },
|
|
13
|
+
31: { r: 205, g: 49, b: 49 },
|
|
14
|
+
32: { r: 85, g: 188, b: 64 },
|
|
15
|
+
33: { r: 229, g: 229, b: 16 },
|
|
16
|
+
34: { r: 36, g: 114, b: 200 },
|
|
17
|
+
35: { r: 188, g: 63, b: 188 },
|
|
18
|
+
36: { r: 17, g: 168, b: 205 },
|
|
19
|
+
37: { r: 229, g: 229, b: 229 },
|
|
20
|
+
90: { r: 102, g: 102, b: 102 },
|
|
21
|
+
91: { r: 241, g: 76, b: 76 },
|
|
22
|
+
92: { r: 35, g: 209, b: 139 },
|
|
23
|
+
93: { r: 245, g: 245, b: 67 },
|
|
24
|
+
94: { r: 59, g: 142, b: 234 },
|
|
25
|
+
95: { r: 214, g: 112, b: 214 },
|
|
26
|
+
96: { r: 41, g: 184, b: 219 },
|
|
27
|
+
97: { r: 255, g: 255, b: 255 },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function parseAnsiColors(input: string): ColorGroup[] {
|
|
31
|
+
const result: ColorGroup[] = [];
|
|
32
|
+
let currentIndex = 0;
|
|
33
|
+
const regex = /\x1b\[(38;2;(\d+);(\d+);(\d+)m|(\d+)m)/g;
|
|
34
|
+
let match;
|
|
35
|
+
|
|
36
|
+
while ((match = regex.exec(input)) !== null) {
|
|
37
|
+
// Add any text before this color code
|
|
38
|
+
if (match.index > currentIndex) {
|
|
39
|
+
const text = input.slice(currentIndex, match.index);
|
|
40
|
+
if (text) {
|
|
41
|
+
result.push({ text });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Find the end of this colored section
|
|
46
|
+
const colorStart = match.index + match[0].length;
|
|
47
|
+
const nextColorIndex = input.indexOf("\x1b", colorStart);
|
|
48
|
+
const textEnd = nextColorIndex === -1 ? input.length : nextColorIndex;
|
|
49
|
+
const text = input.slice(colorStart, textEnd);
|
|
50
|
+
|
|
51
|
+
if (text) {
|
|
52
|
+
const [, fullColor, r, g, b, basicCode] = match;
|
|
53
|
+
if (r && g && b) {
|
|
54
|
+
result.push({
|
|
55
|
+
text,
|
|
56
|
+
color: {
|
|
57
|
+
r: parseInt(r),
|
|
58
|
+
g: parseInt(g),
|
|
59
|
+
b: parseInt(b)
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
} else if (basicCode) {
|
|
63
|
+
const code = parseInt(basicCode);
|
|
64
|
+
const rgbColor = basicToRGB[code];
|
|
65
|
+
result.push({
|
|
66
|
+
text,
|
|
67
|
+
color: rgbColor
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
currentIndex = textEnd;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add any remaining text after the last color code
|
|
76
|
+
if (currentIndex < input.length) {
|
|
77
|
+
const text = input.slice(currentIndex);
|
|
78
|
+
if (text) {
|
|
79
|
+
result.push({ text });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
export function rgbToHsl(color: { r: number; g: number; b: number }) {
|
|
88
|
+
let { r, g, b } = color;
|
|
89
|
+
r /= 255;
|
|
90
|
+
g /= 255;
|
|
91
|
+
b /= 255;
|
|
92
|
+
let max = Math.max(r, g, b);
|
|
93
|
+
let min = Math.min(r, g, b);
|
|
94
|
+
let h = 0;
|
|
95
|
+
let s = 0;
|
|
96
|
+
let l = (max + min) / 2;
|
|
97
|
+
if (max !== min) {
|
|
98
|
+
let d = max - min;
|
|
99
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
100
|
+
switch (max) {
|
|
101
|
+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
|
102
|
+
case g: h = (b - r) / d + 2; break;
|
|
103
|
+
case b: h = (r - g) / d + 4; break;
|
|
104
|
+
}
|
|
105
|
+
h *= 60;
|
|
106
|
+
}
|
|
107
|
+
return { h, s, l };
|
|
108
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { lazy } from "socket-function/src/caching";
|
|
3
|
+
import { getExternalIP } from "socket-function/src/networking";
|
|
4
|
+
import { decodeNodeId } from "../../-a-auth/certs";
|
|
5
|
+
import { getOwnNodeId } from "../../-f-node-discovery/NodeDiscovery";
|
|
6
|
+
import { logErrors } from "../../errors";
|
|
7
|
+
import { addGlobalContext } from "./diskLogger";
|
|
8
|
+
import child_process from "child_process";
|
|
9
|
+
|
|
10
|
+
export function addBuiltInContext() {
|
|
11
|
+
addGlobalContext(() => {
|
|
12
|
+
let nodeId = getOwnNodeId();
|
|
13
|
+
let nodeParts = decodeNodeId(nodeId);
|
|
14
|
+
return {
|
|
15
|
+
__machineId: nodeParts?.machineId,
|
|
16
|
+
__mountId: SocketFunction.mountedNodeId,
|
|
17
|
+
__threadId: nodeParts?.threadId,
|
|
18
|
+
__port: nodeParts?.port,
|
|
19
|
+
__nodeId: nodeId,
|
|
20
|
+
__entry: process.argv[1],
|
|
21
|
+
__hostName: getHostname(),
|
|
22
|
+
__accountName: getAccountName(),
|
|
23
|
+
__externalIP: cachedExternalIP(),
|
|
24
|
+
__os: process.platform,
|
|
25
|
+
__pid: process.pid,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
const getHostname = lazy(() => child_process.execSync("hostname").toString().trim());
|
|
29
|
+
const getAccountName = lazy(() => child_process.execSync("whoami").toString().trim());
|
|
30
|
+
const cachedExternalIP = lazy(() => {
|
|
31
|
+
logErrors((async () => {
|
|
32
|
+
let ip = await getExternalIP();
|
|
33
|
+
cachedExternalIP.set(ip);
|
|
34
|
+
})());
|
|
35
|
+
return "";
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
|
|
2
|
+
import { batchFunction, runInfinitePoll } from "socket-function/src/batching";
|
|
3
|
+
import { timeInDay, timeInHour } from "socket-function/src/misc";
|
|
4
|
+
import { getStorageDir, getStorageFolder } from "../../fs";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import { canHaveChildren } from "socket-function/src/types";
|
|
7
|
+
import { SizeLimiter } from "../SizeLimiter";
|
|
8
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
|
+
import { isNode } from "typesafecss";
|
|
10
|
+
import { logGitHashes } from "./logGitHashes";
|
|
11
|
+
|
|
12
|
+
if (isNode()) {
|
|
13
|
+
// Delayed setup, as we depend on diskLogger early, and we don't want to force high level
|
|
14
|
+
// modules to be required before their level
|
|
15
|
+
setImmediate((async () => {
|
|
16
|
+
await import("./DiskLoggerPage");
|
|
17
|
+
const { addBuiltInContext } = await import("./diskLogGlobalContext");
|
|
18
|
+
addBuiltInContext();
|
|
19
|
+
await logGitHashes();
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// TODO: If we run into size problems with our logs, we might want to use compression?
|
|
24
|
+
// - We want chunks to be < 4096, but... we might still be able to compress a bit?
|
|
25
|
+
|
|
26
|
+
let folder = getStorageFolder("disklogs");
|
|
27
|
+
|
|
28
|
+
let SIZE_LIMIT = new SizeLimiter({
|
|
29
|
+
diskRoot: getStorageDir(),
|
|
30
|
+
|
|
31
|
+
maxBytes: 1024 * 1024 * 1024 * 1,
|
|
32
|
+
minBytes: 1024 * 1024 * 10,
|
|
33
|
+
maxDiskFraction: 0.02,
|
|
34
|
+
maxTotalDiskFraction: 0.95,
|
|
35
|
+
maxFiles: 1000,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const LOG_FILE_DURATION = timeInDay;
|
|
39
|
+
|
|
40
|
+
export type LogObj = {
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
time: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const noDiskLogPrefix = "\u200C";
|
|
46
|
+
|
|
47
|
+
export const diskLog = logDisk;
|
|
48
|
+
export function logDisk(...args: unknown[]) {
|
|
49
|
+
if (!isNode()) return;
|
|
50
|
+
try {
|
|
51
|
+
if (args.length === 0) return;
|
|
52
|
+
// Move the first string argument to the front
|
|
53
|
+
if (args.length > 0 && typeof args[0] !== "string" && args.some(x => typeof x === "string")) {
|
|
54
|
+
let strIndex = args.findIndex(x => typeof x === "string");
|
|
55
|
+
let str = args[strIndex];
|
|
56
|
+
args.splice(strIndex, 1);
|
|
57
|
+
args.unshift(str);
|
|
58
|
+
}
|
|
59
|
+
if (args.length > 0 && typeof args[0] === "string" && args[0].trim().length === 0) return;
|
|
60
|
+
if (args.length > 0 && typeof args[0] === "string" && args[0].startsWith(noDiskLogPrefix)) return;
|
|
61
|
+
let logObj = packageLogObj(args);
|
|
62
|
+
logQueue(logObj);
|
|
63
|
+
} catch (e: any) {
|
|
64
|
+
process.stdout.write(`Error writing to disk logs: ${e.stack || e}\n\t${String(args[0])}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let logPending: LogObj[] = [];
|
|
68
|
+
function logQueue(log: LogObj) {
|
|
69
|
+
if (logPending.length === 0) {
|
|
70
|
+
void Promise.resolve().finally(logClearQueue);
|
|
71
|
+
}
|
|
72
|
+
logPending.push(log);
|
|
73
|
+
}
|
|
74
|
+
function logClearQueue() {
|
|
75
|
+
let commit = logPending;
|
|
76
|
+
logPending = [];
|
|
77
|
+
logBase(commit).catch(e => {
|
|
78
|
+
process.stdout.write(`Error writing to disk logs: ${e.stack || e}\n\t${JSON.stringify(commit[0])}\n`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
let globalContextParts: (() => { [key: string]: unknown })[] = [];
|
|
84
|
+
export function addGlobalContext(fnc: () => { [key: string]: unknown }) {
|
|
85
|
+
// If the contents are identical, don't add it. This stops hot reloading from breaking logging,
|
|
86
|
+
// by making the global context unmanageablely large
|
|
87
|
+
if (globalContextParts.some(v => v.toString() === fnc.toString())) return;
|
|
88
|
+
globalContextParts.push(fnc);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
export type LogFile = {
|
|
94
|
+
startTime: number;
|
|
95
|
+
endTime: number;
|
|
96
|
+
path: string;
|
|
97
|
+
name: string;
|
|
98
|
+
|
|
99
|
+
// Only set in getLogFiles
|
|
100
|
+
size: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export async function getLogFiles(): Promise<LogFile[]> {
|
|
104
|
+
let files = await fs.promises.readdir(folder);
|
|
105
|
+
let paths = files.map(file => folder + file);
|
|
106
|
+
let objs = paths.map(decodeLogFileName);
|
|
107
|
+
for (let obj of objs) {
|
|
108
|
+
try {
|
|
109
|
+
let stat = await fs.promises.stat(obj.path);
|
|
110
|
+
obj.size = stat.size;
|
|
111
|
+
} catch { }
|
|
112
|
+
}
|
|
113
|
+
return objs;
|
|
114
|
+
}
|
|
115
|
+
export async function getLogBuffer(path: string): Promise<Buffer | undefined> {
|
|
116
|
+
if (!path.startsWith(folder)) throw new Error(`Path must start with ${folder}`);
|
|
117
|
+
let buffer: Buffer | undefined;
|
|
118
|
+
try {
|
|
119
|
+
buffer = await fs.promises.readFile(path);
|
|
120
|
+
} catch { }
|
|
121
|
+
return buffer;
|
|
122
|
+
}
|
|
123
|
+
export function parseLogBuffer(buffer: Buffer): LogObj[] {
|
|
124
|
+
let pos = 0;
|
|
125
|
+
let logs: LogObj[] = [];
|
|
126
|
+
const newLine = "\n".charCodeAt(0);
|
|
127
|
+
while (pos < buffer.length) {
|
|
128
|
+
let end = buffer.indexOf(newLine, pos);
|
|
129
|
+
if (end === -1) {
|
|
130
|
+
end = buffer.length;
|
|
131
|
+
}
|
|
132
|
+
let line = buffer.slice(pos, end).toString("utf8");
|
|
133
|
+
try {
|
|
134
|
+
logs.push(JSON.parse(line));
|
|
135
|
+
} catch { }
|
|
136
|
+
pos = end + 1;
|
|
137
|
+
}
|
|
138
|
+
return logs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function safeCopyObject<T>(obj: T): T {
|
|
142
|
+
const maxFields = 20;
|
|
143
|
+
let fieldCount = 0;
|
|
144
|
+
const seen = new WeakSet();
|
|
145
|
+
|
|
146
|
+
function copy(value: unknown): unknown {
|
|
147
|
+
// Handle primitives
|
|
148
|
+
if (!canHaveChildren(value)) {
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for circular references
|
|
153
|
+
if (seen.has(value as object)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
seen.add(value as object);
|
|
157
|
+
|
|
158
|
+
// Check if we've hit the field limit
|
|
159
|
+
if (fieldCount >= maxFields) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
const result: unknown[] = [];
|
|
165
|
+
for (const item of value) {
|
|
166
|
+
fieldCount++;
|
|
167
|
+
if (fieldCount >= maxFields) break;
|
|
168
|
+
result.push(copy(item));
|
|
169
|
+
}
|
|
170
|
+
seen.delete(value);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle plain objects
|
|
175
|
+
const result: Record<string, unknown> = {};
|
|
176
|
+
for (const key of Object.keys(value as object)) {
|
|
177
|
+
fieldCount++;
|
|
178
|
+
if (fieldCount >= maxFields) break;
|
|
179
|
+
result[key] = copy((value as Record<string, unknown>)[key]);
|
|
180
|
+
}
|
|
181
|
+
seen.delete(value);
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return copy(obj) as any;
|
|
186
|
+
}
|
|
187
|
+
function packageLogObj(args: unknown[]): LogObj {
|
|
188
|
+
let logObj: LogObj = {
|
|
189
|
+
time: Date.now(),
|
|
190
|
+
};
|
|
191
|
+
for (let part of globalContextParts) {
|
|
192
|
+
Object.assign(logObj, safeCopyObject(part()));
|
|
193
|
+
}
|
|
194
|
+
args = args.map(safeCopyObject);
|
|
195
|
+
for (let i = 0; i < args.length; i++) {
|
|
196
|
+
let param = args[i];
|
|
197
|
+
if (canHaveChildren(param)) {
|
|
198
|
+
Object.assign(logObj, param);
|
|
199
|
+
} else {
|
|
200
|
+
logObj["param" + i] = param;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return logObj;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
function decodeLogFileName(path: string): LogFile {
|
|
208
|
+
let name = path.split("/").pop()!;
|
|
209
|
+
let [start, end] = name.split(".log")[0].split("-").map(Number);
|
|
210
|
+
return {
|
|
211
|
+
startTime: start,
|
|
212
|
+
endTime: end,
|
|
213
|
+
name,
|
|
214
|
+
path,
|
|
215
|
+
size: 0,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// NOTE: Very little delay, so that during shutdown/crashes we don't lose too much before
|
|
220
|
+
// the crash. One of the biggest reasons for logs is to diagnose crashes, so this is important!
|
|
221
|
+
const logBase = batchFunction({ delay: 0 }, async function logBase(logObjList: LogObj[][]) {
|
|
222
|
+
let logs = logObjList.flat();
|
|
223
|
+
|
|
224
|
+
let byLogPath = new Map<string, LogObj[]>();
|
|
225
|
+
for (let log of logs) {
|
|
226
|
+
let logFile = getLogFileName(log);
|
|
227
|
+
let list = byLogPath.get(logFile.path);
|
|
228
|
+
if (!list) {
|
|
229
|
+
list = [];
|
|
230
|
+
byLogPath.set(logFile.path, list);
|
|
231
|
+
}
|
|
232
|
+
list.push(log);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (let [path, logList] of byLogPath) {
|
|
236
|
+
// Apparently, anything more than this and our writes might not be atomic
|
|
237
|
+
const WRITE_ATOMIC_LIMIT = 4096;
|
|
238
|
+
let lines = logList.map(v => Buffer.from(JSON.stringify(v) + "\n"));
|
|
239
|
+
|
|
240
|
+
// Group lines into WRITE_ATOMIC_LIMIT byte chunks
|
|
241
|
+
let chunks: Buffer[][] = [];
|
|
242
|
+
let currentChunk: Buffer[] = [];
|
|
243
|
+
let currentSize = 0;
|
|
244
|
+
for (let line of lines) {
|
|
245
|
+
if (currentSize + line.length > WRITE_ATOMIC_LIMIT && currentChunk.length > 0) {
|
|
246
|
+
chunks.push(currentChunk);
|
|
247
|
+
currentChunk = [];
|
|
248
|
+
currentSize = 0;
|
|
249
|
+
}
|
|
250
|
+
currentChunk.push(line);
|
|
251
|
+
currentSize += line.length;
|
|
252
|
+
}
|
|
253
|
+
if (currentChunk.length > 0) {
|
|
254
|
+
chunks.push(currentChunk);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (let chunk of chunks) {
|
|
258
|
+
await fs.promises.appendFile(path, Buffer.concat(chunk));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
function getLogFileName(logObj: LogObj): LogFile {
|
|
264
|
+
let start = Math.floor(logObj.time / LOG_FILE_DURATION) * LOG_FILE_DURATION;
|
|
265
|
+
let startTime = start;
|
|
266
|
+
let endTime = start + LOG_FILE_DURATION;
|
|
267
|
+
let name = startTime + "-" + endTime + ".log";
|
|
268
|
+
let path = folder + name;
|
|
269
|
+
let logFile: LogFile = { startTime, endTime, name, path, size: 0, };
|
|
270
|
+
return logFile;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (isNode()) {
|
|
274
|
+
runInfinitePoll(timeInHour, async function logClearOldLogs() {
|
|
275
|
+
// Maintain our size restrictions
|
|
276
|
+
let logFiles = await fs.promises.readdir(folder);
|
|
277
|
+
let objs: { time: number; bytes: number; path: string; }[] = [];
|
|
278
|
+
for (let file of logFiles) {
|
|
279
|
+
let path = folder + file;
|
|
280
|
+
let stat = await fs.promises.stat(path);
|
|
281
|
+
objs.push({ time: stat.mtimeMs, bytes: stat.size, path });
|
|
282
|
+
}
|
|
283
|
+
let { remove } = await SIZE_LIMIT.limit(objs);
|
|
284
|
+
for (let file of remove) {
|
|
285
|
+
await fs.promises.unlink(file.path);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
Append Benchmarks
|
|
292
|
+
10 processes
|
|
293
|
+
Windows = 100K/S
|
|
294
|
+
Linux Digital Ocean = 1M/S
|
|
295
|
+
Linux PI SD Card = 300K/S
|
|
296
|
+
Linux PI USB = 300K/S
|
|
297
|
+
1 process
|
|
298
|
+
Windows = 40K/S
|
|
299
|
+
Linux Digital Ocean = 685K/S
|
|
300
|
+
Linux PI = 200K/S
|
|
301
|
+
|
|
302
|
+
rm test.txt
|
|
303
|
+
for i in {0..9}; do node -e 'const fs=require("fs");const id='$i';let i=0;const start=Date.now();while(Date.now()-start<5000){fs.appendFileSync("test.txt", `${id},${i++}\n`)}' & done; wait
|
|
304
|
+
node -e 'const fs=require("fs");const seqs=new Map();fs.readFileSync("test.txt","utf8").trim().split("\n").forEach((l,i)=>{const[id,seq]=l.split(",").map(Number);if(!seqs.has(id))seqs.set(id,{last:-1,errs:0});const s=seqs.get(id);if(seq!==s.last+1){console.error(`Error for id ${id} at line ${i}: ${s.last}->${seq}`);s.errs++}s.last=seq});seqs.forEach((v,id)=>console.log(`ID ${id}: final seq ${v.last}, ${v.errs} gaps`))'
|
|
305
|
+
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { canHaveChildren } from "socket-function/src/types";
|
|
2
|
+
import { logDisk } from "./diskLogger";
|
|
3
|
+
|
|
4
|
+
let shimmed = false;
|
|
5
|
+
export function shimConsoleLogs() {
|
|
6
|
+
if (shimmed) return;
|
|
7
|
+
shimmed = true;
|
|
8
|
+
function hookConsoleFnc(fncName: "log" | "warn" | "error" | "info") {
|
|
9
|
+
let console = globalThis.console;
|
|
10
|
+
let originalFnc = console[fncName];
|
|
11
|
+
console[fncName] = (...args: any[]) => {
|
|
12
|
+
if (
|
|
13
|
+
args.length > 0
|
|
14
|
+
&& String(args[0]).trim().length > 0
|
|
15
|
+
) {
|
|
16
|
+
if (typeof logDisk === "function") {
|
|
17
|
+
// Don't call it directly, so we don't get extra line debug context added to this call
|
|
18
|
+
// (as it wouldn't be useful, as we really want the caller)
|
|
19
|
+
let stopDoubleShim = logDisk;
|
|
20
|
+
stopDoubleShim(...args, { type: fncName });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Filter out objects added by injectFileLocationToConsole
|
|
24
|
+
args = args.filter(x => !(canHaveChildren(x) && x["__FILE__"]));
|
|
25
|
+
return originalFnc(...args);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
hookConsoleFnc("log");
|
|
29
|
+
hookConsoleFnc("info");
|
|
30
|
+
hookConsoleFnc("warn");
|
|
31
|
+
hookConsoleFnc("error");
|
|
32
|
+
}
|