@vincemakes/kiso-client 0.53.0 → 0.54.1
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 +28 -1
- package/dist/client.js +66 -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?: {
|
|
@@ -75,6 +97,11 @@ export declare class SessionClient {
|
|
|
75
97
|
steer(input: WireInput): Promise<SteerReply>;
|
|
76
98
|
resume(): Promise<RunReply>;
|
|
77
99
|
/** Stop the run in flight. A parked run is a REPLY (kind "parked"), not an error. */
|
|
100
|
+
/** ADR-0057 — take back the steers the live run has not admitted yet; `count` is how many. */
|
|
101
|
+
retract(): Promise<{
|
|
102
|
+
readonly runId: string | null;
|
|
103
|
+
readonly count: number;
|
|
104
|
+
}>;
|
|
78
105
|
abort(options?: {
|
|
79
106
|
force?: boolean;
|
|
80
107
|
}): Promise<AbortReply>;
|
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;
|
|
@@ -67,6 +72,10 @@ export class SessionClient {
|
|
|
67
72
|
return this.#json(await this.#request("POST", "resume", {}));
|
|
68
73
|
}
|
|
69
74
|
/** Stop the run in flight. A parked run is a REPLY (kind "parked"), not an error. */
|
|
75
|
+
/** ADR-0057 — take back the steers the live run has not admitted yet; `count` is how many. */
|
|
76
|
+
async retract() {
|
|
77
|
+
return this.#json(await this.#request("POST", "retract", {}));
|
|
78
|
+
}
|
|
70
79
|
async abort(options = {}) {
|
|
71
80
|
return this.#json(await this.#request("POST", "abort", options), [200, 409]);
|
|
72
81
|
}
|
|
@@ -102,29 +111,40 @@ export class SessionClient {
|
|
|
102
111
|
let lastSeq = options.after ?? -1;
|
|
103
112
|
let attempt = 0;
|
|
104
113
|
const initial = this.#options.reconnect?.initialMs ?? 250;
|
|
105
|
-
const cap = this.#options.reconnect?.maxMs ??
|
|
114
|
+
const cap = this.#options.reconnect?.maxMs ?? 30_000;
|
|
115
|
+
const idle = this.#options.reconnect?.idleMs ?? 35_000;
|
|
116
|
+
const report = options.onState ?? (() => { });
|
|
106
117
|
for (;;) {
|
|
107
118
|
if (options.signal?.aborted)
|
|
108
119
|
return;
|
|
109
|
-
|
|
120
|
+
report({ kind: "connecting" });
|
|
121
|
+
// the watchdog: no frame for `idle` aborts this connection, and the loop below dials again
|
|
122
|
+
const conn = new AbortController();
|
|
123
|
+
const forward = () => conn.abort();
|
|
124
|
+
options.signal?.addEventListener("abort", forward, { once: true });
|
|
125
|
+
let dog;
|
|
126
|
+
const feed = () => {
|
|
127
|
+
clearTimeout(dog);
|
|
128
|
+
dog = setTimeout(forward, idle);
|
|
129
|
+
};
|
|
130
|
+
feed();
|
|
110
131
|
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 {
|
|
132
|
+
const res = await this.#request("GET", "events", undefined, { headers: { accept: "text/event-stream", "last-event-id": String(lastSeq) }, signal: conn.signal });
|
|
133
|
+
if (res.status >= 400 && res.status < 500) {
|
|
134
|
+
try {
|
|
135
|
+
await this.#json(res, []);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err instanceof ClientError)
|
|
139
|
+
report({ kind: "refused", status: err.status, code: err.code });
|
|
140
|
+
}
|
|
141
|
+
return; // a refusal is final, and now it is said
|
|
142
|
+
}
|
|
143
|
+
if (res.status === 200 && res.body !== null) {
|
|
144
|
+
attempt = 0;
|
|
145
|
+
report({ kind: "open" });
|
|
127
146
|
for await (const frame of readSse(res.body)) {
|
|
147
|
+
feed();
|
|
128
148
|
if (options.signal?.aborted)
|
|
129
149
|
return;
|
|
130
150
|
const ev = toClientEvent(frame);
|
|
@@ -140,18 +160,41 @@ export class SessionClient {
|
|
|
140
160
|
return;
|
|
141
161
|
}
|
|
142
162
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// the connection dropped or went silent — fall through to reconnect
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
clearTimeout(dog);
|
|
169
|
+
options.signal?.removeEventListener("abort", forward);
|
|
146
170
|
}
|
|
147
171
|
if (options.signal?.aborted)
|
|
148
172
|
return;
|
|
149
173
|
attempt += 1;
|
|
150
|
-
const wait = Math.min(cap, initial * 2 ** (attempt - 1));
|
|
174
|
+
const wait = (0.5 + Math.random() / 2) * Math.min(cap, initial * 2 ** (attempt - 1));
|
|
151
175
|
options.onReconnect?.(attempt, wait);
|
|
152
|
-
|
|
176
|
+
report({ kind: "retrying", attempt, nextAt: Date.now() + wait });
|
|
177
|
+
await this.#nap(wait, options.signal);
|
|
178
|
+
while (options.paused?.() === true && options.signal?.aborted !== true)
|
|
179
|
+
await this.#nap(1_000, options.signal);
|
|
153
180
|
}
|
|
154
181
|
}
|
|
182
|
+
#nap(ms, signal) {
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
const done = () => {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
signal?.removeEventListener("abort", done);
|
|
187
|
+
this.#wake = null;
|
|
188
|
+
resolve();
|
|
189
|
+
};
|
|
190
|
+
const timer = setTimeout(done, ms);
|
|
191
|
+
this.#wake = done;
|
|
192
|
+
if (signal?.aborted)
|
|
193
|
+
done();
|
|
194
|
+
else
|
|
195
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
155
198
|
}
|
|
156
199
|
export class KisoClient {
|
|
157
200
|
#options;
|
|
@@ -182,16 +225,3 @@ function toClientEvent(frame) {
|
|
|
182
225
|
function isWireError(value) {
|
|
183
226
|
return typeof value === "object" && value !== null && typeof value.code === "string" && typeof value.message === "string";
|
|
184
227
|
}
|
|
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.1",
|
|
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.1"
|
|
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.1",
|
|
28
|
+
"@vincemakes/kiso-evals": "0.54.1",
|
|
29
|
+
"@vincemakes/kiso-runtime": "0.54.1",
|
|
30
|
+
"@vincemakes/kiso-server": "0.54.1",
|
|
31
31
|
"@types/node": "^26.1.2",
|
|
32
32
|
"typescript": "^5.7.2",
|
|
33
33
|
"vitest": "^3.0.0"
|