@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.
Files changed (125) hide show
  1. package/dist/cjs/collection.cjs +25 -9
  2. package/dist/cjs/collection.native.js +25 -9
  3. package/dist/cjs/collection.native.js.map +1 -1
  4. package/dist/cjs/db.cjs +13 -2
  5. package/dist/cjs/db.native.js +18 -2
  6. package/dist/cjs/db.native.js.map +1 -1
  7. package/dist/cjs/frappeReact.cjs +10 -3
  8. package/dist/cjs/frappeReact.native.js +10 -3
  9. package/dist/cjs/frappeReact.native.js.map +1 -1
  10. package/dist/cjs/index.cjs +1 -0
  11. package/dist/cjs/index.native.js +1 -0
  12. package/dist/cjs/index.native.js.map +1 -1
  13. package/dist/cjs/realtime.cjs +222 -11
  14. package/dist/cjs/realtime.native.js +352 -14
  15. package/dist/cjs/realtime.native.js.map +1 -1
  16. package/dist/cjs/sync/index.cjs +6 -1
  17. package/dist/cjs/sync/index.native.js +6 -1
  18. package/dist/cjs/sync/index.native.js.map +1 -1
  19. package/dist/cjs/sync/mutationEngine.cjs +43 -19
  20. package/dist/cjs/sync/mutationEngine.native.js +51 -21
  21. package/dist/cjs/sync/mutationEngine.native.js.map +1 -1
  22. package/dist/cjs/sync/subscriptionManager.cjs +4 -0
  23. package/dist/cjs/sync/subscriptionManager.native.js +4 -0
  24. package/dist/cjs/sync/subscriptionManager.native.js.map +1 -1
  25. package/dist/cjs/sync/types.native.js.map +1 -1
  26. package/dist/esm/collection.mjs +24 -9
  27. package/dist/esm/collection.mjs.map +1 -1
  28. package/dist/esm/collection.native.js +24 -9
  29. package/dist/esm/collection.native.js.map +1 -1
  30. package/dist/esm/db.mjs +13 -2
  31. package/dist/esm/db.mjs.map +1 -1
  32. package/dist/esm/db.native.js +18 -2
  33. package/dist/esm/db.native.js.map +1 -1
  34. package/dist/esm/frappeReact.mjs +11 -4
  35. package/dist/esm/frappeReact.mjs.map +1 -1
  36. package/dist/esm/frappeReact.native.js +11 -4
  37. package/dist/esm/frappeReact.native.js.map +1 -1
  38. package/dist/esm/index.mjs +2 -2
  39. package/dist/esm/index.mjs.map +1 -1
  40. package/dist/esm/index.native.js +2 -2
  41. package/dist/esm/index.native.js.map +1 -1
  42. package/dist/esm/realtime.mjs +222 -11
  43. package/dist/esm/realtime.mjs.map +1 -1
  44. package/dist/esm/realtime.native.js +352 -14
  45. package/dist/esm/realtime.native.js.map +1 -1
  46. package/dist/esm/sync/index.mjs +6 -1
  47. package/dist/esm/sync/index.mjs.map +1 -1
  48. package/dist/esm/sync/index.native.js +6 -1
  49. package/dist/esm/sync/index.native.js.map +1 -1
  50. package/dist/esm/sync/mutationEngine.mjs +43 -19
  51. package/dist/esm/sync/mutationEngine.mjs.map +1 -1
  52. package/dist/esm/sync/mutationEngine.native.js +51 -21
  53. package/dist/esm/sync/mutationEngine.native.js.map +1 -1
  54. package/dist/esm/sync/subscriptionManager.mjs +4 -0
  55. package/dist/esm/sync/subscriptionManager.mjs.map +1 -1
  56. package/dist/esm/sync/subscriptionManager.native.js +4 -0
  57. package/dist/esm/sync/subscriptionManager.native.js.map +1 -1
  58. package/dist/esm/sync/types.mjs.map +1 -1
  59. package/dist/esm/sync/types.native.js.map +1 -1
  60. package/dist/jsx/collection.mjs +24 -9
  61. package/dist/jsx/collection.mjs.map +1 -1
  62. package/dist/jsx/collection.native.js +25 -9
  63. package/dist/jsx/collection.native.js.map +1 -1
  64. package/dist/jsx/db.mjs +13 -2
  65. package/dist/jsx/db.mjs.map +1 -1
  66. package/dist/jsx/db.native.js +18 -2
  67. package/dist/jsx/db.native.js.map +1 -1
  68. package/dist/jsx/frappeReact.mjs +11 -4
  69. package/dist/jsx/frappeReact.mjs.map +1 -1
  70. package/dist/jsx/frappeReact.native.js +10 -3
  71. package/dist/jsx/frappeReact.native.js.map +1 -1
  72. package/dist/jsx/index.js +2 -2
  73. package/dist/jsx/index.js.map +1 -1
  74. package/dist/jsx/index.mjs +2 -2
  75. package/dist/jsx/index.mjs.map +1 -1
  76. package/dist/jsx/index.native.js +1 -0
  77. package/dist/jsx/index.native.js.map +1 -1
  78. package/dist/jsx/realtime.mjs +222 -11
  79. package/dist/jsx/realtime.mjs.map +1 -1
  80. package/dist/jsx/realtime.native.js +352 -14
  81. package/dist/jsx/realtime.native.js.map +1 -1
  82. package/dist/jsx/sync/index.mjs +6 -1
  83. package/dist/jsx/sync/index.mjs.map +1 -1
  84. package/dist/jsx/sync/index.native.js +6 -1
  85. package/dist/jsx/sync/index.native.js.map +1 -1
  86. package/dist/jsx/sync/mutationEngine.mjs +43 -19
  87. package/dist/jsx/sync/mutationEngine.mjs.map +1 -1
  88. package/dist/jsx/sync/mutationEngine.native.js +51 -21
  89. package/dist/jsx/sync/mutationEngine.native.js.map +1 -1
  90. package/dist/jsx/sync/subscriptionManager.mjs +4 -0
  91. package/dist/jsx/sync/subscriptionManager.mjs.map +1 -1
  92. package/dist/jsx/sync/subscriptionManager.native.js +4 -0
  93. package/dist/jsx/sync/subscriptionManager.native.js.map +1 -1
  94. package/dist/jsx/sync/types.mjs.map +1 -1
  95. package/dist/jsx/sync/types.native.js.map +1 -1
  96. package/package.json +7 -7
  97. package/src/collection.ts +53 -8
  98. package/src/db.spec.ts +70 -0
  99. package/src/db.ts +28 -2
  100. package/src/frappeReact.spec.ts +7 -1
  101. package/src/frappeReact.ts +30 -6
  102. package/src/frappeReactLiveQueryGc.spec.ts +44 -4
  103. package/src/index.ts +13 -1
  104. package/src/realtime.ts +406 -14
  105. package/src/realtimeSse.spec.ts +547 -0
  106. package/src/sync/index.ts +10 -1
  107. package/src/sync/mutationEngine.ts +81 -20
  108. package/src/sync/optimisticCreateReconcile.spec.ts +372 -0
  109. package/src/sync/subscriptionManager.spec.ts +41 -0
  110. package/src/sync/subscriptionManager.ts +10 -0
  111. package/src/sync/subscriptions.spec.ts +4 -0
  112. package/src/sync/types.ts +9 -0
  113. package/types/collection.d.ts +12 -0
  114. package/types/collection.d.ts.map +1 -1
  115. package/types/db.d.ts.map +1 -1
  116. package/types/frappeReact.d.ts.map +1 -1
  117. package/types/index.d.ts +2 -1
  118. package/types/index.d.ts.map +1 -1
  119. package/types/realtime.d.ts +166 -1
  120. package/types/realtime.d.ts.map +1 -1
  121. package/types/sync/index.d.ts.map +1 -1
  122. package/types/sync/mutationEngine.d.ts.map +1 -1
  123. package/types/sync/subscriptionManager.d.ts.map +1 -1
  124. package/types/sync/types.d.ts +9 -0
  125. package/types/sync/types.d.ts.map +1 -1
@@ -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.socketHealthy && this.socket?.connected) return Promise.resolve(true);
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.socketHealthy && this.socket?.connected) return;
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.attachSocketListener(event);
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.detachSocketListener(event);
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
  }