@meowpanel/api 0.13.0 → 0.14.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/esm/src/sse.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { TypedEmitter } from './emitter.js';
1
2
  /**
2
3
  * A generic, `fetch`-based Server-Sent Events client.
3
4
  *
@@ -5,9 +6,13 @@
5
6
  * header, making it compatible with bearer token auth in all environments
6
7
  * (browser, Deno, Node.js).
7
8
  *
9
+ * Like `EventSource`, it reconnects with exponential backoff whenever the stream ends without
10
+ * {@link disconnect} being called, including a clean close by the server.
11
+ *
8
12
  * @example
9
13
  * ```ts ignore
10
14
  * const sse = new SseConnection('https://panel.example.com/v1/sse', token);
15
+ * sse.on('state', (state) => console.log('SSE is', state));
11
16
  * sse.connect(
12
17
  * (event, data) => console.log(event, data),
13
18
  * (err) => console.error('SSE error:', err),
@@ -16,18 +21,32 @@
16
21
  * sse.disconnect();
17
22
  * ```
18
23
  */
19
- export class SseConnection {
24
+ export class SseConnection extends TypedEmitter {
20
25
  url;
21
26
  token;
22
27
  abortController;
28
+ reconnectTimer;
23
29
  _state = 'disconnected';
30
+ attempt = 0;
31
+ options;
24
32
  /**
25
- * @param url The full URL to connect to.
26
- * @param token Bearer token used in the `Authorization` header.
33
+ * @param url The full URL to connect to.
34
+ * @param token Bearer token used in the `Authorization` header, or a function returning the
35
+ * current one. A function is read on every attempt, so reconnects pick up a
36
+ * refreshed token.
37
+ * @param options Reconnect and idle-timeout behavior.
27
38
  */
28
- constructor(url, token) {
39
+ constructor(url, token, options = {}) {
40
+ super();
29
41
  this.url = url;
30
42
  this.token = token;
43
+ this.options = {
44
+ reconnect: true,
45
+ minReconnectDelayMs: 1_000,
46
+ maxReconnectDelayMs: 30_000,
47
+ idleTimeoutMs: 0,
48
+ ...options,
49
+ };
31
50
  }
32
51
  /** Update the bearer token for subsequent reconnections. */
33
52
  updateToken(token) {
@@ -44,60 +63,121 @@ export class SseConnection {
44
63
  /**
45
64
  * Open the SSE connection and begin streaming events.
46
65
  *
47
- * This method is **non-blocking**: it starts the connection and returns immediately
48
- * after the HTTP response headers are received. Events are delivered asynchronously
49
- * via the `onEvent` callback.
66
+ * This method is **non-blocking**: it starts the connection and returns immediately.
67
+ * Events are delivered asynchronously via the `onEvent` callback, and state changes via the
68
+ * `state` event.
50
69
  *
51
70
  * @param onEvent Callback invoked for each received SSE event, with the event name and raw data string.
52
- * @param onError Optional callback invoked if the connection fails or is lost (not called on intentional disconnect).
71
+ * @param onError Optional callback invoked each time a connection attempt fails or an open stream
72
+ * is lost (not called on intentional disconnect). Reconnecting continues unless
73
+ * {@link state} is `error`.
53
74
  */
54
75
  connect(onEvent, onError) {
55
76
  this.disconnect();
56
- this._state = 'connecting';
77
+ this.attempt = 0;
78
+ this.open(onEvent, onError);
79
+ }
80
+ /** Close the SSE connection, cancel any pending reconnect, and stop receiving events. */
81
+ disconnect() {
82
+ clearTimeout(this.reconnectTimer);
83
+ this.reconnectTimer = undefined;
84
+ this.abortController?.abort();
85
+ this.abortController = undefined;
86
+ this.setState('disconnected');
87
+ }
88
+ open(onEvent, onError) {
89
+ this.setState(this.attempt === 0 ? 'connecting' : 'reconnecting');
57
90
  const controller = new AbortController();
58
91
  this.abortController = controller;
59
92
  (async () => {
93
+ const { idleTimeoutMs } = this.options;
94
+ let idleTimer;
95
+ let idledOut = false;
96
+ const resetIdleTimer = () => {
97
+ if (!idleTimeoutMs)
98
+ return;
99
+ clearTimeout(idleTimer);
100
+ idleTimer = setTimeout(() => {
101
+ idledOut = true;
102
+ controller.abort();
103
+ }, idleTimeoutMs);
104
+ };
105
+ let retry = true;
106
+ let error;
60
107
  try {
108
+ resetIdleTimer();
109
+ const token = typeof this.token === 'function' ? this.token() : this.token;
61
110
  const response = await globalThis.fetch(this.url, {
62
111
  headers: {
63
- 'Authorization': `Bearer ${this.token}`,
112
+ 'Authorization': `Bearer ${token}`,
64
113
  'Accept': 'text/event-stream',
65
114
  'Cache-Control': 'no-cache',
66
115
  },
67
116
  signal: controller.signal,
68
117
  });
69
- // Guard against a concurrent connect() that replaced our controller.
70
- if (this.abortController !== controller)
71
- return;
72
118
  if (!response.ok || !response.body) {
119
+ await response.body?.cancel();
120
+ retry = response.ok || SseConnection.isRetryableStatus(response.status);
73
121
  throw new Error(`SSE connection failed: HTTP ${response.status}`);
74
122
  }
75
- this._state = 'connected';
76
- await this.readStream(response.body, onEvent);
123
+ this.attempt = 0;
124
+ this.setState('connected');
125
+ await this.readStream(response.body, onEvent, resetIdleTimer);
126
+ error = new Error('SSE stream closed by the server');
77
127
  }
78
128
  catch (err) {
79
- if (err.name !== 'AbortError') {
80
- // Only update state if this is still the active connection.
81
- if (this.abortController !== controller)
82
- return;
83
- this._state = 'error';
84
- onError?.(err instanceof Error ? err : new Error(String(err)));
85
- return;
86
- }
129
+ error = idledOut
130
+ ? new Error(`SSE stream received nothing for ${idleTimeoutMs}ms`)
131
+ : err instanceof Error
132
+ ? err
133
+ : new Error(String(err));
87
134
  }
88
- if (this.abortController === controller) {
89
- this._state = 'disconnected';
135
+ finally {
136
+ clearTimeout(idleTimer);
90
137
  }
138
+ // disconnect() or a newer connect() replaced this attempt, so it ends silently.
139
+ if (this.abortController !== controller)
140
+ return;
141
+ onError?.(error);
142
+ if (this.abortController !== controller)
143
+ return;
144
+ if (!retry || !this.options.reconnect) {
145
+ this.abortController = undefined;
146
+ this.setState('error');
147
+ return;
148
+ }
149
+ this.setState('reconnecting');
150
+ this.reconnectTimer = setTimeout(() => {
151
+ this.reconnectTimer = undefined;
152
+ this.open(onEvent, onError);
153
+ }, this.nextReconnectDelay());
91
154
  })();
92
155
  }
93
- /** Close the SSE connection and stop receiving events. */
94
- disconnect() {
95
- this.abortController?.abort();
96
- this.abortController = undefined;
97
- this._state = 'disconnected';
156
+ /**
157
+ * Exponential backoff with jitter, so clients that lost the same server do not all retry at
158
+ * the same instant when it comes back.
159
+ */
160
+ nextReconnectDelay() {
161
+ const { minReconnectDelayMs, maxReconnectDelayMs } = this.options;
162
+ const base = Math.min(maxReconnectDelayMs, minReconnectDelayMs * 2 ** this.attempt);
163
+ this.attempt++;
164
+ return base / 2 + Math.random() * (base / 2);
165
+ }
166
+ /**
167
+ * 401 is retryable because the token is re-read on every attempt and may have been refreshed
168
+ * in the meantime. Other client errors (403, 404, ...) will not change on their own.
169
+ */
170
+ static isRetryableStatus(status) {
171
+ return status >= 500 || status === 401 || status === 408 || status === 429;
172
+ }
173
+ setState(state) {
174
+ if (this._state === state)
175
+ return;
176
+ this._state = state;
177
+ this.emit('state', state);
98
178
  }
99
179
  /** Parse the raw `ReadableStream` body into SSE frames, calling `onEvent` for each complete frame. */
100
- async readStream(body, onEvent) {
180
+ async readStream(body, onEvent, onChunk) {
101
181
  const reader = body.getReader();
102
182
  const decoder = new TextDecoder();
103
183
  let buffer = '';
@@ -108,6 +188,7 @@ export class SseConnection {
108
188
  const { done, value } = await reader.read();
109
189
  if (done)
110
190
  break;
191
+ onChunk();
111
192
  buffer += decoder.decode(value, { stream: true });
112
193
  const lines = buffer.split('\n');
113
194
  buffer = lines.pop() ?? '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meowpanel/api",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "MeowPanel API client library",
5
5
  "license": "MIT",
6
6
  "main": "./script/mod.js",
package/script/mod.d.ts CHANGED
@@ -54,7 +54,7 @@ export type { HttpClientOptions, HttpLogCode, HttpLogEvent, HttpLogger, HttpRequ
54
54
  export { TypedEmitter } from './src/emitter.js';
55
55
  export type { EventHandler } from './src/emitter.js';
56
56
  export { SseConnection } from './src/sse.js';
57
- export type { SseState } from './src/sse.js';
57
+ export type { SseConnectionEvents, SseOptions, SseState } from './src/sse.js';
58
58
  export { joinIncludes, parseIssuerRef, toNanoID } from './src/types.js';
59
59
  export type { DeletedListOptions, DeletedVisibility, IncludeOptions, ISODate, IssuerRef, ListOptions, NanoID, Paginated, ParsedIssuer, PowerAction, PowerState, ResourceColor, ResourceWith, ServerRestrictedState, SftpPermission, UnionToIntersection, } from './src/types.js';
60
60
  export { Account, AccountResource, AccountSshKeysManager, ApiKeyAccount, SshKey, UserAccount, } from './src/resources/account.js';
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { ReactiveResource } from '../../reactive.js';
2
+ import { type SseState } from '../../sse.js';
2
3
  import type { NanoID, PowerState } from '../../types.js';
3
4
  import type { JobStatus } from './jobs.js';
4
5
  /** Real-time resource utilization reported by the node. */
@@ -83,13 +84,19 @@ export interface ServerConnectionEvents {
83
84
  * Gated on `server.transfer.log`, so it never arrives for a viewer who lacks it.
84
85
  */
85
86
  transfer_logs: string;
87
+ /** The stream to the daemon changed state. Emitted locally by the client, not by the daemon. */
88
+ connection_state: SseState;
86
89
  }
87
90
  /**
88
91
  * A real-time SSE connection to a specific server on its Whisker node daemon.
89
92
  *
90
93
  * Emits typed events for console output, power state changes, and resource utilization.
91
94
  * Also maintains a rolling buffer of console output lines in {@link output}, pre-populated
92
- * with recent history fetched from the node on {@link connect}.
95
+ * with recent history fetched from the node on {@link open}.
96
+ *
97
+ * The stream reconnects on its own after the daemon restarts or the network drops. Track it
98
+ * through {@link state} or the `connection_state` event. After a reconnect, console lines the
99
+ * daemon still holds from the gap are appended to {@link output} and emitted as `console_output`.
93
100
  *
94
101
  * Obtain a connection via `Server.createConnection`.
95
102
  *
@@ -112,12 +119,20 @@ export interface ServerConnectionEvents {
112
119
  export declare class ServerConnection extends ReactiveResource {
113
120
  private readonly serverId;
114
121
  private readonly nodeUrl;
115
- private readonly token;
122
+ private readonly getToken;
116
123
  /** Maximum number of console output lines retained in {@link ServerConnection.output}. */
117
124
  private static readonly maxOutputLines;
125
+ /** Console history lines fetched from the node on open and after each reconnect. */
126
+ private static readonly historyLines;
127
+ /**
128
+ * Whisker sends a keepalive every 20 seconds, so a stream silent for this long is dead even
129
+ * if the socket never closed.
130
+ */
131
+ private static readonly idleTimeoutMs;
118
132
  private readonly listeners;
119
133
  private sse?;
120
- private connected;
134
+ /** Live console lines held back while the post-reconnect history is merged in. */
135
+ private pendingOutput?;
121
136
  /**
122
137
  * Rolling buffer of console output lines, capped at {@link ServerConnection.maxOutputLines}.
123
138
  *
@@ -128,7 +143,9 @@ export declare class ServerConnection extends ReactiveResource {
128
143
  */
129
144
  output: string[];
130
145
  /** @internal */
131
- constructor(serverId: NanoID, nodeUrl: string, token: string);
146
+ constructor(serverId: NanoID, nodeUrl: string, getToken: () => string);
147
+ /** State of the stream to the daemon. */
148
+ get state(): SseState;
132
149
  /** `true` if the SSE connection is currently open. */
133
150
  get isConnected(): boolean;
134
151
  /**
@@ -139,7 +156,7 @@ export declare class ServerConnection extends ReactiveResource {
139
156
  *
140
157
  * Only one stream is maintained at a time.
141
158
  *
142
- * @param onError Optional callback invoked if the connection drops unexpectedly.
159
+ * @param onError Optional callback invoked each time the stream drops or a connection attempt fails.
143
160
  */
144
161
  open(onError?: (error: Error) => void): Promise<void>;
145
162
  /** Close the SSE stream and stop receiving events. */
@@ -160,6 +177,16 @@ export declare class ServerConnection extends ReactiveResource {
160
177
  off<K extends keyof ServerConnectionEvents>(event: K, handler: (payload: ServerConnectionEvents[K]) => void): void;
161
178
  /** Remove all event handlers for all event types. */
162
179
  removeAllListeners(): void;
180
+ private fetchHistory;
181
+ /**
182
+ * Fill in the console lines printed while the stream was down.
183
+ *
184
+ * Live lines are held back until the history arrives, because the history snapshot may
185
+ * already contain them. Lines that fell out of the daemon's history window are lost.
186
+ */
187
+ private resyncOutput;
188
+ private appendOutput;
189
+ private emit;
163
190
  /** Parse and dispatch a raw SSE event to registered handlers. */
164
191
  private dispatch;
165
192
  }
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../src/src/resources/servers/connection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../src/src/resources/servers/connection.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,11 @@ const sse_js_1 = require("../../sse.js");
8
8
  *
9
9
  * Emits typed events for console output, power state changes, and resource utilization.
10
10
  * Also maintains a rolling buffer of console output lines in {@link output}, pre-populated
11
- * with recent history fetched from the node on {@link connect}.
11
+ * with recent history fetched from the node on {@link open}.
12
+ *
13
+ * The stream reconnects on its own after the daemon restarts or the network drops. Track it
14
+ * through {@link state} or the `connection_state` event. After a reconnect, console lines the
15
+ * daemon still holds from the gap are appended to {@link output} and emitted as `console_output`.
12
16
  *
13
17
  * Obtain a connection via `Server.createConnection`.
14
18
  *
@@ -31,12 +35,20 @@ const sse_js_1 = require("../../sse.js");
31
35
  class ServerConnection extends reactive_js_1.ReactiveResource {
32
36
  serverId;
33
37
  nodeUrl;
34
- token;
38
+ getToken;
35
39
  /** Maximum number of console output lines retained in {@link ServerConnection.output}. */
36
40
  static maxOutputLines = 1024;
41
+ /** Console history lines fetched from the node on open and after each reconnect. */
42
+ static historyLines = 100;
43
+ /**
44
+ * Whisker sends a keepalive every 20 seconds, so a stream silent for this long is dead even
45
+ * if the socket never closed.
46
+ */
47
+ static idleTimeoutMs = 45_000;
37
48
  listeners = new Map();
38
49
  sse;
39
- connected = false;
50
+ /** Live console lines held back while the post-reconnect history is merged in. */
51
+ pendingOutput;
40
52
  /**
41
53
  * Rolling buffer of console output lines, capped at {@link ServerConnection.maxOutputLines}.
42
54
  *
@@ -47,15 +59,19 @@ class ServerConnection extends reactive_js_1.ReactiveResource {
47
59
  */
48
60
  output = [];
49
61
  /** @internal */
50
- constructor(serverId, nodeUrl, token) {
62
+ constructor(serverId, nodeUrl, getToken) {
51
63
  super();
52
64
  this.serverId = serverId;
53
65
  this.nodeUrl = nodeUrl;
54
- this.token = token;
66
+ this.getToken = getToken;
67
+ }
68
+ /** State of the stream to the daemon. */
69
+ get state() {
70
+ return this.sse?.state ?? 'disconnected';
55
71
  }
56
72
  /** `true` if the SSE connection is currently open. */
57
73
  get isConnected() {
58
- return this.connected;
74
+ return this.state === 'connected';
59
75
  }
60
76
  /**
61
77
  * Fetch console history and open the SSE stream.
@@ -65,36 +81,35 @@ class ServerConnection extends reactive_js_1.ReactiveResource {
65
81
  *
66
82
  * Only one stream is maintained at a time.
67
83
  *
68
- * @param onError Optional callback invoked if the connection drops unexpectedly.
84
+ * @param onError Optional callback invoked each time the stream drops or a connection attempt fails.
69
85
  */
70
86
  async open(onError) {
71
- this.sse?.disconnect(); // close any existing stream
72
- try {
73
- const res = await fetch(`${this.nodeUrl}/api/servers/${this.serverId}/logs?size=100`, { headers: { Authorization: `Bearer ${this.token}` } });
74
- if (res.ok) {
75
- const json = (await res.json());
76
- this.output = json.data ?? [];
77
- this.notify();
78
- }
79
- }
80
- catch {
81
- // History is best-effort - don't block the SSE connection
87
+ this.close();
88
+ const history = await this.fetchHistory();
89
+ if (history) {
90
+ this.output = history;
91
+ this.notify();
82
92
  }
83
93
  const url = `${this.nodeUrl}/api/servers/${this.serverId}/sse`;
84
- this.sse = new sse_js_1.SseConnection(url, this.token);
85
- this.connected = true;
86
- this.sse.connect((event, data) => {
87
- this.dispatch(event, data);
88
- }, (err) => {
89
- this.connected = false;
94
+ const sse = new sse_js_1.SseConnection(url, this.getToken, { idleTimeoutMs: ServerConnection.idleTimeoutMs });
95
+ this.sse = sse;
96
+ let previous = sse.state;
97
+ sse.on('state', (state) => {
98
+ if (state === 'connected' && previous === 'reconnecting')
99
+ this.resyncOutput(sse);
100
+ previous = state;
90
101
  this.notify();
91
- onError?.(err);
102
+ this.emit('connection_state', state);
92
103
  });
104
+ sse.connect((event, data) => this.dispatch(event, data), onError);
93
105
  }
94
106
  /** Close the SSE stream and stop receiving events. */
95
107
  close() {
96
- this.sse?.disconnect();
97
- this.connected = false;
108
+ const sse = this.sse;
109
+ this.sse = undefined;
110
+ this.pendingOutput = undefined;
111
+ sse?.disconnect();
112
+ sse?.removeAllListeners();
98
113
  }
99
114
  /**
100
115
  * Register a handler for a typed server event.
@@ -124,6 +139,47 @@ class ServerConnection extends reactive_js_1.ReactiveResource {
124
139
  removeAllListeners() {
125
140
  this.listeners.clear();
126
141
  }
142
+ async fetchHistory() {
143
+ try {
144
+ const res = await fetch(`${this.nodeUrl}/api/servers/${this.serverId}/logs?size=${ServerConnection.historyLines}`, { headers: { Authorization: `Bearer ${this.getToken()}` } });
145
+ if (!res.ok)
146
+ return undefined;
147
+ const json = (await res.json());
148
+ return json.data ?? [];
149
+ }
150
+ catch {
151
+ return undefined;
152
+ }
153
+ }
154
+ /**
155
+ * Fill in the console lines printed while the stream was down.
156
+ *
157
+ * Live lines are held back until the history arrives, because the history snapshot may
158
+ * already contain them. Lines that fell out of the daemon's history window are lost.
159
+ */
160
+ async resyncOutput(sse) {
161
+ const pending = [];
162
+ this.pendingOutput = pending;
163
+ const history = await this.fetchHistory();
164
+ if (this.sse !== sse || this.pendingOutput !== pending)
165
+ return;
166
+ this.pendingOutput = undefined;
167
+ const missed = history ? history.slice(overlapLength(this.output, history)) : [];
168
+ const live = history ? pending.slice(overlapLength(history, pending)) : pending;
169
+ for (const line of [...missed, ...live])
170
+ this.appendOutput(line);
171
+ }
172
+ appendOutput(line) {
173
+ this.output.push(line);
174
+ if (this.output.length > ServerConnection.maxOutputLines) {
175
+ this.output.shift();
176
+ }
177
+ this.notify();
178
+ this.emit('console_output', line);
179
+ }
180
+ emit(event, payload) {
181
+ this.listeners.get(event)?.forEach((h) => h(payload));
182
+ }
127
183
  /** Parse and dispatch a raw SSE event to registered handlers. */
128
184
  dispatch(rawEvent, rawData) {
129
185
  // backup_completed is emitted per backup as `backup_completed:<id>`; the id is also in
@@ -139,11 +195,6 @@ class ServerConnection extends reactive_js_1.ReactiveResource {
139
195
  if (typeof parsed !== 'string')
140
196
  return;
141
197
  payload = parsed;
142
- this.output.push(payload);
143
- if (this.output.length > ServerConnection.maxOutputLines) {
144
- this.output.shift();
145
- }
146
- this.notify();
147
198
  break;
148
199
  }
149
200
  case 'status': {
@@ -231,8 +282,30 @@ class ServerConnection extends reactive_js_1.ReactiveResource {
231
282
  catch {
232
283
  return;
233
284
  }
234
- const handlers = this.listeners.get(event);
235
- handlers?.forEach((h) => h(payload));
285
+ if (event === 'console_output') {
286
+ const line = payload;
287
+ if (this.pendingOutput)
288
+ this.pendingOutput.push(line);
289
+ else
290
+ this.appendOutput(line);
291
+ return;
292
+ }
293
+ this.emit(event, payload);
236
294
  }
237
295
  }
238
296
  exports.ServerConnection = ServerConnection;
297
+ /** Length of the longest suffix of `head` that is also a prefix of `tail`. */
298
+ function overlapLength(head, tail) {
299
+ for (let k = Math.min(head.length, tail.length); k > 0; k--) {
300
+ let match = true;
301
+ for (let i = 0; i < k; i++) {
302
+ if (head[head.length - k + i] !== tail[i]) {
303
+ match = false;
304
+ break;
305
+ }
306
+ }
307
+ if (match)
308
+ return k;
309
+ }
310
+ return 0;
311
+ }
@@ -175,7 +175,7 @@ class Server extends reactive_js_1.ReactiveResource {
175
175
  */
176
176
  async createConnection() {
177
177
  const nodeUrl = await this.resolveNodeUrl();
178
- return new connection_js_1.ServerConnection(this.id, nodeUrl, this.http.getToken());
178
+ return new connection_js_1.ServerConnection(this.id, nodeUrl, () => this.http.getToken());
179
179
  }
180
180
  }
181
181
  exports.Server = Server;