querysub 0.343.0 → 0.345.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.343.0",
3
+ "version": "0.345.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -15,7 +15,8 @@
15
15
  "test": "yarn typenode ./test.ts",
16
16
  "test3": "yarn typenode ./src/test/test.tsx --local",
17
17
  "test2": "yarn typenode ./src/4-dom/qreactTest.tsx --local",
18
- "error-watch": "yarn typenode ./src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx"
18
+ "error-watch": "yarn typenode ./src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx",
19
+ "error-email": "yarn typenode ./src/diagnostics/logs/errorNotifications/errorDigestEntry.tsx"
19
20
  },
20
21
  "bin": {
21
22
  "deploy": "./bin/deploy.js",
@@ -1636,6 +1636,7 @@ export class PathValueProxyWatcher {
1636
1636
  // The calls have to happen after our local writes. This is because they are likely to
1637
1637
  // influence the local writes, and we don't want our local writes to be always invalidated
1638
1638
  call.runAtTime = getNextTime();
1639
+ call.fromProxy = watcher.debugName;
1639
1640
  logErrors(runCall(call, metadata));
1640
1641
  watcher.options.onCallCommit?.(call, metadata);
1641
1642
  }
@@ -121,7 +121,7 @@ export function interceptCallsBase<T>(
121
121
  }
122
122
  interceptCalls.declare(interceptCallsBase);
123
123
 
124
-
124
+ /** Writes the function call allowing interceptors to capture it (vs runCall which just runs it). */
125
125
  export function writeFunctionCall(config: {
126
126
  domainName: string;
127
127
  moduleId: string;
@@ -91,6 +91,9 @@ export interface CallSpec {
91
91
  callerIP: string;
92
92
  runAtTime: Time;
93
93
 
94
+ // Not just used for debugging, also used to add special proxy-related warnings.
95
+ fromProxy?: string;
96
+
94
97
  filterable?: Filterable;
95
98
  }
96
99
  export function debugCallSpec(spec: CallSpec): string {
@@ -48,6 +48,9 @@ export type FunctionMetadata<F = unknown> = {
48
48
  */
49
49
  delayCommit?: boolean;
50
50
 
51
+ /** By default, we try to finish calls in the start order when using Querysub.commitAsync, unless the caller explicitly says not to. However, if this is set, then we won't have this call finish or cause other calls to be finished in the start order. This makes locking less safe, but can be useful for long-running functions that shouldn't block other functions and which the order doesn't matter. */
52
+ noFinishInStartOrder?: boolean;
53
+
51
54
  /** Too many locks can lag the server, and eventually cause crashes. Consider using Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. However, if absolutely required, you can override max locks to allow as many locks to be created as you want until the server crashses... */
52
55
  maxLocksOverride?: number;
53
56
  };
@@ -363,7 +366,7 @@ export function syncSchema<Schema>(schema?: Schema2): SyncSchemaResult<Schema> {
363
366
  moduleId,
364
367
  functionId: name,
365
368
  args,
366
- metadata,
369
+ metadata: metadata
367
370
  });
368
371
  }, {
369
372
  debug() {
@@ -18,6 +18,8 @@ import { Button } from "../library-components/Button";
18
18
  import { updateRootDiscoveryLocation } from "../-f-node-discovery/NodeDiscovery";
19
19
 
20
20
  const SWITCH_SERVER_TIMEOUT = timeInSecond * 15;
21
+ const MAX_DISPLAY_INTERVAL = timeInMinute * 5;
22
+ const FINAL_DELAY = timeInSecond * 30;
21
23
 
22
24
  let lastHashServer = "";
23
25
  export function startEdgeNotifier() {
@@ -61,113 +63,105 @@ const notifyClients = throttleFunction(timeInMinute, async function notifyClient
61
63
  // Track current notification state
62
64
  let currentNotification: { close: () => void } | null = null;
63
65
  let curHash = "";
66
+ let lastShownTime = 0;
64
67
  function onLiveHashChange(liveHash: string, refreshThresholdTime: number) {
65
68
  console.log(blue(`Received client liveHash ${liveHash}, prev hash: ${curHash}`));
66
69
  if (liveHash === curHash) return;
67
70
  let prevHash = curHash;
68
71
  // Don't notify the user right away. Hopefully they refresh naturally, and we never have to notify them at all!
69
72
  // Also, refresh BEFORE the server dies, not exactly when it is about to die
70
- let notifyIntervals = [0.4, 0.75, 0.95];
73
+ let delays = [0.4, 0.75, 1].map(x => delay(x * refreshThresholdTime));
74
+
71
75
  console.log(blue(`Client liveHash changed ${liveHash}, prev hash: ${prevHash}`));
72
- // If we are replacing an already existing notification, don't show immediately
73
- let skipFirst = false;
74
- if (currentNotification) {
75
- currentNotification.close();
76
- currentNotification = null;
77
- skipFirst = true;
78
- }
79
- curHash = liveHash;
80
76
 
81
- let duration = refreshThresholdTime - Date.now();
77
+ curHash = liveHash;
82
78
 
83
79
  // Start notification loop
84
80
  void (async () => {
85
81
  // Show notifications at intervals
86
- for (let i = 0; i < notifyIntervals.length - 1; i++) {
82
+ for (let delayTime of delays) {
83
+ await delayTime;
87
84
  // Don't show if a newer notification is active
88
85
  if (curHash !== liveHash) return;
89
86
 
90
- let waitDuration = (notifyIntervals[i + 1] - notifyIntervals[i]) * duration;
91
- // If the duration is short, and it's not the last one, skip it
92
- if (i < notifyIntervals.length - 2 && waitDuration <= 30 * 1000) continue;
93
-
94
87
  // Update the URL override for manual refreshes
95
88
  if (!liveHash.startsWith("forced")) {
96
89
  Querysub.localCommit(() => {
97
90
  liveHashOverrideURL.value = { liveHash, time: Date.now() };
98
91
  });
99
92
  }
100
- if (!skipFirst) {
101
- if (currentNotification) {
102
- currentNotification.close();
103
- currentNotification = null;
104
- }
105
- // Show notification modal
106
- let nextNotification = {
107
- close: showModal({
108
- onClose: () => {
109
- if (currentNotification !== nextNotification) return;
110
- currentNotification = null;
111
- },
112
- content: (
113
- <div
114
- title={`Live Hash: ${liveHash}, prev hash: ${prevHash}`}
115
- className={
116
- css.vbox(10).pad(20)
117
- .hsla(0, 0, 0, 0.8)
118
- .position("fixed")
119
- .bottom(10)
120
- .right(10)
121
- .zIndex(1000)
122
- + " keepModalsOpen"
123
- }
124
- >
125
- <div className={css.vbox(10).maxWidth(250)}>
126
- <h3 className={css.margin(0)}>Server Update Available</h3>
127
- <div>The server has been updated. Please refresh the page to ensure you don't experience compatibility issues.</div>
128
- {i !== notifyIntervals.length - 2 && <div>This notification will be shown again at {formatNiceDateTime(Date.now() + waitDuration)}</div>}
129
- {i === notifyIntervals.length - 2 && <div>The page will automatically refresh at {formatNiceDateTime(Date.now() + waitDuration)}</div>}
130
- </div>
131
- <div className={css.hbox(10).justifyContent("flex-end")}>
132
- <button
133
- className={css.pad(8, 16).hsl(200, 50, 50).color("white").pointer}
134
- onClick={(e) => {
135
- if (!liveHash.startsWith("forced")) {
136
- Querysub.localCommit(() => {
137
- liveHashOverrideURL.value = { liveHash, time: Date.now() };
138
- });
139
- }
140
- window.location.reload();
141
- }}
142
- >
143
- Refresh Now
144
- </button>
145
- <button
146
- className={css.pad(8, 16).pointer}
147
- onClick={(e) => {
148
- if (currentNotification) {
149
- currentNotification.close();
150
- currentNotification = null;
151
- }
152
- }}
153
- >
154
- Dismiss
155
- </button>
156
- </div>
157
- </div>
158
- )
159
- }).close
160
- };
161
- currentNotification = nextNotification;
162
- }
163
- skipFirst = false;
164
93
 
165
- console.log(red(`Notify again in ${formatTime(waitDuration)}`));
166
- await delay(waitDuration);
94
+ let timeSinceLastShown = Date.now() - lastShownTime;
95
+ let isLastDelay = delayTime === delays.at(-1);
96
+ if (timeSinceLastShown < MAX_DISPLAY_INTERVAL && !isLastDelay) continue;
97
+
98
+ lastShownTime = Date.now();
99
+
100
+ if (currentNotification) {
101
+ currentNotification.close();
102
+ currentNotification = null;
103
+ }
104
+ // Show notification modal
105
+ let nextNotification = {
106
+ close: showModal({
107
+ onClose: () => {
108
+ if (currentNotification !== nextNotification) return;
109
+ currentNotification = null;
110
+ },
111
+ content: (
112
+ <div
113
+ title={`Live Hash: ${liveHash}, prev hash: ${prevHash}`}
114
+ className={
115
+ css.vbox(10).pad(20)
116
+ .hsla(0, 0, 0, 0.8)
117
+ .position("fixed")
118
+ .bottom(10)
119
+ .right(10)
120
+ .zIndex(1000)
121
+ + " keepModalsOpen"
122
+ }
123
+ >
124
+ <div className={css.vbox(10).maxWidth(250)}>
125
+ <h3 className={css.margin(0)}>Server Update Available</h3>
126
+ <div>The server has been updated. Please refresh the page to ensure you don't experience compatibility issues.</div>
127
+ {isLastDelay && <div>The page will automatically refresh shortly</div>}
128
+ </div>
129
+ <div className={css.hbox(10).justifyContent("flex-end")}>
130
+ <button
131
+ className={css.pad(8, 16).hsl(200, 50, 50).color("white").pointer}
132
+ onClick={(e) => {
133
+ if (!liveHash.startsWith("forced")) {
134
+ Querysub.localCommit(() => {
135
+ liveHashOverrideURL.value = { liveHash, time: Date.now() };
136
+ });
137
+ }
138
+ window.location.reload();
139
+ }}
140
+ >
141
+ Refresh Now
142
+ </button>
143
+ <button
144
+ className={css.pad(8, 16).pointer}
145
+ onClick={(e) => {
146
+ if (currentNotification) {
147
+ currentNotification.close();
148
+ currentNotification = null;
149
+ }
150
+ }}
151
+ >
152
+ Dismiss
153
+ </button>
154
+ </div>
155
+ </div>
156
+ )
157
+ }).close
158
+ };
159
+ currentNotification = nextNotification;
167
160
  }
168
161
 
169
162
  // Only force refresh if this is still the current notification
170
- if (curHash === liveHash) {
163
+ if (curHash === liveHash && currentNotification) {
164
+ await delay(FINAL_DELAY);
171
165
  window.location.reload();
172
166
  }
173
167
  })();
@@ -418,7 +418,7 @@ export function getCallWrites(config: {
418
418
 
419
419
  let finishInStartOrder: number | boolean | undefined;
420
420
 
421
- if (config.useFinishReordering) {
421
+ if (config.useFinishReordering && !config.metadata?.noFinishInStartOrder && call.fromProxy) {
422
422
  let triggerCaller = getCurrentCallCreationProxy();
423
423
  if (!triggerCaller) {
424
424
  require("debugbreak")(2);
@@ -40,10 +40,21 @@ export type LogDatum = Record<string, unknown> & {
40
40
  export function getLogHash(obj: LogDatum) {
41
41
  return getPathStr2(obj.__threadId || "", obj.time.toString());
42
42
  }
43
+ const errorMessageFileRegex = /\n at .+?\(([^:]+):\d+:\d+\)/;
44
+
43
45
  export function getLogFile(obj: LogDatum) {
44
46
  let logType = obj.param0 || "";
45
47
  if (obj.__FILE__) {
46
48
  logType = String(obj.__FILE__);
49
+ // /root/machine-services/*/git/
50
+ if (logType.startsWith("/root/machine-services/")) {
51
+ logType = logType.split("/").slice(4).join("/");
52
+ }
53
+ } else {
54
+ let match = logType.match(errorMessageFileRegex);
55
+ if (match) {
56
+ logType = match[1];
57
+ }
47
58
  }
48
59
  if (obj[LOG_LINE_LIMIT_ID]) {
49
60
  logType += "::" + String(obj[LOG_LINE_LIMIT_ID]);
@@ -23,6 +23,14 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
23
23
  }
24
24
 
25
25
  let errors: {
26
+ file: string;
27
+ errorsInFile: number;
28
+ warningsInFile: number;
29
+ message: string;
30
+ messageTime: string;
31
+ }[] = [];
32
+ let warnings: {
33
+ file: string;
26
34
  errorsInFile: number;
27
35
  warningsInFile: number;
28
36
  message: string;
@@ -38,6 +46,8 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
38
46
  let corruptWarning = "";
39
47
 
40
48
  let failingFiles = 0;
49
+ let errorFiles = 0;
50
+ let warningFiles = 0;
41
51
 
42
52
  for (let value of digestInfo.histogram.values()) {
43
53
  errorCount += value.unsuppressedErrors;
@@ -52,25 +62,44 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
52
62
  if (value.firstCorruptWarning && !corruptWarning) {
53
63
  corruptWarning = value.firstCorruptWarning;
54
64
  }
55
- if (value.unsuppressedErrors > 0) {
56
- failingFiles++;
57
- }
58
65
  }
59
66
 
60
- for (let value of digestInfo.byFile.values()) {
67
+ for (let [file, value] of digestInfo.byFile) {
61
68
  for (let error of value.latestErrors.slice(-MAX_COUNT_PER_FILE)) {
62
69
  errors.push({
70
+ file,
63
71
  errorsInFile: value.errors,
64
72
  warningsInFile: value.warnings,
65
73
  message: `${error.param0} (${error.__NAME__})`,
66
74
  messageTime: formatDateTime(error.time),
67
75
  });
68
76
  }
77
+
78
+ for (let warning of value.latestWarnings.slice(-MAX_COUNT_PER_FILE)) {
79
+ warnings.push({
80
+ file,
81
+ errorsInFile: value.errors,
82
+ warningsInFile: value.warnings,
83
+ message: `${warning.param0} (${warning.__NAME__})`,
84
+ messageTime: formatDateTime(warning.time),
85
+ });
86
+ }
87
+
88
+ if (value.errors > 0) {
89
+ failingFiles++;
90
+ errorFiles++;
91
+ }
92
+ if (value.warnings > 0) {
93
+ warningFiles++;
94
+ }
69
95
  }
70
96
 
71
97
  sort(errors, x => -x.errorsInFile);
72
98
  errors = errors.slice(0, MAX_COUNT);
73
99
 
100
+ sort(warnings, x => -x.warningsInFile);
101
+ warnings = warnings.slice(0, MAX_COUNT);
102
+
74
103
  let link = createLink([
75
104
  showingManagementURL.getOverride(true),
76
105
  managementPageURL.getOverride("ErrorDigestPage"),
@@ -81,7 +110,7 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
81
110
  await sendEmail({
82
111
  to: notifyEmails,
83
112
  fromPrefix: "error-digest",
84
- subject: `${errorCount} err | >= ${formatNumber(failingFiles)} lines | ${warningCount} warn${corruptErrors + corruptWarnings > 0 ? ` | ${corruptErrors + corruptWarnings} corrupt` : ""} | ${suppressedErrors + suppressedWarnings} suppressed | ${formatTime(digestInfo.scanDuration)} | ${formatNumber(digestInfo.totalCompressedBytes)} / ${formatNumber(digestInfo.totalUncompressedBytes)} | ${formatNumber(digestInfo.totalFiles)} log files`,
113
+ subject: `${errorCount} err | ~${formatNumber(failingFiles)} lines | ${warningCount} warn${corruptErrors + corruptWarnings > 0 ? ` | ${corruptErrors + corruptWarnings} corrupt` : ""} | ${suppressedErrors + suppressedWarnings} hid | ${formatTime(digestInfo.scanDuration)} | ${formatNumber(digestInfo.totalUncompressedBytes)} | ${formatNumber(digestInfo.totalFiles)} log files`,
85
114
  contents: <div>
86
115
  <h2>Error Summary</h2>
87
116
  <ul style="list-style-type: none; padding-left: 0;">
@@ -89,12 +118,14 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
89
118
  <strong style="color: #dc3545;">Errors:</strong>
90
119
  <span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{errorCount}</span>
91
120
  <span style="color: #dc3545;"> unsuppressed</span>
121
+ <span style="color: #6c757d; margin-left: 8px;">({formatNumber(errorFiles)} files)</span>
92
122
  {suppressedErrors > 0 && <span>, <span style="color: #6c757d;">{formatNumber(suppressedErrors)} suppressed</span></span>}
93
123
  </li>
94
124
  <li>
95
125
  <strong style="color: #fd7e14;">Warnings:</strong>
96
126
  <span style="background-color: #fd7e14; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{warningCount}</span>
97
127
  <span style="color: #fd7e14;"> unsuppressed</span>
128
+ <span style="color: #6c757d; margin-left: 8px;">({formatNumber(warningFiles)} files)</span>
98
129
  {suppressedWarnings > 0 && <span>, <span style="color: #6c757d;">{formatNumber(suppressedWarnings)} suppressed</span></span>}
99
130
  </li>
100
131
  </ul>
@@ -147,11 +178,12 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
147
178
  <a href={link} style="display: block; margin-bottom: 20px; padding: 10px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; text-align: center;">View live logs</a>
148
179
 
149
180
  {errors.length > 0 && <div>
150
- <h2 style="color: #495057;">Recent Errors (<span style="color: #dc3545; font-weight: bold;">{errors.length}</span> shown)</h2>
181
+ <h2 style="color: #495057;">Recent Errors (<span style="color: #dc3545; font-weight: bold;">{errors.length}</span> files shown, <span style="color: #dc3545; font-weight: bold;">{errors.reduce((acc, error) => acc + error.errorsInFile, 0)}</span> errors)</h2>
151
182
  <table style="border-collapse: collapse; width: 100%; margin-top: 10px;">
152
183
  <thead>
153
184
  <tr style="background-color: #495057; color: white;">
154
185
  <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Time</th>
186
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">File</th>
155
187
  <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Message</th>
156
188
  <th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #dc3545;">Errors in File</th>
157
189
  <th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #fd7e14;">Warnings in File</th>
@@ -161,6 +193,7 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
161
193
  {errors.map((error, index) => (
162
194
  <tr key={index} style={`background-color: ${index % 2 === 0 ? "#f8f9fa" : "white"}`}>
163
195
  <td style="border: 1px solid #ddd; padding: 8px;">{error.messageTime}</td>
196
+ <td style="border: 1px solid #ddd; padding: 8px; font-family: monospace; font-size: 12px;">{error.file}</td>
164
197
  <td style="border: 1px solid #ddd; padding: 8px;">{error.message}</td>
165
198
  <td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #f8d7da; color: #721c24; font-weight: bold;">{error.errorsInFile}</td>
166
199
  <td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #fff3cd; color: #856404; font-weight: bold;">{error.warningsInFile}</td>
@@ -169,6 +202,32 @@ export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
169
202
  </tbody>
170
203
  </table>
171
204
  </div>}
205
+
206
+ {warnings.length > 0 && <div style="margin-top: 20px;">
207
+ <h2 style="color: #495057;">Recent Warnings (<span style="color: #fd7e14; font-weight: bold;">{warnings.length}</span> files shown, <span style="color: #fd7e14; font-weight: bold;">{warnings.reduce((acc, warning) => acc + warning.warningsInFile, 0)}</span> warnings)</h2>
208
+ <table style="border-collapse: collapse; width: 100%; margin-top: 10px;">
209
+ <thead>
210
+ <tr style="background-color: #495057; color: white;">
211
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Time</th>
212
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">File</th>
213
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Message</th>
214
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #dc3545;">Errors in File</th>
215
+ <th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #fd7e14;">Warnings in File</th>
216
+ </tr>
217
+ </thead>
218
+ <tbody>
219
+ {warnings.map((warning, index) => (
220
+ <tr key={index} style={`background-color: ${index % 2 === 0 ? "#f8f9fa" : "white"}`}>
221
+ <td style="border: 1px solid #ddd; padding: 8px;">{warning.messageTime}</td>
222
+ <td style="border: 1px solid #ddd; padding: 8px; font-family: monospace; font-size: 12px;">{warning.file}</td>
223
+ <td style="border: 1px solid #ddd; padding: 8px;">{warning.message}</td>
224
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #f8d7da; color: #721c24; font-weight: bold;">{warning.errorsInFile}</td>
225
+ <td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #fff3cd; color: #856404; font-weight: bold;">{warning.warningsInFile}</td>
226
+ </tr>
227
+ ))}
228
+ </tbody>
229
+ </table>
230
+ </div>}
172
231
  </div>,
173
232
  });
174
233
  }
@@ -1,7 +1,14 @@
1
- import { runDigestLoop } from "./errorDigests";
1
+ import { Querysub } from "../../../4-querysub/QuerysubController";
2
+ import { runDigest, runDigestLoop } from "./errorDigests";
2
3
 
3
4
  async function main() {
4
- await runDigestLoop();
5
+ if (process.argv.includes("--now")) {
6
+ await Querysub.hostService("error-digests-now");
7
+
8
+ await runDigest();
9
+ } else {
10
+ await runDigestLoop();
11
+ }
5
12
  }
6
13
  // The digest loop should never exit, and if it does, we probably want to terminate ourselves so that the service manager will restart us, hopefully putting us back in a good state.
7
14
  main().catch(console.error).finally(() => process.exit());
@@ -92,7 +92,7 @@ function getClosest(value: number, choices: number[]) {
92
92
  return closest;
93
93
  }
94
94
 
95
- async function runDigest() {
95
+ export async function runDigest() {
96
96
  console.log("Running error digest gathering");
97
97
  // Find the previous day
98
98
  let endTime = getClosest(
@@ -1,6 +1,9 @@
1
1
  import cborx from "cbor-x";
2
2
  import { lazy } from "socket-function/src/caching";
3
- const cborxInstance = lazy(() => new cborx.Encoder({ structuredClone: true }));
3
+ const cborxInstance = lazy(() => new cborx.Encoder({
4
+ structuredClone: true,
5
+
6
+ }));
4
7
  export function deepCloneCborx<T>(value: T): T {
5
8
  return decodeCborx(encodeCborx(value));
6
9
  }