@multiplatform.one/frappe 6.4.3 → 6.7.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/dist/cjs/collection.cjs +25 -9
- package/dist/cjs/collection.native.js +25 -9
- package/dist/cjs/collection.native.js.map +1 -1
- package/dist/cjs/db.cjs +13 -2
- package/dist/cjs/db.native.js +18 -2
- package/dist/cjs/db.native.js.map +1 -1
- package/dist/cjs/frappeReact.cjs +10 -3
- package/dist/cjs/frappeReact.native.js +10 -3
- package/dist/cjs/frappeReact.native.js.map +1 -1
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/realtime.cjs +222 -11
- package/dist/cjs/realtime.native.js +352 -14
- package/dist/cjs/realtime.native.js.map +1 -1
- package/dist/cjs/sync/index.cjs +6 -1
- package/dist/cjs/sync/index.native.js +6 -1
- package/dist/cjs/sync/index.native.js.map +1 -1
- package/dist/cjs/sync/mutationEngine.cjs +43 -19
- package/dist/cjs/sync/mutationEngine.native.js +51 -21
- package/dist/cjs/sync/mutationEngine.native.js.map +1 -1
- package/dist/cjs/sync/subscriptionManager.cjs +4 -0
- package/dist/cjs/sync/subscriptionManager.native.js +4 -0
- package/dist/cjs/sync/subscriptionManager.native.js.map +1 -1
- package/dist/cjs/sync/types.native.js.map +1 -1
- package/dist/esm/collection.mjs +24 -9
- package/dist/esm/collection.mjs.map +1 -1
- package/dist/esm/collection.native.js +24 -9
- package/dist/esm/collection.native.js.map +1 -1
- package/dist/esm/db.mjs +13 -2
- package/dist/esm/db.mjs.map +1 -1
- package/dist/esm/db.native.js +18 -2
- package/dist/esm/db.native.js.map +1 -1
- package/dist/esm/frappeReact.mjs +11 -4
- package/dist/esm/frappeReact.mjs.map +1 -1
- package/dist/esm/frappeReact.native.js +11 -4
- package/dist/esm/frappeReact.native.js.map +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/realtime.mjs +222 -11
- package/dist/esm/realtime.mjs.map +1 -1
- package/dist/esm/realtime.native.js +352 -14
- package/dist/esm/realtime.native.js.map +1 -1
- package/dist/esm/sync/index.mjs +6 -1
- package/dist/esm/sync/index.mjs.map +1 -1
- package/dist/esm/sync/index.native.js +6 -1
- package/dist/esm/sync/index.native.js.map +1 -1
- package/dist/esm/sync/mutationEngine.mjs +43 -19
- package/dist/esm/sync/mutationEngine.mjs.map +1 -1
- package/dist/esm/sync/mutationEngine.native.js +51 -21
- package/dist/esm/sync/mutationEngine.native.js.map +1 -1
- package/dist/esm/sync/subscriptionManager.mjs +4 -0
- package/dist/esm/sync/subscriptionManager.mjs.map +1 -1
- package/dist/esm/sync/subscriptionManager.native.js +4 -0
- package/dist/esm/sync/subscriptionManager.native.js.map +1 -1
- package/dist/esm/sync/types.mjs.map +1 -1
- package/dist/esm/sync/types.native.js.map +1 -1
- package/dist/jsx/collection.mjs +24 -9
- package/dist/jsx/collection.mjs.map +1 -1
- package/dist/jsx/collection.native.js +25 -9
- package/dist/jsx/collection.native.js.map +1 -1
- package/dist/jsx/db.mjs +13 -2
- package/dist/jsx/db.mjs.map +1 -1
- package/dist/jsx/db.native.js +18 -2
- package/dist/jsx/db.native.js.map +1 -1
- package/dist/jsx/frappeReact.mjs +11 -4
- package/dist/jsx/frappeReact.mjs.map +1 -1
- package/dist/jsx/frappeReact.native.js +10 -3
- package/dist/jsx/frappeReact.native.js.map +1 -1
- package/dist/jsx/index.js +2 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +1 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/realtime.mjs +222 -11
- package/dist/jsx/realtime.mjs.map +1 -1
- package/dist/jsx/realtime.native.js +352 -14
- package/dist/jsx/realtime.native.js.map +1 -1
- package/dist/jsx/sync/index.mjs +6 -1
- package/dist/jsx/sync/index.mjs.map +1 -1
- package/dist/jsx/sync/index.native.js +6 -1
- package/dist/jsx/sync/index.native.js.map +1 -1
- package/dist/jsx/sync/mutationEngine.mjs +43 -19
- package/dist/jsx/sync/mutationEngine.mjs.map +1 -1
- package/dist/jsx/sync/mutationEngine.native.js +51 -21
- package/dist/jsx/sync/mutationEngine.native.js.map +1 -1
- package/dist/jsx/sync/subscriptionManager.mjs +4 -0
- package/dist/jsx/sync/subscriptionManager.mjs.map +1 -1
- package/dist/jsx/sync/subscriptionManager.native.js +4 -0
- package/dist/jsx/sync/subscriptionManager.native.js.map +1 -1
- package/dist/jsx/sync/types.mjs.map +1 -1
- package/dist/jsx/sync/types.native.js.map +1 -1
- package/package.json +7 -7
- package/src/collection.ts +53 -8
- package/src/db.spec.ts +70 -0
- package/src/db.ts +28 -2
- package/src/frappeReact.spec.ts +7 -1
- package/src/frappeReact.ts +30 -6
- package/src/frappeReactLiveQueryGc.spec.ts +44 -4
- package/src/index.ts +13 -1
- package/src/realtime.ts +406 -14
- package/src/realtimeSse.spec.ts +547 -0
- package/src/sync/index.ts +10 -1
- package/src/sync/mutationEngine.ts +81 -20
- package/src/sync/optimisticCreateReconcile.spec.ts +372 -0
- package/src/sync/subscriptionManager.spec.ts +41 -0
- package/src/sync/subscriptionManager.ts +10 -0
- package/src/sync/subscriptions.spec.ts +4 -0
- package/src/sync/types.ts +9 -0
- package/types/collection.d.ts +12 -0
- package/types/collection.d.ts.map +1 -1
- package/types/db.d.ts.map +1 -1
- package/types/frappeReact.d.ts.map +1 -1
- package/types/index.d.ts +2 -1
- package/types/index.d.ts.map +1 -1
- package/types/realtime.d.ts +166 -1
- package/types/realtime.d.ts.map +1 -1
- package/types/sync/index.d.ts.map +1 -1
- package/types/sync/mutationEngine.d.ts.map +1 -1
- package/types/sync/subscriptionManager.d.ts.map +1 -1
- package/types/sync/types.d.ts +9 -0
- package/types/sync/types.d.ts.map +1 -1
package/dist/cjs/realtime.cjs
CHANGED
|
@@ -40,11 +40,20 @@ function docRoomKey(doctype, name) {
|
|
|
40
40
|
}
|
|
41
41
|
class RealtimeModule {
|
|
42
42
|
socket;
|
|
43
|
+
/** Live EventSource when the `"sse"` transport is active. */
|
|
44
|
+
eventSource;
|
|
43
45
|
handlerMap = /* @__PURE__ */new Map();
|
|
44
46
|
/** Tracks events with socket listeners attached to prevent duplicates. */
|
|
45
47
|
attachedSocketEvents = /* @__PURE__ */new Set();
|
|
46
48
|
/** Stored socket event handler references for proper cleanup. */
|
|
47
49
|
socketEventHandlers = /* @__PURE__ */new Map();
|
|
50
|
+
/** Tracks SSE event types with listeners attached to prevent duplicates. */
|
|
51
|
+
attachedSseEvents = /* @__PURE__ */new Set();
|
|
52
|
+
/** Stored SSE listener references for proper cleanup. */
|
|
53
|
+
sseListeners = /* @__PURE__ */new Map();
|
|
54
|
+
/** One-shot debug notices for SSE's one-way limitations. */
|
|
55
|
+
sseRoomNoticeLogged = false;
|
|
56
|
+
sseEmitNoticeLogged = false;
|
|
48
57
|
/** Active doc rooms — re-emitted on every (re)connect. */
|
|
49
58
|
docRooms = /* @__PURE__ */new Map();
|
|
50
59
|
/** Active list rooms — re-emitted on every (re)connect. */
|
|
@@ -159,8 +168,11 @@ class RealtimeModule {
|
|
|
159
168
|
for (const resolve of this.healthyWaiters) resolve(ok);
|
|
160
169
|
this.healthyWaiters.clear();
|
|
161
170
|
}
|
|
171
|
+
isSSE() {
|
|
172
|
+
return this.config.transport === "sse";
|
|
173
|
+
}
|
|
162
174
|
waitUntilHealthy(timeoutMs = 2e4) {
|
|
163
|
-
if (this.
|
|
175
|
+
if (this.isHealthy()) return Promise.resolve(true);
|
|
164
176
|
return new Promise(resolve => {
|
|
165
177
|
const timer = setTimeout(() => {
|
|
166
178
|
this.healthyWaiters.delete(onReady);
|
|
@@ -174,8 +186,8 @@ class RealtimeModule {
|
|
|
174
186
|
});
|
|
175
187
|
}
|
|
176
188
|
async ensureConnected() {
|
|
177
|
-
if (this.
|
|
178
|
-
if (!this.socket && !this.isConnecting) {
|
|
189
|
+
if (this.isHealthy()) return;
|
|
190
|
+
if (!this.socket && !this.eventSource && !this.isConnecting) {
|
|
179
191
|
await this.connect();
|
|
180
192
|
}
|
|
181
193
|
await this.waitUntilHealthy();
|
|
@@ -189,7 +201,16 @@ class RealtimeModule {
|
|
|
189
201
|
this.socket.emit("doctype_subscribe", room.doctype);
|
|
190
202
|
}
|
|
191
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Open the realtime transport now. Room-driven auto-connect
|
|
206
|
+
* (subscribeDoc/subscribeList) remains the primary lifecycle in both
|
|
207
|
+
* transports — this is public for SSE consumers that use no doc/list room
|
|
208
|
+
* bookkeeping: a one-way stream has nothing to emit on join, so nothing
|
|
209
|
+
* else would ever open it. Idempotent while a transport exists or a
|
|
210
|
+
* connect is in flight.
|
|
211
|
+
*/
|
|
192
212
|
async connect() {
|
|
213
|
+
if (this.isSSE()) return this.connectSSE();
|
|
193
214
|
if (this.socket || this.isConnecting) return;
|
|
194
215
|
this.isConnecting = true;
|
|
195
216
|
try {
|
|
@@ -218,6 +239,153 @@ class RealtimeModule {
|
|
|
218
239
|
throw err;
|
|
219
240
|
}
|
|
220
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* SSE branch of connect(): construct an EventSource against the configured
|
|
244
|
+
* stream URL. EventSource connects at construction, auto-reconnects on
|
|
245
|
+
* failure, and resumes missed frames itself by sending the stream's last
|
|
246
|
+
* `id:` as the `Last-Event-ID` request header — none of that is
|
|
247
|
+
* reimplemented here.
|
|
248
|
+
*/
|
|
249
|
+
async connectSSE() {
|
|
250
|
+
if (this.eventSource || this.isConnecting) return;
|
|
251
|
+
this.isConnecting = true;
|
|
252
|
+
try {
|
|
253
|
+
const sse = this.config.sse;
|
|
254
|
+
const EventSourceCtor = sse?.EventSource ?? globalThis.EventSource;
|
|
255
|
+
if (!EventSourceCtor) {
|
|
256
|
+
throw new Error('[RealtimeModule] transport "sse" needs an EventSource implementation, and this runtime has no global EventSource (React Native and Node < 22 ship none). Pass a constructor via config.sse.EventSource \u2014 e.g. the "eventsource" npm package on Node or "react-native-sse" on React Native.');
|
|
257
|
+
}
|
|
258
|
+
const url = await this.buildSSEURL();
|
|
259
|
+
this.eventSource = new EventSourceCtor(url, {
|
|
260
|
+
withCredentials: sse?.withCredentials
|
|
261
|
+
});
|
|
262
|
+
this.initSSEHandlers();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
this.isConnecting = false;
|
|
265
|
+
this.resolveHealthyWaiters(false);
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async buildSSEURL() {
|
|
270
|
+
const sse = this.config.sse;
|
|
271
|
+
let url;
|
|
272
|
+
if (sse?.url) {
|
|
273
|
+
url = sse.url;
|
|
274
|
+
} else if (sse?.path) {
|
|
275
|
+
const base = this.config.baseURL.replace(/\/$/, "");
|
|
276
|
+
url = `${base}${sse.path.startsWith("/") ? "" : "/"}${sse.path}`;
|
|
277
|
+
} else {
|
|
278
|
+
throw new Error('[RealtimeModule] transport "sse" requires config.sse.url or config.sse.path. There is no default stream path: SSE endpoints are server-specific (the SHC daemon serves "/api/method/shc.events.stream"; a real Frappe bench has no SSE endpoint at all), so a framework default would only ever 404.');
|
|
279
|
+
}
|
|
280
|
+
const params = Object.entries(this.config.sse?.queryParams ?? {});
|
|
281
|
+
if (params.length === 0) return url;
|
|
282
|
+
const parts = [];
|
|
283
|
+
for (const [key, value] of params) {
|
|
284
|
+
const resolved = typeof value === "function" ? await value() : value;
|
|
285
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(resolved)}`);
|
|
286
|
+
}
|
|
287
|
+
return `${url}${url.includes("?") ? "&" : "?"}${parts.join("&")}`;
|
|
288
|
+
}
|
|
289
|
+
initSSEHandlers() {
|
|
290
|
+
this.attachedSseEvents.clear();
|
|
291
|
+
this.sseListeners.clear();
|
|
292
|
+
this.attachSSEListener("open");
|
|
293
|
+
this.attachSSEListener("error");
|
|
294
|
+
this.attachSSEListener("message");
|
|
295
|
+
for (const type of this.config.sse?.events ?? []) {
|
|
296
|
+
this.attachSSEListener(type);
|
|
297
|
+
}
|
|
298
|
+
for (const event of this.handlerMap.keys()) {
|
|
299
|
+
this.attachSSEListener(event);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
attachSSEListener(type) {
|
|
303
|
+
if (!this.eventSource || this.attachedSseEvents.has(type)) return;
|
|
304
|
+
const listener = event => {
|
|
305
|
+
this.handleSSEEvent(type, event);
|
|
306
|
+
};
|
|
307
|
+
this.sseListeners.set(type, listener);
|
|
308
|
+
this.attachedSseEvents.add(type);
|
|
309
|
+
this.eventSource.addEventListener(type, listener);
|
|
310
|
+
}
|
|
311
|
+
detachSSEListener(type) {
|
|
312
|
+
if (this.isCoreSSEType(type)) return;
|
|
313
|
+
if (!this.eventSource || !this.attachedSseEvents.has(type)) return;
|
|
314
|
+
const listener = this.sseListeners.get(type);
|
|
315
|
+
if (listener) {
|
|
316
|
+
this.eventSource.removeEventListener(type, listener);
|
|
317
|
+
this.sseListeners.delete(type);
|
|
318
|
+
}
|
|
319
|
+
this.attachedSseEvents.delete(type);
|
|
320
|
+
}
|
|
321
|
+
isCoreSSEType(type) {
|
|
322
|
+
return type === "open" || type === "error" || type === "message" || (this.config.sse?.events ?? []).includes(type);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Single entry point for everything the EventSource dispatches. Dataless
|
|
326
|
+
* events are transport lifecycle (`open`/`error` as plain Events); anything
|
|
327
|
+
* carrying `data` is a server frame — including frames the server
|
|
328
|
+
* classified `event: error`, which arrive as MessageEvents WITH data and
|
|
329
|
+
* must not be mistaken for connection errors.
|
|
330
|
+
*/
|
|
331
|
+
handleSSEEvent(type, event) {
|
|
332
|
+
if (event?.data === void 0) {
|
|
333
|
+
if (type === "open") {
|
|
334
|
+
this.isConnecting = false;
|
|
335
|
+
this.socketHealthy = true;
|
|
336
|
+
this.emitToHandlers("connected", {
|
|
337
|
+
transport: "sse"
|
|
338
|
+
});
|
|
339
|
+
this.resolveHealthyWaiters(true);
|
|
340
|
+
} else if (type === "error") {
|
|
341
|
+
this.isConnecting = false;
|
|
342
|
+
this.socketHealthy = false;
|
|
343
|
+
this.emitToHandlers("disconnect", {
|
|
344
|
+
reason: "sse-error"
|
|
345
|
+
});
|
|
346
|
+
this.resolveHealthyWaiters(false);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const frame = {
|
|
351
|
+
type,
|
|
352
|
+
data: String(event.data),
|
|
353
|
+
lastEventId: event.lastEventId ?? ""
|
|
354
|
+
};
|
|
355
|
+
const mapFrame = this.config.sse?.mapFrame;
|
|
356
|
+
if (mapFrame) {
|
|
357
|
+
let mapped;
|
|
358
|
+
try {
|
|
359
|
+
mapped = mapFrame(frame);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.warn("[RealtimeModule] sse.mapFrame threw; frame dropped:", err);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!mapped) return;
|
|
365
|
+
for (const {
|
|
366
|
+
event: mappedEvent,
|
|
367
|
+
payload: payload2
|
|
368
|
+
} of mapped) {
|
|
369
|
+
this.emitToHandlers(mappedEvent, payload2);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
let payload = frame.data;
|
|
374
|
+
try {
|
|
375
|
+
payload = JSON.parse(frame.data);
|
|
376
|
+
} catch {}
|
|
377
|
+
this.emitToHandlers(frame.type, payload);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Logged once: SSE is one-way, so doc/list room membership can't be sent
|
|
381
|
+
* upstream — rooms are recorded client-side (see getDocRooms/getListRooms)
|
|
382
|
+
* for mapFrame-driven filtering and for a future two-way transport.
|
|
383
|
+
*/
|
|
384
|
+
logSSERoomNoticeOnce() {
|
|
385
|
+
if (this.sseRoomNoticeLogged) return;
|
|
386
|
+
this.sseRoomNoticeLogged = true;
|
|
387
|
+
console.debug("[RealtimeModule] SSE transport is one-way: doc/list room subscriptions are recorded client-side only (getDocRooms()/getListRooms()) and nothing is sent to the server \u2014 filter server-side via sse.queryParams or route frames with sse.mapFrame.");
|
|
388
|
+
}
|
|
221
389
|
/**
|
|
222
390
|
* Tear down the socket without clearing app handlers or room subscriptions.
|
|
223
391
|
* Used by auth refresh so listeners/rooms survive credential rotation.
|
|
@@ -230,8 +398,17 @@ class RealtimeModule {
|
|
|
230
398
|
this.socket.disconnect();
|
|
231
399
|
this.socket = void 0;
|
|
232
400
|
}
|
|
401
|
+
if (this.eventSource) {
|
|
402
|
+
for (const [type, listener] of this.sseListeners) {
|
|
403
|
+
this.eventSource.removeEventListener(type, listener);
|
|
404
|
+
}
|
|
405
|
+
this.eventSource.close();
|
|
406
|
+
this.eventSource = void 0;
|
|
407
|
+
}
|
|
233
408
|
this.attachedSocketEvents.clear();
|
|
234
409
|
this.socketEventHandlers.clear();
|
|
410
|
+
this.attachedSseEvents.clear();
|
|
411
|
+
this.sseListeners.clear();
|
|
235
412
|
this.socketHealthy = false;
|
|
236
413
|
this.isConnecting = false;
|
|
237
414
|
this.resolveHealthyWaiters(false);
|
|
@@ -308,7 +485,11 @@ class RealtimeModule {
|
|
|
308
485
|
this.handlerMap.set(event, handlerSet);
|
|
309
486
|
}
|
|
310
487
|
handlerSet.add(handler);
|
|
311
|
-
this.
|
|
488
|
+
if (this.isSSE()) {
|
|
489
|
+
this.attachSSEListener(event);
|
|
490
|
+
} else {
|
|
491
|
+
this.attachSocketListener(event);
|
|
492
|
+
}
|
|
312
493
|
}
|
|
313
494
|
off(event, handler) {
|
|
314
495
|
const handlerSet = this.handlerMap.get(event);
|
|
@@ -316,7 +497,11 @@ class RealtimeModule {
|
|
|
316
497
|
handlerSet.delete(handler);
|
|
317
498
|
if (handlerSet.size === 0) {
|
|
318
499
|
this.handlerMap.delete(event);
|
|
319
|
-
this.
|
|
500
|
+
if (this.isSSE()) {
|
|
501
|
+
this.detachSSEListener(event);
|
|
502
|
+
} else {
|
|
503
|
+
this.detachSocketListener(event);
|
|
504
|
+
}
|
|
320
505
|
}
|
|
321
506
|
}
|
|
322
507
|
async subscribeDoc(doctype, name) {
|
|
@@ -324,14 +509,15 @@ class RealtimeModule {
|
|
|
324
509
|
doctype,
|
|
325
510
|
name
|
|
326
511
|
});
|
|
512
|
+
if (this.isSSE()) this.logSSERoomNoticeOnce();
|
|
327
513
|
await this.ensureConnected();
|
|
328
|
-
if (this.socket && this.socketHealthy) {
|
|
514
|
+
if (!this.isSSE() && this.socket && this.socketHealthy) {
|
|
329
515
|
this.socket.emit("doc_subscribe", doctype, name);
|
|
330
516
|
}
|
|
331
517
|
}
|
|
332
518
|
unsubscribeDoc(doctype, name) {
|
|
333
519
|
this.docRooms.delete(docRoomKey(doctype, name));
|
|
334
|
-
if (this.socket && this.socketHealthy) {
|
|
520
|
+
if (!this.isSSE() && this.socket && this.socketHealthy) {
|
|
335
521
|
this.socket.emit("doc_unsubscribe", doctype, name);
|
|
336
522
|
}
|
|
337
523
|
this.teardownWhenIdle();
|
|
@@ -348,14 +534,15 @@ class RealtimeModule {
|
|
|
348
534
|
doctype,
|
|
349
535
|
filters
|
|
350
536
|
});
|
|
537
|
+
if (this.isSSE()) this.logSSERoomNoticeOnce();
|
|
351
538
|
await this.ensureConnected();
|
|
352
|
-
if (this.socket && this.socketHealthy) {
|
|
539
|
+
if (!this.isSSE() && this.socket && this.socketHealthy) {
|
|
353
540
|
this.socket.emit("doctype_subscribe", doctype);
|
|
354
541
|
}
|
|
355
542
|
}
|
|
356
543
|
unsubscribeList(doctype) {
|
|
357
544
|
this.listRooms.delete(doctype);
|
|
358
|
-
if (this.socket && this.socketHealthy) {
|
|
545
|
+
if (!this.isSSE() && this.socket && this.socketHealthy) {
|
|
359
546
|
this.socket.emit("doctype_unsubscribe", doctype);
|
|
360
547
|
}
|
|
361
548
|
this.teardownWhenIdle();
|
|
@@ -371,13 +558,14 @@ class RealtimeModule {
|
|
|
371
558
|
*/
|
|
372
559
|
teardownWhenIdle() {
|
|
373
560
|
if (this.docRooms.size > 0 || this.listRooms.size > 0) return;
|
|
374
|
-
if (!this.socket && !this.isConnecting) return;
|
|
561
|
+
if (!this.socket && !this.eventSource && !this.isConnecting) return;
|
|
375
562
|
this.emitToHandlers("disconnect", {
|
|
376
563
|
reason: "idle-teardown"
|
|
377
564
|
});
|
|
378
565
|
this.softDisconnect();
|
|
379
566
|
}
|
|
380
567
|
isHealthy() {
|
|
568
|
+
if (this.isSSE()) return this.socketHealthy && this.eventSource !== void 0;
|
|
381
569
|
return this.socketHealthy && this.socket?.connected === true;
|
|
382
570
|
}
|
|
383
571
|
disconnect() {
|
|
@@ -388,13 +576,20 @@ class RealtimeModule {
|
|
|
388
576
|
}
|
|
389
577
|
async updateAuth(auth) {
|
|
390
578
|
this.config.auth = auth;
|
|
391
|
-
const shouldReconnect = this.docRooms.size > 0 || this.listRooms.size > 0;
|
|
579
|
+
const shouldReconnect = this.docRooms.size > 0 || this.listRooms.size > 0 || this.isSSE() && this.eventSource !== void 0;
|
|
392
580
|
this.softDisconnect();
|
|
393
581
|
if (shouldReconnect) {
|
|
394
582
|
await this.connect();
|
|
395
583
|
}
|
|
396
584
|
}
|
|
397
585
|
emit(event, data) {
|
|
586
|
+
if (this.isSSE()) {
|
|
587
|
+
if (!this.sseEmitNoticeLogged) {
|
|
588
|
+
this.sseEmitNoticeLogged = true;
|
|
589
|
+
console.debug("[RealtimeModule] emit() is a no-op under the SSE transport \u2014 SSE is one-way (server \u2192 client). Use HTTP calls for client \u2192 server communication.");
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
398
593
|
if (this.socket && this.socketHealthy) {
|
|
399
594
|
this.socket.emit(event, data);
|
|
400
595
|
}
|
|
@@ -402,4 +597,20 @@ class RealtimeModule {
|
|
|
402
597
|
getSocket() {
|
|
403
598
|
return this.socket;
|
|
404
599
|
}
|
|
600
|
+
/** Live EventSource when the SSE transport is connected (undefined otherwise). */
|
|
601
|
+
getEventSource() {
|
|
602
|
+
return this.eventSource;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Doc rooms recorded by subscribeDoc. Under SSE these are client-side
|
|
606
|
+
* bookkeeping only (nothing is emitted upstream) — mapFrame-driven apps
|
|
607
|
+
* consult them to filter or route frames.
|
|
608
|
+
*/
|
|
609
|
+
getDocRooms() {
|
|
610
|
+
return [...this.docRooms.values()];
|
|
611
|
+
}
|
|
612
|
+
/** List rooms recorded by subscribeList — same client-side semantics as getDocRooms. */
|
|
613
|
+
getListRooms() {
|
|
614
|
+
return [...this.listRooms.values()];
|
|
615
|
+
}
|
|
405
616
|
}
|