oxidejs 0.3.1 → 0.3.3
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/README.md +43 -41
- package/client.d.ts +15 -9
- package/dist/client-DOfB6wxk.mjs +207 -0
- package/dist/context-CmChiQ2W.d.mts +98 -0
- package/dist/context-DUN_VWJF.mjs +108 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +73 -52
- package/dist/plugin-BIb3YnN7.mjs +674 -0
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/rpc/client.d.mts +16 -7
- package/dist/rpc/client.mjs +1 -1
- package/dist/{rpc-DzUkWWgQ.mjs → rpc-gCOSJXql.mjs} +359 -325
- package/dist/rpc.d.mts +42 -33
- package/dist/rpc.mjs +2 -2
- package/dist/rsbuild.d.mts +1 -1
- package/dist/rsbuild.mjs +1 -1
- package/dist/{types-BM4NAnzy.d.mts → types-BaZeMYcP.d.mts} +22 -8
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +1 -1
- package/dist/worker-dom.d.mts +1 -1
- package/dist/worker-dom.mjs +15 -8
- package/package.json +2 -2
- package/virtual.d.ts +16 -3
- package/dist/client-C-s6XXpS.mjs +0 -148
- package/dist/context-C1UFQ0Zc.d.mts +0 -64
- package/dist/context-zrTZyYpF.mjs +0 -42
- package/dist/plugin-HZKRDuCS.mjs +0 -602
|
@@ -1,48 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as withRequestEntry, r as runWithRequest } from "./context-DUN_VWJF.mjs";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Layer } from "effect";
|
|
5
|
-
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
6
5
|
import { HttpRouter } from "effect/unstable/http";
|
|
7
|
-
|
|
8
|
-
var __defProp = Object.defineProperty;
|
|
9
|
-
var __exportAll = (all, no_symbols) => {
|
|
10
|
-
let target = {};
|
|
11
|
-
for (var name in all) __defProp(target, name, {
|
|
12
|
-
get: all[name],
|
|
13
|
-
enumerable: true
|
|
14
|
-
});
|
|
15
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
-
return target;
|
|
17
|
-
};
|
|
18
|
-
//#endregion
|
|
6
|
+
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
19
7
|
//#region src/actions.ts
|
|
20
|
-
var actions_exports = /* @__PURE__ */ __exportAll({
|
|
21
|
-
ACTION_PATH: () => ACTION_PATH,
|
|
22
|
-
RESOLVED_VIRTUAL_ACTIONS_ID: () => RESOLVED_VIRTUAL_ACTIONS_ID,
|
|
23
|
-
RESOLVED_VIRTUAL_CLIENT_ID: () => RESOLVED_VIRTUAL_CLIENT_ID,
|
|
24
|
-
RESOLVED_VIRTUAL_WORKER_ID: () => RESOLVED_VIRTUAL_WORKER_ID,
|
|
25
|
-
RequestBodyTooLargeError: () => RequestBodyTooLargeError,
|
|
26
|
-
VIRTUAL_ACTIONS_ID: () => VIRTUAL_ACTIONS_ID,
|
|
27
|
-
VIRTUAL_CLIENT_ID: () => VIRTUAL_CLIENT_ID,
|
|
28
|
-
VIRTUAL_WORKER_ID: () => VIRTUAL_WORKER_ID,
|
|
29
|
-
generateActionsClientModule: () => generateActionsClientModule,
|
|
30
|
-
generateActionsModule: () => generateActionsModule,
|
|
31
|
-
generateClientModule: () => generateClientModule,
|
|
32
|
-
generateClientStub: () => generateClientStub,
|
|
33
|
-
generateWorkerWrapper: () => generateWorkerWrapper,
|
|
34
|
-
isServerFileId: () => isServerFileId,
|
|
35
|
-
loadClientStub: () => loadClientStub,
|
|
36
|
-
matchesActionPath: () => matchesActionPath,
|
|
37
|
-
moduleKey: () => moduleKey,
|
|
38
|
-
nodeToWebRequest: () => nodeToWebRequest,
|
|
39
|
-
parseExportedNames: () => parseExportedNames,
|
|
40
|
-
parseStreamExports: () => parseStreamExports,
|
|
41
|
-
pluginShouldStub: () => pluginShouldStub,
|
|
42
|
-
scanServerFiles: () => scanServerFiles,
|
|
43
|
-
sendWebResponseFrom: () => sendWebResponseFrom,
|
|
44
|
-
shouldStubServerModule: () => shouldStubServerModule
|
|
45
|
-
});
|
|
46
8
|
const VIRTUAL_ACTIONS_ID = "virtual:oxide/actions";
|
|
47
9
|
const RESOLVED_VIRTUAL_ACTIONS_ID = `\0${VIRTUAL_ACTIONS_ID}`;
|
|
48
10
|
const VIRTUAL_WORKER_ID = "virtual:oxide/worker";
|
|
@@ -51,9 +13,9 @@ const VIRTUAL_CLIENT_ID = "virtual:oxide/client";
|
|
|
51
13
|
const RESOLVED_VIRTUAL_CLIENT_ID = `\0${VIRTUAL_CLIENT_ID}`;
|
|
52
14
|
const ACTION_PATH = "/__oxide/action";
|
|
53
15
|
/** Match the action endpoint with or without a trailing slash (Effect RPC posts to `path/`). */
|
|
54
|
-
function matchesActionPath(pathname,
|
|
55
|
-
return pathname ===
|
|
56
|
-
}
|
|
16
|
+
const matchesActionPath = function matchesActionPath(pathname, actionPath = ACTION_PATH) {
|
|
17
|
+
return pathname === actionPath || pathname === `${actionPath}/`;
|
|
18
|
+
};
|
|
57
19
|
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
58
20
|
"node_modules",
|
|
59
21
|
"dist",
|
|
@@ -61,37 +23,37 @@ const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
|
61
23
|
".wrangler"
|
|
62
24
|
]);
|
|
63
25
|
/** Only `export const name = action(...)` become remote RPC actions. Everything else stays server-local. */
|
|
64
|
-
const EXPORT_RE = /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?action\s*\(/
|
|
65
|
-
const STREAM_EXPORT_RE = /^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*action\s*\(\s*async\s+function\s*\*/
|
|
66
|
-
function isServerFileId(id) {
|
|
67
|
-
const file = id.split("?")[0]?.
|
|
26
|
+
const EXPORT_RE = /^\s*export\s+const\s+(?<exportName>[A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?action\s*\(/gmu;
|
|
27
|
+
const STREAM_EXPORT_RE = /^\s*export\s+const\s+(?<exportName>[A-Za-z_$][\w$]*)\s*=\s*action\s*\(\s*async\s+function\s*\*/gmu;
|
|
28
|
+
const isServerFileId = function isServerFileId(id) {
|
|
29
|
+
const file = id.split("?")[0]?.replaceAll("\\", "/") ?? "";
|
|
68
30
|
return [
|
|
69
31
|
".ts",
|
|
70
32
|
".tsx",
|
|
71
33
|
".js",
|
|
72
34
|
".jsx"
|
|
73
35
|
].some((ext) => file.endsWith(`.server${ext}`));
|
|
74
|
-
}
|
|
75
|
-
function moduleKey(absFile) {
|
|
76
|
-
return path.basename(absFile).replace(/\.server\.(?:[jt]sx?)$/
|
|
77
|
-
}
|
|
78
|
-
function parseExportedNames(source) {
|
|
36
|
+
};
|
|
37
|
+
const moduleKey = function moduleKey(absFile) {
|
|
38
|
+
return path.basename(absFile).replace(/\.server\.(?:[jt]sx?)$/iu, "");
|
|
39
|
+
};
|
|
40
|
+
const parseExportedNames = function parseExportedNames(source) {
|
|
79
41
|
const names = /* @__PURE__ */ new Set();
|
|
80
42
|
for (const match of source.matchAll(EXPORT_RE)) {
|
|
81
|
-
const name = match
|
|
43
|
+
const [, name] = match;
|
|
82
44
|
if (name) names.add(name);
|
|
83
45
|
}
|
|
84
46
|
return [...names];
|
|
85
|
-
}
|
|
86
|
-
function parseStreamExports(source) {
|
|
47
|
+
};
|
|
48
|
+
const parseStreamExports = function parseStreamExports(source) {
|
|
87
49
|
const names = /* @__PURE__ */ new Set();
|
|
88
50
|
for (const match of source.matchAll(STREAM_EXPORT_RE)) {
|
|
89
|
-
const name = match
|
|
51
|
+
const [, name] = match;
|
|
90
52
|
if (name) names.add(name);
|
|
91
53
|
}
|
|
92
54
|
return [...names];
|
|
93
|
-
}
|
|
94
|
-
function scanServerFiles(root) {
|
|
55
|
+
};
|
|
56
|
+
const scanServerFiles = function scanServerFiles(root) {
|
|
95
57
|
const files = [];
|
|
96
58
|
const walk = (dir) => {
|
|
97
59
|
let entries;
|
|
@@ -114,39 +76,40 @@ function scanServerFiles(root) {
|
|
|
114
76
|
walk(root);
|
|
115
77
|
const byKey = /* @__PURE__ */ new Map();
|
|
116
78
|
const modules = [];
|
|
117
|
-
|
|
79
|
+
const sortedFiles = files.toSorted();
|
|
80
|
+
for (const abs of sortedFiles) {
|
|
118
81
|
const key = moduleKey(abs);
|
|
119
82
|
if (!key) throw new Error(`oxidejs: invalid server module name: ${abs}`);
|
|
120
83
|
const existing = byKey.get(key);
|
|
121
84
|
if (existing) throw new Error(`oxidejs: duplicate server module key "${key}": ${existing} and ${abs}`);
|
|
122
85
|
byKey.set(key, abs);
|
|
123
|
-
const source = fs.readFileSync(abs, "
|
|
86
|
+
const source = fs.readFileSync(abs, "utf-8");
|
|
124
87
|
modules.push({
|
|
125
88
|
abs,
|
|
126
|
-
key,
|
|
127
89
|
exports: parseExportedNames(source),
|
|
90
|
+
key,
|
|
128
91
|
streams: parseStreamExports(source)
|
|
129
92
|
});
|
|
130
93
|
}
|
|
131
94
|
return modules;
|
|
132
|
-
}
|
|
133
|
-
function generateClientModule(transport = "http", headers,
|
|
134
|
-
const opts = { url:
|
|
95
|
+
};
|
|
96
|
+
const generateClientModule = function generateClientModule(transport = "http", headers, actionPath = ACTION_PATH) {
|
|
97
|
+
const opts = { url: actionPath };
|
|
135
98
|
if (transport === "ws") opts.transport = "ws";
|
|
136
99
|
if (headers) opts.headers = headers;
|
|
137
100
|
if (transport === "ws") return `import { createClient } from "oxidejs/rpc/client";
|
|
138
101
|
import { actionsGroup } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
139
102
|
const __proto = typeof location === "undefined" ? "ws:" : location.protocol === "https:" ? "wss:" : "ws:";
|
|
140
103
|
const __host = typeof location === "undefined" ? "localhost" : location.host;
|
|
141
|
-
export const client = createClient(actionsGroup, { ...${JSON.stringify(opts)}, url: __proto + "//" + __host + ${JSON.stringify(
|
|
104
|
+
export const client = createClient(actionsGroup, { ...${JSON.stringify(opts)}, url: __proto + "//" + __host + ${JSON.stringify(actionPath)} });
|
|
142
105
|
`;
|
|
143
106
|
return `import { createClient } from "oxidejs/rpc/client";
|
|
144
107
|
import { actionsGroup } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
145
108
|
export const client = createClient(actionsGroup, ${JSON.stringify(opts)});
|
|
146
109
|
`;
|
|
147
|
-
}
|
|
148
|
-
function generateClientStub(mod) {
|
|
149
|
-
const streams = new Set(mod.streams
|
|
110
|
+
};
|
|
111
|
+
const generateClientStub = function generateClientStub(mod) {
|
|
112
|
+
const streams = new Set(mod.streams);
|
|
150
113
|
const lines = [
|
|
151
114
|
`// oxidejs:client-stub`,
|
|
152
115
|
`import { wrapClientRpc, wrapClientStreamRpc } from "oxidejs";`,
|
|
@@ -165,40 +128,30 @@ function generateClientStub(mod) {
|
|
|
165
128
|
else lines.push(`export const ${name} = wrapClientRpc(${peel});`);
|
|
166
129
|
}
|
|
167
130
|
return `${lines.join("\n")}\n`;
|
|
168
|
-
}
|
|
169
|
-
function generateActionsClientModule(modules) {
|
|
131
|
+
};
|
|
132
|
+
const generateActionsClientModule = function generateActionsClientModule(modules) {
|
|
170
133
|
const lines = [`import { Schema } from "effect";`, `import { Rpc, RpcGroup } from "effect/unstable/rpc";`];
|
|
171
134
|
const rpcNames = [];
|
|
172
|
-
modules.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")});`);
|
|
182
|
-
lines.push(`export default actionsGroup;`);
|
|
183
|
-
lines.push(`export { actionsGroup as actions };`);
|
|
135
|
+
for (const [i, mod] of modules.entries()) for (const name of mod.exports) {
|
|
136
|
+
const rpc = `__rpc_${i}_${name}`;
|
|
137
|
+
rpcNames.push(rpc);
|
|
138
|
+
const tag = `${mod.key}.${name}`;
|
|
139
|
+
const stream = mod.streams?.includes(name) ?? false;
|
|
140
|
+
lines.push(`const ${rpc} = Rpc.make(${JSON.stringify(tag)}, { payload: Schema.Struct({ args: Schema.Array(Schema.Unknown) }), success: Schema.Unknown${stream ? ", stream: true" : ""} });`);
|
|
141
|
+
}
|
|
142
|
+
lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")});`, `export default actionsGroup;`, `export { actionsGroup as actions };`);
|
|
184
143
|
return `${lines.join("\n")}\n`;
|
|
185
|
-
}
|
|
186
|
-
function generateActionsModule(modules, opts) {
|
|
144
|
+
};
|
|
145
|
+
const generateActionsModule = function generateActionsModule(modules, opts) {
|
|
187
146
|
const lines = [
|
|
188
147
|
`import { Effect } from "effect";`,
|
|
189
148
|
`import { Schema } from "effect";`,
|
|
190
149
|
`import { Rpc, RpcGroup } from "effect/unstable/rpc";`,
|
|
191
|
-
`import {
|
|
150
|
+
`import { getRequestStore, withRequestStore } from "oxidejs";`,
|
|
192
151
|
`import { asyncGenToStreamInContext } from "oxidejs/rpc";`,
|
|
193
|
-
`const
|
|
194
|
-
`const
|
|
195
|
-
`
|
|
196
|
-
` const ctx = __als.getStore();`,
|
|
197
|
-
` if (!ctx) throw new Error("oxidejs: request context is unavailable");`,
|
|
198
|
-
` return ctx;`,
|
|
199
|
-
`};`,
|
|
200
|
-
`const __run = (fn) =>`,
|
|
201
|
-
` Effect.promise(() => __als.run(__store(), fn)).pipe(`,
|
|
152
|
+
`const __run = (fn) => {`,
|
|
153
|
+
` const __s = getRequestStore();`,
|
|
154
|
+
` return Effect.promise(() => withRequestStore(__s, fn)).pipe(`,
|
|
202
155
|
` Effect.map((value) => {`,
|
|
203
156
|
` if (value instanceof Response) {`,
|
|
204
157
|
` console.error("oxidejs: action() returned a Response; actions must return serializable data. Return a Response from src/server.ts for raw HTTP responses.");`,
|
|
@@ -207,7 +160,8 @@ function generateActionsModule(modules, opts) {
|
|
|
207
160
|
` return value === undefined ? null : value;`,
|
|
208
161
|
` }),`,
|
|
209
162
|
` );`,
|
|
210
|
-
`
|
|
163
|
+
`};`,
|
|
164
|
+
`const __withStore = (store, fn) => withRequestStore(store, fn);`
|
|
211
165
|
];
|
|
212
166
|
const rpcNames = [];
|
|
213
167
|
const aliases = modules.map((mod, i) => {
|
|
@@ -226,60 +180,47 @@ function generateActionsModule(modules, opts) {
|
|
|
226
180
|
mod
|
|
227
181
|
};
|
|
228
182
|
});
|
|
229
|
-
lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")})
|
|
230
|
-
lines.push(`export const actionsHandlers = actionsGroup.toLayer({`);
|
|
183
|
+
lines.push(`export const actionsGroup = RpcGroup.make(${rpcNames.join(", ")});`, `export const actionsHandlers = actionsGroup.toLayer({`);
|
|
231
184
|
for (const { alias, mod } of aliases) for (const name of mod.exports) {
|
|
232
185
|
const tag = `${mod.key}.${name}`;
|
|
233
186
|
const stream = mod.streams?.includes(name) ?? false;
|
|
234
|
-
lines.push(stream ? ` ${JSON.stringify(tag)}: ({ args }) => { const __s =
|
|
187
|
+
lines.push(stream ? ` ${JSON.stringify(tag)}: ({ args }) => { const __s = getRequestStore(); return asyncGenToStreamInContext(() => ${alias}[${JSON.stringify(name)}].apply(null, args), (fn) => __withStore(__s, fn)); },` : ` ${JSON.stringify(tag)}: ({ args }) => __run(() => ${alias}[${JSON.stringify(name)}].apply(null, args)),`);
|
|
235
188
|
}
|
|
236
|
-
lines.push(`});`);
|
|
237
|
-
lines.push(`export default actionsGroup;`);
|
|
238
|
-
lines.push(`export { actionsGroup as actions };`);
|
|
189
|
+
lines.push(`});`, `export default actionsGroup;`, `export { actionsGroup as actions };`);
|
|
239
190
|
return `${lines.join("\n")}\n`;
|
|
240
|
-
}
|
|
241
|
-
function pipeResponse(req, res, response) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
191
|
+
};
|
|
192
|
+
const pipeResponse = async function pipeResponse(req, res, response) {
|
|
193
|
+
res.statusCode = response.status;
|
|
194
|
+
for (const [key, value] of response.headers) res.setHeader(key, value);
|
|
195
|
+
if (!response.body) {
|
|
196
|
+
res.end();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const reader = response.body.getReader();
|
|
200
|
+
const abort = () => {
|
|
201
|
+
reader.cancel();
|
|
202
|
+
};
|
|
203
|
+
req.once("aborted", abort);
|
|
204
|
+
try {
|
|
205
|
+
for (;;) {
|
|
206
|
+
const { done, value } = await reader.read();
|
|
207
|
+
if (done) {
|
|
208
|
+
res.end();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (value) res.write(value);
|
|
251
212
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
if (value) res.write(value);
|
|
266
|
-
pull();
|
|
267
|
-
}, reject);
|
|
268
|
-
};
|
|
269
|
-
pull();
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
function generateWorkerWrapper(userWorkerAbs, opts = {}) {
|
|
273
|
-
const preset = opts.preset ?? "fetch";
|
|
274
|
-
const clientDir = opts.clientDir ?? "client";
|
|
275
|
-
const actionPath = opts.actionPath ?? "/__oxide/action";
|
|
276
|
-
const sameOrigin = opts.actionSameOrigin ?? false;
|
|
277
|
-
const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
|
|
278
|
-
const hasActions = opts.hasActions !== false;
|
|
279
|
-
const ws = hasActions && opts.actions === "ws";
|
|
280
|
-
const bodyLimit = opts.bodyLimit ?? 1048576;
|
|
281
|
-
const nfBlock = `const __nf = ${`() => new Response(${JSON.stringify(opts.notFound ?? "<h1>404 Not Found</h1>")}, { status: 404, headers: { "content-type": "text/html; charset=utf-8" } })`};\n`;
|
|
282
|
-
const assetBlock = serveAssets ? `import { readFile } from "node:fs/promises";
|
|
213
|
+
} finally {
|
|
214
|
+
req.off("aborted", abort);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const isMiddlewareSpecifier = function isMiddlewareSpecifier(entry) {
|
|
218
|
+
return typeof entry === "string";
|
|
219
|
+
};
|
|
220
|
+
const EMPTY_MIDDLEWARE_IMPORTS = [];
|
|
221
|
+
const buildAssetBlock = function buildAssetBlock(serveAssets, clientDir) {
|
|
222
|
+
if (!serveAssets) return "";
|
|
223
|
+
return `import { readFile } from "node:fs/promises";
|
|
283
224
|
import { extname, join } from "node:path";
|
|
284
225
|
const __assets = join(import.meta.dirname, ${JSON.stringify(clientDir)});
|
|
285
226
|
const __types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon", ".woff2": "font/woff2", ".webp": "image/webp" };
|
|
@@ -321,23 +262,29 @@ async function __asset(request, spa) {
|
|
|
321
262
|
return;
|
|
322
263
|
}
|
|
323
264
|
}
|
|
324
|
-
|
|
325
|
-
|
|
265
|
+
`;
|
|
266
|
+
};
|
|
267
|
+
const buildAfterAction = function buildAfterAction(serveAssets, preset, envJson) {
|
|
326
268
|
const celldAfterAction = `{
|
|
327
|
-
const hit =
|
|
269
|
+
const hit = __userFetch ? await __userFetch(request, env ?? ${envJson}, ctx) : undefined;
|
|
328
270
|
if (hit) return hit;
|
|
329
271
|
const assets = env?.ASSETS;
|
|
330
272
|
if (assets && typeof assets.fetch === "function") return assets.fetch(request);
|
|
331
273
|
return __nf();
|
|
332
274
|
}`;
|
|
333
|
-
|
|
334
|
-
const hit = await
|
|
275
|
+
if (serveAssets) return `if (__userFetch) {
|
|
276
|
+
const hit = await __userFetch(request, env ?? ${envJson}, ctx);
|
|
335
277
|
if (hit) return hit;
|
|
336
278
|
}
|
|
337
|
-
return (await __asset(request)) ?? (__nav(request) ? await __asset(request, true) : undefined) ?? __nf()
|
|
338
|
-
|
|
279
|
+
return (await __asset(request)) ?? (__nav(request) ? await __asset(request, true) : undefined) ?? __nf();`;
|
|
280
|
+
if (preset === "celld") return celldAfterAction;
|
|
281
|
+
return `return __userFetch
|
|
282
|
+
? __userFetch(request, env, ctx)
|
|
339
283
|
: __nf();`;
|
|
340
|
-
|
|
284
|
+
};
|
|
285
|
+
const buildListenBlock = function buildListenBlock(preset, bodyLimit, ws) {
|
|
286
|
+
if (preset !== "fetch") return "";
|
|
287
|
+
return `
|
|
341
288
|
import { createServer } from "node:http";
|
|
342
289
|
import { pathToFileURL } from "node:url";
|
|
343
290
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
@@ -383,42 +330,92 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
383
330
|
});
|
|
384
331
|
});` : ""}
|
|
385
332
|
server.listen(port, () => console.log(\`oxidejs listening on \${port}\`));
|
|
386
|
-
}
|
|
387
333
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
388
334
|
process.on(signal, () => {
|
|
389
335
|
server.close(() => process.exit(0));
|
|
390
336
|
setTimeout(() => process.exit(0), 5000).unref();
|
|
391
337
|
});
|
|
392
338
|
}
|
|
393
|
-
|
|
394
|
-
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
};
|
|
342
|
+
const buildActionImports = function buildActionImports(hasActions, ws, actionPath, sameOrigin) {
|
|
343
|
+
if (!hasActions) return "";
|
|
344
|
+
if (ws) return `import { createWsHooks } from "oxidejs/rpc";
|
|
395
345
|
import { actionsGroup, actionsHandlers } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
396
346
|
const __ws = createWsHooks(actionsGroup, actionsHandlers, { path: ${JSON.stringify(actionPath)}, sameOrigin: ${sameOrigin} });
|
|
397
|
-
|
|
347
|
+
`;
|
|
348
|
+
return `import { createActionHandler } from "oxidejs/rpc";
|
|
398
349
|
import { actionsGroup, actionsHandlers } from ${JSON.stringify(VIRTUAL_ACTIONS_ID)};
|
|
399
350
|
const __fetch = Symbol.for("oxidejs.fetch");
|
|
400
351
|
const __rpc = createActionHandler(actionsGroup, actionsHandlers, { path: ${JSON.stringify(actionPath)}, sameOrigin: ${sameOrigin}, createContext: (req) => req[__fetch] ?? {} });
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
352
|
+
`;
|
|
353
|
+
};
|
|
354
|
+
const normalizeMiddlewareEntries = function normalizeMiddlewareEntries(middleware) {
|
|
355
|
+
const ILHA_SSR_IMPLICIT = ["ilha:pages/server", "ilha:loaders"];
|
|
356
|
+
return middleware.map((m) => {
|
|
357
|
+
if (isMiddlewareSpecifier(m)) return {
|
|
358
|
+
imports: m === "@ilha/router/ssr" ? ILHA_SSR_IMPLICIT : EMPTY_MIDDLEWARE_IMPORTS,
|
|
359
|
+
module: m
|
|
360
|
+
};
|
|
361
|
+
return m;
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
const buildMiddlewareImports = function buildMiddlewareImports(mwEntries) {
|
|
365
|
+
return `${mwEntries.map((m, i) => {
|
|
366
|
+
return `${(m.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n")}\nimport __mw${i} from ${JSON.stringify(m.module)};`;
|
|
367
|
+
}).join("\n")}\n`;
|
|
368
|
+
};
|
|
369
|
+
const buildActionGate = function buildActionGate(hasActions, ws) {
|
|
370
|
+
if (!(hasActions && !ws)) return "";
|
|
371
|
+
return `if ((__actionMatch)(new URL(request.url).pathname)) {
|
|
404
372
|
return __rpc(request);
|
|
405
373
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
} : m).map((m, i) => (m.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n") + `\nimport __mw${i} from ${JSON.stringify(m.module)};`).join("\n") + "\n";
|
|
412
|
-
const middlewareList = (opts.middleware ?? []).map((_, i) => `__mw${i}`).join(", ");
|
|
413
|
-
const middlewareGate = opts.middleware?.length ? `for (const __mw of [${middlewareList}]) {
|
|
374
|
+
`;
|
|
375
|
+
};
|
|
376
|
+
const buildMiddlewareGate = function buildMiddlewareGate(middleware) {
|
|
377
|
+
if (!middleware?.length) return "";
|
|
378
|
+
return `for (const __mw of [${middleware.map((_, i) => `__mw${i}`).join(", ")}]) {
|
|
414
379
|
const hit = await __mw(request, { env, ctx });
|
|
415
380
|
if (hit) return hit;
|
|
416
381
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
382
|
+
`;
|
|
383
|
+
};
|
|
384
|
+
const buildCelldDomBlock = function buildCelldDomBlock(preset) {
|
|
385
|
+
if (preset !== "celld") return "";
|
|
386
|
+
return `import "oxidejs/worker-dom/install";\n`;
|
|
387
|
+
};
|
|
388
|
+
const generateWorkerWrapper = function generateWorkerWrapper(userWorkerAbs, opts = {}) {
|
|
389
|
+
const preset = opts.preset ?? "fetch";
|
|
390
|
+
const clientDir = opts.clientDir ?? "client";
|
|
391
|
+
const actionPath = opts.actionPath ?? "/__oxide/action";
|
|
392
|
+
const sameOrigin = opts.actionSameOrigin ?? false;
|
|
393
|
+
const serveAssets = preset === "fetch" && (opts.hasClient === true || opts.hasPublic === true);
|
|
394
|
+
const hasActions = opts.hasActions !== false;
|
|
395
|
+
const ws = hasActions && opts.actions === "ws";
|
|
396
|
+
const bodyLimit = opts.bodyLimit ?? 1048576;
|
|
397
|
+
const nfBlock = `const __nf = ${`() => new Response(${JSON.stringify(opts.notFound ?? "<h1>404 Not Found</h1>")}, { status: 404, headers: { "content-type": "text/html; charset=utf-8" } })`};\n`;
|
|
398
|
+
const assetBlock = buildAssetBlock(serveAssets, clientDir);
|
|
399
|
+
const envJson = JSON.stringify(opts.env ?? {});
|
|
400
|
+
const afterAction = buildAfterAction(serveAssets, preset, envJson);
|
|
401
|
+
const listen = buildListenBlock(preset, bodyLimit, ws);
|
|
402
|
+
const actionImports = buildActionImports(hasActions, ws, actionPath, sameOrigin);
|
|
403
|
+
const actionMatchFn = `const __actionMatch = (p) => p === ${JSON.stringify(actionPath)} || p === ${JSON.stringify(`${actionPath}/`)};`;
|
|
404
|
+
const actionGate = buildActionGate(hasActions, ws);
|
|
405
|
+
const mwEntries = normalizeMiddlewareEntries(opts.middleware ?? []);
|
|
406
|
+
const middlewareImports = buildMiddlewareImports(mwEntries);
|
|
407
|
+
const middlewareGate = buildMiddlewareGate(opts.middleware);
|
|
408
|
+
return `${(opts.imports ?? []).map((spec) => `import ${JSON.stringify(spec)};`).join("\n")}${buildCelldDomBlock(preset)}export * from ${JSON.stringify(userWorkerAbs)};
|
|
409
|
+
import * as __userMod from ${JSON.stringify(userWorkerAbs)};
|
|
410
|
+
const user = __userMod.default;
|
|
411
|
+
const __userFetch =
|
|
412
|
+
user != null && typeof user.fetch === "function"
|
|
413
|
+
? user.fetch.bind(user)
|
|
414
|
+
: typeof __userMod.fetch === "function"
|
|
415
|
+
? __userMod.fetch
|
|
416
|
+
: undefined;
|
|
420
417
|
${middlewareImports}${actionImports}${hasActions ? `${actionMatchFn}\n` : ""}${assetBlock}${nfBlock}const app = {
|
|
421
|
-
...user,
|
|
418
|
+
...(user ?? {}),
|
|
422
419
|
async fetch(request, env, ctx) {
|
|
423
420
|
request[__fetch] = { env, fetchCtx: ctx };
|
|
424
421
|
${middlewareGate}${actionGate}${afterAction}
|
|
@@ -426,7 +423,7 @@ ${middlewareImports}${actionImports}${hasActions ? `${actionMatchFn}\n` : ""}${a
|
|
|
426
423
|
};
|
|
427
424
|
export default app;
|
|
428
425
|
${listen}`;
|
|
429
|
-
}
|
|
426
|
+
};
|
|
430
427
|
const SERVER_TARGETS = /* @__PURE__ */ new Set([
|
|
431
428
|
"node",
|
|
432
429
|
"async-node",
|
|
@@ -442,7 +439,7 @@ const SERVER_NAMES = /* @__PURE__ */ new Set([
|
|
|
442
439
|
]);
|
|
443
440
|
const CLIENT_NAMES = /* @__PURE__ */ new Set(["client", "web"]);
|
|
444
441
|
/** Stub unless the graph is a known server. Unknown graphs stub so *.server.ts never ships. */
|
|
445
|
-
function shouldStubServerModule(environment, extra) {
|
|
442
|
+
const shouldStubServerModule = function shouldStubServerModule(environment, extra) {
|
|
446
443
|
if (extra?.ssr) return false;
|
|
447
444
|
const consumer = environment?.config?.consumer ?? environment?.consumer;
|
|
448
445
|
if (consumer === "server") return false;
|
|
@@ -450,12 +447,12 @@ function shouldStubServerModule(environment, extra) {
|
|
|
450
447
|
const name = environment?.name;
|
|
451
448
|
if (name && CLIENT_NAMES.has(name)) return true;
|
|
452
449
|
if (name && SERVER_NAMES.has(name)) return false;
|
|
453
|
-
const targets = extra?.target
|
|
450
|
+
const targets = extra?.target === void 0 || extra.target === null ? [] : [extra.target].flat();
|
|
454
451
|
if (targets.some((target) => SERVER_TARGETS.has(target))) return false;
|
|
455
452
|
if (targets.some((target) => CLIENT_TARGETS.has(target))) return true;
|
|
456
453
|
return true;
|
|
457
|
-
}
|
|
458
|
-
function pluginShouldStub(pluginThis, options) {
|
|
454
|
+
};
|
|
455
|
+
const pluginShouldStub = function pluginShouldStub(pluginThis, options) {
|
|
459
456
|
const ctx = pluginThis;
|
|
460
457
|
const compiler = ctx.getNativeBuildContext?.()?.compiler;
|
|
461
458
|
const env = {};
|
|
@@ -467,18 +464,23 @@ function pluginShouldStub(pluginThis, options) {
|
|
|
467
464
|
if (options?.ssr) extra.ssr = true;
|
|
468
465
|
if (compiler?.options?.target) extra.target = compiler.options.target;
|
|
469
466
|
return shouldStubServerModule(env, extra);
|
|
470
|
-
}
|
|
471
|
-
function loadClientStub(id) {
|
|
467
|
+
};
|
|
468
|
+
const loadClientStub = function loadClientStub(id) {
|
|
472
469
|
const file = id.split("?")[0] ?? id;
|
|
473
|
-
const source = fs.readFileSync(file, "
|
|
470
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
474
471
|
return generateClientStub({
|
|
475
|
-
key: moduleKey(file),
|
|
476
472
|
exports: parseExportedNames(source),
|
|
473
|
+
key: moduleKey(file),
|
|
477
474
|
streams: parseStreamExports(source)
|
|
478
475
|
});
|
|
479
|
-
}
|
|
480
|
-
var RequestBodyTooLargeError = class extends Error {
|
|
481
|
-
|
|
476
|
+
};
|
|
477
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
478
|
+
constructor(message) {
|
|
479
|
+
super(message);
|
|
480
|
+
this.name = "RequestBodyTooLargeError";
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const nodeToWebRequest = async function nodeToWebRequest(req, maxBytes = Number.POSITIVE_INFINITY) {
|
|
482
484
|
const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
|
483
485
|
const headers = new Headers();
|
|
484
486
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
@@ -490,15 +492,15 @@ async function nodeToWebRequest(req, maxBytes = Number.POSITIVE_INFINITY) {
|
|
|
490
492
|
req.once("aborted", () => ac.abort());
|
|
491
493
|
const method = req.method ?? "GET";
|
|
492
494
|
const init = {
|
|
493
|
-
method,
|
|
494
495
|
headers,
|
|
496
|
+
method,
|
|
495
497
|
signal: ac.signal
|
|
496
498
|
};
|
|
497
499
|
if (method === "GET" || method === "HEAD") return new Request(url, init);
|
|
498
500
|
const chunks = [];
|
|
499
501
|
let size = 0;
|
|
500
502
|
for await (const chunk of req) {
|
|
501
|
-
const buffer =
|
|
503
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
502
504
|
size += buffer.length;
|
|
503
505
|
if (size > maxBytes) throw new RequestBodyTooLargeError();
|
|
504
506
|
chunks.push(buffer);
|
|
@@ -506,13 +508,13 @@ async function nodeToWebRequest(req, maxBytes = Number.POSITIVE_INFINITY) {
|
|
|
506
508
|
const body = Buffer.concat(chunks);
|
|
507
509
|
if (body.length > 0) init.body = body;
|
|
508
510
|
return new Request(url, init);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
+
};
|
|
512
|
+
const sendWebResponseFrom = function sendWebResponseFrom(req, res, response) {
|
|
511
513
|
return pipeResponse(req, res, response);
|
|
512
|
-
}
|
|
514
|
+
};
|
|
513
515
|
//#endregion
|
|
514
516
|
//#region src/rpc/same-origin.ts
|
|
515
|
-
function isSameOrigin(request) {
|
|
517
|
+
const isSameOrigin = function isSameOrigin(request) {
|
|
516
518
|
const site = request.headers.get("sec-fetch-site");
|
|
517
519
|
const origin = request.headers.get("origin");
|
|
518
520
|
if (!origin && !site) return false;
|
|
@@ -523,74 +525,90 @@ function isSameOrigin(request) {
|
|
|
523
525
|
} catch {
|
|
524
526
|
return false;
|
|
525
527
|
}
|
|
526
|
-
}
|
|
528
|
+
};
|
|
527
529
|
//#endregion
|
|
528
530
|
//#region src/rpc/scrub.ts
|
|
529
|
-
/** Strip Effect RPC `Defect` / `Cause` payloads down to plain JSON-RPC errors. */
|
|
530
531
|
const INTERNAL = {
|
|
531
532
|
code: -32603,
|
|
532
533
|
message: "Internal error"
|
|
533
534
|
};
|
|
534
535
|
const NDJSON_CONTENT = "application/json-rpc";
|
|
535
|
-
function
|
|
536
|
-
return value !== null && typeof value === "object";
|
|
537
|
-
}
|
|
538
|
-
function classifyCause(error) {
|
|
536
|
+
const isJsonObject = function isJsonObject(value) {
|
|
537
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
538
|
+
};
|
|
539
|
+
const classifyCause = function classifyCause(error) {
|
|
539
540
|
const blob = `${String(error["message"] ?? "")}${JSON.stringify(error["data"] ?? "")}`;
|
|
540
|
-
if (/Unknown request tag/
|
|
541
|
+
if (/Unknown request tag/iu.test(blob)) return {
|
|
541
542
|
code: -32601,
|
|
542
543
|
message: "Method not found"
|
|
543
544
|
};
|
|
544
|
-
if (/Missing key/
|
|
545
|
+
if (/Missing key/iu.test(blob) || /Expected/iu.test(blob) && /\["args"\]|\[\\"args\\"\]/u.test(blob)) return {
|
|
545
546
|
code: -32602,
|
|
546
547
|
message: "Invalid params"
|
|
547
548
|
};
|
|
548
549
|
return { ...INTERNAL };
|
|
549
|
-
}
|
|
550
|
-
function
|
|
551
|
-
|
|
550
|
+
};
|
|
551
|
+
const isPlainJsonRpcError = function isPlainJsonRpcError(error) {
|
|
552
|
+
return typeof error["code"] === "number" && typeof error["message"] === "string";
|
|
553
|
+
};
|
|
554
|
+
const scrubError = function scrubError(error) {
|
|
555
|
+
if (!isJsonObject(error)) return { ...INTERNAL };
|
|
552
556
|
if (error["_tag"] === "Defect") return { ...INTERNAL };
|
|
553
557
|
if (error["_tag"] === "Cause") return classifyCause(error);
|
|
554
|
-
if (
|
|
558
|
+
if (isPlainJsonRpcError(error)) return {
|
|
555
559
|
code: error["code"],
|
|
556
560
|
message: error["message"]
|
|
557
561
|
};
|
|
558
562
|
return { ...INTERNAL };
|
|
559
|
-
}
|
|
560
|
-
function createIdRepairState(requestIds = []) {
|
|
563
|
+
};
|
|
564
|
+
const createIdRepairState = function createIdRepairState(requestIds = []) {
|
|
561
565
|
return { remaining: new Set(requestIds) };
|
|
562
|
-
}
|
|
566
|
+
};
|
|
567
|
+
const parseOxidejsJson = function parseOxidejsJson(raw) {
|
|
568
|
+
return JSON.parse(raw);
|
|
569
|
+
};
|
|
563
570
|
/**
|
|
564
571
|
* Scrub one JSON-RPC response object.
|
|
565
572
|
* Effect encodes Defects with `id: -32603`; reclaim the originating request id from `state.remaining`.
|
|
566
573
|
*/
|
|
567
|
-
function scrubRpcMessage(msg, requestIds = [], state = createIdRepairState(requestIds)) {
|
|
568
|
-
if (!
|
|
569
|
-
if (
|
|
574
|
+
const scrubRpcMessage = function scrubRpcMessage(msg, requestIds = [], state = createIdRepairState(requestIds)) {
|
|
575
|
+
if (!isJsonObject(msg) || !("error" in msg) || msg["error"] === null || msg["error"] === void 0) {
|
|
576
|
+
if (isJsonObject(msg) && msg["chunk"] !== true && msg["id"] !== -32603 && "id" in msg) state.remaining.delete(msg["id"]);
|
|
570
577
|
return msg;
|
|
571
578
|
}
|
|
572
|
-
const
|
|
573
|
-
let id = msg
|
|
579
|
+
const scrubbedError = scrubError(msg["error"]);
|
|
580
|
+
let { id } = msg;
|
|
574
581
|
if (id === -32603) {
|
|
575
582
|
const next = state.remaining.values().next();
|
|
576
|
-
if (
|
|
577
|
-
|
|
583
|
+
if (next.done) id = null;
|
|
584
|
+
else {
|
|
585
|
+
({value: id} = next);
|
|
578
586
|
state.remaining.delete(next.value);
|
|
579
|
-
}
|
|
587
|
+
}
|
|
580
588
|
} else if (id !== void 0 && id !== null) state.remaining.delete(id);
|
|
581
589
|
if (id === void 0) id = null;
|
|
582
590
|
return {
|
|
583
|
-
|
|
591
|
+
error: {
|
|
592
|
+
code: scrubbedError.code,
|
|
593
|
+
message: scrubbedError.message
|
|
594
|
+
},
|
|
584
595
|
id,
|
|
585
|
-
|
|
596
|
+
jsonrpc: "2.0"
|
|
586
597
|
};
|
|
587
|
-
}
|
|
598
|
+
};
|
|
599
|
+
const scrubRpcLine = function scrubRpcLine(line, state) {
|
|
600
|
+
try {
|
|
601
|
+
return JSON.stringify(scrubRpcMessage(parseOxidejsJson(line), [], state));
|
|
602
|
+
} catch {
|
|
603
|
+
return line;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
588
606
|
/**
|
|
589
607
|
* Rewrite a JSON / NDJSON body so clients never see Effect `_tag` / `data` trees.
|
|
590
608
|
* Accepts a single object, a JSON array, or newline-delimited frames.
|
|
591
609
|
*/
|
|
592
|
-
function scrubRpcJson(body, requestIds = []) {
|
|
593
|
-
const trimmed = body.replace(/^\uFEFF
|
|
610
|
+
const scrubRpcJson = function scrubRpcJson(body, requestIds = []) {
|
|
611
|
+
const trimmed = body.replace(/^\uFEFF/u, "");
|
|
594
612
|
if (!trimmed) return body;
|
|
595
613
|
const state = createIdRepairState(requestIds);
|
|
596
614
|
if (trimmed.includes("\n")) {
|
|
@@ -603,59 +621,71 @@ function scrubRpcJson(body, requestIds = []) {
|
|
|
603
621
|
return trimmed.endsWith("\n") ? `${out.join("\n")}\n` : out.join("\n");
|
|
604
622
|
}
|
|
605
623
|
try {
|
|
606
|
-
const parsed =
|
|
624
|
+
const parsed = parseOxidejsJson(trimmed);
|
|
607
625
|
if (Array.isArray(parsed)) return JSON.stringify(parsed.map((msg) => scrubRpcMessage(msg, requestIds, state)));
|
|
608
626
|
return JSON.stringify(scrubRpcMessage(parsed, requestIds, state));
|
|
609
627
|
} catch {
|
|
610
628
|
return body;
|
|
611
629
|
}
|
|
612
|
-
}
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
630
|
+
};
|
|
631
|
+
const requestBodyText = function requestBodyText(body) {
|
|
632
|
+
if (body instanceof Uint8Array) return new TextDecoder().decode(body);
|
|
633
|
+
if (body instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(body));
|
|
634
|
+
return body;
|
|
635
|
+
};
|
|
636
|
+
const isJsonRpcId = function isJsonRpcId(value) {
|
|
637
|
+
return value === null || typeof value === "string" || typeof value === "number";
|
|
638
|
+
};
|
|
620
639
|
/** Collect JSON-RPC request ids from a unary object or batch array body. */
|
|
621
|
-
function extractJsonRpcRequestIds(body) {
|
|
640
|
+
const extractJsonRpcRequestIds = function extractJsonRpcRequestIds(body) {
|
|
622
641
|
try {
|
|
623
|
-
let text =
|
|
624
|
-
text = text.replace(/^\uFEFF
|
|
642
|
+
let text = requestBodyText(body);
|
|
643
|
+
text = text.replace(/^\uFEFF/u, "").trimEnd();
|
|
625
644
|
if (text.includes("\n")) {
|
|
626
645
|
const ids = [];
|
|
627
646
|
for (const line of text.split("\n")) {
|
|
628
647
|
if (!line) continue;
|
|
629
|
-
const parsed =
|
|
630
|
-
if (
|
|
648
|
+
const parsed = parseOxidejsJson(line);
|
|
649
|
+
if (isJsonObject(parsed) && "id" in parsed && isJsonRpcId(parsed["id"])) ids.push(parsed["id"]);
|
|
631
650
|
}
|
|
632
651
|
return ids;
|
|
633
652
|
}
|
|
634
|
-
const parsed =
|
|
635
|
-
if (Array.isArray(parsed))
|
|
636
|
-
|
|
653
|
+
const parsed = parseOxidejsJson(text);
|
|
654
|
+
if (Array.isArray(parsed)) {
|
|
655
|
+
const ids = [];
|
|
656
|
+
for (const item of parsed) if (isJsonObject(item) && "id" in item && isJsonRpcId(item["id"])) ids.push(item["id"]);
|
|
657
|
+
return ids;
|
|
658
|
+
}
|
|
659
|
+
if (isJsonObject(parsed) && "id" in parsed && isJsonRpcId(parsed["id"])) return [parsed["id"]];
|
|
637
660
|
} catch {}
|
|
638
661
|
return [];
|
|
639
|
-
}
|
|
662
|
+
};
|
|
640
663
|
/** Ensure a body is a valid NDJSON frame (Effect's ndJsonRpc decode requires a trailing newline). */
|
|
641
|
-
function ensureNdjsonBody(buf) {
|
|
664
|
+
const ensureNdjsonBody = function ensureNdjsonBody(buf) {
|
|
642
665
|
const bytes = new Uint8Array(buf);
|
|
643
|
-
if (bytes.length > 0 && bytes
|
|
666
|
+
if (bytes.length > 0 && bytes.at(-1) === 10) return bytes;
|
|
644
667
|
const out = new Uint8Array(bytes.length + 1);
|
|
645
668
|
out.set(bytes);
|
|
646
669
|
out[bytes.length] = 10;
|
|
647
670
|
return out;
|
|
648
|
-
}
|
|
671
|
+
};
|
|
649
672
|
/**
|
|
650
673
|
* TransformStream that scrubs Effect defect payloads one NDJSON line at a time,
|
|
651
674
|
* so long-running stream actions stay incremental.
|
|
652
675
|
*/
|
|
653
|
-
function scrubNdjsonTransform(requestIds = []) {
|
|
676
|
+
const scrubNdjsonTransform = function scrubNdjsonTransform(requestIds = []) {
|
|
654
677
|
const decoder = new TextDecoder();
|
|
655
678
|
const encoder = new TextEncoder();
|
|
656
679
|
const state = createIdRepairState(requestIds);
|
|
657
680
|
let pending = "";
|
|
658
681
|
return new TransformStream({
|
|
682
|
+
flush(controller) {
|
|
683
|
+
pending += decoder.decode();
|
|
684
|
+
if (pending.length > 0) {
|
|
685
|
+
controller.enqueue(encoder.encode(`${scrubRpcLine(pending, state)}\n`));
|
|
686
|
+
pending = "";
|
|
687
|
+
}
|
|
688
|
+
},
|
|
659
689
|
transform(chunk, controller) {
|
|
660
690
|
pending += decoder.decode(chunk, { stream: true });
|
|
661
691
|
let nl = pending.indexOf("\n");
|
|
@@ -665,126 +695,122 @@ function scrubNdjsonTransform(requestIds = []) {
|
|
|
665
695
|
if (line.length > 0) controller.enqueue(encoder.encode(`${scrubRpcLine(line, state)}\n`));
|
|
666
696
|
nl = pending.indexOf("\n");
|
|
667
697
|
}
|
|
668
|
-
},
|
|
669
|
-
flush(controller) {
|
|
670
|
-
pending += decoder.decode();
|
|
671
|
-
if (pending.length > 0) {
|
|
672
|
-
controller.enqueue(encoder.encode(`${scrubRpcLine(pending, state)}\n`));
|
|
673
|
-
pending = "";
|
|
674
|
-
}
|
|
675
698
|
}
|
|
676
699
|
});
|
|
677
|
-
}
|
|
700
|
+
};
|
|
678
701
|
//#endregion
|
|
679
702
|
//#region src/rpc/server.ts
|
|
680
703
|
const JSON_RPC_FORBIDDEN = {
|
|
681
|
-
jsonrpc: "2.0",
|
|
682
704
|
error: {
|
|
683
705
|
code: -32600,
|
|
684
706
|
message: "Forbidden"
|
|
685
707
|
},
|
|
686
|
-
id: null
|
|
708
|
+
id: null,
|
|
709
|
+
jsonrpc: "2.0"
|
|
687
710
|
};
|
|
688
711
|
const bundles = /* @__PURE__ */ new Map();
|
|
689
712
|
const groupIds = /* @__PURE__ */ new WeakMap();
|
|
690
713
|
let nextGroupId = 0;
|
|
691
714
|
const serialization = RpcSerialization.layerNdJsonRpc();
|
|
692
|
-
function bundleKey(group, path, transport) {
|
|
715
|
+
const bundleKey = function bundleKey(group, path, transport) {
|
|
693
716
|
let id = groupIds.get(group);
|
|
694
717
|
if (id === void 0) {
|
|
695
|
-
id = nextGroupId
|
|
718
|
+
id = nextGroupId;
|
|
719
|
+
nextGroupId += 1;
|
|
696
720
|
groupIds.set(group, id);
|
|
697
721
|
}
|
|
698
722
|
return `${id}:${path}:${transport}`;
|
|
699
|
-
}
|
|
700
|
-
function forbidden() {
|
|
701
|
-
return
|
|
702
|
-
|
|
703
|
-
|
|
723
|
+
};
|
|
724
|
+
const forbidden = function forbidden() {
|
|
725
|
+
return Response.json(JSON_RPC_FORBIDDEN, {
|
|
726
|
+
headers: { "content-type": "application/json" },
|
|
727
|
+
status: 403
|
|
704
728
|
});
|
|
705
|
-
}
|
|
706
|
-
function methodNotAllowed() {
|
|
729
|
+
};
|
|
730
|
+
const methodNotAllowed = function methodNotAllowed() {
|
|
707
731
|
return new Response("Method Not Allowed", {
|
|
708
|
-
|
|
709
|
-
|
|
732
|
+
headers: { Allow: "POST" },
|
|
733
|
+
status: 405
|
|
710
734
|
});
|
|
711
|
-
}
|
|
712
|
-
function buildBundle(group, handlers, path, transport) {
|
|
735
|
+
};
|
|
736
|
+
const buildBundle = function buildBundle(group, handlers, path, transport) {
|
|
713
737
|
const app = RpcServer.layerHttp({
|
|
714
738
|
group,
|
|
715
739
|
path,
|
|
716
740
|
protocol: transport === "ws" ? "websocket" : "http"
|
|
717
741
|
}).pipe(Layer.provide(handlers), Layer.provide(serialization));
|
|
718
742
|
return HttpRouter.toWebHandler(app, { disableLogger: true });
|
|
719
|
-
}
|
|
720
|
-
function bundleFor(group, handlers, path, transport) {
|
|
743
|
+
};
|
|
744
|
+
const bundleFor = function bundleFor(group, handlers, path, transport) {
|
|
721
745
|
const key = bundleKey(group, path, transport);
|
|
722
746
|
const cached = bundles.get(key);
|
|
723
747
|
if (cached) return cached;
|
|
724
748
|
const built = buildBundle(group, handlers, path, transport);
|
|
725
749
|
bundles.set(key, built);
|
|
726
750
|
return built;
|
|
727
|
-
}
|
|
728
|
-
function scrubJsonResponse(response, requestIds) {
|
|
751
|
+
};
|
|
752
|
+
const scrubJsonResponse = function scrubJsonResponse(response, requestIds) {
|
|
729
753
|
const contentType = response.headers.get("content-type") ?? "";
|
|
730
754
|
if (!contentType.includes("json")) return response;
|
|
731
755
|
if (response.body && (contentType.includes("application/json-rpc") || contentType.includes("ndjson"))) {
|
|
732
756
|
const headers = new Headers(response.headers);
|
|
733
757
|
headers.delete("content-length");
|
|
734
758
|
return new Response(response.body.pipeThrough(scrubNdjsonTransform(requestIds)), {
|
|
759
|
+
headers,
|
|
735
760
|
status: response.status,
|
|
736
|
-
statusText: response.statusText
|
|
737
|
-
headers
|
|
761
|
+
statusText: response.statusText
|
|
738
762
|
});
|
|
739
763
|
}
|
|
740
764
|
return response;
|
|
741
|
-
}
|
|
742
|
-
async function scrubBufferedJson(response, requestIds) {
|
|
765
|
+
};
|
|
766
|
+
const scrubBufferedJson = async function scrubBufferedJson(response, requestIds) {
|
|
743
767
|
const text = await response.text();
|
|
744
768
|
const headers = new Headers(response.headers);
|
|
745
769
|
headers.delete("content-length");
|
|
746
770
|
return new Response(scrubRpcJson(text, requestIds), {
|
|
771
|
+
headers,
|
|
747
772
|
status: response.status,
|
|
748
|
-
statusText: response.statusText
|
|
749
|
-
headers
|
|
773
|
+
statusText: response.statusText
|
|
750
774
|
});
|
|
751
|
-
}
|
|
752
|
-
function createActionHandler(group, handlers, options = {}) {
|
|
775
|
+
};
|
|
776
|
+
const createActionHandler = function createActionHandler(group, handlers, options = {}) {
|
|
753
777
|
const path = options.path ?? "/__oxide/action";
|
|
754
778
|
const transport = options.transport ?? "http";
|
|
755
779
|
const sameOrigin = options.sameOrigin ?? true;
|
|
756
|
-
return async (request)
|
|
780
|
+
return async function handleActionRequest(request) {
|
|
757
781
|
if (!matchesActionPath(new URL(request.url).pathname, path)) return new Response("Not Found", { status: 404 });
|
|
758
782
|
if (transport === "http" && request.method !== "POST") return methodNotAllowed();
|
|
759
783
|
if (sameOrigin && !isSameOrigin(request)) return forbidden();
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
784
|
+
return await withRequestEntry(async () => {
|
|
785
|
+
const rawBody = ensureNdjsonBody(await request.arrayBuffer());
|
|
786
|
+
const requestIds = extractJsonRpcRequestIds(rawBody);
|
|
787
|
+
const headers = new Headers(request.headers);
|
|
788
|
+
headers.set("content-type", NDJSON_CONTENT);
|
|
789
|
+
const forwarded = new Request(request.url, {
|
|
790
|
+
body: rawBody,
|
|
791
|
+
headers,
|
|
792
|
+
method: request.method,
|
|
793
|
+
signal: request.signal
|
|
794
|
+
});
|
|
795
|
+
const extra = await options.createContext?.(forwarded) ?? {};
|
|
796
|
+
const { handler } = bundleFor(group, handlers, path, transport);
|
|
797
|
+
const response = await runWithRequest(forwarded, () => handler(forwarded), extra);
|
|
798
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
799
|
+
if (contentType.includes("application/json-rpc") || contentType.includes("ndjson")) return scrubJsonResponse(response, requestIds);
|
|
800
|
+
if (contentType.includes("json")) return scrubBufferedJson(response, requestIds);
|
|
801
|
+
return response;
|
|
769
802
|
});
|
|
770
|
-
const extra = await options.createContext?.(forwarded) ?? {};
|
|
771
|
-
const { handler } = bundleFor(group, handlers, path, transport);
|
|
772
|
-
const response = await runWithRequest(forwarded, () => handler(forwarded), extra);
|
|
773
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
774
|
-
if (contentType.includes("application/json-rpc") || contentType.includes("ndjson")) return scrubJsonResponse(response, requestIds);
|
|
775
|
-
if (contentType.includes("json")) return scrubBufferedJson(response, requestIds);
|
|
776
|
-
return response;
|
|
777
803
|
};
|
|
778
|
-
}
|
|
779
|
-
function disposeActionHandler(group, path = ACTION_PATH, transport = "http") {
|
|
804
|
+
};
|
|
805
|
+
const disposeActionHandler = function disposeActionHandler(group, path = ACTION_PATH, transport = "http") {
|
|
780
806
|
const key = bundleKey(group, path, transport);
|
|
781
807
|
const bundle = bundles.get(key);
|
|
782
808
|
bundles.delete(key);
|
|
783
809
|
return bundle?.dispose() ?? Promise.resolve();
|
|
784
|
-
}
|
|
810
|
+
};
|
|
785
811
|
//#endregion
|
|
786
812
|
//#region src/rpc/ws.ts
|
|
787
|
-
function parseMessage(message, maxBytes) {
|
|
813
|
+
const parseMessage = function parseMessage(message, maxBytes) {
|
|
788
814
|
try {
|
|
789
815
|
const raw = message.text();
|
|
790
816
|
if (new TextEncoder().encode(raw).byteLength > maxBytes) return {
|
|
@@ -798,23 +824,26 @@ function parseMessage(message, maxBytes) {
|
|
|
798
824
|
} catch {
|
|
799
825
|
return { ok: false };
|
|
800
826
|
}
|
|
801
|
-
}
|
|
827
|
+
};
|
|
828
|
+
const isJsonRpcPing = function isJsonRpcPing(value) {
|
|
829
|
+
return value !== null && !Array.isArray(value) && typeof value === "object" && value["method"] === "@effect/rpc/Ping";
|
|
830
|
+
};
|
|
802
831
|
/**
|
|
803
832
|
* Effect's socket client sends `@effect/rpc/Ping` keepalives (no id) and hangs
|
|
804
833
|
* up unless the server answers `@effect/rpc/Pong`. Handle control messages here
|
|
805
834
|
* so they never reach the action handler.
|
|
806
835
|
*/
|
|
807
|
-
function controlReply(raw) {
|
|
836
|
+
const controlReply = function controlReply(raw) {
|
|
808
837
|
try {
|
|
809
838
|
const parsed = JSON.parse(raw);
|
|
810
|
-
if (parsed
|
|
839
|
+
if (isJsonRpcPing(parsed)) return JSON.stringify({
|
|
811
840
|
jsonrpc: "2.0",
|
|
812
841
|
method: "@effect/rpc/Pong"
|
|
813
842
|
});
|
|
814
843
|
} catch {}
|
|
815
|
-
}
|
|
844
|
+
};
|
|
816
845
|
/** Forward each complete NDJSON line as its own WS message (keeps streams incremental). */
|
|
817
|
-
async function sendNdjsonFrames(peer, response, signal) {
|
|
846
|
+
const sendNdjsonFrames = async function sendNdjsonFrames(peer, response, signal) {
|
|
818
847
|
if (signal.aborted) {
|
|
819
848
|
await response.body?.cancel();
|
|
820
849
|
return;
|
|
@@ -827,14 +856,15 @@ async function sendNdjsonFrames(peer, response, signal) {
|
|
|
827
856
|
const reader = response.body.getReader();
|
|
828
857
|
const decoder = new TextDecoder();
|
|
829
858
|
let pending = "";
|
|
830
|
-
const onAbort = ()
|
|
859
|
+
const onAbort = function onAbort() {
|
|
831
860
|
reader.cancel();
|
|
832
861
|
};
|
|
833
862
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
834
|
-
|
|
835
|
-
|
|
863
|
+
const pump = async function pump() {
|
|
864
|
+
for (;;) {
|
|
865
|
+
if (signal.aborted) return;
|
|
836
866
|
const { done, value } = await reader.read();
|
|
837
|
-
if (done)
|
|
867
|
+
if (done) return;
|
|
838
868
|
pending += decoder.decode(value, { stream: true });
|
|
839
869
|
let nl = pending.indexOf("\n");
|
|
840
870
|
while (nl !== -1) {
|
|
@@ -844,6 +874,9 @@ async function sendNdjsonFrames(peer, response, signal) {
|
|
|
844
874
|
nl = pending.indexOf("\n");
|
|
845
875
|
}
|
|
846
876
|
}
|
|
877
|
+
};
|
|
878
|
+
try {
|
|
879
|
+
await pump();
|
|
847
880
|
if (!signal.aborted) {
|
|
848
881
|
pending += decoder.decode();
|
|
849
882
|
if (pending.length > 0) peer.send(pending.endsWith("\n") ? pending : `${pending}\n`);
|
|
@@ -851,37 +884,27 @@ async function sendNdjsonFrames(peer, response, signal) {
|
|
|
851
884
|
} finally {
|
|
852
885
|
signal.removeEventListener("abort", onAbort);
|
|
853
886
|
}
|
|
854
|
-
}
|
|
855
|
-
function createWsHooks(group, handlers, options = {}) {
|
|
887
|
+
};
|
|
888
|
+
const createWsHooks = function createWsHooks(group, handlers, options = {}) {
|
|
856
889
|
const path = options.path ?? "/__oxide/action";
|
|
857
890
|
const maxBytes = options.maxMessageSize ?? 1048576;
|
|
858
891
|
const sameOrigin = options.sameOrigin ?? true;
|
|
859
892
|
const baseOptions = {
|
|
860
893
|
path,
|
|
861
|
-
|
|
862
|
-
|
|
894
|
+
sameOrigin,
|
|
895
|
+
transport: "http"
|
|
863
896
|
};
|
|
864
897
|
return {
|
|
865
|
-
upgrade(req) {
|
|
866
|
-
let pathname;
|
|
867
|
-
try {
|
|
868
|
-
pathname = new URL(req.url).pathname;
|
|
869
|
-
} catch {
|
|
870
|
-
return new Response("Bad Request", { status: 400 });
|
|
871
|
-
}
|
|
872
|
-
if (!matchesActionPath(pathname, path)) return new Response("Not Found", { status: 404 });
|
|
873
|
-
if (sameOrigin && !isSameOrigin(req)) return new Response("Forbidden", { status: 403 });
|
|
874
|
-
},
|
|
875
898
|
async message(peer, message) {
|
|
876
899
|
const parsed = parseMessage(message, maxBytes);
|
|
877
900
|
if (!parsed.ok) {
|
|
878
901
|
peer.send(JSON.stringify({
|
|
879
|
-
jsonrpc: "2.0",
|
|
880
902
|
error: {
|
|
881
903
|
code: -32600,
|
|
882
904
|
message: parsed.tooLarge ? "Payload too large" : "Parse error"
|
|
883
905
|
},
|
|
884
|
-
id: null
|
|
906
|
+
id: null,
|
|
907
|
+
jsonrpc: "2.0"
|
|
885
908
|
}));
|
|
886
909
|
return;
|
|
887
910
|
}
|
|
@@ -896,20 +919,31 @@ function createWsHooks(group, handlers, options = {}) {
|
|
|
896
919
|
const headers = new Headers(peer.request?.headers);
|
|
897
920
|
headers.set("content-type", NDJSON_CONTENT);
|
|
898
921
|
const peerCtx = await options.createContext?.(peer) ?? peer.context;
|
|
899
|
-
|
|
922
|
+
const response = await createActionHandler(group, handlers, {
|
|
900
923
|
...baseOptions,
|
|
901
924
|
createContext: (req) => ({
|
|
902
925
|
...peerCtx,
|
|
903
926
|
req
|
|
904
927
|
})
|
|
905
928
|
})(new Request(`http://${host}${path}`, {
|
|
906
|
-
method: "POST",
|
|
907
|
-
headers,
|
|
908
929
|
body: parsed.value,
|
|
930
|
+
headers,
|
|
931
|
+
method: "POST",
|
|
909
932
|
signal: abort.signal
|
|
910
|
-
}))
|
|
933
|
+
}));
|
|
934
|
+
await sendNdjsonFrames(peer, response, abort.signal);
|
|
935
|
+
},
|
|
936
|
+
upgrade(req) {
|
|
937
|
+
let pathname;
|
|
938
|
+
try {
|
|
939
|
+
({pathname} = new URL(req.url));
|
|
940
|
+
} catch {
|
|
941
|
+
return new Response("Bad Request", { status: 400 });
|
|
942
|
+
}
|
|
943
|
+
if (!matchesActionPath(pathname, path)) return new Response("Not Found", { status: 404 });
|
|
944
|
+
if (sameOrigin && !isSameOrigin(req)) return new Response("Forbidden", { status: 403 });
|
|
911
945
|
}
|
|
912
946
|
};
|
|
913
|
-
}
|
|
947
|
+
};
|
|
914
948
|
//#endregion
|
|
915
|
-
export {
|
|
949
|
+
export { scanServerFiles as A, loadClientStub as C, parseExportedNames as D, nodeToWebRequest as E, parseStreamExports as O, isServerFileId as S, moduleKey as T, generateActionsClientModule as _, extractJsonRpcRequestIds as a, generateClientStub as b, scrubRpcMessage as c, RESOLVED_VIRTUAL_CLIENT_ID as d, RESOLVED_VIRTUAL_WORKER_ID as f, VIRTUAL_WORKER_ID as g, VIRTUAL_CLIENT_ID as h, ensureNdjsonBody as i, sendWebResponseFrom as j, pluginShouldStub as k, ACTION_PATH as l, VIRTUAL_ACTIONS_ID as m, createActionHandler as n, scrubNdjsonTransform as o, RequestBodyTooLargeError as p, disposeActionHandler as r, scrubRpcJson as s, createWsHooks as t, RESOLVED_VIRTUAL_ACTIONS_ID as u, generateActionsModule as v, matchesActionPath as w, generateWorkerWrapper as x, generateClientModule as y };
|