ts-bin-rpc 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/brands.d.ts +31 -0
- package/dist/brands.js +1 -0
- package/dist/client.d.ts +30 -0
- package/dist/client.js +111 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +48 -0
- package/dist/router.d.ts +34 -0
- package/dist/router.js +3 -0
- package/dist/rpc.d.ts +39 -0
- package/dist/rpc.js +38 -0
- package/dist/runtime.d.ts +300 -0
- package/dist/runtime.js +973 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.js +113 -0
- package/package.json +62 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artem Shaov
|
|
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,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ts-bin-rpc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A code-first TypeScript RPC with a binary wire format. Your router's TypeScript types are the
|
|
4
|
+
schema: [`ts-bin-rpc-cli`](https://www.npmjs.com/package/ts-bin-rpc-cli) reads them and generates
|
|
5
|
+
codecs. The client gets lazy views over the response bytes, and numeric arrays arrive as
|
|
6
|
+
`TypedArray` columns with no parsing.
|
|
7
|
+
|
|
8
|
+
This package is the runtime: `ts-bin-rpc/server`, `ts-bin-rpc/client`, `ts-bin-rpc/node`, the
|
|
9
|
+
numeric brands and the codec runtime the generated code imports.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install ts-bin-rpc@alpha
|
|
13
|
+
npm install -D ts-bin-rpc-cli@alpha typescript@7.1.0-beta
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Alpha.** The wire format may change before 1.0. Docs: https://tom4ikss.github.io/ts-bin-RPC/.
|
|
17
|
+
Source and issues: https://github.com/Tom4ikss/ts-bin-RPC. MIT license.
|
package/dist/brands.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const tbr: unique symbol;
|
|
2
|
+
export type u8 = number & {
|
|
3
|
+
readonly [tbr]?: 'u8';
|
|
4
|
+
};
|
|
5
|
+
export type i8 = number & {
|
|
6
|
+
readonly [tbr]?: 'i8';
|
|
7
|
+
};
|
|
8
|
+
export type u16 = number & {
|
|
9
|
+
readonly [tbr]?: 'u16';
|
|
10
|
+
};
|
|
11
|
+
export type i16 = number & {
|
|
12
|
+
readonly [tbr]?: 'i16';
|
|
13
|
+
};
|
|
14
|
+
export type u32 = number & {
|
|
15
|
+
readonly [tbr]?: 'u32';
|
|
16
|
+
};
|
|
17
|
+
export type i32 = number & {
|
|
18
|
+
readonly [tbr]?: 'i32';
|
|
19
|
+
};
|
|
20
|
+
export type f32 = number & {
|
|
21
|
+
readonly [tbr]?: 'f32';
|
|
22
|
+
};
|
|
23
|
+
export type f64 = number & {
|
|
24
|
+
readonly [tbr]?: 'f64';
|
|
25
|
+
};
|
|
26
|
+
export type u64 = bigint & {
|
|
27
|
+
readonly [tbr]?: 'u64';
|
|
28
|
+
};
|
|
29
|
+
export type i64 = bigint & {
|
|
30
|
+
readonly [tbr]?: 'i64';
|
|
31
|
+
};
|
package/dist/brands.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Limits } from './runtime.js';
|
|
2
|
+
import { type ClientProcedures } from './rpc.js';
|
|
3
|
+
import type { Client, DecodeMode, ProcedurePath } from './router.js';
|
|
4
|
+
export interface ClientOptions<R = unknown> {
|
|
5
|
+
/** Base URL of the procedures, e.g. `/rpc` or `https://api.example.com/rpc`. */
|
|
6
|
+
url: string;
|
|
7
|
+
/** `clientProcedures` (or `procedures`) from generated code. */
|
|
8
|
+
procedures: ClientProcedures;
|
|
9
|
+
/** Default: globalThis.fetch. */
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
/** Sent with every call; a function is called before each one. */
|
|
12
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
13
|
+
/**
|
|
14
|
+
* What calls return. `'lazy'` (default): views that read the response bytes on access. `'eager'`:
|
|
15
|
+
* plain objects, as `toPlain` gives, safe to mutate, spread and clone. A map sets it per procedure,
|
|
16
|
+
* `CallOptions.decode` per call.
|
|
17
|
+
*/
|
|
18
|
+
decode?: DecodeMode | {
|
|
19
|
+
readonly [P in ProcedurePath<R>]?: DecodeMode;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Verify responses before reading them. Default: on in development, off in production builds
|
|
23
|
+
* (`process.env.NODE_ENV === 'production'`, which bundlers replace at build time).
|
|
24
|
+
*/
|
|
25
|
+
verify?: boolean;
|
|
26
|
+
limits?: Partial<Limits>;
|
|
27
|
+
/** Log every call to the console: procedure, time, size, result. */
|
|
28
|
+
debug?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function createClient<R>(options: ClientOptions<R>): Client<R>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// createClient<AppRouter>: a proxy whose calls POST the encoded input and return views over the response.
|
|
2
|
+
import { decodeError, FLAG_ERROR, toPlain, VerifyError } from './runtime.js';
|
|
3
|
+
import { codeForStatus, CONTENT_TYPE, RpcError, SCHEMA_HEADER } from './rpc.js';
|
|
4
|
+
const decoder = new TextDecoder();
|
|
5
|
+
/** Production builds skip response verification by default; without a bundler this is false. */
|
|
6
|
+
function production() {
|
|
7
|
+
try {
|
|
8
|
+
return process.env.NODE_ENV === 'production';
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function createClient(options) {
|
|
15
|
+
const { procedures, verify = !production(), limits, debug = false } = options;
|
|
16
|
+
const decode = options.decode;
|
|
17
|
+
const base = options.url.replace(/\/+$/, '');
|
|
18
|
+
const warned = new Set();
|
|
19
|
+
async function call(path, args) {
|
|
20
|
+
const proc = procedures[path];
|
|
21
|
+
if (!proc)
|
|
22
|
+
throw new RpcError('NOT_FOUND', `procedure '${path}' is not in the generated code; run tbr generate`);
|
|
23
|
+
const [input, opts = {}] = (proc.input ? args : [undefined, args[0]]);
|
|
24
|
+
opts.signal?.throwIfAborted();
|
|
25
|
+
const body = proc.input && !(input === undefined && proc.inputOptional) ? proc.input.encode(input) : new Uint8Array(0);
|
|
26
|
+
const headers = new Headers(typeof options.headers === 'function' ? await options.headers() : options.headers);
|
|
27
|
+
if (opts.headers)
|
|
28
|
+
new Headers(opts.headers).forEach((v, k) => headers.set(k, v));
|
|
29
|
+
headers.set('content-type', CONTENT_TYPE);
|
|
30
|
+
headers.set(SCHEMA_HEADER, proc.hash);
|
|
31
|
+
const t0 = debug ? performance.now() : 0;
|
|
32
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
33
|
+
const res = await fetchFn(`${base}/${path}`, { method: 'POST', body, headers, signal: opts.signal });
|
|
34
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
35
|
+
let result;
|
|
36
|
+
let error;
|
|
37
|
+
if (res.headers.get('content-type')?.split(';')[0].trim() !== CONTENT_TYPE) {
|
|
38
|
+
error = new RpcError(res.ok ? 'BAD_RESPONSE' : codeForStatus(res.status), `HTTP ${res.status}: not a ts-bin-rpc response`, undefined, { status: res.status });
|
|
39
|
+
}
|
|
40
|
+
else if (!res.ok || (bytes.length > 3 && bytes[3] & FLAG_ERROR)) {
|
|
41
|
+
error = readError(bytes, res.status);
|
|
42
|
+
}
|
|
43
|
+
else if (proc.output) {
|
|
44
|
+
try {
|
|
45
|
+
const mode = opts.decode ?? (typeof decode === 'string' ? decode : decode?.[path]);
|
|
46
|
+
result = mode === 'eager' ? proc.output.decode(bytes, { verify, limits }) : proc.output.view(bytes, { verify, limits });
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (!(e instanceof VerifyError))
|
|
50
|
+
throw e;
|
|
51
|
+
error = new RpcError('BAD_RESPONSE', `invalid response: ${e.message}`, undefined, { status: res.status, cause: e });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (debug) {
|
|
55
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
56
|
+
console.groupCollapsed(`tbr ${path} ${error ? error.code : res.status} ${ms} ms, ${body.length} B → ${bytes.length} B`);
|
|
57
|
+
if (proc.input)
|
|
58
|
+
console.log('input', input);
|
|
59
|
+
if (error)
|
|
60
|
+
console.log('error', error);
|
|
61
|
+
else
|
|
62
|
+
console.log('output', toPlain(result));
|
|
63
|
+
console.groupEnd();
|
|
64
|
+
const serverHash = res.headers.get(SCHEMA_HEADER);
|
|
65
|
+
if (serverHash && serverHash !== proc.hash && !warned.has(path)) {
|
|
66
|
+
warned.add(path);
|
|
67
|
+
console.warn(`ts-bin-rpc: '${path}': the server has another version of this procedure (client ${proc.hash}, server ${serverHash})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (error)
|
|
71
|
+
throw error;
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
const proxy = (path) => {
|
|
75
|
+
const children = new Map();
|
|
76
|
+
return new Proxy(() => { }, {
|
|
77
|
+
get(_, key) {
|
|
78
|
+
// `then` stays undefined so that `await api.user` does not try to call a procedure.
|
|
79
|
+
if (typeof key !== 'string' || key === 'then')
|
|
80
|
+
return undefined;
|
|
81
|
+
let child = children.get(key);
|
|
82
|
+
if (!child)
|
|
83
|
+
children.set(key, (child = proxy(path ? `${path}.${key}` : key)));
|
|
84
|
+
return child;
|
|
85
|
+
},
|
|
86
|
+
apply(_, __, args) {
|
|
87
|
+
return call(path, args);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
return proxy('');
|
|
92
|
+
}
|
|
93
|
+
function readError(bytes, status) {
|
|
94
|
+
let e;
|
|
95
|
+
try {
|
|
96
|
+
e = decodeError(bytes);
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
return new RpcError('BAD_RESPONSE', `HTTP ${status}: malformed error response`, undefined, { status, cause });
|
|
100
|
+
}
|
|
101
|
+
let data;
|
|
102
|
+
if (e.data) {
|
|
103
|
+
try {
|
|
104
|
+
data = JSON.parse(decoder.decode(e.data));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
data = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return new RpcError(e.code, e.message, data, { status });
|
|
111
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { u8, i8, u16, i16, u32, i32, f32, f64, u64, i64 } from './brands.js';
|
|
2
|
+
export type { Client, ClientProcedure, CallOptions, DecodeMode, ProcedurePath, InputOf, OutputOf } from './router.js';
|
|
3
|
+
export { RpcError, STATUS, CONTENT_TYPE, SCHEMA_HEADER, type ClientProcedures, type ServerProcedures, type ClientEntry, type ServerEntry } from './rpc.js';
|
|
4
|
+
export { toPlain, EncodeError, VerifyError } from './runtime.js';
|
package/dist/index.js
ADDED
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
export interface NodeListenerOptions {
|
|
3
|
+
/** Requests with a larger body are answered 413 before the handler runs. Default: 64 MB. */
|
|
4
|
+
maxBytes?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function toNodeListener(handler: (request: Request) => Response | Promise<Response>, options?: NodeListenerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DEFAULT_LIMITS } from './runtime.js';
|
|
2
|
+
export function toNodeListener(handler, options = {}) {
|
|
3
|
+
const maxBytes = options.maxBytes ?? DEFAULT_LIMITS.maxBytes;
|
|
4
|
+
return async (req, res) => {
|
|
5
|
+
try {
|
|
6
|
+
const chunks = [];
|
|
7
|
+
let size = 0;
|
|
8
|
+
for await (const chunk of req) {
|
|
9
|
+
size += chunk.length;
|
|
10
|
+
if (size > maxBytes) {
|
|
11
|
+
res.writeHead(413).end();
|
|
12
|
+
req.destroy();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
chunks.push(chunk);
|
|
16
|
+
}
|
|
17
|
+
const body = new Uint8Array(size);
|
|
18
|
+
let at = 0;
|
|
19
|
+
for (const c of chunks) {
|
|
20
|
+
body.set(c, at);
|
|
21
|
+
at += c.length;
|
|
22
|
+
}
|
|
23
|
+
const headers = new Headers();
|
|
24
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
25
|
+
if (Array.isArray(v))
|
|
26
|
+
for (const x of v)
|
|
27
|
+
headers.append(k, x);
|
|
28
|
+
else if (v !== undefined)
|
|
29
|
+
headers.set(k, v);
|
|
30
|
+
}
|
|
31
|
+
const method = req.method ?? 'GET';
|
|
32
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
33
|
+
const response = await handler(new Request(url, { method, headers, body: method === 'GET' || method === 'HEAD' ? undefined : body }));
|
|
34
|
+
res.statusCode = response.status;
|
|
35
|
+
response.headers.forEach((v, k) => res.setHeader(k, v));
|
|
36
|
+
res.end(new Uint8Array(await response.arrayBuffer()));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
// The client went away (an aborted fetch, a closed tab): nobody to answer, nothing to report.
|
|
40
|
+
if (req.destroyed || res.destroyed)
|
|
41
|
+
return;
|
|
42
|
+
console.error('ts-bin-rpc: request failed:', e);
|
|
43
|
+
if (!res.headersSent)
|
|
44
|
+
res.writeHead(500);
|
|
45
|
+
res.end();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type AnyProcedure = (...args: any[]) => unknown;
|
|
2
|
+
export type AnyRouter = {
|
|
3
|
+
readonly [key: string]: AnyProcedure | AnyRouter;
|
|
4
|
+
};
|
|
5
|
+
/** `'user.get' | 'user.list' | ...` */
|
|
6
|
+
export type ProcedurePath<R, P extends string = ''> = {
|
|
7
|
+
[K in keyof R & string]: R[K] extends AnyProcedure ? `${P}${K}` : ProcedurePath<R[K], `${P}${K}.`>;
|
|
8
|
+
}[keyof R & string];
|
|
9
|
+
export type ProcedureAt<R, P extends string> = P extends `${infer H}.${infer T}` ? H extends keyof R ? ProcedureAt<R[H], T> : never : P extends keyof R ? R[P] : never;
|
|
10
|
+
/** Input of a procedure without `undefined`; `never` when it takes none. */
|
|
11
|
+
export type InputOf<R, P extends string> = Exclude<Parameters<Extract<ProcedureAt<R, P>, AnyProcedure>>[0], undefined | void>;
|
|
12
|
+
export type InputOptional<R, P extends string> = undefined extends Parameters<Extract<ProcedureAt<R, P>, AnyProcedure>>[0] ? true : false;
|
|
13
|
+
export type OutputOf<R, P extends string> = Awaited<ReturnType<Extract<ProcedureAt<R, P>, AnyProcedure>>>;
|
|
14
|
+
/** Client arguments for a procedure's parameter list: the input only, `ctx` stays on the server. */
|
|
15
|
+
export type InputArgs<A extends unknown[]> = A extends [] ? [] : A extends [infer I, ...unknown[]] ? [I] extends [void] ? [] : undefined extends I ? [input?: I] : [input: I] : A extends [input?: infer I, ...rest: unknown[]] ? [input?: I] : [];
|
|
16
|
+
/** What a call returns: `'lazy'` views that read the response on access, or `'eager'` plain objects. */
|
|
17
|
+
export type DecodeMode = 'lazy' | 'eager';
|
|
18
|
+
export interface CallOptions {
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
headers?: HeadersInit;
|
|
21
|
+
/** Overrides the client's `decode` for this call. */
|
|
22
|
+
decode?: DecodeMode;
|
|
23
|
+
}
|
|
24
|
+
export type ClientProcedure<F> = F extends (...args: infer A) => infer O ? (...args: [...InputArgs<A>, options?: CallOptions]) => Promise<Awaited<O>> : never;
|
|
25
|
+
export type Client<R> = {
|
|
26
|
+
readonly [K in keyof R]: R[K] extends AnyProcedure ? ClientProcedure<R[K]> : Client<R[K]>;
|
|
27
|
+
};
|
|
28
|
+
/** Shows up in a type error when generated code no longer matches the router. */
|
|
29
|
+
export interface StaleCodegen_RunTbrGenerate<Where extends string> {
|
|
30
|
+
readonly where: Where;
|
|
31
|
+
}
|
|
32
|
+
/** `true` when the generated and the source type accept the same values, else a readable error type. */
|
|
33
|
+
export type Same<Generated, Source, Where extends string> = [Generated] extends [Source] ? [Source] extends [Generated] ? true : StaleCodegen_RunTbrGenerate<Where> : StaleCodegen_RunTbrGenerate<Where>;
|
|
34
|
+
export type Expect<T extends true> = T;
|
package/dist/router.js
ADDED
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Decoder, Encoder } from './runtime.js';
|
|
2
|
+
export declare const CONTENT_TYPE = "application/vnd.tbr";
|
|
3
|
+
/** Request: the client's hash of the procedure's contract. Response: the server's. */
|
|
4
|
+
export declare const SCHEMA_HEADER = "x-tbr-schema";
|
|
5
|
+
/** HTTP status per error code. Codes not listed here are application errors and map to 400. */
|
|
6
|
+
export declare const STATUS: Readonly<Record<string, number>>;
|
|
7
|
+
export interface RpcErrorOptions {
|
|
8
|
+
status?: number;
|
|
9
|
+
cause?: unknown;
|
|
10
|
+
}
|
|
11
|
+
/** Thrown by procedures to send an error to the client; the client rethrows it with the same code. */
|
|
12
|
+
export declare class RpcError<Data = unknown> extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
readonly code: string;
|
|
15
|
+
readonly data: Data | undefined;
|
|
16
|
+
readonly status: number;
|
|
17
|
+
constructor(code: string, message: string, data?: Data, options?: RpcErrorOptions);
|
|
18
|
+
}
|
|
19
|
+
interface Entry<In, Out> {
|
|
20
|
+
input: In | null;
|
|
21
|
+
inputOptional: boolean;
|
|
22
|
+
output: Out | null;
|
|
23
|
+
/** Hash of the contract: the lockfile entries of every type the procedure reaches. */
|
|
24
|
+
hash: string;
|
|
25
|
+
}
|
|
26
|
+
/** What the client needs of a procedure: encode the input, read the output. */
|
|
27
|
+
export type ClientEntry = Entry<Encoder<any>, Decoder<any>>;
|
|
28
|
+
/** What the server needs of a procedure: read the input, encode the output. */
|
|
29
|
+
export type ServerEntry = Entry<Decoder<any>, Encoder<any>>;
|
|
30
|
+
/** `clientProcedures` of generated code (`procedures` fits too). */
|
|
31
|
+
export type ClientProcedures = {
|
|
32
|
+
readonly [path: string]: ClientEntry;
|
|
33
|
+
};
|
|
34
|
+
/** `serverProcedures` of generated code (`procedures` fits too). */
|
|
35
|
+
export type ServerProcedures = {
|
|
36
|
+
readonly [path: string]: ServerEntry;
|
|
37
|
+
};
|
|
38
|
+
export declare function codeForStatus(status: number): string;
|
|
39
|
+
export {};
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const CONTENT_TYPE = 'application/vnd.tbr';
|
|
2
|
+
/** Request: the client's hash of the procedure's contract. Response: the server's. */
|
|
3
|
+
export const SCHEMA_HEADER = 'x-tbr-schema';
|
|
4
|
+
/** HTTP status per error code. Codes not listed here are application errors and map to 400. */
|
|
5
|
+
export const STATUS = {
|
|
6
|
+
BAD_INPUT: 400,
|
|
7
|
+
UNAUTHORIZED: 401,
|
|
8
|
+
FORBIDDEN: 403,
|
|
9
|
+
NOT_FOUND: 404,
|
|
10
|
+
METHOD_NOT_ALLOWED: 405,
|
|
11
|
+
CONFLICT: 409,
|
|
12
|
+
SCHEMA_MISMATCH: 409,
|
|
13
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
14
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
15
|
+
TOO_MANY_REQUESTS: 429,
|
|
16
|
+
INTERNAL: 500,
|
|
17
|
+
BAD_OUTPUT: 500,
|
|
18
|
+
BAD_RESPONSE: 502,
|
|
19
|
+
};
|
|
20
|
+
/** Thrown by procedures to send an error to the client; the client rethrows it with the same code. */
|
|
21
|
+
export class RpcError extends Error {
|
|
22
|
+
name = 'RpcError';
|
|
23
|
+
code;
|
|
24
|
+
data;
|
|
25
|
+
status;
|
|
26
|
+
constructor(code, message, data, options = {}) {
|
|
27
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.data = data;
|
|
30
|
+
this.status = options.status ?? STATUS[code] ?? 400;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function codeForStatus(status) {
|
|
34
|
+
for (const code in STATUS)
|
|
35
|
+
if (STATUS[code] === status)
|
|
36
|
+
return code;
|
|
37
|
+
return status >= 500 ? 'INTERNAL' : 'BAD_RESPONSE';
|
|
38
|
+
}
|