@rsc-kit/core 0.1.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 (200) hide show
  1. package/README.md +95 -0
  2. package/SECURITY.md +60 -0
  3. package/dist/action.d.ts +90 -0
  4. package/dist/action.d.ts.map +1 -0
  5. package/dist/action.js +137 -0
  6. package/dist/action.js.map +1 -0
  7. package/dist/build-rsc-vite.d.ts +39 -0
  8. package/dist/build-rsc-vite.d.ts.map +1 -0
  9. package/dist/build-rsc-vite.js +172 -0
  10. package/dist/build-rsc-vite.js.map +1 -0
  11. package/dist/cache.d.ts +25 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +108 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +85 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/compile.d.ts +55 -0
  20. package/dist/compile.d.ts.map +1 -0
  21. package/dist/compile.js +141 -0
  22. package/dist/compile.js.map +1 -0
  23. package/dist/devServer.d.ts +18 -0
  24. package/dist/devServer.d.ts.map +1 -0
  25. package/dist/devServer.js +70 -0
  26. package/dist/devServer.js.map +1 -0
  27. package/dist/devUrls.d.ts +11 -0
  28. package/dist/devUrls.d.ts.map +1 -0
  29. package/dist/devUrls.js +68 -0
  30. package/dist/devUrls.js.map +1 -0
  31. package/dist/embed.d.ts +21 -0
  32. package/dist/embed.d.ts.map +1 -0
  33. package/dist/embed.js +119 -0
  34. package/dist/embed.js.map +1 -0
  35. package/dist/errors.d.ts +24 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +38 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/export.d.ts +41 -0
  40. package/dist/export.d.ts.map +1 -0
  41. package/dist/export.js +100 -0
  42. package/dist/export.js.map +1 -0
  43. package/dist/files.d.ts +39 -0
  44. package/dist/files.d.ts.map +1 -0
  45. package/dist/files.js +146 -0
  46. package/dist/files.js.map +1 -0
  47. package/dist/headers.d.ts +81 -0
  48. package/dist/headers.d.ts.map +1 -0
  49. package/dist/headers.js +96 -0
  50. package/dist/headers.js.map +1 -0
  51. package/dist/host.d.ts +86 -0
  52. package/dist/host.d.ts.map +1 -0
  53. package/dist/host.js +590 -0
  54. package/dist/host.js.map +1 -0
  55. package/dist/js/ActivityRouter.d.ts +47 -0
  56. package/dist/js/ActivityRouter.d.ts.map +1 -0
  57. package/dist/js/ActivityRouter.js +87 -0
  58. package/dist/js/ActivityRouter.js.map +1 -0
  59. package/dist/js/DocumentTitle.d.ts +22 -0
  60. package/dist/js/DocumentTitle.d.ts.map +1 -0
  61. package/dist/js/DocumentTitle.js +28 -0
  62. package/dist/js/DocumentTitle.js.map +1 -0
  63. package/dist/js/Form.d.ts +54 -0
  64. package/dist/js/Form.d.ts.map +1 -0
  65. package/dist/js/Form.js +225 -0
  66. package/dist/js/Form.js.map +1 -0
  67. package/dist/js/Link.d.ts +29 -0
  68. package/dist/js/Link.d.ts.map +1 -0
  69. package/dist/js/Link.js +112 -0
  70. package/dist/js/Link.js.map +1 -0
  71. package/dist/js/PathnameProvider.d.ts +8 -0
  72. package/dist/js/PathnameProvider.d.ts.map +1 -0
  73. package/dist/js/PathnameProvider.js +24 -0
  74. package/dist/js/PathnameProvider.js.map +1 -0
  75. package/dist/js/RedirectBoundary.d.ts +18 -0
  76. package/dist/js/RedirectBoundary.d.ts.map +1 -0
  77. package/dist/js/RedirectBoundary.js +34 -0
  78. package/dist/js/RedirectBoundary.js.map +1 -0
  79. package/dist/js/SegmentBoundary.d.ts +8 -0
  80. package/dist/js/SegmentBoundary.d.ts.map +1 -0
  81. package/dist/js/SegmentBoundary.js +38 -0
  82. package/dist/js/SegmentBoundary.js.map +1 -0
  83. package/dist/js/SlotBoundary.d.ts +15 -0
  84. package/dist/js/SlotBoundary.d.ts.map +1 -0
  85. package/dist/js/SlotBoundary.js +17 -0
  86. package/dist/js/SlotBoundary.js.map +1 -0
  87. package/dist/js/createViteRscApp.d.ts +8 -0
  88. package/dist/js/createViteRscApp.d.ts.map +1 -0
  89. package/dist/js/createViteRscApp.js +185 -0
  90. package/dist/js/createViteRscApp.js.map +1 -0
  91. package/dist/js/errors.d.ts +51 -0
  92. package/dist/js/errors.d.ts.map +1 -0
  93. package/dist/js/errors.js +93 -0
  94. package/dist/js/errors.js.map +1 -0
  95. package/dist/js/navigate.d.ts +118 -0
  96. package/dist/js/navigate.d.ts.map +1 -0
  97. package/dist/js/navigate.js +780 -0
  98. package/dist/js/navigate.js.map +1 -0
  99. package/dist/js/onlineStore.d.ts +23 -0
  100. package/dist/js/onlineStore.d.ts.map +1 -0
  101. package/dist/js/onlineStore.js +42 -0
  102. package/dist/js/onlineStore.js.map +1 -0
  103. package/dist/js/pagePayload.d.ts +15 -0
  104. package/dist/js/pagePayload.d.ts.map +1 -0
  105. package/dist/js/pagePayload.js +39 -0
  106. package/dist/js/pagePayload.js.map +1 -0
  107. package/dist/js/router.d.ts +29 -0
  108. package/dist/js/router.d.ts.map +1 -0
  109. package/dist/js/router.js +40 -0
  110. package/dist/js/router.js.map +1 -0
  111. package/dist/js/section.d.ts +16 -0
  112. package/dist/js/section.d.ts.map +1 -0
  113. package/dist/js/section.js +55 -0
  114. package/dist/js/section.js.map +1 -0
  115. package/dist/js/segmentStore.d.ts +69 -0
  116. package/dist/js/segmentStore.d.ts.map +1 -0
  117. package/dist/js/segmentStore.js +127 -0
  118. package/dist/js/segmentStore.js.map +1 -0
  119. package/dist/js/slotStore.d.ts +35 -0
  120. package/dist/js/slotStore.d.ts.map +1 -0
  121. package/dist/js/slotStore.js +46 -0
  122. package/dist/js/slotStore.js.map +1 -0
  123. package/dist/js/standardSchema.d.ts +47 -0
  124. package/dist/js/standardSchema.d.ts.map +1 -0
  125. package/dist/js/standardSchema.js +41 -0
  126. package/dist/js/standardSchema.js.map +1 -0
  127. package/dist/js/useForm.d.ts +43 -0
  128. package/dist/js/useForm.d.ts.map +1 -0
  129. package/dist/js/useForm.js +164 -0
  130. package/dist/js/useForm.js.map +1 -0
  131. package/dist/js/useLinkStatus.d.ts +2 -0
  132. package/dist/js/useLinkStatus.d.ts.map +1 -0
  133. package/dist/js/useLinkStatus.js +3 -0
  134. package/dist/js/useLinkStatus.js.map +1 -0
  135. package/dist/js/useOnline.d.ts +8 -0
  136. package/dist/js/useOnline.d.ts.map +1 -0
  137. package/dist/js/useOnline.js +20 -0
  138. package/dist/js/useOnline.js.map +1 -0
  139. package/dist/js/usePathname.d.ts +9 -0
  140. package/dist/js/usePathname.d.ts.map +1 -0
  141. package/dist/js/usePathname.js +32 -0
  142. package/dist/js/usePathname.js.map +1 -0
  143. package/dist/js/useSearchParams.d.ts +2 -0
  144. package/dist/js/useSearchParams.d.ts.map +1 -0
  145. package/dist/js/useSearchParams.js +60 -0
  146. package/dist/js/useSearchParams.js.map +1 -0
  147. package/dist/manifest.d.ts +66 -0
  148. package/dist/manifest.d.ts.map +1 -0
  149. package/dist/manifest.js +13 -0
  150. package/dist/manifest.js.map +1 -0
  151. package/dist/prerender.d.ts +138 -0
  152. package/dist/prerender.d.ts.map +1 -0
  153. package/dist/prerender.js +387 -0
  154. package/dist/prerender.js.map +1 -0
  155. package/dist/redirect.d.ts +34 -0
  156. package/dist/redirect.d.ts.map +1 -0
  157. package/dist/redirect.js +108 -0
  158. package/dist/redirect.js.map +1 -0
  159. package/dist/redirectDigest.d.ts +20 -0
  160. package/dist/redirectDigest.d.ts.map +1 -0
  161. package/dist/redirectDigest.js +66 -0
  162. package/dist/redirectDigest.js.map +1 -0
  163. package/dist/request.d.ts +93 -0
  164. package/dist/request.d.ts.map +1 -0
  165. package/dist/request.js +308 -0
  166. package/dist/request.js.map +1 -0
  167. package/dist/revalidate.d.ts +43 -0
  168. package/dist/revalidate.d.ts.map +1 -0
  169. package/dist/revalidate.js +99 -0
  170. package/dist/revalidate.js.map +1 -0
  171. package/dist/routes.d.ts +37 -0
  172. package/dist/routes.d.ts.map +1 -0
  173. package/dist/routes.js +28 -0
  174. package/dist/routes.js.map +1 -0
  175. package/dist/routing.d.ts +52 -0
  176. package/dist/routing.d.ts.map +1 -0
  177. package/dist/routing.js +146 -0
  178. package/dist/routing.js.map +1 -0
  179. package/dist/runtime.d.ts +36 -0
  180. package/dist/runtime.d.ts.map +1 -0
  181. package/dist/runtime.js +86 -0
  182. package/dist/runtime.js.map +1 -0
  183. package/dist/safeUrl.d.ts +10 -0
  184. package/dist/safeUrl.d.ts.map +1 -0
  185. package/dist/safeUrl.js +40 -0
  186. package/dist/safeUrl.js.map +1 -0
  187. package/dist/streaming.d.ts +62 -0
  188. package/dist/streaming.d.ts.map +1 -0
  189. package/dist/streaming.js +108 -0
  190. package/dist/streaming.js.map +1 -0
  191. package/dist/types.d.ts +82 -0
  192. package/dist/vite.d.ts +109 -0
  193. package/dist/vite.d.ts.map +1 -0
  194. package/dist/vite.js +1909 -0
  195. package/dist/vite.js.map +1 -0
  196. package/dist/worker.d.ts +2 -0
  197. package/dist/worker.d.ts.map +1 -0
  198. package/dist/worker.js +863 -0
  199. package/dist/worker.js.map +1 -0
  200. package/package.json +223 -0
package/dist/worker.js ADDED
@@ -0,0 +1,863 @@
1
+ import { chmodSync, unlinkSync } from "node:fs";
2
+ import { listen, runtimeName, scanScripts, yieldToEventLoop } from "./runtime.js";
3
+ import { createDeferredHost, streamWithDeferredRelease } from "./streaming.js";
4
+ import { join, resolve } from "node:path";
5
+ import { ServerAuthenticationError, ServerAuthorizationError, ServerRedirectError, ServerValidationError } from "./errors.js";
6
+ import { isRedirectSignal } from "./redirectDigest.js";
7
+ import { withCache } from "./cache.js";
8
+ import { withRequest, withResponseDraft } from "./request.js";
9
+ const functions = {};
10
+ function log(...args) {
11
+ console.error(`[rsc-worker:${runtimeName}]`, ...args);
12
+ }
13
+ async function discoverFunctions(dir) {
14
+ const scripts = scanScripts(dir);
15
+ for (const path of scripts) {
16
+ const mod = await import(join(dir, path));
17
+ for (const [name, fn] of Object.entries(mod)) {
18
+ if (typeof fn !== "function")
19
+ continue;
20
+ if (name in functions) {
21
+ log(`Warning: duplicate function "${name}" from ${path}, skipping`);
22
+ continue;
23
+ }
24
+ functions[name] = fn;
25
+ }
26
+ }
27
+ }
28
+ async function loadEntryPoints() {
29
+ const raw = process.env.RSC_ENTRY_POINTS ?? "";
30
+ const paths = raw.split(",").map((p) => p.trim()).filter(Boolean);
31
+ for (const entryPath of paths) {
32
+ const absolute = resolve(entryPath);
33
+ try {
34
+ const mod = await import(absolute);
35
+ for (const [name, fn] of Object.entries(mod)) {
36
+ if (name === "default" || typeof fn !== "function" || name.length <= 2)
37
+ continue;
38
+ if (name in functions) {
39
+ log(`Warning: duplicate function "${name}" from ${entryPath}, skipping`);
40
+ continue;
41
+ }
42
+ functions[name] = fn;
43
+ }
44
+ if (typeof mod.default === "function" && !("default" in functions)) {
45
+ const baseName = absolute.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "default";
46
+ const fnName = baseName in functions ? "default" : baseName;
47
+ if (fnName in functions) {
48
+ log(`Warning: duplicate function "${fnName}" from ${entryPath}, skipping`);
49
+ }
50
+ else {
51
+ functions[fnName] = mod.default;
52
+ }
53
+ }
54
+ }
55
+ catch (err) {
56
+ log(`Failed to load entry point "${entryPath}":`, err instanceof Error ? err.message : String(err));
57
+ }
58
+ }
59
+ }
60
+ async function handleMessage(message) {
61
+ switch (message.type) {
62
+ case "ping":
63
+ return '{"type":"pong"}';
64
+ case "call": {
65
+ // hasOwn: `constructor` and `toString` are not host functions, and a
66
+ // caller naming one should be told the function is unknown rather than
67
+ // handed Object's own method to invoke.
68
+ const name = message.function ?? "";
69
+ const fn = Object.hasOwn(functions, name) ? functions[name] : undefined;
70
+ if (!fn) {
71
+ return JSON.stringify({
72
+ error: `Function "${message.function}" not found. Available: ${Object.keys(functions).join(", ")}`,
73
+ });
74
+ }
75
+ try {
76
+ const result = await fn(message.args ?? {});
77
+ return JSON.stringify({ result });
78
+ }
79
+ catch (err) {
80
+ return JSON.stringify({
81
+ error: err instanceof Error ? err.message : String(err),
82
+ });
83
+ }
84
+ }
85
+ case "rsc-payload": {
86
+ if (!rscHandler) {
87
+ return '{"error":"RSC not enabled."}';
88
+ }
89
+ if (!message.component) {
90
+ return '{"error":"Missing component in RSC message"}';
91
+ }
92
+ try {
93
+ const result = await rscHandler.handleRscPayload(message.component, message.props ?? {}, message.layouts ?? [], message.loadings ?? [], message.parallelSlots ?? {}, message.from ?? 0, message.pageKey ?? "");
94
+ return JSON.stringify({ result });
95
+ }
96
+ catch (err) {
97
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
98
+ }
99
+ }
100
+ case "rsc": {
101
+ if (!rscHandler) {
102
+ return '{"error":"RSC not enabled. Set RSC_ENABLED=true and run: bun run build:rsc"}';
103
+ }
104
+ if (!message.component) {
105
+ return '{"error":"Missing component in RSC message"}';
106
+ }
107
+ try {
108
+ // Install the host callable if a callback connection is available
109
+ let cleanupPhp = null;
110
+ if (message.callbackId) {
111
+ const cbConn = await getCallbackConnection(message.callbackId);
112
+ cleanupPhp = rscHandler.installHostFn(createPhpFn(cbConn));
113
+ }
114
+ try {
115
+ // A target asks for one part of this page rather than the whole of
116
+ // it, and needs the same host connection a page does.
117
+ if (message.target) {
118
+ const result = await rscHandler.handleRscRevalidate(message.target, {
119
+ component: message.component,
120
+ props: message.props ?? {},
121
+ layouts: message.layouts ?? [],
122
+ loadings: message.loadings ?? [],
123
+ parallelSlots: message.parallelSlots ?? {},
124
+ });
125
+ return JSON.stringify({ result });
126
+ }
127
+ const metadata = await rscHandler.resolveMetadata(message.component, message.props ?? {}, message.layouts ?? []);
128
+ const result = await rscHandler.handleRsc(message.component, message.props ?? {}, message.callbackSocket ?? null, // fallback for old protocol
129
+ message.layouts ?? [], message.loadings ?? [], message.parallelSlots ?? {}, message.from ?? 0, message.pageKey ?? "", message.bootstrap !== false);
130
+ return JSON.stringify({ result: { ...result, metadata } });
131
+ }
132
+ finally {
133
+ cleanupPhp?.();
134
+ if (message.callbackId)
135
+ callbackConnections.delete(message.callbackId);
136
+ }
137
+ }
138
+ catch (err) {
139
+ return JSON.stringify({
140
+ error: err instanceof Error ? err.message : String(err),
141
+ });
142
+ }
143
+ }
144
+ case "rsc-ppr-shell": {
145
+ if (!rscHandler) {
146
+ return '{"error":"RSC not enabled. Set RSC_ENABLED=true and run: bun run build:rsc"}';
147
+ }
148
+ if (!message.component) {
149
+ return '{"error":"Missing component in RSC message"}';
150
+ }
151
+ try {
152
+ const result = await rscHandler.handleRscPprShell(message.component, message.props ?? {}, message.layouts ?? [], message.loadings ?? [], message.parallelSlots ?? {},
153
+ // The url this shell is for, when it is for exactly one. Dropped
154
+ // here, every shell was rendered as if it were a pattern: params
155
+ // never settle, a page that awaits them suspends immediately, and
156
+ // the frozen shell is its loading fallback with no document around
157
+ // it and no bootstrap in it — so nothing hydrates and nothing fills.
158
+ message.pageKey ?? "");
159
+ return JSON.stringify({ result });
160
+ }
161
+ catch (err) {
162
+ return JSON.stringify({
163
+ error: err instanceof Error ? err.message : String(err),
164
+ });
165
+ }
166
+ }
167
+ case "list":
168
+ return JSON.stringify({ result: Object.keys(functions) });
169
+ default:
170
+ return JSON.stringify({ error: `Unknown message type: "${message.type}"` });
171
+ }
172
+ }
173
+ const functionsDir = process.env.RSC_FUNCTIONS_DIR;
174
+ const socketPath = process.env.RSC_SOCKET ?? "/tmp/bun-bridge.sock";
175
+ // Transport: 'unix' (default) listens on socketPath; 'tcp' listens on
176
+ // RSC_HOST:RSC_MAIN_PORT (main) and RSC_HOST:RSC_CB_PORT (callbacks).
177
+ //
178
+ // The protocol has no authentication of any kind. On a unix socket that is
179
+ // fine — the filesystem permissions are the authentication, and the socket is
180
+ // created private to its owner. Over TCP there is nothing: whoever can open
181
+ // the port can render any component, run any registered host function and read
182
+ // anything either returns. It is for a container where the port is not
183
+ // reachable from outside, and binding it anywhere else is a decision to serve
184
+ // the engine to whoever asks.
185
+ const isTcp = process.env.RSC_TRANSPORT === "tcp";
186
+ const tcpHost = process.env.RSC_HOST ?? "127.0.0.1";
187
+ const mainPort = parseInt(process.env.RSC_MAIN_PORT ?? "0", 10);
188
+ const cbPort = parseInt(process.env.RSC_CB_PORT ?? "0", 10);
189
+ // Said once, loudly, rather than left in a comment nobody reads: an
190
+ // unauthenticated protocol bound to a routable address is a remote code
191
+ // execution surface, and the default of 127.0.0.1 is the only reason this is
192
+ // usually safe.
193
+ if (isTcp && tcpHost !== "127.0.0.1" && tcpHost !== "::1" && tcpHost !== "localhost") {
194
+ log(`WARNING: listening on ${tcpHost} over TCP. This protocol has no ` +
195
+ `authentication — anyone who can reach that address can render any component ` +
196
+ `and call any host function. Bind 127.0.0.1 unless the network is private.`);
197
+ }
198
+ if (functionsDir) {
199
+ await discoverFunctions(functionsDir);
200
+ }
201
+ await loadEntryPoints();
202
+ let rscHandler = null;
203
+ if (process.env.RSC_DEV_CONFIG) {
204
+ // Dev mode: the entry comes from Vite's runnable rsc environment rather than
205
+ // a bundle, so an edit is live without a rebuild. Same module contract.
206
+ try {
207
+ const { startDevServer, devEntryPath } = await import("./devServer.js");
208
+ const outDir = process.env.RSC_OUT_DIR;
209
+ const dev = await startDevServer({
210
+ projectRoot: process.env.RSC_PROJECT_ROOT ?? process.cwd(),
211
+ configFile: process.env.RSC_DEV_CONFIG,
212
+ entry: devEntryPath(outDir),
213
+ port: parseInt(process.env.RSC_DEV_PORT ?? "5173", 10),
214
+ });
215
+ rscHandler = dev.handler;
216
+ log(`RSC dev server on ${process.env.RSC_DEV_ORIGIN}`);
217
+ }
218
+ catch (err) {
219
+ log("Failed to start RSC dev server:", err instanceof Error ? err.message : String(err));
220
+ }
221
+ }
222
+ else if (process.env.RSC_BUNDLE) {
223
+ try {
224
+ // The built @vitejs/plugin-rsc entry IS the handler — it exports the
225
+ // installHostFn / handleRsc* / handleAction / resolveMetadata contract.
226
+ rscHandler = (await import(process.env.RSC_BUNDLE));
227
+ log("RSC handler loaded");
228
+ }
229
+ catch (err) {
230
+ log("Failed to load RSC handler:", err instanceof Error ? err.message : String(err));
231
+ }
232
+ }
233
+ if (Object.keys(functions).length === 0 && !rscHandler) {
234
+ log("No functions discovered. Provide a functions directory or entry points.");
235
+ process.exit(1);
236
+ }
237
+ /**
238
+ * Handles rsc-stream messages.
239
+ *
240
+ * Writes Flight data frames back on the main listener's socket (same path as
241
+ * SSR responses). The listener's drain handler handles
242
+ * backpressure for large payloads. Runs via setTimeout so writes are not
243
+ * corked by the data handler's async callback buffering.
244
+ */
245
+ async function handleRscStreamMessage(mainSocket, message, draft) {
246
+ if (!rscHandler) {
247
+ writeFrame(mainSocket, '{"error":"RSC not enabled"}');
248
+ return;
249
+ }
250
+ if (!message.component) {
251
+ writeFrame(mainSocket, '{"error":"Missing component in RSC message"}');
252
+ return;
253
+ }
254
+ let cleanupPhp = null;
255
+ try {
256
+ let deferred = null;
257
+ if (message.callbackId) {
258
+ const cbConn = await getCallbackConnection(message.callbackId);
259
+ deferred = createDeferredHost(createPhpFn(cbConn));
260
+ cleanupPhp = rscHandler.installHostFn(deferred.hostFn);
261
+ }
262
+ const metadata = await rscHandler.resolveMetadata(message.component, message.props ?? {}, message.layouts ?? []);
263
+ deferred?.begin();
264
+ const { stream, clientChunks, segmentDepth } = await rscHandler.handleRscStream(message.component, message.props ?? {}, message.layouts ?? [], message.loadings ?? [], message.parallelSlots ?? {}, message.slotOverrides ?? undefined, message.from ?? 0, message.pageKey ?? "");
265
+ const reader = stream.getReader();
266
+ const decoder = new TextDecoder();
267
+ const writeStreamChunk = (value) => {
268
+ const text = typeof value === "string" ? value : decoder.decode(value, { stream: true });
269
+ writeFrame(mainSocket, JSON.stringify({ type: "stream-chunk", data: text }));
270
+ };
271
+ writeFrame(mainSocket, JSON.stringify({ type: "stream-start", clientChunks, metadata, segmentDepth, headers: draft && draftHeaders(draft) }));
272
+ // Flight's root model and the rows for each Suspense fallback go out before
273
+ // any host call is released; see streamWithDeferredRelease.
274
+ await streamWithDeferredRelease(reader, writeStreamChunk, () => deferred?.flush());
275
+ writeFrame(mainSocket, '{"type":"stream-end"}');
276
+ }
277
+ catch (err) {
278
+ const errorJson = failureFrame(err, draft);
279
+ try {
280
+ writeFrame(mainSocket, errorJson);
281
+ }
282
+ catch {
283
+ // Best effort
284
+ }
285
+ }
286
+ finally {
287
+ cleanupPhp?.();
288
+ if (message.callbackId)
289
+ callbackConnections.delete(message.callbackId);
290
+ }
291
+ }
292
+ /**
293
+ * The frame that describes what went wrong, or where to go instead.
294
+ *
295
+ * A render that redirects is not a render that failed, and the host has to
296
+ * tell them apart: one becomes a Location header, the other becomes a 500.
297
+ * Reported before the shell is written whenever the redirect happened above
298
+ * every Suspense boundary, which is what leaves PHP a status line to use.
299
+ */
300
+ function failureFrame(err, draft) {
301
+ if (isRedirectSignal(err)) {
302
+ // A redirect is still an answer, and middleware that sets a cookie before
303
+ // sending someone to the login page means the cookie to survive the trip —
304
+ // remembering where they were going is the usual one.
305
+ return JSON.stringify({
306
+ redirect: err.location,
307
+ redirectStatus: err.status,
308
+ headers: draft && draftHeaders(draft),
309
+ });
310
+ }
311
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
312
+ }
313
+ /**
314
+ * One request's scopes, opened together.
315
+ *
316
+ * The worker has a socket rather than a request, so the host forwards the url
317
+ * and headers on the message and this rebuilds enough of a Request for
318
+ * headers() and cookies() to read. A host that forwards nothing leaves the
319
+ * scope empty, and reading throws — which is the honest answer, since
320
+ * answering with no headers would look like a visitor who sent none.
321
+ *
322
+ * The response draft is open here too, so middleware can set a header or a
323
+ * cookie. It travels on the stream-start frame, which is the last thing sent
324
+ * before the body — PHP has not written its status line yet, so there is still
325
+ * an answer to put them on. Sealed the instant that frame goes out.
326
+ */
327
+ function inRequest(message, run) {
328
+ // The parts, not a rebuilt Request: constructing one drops Cookie, because a
329
+ // Headers built with the request guard refuses the forbidden header names.
330
+ const forwarded = message.url
331
+ ? { url: message.url, headers: message.headers ?? {} }
332
+ : null;
333
+ return withRequest(forwarded, () => withCache(() => withResponseDraft(run)));
334
+ }
335
+ /**
336
+ * The draft's headers as the wire carries them: repeats preserved.
337
+ *
338
+ * Reported once. An error can arrive after stream-start, and reporting again
339
+ * there would set every cookie twice on a host that applies what it is given.
340
+ */
341
+ const reported = new WeakSet();
342
+ function draftHeaders(draft) {
343
+ if (reported.has(draft))
344
+ return undefined;
345
+ reported.add(draft);
346
+ const headers = draft.taken();
347
+ const collected = [];
348
+ headers.forEach((value, name) => {
349
+ // Set-Cookie is handled once below. forEach already visits it per cookie in
350
+ // some runtimes and as one joined value in others, and neither is what goes
351
+ // on the wire — a joined header is one malformed cookie and no session.
352
+ if (name.toLowerCase() !== "set-cookie")
353
+ collected.push([name, value]);
354
+ });
355
+ for (const cookie of headers.getSetCookie())
356
+ collected.push(["Set-Cookie", cookie]);
357
+ draft.seal();
358
+ return collected;
359
+ }
360
+ /**
361
+ * Handles rsc-html-stream messages for initial page loads with Suspense.
362
+ *
363
+ * Writes HTML + Flight payload frames back on the main listener's socket
364
+ * (same path as SSR responses). The drain handler
365
+ * handles backpressure for large payloads.
366
+ */
367
+ async function handleRscHtmlStreamMessage(mainSocket, message, draft) {
368
+ if (!rscHandler) {
369
+ writeFrame(mainSocket, '{"error":"RSC not enabled"}');
370
+ return;
371
+ }
372
+ if (!message.component) {
373
+ writeFrame(mainSocket, '{"error":"Missing component in RSC message"}');
374
+ return;
375
+ }
376
+ let cleanupPhp = null;
377
+ try {
378
+ let deferred = null;
379
+ if (message.callbackId) {
380
+ const cbConn = await getCallbackConnection(message.callbackId);
381
+ deferred = createDeferredHost(createPhpFn(cbConn));
382
+ cleanupPhp = rscHandler.installHostFn(deferred.hostFn);
383
+ }
384
+ const metadata = await rscHandler.resolveMetadata(message.component, message.props ?? {}, message.layouts ?? []);
385
+ deferred?.begin();
386
+ const { htmlStream, rscPayloadPromise, clientChunks } = await rscHandler.handleRscHtmlStream(message.component, message.props ?? {}, message.layouts ?? [], message.loadings ?? [], message.parallelSlots ?? {}, message.slotOverrides ?? undefined, message.nonce ?? undefined, message.pageKey ?? "");
387
+ const reader = htmlStream.getReader();
388
+ const decoder = new TextDecoder();
389
+ const writeHtmlChunk = (value) => {
390
+ const text = typeof value === "string" ? value : decoder.decode(value, { stream: true });
391
+ writeFrame(mainSocket, JSON.stringify({ type: "html-chunk", data: text }));
392
+ };
393
+ // Write html-start and the shell without yielding the event loop
394
+ writeFrame(mainSocket, JSON.stringify({ type: "html-start", clientChunks, metadata }));
395
+ // The whole shell, with every Suspense fallback in it, goes out before any
396
+ // host call is released; see streamWithDeferredRelease.
397
+ await streamWithDeferredRelease(reader, writeHtmlChunk, () => deferred?.flush());
398
+ const rscPayload = await rscPayloadPromise;
399
+ writeFrame(mainSocket, JSON.stringify({ type: "html-end", rscPayload }));
400
+ }
401
+ catch (err) {
402
+ const errorJson = failureFrame(err, draft);
403
+ try {
404
+ writeFrame(mainSocket, errorJson);
405
+ }
406
+ catch {
407
+ // Best effort
408
+ }
409
+ }
410
+ finally {
411
+ cleanupPhp?.();
412
+ if (message.callbackId)
413
+ callbackConnections.delete(message.callbackId);
414
+ }
415
+ }
416
+ /**
417
+ * Handles rsc-action messages (server action calls).
418
+ *
419
+ * Same streaming pattern as handleRscStreamMessage — writes Flight
420
+ * data frames back on the main socket with action-specific frame types.
421
+ */
422
+ async function handleRscActionMessage(mainSocket, message, draft) {
423
+ if (!rscHandler) {
424
+ writeFrame(mainSocket, '{"error":"RSC not enabled"}');
425
+ return;
426
+ }
427
+ if (!message.actionId) {
428
+ writeFrame(mainSocket, '{"error":"Missing actionId in rsc-action message"}');
429
+ return;
430
+ }
431
+ let cleanupPhp = null;
432
+ // Held beyond the block below: what the host marks stale during the action
433
+ // is collected against this connection and read after the action returns.
434
+ let cbConn = null;
435
+ try {
436
+ if (message.callbackId) {
437
+ cbConn = await getCallbackConnection(message.callbackId);
438
+ cleanupPhp = rscHandler.installHostFn(createPhpFn(cbConn));
439
+ }
440
+ // Already bytes when a body arrived on its own frame; there is nothing to
441
+ // decode and nothing to reinterpret.
442
+ const actionBody = message.body ?? "";
443
+ const { stream } = await rscHandler.handleAction(message.actionId, actionBody, message.contentType ?? "text/plain",
444
+ // The page the action was invoked from, resolved by the host — the
445
+ // browser knows the url but not which components render it.
446
+ message.page, () => takeRevalidated(cbConn));
447
+ // An action's whole point may be the cookie it leaves behind, so its
448
+ // start frame carries them the same way a render's does.
449
+ writeFrame(mainSocket, JSON.stringify({ type: "action-start", headers: draft && draftHeaders(draft) }));
450
+ await yieldToEventLoop();
451
+ const reader = stream.getReader();
452
+ const decoder = new TextDecoder();
453
+ while (true) {
454
+ const { done, value } = await reader.read();
455
+ if (done)
456
+ break;
457
+ const text = typeof value === "string"
458
+ ? value
459
+ : decoder.decode(value, { stream: true });
460
+ writeFrame(mainSocket, JSON.stringify({ type: "action-chunk", data: text }));
461
+ await yieldToEventLoop();
462
+ }
463
+ // What the host marked stale while the action ran. Reported on the way
464
+ // out so the answer can carry it; the parts themselves follow once the
465
+ // worker is given the page context to render them against.
466
+ const revalidated = takeRevalidated(cbConn);
467
+ writeFrame(mainSocket, JSON.stringify(revalidated.length > 0 ? { type: "action-end", revalidated } : { type: "action-end" }));
468
+ }
469
+ catch (err) {
470
+ let errorJson;
471
+ if (err instanceof ServerAuthenticationError) {
472
+ errorJson = JSON.stringify({
473
+ unauthenticated: true,
474
+ error: err.message,
475
+ });
476
+ }
477
+ else if (err instanceof ServerAuthorizationError) {
478
+ errorJson = JSON.stringify({
479
+ unauthorized: true,
480
+ error: err.message,
481
+ });
482
+ }
483
+ else if (err instanceof ServerRedirectError) {
484
+ errorJson = JSON.stringify({
485
+ redirect: err.location,
486
+ });
487
+ }
488
+ else if (err instanceof ServerValidationError) {
489
+ errorJson = JSON.stringify({
490
+ error: err.message,
491
+ validation_errors: err.errors,
492
+ });
493
+ }
494
+ else {
495
+ errorJson = JSON.stringify({
496
+ error: err instanceof Error ? err.message : String(err),
497
+ });
498
+ }
499
+ try {
500
+ writeFrame(mainSocket, errorJson);
501
+ }
502
+ catch {
503
+ // Best effort
504
+ }
505
+ }
506
+ finally {
507
+ cleanupPhp?.();
508
+ if (message.callbackId)
509
+ callbackConnections.delete(message.callbackId);
510
+ }
511
+ }
512
+ if (!isTcp) {
513
+ try {
514
+ unlinkSync(socketPath);
515
+ }
516
+ catch {
517
+ // File doesn't exist
518
+ }
519
+ }
520
+ const pendingWriteBuffers = new Map();
521
+ const socketBuffers = new Map();
522
+ function drainSocket(socket) {
523
+ const pending = pendingWriteBuffers.get(socket);
524
+ if (!pending)
525
+ return;
526
+ const written = socket.write(pending);
527
+ if (written < pending.length) {
528
+ pendingWriteBuffers.set(socket, pending.subarray(written));
529
+ }
530
+ else {
531
+ pendingWriteBuffers.delete(socket);
532
+ }
533
+ }
534
+ function writeFrame(socket, json) {
535
+ const payload = Buffer.from(json, "utf-8");
536
+ const header = Buffer.alloc(4);
537
+ header.writeUInt32BE(payload.length, 0);
538
+ const frame = Buffer.concat([header, payload]);
539
+ // If there are pending writes from a previous partial write,
540
+ // queue this frame behind them to maintain frame ordering.
541
+ // Writing directly would interleave with pending data on the wire.
542
+ const existing = pendingWriteBuffers.get(socket);
543
+ if (existing) {
544
+ pendingWriteBuffers.set(socket, Buffer.concat([existing, frame]));
545
+ return;
546
+ }
547
+ const written = socket.write(frame);
548
+ if (written < frame.length) {
549
+ pendingWriteBuffers.set(socket, frame.subarray(written));
550
+ }
551
+ }
552
+ const MAX_FRAME_SIZE = parseInt(process.env.RSC_MAX_FRAME_SIZE || "1048576", 10); // 1MB default
553
+ /**
554
+ * Messages whose body is arriving as the next frame.
555
+ *
556
+ * An upload body is bytes, and frames are bytes, so it travels as its own
557
+ * frame instead of being encoded to fit inside the JSON one. The frame after
558
+ * such a header is that body and must not be parsed as JSON.
559
+ */
560
+ const pendingBody = new Map();
561
+ /**
562
+ * What the host marked stale while an action was running, per callback
563
+ * connection.
564
+ *
565
+ * An action knows what it changed and the page does not, so the marks ride
566
+ * back with each callback's result. Collected here so the answer to the action
567
+ * can carry the re-rendered parts — rather than telling the browser what went
568
+ * stale and waiting for it to ask.
569
+ */
570
+ const revalidations = new Map();
571
+ function markRevalidated(socket, targets) {
572
+ const marked = revalidations.get(socket) ?? new Set();
573
+ for (const target of targets)
574
+ marked.add(target);
575
+ revalidations.set(socket, marked);
576
+ }
577
+ /** Take what a connection collected and forget it. */
578
+ function takeRevalidated(socket) {
579
+ if (!socket)
580
+ return [];
581
+ const marked = revalidations.get(socket);
582
+ revalidations.delete(socket);
583
+ return marked ? [...marked] : [];
584
+ }
585
+ // Create the socket files owner-only. Without this, any local user could
586
+ // connect to the predictable socket path and drive the bridge (invoke server
587
+ // actions / php() callables) with no authenticated session. PHP-FPM must run
588
+ // as the same user as this worker (already implied by sharing the socket).
589
+ try {
590
+ process.umask(0o077);
591
+ }
592
+ catch { }
593
+ /** Restrict a bound Unix socket to owner read/write only. */
594
+ function secureSocket(path) {
595
+ try {
596
+ chmodSync(path, 0o600);
597
+ }
598
+ catch (err) {
599
+ log("Failed to secure socket permissions:", err instanceof Error ? err.message : String(err));
600
+ }
601
+ }
602
+ const server = listen(isTcp ? { hostname: tcpHost, port: mainPort } : { unix: socketPath }, {
603
+ async data(socket, rawData) {
604
+ let buf = socketBuffers.get(socket);
605
+ buf = buf ? Buffer.concat([buf, Buffer.from(rawData)]) : Buffer.from(rawData);
606
+ while (buf.length >= 4) {
607
+ const frameLength = buf.readUInt32BE(0);
608
+ if (frameLength <= 0 || frameLength > MAX_FRAME_SIZE) {
609
+ // Hang up, rather than drop the buffer and read on.
610
+ //
611
+ // Dropping it leaves the connection open, so the next chunk is
612
+ // parsed as a fresh frame from an arbitrary offset inside the
613
+ // payload that was just rejected — and that payload is an action
614
+ // body, which is to say whatever was uploaded. Bytes chosen to look
615
+ // like a length prefix and a JSON message then arrive as a control
616
+ // message on a pooled, long-lived socket: a host function invoked
617
+ // with arguments from inside a file. There is no way to resynchronise
618
+ // a stream whose framing is already lost, so the only correct move
619
+ // is to stop reading it.
620
+ log("Invalid frame length, closing the connection:", frameLength);
621
+ socketBuffers.delete(socket);
622
+ pendingBody.delete(socket);
623
+ socket.end?.();
624
+ return;
625
+ }
626
+ if (buf.length < 4 + frameLength) {
627
+ break;
628
+ }
629
+ const payload = buf.subarray(4, 4 + frameLength);
630
+ buf = buf.subarray(4 + frameLength);
631
+ const awaiting = pendingBody.get(socket);
632
+ if (awaiting) {
633
+ pendingBody.delete(socket);
634
+ // Copied because the read buffer is reused for the frames after it.
635
+ awaiting.body = Buffer.from(payload);
636
+ // Through inRequest, like every other message. Dispatching bare left
637
+ // an action whose body arrived as bytes — which is every file upload
638
+ // — with no request scope, no response draft and no memo table:
639
+ // headers() and cookies() threw, cookies().set() threw, and nothing
640
+ // the action set could reach the response, because the action-start
641
+ // frame carried no headers to put there.
642
+ setTimeout(() => void inRequest(awaiting, (draft) => handleRscActionMessage(socket, awaiting, draft)), 0);
643
+ continue;
644
+ }
645
+ try {
646
+ const message = JSON.parse(payload.toString("utf-8"));
647
+ if (message.bodyEncoding === "binary" && (message.bodyLength ?? 0) > 0) {
648
+ pendingBody.set(socket, message);
649
+ continue;
650
+ }
651
+ if (message.type === "rsc-stream" || message.type === "rsc-html-stream" || message.type === "rsc-action" || message.type === "rsc-ppr-shell") {
652
+ // Run outside the data handler so socket writes are not corked
653
+ // and setTimeout/Promise.race timeouts can fire.
654
+ if (message.type === "rsc-ppr-shell") {
655
+ setTimeout(async () => {
656
+ const response = await inRequest(message, () => handleMessage(message));
657
+ writeFrame(socket, response);
658
+ }, 0);
659
+ }
660
+ else {
661
+ const handler = message.type === "rsc-html-stream"
662
+ ? handleRscHtmlStreamMessage
663
+ : message.type === "rsc-action"
664
+ ? handleRscActionMessage
665
+ : handleRscStreamMessage;
666
+ // One memo table per message, which is one per request: the
667
+ // scope has to be open before the render starts, because that is
668
+ // where a guard and a layout ask the same question.
669
+ setTimeout(() => void inRequest(message, (draft) => handler(socket, message, draft)), 0);
670
+ }
671
+ }
672
+ else {
673
+ const response = await inRequest(message, () => handleMessage(message));
674
+ writeFrame(socket, response);
675
+ }
676
+ }
677
+ catch (err) {
678
+ log("Failed to parse message:", err);
679
+ writeFrame(socket, '{"error":"Invalid JSON"}');
680
+ }
681
+ }
682
+ if (buf.length > 0) {
683
+ socketBuffers.set(socket, buf);
684
+ }
685
+ else {
686
+ socketBuffers.delete(socket);
687
+ }
688
+ },
689
+ drain(socket) {
690
+ drainSocket(socket);
691
+ },
692
+ open() { },
693
+ close(socket) {
694
+ pendingWriteBuffers.delete(socket);
695
+ socketBuffers.delete(socket);
696
+ },
697
+ error(_, err) {
698
+ log("Socket error:", err.message);
699
+ },
700
+ });
701
+ if (!isTcp)
702
+ secureSocket(socketPath);
703
+ log(`Listening on ${isTcp ? `${tcpHost}:${mainPort}` : socketPath}`);
704
+ log(`Discovered ${Object.keys(functions).length} functions: ${Object.keys(functions).join(", ")}`);
705
+ // ─── Persistent Callback Server ─────────────────────────────────────────────
706
+ // PHP connects here for php() callbacks instead of per-request temp sockets.
707
+ // Each connection registers with a callbackId that matches the render request.
708
+ const callbackSocketPath = socketPath + ".cb";
709
+ if (!isTcp) {
710
+ try {
711
+ unlinkSync(callbackSocketPath);
712
+ }
713
+ catch { }
714
+ }
715
+ const callbackConnections = new Map();
716
+ const cbSocketBuffers = new Map();
717
+ const pendingPhpCallbacks = new Map();
718
+ function handleCbResponse(response) {
719
+ const id = response.id;
720
+ const pending = pendingPhpCallbacks.get(id);
721
+ if (!pending)
722
+ return;
723
+ pendingPhpCallbacks.delete(id);
724
+ if (Array.isArray(response.revalidate) && response.revalidate.length > 0) {
725
+ markRevalidated(pending.socket, response.revalidate);
726
+ }
727
+ if (response.unauthenticated) {
728
+ pending.reject(new ServerAuthenticationError(response.error));
729
+ }
730
+ else if (response.unauthorized) {
731
+ pending.reject(new ServerAuthorizationError(response.error));
732
+ }
733
+ else if (response.validation_errors) {
734
+ pending.reject(new ServerValidationError(response.error ?? "Validation failed", response.validation_errors));
735
+ }
736
+ else if (response.redirect) {
737
+ pending.reject(new ServerRedirectError(response.redirect));
738
+ }
739
+ else if (response.error) {
740
+ pending.reject(new Error(response.error));
741
+ }
742
+ else {
743
+ pending.resolve(response.result);
744
+ }
745
+ }
746
+ const cbServer = listen(isTcp ? { hostname: tcpHost, port: cbPort } : { unix: callbackSocketPath }, {
747
+ data(socket, rawData) {
748
+ let buf = cbSocketBuffers.get(socket);
749
+ buf = buf ? Buffer.concat([buf, Buffer.from(rawData)]) : Buffer.from(rawData);
750
+ while (buf.length >= 4) {
751
+ const frameLength = buf.readUInt32BE(0);
752
+ if (frameLength <= 0 || frameLength > MAX_FRAME_SIZE) {
753
+ // Hang up, as on the main listener: framing that cannot be parsed
754
+ // cannot be resynchronised, and reading on from an arbitrary offset
755
+ // turns payload bytes into control messages.
756
+ log("Invalid callback frame length, closing the connection:", frameLength);
757
+ cbSocketBuffers.delete(socket);
758
+ socket.end?.();
759
+ return;
760
+ }
761
+ if (buf.length < 4 + frameLength)
762
+ break;
763
+ const json = buf.subarray(4, 4 + frameLength).toString("utf-8");
764
+ buf = buf.subarray(4 + frameLength);
765
+ try {
766
+ const msg = JSON.parse(json);
767
+ if (msg.type === "register" && msg.id) {
768
+ callbackConnections.set(msg.id, socket);
769
+ }
770
+ else {
771
+ handleCbResponse(msg);
772
+ }
773
+ }
774
+ catch { }
775
+ }
776
+ if (buf.length > 0) {
777
+ cbSocketBuffers.set(socket, buf);
778
+ }
779
+ else {
780
+ cbSocketBuffers.delete(socket);
781
+ }
782
+ },
783
+ open() { },
784
+ close(socket) {
785
+ for (const [id, s] of callbackConnections) {
786
+ if (s === socket)
787
+ callbackConnections.delete(id);
788
+ }
789
+ cbSocketBuffers.delete(socket);
790
+ // Reject any php() callbacks still awaiting a response on this
791
+ // connection. Without this the awaiting render hangs forever and the
792
+ // pending map grows for the life of the worker (a slow whole-app leak
793
+ // when requests are aborted or the PHP side disconnects mid-render).
794
+ for (const [id, pending] of pendingPhpCallbacks) {
795
+ if (pending.socket === socket) {
796
+ pendingPhpCallbacks.delete(id);
797
+ pending.reject(new Error("Callback connection closed before php() responded"));
798
+ }
799
+ }
800
+ },
801
+ drain(socket) { drainSocket(socket); },
802
+ error() { },
803
+ });
804
+ if (!isTcp)
805
+ secureSocket(callbackSocketPath);
806
+ log(`Callback listener on ${isTcp ? `${tcpHost}:${cbPort}` : callbackSocketPath}`);
807
+ let cbIdCounter = 0;
808
+ function getCallbackConnection(id, timeoutMs = 3000) {
809
+ const existing = callbackConnections.get(id);
810
+ if (existing)
811
+ return Promise.resolve(existing);
812
+ return new Promise((resolve, reject) => {
813
+ const check = setInterval(() => {
814
+ const conn = callbackConnections.get(id);
815
+ if (conn) {
816
+ clearInterval(check);
817
+ clearTimeout(timer);
818
+ resolve(conn);
819
+ }
820
+ }, 5);
821
+ const timer = setTimeout(() => {
822
+ clearInterval(check);
823
+ reject(new Error(`Callback ${id} not registered within ${timeoutMs}ms`));
824
+ }, timeoutMs);
825
+ });
826
+ }
827
+ function createPhpFn(cbSocket) {
828
+ return (functionName, ...args) => {
829
+ const id = `cb_${++cbIdCounter}`;
830
+ writeFrame(cbSocket, JSON.stringify({ type: "callback", id, function: functionName, args }));
831
+ return new Promise((resolve, reject) => {
832
+ pendingPhpCallbacks.set(id, { resolve, reject, socket: cbSocket });
833
+ });
834
+ };
835
+ }
836
+ function shutdown(signal) {
837
+ log(`Received ${signal}, shutting down`);
838
+ server.stop();
839
+ cbServer.stop();
840
+ if (!isTcp) {
841
+ try {
842
+ unlinkSync(socketPath);
843
+ }
844
+ catch { }
845
+ try {
846
+ unlinkSync(callbackSocketPath);
847
+ }
848
+ catch { }
849
+ }
850
+ process.exit(0);
851
+ }
852
+ process.on("SIGINT", () => shutdown("SIGINT"));
853
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
854
+ // Prevent unhandled errors from crashing the worker process.
855
+ // These can occur during socket cleanup (e.g., callback socket closed by PHP)
856
+ // or from deferred React rendering microtasks.
857
+ process.on("uncaughtException", (err) => {
858
+ log("Uncaught exception (worker kept alive):", err.message);
859
+ });
860
+ process.on("unhandledRejection", (reason) => {
861
+ log("Unhandled rejection (worker kept alive):", reason instanceof Error ? reason.message : String(reason));
862
+ });
863
+ //# sourceMappingURL=worker.js.map