oxidejs 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/actions-DE6p5Cyp.mjs +504 -0
- package/dist/client-Bc4g9AEw.mjs +145 -0
- package/dist/context-C1UFQ0Zc.d.mts +64 -0
- package/dist/context-zrTZyYpF.mjs +42 -0
- package/dist/index.d.mts +2 -29
- package/dist/index.mjs +103 -44
- package/dist/plugin-IQa_lKkT.mjs +601 -0
- package/dist/plugin.mjs +1 -1
- package/dist/rpc/client.d.mts +13 -0
- package/dist/rpc/client.mjs +2 -0
- package/dist/rpc-DYFEdah8.mjs +382 -0
- package/dist/rpc.d.mts +75 -0
- package/dist/rpc.mjs +3 -0
- package/dist/rsbuild.mjs +1 -1
- package/dist/vite.mjs +1 -1
- package/dist/worker-dom/install.d.mts +1 -0
- package/dist/worker-dom/install.mjs +6 -0
- package/dist/worker-dom.d.mts +5 -0
- package/dist/worker-dom.mjs +16 -0
- package/package.json +19 -8
- package/virtual.d.ts +7 -4
- package/dist/plugin-nO0yjtk0.mjs +0 -954
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { _ as matchesActionPath, t as ACTION_PATH } from "./actions-DE6p5Cyp.mjs";
|
|
2
|
+
import { t as runWithRequest } from "./context-zrTZyYpF.mjs";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
5
|
+
import { HttpRouter } from "effect/unstable/http";
|
|
6
|
+
//#region src/rpc/same-origin.ts
|
|
7
|
+
function isSameOrigin(request) {
|
|
8
|
+
const site = request.headers.get("sec-fetch-site");
|
|
9
|
+
const origin = request.headers.get("origin");
|
|
10
|
+
if (!origin && !site) return false;
|
|
11
|
+
if (site && site !== "same-origin" && site !== "none") return false;
|
|
12
|
+
if (!origin) return site === "same-origin" || site === "none";
|
|
13
|
+
try {
|
|
14
|
+
return new URL(origin).host === (request.headers.get("host") ?? new URL(request.url).host);
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/rpc/scrub.ts
|
|
21
|
+
/** Strip Effect RPC `Defect` / `Cause` payloads down to plain JSON-RPC errors. */
|
|
22
|
+
const INTERNAL = {
|
|
23
|
+
code: -32603,
|
|
24
|
+
message: "Internal error"
|
|
25
|
+
};
|
|
26
|
+
const NDJSON_CONTENT = "application/json-rpc";
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return value !== null && typeof value === "object";
|
|
29
|
+
}
|
|
30
|
+
function classifyCause(error) {
|
|
31
|
+
const blob = `${String(error["message"] ?? "")}${JSON.stringify(error["data"] ?? "")}`;
|
|
32
|
+
if (/Unknown request tag/i.test(blob)) return {
|
|
33
|
+
code: -32601,
|
|
34
|
+
message: "Method not found"
|
|
35
|
+
};
|
|
36
|
+
if (/Missing key/i.test(blob) || /Expected/i.test(blob) && /\["args"\]|\[\\"args\\"\]/.test(blob)) return {
|
|
37
|
+
code: -32602,
|
|
38
|
+
message: "Invalid params"
|
|
39
|
+
};
|
|
40
|
+
return { ...INTERNAL };
|
|
41
|
+
}
|
|
42
|
+
function scrubError(error) {
|
|
43
|
+
if (!isRecord(error)) return { ...INTERNAL };
|
|
44
|
+
if (error["_tag"] === "Defect") return { ...INTERNAL };
|
|
45
|
+
if (error["_tag"] === "Cause") return classifyCause(error);
|
|
46
|
+
if (typeof error["code"] === "number" && typeof error["message"] === "string") return {
|
|
47
|
+
code: error["code"],
|
|
48
|
+
message: error["message"]
|
|
49
|
+
};
|
|
50
|
+
return { ...INTERNAL };
|
|
51
|
+
}
|
|
52
|
+
function createIdRepairState(requestIds = []) {
|
|
53
|
+
return { remaining: new Set(requestIds) };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Scrub one JSON-RPC response object.
|
|
57
|
+
* Effect encodes Defects with `id: -32603`; reclaim the originating request id from `state.remaining`.
|
|
58
|
+
*/
|
|
59
|
+
function scrubRpcMessage(msg, requestIds = [], state = createIdRepairState(requestIds)) {
|
|
60
|
+
if (!isRecord(msg) || !("error" in msg) || msg["error"] == null) {
|
|
61
|
+
if (isRecord(msg) && msg["chunk"] !== true && msg["id"] !== -32603 && "id" in msg) state.remaining.delete(msg["id"]);
|
|
62
|
+
return msg;
|
|
63
|
+
}
|
|
64
|
+
const error = scrubError(msg["error"]);
|
|
65
|
+
let id = msg["id"];
|
|
66
|
+
if (id === -32603) {
|
|
67
|
+
const next = state.remaining.values().next();
|
|
68
|
+
if (!next.done) {
|
|
69
|
+
id = next.value;
|
|
70
|
+
state.remaining.delete(next.value);
|
|
71
|
+
} else id = null;
|
|
72
|
+
} else if (id !== void 0 && id !== null) state.remaining.delete(id);
|
|
73
|
+
if (id === void 0) id = null;
|
|
74
|
+
return {
|
|
75
|
+
jsonrpc: "2.0",
|
|
76
|
+
id,
|
|
77
|
+
error
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Rewrite a JSON / NDJSON body so clients never see Effect `_tag` / `data` trees.
|
|
82
|
+
* Accepts a single object, a JSON array, or newline-delimited frames.
|
|
83
|
+
*/
|
|
84
|
+
function scrubRpcJson(body, requestIds = []) {
|
|
85
|
+
const trimmed = body.replace(/^\uFEFF/, "");
|
|
86
|
+
if (!trimmed) return body;
|
|
87
|
+
const state = createIdRepairState(requestIds);
|
|
88
|
+
if (trimmed.includes("\n")) {
|
|
89
|
+
const lines = trimmed.split("\n");
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
if (line === "") continue;
|
|
93
|
+
out.push(scrubRpcLine(line, state));
|
|
94
|
+
}
|
|
95
|
+
return trimmed.endsWith("\n") ? `${out.join("\n")}\n` : out.join("\n");
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(trimmed);
|
|
99
|
+
if (Array.isArray(parsed)) return JSON.stringify(parsed.map((msg) => scrubRpcMessage(msg, requestIds, state)));
|
|
100
|
+
return JSON.stringify(scrubRpcMessage(parsed, requestIds, state));
|
|
101
|
+
} catch {
|
|
102
|
+
return body;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function scrubRpcLine(line, state) {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(scrubRpcMessage(JSON.parse(line), [], state));
|
|
108
|
+
} catch {
|
|
109
|
+
return line;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Collect JSON-RPC request ids from a unary object or batch array body. */
|
|
113
|
+
function extractJsonRpcRequestIds(body) {
|
|
114
|
+
try {
|
|
115
|
+
let text = typeof body === "string" ? body : new TextDecoder().decode(body instanceof Uint8Array ? body : new Uint8Array(body));
|
|
116
|
+
text = text.replace(/^\uFEFF/, "").trimEnd();
|
|
117
|
+
if (text.includes("\n")) {
|
|
118
|
+
const ids = [];
|
|
119
|
+
for (const line of text.split("\n")) {
|
|
120
|
+
if (!line) continue;
|
|
121
|
+
const parsed = JSON.parse(line);
|
|
122
|
+
if (isRecord(parsed) && "id" in parsed) ids.push(parsed["id"]);
|
|
123
|
+
}
|
|
124
|
+
return ids;
|
|
125
|
+
}
|
|
126
|
+
const parsed = JSON.parse(text);
|
|
127
|
+
if (Array.isArray(parsed)) return parsed.filter(isRecord).filter((item) => "id" in item).map((item) => item["id"]);
|
|
128
|
+
if (isRecord(parsed) && "id" in parsed) return [parsed["id"]];
|
|
129
|
+
} catch {}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
/** Ensure a body is a valid NDJSON frame (Effect's ndJsonRpc decode requires a trailing newline). */
|
|
133
|
+
function ensureNdjsonBody(buf) {
|
|
134
|
+
const bytes = new Uint8Array(buf);
|
|
135
|
+
if (bytes.length > 0 && bytes[bytes.length - 1] === 10) return bytes;
|
|
136
|
+
const out = new Uint8Array(bytes.length + 1);
|
|
137
|
+
out.set(bytes);
|
|
138
|
+
out[bytes.length] = 10;
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* TransformStream that scrubs Effect defect payloads one NDJSON line at a time,
|
|
143
|
+
* so long-running stream actions stay incremental.
|
|
144
|
+
*/
|
|
145
|
+
function scrubNdjsonTransform(requestIds = []) {
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
const encoder = new TextEncoder();
|
|
148
|
+
const state = createIdRepairState(requestIds);
|
|
149
|
+
let pending = "";
|
|
150
|
+
return new TransformStream({
|
|
151
|
+
transform(chunk, controller) {
|
|
152
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
153
|
+
let nl = pending.indexOf("\n");
|
|
154
|
+
while (nl !== -1) {
|
|
155
|
+
const line = pending.slice(0, nl);
|
|
156
|
+
pending = pending.slice(nl + 1);
|
|
157
|
+
if (line.length > 0) controller.enqueue(encoder.encode(`${scrubRpcLine(line, state)}\n`));
|
|
158
|
+
nl = pending.indexOf("\n");
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
flush(controller) {
|
|
162
|
+
pending += decoder.decode();
|
|
163
|
+
if (pending.length > 0) {
|
|
164
|
+
controller.enqueue(encoder.encode(`${scrubRpcLine(pending, state)}\n`));
|
|
165
|
+
pending = "";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/rpc/server.ts
|
|
172
|
+
const JSON_RPC_FORBIDDEN = {
|
|
173
|
+
jsonrpc: "2.0",
|
|
174
|
+
error: {
|
|
175
|
+
code: -32600,
|
|
176
|
+
message: "Forbidden"
|
|
177
|
+
},
|
|
178
|
+
id: null
|
|
179
|
+
};
|
|
180
|
+
const bundles = /* @__PURE__ */ new Map();
|
|
181
|
+
const groupIds = /* @__PURE__ */ new WeakMap();
|
|
182
|
+
let nextGroupId = 0;
|
|
183
|
+
const serialization = RpcSerialization.layerNdJsonRpc();
|
|
184
|
+
function bundleKey(group, path, transport) {
|
|
185
|
+
let id = groupIds.get(group);
|
|
186
|
+
if (id === void 0) {
|
|
187
|
+
id = nextGroupId++;
|
|
188
|
+
groupIds.set(group, id);
|
|
189
|
+
}
|
|
190
|
+
return `${id}:${path}:${transport}`;
|
|
191
|
+
}
|
|
192
|
+
function forbidden() {
|
|
193
|
+
return new Response(JSON.stringify(JSON_RPC_FORBIDDEN), {
|
|
194
|
+
status: 403,
|
|
195
|
+
headers: { "content-type": "application/json" }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function methodNotAllowed() {
|
|
199
|
+
return new Response("Method Not Allowed", {
|
|
200
|
+
status: 405,
|
|
201
|
+
headers: { Allow: "POST" }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function buildBundle(group, handlers, path, transport) {
|
|
205
|
+
const app = RpcServer.layerHttp({
|
|
206
|
+
group,
|
|
207
|
+
path,
|
|
208
|
+
protocol: transport === "ws" ? "websocket" : "http"
|
|
209
|
+
}).pipe(Layer.provide(handlers), Layer.provide(serialization));
|
|
210
|
+
return HttpRouter.toWebHandler(app, { disableLogger: true });
|
|
211
|
+
}
|
|
212
|
+
function bundleFor(group, handlers, path, transport) {
|
|
213
|
+
const key = bundleKey(group, path, transport);
|
|
214
|
+
const cached = bundles.get(key);
|
|
215
|
+
if (cached) return cached;
|
|
216
|
+
const built = buildBundle(group, handlers, path, transport);
|
|
217
|
+
bundles.set(key, built);
|
|
218
|
+
return built;
|
|
219
|
+
}
|
|
220
|
+
function scrubJsonResponse(response, requestIds) {
|
|
221
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
222
|
+
if (!contentType.includes("json")) return response;
|
|
223
|
+
if (response.body && (contentType.includes("application/json-rpc") || contentType.includes("ndjson"))) {
|
|
224
|
+
const headers = new Headers(response.headers);
|
|
225
|
+
headers.delete("content-length");
|
|
226
|
+
return new Response(response.body.pipeThrough(scrubNdjsonTransform(requestIds)), {
|
|
227
|
+
status: response.status,
|
|
228
|
+
statusText: response.statusText,
|
|
229
|
+
headers
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return response;
|
|
233
|
+
}
|
|
234
|
+
async function scrubBufferedJson(response, requestIds) {
|
|
235
|
+
const text = await response.text();
|
|
236
|
+
const headers = new Headers(response.headers);
|
|
237
|
+
headers.delete("content-length");
|
|
238
|
+
return new Response(scrubRpcJson(text, requestIds), {
|
|
239
|
+
status: response.status,
|
|
240
|
+
statusText: response.statusText,
|
|
241
|
+
headers
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function createActionHandler(group, handlers, options = {}) {
|
|
245
|
+
const path = options.path ?? "/__oxide/action";
|
|
246
|
+
const transport = options.transport ?? "http";
|
|
247
|
+
const sameOrigin = options.sameOrigin ?? true;
|
|
248
|
+
return async (request) => {
|
|
249
|
+
if (!matchesActionPath(new URL(request.url).pathname, path)) return new Response("Not Found", { status: 404 });
|
|
250
|
+
if (transport === "http" && request.method !== "POST") return methodNotAllowed();
|
|
251
|
+
if (sameOrigin && !isSameOrigin(request)) return forbidden();
|
|
252
|
+
const rawBody = ensureNdjsonBody(await request.arrayBuffer());
|
|
253
|
+
const requestIds = extractJsonRpcRequestIds(rawBody);
|
|
254
|
+
const headers = new Headers(request.headers);
|
|
255
|
+
headers.set("content-type", NDJSON_CONTENT);
|
|
256
|
+
const forwarded = new Request(request.url, {
|
|
257
|
+
method: request.method,
|
|
258
|
+
headers,
|
|
259
|
+
body: rawBody,
|
|
260
|
+
signal: request.signal
|
|
261
|
+
});
|
|
262
|
+
const extra = await options.createContext?.(forwarded) ?? {};
|
|
263
|
+
const { handler } = bundleFor(group, handlers, path, transport);
|
|
264
|
+
const response = await runWithRequest(forwarded, () => handler(forwarded), extra);
|
|
265
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
266
|
+
if (contentType.includes("application/json-rpc") || contentType.includes("ndjson")) return scrubJsonResponse(response, requestIds);
|
|
267
|
+
if (contentType.includes("json")) return scrubBufferedJson(response, requestIds);
|
|
268
|
+
return response;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function disposeActionHandler(group, path = ACTION_PATH, transport = "http") {
|
|
272
|
+
const key = bundleKey(group, path, transport);
|
|
273
|
+
const bundle = bundles.get(key);
|
|
274
|
+
bundles.delete(key);
|
|
275
|
+
return bundle?.dispose() ?? Promise.resolve();
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/rpc/ws.ts
|
|
279
|
+
function parseMessage(message, maxBytes) {
|
|
280
|
+
try {
|
|
281
|
+
const raw = message.text();
|
|
282
|
+
if (new TextEncoder().encode(raw).byteLength > maxBytes) return {
|
|
283
|
+
ok: false,
|
|
284
|
+
tooLarge: true
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
value: raw
|
|
289
|
+
};
|
|
290
|
+
} catch {
|
|
291
|
+
return { ok: false };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Forward each complete NDJSON line as its own WS message (keeps streams incremental). */
|
|
295
|
+
async function sendNdjsonFrames(peer, response, signal) {
|
|
296
|
+
if (signal.aborted) {
|
|
297
|
+
await response.body?.cancel();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!response.body) {
|
|
301
|
+
const text = await response.text();
|
|
302
|
+
if (text && !signal.aborted) peer.send(text);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const reader = response.body.getReader();
|
|
306
|
+
const decoder = new TextDecoder();
|
|
307
|
+
let pending = "";
|
|
308
|
+
const onAbort = () => {
|
|
309
|
+
reader.cancel();
|
|
310
|
+
};
|
|
311
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
312
|
+
try {
|
|
313
|
+
while (!signal.aborted) {
|
|
314
|
+
const { done, value } = await reader.read();
|
|
315
|
+
if (done) break;
|
|
316
|
+
pending += decoder.decode(value, { stream: true });
|
|
317
|
+
let nl = pending.indexOf("\n");
|
|
318
|
+
while (nl !== -1) {
|
|
319
|
+
const line = pending.slice(0, nl);
|
|
320
|
+
pending = pending.slice(nl + 1);
|
|
321
|
+
if (line.length > 0 && !signal.aborted) peer.send(`${line}\n`);
|
|
322
|
+
nl = pending.indexOf("\n");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!signal.aborted) {
|
|
326
|
+
pending += decoder.decode();
|
|
327
|
+
if (pending.length > 0) peer.send(pending.endsWith("\n") ? pending : `${pending}\n`);
|
|
328
|
+
}
|
|
329
|
+
} finally {
|
|
330
|
+
signal.removeEventListener("abort", onAbort);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function createWsHooks(group, handlers, options = {}) {
|
|
334
|
+
const path = options.path ?? "/__oxide/action";
|
|
335
|
+
const maxBytes = options.maxMessageSize ?? 1048576;
|
|
336
|
+
const sameOrigin = options.sameOrigin ?? true;
|
|
337
|
+
const baseOptions = {
|
|
338
|
+
path,
|
|
339
|
+
transport: "http",
|
|
340
|
+
sameOrigin
|
|
341
|
+
};
|
|
342
|
+
return {
|
|
343
|
+
upgrade(req) {
|
|
344
|
+
if (!matchesActionPath(new URL(req.url).pathname, path)) return new Response("Not Found", { status: 404 });
|
|
345
|
+
if (sameOrigin && !isSameOrigin(req)) return new Response("Forbidden", { status: 403 });
|
|
346
|
+
},
|
|
347
|
+
async message(peer, message) {
|
|
348
|
+
const parsed = parseMessage(message, maxBytes);
|
|
349
|
+
if (!parsed.ok) {
|
|
350
|
+
peer.send(JSON.stringify({
|
|
351
|
+
jsonrpc: "2.0",
|
|
352
|
+
error: {
|
|
353
|
+
code: -32600,
|
|
354
|
+
message: parsed.tooLarge ? "Payload too large" : "Parse error"
|
|
355
|
+
},
|
|
356
|
+
id: null
|
|
357
|
+
}));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const abort = new AbortController();
|
|
361
|
+
peer.onClose?.(() => abort.abort());
|
|
362
|
+
const host = peer.request?.headers.get("host") ?? "localhost";
|
|
363
|
+
const headers = new Headers(peer.request?.headers);
|
|
364
|
+
headers.set("content-type", NDJSON_CONTENT);
|
|
365
|
+
const peerCtx = await options.createContext?.(peer) ?? peer.context;
|
|
366
|
+
await sendNdjsonFrames(peer, await createActionHandler(group, handlers, {
|
|
367
|
+
...baseOptions,
|
|
368
|
+
createContext: (req) => ({
|
|
369
|
+
...peerCtx,
|
|
370
|
+
req
|
|
371
|
+
})
|
|
372
|
+
})(new Request(`http://${host}${path}`, {
|
|
373
|
+
method: "POST",
|
|
374
|
+
headers,
|
|
375
|
+
body: parsed.value,
|
|
376
|
+
signal: abort.signal
|
|
377
|
+
})), abort.signal);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
export { extractJsonRpcRequestIds as a, scrubRpcMessage as c, ensureNdjsonBody as i, createActionHandler as n, scrubNdjsonTransform as o, disposeActionHandler as r, scrubRpcJson as s, createWsHooks as t };
|
package/dist/rpc.d.mts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { t as ActionContext } from "./context-C1UFQ0Zc.mjs";
|
|
2
|
+
import { RpcClientOptions, createClient } from "./rpc/client.mjs";
|
|
3
|
+
import { Layer, Stream } from "effect";
|
|
4
|
+
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
5
|
+
//#region src/rpc/server.d.ts
|
|
6
|
+
type ActionHandlerOptions = {
|
|
7
|
+
path?: string;
|
|
8
|
+
sameOrigin?: boolean;
|
|
9
|
+
transport?: "http" | "ws";
|
|
10
|
+
createContext?: (req: Request) => ActionContext | Promise<ActionContext>;
|
|
11
|
+
};
|
|
12
|
+
type ActionGroup = RpcGroup.RpcGroup<Rpc.Any>;
|
|
13
|
+
declare function createActionHandler(group: ActionGroup, handlers: Layer.Layer<unknown, unknown, unknown>, options?: ActionHandlerOptions): (request: Request) => Promise<Response>;
|
|
14
|
+
declare function disposeActionHandler(group: ActionGroup, path?: string, transport?: "http" | "ws"): Promise<void>;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/rpc/ws.d.ts
|
|
17
|
+
type WsPeer = {
|
|
18
|
+
request?: Request;
|
|
19
|
+
context: Record<string, unknown>;
|
|
20
|
+
send: (data: unknown) => unknown;
|
|
21
|
+
/** Optional: register a listener when the peer disconnects. */
|
|
22
|
+
onClose?: (fn: () => void) => void;
|
|
23
|
+
};
|
|
24
|
+
type WsMessage = {
|
|
25
|
+
text: () => string;
|
|
26
|
+
};
|
|
27
|
+
type WsHooksOptions = {
|
|
28
|
+
path?: string;
|
|
29
|
+
sameOrigin?: boolean;
|
|
30
|
+
maxMessageSize?: number;
|
|
31
|
+
createContext?: (peer: WsPeer) => ActionContext | Promise<ActionContext>;
|
|
32
|
+
};
|
|
33
|
+
declare function createWsHooks(group: RpcGroup.RpcGroup<Rpc.Any>, handlers: Layer.Layer<unknown, unknown, unknown>, options?: WsHooksOptions): {
|
|
34
|
+
upgrade(req: Request): Response | undefined;
|
|
35
|
+
message(peer: WsPeer, message: WsMessage): Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/rpc/stream.d.ts
|
|
39
|
+
declare function asyncGenToStream<T>(gen: AsyncGenerator<T, unknown, unknown>): Stream.Stream<T, Error, never>;
|
|
40
|
+
/**
|
|
41
|
+
* Re-enter `run` for every generator pull so AsyncLocalStorage request context
|
|
42
|
+
* stays available across yields (Effect may drain the stream after the outer ALS scope ends).
|
|
43
|
+
*/
|
|
44
|
+
declare function bindAsyncGenContext<T>(gen: AsyncGenerator<T, unknown, unknown>, run: <R>(fn: () => R) => R): AsyncGenerator<T, unknown, unknown>;
|
|
45
|
+
/** Create a generator inside `run`, then keep every subsequent pull inside `run`. */
|
|
46
|
+
declare function asyncGenToStreamInContext<T>(create: () => AsyncGenerator<T, unknown, unknown>, run: <R>(fn: () => R) => R): Stream.Stream<T, Error, never>;
|
|
47
|
+
declare function streamToAsyncGen<T>(stream: Stream.Stream<T>): AsyncIterable<T>;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/rpc/scrub.d.ts
|
|
50
|
+
/** Mutable repair state so each Defect can claim a distinct originating request id. */
|
|
51
|
+
type IdRepairState = {
|
|
52
|
+
/** Request ids not yet claimed by a terminal (non-chunk) response. */
|
|
53
|
+
remaining: Set<unknown>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Scrub one JSON-RPC response object.
|
|
57
|
+
* Effect encodes Defects with `id: -32603`; reclaim the originating request id from `state.remaining`.
|
|
58
|
+
*/
|
|
59
|
+
declare function scrubRpcMessage(msg: unknown, requestIds?: readonly unknown[], state?: IdRepairState): unknown;
|
|
60
|
+
/**
|
|
61
|
+
* Rewrite a JSON / NDJSON body so clients never see Effect `_tag` / `data` trees.
|
|
62
|
+
* Accepts a single object, a JSON array, or newline-delimited frames.
|
|
63
|
+
*/
|
|
64
|
+
declare function scrubRpcJson(body: string, requestIds?: readonly unknown[]): string;
|
|
65
|
+
/** Collect JSON-RPC request ids from a unary object or batch array body. */
|
|
66
|
+
declare function extractJsonRpcRequestIds(body: ArrayBuffer | Uint8Array | string): unknown[];
|
|
67
|
+
/** Ensure a body is a valid NDJSON frame (Effect's ndJsonRpc decode requires a trailing newline). */
|
|
68
|
+
declare function ensureNdjsonBody(buf: ArrayBuffer): Uint8Array<ArrayBuffer>;
|
|
69
|
+
/**
|
|
70
|
+
* TransformStream that scrubs Effect defect payloads one NDJSON line at a time,
|
|
71
|
+
* so long-running stream actions stay incremental.
|
|
72
|
+
*/
|
|
73
|
+
declare function scrubNdjsonTransform(requestIds?: readonly unknown[]): TransformStream<Uint8Array, Uint8Array>;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { type ActionHandlerOptions, type RpcClientOptions, type WsHooksOptions, asyncGenToStream, asyncGenToStreamInContext, bindAsyncGenContext, createActionHandler, createClient, createWsHooks, disposeActionHandler, ensureNdjsonBody, extractJsonRpcRequestIds, scrubNdjsonTransform, scrubRpcJson, scrubRpcMessage, streamToAsyncGen };
|
package/dist/rpc.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as streamToAsyncGen, i as bindAsyncGenContext, n as asyncGenToStream, r as asyncGenToStreamInContext, t as createClient } from "./client-Bc4g9AEw.mjs";
|
|
2
|
+
import { a as extractJsonRpcRequestIds, c as scrubRpcMessage, i as ensureNdjsonBody, n as createActionHandler, o as scrubNdjsonTransform, r as disposeActionHandler, s as scrubRpcJson, t as createWsHooks } from "./rpc-DYFEdah8.mjs";
|
|
3
|
+
export { asyncGenToStream, asyncGenToStreamInContext, bindAsyncGenContext, createActionHandler, createClient, createWsHooks, disposeActionHandler, ensureNdjsonBody, extractJsonRpcRequestIds, scrubNdjsonTransform, scrubRpcJson, scrubRpcMessage, streamToAsyncGen };
|
package/dist/rsbuild.mjs
CHANGED
package/dist/vite.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as linkedom from "linkedom";
|
|
2
|
+
//#region src/worker-dom.ts
|
|
3
|
+
/** Install a minimal DOM on `globalThis` for Ilha `renderToString` in Workers. */
|
|
4
|
+
function ensureWorkerDom() {
|
|
5
|
+
if (typeof globalThis.document !== "undefined") return;
|
|
6
|
+
const window = linkedom.parseHTML("<!DOCTYPE html><html><body></body></html>");
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
g.document = window.document;
|
|
9
|
+
g.window = window;
|
|
10
|
+
for (const [key, value] of Object.entries(linkedom)) {
|
|
11
|
+
if (key === "parseHTML" || key === "parseJSON" || key === "toJSON" || key === "Document") continue;
|
|
12
|
+
if (typeof value === "function" && /^[A-Z]/.test(key)) g[key] = value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ensureWorkerDom };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxidejs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Vite/Rsbuild plugin. One build → dist/server.js + optional client. Server actions via *.server.ts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -45,6 +45,22 @@
|
|
|
45
45
|
"./client": {
|
|
46
46
|
"types": "./client.d.ts"
|
|
47
47
|
},
|
|
48
|
+
"./rpc": {
|
|
49
|
+
"types": "./dist/rpc.d.mts",
|
|
50
|
+
"import": "./dist/rpc.mjs"
|
|
51
|
+
},
|
|
52
|
+
"./rpc/client": {
|
|
53
|
+
"types": "./dist/rpc/client.d.mts",
|
|
54
|
+
"import": "./dist/rpc/client.mjs"
|
|
55
|
+
},
|
|
56
|
+
"./worker-dom": {
|
|
57
|
+
"types": "./dist/worker-dom.d.mts",
|
|
58
|
+
"import": "./dist/worker-dom.mjs"
|
|
59
|
+
},
|
|
60
|
+
"./worker-dom/install": {
|
|
61
|
+
"types": "./dist/worker-dom/install.d.mts",
|
|
62
|
+
"import": "./dist/worker-dom/install.mjs"
|
|
63
|
+
},
|
|
48
64
|
"./tsconfig": "./tsconfig.app.json"
|
|
49
65
|
},
|
|
50
66
|
"scripts": {
|
|
@@ -56,15 +72,13 @@
|
|
|
56
72
|
"prepublishOnly": "bun run build"
|
|
57
73
|
},
|
|
58
74
|
"dependencies": {
|
|
75
|
+
"effect": "~4.0.0-rc.112",
|
|
76
|
+
"linkedom": "^0.18.13",
|
|
59
77
|
"unplugin": "^3.3.0"
|
|
60
78
|
},
|
|
61
|
-
"devDependencies": {
|
|
62
|
-
"tacho": "^0.6.1"
|
|
63
|
-
},
|
|
64
79
|
"peerDependencies": {
|
|
65
80
|
"@rsbuild/core": "*",
|
|
66
81
|
"crossws": "*",
|
|
67
|
-
"tacho": "*",
|
|
68
82
|
"vite": "*"
|
|
69
83
|
},
|
|
70
84
|
"peerDependenciesMeta": {
|
|
@@ -76,9 +90,6 @@
|
|
|
76
90
|
},
|
|
77
91
|
"vite": {
|
|
78
92
|
"optional": true
|
|
79
|
-
},
|
|
80
|
-
"tacho": {
|
|
81
|
-
"optional": true
|
|
82
93
|
}
|
|
83
94
|
},
|
|
84
95
|
"engines": {
|
package/virtual.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
declare module "virtual:oxide/actions" {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import type { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
3
|
+
import type { Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
const actionsGroup: RpcGroup.RpcGroup<Rpc.Any>;
|
|
6
|
+
export const actionsHandlers: Layer.Layer<unknown, unknown, unknown>;
|
|
7
|
+
export default actionsGroup;
|
|
8
|
+
export { actionsGroup as actions };
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
declare module "virtual:oxide/client" {
|