@multiplatform.one/frappe 7.8.0 → 7.11.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 (87) hide show
  1. package/README.md +89 -1
  2. package/dist/cjs/frappeReact.cjs +54 -30
  3. package/dist/cjs/frappeReact.native.js +67 -32
  4. package/dist/cjs/frappeReact.native.js.map +1 -1
  5. package/dist/cjs/index.cjs +1 -0
  6. package/dist/cjs/index.native.js +1 -0
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/sync/index.cjs +97 -13
  9. package/dist/cjs/sync/index.native.js +127 -13
  10. package/dist/cjs/sync/index.native.js.map +1 -1
  11. package/dist/cjs/sync/subscriptionManager.cjs +94 -50
  12. package/dist/cjs/sync/subscriptionManager.native.js +133 -73
  13. package/dist/cjs/sync/subscriptionManager.native.js.map +1 -1
  14. package/dist/cjs/sync/types.native.js.map +1 -1
  15. package/dist/cjs/syncOptionsKey.cjs +59 -0
  16. package/dist/cjs/syncOptionsKey.native.js +82 -0
  17. package/dist/cjs/syncOptionsKey.native.js.map +1 -0
  18. package/dist/esm/frappeReact.mjs +55 -32
  19. package/dist/esm/frappeReact.mjs.map +1 -1
  20. package/dist/esm/frappeReact.native.js +68 -34
  21. package/dist/esm/frappeReact.native.js.map +1 -1
  22. package/dist/esm/index.mjs +2 -2
  23. package/dist/esm/index.mjs.map +1 -1
  24. package/dist/esm/index.native.js +2 -2
  25. package/dist/esm/index.native.js.map +1 -1
  26. package/dist/esm/sync/index.mjs +97 -13
  27. package/dist/esm/sync/index.mjs.map +1 -1
  28. package/dist/esm/sync/index.native.js +127 -13
  29. package/dist/esm/sync/index.native.js.map +1 -1
  30. package/dist/esm/sync/subscriptionManager.mjs +94 -50
  31. package/dist/esm/sync/subscriptionManager.mjs.map +1 -1
  32. package/dist/esm/sync/subscriptionManager.native.js +133 -73
  33. package/dist/esm/sync/subscriptionManager.native.js.map +1 -1
  34. package/dist/esm/sync/types.mjs.map +1 -1
  35. package/dist/esm/sync/types.native.js.map +1 -1
  36. package/dist/esm/syncOptionsKey.mjs +34 -0
  37. package/dist/esm/syncOptionsKey.mjs.map +1 -0
  38. package/dist/esm/syncOptionsKey.native.js +54 -0
  39. package/dist/esm/syncOptionsKey.native.js.map +1 -0
  40. package/dist/jsx/frappeReact.mjs +55 -32
  41. package/dist/jsx/frappeReact.mjs.map +1 -1
  42. package/dist/jsx/frappeReact.native.js +67 -32
  43. package/dist/jsx/frappeReact.native.js.map +1 -1
  44. package/dist/jsx/index.js +2 -2
  45. package/dist/jsx/index.js.map +1 -1
  46. package/dist/jsx/index.mjs +2 -2
  47. package/dist/jsx/index.mjs.map +1 -1
  48. package/dist/jsx/index.native.js +1 -0
  49. package/dist/jsx/index.native.js.map +1 -1
  50. package/dist/jsx/sync/index.mjs +97 -13
  51. package/dist/jsx/sync/index.mjs.map +1 -1
  52. package/dist/jsx/sync/index.native.js +127 -13
  53. package/dist/jsx/sync/index.native.js.map +1 -1
  54. package/dist/jsx/sync/subscriptionManager.mjs +94 -50
  55. package/dist/jsx/sync/subscriptionManager.mjs.map +1 -1
  56. package/dist/jsx/sync/subscriptionManager.native.js +133 -73
  57. package/dist/jsx/sync/subscriptionManager.native.js.map +1 -1
  58. package/dist/jsx/sync/types.mjs.map +1 -1
  59. package/dist/jsx/sync/types.native.js.map +1 -1
  60. package/dist/jsx/syncOptionsKey.mjs +34 -0
  61. package/dist/jsx/syncOptionsKey.mjs.map +1 -0
  62. package/dist/jsx/syncOptionsKey.native.js +82 -0
  63. package/dist/jsx/syncOptionsKey.native.js.map +1 -0
  64. package/package.json +9 -9
  65. package/src/frappeReact.catchUp.spec.tsx +105 -0
  66. package/src/frappeReact.ts +62 -38
  67. package/src/index.ts +8 -1
  68. package/src/sync/catchUp.spec.ts +142 -0
  69. package/src/sync/index.ts +108 -8
  70. package/src/sync/refreshSubscriptions.lifecycle.spec.ts +268 -0
  71. package/src/sync/refreshSubscriptions.spec.ts +295 -0
  72. package/src/sync/subscriptionManager.ts +107 -44
  73. package/src/sync/types.ts +9 -1
  74. package/src/syncOptionsKey.spec.ts +46 -0
  75. package/src/syncOptionsKey.ts +45 -0
  76. package/types/frappeReact.d.ts +6 -1
  77. package/types/frappeReact.d.ts.map +1 -1
  78. package/types/index.d.ts +4 -4
  79. package/types/index.d.ts.map +1 -1
  80. package/types/sync/index.d.ts +13 -1
  81. package/types/sync/index.d.ts.map +1 -1
  82. package/types/sync/subscriptionManager.d.ts +13 -10
  83. package/types/sync/subscriptionManager.d.ts.map +1 -1
  84. package/types/sync/types.d.ts +8 -1
  85. package/types/sync/types.d.ts.map +1 -1
  86. package/types/syncOptionsKey.d.ts +3 -0
  87. package/types/syncOptionsKey.d.ts.map +1 -0
@@ -0,0 +1,295 @@
1
+ import { afterEach, expect, it, vi } from "vitest";
2
+ import { createFrappeCollection } from "../collection";
3
+ import { SyncModule as PublicSyncModule } from "../index";
4
+ import { SyncModule } from "./index";
5
+ import { Order, defaultPageLength } from "./types";
6
+ import type { SubscriptionQuery } from "./types";
7
+
8
+ vi.mock("@multiplatform.one/platform", () => ({
9
+ isNative: false,
10
+ isServer: true,
11
+ isWindowDefined: false,
12
+ onAppFocus: () => () => {},
13
+ onOnline: () => () => {},
14
+ onVisibilityChange: () => () => {},
15
+ }));
16
+ const instances: SyncModule[] = [];
17
+ const cleanups: (() => unknown)[] = [];
18
+ afterEach(async () => {
19
+ try {
20
+ for (const cleanup of cleanups.splice(0)) await cleanup();
21
+ } finally {
22
+ for (const sync of instances.splice(0)) await sync.dispose();
23
+ }
24
+ });
25
+ const row = (name: string, title = name) => ({
26
+ name,
27
+ title,
28
+ doctype: "Item",
29
+ modified: "2026-09-09T01:00:00Z",
30
+ });
31
+ function setup() {
32
+ const db = { getDocList: vi.fn().mockResolvedValue([row("a"), row("b")]) };
33
+ const sync = new SyncModule(db as never, undefined);
34
+ instances.push(sync);
35
+ return { db, sync };
36
+ }
37
+ function deferred<T>() {
38
+ let resolve!: (value: T) => void;
39
+ const promise = new Promise<T>((done) => {
40
+ resolve = done;
41
+ });
42
+ return { promise, resolve };
43
+ }
44
+ it("replaces mounted collection membership and clears authoritative empty snapshots", async () => {
45
+ const { db, sync } = setup();
46
+ const subscribe = vi.spyOn(sync, "subscribe");
47
+ const collection = createFrappeCollection<ReturnType<typeof row>>("Item", { sync });
48
+ cleanups.push(() => collection.cleanup());
49
+ await collection.preload();
50
+ const originalCollection = collection;
51
+ const id = (await subscribe.mock.results[0].value).subscriptionId;
52
+ const observed: string[][] = [];
53
+ cleanups.push(
54
+ sync.subscribeStore(() => {
55
+ observed.push(sync.getDocsForSubscription(id!).map((doc) => doc.name));
56
+ }),
57
+ );
58
+ db.getDocList.mockResolvedValue({
59
+ data: [row("b", "updated"), row("c")],
60
+ cursor: "replacement-cursor",
61
+ });
62
+ const [result] = await sync.refreshSubscriptions();
63
+ expect(result.ok).toBe(true);
64
+ expect(result.subscriptionId).toBe(id);
65
+ expect(collection).toBe(originalCollection);
66
+ expect(
67
+ [...collection.state.values()]
68
+ .map(({ name, title }) => ({ name, title }))
69
+ .sort((a, b) => a.name.localeCompare(b.name)),
70
+ ).toEqual([
71
+ { name: "b", title: "updated" },
72
+ { name: "c", title: "c" },
73
+ ]);
74
+ expect(observed).toEqual([["b", "c"]]);
75
+ expect(sync.getSubscription(id)?.cursor).toBe("replacement-cursor");
76
+ expect(sync.getDocFromCache("Item", "a")).toEqual(row("a"));
77
+ db.getDocList.mockResolvedValue([]);
78
+ expect(await sync.refreshSubscriptions()).toEqual([result]);
79
+ expect([...collection.state.values()]).toEqual([]);
80
+ expect(sync.getSubscription(result.subscriptionId)?.boundaries).toBeUndefined();
81
+ expect(collection).toBe(originalCollection);
82
+ expect(observed).toEqual([["b", "c"], []]);
83
+ expect(subscribe).toHaveBeenCalledTimes(1);
84
+ expect(sync.getSubscription(id)).toMatchObject({
85
+ id,
86
+ docIds: [],
87
+ metadata: { itemsBeforePage: 0, itemsAfterPage: 0 },
88
+ });
89
+ expect(sync.getSubscription(id)?.cursor).toBeUndefined();
90
+ expect(sync.getSubscription(id)?.checkpoint).toBeUndefined();
91
+ });
92
+ it("retains data and checkpoint on failure and preserves the query", async () => {
93
+ const { db, sync } = setup();
94
+ db.getDocList.mockResolvedValue({ data: [row("a"), row("b")], cursor: "committed-cursor" });
95
+ const query = {
96
+ doctype: "Item",
97
+ fields: ["title"],
98
+ filters: { enabled: 1 },
99
+ orderBy: [{ field: "title", order: Order.Desc }],
100
+ limit: 3,
101
+ limitStart: 6,
102
+ };
103
+ const { subscriptionId } = await sync.subscribe(query);
104
+ const before = structuredClone(sync.getSubscription(subscriptionId));
105
+ const error = new Error("offline");
106
+ db.getDocList.mockRejectedValue(error);
107
+ const results = await sync.refreshSubscriptions();
108
+ expect(results).toEqual([{ subscriptionId, ok: false, error }]);
109
+ expect(results[0].error).toBe(error);
110
+ expect(sync.getSubscription(subscriptionId)).toEqual({ ...before, stale: true });
111
+ expect(db.getDocList.mock.calls).toEqual(
112
+ Array(2).fill([
113
+ "Item",
114
+ {
115
+ fields: ["title", "modified", "name"],
116
+ filters: { enabled: 1 },
117
+ orderBy: [{ field: "title", order: Order.Desc }],
118
+ limit: 3,
119
+ limitStart: 6,
120
+ },
121
+ ]),
122
+ );
123
+ expect(sync.getDocsForSubscription(subscriptionId)).toEqual([row("a"), row("b")]);
124
+ });
125
+ it("coalesces overlapping requests into a trailing authoritative fetch", async () => {
126
+ const { db, sync } = setup();
127
+ const { subscriptionId } = await sync.subscribe({ doctype: "Item" });
128
+ const first = deferred<ReturnType<typeof row>[]>();
129
+ const last = deferred<ReturnType<typeof row>[]>();
130
+ db.getDocList.mockReturnValueOnce(first.promise).mockReturnValueOnce(last.promise);
131
+ const one = sync.refreshSubscriptions();
132
+ await vi.waitFor(() => expect(db.getDocList).toHaveBeenCalledTimes(2));
133
+ const two = sync.refreshSubscriptions();
134
+ await Promise.resolve();
135
+ first.resolve([row("old")]);
136
+ await vi.waitFor(() => expect(db.getDocList).toHaveBeenCalledTimes(3));
137
+ expect(sync.getDocsForSubscription(subscriptionId)).toEqual([row("a"), row("b")]);
138
+ last.resolve([row("new")]);
139
+ expect(await one).toEqual(await two);
140
+ expect(sync.getDocsForSubscription(subscriptionId)).toEqual([row("new")]);
141
+ });
142
+ it("does not resurrect an unsubscribed request or modify its cached rows", async () => {
143
+ const { db, sync } = setup();
144
+ const { subscriptionId } = await sync.subscribe({ doctype: "Item" });
145
+ const pending = deferred<ReturnType<typeof row>[]>();
146
+ db.getDocList.mockReturnValueOnce(pending.promise);
147
+ const refresh = sync.refreshSubscriptions();
148
+ await vi.waitFor(() => expect(db.getDocList).toHaveBeenCalledTimes(2));
149
+ sync.unsubscribe(subscriptionId);
150
+ pending.resolve([row("a", "obsolete")]);
151
+ expect((await refresh)[0].ok).toBe(false);
152
+ expect(sync.getSubscription(subscriptionId)).toBeUndefined();
153
+ expect(sync.getDocFromCache("Item", "a")).toEqual(row("a"));
154
+ });
155
+ it("filters active doctypes and explicitly declines progressive refresh", async () => {
156
+ const { db, sync } = setup();
157
+ await sync.subscribe({ doctype: "Item" });
158
+ const progressive = await sync.subscribe({ doctype: "Paged", progressive: true });
159
+ const before = structuredClone(sync.getSubscription(progressive.subscriptionId));
160
+ const pageBefore = structuredClone(sync.getPageInfo(progressive.subscriptionId));
161
+ expect(db.getDocList).toHaveBeenCalledTimes(2);
162
+ db.getDocList.mockClear();
163
+ expect(await sync.refreshSubscriptions({ doctypes: [] })).toEqual([]);
164
+ expect(await sync.refreshSubscriptions({ doctypes: ["Unknown"] })).toEqual([]);
165
+ expect(db.getDocList).not.toHaveBeenCalled();
166
+ expect((await sync.refreshSubscriptions({ doctypes: ["Item"] }))[0].ok).toBe(true);
167
+ expect(db.getDocList).toHaveBeenCalledTimes(1);
168
+ const result = await sync.refreshSubscriptions({ doctypes: ["Paged"] });
169
+ expect(sync.getSubscription(progressive.subscriptionId)).toEqual(before);
170
+ expect(sync.getPageInfo(progressive.subscriptionId)).toEqual(pageBefore);
171
+ expect(result).toEqual([
172
+ {
173
+ subscriptionId: progressive.subscriptionId,
174
+ ok: false,
175
+ error: new Error("Snapshot refresh does not support progressive subscriptions"),
176
+ },
177
+ ]);
178
+ expect(db.getDocList).toHaveBeenCalledTimes(1);
179
+ });
180
+
181
+ it("exports the public runtime constructor", () => {
182
+ expect(PublicSyncModule).toBe(SyncModule);
183
+ expect(PublicSyncModule.prototype.refreshSubscriptions).toBeTypeOf("function");
184
+ });
185
+
186
+ it.each([
187
+ [undefined, undefined, defaultPageLength],
188
+ [undefined, 8, defaultPageLength],
189
+ [null, 4, defaultPageLength],
190
+ [0, 0, 0],
191
+ [7, 2, 7],
192
+ ])("preserves query window limit=%s offset=%s", async (limit, limitStart, expectedLimit) => {
193
+ const { sync, db } = setup();
194
+ const query = {
195
+ doctype: "Item",
196
+ fields: ["title", "name", "title"],
197
+ filters: { enabled: 1 },
198
+ orderBy: [{ field: "title", order: Order.Desc }],
199
+ ...(limit === undefined ? {} : { limit }),
200
+ ...(limitStart === undefined ? {} : { limitStart }),
201
+ } as SubscriptionQuery;
202
+ const { subscriptionId } = await sync.subscribe(query);
203
+ expect(sync.getSubscription(subscriptionId)?.boundaries?.pageSize).toBe(expectedLimit);
204
+ db.getDocList.mockResolvedValue({
205
+ data: [row("z"), { ...row("b"), modified: "2026-09-09T02:00:00Z" }],
206
+ cursor: "new-cursor",
207
+ });
208
+ await sync.refreshSubscriptions();
209
+ expect(db.getDocList.mock.calls).toEqual(
210
+ Array(2).fill([
211
+ "Item",
212
+ {
213
+ fields: ["title", "name", "modified"],
214
+ filters: query.filters,
215
+ orderBy: query.orderBy,
216
+ limit: expectedLimit,
217
+ ...(limitStart === undefined ? {} : { limitStart }),
218
+ },
219
+ ]),
220
+ );
221
+ expect(sync.getSubscription(subscriptionId)).toMatchObject({
222
+ docIds: ["z", "b"],
223
+ cursor: "new-cursor",
224
+ checkpoint: "2026-09-09T02:00:00Z",
225
+ boundaries: { pageSize: expectedLimit, offset: limitStart ?? 0 },
226
+ });
227
+ });
228
+
229
+ it.each([404, 403])("handles single-name getDoc status %s", async (status) => {
230
+ const db = { getDoc: vi.fn().mockResolvedValue(row("a")), getDocList: vi.fn() };
231
+ const sync = new SyncModule(db as never, undefined);
232
+ instances.push(sync);
233
+ const { subscriptionId } = await sync.subscribe({ doctype: "Item", filters: { name: "a" } });
234
+ const before = structuredClone(sync.getSubscription(subscriptionId));
235
+ const error = Object.assign(new Error("getDoc failed"), { status });
236
+ db.getDoc.mockRejectedValue(error);
237
+ expect(await sync.refreshSubscriptions()).toEqual([
238
+ { subscriptionId, ok: status === 404, ...(status === 404 ? {} : { error }) },
239
+ ]);
240
+ expect(db.getDoc.mock.calls).toEqual([
241
+ ["Item", "a"],
242
+ ["Item", "a"],
243
+ ]);
244
+ expect(db.getDocList).not.toHaveBeenCalled();
245
+ if (status === 404) {
246
+ expect(sync.getDocsForSubscription(subscriptionId)).toEqual([]);
247
+ expect(sync.getSubscription(subscriptionId)?.boundaries).toBeUndefined();
248
+ } else expect(sync.getSubscription(subscriptionId)).toEqual({ ...before, stale: true });
249
+ expect(sync.getDocFromCache("Item", "a")).toEqual(row("a"));
250
+ });
251
+
252
+ it("returns independent success, failure and progressive refusal", async () => {
253
+ const { sync, db } = setup();
254
+ const item = await sync.subscribe({ doctype: "Item" });
255
+ const other = await sync.subscribe({ doctype: "Other" });
256
+ const progressive = await sync.subscribe({ doctype: "Paged", progressive: true });
257
+ const before = structuredClone(sync.getSubscription(progressive.subscriptionId));
258
+ const pageBefore = structuredClone(sync.getPageInfo(progressive.subscriptionId));
259
+ const error = new Error("offline");
260
+ db.getDocList.mockRejectedValueOnce(error).mockResolvedValueOnce([row("success")]);
261
+ expect(await sync.refreshSubscriptions()).toEqual([
262
+ { subscriptionId: item.subscriptionId, ok: false, error },
263
+ { subscriptionId: other.subscriptionId, ok: true },
264
+ {
265
+ subscriptionId: progressive.subscriptionId,
266
+ ok: false,
267
+ error: new Error("Snapshot refresh does not support progressive subscriptions"),
268
+ },
269
+ ]);
270
+ expect(sync.getSubscription(progressive.subscriptionId)).toEqual(before);
271
+ expect(sync.getPageInfo(progressive.subscriptionId)).toEqual(pageBefore);
272
+ expect(db.getDocList).toHaveBeenCalledTimes(5);
273
+ });
274
+
275
+ it.each(["resetCache", "dispose"] as const)(
276
+ "invalidates a pending refresh on %s",
277
+ async (action) => {
278
+ const { sync, db } = setup();
279
+ const { subscriptionId } = await sync.subscribe({ doctype: "Item" });
280
+ const pending = deferred<ReturnType<typeof row>[]>();
281
+ const started = deferred<void>();
282
+ db.getDocList.mockImplementationOnce(() => {
283
+ started.resolve();
284
+ return pending.promise;
285
+ });
286
+ const result = sync.refreshSubscriptions();
287
+ await started.promise;
288
+ await sync[action]();
289
+ pending.resolve([row("a", "obsolete"), row("obsolete")]);
290
+ expect((await result)[0].ok).toBe(false);
291
+ expect(sync.getSubscription(subscriptionId)).toBeUndefined();
292
+ expect(sync.getDocFromCache("Item", "a")).toBeUndefined();
293
+ expect(sync.getDocFromCache("Item", "obsolete")).toBeUndefined();
294
+ },
295
+ );
@@ -30,7 +30,24 @@ export interface SubscribeResult<Data extends FrappeDocData> {
30
30
  refresh?: Promise<{ ok: boolean; error?: unknown }>;
31
31
  }
32
32
 
33
+ export interface SubscriptionRefreshOptions {
34
+ doctypes?: string[];
35
+ }
36
+
37
+ export interface SubscriptionRefreshResult {
38
+ subscriptionId: string;
39
+ ok: boolean;
40
+ error?: unknown;
41
+ }
42
+
33
43
  export class SubscriptionManager {
44
+ private refreshes = new Map<
45
+ string,
46
+ {
47
+ requested: boolean;
48
+ promise: Promise<SubscriptionRefreshResult>;
49
+ }
50
+ >();
34
51
  private subscriptions = new Map<string, Subscription>();
35
52
  private virtualPageManager: VirtualPageManager;
36
53
  private backfillManager: BackfillManager;
@@ -85,6 +102,7 @@ export class SubscriptionManager {
85
102
 
86
103
  const subscriptionId = cryptoRandomId();
87
104
  const { rows, cursor, checkpoint } = await this.fetchInitialRows<Data>(query);
105
+ this.seedRows(query, rows);
88
106
  // Materialize the in-memory subscription record with initial membership
89
107
  const subscription: Subscription = {
90
108
  id: subscriptionId,
@@ -100,8 +118,8 @@ export class SubscriptionManager {
100
118
  subscription.boundaries = {
101
119
  firstItem: rows[0] as unknown as FrappeDoc<Data>,
102
120
  lastItem: rows[rows.length - 1] as unknown as FrappeDoc<Data>,
103
- offset: query.limitStart || 0,
104
- pageSize: query.limit || 20,
121
+ offset: query.limitStart ?? 0,
122
+ pageSize: query.limit ?? defaultPageLength,
105
123
  };
106
124
  }
107
125
 
@@ -130,9 +148,85 @@ export class SubscriptionManager {
130
148
  return { subscriptionId: subscriptionId, rows, checkpoint };
131
149
  }
132
150
 
151
+ async refreshSubscriptions(
152
+ options: SubscriptionRefreshOptions = {},
153
+ ): Promise<SubscriptionRefreshResult[]> {
154
+ const subscriptions = [...this.subscriptions.values()].filter(
155
+ (subscription) =>
156
+ (this.subRefCounts.get(subscription.id) ?? 0) > 0 &&
157
+ (!options.doctypes || options.doctypes.includes(subscription.query.doctype)),
158
+ );
159
+ return Promise.all(subscriptions.map((subscription) => this.refreshSubscription(subscription)));
160
+ }
161
+
162
+ private refreshSubscription(subscription: Subscription): Promise<SubscriptionRefreshResult> {
163
+ const subscriptionId = subscription.id;
164
+ if (subscription.query.progressive) {
165
+ return Promise.resolve({
166
+ subscriptionId,
167
+ ok: false,
168
+ error: new Error("Snapshot refresh does not support progressive subscriptions"),
169
+ });
170
+ }
171
+ const existing = this.refreshes.get(subscriptionId);
172
+ if (existing) {
173
+ existing.requested = true;
174
+ return existing.promise;
175
+ }
176
+ const state = {
177
+ requested: false,
178
+ promise: Promise.resolve<SubscriptionRefreshResult>({ subscriptionId, ok: false }),
179
+ };
180
+ const active = () =>
181
+ this.refreshes.get(subscriptionId) === state &&
182
+ this.subscriptions.get(subscriptionId) === subscription &&
183
+ (this.subRefCounts.get(subscriptionId) ?? 0) > 0;
184
+ state.promise = Promise.resolve().then(async () => {
185
+ try {
186
+ while (true) {
187
+ state.requested = false;
188
+ try {
189
+ const { rows, cursor, checkpoint } = await this.fetchInitialRows(subscription.query);
190
+ if (!active()) {
191
+ return {
192
+ subscriptionId,
193
+ ok: false,
194
+ error: new Error("Subscription is no longer active"),
195
+ };
196
+ }
197
+ if (state.requested) continue;
198
+ this.seedRows(subscription.query, rows);
199
+ subscription.docIds = rows.map((row) => row.name);
200
+ subscription.cursor = cursor;
201
+ subscription.checkpoint = checkpoint;
202
+ subscription.boundaries = rows.length
203
+ ? {
204
+ firstItem: rows[0],
205
+ lastItem: rows[rows.length - 1],
206
+ offset: subscription.query.limitStart ?? 0,
207
+ pageSize: subscription.query.limit ?? defaultPageLength,
208
+ }
209
+ : undefined;
210
+ subscription.metadata = { itemsBeforePage: 0, itemsAfterPage: 0 };
211
+ subscription.stale = false;
212
+ return { subscriptionId, ok: true };
213
+ } catch (error) {
214
+ if (active() && state.requested) continue;
215
+ if (active()) subscription.stale = true;
216
+ return { subscriptionId, ok: false, error };
217
+ }
218
+ }
219
+ } finally {
220
+ if (this.refreshes.get(subscriptionId) === state) this.refreshes.delete(subscriptionId);
221
+ }
222
+ });
223
+ this.refreshes.set(subscriptionId, state);
224
+ return state.promise;
225
+ }
226
+
133
227
  /**
134
228
  * Performs the initial subscription fetch (single doc via getDoc, lists via
135
- * getDocList), seeds the normalized cache, and computes the CDC checkpoint
229
+ * getDocList) and computes the CDC checkpoint
136
230
  * as the max `modified` across returned rows. Shared by fresh subscribes
137
231
  * and adopted-subscription refreshes so both have identical semantics.
138
232
  */
@@ -186,13 +280,6 @@ export class SubscriptionManager {
186
280
  rows = response;
187
281
  }
188
282
  }
189
- // Seed the normalized cache with every returned row for instant local reads.
190
- // Frappe's standard get_list API omits `doctype` from each row, so inject
191
- // it from the subscription query to ensure the cache keys correctly.
192
- for (const row of rows) {
193
- if (!row.doctype) row.doctype = query.doctype;
194
- this.normalizedCache.upsert(row);
195
- }
196
283
  // Compute checkpoint for CDC as the max `modified` across returned rows (robust to server ordering)
197
284
  let checkpoint: string | undefined = undefined;
198
285
  for (const row of rows) {
@@ -202,6 +289,13 @@ export class SubscriptionManager {
202
289
  return { rows, cursor, checkpoint };
203
290
  }
204
291
 
292
+ private seedRows<Data extends FrappeDocData>(query: SubscriptionQuery, rows: FrappeDoc<Data>[]) {
293
+ for (const row of rows) {
294
+ if (!row.doctype) row.doctype = query.doctype;
295
+ this.normalizedCache.upsert(row);
296
+ }
297
+ }
298
+
205
299
  /**
206
300
  * Re-adopts a subscription restored from a persisted snapshot when an
207
301
  * identical query subscribes: the cached membership renders immediately and
@@ -225,44 +319,11 @@ export class SubscriptionManager {
225
319
  this.queryKeyToSubId.set(queryKey, restoredId);
226
320
  this.subRefCounts.set(restoredId, 1);
227
321
 
228
- const refresh = this.refreshAdoptedSubscription<Data>(subscription);
322
+ const refresh = this.refreshSubscription(subscription);
229
323
  const rows = this.getDocsForSubscription<Data>(restoredId);
230
324
  return { subscriptionId: restoredId, rows, checkpoint: subscription.checkpoint, refresh };
231
325
  }
232
326
 
233
- /**
234
- * Network refresh for an adopted subscription with fresh-subscribe
235
- * semantics: an authoritative page fetch replaces membership and recomputes
236
- * the checkpoint (identical to what a brand-new subscribe would produce).
237
- * On failure (offline) the cached membership and the restored CDC
238
- * checkpoint are kept and the subscription is marked stale — the next
239
- * backfill trigger drains changes from that checkpoint. Never rejects.
240
- */
241
- private async refreshAdoptedSubscription<Data extends FrappeDocData>(
242
- subscription: Subscription,
243
- ): Promise<{ ok: boolean; error?: unknown }> {
244
- try {
245
- const { rows, cursor, checkpoint } = await this.fetchInitialRows<Data>(subscription.query);
246
- subscription.checkpoint = checkpoint;
247
- subscription.cursor = cursor;
248
- subscription.docIds = rows.map((row) => row.name);
249
- if (rows.length > 0) {
250
- subscription.boundaries = {
251
- firstItem: rows[0] as unknown as FrappeDoc<FrappeDocData>,
252
- lastItem: rows[rows.length - 1] as unknown as FrappeDoc<FrappeDocData>,
253
- offset: subscription.query.limitStart || 0,
254
- pageSize: subscription.query.limit || 20,
255
- };
256
- }
257
- subscription.metadata = { itemsBeforePage: 0, itemsAfterPage: 0 };
258
- subscription.stale = false;
259
- return { ok: true };
260
- } catch (error) {
261
- subscription.stale = true;
262
- return { ok: false, error };
263
- }
264
- }
265
-
266
327
  /**
267
328
  * Registers a subscription restored from a persisted snapshot. Restored
268
329
  * subscriptions are dormant: they hold the persisted query, CDC checkpoint,
@@ -343,6 +404,7 @@ export class SubscriptionManager {
343
404
  this.subRefCounts.set(subscriptionId, refCount);
344
405
  return;
345
406
  }
407
+ this.refreshes.delete(subscriptionId);
346
408
  // Last consumer released — clean up
347
409
  this.subRefCounts.delete(subscriptionId);
348
410
  let queryKey: string | undefined;
@@ -753,6 +815,7 @@ export class SubscriptionManager {
753
815
  }
754
816
 
755
817
  clear() {
818
+ this.refreshes.clear();
756
819
  this.subscriptions.clear();
757
820
  this.queryKeyToSubId.clear();
758
821
  this.subRefCounts.clear();
package/src/sync/types.ts CHANGED
@@ -197,7 +197,15 @@ export enum RealtimeEventType {
197
197
  PermEpochBump = "perm_epoch_bump",
198
198
  }
199
199
 
200
+ export interface SyncCatchUpState {
201
+ readonly refreshing: boolean;
202
+ readonly stale: boolean;
203
+ readonly error: unknown | null;
204
+ }
205
+
200
206
  export interface SyncOptions {
207
+ /** Lifecycle catch-up uses CDC by default; snapshot requires only resource reads. */
208
+ catchUpStrategy?: "cdc" | "snapshot";
201
209
  pageSize?: number;
202
210
  persistKey?: string;
203
211
  onConflict?: (info: { doctype: string; name?: string }) => void;
@@ -261,7 +269,7 @@ export interface SyncOptions {
261
269
  /** Rate limit for mutation replay - max mutations per second (default: 10) */
262
270
  replayRateLimit?: number;
263
271
  /**
264
- * Optional interval (in milliseconds) for periodic CDC polling.
272
+ * Optional interval (in milliseconds) for the selected catch-up strategy.
265
273
  * Acts as a safety net for silent socket drops. Set to 0 or omit
266
274
  * to rely solely on event-driven backfill (focus, online, reconnect).
267
275
  * Recommended: 30000 (30 seconds) for production.
@@ -0,0 +1,46 @@
1
+ import { expect, it } from "vitest";
2
+ import { syncOptionsKey } from "./syncOptionsKey";
3
+
4
+ it("normalizes lifecycle defaults and plain options independent of key order", () => {
5
+ expect(syncOptionsKey()).toBe(syncOptionsKey({ catchUpStrategy: "cdc", cdcInterval: 0 }));
6
+ expect(syncOptionsKey({ cdcInterval: -1 })).toBe(syncOptionsKey());
7
+ expect(
8
+ syncOptionsKey({ versionFields: { Task: "modified", Item: "version" }, pageSize: 20 }),
9
+ ).toBe(
10
+ syncOptionsKey({
11
+ pageSize: 20,
12
+ versionFields: { Item: "version", Task: "modified" },
13
+ persistKey: undefined,
14
+ }),
15
+ );
16
+ });
17
+
18
+ it("separates strategy, interval, persistence and cursor capabilities", () => {
19
+ const keys = [
20
+ undefined,
21
+ { catchUpStrategy: "snapshot" as const },
22
+ { cdcInterval: 1000 },
23
+ { persistKey: "one" },
24
+ { useCursorPagination: true },
25
+ ].map(syncOptionsKey);
26
+ expect(new Set(keys).size).toBe(keys.length);
27
+ });
28
+
29
+ it("keeps callback and storage identities without dropping functions", () => {
30
+ const callback = () => 0;
31
+ const storage = {
32
+ getItem: async () => null,
33
+ setItem: async () => {},
34
+ removeItem: async () => {},
35
+ };
36
+ expect(syncOptionsKey({ onBackfill: callback, storage })).toBe(
37
+ syncOptionsKey({ storage, onBackfill: callback }),
38
+ );
39
+ expect(syncOptionsKey({ onBackfill: callback })).not.toBe(
40
+ syncOptionsKey({ onBackfill: () => {} }),
41
+ );
42
+ expect(syncOptionsKey({ storage })).not.toBe(syncOptionsKey({ storage: { ...storage } }));
43
+ expect(syncOptionsKey({ fieldMergeConfig: { Task: { value: callback } } })).not.toBe(
44
+ syncOptionsKey(),
45
+ );
46
+ });
@@ -0,0 +1,45 @@
1
+ import type { SyncOptions } from "./sync/types";
2
+
3
+ const identities = new WeakMap<object, number>();
4
+ let nextIdentity = 0;
5
+ function identity(value: object) {
6
+ let id = identities.get(value);
7
+ if (id === undefined) {
8
+ id = ++nextIdentity;
9
+ identities.set(value, id);
10
+ }
11
+ return ["reference", id];
12
+ }
13
+
14
+ function stableValue(value: unknown, ancestors = new Set<object>()): unknown {
15
+ if (typeof value === "function") return identity(value);
16
+ if (value === null || typeof value !== "object") return [typeof value, String(value)];
17
+ const prototype = Object.getPrototypeOf(value);
18
+ if (
19
+ ancestors.has(value) ||
20
+ (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
21
+ ) {
22
+ return identity(value);
23
+ }
24
+ const nested = new Set(ancestors).add(value);
25
+ if (Array.isArray(value)) return ["array", value.map((item) => stableValue(item, nested))];
26
+ return [
27
+ "record",
28
+ Object.entries(value)
29
+ .filter(([, item]) => item !== undefined)
30
+ .sort(([left], [right]) => left.localeCompare(right))
31
+ .map(([key, item]) => [key, stableValue(item, nested)]),
32
+ ];
33
+ }
34
+
35
+ export function syncOptionsKey(options: SyncOptions = {}): string {
36
+ const { storage, ...values } = options;
37
+ return JSON.stringify([
38
+ stableValue({
39
+ ...values,
40
+ catchUpStrategy: options.catchUpStrategy ?? "cdc",
41
+ cdcInterval: options.cdcInterval && options.cdcInterval > 0 ? options.cdcInterval : 0,
42
+ }),
43
+ storage ? identity(storage) : null,
44
+ ]);
45
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Collection } from "@tanstack/db";
2
2
  import { type FrappeCollectionOptionsConfig } from "./collection";
3
3
  import { SyncModule } from "./sync";
4
- import type { DebugMutationEntry } from "./sync/types";
4
+ import type { DebugMutationEntry, SyncCatchUpState } from "./sync/types";
5
5
  /**
6
6
  * Configuration for creating a Frappe collection with connection details.
7
7
  */
@@ -115,6 +115,11 @@ export declare function useFrappeCollection<TDoc extends object & {
115
115
  name: string;
116
116
  doctype: string;
117
117
  }>(config: UseFrappeCollectionConfig | undefined, doctype: string, options?: Omit<FrappeCollectionOptionsConfig<TDoc>, "sync">): Collection<TDoc, string> | undefined;
118
+ export interface UseFrappeCatchUpResult extends SyncCatchUpState {
119
+ retry: () => Promise<SyncCatchUpState>;
120
+ }
121
+ /** Connection-level refresh qualification. Retry resolves with state, including failures. */
122
+ export declare function useFrappeCatchUp(config: UseFrappeCollectionConfig | undefined): UseFrappeCatchUpResult;
118
123
  /** Snapshot returned by {@link useFrappeConnection}. */
119
124
  export interface FrappeConnectionState {
120
125
  /** Socket connected right now. */
@@ -1 +1 @@
1
- {"version":3,"file":"frappeReact.d.ts","sourceRoot":"","sources":["../src/frappeReact.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"frappeReact.d.ts","sourceRoot":"","sources":["../src/frappeReact.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,cAAc,CAAC;AAMtB,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,IAAI,CAAC,EAAE;QACL,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAClD,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,kEAAkE;QAClE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;KACvD,CAAC;IACF,sCAAsC;IACtC,WAAW,CAAC,EAAE,OAAO,cAAc,EAAE,WAAW,CAAC;IACjD;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,YAAY,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;IACrE,sEAAsE;IACtE,QAAQ,CAAC,EAAE,OAAO,YAAY,EAAE,eAAe,CAAC;CACjD;AAWD;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,UAAU,GAAG,SAAS,CAG9D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,yBAAyB,GAAG,SAAS,GAC5C,UAAU,GAAG,SAAS,CAGxB;AA8WD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACzF,MAAM,EAAE,yBAAyB,GAAG,SAAS,EAC7C,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,IAAI,CAAC,6BAA6B,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,GAC1D,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,SAAS,CA+DtC;AAMD,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC9D,KAAK,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACxC;AAQD,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,yBAAyB,GAAG,SAAS,GAC5C,sBAAsB,CAUxB;AAED,wDAAwD;AACxD,MAAM,WAAW,qBAAqB;IACpC,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,aAAa,EAAE,OAAO,CAAC;CACxB;AAOD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,yBAAyB,GAAG,SAAS,GAC5C,qBAAqB,CAyBvB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,yBAAyB,GAAG,SAAS,EAC7C,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,EAC7D,OAAO,UAAO,GACb,IAAI,CAwBN;AAMD,mDAAmD;AACnD,MAAM,WAAW,+BAA+B;IAC9C,oEAAoE;IACpE,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,2CAA2C;IAC3C,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,sEAAsE;IACtE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B;yDACqD;IACrD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,0EAA0E;IAC1E,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAID;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,yBAAyB,GAAG,SAAS,EAC7C,OAAO,CAAC,EAAE,MAAM,GACf,+BAA+B,CA0CjC;AAMD;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,6CAA6C;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,UAAU,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,wDAAwD;IACxD,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,4CAA4C;IAC5C,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC/C,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,uBAAuB,GAAG,uBAAuB,CAmH1F;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B,CAAC,IAAI;IAC/C,sCAAsC;IACtC,MAAM,EAAE,yBAAyB,GAAG,SAAS,CAAC;IAC9C,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,uBAAuB;IACvB,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrE,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,cAAc,EAAE,OAAO,CAAC;IACzC,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B,CAAC,IAAI;IAC/C,uBAAuB;IACvB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,uCAAuC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,aAAa,EAAE,OAAO,CAAC;IACvB,8BAA8B;IAC9B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,wCAAwC;IACxC,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC/C,oCAAoC;IACpC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,mCAAmC;IACnC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,SAAS,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC3F,OAAO,EAAE,2BAA2B,CAAC,IAAI,CAAC,GACzC,2BAA2B,CAAC,IAAI,CAAC,CAsJnC"}