@opencode-ai/client 0.0.0-dev-19178 → 0.0.0-dev-19182
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/promise/client.d.ts +2 -3
- package/dist/promise/client.js +1 -1
- package/dist/promise/generated/client.d.ts +2 -0
- package/dist/promise/generated/client.js +2 -0
- package/dist/shared-events.d.ts +7 -4
- package/dist/shared-events.js +4 -1
- package/dist/solid/connection.d.ts +8 -0
- package/dist/solid/connection.js +74 -19
- package/package.json +4 -4
package/dist/promise/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * as OpenCode from "./client.js";
|
|
2
|
+
import { SharedEvents } from "../shared-events.js";
|
|
2
3
|
import { OpenCode } from "./generated/index.js";
|
|
3
4
|
import type { ClientOptions } from "./generated/client.js";
|
|
4
5
|
export type { ClientOptions, RequestOptions } from "./generated/client.js";
|
|
@@ -7,9 +8,7 @@ export declare function make(options: ClientOptions): {
|
|
|
7
8
|
call: (input: import("./index.js").RpcCallInput, requestOptions?: OpenCode.RequestOptions) => Promise<import("./index.js").RpcOutput>;
|
|
8
9
|
};
|
|
9
10
|
event: {
|
|
10
|
-
subscribe(options?:
|
|
11
|
-
readonly signal?: AbortSignal;
|
|
12
|
-
} | undefined): AsyncIterable<import("./index.js").V2Event>;
|
|
11
|
+
subscribe(options?: SharedEvents.SubscribeOptions): AsyncIterable<import("./index.js").V2Event>;
|
|
13
12
|
};
|
|
14
13
|
health: {
|
|
15
14
|
get: (requestOptions?: OpenCode.RequestOptions) => Promise<import("./index.js").ServiceHealth>;
|
package/dist/promise/client.js
CHANGED
|
@@ -4,7 +4,7 @@ import { OpenCode } from "./generated/index.js";
|
|
|
4
4
|
import { makeRpc } from "./rpc.js";
|
|
5
5
|
export function make(options) {
|
|
6
6
|
const raw = OpenCode.make(options);
|
|
7
|
-
const events = SharedEvents.make((signal) => raw.event.subscribe({ signal }));
|
|
7
|
+
const events = SharedEvents.make((signal, onActivity) => raw.event.subscribe({ signal, onActivity }));
|
|
8
8
|
return {
|
|
9
9
|
...raw,
|
|
10
10
|
rpc: Object.assign(makeRpc(raw, events), raw.rpc),
|
|
@@ -7,6 +7,8 @@ export interface ClientOptions {
|
|
|
7
7
|
export interface RequestOptions {
|
|
8
8
|
readonly signal?: AbortSignal;
|
|
9
9
|
readonly headers?: RequestInit["headers"];
|
|
10
|
+
/** Reports every chunk a streaming response receives, including keepalive comments that yield no event. */
|
|
11
|
+
readonly onActivity?: () => void;
|
|
10
12
|
}
|
|
11
13
|
export declare function make(options: ClientOptions): {
|
|
12
14
|
health: {
|
|
@@ -84,6 +84,8 @@ export function make(options) {
|
|
|
84
84
|
catch (cause) {
|
|
85
85
|
throw new ClientError("Transport", { cause });
|
|
86
86
|
}
|
|
87
|
+
if (!next.done)
|
|
88
|
+
requestOptions?.onActivity?.();
|
|
87
89
|
buffer += decoder.decode(next.value, { stream: !next.done });
|
|
88
90
|
if (buffer.length > maxSseEventBytes)
|
|
89
91
|
throw new ClientError("SseEventTooLarge");
|
package/dist/shared-events.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export * as SharedEvents from "./shared-events.js";
|
|
2
|
+
export type SubscribeOptions = {
|
|
3
|
+
readonly signal?: AbortSignal;
|
|
4
|
+
/** Reports transport activity on the shared stream, including keepalive frames that carry no event. */
|
|
5
|
+
readonly onActivity?: () => void;
|
|
6
|
+
};
|
|
2
7
|
export declare function make<A extends {
|
|
3
8
|
readonly type: string;
|
|
4
|
-
}>(connect: (signal: AbortSignal) => AsyncIterable<A>): {
|
|
5
|
-
subscribe(options?:
|
|
6
|
-
readonly signal?: AbortSignal;
|
|
7
|
-
}): AsyncIterable<A>;
|
|
9
|
+
}>(connect: (signal: AbortSignal, onActivity: () => void) => AsyncIterable<A>): {
|
|
10
|
+
subscribe(options?: SubscribeOptions): AsyncIterable<A>;
|
|
8
11
|
};
|
package/dist/shared-events.js
CHANGED
|
@@ -14,7 +14,9 @@ export function make(connect) {
|
|
|
14
14
|
try {
|
|
15
15
|
if (connection.controller.signal.aborted)
|
|
16
16
|
return;
|
|
17
|
-
iterator = connect(connection.controller.signal
|
|
17
|
+
iterator = connect(connection.controller.signal, () => {
|
|
18
|
+
connection.subscribers.forEach((subscriber) => subscriber.activity?.());
|
|
19
|
+
})[Symbol.asyncIterator]();
|
|
18
20
|
while (!connection.controller.signal.aborted) {
|
|
19
21
|
const item = await iterator.next();
|
|
20
22
|
if (item.done || connection.controller.signal.aborted)
|
|
@@ -66,6 +68,7 @@ export function make(connect) {
|
|
|
66
68
|
finish({});
|
|
67
69
|
}
|
|
68
70
|
const subscriber = {
|
|
71
|
+
activity: options?.onActivity,
|
|
69
72
|
finish(result) {
|
|
70
73
|
finish(result, false);
|
|
71
74
|
},
|
|
@@ -14,16 +14,24 @@ export type ClientConnectionOptions = {
|
|
|
14
14
|
readonly onEvent: (event: OpenCodeEvent) => void;
|
|
15
15
|
readonly flushInterval?: number;
|
|
16
16
|
readonly pageLifecycle?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Abort and reconnect a stream that receives no bytes for this long. The server writes a keepalive
|
|
19
|
+
* comment every 15 seconds, so a quiet but healthy stream never trips this.
|
|
20
|
+
*/
|
|
21
|
+
readonly idleTimeout?: number;
|
|
17
22
|
readonly log?: {
|
|
18
23
|
readonly debug?: (message: string, data?: Readonly<Record<string, unknown>>) => void;
|
|
19
24
|
readonly info?: (message: string, data?: Readonly<Record<string, unknown>>) => void;
|
|
20
25
|
};
|
|
21
26
|
};
|
|
27
|
+
export declare const defaultIdleTimeout = 45000;
|
|
28
|
+
export declare const foregroundIdleThreshold = 20000;
|
|
22
29
|
export declare function createClientConnection(initialApi: OpenCodeClient, options: ClientConnectionOptions): {
|
|
23
30
|
status: () => ClientConnectionStatus;
|
|
24
31
|
attempt: () => number;
|
|
25
32
|
error: () => string | undefined;
|
|
26
33
|
internal: {
|
|
27
34
|
history: () => ClientConnectionEvent[];
|
|
35
|
+
resync: (reason: string) => void;
|
|
28
36
|
};
|
|
29
37
|
};
|
package/dist/solid/connection.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import { batch, onCleanup
|
|
1
|
+
import { batch, onCleanup } from "solid-js";
|
|
2
2
|
import { createStore } from "solid-js/store";
|
|
3
3
|
const connectTimeout = 2_000;
|
|
4
4
|
const reconnectDelay = 1_000;
|
|
5
5
|
const connectionHistoryLimit = 50;
|
|
6
|
+
export const defaultIdleTimeout = 45_000;
|
|
7
|
+
// Longer than one server keepalive interval: a stream that is silent this long when the page
|
|
8
|
+
// returns to the foreground is probably half-open after the device slept.
|
|
9
|
+
export const foregroundIdleThreshold = 20_000;
|
|
6
10
|
export function createClientConnection(initialApi, options) {
|
|
7
11
|
const abort = new AbortController();
|
|
8
12
|
const history = [];
|
|
13
|
+
const idleTimeout = options.idleTimeout ?? defaultIdleTimeout;
|
|
9
14
|
const [connection, setConnection] = createStore({ status: "connecting", attempt: 0 });
|
|
10
15
|
let api = initialApi;
|
|
11
16
|
let pending = [];
|
|
12
17
|
let flushTimer;
|
|
13
18
|
let stream;
|
|
19
|
+
let current;
|
|
14
20
|
let run;
|
|
15
21
|
let started = false;
|
|
16
22
|
let generation = 0;
|
|
23
|
+
let lastActivity = 0;
|
|
24
|
+
let forced = false;
|
|
17
25
|
function record(status, attempt, error) {
|
|
18
26
|
history.push({ type: "client.connection", created: Date.now(), data: { status, attempt, error } });
|
|
19
27
|
if (history.length > connectionHistoryLimit)
|
|
@@ -33,13 +41,24 @@ export function createClientConnection(initialApi, options) {
|
|
|
33
41
|
async function connect(signal, attempt) {
|
|
34
42
|
let connectedAt;
|
|
35
43
|
const request = new AbortController();
|
|
44
|
+
current = request;
|
|
36
45
|
const cancel = () => request.abort(signal.reason);
|
|
37
46
|
const timeout = setTimeout(() => request.abort(new Error("Timed out connecting to server")), connectTimeout);
|
|
38
47
|
signal.addEventListener("abort", cancel, { once: true });
|
|
48
|
+
// Any received bytes, including keepalive comments, push the stall deadline out. A timer whose
|
|
49
|
+
// deadline passed while the page was suspended fires as soon as the page resumes.
|
|
50
|
+
let watchdog;
|
|
51
|
+
const touch = () => {
|
|
52
|
+
lastActivity = Date.now();
|
|
53
|
+
if (connectedAt === undefined)
|
|
54
|
+
return;
|
|
55
|
+
clearTimeout(watchdog);
|
|
56
|
+
watchdog = setTimeout(() => request.abort(new Error("Event stream stalled")), idleTimeout);
|
|
57
|
+
};
|
|
39
58
|
try {
|
|
40
59
|
record(attempt === 0 ? "connecting" : "reconnecting", attempt);
|
|
41
60
|
options.log?.info?.("event stream connecting", { attempt });
|
|
42
|
-
const iterator = api.event.subscribe({ signal: request.signal })[Symbol.asyncIterator]();
|
|
61
|
+
const iterator = api.event.subscribe({ signal: request.signal, onActivity: touch })[Symbol.asyncIterator]();
|
|
43
62
|
const first = await iterator.next();
|
|
44
63
|
if (signal.aborted)
|
|
45
64
|
return { error: undefined, connectedAt };
|
|
@@ -53,6 +72,7 @@ export function createClientConnection(initialApi, options) {
|
|
|
53
72
|
clearTimeout(timeout);
|
|
54
73
|
record("connected", attempt);
|
|
55
74
|
connectedAt = Date.now();
|
|
75
|
+
touch();
|
|
56
76
|
options.log?.info?.("event stream connected");
|
|
57
77
|
publish(first.value);
|
|
58
78
|
setConnection({ status: "connected", attempt: 0, error: undefined });
|
|
@@ -61,7 +81,11 @@ export function createClientConnection(initialApi, options) {
|
|
|
61
81
|
if (signal.aborted)
|
|
62
82
|
return { error: undefined, connectedAt };
|
|
63
83
|
if (event.done)
|
|
64
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
error: request.signal.reason instanceof Error ? request.signal.reason : new Error("Event stream disconnected"),
|
|
86
|
+
connectedAt,
|
|
87
|
+
};
|
|
88
|
+
touch();
|
|
65
89
|
if ("durable" in event.value && event.value.durable)
|
|
66
90
|
options.log?.debug?.("event", {
|
|
67
91
|
type: event.value.type,
|
|
@@ -77,7 +101,10 @@ export function createClientConnection(initialApi, options) {
|
|
|
77
101
|
}
|
|
78
102
|
finally {
|
|
79
103
|
request.abort();
|
|
104
|
+
if (current === request)
|
|
105
|
+
current = undefined;
|
|
80
106
|
clearTimeout(timeout);
|
|
107
|
+
clearTimeout(watchdog);
|
|
81
108
|
signal.removeEventListener("abort", cancel);
|
|
82
109
|
}
|
|
83
110
|
}
|
|
@@ -113,6 +140,11 @@ export function createClientConnection(initialApi, options) {
|
|
|
113
140
|
continue;
|
|
114
141
|
}
|
|
115
142
|
}
|
|
143
|
+
// A deliberate resync already knows the old socket is gone; reconnect without backing off.
|
|
144
|
+
if (forced) {
|
|
145
|
+
forced = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
116
148
|
await wait(reconnectDelay, controller.signal);
|
|
117
149
|
}
|
|
118
150
|
}
|
|
@@ -120,6 +152,7 @@ export function createClientConnection(initialApi, options) {
|
|
|
120
152
|
if (started)
|
|
121
153
|
return run;
|
|
122
154
|
started = true;
|
|
155
|
+
forced = false;
|
|
123
156
|
const active = ++generation;
|
|
124
157
|
const previous = run;
|
|
125
158
|
const current = (async () => {
|
|
@@ -135,26 +168,47 @@ export function createClientConnection(initialApi, options) {
|
|
|
135
168
|
return run;
|
|
136
169
|
}
|
|
137
170
|
function stop() {
|
|
171
|
+
if (!started)
|
|
172
|
+
return;
|
|
138
173
|
started = false;
|
|
139
174
|
generation += 1;
|
|
140
175
|
stream?.abort();
|
|
176
|
+
// Nothing is listening once stopped, so consumers must treat their data as stale until start() reconnects.
|
|
177
|
+
setConnection({ status: "connecting", attempt: 0, error: undefined });
|
|
141
178
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
179
|
+
// Drop the live request so the reconnect loop replaces it now instead of waiting for the idle watchdog.
|
|
180
|
+
function resync(reason) {
|
|
181
|
+
if (!started || connection.status !== "connected")
|
|
182
|
+
return;
|
|
183
|
+
options.log?.info?.("event stream resync", { reason, idle: Date.now() - lastActivity });
|
|
184
|
+
forced = true;
|
|
185
|
+
current?.abort(new Error(reason));
|
|
186
|
+
}
|
|
187
|
+
if (options.pageLifecycle) {
|
|
188
|
+
const pagehide = () => stop();
|
|
189
|
+
const pageshow = () => void start();
|
|
190
|
+
// Locking a phone or switching apps hides the document without a pagehide; the socket usually
|
|
191
|
+
// dies while the page is suspended, and the browser may never report that on the hung read.
|
|
192
|
+
const visibility = () => {
|
|
193
|
+
if (document.visibilityState !== "visible")
|
|
194
|
+
return;
|
|
195
|
+
if (Date.now() - lastActivity < foregroundIdleThreshold)
|
|
196
|
+
return;
|
|
197
|
+
resync("Page returned to the foreground after the event stream went quiet");
|
|
198
|
+
};
|
|
199
|
+
const online = () => resync("Network connection restored");
|
|
200
|
+
window.addEventListener("pagehide", pagehide);
|
|
201
|
+
window.addEventListener("pageshow", pageshow);
|
|
202
|
+
window.addEventListener("online", online);
|
|
203
|
+
document.addEventListener("visibilitychange", visibility);
|
|
204
|
+
onCleanup(() => {
|
|
205
|
+
window.removeEventListener("pagehide", pagehide);
|
|
206
|
+
window.removeEventListener("pageshow", pageshow);
|
|
207
|
+
window.removeEventListener("online", online);
|
|
208
|
+
document.removeEventListener("visibilitychange", visibility);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
void start();
|
|
158
212
|
onCleanup(() => {
|
|
159
213
|
stop();
|
|
160
214
|
abort.abort();
|
|
@@ -168,6 +222,7 @@ export function createClientConnection(initialApi, options) {
|
|
|
168
222
|
error: () => connection.error,
|
|
169
223
|
internal: {
|
|
170
224
|
history: () => history.slice(),
|
|
225
|
+
resync,
|
|
171
226
|
},
|
|
172
227
|
};
|
|
173
228
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@opencode-ai/client",
|
|
4
|
-
"version": "0.0.0-dev-
|
|
4
|
+
"version": "0.0.0-dev-19182",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"typecheck": "tsgo --noEmit"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@opencode-ai/schema": "0.0.0-dev-
|
|
61
|
-
"@opencode-ai/protocol": "0.0.0-dev-
|
|
60
|
+
"@opencode-ai/schema": "0.0.0-dev-19182",
|
|
61
|
+
"@opencode-ai/protocol": "0.0.0-dev-19182"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"effect": "4.0.0-rc.112",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
77
|
-
"@opencode-ai/httpapi-codegen": "0.0.0-dev-
|
|
77
|
+
"@opencode-ai/httpapi-codegen": "0.0.0-dev-19182",
|
|
78
78
|
"@tsconfig/bun": "1.0.9",
|
|
79
79
|
"@types/bun": "1.4.0",
|
|
80
80
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|