@kyneta/sse-transport 1.1.0 → 1.2.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/README.md +11 -3
- package/dist/{chunk-TR4Y3HFB.js → chunk-ZBE5AMNA.js} +2 -2
- package/dist/chunk-ZBE5AMNA.js.map +1 -0
- package/dist/client.d.ts +63 -44
- package/dist/client.js +244 -208
- package/dist/client.js.map +1 -1
- package/dist/express.d.ts +2 -2
- package/dist/express.js +1 -1
- package/dist/express.js.map +1 -1
- package/dist/{server-transport-BrMRLsmp.d.ts → server-transport-D9l5s5PV.d.ts} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{types-BTgljZGe.d.ts → types-DaVaqfwF.d.ts} +1 -2
- package/package.json +9 -6
- package/src/__tests__/client-program.test.ts +513 -0
- package/src/__tests__/connection.test.ts +11 -5
- package/src/__tests__/sse-handler.test.ts +11 -8
- package/src/client-program.ts +207 -0
- package/src/client-transport.ts +205 -274
- package/src/client.ts +11 -5
- package/src/connection.ts +1 -1
- package/src/express-router.ts +1 -1
- package/src/server-transport.ts +3 -7
- package/src/sse-handler.ts +1 -1
- package/src/types.ts +3 -9
- package/dist/chunk-TR4Y3HFB.js.map +0 -1
- package/src/__tests__/client-state-machine.test.ts +0 -201
- package/src/client-state-machine.ts +0 -69
package/dist/client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import "./chunk-7D4SUZUM.js";
|
|
2
2
|
|
|
3
3
|
// src/client-transport.ts
|
|
4
|
-
import {
|
|
4
|
+
import { createObservableProgram } from "@kyneta/machine";
|
|
5
|
+
import { Transport } from "@kyneta/transport";
|
|
5
6
|
import {
|
|
6
7
|
encodeTextComplete,
|
|
7
8
|
fragmentTextPayload,
|
|
@@ -9,37 +10,131 @@ import {
|
|
|
9
10
|
textCodec
|
|
10
11
|
} from "@kyneta/wire";
|
|
11
12
|
|
|
12
|
-
// src/client-
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
// src/client-program.ts
|
|
14
|
+
import { computeBackoffDelay, DEFAULT_RECONNECT } from "@kyneta/transport";
|
|
15
|
+
function createSseClientProgram(options) {
|
|
16
|
+
const { url, jitterFn = () => Math.random() * 1e3 } = options;
|
|
17
|
+
const reconnect = {
|
|
18
|
+
...DEFAULT_RECONNECT,
|
|
19
|
+
...options.reconnect
|
|
20
|
+
};
|
|
21
|
+
function tryReconnect(currentAttempt, reason, ...extraEffects) {
|
|
22
|
+
if (!reconnect.enabled) {
|
|
23
|
+
return [{ status: "disconnected", reason }, ...extraEffects];
|
|
24
|
+
}
|
|
25
|
+
if (currentAttempt >= reconnect.maxAttempts) {
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
status: "disconnected",
|
|
29
|
+
reason: { type: "max-retries-exceeded", attempts: currentAttempt }
|
|
30
|
+
},
|
|
31
|
+
...extraEffects
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
const delay = computeBackoffDelay(
|
|
35
|
+
currentAttempt + 1,
|
|
36
|
+
reconnect.baseDelay,
|
|
37
|
+
reconnect.maxDelay,
|
|
38
|
+
jitterFn()
|
|
39
|
+
);
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
status: "reconnecting",
|
|
43
|
+
attempt: currentAttempt + 1,
|
|
44
|
+
nextAttemptMs: delay
|
|
45
|
+
},
|
|
46
|
+
...extraEffects,
|
|
47
|
+
{ type: "start-reconnect-timer", delayMs: delay }
|
|
48
|
+
];
|
|
32
49
|
}
|
|
33
|
-
|
|
50
|
+
return {
|
|
51
|
+
init: [{ status: "disconnected" }],
|
|
52
|
+
update(msg, model) {
|
|
53
|
+
switch (msg.type) {
|
|
54
|
+
// -----------------------------------------------------------------
|
|
55
|
+
// start
|
|
56
|
+
// -----------------------------------------------------------------
|
|
57
|
+
case "start": {
|
|
58
|
+
if (model.status !== "disconnected") return [model];
|
|
59
|
+
return [
|
|
60
|
+
{ status: "connecting", attempt: 1 },
|
|
61
|
+
{ type: "create-event-source", url, attempt: 1 }
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
// -----------------------------------------------------------------
|
|
65
|
+
// event-source-opened
|
|
66
|
+
// -----------------------------------------------------------------
|
|
67
|
+
case "event-source-opened": {
|
|
68
|
+
if (model.status !== "connecting") return [model];
|
|
69
|
+
return [
|
|
70
|
+
{ status: "connected" },
|
|
71
|
+
{ type: "add-channel-and-establish" }
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
// -----------------------------------------------------------------
|
|
75
|
+
// event-source-error
|
|
76
|
+
// -----------------------------------------------------------------
|
|
77
|
+
case "event-source-error": {
|
|
78
|
+
const reason = {
|
|
79
|
+
type: "error",
|
|
80
|
+
error: new Error("EventSource connection error")
|
|
81
|
+
};
|
|
82
|
+
if (model.status === "connecting") {
|
|
83
|
+
return tryReconnect(model.attempt, reason, {
|
|
84
|
+
type: "close-event-source"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (model.status === "connected") {
|
|
88
|
+
return tryReconnect(
|
|
89
|
+
0,
|
|
90
|
+
reason,
|
|
91
|
+
{ type: "remove-channel" },
|
|
92
|
+
{ type: "close-event-source" },
|
|
93
|
+
{ type: "abort-pending-posts" }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return [model];
|
|
97
|
+
}
|
|
98
|
+
// -----------------------------------------------------------------
|
|
99
|
+
// reconnect-timer-fired
|
|
100
|
+
// -----------------------------------------------------------------
|
|
101
|
+
case "reconnect-timer-fired": {
|
|
102
|
+
if (model.status !== "reconnecting") return [model];
|
|
103
|
+
return [
|
|
104
|
+
{ status: "connecting", attempt: model.attempt },
|
|
105
|
+
{ type: "create-event-source", url, attempt: model.attempt }
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
// -----------------------------------------------------------------
|
|
109
|
+
// stop
|
|
110
|
+
// -----------------------------------------------------------------
|
|
111
|
+
case "stop": {
|
|
112
|
+
if (model.status === "disconnected") return [model];
|
|
113
|
+
const effects = [
|
|
114
|
+
{ type: "cancel-reconnect-timer" }
|
|
115
|
+
];
|
|
116
|
+
if (model.status === "connecting") {
|
|
117
|
+
effects.push({ type: "close-event-source" });
|
|
118
|
+
}
|
|
119
|
+
if (model.status === "connected") {
|
|
120
|
+
effects.push(
|
|
121
|
+
{ type: "close-event-source" },
|
|
122
|
+
{ type: "remove-channel" },
|
|
123
|
+
{ type: "abort-pending-posts" }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
{ status: "disconnected", reason: { type: "intentional" } },
|
|
128
|
+
...effects
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
34
135
|
|
|
35
136
|
// src/client-transport.ts
|
|
36
137
|
var DEFAULT_FRAGMENT_THRESHOLD = 6e4;
|
|
37
|
-
var DEFAULT_RECONNECT = {
|
|
38
|
-
enabled: true,
|
|
39
|
-
maxAttempts: 10,
|
|
40
|
-
baseDelay: 1e3,
|
|
41
|
-
maxDelay: 3e4
|
|
42
|
-
};
|
|
43
138
|
var DEFAULT_POST_RETRY = {
|
|
44
139
|
maxAttempts: 3,
|
|
45
140
|
baseDelay: 1e3,
|
|
@@ -47,14 +142,13 @@ var DEFAULT_POST_RETRY = {
|
|
|
47
142
|
};
|
|
48
143
|
var SseClientTransport = class extends Transport {
|
|
49
144
|
#peerId;
|
|
145
|
+
#options;
|
|
146
|
+
// Observable program handle — created in onStart(), drives all state
|
|
147
|
+
#handle;
|
|
148
|
+
// Executor-local I/O state — not in the program model
|
|
50
149
|
#eventSource;
|
|
51
150
|
#serverChannel;
|
|
52
151
|
#reconnectTimer;
|
|
53
|
-
#options;
|
|
54
|
-
#shouldReconnect = true;
|
|
55
|
-
#wasConnectedBefore = false;
|
|
56
|
-
// State machine
|
|
57
|
-
#stateMachine = new SseClientStateMachine();
|
|
58
152
|
// Fragmentation
|
|
59
153
|
#fragmentThreshold;
|
|
60
154
|
// Inbound reassembly for fragmented SSE messages from server
|
|
@@ -68,13 +162,26 @@ var SseClientTransport = class extends Transport {
|
|
|
68
162
|
this.#reassembler = new TextReassembler({
|
|
69
163
|
timeoutMs: 1e4
|
|
70
164
|
});
|
|
165
|
+
const program = createSseClientProgram({
|
|
166
|
+
url: "__deferred__",
|
|
167
|
+
reconnect: options.reconnect
|
|
168
|
+
});
|
|
169
|
+
this.#handle = createObservableProgram(program, (effect, dispatch) => {
|
|
170
|
+
this.#executeEffect(effect, dispatch);
|
|
171
|
+
});
|
|
71
172
|
this.#setupLifecycleEvents();
|
|
72
173
|
}
|
|
73
174
|
// ==========================================================================
|
|
74
175
|
// Lifecycle event forwarding
|
|
75
176
|
// ==========================================================================
|
|
177
|
+
/**
|
|
178
|
+
* Subscribe to the observable handle's transitions and forward them to
|
|
179
|
+
* the lifecycle callbacks. `wasConnectedBefore` is observer-local state,
|
|
180
|
+
* not in the program model.
|
|
181
|
+
*/
|
|
76
182
|
#setupLifecycleEvents() {
|
|
77
|
-
|
|
183
|
+
let wasConnectedBefore = false;
|
|
184
|
+
this.#handle.subscribeToTransitions((transition) => {
|
|
78
185
|
this.#options.lifecycle?.onStateChange?.(transition);
|
|
79
186
|
const { from, to } = transition;
|
|
80
187
|
if (to.status === "disconnected" && to.reason) {
|
|
@@ -83,47 +190,134 @@ var SseClientTransport = class extends Transport {
|
|
|
83
190
|
if (to.status === "reconnecting") {
|
|
84
191
|
this.#options.lifecycle?.onReconnecting?.(to.attempt, to.nextAttemptMs);
|
|
85
192
|
}
|
|
86
|
-
if (
|
|
193
|
+
if (wasConnectedBefore && (from.status === "reconnecting" || from.status === "connecting") && to.status === "connected") {
|
|
87
194
|
this.#options.lifecycle?.onReconnected?.();
|
|
88
195
|
}
|
|
196
|
+
if (to.status === "connected") {
|
|
197
|
+
wasConnectedBefore = true;
|
|
198
|
+
}
|
|
199
|
+
if (to.status === "disconnected" && to.reason?.type === "intentional") {
|
|
200
|
+
wasConnectedBefore = false;
|
|
201
|
+
}
|
|
89
202
|
});
|
|
90
203
|
}
|
|
91
204
|
// ==========================================================================
|
|
92
|
-
//
|
|
205
|
+
// Effect executor — interprets data effects as I/O
|
|
206
|
+
// ==========================================================================
|
|
207
|
+
#executeEffect(effect, dispatch) {
|
|
208
|
+
switch (effect.type) {
|
|
209
|
+
case "create-event-source": {
|
|
210
|
+
this.#doCreateEventSource(dispatch);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "close-event-source": {
|
|
214
|
+
if (this.#eventSource) {
|
|
215
|
+
this.#eventSource.onopen = null;
|
|
216
|
+
this.#eventSource.onmessage = null;
|
|
217
|
+
this.#eventSource.onerror = null;
|
|
218
|
+
this.#eventSource.close();
|
|
219
|
+
this.#eventSource = void 0;
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case "add-channel-and-establish": {
|
|
224
|
+
if (this.#serverChannel) {
|
|
225
|
+
this.removeChannel(this.#serverChannel.channelId);
|
|
226
|
+
this.#serverChannel = void 0;
|
|
227
|
+
}
|
|
228
|
+
this.#serverChannel = this.addChannel();
|
|
229
|
+
this.establishChannel(this.#serverChannel.channelId);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "remove-channel": {
|
|
233
|
+
if (this.#serverChannel) {
|
|
234
|
+
this.removeChannel(this.#serverChannel.channelId);
|
|
235
|
+
this.#serverChannel = void 0;
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "start-reconnect-timer": {
|
|
240
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
241
|
+
this.#reconnectTimer = void 0;
|
|
242
|
+
dispatch({ type: "reconnect-timer-fired" });
|
|
243
|
+
}, effect.delayMs);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "cancel-reconnect-timer": {
|
|
247
|
+
if (this.#reconnectTimer !== void 0) {
|
|
248
|
+
clearTimeout(this.#reconnectTimer);
|
|
249
|
+
this.#reconnectTimer = void 0;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "abort-pending-posts": {
|
|
254
|
+
if (this.#currentRetryAbortController) {
|
|
255
|
+
this.#currentRetryAbortController.abort();
|
|
256
|
+
this.#currentRetryAbortController = void 0;
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create an EventSource and wire up event handlers.
|
|
264
|
+
* The URL is resolved here (may be a function of peerId).
|
|
265
|
+
*/
|
|
266
|
+
#doCreateEventSource(dispatch) {
|
|
267
|
+
if (!this.#peerId) {
|
|
268
|
+
throw new Error("Cannot connect: peerId not set");
|
|
269
|
+
}
|
|
270
|
+
const url = typeof this.#options.eventSourceUrl === "function" ? this.#options.eventSourceUrl(this.#peerId) : this.#options.eventSourceUrl;
|
|
271
|
+
try {
|
|
272
|
+
this.#eventSource = new EventSource(url);
|
|
273
|
+
this.#eventSource.onopen = () => {
|
|
274
|
+
dispatch({ type: "event-source-opened" });
|
|
275
|
+
};
|
|
276
|
+
this.#eventSource.onmessage = (event) => {
|
|
277
|
+
this.#handleMessage(event);
|
|
278
|
+
};
|
|
279
|
+
this.#eventSource.onerror = () => {
|
|
280
|
+
dispatch({ type: "event-source-error" });
|
|
281
|
+
};
|
|
282
|
+
} catch (_error) {
|
|
283
|
+
dispatch({ type: "event-source-error" });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ==========================================================================
|
|
287
|
+
// State observation — delegated to the observable handle
|
|
93
288
|
// ==========================================================================
|
|
94
289
|
/**
|
|
95
|
-
* Get the current state
|
|
290
|
+
* Get the current connection state.
|
|
96
291
|
*/
|
|
97
292
|
getState() {
|
|
98
|
-
return this.#
|
|
293
|
+
return this.#handle.getState();
|
|
99
294
|
}
|
|
100
295
|
/**
|
|
101
296
|
* Subscribe to state transitions.
|
|
102
|
-
* @returns Unsubscribe function
|
|
103
297
|
*/
|
|
104
298
|
subscribeToTransitions(listener) {
|
|
105
|
-
return this.#
|
|
299
|
+
return this.#handle.subscribeToTransitions(listener);
|
|
106
300
|
}
|
|
107
301
|
/**
|
|
108
302
|
* Wait for a specific state.
|
|
109
303
|
*/
|
|
110
304
|
waitForState(predicate, options) {
|
|
111
|
-
return this.#
|
|
305
|
+
return this.#handle.waitForState(predicate, options);
|
|
112
306
|
}
|
|
113
307
|
/**
|
|
114
308
|
* Wait for a specific status.
|
|
115
309
|
*/
|
|
116
310
|
waitForStatus(status, options) {
|
|
117
|
-
return this.#
|
|
311
|
+
return this.#handle.waitForStatus(status, options);
|
|
118
312
|
}
|
|
119
313
|
/**
|
|
120
|
-
*
|
|
314
|
+
* Whether the client is connected and ready to send/receive.
|
|
121
315
|
*/
|
|
122
316
|
get isConnected() {
|
|
123
|
-
return this.#
|
|
317
|
+
return this.#handle.getState().status === "connected";
|
|
124
318
|
}
|
|
125
319
|
// ==========================================================================
|
|
126
|
-
//
|
|
320
|
+
// Transport abstract method implementations
|
|
127
321
|
// ==========================================================================
|
|
128
322
|
generate() {
|
|
129
323
|
return {
|
|
@@ -161,102 +355,15 @@ var SseClientTransport = class extends Transport {
|
|
|
161
355
|
);
|
|
162
356
|
}
|
|
163
357
|
this.#peerId = this.identity.peerId;
|
|
164
|
-
this.#
|
|
165
|
-
this.#wasConnectedBefore = false;
|
|
166
|
-
this.#connect();
|
|
358
|
+
this.#handle.dispatch({ type: "start" });
|
|
167
359
|
}
|
|
168
360
|
async onStop() {
|
|
169
|
-
this.#shouldReconnect = false;
|
|
170
361
|
this.#reassembler.dispose();
|
|
171
|
-
this.#
|
|
172
|
-
this.#currentRetryAbortController = void 0;
|
|
173
|
-
this.#disconnect({ type: "intentional" });
|
|
174
|
-
}
|
|
175
|
-
// ==========================================================================
|
|
176
|
-
// Connection management
|
|
177
|
-
// ==========================================================================
|
|
178
|
-
/**
|
|
179
|
-
* Connect to the SSE server by creating an EventSource.
|
|
180
|
-
*/
|
|
181
|
-
#connect() {
|
|
182
|
-
const currentState = this.#stateMachine.getState();
|
|
183
|
-
if (currentState.status === "connecting") {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (!this.#peerId) {
|
|
187
|
-
throw new Error("Cannot connect: peerId not set");
|
|
188
|
-
}
|
|
189
|
-
const attempt = currentState.status === "reconnecting" ? currentState.attempt : 1;
|
|
190
|
-
this.#stateMachine.transition({ status: "connecting", attempt });
|
|
191
|
-
const url = typeof this.#options.eventSourceUrl === "function" ? this.#options.eventSourceUrl(this.#peerId) : this.#options.eventSourceUrl;
|
|
192
|
-
try {
|
|
193
|
-
this.#eventSource = new EventSource(url);
|
|
194
|
-
this.#eventSource.onopen = () => {
|
|
195
|
-
this.#handleOpen();
|
|
196
|
-
};
|
|
197
|
-
this.#eventSource.onmessage = (event) => {
|
|
198
|
-
this.#handleMessage(event);
|
|
199
|
-
};
|
|
200
|
-
this.#eventSource.onerror = () => {
|
|
201
|
-
this.#handleError();
|
|
202
|
-
};
|
|
203
|
-
} catch (error) {
|
|
204
|
-
this.#scheduleReconnect({
|
|
205
|
-
type: "error",
|
|
206
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Disconnect from the SSE server.
|
|
212
|
-
*/
|
|
213
|
-
#disconnect(reason) {
|
|
214
|
-
this.#clearReconnectTimer();
|
|
215
|
-
if (this.#eventSource) {
|
|
216
|
-
this.#eventSource.onopen = null;
|
|
217
|
-
this.#eventSource.onmessage = null;
|
|
218
|
-
this.#eventSource.onerror = null;
|
|
219
|
-
this.#eventSource.close();
|
|
220
|
-
this.#eventSource = void 0;
|
|
221
|
-
}
|
|
222
|
-
if (this.#serverChannel) {
|
|
223
|
-
this.removeChannel(this.#serverChannel.channelId);
|
|
224
|
-
this.#serverChannel = void 0;
|
|
225
|
-
}
|
|
226
|
-
const currentState = this.#stateMachine.getState();
|
|
227
|
-
if (currentState.status !== "disconnected") {
|
|
228
|
-
this.#stateMachine.transition({ status: "disconnected", reason });
|
|
229
|
-
}
|
|
362
|
+
this.#handle.dispatch({ type: "stop" });
|
|
230
363
|
}
|
|
231
364
|
// ==========================================================================
|
|
232
|
-
//
|
|
365
|
+
// Inbound message handling
|
|
233
366
|
// ==========================================================================
|
|
234
|
-
/**
|
|
235
|
-
* Handle EventSource open event.
|
|
236
|
-
*
|
|
237
|
-
* The SSE connection is usable immediately — no "ready" signal needed.
|
|
238
|
-
* Create the channel and initiate establishment.
|
|
239
|
-
*/
|
|
240
|
-
#handleOpen() {
|
|
241
|
-
const currentState = this.#stateMachine.getState();
|
|
242
|
-
if (currentState.status !== "connecting" && currentState.status !== "connected") {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (currentState.status === "connecting") {
|
|
246
|
-
this.#stateMachine.transition({ status: "connected" });
|
|
247
|
-
}
|
|
248
|
-
this.#wasConnectedBefore = true;
|
|
249
|
-
if (this.#currentRetryAbortController) {
|
|
250
|
-
this.#currentRetryAbortController.abort();
|
|
251
|
-
this.#currentRetryAbortController = void 0;
|
|
252
|
-
}
|
|
253
|
-
if (this.#serverChannel) {
|
|
254
|
-
this.removeChannel(this.#serverChannel.channelId);
|
|
255
|
-
this.#serverChannel = void 0;
|
|
256
|
-
}
|
|
257
|
-
this.#serverChannel = this.addChannel();
|
|
258
|
-
this.establishChannel(this.#serverChannel.channelId);
|
|
259
|
-
}
|
|
260
367
|
/**
|
|
261
368
|
* Handle incoming SSE message.
|
|
262
369
|
*
|
|
@@ -287,30 +394,6 @@ var SseClientTransport = class extends Transport {
|
|
|
287
394
|
console.error("SSE message reassembly error:", result.error);
|
|
288
395
|
}
|
|
289
396
|
}
|
|
290
|
-
/**
|
|
291
|
-
* Handle EventSource error.
|
|
292
|
-
*
|
|
293
|
-
* Closes the EventSource immediately and takes over reconnection
|
|
294
|
-
* via the state machine's backoff logic. This prevents the browser's
|
|
295
|
-
* built-in EventSource reconnection from running.
|
|
296
|
-
*/
|
|
297
|
-
#handleError() {
|
|
298
|
-
if (this.#eventSource) {
|
|
299
|
-
this.#eventSource.onopen = null;
|
|
300
|
-
this.#eventSource.onmessage = null;
|
|
301
|
-
this.#eventSource.onerror = null;
|
|
302
|
-
this.#eventSource.close();
|
|
303
|
-
this.#eventSource = void 0;
|
|
304
|
-
}
|
|
305
|
-
if (this.#serverChannel) {
|
|
306
|
-
this.removeChannel(this.#serverChannel.channelId);
|
|
307
|
-
this.#serverChannel = void 0;
|
|
308
|
-
}
|
|
309
|
-
this.#scheduleReconnect({
|
|
310
|
-
type: "error",
|
|
311
|
-
error: new Error("EventSource connection error")
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
397
|
// ==========================================================================
|
|
315
398
|
// POST sending with retry
|
|
316
399
|
// ==========================================================================
|
|
@@ -400,61 +483,14 @@ var SseClientTransport = class extends Transport {
|
|
|
400
483
|
}
|
|
401
484
|
}
|
|
402
485
|
}
|
|
403
|
-
// ==========================================================================
|
|
404
|
-
// Reconnection
|
|
405
|
-
// ==========================================================================
|
|
406
|
-
/**
|
|
407
|
-
* Schedule a reconnection attempt or transition to disconnected.
|
|
408
|
-
*/
|
|
409
|
-
#scheduleReconnect(reason) {
|
|
410
|
-
const currentState = this.#stateMachine.getState();
|
|
411
|
-
if (currentState.status === "disconnected") {
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const reconnectOpts = {
|
|
415
|
-
...DEFAULT_RECONNECT,
|
|
416
|
-
...this.#options.reconnect
|
|
417
|
-
};
|
|
418
|
-
if (!this.#shouldReconnect || !reconnectOpts.enabled) {
|
|
419
|
-
this.#stateMachine.transition({ status: "disconnected", reason });
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
const currentAttempt = currentState.status === "reconnecting" ? currentState.attempt : currentState.status === "connecting" ? currentState.attempt : 0;
|
|
423
|
-
if (currentAttempt >= reconnectOpts.maxAttempts) {
|
|
424
|
-
this.#stateMachine.transition({
|
|
425
|
-
status: "disconnected",
|
|
426
|
-
reason: { type: "max-retries-exceeded", attempts: currentAttempt }
|
|
427
|
-
});
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
const nextAttempt = currentAttempt + 1;
|
|
431
|
-
const delay = Math.min(
|
|
432
|
-
reconnectOpts.baseDelay * 2 ** (nextAttempt - 1) + Math.random() * 1e3,
|
|
433
|
-
reconnectOpts.maxDelay
|
|
434
|
-
);
|
|
435
|
-
this.#stateMachine.transition({
|
|
436
|
-
status: "reconnecting",
|
|
437
|
-
attempt: nextAttempt,
|
|
438
|
-
nextAttemptMs: delay
|
|
439
|
-
});
|
|
440
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
441
|
-
this.#connect();
|
|
442
|
-
}, delay);
|
|
443
|
-
}
|
|
444
|
-
#clearReconnectTimer() {
|
|
445
|
-
if (this.#reconnectTimer) {
|
|
446
|
-
clearTimeout(this.#reconnectTimer);
|
|
447
|
-
this.#reconnectTimer = void 0;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
486
|
};
|
|
451
487
|
function createSseClient(options) {
|
|
452
488
|
return () => new SseClientTransport(options);
|
|
453
489
|
}
|
|
454
490
|
export {
|
|
455
491
|
DEFAULT_FRAGMENT_THRESHOLD,
|
|
456
|
-
SseClientStateMachine,
|
|
457
492
|
SseClientTransport,
|
|
458
|
-
createSseClient
|
|
493
|
+
createSseClient,
|
|
494
|
+
createSseClientProgram
|
|
459
495
|
};
|
|
460
496
|
//# sourceMappingURL=client.js.map
|