feat(rpc): scaffold the package and shared contract types

This commit is contained in:
2026-08-05 09:52:48 +05:30
parent 63e6148cdb
commit e1fca3eddf
6 changed files with 137 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@wrnexus/rpc",
"version": "0.8.4",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"description": "Typed request/response calls between workspace apps, carrying end-user identity.",
"files": [
"src",
"README.md"
],
"scripts": {
"test": "bun test",
"typecheck": "tsc --noEmit",
"check": "bun run typecheck && bun run test"
},
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/jwt": "workspace:*",
"@wrnexus/validation": "workspace:*"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"typescript": "^5.9.2"
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* @wrnexus/rpc — typed request/response between workspace apps.
*
* A contract lives in the workspace's shared package and is imported by both
* sides: the callee `implement`s it, the caller gets a typed proxy. Types flow
* through a normal import, so there is no code generator and no generated file
* to go stale.
*/
export type {
AnyProcedures,
InferInput,
InputSchema,
InferProcedureInput,
InferProcedureOutput,
ProcedureDef,
ServiceContract,
ServiceResult,
} from "./types.ts";
+49
View File
@@ -0,0 +1,49 @@
import type { ObjectSchema } from "@wrnexus/validation";
/** Extract the validated value type from a `v.object(...)` schema. */
export type InferInput<S> = S extends ObjectSchema<infer T> ? T : never;
/**
* One callable procedure on a service. `input` is validated on the callee
* before the handler runs; `permission` is enforced there too.
*/
/** Structural shape of a validation schema, so ProcedureDef needs no generic. */
export interface InputSchema {
parse(value: Record<string, unknown>): {
ok: boolean;
value: unknown;
errors: Record<string, string>;
};
}
export interface ProcedureDef<Input = unknown, Output = unknown> {
input?: InputSchema;
/** Permission the callee checks before invoking the handler. */
permission?: string;
/** Only idempotent procedures are ever retried. */
idempotent?: boolean;
/** Type-only markers; never present at runtime. */
readonly __input?: Input;
readonly __output?: Output;
}
/**
* A procedure map with its element types erased. The `any` is deliberate and
* confined to this alias: the phantom `__input`/`__output` markers make
* ProcedureDef invariant, so no narrower erasure accepts a real contract.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyProcedures = Record<string, ProcedureDef<any, any>>;
export interface ServiceContract<Procedures extends AnyProcedures = AnyProcedures> {
/** Stable service id, used in the mounted path. */
name: string;
procedures: Procedures;
}
export type InferProcedureInput<P> = P extends ProcedureDef<infer I, unknown> ? I : never;
export type InferProcedureOutput<P> = P extends ProcedureDef<unknown, infer O> ? O : never;
/** What a transport returns: either a value or a structured failure. */
export type ServiceResult<T = unknown> =
{ ok: true; value: T } | { ok: false; code: string; message: string; retryable: boolean };
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, test } from "bun:test";
import { v } from "@wrnexus/validation";
import type { InferInput, ProcedureDef, ServiceContract } from "../src/types.ts";
describe("rpc types", () => {
test("InferInput extracts the validated shape from a schema", () => {
const schema = v.object({ userId: v.string(), amountCents: v.number() });
// Compile-time assertion: assigning a correctly-shaped value must typecheck.
const value: InferInput<typeof schema> = { userId: "u1", amountCents: 10 };
expect(value.userId).toBe("u1");
});
test("a contract carries its procedure map", () => {
const contract: ServiceContract<{ ping: ProcedureDef }> = {
name: "demo",
procedures: { ping: {} },
};
expect(contract.name).toBe("demo");
expect(Object.keys(contract.procedures)).toEqual(["ping"]);
});
});