@multiplatform.one/frappe 6.7.0 → 7.1.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 (153) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/core/http.cjs +45 -1
  3. package/dist/cjs/core/http.native.js +72 -1
  4. package/dist/cjs/core/http.native.js.map +1 -1
  5. package/dist/cjs/doctype.cjs +15 -2
  6. package/dist/cjs/doctype.native.js +22 -5
  7. package/dist/cjs/doctype.native.js.map +1 -1
  8. package/dist/cjs/frappeReact.cjs +55 -1
  9. package/dist/cjs/frappeReact.native.js +66 -1
  10. package/dist/cjs/frappeReact.native.js.map +1 -1
  11. package/dist/cjs/index.cjs +7 -0
  12. package/dist/cjs/index.native.js +7 -0
  13. package/dist/cjs/index.native.js.map +1 -1
  14. package/dist/cjs/optimistic.cjs +32 -2
  15. package/dist/cjs/optimistic.native.js +34 -1
  16. package/dist/cjs/optimistic.native.js.map +1 -1
  17. package/dist/cjs/pacer.native.js.map +1 -1
  18. package/dist/cjs/realtime.cjs +12 -0
  19. package/dist/cjs/realtime.native.js +16 -4
  20. package/dist/cjs/realtime.native.js.map +1 -1
  21. package/dist/cjs/sync/index.cjs +45 -1
  22. package/dist/cjs/sync/index.native.js +71 -1
  23. package/dist/cjs/sync/index.native.js.map +1 -1
  24. package/dist/cjs/sync/mutationEngine.cjs +51 -15
  25. package/dist/cjs/sync/mutationEngine.native.js +69 -16
  26. package/dist/cjs/sync/mutationEngine.native.js.map +1 -1
  27. package/dist/cjs/sync/mutationJournal.cjs +8 -0
  28. package/dist/cjs/sync/mutationJournal.native.js +10 -0
  29. package/dist/cjs/sync/mutationJournal.native.js.map +1 -1
  30. package/dist/cjs/sync/types.native.js.map +1 -1
  31. package/dist/esm/core/http.mjs +45 -2
  32. package/dist/esm/core/http.mjs.map +1 -1
  33. package/dist/esm/core/http.native.js +72 -2
  34. package/dist/esm/core/http.native.js.map +1 -1
  35. package/dist/esm/doctype.mjs +15 -2
  36. package/dist/esm/doctype.mjs.map +1 -1
  37. package/dist/esm/doctype.native.js +22 -5
  38. package/dist/esm/doctype.native.js.map +1 -1
  39. package/dist/esm/frappeReact.mjs +55 -3
  40. package/dist/esm/frappeReact.mjs.map +1 -1
  41. package/dist/esm/frappeReact.native.js +66 -3
  42. package/dist/esm/frappeReact.native.js.map +1 -1
  43. package/dist/esm/index.mjs +4 -4
  44. package/dist/esm/index.mjs.map +1 -1
  45. package/dist/esm/index.native.js +4 -4
  46. package/dist/esm/index.native.js.map +1 -1
  47. package/dist/esm/optimistic.mjs +27 -1
  48. package/dist/esm/optimistic.mjs.map +1 -1
  49. package/dist/esm/optimistic.native.js +30 -1
  50. package/dist/esm/optimistic.native.js.map +1 -1
  51. package/dist/esm/pacer.mjs.map +1 -1
  52. package/dist/esm/pacer.native.js.map +1 -1
  53. package/dist/esm/realtime.mjs +12 -0
  54. package/dist/esm/realtime.mjs.map +1 -1
  55. package/dist/esm/realtime.native.js +16 -4
  56. package/dist/esm/realtime.native.js.map +1 -1
  57. package/dist/esm/sync/index.mjs +45 -1
  58. package/dist/esm/sync/index.mjs.map +1 -1
  59. package/dist/esm/sync/index.native.js +71 -1
  60. package/dist/esm/sync/index.native.js.map +1 -1
  61. package/dist/esm/sync/mutationEngine.mjs +51 -15
  62. package/dist/esm/sync/mutationEngine.mjs.map +1 -1
  63. package/dist/esm/sync/mutationEngine.native.js +69 -16
  64. package/dist/esm/sync/mutationEngine.native.js.map +1 -1
  65. package/dist/esm/sync/mutationJournal.mjs +8 -0
  66. package/dist/esm/sync/mutationJournal.mjs.map +1 -1
  67. package/dist/esm/sync/mutationJournal.native.js +10 -0
  68. package/dist/esm/sync/mutationJournal.native.js.map +1 -1
  69. package/dist/esm/sync/types.mjs.map +1 -1
  70. package/dist/esm/sync/types.native.js.map +1 -1
  71. package/dist/jsx/core/http.mjs +45 -2
  72. package/dist/jsx/core/http.mjs.map +1 -1
  73. package/dist/jsx/core/http.native.js +72 -1
  74. package/dist/jsx/core/http.native.js.map +1 -1
  75. package/dist/jsx/doctype.mjs +15 -2
  76. package/dist/jsx/doctype.mjs.map +1 -1
  77. package/dist/jsx/doctype.native.js +22 -5
  78. package/dist/jsx/doctype.native.js.map +1 -1
  79. package/dist/jsx/frappeReact.mjs +55 -3
  80. package/dist/jsx/frappeReact.mjs.map +1 -1
  81. package/dist/jsx/frappeReact.native.js +66 -1
  82. package/dist/jsx/frappeReact.native.js.map +1 -1
  83. package/dist/jsx/index.js +4 -4
  84. package/dist/jsx/index.js.map +1 -1
  85. package/dist/jsx/index.mjs +4 -4
  86. package/dist/jsx/index.mjs.map +1 -1
  87. package/dist/jsx/index.native.js +7 -0
  88. package/dist/jsx/index.native.js.map +1 -1
  89. package/dist/jsx/optimistic.mjs +27 -1
  90. package/dist/jsx/optimistic.mjs.map +1 -1
  91. package/dist/jsx/optimistic.native.js +34 -1
  92. package/dist/jsx/optimistic.native.js.map +1 -1
  93. package/dist/jsx/pacer.mjs.map +1 -1
  94. package/dist/jsx/pacer.native.js.map +1 -1
  95. package/dist/jsx/realtime.mjs +12 -0
  96. package/dist/jsx/realtime.mjs.map +1 -1
  97. package/dist/jsx/realtime.native.js +16 -4
  98. package/dist/jsx/realtime.native.js.map +1 -1
  99. package/dist/jsx/sync/index.mjs +45 -1
  100. package/dist/jsx/sync/index.mjs.map +1 -1
  101. package/dist/jsx/sync/index.native.js +71 -1
  102. package/dist/jsx/sync/index.native.js.map +1 -1
  103. package/dist/jsx/sync/mutationEngine.mjs +51 -15
  104. package/dist/jsx/sync/mutationEngine.mjs.map +1 -1
  105. package/dist/jsx/sync/mutationEngine.native.js +69 -16
  106. package/dist/jsx/sync/mutationEngine.native.js.map +1 -1
  107. package/dist/jsx/sync/mutationJournal.mjs +8 -0
  108. package/dist/jsx/sync/mutationJournal.mjs.map +1 -1
  109. package/dist/jsx/sync/mutationJournal.native.js +10 -0
  110. package/dist/jsx/sync/mutationJournal.native.js.map +1 -1
  111. package/dist/jsx/sync/types.mjs.map +1 -1
  112. package/dist/jsx/sync/types.native.js.map +1 -1
  113. package/package.json +7 -7
  114. package/src/core/http.spec.ts +81 -0
  115. package/src/core/http.ts +80 -3
  116. package/src/doctype.spec.ts +82 -3
  117. package/src/doctype.ts +51 -7
  118. package/src/frappeReact.ts +126 -4
  119. package/src/index.ts +15 -2
  120. package/src/optimistic.spec.ts +113 -2
  121. package/src/optimistic.ts +95 -0
  122. package/src/pacer.ts +8 -0
  123. package/src/realtime.spec.ts +62 -0
  124. package/src/realtime.ts +34 -0
  125. package/src/sync/index.ts +44 -1
  126. package/src/sync/mutationEngine.spec.ts +7 -2
  127. package/src/sync/mutationEngine.ts +83 -27
  128. package/src/sync/mutationJournal.ts +9 -0
  129. package/src/sync/mutations.spec.ts +1 -1
  130. package/src/sync/syncModule.spec.ts +255 -1
  131. package/src/sync/types.ts +20 -0
  132. package/types/core/http.d.ts +11 -0
  133. package/types/core/http.d.ts.map +1 -1
  134. package/types/doctype.d.ts +7 -0
  135. package/types/doctype.d.ts.map +1 -1
  136. package/types/frappeReact.d.ts +50 -2
  137. package/types/frappeReact.d.ts.map +1 -1
  138. package/types/index.d.ts +6 -5
  139. package/types/index.d.ts.map +1 -1
  140. package/types/optimistic.d.ts +38 -0
  141. package/types/optimistic.d.ts.map +1 -1
  142. package/types/pacer.d.ts +8 -0
  143. package/types/pacer.d.ts.map +1 -1
  144. package/types/realtime.d.ts +17 -0
  145. package/types/realtime.d.ts.map +1 -1
  146. package/types/sync/index.d.ts +16 -1
  147. package/types/sync/index.d.ts.map +1 -1
  148. package/types/sync/mutationEngine.d.ts +7 -0
  149. package/types/sync/mutationEngine.d.ts.map +1 -1
  150. package/types/sync/mutationJournal.d.ts +6 -0
  151. package/types/sync/mutationJournal.d.ts.map +1 -1
  152. package/types/sync/types.d.ts +20 -0
  153. package/types/sync/types.d.ts.map +1 -1
package/src/sync/index.ts CHANGED
@@ -130,7 +130,14 @@ export class SyncModule {
130
130
  this.mutationJournal,
131
131
  this.subscriptionManager,
132
132
  this.rehydrationManager,
133
- this.options,
133
+ {
134
+ ...this.options,
135
+ // Bridge engine-internal optimistic applies to store listeners so
136
+ // pending state paints BEFORE the server round trip. Without this,
137
+ // subscribeStore consumers only re-read after the ack (mutate()'s
138
+ // trailing emit) and the in-memory optimism was invisible to them.
139
+ onStoreChange: () => this.emit(),
140
+ },
134
141
  );
135
142
 
136
143
  // Setup realtime handlers with callbacks to main module
@@ -257,6 +264,42 @@ export class SyncModule {
257
264
  this.emit();
258
265
  }
259
266
 
267
+ /**
268
+ * Public read of the mutation ledger: journal entries still awaiting the
269
+ * server (`Status.Pending`) or rejected by it (`Status.Failed`), ordered
270
+ * by submission sequence. Confirmed entries are dropped from the journal
271
+ * and never appear here. Read inside a `subscribeStore` callback for a
272
+ * reactive pending/error surface (see `useFrappePendingMutations`).
273
+ */
274
+ getMutationEntries(doctype?: string): DebugMutationEntry[] {
275
+ const entries: DebugMutationEntry[] = [];
276
+ for (const { id, entry } of this.mutationJournal.listPending()) {
277
+ if (doctype && entry.doctype !== doctype) continue;
278
+ entries.push({
279
+ id,
280
+ operation: entry.operation,
281
+ doctype: entry.doctype,
282
+ docname: entry.docname,
283
+ status: entry.status,
284
+ createdAt: entry.createdAt ?? 0,
285
+ retryCount: entry.retryCount ?? 0,
286
+ error: entry.error ? ((entry.error as Error).message ?? String(entry.error)) : undefined,
287
+ });
288
+ }
289
+ return entries;
290
+ }
291
+
292
+ /**
293
+ * Drops a failed (or still-pending) ledger entry without retrying it —
294
+ * the "dismiss" half of the rejected-write contract. The optimistic
295
+ * rollback already ran when the mutation failed; this only clears the
296
+ * error affordance.
297
+ */
298
+ dismissMutation(id: string) {
299
+ this.mutationJournal.remove(id);
300
+ this.emit();
301
+ }
302
+
260
303
  /** Reads a single document from the local cache. */
261
304
  getDocFromCache<T extends FrappeDocData = FrappeDocData>(
262
305
  doctype: string,
@@ -671,7 +671,9 @@ describe("MutationEngine", () => {
671
671
  };
672
672
 
673
673
  await expect(mutationEngine.mutate(mutation)).rejects.toThrow("Server error");
674
- expect(mockMutationJournal.fail).toHaveBeenCalledWith("mut16");
674
+ expect(mockMutationJournal.fail).toHaveBeenCalledWith("mut16", {
675
+ message: "Server error",
676
+ });
675
677
  });
676
678
 
677
679
  it("should handle conflict errors (409)", async () => {
@@ -933,7 +935,10 @@ describe("MutationEngine", () => {
933
935
 
934
936
  await mutationEngine.replayPending();
935
937
 
936
- expect(mockMutationJournal.fail).toHaveBeenCalledWith("mut26");
938
+ expect(mockMutationJournal.fail).toHaveBeenCalledWith(
939
+ "mut26",
940
+ expect.objectContaining({ message: expect.any(String) }),
941
+ );
937
942
  });
938
943
 
939
944
  it("should call replay callbacks", async () => {
@@ -28,6 +28,18 @@ import type {
28
28
  UpdateMutation,
29
29
  } from "./types";
30
30
 
31
+ /**
32
+ * Normalizes a rejection into the plain `{ message }` shape the journal
33
+ * stores — plain because journal entries are persisted and Error instances
34
+ * JSON-serialize to `{}`, which would strip the message the rejected-write
35
+ * UI (SyncErrorNotice) needs to show.
36
+ */
37
+ function toJournalError(err: unknown): { message: string } {
38
+ if (err instanceof Error) return { message: err.message };
39
+ const message = (err as { message?: string })?.message;
40
+ return { message: message ?? String(err) };
41
+ }
42
+
31
43
  /** Pending batch mutation entry */
32
44
  interface PendingBatchMutation<Data extends FrappeDocData> {
33
45
  mutation: Mutation<Data>;
@@ -174,14 +186,14 @@ export class MutationEngine {
174
186
  } else {
175
187
  // Handle error
176
188
  const error = new Error(result.error || "Batch mutation failed");
177
- this.mutationJournal.fail(mutation.mutationId);
189
+ this.mutationJournal.fail(mutation.mutationId, toJournalError(error));
178
190
  reject(error);
179
191
  }
180
192
  }
181
193
  } catch (err) {
182
194
  // Batch request failed - reject all
183
195
  for (const { mutation, reject } of batch) {
184
- this.mutationJournal.fail(mutation.mutationId);
196
+ this.mutationJournal.fail(mutation.mutationId, toJournalError(err));
185
197
  reject(err);
186
198
  }
187
199
  }
@@ -220,9 +232,16 @@ export class MutationEngine {
220
232
  }
221
233
  if (mutation.operation === Operation.Update && doc.name) {
222
234
  await this.handleOptimisticUpdate(mutation as UpdateMutation<FrappeDocData>, cachedDoc);
235
+ // Optimistic state must PAINT before the server round trip: without
236
+ // this bridge, store-subscription consumers (subscribeStore /
237
+ // useFrappeInfiniteList) only re-read after the ack, so the engine's
238
+ // optimism was invisible to them (TanStack collections masked it with
239
+ // their own overlay).
240
+ this.options.onStoreChange?.();
223
241
  }
224
242
  if (mutation.operation === Operation.Create) {
225
243
  await this.handleOptimisticCreate(mutation as CreateMutation<FrappeDocData>);
244
+ this.options.onStoreChange?.();
226
245
  }
227
246
  if (
228
247
  version === undefined &&
@@ -268,8 +287,19 @@ export class MutationEngine {
268
287
  snapshot: doc.name ? cachedDoc : undefined,
269
288
  });
270
289
 
271
- // Use batching if enabled
272
- if (this.options.enableBatching) {
290
+ // Optimistic delete: evict now, in BOTH the batched and non-batched
291
+ // paths (it used to live inside handleServerDelete, so batched deletes
292
+ // never painted optimistically). Placed after the journal add so the
293
+ // rollback snapshot was captured, and after the version fetch so that
294
+ // fetch's cache upsert cannot resurrect the row we just evicted.
295
+ if (mutation.operation === Operation.Delete && doc.name) {
296
+ this.applyOptimisticDelete(doc.doctype, doc.name);
297
+ this.options.onStoreChange?.();
298
+ }
299
+
300
+ // Use batching if enabled. Custom-transport mutations can't ride the
301
+ // batch RPC — their whole point is a different server endpoint.
302
+ if (this.options.enableBatching && !mutation.execute) {
273
303
  try {
274
304
  await this.queueForBatch(mutation);
275
305
  // Schedule rehydration for creates
@@ -394,10 +424,14 @@ export class MutationEngine {
394
424
  // Idempotency-Key rides the first attempt (not just replays) so an
395
425
  // HTTP-level retry after a lost response replays the recorded outcome
396
426
  // instead of double-inserting (live-protocol FRAPPE-EXT-001).
397
- const created = await this.db.createDoc<FrappeDoc<Data>>({
398
- ...mutation.doc,
399
- client_mutation_id: mutation.mutationId,
400
- });
427
+ // A custom transport (mutation.execute) replaces only this server call;
428
+ // the ack reconcile below is identical.
429
+ const created = mutation.execute
430
+ ? await mutation.execute()
431
+ : await this.db.createDoc<FrappeDoc<Data>>({
432
+ ...mutation.doc,
433
+ client_mutation_id: mutation.mutationId,
434
+ });
401
435
  // Authoritative name assigned by the backend (may equal the provided temp name)
402
436
  const realName = created?.name;
403
437
  // The client-synthesized or user-provided temporary name used during optimism
@@ -430,31 +464,53 @@ export class MutationEngine {
430
464
  mutation: UpdateMutation<Data>,
431
465
  _version: string | undefined,
432
466
  ) {
467
+ if (mutation.execute) {
468
+ // Custom transport: seed the cache when it returns the updated doc,
469
+ // otherwise the optimistic value stands until the realtime echo.
470
+ const updated = await mutation.execute();
471
+ if (updated?.name) {
472
+ this.normalizedCache.upsert({
473
+ ...updated,
474
+ doctype: updated.doctype || mutation.doc.doctype,
475
+ });
476
+ }
477
+ return;
478
+ }
433
479
  // Send partial update with idempotency and OCC token
434
480
  await this.db.updateDoc(mutation.doc);
435
481
  }
436
482
 
483
+ /**
484
+ * Applies the local side of a delete: cache eviction, subscription
485
+ * membership removal, and gap-filling rehydration. Runs before the server
486
+ * call (mutate() body) so the row disappears immediately; the journal
487
+ * snapshot restores it if the server rejects.
488
+ */
489
+ private applyOptimisticDelete(doctype: string, name: string) {
490
+ // Rehydration targets must be resolved BEFORE membership removal —
491
+ // removeFromAllSubscriptions strips the id the includes() check needs.
492
+ const affected = [...this.subscriptionManager.getAllSubscriptions().values()].filter(
493
+ (subscription) =>
494
+ subscription.query.doctype === doctype && subscription.docIds.includes(name),
495
+ );
496
+ this.normalizedCache.remove({ doctype, name });
497
+ this.subscriptionManager.removeFromAllSubscriptions(doctype, name);
498
+ // Schedule full refresh for subscriptions to pull in items from next page
499
+ for (const subscription of affected) {
500
+ this.rehydrationManager.scheduleRehydration(subscription, RehydrationReason.Delete);
501
+ }
502
+ }
503
+
437
504
  private async handleServerDelete<Data extends FrappeDocData>(
438
505
  mutation: DeleteMutation<Data>,
439
506
  _version: string | undefined,
440
507
  ) {
441
- // Optimistic delete: evict now; REST confirms or we rollback in catch.
442
- this.normalizedCache.remove({
443
- doctype: mutation.doc.doctype,
444
- name: mutation.doc.name,
445
- });
446
- this.subscriptionManager.removeFromAllSubscriptions(mutation.doc.doctype, mutation.doc.name);
447
-
448
- // Schedule full refresh for subscriptions to pull in items from next page
449
- for (const subscription of this.subscriptionManager.getAllSubscriptions().values()) {
450
- if (
451
- subscription.query.doctype === mutation.doc.doctype &&
452
- subscription.docIds.includes(mutation.doc.name)
453
- ) {
454
- this.rehydrationManager.scheduleRehydration(subscription, RehydrationReason.Delete);
455
- }
508
+ // Local eviction already happened in mutate() (shared with the batched
509
+ // path); this is only the idempotent server confirmation.
510
+ if (mutation.execute) {
511
+ await mutation.execute();
512
+ return;
456
513
  }
457
- // Idempotent delete with mutation id
458
514
  await this.db.deleteDoc(mutation.doc);
459
515
  }
460
516
 
@@ -511,7 +567,7 @@ export class MutationEngine {
511
567
  ) {
512
568
  if (this.options.onMutationError) this.options.onMutationError({ mutation, err });
513
569
  // Failure path: mark failed and rollback optimistic state where possible.
514
- this.mutationJournal.fail(mutation.mutationId);
570
+ this.mutationJournal.fail(mutation.mutationId, toJournalError(err));
515
571
  const entry = this.mutationJournal.get(mutation.mutationId);
516
572
 
517
573
  // Skip rollback when offline — mutation may be retried when back online
@@ -891,11 +947,11 @@ export class MutationEngine {
891
947
  // Don't mark as failed since this might resolve later
892
948
  } else if (errorClass === "permanent") {
893
949
  // Permanent validation error - remove from journal
894
- this.mutationJournal.fail(id);
950
+ this.mutationJournal.fail(id, toJournalError(err));
895
951
  this.notifyMutationError(id, entry, err);
896
952
  } else if (errorClass === "failed") {
897
953
  // Retries exhausted - keep in journal but notify
898
- this.mutationJournal.fail(id);
954
+ this.mutationJournal.fail(id, toJournalError(err));
899
955
  this.notifyMutationError(id, entry, err);
900
956
  }
901
957
  }
@@ -85,6 +85,15 @@ export class MutationJournal {
85
85
  this.pending.set(id, entry);
86
86
  }
87
87
 
88
+ /**
89
+ * Drops an entry without confirming it — the "dismiss" affordance for a
90
+ * failed mutation the user chose not to retry. Unlike `confirm` this makes
91
+ * no claim the write reached the server.
92
+ */
93
+ remove(id: string) {
94
+ this.pending.delete(id);
95
+ }
96
+
88
97
  listPending() {
89
98
  const pending: Array<{ id: string; entry: Pending }> = [];
90
99
  for (const [id, entry] of this.pending) {
@@ -308,7 +308,7 @@ describe("MutationEngine", () => {
308
308
  vi.mocked(mockJournal.listPending).mockReturnValue([pending]);
309
309
  vi.mocked(mockDb.updateDoc).mockRejectedValue(error);
310
310
  await mutationEngine.replayPending();
311
- expect(mockJournal.fail).toHaveBeenCalledWith("mut-1");
311
+ expect(mockJournal.fail).toHaveBeenCalledWith("mut-1", { message: "Network error" });
312
312
  });
313
313
  });
314
314
 
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { SyncModule } from "./index";
3
- import { BackfillScope } from "./types";
3
+ import { BackfillScope, Operation } from "./types";
4
4
  import type { SubscriptionQuery } from "./types";
5
5
 
6
6
  vi.mock("@multiplatform.one/platform", () => ({
@@ -351,4 +351,258 @@ describe("SyncModule", () => {
351
351
  expect(listener).toHaveBeenCalled();
352
352
  });
353
353
  });
354
+
355
+ // The optimistic paint contract: store listeners MUST be notified when the
356
+ // optimistic state is applied — strictly before the server ack resolves —
357
+ // otherwise subscribeStore consumers only repaint after the round trip and
358
+ // the in-memory store's whole reason to exist is defeated.
359
+ describe("optimistic emit timing", () => {
360
+ /** Flushes chained microtasks without advancing fake timers. */
361
+ async function flushMicrotasks(rounds = 10) {
362
+ for (let i = 0; i < rounds; i++) await Promise.resolve();
363
+ }
364
+
365
+ it("notifies store listeners on optimistic create BEFORE the server ack", async () => {
366
+ let resolveCreate: (doc: unknown) => void = () => {};
367
+ mockDb.createDoc = vi.fn(
368
+ () =>
369
+ new Promise((resolve) => {
370
+ resolveCreate = resolve;
371
+ }),
372
+ );
373
+ sync = createSync();
374
+ const listener = vi.fn();
375
+ sync.subscribeStore(listener);
376
+
377
+ const mutatePromise = sync.mutate({
378
+ mutationId: "m-create",
379
+ operation: Operation.Create,
380
+ doc: { doctype: "TestDoc", title: "optimistic" } as any,
381
+ });
382
+ await flushMicrotasks();
383
+
384
+ // Server still hasn't answered…
385
+ expect(mockDb.createDoc).toHaveBeenCalled();
386
+ // …but the optimistic apply already painted.
387
+ expect(listener).toHaveBeenCalled();
388
+
389
+ resolveCreate({ name: "SRV-1", doctype: "TestDoc", title: "optimistic" });
390
+ await mutatePromise;
391
+ });
392
+
393
+ it("notifies store listeners on optimistic update BEFORE the server ack", async () => {
394
+ let resolveUpdate: (doc: unknown) => void = () => {};
395
+ mockDb.updateDoc = vi.fn(
396
+ () =>
397
+ new Promise((resolve) => {
398
+ resolveUpdate = resolve;
399
+ }),
400
+ );
401
+ sync = createSync();
402
+ const { subscriptionId } = await sync.subscribe({ doctype: "TestDoc", fields: ["title"] });
403
+
404
+ const listener = vi.fn();
405
+ sync.subscribeStore(listener);
406
+
407
+ const mutatePromise = sync.mutate({
408
+ mutationId: "m-update",
409
+ operation: Operation.Update,
410
+ doc: { doctype: "TestDoc", name: "doc1", title: "edited" } as any,
411
+ });
412
+ await flushMicrotasks();
413
+
414
+ expect(listener).toHaveBeenCalled();
415
+ // The optimistic value is readable from the store during the round trip.
416
+ const rows = sync.getDocsForSubscription<{ name: string; doctype: string; title?: string }>(subscriptionId);
417
+ expect(rows.find((d) => d.name === "doc1")?.title).toBe("edited");
418
+
419
+ resolveUpdate({ name: "doc1", doctype: "TestDoc", title: "edited" });
420
+ await mutatePromise;
421
+ });
422
+
423
+ it("evicts optimistically on delete BEFORE the server ack (and notifies)", async () => {
424
+ let resolveDelete: (v: unknown) => void = () => {};
425
+ mockDb.deleteDoc = vi.fn(
426
+ () =>
427
+ new Promise((resolve) => {
428
+ resolveDelete = resolve;
429
+ }),
430
+ );
431
+ sync = createSync();
432
+ const { subscriptionId } = await sync.subscribe({ doctype: "TestDoc", fields: ["title"] });
433
+ expect(sync.getDocsForSubscription(subscriptionId).map((d) => d.name)).toContain("doc1");
434
+
435
+ const listener = vi.fn();
436
+ sync.subscribeStore(listener);
437
+
438
+ const mutatePromise = sync.mutate({
439
+ mutationId: "m-delete",
440
+ operation: Operation.Delete,
441
+ doc: { doctype: "TestDoc", name: "doc1" } as any,
442
+ });
443
+ await flushMicrotasks();
444
+
445
+ expect(listener).toHaveBeenCalled();
446
+ expect(sync.getDocsForSubscription(subscriptionId).map((d) => d.name)).not.toContain("doc1");
447
+
448
+ resolveDelete({});
449
+ await mutatePromise;
450
+ });
451
+ });
452
+
453
+ // Custom server transport: mutation.execute replaces ONLY the server call
454
+ // (for desk RPCs a REST insert can't reproduce, e.g. add_comment) — the
455
+ // optimistic apply, temp→real remap, rollback, and ledger are unchanged.
456
+ describe("custom transport (mutation.execute)", () => {
457
+ async function flushMicrotasks(rounds = 10) {
458
+ for (let i = 0; i < rounds; i++) await Promise.resolve();
459
+ }
460
+
461
+ it("paints optimistically before the transport ack and remaps on it", async () => {
462
+ let resolveExecute: (doc: unknown) => void = () => {};
463
+ const execute = vi.fn(
464
+ () =>
465
+ new Promise<any>((resolve) => {
466
+ resolveExecute = resolve;
467
+ }),
468
+ );
469
+ sync = createSync();
470
+ const { subscriptionId } = await sync.subscribe({ doctype: "TestDoc", fields: ["title"] });
471
+
472
+ const mutatePromise = sync.mutate({
473
+ mutationId: "m-transport",
474
+ operation: Operation.Create,
475
+ doc: { doctype: "TestDoc", name: "local:1", title: "via rpc" } as any,
476
+ execute,
477
+ });
478
+ await flushMicrotasks();
479
+
480
+ // Optimistic row visible under its local identity, ack still pending.
481
+ expect(sync.getDocsForSubscription(subscriptionId).map((d) => d.name)).toContain("local:1");
482
+ expect(execute).toHaveBeenCalled();
483
+ expect(mockDb.createDoc).not.toHaveBeenCalled();
484
+
485
+ resolveExecute({
486
+ name: "SRV-9",
487
+ doctype: "TestDoc",
488
+ title: "via rpc",
489
+ modified: "2024-01-01T12:00:00",
490
+ });
491
+ await mutatePromise;
492
+
493
+ // Temp identity remapped to the server-assigned name — exactly once.
494
+ const names = sync.getDocsForSubscription(subscriptionId).map((d) => d.name);
495
+ expect(names).toContain("SRV-9");
496
+ expect(names).not.toContain("local:1");
497
+ expect(names.filter((n) => n === "SRV-9")).toHaveLength(1);
498
+ });
499
+
500
+ it("rolls back the optimistic row when the transport rejects", async () => {
501
+ const execute = vi.fn().mockRejectedValue(new Error("no permission"));
502
+ sync = createSync();
503
+ const { subscriptionId } = await sync.subscribe({ doctype: "TestDoc", fields: ["title"] });
504
+
505
+ await expect(
506
+ sync.mutate({
507
+ mutationId: "m-transport-fail",
508
+ operation: Operation.Create,
509
+ doc: { doctype: "TestDoc", name: "local:2", title: "rejected" } as any,
510
+ execute,
511
+ }),
512
+ ).rejects.toThrow("no permission");
513
+
514
+ expect(sync.getDocsForSubscription(subscriptionId).map((d) => d.name)).not.toContain(
515
+ "local:2",
516
+ );
517
+ const failed = sync.getMutationEntries("TestDoc");
518
+ expect(failed).toHaveLength(1);
519
+ expect(failed[0].error).toBe("no permission");
520
+ });
521
+
522
+ it("skips request batching for transport mutations", async () => {
523
+ const execute = vi.fn().mockResolvedValue({
524
+ name: "SRV-10",
525
+ doctype: "TestDoc",
526
+ title: "x",
527
+ });
528
+ sync = createSync({ enableBatching: true });
529
+
530
+ await sync.mutate({
531
+ mutationId: "m-transport-nobatch",
532
+ operation: Operation.Create,
533
+ doc: { doctype: "TestDoc", name: "local:3", title: "x" } as any,
534
+ execute,
535
+ });
536
+
537
+ expect(execute).toHaveBeenCalled();
538
+ // Neither the batch RPC nor the REST create ran.
539
+ expect(mockDb.call.post).not.toHaveBeenCalled();
540
+ expect(mockDb.createDoc).not.toHaveBeenCalled();
541
+ });
542
+ });
543
+
544
+ describe("mutation ledger (getMutationEntries / dismissMutation)", () => {
545
+ it("exposes a failed mutation with its error, and dismiss drops it", async () => {
546
+ mockDb.updateDoc = vi.fn().mockRejectedValue(Object.assign(new Error("Validation failed")));
547
+ sync = createSync();
548
+ await sync.subscribe({ doctype: "TestDoc", fields: ["title"] });
549
+
550
+ await expect(
551
+ sync.mutate({
552
+ mutationId: "m-fail",
553
+ operation: Operation.Update,
554
+ doc: { doctype: "TestDoc", name: "doc1", title: "rejected" } as any,
555
+ }),
556
+ ).rejects.toThrow("Validation failed");
557
+
558
+ const failed = sync.getMutationEntries("TestDoc");
559
+ expect(failed).toHaveLength(1);
560
+ expect(failed[0]).toMatchObject({
561
+ id: "m-fail",
562
+ doctype: "TestDoc",
563
+ docname: "doc1",
564
+ error: "Validation failed",
565
+ });
566
+
567
+ const listener = vi.fn();
568
+ sync.subscribeStore(listener);
569
+ sync.dismissMutation("m-fail");
570
+ expect(sync.getMutationEntries("TestDoc")).toHaveLength(0);
571
+ expect(listener).toHaveBeenCalled();
572
+ });
573
+
574
+ it("rolls the optimistic update back when the server rejects", async () => {
575
+ mockDb.updateDoc = vi.fn().mockRejectedValue(new Error("boom"));
576
+ sync = createSync();
577
+ const { subscriptionId } = await sync.subscribe({ doctype: "TestDoc", fields: ["title"] });
578
+
579
+ await expect(
580
+ sync.mutate({
581
+ mutationId: "m-rollback",
582
+ operation: Operation.Update,
583
+ doc: { doctype: "TestDoc", name: "doc1", title: "never lands" } as any,
584
+ }),
585
+ ).rejects.toThrow("boom");
586
+
587
+ const rows = sync.getDocsForSubscription<{ name: string; doctype: string; title?: string }>(subscriptionId);
588
+ expect(rows.find((d) => d.name === "doc1")?.title).toBe("Doc 1");
589
+ });
590
+
591
+ it("scopes entries by doctype", async () => {
592
+ mockDb.updateDoc = vi.fn().mockRejectedValue(new Error("nope"));
593
+ sync = createSync();
594
+ await sync.subscribe({ doctype: "TestDoc", fields: ["title"] });
595
+
596
+ await sync
597
+ .mutate({
598
+ mutationId: "m-a",
599
+ operation: Operation.Update,
600
+ doc: { doctype: "TestDoc", name: "doc1", title: "x" } as any,
601
+ })
602
+ .catch(() => {});
603
+
604
+ expect(sync.getMutationEntries("OtherDoc")).toHaveLength(0);
605
+ expect(sync.getMutationEntries()).toHaveLength(1);
606
+ });
607
+ });
354
608
  });
package/src/sync/types.ts CHANGED
@@ -115,6 +115,18 @@ export interface CreateMutation<D extends FrappeDocData> {
115
115
  * this single field.
116
116
  */
117
117
  temporaryName?: string;
118
+ /**
119
+ * Custom server transport. When set, the engine performs the write via
120
+ * this function instead of `db.createDoc` — everything else (optimistic
121
+ * apply, journal, temp→real remap, rollback, ledger) is unchanged. Used
122
+ * for desk RPCs whose server-side behavior a plain REST insert cannot
123
+ * reproduce (e.g. `frappe.desk.form.utils.add_comment` inserts with
124
+ * ignore_permissions and extracts inline images). Must resolve with the
125
+ * created document (the ack the remap needs). Mutations carrying a
126
+ * transport skip request batching, and a journal replay after a page
127
+ * refresh falls back to the REST path (functions don't persist).
128
+ */
129
+ execute?: () => Promise<FrappeDoc<D>>;
118
130
  }
119
131
 
120
132
  export interface UpdateMutation<Data extends FrappeDocData> {
@@ -123,6 +135,12 @@ export interface UpdateMutation<Data extends FrappeDocData> {
123
135
  doc: FrappeDoc<Data>;
124
136
  version?: string;
125
137
  affected?: { doctype: string; name: string }[];
138
+ /**
139
+ * Custom server transport (see {@link CreateMutation.execute}). May
140
+ * resolve with the updated document to seed the cache; resolving void
141
+ * keeps the optimistic value until the realtime echo reconciles.
142
+ */
143
+ execute?: () => Promise<FrappeDoc<Data> | void>;
126
144
  }
127
145
 
128
146
  export interface DeleteMutation<Data extends FrappeDocData> {
@@ -131,6 +149,8 @@ export interface DeleteMutation<Data extends FrappeDocData> {
131
149
  version?: string;
132
150
  doc: FrappeDoc<Data>;
133
151
  affected?: { doctype: string; name: string }[];
152
+ /** Custom server transport (see {@link CreateMutation.execute}). */
153
+ execute?: () => Promise<void>;
134
154
  }
135
155
 
136
156
  export type Mutation<Data extends FrappeDocData> =
@@ -2,6 +2,17 @@ import { getCookie, setCookie, deleteCookie } from "@multiplatform.one/platform"
2
2
  import { HttpMethod } from "./types";
3
3
  import type { AuthConfig, RetryConfig } from "./types";
4
4
  export { getCookie, setCookie, deleteCookie };
5
+ /**
6
+ * The user-facing sentence for a failed response.
7
+ *
8
+ * With `developer_mode` on, Frappe puts a full python traceback in `exc`, and
9
+ * reading that first meant every permission failure surfaced a traceback as its
10
+ * error message — which components then rendered verbatim into the UI. The
11
+ * readable sentence is in `_server_messages`, and it is preferred here; `exc`
12
+ * is a last resort and contributes only its final line (the actual raise),
13
+ * never the whole stack.
14
+ */
15
+ export declare function errorMessageFromPayload(payload: unknown, statusText: string): string;
5
16
  export type RequestBody = BodyInit | Record<string, unknown> | unknown[] | ArrayBufferView;
6
17
  export interface HttpResponse<T> {
7
18
  _res: Response;
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"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"}
@@ -143,6 +143,13 @@ export declare function parseFormLayout(fields: DocTypeField[]): FormSection[];
143
143
  /**
144
144
  * Fetch and cache DocType metadata (schema, fields, layout, permissions).
145
145
  *
146
+ * Metadata comes from `frappe.desk.form.load.getdoctype`, the whitelisted RPC
147
+ * the Desk itself uses, so ordinary users load forms too — the REST
148
+ * `/api/resource/DocType/<name>` route reads the DocType *document* and is
149
+ * System-Manager-only. That route stays as a fallback for desk-less sites
150
+ * where the RPC is not registered. When `config.fixtures` carries a "DocType"
151
+ * entry for the doctype it is used instead and no request is made.
152
+ *
146
153
  * Responses are cached per-session in memory keyed by `{baseURL}:{doctype}`.
147
154
  * The `refetch` function clears the cache entry and forces a fresh fetch.
148
155
  *
@@ -1 +1 @@
1
- {"version":3,"file":"doctype.d.ts","sourceRoot":"","sources":["../src/doctype.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"doctype.d.ts","sourceRoot":"","sources":["../src/doctype.ts"],"names":[],"mappings":"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"}