@vincemakes/kiso-client 0.53.0 → 0.54.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/dist/client.d.ts +23 -1
- package/dist/client.js +62 -36
- package/dist/index.d.ts +1 -1
- package/package.json +6 -6
package/dist/client.d.ts
CHANGED
|
@@ -16,12 +16,28 @@ export interface ClientOptions {
|
|
|
16
16
|
readonly headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
17
17
|
/** Injected for tests or non-global environments. Default: globalThis.fetch. */
|
|
18
18
|
readonly fetch?: typeof fetch;
|
|
19
|
-
/** Reconnect backoff for `events()`: first wait and cap, in ms.
|
|
19
|
+
/** Reconnect backoff for `events()`: first wait and cap, in ms (defaults 250 and 30 000), jittered to 0.5..1 of the step;
|
|
20
|
+
* `idleMs`: no frame (event or keepalive comment) for this long and the connection is dead (default 35 000). */
|
|
20
21
|
readonly reconnect?: {
|
|
21
22
|
readonly initialMs?: number;
|
|
22
23
|
readonly maxMs?: number;
|
|
24
|
+
readonly idleMs?: number;
|
|
23
25
|
};
|
|
24
26
|
}
|
|
27
|
+
/** Where `events()` is: dialing, streaming, waiting to dial again (attempt, and the time of the next try), or turned away for good. */
|
|
28
|
+
export type ConnectionState = {
|
|
29
|
+
readonly kind: "connecting";
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: "open";
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: "retrying";
|
|
34
|
+
readonly attempt: number;
|
|
35
|
+
readonly nextAt: number;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: "refused";
|
|
38
|
+
readonly status: number;
|
|
39
|
+
readonly code: WireErrorCode;
|
|
40
|
+
};
|
|
25
41
|
/** A non-2xx reply, carrying the wire's error shape. */
|
|
26
42
|
export declare class ClientError extends Error {
|
|
27
43
|
readonly status: number;
|
|
@@ -53,6 +69,10 @@ export interface EventsOptions {
|
|
|
53
69
|
readonly until?: (event: ClientEvent) => boolean;
|
|
54
70
|
/** Called before each reconnect with the attempt number and the wait. */
|
|
55
71
|
readonly onReconnect?: (attempt: number, waitMs: number) => void;
|
|
72
|
+
/** Called on every change of the connection's state. A refusal (a 4xx) is reported here and ends the stream. */
|
|
73
|
+
readonly onState?: (state: ConnectionState) => void;
|
|
74
|
+
/** While true no reconnect is tried (a hidden tab); `retryNow()` or the next check ends the wait. */
|
|
75
|
+
readonly paused?: () => boolean;
|
|
56
76
|
}
|
|
57
77
|
export interface RunStreamOptions extends Omit<EventsOptions, "until"> {
|
|
58
78
|
readonly source?: WireSource;
|
|
@@ -61,6 +81,8 @@ export interface RunStreamOptions extends Omit<EventsOptions, "until"> {
|
|
|
61
81
|
export declare class SessionClient {
|
|
62
82
|
#private;
|
|
63
83
|
readonly id: string;
|
|
84
|
+
/** Cut a sleeping reconnect wait short and dial now (the browser came back online, the tab became visible, a button). */
|
|
85
|
+
retryNow(): void;
|
|
64
86
|
constructor(options: ClientOptions, sessionId: string);
|
|
65
87
|
/** Start a turn; refusals arrive as ClientError (in_flight, open_run, draining, forbidden). */
|
|
66
88
|
run(input: WireInput, options?: {
|
package/dist/client.js
CHANGED
|
@@ -15,6 +15,11 @@ export class ClientError extends Error {
|
|
|
15
15
|
export class SessionClient {
|
|
16
16
|
#options;
|
|
17
17
|
id;
|
|
18
|
+
#wake = null;
|
|
19
|
+
/** Cut a sleeping reconnect wait short and dial now (the browser came back online, the tab became visible, a button). */
|
|
20
|
+
retryNow() {
|
|
21
|
+
this.#wake?.();
|
|
22
|
+
}
|
|
18
23
|
constructor(options, sessionId) {
|
|
19
24
|
this.#options = options;
|
|
20
25
|
this.id = sessionId;
|
|
@@ -102,29 +107,40 @@ export class SessionClient {
|
|
|
102
107
|
let lastSeq = options.after ?? -1;
|
|
103
108
|
let attempt = 0;
|
|
104
109
|
const initial = this.#options.reconnect?.initialMs ?? 250;
|
|
105
|
-
const cap = this.#options.reconnect?.maxMs ??
|
|
110
|
+
const cap = this.#options.reconnect?.maxMs ?? 30_000;
|
|
111
|
+
const idle = this.#options.reconnect?.idleMs ?? 35_000;
|
|
112
|
+
const report = options.onState ?? (() => { });
|
|
106
113
|
for (;;) {
|
|
107
114
|
if (options.signal?.aborted)
|
|
108
115
|
return;
|
|
109
|
-
|
|
116
|
+
report({ kind: "connecting" });
|
|
117
|
+
// the watchdog: no frame for `idle` aborts this connection, and the loop below dials again
|
|
118
|
+
const conn = new AbortController();
|
|
119
|
+
const forward = () => conn.abort();
|
|
120
|
+
options.signal?.addEventListener("abort", forward, { once: true });
|
|
121
|
+
let dog;
|
|
122
|
+
const feed = () => {
|
|
123
|
+
clearTimeout(dog);
|
|
124
|
+
dog = setTimeout(forward, idle);
|
|
125
|
+
};
|
|
126
|
+
feed();
|
|
110
127
|
try {
|
|
111
|
-
res = await this.#request("GET", "events", undefined, {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
attempt = 0;
|
|
126
|
-
try {
|
|
128
|
+
const res = await this.#request("GET", "events", undefined, { headers: { accept: "text/event-stream", "last-event-id": String(lastSeq) }, signal: conn.signal });
|
|
129
|
+
if (res.status >= 400 && res.status < 500) {
|
|
130
|
+
try {
|
|
131
|
+
await this.#json(res, []);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (err instanceof ClientError)
|
|
135
|
+
report({ kind: "refused", status: err.status, code: err.code });
|
|
136
|
+
}
|
|
137
|
+
return; // a refusal is final, and now it is said
|
|
138
|
+
}
|
|
139
|
+
if (res.status === 200 && res.body !== null) {
|
|
140
|
+
attempt = 0;
|
|
141
|
+
report({ kind: "open" });
|
|
127
142
|
for await (const frame of readSse(res.body)) {
|
|
143
|
+
feed();
|
|
128
144
|
if (options.signal?.aborted)
|
|
129
145
|
return;
|
|
130
146
|
const ev = toClientEvent(frame);
|
|
@@ -140,18 +156,41 @@ export class SessionClient {
|
|
|
140
156
|
return;
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// the connection dropped or went silent — fall through to reconnect
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
clearTimeout(dog);
|
|
165
|
+
options.signal?.removeEventListener("abort", forward);
|
|
146
166
|
}
|
|
147
167
|
if (options.signal?.aborted)
|
|
148
168
|
return;
|
|
149
169
|
attempt += 1;
|
|
150
|
-
const wait = Math.min(cap, initial * 2 ** (attempt - 1));
|
|
170
|
+
const wait = (0.5 + Math.random() / 2) * Math.min(cap, initial * 2 ** (attempt - 1));
|
|
151
171
|
options.onReconnect?.(attempt, wait);
|
|
152
|
-
|
|
172
|
+
report({ kind: "retrying", attempt, nextAt: Date.now() + wait });
|
|
173
|
+
await this.#nap(wait, options.signal);
|
|
174
|
+
while (options.paused?.() === true && options.signal?.aborted !== true)
|
|
175
|
+
await this.#nap(1_000, options.signal);
|
|
153
176
|
}
|
|
154
177
|
}
|
|
178
|
+
#nap(ms, signal) {
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
const done = () => {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
signal?.removeEventListener("abort", done);
|
|
183
|
+
this.#wake = null;
|
|
184
|
+
resolve();
|
|
185
|
+
};
|
|
186
|
+
const timer = setTimeout(done, ms);
|
|
187
|
+
this.#wake = done;
|
|
188
|
+
if (signal?.aborted)
|
|
189
|
+
done();
|
|
190
|
+
else
|
|
191
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
155
194
|
}
|
|
156
195
|
export class KisoClient {
|
|
157
196
|
#options;
|
|
@@ -182,16 +221,3 @@ function toClientEvent(frame) {
|
|
|
182
221
|
function isWireError(value) {
|
|
183
222
|
return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
|
|
184
223
|
}
|
|
185
|
-
function sleep(ms, signal) {
|
|
186
|
-
return new Promise((resolve) => {
|
|
187
|
-
if (signal?.aborted)
|
|
188
|
-
return resolve();
|
|
189
|
-
const timer = setTimeout(done, ms);
|
|
190
|
-
function done() {
|
|
191
|
-
clearTimeout(timer);
|
|
192
|
-
signal?.removeEventListener("abort", done);
|
|
193
|
-
resolve();
|
|
194
|
-
}
|
|
195
|
-
signal?.addEventListener("abort", done, { once: true });
|
|
196
|
-
});
|
|
197
|
-
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
* event. Browser and Node; depends on `@vincemakes/kiso-protocol` alone.
|
|
7
7
|
*/
|
|
8
8
|
export { ClientError, createClient, KisoClient, SessionClient } from "./client.js";
|
|
9
|
-
export type { ClientEvent, ClientOptions, EventsOptions, RunStreamOptions } from "./client.js";
|
|
9
|
+
export type { ClientEvent, ClientOptions, ConnectionState, EventsOptions, RunStreamOptions } from "./client.js";
|
|
10
10
|
export { parseSseBlock, readSse } from "./sse.js";
|
|
11
11
|
export type { SseFrame } from "./sse.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "kiso client — the typed client for a hosted kiso session over the wire protocol: run, resume, abort, approve, the snapshot, and a stream that reconnects with Last-Event-ID and never repeats an event. Browser and Node; depends on the protocol package only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-protocol": "0.
|
|
24
|
+
"@vincemakes/kiso-protocol": "0.54.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@vincemakes/kiso-core": "0.
|
|
28
|
-
"@vincemakes/kiso-evals": "0.
|
|
29
|
-
"@vincemakes/kiso-runtime": "0.
|
|
30
|
-
"@vincemakes/kiso-server": "0.
|
|
27
|
+
"@vincemakes/kiso-core": "0.54.0",
|
|
28
|
+
"@vincemakes/kiso-evals": "0.54.0",
|
|
29
|
+
"@vincemakes/kiso-runtime": "0.54.0",
|
|
30
|
+
"@vincemakes/kiso-server": "0.54.0",
|
|
31
31
|
"@types/node": "^26.1.2",
|
|
32
32
|
"typescript": "^5.7.2",
|
|
33
33
|
"vitest": "^3.0.0"
|