@superblocksteam/sdk 2.0.157-next.4 → 2.0.157-next.5

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 (67) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
  3. package/dist/cli-replacement/automatic-upgrades.js +4 -8
  4. package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
  5. package/dist/cli-replacement/automatic-upgrades.test.js +2 -1
  6. package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
  7. package/dist/cli-replacement/dev-s3-restore.test.mjs +6 -2
  8. package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
  9. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +6 -2
  10. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
  11. package/dist/cli-replacement/dev-startup-lock-acquisition.test.d.mts +2 -0
  12. package/dist/cli-replacement/dev-startup-lock-acquisition.test.d.mts.map +1 -0
  13. package/dist/cli-replacement/dev-startup-lock-acquisition.test.mjs +487 -0
  14. package/dist/cli-replacement/dev-startup-lock-acquisition.test.mjs.map +1 -0
  15. package/dist/cli-replacement/dev.d.mts +6 -6
  16. package/dist/cli-replacement/dev.d.mts.map +1 -1
  17. package/dist/cli-replacement/dev.interception.test.mjs +12 -12
  18. package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
  19. package/dist/cli-replacement/dev.mjs +100 -42
  20. package/dist/cli-replacement/dev.mjs.map +1 -1
  21. package/dist/dev-utils/dev-server-json-parser.test.d.mts +2 -0
  22. package/dist/dev-utils/dev-server-json-parser.test.d.mts.map +1 -0
  23. package/dist/dev-utils/dev-server-json-parser.test.mjs +60 -0
  24. package/dist/dev-utils/dev-server-json-parser.test.mjs.map +1 -0
  25. package/dist/dev-utils/dev-server.d.mts +36 -1
  26. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  27. package/dist/dev-utils/dev-server.mjs +80 -73
  28. package/dist/dev-utils/dev-server.mjs.map +1 -1
  29. package/dist/dev-utils/dev-server.shutdown-lock.test.d.mts +2 -0
  30. package/dist/dev-utils/dev-server.shutdown-lock.test.d.mts.map +1 -0
  31. package/dist/dev-utils/dev-server.shutdown-lock.test.mjs +48 -0
  32. package/dist/dev-utils/dev-server.shutdown-lock.test.mjs.map +1 -0
  33. package/dist/dev-utils/fatal-process-barrier.d.mts +71 -0
  34. package/dist/dev-utils/fatal-process-barrier.d.mts.map +1 -0
  35. package/dist/dev-utils/fatal-process-barrier.mjs +161 -0
  36. package/dist/dev-utils/fatal-process-barrier.mjs.map +1 -0
  37. package/dist/dev-utils/fatal-process-barrier.spawn-fixture.d.mts +2 -0
  38. package/dist/dev-utils/fatal-process-barrier.spawn-fixture.d.mts.map +1 -0
  39. package/dist/dev-utils/fatal-process-barrier.spawn-fixture.mjs +9 -0
  40. package/dist/dev-utils/fatal-process-barrier.spawn-fixture.mjs.map +1 -0
  41. package/dist/dev-utils/fatal-process-barrier.test.d.mts +2 -0
  42. package/dist/dev-utils/fatal-process-barrier.test.d.mts.map +1 -0
  43. package/dist/dev-utils/fatal-process-barrier.test.mjs +281 -0
  44. package/dist/dev-utils/fatal-process-barrier.test.mjs.map +1 -0
  45. package/dist/telemetry/logging.js +1 -1
  46. package/dist/telemetry/logging.js.map +1 -1
  47. package/dist/telemetry/safe-stringify.d.ts.map +1 -1
  48. package/dist/telemetry/safe-stringify.js +25 -9
  49. package/dist/telemetry/safe-stringify.js.map +1 -1
  50. package/package.json +10 -6
  51. package/src/cli-replacement/automatic-upgrades.test.ts +2 -1
  52. package/src/cli-replacement/automatic-upgrades.ts +4 -11
  53. package/src/cli-replacement/dev-s3-restore.test.mts +6 -2
  54. package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +6 -2
  55. package/src/cli-replacement/dev-startup-lock-acquisition.test.mts +573 -0
  56. package/src/cli-replacement/dev.interception.test.mts +12 -12
  57. package/src/cli-replacement/dev.mts +112 -56
  58. package/src/dev-utils/dev-server-json-parser.test.mts +89 -0
  59. package/src/dev-utils/dev-server.mts +110 -93
  60. package/src/dev-utils/dev-server.shutdown-lock.test.mts +58 -0
  61. package/src/dev-utils/fatal-process-barrier.mts +262 -0
  62. package/src/dev-utils/fatal-process-barrier.spawn-fixture.mts +12 -0
  63. package/src/dev-utils/fatal-process-barrier.test.mts +346 -0
  64. package/src/telemetry/logging.ts +1 -1
  65. package/src/telemetry/safe-stringify.ts +25 -10
  66. package/test/safe-stringify.test.mts +25 -0
  67. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,346 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import { FATAL_EXIT_EVENT } from "./fatal-exit.mjs";
8
+ import {
9
+ fatalAwareExitCode,
10
+ installFatalProcessBarrier,
11
+ isFatalProcessHandled,
12
+ type FatalProcessHost,
13
+ } from "./fatal-process-barrier.mjs";
14
+
15
+ /**
16
+ * Stands in for `process` so a test can emit crash events without arming the
17
+ * real handlers, which would take the test runner down with them.
18
+ */
19
+ function fakeProcess(): FatalProcessHost & {
20
+ emit: (event: string, ...args: unknown[]) => void;
21
+ listenerCount: (event: string) => number;
22
+ } {
23
+ const handlers = new Map<string, ((...args: unknown[]) => void)[]>();
24
+ return {
25
+ emit(event: string, ...args: unknown[]): void {
26
+ for (const handler of handlers.get(event) ?? []) {
27
+ handler(...args);
28
+ }
29
+ },
30
+ listenerCount(event: string): number {
31
+ return handlers.get(event)?.length ?? 0;
32
+ },
33
+ off(event: string, handler: (...args: unknown[]) => void): void {
34
+ handlers.set(
35
+ event,
36
+ (handlers.get(event) ?? []).filter((each) => each !== handler),
37
+ );
38
+ },
39
+ on(event: string, handler: (...args: unknown[]) => void): void {
40
+ handlers.set(event, [...(handlers.get(event) ?? []), handler]);
41
+ },
42
+ };
43
+ }
44
+
45
+ describe("installFatalProcessBarrier", () => {
46
+ it("logs a fatal-exit line with error meta and exits 1 when a throw happens before the server exists", async () => {
47
+ const host = fakeProcess();
48
+ const exit = vi.fn();
49
+ const log = vi.fn();
50
+ const crash = new TypeError("cannot read properties of null");
51
+
52
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
53
+ host.emit("uncaughtException", crash);
54
+
55
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
56
+ expect(log).toHaveBeenCalledTimes(1);
57
+ const [message, meta] = log.mock.calls[0] ?? [];
58
+ expect(message).toContain(`event=${FATAL_EXIT_EVENT}`);
59
+ expect(message).toContain("handler=uncaughtException");
60
+ expect(message).toContain("exit_code=1");
61
+ expect(meta).toEqual({
62
+ error: {
63
+ kind: "TypeError",
64
+ message: "cannot read properties of null",
65
+ stack: crash.stack,
66
+ },
67
+ });
68
+ });
69
+
70
+ it("serializes a plain-object rejection so the message is not [object Object]", async () => {
71
+ const host = fakeProcess();
72
+ const exit = vi.fn();
73
+ const log = vi.fn();
74
+ const reason = { code: 8, message: "[internal] Connection closed" };
75
+
76
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
77
+ host.emit("unhandledRejection", reason);
78
+
79
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
80
+ const [, meta] = log.mock.calls[0] ?? [];
81
+ expect(meta).toEqual({
82
+ error: {
83
+ kind: "Unknown Error",
84
+ message: JSON.stringify(reason),
85
+ },
86
+ });
87
+ });
88
+
89
+ it("serializes a BigInt field in a non-Error rejection and still exits 1", async () => {
90
+ const host = fakeProcess();
91
+ const exit = vi.fn();
92
+ const log = vi.fn();
93
+ const reason = { id: 8n, message: "closed" };
94
+
95
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
96
+ host.emit("unhandledRejection", reason);
97
+
98
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
99
+ const [message, meta] = log.mock.calls[0] ?? [];
100
+ expect(String(message)).toContain(`event=${FATAL_EXIT_EVENT}`);
101
+ expect(meta).toEqual({
102
+ error: {
103
+ kind: "Unknown Error",
104
+ message: '{"id":"8","message":"closed"}',
105
+ },
106
+ });
107
+ });
108
+
109
+ it("still writes the fatal-exit line and exits 1 when toJSON throws", async () => {
110
+ const host = fakeProcess();
111
+ const exit = vi.fn();
112
+ const log = vi.fn();
113
+ const reason = {
114
+ toJSON() {
115
+ throw new Error("cannot serialize");
116
+ },
117
+ };
118
+
119
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
120
+ host.emit("unhandledRejection", reason);
121
+
122
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
123
+ expect(log).toHaveBeenCalledTimes(1);
124
+ const [message, meta] = log.mock.calls[0] ?? [];
125
+ expect(String(message)).toContain(`event=${FATAL_EXIT_EVENT}`);
126
+ expect(meta).toEqual({
127
+ error: {
128
+ kind: "Unknown Error",
129
+ message: "[object Object]",
130
+ },
131
+ });
132
+ expect(isFatalProcessHandled(host)).toBe(true);
133
+ });
134
+
135
+ it("still exits 1 when the fatal log itself throws after the barrier is marked handled", async () => {
136
+ const host = fakeProcess();
137
+ const exit = vi.fn();
138
+
139
+ installFatalProcessBarrier({
140
+ exit,
141
+ isWarm: false,
142
+ log: () => {
143
+ throw new Error("logger exploded");
144
+ },
145
+ process: host,
146
+ });
147
+ host.emit("uncaughtException", new Error("boom"));
148
+
149
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
150
+ expect(isFatalProcessHandled(host)).toBe(true);
151
+ });
152
+
153
+ it("labels an unhandled rejection as a rejection, not an exception", async () => {
154
+ const host = fakeProcess();
155
+ const exit = vi.fn();
156
+ const log = vi.fn();
157
+
158
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
159
+ host.emit("unhandledRejection", new Error("upstream timed out"));
160
+
161
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
162
+ const [message] = log.mock.calls[0] ?? [];
163
+ expect(message).toContain("handler=unhandledRejection");
164
+ });
165
+
166
+ it("writes one fatal-exit line for a burst of crashes", async () => {
167
+ const host = fakeProcess();
168
+ const exit = vi.fn();
169
+ const log = vi.fn();
170
+
171
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
172
+ host.emit("uncaughtException", new Error("first"));
173
+ host.emit("unhandledRejection", new Error("second"));
174
+ host.emit("uncaughtException", new Error("third"));
175
+
176
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledTimes(1));
177
+ expect(log).toHaveBeenCalledTimes(1);
178
+ expect(String(log.mock.calls[0]?.[0])).toContain(
179
+ "handler=uncaughtException",
180
+ );
181
+ });
182
+
183
+ it("arms one set of handlers however many times it is installed", async () => {
184
+ const host = fakeProcess();
185
+ const exit = vi.fn();
186
+ const log = vi.fn();
187
+
188
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
189
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
190
+ host.emit("uncaughtException", new Error("boom"));
191
+
192
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledTimes(1));
193
+ expect(log).toHaveBeenCalledTimes(1);
194
+ });
195
+
196
+ it("still exits fatally when isNonFatal throws, and logs the predicate failure", async () => {
197
+ const host = fakeProcess();
198
+ const exit = vi.fn();
199
+ const log = vi.fn();
200
+ const predicateError = new Error("predicate exploded");
201
+
202
+ installFatalProcessBarrier({
203
+ exit,
204
+ isNonFatal: () => {
205
+ throw predicateError;
206
+ },
207
+ log,
208
+ process: host,
209
+ });
210
+ host.emit("unhandledRejection", new Error("closed"));
211
+
212
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
213
+ expect(log).toHaveBeenCalledTimes(2);
214
+ const [predicateMessage, predicateMeta] = log.mock.calls[0] ?? [];
215
+ expect(String(predicateMessage)).not.toContain(`event=${FATAL_EXIT_EVENT}`);
216
+ expect(predicateMeta).toEqual({
217
+ error: {
218
+ kind: "Error",
219
+ message: "predicate exploded",
220
+ stack: predicateError.stack,
221
+ },
222
+ });
223
+ const [fatalMessage] = log.mock.calls[1] ?? [];
224
+ expect(String(fatalMessage)).toContain(`event=${FATAL_EXIT_EVENT}`);
225
+ });
226
+
227
+ it("keeps the process alive when isNonFatal says the reason is recoverable", () => {
228
+ const host = fakeProcess();
229
+ const exit = vi.fn();
230
+ const log = vi.fn();
231
+
232
+ installFatalProcessBarrier({
233
+ exit,
234
+ isNonFatal: (_handler, reason) =>
235
+ reason instanceof Error && reason.message === "closed",
236
+ log,
237
+ process: host,
238
+ });
239
+ host.emit("unhandledRejection", new Error("closed"));
240
+
241
+ expect(exit).not.toHaveBeenCalled();
242
+ expect(log).not.toHaveBeenCalled();
243
+ });
244
+
245
+ it("upgrades onFatal on an already-armed host without a second listener", async () => {
246
+ const host = fakeProcess();
247
+ const exit = vi.fn();
248
+ const log = vi.fn();
249
+ const onFatal = vi.fn();
250
+
251
+ installFatalProcessBarrier({ exit, isWarm: false, log, process: host });
252
+ installFatalProcessBarrier({ onFatal, process: host });
253
+ host.emit("uncaughtException", new Error("boom"));
254
+
255
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
256
+ expect(onFatal).toHaveBeenCalledTimes(1);
257
+ expect(onFatal.mock.calls[0]?.[0]).toBe("uncaughtException");
258
+ });
259
+
260
+ it("reports a crash as exit code 1 so graceful shutdown cannot claim 0", async () => {
261
+ const host = fakeProcess();
262
+ const exit = vi.fn();
263
+
264
+ installFatalProcessBarrier({
265
+ exit,
266
+ log: () => undefined,
267
+ onFatal: () => undefined,
268
+ process: host,
269
+ });
270
+ expect(fatalAwareExitCode(host)).toBe(0);
271
+ host.emit("uncaughtException", new Error("boom"));
272
+ await vi.waitFor(() => expect(isFatalProcessHandled(host)).toBe(true));
273
+ expect(fatalAwareExitCode(host)).toBe(1);
274
+ });
275
+ });
276
+
277
+ describe("installFatalProcessBarrier across CLI module copies", () => {
278
+ afterEach(() => {
279
+ vi.resetModules();
280
+ });
281
+
282
+ it("shares hooks across module instances, as when bin/run.js and the esbuild bundle each load their own", async () => {
283
+ const host = fakeProcess();
284
+ const exit = vi.fn();
285
+ const log = vi.fn();
286
+ const onFatal = vi.fn();
287
+
288
+ const early = await import("./fatal-process-barrier.mjs");
289
+ early.installFatalProcessBarrier({
290
+ exit,
291
+ isWarm: false,
292
+ log,
293
+ process: host,
294
+ });
295
+ expect(host.listenerCount("uncaughtException")).toBe(1);
296
+ expect(host.listenerCount("unhandledRejection")).toBe(1);
297
+
298
+ vi.resetModules();
299
+ const later = await import("./fatal-process-barrier.mjs");
300
+ later.installFatalProcessBarrier({
301
+ exit,
302
+ isNonFatal: (_handler, reason) =>
303
+ reason instanceof Error && reason.message === "closed",
304
+ log,
305
+ onFatal,
306
+ process: host,
307
+ });
308
+
309
+ expect(host.listenerCount("uncaughtException")).toBe(1);
310
+ expect(host.listenerCount("unhandledRejection")).toBe(1);
311
+
312
+ host.emit("unhandledRejection", new Error("closed"));
313
+ expect(exit).not.toHaveBeenCalled();
314
+ expect(log).not.toHaveBeenCalled();
315
+ expect(onFatal).not.toHaveBeenCalled();
316
+
317
+ host.emit("uncaughtException", new Error("boom"));
318
+ await vi.waitFor(() => expect(exit).toHaveBeenCalledWith(1));
319
+ expect(onFatal).toHaveBeenCalledTimes(1);
320
+ expect(log).toHaveBeenCalledTimes(1);
321
+ });
322
+ });
323
+
324
+ describe("installEarlyFatalProcessBarrier in a real Node process", () => {
325
+ it("writes the fatal-exit token and stack to stderr before exiting 1", () => {
326
+ const fixture = fileURLToPath(
327
+ new URL("./fatal-process-barrier.spawn-fixture.mts", import.meta.url),
328
+ );
329
+ const require = createRequire(
330
+ fileURLToPath(new URL("../../package.json", import.meta.url)),
331
+ );
332
+ const tsx = require.resolve("tsx");
333
+ const result = spawnSync(process.execPath, ["--import", tsx, fixture], {
334
+ encoding: "utf8",
335
+ env: { ...process.env, NODE_OPTIONS: "" },
336
+ timeout: 30_000,
337
+ });
338
+
339
+ const output = `${result.stdout}${result.stderr}`;
340
+ expect(result.status).toBe(1);
341
+ expect(output).toContain(`event=${FATAL_EXIT_EVENT}`);
342
+ expect(output).toContain("handler=uncaughtException");
343
+ expect(output).toContain("spawn-fixture crash");
344
+ expect(output).toMatch(/TypeError/);
345
+ });
346
+ });
@@ -214,7 +214,7 @@ export function getErrorMeta(error: unknown): ErrorMeta {
214
214
  return {
215
215
  error: {
216
216
  kind: "Unknown Error",
217
- message: safeStringify(error) ?? String(error),
217
+ message: safeStringify(error),
218
218
  },
219
219
  };
220
220
  }
@@ -1,3 +1,11 @@
1
+ function fallbackString(value: unknown): string {
2
+ try {
3
+ return String(value);
4
+ } catch {
5
+ return "[unserializable]";
6
+ }
7
+ }
8
+
1
9
  export function safeStringify(obj: unknown): string {
2
10
  if (obj === null || obj === undefined) {
3
11
  return String(obj);
@@ -9,15 +17,22 @@ export function safeStringify(obj: unknown): string {
9
17
  return String(obj);
10
18
  }
11
19
 
12
- const seen = new WeakSet();
13
- return JSON.stringify(obj, (_key, value) => {
14
- if (value === null || typeof value !== "object") {
20
+ const seen = new WeakSet<object>();
21
+ try {
22
+ return JSON.stringify(obj, (_key, value: unknown) => {
23
+ if (typeof value === "bigint") {
24
+ return value.toString();
25
+ }
26
+ if (value === null || typeof value !== "object") {
27
+ return value;
28
+ }
29
+ if (seen.has(value)) {
30
+ return "[Circular]";
31
+ }
32
+ seen.add(value);
15
33
  return value;
16
- }
17
- if (seen.has(value)) {
18
- return "[Circular]";
19
- }
20
- seen.add(value);
21
- return value;
22
- });
34
+ });
35
+ } catch {
36
+ return fallbackString(obj);
37
+ }
23
38
  }
@@ -15,4 +15,29 @@ describe("safeStringify", () => {
15
15
  obj.self = obj;
16
16
  expect(safeStringify(obj)).toBe('{"a":1,"self":"[Circular]"}');
17
17
  });
18
+
19
+ it("does not throw on BigInt fields", () => {
20
+ expect(safeStringify({ id: 8n })).toBe('{"id":"8"}');
21
+ });
22
+
23
+ it("does not throw when toJSON throws", () => {
24
+ const obj = {
25
+ toJSON() {
26
+ throw new Error("cannot serialize");
27
+ },
28
+ };
29
+ expect(safeStringify(obj)).toBe("[object Object]");
30
+ });
31
+
32
+ it("does not throw when both toJSON and toString throw", () => {
33
+ const obj = {
34
+ toJSON() {
35
+ throw new Error("cannot serialize");
36
+ },
37
+ toString() {
38
+ throw new Error("cannot stringify");
39
+ },
40
+ };
41
+ expect(safeStringify(obj)).toBe("[unserializable]");
42
+ });
18
43
  });