@zhin.js/client 1.1.5 → 1.1.6
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 +25 -19
- package/client/app.test.ts +37 -0
- package/client/app.ts +130 -138
- package/client/bootstrap/loadConsoleEntries.test.ts +0 -15
- package/client/bootstrap/loadConsoleEntries.ts +1 -20
- package/client/console-client-context.ts +24 -0
- package/client/console-client.test.ts +46 -0
- package/client/console-client.ts +70 -0
- package/client/console-events.ts +4 -3
- package/client/console-utils/remoteApi.ts +0 -7
- package/client/index.ts +21 -13
- package/client/persistence/idb-store.test.ts +0 -18
- package/client/persistence/idb-store.ts +3 -6
- package/client/{websocket/manager.test.ts → transport/console-transport.test.ts} +79 -34
- package/client/{websocket/manager.ts → transport/console-transport.ts} +237 -178
- package/client/{websocket → transport}/hooks.ts +240 -23
- package/client/transport/index.ts +23 -0
- package/client/{websocket → transport}/types.ts +11 -17
- package/client/types.ts +0 -3
- package/client/workroom/registerWorkroomConsole.test.ts +0 -1
- package/dist/app.d.ts +17 -52
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +110 -95
- package/dist/app.js.map +1 -1
- package/dist/bootstrap/loadConsoleEntries.d.ts +0 -6
- package/dist/bootstrap/loadConsoleEntries.d.ts.map +1 -1
- package/dist/bootstrap/loadConsoleEntries.js +0 -9
- package/dist/bootstrap/loadConsoleEntries.js.map +1 -1
- package/dist/console-client-context.d.ts +9 -0
- package/dist/console-client-context.d.ts.map +1 -0
- package/dist/console-client-context.js +13 -0
- package/dist/console-client-context.js.map +1 -0
- package/dist/console-client.d.ts +25 -0
- package/dist/console-client.d.ts.map +1 -0
- package/dist/console-client.js +45 -0
- package/dist/console-client.js.map +1 -0
- package/dist/console-events.d.ts +1 -0
- package/dist/console-events.d.ts.map +1 -1
- package/dist/console-events.js +3 -3
- package/dist/console-events.js.map +1 -1
- package/dist/console-utils/remoteApi.d.ts +0 -1
- package/dist/console-utils/remoteApi.d.ts.map +1 -1
- package/dist/console-utils/remoteApi.js +0 -6
- package/dist/console-utils/remoteApi.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/persistence/idb-store.d.ts.map +1 -1
- package/dist/persistence/idb-store.js +3 -5
- package/dist/persistence/idb-store.js.map +1 -1
- package/dist/transport/console-transport.d.ts +217 -0
- package/dist/transport/console-transport.d.ts.map +1 -0
- package/dist/{websocket/manager.js → transport/console-transport.js} +147 -165
- package/dist/transport/console-transport.js.map +1 -0
- package/dist/transport/hooks.d.ts +253 -0
- package/dist/transport/hooks.d.ts.map +1 -0
- package/dist/{websocket → transport}/hooks.js +234 -19
- package/dist/transport/hooks.js.map +1 -0
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +4 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/{websocket → transport}/types.d.ts +10 -13
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/{websocket → transport}/types.js +4 -9
- package/dist/transport/types.js.map +1 -0
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/client/runtime/index.ts +0 -12
- package/client/websocket/index.ts +0 -11
- package/client/websocket/instance.ts +0 -24
- package/client/websocket/remote-settings.ts +0 -47
- package/dist/runtime/index.d.ts +0 -6
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js +0 -10
- package/dist/runtime/index.js.map +0 -1
- package/dist/websocket/hooks.d.ts +0 -114
- package/dist/websocket/hooks.d.ts.map +0 -1
- package/dist/websocket/hooks.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -7
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -7
- package/dist/websocket/index.js.map +0 -1
- package/dist/websocket/instance.d.ts +0 -5
- package/dist/websocket/instance.d.ts.map +0 -1
- package/dist/websocket/instance.js +0 -21
- package/dist/websocket/instance.js.map +0 -1
- package/dist/websocket/manager.d.ts +0 -131
- package/dist/websocket/manager.d.ts.map +0 -1
- package/dist/websocket/manager.js.map +0 -1
- package/dist/websocket/remote-settings.d.ts +0 -5
- package/dist/websocket/remote-settings.d.ts.map +0 -1
- package/dist/websocket/remote-settings.js +0 -47
- package/dist/websocket/remote-settings.js.map +0 -1
- package/dist/websocket/types.d.ts.map +0 -1
- package/dist/websocket/types.js.map +0 -1
- /package/client/{websocket → transport}/hooks.test.ts +0 -0
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
ConnectionState,
|
|
3
|
+
ConnectionError,
|
|
4
|
+
ConsoleTransportError,
|
|
5
|
+
MessageError,
|
|
6
|
+
type ConsoleTransportMessage,
|
|
7
|
+
type ConsoleTransportConfig,
|
|
8
|
+
type ConsoleTransportCallbacks,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
import { getApiBase, getToken, resolveApiUrl } from '../console-utils/remoteApi.js';
|
|
4
11
|
import { applyConsoleEvent } from "../persistence/idb-store.js";
|
|
5
12
|
import { fetchConsoleEventHistory } from '../console-events.js';
|
|
6
13
|
import {
|
|
14
|
+
CONFIG_RPC,
|
|
15
|
+
PLUGIN_RPC,
|
|
16
|
+
ENDPOINT_RPC,
|
|
7
17
|
CONSOLE_EVENT_RECOVERY_GAP_EVENT,
|
|
8
18
|
SIDE_EVENT_PUSH,
|
|
9
|
-
normalizeConsolePushMessage,
|
|
10
19
|
parseConsoleSseFrame,
|
|
11
20
|
type ConsoleEventData,
|
|
12
21
|
type ConsoleEventEnvelope,
|
|
13
22
|
type ConsoleEventHistoryPage,
|
|
14
23
|
type ConsoleEventHistoryQuery,
|
|
24
|
+
type ConsoleConfigSource,
|
|
15
25
|
} from "@zhin.js/console-protocol";
|
|
16
26
|
|
|
17
27
|
interface ConsoleEventCursor {
|
|
@@ -28,18 +38,13 @@ export interface ConsoleEventRecoveryGap {
|
|
|
28
38
|
}
|
|
29
39
|
export type ConsoleEventRecoveryGapListener = (gap: ConsoleEventRecoveryGap) => void;
|
|
30
40
|
|
|
31
|
-
export class
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
private callbacks: WebSocketCallbacks;
|
|
41
|
+
export class ConsoleTransport {
|
|
42
|
+
private config: Required<ConsoleTransportConfig>;
|
|
43
|
+
private callbacks: ConsoleTransportCallbacks;
|
|
35
44
|
private state: ConnectionState = ConnectionState.DISCONNECTED;
|
|
36
45
|
private reconnectAttempts = 0;
|
|
37
46
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
47
|
private requestId = 0;
|
|
39
|
-
private pendingRequests = new Map<
|
|
40
|
-
number,
|
|
41
|
-
{ resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }
|
|
42
|
-
>();
|
|
43
48
|
private connectedListeners = new Set<(connected: boolean) => void>();
|
|
44
49
|
private eventListeners = new Map<string, Set<ConsoleEventListener<string>>>();
|
|
45
50
|
private recoveryGapListeners = new Set<ConsoleEventRecoveryGapListener>();
|
|
@@ -47,11 +52,12 @@ export class WebSocketManager {
|
|
|
47
52
|
private eventRuntimeId = '';
|
|
48
53
|
private lastEventId = 0;
|
|
49
54
|
private durableCursorBlocked = false;
|
|
50
|
-
private
|
|
55
|
+
private disposed = false;
|
|
56
|
+
private connectionGeneration = 0;
|
|
51
57
|
|
|
52
|
-
constructor(config:
|
|
58
|
+
constructor(config: ConsoleTransportConfig = {}, callbacks: ConsoleTransportCallbacks = {}) {
|
|
53
59
|
this.config = {
|
|
54
|
-
|
|
60
|
+
fetch: config.fetch ?? globalThis.fetch,
|
|
55
61
|
reconnectInterval: config.reconnectInterval ?? 3000,
|
|
56
62
|
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
57
63
|
requestTimeout: config.requestTimeout ?? 10000,
|
|
@@ -60,12 +66,14 @@ export class WebSocketManager {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
onConnectionChange(listener: (connected: boolean) => void): () => void {
|
|
69
|
+
this.assertActive();
|
|
63
70
|
this.connectedListeners.add(listener);
|
|
64
71
|
return () => this.connectedListeners.delete(listener);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/** Subscribe by event name; known names infer their exact payload type. */
|
|
68
75
|
onConsoleEvent<Type extends string>(type: Type, listener: ConsoleEventListener<Type>): () => void {
|
|
76
|
+
this.assertActive();
|
|
69
77
|
let listeners = this.eventListeners.get(type);
|
|
70
78
|
if (!listeners) {
|
|
71
79
|
listeners = new Set();
|
|
@@ -81,6 +89,7 @@ export class WebSocketManager {
|
|
|
81
89
|
|
|
82
90
|
/** Observe a non-resumable cursor so domain views can perform a full resync. */
|
|
83
91
|
onConsoleEventRecoveryGap(listener: ConsoleEventRecoveryGapListener): () => void {
|
|
92
|
+
this.assertActive();
|
|
84
93
|
this.recoveryGapListeners.add(listener);
|
|
85
94
|
return () => this.recoveryGapListeners.delete(listener);
|
|
86
95
|
}
|
|
@@ -90,28 +99,17 @@ export class WebSocketManager {
|
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
connect(): void {
|
|
93
|
-
|
|
94
|
-
void this.connectRestSse();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
102
|
+
this.assertActive();
|
|
97
103
|
if (this.state === ConnectionState.CONNECTED || this.state === ConnectionState.CONNECTING) return;
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
this.ws = new WebSocket(this.config.url);
|
|
101
|
-
this.attachEventHandlers();
|
|
102
|
-
} catch (error) {
|
|
103
|
-
this.handleConnectionError(new ConnectionError("Failed to create WebSocket", error as Error));
|
|
104
|
-
this.setState(ConnectionState.RECONNECTING);
|
|
105
|
-
this.notifyConnection(false);
|
|
106
|
-
this.scheduleReconnect();
|
|
107
|
-
}
|
|
104
|
+
const generation = ++this.connectionGeneration;
|
|
105
|
+
void this.connectSse(generation);
|
|
108
106
|
}
|
|
109
107
|
|
|
110
|
-
private async
|
|
108
|
+
private async connectSse(generation: number): Promise<void> {
|
|
111
109
|
// Host without http.token allows unauthenticated SSE (authenticateHttp
|
|
112
110
|
// returns full when TokenRegistry is empty). Still open the stream so
|
|
113
111
|
// local dev Console receives hmr:reload / message.receive broadcasts.
|
|
114
|
-
const token =
|
|
112
|
+
const token = getToken();
|
|
115
113
|
this.setState(ConnectionState.CONNECTING);
|
|
116
114
|
this.sseAbort?.abort();
|
|
117
115
|
this.sseAbort = new AbortController();
|
|
@@ -121,14 +119,19 @@ export class WebSocketManager {
|
|
|
121
119
|
};
|
|
122
120
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
123
121
|
await this.recoverEventHistory(this.sseAbort.signal);
|
|
122
|
+
if (!this.isCurrentConnection(generation)) return;
|
|
124
123
|
const params = new URLSearchParams();
|
|
125
124
|
if (this.eventRuntimeId) params.set('runtimeId', this.eventRuntimeId);
|
|
126
125
|
if (this.lastEventId > 0) params.set('after', String(this.lastEventId));
|
|
127
126
|
const suffix = params.size ? `?${params}` : '';
|
|
128
|
-
const res = await fetch(resolveApiUrl(`/api/events${suffix}`), {
|
|
127
|
+
const res = await this.config.fetch(resolveApiUrl(`/api/events${suffix}`), {
|
|
129
128
|
headers,
|
|
130
129
|
signal: this.sseAbort.signal,
|
|
131
130
|
});
|
|
131
|
+
if (!this.isCurrentConnection(generation)) {
|
|
132
|
+
await res.body?.cancel();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
132
135
|
if (!res.ok || !res.body) {
|
|
133
136
|
throw new ConnectionError(`SSE failed: HTTP ${res.status}`);
|
|
134
137
|
}
|
|
@@ -136,28 +139,31 @@ export class WebSocketManager {
|
|
|
136
139
|
this.reconnectAttempts = 0;
|
|
137
140
|
this.notifyConnection(true);
|
|
138
141
|
this.callbacks.onConnect?.();
|
|
139
|
-
void this.pumpSse(res.body);
|
|
142
|
+
void this.pumpSse(res.body, generation);
|
|
140
143
|
} catch (error) {
|
|
144
|
+
if (!this.isCurrentConnection(generation)) return;
|
|
141
145
|
if ((error as Error).name === "AbortError") return;
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.error("[Console transport] SSE connect error:", error);
|
|
146
|
+
// Reconnect timers only run while the transport remains in RECONNECTING.
|
|
147
|
+
const connectionError = error instanceof Error ? error : new Error(String(error));
|
|
148
|
+
this.callbacks.onError?.(connectionError);
|
|
149
|
+
console.error("[Console transport] SSE connect error:", connectionError);
|
|
147
150
|
this.setState(ConnectionState.RECONNECTING);
|
|
148
151
|
this.notifyConnection(false);
|
|
149
152
|
this.scheduleReconnect();
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
|
|
153
|
-
private async pumpSse(
|
|
156
|
+
private async pumpSse(
|
|
157
|
+
body: ReadableStream<Uint8Array>,
|
|
158
|
+
generation: number,
|
|
159
|
+
): Promise<void> {
|
|
154
160
|
const reader = body.getReader();
|
|
155
161
|
const decoder = new TextDecoder();
|
|
156
162
|
let buffer = "";
|
|
157
163
|
try {
|
|
158
164
|
while (true) {
|
|
159
165
|
const { done, value } = await reader.read();
|
|
160
|
-
if (done) break;
|
|
166
|
+
if (done || !this.isCurrentConnection(generation)) break;
|
|
161
167
|
buffer += decoder.decode(value, { stream: true });
|
|
162
168
|
const parts = buffer.split(/\r?\n\r?\n/u);
|
|
163
169
|
buffer = parts.pop() ?? "";
|
|
@@ -179,9 +185,10 @@ export class WebSocketManager {
|
|
|
179
185
|
} catch {
|
|
180
186
|
/* closed */
|
|
181
187
|
} finally {
|
|
182
|
-
if (this.state === ConnectionState.CONNECTED) {
|
|
188
|
+
if (this.isCurrentConnection(generation) && this.state === ConnectionState.CONNECTED) {
|
|
183
189
|
this.notifyConnection(false);
|
|
184
190
|
this.setState(ConnectionState.RECONNECTING);
|
|
191
|
+
this.callbacks.onDisconnect?.();
|
|
185
192
|
this.scheduleReconnect();
|
|
186
193
|
}
|
|
187
194
|
}
|
|
@@ -199,7 +206,7 @@ export class WebSocketManager {
|
|
|
199
206
|
}
|
|
200
207
|
for (;;) {
|
|
201
208
|
const query = { runtimeId, after, limit: 500 } satisfies ConsoleEventHistoryQuery;
|
|
202
|
-
const page = await fetchConsoleEventHistory(query, { signal });
|
|
209
|
+
const page = await fetchConsoleEventHistory(query, { signal, fetch: this.config.fetch });
|
|
203
210
|
if (page.runtimeId !== runtimeId) {
|
|
204
211
|
runtimeId = page.runtimeId;
|
|
205
212
|
after = 0;
|
|
@@ -232,14 +239,14 @@ export class WebSocketManager {
|
|
|
232
239
|
if (event.eventId > 0
|
|
233
240
|
&& event.runtimeId === this.eventRuntimeId
|
|
234
241
|
&& event.eventId <= this.lastEventId) return;
|
|
235
|
-
const message =
|
|
242
|
+
const message: ConsoleTransportMessage = Object.freeze({
|
|
236
243
|
type: event.type,
|
|
237
244
|
data: event.data,
|
|
238
245
|
runtimeId: event.runtimeId,
|
|
239
246
|
eventId: event.eventId || undefined,
|
|
240
247
|
timestamp: event.timestamp,
|
|
241
248
|
delivery: event.delivery,
|
|
242
|
-
})
|
|
249
|
+
});
|
|
243
250
|
try {
|
|
244
251
|
await applyConsoleEvent(message);
|
|
245
252
|
} catch (error) {
|
|
@@ -281,59 +288,27 @@ export class WebSocketManager {
|
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
disconnect(): void {
|
|
291
|
+
const wasConnected = this.state === ConnectionState.CONNECTED;
|
|
292
|
+
this.connectionGeneration += 1;
|
|
284
293
|
this.clearReconnectTimer();
|
|
285
|
-
this.clearPendingRequests();
|
|
286
294
|
this.sseAbort?.abort();
|
|
287
295
|
this.sseAbort = null;
|
|
288
|
-
if (this.ws) {
|
|
289
|
-
this.ws.close();
|
|
290
|
-
this.ws = null;
|
|
291
|
-
}
|
|
292
296
|
this.setState(ConnectionState.DISCONNECTED);
|
|
293
297
|
this.notifyConnection(false);
|
|
298
|
+
if (wasConnected) this.callbacks.onDisconnect?.();
|
|
294
299
|
}
|
|
295
300
|
|
|
296
301
|
send(message: unknown): void {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (!this.isConnected()) throw new WebSocketError("WebSocket is not connected", "NOT_CONNECTED");
|
|
302
|
-
try {
|
|
303
|
-
this.ws!.send(JSON.stringify(message));
|
|
304
|
-
} catch (error) {
|
|
305
|
-
throw new MessageError("Failed to send message", error as Error);
|
|
306
|
-
}
|
|
302
|
+
this.assertActive();
|
|
303
|
+
void this.sendRequest(message);
|
|
307
304
|
}
|
|
308
305
|
|
|
309
306
|
async sendRequest<T = unknown>(message: unknown): Promise<T> {
|
|
310
|
-
|
|
311
|
-
return this.sendRestRequest<T>(message);
|
|
312
|
-
}
|
|
313
|
-
if (!this.isConnected()) throw new WebSocketError("WebSocket is not connected", "NOT_CONNECTED");
|
|
314
|
-
return new Promise((resolve, reject) => {
|
|
315
|
-
const requestId = ++this.requestId;
|
|
316
|
-
const messageWithId = { ...(message as Record<string, unknown>), requestId };
|
|
317
|
-
const timer = setTimeout(() => {
|
|
318
|
-
this.pendingRequests.delete(requestId);
|
|
319
|
-
reject(new RequestTimeoutError(requestId));
|
|
320
|
-
}, this.config.requestTimeout);
|
|
321
|
-
this.pendingRequests.set(requestId, { resolve: resolve as (v: unknown) => void, reject, timer });
|
|
322
|
-
try {
|
|
323
|
-
this.ws!.send(JSON.stringify(messageWithId));
|
|
324
|
-
} catch (error) {
|
|
325
|
-
this.pendingRequests.delete(requestId);
|
|
326
|
-
clearTimeout(timer);
|
|
327
|
-
reject(new MessageError("Failed to send request", error as Error));
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private async sendRestRequest<T>(message: unknown): Promise<T> {
|
|
307
|
+
this.assertActive();
|
|
333
308
|
// Host may run without http.token (local smoke). Only require a token when
|
|
334
309
|
// one is configured client-side; otherwise POST without Authorization and
|
|
335
310
|
// let the Host accept (TokenRegistry empty → full scope).
|
|
336
|
-
const token =
|
|
311
|
+
const token = getToken();
|
|
337
312
|
const requestId = ++this.requestId;
|
|
338
313
|
const body = { ...(message as Record<string, unknown>), requestId };
|
|
339
314
|
try {
|
|
@@ -341,7 +316,7 @@ export class WebSocketManager {
|
|
|
341
316
|
"Content-Type": "application/json",
|
|
342
317
|
};
|
|
343
318
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
344
|
-
const res = await fetch(resolveApiUrl("/api/console/request"), {
|
|
319
|
+
const res = await this.config.fetch(resolveApiUrl("/api/console/request"), {
|
|
345
320
|
method: "POST",
|
|
346
321
|
headers,
|
|
347
322
|
body: JSON.stringify(body),
|
|
@@ -354,32 +329,162 @@ export class WebSocketManager {
|
|
|
354
329
|
requestId?: number;
|
|
355
330
|
};
|
|
356
331
|
if (!res.ok || json.success === false) {
|
|
357
|
-
throw new
|
|
332
|
+
throw new ConsoleTransportError(json.error ?? `HTTP ${res.status}`, "SERVER_ERROR");
|
|
358
333
|
}
|
|
359
334
|
return json.data as T;
|
|
360
335
|
} catch (error) {
|
|
361
|
-
if (error instanceof
|
|
336
|
+
if (error instanceof ConsoleTransportError) throw error;
|
|
362
337
|
throw new MessageError("REST request failed", error as Error);
|
|
363
338
|
}
|
|
364
339
|
}
|
|
365
340
|
|
|
366
341
|
isConnected(): boolean {
|
|
367
|
-
|
|
368
|
-
return this.state === ConnectionState.CONNECTED;
|
|
369
|
-
}
|
|
370
|
-
return this.ws?.readyState === WebSocket.OPEN;
|
|
342
|
+
return this.state === ConnectionState.CONNECTED;
|
|
371
343
|
}
|
|
372
344
|
|
|
373
345
|
getState(): ConnectionState {
|
|
374
346
|
return this.state;
|
|
375
347
|
}
|
|
376
348
|
|
|
349
|
+
dispose(): void {
|
|
350
|
+
if (this.disposed) return;
|
|
351
|
+
this.disconnect();
|
|
352
|
+
this.disposed = true;
|
|
353
|
+
this.connectedListeners.clear();
|
|
354
|
+
this.eventListeners.clear();
|
|
355
|
+
this.recoveryGapListeners.clear();
|
|
356
|
+
}
|
|
357
|
+
|
|
377
358
|
async getConfig(pluginName: string) {
|
|
378
|
-
return this.sendRequest<unknown>({ type:
|
|
359
|
+
return this.sendRequest<unknown>({ type: CONFIG_RPC.GET, pluginName });
|
|
360
|
+
}
|
|
361
|
+
async planPluginInstall(packageName: string) {
|
|
362
|
+
return this.sendRequest<{
|
|
363
|
+
packageName: string;
|
|
364
|
+
instanceKey: string;
|
|
365
|
+
alreadyDeclared: boolean;
|
|
366
|
+
alreadyInstalled: boolean;
|
|
367
|
+
restartRequired: boolean;
|
|
368
|
+
changes: { packageManifest: string; config: string };
|
|
369
|
+
warnings: string[];
|
|
370
|
+
}>({ type: PLUGIN_RPC.PLAN_INSTALL, packageName });
|
|
371
|
+
}
|
|
372
|
+
async installPlugin(packageName: string, expectedRevision?: string) {
|
|
373
|
+
return this.sendRequest<{
|
|
374
|
+
success: boolean;
|
|
375
|
+
plan: Awaited<ReturnType<ConsoleTransport['planPluginInstall']>>;
|
|
376
|
+
restartRequired: boolean;
|
|
377
|
+
}>({ type: PLUGIN_RPC.INSTALL, packageName, expectedRevision });
|
|
378
|
+
}
|
|
379
|
+
async planPluginUninstall(packageName: string) {
|
|
380
|
+
return this.sendRequest<{
|
|
381
|
+
packageName: string;
|
|
382
|
+
instanceKey: string;
|
|
383
|
+
installed: boolean;
|
|
384
|
+
declared: boolean;
|
|
385
|
+
hasConfig: boolean;
|
|
386
|
+
restartRequired: boolean;
|
|
387
|
+
}>({ type: PLUGIN_RPC.PLAN_UNINSTALL, packageName });
|
|
388
|
+
}
|
|
389
|
+
async uninstallPlugin(packageName: string, expectedRevision?: string) {
|
|
390
|
+
return this.sendRequest<{
|
|
391
|
+
success: boolean;
|
|
392
|
+
plan: Awaited<ReturnType<ConsoleTransport['planPluginUninstall']>>;
|
|
393
|
+
restartRequired: boolean;
|
|
394
|
+
}>({
|
|
395
|
+
type: PLUGIN_RPC.UNINSTALL,
|
|
396
|
+
packageName,
|
|
397
|
+
confirmation: packageName,
|
|
398
|
+
expectedRevision,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
async planPluginUpdate(packageName: string, targetVersion: string) {
|
|
402
|
+
return this.sendRequest<{
|
|
403
|
+
packageName: string;
|
|
404
|
+
instanceKey: string;
|
|
405
|
+
currentVersion: string | null;
|
|
406
|
+
targetVersion: string;
|
|
407
|
+
installed: boolean;
|
|
408
|
+
declared: boolean;
|
|
409
|
+
alreadyCurrent: boolean;
|
|
410
|
+
restartRequired: boolean;
|
|
411
|
+
}>({ type: PLUGIN_RPC.PLAN_UPDATE, packageName, targetVersion });
|
|
412
|
+
}
|
|
413
|
+
async updatePlugin(packageName: string, targetVersion: string, expectedRevision?: string) {
|
|
414
|
+
return this.sendRequest<{
|
|
415
|
+
success: boolean;
|
|
416
|
+
plan: Awaited<ReturnType<ConsoleTransport['planPluginUpdate']>>;
|
|
417
|
+
installedVersion: string;
|
|
418
|
+
restartRequired: boolean;
|
|
419
|
+
}>({ type: PLUGIN_RPC.UPDATE, packageName, targetVersion, expectedRevision });
|
|
420
|
+
}
|
|
421
|
+
async validatePluginConfig(pluginName: string, data: unknown) {
|
|
422
|
+
return this.sendRequest<{
|
|
423
|
+
valid: boolean;
|
|
424
|
+
errors: Array<{ path: string; message: string }>;
|
|
425
|
+
missingEnv: string[];
|
|
426
|
+
}>({ type: PLUGIN_RPC.VALIDATE_CONFIG, pluginName, data });
|
|
427
|
+
}
|
|
428
|
+
async diagnosePlugin(pluginName: string) {
|
|
429
|
+
return this.sendRequest<{
|
|
430
|
+
pluginName: string;
|
|
431
|
+
plan: unknown;
|
|
432
|
+
validation: { valid: boolean; errors: Array<{ path: string; message: string }>; missingEnv: string[] };
|
|
433
|
+
}>({ type: PLUGIN_RPC.DIAGNOSE, pluginName });
|
|
434
|
+
}
|
|
435
|
+
async setPluginEnabled(instanceKey: string, enabled: boolean) {
|
|
436
|
+
return this.sendRequest<{
|
|
437
|
+
success: boolean;
|
|
438
|
+
instanceKey: string;
|
|
439
|
+
enabled: boolean;
|
|
440
|
+
disabled: string[];
|
|
441
|
+
restartRequired: boolean;
|
|
442
|
+
message: string;
|
|
443
|
+
}>({ type: PLUGIN_RPC.SET_ENABLED, instanceKey, enabled });
|
|
444
|
+
}
|
|
445
|
+
async listPlugins() {
|
|
446
|
+
return this.getRest<Array<Record<string, unknown>>>('/api/plugins');
|
|
447
|
+
}
|
|
448
|
+
async searchMarketplace(query: {
|
|
449
|
+
keyword?: string;
|
|
450
|
+
category?: string;
|
|
451
|
+
official?: boolean;
|
|
452
|
+
page?: number;
|
|
453
|
+
pageSize?: number;
|
|
454
|
+
} = {}) {
|
|
455
|
+
const params = new URLSearchParams();
|
|
456
|
+
const normalized = {
|
|
457
|
+
q: query.keyword,
|
|
458
|
+
category: query.category,
|
|
459
|
+
official: query.official,
|
|
460
|
+
page: query.page,
|
|
461
|
+
size: query.pageSize,
|
|
462
|
+
};
|
|
463
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
464
|
+
if (value !== undefined) params.set(key, String(value));
|
|
465
|
+
}
|
|
466
|
+
return this.getRest<unknown>(`/pub/marketplace/search${params.size ? `?${params}` : ''}`);
|
|
467
|
+
}
|
|
468
|
+
async getMarketplacePlugin(packageName: string) {
|
|
469
|
+
return this.getRest<unknown>(`/pub/marketplace/detail/${encodeURIComponent(packageName)}`);
|
|
470
|
+
}
|
|
471
|
+
async getPluginUpdates() {
|
|
472
|
+
return this.getRest<Array<Record<string, unknown>>>('/api/marketplace/updates');
|
|
473
|
+
}
|
|
474
|
+
async testEndpoint(adapter: string, endpointKey: string) {
|
|
475
|
+
return this.sendRequest<{
|
|
476
|
+
reachable: boolean;
|
|
477
|
+
connected: boolean;
|
|
478
|
+
status?: 'online' | 'offline';
|
|
479
|
+
phase: string;
|
|
480
|
+
pendingLogin?: boolean;
|
|
481
|
+
latencyMs: number;
|
|
482
|
+
message: string;
|
|
483
|
+
}>({ type: ENDPOINT_RPC.TEST, adapter, endpointKey });
|
|
379
484
|
}
|
|
380
485
|
async setConfig(pluginName: string, config: unknown) {
|
|
381
486
|
return this.sendRequest<{ success?: boolean; reloaded?: boolean; message?: string }>({
|
|
382
|
-
type:
|
|
487
|
+
type: CONFIG_RPC.SET,
|
|
383
488
|
pluginName,
|
|
384
489
|
data: config,
|
|
385
490
|
});
|
|
@@ -388,16 +493,25 @@ export class WebSocketManager {
|
|
|
388
493
|
return this.sendRequest<unknown>({ type: "schema:get", pluginName });
|
|
389
494
|
}
|
|
390
495
|
async getAllConfigs() {
|
|
391
|
-
return this.sendRequest<Record<string, unknown>>({ type:
|
|
496
|
+
return this.sendRequest<Record<string, unknown>>({ type: CONFIG_RPC.GET_ALL });
|
|
392
497
|
}
|
|
393
498
|
async getAllSchemas() {
|
|
394
499
|
return this.sendRequest<Record<string, unknown>>({ type: "schema:get-all" });
|
|
395
500
|
}
|
|
396
|
-
async
|
|
397
|
-
return this.sendRequest<
|
|
398
|
-
}
|
|
399
|
-
async
|
|
400
|
-
return this.sendRequest<{
|
|
501
|
+
async getConfigSource() {
|
|
502
|
+
return this.sendRequest<ConsoleConfigSource>({ type: CONFIG_RPC.GET_SOURCE });
|
|
503
|
+
}
|
|
504
|
+
async replaceConfigSource(source: string, expectedRevision: string) {
|
|
505
|
+
return this.sendRequest<{
|
|
506
|
+
success: boolean;
|
|
507
|
+
revision: string;
|
|
508
|
+
restartRequired: boolean;
|
|
509
|
+
message?: string;
|
|
510
|
+
}>({
|
|
511
|
+
type: CONFIG_RPC.REPLACE_SOURCE,
|
|
512
|
+
source,
|
|
513
|
+
expectedRevision,
|
|
514
|
+
});
|
|
401
515
|
}
|
|
402
516
|
async getEnvList() {
|
|
403
517
|
return this.sendRequest<{ files: Array<{ name: string; exists: boolean }> }>({ type: "env:list" });
|
|
@@ -409,7 +523,7 @@ export class WebSocketManager {
|
|
|
409
523
|
return this.sendRequest<{ success: boolean; message?: string }>({ type: "env:save", filename, content });
|
|
410
524
|
}
|
|
411
525
|
async getFileTree() {
|
|
412
|
-
return this.sendRequest<{ tree: import("./types").FileTreeNode[] }>({ type: "files:tree" });
|
|
526
|
+
return this.sendRequest<{ tree: import("./types.js").FileTreeNode[] }>({ type: "files:tree" });
|
|
413
527
|
}
|
|
414
528
|
async readFile(filePath: string) {
|
|
415
529
|
return this.sendRequest<{ content: string; size: number }>({ type: "files:read", filePath });
|
|
@@ -418,13 +532,13 @@ export class WebSocketManager {
|
|
|
418
532
|
return this.sendRequest<{ success: boolean; message?: string }>({ type: "files:save", filePath, content });
|
|
419
533
|
}
|
|
420
534
|
async getDbInfo() {
|
|
421
|
-
return this.sendRequest<import("./types").DatabaseInfo>({ type: "db:info" });
|
|
535
|
+
return this.sendRequest<import("./types.js").DatabaseInfo>({ type: "db:info" });
|
|
422
536
|
}
|
|
423
537
|
async getDbTables() {
|
|
424
|
-
return this.sendRequest<{ tables: import("./types").TableInfo[] }>({ type: "db:tables" });
|
|
538
|
+
return this.sendRequest<{ tables: import("./types.js").TableInfo[] }>({ type: "db:tables" });
|
|
425
539
|
}
|
|
426
540
|
async dbSelect(table: string, page?: number, pageSize?: number, where?: unknown) {
|
|
427
|
-
return this.sendRequest<import("./types").SelectResult>({ type: "db:select", table, page, pageSize, where });
|
|
541
|
+
return this.sendRequest<import("./types.js").SelectResult>({ type: "db:select", table, page, pageSize, where });
|
|
428
542
|
}
|
|
429
543
|
async dbInsert(table: string, row: unknown) {
|
|
430
544
|
return this.sendRequest<{ success: boolean }>({ type: "db:insert", table, row });
|
|
@@ -448,53 +562,29 @@ export class WebSocketManager {
|
|
|
448
562
|
return this.sendRequest<{ success: boolean }>({ type: "db:kv:delete", table, key });
|
|
449
563
|
}
|
|
450
564
|
async kvGetEntries(table: string) {
|
|
451
|
-
return this.sendRequest<{ entries: import("./types").KvEntry[] }>({ type: "db:kv:entries", table });
|
|
565
|
+
return this.sendRequest<{ entries: import("./types.js").KvEntry[] }>({ type: "db:kv:entries", table });
|
|
452
566
|
}
|
|
453
567
|
|
|
454
|
-
private
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
568
|
+
private async getRest<T>(path: string): Promise<T> {
|
|
569
|
+
const token = getToken();
|
|
570
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
571
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
572
|
+
const response = await this.config.fetch(resolveApiUrl(path), {
|
|
573
|
+
headers,
|
|
574
|
+
signal: AbortSignal.timeout(this.config.requestTimeout),
|
|
575
|
+
});
|
|
576
|
+
const body = await response.json() as { success?: boolean; data?: T; error?: string };
|
|
577
|
+
if (!response.ok || body.success === false) {
|
|
578
|
+
throw new ConsoleTransportError(body.error ?? `HTTP ${response.status}`, 'SERVER_ERROR');
|
|
579
|
+
}
|
|
580
|
+
return body.data as T;
|
|
458
581
|
}
|
|
459
582
|
|
|
460
583
|
private setState(newState: ConnectionState): void {
|
|
461
584
|
this.state = newState;
|
|
462
585
|
}
|
|
463
586
|
|
|
464
|
-
private
|
|
465
|
-
if (!this.ws) return;
|
|
466
|
-
this.ws.onopen = () => this.handleConnectionOpen();
|
|
467
|
-
this.ws.onmessage = (event) => this.handleMessage(event);
|
|
468
|
-
this.ws.onclose = () => this.handleConnectionClose();
|
|
469
|
-
this.ws.onerror = (event) =>
|
|
470
|
-
this.handleConnectionError(new ConnectionError("WebSocket error", event as unknown as Error));
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
private handleConnectionOpen(): void {
|
|
474
|
-
this.setState(ConnectionState.CONNECTED);
|
|
475
|
-
this.reconnectAttempts = 0;
|
|
476
|
-
this.notifyConnection(true);
|
|
477
|
-
this.callbacks.onConnect?.();
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
private handleMessage(event: MessageEvent): void {
|
|
481
|
-
try {
|
|
482
|
-
const message = normalizeConsolePushMessage(
|
|
483
|
-
JSON.parse(event.data) as WebSocketMessage,
|
|
484
|
-
) as WebSocketMessage;
|
|
485
|
-
if (message.requestId && this.pendingRequests.has(message.requestId)) {
|
|
486
|
-
this.handleRequestResponse(message);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
void applyConsoleEvent(message);
|
|
490
|
-
this.handleBroadcast(message);
|
|
491
|
-
this.callbacks.onMessage?.(message);
|
|
492
|
-
} catch (error) {
|
|
493
|
-
console.error("[WebSocket] Message parsing error:", error);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
private handleBroadcast(message: WebSocketMessage): void {
|
|
587
|
+
private handleBroadcast(message: ConsoleTransportMessage): void {
|
|
498
588
|
const t = message.type;
|
|
499
589
|
if (
|
|
500
590
|
t === SIDE_EVENT_PUSH.REQUEST_RECEIVE
|
|
@@ -518,37 +608,6 @@ export class WebSocketManager {
|
|
|
518
608
|
}
|
|
519
609
|
}
|
|
520
610
|
|
|
521
|
-
private handleRequestResponse(message: WebSocketMessage): void {
|
|
522
|
-
const { requestId } = message;
|
|
523
|
-
const pending = this.pendingRequests.get(requestId!);
|
|
524
|
-
if (!pending) return;
|
|
525
|
-
this.pendingRequests.delete(requestId!);
|
|
526
|
-
clearTimeout(pending.timer);
|
|
527
|
-
if (message.error) {
|
|
528
|
-
pending.reject(new WebSocketError(message.error, "SERVER_ERROR"));
|
|
529
|
-
} else {
|
|
530
|
-
pending.resolve(message.data);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private handleConnectionClose(): void {
|
|
535
|
-
this.ws = null;
|
|
536
|
-
this.notifyConnection(false);
|
|
537
|
-
if (this.state === ConnectionState.DISCONNECTED) return;
|
|
538
|
-
this.setState(ConnectionState.RECONNECTING);
|
|
539
|
-
this.callbacks.onDisconnect?.();
|
|
540
|
-
this.scheduleReconnect();
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
private handleConnectionError(error: Error): void {
|
|
544
|
-
// Used by the legacy WS path. Do NOT clear the reconnect timer here —
|
|
545
|
-
// callers that still want to retry should set RECONNECTING then call
|
|
546
|
-
// scheduleReconnect(). Terminal failures go through scheduleReconnect's
|
|
547
|
-
// maxAttempts branch which sets ERROR.
|
|
548
|
-
console.error("[Console transport] Connection error:", error);
|
|
549
|
-
this.callbacks.onError?.(error as unknown as Event);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
611
|
private scheduleReconnect(): void {
|
|
553
612
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
554
613
|
this.setState(ConnectionState.ERROR);
|
|
@@ -579,12 +638,12 @@ export class WebSocketManager {
|
|
|
579
638
|
}
|
|
580
639
|
}
|
|
581
640
|
|
|
582
|
-
private
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
this.
|
|
641
|
+
private assertActive(): void {
|
|
642
|
+
if (this.disposed) throw new Error('ConsoleTransport has been disposed');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private isCurrentConnection(generation: number): boolean {
|
|
646
|
+
return !this.disposed && generation === this.connectionGeneration;
|
|
588
647
|
}
|
|
589
648
|
}
|
|
590
649
|
|