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 CHANGED
@@ -71,23 +71,24 @@ import { ping } from "./test.server";
71
71
  console.log(await ping()); // "pong"
72
72
 
73
73
  // src/server.ts
74
- export default {
75
- fetch(request: Request) {
76
- if (new URL(request.url).pathname === "/api/ok") return new Response("ok");
77
- },
78
- };
74
+ import type { FetchHandler } from "oxidejs";
75
+
76
+ export const fetch = ((request) => {
77
+ if (new URL(request.url).pathname === "/api/ok") return new Response("ok");
78
+ return;
79
+ }) satisfies FetchHandler;
79
80
  ```
80
81
 
81
82
  ### Call shape
82
83
 
83
84
  Unary actions return a Promise and expose helpers for UI wiring:
84
85
 
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 |
86
+ | Call | What it does |
87
+ | --- | --- |
88
+ | `await ping()` | Run the action (always invokes RPC on client) |
89
+ | `ping.set(...args)` | Same as calling with args; also writes the atom |
90
+ | `ping.bind(...args)` / `ping.with(...args)` | Return an event handler that invokes the action |
91
+ | `ping.result` | Read the last `AsyncResult` from the client atom |
91
92
 
92
93
  `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`:
93
94
 
@@ -130,27 +131,27 @@ Same factory as Vite: client stubs, `/__oxide/action`, and `dist/server.js`.
130
131
 
131
132
  ## Options
132
133
 
133
- | Option | Default | Notes |
134
- | ------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
135
- | `preset` | `"fetch"` | `"fetch"` or `"celld"` |
136
- | `workerEntry` | `src/server.ts` | Relative to project root |
137
- | `outDir` | `dist` | Output root |
138
- | `clientDir` | `client` | Must stay inside `outDir` |
139
- | `wrangler.name` | required if `emitConfig` | |
140
- | `wrangler.compatibility_date` | required if `emitConfig` | |
141
- | `wrangler.compatibility_flags` | — | optional; `nodejs_compat` is merged in automatically on `celld` |
142
- | `wrangler.durable_objects` | — | optional |
143
- | `wrangler.migrations` | — | optional |
144
- | `wrangler.services` | — | optional |
145
- | `wrangler.vars` | — | optional |
146
- | `emitConfig` | `true` on `celld` | Set `false` to skip `wrangler.jsonc` |
147
- | `actions` | `"http"` | `"ws"` needs `crossws`; object form: `{ transport, path, sameOrigin }` (`sameOrigin: true`) |
148
- | `actionHeaders` | — | Static headers on the HTTP client |
149
- | `middleware` | `[]` | Fetch middleware, run in order before actions and the server entry |
150
- | `imports` | `[]` | Modules imported for side effects at server startup |
151
- | `bodyLimit` | `1048576` | Max Node request body size; larger requests get 413 |
152
- | `notFound` | — | Custom HTML 404 body when no route or asset matches |
153
- | `env` | — | Node preset value passed to `fetch(request, env, ctx)` |
134
+ | Option | Default | Notes |
135
+ | --- | --- | --- |
136
+ | `preset` | `"fetch"` | `"fetch"` or `"celld"` |
137
+ | `workerEntry` | `src/server.ts` | Relative to project root |
138
+ | `outDir` | `dist` | Output root |
139
+ | `clientDir` | `client` | Must stay inside `outDir` |
140
+ | `wrangler.name` | required if `emitConfig` | |
141
+ | `wrangler.compatibility_date` | required if `emitConfig` | |
142
+ | `wrangler.compatibility_flags` | — | optional; `nodejs_compat` is merged in automatically on `celld` |
143
+ | `wrangler.durable_objects` | — | optional |
144
+ | `wrangler.migrations` | — | optional |
145
+ | `wrangler.services` | — | optional |
146
+ | `wrangler.vars` | — | optional |
147
+ | `emitConfig` | `true` on `celld` | Set `false` to skip `wrangler.jsonc` |
148
+ | `actions` | `"http"` | `"ws"` needs `crossws`; object form: `{ transport, path, sameOrigin }` (`sameOrigin: true`) |
149
+ | `actionHeaders` | — | Static headers on the HTTP client |
150
+ | `middleware` | `[]` | Fetch middleware, run in order before actions and the server entry |
151
+ | `imports` | `[]` | Modules imported for side effects at server startup |
152
+ | `bodyLimit` | `1048576` | Max Node request body size; larger requests get 413 |
153
+ | `notFound` | — | Custom HTML 404 body when no route or asset matches |
154
+ | `env` | — | Node preset value passed to `fetch(request, env, ctx)` |
154
155
 
155
156
  ### `middleware` and `imports`
156
157
 
@@ -177,15 +178,15 @@ Middleware modules receive `(request, { env, ctx })`. They run before actions, t
177
178
 
178
179
  The generated server serves static files from `dist/client/` (or the `public/` directory merged into it). These guards are active:
179
180
 
180
- | Attack vector | Guard |
181
- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
182
- | **Traversal** (`%2e%2e/`, `..%2f`) | `__rel()` rejects paths containing `..` segments. |
183
- | **Double-slash** (`///etc/passwd`) | `__rel()` rejects results that still start with `/` after `slice(1)`. |
184
- | **Null byte** (`%00`, `\0`) | `__rel()` rejects paths containing null bytes before and after `decodeURIComponent`. |
185
- | **Absolute path** (`/etc/passwd`) | `__rel()` returns `null` for paths not starting with `/`. |
186
- | **SPA fallback** | Unknown paths → `index.html`, never a directory listing. |
187
- | **`clientDir` escape** | `resolveOptions` throws at build time if `clientDir` resolves outside `outDir`. |
188
- | **Hashed assets** | Files matching `[-.][0-9a-f]{8,}.ext` get `Cache-Control: public, max-age=31536000, immutable`. Other files are not cached by default. |
181
+ | Attack vector | Guard |
182
+ | --- | --- |
183
+ | **Traversal** (`%2e%2e/`, `..%2f`) | `__rel()` rejects paths containing `..` segments. |
184
+ | **Double-slash** (`///etc/passwd`) | `__rel()` rejects results that still start with `/` after `slice(1)`. |
185
+ | **Null byte** (`%00`, `\0`) | `__rel()` rejects paths containing null bytes before and after `decodeURIComponent`. |
186
+ | **Absolute path** (`/etc/passwd`) | `__rel()` returns `null` for paths not starting with `/`. |
187
+ | **SPA fallback** | Unknown paths → `index.html`, never a directory listing. |
188
+ | **`clientDir` escape** | `resolveOptions` throws at build time if `clientDir` resolves outside `outDir`. |
189
+ | **Hashed assets** | Files matching `[-.][0-9a-f]{8,}.ext` get `Cache-Control: public, max-age=31536000, immutable`. Other files are not cached by default. |
189
190
 
190
191
  The generated `__asset` function uses `path.join` — not `path.resolve` — so a leading `/` in the relative path stays inside the asset root.
191
192
 
@@ -201,6 +202,7 @@ The generated `__asset` function uses `path.join` — not `path.resolve` — so
201
202
  - Batch requests capped at 20 items (both HTTP and WebSocket transports).
202
203
  - 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
204
  - `actions.sameOrigin` defaults to `true`. Requests without both `Origin` and `Sec-Fetch-Site` are rejected when that check is on.
205
+ - StackBlitz WebContainers do not keep `AsyncLocalStorage` across `async/await`. Oxide detects `process.versions.webcontainer` and falls back to a sync request store, capturing context before Effect schedules work and serializing handler entry so concurrent requests do not stomp that store. Stream pulls re-enter the captured store. This is a demo/dev workaround, not a concurrency model for production.
204
206
 
205
207
  ### Host header
206
208
 
package/client.d.ts CHANGED
@@ -1,10 +1,16 @@
1
- interface AsyncGenerator<T = unknown, TReturn = any, TNext = any> extends AsyncIteratorObject<
2
- T,
3
- TReturn,
4
- TNext
5
- > {
6
- then<TResult1 = AsyncIterable<T>, TResult2 = never>(
7
- onfulfilled?: ((value: AsyncIterable<T>) => TResult1 | PromiseLike<TResult1>) | null,
8
- onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
9
- ): Promise<TResult1 | TResult2>;
1
+ interface AsyncGenerator<
2
+ T = unknown,
3
+ TReturn = unknown,
4
+ TNext = unknown,
5
+ > extends AsyncIteratorObject<T, TReturn, TNext> {
6
+ then: <TResult1 = AsyncIterable<T>, TResult2 = never>(
7
+ onfulfilled?:
8
+ | ((value: AsyncIterable<T>) => TResult1 | PromiseLike<TResult1>)
9
+ | null,
10
+ onrejected?:
11
+ | ((
12
+ reason: Error | string | number | boolean | null | undefined
13
+ ) => TResult2 | PromiseLike<TResult2>)
14
+ | null
15
+ ) => Promise<TResult1 | TResult2>;
10
16
  }
@@ -0,0 +1,207 @@
1
+ import { n as inWebcontainer } from "./context-DUN_VWJF.mjs";
2
+ import { Effect, Layer, Scope, Stream } from "effect";
3
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
4
+ import { RpcClient, RpcSchema, RpcSerialization } from "effect/unstable/rpc";
5
+ import { Socket } from "effect/unstable/socket";
6
+ //#region src/rpc/stream.ts
7
+ const onAsyncIterableError = function onAsyncIterableError(cause) {
8
+ return cause instanceof Error ? cause : new Error(String(cause));
9
+ };
10
+ const asyncGenToStream = function asyncGenToStream(gen) {
11
+ return Stream.fromAsyncIterable(gen, onAsyncIterableError);
12
+ };
13
+ /** Serialize WebContainer stream pulls so the shared syncStore is not stomped. */
14
+ let pullTail = Promise.resolve(null);
15
+ /**
16
+ * Re-enter `run` for every generator pull so request context stays available
17
+ * across yields (Effect may drain the stream after the outer ALS scope ends;
18
+ * WebContainer also loses ALS across awaits, so `run` must reinstall the store).
19
+ * On WebContainer, pulls are serialized through settlement so concurrent streams
20
+ * cannot replace the sync fallback mid-pull. `withRequestEntry` is unchanged.
21
+ */
22
+ const bindAsyncGenContext = function bindAsyncGenContext(gen, run) {
23
+ const runPull = function runPull(fn) {
24
+ if (!inWebcontainer()) return run(fn);
25
+ const { promise: gate, resolve: release } = Promise.withResolvers();
26
+ const prev = pullTail;
27
+ pullTail = gate;
28
+ return async function runSerialized() {
29
+ await prev;
30
+ try {
31
+ return await run(fn);
32
+ } finally {
33
+ release(null);
34
+ }
35
+ }();
36
+ };
37
+ return {
38
+ next: (...args) => runPull(() => gen.next(...args)),
39
+ return: (...args) => runPull(() => gen.return(...args)),
40
+ throw: (...args) => runPull(() => gen.throw(...args)),
41
+ [Symbol.asyncIterator]() {
42
+ return this;
43
+ },
44
+ async [Symbol.asyncDispose]() {
45
+ await runPull(() => gen.return(void 0));
46
+ }
47
+ };
48
+ };
49
+ /** Create a generator inside `run`, then keep every subsequent pull inside `run`. */
50
+ const asyncGenToStreamInContext = function asyncGenToStreamInContext(create, run) {
51
+ return asyncGenToStream(bindAsyncGenContext(run(create), run));
52
+ };
53
+ const streamToAsyncGen = function streamToAsyncGen(stream) {
54
+ return Stream.toAsyncIterable(stream);
55
+ };
56
+ //#endregion
57
+ //#region src/rpc/client.ts
58
+ const clientCache = /* @__PURE__ */ new Map();
59
+ let nextGroupId = 0;
60
+ const isStringPropertyKey = function isStringPropertyKey(key) {
61
+ return typeof key === "string";
62
+ };
63
+ const normalizeActionHeaders = function normalizeActionHeaders(headers) {
64
+ if (Array.isArray(headers)) {
65
+ const out = {};
66
+ for (const [key, value] of headers) out[key] = value;
67
+ return out;
68
+ }
69
+ return headers;
70
+ };
71
+ const cacheKey = function cacheKey(group, options) {
72
+ let headerKey = "";
73
+ if (options.headers) {
74
+ const entries = Object.entries(normalizeActionHeaders(options.headers));
75
+ entries.sort(([a], [b]) => a.localeCompare(b));
76
+ headerKey = entries.map(([k, v]) => `${k}=${v}`).join("&");
77
+ }
78
+ const stamped = group;
79
+ let groupId = stamped.__oxideClientId;
80
+ if (groupId === void 0) {
81
+ nextGroupId += 1;
82
+ groupId = nextGroupId;
83
+ stamped.__oxideClientId = groupId;
84
+ }
85
+ return `${groupId}|${options.transport ?? "http"}|${options.url}|${headerKey}`;
86
+ };
87
+ const httpLayer = function httpLayer(options) {
88
+ const { headers } = options;
89
+ if (!headers) return RpcClient.layerProtocolHttp({ url: options.url }).pipe(Layer.provide(RpcSerialization.layerNdJsonRpc()), Layer.provide(FetchHttpClient.layer));
90
+ const headerMap = normalizeActionHeaders(headers);
91
+ return RpcClient.layerProtocolHttp({
92
+ transformClient: (client) => HttpClient.mapRequest(client, (req) => {
93
+ let next = req;
94
+ for (const [key, value] of Object.entries(headerMap)) next = HttpClientRequest.setHeader(next, key, value);
95
+ return next;
96
+ }),
97
+ url: options.url
98
+ }).pipe(Layer.provide(RpcSerialization.layerNdJsonRpc()), Layer.provide(FetchHttpClient.layer));
99
+ };
100
+ const wsLayer = function wsLayer(url) {
101
+ return RpcClient.layerProtocolSocket().pipe(Layer.provide(RpcSerialization.layerNdJsonRpc()), Layer.provide(Socket.layerWebSocket(url)), Layer.provide(Socket.layerWebSocketConstructorGlobal));
102
+ };
103
+ const clientLayer = function clientLayer(options) {
104
+ return options.transport === "ws" ? wsLayer(options.url) : httpLayer(options);
105
+ };
106
+ const loadClient = function loadClient(group, options) {
107
+ return Effect.gen(function* loadClientGen() {
108
+ const scope = yield* Scope.make();
109
+ const context = yield* Scope.provide(scope)(Layer.build(clientLayer(options)));
110
+ return yield* Scope.provide(scope)(RpcClient.make(group).pipe(Effect.provide(context)));
111
+ });
112
+ };
113
+ const isStreamResult = function isStreamResult(value) {
114
+ return Stream.isStream(value);
115
+ };
116
+ const isStreamTag = function isStreamTag(group, tag) {
117
+ const rpc = group.requests.get(tag);
118
+ if (!rpc?.successSchema) return false;
119
+ return RpcSchema.isStreamSchema(rpc.successSchema);
120
+ };
121
+ const streamToAsyncGenerator = function streamToAsyncGenerator(stream, signal) {
122
+ const iterable = streamToAsyncGen(stream);
123
+ return (async function* streamProxy() {
124
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
125
+ const iterator = iterable[Symbol.asyncIterator]();
126
+ const onAbort = function onAbort() {
127
+ iterator.return?.();
128
+ };
129
+ signal?.addEventListener("abort", onAbort, { once: true });
130
+ const pull = async function* pull() {
131
+ for (;;) {
132
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
133
+ const next = await iterator.next();
134
+ if (next.done) return;
135
+ yield next.value;
136
+ }
137
+ };
138
+ try {
139
+ yield* pull();
140
+ } finally {
141
+ signal?.removeEventListener("abort", onAbort);
142
+ }
143
+ })();
144
+ };
145
+ const callFlat = function callFlat(client, tag, args, callOpts) {
146
+ const caller = client[tag];
147
+ if (!caller) return Promise.reject(/* @__PURE__ */ new Error(`Unknown action ${tag}`));
148
+ const result = caller({ args }, callOpts);
149
+ if (isStreamResult(result)) return streamToAsyncGenerator(result, callOpts?.signal);
150
+ return Effect.runPromise(result, { signal: callOpts?.signal });
151
+ };
152
+ const isCallOptions = function isCallOptions(value) {
153
+ return value !== null && typeof value === "object" && "signal" in value && value.signal instanceof AbortSignal && Object.keys(value).length === 1;
154
+ };
155
+ const nestClient = function nestClient(group, flat) {
156
+ const nested = {};
157
+ for (const tag of group.requests.keys()) {
158
+ const dot = tag.indexOf(".");
159
+ if (dot === -1) continue;
160
+ const mod = tag.slice(0, dot);
161
+ const name = tag.slice(dot + 1);
162
+ nested[mod] ??= {};
163
+ nested[mod][name] = (...args) => {
164
+ const opts = args.at(-1);
165
+ const hasSignal = opts !== void 0 && isCallOptions(opts);
166
+ const params = hasSignal ? args.slice(0, -1) : args;
167
+ return callFlat(flat, tag, params, hasSignal ? opts : void 0);
168
+ };
169
+ }
170
+ return nested;
171
+ };
172
+ const createClient = function createClient(group, options) {
173
+ const key = cacheKey(group, options);
174
+ let entry = clientCache.get(key);
175
+ if (!entry) {
176
+ entry = { pending: (async function loadNestedClient() {
177
+ try {
178
+ const flat = await Effect.runPromise(loadClient(group, options));
179
+ return nestClient(group, flat);
180
+ } catch (error) {
181
+ clientCache.delete(key);
182
+ throw error;
183
+ }
184
+ })() };
185
+ clientCache.set(key, entry);
186
+ }
187
+ const cached = entry;
188
+ return new Proxy({}, { get(_target, mod) {
189
+ if (!isStringPropertyKey(mod)) return;
190
+ return new Proxy({}, { get(_inner, name) {
191
+ if (!isStringPropertyKey(name)) return;
192
+ const tag = `${mod}.${name}`;
193
+ if (isStreamTag(group, tag)) return (...args) => (async function* streamAction() {
194
+ const out = (await cached.pending)[mod]?.[name]?.(...args);
195
+ if (!out) throw new Error(`Unknown action ${mod}.${name}`);
196
+ yield* out;
197
+ })();
198
+ return async (...args) => {
199
+ const out = (await cached.pending)[mod]?.[name]?.(...args);
200
+ if (!out) throw new Error(`Unknown action ${mod}.${name}`);
201
+ return out;
202
+ };
203
+ } });
204
+ } });
205
+ };
206
+ //#endregion
207
+ export { streamToAsyncGen as a, bindAsyncGenContext as i, asyncGenToStream as n, asyncGenToStreamInContext as r, createClient as t };
@@ -0,0 +1,98 @@
1
+ import { r as OxidejsJson } from "./types-BaZeMYcP.mjs";
2
+ import * as Atom from "effect/unstable/reactivity/Atom";
3
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+ //#region src/action.d.ts
5
+ declare const ACTION_CALL: unique symbol;
6
+ interface CallOptions {
7
+ signal?: AbortSignal;
8
+ }
9
+ interface ServerActionHandle<Args extends unknown[], A> {
10
+ (...args: Args | [...Args, CallOptions]): Promise<A>;
11
+ set: (...args: Args) => Promise<A>;
12
+ bind: (...args: Args) => (...ev: unknown[]) => void;
13
+ with: (...args: Args) => (...ev: unknown[]) => void;
14
+ /** Last `AsyncResult` for this action's client atom (does not invoke RPC). */
15
+ readonly result: AsyncResult.AsyncResult<A, unknown>;
16
+ readonly atom: Atom.Atom<unknown>;
17
+ readonly $$atom: 1;
18
+ }
19
+ interface StreamActionHandle<Args extends unknown[], Y, R = void> {
20
+ (...args: Args | [...Args, CallOptions]): AsyncGenerator<Y, R, undefined>;
21
+ set: (...args: Args) => AsyncGenerator<Y, R, undefined>;
22
+ bind: (...args: Args) => (...ev: unknown[]) => void;
23
+ with: (...args: Args) => (...ev: unknown[]) => void;
24
+ readonly atom: undefined;
25
+ readonly $$atom: 1;
26
+ }
27
+ /** Attach an ilha server-island capture key to an action handle. */
28
+ declare const brandServerAction: <Args extends unknown[], A>(key: string, handle: ServerActionHandle<Args, A>) => ServerActionHandle<Args, A>;
29
+ /** Wrap an RPC caller as an `Atom.fn`-shaped client action handle. */
30
+ declare const wrapClientRpc: <Args extends unknown[], A>(rpc: (...args: Args | [...Args, CallOptions]) => Promise<A>) => ServerActionHandle<Args, A>;
31
+ /**
32
+ * Wrap a streaming RPC caller so the client handle returns an async generator
33
+ * (awaiting the underlying client lazily), not `Promise<AsyncGenerator>`.
34
+ */
35
+ declare const wrapClientStreamRpc: <Args extends unknown[], Y, R = void>(rpc: (...args: Args | [...Args, CallOptions]) => AsyncGenerator<Y, R, undefined>) => StreamActionHandle<Args, Y, R>;
36
+ /**
37
+ * Marks a `*.server.ts` export as a remote RPC action. On the server the
38
+ * underlying function runs locally; on the client the build replaces the module
39
+ * with an `Atom.fn`-shaped RPC handle (`set`, `bind`, `result`).
40
+ */
41
+ declare function action<Args extends unknown[], Y, R = void>(fn: (...args: Args) => AsyncGenerator<Y, R, unknown>): StreamActionHandle<Args, Y, R>;
42
+ declare function action<Args extends unknown[], Result>(fn: (...args: Args) => Result): ServerActionHandle<Args, Awaited<Result>>;
43
+ //#endregion
44
+ //#region src/context.d.ts
45
+ interface ExecutionContext {
46
+ passThroughOnException?: () => void;
47
+ waitUntil?: (promise: PromiseLike<OxidejsJson | object | null | undefined>) => void;
48
+ }
49
+ /** Return from `src/server.ts` `fetch`. `undefined` falls through to assets. */
50
+ type FetchResult = Response | undefined;
51
+ /**
52
+ * `src/server.ts` fetch handler. The generated wrapper always calls
53
+ * `fetch(request, env, ctx)` — `env` may be `{}` on Node without the `env` option.
54
+ * Return `undefined` (or bare `return`) to fall through to assets.
55
+ */
56
+ type FetchHandler<Env extends object = {
57
+ [key: string]: OxidejsJson;
58
+ }> = (request: Request, env: Env, ctx: ExecutionContext) => FetchResult | Promise<FetchResult>;
59
+ /** Default export shape for `src/server.ts`. */
60
+ interface ServerEntry<Env extends object = {
61
+ [key: string]: OxidejsJson;
62
+ }> {
63
+ fetch: FetchHandler<Env>;
64
+ }
65
+ /** Values middleware may attach on the request context bag. */
66
+ type ActionContextValue = Request | ExecutionContext | OxidejsJson | {
67
+ [key: string]: OxidejsJson;
68
+ } | undefined;
69
+ /** RPC procedure request context. Starts as `{ req }` plus Worker extras. Middleware can add fields. */
70
+ interface ActionContext {
71
+ [key: string]: ActionContextValue;
72
+ req: Request;
73
+ env?: {
74
+ [key: string]: OxidejsJson;
75
+ };
76
+ fetchCtx?: ExecutionContext;
77
+ }
78
+ /** Current request context: ALS first, then the WebContainer sync fallback. */
79
+ declare const getRequestStore: () => ActionContext;
80
+ /**
81
+ * Run `fn` with `store` on ALS and the sync fallback. On WebContainer the sync
82
+ * slot is restored only after an async `fn` settles (streams capture the store
83
+ * at invoke time and re-enter via this helper on each pull). Sync returns and
84
+ * throws restore immediately so a completed request is not left visible.
85
+ */
86
+ declare const withRequestStore: <T>(ctx: ActionContext, fn: () => T) => T;
87
+ /** Current RPC or host request context. Throws outside request handling. */
88
+ declare const useCtx: <C extends ActionContext = ActionContext>() => C;
89
+ /** Current server `Request`. Available in actions, SSR, and frame renders. */
90
+ declare const useRequest: () => Request;
91
+ /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
92
+ declare const useEnv: <E = {
93
+ [key: string]: OxidejsJson;
94
+ }>() => E | undefined;
95
+ /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). `undefined` on Node. */
96
+ declare const useFetchCtx: () => ExecutionContext | undefined;
97
+ //#endregion
98
+ export { brandServerAction as _, FetchResult as a, useCtx as c, useRequest as d, withRequestStore as f, action as g, StreamActionHandle as h, FetchHandler as i, useEnv as l, ServerActionHandle as m, ActionContextValue as n, ServerEntry as o, ACTION_CALL as p, ExecutionContext as r, getRequestStore as s, ActionContext as t, useFetchCtx as u, wrapClientRpc as v, wrapClientStreamRpc as y };
@@ -0,0 +1,108 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import process from "node:process";
3
+ //#region src/context.ts
4
+ const ALS_KEY = Symbol.for("oxidejs.requestContext");
5
+ const FETCH_KEY = Symbol.for("oxidejs.fetch");
6
+ /** StackBlitz WebContainers lose AsyncLocalStorage across `async/await`. */
7
+ const inWebcontainer = function inWebcontainer() {
8
+ if (process === void 0) return false;
9
+ const versions = process.versions;
10
+ return Boolean(versions.webcontainer);
11
+ };
12
+ /** Module fallback when ALS does not survive awaits (WebContainer). */
13
+ let syncStore = null;
14
+ /** Serialize handler *entry* on WebContainer so syncStore is not stomped. */
15
+ let entryTail = Promise.resolve(null);
16
+ const als = function als() {
17
+ const g = globalThis;
18
+ const existing = g[ALS_KEY];
19
+ if (existing) return existing;
20
+ const created = new AsyncLocalStorage();
21
+ g[ALS_KEY] = created;
22
+ return created;
23
+ };
24
+ const isPromiseLike = function isPromiseLike(value) {
25
+ if (value === null || value === void 0) return false;
26
+ return typeof value.then === "function";
27
+ };
28
+ /** Current request context: ALS first, then the WebContainer sync fallback. */
29
+ const getRequestStore = function getRequestStore() {
30
+ const current = als().getStore() ?? syncStore;
31
+ if (!current) throw new Error("oxidejs: request context is unavailable");
32
+ return current;
33
+ };
34
+ const store = function store() {
35
+ return getRequestStore();
36
+ };
37
+ /**
38
+ * Run `fn` with `store` on ALS and the sync fallback. On WebContainer the sync
39
+ * slot is restored only after an async `fn` settles (streams capture the store
40
+ * at invoke time and re-enter via this helper on each pull). Sync returns and
41
+ * throws restore immediately so a completed request is not left visible.
42
+ */
43
+ const withRequestStore = function withRequestStore(ctx, fn) {
44
+ const previous = syncStore;
45
+ syncStore = ctx;
46
+ let deferRestore = false;
47
+ try {
48
+ const result = als().run(ctx, fn);
49
+ if (inWebcontainer() && isPromiseLike(result)) {
50
+ deferRestore = true;
51
+ return (async () => {
52
+ try {
53
+ return await result;
54
+ } finally {
55
+ if (syncStore === ctx) syncStore = previous;
56
+ }
57
+ })();
58
+ }
59
+ return result;
60
+ } finally {
61
+ if (!deferRestore) syncStore = previous;
62
+ }
63
+ };
64
+ /**
65
+ * Serialize async work that installs request context on WebContainer.
66
+ * Release as soon as `fn` settles — do not wait for streamed response bodies.
67
+ */
68
+ const withRequestEntry = async function withRequestEntry(fn) {
69
+ if (!inWebcontainer()) return fn();
70
+ const { promise: gate, resolve: release } = Promise.withResolvers();
71
+ const previous = entryTail;
72
+ entryTail = gate;
73
+ await previous;
74
+ try {
75
+ return await fn();
76
+ } finally {
77
+ release(null);
78
+ }
79
+ };
80
+ /** Current RPC or host request context. Throws outside request handling. */
81
+ const useCtx = function useCtx() {
82
+ return store();
83
+ };
84
+ /** Current server `Request`. Available in actions, SSR, and frame renders. */
85
+ const useRequest = function useRequest() {
86
+ return store().req;
87
+ };
88
+ /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
89
+ const useEnv = function useEnv() {
90
+ return store().env;
91
+ };
92
+ /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). `undefined` on Node. */
93
+ const useFetchCtx = function useFetchCtx() {
94
+ return store().fetchCtx;
95
+ };
96
+ const runWithRequest = function runWithRequest(req, fn, extra) {
97
+ return withRequestStore({
98
+ ...extra,
99
+ req
100
+ }, fn);
101
+ };
102
+ const HOOK_KEY = Symbol.for("oxidejs.runWithRequest");
103
+ const hookGlobal = globalThis;
104
+ hookGlobal[HOOK_KEY] ??= function oxideRunWithRequest(req, fn) {
105
+ return runWithRequest(req, fn, req[FETCH_KEY]);
106
+ };
107
+ //#endregion
108
+ export { useEnv as a, withRequestEntry as c, useCtx as i, withRequestStore as l, inWebcontainer as n, useFetchCtx as o, runWithRequest as r, useRequest as s, getRequestStore as t };
package/dist/index.d.mts CHANGED
@@ -1,3 +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";
2
- import { a as OxidejsWranglerOptions, i as OxidejsPreset, n as OxidejsActionTransport, o as ResolvedOptions, r as OxidejsOptions, t as OxidejsActionHeaders } from "./types-BM4NAnzy.mjs";
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 };
1
+ import { a as OxidejsPreset, i as OxidejsOptions, n as OxidejsActionTransport, o as OxidejsWranglerOptions, s as ResolvedOptions, t as OxidejsActionHeaders } from "./types-BaZeMYcP.mjs";
2
+ import { _ as brandServerAction, a as FetchResult, c as useCtx, d as useRequest, f as withRequestStore, g as action, h as StreamActionHandle, i as FetchHandler, l as useEnv, m as ServerActionHandle, o as ServerEntry, p as ACTION_CALL, r as ExecutionContext, s as getRequestStore, t as ActionContext, u as useFetchCtx, v as wrapClientRpc, y as wrapClientStreamRpc } from "./context-CmChiQ2W.mjs";
3
+ export { ACTION_CALL, type ActionContext, type ExecutionContext, type FetchHandler, type FetchResult, type OxidejsActionHeaders, type OxidejsActionTransport, type OxidejsOptions, type OxidejsPreset, type OxidejsWranglerOptions, type ResolvedOptions, type ServerActionHandle, type ServerEntry, type StreamActionHandle, action, brandServerAction, getRequestStore, useCtx, useEnv, useFetchCtx, useRequest, withRequestStore, wrapClientRpc, wrapClientStreamRpc };