velocious 1.0.591 → 1.0.593

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 (118) hide show
  1. package/README.md +9 -1
  2. package/build/configuration-types.js +1 -1
  3. package/build/database/drivers/base.js +53 -34
  4. package/build/database/drivers/mssql/index.js +32 -0
  5. package/build/database/drivers/mysql/index.js +17 -0
  6. package/build/database/drivers/pgsql/index.js +11 -0
  7. package/build/database/drivers/sqlite/base.js +12 -0
  8. package/build/database/drivers/sqlite/connection-sql-js.js +3 -2
  9. package/build/database/drivers/sqlite/index.js +7 -1
  10. package/build/database/drivers/sqlite/index.native.js +9 -3
  11. package/build/database/drivers/sqlite/index.web.js +6 -3
  12. package/build/environment-handlers/base.js +18 -0
  13. package/build/environment-handlers/node.js +32 -0
  14. package/build/src/configuration-types.d.ts +2 -2
  15. package/build/src/configuration-types.js +2 -2
  16. package/build/src/database/drivers/base.d.ts +11 -0
  17. package/build/src/database/drivers/base.d.ts.map +1 -1
  18. package/build/src/database/drivers/base.js +54 -37
  19. package/build/src/database/drivers/mssql/index.d.ts +7 -0
  20. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  21. package/build/src/database/drivers/mssql/index.js +15 -1
  22. package/build/src/database/drivers/mysql/index.d.ts +7 -0
  23. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  24. package/build/src/database/drivers/mysql/index.js +15 -1
  25. package/build/src/database/drivers/pgsql/index.d.ts +6 -0
  26. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  27. package/build/src/database/drivers/pgsql/index.js +10 -1
  28. package/build/src/database/drivers/sqlite/base.d.ts +7 -0
  29. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  30. package/build/src/database/drivers/sqlite/base.js +11 -1
  31. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +4 -1
  32. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  33. package/build/src/database/drivers/sqlite/connection-sql-js.js +4 -3
  34. package/build/src/database/drivers/sqlite/index.d.ts +2 -1
  35. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  36. package/build/src/database/drivers/sqlite/index.js +7 -2
  37. package/build/src/database/drivers/sqlite/index.native.d.ts +2 -1
  38. package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
  39. package/build/src/database/drivers/sqlite/index.native.js +9 -4
  40. package/build/src/database/drivers/sqlite/index.web.d.ts +2 -1
  41. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  42. package/build/src/database/drivers/sqlite/index.web.js +7 -4
  43. package/build/src/environment-handlers/base.d.ts +16 -0
  44. package/build/src/environment-handlers/base.d.ts.map +1 -1
  45. package/build/src/environment-handlers/base.js +17 -1
  46. package/build/src/environment-handlers/node.d.ts +20 -0
  47. package/build/src/environment-handlers/node.d.ts.map +1 -1
  48. package/build/src/environment-handlers/node.js +29 -1
  49. package/build/src/sync/conflict-strategy.d.ts.map +1 -1
  50. package/build/src/sync/conflict-strategy.js +7 -1
  51. package/build/src/sync/local-mutation-log.d.ts +10 -0
  52. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  53. package/build/src/sync/local-mutation-log.js +21 -1
  54. package/build/src/sync/sync-api-client-types.d.ts +15 -0
  55. package/build/src/sync/sync-api-client-types.d.ts.map +1 -1
  56. package/build/src/sync/sync-api-client-types.js +4 -1
  57. package/build/src/sync/sync-api-client.d.ts +91 -0
  58. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  59. package/build/src/sync/sync-api-client.js +184 -1
  60. package/build/src/sync/sync-client-types.d.ts +57 -0
  61. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  62. package/build/src/sync/sync-client-types.js +16 -1
  63. package/build/src/sync/sync-client.d.ts +56 -7
  64. package/build/src/sync/sync-client.d.ts.map +1 -1
  65. package/build/src/sync/sync-client.js +180 -15
  66. package/build/src/sync/sync-envelope-replay-service.d.ts +31 -2
  67. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  68. package/build/src/sync/sync-envelope-replay-service.js +98 -16
  69. package/build/src/sync/sync-model-change-feed-service.d.ts.map +1 -1
  70. package/build/src/sync/sync-model-change-feed-service.js +3 -4
  71. package/build/src/sync/sync-replay-persisted-data.d.ts +26 -0
  72. package/build/src/sync/sync-replay-persisted-data.d.ts.map +1 -0
  73. package/build/src/sync/sync-replay-persisted-data.js +43 -0
  74. package/build/src/testing/shared-transaction-broker.d.ts +14 -0
  75. package/build/src/testing/shared-transaction-broker.d.ts.map +1 -1
  76. package/build/src/testing/shared-transaction-broker.js +44 -7
  77. package/build/src/testing/shared-transaction-connection-coordinator.d.ts +5 -4
  78. package/build/src/testing/shared-transaction-connection-coordinator.d.ts.map +1 -1
  79. package/build/src/testing/shared-transaction-connection-coordinator.js +21 -10
  80. package/build/sync/conflict-strategy.js +3 -0
  81. package/build/sync/local-mutation-log.js +24 -0
  82. package/build/sync/sync-api-client-types.js +3 -0
  83. package/build/sync/sync-api-client.js +203 -0
  84. package/build/sync/sync-client-types.js +16 -0
  85. package/build/sync/sync-client.js +198 -14
  86. package/build/sync/sync-envelope-replay-service.js +109 -12
  87. package/build/sync/sync-model-change-feed-service.js +2 -4
  88. package/build/sync/sync-replay-persisted-data.js +50 -0
  89. package/build/testing/shared-transaction-broker.js +48 -7
  90. package/build/testing/shared-transaction-connection-coordinator.js +23 -9
  91. package/build/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +1 -1
  93. package/scripts/browser-test-session.js +489 -0
  94. package/scripts/prewarm-chromedriver.js +3 -22
  95. package/scripts/test-browser.js +47 -15
  96. package/src/configuration-types.js +1 -1
  97. package/src/database/drivers/base.js +53 -34
  98. package/src/database/drivers/mssql/index.js +32 -0
  99. package/src/database/drivers/mysql/index.js +17 -0
  100. package/src/database/drivers/pgsql/index.js +11 -0
  101. package/src/database/drivers/sqlite/base.js +12 -0
  102. package/src/database/drivers/sqlite/connection-sql-js.js +3 -2
  103. package/src/database/drivers/sqlite/index.js +7 -1
  104. package/src/database/drivers/sqlite/index.native.js +9 -3
  105. package/src/database/drivers/sqlite/index.web.js +6 -3
  106. package/src/environment-handlers/base.js +18 -0
  107. package/src/environment-handlers/node.js +32 -0
  108. package/src/sync/conflict-strategy.js +3 -0
  109. package/src/sync/local-mutation-log.js +24 -0
  110. package/src/sync/sync-api-client-types.js +3 -0
  111. package/src/sync/sync-api-client.js +203 -0
  112. package/src/sync/sync-client-types.js +16 -0
  113. package/src/sync/sync-client.js +198 -14
  114. package/src/sync/sync-envelope-replay-service.js +109 -12
  115. package/src/sync/sync-model-change-feed-service.js +2 -4
  116. package/src/sync/sync-replay-persisted-data.js +50 -0
  117. package/src/testing/shared-transaction-broker.js +48 -7
  118. package/src/testing/shared-transaction-connection-coordinator.js +23 -9
@@ -2,9 +2,9 @@
2
2
  * Registers test-only serialization owned by the active broker.
3
3
  * @param {object} connection - Parent physical connection.
4
4
  * @param {(callback: () => Promise<unknown>) => Promise<unknown>} coordinator - Serializer.
5
- * @returns {void}
5
+ * @returns {symbol} - Opaque owner for broker-internal connection calls.
6
6
  */
7
- export declare function setSharedTransactionCoordinator(connection: object, coordinator: (callback: () => Promise<unknown>) => Promise<unknown>): void;
7
+ export declare function setSharedTransactionCoordinator(connection: object, coordinator: (callback: () => Promise<unknown>) => Promise<unknown>): symbol;
8
8
  /**
9
9
  * Removes a broker-owned coordinator without disturbing a replacement.
10
10
  * @param {object} connection - Parent physical connection.
@@ -15,9 +15,10 @@ export declare function clearSharedTransactionCoordinator(connection: object, co
15
15
  /**
16
16
  * Runs parent work through broker serialization when registered.
17
17
  * @template T
18
- * @param {object} connection - Parent physical connection.
18
+ * @param {import("../database/drivers/base.js").default} connection - Parent physical connection.
19
19
  * @param {() => Promise<T>} callback - Parent operation.
20
+ * @param {symbol} [operationOwner] - Broker owner for an already-coordinated operation.
20
21
  * @returns {Promise<T>} - Operation result.
21
22
  */
22
- export declare function coordinateSharedTransactionConnection<T>(connection: object, callback: () => Promise<T>): Promise<T>;
23
+ export declare function coordinateSharedTransactionConnection<T>(connection: import("../database/drivers/base.js").default, callback: () => Promise<T>, operationOwner?: symbol): Promise<T>;
23
24
  //# sourceMappingURL=shared-transaction-connection-coordinator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared-transaction-connection-coordinator.d.ts","sourceRoot":"","sources":["../../../src/testing/shared-transaction-connection-coordinator.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,UAAU,EAJ/C,MAI+C,EAAE,WAAW,EAH5D,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAGO,GAF1D,IAAI,CAIhB;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,UAAU,EAJjD,MAIiD,EAAE,WAAW,EAH9D,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAGS,GAF5D,IAAI,CAIhB;AAED;;;;;;GAMG;AACH,wBAAsB,qCAAqC,CAL9C,CAAC,EAK8C,UAAU,EAJ3D,MAI2D,EAAE,QAAQ,EAHrE,MAAM,OAAO,CAAC,CAAC,CAGsD,GAFnE,OAAO,CAAC,CAAC,CAAC,CAOtB"}
1
+ {"version":3,"file":"shared-transaction-connection-coordinator.d.ts","sourceRoot":"","sources":["../../../src/testing/shared-transaction-connection-coordinator.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,UAAU,EAJ/C,MAI+C,EAAE,WAAW,EAH5D,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAGO,GAF1D,MAAM,CAOlB;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,UAAU,EAJjD,MAIiD,EAAE,WAAW,EAH9D,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAGS,GAF5D,IAAI,CAIhB;AAED;;;;;;;GAOG;AACH,wBAAsB,qCAAqC,CAN9C,CAAC,EAM8C,UAAU,EAL3D,OAAO,6BAA6B,EAAE,OAKqB,EAAE,QAAQ,EAJrE,MAAM,OAAO,CAAC,CAAC,CAIsD,EAAE,cAAc,AAH7F,CACA,EADQ,MAGqF,GAFnF,OAAO,CAAC,CAAC,CAAC,CAiBtB"}
@@ -1,14 +1,16 @@
1
1
  // @ts-check
2
- /** @type {WeakMap<object, (callback: () => Promise<unknown>) => Promise<unknown>>} */
2
+ /** @type {WeakMap<object, {coordinator: (callback: () => Promise<unknown>) => Promise<unknown>, owner: symbol}>} */
3
3
  const coordinators = new WeakMap();
4
4
  /**
5
5
  * Registers test-only serialization owned by the active broker.
6
6
  * @param {object} connection - Parent physical connection.
7
7
  * @param {(callback: () => Promise<unknown>) => Promise<unknown>} coordinator - Serializer.
8
- * @returns {void}
8
+ * @returns {symbol} - Opaque owner for broker-internal connection calls.
9
9
  */
10
10
  export function setSharedTransactionCoordinator(connection, coordinator) {
11
- coordinators.set(connection, coordinator);
11
+ const owner = Symbol("shared-transaction-coordinator");
12
+ coordinators.set(connection, { coordinator, owner });
13
+ return owner;
12
14
  }
13
15
  /**
14
16
  * Removes a broker-owned coordinator without disturbing a replacement.
@@ -17,20 +19,29 @@ export function setSharedTransactionCoordinator(connection, coordinator) {
17
19
  * @returns {void}
18
20
  */
19
21
  export function clearSharedTransactionCoordinator(connection, coordinator) {
20
- if (coordinators.get(connection) === coordinator)
22
+ if (coordinators.get(connection)?.coordinator === coordinator)
21
23
  coordinators.delete(connection);
22
24
  }
23
25
  /**
24
26
  * Runs parent work through broker serialization when registered.
25
27
  * @template T
26
- * @param {object} connection - Parent physical connection.
28
+ * @param {import("../database/drivers/base.js").default} connection - Parent physical connection.
27
29
  * @param {() => Promise<T>} callback - Parent operation.
30
+ * @param {symbol} [operationOwner] - Broker owner for an already-coordinated operation.
28
31
  * @returns {Promise<T>} - Operation result.
29
32
  */
30
- export async function coordinateSharedTransactionConnection(connection, callback) {
31
- const coordinator = coordinators.get(connection);
32
- if (!coordinator)
33
+ export async function coordinateSharedTransactionConnection(connection, callback, operationOwner) {
34
+ const registration = coordinators.get(connection);
35
+ if (!registration)
36
+ return await callback();
37
+ if (operationOwner === registration.owner)
38
+ return await callback();
39
+ const environmentHandler = connection.configuration.getEnvironmentHandler();
40
+ if (environmentHandler.getSharedTransactionCoordinatorOwner(connection) === registration.owner) {
33
41
  return await callback();
34
- return /** @type {T} */ (await coordinator(callback));
42
+ }
43
+ return /** @type {T} */ (await registration.coordinator(async () => {
44
+ return await environmentHandler.runWithSharedTransactionCoordinatorOwner(connection, registration.owner, callback);
45
+ }));
35
46
  }
36
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2hhcmVkLXRyYW5zYWN0aW9uLWNvbm5lY3Rpb24tY29vcmRpbmF0b3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvdGVzdGluZy9zaGFyZWQtdHJhbnNhY3Rpb24tY29ubmVjdGlvbi1jb29yZGluYXRvci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVosc0ZBQXNGO0FBQ3RGLE1BQU0sWUFBWSxHQUFHLElBQUksT0FBTyxFQUFFLENBQUE7QUFFbEM7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsK0JBQStCLENBQUMsVUFBVSxFQUFFLFdBQVc7SUFDckUsWUFBWSxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsV0FBVyxDQUFDLENBQUE7QUFDM0MsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxVQUFVLGlDQUFpQyxDQUFDLFVBQVUsRUFBRSxXQUFXO0lBQ3ZFLElBQUksWUFBWSxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsS0FBSyxXQUFXO1FBQUUsWUFBWSxDQUFDLE1BQU0sQ0FBQyxVQUFVLENBQUMsQ0FBQTtBQUNuRixDQUFDO0FBRUQ7Ozs7OztHQU1HO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxxQ0FBcUMsQ0FBQyxVQUFVLEVBQUUsUUFBUTtJQUM5RSxNQUFNLFdBQVcsR0FBRyxZQUFZLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxDQUFBO0lBRWhELElBQUksQ0FBQyxXQUFXO1FBQUUsT0FBTyxNQUFNLFFBQVEsRUFBRSxDQUFBO0lBQ3pDLE9BQU8sZ0JBQWdCLENBQUMsQ0FBQyxNQUFNLFdBQVcsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFBO0FBQ3ZELENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBAdHMtY2hlY2tcblxuLyoqIEB0eXBlIHtXZWFrTWFwPG9iamVjdCwgKGNhbGxiYWNrOiAoKSA9PiBQcm9taXNlPHVua25vd24+KSA9PiBQcm9taXNlPHVua25vd24+Pn0gKi9cbmNvbnN0IGNvb3JkaW5hdG9ycyA9IG5ldyBXZWFrTWFwKClcblxuLyoqXG4gKiBSZWdpc3RlcnMgdGVzdC1vbmx5IHNlcmlhbGl6YXRpb24gb3duZWQgYnkgdGhlIGFjdGl2ZSBicm9rZXIuXG4gKiBAcGFyYW0ge29iamVjdH0gY29ubmVjdGlvbiAtIFBhcmVudCBwaHlzaWNhbCBjb25uZWN0aW9uLlxuICogQHBhcmFtIHsoY2FsbGJhY2s6ICgpID0+IFByb21pc2U8dW5rbm93bj4pID0+IFByb21pc2U8dW5rbm93bj59IGNvb3JkaW5hdG9yIC0gU2VyaWFsaXplci5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5leHBvcnQgZnVuY3Rpb24gc2V0U2hhcmVkVHJhbnNhY3Rpb25Db29yZGluYXRvcihjb25uZWN0aW9uLCBjb29yZGluYXRvcikge1xuICBjb29yZGluYXRvcnMuc2V0KGNvbm5lY3Rpb24sIGNvb3JkaW5hdG9yKVxufVxuXG4vKipcbiAqIFJlbW92ZXMgYSBicm9rZXItb3duZWQgY29vcmRpbmF0b3Igd2l0aG91dCBkaXN0dXJiaW5nIGEgcmVwbGFjZW1lbnQuXG4gKiBAcGFyYW0ge29iamVjdH0gY29ubmVjdGlvbiAtIFBhcmVudCBwaHlzaWNhbCBjb25uZWN0aW9uLlxuICogQHBhcmFtIHsoY2FsbGJhY2s6ICgpID0+IFByb21pc2U8dW5rbm93bj4pID0+IFByb21pc2U8dW5rbm93bj59IGNvb3JkaW5hdG9yIC0gRXhwZWN0ZWQgc2VyaWFsaXplci5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5leHBvcnQgZnVuY3Rpb24gY2xlYXJTaGFyZWRUcmFuc2FjdGlvbkNvb3JkaW5hdG9yKGNvbm5lY3Rpb24sIGNvb3JkaW5hdG9yKSB7XG4gIGlmIChjb29yZGluYXRvcnMuZ2V0KGNvbm5lY3Rpb24pID09PSBjb29yZGluYXRvcikgY29vcmRpbmF0b3JzLmRlbGV0ZShjb25uZWN0aW9uKVxufVxuXG4vKipcbiAqIFJ1bnMgcGFyZW50IHdvcmsgdGhyb3VnaCBicm9rZXIgc2VyaWFsaXphdGlvbiB3aGVuIHJlZ2lzdGVyZWQuXG4gKiBAdGVtcGxhdGUgVFxuICogQHBhcmFtIHtvYmplY3R9IGNvbm5lY3Rpb24gLSBQYXJlbnQgcGh5c2ljYWwgY29ubmVjdGlvbi5cbiAqIEBwYXJhbSB7KCkgPT4gUHJvbWlzZTxUPn0gY2FsbGJhY2sgLSBQYXJlbnQgb3BlcmF0aW9uLlxuICogQHJldHVybnMge1Byb21pc2U8VD59IC0gT3BlcmF0aW9uIHJlc3VsdC5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNvb3JkaW5hdGVTaGFyZWRUcmFuc2FjdGlvbkNvbm5lY3Rpb24oY29ubmVjdGlvbiwgY2FsbGJhY2spIHtcbiAgY29uc3QgY29vcmRpbmF0b3IgPSBjb29yZGluYXRvcnMuZ2V0KGNvbm5lY3Rpb24pXG5cbiAgaWYgKCFjb29yZGluYXRvcikgcmV0dXJuIGF3YWl0IGNhbGxiYWNrKClcbiAgcmV0dXJuIC8qKiBAdHlwZSB7VH0gKi8gKGF3YWl0IGNvb3JkaW5hdG9yKGNhbGxiYWNrKSlcbn1cbiJdfQ==
47
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2hhcmVkLXRyYW5zYWN0aW9uLWNvbm5lY3Rpb24tY29vcmRpbmF0b3IuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvdGVzdGluZy9zaGFyZWQtdHJhbnNhY3Rpb24tY29ubmVjdGlvbi1jb29yZGluYXRvci5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxZQUFZO0FBRVosb0hBQW9IO0FBQ3BILE1BQU0sWUFBWSxHQUFHLElBQUksT0FBTyxFQUFFLENBQUE7QUFFbEM7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsK0JBQStCLENBQUMsVUFBVSxFQUFFLFdBQVc7SUFDckUsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLGdDQUFnQyxDQUFDLENBQUE7SUFFdEQsWUFBWSxDQUFDLEdBQUcsQ0FBQyxVQUFVLEVBQUUsRUFBQyxXQUFXLEVBQUUsS0FBSyxFQUFDLENBQUMsQ0FBQTtJQUNsRCxPQUFPLEtBQUssQ0FBQTtBQUNkLENBQUM7QUFFRDs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxpQ0FBaUMsQ0FBQyxVQUFVLEVBQUUsV0FBVztJQUN2RSxJQUFJLFlBQVksQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLEVBQUUsV0FBVyxLQUFLLFdBQVc7UUFBRSxZQUFZLENBQUMsTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFBO0FBQ2hHLENBQUM7QUFFRDs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxxQ0FBcUMsQ0FBQyxVQUFVLEVBQUUsUUFBUSxFQUFFLGNBQWM7SUFDOUYsTUFBTSxZQUFZLEdBQUcsWUFBWSxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsQ0FBQTtJQUVqRCxJQUFJLENBQUMsWUFBWTtRQUFFLE9BQU8sTUFBTSxRQUFRLEVBQUUsQ0FBQTtJQUMxQyxJQUFJLGNBQWMsS0FBSyxZQUFZLENBQUMsS0FBSztRQUFFLE9BQU8sTUFBTSxRQUFRLEVBQUUsQ0FBQTtJQUVsRSxNQUFNLGtCQUFrQixHQUFHLFVBQVUsQ0FBQyxhQUFhLENBQUMscUJBQXFCLEVBQUUsQ0FBQTtJQUUzRSxJQUFJLGtCQUFrQixDQUFDLG9DQUFvQyxDQUFDLFVBQVUsQ0FBQyxLQUFLLFlBQVksQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUMvRixPQUFPLE1BQU0sUUFBUSxFQUFFLENBQUE7SUFDekIsQ0FBQztJQUVELE9BQU8sZ0JBQWdCLENBQUMsQ0FBQyxNQUFNLFlBQVksQ0FBQyxXQUFXLENBQUMsS0FBSyxJQUFJLEVBQUU7UUFDakUsT0FBTyxNQUFNLGtCQUFrQixDQUFDLHdDQUF3QyxDQUFDLFVBQVUsRUFBRSxZQUFZLENBQUMsS0FBSyxFQUFFLFFBQVEsQ0FBQyxDQUFBO0lBQ3BILENBQUMsQ0FBQyxDQUFDLENBQUE7QUFDTCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQHRzLWNoZWNrXG5cbi8qKiBAdHlwZSB7V2Vha01hcDxvYmplY3QsIHtjb29yZGluYXRvcjogKGNhbGxiYWNrOiAoKSA9PiBQcm9taXNlPHVua25vd24+KSA9PiBQcm9taXNlPHVua25vd24+LCBvd25lcjogc3ltYm9sfT59ICovXG5jb25zdCBjb29yZGluYXRvcnMgPSBuZXcgV2Vha01hcCgpXG5cbi8qKlxuICogUmVnaXN0ZXJzIHRlc3Qtb25seSBzZXJpYWxpemF0aW9uIG93bmVkIGJ5IHRoZSBhY3RpdmUgYnJva2VyLlxuICogQHBhcmFtIHtvYmplY3R9IGNvbm5lY3Rpb24gLSBQYXJlbnQgcGh5c2ljYWwgY29ubmVjdGlvbi5cbiAqIEBwYXJhbSB7KGNhbGxiYWNrOiAoKSA9PiBQcm9taXNlPHVua25vd24+KSA9PiBQcm9taXNlPHVua25vd24+fSBjb29yZGluYXRvciAtIFNlcmlhbGl6ZXIuXG4gKiBAcmV0dXJucyB7c3ltYm9sfSAtIE9wYXF1ZSBvd25lciBmb3IgYnJva2VyLWludGVybmFsIGNvbm5lY3Rpb24gY2FsbHMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZXRTaGFyZWRUcmFuc2FjdGlvbkNvb3JkaW5hdG9yKGNvbm5lY3Rpb24sIGNvb3JkaW5hdG9yKSB7XG4gIGNvbnN0IG93bmVyID0gU3ltYm9sKFwic2hhcmVkLXRyYW5zYWN0aW9uLWNvb3JkaW5hdG9yXCIpXG5cbiAgY29vcmRpbmF0b3JzLnNldChjb25uZWN0aW9uLCB7Y29vcmRpbmF0b3IsIG93bmVyfSlcbiAgcmV0dXJuIG93bmVyXG59XG5cbi8qKlxuICogUmVtb3ZlcyBhIGJyb2tlci1vd25lZCBjb29yZGluYXRvciB3aXRob3V0IGRpc3R1cmJpbmcgYSByZXBsYWNlbWVudC5cbiAqIEBwYXJhbSB7b2JqZWN0fSBjb25uZWN0aW9uIC0gUGFyZW50IHBoeXNpY2FsIGNvbm5lY3Rpb24uXG4gKiBAcGFyYW0geyhjYWxsYmFjazogKCkgPT4gUHJvbWlzZTx1bmtub3duPikgPT4gUHJvbWlzZTx1bmtub3duPn0gY29vcmRpbmF0b3IgLSBFeHBlY3RlZCBzZXJpYWxpemVyLlxuICogQHJldHVybnMge3ZvaWR9XG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjbGVhclNoYXJlZFRyYW5zYWN0aW9uQ29vcmRpbmF0b3IoY29ubmVjdGlvbiwgY29vcmRpbmF0b3IpIHtcbiAgaWYgKGNvb3JkaW5hdG9ycy5nZXQoY29ubmVjdGlvbik/LmNvb3JkaW5hdG9yID09PSBjb29yZGluYXRvcikgY29vcmRpbmF0b3JzLmRlbGV0ZShjb25uZWN0aW9uKVxufVxuXG4vKipcbiAqIFJ1bnMgcGFyZW50IHdvcmsgdGhyb3VnaCBicm9rZXIgc2VyaWFsaXphdGlvbiB3aGVuIHJlZ2lzdGVyZWQuXG4gKiBAdGVtcGxhdGUgVFxuICogQHBhcmFtIHtpbXBvcnQoXCIuLi9kYXRhYmFzZS9kcml2ZXJzL2Jhc2UuanNcIikuZGVmYXVsdH0gY29ubmVjdGlvbiAtIFBhcmVudCBwaHlzaWNhbCBjb25uZWN0aW9uLlxuICogQHBhcmFtIHsoKSA9PiBQcm9taXNlPFQ+fSBjYWxsYmFjayAtIFBhcmVudCBvcGVyYXRpb24uXG4gKiBAcGFyYW0ge3N5bWJvbH0gW29wZXJhdGlvbk93bmVyXSAtIEJyb2tlciBvd25lciBmb3IgYW4gYWxyZWFkeS1jb29yZGluYXRlZCBvcGVyYXRpb24uXG4gKiBAcmV0dXJucyB7UHJvbWlzZTxUPn0gLSBPcGVyYXRpb24gcmVzdWx0LlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY29vcmRpbmF0ZVNoYXJlZFRyYW5zYWN0aW9uQ29ubmVjdGlvbihjb25uZWN0aW9uLCBjYWxsYmFjaywgb3BlcmF0aW9uT3duZXIpIHtcbiAgY29uc3QgcmVnaXN0cmF0aW9uID0gY29vcmRpbmF0b3JzLmdldChjb25uZWN0aW9uKVxuXG4gIGlmICghcmVnaXN0cmF0aW9uKSByZXR1cm4gYXdhaXQgY2FsbGJhY2soKVxuICBpZiAob3BlcmF0aW9uT3duZXIgPT09IHJlZ2lzdHJhdGlvbi5vd25lcikgcmV0dXJuIGF3YWl0IGNhbGxiYWNrKClcblxuICBjb25zdCBlbnZpcm9ubWVudEhhbmRsZXIgPSBjb25uZWN0aW9uLmNvbmZpZ3VyYXRpb24uZ2V0RW52aXJvbm1lbnRIYW5kbGVyKClcblxuICBpZiAoZW52aXJvbm1lbnRIYW5kbGVyLmdldFNoYXJlZFRyYW5zYWN0aW9uQ29vcmRpbmF0b3JPd25lcihjb25uZWN0aW9uKSA9PT0gcmVnaXN0cmF0aW9uLm93bmVyKSB7XG4gICAgcmV0dXJuIGF3YWl0IGNhbGxiYWNrKClcbiAgfVxuXG4gIHJldHVybiAvKiogQHR5cGUge1R9ICovIChhd2FpdCByZWdpc3RyYXRpb24uY29vcmRpbmF0b3IoYXN5bmMgKCkgPT4ge1xuICAgIHJldHVybiBhd2FpdCBlbnZpcm9ubWVudEhhbmRsZXIucnVuV2l0aFNoYXJlZFRyYW5zYWN0aW9uQ29vcmRpbmF0b3JPd25lcihjb25uZWN0aW9uLCByZWdpc3RyYXRpb24ub3duZXIsIGNhbGxiYWNrKVxuICB9KSlcbn1cbiJdfQ==
@@ -63,6 +63,9 @@ export async function applySyncReplayResultToLocalMutationLog({mutationLog, reco
63
63
  * @returns {import("./local-mutation-log.js").LocalMutationStatus} - Local mutation-log status.
64
64
  */
65
65
  export function replayResultLocalStatus(result) {
66
+ if (result.syncState === "conflict") return "conflict"
67
+ if (result.syncState === "failed" || result.syncState === "rejected") return "rejected"
68
+ if (result.syncState === "successful" || result.syncState === "duplicate") return "synced"
66
69
  if (result.status === "conflict") return "conflict"
67
70
  if (result.status === "error" || result.status === "rejected") return "rejected"
68
71
  if (result.status === "success" && replayResultHasFailedApplication(result)) return "rejected"
@@ -148,6 +148,30 @@ export default class LocalMutationLog {
148
148
  })
149
149
  }
150
150
 
151
+ /**
152
+ * Replaces the mutation payload of a still-pending record. Used when an
153
+ * acknowledged predecessor supplies the authoritative base for its successor.
154
+ * @param {{id: string, mutation: import("./device-identity.js").SyncMutation}} args - Record and replacement mutation.
155
+ * @returns {Promise<LocalMutationLogRecord>} Updated record.
156
+ */
157
+ async updateMutation({id, mutation}) {
158
+ return await withStorageKeyLock(this.storageKey, async () => {
159
+ const rawRecord = await this.storage.record(this.storageKey, id)
160
+
161
+ if (!rawRecord) throw new Error(`No local mutation log record '${id}'`)
162
+
163
+ const record = normalizeRecord(rawRecord)
164
+
165
+ if (!PENDING_STATUSES.has(record.status)) throw new Error(`Cannot update mutation for ${record.status} local mutation '${id}'`)
166
+
167
+ record.mutation = normalizeMutation(mutation)
168
+ record.updatedAt = this.currentTimestamp()
169
+ await this.storage.updateRecord(this.storageKey, cloneRecord(record))
170
+
171
+ return cloneRecord(record)
172
+ })
173
+ }
174
+
151
175
  /**
152
176
  * Prunes terminal records that are no longer needed for replay dependencies.
153
177
  * @param {object} [args] - Compaction options.
@@ -73,6 +73,9 @@
73
73
  /**
74
74
  * @typedef {object} SyncReplayItem
75
75
  * @property {string | number} id - Sync id.
76
+ * @property {Record<string, ReturnType<typeof JSON.parse>>} [conflict] - Structured durable conflict payload.
77
+ * @property {string} [reason] - Rejection reason.
78
+ * @property {string | number | null} [serverVersion] - Authoritative version after successful replay.
76
79
  * @property {string} syncState - Replay state.
77
80
  */
78
81
 
@@ -3,6 +3,7 @@
3
3
  import {optionalBoolean, optionalInteger} from "typanic"
4
4
 
5
5
  import recordChanges from "../database/record-changes.js"
6
+ import {applySyncReplayResultToLocalMutationLog} from "./conflict-strategy.js"
6
7
 
7
8
  /** @typedef {import("./sync-api-client-types.js").SyncChangeApplyResult} SyncChangeApplyResult */
8
9
  /** @typedef {import("./sync-api-client-types.js").SyncChangeEnvelope} SyncChangeEnvelope */
@@ -21,6 +22,208 @@ const syncTaskPromises = new Map()
21
22
  * persistence/auth hooks.
22
23
  */
23
24
  export default class SyncApiClient {
25
+ /**
26
+ * Appends one conflict-tracked intent to the existing durable mutation log.
27
+ * @param {object} args - Queue arguments.
28
+ * @param {string | number | null} args.baseVersion - Authoritative version observed before the local mutation.
29
+ * @param {import("./sync-client-types.js").SyncClientConflictTrackingConfig} args.conflictTracking - Durable tracking configuration.
30
+ * @param {Record<string, unknown>} args.data - Backend-safe mutation attributes.
31
+ * @param {"create" | "update" | "destroy"} args.operation - Local operation.
32
+ * @param {ReturnType<typeof JSON.parse>} args.resource - Local resource.
33
+ * @param {string} args.resourceType - Resource type.
34
+ * @param {string} args.syncType - Wire operation.
35
+ * @returns {Promise<import("./local-mutation-log.js").LocalMutationLogRecord>} Appended intent.
36
+ */
37
+ static async queueConflictTrackedSync({baseVersion, conflictTracking, data, operation, resource, resourceType, syncType}) {
38
+ const resourceId = String(resource.id())
39
+ const records = await conflictTracking.mutationLog.records()
40
+ const predecessor = records
41
+ .filter((record) => record.mutation.model === resourceType && record.mutation.payload?.resourceId === resourceId)
42
+ .at(-1)
43
+ const clientMutationId = conflictTracking.clientMutationId()
44
+ const now = conflictTracking.now ? conflictTracking.now() : new Date()
45
+ const predecessorTime = predecessor ? new Date(predecessor.mutation.occurredAt).getTime() : Number.NEGATIVE_INFINITY
46
+ const occurredAt = new Date(Math.max(now.getTime(), predecessorTime + 1)).toISOString()
47
+
48
+ return await conflictTracking.mutationLog.append({
49
+ dependencies: predecessor ? [{clientMutationId: predecessor.mutation.clientMutationId, model: resourceType}] : [],
50
+ mutation: {
51
+ actorDeviceId: conflictTracking.actorDeviceId,
52
+ actorUserId: conflictTracking.actorUserId,
53
+ attributes: /** @type {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue>} */ (data),
54
+ baseVersion,
55
+ clientMutationId,
56
+ model: resourceType,
57
+ occurredAt,
58
+ offlineGrantId: conflictTracking.offlineGrantId,
59
+ operation,
60
+ payload: {resourceId, syncType},
61
+ policyHash: conflictTracking.policyHash
62
+ }
63
+ })
64
+ }
65
+
66
+ /**
67
+ * Drains the existing mutation log in predecessor order. Independent records
68
+ * continue after durable conflicts/rejections; successors stay blocked.
69
+ * @param {object} args - Replay arguments.
70
+ * @param {string} args.authenticationToken - Authentication token.
71
+ * @param {number} [args.batchSize] - Batch size.
72
+ * @param {import("./sync-client-types.js").SyncClientConflictTrackingConfig} args.conflictTracking - Tracking configuration.
73
+ * @param {(payload: {authenticationToken: string, syncs: Array<Record<string, ReturnType<typeof JSON.parse>>>}) => Promise<SyncReplayResponse>} args.postReplay - Transport boundary.
74
+ * @param {(identity: string) => number} args.remoteGeneration - Current remote generation.
75
+ * @param {string} args.resourceType - Resource whose log records should drain.
76
+ * @returns {Promise<void>} Resolves when no ready intent remains.
77
+ */
78
+ static async replayConflictTrackedSyncs({authenticationToken, batchSize, conflictTracking, postReplay, remoteGeneration, resourceType}) {
79
+ const maxBatchSize = this.normalizedBatchSize(batchSize)
80
+
81
+ while (true) {
82
+ const records = await conflictTracking.mutationLog.records()
83
+ const statuses = new Map(records.map((record) => [record.mutation.clientMutationId, record.status]))
84
+ const pending = records.filter((record) => record.status === "pending" && record.mutation.model === resourceType)
85
+ const ready = pending.filter((record) => record.dependencies.every((dependency) => statuses.get(dependency.clientMutationId) === "synced"))
86
+
87
+ if (ready.length === 0) return
88
+
89
+ const groups = this.conflictReplayGroups({pending, ready}).slice(0, maxBatchSize)
90
+ const generations = new Map(groups.map((group) => [group[0].mutation.clientMutationId, remoteGeneration(this.conflictRecordIdentity(group[0]))]))
91
+ const response = await postReplay({
92
+ authenticationToken,
93
+ syncs: groups.map((group) => this.conflictReplayPayload(group))
94
+ })
95
+
96
+ this.ensureSuccessfulResponse(response)
97
+
98
+ const responsesById = new Map((response.syncs || []).map((result) => [String(result.id), result]))
99
+
100
+ for (const group of groups) {
101
+ const result = responsesById.get(group[0].mutation.clientMutationId)
102
+
103
+ if (!result) throw new Error(`Sync response missing result for mutation ${group[0].mutation.clientMutationId}`)
104
+ if (!["successful", "duplicate", "conflict", "failed", "rejected"].includes(result.syncState)) {
105
+ throw new Error(`Invalid sync state returned for mutation ${group[0].mutation.clientMutationId}: ${result.syncState}`)
106
+ }
107
+
108
+ for (const record of group) {
109
+ await applySyncReplayResultToLocalMutationLog({mutationLog: conflictTracking.mutationLog, record, result: /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (result)})
110
+ }
111
+
112
+ if (["successful", "duplicate"].includes(result.syncState) && result.serverVersion !== undefined) {
113
+ const identity = this.conflictRecordIdentity(group[0])
114
+
115
+ if (remoteGeneration(identity) === generations.get(group[0].mutation.clientMutationId)) {
116
+ await this.rebaseConflictSuccessor({conflictTracking, predecessor: group[group.length - 1], serverVersion: result.serverVersion})
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Builds safe transport groups from root-ready records and their successors.
125
+ * @param {{pending: Array<import("./local-mutation-log.js").LocalMutationLogRecord>, ready: Array<import("./local-mutation-log.js").LocalMutationLogRecord>}} args - Pending and root-ready records.
126
+ * @returns {Array<Array<import("./local-mutation-log.js").LocalMutationLogRecord>>} Safe transport groups.
127
+ */
128
+ static conflictReplayGroups({pending, ready}) {
129
+ const groups = []
130
+ const selectedIdentities = new Set()
131
+
132
+ for (const record of ready) {
133
+ const identity = this.conflictRecordIdentity(record)
134
+
135
+ if (selectedIdentities.has(identity)) continue
136
+
137
+ const group = [record]
138
+ let tail = record
139
+
140
+ while (true) {
141
+ const successor = pending.find((candidate) => candidate.dependencies.some((dependency) => dependency.clientMutationId === tail.mutation.clientMutationId))
142
+
143
+ if (!successor || !this.canCoalesceConflictRecords(tail, successor)) break
144
+ group.push(successor)
145
+ tail = successor
146
+ }
147
+
148
+ groups.push(group)
149
+ selectedIdentities.add(identity)
150
+ }
151
+
152
+ return groups
153
+ }
154
+
155
+ /**
156
+ * Checks whether two durable intents can share one transport mutation.
157
+ * @param {import("./local-mutation-log.js").LocalMutationLogRecord} left - Earlier intent.
158
+ * @param {import("./local-mutation-log.js").LocalMutationLogRecord} right - Later intent.
159
+ * @returns {boolean} Whether scalar updates can share one transport mutation.
160
+ */
161
+ static canCoalesceConflictRecords(left, right) {
162
+ if (left.mutation.operation !== "update" || right.mutation.operation !== "update") return false
163
+ if (this.conflictRecordIdentity(left) !== this.conflictRecordIdentity(right)) return false
164
+ if (left.mutation.baseVersion !== right.mutation.baseVersion) return false
165
+ if (!this.scalarSyncAttributes(left.mutation.attributes) || !this.scalarSyncAttributes(right.mutation.attributes)) return false
166
+
167
+ return !Object.keys(left.mutation.attributes || {}).some((key) => Object.hasOwn(right.mutation.attributes || {}, key))
168
+ }
169
+
170
+ /**
171
+ * Checks whether attributes contain scalar JSON values only.
172
+ * @param {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue> | undefined} attributes - Attributes.
173
+ * @returns {boolean} Whether every value is scalar.
174
+ */
175
+ static scalarSyncAttributes(attributes) {
176
+ return Boolean(attributes) && Object.values(attributes || {}).every((value) => value === null || ["string", "number", "boolean"].includes(typeof value))
177
+ }
178
+
179
+ /**
180
+ * Builds one replay envelope for a safe transport group.
181
+ * @param {Array<import("./local-mutation-log.js").LocalMutationLogRecord>} group - Transport group.
182
+ * @returns {Record<string, ReturnType<typeof JSON.parse>>} Replay envelope.
183
+ */
184
+ static conflictReplayPayload(group) {
185
+ const first = group[0]
186
+ const payload = /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ ({
187
+ baseVersion: first.mutation.baseVersion,
188
+ clientUpdatedAt: first.mutation.occurredAt,
189
+ data: Object.assign({}, ...group.map((record) => record.mutation.attributes || {})),
190
+ id: first.mutation.clientMutationId,
191
+ resourceId: first.mutation.payload?.resourceId,
192
+ resourceType: first.mutation.model,
193
+ syncType: first.mutation.payload?.syncType
194
+ })
195
+
196
+ return payload
197
+ }
198
+
199
+ /**
200
+ * Builds a stable resource identity for ordering and remote generations.
201
+ * @param {import("./local-mutation-log.js").LocalMutationLogRecord} record - Record.
202
+ * @returns {string} Resource identity.
203
+ */
204
+ static conflictRecordIdentity(record) {
205
+ return `${record.mutation.model}:${String(record.mutation.payload?.resourceId)}`
206
+ }
207
+
208
+ /**
209
+ * Rebases the direct pending successor from an authoritative acknowledgement.
210
+ * @param {object} args - Rebase args.
211
+ * @param {import("./sync-client-types.js").SyncClientConflictTrackingConfig} args.conflictTracking - Tracking config.
212
+ * @param {import("./local-mutation-log.js").LocalMutationLogRecord} args.predecessor - Acknowledged predecessor.
213
+ * @param {string | number | null} args.serverVersion - Authoritative server version.
214
+ * @returns {Promise<void>}
215
+ */
216
+ static async rebaseConflictSuccessor({conflictTracking, predecessor, serverVersion}) {
217
+ const successor = (await conflictTracking.mutationLog.pendingRecords())
218
+ .find((record) => record.dependencies.some((dependency) => dependency.clientMutationId === predecessor.mutation.clientMutationId))
219
+
220
+ if (!successor) return
221
+
222
+ await conflictTracking.mutationLog.updateMutation({
223
+ id: successor.id,
224
+ mutation: {...successor.mutation, baseVersion: serverVersion}
225
+ })
226
+ }
24
227
  /**
25
228
  * Serializes sync work with the same key so callers do not have to keep app-local locks.
26
229
  * @param {string} key - Lock key.
@@ -17,6 +17,20 @@
17
17
  * @property {Record<string, ReturnType<typeof JSON.parse>>} [params] - Static subscribe params. The framework injects `authenticationToken` automatically.
18
18
  */
19
19
 
20
+ /**
21
+ * Durable optimistic-version tracking configuration. Identity/grant fields are
22
+ * retained in the shared LocalMutationLog mutation shape used by offline sync.
23
+ * @typedef {object} SyncClientConflictTrackingConfig
24
+ * @property {string} actorDeviceId - Stable device identity.
25
+ * @property {string} actorUserId - Stable user identity.
26
+ * @property {() => string} clientMutationId - Durable idempotency-id generator.
27
+ * @property {import("./local-mutation-log.js").default} mutationLog - Existing durable mutation log.
28
+ * @property {() => Date} [now] - Mutation clock.
29
+ * @property {string} offlineGrantId - Grant identifier retained with intent.
30
+ * @property {string} policyHash - Policy hash retained with intent.
31
+ * @property {string} [versionAttribute] - Authoritative version attribute; defaults to updatedAt when present.
32
+ */
33
+
20
34
  /**
21
35
  * Declarative per-resource sync policy.
22
36
  * @typedef {object} SyncClientResourceConfig
@@ -26,6 +40,7 @@
26
40
  * @property {import("./sync-api-client-types.js").SyncResourceConfig["findRecordForDelete"]} [findRecordForDelete] - Custom pull-apply delete resolver.
27
41
  * @property {import("./sync-api-client-types.js").SyncResourceConfig["afterApply"]} [afterApply] - Post-apply hook.
28
42
  * @property {string[]} [booleanAttributes] - Attributes coerced through sync boolean parsing when queueing.
43
+ * @property {SyncClientConflictTrackingConfig} [conflictTracking] - Opt-in durable base-version conflict tracking.
29
44
  * @property {string[]} [localOnlyAttributes] - Attributes stripped from queued payloads.
30
45
  * @property {"upsert" | ((args: {operation: "create" | "update" | "destroy", record: ReturnType<typeof JSON.parse>}) => string)} [syncType] - Maps a mutation operation to a sync type. The "upsert" flag queues creates and updates as "update" rows (the server upserts by resource id) and destroys as "delete". Defaults to the operation name with destroy mapped to "delete".
31
46
  * @property {(args: {operation: "create" | "update" | "destroy", record: ReturnType<typeof JSON.parse>}) => Record<string, ReturnType<typeof JSON.parse>>} [trackedData] - Custom queued-payload builder for tracked mutations.
@@ -42,6 +57,7 @@
42
57
  * @property {import("./sync-api-client-types.js").SyncResourceConfig["afterApply"]} [afterApply] - Post-apply hook.
43
58
  * @property {import("./sync-api-client-types.js").SyncResourceConfig["attributes"]} [attributes] - Pull-apply attribute mapper. Required for resources that receive pulled changes.
44
59
  * @property {string[]} [booleanAttributes] - Extra boolean attributes merged with the boolean columns derived from column types.
60
+ * @property {SyncClientConflictTrackingConfig} [conflictTracking] - Opt-in durable base-version conflict tracking.
45
61
  * @property {import("./sync-api-client-types.js").SyncResourceConfig["findRecord"]} [findRecord] - Custom pull-apply record resolver.
46
62
  * @property {import("./sync-api-client-types.js").SyncResourceConfig["findRecordForDelete"]} [findRecordForDelete] - Custom pull-apply delete resolver.
47
63
  * @property {string[]} [localOnlyAttributes] - Extra local-only attributes merged with the derived primary key, createdAt/updatedAt, and sync bookkeeping attributes.