@graphorin/client 0.6.0 → 0.7.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/CHANGELOG.md +30 -0
- package/README.md +3 -3
- package/dist/errors.d.ts +7 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/graphorin-client.d.ts +22 -0
- package/dist/graphorin-client.d.ts.map +1 -1
- package/dist/graphorin-client.js +20 -4
- package/dist/graphorin-client.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/reconnect.d.ts +3 -3
- package/dist/reconnect.js +2 -2
- package/dist/reconnect.js.map +1 -1
- package/dist/transport/types.d.ts +1 -1
- package/package.json +12 -11
- package/src/errors.ts +170 -0
- package/src/graphorin-client.ts +1075 -0
- package/src/index.ts +49 -0
- package/src/reconnect.ts +91 -0
- package/src/transport/index.ts +16 -0
- package/src/transport/sse.ts +174 -0
- package/src/transport/types.ts +106 -0
- package/src/transport/ws.ts +211 -0
package/dist/reconnect.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconnect.js","names":[],"sources":["../src/reconnect.ts"],"sourcesContent":["/**\n * Pure-functional reconnect-backoff helper. Encapsulated in its own\n * module so the
|
|
1
|
+
{"version":3,"file":"reconnect.js","names":[],"sources":["../src/reconnect.ts"],"sourcesContent":["/**\n * Pure-functional reconnect-backoff helper. Encapsulated in its own\n * module so the `GraphorinClient` stays free of timing\n * heuristics - and so tests can drive the policy with a deterministic\n * RNG.\n *\n * Algorithm: exponential backoff with full-jitter\n * (`delay = random(0, min(maxMs, baseMs * 2^attempt))`). The\n * implementation matches the AWS Architecture Blog \"exponential\n * backoff and jitter\" reference but is otherwise an original\n * formulation.\n *\n * @packageDocumentation\n */\n\n/**\n * Stable shape consumed by {@link computeBackoffMs}.\n *\n * @stable\n */\nexport interface BackoffPolicy {\n /** Initial slot in milliseconds. Default `500`. */\n readonly baseMs?: number;\n /** Cap on every individual sleep. Default `30_000`. */\n readonly maxMs?: number;\n /**\n * Hard cap on the number of attempts. The client surfaces a\n * `TransportFailedError` once exceeded. Default `Infinity`.\n */\n readonly maxAttempts?: number;\n /**\n * Optional injection seam used by tests; defaults to `Math.random`.\n */\n readonly random?: () => number;\n}\n\n/**\n * Compute the number of milliseconds to sleep before the\n * `attempt`-th reconnect (1-indexed). Returns `null` when the policy\n * has been exhausted (`attempt > maxAttempts`).\n *\n * @stable\n */\nexport function computeBackoffMs(attempt: number, policy: BackoffPolicy = {}): number | null {\n if (!Number.isFinite(attempt) || attempt < 1) {\n throw new RangeError('computeBackoffMs: attempt must be a positive integer.');\n }\n const baseMs = policy.baseMs ?? 500;\n const maxMs = policy.maxMs ?? 30_000;\n const maxAttempts = policy.maxAttempts ?? Number.POSITIVE_INFINITY;\n const random = policy.random ?? Math.random;\n if (attempt > maxAttempts) return null;\n // Exponential growth with full jitter, clamped to maxMs.\n const exp = Math.min(maxMs, baseMs * 2 ** Math.min(attempt - 1, 30));\n return Math.floor(random() * exp);\n}\n\n/**\n * Resolve when the requested number of milliseconds elapsed, or\n * reject (with a `DOMException`-style abort error) when the\n * supplied `AbortSignal` fires first.\n *\n * @stable\n */\nexport function sleep(durationMs: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(abortError(signal.reason));\n }\n return new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n cleanup();\n resolve();\n }, durationMs);\n const onAbort = (): void => {\n cleanup();\n reject(abortError(signal?.reason));\n };\n const cleanup = (): void => {\n clearTimeout(timeout);\n signal?.removeEventListener('abort', onAbort);\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\nfunction abortError(reason: unknown): Error {\n if (reason instanceof Error) return reason;\n const err = new Error('Aborted.');\n err.name = 'AbortError';\n return err;\n}\n"],"mappings":";;;;;;;;AA2CA,SAAgB,iBAAiB,SAAiB,SAAwB,EAAE,EAAiB;AAC3F,KAAI,CAAC,OAAO,SAAS,QAAQ,IAAI,UAAU,EACzC,OAAM,IAAI,WAAW,wDAAwD;CAE/E,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,QAAQ,OAAO,SAAS;CAC9B,MAAM,cAAc,OAAO,eAAe,OAAO;CACjD,MAAM,SAAS,OAAO,UAAU,KAAK;AACrC,KAAI,UAAU,YAAa,QAAO;CAElC,MAAM,MAAM,KAAK,IAAI,OAAO,SAAS,KAAK,KAAK,IAAI,UAAU,GAAG,GAAG,CAAC;AACpE,QAAO,KAAK,MAAM,QAAQ,GAAG,IAAI;;;;;;;;;AAUnC,SAAgB,MAAM,YAAoB,QAAqC;AAC7E,KAAI,QAAQ,QACV,QAAO,QAAQ,OAAO,WAAW,OAAO,OAAO,CAAC;AAElD,QAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,UAAU,iBAAiB;AAC/B,YAAS;AACT,YAAS;KACR,WAAW;EACd,MAAM,gBAAsB;AAC1B,YAAS;AACT,UAAO,WAAW,QAAQ,OAAO,CAAC;;EAEpC,MAAM,gBAAsB;AAC1B,gBAAa,QAAQ;AACrB,WAAQ,oBAAoB,SAAS,QAAQ;;AAE/C,UAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;GAC1D;;AAGJ,SAAS,WAAW,QAAwB;AAC1C,KAAI,kBAAkB,MAAO,QAAO;CACpC,MAAM,sBAAM,IAAI,MAAM,WAAW;AACjC,KAAI,OAAO;AACX,QAAO"}
|
|
@@ -88,7 +88,7 @@ interface Transport {
|
|
|
88
88
|
* Send a client → server frame. Throws when the transport is not
|
|
89
89
|
* in the open state, or when the underlying back-end does not
|
|
90
90
|
* support send (the SSE transport throws every send via
|
|
91
|
-
*
|
|
91
|
+
* `TransportFailedError` - clients
|
|
92
92
|
* should fall back to REST for control-plane operations on SSE).
|
|
93
93
|
*/
|
|
94
94
|
send(frame: ClientMessage): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphorin/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Reference TypeScript client for the Graphorin standalone server. Wraps the WebSocket subprotocol `graphorin.protocol.v1` (with the optional Server-Sent Events fallback for proxy-restricted environments) behind an ergonomic `GraphorinClient` class: `connect()`, `subscribe({ target, id })` returning an async-iterable subscription, `cancel(runId, opts)`, `resume(runId, directive)`, `ping()`, `disconnect()`. Handles the browser ticket flow (single-use ticket attached as a `ticket.<value>` `Sec-WebSocket-Protocol` token), exponential-backoff reconnect with `lastEventId` resume against the server replay buffer, and Zod-validated frame parsing on both directions. Browser-friendly: zero Node-only dependencies; the only runtime dependencies are `@graphorin/protocol` and `zod`. Created and maintained by Oleksiy Stepurenko.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Oleksiy Stepurenko",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"type": "module",
|
|
29
29
|
"engines": {
|
|
30
|
-
"node": ">=22.
|
|
30
|
+
"node": ">=22.12.0"
|
|
31
31
|
},
|
|
32
32
|
"main": "./dist/index.js",
|
|
33
33
|
"module": "./dist/index.js",
|
|
@@ -36,35 +36,36 @@
|
|
|
36
36
|
"exports": {
|
|
37
37
|
".": {
|
|
38
38
|
"types": "./dist/index.d.ts",
|
|
39
|
-
"
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
40
|
},
|
|
41
41
|
"./client": {
|
|
42
42
|
"types": "./dist/graphorin-client.d.ts",
|
|
43
|
-
"
|
|
43
|
+
"default": "./dist/graphorin-client.js"
|
|
44
44
|
},
|
|
45
45
|
"./transport": {
|
|
46
46
|
"types": "./dist/transport/index.d.ts",
|
|
47
|
-
"
|
|
47
|
+
"default": "./dist/transport/index.js"
|
|
48
48
|
},
|
|
49
49
|
"./reconnect": {
|
|
50
50
|
"types": "./dist/reconnect.d.ts",
|
|
51
|
-
"
|
|
51
|
+
"default": "./dist/reconnect.js"
|
|
52
52
|
},
|
|
53
53
|
"./errors": {
|
|
54
54
|
"types": "./dist/errors.d.ts",
|
|
55
|
-
"
|
|
55
|
+
"default": "./dist/errors.js"
|
|
56
56
|
},
|
|
57
57
|
"./package.json": "./package.json"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"dist",
|
|
61
|
+
"src",
|
|
61
62
|
"README.md",
|
|
62
63
|
"CHANGELOG.md",
|
|
63
64
|
"LICENSE"
|
|
64
65
|
],
|
|
65
66
|
"dependencies": {
|
|
66
67
|
"zod": "^3.25.0",
|
|
67
|
-
"@graphorin/protocol": "0.
|
|
68
|
+
"@graphorin/protocol": "0.7.0"
|
|
68
69
|
},
|
|
69
70
|
"publishConfig": {
|
|
70
71
|
"access": "public",
|
|
@@ -73,9 +74,9 @@
|
|
|
73
74
|
"devDependencies": {
|
|
74
75
|
"@types/ws": "^8.18.1",
|
|
75
76
|
"ws": "^8.20.1",
|
|
76
|
-
"@graphorin/security": "0.
|
|
77
|
-
"@graphorin/server": "0.
|
|
78
|
-
"@graphorin/store-sqlite": "0.
|
|
77
|
+
"@graphorin/security": "0.7.0",
|
|
78
|
+
"@graphorin/server": "0.7.0",
|
|
79
|
+
"@graphorin/store-sqlite": "0.7.0"
|
|
79
80
|
},
|
|
80
81
|
"scripts": {
|
|
81
82
|
"build": "tsdown",
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error hierarchy surfaced by `@graphorin/client`. Every error
|
|
3
|
+
* class extends the JavaScript built-in `Error` and exposes a stable
|
|
4
|
+
* `kind` discriminator so consumers can pattern-match without
|
|
5
|
+
* relying on `instanceof` (which behaves badly across module-system
|
|
6
|
+
* boundaries when the package is dual-loaded).
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { RPC_ERROR_CODES } from '@graphorin/protocol';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* IP-19: map a JSON-RPC error code from a server `RpcFailure` frame to the
|
|
15
|
+
* client's discriminated {@link GraphorinClientErrorKind}, so a rate-limited
|
|
16
|
+
* or scope-denied RPC is distinguishable from a genuine protocol violation.
|
|
17
|
+
*/
|
|
18
|
+
export function kindForRpcCode(code: number): GraphorinClientErrorKind {
|
|
19
|
+
switch (code) {
|
|
20
|
+
case RPC_ERROR_CODES.RATE_LIMITED:
|
|
21
|
+
return 'rate-limited';
|
|
22
|
+
case RPC_ERROR_CODES.SCOPE_DENIED:
|
|
23
|
+
return 'scope-denied';
|
|
24
|
+
case RPC_ERROR_CODES.AUTH_REQUIRED:
|
|
25
|
+
case RPC_ERROR_CODES.AUTH_INVALID:
|
|
26
|
+
return 'auth-failed';
|
|
27
|
+
case RPC_ERROR_CODES.RUN_NOT_FOUND:
|
|
28
|
+
return 'run-not-found';
|
|
29
|
+
case RPC_ERROR_CODES.SUBSCRIPTION_NOT_FOUND:
|
|
30
|
+
return 'subscription-not-found';
|
|
31
|
+
case RPC_ERROR_CODES.INTERNAL_ERROR:
|
|
32
|
+
return 'server-error';
|
|
33
|
+
default:
|
|
34
|
+
// INVALID_REQUEST / INVALID_PARAMS / METHOD_NOT_FOUND / PROTOCOL_VIOLATION
|
|
35
|
+
// and any unknown code stay a protocol violation.
|
|
36
|
+
return 'protocol-violation';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Discriminator union of every error kind raised by the client.
|
|
42
|
+
*
|
|
43
|
+
* @stable
|
|
44
|
+
*/
|
|
45
|
+
export type GraphorinClientErrorKind =
|
|
46
|
+
| 'client-not-connected'
|
|
47
|
+
| 'transport-failed'
|
|
48
|
+
| 'subprotocol-mismatch'
|
|
49
|
+
| 'auth-failed'
|
|
50
|
+
| 'protocol-violation'
|
|
51
|
+
| 'subscription-not-found'
|
|
52
|
+
// IP-19: discriminated RPC-failure kinds so callers can branch on the
|
|
53
|
+
// server's error class instead of pattern-matching the message string.
|
|
54
|
+
| 'rate-limited'
|
|
55
|
+
| 'scope-denied'
|
|
56
|
+
| 'run-not-found'
|
|
57
|
+
| 'server-error'
|
|
58
|
+
| 'aborted'
|
|
59
|
+
/**
|
|
60
|
+
* W-152: the per-subscription client buffer hit
|
|
61
|
+
* `subscriptionQueueLimit` because the `for await` consumer fell
|
|
62
|
+
* behind the event flow; the subscription is closed with this typed
|
|
63
|
+
* error instead of growing the heap or silently dropping frames.
|
|
64
|
+
*/
|
|
65
|
+
| 'flow-overflow'
|
|
66
|
+
| 'invalid-server-frame';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Base class for every error raised by `@graphorin/client`. Carries a
|
|
70
|
+
* stable {@link GraphorinClientErrorKind} discriminator and an
|
|
71
|
+
* optional `cause` chain.
|
|
72
|
+
*
|
|
73
|
+
* @stable
|
|
74
|
+
*/
|
|
75
|
+
export class GraphorinClientError extends Error {
|
|
76
|
+
readonly kind: GraphorinClientErrorKind;
|
|
77
|
+
|
|
78
|
+
constructor(kind: GraphorinClientErrorKind, message: string, options: ErrorOptions = {}) {
|
|
79
|
+
super(message, options);
|
|
80
|
+
this.kind = kind;
|
|
81
|
+
this.name = 'GraphorinClientError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @stable */
|
|
86
|
+
export class ClientNotConnectedError extends GraphorinClientError {
|
|
87
|
+
constructor(message = 'GraphorinClient is not connected. Call connect() first.') {
|
|
88
|
+
super('client-not-connected', message);
|
|
89
|
+
this.name = 'ClientNotConnectedError';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @stable */
|
|
94
|
+
export class TransportFailedError extends GraphorinClientError {
|
|
95
|
+
readonly code: number | undefined;
|
|
96
|
+
|
|
97
|
+
constructor(message: string, options: ErrorOptions & { readonly code?: number } = {}) {
|
|
98
|
+
super('transport-failed', message, options);
|
|
99
|
+
this.name = 'TransportFailedError';
|
|
100
|
+
this.code = options.code;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @stable */
|
|
105
|
+
export class SubprotocolMismatchError extends GraphorinClientError {
|
|
106
|
+
readonly expected: string;
|
|
107
|
+
readonly actual: string | null;
|
|
108
|
+
|
|
109
|
+
constructor(expected: string, actual: string | null) {
|
|
110
|
+
super(
|
|
111
|
+
'subprotocol-mismatch',
|
|
112
|
+
`Server selected subprotocol '${actual ?? '<none>'}'; expected '${expected}'.`,
|
|
113
|
+
);
|
|
114
|
+
this.name = 'SubprotocolMismatchError';
|
|
115
|
+
this.expected = expected;
|
|
116
|
+
this.actual = actual;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @stable */
|
|
121
|
+
export class AuthFailedError extends GraphorinClientError {
|
|
122
|
+
constructor(message = 'Authentication failed. Re-mint a token or request a new ticket.') {
|
|
123
|
+
super('auth-failed', message);
|
|
124
|
+
this.name = 'AuthFailedError';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @stable */
|
|
129
|
+
export class ProtocolViolationError extends GraphorinClientError {
|
|
130
|
+
constructor(message: string, options: ErrorOptions = {}) {
|
|
131
|
+
super('protocol-violation', message, options);
|
|
132
|
+
this.name = 'ProtocolViolationError';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @stable */
|
|
137
|
+
export class SubscriptionNotFoundError extends GraphorinClientError {
|
|
138
|
+
readonly subscriptionId: string;
|
|
139
|
+
|
|
140
|
+
constructor(subscriptionId: string) {
|
|
141
|
+
super(
|
|
142
|
+
'subscription-not-found',
|
|
143
|
+
`Subscription '${subscriptionId}' is not active on this client.`,
|
|
144
|
+
);
|
|
145
|
+
this.name = 'SubscriptionNotFoundError';
|
|
146
|
+
this.subscriptionId = subscriptionId;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @stable */
|
|
151
|
+
export class ClientAbortedError extends GraphorinClientError {
|
|
152
|
+
constructor(message = 'Operation aborted before completion.') {
|
|
153
|
+
super('aborted', message);
|
|
154
|
+
this.name = 'ClientAbortedError';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @stable */
|
|
159
|
+
export class InvalidServerFrameError extends GraphorinClientError {
|
|
160
|
+
readonly issues: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>;
|
|
161
|
+
|
|
162
|
+
constructor(
|
|
163
|
+
message: string,
|
|
164
|
+
issues: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>,
|
|
165
|
+
) {
|
|
166
|
+
super('invalid-server-frame', message);
|
|
167
|
+
this.name = 'InvalidServerFrameError';
|
|
168
|
+
this.issues = issues;
|
|
169
|
+
}
|
|
170
|
+
}
|