@multiplatform.one/frappe 6.7.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/core/http.cjs +45 -1
- package/dist/cjs/core/http.native.js +72 -1
- package/dist/cjs/core/http.native.js.map +1 -1
- package/dist/cjs/doctype.cjs +15 -2
- package/dist/cjs/doctype.native.js +22 -5
- package/dist/cjs/doctype.native.js.map +1 -1
- package/dist/cjs/frappeReact.cjs +55 -1
- package/dist/cjs/frappeReact.native.js +66 -1
- package/dist/cjs/frappeReact.native.js.map +1 -1
- package/dist/cjs/index.cjs +7 -0
- package/dist/cjs/index.native.js +7 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/optimistic.cjs +32 -2
- package/dist/cjs/optimistic.native.js +34 -1
- package/dist/cjs/optimistic.native.js.map +1 -1
- package/dist/cjs/pacer.native.js.map +1 -1
- package/dist/cjs/realtime.cjs +12 -0
- package/dist/cjs/realtime.native.js +16 -4
- package/dist/cjs/realtime.native.js.map +1 -1
- package/dist/cjs/sync/index.cjs +45 -1
- package/dist/cjs/sync/index.native.js +71 -1
- package/dist/cjs/sync/index.native.js.map +1 -1
- package/dist/cjs/sync/mutationEngine.cjs +51 -15
- package/dist/cjs/sync/mutationEngine.native.js +69 -16
- package/dist/cjs/sync/mutationEngine.native.js.map +1 -1
- package/dist/cjs/sync/mutationJournal.cjs +8 -0
- package/dist/cjs/sync/mutationJournal.native.js +10 -0
- package/dist/cjs/sync/mutationJournal.native.js.map +1 -1
- package/dist/cjs/sync/types.native.js.map +1 -1
- package/dist/esm/core/http.mjs +45 -2
- package/dist/esm/core/http.mjs.map +1 -1
- package/dist/esm/core/http.native.js +72 -2
- package/dist/esm/core/http.native.js.map +1 -1
- package/dist/esm/doctype.mjs +15 -2
- package/dist/esm/doctype.mjs.map +1 -1
- package/dist/esm/doctype.native.js +22 -5
- package/dist/esm/doctype.native.js.map +1 -1
- package/dist/esm/frappeReact.mjs +55 -3
- package/dist/esm/frappeReact.mjs.map +1 -1
- package/dist/esm/frappeReact.native.js +66 -3
- package/dist/esm/frappeReact.native.js.map +1 -1
- package/dist/esm/index.mjs +4 -4
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +4 -4
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/optimistic.mjs +27 -1
- package/dist/esm/optimistic.mjs.map +1 -1
- package/dist/esm/optimistic.native.js +30 -1
- package/dist/esm/optimistic.native.js.map +1 -1
- package/dist/esm/pacer.mjs.map +1 -1
- package/dist/esm/pacer.native.js.map +1 -1
- package/dist/esm/realtime.mjs +12 -0
- package/dist/esm/realtime.mjs.map +1 -1
- package/dist/esm/realtime.native.js +16 -4
- package/dist/esm/realtime.native.js.map +1 -1
- package/dist/esm/sync/index.mjs +45 -1
- package/dist/esm/sync/index.mjs.map +1 -1
- package/dist/esm/sync/index.native.js +71 -1
- package/dist/esm/sync/index.native.js.map +1 -1
- package/dist/esm/sync/mutationEngine.mjs +51 -15
- package/dist/esm/sync/mutationEngine.mjs.map +1 -1
- package/dist/esm/sync/mutationEngine.native.js +69 -16
- package/dist/esm/sync/mutationEngine.native.js.map +1 -1
- package/dist/esm/sync/mutationJournal.mjs +8 -0
- package/dist/esm/sync/mutationJournal.mjs.map +1 -1
- package/dist/esm/sync/mutationJournal.native.js +10 -0
- package/dist/esm/sync/mutationJournal.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/core/http.mjs +45 -2
- package/dist/jsx/core/http.mjs.map +1 -1
- package/dist/jsx/core/http.native.js +72 -1
- package/dist/jsx/core/http.native.js.map +1 -1
- package/dist/jsx/doctype.mjs +15 -2
- package/dist/jsx/doctype.mjs.map +1 -1
- package/dist/jsx/doctype.native.js +22 -5
- package/dist/jsx/doctype.native.js.map +1 -1
- package/dist/jsx/frappeReact.mjs +55 -3
- package/dist/jsx/frappeReact.mjs.map +1 -1
- package/dist/jsx/frappeReact.native.js +66 -1
- package/dist/jsx/frappeReact.native.js.map +1 -1
- package/dist/jsx/index.js +4 -4
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +4 -4
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +7 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/optimistic.mjs +27 -1
- package/dist/jsx/optimistic.mjs.map +1 -1
- package/dist/jsx/optimistic.native.js +34 -1
- package/dist/jsx/optimistic.native.js.map +1 -1
- package/dist/jsx/pacer.mjs.map +1 -1
- package/dist/jsx/pacer.native.js.map +1 -1
- package/dist/jsx/realtime.mjs +12 -0
- package/dist/jsx/realtime.mjs.map +1 -1
- package/dist/jsx/realtime.native.js +16 -4
- package/dist/jsx/realtime.native.js.map +1 -1
- package/dist/jsx/sync/index.mjs +45 -1
- package/dist/jsx/sync/index.mjs.map +1 -1
- package/dist/jsx/sync/index.native.js +71 -1
- package/dist/jsx/sync/index.native.js.map +1 -1
- package/dist/jsx/sync/mutationEngine.mjs +51 -15
- package/dist/jsx/sync/mutationEngine.mjs.map +1 -1
- package/dist/jsx/sync/mutationEngine.native.js +69 -16
- package/dist/jsx/sync/mutationEngine.native.js.map +1 -1
- package/dist/jsx/sync/mutationJournal.mjs +8 -0
- package/dist/jsx/sync/mutationJournal.mjs.map +1 -1
- package/dist/jsx/sync/mutationJournal.native.js +10 -0
- package/dist/jsx/sync/mutationJournal.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/core/http.spec.ts +81 -0
- package/src/core/http.ts +80 -3
- package/src/doctype.spec.ts +82 -3
- package/src/doctype.ts +51 -7
- package/src/frappeReact.ts +126 -4
- package/src/index.ts +15 -2
- package/src/optimistic.spec.ts +113 -2
- package/src/optimistic.ts +95 -0
- package/src/pacer.ts +8 -0
- package/src/realtime.spec.ts +62 -0
- package/src/realtime.ts +34 -0
- package/src/sync/index.ts +44 -1
- package/src/sync/mutationEngine.spec.ts +7 -2
- package/src/sync/mutationEngine.ts +83 -27
- package/src/sync/mutationJournal.ts +9 -0
- package/src/sync/mutations.spec.ts +1 -1
- package/src/sync/syncModule.spec.ts +255 -1
- package/src/sync/types.ts +20 -0
- package/types/core/http.d.ts +11 -0
- package/types/core/http.d.ts.map +1 -1
- package/types/doctype.d.ts +7 -0
- package/types/doctype.d.ts.map +1 -1
- package/types/frappeReact.d.ts +50 -2
- package/types/frappeReact.d.ts.map +1 -1
- package/types/index.d.ts +6 -5
- package/types/index.d.ts.map +1 -1
- package/types/optimistic.d.ts +38 -0
- package/types/optimistic.d.ts.map +1 -1
- package/types/pacer.d.ts +8 -0
- package/types/pacer.d.ts.map +1 -1
- package/types/realtime.d.ts +17 -0
- package/types/realtime.d.ts.map +1 -1
- package/types/sync/index.d.ts +16 -1
- package/types/sync/index.d.ts.map +1 -1
- package/types/sync/mutationEngine.d.ts +7 -0
- package/types/sync/mutationEngine.d.ts.map +1 -1
- package/types/sync/mutationJournal.d.ts +6 -0
- package/types/sync/mutationJournal.d.ts.map +1 -1
- package/types/sync/types.d.ts +20 -0
- 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
|
-
|
|
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(
|
|
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
|
-
//
|
|
272
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
//
|
|
442
|
-
this.
|
|
443
|
-
|
|
444
|
-
|
|
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> =
|
package/types/core/http.d.ts
CHANGED
|
@@ -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;
|
package/types/core/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAY,MAAM,6BAA6B,CAAC;AAE3F,OAAO,EAAe,UAAU,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"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"}
|
package/types/doctype.d.ts
CHANGED
|
@@ -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
|
*
|
package/types/doctype.d.ts.map
CHANGED
|
@@ -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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"}
|