oxidejs 0.2.3 → 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.
@@ -0,0 +1,145 @@
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
+ return yield* Scope.provide(scope)(RpcClient.make(group).pipe(Effect.provide(clientLayer(options))));
66
+ });
67
+ }
68
+ function isStreamResult(value) {
69
+ return Stream.isStream(value);
70
+ }
71
+ function isStreamTag(group, tag) {
72
+ const rpc = group.requests.get(tag);
73
+ if (!rpc?.successSchema) return false;
74
+ return RpcSchema.isStreamSchema(rpc.successSchema);
75
+ }
76
+ function streamToAsyncGenerator(stream, signal) {
77
+ const iterable = streamToAsyncGen(stream);
78
+ return (async function* () {
79
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
80
+ const iterator = iterable[Symbol.asyncIterator]();
81
+ const onAbort = () => {
82
+ iterator.return?.();
83
+ };
84
+ signal?.addEventListener("abort", onAbort, { once: true });
85
+ try {
86
+ while (true) {
87
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
88
+ const next = await iterator.next();
89
+ if (next.done) return void 0;
90
+ yield next.value;
91
+ }
92
+ } finally {
93
+ signal?.removeEventListener("abort", onAbort);
94
+ }
95
+ })();
96
+ }
97
+ function callFlat(client, tag, args, callOpts) {
98
+ const caller = client[tag];
99
+ if (!caller) return Promise.reject(/* @__PURE__ */ new Error(`Unknown action ${tag}`));
100
+ const result = caller({ args }, callOpts);
101
+ if (isStreamResult(result)) return streamToAsyncGenerator(result, callOpts?.signal);
102
+ return Effect.runPromise(result, { signal: callOpts?.signal });
103
+ }
104
+ function nestClient(group, flat) {
105
+ const nested = {};
106
+ for (const tag of group.requests.keys()) {
107
+ const dot = tag.indexOf(".");
108
+ if (dot === -1) continue;
109
+ const mod = tag.slice(0, dot);
110
+ const name = tag.slice(dot + 1);
111
+ nested[mod] ??= {};
112
+ nested[mod][name] = (...args) => {
113
+ const opts = args.at(-1);
114
+ const hasSignal = opts && typeof opts === "object" && "signal" in opts && opts.signal instanceof AbortSignal && Object.keys(opts).length === 1;
115
+ const params = hasSignal ? args.slice(0, -1) : args;
116
+ return callFlat(flat, tag, params, hasSignal ? opts : void 0);
117
+ };
118
+ }
119
+ return nested;
120
+ }
121
+ function createClient(group, options) {
122
+ const key = cacheKey(group, options);
123
+ let entry = clientCache.get(key);
124
+ if (!entry) {
125
+ entry = { pending: Effect.runPromise(loadClient(group, options)).then((flat) => nestClient(group, flat)).catch((error) => {
126
+ clientCache.delete(key);
127
+ throw error;
128
+ }) };
129
+ clientCache.set(key, entry);
130
+ }
131
+ return new Proxy({}, { get(_target, mod) {
132
+ if (typeof mod !== "string") return void 0;
133
+ return new Proxy({}, { get(_inner, name) {
134
+ if (typeof name !== "string") return void 0;
135
+ if (isStreamTag(group, `${mod}.${name}`)) return (...args) => (async function* () {
136
+ const out = (await entry.pending)[mod]?.[name]?.(...args);
137
+ if (!out) throw new Error(`Unknown action ${mod}.${name}`);
138
+ yield* out;
139
+ })();
140
+ return (...args) => entry.pending.then((client) => client[mod]?.[name]?.(...args) ?? Promise.reject(/* @__PURE__ */ new Error(`Unknown action ${mod}.${name}`)));
141
+ } });
142
+ } });
143
+ }
144
+ //#endregion
145
+ 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,36 +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
- import { UnpluginFactory } from "unplugin";
3
- //#region src/context.d.ts
4
- type ExecutionContext = {
5
- waitUntil?(promise: Promise<unknown>): void;
6
- passThroughOnException?(): void;
7
- };
8
- /** Tacho procedure `ctx`. Starts as `{ req }` plus Worker extras. Middleware can add fields. */
9
- type ActionContext = {
10
- req: Request;
11
- env?: unknown;
12
- fetchCtx?: ExecutionContext;
13
- [key: string]: unknown;
14
- };
15
- /** Current tacho or host request context. Throws outside request handling. */
16
- declare function useCtx<C extends ActionContext = ActionContext>(): C;
17
- /** Current server `Request`. Available in actions, SSR, and frame renders. */
18
- declare function useRequest(): Request;
19
- /** Worker `env` from `fetch(request, env, ctx)`. `undefined` on the Node fetch preset. */
20
- declare function useEnv<E = unknown>(): E | undefined;
21
- /** Worker `ctx` from `fetch(request, env, ctx)` (`waitUntil`). Not tacho `ctx`. `undefined` on Node. */
22
- declare function useFetchCtx(): ExecutionContext | undefined;
23
- /**
24
- * Marks a `*.server.ts` export as a remote RPC action. Runtime identity; the
25
- * second call signature adds the transport-only `{ signal }` argument.
26
- */
27
- declare function action<Args extends unknown[], Result>(fn: (...args: Args) => Result): typeof fn & ((...args: [...Args, options: {
28
- signal?: AbortSignal;
29
- }]) => Result);
30
- //#endregion
31
- //#region src/index.d.ts
32
- declare const unpluginFactory: UnpluginFactory<OxidejsOptions | undefined>;
33
- declare const oxidejs: import("unplugin").UnpluginInstance<OxidejsOptions | undefined, boolean>;
34
- declare const vite: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
35
- //#endregion
36
- export { type ActionContext, type ExecutionContext, type OxidejsActionHeaders, type OxidejsActionTransport, type OxidejsOptions, type OxidejsPreset, type OxidejsWranglerOptions, type ResolvedOptions, action, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
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,2 +1,108 @@
1
- import { a as useCtx, c as useRequest, i as action, n as unpluginFactory, o as useEnv, r as vite, s as useFetchCtx, t as oxidejs } from "./src-D-qdNVqg.mjs";
2
- export { action, oxidejs as default, oxidejs, unpluginFactory, useCtx, useEnv, useFetchCtx, useRequest, vite };
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
+ }
92
+ /**
93
+ * Wrap a streaming RPC caller so the client handle returns an async generator
94
+ * (awaiting the underlying client lazily), not `Promise<AsyncGenerator>`.
95
+ */
96
+ function wrapClientStreamRpc(rpc) {
97
+ return defineStreamAction((...args) => rpc(...args));
98
+ }
99
+ function action(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);
106
+ }
107
+ //#endregion
108
+ export { ACTION_CALL, action, brandServerAction, useCtx, useEnv, useFetchCtx, useRequest, wrapClientRpc, wrapClientStreamRpc };