@c9up/aurora 0.1.11 → 0.1.12
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/dist/index.d.ts +7 -6
- package/dist/index.js +7 -6
- package/dist/rpc.d.ts +55 -0
- package/dist/rpc.js +97 -0
- package/package.json +1 -1
- package/src/index.ts +40 -31
- package/src/rpc.ts +145 -0
package/dist/index.d.ts
CHANGED
|
@@ -9,14 +9,15 @@ export { html, isTemplateResult } from "./html.js";
|
|
|
9
9
|
export type { HttpClientOptions, HttpRequestOptions, HttpResult, } from "./http.js";
|
|
10
10
|
export { HttpClient, HttpError, http, isAbortError, isHttpError, } from "./http.js";
|
|
11
11
|
export { hydrate } from "./hydrate.js";
|
|
12
|
+
export { type LiveComponentDefinition, type LiveSession, mountLiveSession, type SlotPatch, } from "./live.js";
|
|
13
|
+
export { connectPatches, type LiveStore, liveStore, type RelayBroadcaster, } from "./liveBroadcast.js";
|
|
14
|
+
export { buildLiveTransport, type LiveClientOptions, type LiveClientTransport, type LiveHttpPoster, liveClient, type RelaySubscribeClient, } from "./liveClient.js";
|
|
15
|
+
export { createLiveRegistry, type LiveRegistry, type LiveSessionHandle, } from "./liveRegistry.js";
|
|
16
|
+
export { createLiveRouter, type LiveMount, type LiveRouter, } from "./liveRouter.js";
|
|
17
|
+
export { DEFAULT_LIVE_EVENT_PATH, type LiveHttpContext, type LiveHttpRouter, type WireLiveEventsOptions, wireLiveEvents, } from "./liveServer.js";
|
|
12
18
|
export { batch, effect, isSignal, memo, onCleanup, type ReadSignal, type Signal, signal, untrack, } from "./reactive.js";
|
|
13
19
|
export { type Disposer, render } from "./render.js";
|
|
14
20
|
export { type AuroraHttpContext, type AuroraResponse, type AuroraRouteConfig, auroraRoute, } from "./route.js";
|
|
21
|
+
export { createRpcClient, isRpcError, type RpcCall, type RpcClient, type RpcClientOptions, RpcError, type RpcResult, } from "./rpc.js";
|
|
15
22
|
export { renderToString } from "./ssr.js";
|
|
16
|
-
export { type LiveComponentDefinition, type LiveSession, mountLiveSession, type SlotPatch, } from "./live.js";
|
|
17
|
-
export { createLiveRegistry, type LiveRegistry, type LiveSessionHandle, } from "./liveRegistry.js";
|
|
18
|
-
export { connectPatches, type LiveStore, liveStore, type RelayBroadcaster, } from "./liveBroadcast.js";
|
|
19
|
-
export { createLiveRouter, type LiveMount, type LiveRouter, } from "./liveRouter.js";
|
|
20
|
-
export { buildLiveTransport, liveClient, type LiveClientOptions, type LiveClientTransport, type LiveHttpPoster, type RelaySubscribeClient, } from "./liveClient.js";
|
|
21
|
-
export { DEFAULT_LIVE_EVENT_PATH, type LiveHttpContext, type LiveHttpRouter, wireLiveEvents, type WireLiveEventsOptions, } from "./liveServer.js";
|
|
22
23
|
export type { TemplateResult } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,14 @@ export { form } from "./form.js";
|
|
|
5
5
|
export { html, isTemplateResult } from "./html.js";
|
|
6
6
|
export { HttpClient, HttpError, http, isAbortError, isHttpError, } from "./http.js";
|
|
7
7
|
export { hydrate } from "./hydrate.js";
|
|
8
|
-
export { batch, effect, isSignal, memo, onCleanup, signal, untrack, } from "./reactive.js";
|
|
9
|
-
export { render } from "./render.js";
|
|
10
|
-
export { auroraRoute, } from "./route.js";
|
|
11
|
-
export { renderToString } from "./ssr.js";
|
|
12
8
|
export { mountLiveSession, } from "./live.js";
|
|
13
|
-
export { createLiveRegistry, } from "./liveRegistry.js";
|
|
14
9
|
export { connectPatches, liveStore, } from "./liveBroadcast.js";
|
|
15
|
-
export { createLiveRouter, } from "./liveRouter.js";
|
|
16
10
|
export { buildLiveTransport, liveClient, } from "./liveClient.js";
|
|
11
|
+
export { createLiveRegistry, } from "./liveRegistry.js";
|
|
12
|
+
export { createLiveRouter, } from "./liveRouter.js";
|
|
17
13
|
export { DEFAULT_LIVE_EVENT_PATH, wireLiveEvents, } from "./liveServer.js";
|
|
14
|
+
export { batch, effect, isSignal, memo, onCleanup, signal, untrack, } from "./reactive.js";
|
|
15
|
+
export { render } from "./render.js";
|
|
16
|
+
export { auroraRoute, } from "./route.js";
|
|
17
|
+
export { createRpcClient, isRpcError, RpcError, } from "./rpc.js";
|
|
18
|
+
export { renderToString } from "./ssr.js";
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint. `@c9up/ream`'s
|
|
3
|
+
* RpcProvider mounts `POST /rpc` and speaks JSON-RPC 2.0 (single + batch); this
|
|
4
|
+
* client builds on aurora's {@link HttpClient}, inheriting its base URL, auth
|
|
5
|
+
* headers, and timeouts.
|
|
6
|
+
*
|
|
7
|
+
* const rpc = createRpcClient() // POST /rpc, same-origin
|
|
8
|
+
* const result = await rpc.call('task.validate', { id }) // typed via call<T>()
|
|
9
|
+
* const user = await rpc.call('user.find', { id }, isUser) // validated, cast-free
|
|
10
|
+
*
|
|
11
|
+
* Pairs with aurora's `command()` for reactive calls:
|
|
12
|
+
* const validate = command((p) => rpc.call('task.validate', p))
|
|
13
|
+
*/
|
|
14
|
+
import { HttpClient } from "./http.js";
|
|
15
|
+
export interface RpcClientOptions {
|
|
16
|
+
/** Endpoint path. Default `/rpc` (matches RpcProvider's default). */
|
|
17
|
+
url?: string;
|
|
18
|
+
/** Reuse an existing HttpClient — its baseURL / headers / auth carry over. */
|
|
19
|
+
http?: HttpClient;
|
|
20
|
+
/** Default headers — only used when no `http` client is supplied. */
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
/** A JSON-RPC 2.0 error returned by the server (code + message + optional data). */
|
|
24
|
+
export declare class RpcError extends Error {
|
|
25
|
+
readonly code: number;
|
|
26
|
+
readonly data?: unknown;
|
|
27
|
+
constructor(code: number, message: string, data?: unknown);
|
|
28
|
+
}
|
|
29
|
+
/** Type guard for {@link RpcError}. */
|
|
30
|
+
export declare function isRpcError(value: unknown): value is RpcError;
|
|
31
|
+
/** One call in a batch. `parse` optionally validates that call's result (cast-free). */
|
|
32
|
+
export interface RpcCall<T = unknown> {
|
|
33
|
+
method: string;
|
|
34
|
+
params?: unknown;
|
|
35
|
+
parse?: (data: unknown) => T;
|
|
36
|
+
}
|
|
37
|
+
/** A settled batch entry — the result, or the JSON-RPC error for that call. */
|
|
38
|
+
export type RpcResult<T = unknown> = {
|
|
39
|
+
ok: true;
|
|
40
|
+
value: T;
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
error: RpcError;
|
|
44
|
+
};
|
|
45
|
+
export interface RpcClient {
|
|
46
|
+
/**
|
|
47
|
+
* Call one method. Returns the result, or throws {@link RpcError} on a
|
|
48
|
+
* JSON-RPC error. Pass `parse` to validate the result at runtime (and skip
|
|
49
|
+
* the unchecked `T` assertion).
|
|
50
|
+
*/
|
|
51
|
+
call<T = unknown>(method: string, params?: unknown, parse?: (data: unknown) => T): Promise<T>;
|
|
52
|
+
/** Send a JSON-RPC batch. Returns one settled entry per call, in request order. */
|
|
53
|
+
batch(calls: RpcCall[]): Promise<RpcResult[]>;
|
|
54
|
+
}
|
|
55
|
+
export declare function createRpcClient(options?: RpcClientOptions): RpcClient;
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint. `@c9up/ream`'s
|
|
3
|
+
* RpcProvider mounts `POST /rpc` and speaks JSON-RPC 2.0 (single + batch); this
|
|
4
|
+
* client builds on aurora's {@link HttpClient}, inheriting its base URL, auth
|
|
5
|
+
* headers, and timeouts.
|
|
6
|
+
*
|
|
7
|
+
* const rpc = createRpcClient() // POST /rpc, same-origin
|
|
8
|
+
* const result = await rpc.call('task.validate', { id }) // typed via call<T>()
|
|
9
|
+
* const user = await rpc.call('user.find', { id }, isUser) // validated, cast-free
|
|
10
|
+
*
|
|
11
|
+
* Pairs with aurora's `command()` for reactive calls:
|
|
12
|
+
* const validate = command((p) => rpc.call('task.validate', p))
|
|
13
|
+
*/
|
|
14
|
+
import { HttpClient } from "./http.js";
|
|
15
|
+
/** A JSON-RPC 2.0 error returned by the server (code + message + optional data). */
|
|
16
|
+
export class RpcError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
data;
|
|
19
|
+
constructor(code, message, data) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "RpcError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.data = data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Type guard for {@link RpcError}. */
|
|
27
|
+
export function isRpcError(value) {
|
|
28
|
+
return value instanceof RpcError;
|
|
29
|
+
}
|
|
30
|
+
function isObject(value) {
|
|
31
|
+
return typeof value === "object" && value !== null;
|
|
32
|
+
}
|
|
33
|
+
/** Turn a JSON-RPC `error` member into an {@link RpcError}. */
|
|
34
|
+
function toRpcError(error) {
|
|
35
|
+
if (isObject(error) &&
|
|
36
|
+
typeof error.code === "number" &&
|
|
37
|
+
typeof error.message === "string") {
|
|
38
|
+
return new RpcError(error.code, error.message, error.data);
|
|
39
|
+
}
|
|
40
|
+
return new RpcError(-32603, "Malformed JSON-RPC error envelope", error);
|
|
41
|
+
}
|
|
42
|
+
export function createRpcClient(options = {}) {
|
|
43
|
+
const http = options.http ?? new HttpClient({ headers: options.headers });
|
|
44
|
+
const url = options.url ?? "/rpc";
|
|
45
|
+
let nextId = 0;
|
|
46
|
+
return {
|
|
47
|
+
async call(method, params, parse) {
|
|
48
|
+
const id = ++nextId;
|
|
49
|
+
const res = await http.post(url, {
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
method,
|
|
52
|
+
params,
|
|
53
|
+
id,
|
|
54
|
+
});
|
|
55
|
+
if (!isObject(res)) {
|
|
56
|
+
throw new RpcError(-32603, `Malformed JSON-RPC response for "${method}"`);
|
|
57
|
+
}
|
|
58
|
+
if (res.error !== undefined)
|
|
59
|
+
throw toRpcError(res.error);
|
|
60
|
+
// Result boundary — the same unchecked `T` assertion HttpClient uses,
|
|
61
|
+
// with `parse` as the cast-free, runtime-validated escape hatch.
|
|
62
|
+
return parse ? parse(res.result) : res.result;
|
|
63
|
+
},
|
|
64
|
+
async batch(calls) {
|
|
65
|
+
if (calls.length === 0)
|
|
66
|
+
return [];
|
|
67
|
+
const requests = calls.map((c, index) => ({
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
method: c.method,
|
|
70
|
+
params: c.params,
|
|
71
|
+
id: index, // index = request position; responses are matched back by id
|
|
72
|
+
}));
|
|
73
|
+
const res = await http.post(url, requests);
|
|
74
|
+
if (!Array.isArray(res)) {
|
|
75
|
+
throw new RpcError(-32603, "Malformed JSON-RPC batch response");
|
|
76
|
+
}
|
|
77
|
+
const byId = new Map();
|
|
78
|
+
for (const item of res)
|
|
79
|
+
if (isObject(item))
|
|
80
|
+
byId.set(item.id, item);
|
|
81
|
+
return calls.map((c, index) => {
|
|
82
|
+
const envelope = byId.get(index);
|
|
83
|
+
if (!envelope) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: new RpcError(-32603, `No response for "${c.method}"`),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (envelope.error !== undefined) {
|
|
90
|
+
return { ok: false, error: toRpcError(envelope.error) };
|
|
91
|
+
}
|
|
92
|
+
const value = c.parse ? c.parse(envelope.result) : envelope.result;
|
|
93
|
+
return { ok: true, value };
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/aurora",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -60,60 +60,69 @@ export {
|
|
|
60
60
|
isHttpError,
|
|
61
61
|
} from "./http.js";
|
|
62
62
|
export { hydrate } from "./hydrate.js";
|
|
63
|
-
export {
|
|
64
|
-
batch,
|
|
65
|
-
effect,
|
|
66
|
-
isSignal,
|
|
67
|
-
memo,
|
|
68
|
-
onCleanup,
|
|
69
|
-
type ReadSignal,
|
|
70
|
-
type Signal,
|
|
71
|
-
signal,
|
|
72
|
-
untrack,
|
|
73
|
-
} from "./reactive.js";
|
|
74
|
-
export { type Disposer, render } from "./render.js";
|
|
75
|
-
export {
|
|
76
|
-
type AuroraHttpContext,
|
|
77
|
-
type AuroraResponse,
|
|
78
|
-
type AuroraRouteConfig,
|
|
79
|
-
auroraRoute,
|
|
80
|
-
} from "./route.js";
|
|
81
|
-
export { renderToString } from "./ssr.js";
|
|
82
63
|
export {
|
|
83
64
|
type LiveComponentDefinition,
|
|
84
65
|
type LiveSession,
|
|
85
66
|
mountLiveSession,
|
|
86
67
|
type SlotPatch,
|
|
87
68
|
} from "./live.js";
|
|
88
|
-
export {
|
|
89
|
-
createLiveRegistry,
|
|
90
|
-
type LiveRegistry,
|
|
91
|
-
type LiveSessionHandle,
|
|
92
|
-
} from "./liveRegistry.js";
|
|
93
69
|
export {
|
|
94
70
|
connectPatches,
|
|
95
71
|
type LiveStore,
|
|
96
72
|
liveStore,
|
|
97
73
|
type RelayBroadcaster,
|
|
98
74
|
} from "./liveBroadcast.js";
|
|
99
|
-
export {
|
|
100
|
-
createLiveRouter,
|
|
101
|
-
type LiveMount,
|
|
102
|
-
type LiveRouter,
|
|
103
|
-
} from "./liveRouter.js";
|
|
104
75
|
export {
|
|
105
76
|
buildLiveTransport,
|
|
106
|
-
liveClient,
|
|
107
77
|
type LiveClientOptions,
|
|
108
78
|
type LiveClientTransport,
|
|
109
79
|
type LiveHttpPoster,
|
|
80
|
+
liveClient,
|
|
110
81
|
type RelaySubscribeClient,
|
|
111
82
|
} from "./liveClient.js";
|
|
83
|
+
export {
|
|
84
|
+
createLiveRegistry,
|
|
85
|
+
type LiveRegistry,
|
|
86
|
+
type LiveSessionHandle,
|
|
87
|
+
} from "./liveRegistry.js";
|
|
88
|
+
export {
|
|
89
|
+
createLiveRouter,
|
|
90
|
+
type LiveMount,
|
|
91
|
+
type LiveRouter,
|
|
92
|
+
} from "./liveRouter.js";
|
|
112
93
|
export {
|
|
113
94
|
DEFAULT_LIVE_EVENT_PATH,
|
|
114
95
|
type LiveHttpContext,
|
|
115
96
|
type LiveHttpRouter,
|
|
116
|
-
wireLiveEvents,
|
|
117
97
|
type WireLiveEventsOptions,
|
|
98
|
+
wireLiveEvents,
|
|
118
99
|
} from "./liveServer.js";
|
|
100
|
+
export {
|
|
101
|
+
batch,
|
|
102
|
+
effect,
|
|
103
|
+
isSignal,
|
|
104
|
+
memo,
|
|
105
|
+
onCleanup,
|
|
106
|
+
type ReadSignal,
|
|
107
|
+
type Signal,
|
|
108
|
+
signal,
|
|
109
|
+
untrack,
|
|
110
|
+
} from "./reactive.js";
|
|
111
|
+
export { type Disposer, render } from "./render.js";
|
|
112
|
+
export {
|
|
113
|
+
type AuroraHttpContext,
|
|
114
|
+
type AuroraResponse,
|
|
115
|
+
type AuroraRouteConfig,
|
|
116
|
+
auroraRoute,
|
|
117
|
+
} from "./route.js";
|
|
118
|
+
export {
|
|
119
|
+
createRpcClient,
|
|
120
|
+
isRpcError,
|
|
121
|
+
type RpcCall,
|
|
122
|
+
type RpcClient,
|
|
123
|
+
type RpcClientOptions,
|
|
124
|
+
RpcError,
|
|
125
|
+
type RpcResult,
|
|
126
|
+
} from "./rpc.js";
|
|
127
|
+
export { renderToString } from "./ssr.js";
|
|
119
128
|
export type { TemplateResult } from "./types.js";
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser JSON-RPC 2.0 client for Ream's RPC endpoint. `@c9up/ream`'s
|
|
3
|
+
* RpcProvider mounts `POST /rpc` and speaks JSON-RPC 2.0 (single + batch); this
|
|
4
|
+
* client builds on aurora's {@link HttpClient}, inheriting its base URL, auth
|
|
5
|
+
* headers, and timeouts.
|
|
6
|
+
*
|
|
7
|
+
* const rpc = createRpcClient() // POST /rpc, same-origin
|
|
8
|
+
* const result = await rpc.call('task.validate', { id }) // typed via call<T>()
|
|
9
|
+
* const user = await rpc.call('user.find', { id }, isUser) // validated, cast-free
|
|
10
|
+
*
|
|
11
|
+
* Pairs with aurora's `command()` for reactive calls:
|
|
12
|
+
* const validate = command((p) => rpc.call('task.validate', p))
|
|
13
|
+
*/
|
|
14
|
+
import { HttpClient } from "./http.js";
|
|
15
|
+
|
|
16
|
+
export interface RpcClientOptions {
|
|
17
|
+
/** Endpoint path. Default `/rpc` (matches RpcProvider's default). */
|
|
18
|
+
url?: string;
|
|
19
|
+
/** Reuse an existing HttpClient — its baseURL / headers / auth carry over. */
|
|
20
|
+
http?: HttpClient;
|
|
21
|
+
/** Default headers — only used when no `http` client is supplied. */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A JSON-RPC 2.0 error returned by the server (code + message + optional data). */
|
|
26
|
+
export class RpcError extends Error {
|
|
27
|
+
readonly code: number;
|
|
28
|
+
readonly data?: unknown;
|
|
29
|
+
constructor(code: number, message: string, data?: unknown) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "RpcError";
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.data = data;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Type guard for {@link RpcError}. */
|
|
38
|
+
export function isRpcError(value: unknown): value is RpcError {
|
|
39
|
+
return value instanceof RpcError;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One call in a batch. `parse` optionally validates that call's result (cast-free). */
|
|
43
|
+
export interface RpcCall<T = unknown> {
|
|
44
|
+
method: string;
|
|
45
|
+
params?: unknown;
|
|
46
|
+
parse?: (data: unknown) => T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A settled batch entry — the result, or the JSON-RPC error for that call. */
|
|
50
|
+
export type RpcResult<T = unknown> =
|
|
51
|
+
| { ok: true; value: T }
|
|
52
|
+
| { ok: false; error: RpcError };
|
|
53
|
+
|
|
54
|
+
export interface RpcClient {
|
|
55
|
+
/**
|
|
56
|
+
* Call one method. Returns the result, or throws {@link RpcError} on a
|
|
57
|
+
* JSON-RPC error. Pass `parse` to validate the result at runtime (and skip
|
|
58
|
+
* the unchecked `T` assertion).
|
|
59
|
+
*/
|
|
60
|
+
call<T = unknown>(
|
|
61
|
+
method: string,
|
|
62
|
+
params?: unknown,
|
|
63
|
+
parse?: (data: unknown) => T,
|
|
64
|
+
): Promise<T>;
|
|
65
|
+
/** Send a JSON-RPC batch. Returns one settled entry per call, in request order. */
|
|
66
|
+
batch(calls: RpcCall[]): Promise<RpcResult[]>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
70
|
+
return typeof value === "object" && value !== null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Turn a JSON-RPC `error` member into an {@link RpcError}. */
|
|
74
|
+
function toRpcError(error: unknown): RpcError {
|
|
75
|
+
if (
|
|
76
|
+
isObject(error) &&
|
|
77
|
+
typeof error.code === "number" &&
|
|
78
|
+
typeof error.message === "string"
|
|
79
|
+
) {
|
|
80
|
+
return new RpcError(error.code, error.message, error.data);
|
|
81
|
+
}
|
|
82
|
+
return new RpcError(-32603, "Malformed JSON-RPC error envelope", error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createRpcClient(options: RpcClientOptions = {}): RpcClient {
|
|
86
|
+
const http = options.http ?? new HttpClient({ headers: options.headers });
|
|
87
|
+
const url = options.url ?? "/rpc";
|
|
88
|
+
let nextId = 0;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
async call<T>(
|
|
92
|
+
method: string,
|
|
93
|
+
params?: unknown,
|
|
94
|
+
parse?: (data: unknown) => T,
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
const id = ++nextId;
|
|
97
|
+
const res = await http.post<unknown>(url, {
|
|
98
|
+
jsonrpc: "2.0",
|
|
99
|
+
method,
|
|
100
|
+
params,
|
|
101
|
+
id,
|
|
102
|
+
});
|
|
103
|
+
if (!isObject(res)) {
|
|
104
|
+
throw new RpcError(
|
|
105
|
+
-32603,
|
|
106
|
+
`Malformed JSON-RPC response for "${method}"`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (res.error !== undefined) throw toRpcError(res.error);
|
|
110
|
+
// Result boundary — the same unchecked `T` assertion HttpClient uses,
|
|
111
|
+
// with `parse` as the cast-free, runtime-validated escape hatch.
|
|
112
|
+
return parse ? parse(res.result) : (res.result as T);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async batch(calls: RpcCall[]): Promise<RpcResult[]> {
|
|
116
|
+
if (calls.length === 0) return [];
|
|
117
|
+
const requests = calls.map((c, index) => ({
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
method: c.method,
|
|
120
|
+
params: c.params,
|
|
121
|
+
id: index, // index = request position; responses are matched back by id
|
|
122
|
+
}));
|
|
123
|
+
const res = await http.post<unknown>(url, requests);
|
|
124
|
+
if (!Array.isArray(res)) {
|
|
125
|
+
throw new RpcError(-32603, "Malformed JSON-RPC batch response");
|
|
126
|
+
}
|
|
127
|
+
const byId = new Map<unknown, Record<string, unknown>>();
|
|
128
|
+
for (const item of res) if (isObject(item)) byId.set(item.id, item);
|
|
129
|
+
return calls.map((c, index) => {
|
|
130
|
+
const envelope = byId.get(index);
|
|
131
|
+
if (!envelope) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: new RpcError(-32603, `No response for "${c.method}"`),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (envelope.error !== undefined) {
|
|
138
|
+
return { ok: false, error: toRpcError(envelope.error) };
|
|
139
|
+
}
|
|
140
|
+
const value = c.parse ? c.parse(envelope.result) : envelope.result;
|
|
141
|
+
return { ok: true, value };
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|