@larc-iu/plaid-client 0.2.0-alpha.1 → 0.2.0-alpha.11
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/index.d.ts +581 -105
- package/package.json +1 -1
- package/src/http.js +254 -35
- package/src/index.js +1171 -365
- package/src/provenance.js +259 -27
- package/src/serviceSchema.js +50 -30
- package/src/services.js +202 -37
- package/src/sse.js +51 -1
package/src/services.js
CHANGED
|
@@ -52,13 +52,38 @@ export function discardService(client, projectId, serviceId) {
|
|
|
52
52
|
* @param {Object} serviceInfo - {serviceId, serviceName, description}
|
|
53
53
|
* @param {function} onServiceRequest - Handler callback (data, responseHelper)
|
|
54
54
|
* @param {Object} extras - Optional additional metadata
|
|
55
|
-
* @
|
|
55
|
+
* @param {function} [onStatus] - Optional callback (event, projectId, detail) for
|
|
56
|
+
* connection-state transitions: 'registered', 'reconnected', 'disconnected'.
|
|
57
|
+
* Called once per transition, not once per retry.
|
|
58
|
+
* @returns {Object} ServiceRegistration with .stop(), .isRunning(), .isConnected(),
|
|
59
|
+
* .serviceInfo
|
|
56
60
|
*/
|
|
57
|
-
export function serve(client, projectId, serviceInfo, onServiceRequest, extras = {}) {
|
|
61
|
+
export function serve(client, projectId, serviceInfo, onServiceRequest, extras = {}, onStatus = null) {
|
|
58
62
|
const { serviceId, serviceName, description = '' } = serviceInfo;
|
|
59
63
|
let connection = null;
|
|
60
64
|
let isRunning = true;
|
|
61
65
|
let reconnectTimer = null;
|
|
66
|
+
// Last state REPORTED, so an outage produces one "lost" line and one "back"
|
|
67
|
+
// line however many retries it took.
|
|
68
|
+
let isConnected = false;
|
|
69
|
+
let everConnected = false;
|
|
70
|
+
|
|
71
|
+
const report = (event, detail) => {
|
|
72
|
+
if (!onStatus) return;
|
|
73
|
+
try { onStatus(event, projectId, detail); } catch (_) { /* never disturb the loop */ }
|
|
74
|
+
};
|
|
75
|
+
const noteConnected = () => {
|
|
76
|
+
if (isConnected) return;
|
|
77
|
+
const first = !everConnected;
|
|
78
|
+
isConnected = true;
|
|
79
|
+
everConnected = true;
|
|
80
|
+
report(first ? 'registered' : 'reconnected');
|
|
81
|
+
};
|
|
82
|
+
const noteDisconnected = () => {
|
|
83
|
+
if (!isConnected) return;
|
|
84
|
+
isConnected = false;
|
|
85
|
+
report('disconnected');
|
|
86
|
+
};
|
|
62
87
|
|
|
63
88
|
const reportEvent = (requestId, body) =>
|
|
64
89
|
client
|
|
@@ -76,6 +101,10 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
76
101
|
if (connection) connection.close();
|
|
77
102
|
},
|
|
78
103
|
isRunning: () => isRunning,
|
|
104
|
+
// Whether the request channel is open RIGHT NOW, i.e. whether the server
|
|
105
|
+
// currently sees this service as online. A registration that is retrying
|
|
106
|
+
// through a server restart is still running but not connected.
|
|
107
|
+
isConnected: () => isRunning && !!connection && connection.readyState === 1,
|
|
79
108
|
serviceInfo: { serviceId, serviceName, description, extras },
|
|
80
109
|
};
|
|
81
110
|
|
|
@@ -89,24 +118,58 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
89
118
|
const qs = params.toString();
|
|
90
119
|
const channelPath = `/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}/requests${qs ? `?${qs}` : ''}`;
|
|
91
120
|
|
|
92
|
-
// The channel
|
|
121
|
+
// The requests this channel is serving, by id, each flagged once a
|
|
122
|
+
// `service_cancel` arrives. The handler reads it as `responseHelper.cancelled`;
|
|
123
|
+
// whether to stop is the handler's decision (a write under way should
|
|
124
|
+
// finish), and the request still ends with whatever it reports.
|
|
125
|
+
const cancelled = new Map();
|
|
126
|
+
|
|
127
|
+
// Beyond `connected` (ignored), the channel carries `service_request` and
|
|
128
|
+
// `service_cancel` events.
|
|
93
129
|
const onChannelEvent = (eventType, payload) => {
|
|
94
130
|
if (!isRunning) return true;
|
|
95
|
-
if (
|
|
131
|
+
if (!payload) return;
|
|
132
|
+
if (eventType === 'service_cancel') {
|
|
133
|
+
if (cancelled.has(payload.requestId)) cancelled.set(payload.requestId, true);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (eventType !== 'service_request') return;
|
|
96
137
|
const requestId = payload.requestId;
|
|
97
138
|
if (!requestId) return;
|
|
139
|
+
cancelled.set(requestId, false);
|
|
140
|
+
const finished = () => cancelled.delete(requestId);
|
|
141
|
+
|
|
142
|
+
// Every service is told who asked (`requesterId`), beside the payload
|
|
143
|
+
// when that is a plain object, and on the helper regardless.
|
|
144
|
+
const requesterId = payload.requesterId || null;
|
|
145
|
+
const data =
|
|
146
|
+
requesterId && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)
|
|
147
|
+
? { ...payload.data, requesterId }
|
|
148
|
+
: payload.data;
|
|
98
149
|
|
|
99
150
|
const responseHelper = {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
151
|
+
requestId,
|
|
152
|
+
requesterId,
|
|
153
|
+
// True once the requester asked for the request to stop.
|
|
154
|
+
get cancelled() {
|
|
155
|
+
return cancelled.get(requestId) === true;
|
|
156
|
+
},
|
|
157
|
+
// Extra fields ride in the progress payload (a chat service sends the
|
|
158
|
+
// reply text so far as `text`).
|
|
159
|
+
progress: (percent, msg, extra) =>
|
|
160
|
+
reportEvent(requestId, { status: 'progress', progress: { percent, message: msg, ...extra } }),
|
|
161
|
+
complete: (data) => {
|
|
162
|
+
finished();
|
|
163
|
+
return reportEvent(requestId, { status: 'completed', data });
|
|
164
|
+
},
|
|
165
|
+
error: (error) => {
|
|
166
|
+
finished();
|
|
167
|
+
return reportEvent(requestId, { status: 'error', data: { error: error?.message || error } });
|
|
168
|
+
},
|
|
106
169
|
};
|
|
107
170
|
|
|
108
171
|
try {
|
|
109
|
-
onServiceRequest(
|
|
172
|
+
onServiceRequest(data, responseHelper);
|
|
110
173
|
} catch (error) {
|
|
111
174
|
responseHelper.error(error?.message || error);
|
|
112
175
|
}
|
|
@@ -118,12 +181,17 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
118
181
|
throw new Error(`Failed to start service: ${error.message}`);
|
|
119
182
|
}
|
|
120
183
|
|
|
121
|
-
// Reopen the channel
|
|
122
|
-
// re-registers the service server-side,
|
|
123
|
-
// back together
|
|
184
|
+
// Reopen the channel whenever it drops (e.g. the server restarted), for as
|
|
185
|
+
// long as the service runs. Reopening re-registers the service server-side,
|
|
186
|
+
// so presence and reachability come back together, and a server restart never
|
|
187
|
+
// means a service restart. A reopened channel counts as connected only once
|
|
188
|
+
// the server has actually answered it (readyState OPEN), not merely because
|
|
189
|
+
// the attempt was made.
|
|
124
190
|
reconnectTimer = setInterval(() => {
|
|
125
|
-
if (!isRunning) return;
|
|
126
|
-
if (connection
|
|
191
|
+
if (!isRunning || !connection) return;
|
|
192
|
+
if (connection.readyState === 1) { noteConnected(); return; }
|
|
193
|
+
if (connection.readyState === 2) { // CLOSED (dropped or failed)
|
|
194
|
+
noteDisconnected();
|
|
127
195
|
try { connection = openChannel(); } catch (_) { /* retry next tick */ }
|
|
128
196
|
}
|
|
129
197
|
}, 3000);
|
|
@@ -138,15 +206,26 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
138
206
|
* response (no broadcast). Rejects if no service is connected (503), if the
|
|
139
207
|
* service reports an error, or on timeout.
|
|
140
208
|
*
|
|
209
|
+
* The request outlives this call: after a timeout, an abort, or a dropped
|
|
210
|
+
* connection the service goes on, and `attachServiceRequest` collects the
|
|
211
|
+
* result given the request id, which `opts.onAccepted` receives as soon as
|
|
212
|
+
* the server has taken the request. Pass `opts.requestId` (a UUID you mint)
|
|
213
|
+
* to know the id before submitting; submitting an id that names a request
|
|
214
|
+
* you already made rejoins it instead of starting another.
|
|
215
|
+
*
|
|
141
216
|
* @param {Object} client - PlaidClient instance
|
|
142
217
|
* @param {string} projectId - Project UUID
|
|
143
218
|
* @param {string} serviceId - Service ID to request
|
|
144
219
|
* @param {any} data - Request payload
|
|
145
220
|
* @param {number} [timeout=10000] - Timeout in ms
|
|
146
221
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
222
|
+
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
223
|
+
* @param {Object} [opts]
|
|
224
|
+
* @param {string} [opts.requestId] - A client-minted request id (UUID)
|
|
225
|
+
* @param {function} [opts.onAccepted] - Called with the request id once the server has it
|
|
147
226
|
* @returns {Promise<any>} The service's result
|
|
148
227
|
*/
|
|
149
|
-
export function requestService(client, projectId, serviceId, data, timeout = 10000, onProgress) {
|
|
228
|
+
export function requestService(client, projectId, serviceId, data, timeout = 10000, onProgress, signal, opts = {}) {
|
|
150
229
|
// Propagate an open logical operation (client.beginOperation) to the
|
|
151
230
|
// service: its writes then fold under the requester's audit-log entry
|
|
152
231
|
// (the Python BaseService adopts the id around process_request). Only for a
|
|
@@ -157,6 +236,73 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
157
236
|
group && data && typeof data === 'object' && !Array.isArray(data)
|
|
158
237
|
? { ...data, operationGroup: { id: group.id, message: group.message } }
|
|
159
238
|
: data;
|
|
239
|
+
const qs = opts.requestId ? `?request-id=${encodeURIComponent(opts.requestId)}` : '';
|
|
240
|
+
return streamServiceRequest(
|
|
241
|
+
client,
|
|
242
|
+
{
|
|
243
|
+
url: `${client.baseUrl}/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}/requests${qs}`,
|
|
244
|
+
method: 'POST',
|
|
245
|
+
body: JSON.stringify(payload === undefined ? null : transformRequest(payload)),
|
|
246
|
+
onStatus: (response) =>
|
|
247
|
+
response.status === 503 ? new Error(`No live service '${serviceId}' on this project`) : null,
|
|
248
|
+
what: 'Service request',
|
|
249
|
+
},
|
|
250
|
+
timeout,
|
|
251
|
+
onProgress,
|
|
252
|
+
signal,
|
|
253
|
+
opts.onAccepted,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Rejoin a service request made earlier (by this user) and await its result:
|
|
259
|
+
* the latest progress is replayed, then the result comes, or at once if the
|
|
260
|
+
* request already finished. Rejects with an error whose `status` is 404 when
|
|
261
|
+
* the request is unknown or expired (the server keeps a finished request's
|
|
262
|
+
* result for a while, not forever).
|
|
263
|
+
*
|
|
264
|
+
* @param {Object} client - PlaidClient instance
|
|
265
|
+
* @param {string} projectId - Project UUID
|
|
266
|
+
* @param {string} requestId - The request id (from `onAccepted` or your own)
|
|
267
|
+
* @param {number} [timeout=10000] - Timeout in ms
|
|
268
|
+
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
269
|
+
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
270
|
+
* @returns {Promise<any>} The service's result
|
|
271
|
+
*/
|
|
272
|
+
export function attachServiceRequest(client, projectId, requestId, timeout = 10000, onProgress, signal) {
|
|
273
|
+
return streamServiceRequest(
|
|
274
|
+
client,
|
|
275
|
+
{
|
|
276
|
+
url: `${client.baseUrl}/api/v1/projects/${projectId}/service-requests/${encodeURIComponent(requestId)}`,
|
|
277
|
+
method: 'GET',
|
|
278
|
+
what: 'Attach to service request',
|
|
279
|
+
},
|
|
280
|
+
timeout,
|
|
281
|
+
onProgress,
|
|
282
|
+
signal,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Ask the service to stop a request made earlier (by this user). The request
|
|
288
|
+
* still ends with whatever the service then reports, on the stream of whoever
|
|
289
|
+
* is awaiting it. Rejects with 404 if unknown or expired, 409 once finished.
|
|
290
|
+
*
|
|
291
|
+
* @param {Object} client - PlaidClient instance
|
|
292
|
+
* @param {string} projectId - Project UUID
|
|
293
|
+
* @param {string} requestId - The request id
|
|
294
|
+
* @returns {Promise<void>}
|
|
295
|
+
*/
|
|
296
|
+
export function cancelServiceRequest(client, projectId, requestId) {
|
|
297
|
+
return client._request('DELETE', `/api/v1/projects/${projectId}/service-requests/${encodeURIComponent(requestId)}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Open a request stream (submit or attach) and read it to its terminal
|
|
302
|
+
* event: `accepted` names the request, `progress` events go to `onProgress`,
|
|
303
|
+
* and `result` / `error` settle the promise.
|
|
304
|
+
*/
|
|
305
|
+
function streamServiceRequest(client, { url, method, body, onStatus, what }, timeout, onProgress, signal, onAccepted) {
|
|
160
306
|
return new Promise((resolve, reject) => {
|
|
161
307
|
const abortController = new AbortController();
|
|
162
308
|
let settled = false;
|
|
@@ -168,37 +314,54 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
168
314
|
fn(arg);
|
|
169
315
|
};
|
|
170
316
|
const timer = setTimeout(
|
|
171
|
-
() => finish(reject, new Error(
|
|
317
|
+
() => finish(reject, new Error(`${what} timed out after ${timeout}ms`)),
|
|
172
318
|
timeout,
|
|
173
319
|
);
|
|
174
320
|
|
|
321
|
+
// An external signal stops waiting on a long request (a UI's Stop button).
|
|
322
|
+
// Reject with an AbortError so a caller can tell a deliberate stop from a
|
|
323
|
+
// failure. The service is not told here: it goes on, and its result can
|
|
324
|
+
// still be collected by request id (see cancelServiceRequest to stop it).
|
|
325
|
+
const stop = () => {
|
|
326
|
+
const err = new Error('The service request was stopped');
|
|
327
|
+
err.name = 'AbortError';
|
|
328
|
+
finish(reject, err);
|
|
329
|
+
};
|
|
330
|
+
if (signal) {
|
|
331
|
+
if (signal.aborted) {
|
|
332
|
+
stop();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
336
|
+
}
|
|
337
|
+
|
|
175
338
|
(async () => {
|
|
176
339
|
let response;
|
|
177
340
|
try {
|
|
178
|
-
response = await fetch(
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
'Content-Type': 'application/json',
|
|
185
|
-
'Accept': 'text/event-stream',
|
|
186
|
-
},
|
|
187
|
-
body: JSON.stringify(payload === undefined ? null : transformRequest(payload)),
|
|
188
|
-
signal: abortController.signal,
|
|
341
|
+
response = await fetch(url, {
|
|
342
|
+
method,
|
|
343
|
+
headers: {
|
|
344
|
+
'Authorization': `Bearer ${client.token}`,
|
|
345
|
+
'Content-Type': 'application/json',
|
|
346
|
+
'Accept': 'text/event-stream',
|
|
189
347
|
},
|
|
190
|
-
|
|
348
|
+
...(body === undefined ? {} : { body }),
|
|
349
|
+
signal: abortController.signal,
|
|
350
|
+
});
|
|
191
351
|
} catch (error) {
|
|
192
|
-
if (error.name !== 'AbortError') finish(reject, new Error(
|
|
352
|
+
if (error.name !== 'AbortError') finish(reject, new Error(`${what} could not be sent: ${error.message}`));
|
|
193
353
|
return;
|
|
194
354
|
}
|
|
195
355
|
|
|
196
|
-
|
|
197
|
-
|
|
356
|
+
const special = onStatus ? onStatus(response) : null;
|
|
357
|
+
if (special) {
|
|
358
|
+
finish(reject, special);
|
|
198
359
|
return;
|
|
199
360
|
}
|
|
200
361
|
if (!response.ok) {
|
|
201
|
-
|
|
362
|
+
const err = new Error(`${what} failed: HTTP ${response.status} ${response.statusText}`);
|
|
363
|
+
err.status = response.status;
|
|
364
|
+
finish(reject, err);
|
|
202
365
|
return;
|
|
203
366
|
}
|
|
204
367
|
|
|
@@ -223,13 +386,15 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
223
386
|
dataLine = line.slice(6);
|
|
224
387
|
} else if (line === '' && eventType && dataLine) {
|
|
225
388
|
const payload = transformResponse(JSON.parse(dataLine));
|
|
226
|
-
if (eventType === '
|
|
389
|
+
if (eventType === 'accepted') {
|
|
390
|
+
if (onAccepted) { try { onAccepted(payload.requestId); } catch (_) { /* ignore */ } }
|
|
391
|
+
} else if (eventType === 'progress') {
|
|
227
392
|
if (onProgress) { try { onProgress(payload.progress); } catch (_) { /* ignore */ } }
|
|
228
393
|
} else if (eventType === 'result') {
|
|
229
394
|
finish(resolve, payload.data);
|
|
230
395
|
return;
|
|
231
396
|
} else if (eventType === 'error') {
|
|
232
|
-
finish(reject, new Error(payload?.error ||
|
|
397
|
+
finish(reject, new Error(payload?.error || `${what} failed`));
|
|
233
398
|
return;
|
|
234
399
|
}
|
|
235
400
|
eventType = '';
|
|
@@ -239,7 +404,7 @@ export function requestService(client, projectId, serviceId, data, timeout = 100
|
|
|
239
404
|
}
|
|
240
405
|
finish(reject, new Error('Service closed the connection without a result'));
|
|
241
406
|
} catch (error) {
|
|
242
|
-
if (error.name !== 'AbortError') finish(reject, new Error(
|
|
407
|
+
if (error.name !== 'AbortError') finish(reject, new Error(`${what} stream error: ${error.message}`));
|
|
243
408
|
}
|
|
244
409
|
})();
|
|
245
410
|
});
|
package/src/sse.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { transformResponse } from './transforms.js';
|
|
2
2
|
|
|
3
|
+
// A live SSE stream is never silent for long: the server sends a keepalive
|
|
4
|
+
// comment on service channels (every 25s) and a heartbeat event on the /listen
|
|
5
|
+
// bus (every 30s). So if NOTHING arrives for longer than this, the connection is
|
|
6
|
+
// dead: the server was killed without closing the socket, the network dropped
|
|
7
|
+
// it, or a reconnect caught the server mid-restart (port accepting, app not yet
|
|
8
|
+
// streaming). We must surface that as a drop so the registration supervisor in
|
|
9
|
+
// serve() can reopen it. Without the watchdog such a silent drop
|
|
10
|
+
// leaves `reader.read()` pending forever: readyState never reaches CLOSED, so
|
|
11
|
+
// nothing reconnects and the service never heals. Comfortably above both server
|
|
12
|
+
// cadences, so a healthy idle stream never trips it.
|
|
13
|
+
export const SSE_IDLE_TIMEOUT_MS = 60000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Project a parsed SSE `data:` object onto what a listener callback sees.
|
|
17
|
+
*
|
|
18
|
+
* The envelope is API surface and gets the usual `transformResponse` recasing.
|
|
19
|
+
* A `message` event's `data` is NOT: it is opaque application data (whatever
|
|
20
|
+
* `sendMessage` was handed), so its keys must survive verbatim, exactly as
|
|
21
|
+
* `metadata` and `config` do elsewhere. Without this, a key written
|
|
22
|
+
* `case-marker` reaches a JavaScript listener as `caseMarker` and a Python
|
|
23
|
+
* listener as `case_marker`, so the same broadcast reads differently depending
|
|
24
|
+
* on the reader's language.
|
|
25
|
+
*/
|
|
26
|
+
export function eventPayload(eventType, parsed) {
|
|
27
|
+
const payload = transformResponse(parsed);
|
|
28
|
+
if (eventType === 'message' && parsed && typeof parsed === 'object' && 'data' in parsed) {
|
|
29
|
+
payload.data = parsed.data;
|
|
30
|
+
}
|
|
31
|
+
return payload;
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
/**
|
|
4
35
|
* Create an SSE connection to the listen endpoint using fetch-based streaming.
|
|
5
36
|
* Automatically handles heartbeat confirmations and event parsing.
|
|
@@ -40,6 +71,21 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
40
71
|
}
|
|
41
72
|
};
|
|
42
73
|
|
|
74
|
+
// Watchdog: abort the fetch if the stream goes silent for longer than the
|
|
75
|
+
// server's keepalive cadence, so a silently-dropped connection lands in the
|
|
76
|
+
// catch below (readyState CLOSED) instead of blocking on a read that will
|
|
77
|
+
// never resolve.
|
|
78
|
+
let idleTimer = null;
|
|
79
|
+
const clearIdleTimer = () => {
|
|
80
|
+
if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
|
|
81
|
+
};
|
|
82
|
+
const armIdleTimer = () => {
|
|
83
|
+
clearIdleTimer();
|
|
84
|
+
idleTimer = setTimeout(() => {
|
|
85
|
+
if (!isClosed) abortController.abort(new Error('SSE stream went silent'));
|
|
86
|
+
}, SSE_IDLE_TIMEOUT_MS);
|
|
87
|
+
};
|
|
88
|
+
|
|
43
89
|
const sseConnection = {
|
|
44
90
|
readyState: 0, // CONNECTING
|
|
45
91
|
close: () => {
|
|
@@ -47,6 +93,7 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
47
93
|
isClosed = true;
|
|
48
94
|
isConnected = false;
|
|
49
95
|
sseConnection.readyState = 2; // CLOSED
|
|
96
|
+
clearIdleTimer();
|
|
50
97
|
abortController.abort();
|
|
51
98
|
}
|
|
52
99
|
},
|
|
@@ -64,6 +111,7 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
64
111
|
(async () => {
|
|
65
112
|
try {
|
|
66
113
|
const url = `${client.baseUrl}${streamPath}`;
|
|
114
|
+
armIdleTimer();
|
|
67
115
|
const response = await fetch(url, {
|
|
68
116
|
method: 'GET',
|
|
69
117
|
headers: {
|
|
@@ -101,6 +149,7 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
101
149
|
while (true) {
|
|
102
150
|
const { done, value } = await reader.read();
|
|
103
151
|
if (done || isClosed) break;
|
|
152
|
+
armIdleTimer();
|
|
104
153
|
|
|
105
154
|
buffer += decoder.decode(value, { stream: true });
|
|
106
155
|
const lines = buffer.split('\n');
|
|
@@ -124,7 +173,7 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
124
173
|
sendHeartbeatConfirmation();
|
|
125
174
|
} else {
|
|
126
175
|
const parsedData = JSON.parse(data);
|
|
127
|
-
const shouldStop = onEvent(eventType,
|
|
176
|
+
const shouldStop = onEvent(eventType, eventPayload(eventType, parsedData));
|
|
128
177
|
if (shouldStop === true) {
|
|
129
178
|
sseConnection.close();
|
|
130
179
|
return;
|
|
@@ -144,6 +193,7 @@ export function createSSEConnection(client, projectId, onEvent, path) {
|
|
|
144
193
|
console.warn('SSE connection error:', error);
|
|
145
194
|
}
|
|
146
195
|
} finally {
|
|
196
|
+
clearIdleTimer();
|
|
147
197
|
isConnected = false;
|
|
148
198
|
isClosed = true;
|
|
149
199
|
sseConnection.readyState = 2; // CLOSED
|