Skip to content

Commit c94c8d3

Browse files
Merge pull request #383 from Web3Auth/feat/jrpc-v2-readiness
feat: jrpc v2 readiness
2 parents ff3a3af + b01101b commit c94c8d3

8 files changed

Lines changed: 1027 additions & 20 deletions

package-lock.json

Lines changed: 0 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/jrpc/v2/compatibility-utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ export function propagateToRequest(req: Record<string, unknown>, context: Middle
9797
});
9898
}
9999

100+
/**
101+
* Copies non-JSON-RPC string properties from the context to the request.
102+
*
103+
* Clone the original request object and propagate the context to the cloned request.
104+
*
105+
* **ATTN:** Only string properties are copied.
106+
*
107+
* @param req - The request to propagate the context to.
108+
* @param context - The context to propagate from.
109+
* @returns The mutable cloned request.
110+
*/
111+
export function propagateToMutableRequest(req: Record<string, unknown>, context: MiddlewareContext): Record<string, unknown> {
112+
const clonedRequest = deepClone(req);
113+
propagateToRequest(clonedRequest, context);
114+
return clonedRequest;
115+
}
116+
100117
/**
101118
* Deserialize the error property for a thrown error, merging in the cause where possible.
102119
*

src/jrpc/v2/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
export { getUniqueId, isNotification, isRequest } from "../../utils/jrpc";
22
export { asLegacyMiddleware } from "./asLegacyMiddleware";
3+
export { deepClone, fromLegacyRequest, makeContext, propagateToContext, propagateToMutableRequest, propagateToRequest } from "./compatibility-utils";
34
export { createScaffoldMiddleware as createScaffoldMiddlewareV2 } from "./createScaffoldMiddleware";
45
export { JRPCEngineV2 } from "./jrpcEngineV2";
56
export { JRPCServer } from "./jrpcServer";
7+
export { createEngineStreamV2 } from "./messageStream";
68
export { MiddlewareContext } from "./MiddlewareContext";
9+
export {
10+
providerAsMiddleware as providerAsMiddlewareV2,
11+
providerFromEngine as providerFromEngineV2,
12+
providerFromMiddleware as providerFromMiddlewareV2,
13+
} from "./providerUtils";
714
export type {
815
ContextConstraint,
916
EmptyContext,

src/jrpc/v2/messageStream.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import log from "loglevel";
2+
import { Duplex } from "readable-stream";
3+
4+
import { isRequest } from "../../utils/jrpc";
5+
import { rpcErrors } from "../errors";
6+
import { JRPCRequest } from "../interfaces";
7+
import { SafeEventEmitter } from "../safeEventEmitter";
8+
import { JRPCEngineV2 } from "./jrpcEngineV2";
9+
10+
/**
11+
* Creates a Duplex object stream for an engine (JRPCEngineV2) + a separate notification emitter.
12+
*
13+
* Replaces V1's createEngineStream by decoupling notification forwarding from
14+
* the engine itself. Notifications are routed through a SafeEventEmitter that
15+
* pushes onto the same stream, so the engine no longer needs to be an EventEmitter.
16+
*/
17+
export function createEngineStreamV2({ engine, notificationEmitter }: { engine: JRPCEngineV2; notificationEmitter?: SafeEventEmitter }): Duplex {
18+
let stream: Duplex | undefined = undefined;
19+
20+
function noop() {
21+
// noop
22+
}
23+
24+
function handleRequest(req: JRPCRequest<unknown>) {
25+
return engine
26+
.handle(req)
27+
.then((res): undefined => {
28+
if (res !== undefined && isRequest(req)) {
29+
stream?.push({
30+
id: req.id,
31+
jsonrpc: "2.0",
32+
result: res,
33+
});
34+
}
35+
return undefined;
36+
})
37+
.catch((err: unknown) => {
38+
if (isRequest(req)) {
39+
const message = err instanceof Error ? err.message : "Internal JSON-RPC error";
40+
stream?.push({
41+
id: req.id,
42+
jsonrpc: "2.0",
43+
error: rpcErrors.internal({ message }),
44+
});
45+
}
46+
log.error(err);
47+
});
48+
}
49+
50+
function write(req: JRPCRequest<unknown>, _encoding: BufferEncoding, cb: (error?: Error | null) => void) {
51+
return handleRequest(req).finally(() => {
52+
cb();
53+
});
54+
}
55+
56+
stream = new Duplex({ objectMode: true, read: noop, write });
57+
58+
if (notificationEmitter) {
59+
const onNotification = (message: unknown) => {
60+
stream?.push(message);
61+
};
62+
63+
notificationEmitter.on("notification", onNotification);
64+
stream?.once("close", () => {
65+
notificationEmitter.removeListener("notification", onNotification);
66+
});
67+
}
68+
69+
return stream;
70+
}

src/jrpc/v2/providerUtils.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { getUniqueId } from "../../utils";
2+
import { serializeJrpcError } from "../errors";
3+
import { JRPCParams, JRPCRequest, JRPCResponse, Json, RequestArguments } from "../interfaces";
4+
import { ProviderEvents, SafeEventEmitterProvider } from "../jrpcEngine";
5+
import { SafeEventEmitter } from "../safeEventEmitter";
6+
import { deepClone, propagateToRequest } from "./compatibility-utils";
7+
import { JRPCEngineV2 } from "./jrpcEngineV2";
8+
import type { JRPCMiddlewareV2 } from "./v2interfaces";
9+
10+
/**
11+
* Create a {@link SafeEventEmitterProvider} from a {@link JRPCEngineV2}.
12+
*
13+
* Unlike the V1 counterpart, the V2 engine throws errors directly rather than
14+
* wrapping them in response objects, so `sendAsync` simply propagates thrown errors.
15+
* Notification forwarding is not supported since {@link JRPCEngineV2} is not an event emitter.
16+
*
17+
* @param engine - The V2 JSON-RPC engine.
18+
* @returns A provider backed by the engine.
19+
*/
20+
export function providerFromEngine(engine: JRPCEngineV2): SafeEventEmitterProvider {
21+
const provider: SafeEventEmitterProvider = new SafeEventEmitter<ProviderEvents>() as SafeEventEmitterProvider;
22+
23+
provider.sendAsync = async <T extends JRPCParams, U>(req: JRPCRequest<T>) => {
24+
const result = await engine.handle(req as JRPCRequest);
25+
return result as U;
26+
};
27+
28+
async function handleWithCallback<T extends JRPCParams, U>(req: JRPCRequest<T>, callback: (error: unknown, providerRes: JRPCResponse<U>) => void) {
29+
try {
30+
const result = await engine.handle(req as JRPCRequest);
31+
callback(null, { id: req.id, jsonrpc: "2.0", result: result as U });
32+
} catch (error) {
33+
const serializedError = serializeJrpcError(error, {
34+
shouldIncludeStack: false,
35+
shouldPreserveMessage: true,
36+
});
37+
callback(serializedError, { id: req.id, jsonrpc: "2.0", error: serializedError });
38+
}
39+
}
40+
41+
provider.send = <T extends JRPCParams, U>(req: JRPCRequest<T>, callback: (error: unknown, providerRes: JRPCResponse<U>) => void) => {
42+
if (typeof callback !== "function") {
43+
throw new Error('Must provide callback to "send" method.');
44+
}
45+
handleWithCallback(req, callback);
46+
};
47+
48+
provider.request = async <T extends JRPCParams, U>(args: RequestArguments<T>) => {
49+
const req: JRPCRequest<JRPCParams> = {
50+
...args,
51+
id: getUniqueId(),
52+
jsonrpc: "2.0",
53+
};
54+
const res = await provider.sendAsync(req);
55+
return res as U;
56+
};
57+
58+
return provider;
59+
}
60+
61+
/**
62+
* Create a {@link SafeEventEmitterProvider} from one or more V2 middleware.
63+
*
64+
* @param middleware - The V2 middleware to back the provider.
65+
* @returns A provider backed by an engine composed of the given middleware.
66+
*/
67+
export function providerFromMiddleware(middleware: JRPCMiddlewareV2): SafeEventEmitterProvider {
68+
const engine = JRPCEngineV2.create({ middleware: [middleware] });
69+
return providerFromEngine(engine as JRPCEngineV2);
70+
}
71+
72+
/**
73+
* Convert a {@link SafeEventEmitterProvider} into a V2 middleware.
74+
* The middleware delegates all requests to the provider's `sendAsync` method.
75+
*
76+
* @param provider - The provider to wrap as middleware.
77+
* @returns A V2 middleware that forwards requests to the provider.
78+
*/
79+
export function providerAsMiddleware(provider: SafeEventEmitterProvider): JRPCMiddlewareV2<JRPCRequest, Json> {
80+
return async ({ request, context }) => {
81+
const providerRequest = deepClone(request);
82+
propagateToRequest(providerRequest, context);
83+
return (await provider.sendAsync(providerRequest)) as Json;
84+
};
85+
}

0 commit comments

Comments
 (0)