oxidejs 0.2.4 → 0.3.1

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 CHANGED
@@ -45,11 +45,11 @@ oxide({
45
45
  });
46
46
  ```
47
47
 
48
- `"celld"` writes `dist/wrangler.jsonc` for celld, a self-hosted alternative to Cloudflare Workers, and skips asset serving (`ASSETS` does that).
48
+ `"celld"` writes `dist/wrangler.jsonc` for celld, a self-hosted alternative to Cloudflare Workers, and skips asset serving (`ASSETS` does that). The generated worker imports `oxidejs/worker-dom/install` so Ilha SSR has a DOM before your entry evaluates. Oxide merges `nodejs_compat` into `compatibility_flags` when you do not set it.
49
49
 
50
50
  ## Server actions
51
51
 
52
- Install `tacho` if you use actions. Files named `*.server.ts`, `*.server.tsx`, `*.server.js`, or `*.server.jsx` are server-only. A client import is replaced with a tacho stub that POSTs `/__oxide/action`. The original module never enters the client graph. **Only exports wrapped in `action()` become remote actions** — any other export stays server-local and is not callable over the wire. Server and Vite SSR (`import.meta.env.SSR === true`) keep the real functions. Methods are `<file>.<fn>` (`test.ping`). Call `useRequest()` inside an action for the inbound `Request`. `useCtx()` is tacho `ctx` (`{ req }` plus anything middleware or `createContext` added). On `preset: "celld"`, `useEnv()` and `useFetchCtx()` are the Worker `env` and `ctx` from `fetch(request, env, ctx)` — same values as `useCtx().env` / `useCtx().fetchCtx`. Return `undefined` from `src/server.ts` to fall through to static files. No server action files → the bundle does not import tacho.
52
+ Files named `*.server.ts`, `*.server.tsx`, `*.server.js`, or `*.server.jsx` are server-only. A client import is replaced with an Effect RPC stub that POSTs `/__oxide/action` as newline-delimited JSON-RPC (`application/json-rpc`). The original module never enters the client graph. **Only exports wrapped in `action()` become remote actions** — any other export stays server-local and is not callable over the wire. Server and Vite SSR (`import.meta.env.SSR === true`) keep the real functions. Methods are `<file>.<fn>` (`test.ping`). Call `useRequest()` inside an action for the inbound `Request`. `useCtx()` is the request context (`{ req }` plus anything middleware or `createContext` added). On `preset: "celld"`, `useEnv()` and `useFetchCtx()` are the Worker `env` and `ctx` from `fetch(request, env, ctx)` — same values as `useCtx().env` / `useCtx().fetchCtx`. Return `undefined` from `src/server.ts` to fall through to static files. No server action files → the bundle does not import `oxidejs/rpc`. `action()` results are JSON-RPC data — returning a `Response` from an action is an error; return a raw `Response` from `src/server.ts` for raw HTTP responses.
53
53
 
54
54
  ```ts
55
55
  // src/test.server.ts
@@ -78,7 +78,18 @@ export default {
78
78
  };
79
79
  ```
80
80
 
81
- `action()` is runtime identity — it marks the export and adds a typed transport-only `{ signal }` argument. Wrap `async function*` in it to stream over tacho SSE. Inside server code, always read the non-optional signal from `useRequest().signal`:
81
+ ### Call shape
82
+
83
+ Unary actions return a Promise and expose helpers for UI wiring:
84
+
85
+ | Call | What it does |
86
+ | ------------------------------------------- | ------------------------------------------------ |
87
+ | `await ping()` | Run the action (always invokes RPC on client) |
88
+ | `ping.set(...args)` | Same as calling with args; also writes the atom |
89
+ | `ping.bind(...args)` / `ping.with(...args)` | Return an event handler that invokes the action |
90
+ | `ping.result` | Read the last `AsyncResult` from the client atom |
91
+
92
+ `action()` marks the export and adds a typed transport-only `{ signal }` argument. Wrap `async function*` in it to stream over Effect RPC as newline-delimited JSON-RPC (not SSE). On the client the stub returns an async generator — iterate it directly. Inside server code, always read the non-optional signal from `useRequest().signal`:
82
93
 
83
94
  ```ts
84
95
  // src/test.server.ts
@@ -93,10 +104,14 @@ export const ticks = action(async function* (n: number) {
93
104
  import { ticks } from "./test.server";
94
105
 
95
106
  const ac = new AbortController();
96
- const stream = await ticks(10, { signal: ac.signal });
107
+ for await (const value of ticks(10, { signal: ac.signal })) {
108
+ console.log(value);
109
+ }
97
110
  ac.abort();
98
111
  ```
99
112
 
113
+ Stream actions do not support `bind` / `with`. Breaking the `for await` loop or calling `return()` on the generator cleans up the server generator.
114
+
100
115
  `vite dev` and `rsbuild dev` serve the endpoint via middleware. `actions: "http"` (default) serves `/__oxide/action`; `actions: "ws"` uses a WebSocket instead (needs `crossws`; not with `preset: "celld"`). `actions.sameOrigin` defaults to `true` for both transports; set it to `false` only when you intentionally accept cross-origin requests. Set `actions.path` to move the endpoint. `actionHeaders` are static headers on the shared HTTP client and are ignored for WebSocket actions.
101
116
 
102
117
  ## Rsbuild
@@ -123,7 +138,7 @@ Same factory as Vite: client stubs, `/__oxide/action`, and `dist/server.js`.
123
138
  | `clientDir` | `client` | Must stay inside `outDir` |
124
139
  | `wrangler.name` | required if `emitConfig` | |
125
140
  | `wrangler.compatibility_date` | required if `emitConfig` | |
126
- | `wrangler.compatibility_flags` | — | optional |
141
+ | `wrangler.compatibility_flags` | — | optional; `nodejs_compat` is merged in automatically on `celld` |
127
142
  | `wrangler.durable_objects` | — | optional |
128
143
  | `wrangler.migrations` | — | optional |
129
144
  | `wrangler.services` | — | optional |
@@ -176,13 +191,16 @@ The generated `__asset` function uses `path.join` — not `path.resolve` — so
176
191
 
177
192
  ### Server actions (`*.server.{ts,tsx,js,jsx}`)
178
193
 
179
- - Server action code is **never bundled into the client**. Client imports are replaced with tacho stubs that POST the action endpoint (default `/__oxide/action`). The original source stays server-only.
194
+ - Server action code is **never bundled into the client**. Client imports are replaced with Effect RPC stubs that POST the action endpoint (default `/__oxide/action`). The original source stays server-only.
180
195
  - Only `action()`-wrapped exports are exposed as RPC; other exports stay server-local.
196
+ - Stream actions use newline-delimited JSON-RPC (`application/json-rpc`) over that same endpoint — not Server-Sent Events. Frames are scrubbed as they flush on HTTP and WebSocket.
181
197
  - The endpoint is POST-only. Non-POST requests return `405`.
182
198
  - Method dispatch uses `Object.hasOwn`, blocking `__proto__` / `constructor` walks.
183
199
  - Unknown or missing content-types → `415`.
184
200
  - Body size capped at 1 MB by default (enforced on the actual body, not just `Content-Length`).
185
201
  - Batch requests capped at 20 items (both HTTP and WebSocket transports).
202
+ - Effect `Defect` / `Cause` payloads are scrubbed before they leave the endpoint. Clients see plain JSON-RPC errors (`code` + `message` only). Thrown messages become `Internal error` (`-32603`). Unknown methods → `-32601`; invalid params → `-32602`.
203
+ - `actions.sameOrigin` defaults to `true`. Requests without both `Origin` and `Sec-Fetch-Site` are rejected when that check is on.
186
204
 
187
205
  ### Host header
188
206
 
@@ -0,0 +1,148 @@
1
+ import { Effect, Layer, Scope, Stream } from "effect";
2
+ import { RpcClient, RpcSchema, RpcSerialization } from "effect/unstable/rpc";
3
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
4
+ import { Socket } from "effect/unstable/socket";
5
+ //#region src/rpc/stream.ts
6
+ function asyncGenToStream(gen) {
7
+ return Stream.fromAsyncIterable(gen, (error) => error instanceof Error ? error : new Error(String(error)));
8
+ }
9
+ /**
10
+ * Re-enter `run` for every generator pull so AsyncLocalStorage request context
11
+ * stays available across yields (Effect may drain the stream after the outer ALS scope ends).
12
+ */
13
+ function bindAsyncGenContext(gen, run) {
14
+ return {
15
+ next: (value) => run(() => gen.next(value)),
16
+ return: (value) => run(() => gen.return(value)),
17
+ throw: (error) => run(() => gen.throw(error)),
18
+ [Symbol.asyncIterator]() {
19
+ return this;
20
+ },
21
+ async [Symbol.asyncDispose]() {
22
+ await run(() => gen.return(void 0));
23
+ }
24
+ };
25
+ }
26
+ /** Create a generator inside `run`, then keep every subsequent pull inside `run`. */
27
+ function asyncGenToStreamInContext(create, run) {
28
+ return asyncGenToStream(bindAsyncGenContext(run(create), run));
29
+ }
30
+ function streamToAsyncGen(stream) {
31
+ return Stream.toAsyncIterable(stream);
32
+ }
33
+ //#endregion
34
+ //#region src/rpc/client.ts
35
+ const clientCache = /* @__PURE__ */ new Map();
36
+ let nextGroupId = 0;
37
+ function cacheKey(group, options) {
38
+ const headerKey = options.headers ? Object.entries(options.headers).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join("&") : "";
39
+ let groupId = group.__oxideClientId;
40
+ if (groupId === void 0) {
41
+ groupId = ++nextGroupId;
42
+ group.__oxideClientId = groupId;
43
+ }
44
+ return `${groupId}|${options.transport ?? "http"}|${options.url}|${headerKey}`;
45
+ }
46
+ function httpLayer(options) {
47
+ const headers = options.headers;
48
+ return RpcClient.layerProtocolHttp({
49
+ url: options.url,
50
+ ...headers ? { transformClient: (client) => HttpClient.mapRequest(client, (req) => {
51
+ for (const [key, value] of Object.entries(headers)) req = HttpClientRequest.setHeader(req, key, value);
52
+ return req;
53
+ }) } : {}
54
+ }).pipe(Layer.provide(RpcSerialization.layerNdJsonRpc()), Layer.provide(FetchHttpClient.layer));
55
+ }
56
+ function wsLayer(url) {
57
+ return RpcClient.layerProtocolSocket().pipe(Layer.provide(RpcSerialization.layerNdJsonRpc()), Layer.provide(Socket.layerWebSocket(url)), Layer.provide(Socket.layerWebSocketConstructorGlobal));
58
+ }
59
+ function clientLayer(options) {
60
+ return options.transport === "ws" ? wsLayer(options.url) : httpLayer(options);
61
+ }
62
+ function loadClient(group, options) {
63
+ return Effect.gen(function* () {
64
+ const scope = yield* Scope.make();
65
+ const context = yield* Scope.provide(scope)(Layer.build(clientLayer(options)));
66
+ return yield* Scope.provide(scope)(RpcClient.make(group).pipe(Effect.provide(context)));
67
+ });
68
+ }
69
+ function isStreamResult(value) {
70
+ return Stream.isStream(value);
71
+ }
72
+ function isStreamTag(group, tag) {
73
+ const rpc = group.requests.get(tag);
74
+ if (!rpc?.successSchema) return false;
75
+ return RpcSchema.isStreamSchema(rpc.successSchema);
76
+ }
77
+ function streamToAsyncGenerator(stream, signal) {
78
+ const iterable = streamToAsyncGen(stream);
79
+ return (async function* () {
80
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
81
+ const iterator = iterable[Symbol.asyncIterator]();
82
+ const onAbort = () => {
83
+ iterator.return?.();
84
+ };
85
+ signal?.addEventListener("abort", onAbort, { once: true });
86
+ try {
87
+ while (true) {
88
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
89
+ const next = await iterator.next();
90
+ if (next.done) return void 0;
91
+ yield next.value;
92
+ }
93
+ } finally {
94
+ signal?.removeEventListener("abort", onAbort);
95
+ }
96
+ })();
97
+ }
98
+ function callFlat(client, tag, args, callOpts) {
99
+ const caller = client[tag];
100
+ if (!caller) return Promise.reject(/* @__PURE__ */ new Error(`Unknown action ${tag}`));
101
+ const result = caller({ args }, callOpts);
102
+ if (isStreamResult(result)) return streamToAsyncGenerator(result, callOpts?.signal);
103
+ return Effect.runPromise(result, { signal: callOpts?.signal });
104
+ }
105
+ function nestClient(group, flat) {
106
+ const nested = {};
107
+ for (const tag of group.requests.keys()) {
108
+ const dot = tag.indexOf(".");
109
+ if (dot === -1) continue;
110
+ const mod = tag.slice(0, dot);
111
+ const name = tag.slice(dot + 1);
112
+ nested[mod] ??= {};
113
+ nested[mod][name] = (...args) => {
114
+ const opts = args.at(-1);
115
+ const hasSignal = opts && typeof opts === "object" && "signal" in opts && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1;
116
+ const params = hasSignal ? args.slice(0, -1) : args;
117
+ return callFlat(flat, tag, params, hasSignal ? opts : void 0);
118
+ };
119
+ }
120
+ return nested;
121
+ }
122
+ function createClient(group, options) {
123
+ const key = cacheKey(group, options);
124
+ let entry = clientCache.get(key);
125
+ if (!entry) {
126
+ entry = { pending: Effect.runPromise(loadClient(group, options)).then((flat) => {
127
+ return nestClient(group, flat);
128
+ }).catch((error) => {
129
+ clientCache.delete(key);
130
+ throw error;
131
+ }) };
132
+ clientCache.set(key, entry);
133
+ }
134
+ return new Proxy({}, { get(_target, mod) {
135
+ if (typeof mod !== "string") return void 0;
136
+ return new Proxy({}, { get(_inner, name) {
137
+ if (typeof name !== "string") return void 0;
138
+ if (isStreamTag(group, `${mod}.${name}`)) return (...args) => (async function* () {
139
+ const out = (await entry.pending)[mod]?.[name]?.(...args);
140
+ if (!out) throw new Error(`Unknown action ${mod}.${name}`);
141
+ yield* out;
142
+ })();
143
+ return (...args) => entry.pending.then((client) => client[mod]?.[name]?.(...args) ?? Promise.reject(/* @__PURE__ */ new Error(`Unknown action ${mod}.${name}`)));
144
+ } });
145
+ } });
146
+ }
147
+ //#endregion
148
+ export { streamToAsyncGen as a, bindAsyncGenContext as i, asyncGenToStream as n, asyncGenToStreamInContext as r, createClient as t };
@@ -0,0 +1,64 @@
1
+ import * as Atom from "effect/unstable/reactivity/Atom";
2
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
3
+ //#region src/action.d.ts
4
+ declare const ACTION_CALL: unique symbol;
5
+ type CallOptions = {
6
+ signal?: AbortSignal;
7
+ };
8
+ type ServerActionHandle<Args extends unknown[], A> = {
9
+ (...args: Args | [...Args, CallOptions]): Promise<A>;
10
+ set(...args: Args): Promise<A>;
11
+ bind(...args: Args): (...ev: unknown[]) => void;
12
+ with(...args: Args): (...ev: unknown[]) => void;
13
+ /** Last `AsyncResult` for this action's client atom (does not invoke RPC). */
14
+ readonly result: AsyncResult.AsyncResult<A, unknown>;
15
+ readonly atom: Atom.Atom<unknown>;
16
+ readonly $$atom: 1;
17
+ };
18
+ type StreamActionHandle<Args extends unknown[], Y, R = void> = {
19
+ (...args: Args | [...Args, CallOptions]): AsyncGenerator<Y, R, undefined>;
20
+ set(...args: Args): AsyncGenerator<Y, R, undefined>;
21
+ bind(...args: Args): (...ev: unknown[]) => void;
22
+ with(...args: Args): (...ev: unknown[]) => void;
23
+ readonly atom: undefined;
24
+ readonly $$atom: 1;
25
+ };
26
+ /** Attach an ilha server-island capture key to an action handle. */
27
+ declare function brandServerAction<Args extends unknown[], A>(key: string, handle: ServerActionHandle<Args, A>): ServerActionHandle<Args, A>;
28
+ /** Wrap an RPC caller as an `Atom.fn`-shaped client action handle. */
29
+ declare function wrapClientRpc<Args extends unknown[], A>(rpc: (...args: Args | [...Args, CallOptions]) => Promise<A>): ServerActionHandle<Args, A>;
30
+ /**
31
+ * Wrap a streaming RPC caller so the client handle returns an async generator
32
+ * (awaiting the underlying client lazily), not `Promise<AsyncGenerator>`.
33
+ */
34
+ declare function wrapClientStreamRpc<Args extends unknown[], Y, R = void>(rpc: (...args: Args | [...Args, CallOptions]) => AsyncGenerator<Y, R, undefined>): StreamActionHandle<Args, Y, R>;
35
+ /**
36
+ * Marks a `*.server.ts` export as a remote RPC action. On the server the
37
+ * underlying function runs locally; on the client the build replaces the module
38
+ * with an `Atom.fn`-shaped RPC handle (`set`, `bind`, `result`).
39
+ */
40
+ declare function action<Args extends unknown[], Y, R = void>(fn: (...args: Args) => AsyncGenerator<Y, R, unknown>): StreamActionHandle<Args, Y, R>;
41
+ declare function action<Args extends unknown[], Result>(fn: (...args: Args) => Result): ServerActionHandle<Args, Awaited<Result>>;
42
+ //#endregion
43
+ //#region src/context.d.ts
44
+ type ExecutionContext = {
45
+ waitUntil?(promise: Promise<unknown>): void;
46
+ passThroughOnException?(): void;
47
+ };
48
+ /** RPC procedure request context. Starts as `{ req }` plus Worker extras. Middleware can add fields. */
49
+ type ActionContext = {
50
+ req: Request;
51
+ env?: unknown;
52
+ fetchCtx?: ExecutionContext;
53
+ [key: string]: unknown;
54
+ };
55
+ /** Current RPC or host request context. Throws outside request handling. */
56
+ declare function useCtx<C extends ActionContext = ActionContext>(): C;
57
+ /** Current server `Request`. Available in actions, SSR, and frame renders. */
58
+ declare function useRequest(): Request;
59
+ /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
60
+ declare function useEnv<E = unknown>(): E | undefined;
61
+ /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). `undefined` on Node. */
62
+ declare function useFetchCtx(): ExecutionContext | undefined;
63
+ //#endregion
64
+ export { useFetchCtx as a, ServerActionHandle as c, brandServerAction as d, wrapClientRpc as f, useEnv as i, StreamActionHandle as l, ExecutionContext as n, useRequest as o, wrapClientStreamRpc as p, useCtx as r, ACTION_CALL as s, ActionContext as t, action as u };
@@ -0,0 +1,42 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/context.ts
3
+ const ALS_KEY = Symbol.for("oxidejs.requestContext");
4
+ const FETCH_KEY = Symbol.for("oxidejs.fetch");
5
+ function als() {
6
+ const g = globalThis;
7
+ return g[ALS_KEY] ??= new AsyncLocalStorage();
8
+ }
9
+ function store() {
10
+ const current = als().getStore();
11
+ if (!current) throw new Error("oxidejs: request context is unavailable");
12
+ return current;
13
+ }
14
+ /** Current RPC or host request context. Throws outside request handling. */
15
+ function useCtx() {
16
+ return store();
17
+ }
18
+ /** Current server `Request`. Available in actions, SSR, and frame renders. */
19
+ function useRequest() {
20
+ return store().req;
21
+ }
22
+ /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
23
+ function useEnv() {
24
+ return store().env;
25
+ }
26
+ /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). `undefined` on Node. */
27
+ function useFetchCtx() {
28
+ return store().fetchCtx;
29
+ }
30
+ function runWithRequest(req, fn, extra) {
31
+ return als().run({
32
+ ...extra,
33
+ req
34
+ }, fn);
35
+ }
36
+ const HOOK_KEY = Symbol.for("oxidejs.runWithRequest");
37
+ globalThis[HOOK_KEY] ??= (req, fn) => {
38
+ const extra = req[FETCH_KEY];
39
+ return runWithRequest(req, fn, extra);
40
+ };
41
+ //#endregion
42
+ export { useRequest as a, useFetchCtx as i, useCtx as n, useEnv as r, runWithRequest as t };
package/dist/index.d.mts CHANGED
@@ -1,30 +1,3 @@
1
+ import { a as useFetchCtx, c as ServerActionHandle, d as brandServerAction, f as wrapClientRpc, i as useEnv, l as StreamActionHandle, n as ExecutionContext, o as useRequest, p as wrapClientStreamRpc, r as useCtx, s as ACTION_CALL, t as ActionContext, u as action } from "./context-C1UFQ0Zc.mjs";
1
2
  import { a as OxidejsWranglerOptions, i as OxidejsPreset, n as OxidejsActionTransport, o as ResolvedOptions, r as OxidejsOptions, t as OxidejsActionHeaders } from "./types-BM4NAnzy.mjs";
2
- //#region src/context.d.ts
3
- type ExecutionContext = {
4
- waitUntil?(promise: Promise<unknown>): void;
5
- passThroughOnException?(): void;
6
- };
7
- /** Tacho procedure `ctx`. Starts as `{ req }` plus Worker extras. Middleware can add fields. */
8
- type ActionContext = {
9
- req: Request;
10
- env?: unknown;
11
- fetchCtx?: ExecutionContext;
12
- [key: string]: unknown;
13
- };
14
- /** Current tacho or host request context. Throws outside request handling. */
15
- declare function useCtx<C extends ActionContext = ActionContext>(): C;
16
- /** Current server `Request`. Available in actions, SSR, and frame renders. */
17
- declare function useRequest(): Request;
18
- /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
19
- declare function useEnv<E = unknown>(): E | undefined;
20
- /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). Not tacho `ctx`. `undefined` on Node. */
21
- declare function useFetchCtx(): ExecutionContext | undefined;
22
- /**
23
- * Marks a `*.server.ts` export as a remote RPC action. Runtime identity; the
24
- * second call signature adds the transport-only `{ signal }` argument.
25
- */
26
- declare function action<Args extends unknown[], Result>(fn: (...args: Args) => Result): typeof fn & ((...args: [...Args, options: {
27
- signal?: AbortSignal;
28
- }]) => Result);
29
- //#endregion
30
- export { type ActionContext, type ExecutionContext, type OxidejsActionHeaders, type OxidejsActionTransport, type OxidejsOptions, type OxidejsPreset, type OxidejsWranglerOptions, type ResolvedOptions, action, useCtx, useEnv, useFetchCtx, useRequest };
3
+ export { ACTION_CALL, type ActionContext, type ExecutionContext, type OxidejsActionHeaders, type OxidejsActionTransport, type OxidejsOptions, type OxidejsPreset, type OxidejsWranglerOptions, type ResolvedOptions, type ServerActionHandle, type StreamActionHandle, action, brandServerAction, useCtx, useEnv, useFetchCtx, useRequest, wrapClientRpc, wrapClientStreamRpc };
package/dist/index.mjs CHANGED
@@ -1,49 +1,108 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- //#region src/context.ts
3
- const ALS_KEY = Symbol.for("oxidejs.requestContext");
4
- const FETCH_KEY = Symbol.for("oxidejs.fetch");
5
- function als() {
6
- const g = globalThis;
7
- return g[ALS_KEY] ??= new AsyncLocalStorage();
8
- }
9
- function store() {
10
- const current = als().getStore();
11
- if (!current) throw new Error("oxidejs: request context is unavailable");
12
- return current;
13
- }
14
- /** Current tacho or host request context. Throws outside request handling. */
15
- function useCtx() {
16
- return store();
17
- }
18
- /** Current server `Request`. Available in actions, SSR, and frame renders. */
19
- function useRequest() {
20
- return store().req;
21
- }
22
- /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
23
- function useEnv() {
24
- return store().env;
25
- }
26
- /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). Not tacho `ctx`. `undefined` on Node. */
27
- function useFetchCtx() {
28
- return store().fetchCtx;
29
- }
30
- function runWithRequest(req, fn, extra) {
31
- return als().run({
32
- ...extra,
33
- req
34
- }, fn);
35
- }
36
- const HOOK_KEY = Symbol.for("oxidejs.runWithRequest");
37
- globalThis[HOOK_KEY] ??= (req, fn) => {
38
- const extra = req[FETCH_KEY];
39
- return runWithRequest(req, fn, extra);
40
- };
1
+ import { a as useRequest, i as useFetchCtx, n as useCtx, r as useEnv } from "./context-zrTZyYpF.mjs";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Atom from "effect/unstable/reactivity/Atom";
4
+ import * as Registry from "effect/unstable/reactivity/AtomRegistry";
5
+ //#region src/action.ts
6
+ const ACTION_CALL = Symbol.for("ilha.actionCall");
7
+ const ACTION_KEY = Symbol.for("oxidejs.actionKey");
8
+ const AsyncGeneratorFunction = Object.getPrototypeOf(async function* () {}).constructor;
9
+ let defaultRegistry;
10
+ function registry() {
11
+ return defaultRegistry ??= Registry.make();
12
+ }
13
+ function isAsyncGeneratorFunction(fn) {
14
+ return typeof fn === "function" && fn instanceof AsyncGeneratorFunction;
15
+ }
16
+ function splitCallOptions(args) {
17
+ if (args.length === 0) return { args: [] };
18
+ const last = args[args.length - 1];
19
+ if (last && typeof last === "object" && "signal" in last && last.signal instanceof AbortSignal && Object.keys(last).length === 1) return {
20
+ args: args.slice(0, -1),
21
+ options: last
22
+ };
23
+ return { args };
24
+ }
25
+ function bindHandler(key, invoke, args) {
26
+ const handler = ((..._ev) => invoke(...args));
27
+ if (key) handler[ACTION_CALL] = {
28
+ k: key,
29
+ a: args
30
+ };
31
+ return handler;
32
+ }
33
+ function defineServerAction(atom, invoke, opts) {
34
+ const captureKey = opts?.captureKey;
35
+ const stripCallOptions = opts?.stripCallOptions ?? true;
36
+ const run = ((...allArgs) => {
37
+ if (!stripCallOptions) return Promise.resolve(invoke(...allArgs));
38
+ const { args } = splitCallOptions(allArgs);
39
+ return Promise.resolve(invoke(...args));
40
+ });
41
+ const set = (...args) => {
42
+ registry().set(atom, args);
43
+ return Promise.resolve(invoke(...args));
44
+ };
45
+ const bind = (...args) => {
46
+ return bindHandler(captureKey ?? run[ACTION_KEY], (...a) => void set(...a), args);
47
+ };
48
+ Object.assign(run, {
49
+ set,
50
+ bind,
51
+ with: bind,
52
+ atom,
53
+ $$atom: 1
54
+ });
55
+ Object.defineProperty(run, "result", {
56
+ enumerable: true,
57
+ get: () => registry().get(atom)
58
+ });
59
+ if (captureKey) run[ACTION_KEY] = captureKey;
60
+ return run;
61
+ }
62
+ function defineStreamAction(fn) {
63
+ const call = ((...allArgs) => {
64
+ const { args } = splitCallOptions(allArgs);
65
+ return fn(...args);
66
+ });
67
+ const deny = () => {
68
+ throw new Error("oxidejs: stream actions cannot be bound to DOM events");
69
+ };
70
+ Object.assign(call, {
71
+ set: call,
72
+ bind: deny,
73
+ with: deny,
74
+ atom: void 0,
75
+ $$atom: 1
76
+ });
77
+ return call;
78
+ }
79
+ /** Attach an ilha server-island capture key to an action handle. */
80
+ function brandServerAction(key, handle) {
81
+ handle[ACTION_KEY] = key;
82
+ return handle;
83
+ }
84
+ /** Wrap an RPC caller as an `Atom.fn`-shaped client action handle. */
85
+ function wrapClientRpc(rpc) {
86
+ const invoke = (...args) => Promise.resolve(rpc(...args));
87
+ return defineServerAction(Atom.make(Atom.fn((packed) => Effect.tryPromise({
88
+ try: () => invoke(...packed),
89
+ catch: (error) => error instanceof Error ? error : new Error(String(error))
90
+ }))), (...args) => invoke(...args), { stripCallOptions: false });
91
+ }
41
92
  /**
42
- * Marks a `*.server.ts` export as a remote RPC action. Runtime identity; the
43
- * second call signature adds the transport-only `{ signal }` argument.
93
+ * Wrap a streaming RPC caller so the client handle returns an async generator
94
+ * (awaiting the underlying client lazily), not `Promise<AsyncGenerator>`.
44
95
  */
96
+ function wrapClientStreamRpc(rpc) {
97
+ return defineStreamAction((...args) => rpc(...args));
98
+ }
45
99
  function action(fn) {
46
- return fn;
100
+ if (isAsyncGeneratorFunction(fn)) return defineStreamAction(fn);
101
+ const invoke = (...args) => Promise.resolve(fn(...args));
102
+ return defineServerAction(Atom.make(Atom.fn((packed) => Effect.tryPromise({
103
+ try: () => invoke(...packed),
104
+ catch: (error) => error instanceof Error ? error : new Error(String(error))
105
+ }))), invoke);
47
106
  }
48
107
  //#endregion
49
- export { action, useCtx, useEnv, useFetchCtx, useRequest };
108
+ export { ACTION_CALL, action, brandServerAction, useCtx, useEnv, useFetchCtx, useRequest, wrapClientRpc, wrapClientStreamRpc };