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.
Files changed (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. 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
+ }