bunite-core 0.9.0 → 0.10.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.
@@ -3,23 +3,22 @@ import {
3
3
  createFrameTransport,
4
4
  createWebSocketPipe,
5
5
  type Connection,
6
+ type CapDef,
6
7
  type Schema,
7
- type SchemaShape,
8
+ type SchemaRoots,
8
9
  type ClientOf,
9
- type ServerDescriptor,
10
10
  type WebSocketLike,
11
11
  } from "./index";
12
12
 
13
13
  declare global {
14
14
  interface Window {
15
15
  host?: {
16
- bootstrap<S extends SchemaShape, K extends keyof S["roots"] & string>(
17
- schema: Schema<S>,
18
- name: K
19
- ): Promise<ClientOf<S["roots"][K]>>;
20
- serve<S extends SchemaShape>(descriptor: ServerDescriptor<S>): Promise<void>;
16
+ bootstrap<C extends CapDef<any, any>>(cap: C): Promise<ClientOf<C>>;
17
+ bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
21
18
  runtime(): Promise<ClientOf<typeof import("./framework").RuntimeCap>>;
22
19
  releaseRef(proxy: unknown): Promise<void>;
20
+ /** Full Connection for renderer-as-server (serve / serveAll / unserve / replace / on). */
21
+ getConnection(): Promise<Connection>;
23
22
  };
24
23
  }
25
24
  }
@@ -60,23 +59,22 @@ function ensureWebConnection(path = "/rpc"): Promise<Connection> {
60
59
  return attempt;
61
60
  }
62
61
 
63
- export async function bootstrap<S extends SchemaShape, K extends keyof S["roots"] & string>(
64
- schema: Schema<S>,
65
- name: K
66
- ): Promise<ClientOf<S["roots"][K]>> {
62
+ export function bootstrap<C extends CapDef<any, any>>(cap: C): Promise<ClientOf<C>>;
63
+ export function bootstrap<R extends SchemaRoots>(schema: Schema<R>): Promise<{ [K in keyof R]: ClientOf<R[K]> }>;
64
+ export async function bootstrap(target: CapDef<any, any> | Schema<any>): Promise<unknown> {
67
65
  if (isNative()) {
68
66
  if (!window.host?.bootstrap) throw new Error("host preload not ready");
69
- return window.host.bootstrap(schema, name);
67
+ return (window.host.bootstrap as (t: unknown) => Promise<unknown>)(target);
70
68
  }
71
69
  const conn = await ensureWebConnection();
72
- return conn.bootstrap(schema, name);
70
+ return (conn.bootstrap as (t: unknown) => Promise<unknown>)(target);
73
71
  }
74
72
 
75
- export async function serve<S extends SchemaShape>(descriptor: ServerDescriptor<S>): Promise<void> {
73
+ /** Returns the underlying Connection — for renderer-as-server (`conn.serve(cap, impl)`), observability hooks (`conn.on(...)`), etc. */
74
+ export async function getConnection(): Promise<Connection> {
76
75
  if (isNative()) {
77
- if (!window.host?.serve) throw new Error("host preload not ready");
78
- return window.host.serve(descriptor);
76
+ if (!window.host?.getConnection) throw new Error("host preload not ready");
77
+ return window.host.getConnection();
79
78
  }
80
- const conn = await ensureWebConnection();
81
- conn.serve(descriptor);
79
+ return ensureWebConnection();
82
80
  }
package/src/rpc/schema.ts CHANGED
@@ -1,10 +1,11 @@
1
- const CALL_TAG = Symbol("CallDef");
2
- const STREAM_TAG = Symbol("StreamDef");
3
- const CAP_TAG = Symbol("CapDef");
4
- const CAP_REF_TAG = Symbol("CapRefToken");
5
- const CAP_ARRAY_TAG = Symbol("CapArrayToken");
6
- const CAP_RECORD_TAG = Symbol("CapRecordToken");
7
- const SCHEMA_TAG = Symbol("Schema");
1
+ // Symbol.for() shared identity across separately-built bundles (preload vs renderer).
2
+ const CALL_TAG = Symbol.for("bunite.rpc.CallDef");
3
+ const STREAM_TAG = Symbol.for("bunite.rpc.StreamDef");
4
+ const CAP_TAG = Symbol.for("bunite.rpc.CapDef");
5
+ const CAP_REF_TAG = Symbol.for("bunite.rpc.CapRefToken");
6
+ const CAP_ARRAY_TAG = Symbol.for("bunite.rpc.CapArrayToken");
7
+ const CAP_RECORD_TAG = Symbol.for("bunite.rpc.CapRecordToken");
8
+ const SCHEMA_TAG = Symbol.for("bunite.rpc.Schema");
8
9
  declare const EXPORTED_CAP_BRAND: unique symbol;
9
10
 
10
11
  export type ReturnsKind = "type" | "cap" | "capArray" | "capRecord";
@@ -96,25 +97,35 @@ export function stream<P = void, Y = unknown>(opts?: { hint?: Record<string, unk
96
97
  return { [STREAM_TAG]: true, hint: opts?.hint };
97
98
  }
98
99
 
100
+ // Disposal: method only. Sync Disposable — wire drop is fire-and-forget.
99
101
  export interface DisposalSpec<M extends MethodsRecord = MethodsRecord> {
100
102
  method: keyof M & string;
101
- async?: boolean;
102
103
  }
103
104
 
104
105
  export type MethodsRecord = Record<string, MethodDef>;
105
106
 
106
107
  export interface CapDef<M extends MethodsRecord = MethodsRecord, D extends DisposalSpec<M> | undefined = undefined> {
107
108
  readonly [CAP_TAG]: true;
109
+ readonly name: string;
110
+ readonly version?: string;
108
111
  readonly methods: M;
109
112
  readonly disposal: D;
110
113
  }
111
114
 
115
+ export interface DefineCapOpts<M extends MethodsRecord, D extends DisposalSpec<M> | undefined> {
116
+ version?: string | number;
117
+ disposal?: D;
118
+ }
119
+
112
120
  export function defineCap<M extends MethodsRecord, D extends DisposalSpec<M> | undefined = undefined>(
121
+ name: string,
113
122
  methods: M,
114
- opts?: { disposal?: D }
123
+ opts?: DefineCapOpts<M, D>
115
124
  ): CapDef<M, D> {
116
125
  return {
117
126
  [CAP_TAG]: true,
127
+ name,
128
+ version: opts?.version != null ? String(opts.version) : undefined,
118
129
  methods,
119
130
  disposal: (opts?.disposal as D) ?? (undefined as D),
120
131
  };
@@ -124,47 +135,20 @@ export function isCapDef(v: unknown): v is CapDef {
124
135
  return typeof v === "object" && v !== null && (v as any)[CAP_TAG] === true;
125
136
  }
126
137
 
127
- export interface SchemaShape {
128
- roots: Record<string, CapDef<any, any>>;
129
- caps?: readonly CapDef<any, any>[];
130
- }
138
+ // Schema = grouping sugar (Record<rootName, CapDef>). TS atomicity for serveAll.
139
+ export type SchemaRoots = Record<string, CapDef<any, any>>;
131
140
 
132
- export interface Schema<S extends SchemaShape = SchemaShape> {
141
+ export interface Schema<R extends SchemaRoots = SchemaRoots> {
133
142
  readonly [SCHEMA_TAG]: true;
134
- readonly roots: S["roots"];
135
- readonly caps: readonly CapDef<any, any>[];
136
- topologyHash(): Promise<string>;
137
- serve(impls: ImplsOf<S>): ServerDescriptor<S>;
138
- }
139
-
140
- export type ImplsOf<S extends SchemaShape> = {
141
- [K in keyof S["roots"]]: ImplOf<S["roots"][K]>;
142
- };
143
-
144
- export interface ServerDescriptor<S extends SchemaShape = SchemaShape> {
145
- readonly schema: Schema<S>;
146
- readonly impls: ImplsOf<S>;
147
- }
148
-
149
- export function defineSchema<S extends SchemaShape>(shape: S): Schema<S> {
150
- const schema: Schema<S> = {
151
- [SCHEMA_TAG]: true,
152
- roots: shape.roots,
153
- caps: shape.caps ?? [],
154
- topologyHash: () => topologyHashImpl(schema),
155
- serve(impls) {
156
- return { schema, impls };
157
- },
158
- };
159
- return schema;
143
+ readonly roots: R;
160
144
  }
161
145
 
162
- let topologyHashImpl: (s: Schema<any>) => Promise<string> = () => {
163
- throw new Error("schema.topologyHash bound after hash.ts import");
146
+ export type ImplsOf<R extends SchemaRoots> = {
147
+ [K in keyof R]: ImplOf<R[K]>;
164
148
  };
165
149
 
166
- export function _bindTopologyHash(fn: (s: Schema<any>) => Promise<string>) {
167
- topologyHashImpl = fn;
150
+ export function defineSchema<R extends SchemaRoots>(roots: R): Schema<R> {
151
+ return { [SCHEMA_TAG]: true, roots };
168
152
  }
169
153
 
170
154
  export function isSchema(v: unknown): v is Schema {
@@ -210,8 +194,7 @@ export type ClientReturn<R> =
210
194
  R extends CapRefToken<infer C> ? ClientOf<C> :
211
195
  R extends CapArrayToken<infer C> ?
212
196
  C extends CapDef<any, infer D>
213
- ? [D] extends [{ async: true }] ? ClientOf<C>[] & AsyncDisposable
214
- : [D] extends [DisposalSpec] ? ClientOf<C>[] & Disposable
197
+ ? [D] extends [DisposalSpec] ? ClientOf<C>[] & Disposable
215
198
  : ClientOf<C>[]
216
199
  : ClientOf<C>[] :
217
200
  R extends CapRecordToken<infer C> ? Record<string, ClientOf<C>> :
@@ -228,7 +211,7 @@ export type ClientOf<T> = T extends CapDef<infer M, infer D>
228
211
  ? [P] extends [void] ? () => Stream<Y>
229
212
  : (params: P) => Stream<Y>
230
213
  : never;
231
- } & ([D] extends [{ async: true }] ? AsyncDisposable : [D] extends [DisposalSpec] ? Disposable : {})
214
+ } & ([D] extends [DisposalSpec] ? Disposable : {})
232
215
  : never;
233
216
 
234
217
  export type ImplOf<T> = T extends CapDef<infer M, any>
package/src/rpc/wire.ts CHANGED
@@ -16,7 +16,6 @@ export interface CallMeta {
16
16
  parentCallId?: u53;
17
17
  deadlineMs?: u32;
18
18
  context?: Record<string, string>;
19
- topologyHash?: string;
20
19
  }
21
20
 
22
21
  export type StreamEvent =
@@ -39,7 +38,7 @@ export type Frame =
39
38
  op: "call";
40
39
  id: u53;
41
40
  target: Target;
42
- method: u32;
41
+ method: string;
43
42
  args: unknown;
44
43
  meta?: CallMeta;
45
44
  }
@@ -47,7 +46,8 @@ export type Frame =
47
46
  | { op: "result"; id: u53; ok: false; error: IpcStatus }
48
47
  | { op: "cancel"; id: u53; reason?: string }
49
48
  | ({ op: "stream"; id: u53 } & StreamEvent)
50
- | { op: "drop"; caps: { id: u32; delta: u32 }[] };
49
+ | { op: "drop"; caps: { id: u32; delta: u32 }[] }
50
+ | { op: "cap_revoked"; capIds: u32[] };
51
51
 
52
52
  export type CallFrame = Extract<Frame, { op: "call" }>;
53
53
  export type ResultFrame = Extract<Frame, { op: "result" }>;
@@ -56,6 +56,7 @@ export type CancelFrame = Extract<Frame, { op: "cancel" }>;
56
56
  export type DropFrame = Extract<Frame, { op: "drop" }>;
57
57
  export type HelloFrame = Extract<Frame, { op: "hello" }>;
58
58
  export type GoAwayFrame = Extract<Frame, { op: "goaway" }>;
59
+ export type CapRevokedFrame = Extract<Frame, { op: "cap_revoked" }>;
59
60
 
60
61
  const OPS = new Set<Frame["op"]>([
61
62
  "hello",
@@ -65,6 +66,7 @@ const OPS = new Set<Frame["op"]>([
65
66
  "cancel",
66
67
  "stream",
67
68
  "drop",
69
+ "cap_revoked",
68
70
  ]);
69
71
 
70
72
  export function isFrame(value: unknown): value is Frame {
@@ -79,7 +81,7 @@ export function isFrame(value: unknown): value is Frame {
79
81
  case "goaway":
80
82
  return f.reason === undefined || typeof f.reason === "string";
81
83
  case "call":
82
- return typeof f.id === "number" && typeof f.method === "number"
84
+ return typeof f.id === "number" && typeof f.method === "string"
83
85
  && typeof f.target === "object" && f.target !== null
84
86
  && (f.target as { kind?: unknown }).kind === "cap"
85
87
  && typeof (f.target as { id?: unknown }).id === "number";
@@ -92,6 +94,12 @@ export function isFrame(value: unknown): value is Frame {
92
94
  && (f.ev === "next" || f.ev === "credit" || f.ev === "end" || f.ev === "error");
93
95
  case "drop":
94
96
  return Array.isArray(f.caps);
97
+ case "cap_revoked":
98
+ if (!Array.isArray(f.capIds)) return false;
99
+ for (const id of f.capIds) {
100
+ if (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id > 0xFFFFFFFF) return false;
101
+ }
102
+ return true;
95
103
  }
96
104
  }
97
105
 
@@ -148,3 +156,9 @@ function readVarUint(buf: Buffer | Uint8Array): number {
148
156
 
149
157
  export const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
150
158
  export const PROTOCOL_VERSION = 1;
159
+
160
+ /** Reserved cap-name prefix for framework caps. User caps starting with this prefix are rejected. */
161
+ export const FRAMEWORK_NAME_PREFIX = "bunite.";
162
+
163
+ /** Synthetic method on cap-id 0 — dispatches to the bootstrap registry. */
164
+ export const BOOTSTRAP_METHOD = "bootstrap";
package/src/rpc/hash.ts DELETED
@@ -1,142 +0,0 @@
1
- import {
2
- type Schema,
3
- type CapDef,
4
- type MethodDef,
5
- type DisposalSpec,
6
- type AnyCapToken,
7
- type ReturnsKind,
8
- isCallDef,
9
- isStreamDef,
10
- isCapRef,
11
- isCapArray,
12
- isCapRecord,
13
- _bindTopologyHash,
14
- } from "./schema";
15
-
16
- type CanonicalReturns =
17
- | { kind: "type" }
18
- | { kind: "cap" | "capArray" | "capRecord"; capIndex: number };
19
-
20
- type CanonicalMethod =
21
- | { name: string; kind: "call"; idempotent: boolean; returns: CanonicalReturns }
22
- | { name: string; kind: "stream" };
23
-
24
- type CanonicalCap = {
25
- methods: CanonicalMethod[];
26
- disposal?: { method: string; async: boolean };
27
- };
28
-
29
- type CanonicalRoot = {
30
- name: string;
31
- capIndex: number;
32
- };
33
-
34
- type CanonicalSchema = {
35
- v: 1;
36
- roots: CanonicalRoot[];
37
- caps: CanonicalCap[];
38
- };
39
-
40
- export function canonicalize(schema: Schema<any>): CanonicalSchema {
41
- const capIndex = new Map<CapDef<any, any>, number>();
42
- const caps: CanonicalCap[] = [];
43
-
44
- for (const declared of schema.caps) {
45
- if (capIndex.has(declared)) continue;
46
- capIndex.set(declared, caps.length);
47
- caps.push(null as unknown as CanonicalCap);
48
- }
49
-
50
- const intern = (c: CapDef<any, any>): number => {
51
- const existing = capIndex.get(c);
52
- if (existing !== undefined) return existing;
53
- const idx = caps.length;
54
- capIndex.set(c, idx);
55
- caps.push(null as unknown as CanonicalCap);
56
- return idx;
57
- };
58
-
59
- for (let i = 0; i < caps.length; i++) {
60
- if (caps[i] !== null) continue;
61
- const declared = schema.caps[i];
62
- caps[i] = capToCanonical(declared, intern);
63
- }
64
-
65
- const roots: CanonicalRoot[] = Object.keys(schema.roots).map((name) => ({
66
- name,
67
- capIndex: intern(schema.roots[name]),
68
- }));
69
-
70
- for (let i = 0; i < caps.length; i++) {
71
- if (caps[i] !== null) continue;
72
- const cap = [...capIndex.keys()][i];
73
- caps[i] = capToCanonical(cap, intern);
74
- }
75
-
76
- return { v: 1, roots, caps };
77
- }
78
-
79
- function capToCanonical(c: CapDef<any, any>, intern: (c: CapDef<any, any>) => number): CanonicalCap {
80
- const methods: CanonicalMethod[] = Object.keys(c.methods).map((name) => {
81
- const m = c.methods[name] as MethodDef;
82
- if (isCallDef(m)) {
83
- return {
84
- name,
85
- kind: "call",
86
- idempotent: m.idempotent,
87
- returns: returnsToCanonical(m.returns, intern),
88
- };
89
- }
90
- if (isStreamDef(m)) {
91
- return { name, kind: "stream" };
92
- }
93
- throw new Error(`Unknown method def for "${name}"`);
94
- });
95
-
96
- const result: CanonicalCap = { methods };
97
- const disposal = c.disposal as DisposalSpec | undefined;
98
- if (disposal) {
99
- result.disposal = { method: disposal.method, async: !!disposal.async };
100
- }
101
- return result;
102
- }
103
-
104
- function returnsToCanonical(
105
- ret: AnyCapToken | undefined,
106
- intern: (c: CapDef<any, any>) => number
107
- ): CanonicalReturns {
108
- if (!ret) return { kind: "type" };
109
- if (isCapRef(ret)) return { kind: "cap", capIndex: intern(ret.cap) };
110
- if (isCapArray(ret)) return { kind: "capArray", capIndex: intern(ret.cap) };
111
- if (isCapRecord(ret)) return { kind: "capRecord", capIndex: intern(ret.cap) };
112
- return { kind: "type" };
113
- }
114
-
115
- function canonicalJSON(value: unknown): string {
116
- if (Array.isArray(value)) {
117
- return "[" + value.map(canonicalJSON).join(",") + "]";
118
- }
119
- if (value && typeof value === "object") {
120
- const keys = Object.keys(value as object).sort();
121
- return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJSON((value as Record<string, unknown>)[k])).join(",") + "}";
122
- }
123
- return JSON.stringify(value);
124
- }
125
-
126
- export async function topologyHash(schema: Schema<any>): Promise<string> {
127
- const canonical = canonicalize(schema);
128
- const json = canonicalJSON(canonical);
129
- const bytes = new TextEncoder().encode(json);
130
- const digest = await crypto.subtle.digest("SHA-256", bytes);
131
- return toHex(new Uint8Array(digest));
132
- }
133
-
134
- function toHex(bytes: Uint8Array): string {
135
- let out = "";
136
- for (let i = 0; i < bytes.length; i++) {
137
- out += bytes[i].toString(16).padStart(2, "0");
138
- }
139
- return out;
140
- }
141
-
142
- _bindTopologyHash(topologyHash);