querysub 0.327.0 → 0.328.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 (41) hide show
  1. package/package.json +3 -4
  2. package/src/-a-archives/archivesBackBlaze.ts +20 -0
  3. package/src/-a-archives/archivesDisk.ts +5 -5
  4. package/src/-a-archives/archivesLimitedCache.ts +118 -7
  5. package/src/-a-archives/archivesPrivateFileSystem.ts +3 -0
  6. package/src/-g-core-values/NodeCapabilities.ts +26 -11
  7. package/src/0-path-value-core/auditLogs.ts +4 -2
  8. package/src/2-proxy/PathValueProxyWatcher.ts +3 -0
  9. package/src/3-path-functions/PathFunctionRunner.ts +2 -2
  10. package/src/4-querysub/Querysub.ts +1 -1
  11. package/src/5-diagnostics/GenericFormat.tsx +2 -2
  12. package/src/deployManager/machineApplyMainCode.ts +10 -8
  13. package/src/deployManager/machineSchema.ts +4 -3
  14. package/src/deployManager/setupMachineMain.ts +3 -2
  15. package/src/diagnostics/logs/FastArchiveAppendable.ts +75 -51
  16. package/src/diagnostics/logs/FastArchiveController.ts +5 -2
  17. package/src/diagnostics/logs/FastArchiveViewer.tsx +205 -48
  18. package/src/diagnostics/logs/LogViewer2.tsx +78 -34
  19. package/src/diagnostics/logs/TimeRangeSelector.tsx +8 -0
  20. package/src/diagnostics/logs/diskLogGlobalContext.ts +3 -3
  21. package/src/diagnostics/logs/diskLogger.ts +70 -23
  22. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +82 -63
  23. package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +37 -3
  24. package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +45 -16
  25. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +8 -0
  26. package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +198 -56
  27. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +3 -2
  28. package/src/diagnostics/managementPages.tsx +5 -0
  29. package/src/email_ims_notifications/discord.tsx +203 -0
  30. package/src/fs.ts +9 -0
  31. package/src/functional/SocketChannel.ts +9 -0
  32. package/src/functional/throttleRender.ts +134 -0
  33. package/src/library-components/ATag.tsx +2 -2
  34. package/src/misc.ts +13 -0
  35. package/src/misc2.ts +54 -0
  36. package/src/user-implementation/SecurityPage.tsx +11 -5
  37. package/src/user-implementation/userData.ts +31 -16
  38. package/testEntry2.ts +14 -5
  39. package/src/user-implementation/setEmailKey.ts +0 -25
  40. /package/src/{email → email_ims_notifications}/postmark.tsx +0 -0
  41. /package/src/{email → email_ims_notifications}/sendgrid.tsx +0 -0
@@ -0,0 +1,134 @@
1
+
2
+
3
+ // NOTE: Many cases we don't know if we want to throttle, or how long we want to throttle for, until we start the watcher, so this has to be done inside our render, instead of in ProxyWatcher.
4
+
5
+ import { cache } from "socket-function/src/caching";
6
+ import { proxyWatcher, SyncWatcher } from "../2-proxy/PathValueProxyWatcher";
7
+ import { qreact } from "../4-dom/qreact";
8
+ import { onNextPaint } from "./onNextPaint";
9
+
10
+ // Throttles calls that have the same throttleKey
11
+ /** Used near the start of your render, like so:
12
+ if (throttleRender({ key: "NodeControls", frameDelay: 0, frameDelaySmear: 60 })) return undefined;
13
+ - Doesn't break watches, so debug tools will still show you are watching them
14
+ - Doesn't break the rendered output (uses the last fully rendered output)
15
+ - Pauses rendering until the throttle finishes, then forcefully renders once.
16
+ - Always delays by at least 1 frame.
17
+ */
18
+ export function throttleRender(config: {
19
+ // Throttles are smeared perkey
20
+ key: string;
21
+ // We pick a large value in this range as throttleKey overlaps, allowing you to smear a large number of changes over a larger period of time, BUT, while also waiting less time if there are few changes!
22
+ // - Throttles won't necessarily finish in the same order you call them (we have to wrap around in the range after we hit the end, so later calls might re-trigger earlier
23
+ frameDelay: number;
24
+ // Range is [frameDelay, frameDelay + smear]
25
+ frameDelaySmear?: number;
26
+ // Delays for additional frames. Ex, set frameDelay to 0, smear to 60, and count to 30. The first delay is 0, then 30, then 60
27
+ frameDelayCount?: number;
28
+ }): boolean {
29
+ let watcher = proxyWatcher.getTriggeredWatcher();
30
+ // Never throttle the first render, as that would be noticeable and always unintended
31
+ if (watcher.syncRunCount === 0) return false;
32
+ let { schedule, inSchedule, isTriggered, runScheduler } = throttleManager(config.key);
33
+ if (isTriggered(watcher)) {
34
+ //console.log(`Triggering throttle render for ${watcher.debugName} on frame ${getFrameNumber()}`);
35
+ return false;
36
+ }
37
+ runScheduler();
38
+ if (inSchedule.has(watcher)) {
39
+ //console.log(`Skipping throttle render for ${watcher.debugName} because it is already in the schedule`);
40
+ proxyWatcher.reuseLastWatches();
41
+ qreact.cancelRender();
42
+ return true;
43
+ }
44
+ inSchedule.add(watcher);
45
+ //console.log(`Adding throttle render for ${watcher.debugName}`);
46
+
47
+
48
+ let count = config.frameDelayCount || 1;
49
+ let smear = config.frameDelaySmear ?? 0;
50
+ let curIndex = config.frameDelay;
51
+ let endIndex = config.frameDelay + smear;
52
+ let targetFillCount = 0;
53
+ // Annoying algorithm to find the lowest slow available...
54
+ while (true) {
55
+ let cur = schedule[curIndex];
56
+ if ((cur?.length || 0) === targetFillCount) {
57
+ schedule[curIndex] = schedule[curIndex] || [];
58
+ schedule[curIndex]!.push(watcher);
59
+ for (let i = 1; i < count; i++) {
60
+ schedule[curIndex + i] = schedule[curIndex + i] || [];
61
+ schedule[curIndex + i]!.push("delay");
62
+ }
63
+ break;
64
+ }
65
+ curIndex++;
66
+ if (curIndex > endIndex) {
67
+ curIndex = config.frameDelay;
68
+ targetFillCount++;
69
+ }
70
+ }
71
+
72
+ proxyWatcher.reuseLastWatches();
73
+ qreact.cancelRender();
74
+ return true;
75
+ }
76
+
77
+ let throttleManager = cache((key: string) => {
78
+ let schedule: ((SyncWatcher | "delay")[] | undefined)[] = [];
79
+ let inSchedule = new Set<SyncWatcher>();
80
+ let triggered = new Set<SyncWatcher>();
81
+ let runningScheduler = false;
82
+ function isTriggered(watcher: SyncWatcher) {
83
+ return triggered.has(watcher);
84
+ }
85
+
86
+ return {
87
+ schedule,
88
+ inSchedule,
89
+ isTriggered,
90
+ runScheduler: () => {
91
+ if (runningScheduler) return;
92
+ runningScheduler = true;
93
+ void onNextPaint().finally(runNextTick);
94
+ function runNextTick() {
95
+ let next = schedule.shift();
96
+ void onNextPaint().finally(() => {
97
+ triggered.clear();
98
+ });
99
+ if (next) {
100
+ for (let watcher of next) {
101
+ if (watcher === "delay") continue;
102
+ inSchedule.delete(watcher);
103
+ triggered.add(watcher);
104
+ //console.log(`Triggering throttle render for ${watcher.debugName}`);
105
+ watcher.explicitlyTrigger({
106
+ paths: new Set(),
107
+ pathSources: new Set(),
108
+ newParentsSynced: new Set(),
109
+ extraReasons: ["throttleRender trigger"],
110
+ });
111
+ }
112
+ }
113
+ if (schedule.length > 0) {
114
+ void onNextPaint().finally(runNextTick);
115
+ } else {
116
+ runningScheduler = false;
117
+ }
118
+ }
119
+ },
120
+ };
121
+ });
122
+
123
+
124
+
125
+ let rendersInFrame = 0;
126
+ export function countRendersInFrame() {
127
+ if (rendersInFrame === 0) {
128
+ void onNextPaint().finally(() => {
129
+ //console.log(`Render in frame ${rendersInFrame}`);
130
+ rendersInFrame = 0;
131
+ });
132
+ }
133
+ rendersInFrame++;
134
+ }
@@ -3,6 +3,7 @@ import { css, isNode } from "typesafecss";
3
3
  import { URLParam, parseSearchString, encodeSearchString } from "./URLParam";
4
4
  import { qreact } from "../4-dom/qreact";
5
5
  import { niceStringify } from "../niceStringify";
6
+ import { getDomain } from "../config";
6
7
 
7
8
  export type URLOverride<T = unknown> = {
8
9
  param: URLParam<T>;
@@ -118,8 +119,7 @@ export const Anchor = ATag;
118
119
  export const Link = ATag;
119
120
 
120
121
  export function createLink(values: URLOverride[]) {
121
- if (isNode()) throw new Error(`Create link is not implemented yet on node.`);
122
- let urlObj = new URL(document.location.href);
122
+ let urlObj = new URL(isNode() ? "https://" + getDomain() : document.location.href);
123
123
  let params = parseSearchString(urlObj.search);
124
124
  for (let value of values) {
125
125
  params[value.param.urlKey] = value.value;
package/src/misc.ts CHANGED
@@ -126,6 +126,19 @@ export function partialCopyObject(data: unknown, maxFields: number = 500): unkno
126
126
 
127
127
  // Handle plain objects
128
128
  const result: Record<string, unknown> = {};
129
+
130
+ // Special case for Error objects - include stack and message
131
+ if (value instanceof Error) {
132
+ fieldCount++;
133
+ if (fieldCount < maxFields) {
134
+ result.message = value.message;
135
+ }
136
+ fieldCount++;
137
+ if (fieldCount < maxFields) {
138
+ result.stack = value.stack;
139
+ }
140
+ }
141
+
129
142
  for (const key of Object.keys(value as object)) {
130
143
  fieldCount++;
131
144
  if (fieldCount >= maxFields) break;
package/src/misc2.ts CHANGED
@@ -2,4 +2,58 @@ import { atomic } from "./2-proxy/PathValueProxyWatcher";
2
2
 
3
3
  export function isStrSimilar(a: string | undefined, b: string | undefined) {
4
4
  return atomic(a)?.toLowerCase().trim().replaceAll(" ", "") === atomic(b)?.toLowerCase().trim().replaceAll(" ", "");
5
+ }
6
+
7
+ export interface StreamLike {
8
+ on(event: "data", listener: (chunk: Buffer) => void): void;
9
+ on(event: "end", listener: () => void): void;
10
+ on(event: "error", listener: (error: Error) => void): void;
11
+ }
12
+ export async function* streamToAsyncIterable(stream: StreamLike): AsyncIterable<Buffer> {
13
+ let pendingChunks: Buffer[] = [];
14
+ let streamEnded = false;
15
+ let streamError: Error | undefined = undefined;
16
+ let resolveNext: (() => void) | undefined = undefined;
17
+
18
+ stream.on("data", (chunk: Buffer) => {
19
+ pendingChunks.push(chunk);
20
+ if (resolveNext) {
21
+ resolveNext();
22
+ resolveNext = undefined;
23
+ }
24
+ });
25
+
26
+ stream.on("end", () => {
27
+ streamEnded = true;
28
+ if (resolveNext) {
29
+ resolveNext();
30
+ resolveNext = undefined;
31
+ }
32
+ });
33
+
34
+ stream.on("error", (error) => {
35
+ streamError = error;
36
+ if (resolveNext) {
37
+ resolveNext();
38
+ resolveNext = undefined;
39
+ }
40
+ });
41
+
42
+ while (!streamEnded && !streamError) {
43
+ if (pendingChunks.length > 0) {
44
+ yield pendingChunks.shift()!;
45
+ } else {
46
+ await new Promise<void>((resolve) => {
47
+ resolveNext = resolve;
48
+ });
49
+ }
50
+ }
51
+
52
+ if (streamError) {
53
+ throw streamError;
54
+ }
55
+
56
+ while (pendingChunks.length > 0) {
57
+ yield pendingChunks.shift()!;
58
+ }
5
59
  }
@@ -13,7 +13,7 @@ import { redButton } from "../library-components/colors";
13
13
 
14
14
  export class SecurityPage extends qreact.Component {
15
15
  render() {
16
- return <div class={css.pad(10)}>
16
+ return <div class={css.pad(10).fillWidth}>
17
17
  <TabbedUI
18
18
  tabs={[
19
19
  { value: "config", title: "Config", contents: <ConfigTab /> },
@@ -49,13 +49,19 @@ class ConfigTab extends qreact.Component {
49
49
  <InputLabel
50
50
  label={"Postmark API Key"}
51
51
  edit
52
- editClass={css.ellipsis.maxWidth("100px")}
52
+ fillWidth
53
53
  value={user_data().secure.postmarkAPIKey}
54
54
  onChangeValue={value => user_functions.setPostmarkAPIKey({ apiKey: value })}
55
55
  />
56
- {isCurrentUserSuperUser() && <Button onClick={() => user_functions.test()}>
57
- test
58
- </Button>}
56
+ <InputLabel
57
+ label={"Discord Webhook URL"}
58
+ edit
59
+ value={user_data().secure.notifyDiscordWebhookURL}
60
+ onChangeValue={value => user_functions.setNotifyDiscordWebhookURL({ webhookURL: value })}
61
+ />
62
+ <Button onClick={() => user_functions.testSendDiscordMessage()}>
63
+ Test Discord Message
64
+ </Button>
59
65
  </>
60
66
  );
61
67
  }
@@ -6,7 +6,7 @@ import { registerAliveChecker } from "../2-proxy/garbageCollection";
6
6
  import { generateLoginEmail } from "./loginEmail";
7
7
  import { logErrors } from "../errors";
8
8
  import { PermissionsParameters } from "../3-path-functions/syncSchema";
9
- import { sendEmail_postmark } from "../email/postmark";
9
+ import { sendEmail_postmark } from "../email_ims_notifications/postmark";
10
10
  import { isClient } from "../config2";
11
11
  import { getExternalIP } from "../misc/networking";
12
12
  import { MAX_ACCEPTED_CHANGE_AGE } from "../0-path-value-core/pathValueCore";
@@ -17,6 +17,10 @@ import { enableErrorNotifications } from "../library-components/errorNotificatio
17
17
  import { clamp } from "../misc";
18
18
  import { sha256 } from "js-sha256";
19
19
  import { logDisk } from "../diagnostics/logs/diskLogger";
20
+ import { createLink } from "../library-components/ATag";
21
+ import { showingManagementURL, managementPageURL } from "../diagnostics/managementPages";
22
+ import { sendDiscordMessage } from "../email_ims_notifications/discord";
23
+ import { formatDateTime } from "socket-function/src/formatting/format";
20
24
 
21
25
  /*
22
26
  IMPORTANT!
@@ -161,6 +165,7 @@ const { data, functions } = Querysub.syncSchema<{
161
165
  secure: {
162
166
  signupsOpen?: boolean;
163
167
  postmarkAPIKey?: string;
168
+ notifyDiscordWebhookURL?: string;
164
169
 
165
170
  // NOTE: For now these will only be blocked at a user level. If we run into issues, we can
166
171
  // make Querysub have configurable IP blocking that accepts a synced read function, which
@@ -217,18 +222,25 @@ const { data, functions } = Querysub.syncSchema<{
217
222
  specialBlockIP, specialUnblockIP,
218
223
  specialSetInviteCount,
219
224
  setPostmarkAPIKey,
225
+ setNotifyDiscordWebhookURL,
220
226
  specialSetUserType,
221
227
  registerPageLoadTime,
222
228
  banUser, unbanUser,
223
- test: () => {
229
+ testSendDiscordMessage: () => {
224
230
  assertUserType("superuser");
225
- let ip = Querysub.getCallerIP();
226
- let machineId = Querysub.getCallerMachineId();
227
- data().users["local"].allowedIPs[ip] = atomicObjectWrite({ time: Date.now(), machineId });
228
- data().users["local"].machineIds[machineId] = atomicObjectWrite({ time: Date.now(), ip });
229
- data().machineSecure[machineId].userId = "local";
230
- data().users["local"].userId = "local";
231
- }
231
+ const webhookURL = atomic(data().secure.notifyDiscordWebhookURL);
232
+ if (!webhookURL) {
233
+ throw new Error("No Discord webhook URL set");
234
+ }
235
+ let callerUserId = getCurrentUserAssert();
236
+ let email = data().users[callerUserId].email;
237
+ Querysub.onCommitFinished(async () => {
238
+ await sendDiscordMessage({
239
+ webhookURL,
240
+ message: `Discord webhook test at ${formatDateTime(Date.now())} by ${email} (${JSON.stringify(callerUserId)})`,
241
+ });
242
+ });
243
+ },
232
244
  },
233
245
  module,
234
246
  moduleId: "userData2",
@@ -269,11 +281,13 @@ const { data, functions } = Querysub.syncSchema<{
269
281
  },
270
282
  },
271
283
  functionMetadata: {
284
+ // There's no point to predict sending emails, or inviting users, or registering the page load time or sending discord messages, As the main function of these can only be done server-side.
285
+ // verifyMachineId is used when logging in with the login token. We could predict it, but there's no real reason to because if we do, all of our reads are gonna fail until it runs, Which will actually be really confusing to the user and look like a bug.
272
286
  sendLoginEmail: { nopredict: true },
273
287
  verifyMachineId: { nopredict: true },
274
288
  inviteUser: { nopredict: true },
275
- setPostmarkAPIKey: { nopredict: true },
276
289
  registerPageLoadTime: { nopredict: true },
290
+ testSendDiscordMessage: { nopredict: true },
277
291
  },
278
292
  });
279
293
 
@@ -595,7 +609,11 @@ function sendLoginEmail(config: {
595
609
 
596
610
  const apiKey = atomic(data().secure.postmarkAPIKey);
597
611
  if (!apiKey) {
598
- throw new Error(`No postmark API key, so we can't send login emails. Run "yarn setemailkey <key>" first.`);
612
+ let link = createLink([
613
+ showingManagementURL.getOverride(true),
614
+ managementPageURL.getOverride("SecurityPage"),
615
+ ]);
616
+ throw new Error(`No postmark API key, so we can't send login emails. Set the key at ${link}.`);
599
617
  }
600
618
 
601
619
  {
@@ -938,11 +956,8 @@ export function scriptCreateUser(config: {
938
956
  function setPostmarkAPIKey(config: { apiKey: string; }) {
939
957
  data().secure.postmarkAPIKey = config.apiKey;
940
958
  }
941
-
942
- export function scriptSetPostmarkAPIKey(config: {
943
- apiKey: string;
944
- }) {
945
- setPostmarkAPIKey(config);
959
+ function setNotifyDiscordWebhookURL(config: { webhookURL: string; }) {
960
+ data().secure.notifyDiscordWebhookURL = config.webhookURL;
946
961
  }
947
962
 
948
963
 
package/testEntry2.ts CHANGED
@@ -5,21 +5,34 @@ import { shutdown } from "./src/diagnostics/periodic";
5
5
  import { testTCPIsListening } from "socket-function/src/networking";
6
6
  import { Querysub } from "./src/4-querysub/QuerysubController";
7
7
  import { timeInSecond } from "socket-function/src/misc";
8
+ import { LOG_LINE_LIMIT_ID } from "./src/diagnostics/logs/diskLogger";
9
+ import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
8
10
 
9
11
  export async function testMain() {
10
12
  Querysub;
13
+ await waitForFirstTimeSync();
14
+
11
15
  //let test = await testTCPIsListening("1.1.1.1", 443);
12
16
  //console.log(test);
13
17
  // Writing heartbeat 2025/09/14 08:37:46 PM for self (5ac8a2fa78fce4ea.971ed8b01743d123.querysubtest.com:13900)
14
18
  await delay(timeInSecond);
15
19
  await Querysub.hostService("test");
16
20
  await delay(timeInSecond * 5);
21
+
22
+ console.error(`This should show up locally, but not remotely.`);
23
+ await delay(timeInSecond * 15);
24
+ await shutdown();
25
+ return;
26
+
27
+
17
28
  // console.log(getOwnThreadId());
18
29
  // Log an error every 30 seconds forever.
19
30
  while (true) {
20
31
  console.error(`Test warning for im testing ${Date.now()}`);
21
32
  await delay(timeInSecond * 30);
22
33
  }
34
+
35
+
23
36
  // console.log(getOwnThreadId());
24
37
  // await shutdown();
25
38
  //await Querysub.hostService("test");
@@ -36,8 +49,4 @@ export async function testMain() {
36
49
  // }
37
50
  await delay(timeInSecond * 15);
38
51
  await shutdown();
39
- }
40
- async function main() {
41
-
42
- }
43
-
52
+ }
@@ -1,25 +0,0 @@
1
- import { Querysub } from "../4-querysub/Querysub";
2
- import { pathValueCommitter } from "../0-path-value-core/PathValueCommitter";
3
- import { scriptSetPostmarkAPIKey } from "./userData";
4
- import { isNodeTrue } from "socket-function/src/misc";
5
- import yargs from "yargs";
6
-
7
- let yargObj = isNodeTrue() && yargs(process.argv)
8
- .option("key", { type: "string", desc: `The key to set for the email.` })
9
- .argv || {}
10
- ;
11
-
12
- async function main() {
13
- const howToCall = ` Call with yarn setemailkey --key <key>`;
14
- const key = yargObj.key;
15
- if (!key) throw new Error("No key provided." + howToCall);
16
-
17
- await Querysub.hostService("setEmailKey");
18
-
19
- await Querysub.commitSynced(() => {
20
- scriptSetPostmarkAPIKey({ apiKey: key });
21
- });
22
-
23
- await pathValueCommitter.waitForValuesToCommit();
24
- }
25
- main().catch(e => console.log(e)).finally(() => process.exit());