@arpabet/vrpc 0.2.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.
- package/README.md +75 -0
- package/dist/index.d.ts +561 -0
- package/dist/index.js +2262 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @arpabet/vrpc
|
|
2
|
+
|
|
3
|
+
value-rpc protocol core for browsers, Node ≥ 22, and Bun. Zero runtime
|
|
4
|
+
dependencies. See the [repo README](../../README.md) for the full tour.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createClient } from "@arpabet/vrpc";
|
|
8
|
+
|
|
9
|
+
const client = createClient({ url: "wss://example.com/rpc" });
|
|
10
|
+
|
|
11
|
+
await client.call("greet", ["world"]); // unary
|
|
12
|
+
for await (const v of client.getStream("count", [10])) {} // server stream
|
|
13
|
+
await client.putStream("upload", null, values); // client stream
|
|
14
|
+
const chat = client.chat("chat.echo"); // bidirectional
|
|
15
|
+
|
|
16
|
+
client.addFunction("notify", (args) => null); // server calls YOU
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Options (`createClient`)
|
|
20
|
+
|
|
21
|
+
| Option | Default | Meaning |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `url` | — | `ws://` / `wss://` endpoint |
|
|
24
|
+
| `codec` | `"msgpack"` | `"msgpack"` works with every server; `"json"` needs a JSON-codec server (negotiated via the `vrpc.json` subprotocol) |
|
|
25
|
+
| `auth` | — | credential (or async factory) for the handshake `auth` field |
|
|
26
|
+
| `timeoutMs` | `5000` | default unary timeout, sent as the request SLA |
|
|
27
|
+
| `connectTimeoutMs` | `10000` | dial + handshake bound |
|
|
28
|
+
| `metadata` | — | `Metadata` or per-request factory (`traceparent`, …) |
|
|
29
|
+
| `reconnect` | enabled | backoff options or `false` |
|
|
30
|
+
| `resume` | `true` | hash-chain session resumption across reconnects |
|
|
31
|
+
| `maxPending` | `4096` | per-stream receive window (flow-control credit) |
|
|
32
|
+
| `dialect` | standard | wire field names; must match the server's `Dialect` |
|
|
33
|
+
| `webSocket` / `dialer` | global / WS | overrides for tests and custom transports |
|
|
34
|
+
|
|
35
|
+
## Error model
|
|
36
|
+
|
|
37
|
+
Every failure is a `VrpcError` with a `code` mirroring `valuerpc.Code`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
try {
|
|
41
|
+
await client.call("user.get", [42]);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof VrpcError && err.code === Code.Unavailable) retryLater();
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Timeouts map to `Code.DeadlineExceeded`, aborts to `Code.Canceled`,
|
|
48
|
+
connection loss to `Code.Unavailable` — the same branches a Go caller writes
|
|
49
|
+
with `valuerpc.CodeOf`.
|
|
50
|
+
|
|
51
|
+
## Value model
|
|
52
|
+
|
|
53
|
+
`null | boolean | number | bigint | string | Uint8Array | Value[] | {…} |
|
|
54
|
+
VrpcDouble | VrpcDecimal | VrpcExt`
|
|
55
|
+
|
|
56
|
+
- Integer `number`s encode as value LONG; beyond ±2^53 pass a `bigint`
|
|
57
|
+
(exact int64 over msgpack). `double(x)` forces a DOUBLE encoding.
|
|
58
|
+
- `Uint8Array` is value RAW bytes.
|
|
59
|
+
- Maps are plain objects; keys encode sorted (canonical frames).
|
|
60
|
+
- `VrpcDecimal` (`decimal("1.045")`) and `VrpcExt` round-trip over msgpack.
|
|
61
|
+
|
|
62
|
+
## Streams & flow control
|
|
63
|
+
|
|
64
|
+
`getStream` returns an async iterator; **credit is granted to the server as
|
|
65
|
+
you consume**, so a slow consumer backpressures the producer losslessly.
|
|
66
|
+
`break`/`return` sends a `CancelRequest`. `putStream`/`chat.send` honor the
|
|
67
|
+
credit the server grants — a misbehaving peer is cut off with
|
|
68
|
+
`Code.ResourceExhausted`, exactly like the Go client.
|
|
69
|
+
|
|
70
|
+
## Reverse calls
|
|
71
|
+
|
|
72
|
+
The server can invoke functions and open streams on this client
|
|
73
|
+
(`addFunction`, `addOutgoingStream`, `addIncomingStream`, `addChat`) — the
|
|
74
|
+
same registrar surface `valueserver.Server` has. Handlers receive
|
|
75
|
+
`(args, ctx)` where `ctx.signal` aborts on cancellation/disconnect.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TypeScript projection of the `go.arpabet.com/value` model.
|
|
3
|
+
*
|
|
4
|
+
* | value kind | TypeScript |
|
|
5
|
+
* |-----------------|-------------------------------------|
|
|
6
|
+
* | NULL | null (undefined encodes as null) |
|
|
7
|
+
* | BOOL | boolean |
|
|
8
|
+
* | NUMBER/LONG | number (safe integers) or bigint |
|
|
9
|
+
* | NUMBER/DOUBLE | number (or VrpcDouble to force) |
|
|
10
|
+
* | NUMBER/BIGINT | bigint beyond int64 |
|
|
11
|
+
* | NUMBER/DECIMAL | VrpcDecimal |
|
|
12
|
+
* | STRING/UTF8 | string |
|
|
13
|
+
* | STRING/RAW | Uint8Array |
|
|
14
|
+
* | LIST | Array |
|
|
15
|
+
* | MAP | plain object (string keys) |
|
|
16
|
+
* | UNKNOWN (ext) | VrpcExt |
|
|
17
|
+
*/
|
|
18
|
+
type Value = null | boolean | number | bigint | string | Uint8Array | VrpcDouble | VrpcDecimal | VrpcExt | Value[] | {
|
|
19
|
+
[key: string]: Value;
|
|
20
|
+
};
|
|
21
|
+
/** A string-keyed map value (the envelope shape). */
|
|
22
|
+
type ValueMap = {
|
|
23
|
+
[key: string]: Value;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Forces a JS number to encode as a value DOUBLE (float64) even when it is
|
|
27
|
+
* integer-valued. Bare integer-valued numbers encode as LONG; use
|
|
28
|
+
* `double(42)` when the peer's handler verifies a DOUBLE argument.
|
|
29
|
+
*/
|
|
30
|
+
declare class VrpcDouble {
|
|
31
|
+
readonly value: number;
|
|
32
|
+
constructor(value: number);
|
|
33
|
+
valueOf(): number;
|
|
34
|
+
toString(): string;
|
|
35
|
+
}
|
|
36
|
+
/** Convenience constructor for {@link VrpcDouble}. */
|
|
37
|
+
declare function double(value: number): VrpcDouble;
|
|
38
|
+
/**
|
|
39
|
+
* An arbitrary-precision decimal: `coefficient * 10^exponent`, mirroring the
|
|
40
|
+
* shopspring/decimal representation used by value's NUMBER/DECIMAL kind.
|
|
41
|
+
* Round-trips through the msgpack codec (ext tag 2). The JSON codec cannot
|
|
42
|
+
* represent it (matching the Go-side asymmetry) and rejects it on encode.
|
|
43
|
+
*/
|
|
44
|
+
declare class VrpcDecimal {
|
|
45
|
+
readonly coefficient: bigint;
|
|
46
|
+
readonly exponent: number;
|
|
47
|
+
constructor(coefficient: bigint, exponent: number);
|
|
48
|
+
/** Parses "123", "-1.045", "1.2e-5" style decimal strings. */
|
|
49
|
+
static fromString(s: string): VrpcDecimal;
|
|
50
|
+
toString(): string;
|
|
51
|
+
toNumber(): number;
|
|
52
|
+
equals(other: VrpcDecimal): boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Convenience constructor for {@link VrpcDecimal} from a decimal string. */
|
|
55
|
+
declare function decimal(s: string): VrpcDecimal;
|
|
56
|
+
/**
|
|
57
|
+
* An opaque msgpack extension value (value kind UNKNOWN): the ext tag plus its
|
|
58
|
+
* payload bytes. Preserved through both codecs so unrecognized extensions
|
|
59
|
+
* round-trip unchanged. The JSON form is the value-library convention
|
|
60
|
+
* `"data:application/x-msgpack-ext;base64,<tag byte + data, base64>"`.
|
|
61
|
+
*/
|
|
62
|
+
declare class VrpcExt {
|
|
63
|
+
readonly tag: number;
|
|
64
|
+
readonly data: Uint8Array;
|
|
65
|
+
constructor(tag: number, data: Uint8Array);
|
|
66
|
+
equals(other: VrpcExt): boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* MessagePack codec producing the canonical byte form of go.arpabet.com/value
|
|
71
|
+
* (see its CANONICAL.md): minimal integer family, doubles always float64,
|
|
72
|
+
* str/bin distinct, map keys sorted by UTF-8 byte order, BIGINT as ext tag 1
|
|
73
|
+
* (big.Int gob framing), DECIMAL as ext tag 2 (shopspring framing). Equal
|
|
74
|
+
* values therefore produce byte-identical frames on both sides of the wire.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/** Decode limits mirroring value's limits.go defaults. */
|
|
78
|
+
interface DecodeLimits {
|
|
79
|
+
maxDepth: number;
|
|
80
|
+
maxCollectionLen: number;
|
|
81
|
+
maxByteLen: number;
|
|
82
|
+
}
|
|
83
|
+
declare const DEFAULT_LIMITS: DecodeLimits;
|
|
84
|
+
/** Encodes a single value in value's canonical MessagePack form. */
|
|
85
|
+
declare function msgpackEncode(v: Value): Uint8Array;
|
|
86
|
+
/** Decodes one value; throws on malformed, truncated, or oversized input. */
|
|
87
|
+
declare function msgpackDecode(data: Uint8Array, limits?: DecodeLimits): Value;
|
|
88
|
+
|
|
89
|
+
type CodecName = "msgpack" | "json";
|
|
90
|
+
/**
|
|
91
|
+
* The wire codec seam (WEB.md §3.3): one envelope per WebSocket frame,
|
|
92
|
+
* msgpack in binary frames, JSON in text frames.
|
|
93
|
+
*/
|
|
94
|
+
interface WireCodec {
|
|
95
|
+
readonly name: CodecName;
|
|
96
|
+
/**
|
|
97
|
+
* Subprotocol offered on dial. msgpack offers none: "no subprotocol" IS the
|
|
98
|
+
* msgpack negotiation, and it keeps compatibility with servers that predate
|
|
99
|
+
* codec negotiation. JSON offers "vrpc.json" and requires the server to echo
|
|
100
|
+
* it (a server that does not support JSON fails the connect with a clear
|
|
101
|
+
* error instead of choking on text frames).
|
|
102
|
+
*/
|
|
103
|
+
readonly subprotocols: string[];
|
|
104
|
+
readonly textFrames: boolean;
|
|
105
|
+
encode(msg: ValueMap): Uint8Array | string;
|
|
106
|
+
decode(data: Uint8Array | string): ValueMap;
|
|
107
|
+
}
|
|
108
|
+
declare function msgpackCodec(limits?: DecodeLimits): WireCodec;
|
|
109
|
+
declare function jsonCodec(limits?: DecodeLimits): WireCodec;
|
|
110
|
+
declare function codecByName(name: CodecName, limits?: DecodeLimits): WireCodec;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Machine-readable RPC status, mirroring valuerpc.Code (a useful subset of
|
|
114
|
+
* gRPC status codes). Carried on the wire in ErrorResponse frames.
|
|
115
|
+
*/
|
|
116
|
+
declare enum Code {
|
|
117
|
+
OK = 0,
|
|
118
|
+
Unknown = 1,
|
|
119
|
+
Canceled = 2,
|
|
120
|
+
InvalidArgument = 3,
|
|
121
|
+
DeadlineExceeded = 4,
|
|
122
|
+
NotFound = 5,
|
|
123
|
+
ResourceExhausted = 6,
|
|
124
|
+
Unavailable = 7,
|
|
125
|
+
Unauthenticated = 8,
|
|
126
|
+
Internal = 9
|
|
127
|
+
}
|
|
128
|
+
declare function codeName(code: Code): string;
|
|
129
|
+
/**
|
|
130
|
+
* A coded RPC error, mirroring *valuerpc.Error. Server-raised failures arrive
|
|
131
|
+
* with the server's code; client-local failures (timeout, connection loss,
|
|
132
|
+
* cancellation) use the matching local code so callers can branch on
|
|
133
|
+
* `err.code` the way Go callers branch on valuerpc.CodeOf(err).
|
|
134
|
+
*/
|
|
135
|
+
declare class VrpcError extends Error {
|
|
136
|
+
readonly code: Code;
|
|
137
|
+
readonly name = "VrpcError";
|
|
138
|
+
constructor(code: Code, message: string, options?: {
|
|
139
|
+
cause?: unknown;
|
|
140
|
+
});
|
|
141
|
+
/** The message without the "vrpc <Code>: " prefix. */
|
|
142
|
+
get detail(): string;
|
|
143
|
+
}
|
|
144
|
+
/** The Code carried by err: VrpcError's code, or Unknown for foreign errors. */
|
|
145
|
+
declare function codeOf(err: unknown): Code;
|
|
146
|
+
|
|
147
|
+
/** vRPC message types (wire numbering; 12/13 reserved). */
|
|
148
|
+
declare enum MessageType {
|
|
149
|
+
HandshakeRequest = 0,
|
|
150
|
+
HandshakeResponse = 1,
|
|
151
|
+
FunctionRequest = 2,
|
|
152
|
+
FunctionResponse = 3,
|
|
153
|
+
GetStreamRequest = 4,
|
|
154
|
+
PutStreamRequest = 5,
|
|
155
|
+
ChatRequest = 6,
|
|
156
|
+
ErrorResponse = 7,
|
|
157
|
+
StreamReady = 8,
|
|
158
|
+
StreamValue = 9,
|
|
159
|
+
StreamEnd = 10,
|
|
160
|
+
CancelRequest = 11,
|
|
161
|
+
StreamCredit = 14
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Dialect names the wire-level fields and markers of the vRPC envelope,
|
|
165
|
+
* mirroring valuerpc.Dialect. Both peers MUST share the same dialect.
|
|
166
|
+
* Override fields only if your Go deployment installed a custom Dialect.
|
|
167
|
+
*/
|
|
168
|
+
interface Dialect {
|
|
169
|
+
magic: string;
|
|
170
|
+
version: number;
|
|
171
|
+
handshakeRequestId: number;
|
|
172
|
+
messageTypeField: string;
|
|
173
|
+
magicField: string;
|
|
174
|
+
versionField: string;
|
|
175
|
+
requestIdField: string;
|
|
176
|
+
timeoutField: string;
|
|
177
|
+
clientIdField: string;
|
|
178
|
+
sessionTokenField: string;
|
|
179
|
+
authField: string;
|
|
180
|
+
functionNameField: string;
|
|
181
|
+
argumentsField: string;
|
|
182
|
+
resultField: string;
|
|
183
|
+
errorField: string;
|
|
184
|
+
codeField: string;
|
|
185
|
+
creditField: string;
|
|
186
|
+
metadataField: string;
|
|
187
|
+
valueField: string;
|
|
188
|
+
}
|
|
189
|
+
/** The standard vRPC dialect (the default wire format). */
|
|
190
|
+
declare function newDialect(): Dialect;
|
|
191
|
+
type Metadata = Record<string, string>;
|
|
192
|
+
/** Envelope builders and field accessors bound to one dialect. */
|
|
193
|
+
declare class Protocol {
|
|
194
|
+
readonly d: Dialect;
|
|
195
|
+
constructor(d: Dialect);
|
|
196
|
+
handshakeRequest(clientId: number, token: string, auth: Value | undefined): ValueMap;
|
|
197
|
+
request(mt: MessageType, requestId: number, name: string, args: Value | undefined, timeoutMs: number, metadata: Metadata | undefined): ValueMap;
|
|
198
|
+
functionResult(requestId: number, result: Value): ValueMap;
|
|
199
|
+
errorResponse(requestId: number, code: Code, message: string): ValueMap;
|
|
200
|
+
streamReady(requestId: number): ValueMap;
|
|
201
|
+
streamValue(requestId: number, value: Value): ValueMap;
|
|
202
|
+
streamEnd(requestId: number, value?: Value): ValueMap;
|
|
203
|
+
streamCredit(requestId: number, credit: number): ValueMap;
|
|
204
|
+
cancelRequest(requestId: number): ValueMap;
|
|
205
|
+
messageType(msg: ValueMap): MessageType | undefined;
|
|
206
|
+
requestId(msg: ValueMap): number | undefined;
|
|
207
|
+
functionName(msg: ValueMap): string | undefined;
|
|
208
|
+
args(msg: ValueMap): Value;
|
|
209
|
+
result(msg: ValueMap): Value;
|
|
210
|
+
streamVal(msg: ValueMap): Value;
|
|
211
|
+
credit(msg: ValueMap): number | undefined;
|
|
212
|
+
errorOf(msg: ValueMap): {
|
|
213
|
+
code: Code;
|
|
214
|
+
message: string;
|
|
215
|
+
};
|
|
216
|
+
metadata(msg: ValueMap): Metadata | undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* One established message connection. Deliberately tiny (WEB.md §7): a future
|
|
221
|
+
* WebTransport implementation slots in without touching the protocol layer.
|
|
222
|
+
*/
|
|
223
|
+
interface Transport {
|
|
224
|
+
send(data: Uint8Array | string): void;
|
|
225
|
+
close(): void;
|
|
226
|
+
onMessage: ((data: Uint8Array | string) => void) | null;
|
|
227
|
+
/** Fired exactly once, for both clean and failed closes. */
|
|
228
|
+
onClose: ((err?: Error) => void) | null;
|
|
229
|
+
}
|
|
230
|
+
interface DialOptions {
|
|
231
|
+
url: string;
|
|
232
|
+
subprotocols: string[];
|
|
233
|
+
timeoutMs: number;
|
|
234
|
+
/** Override the WebSocket constructor (tests, custom agents). */
|
|
235
|
+
webSocket?: typeof WebSocket;
|
|
236
|
+
}
|
|
237
|
+
type Dialer = (opts: DialOptions) => Promise<Transport>;
|
|
238
|
+
/** Dials a WebSocket and resolves once the connection is open. */
|
|
239
|
+
declare const webSocketDialer: Dialer;
|
|
240
|
+
|
|
241
|
+
type Status = "idle" | "connecting" | "open" | "resuming" | "closed";
|
|
242
|
+
interface ReconnectOptions {
|
|
243
|
+
/** Re-establish dropped connections automatically. Default true. */
|
|
244
|
+
enabled?: boolean;
|
|
245
|
+
/** First backoff delay. Default 300 ms. */
|
|
246
|
+
initialDelayMs?: number;
|
|
247
|
+
/** Backoff cap. Default 10 000 ms. */
|
|
248
|
+
maxDelayMs?: number;
|
|
249
|
+
/** Give up after this many consecutive failed attempts. Default Infinity. */
|
|
250
|
+
maxAttempts?: number;
|
|
251
|
+
/** Equal jitter (half fixed, half random), like the Go client. Default true. */
|
|
252
|
+
jitter?: boolean;
|
|
253
|
+
}
|
|
254
|
+
interface ClientOptions {
|
|
255
|
+
/** WebSocket endpoint, e.g. "wss://example.com/rpc". */
|
|
256
|
+
url: string;
|
|
257
|
+
/**
|
|
258
|
+
* Wire codec. "msgpack" (default) speaks to every value-rpc server —
|
|
259
|
+
* binary frames, no subprotocol. "json" offers the "vrpc.json" subprotocol
|
|
260
|
+
* and requires a server with JSON codec support (WEB.md phase 1).
|
|
261
|
+
*/
|
|
262
|
+
codec?: CodecName | WireCodec;
|
|
263
|
+
/**
|
|
264
|
+
* Credential for the handshake `auth` field, validated by the server's
|
|
265
|
+
* Authenticator. A function is re-evaluated on every (re)connect, so
|
|
266
|
+
* refreshed tokens are picked up automatically.
|
|
267
|
+
*/
|
|
268
|
+
auth?: Value | (() => Value | Promise<Value>);
|
|
269
|
+
/** Default unary timeout / request SLA in ms. Default 5000. */
|
|
270
|
+
timeoutMs?: number;
|
|
271
|
+
/** Dial + handshake timeout in ms. Default 10 000. */
|
|
272
|
+
connectTimeoutMs?: number;
|
|
273
|
+
/** Metadata attached to every request (static or per-request factory). */
|
|
274
|
+
metadata?: Metadata | ((name: string) => Metadata | undefined);
|
|
275
|
+
reconnect?: boolean | ReconnectOptions;
|
|
276
|
+
/**
|
|
277
|
+
* Hash-chain session resumption (default true): reconnects present a
|
|
278
|
+
* one-time token proving continuity, so the server reattaches the session.
|
|
279
|
+
* When disabled, every connection is a fresh session with a fresh clientId.
|
|
280
|
+
*/
|
|
281
|
+
resume?: boolean;
|
|
282
|
+
/** Resumption chain length = max reconnects per session. Default 1024. */
|
|
283
|
+
chainLength?: number;
|
|
284
|
+
/** Per-stream receive window (flow-control credit). Default 4096. */
|
|
285
|
+
maxPending?: number;
|
|
286
|
+
/** Wire dialect overrides; must match the server's Dialect. */
|
|
287
|
+
dialect?: Partial<Dialect>;
|
|
288
|
+
/** WebSocket constructor override (tests, custom agents). */
|
|
289
|
+
webSocket?: typeof WebSocket;
|
|
290
|
+
/** Transport override (in-memory tests, future WebTransport). */
|
|
291
|
+
dialer?: Dialer;
|
|
292
|
+
}
|
|
293
|
+
interface CallOptions {
|
|
294
|
+
/** Per-call timeout in ms; also sent to the server as the request SLA. */
|
|
295
|
+
timeoutMs?: number;
|
|
296
|
+
/** Abort maps to a vRPC CancelRequest toward the server. */
|
|
297
|
+
signal?: AbortSignal;
|
|
298
|
+
metadata?: Metadata;
|
|
299
|
+
}
|
|
300
|
+
/** A server->client stream (getStream): pull-based, credit-backed. */
|
|
301
|
+
interface VrpcStream extends AsyncIterableIterator<Value> {
|
|
302
|
+
readonly requestId: number;
|
|
303
|
+
/** Resolves when the server acks the stream (StreamReady). */
|
|
304
|
+
readonly ready: Promise<void>;
|
|
305
|
+
/** Cancels the stream (sends CancelRequest); the iterator ends. */
|
|
306
|
+
cancel(): void;
|
|
307
|
+
}
|
|
308
|
+
/** A bidirectional stream (chat). */
|
|
309
|
+
interface VrpcChat extends AsyncIterable<Value> {
|
|
310
|
+
readonly requestId: number;
|
|
311
|
+
readonly ready: Promise<void>;
|
|
312
|
+
readonly incoming: AsyncIterableIterator<Value>;
|
|
313
|
+
/** Sends one value; awaits flow-control credit from the peer. */
|
|
314
|
+
send(v: Value): Promise<void>;
|
|
315
|
+
/** Half-closes our sending side (StreamEnd); receiving continues. */
|
|
316
|
+
end(): void;
|
|
317
|
+
/** Tears the whole chat down (CancelRequest). */
|
|
318
|
+
cancel(): void;
|
|
319
|
+
}
|
|
320
|
+
interface HandlerContext {
|
|
321
|
+
readonly requestId: number;
|
|
322
|
+
readonly metadata: Metadata | undefined;
|
|
323
|
+
/** Aborted when the peer cancels the request or the connection drops. */
|
|
324
|
+
readonly signal: AbortSignal;
|
|
325
|
+
}
|
|
326
|
+
type UnaryHandler = (args: Value, ctx: HandlerContext) => Value | Promise<Value>;
|
|
327
|
+
type OutgoingStreamHandler = (args: Value, ctx: HandlerContext) => Iterable<Value> | AsyncIterable<Value> | Promise<Iterable<Value> | AsyncIterable<Value>>;
|
|
328
|
+
type IncomingStreamHandler = (args: Value, incoming: AsyncIterableIterator<Value>, ctx: HandlerContext) => void | Promise<void>;
|
|
329
|
+
type ChatHandler = (args: Value, incoming: AsyncIterableIterator<Value>, ctx: HandlerContext) => Iterable<Value> | AsyncIterable<Value> | Promise<Iterable<Value> | AsyncIterable<Value>>;
|
|
330
|
+
interface ClientEvents {
|
|
331
|
+
status: (status: Status) => void;
|
|
332
|
+
/** Fired per successful handshake with the server's handshake response. */
|
|
333
|
+
open: (handshake: ValueMap) => void;
|
|
334
|
+
close: () => void;
|
|
335
|
+
/** Non-fatal protocol anomalies (unroutable or malformed frames). */
|
|
336
|
+
error: (err: Error) => void;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A value-rpc client over WebSocket. Symmetric peer: it calls the server
|
|
340
|
+
* (call / getStream / putStream / chat) and serves the server's reverse
|
|
341
|
+
* calls (addFunction / addOutgoingStream / addIncomingStream / addChat).
|
|
342
|
+
*/
|
|
343
|
+
declare class VrpcClient {
|
|
344
|
+
readonly protocol: Protocol;
|
|
345
|
+
private readonly codec;
|
|
346
|
+
private readonly opts;
|
|
347
|
+
private readonly reconnect;
|
|
348
|
+
private readonly timeoutMs;
|
|
349
|
+
private readonly connectTimeoutMs;
|
|
350
|
+
private readonly maxPending;
|
|
351
|
+
private readonly dialer;
|
|
352
|
+
private _status;
|
|
353
|
+
private _clientId;
|
|
354
|
+
private transport;
|
|
355
|
+
private chain;
|
|
356
|
+
private established;
|
|
357
|
+
private nextRequestId;
|
|
358
|
+
private reconnects;
|
|
359
|
+
private pending;
|
|
360
|
+
private serving;
|
|
361
|
+
private handlers;
|
|
362
|
+
private listeners;
|
|
363
|
+
private connectPromise;
|
|
364
|
+
private resumeAbort;
|
|
365
|
+
private openWaiters;
|
|
366
|
+
private handshakeWaiter;
|
|
367
|
+
private handshakeTransport;
|
|
368
|
+
private hintsInstalled;
|
|
369
|
+
constructor(options: ClientOptions);
|
|
370
|
+
get status(): Status;
|
|
371
|
+
get connected(): boolean;
|
|
372
|
+
get clientId(): number;
|
|
373
|
+
get codecName(): CodecName;
|
|
374
|
+
stats(): {
|
|
375
|
+
requests: number;
|
|
376
|
+
reconnects: number;
|
|
377
|
+
pending: number;
|
|
378
|
+
serving: number;
|
|
379
|
+
};
|
|
380
|
+
on<E extends keyof ClientEvents>(event: E, fn: ClientEvents[E]): () => void;
|
|
381
|
+
private emit;
|
|
382
|
+
private setStatus;
|
|
383
|
+
/** Connects (idempotent). Calls also auto-connect on first use. */
|
|
384
|
+
connect(): Promise<void>;
|
|
385
|
+
private handshakeToken;
|
|
386
|
+
private doConnect;
|
|
387
|
+
/** Waits until the client is open, connecting if idle. */
|
|
388
|
+
private ensureOpen;
|
|
389
|
+
private onTransportClose;
|
|
390
|
+
private resumeLoop;
|
|
391
|
+
/** Browser wake hints: retry immediately when connectivity likely returned. */
|
|
392
|
+
private installBrowserHints;
|
|
393
|
+
private failAllInFlight;
|
|
394
|
+
/** Closes the client permanently. */
|
|
395
|
+
close(): void;
|
|
396
|
+
private sendFrame;
|
|
397
|
+
private trySendFrame;
|
|
398
|
+
cancelRequest(requestId: number): void;
|
|
399
|
+
private buildMetadata;
|
|
400
|
+
/** Calls a unary function on the server. */
|
|
401
|
+
call(name: string, args?: Value, opts?: CallOptions): Promise<Value>;
|
|
402
|
+
/**
|
|
403
|
+
* Opens a server->client stream. Consume with `for await`; breaking out of
|
|
404
|
+
* the loop cancels the stream. Credit is granted as values are consumed, so
|
|
405
|
+
* a slow consumer applies real backpressure to the server.
|
|
406
|
+
*/
|
|
407
|
+
getStream(name: string, args?: Value, opts?: CallOptions): VrpcStream;
|
|
408
|
+
private openConsumingStream;
|
|
409
|
+
private establishStream;
|
|
410
|
+
private retireGetSide;
|
|
411
|
+
/**
|
|
412
|
+
* Opens a client->server stream and sends every value from source, honoring
|
|
413
|
+
* the server's flow-control credit. Resolves once the stream ended cleanly.
|
|
414
|
+
*/
|
|
415
|
+
putStream(name: string, args: Value | undefined, source: Iterable<Value> | AsyncIterable<Value>, opts?: CallOptions): Promise<void>;
|
|
416
|
+
/** Opens a bidirectional stream (chat). */
|
|
417
|
+
chat(name: string, args?: Value, opts?: CallOptions): VrpcChat;
|
|
418
|
+
/** Registers a unary function the server may call on this client. */
|
|
419
|
+
addFunction(name: string, fn: UnaryHandler): void;
|
|
420
|
+
/** Registers a stream the server can consume from this client (GetStream). */
|
|
421
|
+
addOutgoingStream(name: string, fn: OutgoingStreamHandler): void;
|
|
422
|
+
/** Registers a stream the server can send to this client (PutStream). */
|
|
423
|
+
addIncomingStream(name: string, fn: IncomingStreamHandler): void;
|
|
424
|
+
/** Registers a bidirectional stream the server can open (Chat). */
|
|
425
|
+
addChat(name: string, fn: ChatHandler): void;
|
|
426
|
+
removeHandler(name: string): void;
|
|
427
|
+
private onFrame;
|
|
428
|
+
private processResponse;
|
|
429
|
+
private handlerCtx;
|
|
430
|
+
private serveInboundCall;
|
|
431
|
+
private serveInboundStream;
|
|
432
|
+
private serveRunning;
|
|
433
|
+
}
|
|
434
|
+
/** Creates a {@link VrpcClient}. The client connects lazily on first call. */
|
|
435
|
+
declare function createClient(options: ClientOptions): VrpcClient;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* JSON codec per WEB.md §3.1 — the value library's existing JSON conventions:
|
|
439
|
+
*
|
|
440
|
+
* - raw bytes <-> "base64,<standard base64, no padding>"
|
|
441
|
+
* - msgpack ext <-> "data:application/x-msgpack-ext;base64,<tag+data>"
|
|
442
|
+
* - NaN / ±Inf -> null (decode never produces them)
|
|
443
|
+
* - map keys -> sorted by UTF-8 byte order (canonical, deterministic)
|
|
444
|
+
* - integers -> plain JSON numbers; throws beyond ±2^53 on encode
|
|
445
|
+
* - bigint -> encoded as a number when within ±2^53, else rejected
|
|
446
|
+
* (the wire cannot carry it exactly; use msgpack)
|
|
447
|
+
* - VrpcDecimal -> rejected on encode (does not round-trip through JSON)
|
|
448
|
+
*
|
|
449
|
+
* Known hazard inherited from the convention: a *string* that genuinely starts
|
|
450
|
+
* with "base64," or "data:application/x-msgpack-ext;" is indistinguishable
|
|
451
|
+
* from encoded bytes/ext and will decode as such.
|
|
452
|
+
*/
|
|
453
|
+
|
|
454
|
+
/** Serializes a value to canonical JSON text (sorted keys). */
|
|
455
|
+
declare function jsonEncode(v: Value): string;
|
|
456
|
+
/** Parses JSON text into a value, applying the string-prefix conventions. */
|
|
457
|
+
declare function jsonDecode(text: string, limits?: DecodeLimits): Value;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Client-held reverse hash chain (S/KEY style) for replay-resistant session
|
|
461
|
+
* resumption, mirroring valuerpc.HashChain:
|
|
462
|
+
*
|
|
463
|
+
* seed = h[0], h[i] = SHA-256(h[i-1]), anchor = h[N]
|
|
464
|
+
*
|
|
465
|
+
* The anchor is presented (hex) in the first handshake's `tok` field; each
|
|
466
|
+
* reconnect reveals the next pre-image in reverse order. The chain always
|
|
467
|
+
* advances — even when a handshake is lost — and the server self-heals by
|
|
468
|
+
* hashing forward (its resync window), so a link is never revealed twice.
|
|
469
|
+
*
|
|
470
|
+
* Browser-sized default: 1024 links (~1024 awaited SHA-256 digests at build
|
|
471
|
+
* time, milliseconds) bounds reconnects per session; an exhausted chain makes
|
|
472
|
+
* the client start a fresh session (new clientId + new chain).
|
|
473
|
+
*/
|
|
474
|
+
declare class HashChain {
|
|
475
|
+
private readonly links;
|
|
476
|
+
private next;
|
|
477
|
+
private constructor();
|
|
478
|
+
static readonly DEFAULT_LENGTH = 1024;
|
|
479
|
+
static create(n?: number): Promise<HashChain>;
|
|
480
|
+
/** The public commitment h[N], hex-encoded. Resending it consumes nothing. */
|
|
481
|
+
anchor(): string;
|
|
482
|
+
/**
|
|
483
|
+
* Reveals the next one-time resumption token and advances. Returns "" when
|
|
484
|
+
* the chain is exhausted and a fresh session is required.
|
|
485
|
+
*/
|
|
486
|
+
nextToken(): string;
|
|
487
|
+
remaining(): number;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Typed calls without codegen — the TS analogue of valuerpc.Codec[T] /
|
|
492
|
+
* CallUnary. Describe your API as an interface and get compile-time checked
|
|
493
|
+
* method names, argument tuples, and result types:
|
|
494
|
+
*
|
|
495
|
+
* ```ts
|
|
496
|
+
* interface Api {
|
|
497
|
+
* "user.get": { args: [id: number]; result: User };
|
|
498
|
+
* "events.tail": { args: [topic: string]; stream: OrderEvent };
|
|
499
|
+
* "logs.upload": { args: []; put: string };
|
|
500
|
+
* "support.chat": { args: [ticket: number]; in: Message; out: string };
|
|
501
|
+
* }
|
|
502
|
+
* const api = typedClient<Api>(client);
|
|
503
|
+
* const user = await api.call("user.get", [42]);
|
|
504
|
+
* for await (const ev of api.getStream("events.tail", ["orders"])) { ... }
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
|
|
508
|
+
interface UnaryShape {
|
|
509
|
+
args: Value[];
|
|
510
|
+
result: unknown;
|
|
511
|
+
}
|
|
512
|
+
interface GetStreamShape {
|
|
513
|
+
args: Value[];
|
|
514
|
+
stream: unknown;
|
|
515
|
+
}
|
|
516
|
+
interface PutStreamShape {
|
|
517
|
+
args: Value[];
|
|
518
|
+
put: unknown;
|
|
519
|
+
}
|
|
520
|
+
interface ChatShape {
|
|
521
|
+
args: Value[];
|
|
522
|
+
in: unknown;
|
|
523
|
+
out: unknown;
|
|
524
|
+
}
|
|
525
|
+
type ApiShape = Record<string, UnaryShape | GetStreamShape | PutStreamShape | ChatShape>;
|
|
526
|
+
type UnaryNames<A extends ApiShape> = {
|
|
527
|
+
[K in keyof A]: A[K] extends UnaryShape ? K : never;
|
|
528
|
+
}[keyof A];
|
|
529
|
+
type GetNames<A extends ApiShape> = {
|
|
530
|
+
[K in keyof A]: A[K] extends GetStreamShape ? K : never;
|
|
531
|
+
}[keyof A];
|
|
532
|
+
type PutNames<A extends ApiShape> = {
|
|
533
|
+
[K in keyof A]: A[K] extends PutStreamShape ? K : never;
|
|
534
|
+
}[keyof A];
|
|
535
|
+
type ChatNames<A extends ApiShape> = {
|
|
536
|
+
[K in keyof A]: A[K] extends ChatShape ? K : never;
|
|
537
|
+
}[keyof A];
|
|
538
|
+
interface TypedStream<T> extends AsyncIterableIterator<T> {
|
|
539
|
+
readonly requestId: number;
|
|
540
|
+
readonly ready: Promise<void>;
|
|
541
|
+
cancel(): void;
|
|
542
|
+
}
|
|
543
|
+
interface TypedChat<TIn, TOut> extends AsyncIterable<TIn> {
|
|
544
|
+
readonly requestId: number;
|
|
545
|
+
readonly ready: Promise<void>;
|
|
546
|
+
readonly incoming: AsyncIterableIterator<TIn>;
|
|
547
|
+
send(v: TOut): Promise<void>;
|
|
548
|
+
end(): void;
|
|
549
|
+
cancel(): void;
|
|
550
|
+
}
|
|
551
|
+
interface TypedClient<A extends ApiShape> {
|
|
552
|
+
call<K extends UnaryNames<A>>(name: K, args: A[K] extends UnaryShape ? A[K]["args"] : never, opts?: CallOptions): Promise<A[K] extends UnaryShape ? A[K]["result"] : never>;
|
|
553
|
+
getStream<K extends GetNames<A>>(name: K, args: A[K] extends GetStreamShape ? A[K]["args"] : never, opts?: CallOptions): TypedStream<A[K] extends GetStreamShape ? A[K]["stream"] : never>;
|
|
554
|
+
putStream<K extends PutNames<A>>(name: K, args: A[K] extends PutStreamShape ? A[K]["args"] : never, source: A[K] extends PutStreamShape ? Iterable<A[K]["put"]> | AsyncIterable<A[K]["put"]> : never, opts?: CallOptions): Promise<void>;
|
|
555
|
+
chat<K extends ChatNames<A>>(name: K, args: A[K] extends ChatShape ? A[K]["args"] : never, opts?: CallOptions): TypedChat<A[K] extends ChatShape ? A[K]["in"] : never, A[K] extends ChatShape ? A[K]["out"] : never>;
|
|
556
|
+
readonly client: VrpcClient;
|
|
557
|
+
}
|
|
558
|
+
/** Wraps a client with a compile-time-typed API surface (zero runtime cost). */
|
|
559
|
+
declare function typedClient<A extends ApiShape>(client: VrpcClient): TypedClient<A>;
|
|
560
|
+
|
|
561
|
+
export { type ApiShape, type CallOptions, type ChatHandler, type ClientEvents, type ClientOptions, Code, type CodecName, DEFAULT_LIMITS, type DecodeLimits, type DialOptions, type Dialect, type Dialer, type HandlerContext, HashChain, type IncomingStreamHandler, MessageType, type Metadata, type OutgoingStreamHandler, Protocol, type ReconnectOptions, type Status, type Transport, type TypedChat, type TypedClient, type TypedStream, type UnaryHandler, type Value, type ValueMap, type VrpcChat, VrpcClient, VrpcDecimal, VrpcDouble, VrpcError, VrpcExt, type VrpcStream, type WireCodec, codeName, codeOf, codecByName, createClient, decimal, double, jsonCodec, jsonDecode, jsonEncode, msgpackCodec, msgpackDecode, msgpackEncode, newDialect, typedClient, webSocketDialer };
|