querysub 0.327.0 → 0.329.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 (50) hide show
  1. package/bin/error-email.js +8 -0
  2. package/bin/error-im.js +8 -0
  3. package/package.json +4 -3
  4. package/src/-a-archives/archivesBackBlaze.ts +20 -0
  5. package/src/-a-archives/archivesCborT.ts +52 -0
  6. package/src/-a-archives/archivesDisk.ts +5 -5
  7. package/src/-a-archives/archivesJSONT.ts +19 -5
  8. package/src/-a-archives/archivesLimitedCache.ts +118 -7
  9. package/src/-a-archives/archivesPrivateFileSystem.ts +3 -0
  10. package/src/-g-core-values/NodeCapabilities.ts +26 -11
  11. package/src/0-path-value-core/auditLogs.ts +4 -2
  12. package/src/2-proxy/PathValueProxyWatcher.ts +7 -0
  13. package/src/3-path-functions/PathFunctionRunner.ts +2 -2
  14. package/src/4-querysub/Querysub.ts +1 -1
  15. package/src/5-diagnostics/GenericFormat.tsx +2 -2
  16. package/src/config.ts +15 -3
  17. package/src/deployManager/machineApplyMainCode.ts +10 -8
  18. package/src/deployManager/machineSchema.ts +4 -3
  19. package/src/deployManager/setupMachineMain.ts +3 -2
  20. package/src/diagnostics/logs/FastArchiveAppendable.ts +86 -53
  21. package/src/diagnostics/logs/FastArchiveController.ts +11 -2
  22. package/src/diagnostics/logs/FastArchiveViewer.tsx +205 -48
  23. package/src/diagnostics/logs/LogViewer2.tsx +78 -34
  24. package/src/diagnostics/logs/TimeRangeSelector.tsx +8 -0
  25. package/src/diagnostics/logs/diskLogGlobalContext.ts +5 -4
  26. package/src/diagnostics/logs/diskLogger.ts +70 -23
  27. package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +409 -0
  28. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +94 -67
  29. package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +37 -3
  30. package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +50 -16
  31. package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +174 -0
  32. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +291 -0
  33. package/src/diagnostics/logs/errorNotifications/errorLoopEntry.tsx +7 -0
  34. package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +185 -68
  35. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +10 -19
  36. package/src/diagnostics/managementPages.tsx +33 -15
  37. package/src/email_ims_notifications/discord.tsx +203 -0
  38. package/src/{email → email_ims_notifications}/postmark.tsx +3 -3
  39. package/src/fs.ts +9 -0
  40. package/src/functional/SocketChannel.ts +9 -0
  41. package/src/functional/throttleRender.ts +134 -0
  42. package/src/library-components/ATag.tsx +2 -2
  43. package/src/library-components/SyncedController.ts +3 -3
  44. package/src/misc.ts +18 -0
  45. package/src/misc2.ts +106 -0
  46. package/src/user-implementation/SecurityPage.tsx +11 -5
  47. package/src/user-implementation/userData.ts +57 -23
  48. package/testEntry2.ts +14 -5
  49. package/src/user-implementation/setEmailKey.ts +0 -25
  50. /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;
@@ -109,7 +109,7 @@ export function getSyncedController<T extends SocketRegistered>(
109
109
  resetAll(): void;
110
110
  refreshAll(): void;
111
111
  isAnyLoading(): boolean;
112
- base: T;
112
+ __baseController: T;
113
113
  } {
114
114
  if (isNode()) {
115
115
  let result = cache((nodeId: string) => {
@@ -161,7 +161,7 @@ export function getSyncedController<T extends SocketRegistered>(
161
161
  result.isAnyLoading = () => {
162
162
  notAllowedOnServer();
163
163
  };
164
- result.base = controller;
164
+ result.__baseController = controller;
165
165
  return result;
166
166
  }
167
167
  let id = nextId();
@@ -416,6 +416,6 @@ export function getSyncedController<T extends SocketRegistered>(
416
416
  }
417
417
  });
418
418
  };
419
- result.base = controller;
419
+ result.__baseController = controller;
420
420
  return result;
421
421
  }
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;
@@ -172,4 +185,9 @@ export function streamToIteratable<T>(reader: {
172
185
  }
173
186
  }
174
187
  };
188
+ }
189
+
190
+ const AsyncFunction = (async () => { }).constructor;
191
+ export function isAsyncFunction(func: unknown): boolean {
192
+ return func instanceof AsyncFunction;
175
193
  }
package/src/misc2.ts CHANGED
@@ -1,5 +1,111 @@
1
+ import { delay } from "socket-function/src/batching";
2
+ import { formatTime } from "socket-function/src/formatting/format";
3
+ import { timeInHour } from "socket-function/src/misc";
1
4
  import { atomic } from "./2-proxy/PathValueProxyWatcher";
2
5
 
3
6
  export function isStrSimilar(a: string | undefined, b: string | undefined) {
4
7
  return atomic(a)?.toLowerCase().trim().replaceAll(" ", "") === atomic(b)?.toLowerCase().trim().replaceAll(" ", "");
8
+ }
9
+
10
+ export interface StreamLike {
11
+ on(event: "data", listener: (chunk: Buffer) => void): void;
12
+ on(event: "end", listener: () => void): void;
13
+ on(event: "error", listener: (error: Error) => void): void;
14
+ }
15
+ export async function* streamToAsyncIterable(stream: StreamLike): AsyncIterable<Buffer> {
16
+ let pendingChunks: Buffer[] = [];
17
+ let streamEnded = false;
18
+ let streamError: Error | undefined = undefined;
19
+ let resolveNext: (() => void) | undefined = undefined;
20
+
21
+ stream.on("data", (chunk: Buffer) => {
22
+ pendingChunks.push(chunk);
23
+ if (resolveNext) {
24
+ resolveNext();
25
+ resolveNext = undefined;
26
+ }
27
+ });
28
+
29
+ stream.on("end", () => {
30
+ streamEnded = true;
31
+ if (resolveNext) {
32
+ resolveNext();
33
+ resolveNext = undefined;
34
+ }
35
+ });
36
+
37
+ stream.on("error", (error) => {
38
+ streamError = error;
39
+ if (resolveNext) {
40
+ resolveNext();
41
+ resolveNext = undefined;
42
+ }
43
+ });
44
+
45
+ while (!streamEnded && !streamError) {
46
+ if (pendingChunks.length > 0) {
47
+ yield pendingChunks.shift()!;
48
+ } else {
49
+ await new Promise<void>((resolve) => {
50
+ resolveNext = resolve;
51
+ });
52
+ }
53
+ }
54
+
55
+ if (streamError) {
56
+ throw streamError;
57
+ }
58
+
59
+ while (pendingChunks.length > 0) {
60
+ yield pendingChunks.shift()!;
61
+ }
62
+ }
63
+
64
+
65
+ export async function runScheduler(hours: number[], func: () => Promise<void>) {
66
+ while (true) {
67
+ try {
68
+ const now = new Date();
69
+
70
+ // Find the next check time from our list
71
+ const sortedHours = [...hours].sort((a, b) => a - b);
72
+ let targetTime = new Date(now);
73
+
74
+ // Find the next check hour after the current time
75
+ let nextHour = sortedHours.find(hour => {
76
+ const checkTime = new Date(now);
77
+ checkTime.setHours(hour, 0, 0, 0);
78
+ return checkTime > now;
79
+ });
80
+
81
+ if (nextHour === undefined) {
82
+ // No more checks today, take the first one tomorrow
83
+ nextHour = sortedHours[0];
84
+ targetTime.setDate(targetTime.getDate() + 1);
85
+ }
86
+
87
+ targetTime.setHours(nextHour, 0, 0, 0);
88
+
89
+ console.log(`Next target time: ${targetTime.toLocaleString()}`);
90
+
91
+ // Wait and check every 15 minutes until it's time
92
+ while (true) {
93
+ const currentTime = new Date();
94
+ const timeUntilCheck = targetTime.getTime() - currentTime.getTime();
95
+
96
+ if (timeUntilCheck <= 0) {
97
+ console.log("Time to run the check!");
98
+ await func();
99
+ break; // Break out of inner loop to calculate next check time
100
+ }
101
+
102
+ console.log(`Time until next check: ${formatTime(timeUntilCheck)}`);
103
+ await delay(timeInHour / 4); // Wait 15 minutes (1/4 of an hour)
104
+ }
105
+ } catch (error) {
106
+ console.error(error);
107
+ // Even if there's an error, wait 15 minutes before trying again
108
+ await delay(timeInHour / 4);
109
+ }
110
+ }
5
111
  }
@@ -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,17 +6,21 @@ 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";
13
13
  import { createURLSync } from "../library-components/URLParam";
14
- import { devDebugbreak, getDomain, isDevDebugbreak, isRecovery } from "../config";
14
+ import { devDebugbreak, getDomain, getEmailDomain, isDevDebugbreak, isRecovery } from "../config";
15
15
  import { delay } from "socket-function/src/batching";
16
16
  import { enableErrorNotifications } from "../library-components/errorNotifications";
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
 
@@ -593,9 +607,14 @@ function sendLoginEmail(config: {
593
607
  const machineId = Querysub.getCallerMachineId();
594
608
  const now = Querysub.getCallTime();
595
609
 
610
+ // Check for the API key, even though we don't immediately use it, so that we can get good errors to tell the user (likely the developer) to setup the postmark API key.
596
611
  const apiKey = atomic(data().secure.postmarkAPIKey);
597
612
  if (!apiKey) {
598
- throw new Error(`No postmark API key, so we can't send login emails. Run "yarn setemailkey <key>" first.`);
613
+ let link = createLink([
614
+ showingManagementURL.getOverride(true),
615
+ managementPageURL.getOverride("SecurityPage"),
616
+ ]);
617
+ throw new Error(`No postmark API key, so we can't send login emails. Set the key at ${link}.`);
599
618
  }
600
619
 
601
620
  {
@@ -670,18 +689,36 @@ function sendLoginEmail(config: {
670
689
  timeoutTime,
671
690
  });
672
691
  Querysub.onCommitFinished(async () => {
673
- await sendEmail_postmark({
674
- apiKey,
675
- to: email,
676
- // TODO: Allow configuring this (defaulting to getDomain() if unconfigured). For now we hardcode it, because it takes
677
- // a while to verify a new postmark email, and we don't even know what final domain we will be using.
678
- from: "login@querysub.com",
692
+ await sendEmail({
693
+ to: [email],
694
+ fromPrefix: "login",
679
695
  subject,
680
696
  contents,
681
697
  });
682
698
  });
683
699
  });
684
700
  }
701
+ export async function sendEmail(config: {
702
+ to: string[];
703
+ // The domain should be getDomain
704
+ // TODO: Actually use git domain, for now it's hardcoded, because setting up a new
705
+ fromPrefix: string;
706
+ subject: string;
707
+ contents: preact.VNode;
708
+ }) {
709
+ let key = await Querysub.commitAsync(() => atomic(data().secure.postmarkAPIKey));
710
+ if (!key) {
711
+ console.warn(`No postmark API key setup, so we can't send email`);
712
+ return;
713
+ }
714
+ await sendEmail_postmark({
715
+ apiKey: key,
716
+ to: config.to,
717
+ from: `${config.fromPrefix}@${getEmailDomain()}`,
718
+ subject: config.subject,
719
+ contents: config.contents,
720
+ });
721
+ }
685
722
 
686
723
  function verifyMachineId(config: {
687
724
  loginToken: string
@@ -938,11 +975,8 @@ export function scriptCreateUser(config: {
938
975
  function setPostmarkAPIKey(config: { apiKey: string; }) {
939
976
  data().secure.postmarkAPIKey = config.apiKey;
940
977
  }
941
-
942
- export function scriptSetPostmarkAPIKey(config: {
943
- apiKey: string;
944
- }) {
945
- setPostmarkAPIKey(config);
978
+ function setNotifyDiscordWebhookURL(config: { webhookURL: string; }) {
979
+ data().secure.notifyDiscordWebhookURL = config.webhookURL;
946
980
  }
947
981
 
948
982
 
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(`A completely new error that is not suppressed. .`);
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());