feat(rpc): scaffold the package and shared contract types
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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 };
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user