@typion/rpc 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +28 -2
- package/dist/client.d.ts +28 -0
- package/dist/client.js +67 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simone Ergotino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @typion/rpc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The RPC transport for [Typion](../../typion/docs/README.md) and its browser client: a descriptor (`{ entity, op, input }`), or an
|
|
4
|
+
array of them, posted to one endpoint and answered typed, with no codegen. Install it and list it in the handler's
|
|
5
|
+
`transports`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { rpc } from '@typion/rpc'
|
|
9
|
+
import { routeHandlers } from 'typion/next'
|
|
10
|
+
import { config } from '@/typion.config.ts'
|
|
11
|
+
|
|
12
|
+
export const { GET, POST, PATCH, PUT, DELETE, OPTIONS } = await routeHandlers(config, {
|
|
13
|
+
transports: [rpc()],
|
|
14
|
+
})
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// a client component: the queries are values that carry their result type
|
|
19
|
+
import { createClient } from '@typion/rpc/client'
|
|
20
|
+
import { postCards } from '@/typion/queries.ts'
|
|
21
|
+
|
|
22
|
+
const client = createClient({ url: '/api' })
|
|
23
|
+
const cards = await client.run(postCards)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`rpc({ path })` moves the endpoint (`/api/_rpc` by default). A batch runs one descriptor after the other, each in its
|
|
27
|
+
own transaction, and answers `{ results }` with the outcome of each. Every descriptor is checked by the core before it
|
|
28
|
+
runs, as REST routes are. The client imports no server code and no Drizzle; a failed call throws `TypionRequestError`
|
|
29
|
+
with the wire error's `code`, `status` and `issues`.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AnyDescriptor, InferResult, Jsonified, WireError, WireErrorCode } from "typion/contract";
|
|
2
|
+
//#region src/client.d.ts
|
|
3
|
+
export type ClientResult<D extends AnyDescriptor> = Jsonified<InferResult<D>>;
|
|
4
|
+
export declare class TypionRequestError extends Error {
|
|
5
|
+
readonly code: WireErrorCode | 'TRANSPORT';
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly entity: string | undefined;
|
|
8
|
+
readonly issues: WireError['issues'];
|
|
9
|
+
readonly details: Record<string, unknown> | undefined;
|
|
10
|
+
constructor(error: WireError | {
|
|
11
|
+
code: 'TRANSPORT';
|
|
12
|
+
status: number;
|
|
13
|
+
message: string;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export type ClientHeaders = Record<string, string> | Headers;
|
|
17
|
+
export type ClientOptions = {
|
|
18
|
+
url?: string;
|
|
19
|
+
headers?: ClientHeaders | (() => ClientHeaders | Promise<ClientHeaders>);
|
|
20
|
+
fetch?: (input: string, init: RequestInit) => Response | Promise<Response>;
|
|
21
|
+
};
|
|
22
|
+
export type TypionClient = {
|
|
23
|
+
run<D extends AnyDescriptor>(descriptor: D): Promise<ClientResult<D>>;
|
|
24
|
+
runAll<const D extends readonly AnyDescriptor[]>(descriptors: D): Promise<{ [K in keyof D]: ClientResult<Extract<D[K], AnyDescriptor>>; }>;
|
|
25
|
+
};
|
|
26
|
+
export declare function createClient(options?: ClientOptions): TypionClient;
|
|
27
|
+
//#endregion
|
|
28
|
+
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/client.ts
|
|
2
|
+
var TypionRequestError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
entity;
|
|
6
|
+
issues;
|
|
7
|
+
details;
|
|
8
|
+
constructor(error) {
|
|
9
|
+
super(error.message);
|
|
10
|
+
this.name = "TypionRequestError";
|
|
11
|
+
this.code = error.code;
|
|
12
|
+
this.status = error.status;
|
|
13
|
+
this.entity = "entity" in error ? error.entity : void 0;
|
|
14
|
+
this.issues = "issues" in error ? error.issues : void 0;
|
|
15
|
+
this.details = "details" in error ? error.details : void 0;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function createClient(options = {}) {
|
|
19
|
+
const base = (options.url ?? "/api").replace(/\/$/, "");
|
|
20
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
21
|
+
const post = async (body) => {
|
|
22
|
+
const extra = typeof options.headers === "function" ? await options.headers() : options.headers;
|
|
23
|
+
const headers = new Headers(extra === void 0 ? void 0 : extra);
|
|
24
|
+
headers.set("content-type", "application/json");
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await doFetch(`${base}/_rpc`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers,
|
|
30
|
+
body: JSON.stringify(body)
|
|
31
|
+
});
|
|
32
|
+
} catch (cause) {
|
|
33
|
+
throw new TypionRequestError({
|
|
34
|
+
code: "TRANSPORT",
|
|
35
|
+
status: 0,
|
|
36
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const payload = await response.json().catch(() => null);
|
|
40
|
+
if (payload === null || typeof payload !== "object") throw new TypionRequestError({
|
|
41
|
+
code: "TRANSPORT",
|
|
42
|
+
status: response.status,
|
|
43
|
+
message: `The handler at ${base}/_rpc answered ${response.status} with no envelope`
|
|
44
|
+
});
|
|
45
|
+
return payload;
|
|
46
|
+
};
|
|
47
|
+
const unwrap = (envelope) => {
|
|
48
|
+
if ("error" in envelope) throw new TypionRequestError(envelope.error);
|
|
49
|
+
return envelope.data;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
run: async (descriptor) => unwrap(await post(descriptor)),
|
|
53
|
+
runAll: async (descriptors) => {
|
|
54
|
+
const payload = await post(descriptors);
|
|
55
|
+
if (!Array.isArray(payload.results)) throw new TypionRequestError({
|
|
56
|
+
code: "TRANSPORT",
|
|
57
|
+
status: 200,
|
|
58
|
+
message: "A batch answered without `results`"
|
|
59
|
+
});
|
|
60
|
+
return payload.results.map((envelope) => unwrap(envelope));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { TypionRequestError, createClient };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n AnyDescriptor,\n InferResult,\n Jsonified,\n WireEnvelope,\n WireError,\n WireErrorCode,\n} from 'typion/contract'\n\n// The browser half of the HTTP handler: it posts descriptors to `/_rpc` and gives back what their type says.\n// It imports the contract only, so no Drizzle and no engine runtime reach the bundle.\n\nexport type ClientResult<D extends AnyDescriptor> = Jsonified<InferResult<D>>\n\nexport class TypionRequestError extends Error {\n // `UNAUTHENTICATED` when the caller has not signed in: the page asks them to.\n readonly code: WireErrorCode | 'TRANSPORT'\n readonly status: number\n readonly entity: string | undefined\n readonly issues: WireError['issues']\n readonly details: Record<string, unknown> | undefined\n\n constructor(error: WireError | { code: 'TRANSPORT'; status: number; message: string }) {\n super(error.message)\n this.name = 'TypionRequestError'\n this.code = error.code\n this.status = error.status\n this.entity = 'entity' in error ? error.entity : undefined\n this.issues = 'issues' in error ? error.issues : undefined\n this.details = 'details' in error ? error.details : undefined\n }\n}\n\nexport type ClientHeaders = Record<string, string> | Headers\n\nexport type ClientOptions = {\n // Where the handler is mounted; `/_rpc` is appended to it.\n url?: string\n // Resolved per request, so a token that changes is read at call time.\n headers?: ClientHeaders | (() => ClientHeaders | Promise<ClientHeaders>)\n // Any function shaped like `fetch`, a test's in-process one included (Hono's `app.request`, which may answer\n // without a promise).\n fetch?: (input: string, init: RequestInit) => Response | Promise<Response>\n}\n\nexport type TypionClient = {\n run<D extends AnyDescriptor>(descriptor: D): Promise<ClientResult<D>>\n // One request, one result per descriptor, in order; a descriptor that fails throws where it is read.\n runAll<const D extends readonly AnyDescriptor[]>(\n descriptors: D,\n ): Promise<{ [K in keyof D]: ClientResult<Extract<D[K], AnyDescriptor>> }>\n}\n\nexport function createClient(options: ClientOptions = {}): TypionClient {\n const base = (options.url ?? '/api').replace(/\\/$/, '')\n const doFetch = options.fetch ?? globalThis.fetch\n\n const post = async (body: unknown): Promise<unknown> => {\n const extra = typeof options.headers === 'function' ? await options.headers() : options.headers\n const headers = new Headers(extra === undefined ? undefined : extra)\n headers.set('content-type', 'application/json')\n let response: Response\n try {\n response = await doFetch(`${base}/_rpc`, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n })\n } catch (cause) {\n throw new TypionRequestError({\n code: 'TRANSPORT',\n status: 0,\n message: cause instanceof Error ? cause.message : String(cause),\n })\n }\n const payload: unknown = await response.json().catch(() => null)\n if (payload === null || typeof payload !== 'object') {\n throw new TypionRequestError({\n code: 'TRANSPORT',\n status: response.status,\n message: `The handler at ${base}/_rpc answered ${response.status} with no envelope`,\n })\n }\n return payload\n }\n\n const unwrap = <T>(envelope: WireEnvelope<T>): T => {\n if ('error' in envelope) throw new TypionRequestError(envelope.error)\n return envelope.data\n }\n\n return {\n run: async (descriptor) => unwrap(await (post(descriptor) as Promise<WireEnvelope<never>>)),\n runAll: async (descriptors) => {\n const payload = (await post(descriptors)) as { results?: WireEnvelope<unknown>[] }\n if (!Array.isArray(payload.results)) {\n throw new TypionRequestError({\n code: 'TRANSPORT',\n status: 200,\n message: 'A batch answered without `results`',\n })\n }\n return payload.results.map((envelope) => unwrap(envelope)) as never\n },\n }\n}\n"],"mappings":";AAcA,IAAa,qBAAb,cAAwC,MAAM;CAE5C;CACA;CACA;CACA;CACA;CAEA,YAAY,OAA2E;EACrF,MAAM,MAAM,OAAO;EACnB,KAAK,OAAO;EACZ,KAAK,OAAO,MAAM;EAClB,KAAK,SAAS,MAAM;EACpB,KAAK,SAAS,YAAY,QAAQ,MAAM,SAAS,KAAA;EACjD,KAAK,SAAS,YAAY,QAAQ,MAAM,SAAS,KAAA;EACjD,KAAK,UAAU,aAAa,QAAQ,MAAM,UAAU,KAAA;CACtD;AACF;AAsBA,SAAgB,aAAa,UAAyB,CAAC,GAAiB;CACtE,MAAM,QAAQ,QAAQ,OAAO,OAAA,CAAQ,QAAQ,OAAO,EAAE;CACtD,MAAM,UAAU,QAAQ,SAAS,WAAW;CAE5C,MAAM,OAAO,OAAO,SAAoC;EACtD,MAAM,QAAQ,OAAO,QAAQ,YAAY,aAAa,MAAM,QAAQ,QAAQ,IAAI,QAAQ;EACxF,MAAM,UAAU,IAAI,QAAQ,UAAU,KAAA,IAAY,KAAA,IAAY,KAAK;EACnE,QAAQ,IAAI,gBAAgB,kBAAkB;EAC9C,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,QAAQ,GAAG,KAAK,QAAQ;IACvC,QAAQ;IACR;IACA,MAAM,KAAK,UAAU,IAAI;GAC3B,CAAC;EACH,SAAS,OAAO;GACd,MAAM,IAAI,mBAAmB;IAC3B,MAAM;IACN,QAAQ;IACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAChE,CAAC;EACH;EACA,MAAM,UAAmB,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;EAC/D,IAAI,YAAY,QAAQ,OAAO,YAAY,UACzC,MAAM,IAAI,mBAAmB;GAC3B,MAAM;GACN,QAAQ,SAAS;GACjB,SAAS,kBAAkB,KAAK,iBAAiB,SAAS,OAAO;EACnE,CAAC;EAEH,OAAO;CACT;CAEA,MAAM,UAAa,aAAiC;EAClD,IAAI,WAAW,UAAU,MAAM,IAAI,mBAAmB,SAAS,KAAK;EACpE,OAAO,SAAS;CAClB;CAEA,OAAO;EACL,KAAK,OAAO,eAAe,OAAO,MAAO,KAAK,UAAU,CAAkC;EAC1F,QAAQ,OAAO,gBAAgB;GAC7B,MAAM,UAAW,MAAM,KAAK,WAAW;GACvC,IAAI,CAAC,MAAM,QAAQ,QAAQ,OAAO,GAChC,MAAM,IAAI,mBAAmB;IAC3B,MAAM;IACN,QAAQ;IACR,SAAS;GACX,CAAC;GAEH,OAAO,QAAQ,QAAQ,KAAK,aAAa,OAAO,QAAQ,CAAC;EAC3D;CACF;AACF"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
function rpc(options = {}) {
|
|
3
|
+
const path = options.path ?? "_rpc";
|
|
4
|
+
return {
|
|
5
|
+
name: "rpc",
|
|
6
|
+
async handle(call) {
|
|
7
|
+
if (call.method !== "POST" || call.segments.length !== 1 || call.segments[0] !== path) return;
|
|
8
|
+
const payload = await call.body();
|
|
9
|
+
if (!Array.isArray(payload)) return call.respond(200, { data: await call.run(call.descriptor(payload)) });
|
|
10
|
+
const results = [];
|
|
11
|
+
for (const item of payload) try {
|
|
12
|
+
results.push({ data: await call.run(call.descriptor(item)) });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
results.push({ error: call.error(error) });
|
|
15
|
+
}
|
|
16
|
+
return call.respond(200, { results });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { rpc };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Transport } from 'typion/http'\n\nexport type RpcOptions = {\n // The segment below the handler's base path; `_rpc` by default, which is what the client posts to.\n path?: string\n}\n\n// One descriptor, or an array of them run one after the other, each in its own transaction: what the browser\n// client and `defineQueries` speak. Every descriptor is checked by the core before it runs, as REST routes are.\nexport function rpc(options: RpcOptions = {}): Transport {\n const path = options.path ?? '_rpc'\n return {\n name: 'rpc',\n async handle(call) {\n if (call.method !== 'POST' || call.segments.length !== 1 || call.segments[0] !== path) {\n return undefined\n }\n const payload = await call.body()\n if (!Array.isArray(payload)) {\n return call.respond(200, { data: await call.run(call.descriptor(payload)) })\n }\n const results: unknown[] = []\n for (const item of payload) {\n try {\n results.push({ data: await call.run(call.descriptor(item)) })\n } catch (error) {\n results.push({ error: call.error(error) })\n }\n }\n return call.respond(200, { results })\n },\n }\n}\n"],"mappings":";AASA,SAAgB,IAAI,UAAsB,CAAC,GAAc;CACvD,MAAM,OAAO,QAAQ,QAAQ;CAC7B,OAAO;EACL,MAAM;EACN,MAAM,OAAO,MAAM;GACjB,IAAI,KAAK,WAAW,UAAU,KAAK,SAAS,WAAW,KAAK,KAAK,SAAS,OAAO,MAC/E;GAEF,MAAM,UAAU,MAAM,KAAK,KAAK;GAChC,IAAI,CAAC,MAAM,QAAQ,OAAO,GACxB,OAAO,KAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,KAAK,IAAI,KAAK,WAAW,OAAO,CAAC,EAAE,CAAC;GAE7E,MAAM,UAAqB,CAAC;GAC5B,KAAK,MAAM,QAAQ,SACjB,IAAI;IACF,QAAQ,KAAK,EAAE,MAAM,MAAM,KAAK,IAAI,KAAK,WAAW,IAAI,CAAC,EAAE,CAAC;GAC9D,SAAS,OAAO;IACd,QAAQ,KAAK,EAAE,OAAO,KAAK,MAAM,KAAK,EAAE,CAAC;GAC3C;GAEF,OAAO,KAAK,QAAQ,KAAK,EAAE,QAAQ,CAAC;EACtC;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typion/rpc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "An RPC endpoint for Typion, and its browser client: descriptors posted and answered typed",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/the-escape-studio/typion.git",
|
|
10
|
+
"directory": "packages/transports/rpc"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/the-escape-studio/typion/tree/main/packages/transports/rpc#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/the-escape-studio/typion/issues"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./client": {
|
|
27
|
+
"types": "./dist/client.d.ts",
|
|
28
|
+
"default": "./dist/client.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"typion": "^0.1.1"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
6
38
|
}
|