@interop/was-react 0.1.2 → 0.1.4

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 (150) hide show
  1. package/README.md +9 -1
  2. package/dist/auth/chapi.d.ts +3 -1
  3. package/dist/auth/chapi.d.ts.map +1 -1
  4. package/dist/auth/chapi.js +6 -2
  5. package/dist/auth/chapi.js.map +1 -1
  6. package/dist/auth/loginFlow.d.ts +30 -10
  7. package/dist/auth/loginFlow.d.ts.map +1 -1
  8. package/dist/auth/loginFlow.js +3 -1
  9. package/dist/auth/loginFlow.js.map +1 -1
  10. package/dist/auth/loginRequest.d.ts +9 -3
  11. package/dist/auth/loginRequest.d.ts.map +1 -1
  12. package/dist/auth/loginRequest.js +9 -3
  13. package/dist/auth/loginRequest.js.map +1 -1
  14. package/dist/auth/verifyResponse.d.ts +9 -3
  15. package/dist/auth/verifyResponse.d.ts.map +1 -1
  16. package/dist/auth/verifyResponse.js +6 -2
  17. package/dist/auth/verifyResponse.js.map +1 -1
  18. package/dist/config.d.ts +60 -20
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +12 -4
  21. package/dist/config.js.map +1 -1
  22. package/dist/dev/cli.d.ts.map +1 -1
  23. package/dist/dev/cli.js +4 -7
  24. package/dist/dev/cli.js.map +1 -1
  25. package/dist/dev/provisionDevGrants.d.ts +15 -5
  26. package/dist/dev/provisionDevGrants.d.ts.map +1 -1
  27. package/dist/dev/provisionDevGrants.js +2 -2
  28. package/dist/dev/provisionDevGrants.js.map +1 -1
  29. package/dist/grants.d.ts +7 -19
  30. package/dist/grants.d.ts.map +1 -1
  31. package/dist/grants.js +28 -7
  32. package/dist/grants.js.map +1 -1
  33. package/dist/identity/appSession.d.ts +9 -3
  34. package/dist/identity/appSession.d.ts.map +1 -1
  35. package/dist/identity/appSession.js +3 -1
  36. package/dist/identity/appSession.js.map +1 -1
  37. package/dist/identity/documentLoader.d.ts +3 -1
  38. package/dist/identity/documentLoader.d.ts.map +1 -1
  39. package/dist/identity/documentLoader.js +6 -2
  40. package/dist/identity/documentLoader.js.map +1 -1
  41. package/dist/identity/seedCredential.d.ts +9 -3
  42. package/dist/identity/seedCredential.d.ts.map +1 -1
  43. package/dist/identity/seedCredential.js +9 -15
  44. package/dist/identity/seedCredential.js.map +1 -1
  45. package/dist/identity/seedStore.d.ts +18 -6
  46. package/dist/identity/seedStore.d.ts.map +1 -1
  47. package/dist/identity/seedStore.js.map +1 -1
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/react/WasSessionProvider.d.ts.map +1 -1
  53. package/dist/react/WasSessionProvider.js +10 -1
  54. package/dist/react/WasSessionProvider.js.map +1 -1
  55. package/dist/react/hooks.d.ts +10 -5
  56. package/dist/react/hooks.d.ts.map +1 -1
  57. package/dist/react/hooks.js +6 -30
  58. package/dist/react/hooks.js.map +1 -1
  59. package/dist/session/authStore.d.ts +29 -7
  60. package/dist/session/authStore.d.ts.map +1 -1
  61. package/dist/session/authStore.js +125 -64
  62. package/dist/session/authStore.js.map +1 -1
  63. package/dist/storage/entityStore.d.ts +32 -9
  64. package/dist/storage/entityStore.d.ts.map +1 -1
  65. package/dist/storage/entityStore.js +88 -48
  66. package/dist/storage/entityStore.js.map +1 -1
  67. package/dist/storage/localStore.d.ts +53 -0
  68. package/dist/storage/localStore.d.ts.map +1 -1
  69. package/dist/storage/localStore.js +140 -12
  70. package/dist/storage/localStore.js.map +1 -1
  71. package/dist/storage/rehydrate.d.ts +8 -0
  72. package/dist/storage/rehydrate.d.ts.map +1 -1
  73. package/dist/storage/rehydrate.js +31 -2
  74. package/dist/storage/rehydrate.js.map +1 -1
  75. package/dist/storage/syncController.d.ts +7 -3
  76. package/dist/storage/syncController.d.ts.map +1 -1
  77. package/dist/storage/syncController.js +28 -15
  78. package/dist/storage/syncController.js.map +1 -1
  79. package/dist/storage/syncStatusStore.d.ts +26 -1
  80. package/dist/storage/syncStatusStore.d.ts.map +1 -1
  81. package/dist/storage/syncStatusStore.js +40 -0
  82. package/dist/storage/syncStatusStore.js.map +1 -1
  83. package/dist/storage/wasRemoteStore.d.ts +3 -1
  84. package/dist/storage/wasRemoteStore.d.ts.map +1 -1
  85. package/dist/storage/wasRemoteStore.js +3 -3
  86. package/dist/storage/wasRemoteStore.js.map +1 -1
  87. package/dist/sync/feedMasterPort.d.ts +21 -19
  88. package/dist/sync/feedMasterPort.d.ts.map +1 -1
  89. package/dist/sync/feedMasterPort.js +18 -5
  90. package/dist/sync/feedMasterPort.js.map +1 -1
  91. package/dist/sync/index.d.ts +3 -3
  92. package/dist/sync/index.d.ts.map +1 -1
  93. package/dist/sync/index.js +2 -2
  94. package/dist/sync/index.js.map +1 -1
  95. package/dist/sync/lwwConflictHandler.d.ts +35 -2
  96. package/dist/sync/lwwConflictHandler.d.ts.map +1 -1
  97. package/dist/sync/lwwConflictHandler.js +47 -9
  98. package/dist/sync/lwwConflictHandler.js.map +1 -1
  99. package/dist/sync/pushWrites.d.ts +27 -8
  100. package/dist/sync/pushWrites.d.ts.map +1 -1
  101. package/dist/sync/pushWrites.js +43 -15
  102. package/dist/sync/pushWrites.js.map +1 -1
  103. package/dist/sync/types.d.ts +49 -16
  104. package/dist/sync/types.d.ts.map +1 -1
  105. package/dist/sync/types.js +17 -0
  106. package/dist/sync/types.js.map +1 -1
  107. package/dist/sync/wasReplication.d.ts.map +1 -1
  108. package/dist/sync/wasReplication.js +38 -1
  109. package/dist/sync/wasReplication.js.map +1 -1
  110. package/dist/sync/wasSyncPort.d.ts +29 -6
  111. package/dist/sync/wasSyncPort.d.ts.map +1 -1
  112. package/dist/sync/wasSyncPort.js +105 -101
  113. package/dist/sync/wasSyncPort.js.map +1 -1
  114. package/package.json +4 -3
  115. package/dist/auth/verifyResponse.test.d.ts +0 -2
  116. package/dist/auth/verifyResponse.test.d.ts.map +0 -1
  117. package/dist/auth/verifyResponse.test.js +0 -282
  118. package/dist/auth/verifyResponse.test.js.map +0 -1
  119. package/dist/dev/cli.test.d.ts +0 -2
  120. package/dist/dev/cli.test.d.ts.map +0 -1
  121. package/dist/dev/cli.test.js +0 -39
  122. package/dist/dev/cli.test.js.map +0 -1
  123. package/dist/grants.test.d.ts +0 -2
  124. package/dist/grants.test.d.ts.map +0 -1
  125. package/dist/grants.test.js +0 -79
  126. package/dist/grants.test.js.map +0 -1
  127. package/dist/identity/seedCredential.test.d.ts +0 -2
  128. package/dist/identity/seedCredential.test.d.ts.map +0 -1
  129. package/dist/identity/seedCredential.test.js +0 -205
  130. package/dist/identity/seedCredential.test.js.map +0 -1
  131. package/dist/storage/rehydrate.test.d.ts +0 -2
  132. package/dist/storage/rehydrate.test.d.ts.map +0 -1
  133. package/dist/storage/rehydrate.test.js +0 -172
  134. package/dist/storage/rehydrate.test.js.map +0 -1
  135. package/dist/sync/changesQuery.test.d.ts +0 -2
  136. package/dist/sync/changesQuery.test.d.ts.map +0 -1
  137. package/dist/sync/changesQuery.test.js +0 -145
  138. package/dist/sync/changesQuery.test.js.map +0 -1
  139. package/dist/sync/lww.test.d.ts +0 -2
  140. package/dist/sync/lww.test.d.ts.map +0 -1
  141. package/dist/sync/lww.test.js +0 -28
  142. package/dist/sync/lww.test.js.map +0 -1
  143. package/dist/sync/lwwConflictHandler.test.d.ts +0 -2
  144. package/dist/sync/lwwConflictHandler.test.d.ts.map +0 -1
  145. package/dist/sync/lwwConflictHandler.test.js +0 -78
  146. package/dist/sync/lwwConflictHandler.test.js.map +0 -1
  147. package/dist/sync/pushWrites.test.d.ts +0 -2
  148. package/dist/sync/pushWrites.test.d.ts.map +0 -1
  149. package/dist/sync/pushWrites.test.js +0 -287
  150. package/dist/sync/pushWrites.test.js.map +0 -1
@@ -16,4 +16,44 @@ export const useSyncStatusStore = create()(set => ({
16
16
  })),
17
17
  reset: () => set({ statuses: {} })
18
18
  }));
19
+ /**
20
+ * Rolls the per-collection replication statuses up to a single aggregate plus
21
+ * its display copy. With no collections registered it reports `offline`
22
+ * (local-only, no sync running); otherwise it applies the
23
+ * error > syncing > synced precedence (`idle` counts as syncing -- a collection
24
+ * configured but not yet cycled). Kept beside the status vocabulary so the
25
+ * precedence lives with the store rather than the view; `useSyncStatus` is a
26
+ * thin subscription over it.
27
+ *
28
+ * @param statuses {SyncStatus[]} the per-collection statuses (store values)
29
+ * @returns {{ state: SyncRollup, label: string, title: string }}
30
+ */
31
+ export function deriveSyncRollup(statuses) {
32
+ if (statuses.length === 0) {
33
+ return {
34
+ state: 'offline',
35
+ label: 'Offline',
36
+ title: 'Local-only mode -- no storage sync running'
37
+ };
38
+ }
39
+ if (statuses.includes('error')) {
40
+ return {
41
+ state: 'error',
42
+ label: 'Sync error',
43
+ title: 'A collection failed to sync; retrying'
44
+ };
45
+ }
46
+ if (statuses.includes('syncing') || statuses.includes('idle')) {
47
+ return {
48
+ state: 'syncing',
49
+ label: 'Syncing',
50
+ title: 'Replicating with your wallet storage'
51
+ };
52
+ }
53
+ return {
54
+ state: 'synced',
55
+ label: 'Synced',
56
+ title: 'All collections replicated'
57
+ };
58
+ }
19
59
  //# sourceMappingURL=syncStatusStore.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"syncStatusStore.js","sourceRoot":"","sources":["../../src/storage/syncStatusStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAkBhC,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,EAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClE,QAAQ,EAAE,EAAE;IACZ,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,CAClC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACZ,QAAQ,EAAE,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE;KACxD,CAAC,CAAC;IACL,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;CACnC,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"syncStatusStore.js","sourceRoot":"","sources":["../../src/storage/syncStatusStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAoBhC,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,EAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClE,QAAQ,EAAE,EAAE;IACZ,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,CAClC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACZ,QAAQ,EAAE,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE;KACxD,CAAC,CAAC;IACL,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;CACnC,CAAC,CAAC,CAAA;AASH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAsB;IAKrD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO;YACL,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,4CAA4C;SACpD,CAAA;IACH,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,OAAO;YACL,KAAK,EAAE,OAAO;YACd,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,uCAAuC;SAC/C,CAAA;IACH,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9D,OAAO;YACL,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,sCAAsC;SAC9C,CAAA;IACH,CAAC;IACD,OAAO;QACL,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,4BAA4B;KACpC,CAAA;AACH,CAAC"}
@@ -21,7 +21,9 @@ import type { ZcapClient } from '@interop/ezcap';
21
21
  import type { IZcap } from '@interop/data-integrity-core';
22
22
  import { WasClient } from '@interop/was-client';
23
23
  import type { ParsedGrants } from '../grants.js';
24
- /** The outcome of a best-effort encryption-marker PUT, for diagnostics. */
24
+ /**
25
+ * The outcome of a best-effort encryption-marker PUT, for diagnostics.
26
+ */
25
27
  export interface MarkerResult {
26
28
  collectionId: string;
27
29
  ok: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"wasRemoteStore.d.ts","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,qBAAa,cAAc;IACzB,SAAgB,GAAG,EAAE,SAAS,CAAA;IAC9B,SAAgB,SAAS,EAAE,MAAM,CAAA;IACjC,SAAgB,OAAO,EAAE,MAAM,CAAA;IAC/B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuB;IAEvD,OAAO;IAaP;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,CAAC,EAChB,MAAM,EACN,UAAU,EACX,EAAE;QACD,MAAM,EAAE,YAAY,CAAA;QACpB,UAAU,EAAE,UAAU,CAAA;KACvB,GAAG,cAAc;IASlB;;;;;;;OAOG;IACH,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAI7D;;;;;;;;;OASG;IACG,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAqB3E"}
1
+ {"version":3,"file":"wasRemoteStore.d.ts","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAG/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,qBAAa,cAAc;IACzB,SAAgB,GAAG,EAAE,SAAS,CAAA;IAC9B,SAAgB,SAAS,EAAE,MAAM,CAAA;IACjC,SAAgB,OAAO,EAAE,MAAM,CAAA;IAC/B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuB;IAEvD,OAAO;IAaP;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,CAAC,EAChB,MAAM,EACN,UAAU,EACX,EAAE;QACD,MAAM,EAAE,YAAY,CAAA;QACpB,UAAU,EAAE,UAAU,CAAA;KACvB,GAAG,cAAc;IASlB;;;;;;;OAOG;IACH,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAI7D;;;;;;;;;OASG;IACG,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAmB3E"}
@@ -1,5 +1,6 @@
1
1
  import { WasClient } from '@interop/was-client';
2
2
  import { createEdvEncryption } from '@interop/was-client/edv';
3
+ import { errorStatus, errorMessage } from '../sync/index.js';
3
4
  export class WasRemoteStore {
4
5
  was;
5
6
  serverUrl;
@@ -66,9 +67,8 @@ export class WasRemoteStore {
66
67
  return { collectionId, ok: true, status: response.status };
67
68
  }
68
69
  catch (err) {
69
- const status = err.status ??
70
- err.response?.status;
71
- const message = err instanceof Error ? err.message : String(err);
70
+ const status = errorStatus(err);
71
+ const message = errorMessage(err);
72
72
  return { collectionId, ok: false, status, error: message };
73
73
  }
74
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wasRemoteStore.js","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAW7D,MAAM,OAAO,cAAc;IACT,GAAG,CAAW;IACd,SAAS,CAAQ;IACjB,OAAO,CAAQ;IACd,eAAe,CAAuB;IAEvD,YAAoB,EAClB,GAAG,EACH,MAAM,EAIP;QACC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAA;QACjC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;QAC7B,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,cAAc,CAAA;IAC9C,CAAC;IAED;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,CAAC,EAChB,MAAM,EACN,UAAU,EAIX;QACC,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,UAAU;YACV,UAAU,EAAE,mBAAmB,CAAC,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;SACnE,CAAC,CAAA;QACF,OAAO,IAAI,cAAc,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;IAC5C,CAAC;IAED;;;;;;;OAOG;IACH,oBAAoB,CAAC,YAAoB;QACvC,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAA;IAC3C,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,uBAAuB,CAAC,YAAoB;QAChD,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAA;QAC1D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAA;QAC5D,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBACtC,UAAU;gBACV,IAAI,EAAE,UAAU,IAAI,CAAC,OAAO,IAAI,YAAY,EAAE;gBAC9C,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;aAC1D,CAAC,CAAA;YACF,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;QAC5D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GACT,GAA2B,CAAC,MAAM;gBAClC,GAA0C,CAAC,QAAQ,EAAE,MAAM,CAAA;YAC9D,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAChE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;QAC5D,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"wasRemoteStore.js","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAa5D,MAAM,OAAO,cAAc;IACT,GAAG,CAAW;IACd,SAAS,CAAQ;IACjB,OAAO,CAAQ;IACd,eAAe,CAAuB;IAEvD,YAAoB,EAClB,GAAG,EACH,MAAM,EAIP;QACC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAA;QACjC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;QAC7B,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,cAAc,CAAA;IAC9C,CAAC;IAED;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,CAAC,EAChB,MAAM,EACN,UAAU,EAIX;QACC,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,UAAU;YACV,UAAU,EAAE,mBAAmB,CAAC,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;SACnE,CAAC,CAAA;QACF,OAAO,IAAI,cAAc,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;IAC5C,CAAC;IAED;;;;;;;OAOG;IACH,oBAAoB,CAAC,YAAoB;QACvC,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAA;IAC3C,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,uBAAuB,CAAC,YAAoB;QAChD,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAA;QAC1D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAA;QAC5D,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBACtC,UAAU;gBACV,IAAI,EAAE,UAAU,IAAI,CAAC,OAAO,IAAI,YAAY,EAAE;gBAC9C,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;aAC1D,CAAC,CAAA;YACF,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;QAC5D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;YAC/B,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;YACjC,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;QAC5D,CAAC;IACH,CAAC;CACF"}
@@ -2,29 +2,31 @@
2
2
  * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
3
  */
4
4
  /**
5
- * A thin adapter over a {@link WasSyncPort} that resolves a resource's current
6
- * master state (for the 412 conflict re-read) from the CHANGES-FEED BODY rather
7
- * than the base port's `GET` ETag header.
5
+ * The `get` half of a {@link WasSyncPort}: resolves a resource's current master
6
+ * state (for the 412 conflict re-read) from the CHANGES-FEED BODY. Wraps a
7
+ * {@link WasSyncBasePort} (query + conditional writes) and adds the `get` the
8
+ * conflict assembler needs, producing a full {@link WasSyncPort}.
8
9
  *
9
- * Why this exists: the mutable-head model settles a 412 by re-reading the
10
- * resource's current `version` and comparing payloads (LWW). The verbatim
11
- * `wasSyncPort.get()` reads that `version` from the response's `etag` header --
12
- * which a browser hides on a CROSS-ORIGIN response unless the server sends
13
- * `Access-Control-Expose-Headers: etag`. A server that does not expose it makes
14
- * cross-origin `get()` report `version: 0`, which makes the loser of a push race
15
- * re-push forever with a stale `If-Match` and never converge. The `changes` feed
16
- * carries `version` (and `data`, `_deleted`, ...) in the JSON BODY, which CORS
17
- * never strips, so resolving the master from the feed is both correct and
18
- * origin-independent. Normal pull/push are unaffected (they already read
19
- * `version` from the feed body), so only `get` is overridden.
10
+ * Why the feed body rather than a `GET` ETag header: the mutable-head model
11
+ * settles a 412 by re-reading the resource's current `version` and comparing
12
+ * payloads (LWW). A raw `GET` reads that `version` from the response's `etag`
13
+ * header -- which a browser hides on a CROSS-ORIGIN response unless the server
14
+ * sends `Access-Control-Expose-Headers: etag`. A server that does not expose it
15
+ * would make a cross-origin re-read report `version: 0`, so the loser of a push
16
+ * race would re-push forever with a stale `If-Match` and never converge. The
17
+ * `changes` feed carries `version` (and `data`, `_deleted`, ...) in the JSON
18
+ * BODY, which CORS never strips, so resolving the master from the feed is both
19
+ * correct and origin-independent. Normal pull/push are unaffected (they already
20
+ * read `version` from the feed body).
20
21
  */
21
- import type { WasSyncPort } from './types.js';
22
+ import type { WasSyncBasePort, WasSyncPort } from './types.js';
22
23
  /**
23
- * Wraps a base port so its `get` resolves the master state from the changes feed.
24
- * `query`, `putContent`, `deleteContent`, and `putMeta` pass straight through.
24
+ * Wraps a base port with a `get` that resolves the master state from the changes
25
+ * feed. `query`, `putContent`, `deleteContent`, and `putMeta` pass straight
26
+ * through.
25
27
  *
26
- * @param base {WasSyncPort}
28
+ * @param base {WasSyncBasePort}
27
29
  * @returns {WasSyncPort}
28
30
  */
29
- export declare function withFeedMasterRead(base: WasSyncPort): WasSyncPort;
31
+ export declare function withFeedMasterRead(base: WasSyncBasePort): WasSyncPort;
30
32
  //# sourceMappingURL=feedMasterPort.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"feedMasterPort.d.ts","sourceRoot":"","sources":["../../src/sync/feedMasterPort.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAIV,WAAW,EAEZ,MAAM,YAAY,CAAA;AA4BnB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAyBjE"}
1
+ {"version":3,"file":"feedMasterPort.d.ts","sourceRoot":"","sources":["../../src/sync/feedMasterPort.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAIV,eAAe,EACf,WAAW,EAEZ,MAAM,YAAY,CAAA;AA4BnB;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CAuCrE"}
@@ -23,10 +23,11 @@ function toMasterState(doc) {
23
23
  return master;
24
24
  }
25
25
  /**
26
- * Wraps a base port so its `get` resolves the master state from the changes feed.
27
- * `query`, `putContent`, `deleteContent`, and `putMeta` pass straight through.
26
+ * Wraps a base port with a `get` that resolves the master state from the changes
27
+ * feed. `query`, `putContent`, `deleteContent`, and `putMeta` pass straight
28
+ * through.
28
29
  *
29
- * @param base {WasSyncPort}
30
+ * @param base {WasSyncBasePort}
30
31
  * @returns {WasSyncPort}
31
32
  */
32
33
  export function withFeedMasterRead(base) {
@@ -46,12 +47,24 @@ export function withFeedMasterRead(base) {
46
47
  if (found) {
47
48
  return toMasterState(found);
48
49
  }
50
+ // Reached the end of the feed without finding it: the resource is
51
+ // genuinely absent (a delete/delete race), so report `null` and let the
52
+ // conflict assembler synthesize a tombstone.
49
53
  if (next === null || documents.length === 0) {
50
- break;
54
+ return null;
51
55
  }
52
56
  checkpoint = next;
53
57
  }
54
- return null;
58
+ // The page budget ran out before the feed's end. The feed is keyset-
59
+ // ordered ascending on `updatedAt`, so a just-conflicted resource sits at
60
+ // the tail -- the least-reachable position -- and may well be past the
61
+ // cap. Reporting `null` here would fabricate a false tombstone in the
62
+ // conflict assembler and drop the winner's payload. Throw instead: RxDB
63
+ // treats a thrown push-handler error as retryable, so the whole
64
+ // replication cycle retries rather than diverging.
65
+ throw new Error(`Feed master re-read for resource "${id}" exhausted its ` +
66
+ `${MAX_PAGES}-page scan budget without reaching the end of the ` +
67
+ `changes feed; retrying.`);
55
68
  }
56
69
  };
57
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"feedMasterPort.js","sourceRoot":"","sources":["../../src/sync/feedMasterPort.ts"],"names":[],"mappings":"AA4BA,gFAAgF;AAChF,oEAAoE;AACpE,MAAM,SAAS,GAAG,EAAE,CAAA;AACpB,MAAM,SAAS,GAAG,GAAG,CAAA;AAErB;;GAEG;AACH,SAAS,aAAa,CAAC,GAAY;IACjC,MAAM,MAAM,GAAgB;QAC1B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,OAAO,EAAE,GAAG,CAAC,QAAQ;KACtB,CAAA;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAY,CAAA;IAChC,CAAC;IACD,IAAI,GAAG,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAA;IACtC,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,MAAc,CAAA;IACpC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAiB;IAClD,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QACtC,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5C,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;QAChC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE;YACd,IAAI,UAAsC,CAAA;YAC1C,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;gBAC5C,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;oBACvD,GAAG,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,CAAC;oBAC/C,KAAK,EAAE,SAAS;iBACjB,CAAC,CAAA;gBACF,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;gBAClD,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,aAAa,CAAC,KAAK,CAAC,CAAA;gBAC7B,CAAC;gBACD,IAAI,IAAI,KAAK,IAAI,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC5C,MAAK;gBACP,CAAC;gBACD,UAAU,GAAG,IAAI,CAAA;YACnB,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC;KACF,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"feedMasterPort.js","sourceRoot":"","sources":["../../src/sync/feedMasterPort.ts"],"names":[],"mappings":"AA8BA,gFAAgF;AAChF,oEAAoE;AACpE,MAAM,SAAS,GAAG,EAAE,CAAA;AACpB,MAAM,SAAS,GAAG,GAAG,CAAA;AAErB;;GAEG;AACH,SAAS,aAAa,CAAC,GAAY;IACjC,MAAM,MAAM,GAAgB;QAC1B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,OAAO,EAAE,GAAG,CAAC,QAAQ;KACtB,CAAA;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAY,CAAA;IAChC,CAAC;IACD,IAAI,GAAG,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAA;IACtC,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,MAAc,CAAA;IACpC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAqB;IACtD,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5B,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QACtC,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5C,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;QAChC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE;YACd,IAAI,UAAsC,CAAA;YAC1C,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;gBAC5C,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;oBACvD,GAAG,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,CAAC;oBAC/C,KAAK,EAAE,SAAS;iBACjB,CAAC,CAAA;gBACF,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;gBAClD,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,aAAa,CAAC,KAAK,CAAC,CAAA;gBAC7B,CAAC;gBACD,kEAAkE;gBAClE,wEAAwE;gBACxE,6CAA6C;gBAC7C,IAAI,IAAI,KAAK,IAAI,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC5C,OAAO,IAAI,CAAA;gBACb,CAAC;gBACD,UAAU,GAAG,IAAI,CAAA;YACnB,CAAC;YACD,qEAAqE;YACrE,0EAA0E;YAC1E,uEAAuE;YACvE,sEAAsE;YACtE,wEAAwE;YACxE,gEAAgE;YAChE,mDAAmD;YACnD,MAAM,IAAI,KAAK,CACb,qCAAqC,EAAE,kBAAkB;gBACvD,GAAG,SAAS,oDAAoD;gBAChE,yBAAyB,CAC5B,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -11,9 +11,9 @@
11
11
  export { createWasReplication } from './wasReplication.js';
12
12
  export { syncedDocSchema } from './syncedDocSchema.js';
13
13
  export { createPullHandler, wireDocToRxDoc } from './changesQuery.js';
14
- export { createPushHandler, formatEtag } from './pushWrites.js';
15
- export { WasSyncConflictError, type Json, type SyncCheckpoint, type WireDoc, type SyncedDoc, type MasterState, type WasSyncPort } from './types.js';
16
- export { createWasSyncPort } from './wasSyncPort.js';
14
+ export { createPushHandler, formatEtag, type PushWriteAck } from './pushWrites.js';
15
+ export { WasSyncConflictError, WasSyncAuthError, type Json, type SyncCheckpoint, type WireDoc, type SyncedDoc, type MasterState, type WasSyncBasePort, type WasSyncPort } from './types.js';
16
+ export { createWasSyncPort, errorStatus, errorMessage } from './wasSyncPort.js';
17
17
  export { withFeedMasterRead } from './feedMasterPort.js';
18
18
  export { createDocCipher, isEncryptedEnvelope, type DocCipher } from './docCipher.js';
19
19
  export { makeLwwConflictHandler } from './lwwConflictHandler.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,EACL,oBAAoB,EACpB,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,WAAW,EACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA"}
@@ -12,8 +12,8 @@ export { createWasReplication } from './wasReplication.js';
12
12
  export { syncedDocSchema } from './syncedDocSchema.js';
13
13
  export { createPullHandler, wireDocToRxDoc } from './changesQuery.js';
14
14
  export { createPushHandler, formatEtag } from './pushWrites.js';
15
- export { WasSyncConflictError } from './types.js';
16
- export { createWasSyncPort } from './wasSyncPort.js';
15
+ export { WasSyncConflictError, WasSyncAuthError } from './types.js';
16
+ export { createWasSyncPort, errorStatus, errorMessage } from './wasSyncPort.js';
17
17
  export { withFeedMasterRead } from './feedMasterPort.js';
18
18
  export { createDocCipher, isEncryptedEnvelope } from './docCipher.js';
19
19
  export { makeLwwConflictHandler } from './lwwConflictHandler.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC/D,OAAO,EACL,oBAAoB,EAOrB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,eAAe,EACf,mBAAmB,EAEpB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,EACL,iBAAiB,EACjB,UAAU,EAEX,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAQjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,eAAe,EACf,mBAAmB,EAEpB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA"}
@@ -19,10 +19,42 @@
19
19
  * every replica compares that same master against its own local edit, and the
20
20
  * `payloadWins` comparator is a total order over `(updatedAt, deviceId)`, so the
21
21
  * globally-latest payload wins on every replica with no coordination.
22
+ *
23
+ * The resolution rules, in order:
24
+ *
25
+ * 1. Version-only conflict: when the real master's whole content (`data` +
26
+ * `custom` + `_deleted`) still equals the assumed master's, the server holds
27
+ * nothing newer than what this replica last synced -- the 412 came from a
28
+ * stale `If-Match` (typically our own earlier write racing its feed echo).
29
+ * The local state (edit or tombstone) is re-asserted and re-pushed against
30
+ * the corrected version. Without this rule a local delete would be dropped by
31
+ * rule 5 and the entity would silently resurrect. `custom` MUST be part of
32
+ * this comparison, or a concurrent metadata-only edit committed on the server
33
+ * would be misclassified here and clobbered (rule 2 is what settles it).
34
+ * 2. Metadata conflict: `data` and `_deleted` are unchanged from the assumed
35
+ * master but `custom` differs -- a metadata-only edit committed on the server
36
+ * since this replica last synced (another device won the `/meta` race).
37
+ * Metadata carries no LWW timestamp of its own (the payload `updatedAt` lives
38
+ * in the encrypted `data`, which is equal on both sides here), so there is no
39
+ * payload to compare; the sound, replica-independent default is that the
40
+ * server-committed state wins -- the real master is adopted for `custom`.
41
+ * Without this rule the equal-`data` case would fall through to rule 3, where
42
+ * the two payloads compare equal and the tie keeps the local (stale) `custom`,
43
+ * silently clobbering the committed metadata.
44
+ * 3. Both sides carry an LWW payload: pure payload LWW via `payloadWins`.
45
+ * 4. A live local edit vs an incomparable remote (e.g. a remote tombstone):
46
+ * the edit wins and is re-pushed (resurrection).
47
+ * 5. Everything else -- a local tombstone vs a REAL remote content change, or
48
+ * both sides incomparable: the master wins. Together with rule 4 this makes
49
+ * the delete-vs-concurrent-edit rule "the edit wins" on every replica: a
50
+ * tombstone carries no LWW payload of its own, so a genuine racing edit
51
+ * deterministically survives, whichever write reached the server first.
22
52
  */
23
53
  import type { WithDeleted } from 'rxdb/plugins/core';
24
54
  import type { Json, SyncedDoc } from './types.js';
25
- /** The LWW fields read out of a decrypted entity payload. */
55
+ /**
56
+ * The LWW fields read out of a decrypted entity payload.
57
+ */
26
58
  interface LwwPayload {
27
59
  updatedAt: string;
28
60
  deviceId: string;
@@ -40,9 +72,10 @@ interface LwwPayload {
40
72
  */
41
73
  export declare function makeLwwConflictHandler(decrypt: (envelope: Json) => Promise<Json>, payloadWins?: (remote: LwwPayload, local: LwwPayload) => boolean): {
42
74
  isEqual(a: WithDeleted<SyncedDoc>, b: WithDeleted<SyncedDoc>): boolean;
43
- resolve({ realMasterState, newDocumentState }: {
75
+ resolve({ realMasterState, newDocumentState, assumedMasterState }: {
44
76
  realMasterState: WithDeleted<SyncedDoc>;
45
77
  newDocumentState: WithDeleted<SyncedDoc>;
78
+ assumedMasterState?: WithDeleted<SyncedDoc>;
46
79
  }): Promise<WithDeleted<SyncedDoc>>;
47
80
  };
48
81
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"lwwConflictHandler.d.ts","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAGjD,6DAA6D;AAC7D,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAiCD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,EAC1C,WAAW,GAAE,CACX,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,UAAU,KACd,OAA2B;eAKnB,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW,CAAC,SAAS,CAAC,GAAG,OAAO;mDAOnE;QACD,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;QACvC,gBAAgB,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;KACzC,GAAG,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;EAmBtC"}
1
+ {"version":3,"file":"lwwConflictHandler.d.ts","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAGjD;;GAEG;AACH,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,EAC1C,WAAW,GAAE,CACX,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,UAAU,KACd,OAA2B;eAQnB,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW,CAAC,SAAS,CAAC,GAAG,OAAO;uEAcnE;QACD,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;QACvC,gBAAgB,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;QACxC,kBAAkB,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;KAC5C,GAAG,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;EAoDtC"}
@@ -1,5 +1,7 @@
1
1
  import { remotePayloadWins } from './lww.js';
2
- /** Structural equality of two opaque bodies (used for the fast `isEqual`). */
2
+ /**
3
+ * Structural equality of two opaque bodies (used for the fast `isEqual`).
4
+ */
3
5
  function bodiesEqual(a, b) {
4
6
  return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
5
7
  }
@@ -37,27 +39,63 @@ async function lwwFieldsOf(doc, decrypt) {
37
39
  */
38
40
  export function makeLwwConflictHandler(decrypt, payloadWins = remotePayloadWins) {
39
41
  return {
40
- // Non-async and fast, as RxDB requires: no real conflict when the opaque
41
- // content + deletion flag already agree (e.g. our own write echoing back).
42
+ // Non-async and fast, as RxDB requires. The server revisions (`version` /
43
+ // `metaVersion`) participate deliberately: our own write's feed echo comes
44
+ // back byte-identical but one revision ahead, and it must NOT compare
45
+ // equal, or the higher version is never adopted and every later
46
+ // conditional write sends a stale `If-Match` (a guaranteed 412).
42
47
  isEqual(a, b) {
43
- return a._deleted === b._deleted && bodiesEqual(a.data, b.data);
48
+ return (a._deleted === b._deleted &&
49
+ a.version === b.version &&
50
+ a.metaVersion === b.metaVersion &&
51
+ bodiesEqual(a.data, b.data) &&
52
+ bodiesEqual(a.custom, b.custom));
44
53
  },
45
- async resolve({ realMasterState, newDocumentState }) {
54
+ async resolve({ realMasterState, newDocumentState, assumedMasterState }) {
55
+ // Rule 1 -- version-only conflict: the master's whole content (`data` +
56
+ // `custom`) is exactly what this replica last synced (only the revision
57
+ // moved, e.g. our own write racing its feed echo), so nothing remote is
58
+ // actually newer. Re-assert the local state -- crucially including a local
59
+ // TOMBSTONE, which rule 5 would otherwise drop (the silent-resurrection
60
+ // bug). `custom` is part of this equality so a concurrent metadata-only
61
+ // edit is NOT misclassified here (rule 2 handles it).
62
+ if (assumedMasterState !== undefined &&
63
+ realMasterState._deleted === assumedMasterState._deleted &&
64
+ bodiesEqual(realMasterState.data, assumedMasterState.data) &&
65
+ bodiesEqual(realMasterState.custom, assumedMasterState.custom)) {
66
+ return newDocumentState;
67
+ }
68
+ // Rule 2 -- metadata conflict: `data` and `_deleted` are unchanged from
69
+ // the assumed master, but `custom` moved on the server (a metadata-only
70
+ // edit that won the `/meta` race on another device). Metadata has no LWW
71
+ // timestamp of its own -- the payload `updatedAt` lives in `data`, equal
72
+ // on both sides here -- so the sound, replica-independent default is that
73
+ // the server-committed state wins: adopt the real master for `custom`.
74
+ // Without this rule the equal-`data` case would reach rule 3 with two
75
+ // equal payloads and the tie would keep the local (stale) metadata.
76
+ if (assumedMasterState !== undefined &&
77
+ realMasterState._deleted === assumedMasterState._deleted &&
78
+ bodiesEqual(realMasterState.data, assumedMasterState.data) &&
79
+ !bodiesEqual(realMasterState.custom, assumedMasterState.custom)) {
80
+ return realMasterState;
81
+ }
46
82
  const [remote, local] = await Promise.all([
47
83
  lwwFieldsOf(realMasterState, decrypt),
48
84
  lwwFieldsOf(newDocumentState, decrypt)
49
85
  ]);
50
- // Both sides comparable: pure payload LWW.
86
+ // Rule 3 -- both sides comparable: pure payload LWW.
51
87
  if (remote && local) {
52
88
  return payloadWins(remote, local) ? realMasterState : newDocumentState;
53
89
  }
54
- // A live local edit against a remote tombstone (or otherwise
90
+ // Rule 4 -- a live local edit against a remote tombstone (or otherwise
55
91
  // incomparable remote): keep the edit (resurrect / re-assert the write).
56
92
  if (local && !remote) {
57
93
  return newDocumentState;
58
94
  }
59
- // Everything else (local tombstone, both incomparable): the master wins,
60
- // which is deterministic and convergent across replicas.
95
+ // Rule 5 -- everything else (a local tombstone racing a REAL remote
96
+ // content change, or both incomparable): the master wins, which is
97
+ // deterministic and convergent across replicas (the edit survives the
98
+ // delete on every device).
61
99
  return realMasterState;
62
100
  }
63
101
  };
@@ -1 +1 @@
1
- {"version":3,"file":"lwwConflictHandler.js","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAQ5C,8EAA8E;AAC9E,SAAS,WAAW,CAAC,CAAmB,EAAE,CAAmB;IAC3D,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,IAAI,CAAC,CAAA;AAChE,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,WAAW,CACxB,GAA2B,EAC3B,OAA0C;IAE1C,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAwB,CAAA;QAChE,IACE,OAAO,OAAO,CAAC,SAAS,KAAK,QAAQ;YACrC,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ,EACpC,CAAC;YACD,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAA;QACrE,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAA0C,EAC1C,cAGe,iBAAiB;IAEhC,OAAO;QACL,yEAAyE;QACzE,2EAA2E;QAC3E,OAAO,CAAC,CAAyB,EAAE,CAAyB;YAC1D,OAAO,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,IAAI,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;QACjE,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,EACZ,eAAe,EACf,gBAAgB,EAIjB;YACC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACxC,WAAW,CAAC,eAAe,EAAE,OAAO,CAAC;gBACrC,WAAW,CAAC,gBAAgB,EAAE,OAAO,CAAC;aACvC,CAAC,CAAA;YACF,2CAA2C;YAC3C,IAAI,MAAM,IAAI,KAAK,EAAE,CAAC;gBACpB,OAAO,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,gBAAgB,CAAA;YACxE,CAAC;YACD,6DAA6D;YAC7D,yEAAyE;YACzE,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;gBACrB,OAAO,gBAAgB,CAAA;YACzB,CAAC;YACD,yEAAyE;YACzE,yDAAyD;YACzD,OAAO,eAAe,CAAA;QACxB,CAAC;KACF,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"lwwConflictHandler.js","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"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"}
@@ -20,16 +20,29 @@
20
20
  * `/meta` write to a resource that does not yet exist.
21
21
  *
22
22
  * RxDB's push contract asks only for *conflicts* back (the current master state
23
- * of each rejected row); a successful write's new `version` / `metaVersion` is
24
- * not returned here -- it is picked up on the next pull, where our own write
25
- * echoes back as a remote change (idempotent, since the bodies are
26
- * byte-identical). We deliberately do not capture the `204` ETag to short-circuit
27
- * that echo: doing so would require a side write to the local revision fields
28
- * from inside the push handler, which risks a re-push loop, for no correctness
29
- * gain on immutable content-addressed collections.
23
+ * of each rejected row), so a successful write's new `version` / `metaVersion`
24
+ * is reported out-of-band: the response ETag of each accepted write is captured
25
+ * and handed to the optional `onWriteAccepted` callback, which writes the acked
26
+ * revision back into the local row (see `createWasReplication`). Without that
27
+ * write-back the local `version` would stay one revision behind the server and
28
+ * every subsequent conditional write would send a stale `If-Match` and 412. The
29
+ * write-back only touches the revision fields (never `data` / `updatedAt`), so
30
+ * the follow-up push cycle it triggers finds nothing changed to write and
31
+ * settles -- no re-push loop.
30
32
  */
31
33
  import type { WithDeleted } from 'rxdb/plugins/core';
32
34
  import type { SyncedDoc, WasSyncPort } from './types.js';
35
+ /**
36
+ * The acked server revisions of one row's accepted writes: the new content
37
+ * `version` (from a `PUT /:id` or `DELETE /:id` response ETag) and/or the new
38
+ * `metaVersion` (from a `PUT /:id/meta` response ETag). Absent fields mean the
39
+ * corresponding write did not run or its response carried no ETag.
40
+ */
41
+ export interface PushWriteAck {
42
+ id: string;
43
+ version?: number;
44
+ metaVersion?: number;
45
+ }
33
46
  /**
34
47
  * Formats a master revision (`version` or `metaVersion`) as the quoted strong
35
48
  * ETag the server compares `If-Match` against (revision `3` becomes `"3"`).
@@ -45,12 +58,18 @@ export declare function formatEtag(revision: number): string;
45
58
  * Rows are pushed concurrently; if any non-conflict error is thrown the whole
46
59
  * batch rejects (RxDB re-sends it later), matching RxDB's all-or-nothing retry.
47
60
  *
61
+ * Each accepted write's acked server revision(s) are handed to
62
+ * `onWriteAccepted` (when supplied) as soon as that row's writes settle, so the
63
+ * caller can write the new `version` / `metaVersion` back into the local row
64
+ * and keep subsequent conditional writes' `If-Match` in step with the server.
65
+ *
48
66
  * @param port {WasSyncPort}
67
+ * @param [onWriteAccepted] {(ack: PushWriteAck) => Promise<void>}
49
68
  * @returns {(rows: Array<{ newDocumentState: WithDeleted<SyncedDoc>,
50
69
  * assumedMasterState?: WithDeleted<SyncedDoc> }>) =>
51
70
  * Promise<WithDeleted<SyncedDoc>[]>}
52
71
  */
53
- export declare function createPushHandler(port: WasSyncPort): (rows: Array<{
72
+ export declare function createPushHandler(port: WasSyncPort, onWriteAccepted?: (ack: PushWriteAck) => Promise<void>): (rows: Array<{
54
73
  newDocumentState: WithDeleted<SyncedDoc>;
55
74
  assumedMasterState?: WithDeleted<SyncedDoc>;
56
75
  }>) => Promise<WithDeleted<SyncedDoc>[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"pushWrites.d.ts","sourceRoot":"","sources":["../../src/sync/pushWrites.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAqB,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG3E;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AA0JD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,IAE/C,MAAM,KAAK,CAAC;IACV,gBAAgB,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;IACxC,kBAAkB,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;CAC5C,CAAC,KACD,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC,CAcrC"}
1
+ {"version":3,"file":"pushWrites.d.ts","sourceRoot":"","sources":["../../src/sync/pushWrites.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAqB,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG3E;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AA4KD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EACjB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,IAGpD,MAAM,KAAK,CAAC;IACV,gBAAgB,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;IACxC,kBAAkB,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;CAC5C,CAAC,KACD,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC,CAoBrC"}
@@ -67,35 +67,41 @@ async function assembleConflict({ port, id, fallbackUpdatedAt, fallbackVersion }
67
67
  /**
68
68
  * Sends one local change to the remote Collection as up to two conditional
69
69
  * writes (content, then metadata). Returns the master-state conflict entry on a
70
- * `412` at either step, or `null` on success.
70
+ * `412` at either step (with `ack: null`), or the accepted writes' acked
71
+ * revisions on success (`ack: null` when no response carried a revision).
71
72
  *
72
73
  * @param options {object}
73
74
  * @param options.port {WasSyncPort}
74
75
  * @param options.newDocumentState {WithDeleted<SyncedDoc>}
75
76
  * @param [options.assumedMasterState] {WithDeleted<SyncedDoc>}
76
- * @returns {Promise<WithDeleted<SyncedDoc> | null>}
77
+ * @returns {Promise<{ conflict: WithDeleted<SyncedDoc> | null,
78
+ * ack: PushWriteAck | null }>}
77
79
  */
78
80
  async function pushRow({ port, newDocumentState, assumedMasterState }) {
79
81
  const { id } = newDocumentState;
80
82
  const assumedVersion = assumedMasterState?.version;
81
83
  const isCreate = assumedMasterState === undefined;
82
84
  try {
85
+ const ack = { id };
83
86
  if (newDocumentState._deleted) {
84
87
  // Delete supersedes any metadata write: drop the content, tombstone wins.
85
- await port.deleteContent({
88
+ const ackedVersion = await port.deleteContent({
86
89
  id,
87
90
  ...(assumedVersion !== undefined && {
88
91
  ifMatch: formatEtag(assumedVersion)
89
92
  })
90
93
  });
91
- return null;
94
+ if (ackedVersion !== undefined) {
95
+ ack.version = ackedVersion;
96
+ }
97
+ return { conflict: null, ack: ack.version !== undefined ? ack : null };
92
98
  }
93
99
  // Content half: write on create, or when the content body changed. For a
94
100
  // content-addressed collection the update case never fires (an immutable
95
101
  // body for a stable id), but it is handled for generality.
96
102
  const contentChanged = isCreate || !bodiesEqual(newDocumentState.data, assumedMasterState?.data);
97
103
  if (contentChanged) {
98
- await port.putContent({
104
+ const ackedVersion = await port.putContent({
99
105
  id,
100
106
  data: newDocumentState.data ?? null,
101
107
  ...(isCreate
@@ -104,30 +110,38 @@ async function pushRow({ port, newDocumentState, assumedMasterState }) {
104
110
  ifMatch: formatEtag(assumedVersion)
105
111
  })
106
112
  });
113
+ if (ackedVersion !== undefined) {
114
+ ack.version = ackedVersion;
115
+ }
107
116
  }
108
117
  // Metadata half: write when the resource has metadata and it changed. On a
109
118
  // create this runs after the content write (the resource must exist first).
110
119
  const metadataChanged = !bodiesEqual(newDocumentState.custom, assumedMasterState?.custom);
111
120
  if (newDocumentState.custom !== undefined && metadataChanged) {
112
121
  const assumedMetaVersion = assumedMasterState?.metaVersion;
113
- await port.putMeta({
122
+ const ackedMetaVersion = await port.putMeta({
114
123
  id,
115
124
  custom: newDocumentState.custom,
116
125
  ...(assumedMetaVersion !== undefined
117
126
  ? { ifMatch: formatEtag(assumedMetaVersion) }
118
127
  : { ifNoneMatch: true })
119
128
  });
129
+ if (ackedMetaVersion !== undefined) {
130
+ ack.metaVersion = ackedMetaVersion;
131
+ }
120
132
  }
121
- return null;
133
+ const hasAck = ack.version !== undefined || ack.metaVersion !== undefined;
134
+ return { conflict: null, ack: hasAck ? ack : null };
122
135
  }
123
136
  catch (err) {
124
137
  if (err instanceof WasSyncConflictError) {
125
- return assembleConflict({
138
+ const conflict = await assembleConflict({
126
139
  port,
127
140
  id,
128
141
  fallbackUpdatedAt: newDocumentState.updatedAt,
129
142
  fallbackVersion: assumedVersion ?? newDocumentState.version
130
143
  });
144
+ return { conflict, ack: null };
131
145
  }
132
146
  // Any non-conflict error (network, 5xx, auth) propagates so RxDB retries
133
147
  // the whole batch with backoff.
@@ -141,19 +155,33 @@ async function pushRow({ port, newDocumentState, assumedMasterState }) {
141
155
  * Rows are pushed concurrently; if any non-conflict error is thrown the whole
142
156
  * batch rejects (RxDB re-sends it later), matching RxDB's all-or-nothing retry.
143
157
  *
158
+ * Each accepted write's acked server revision(s) are handed to
159
+ * `onWriteAccepted` (when supplied) as soon as that row's writes settle, so the
160
+ * caller can write the new `version` / `metaVersion` back into the local row
161
+ * and keep subsequent conditional writes' `If-Match` in step with the server.
162
+ *
144
163
  * @param port {WasSyncPort}
164
+ * @param [onWriteAccepted] {(ack: PushWriteAck) => Promise<void>}
145
165
  * @returns {(rows: Array<{ newDocumentState: WithDeleted<SyncedDoc>,
146
166
  * assumedMasterState?: WithDeleted<SyncedDoc> }>) =>
147
167
  * Promise<WithDeleted<SyncedDoc>[]>}
148
168
  */
149
- export function createPushHandler(port) {
169
+ export function createPushHandler(port, onWriteAccepted) {
150
170
  return async function push(rows) {
151
- const results = await Promise.all(rows.map(row => pushRow({
152
- port,
153
- newDocumentState: row.newDocumentState,
154
- assumedMasterState: row.assumedMasterState
155
- })));
156
- return results.filter((conflict) => conflict !== null);
171
+ const results = await Promise.all(rows.map(async (row) => {
172
+ const result = await pushRow({
173
+ port,
174
+ newDocumentState: row.newDocumentState,
175
+ assumedMasterState: row.assumedMasterState
176
+ });
177
+ if (result.ack !== null && onWriteAccepted !== undefined) {
178
+ await onWriteAccepted(result.ack);
179
+ }
180
+ return result;
181
+ }));
182
+ return results
183
+ .map(result => result.conflict)
184
+ .filter((conflict) => conflict !== null);
157
185
  };
158
186
  }
159
187
  //# sourceMappingURL=pushWrites.js.map