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.
- package/README.md +9 -1
- package/build/configuration-types.js +1 -1
- package/build/database/drivers/base.js +53 -34
- package/build/database/drivers/mssql/index.js +32 -0
- package/build/database/drivers/mysql/index.js +17 -0
- package/build/database/drivers/pgsql/index.js +11 -0
- package/build/database/drivers/sqlite/base.js +12 -0
- package/build/database/drivers/sqlite/connection-sql-js.js +3 -2
- package/build/database/drivers/sqlite/index.js +7 -1
- package/build/database/drivers/sqlite/index.native.js +9 -3
- package/build/database/drivers/sqlite/index.web.js +6 -3
- package/build/environment-handlers/base.js +18 -0
- package/build/environment-handlers/node.js +32 -0
- package/build/src/configuration-types.d.ts +2 -2
- package/build/src/configuration-types.js +2 -2
- package/build/src/database/drivers/base.d.ts +11 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +54 -37
- package/build/src/database/drivers/mssql/index.d.ts +7 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +15 -1
- package/build/src/database/drivers/mysql/index.d.ts +7 -0
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +15 -1
- package/build/src/database/drivers/pgsql/index.d.ts +6 -0
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +10 -1
- package/build/src/database/drivers/sqlite/base.d.ts +7 -0
- package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/base.js +11 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +4 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.js +4 -3
- package/build/src/database/drivers/sqlite/index.d.ts +2 -1
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +7 -2
- package/build/src/database/drivers/sqlite/index.native.d.ts +2 -1
- package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.native.js +9 -4
- package/build/src/database/drivers/sqlite/index.web.d.ts +2 -1
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.js +7 -4
- package/build/src/environment-handlers/base.d.ts +16 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +17 -1
- package/build/src/environment-handlers/node.d.ts +20 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +29 -1
- package/build/src/sync/conflict-strategy.d.ts.map +1 -1
- package/build/src/sync/conflict-strategy.js +7 -1
- package/build/src/sync/local-mutation-log.d.ts +10 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +21 -1
- package/build/src/sync/sync-api-client-types.d.ts +15 -0
- package/build/src/sync/sync-api-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-api-client-types.js +4 -1
- package/build/src/sync/sync-api-client.d.ts +91 -0
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +184 -1
- package/build/src/sync/sync-client-types.d.ts +57 -0
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +16 -1
- package/build/src/sync/sync-client.d.ts +56 -7
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +180 -15
- package/build/src/sync/sync-envelope-replay-service.d.ts +31 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +98 -16
- package/build/src/sync/sync-model-change-feed-service.d.ts.map +1 -1
- package/build/src/sync/sync-model-change-feed-service.js +3 -4
- package/build/src/sync/sync-replay-persisted-data.d.ts +26 -0
- package/build/src/sync/sync-replay-persisted-data.d.ts.map +1 -0
- package/build/src/sync/sync-replay-persisted-data.js +43 -0
- package/build/src/testing/shared-transaction-broker.d.ts +14 -0
- package/build/src/testing/shared-transaction-broker.d.ts.map +1 -1
- package/build/src/testing/shared-transaction-broker.js +44 -7
- package/build/src/testing/shared-transaction-connection-coordinator.d.ts +5 -4
- package/build/src/testing/shared-transaction-connection-coordinator.d.ts.map +1 -1
- package/build/src/testing/shared-transaction-connection-coordinator.js +21 -10
- package/build/sync/conflict-strategy.js +3 -0
- package/build/sync/local-mutation-log.js +24 -0
- package/build/sync/sync-api-client-types.js +3 -0
- package/build/sync/sync-api-client.js +203 -0
- package/build/sync/sync-client-types.js +16 -0
- package/build/sync/sync-client.js +198 -14
- package/build/sync/sync-envelope-replay-service.js +109 -12
- package/build/sync/sync-model-change-feed-service.js +2 -4
- package/build/sync/sync-replay-persisted-data.js +50 -0
- package/build/testing/shared-transaction-broker.js +48 -7
- package/build/testing/shared-transaction-connection-coordinator.js +23 -9
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/browser-test-session.js +489 -0
- package/scripts/prewarm-chromedriver.js +3 -22
- package/scripts/test-browser.js +47 -15
- package/src/configuration-types.js +1 -1
- package/src/database/drivers/base.js +53 -34
- package/src/database/drivers/mssql/index.js +32 -0
- package/src/database/drivers/mysql/index.js +17 -0
- package/src/database/drivers/pgsql/index.js +11 -0
- package/src/database/drivers/sqlite/base.js +12 -0
- package/src/database/drivers/sqlite/connection-sql-js.js +3 -2
- package/src/database/drivers/sqlite/index.js +7 -1
- package/src/database/drivers/sqlite/index.native.js +9 -3
- package/src/database/drivers/sqlite/index.web.js +6 -3
- package/src/environment-handlers/base.js +18 -0
- package/src/environment-handlers/node.js +32 -0
- package/src/sync/conflict-strategy.js +3 -0
- package/src/sync/local-mutation-log.js +24 -0
- package/src/sync/sync-api-client-types.js +3 -0
- package/src/sync/sync-api-client.js +203 -0
- package/src/sync/sync-client-types.js +16 -0
- package/src/sync/sync-client.js +198 -14
- package/src/sync/sync-envelope-replay-service.js +109 -12
- package/src/sync/sync-model-change-feed-service.js +2 -4
- package/src/sync/sync-replay-persisted-data.js +50 -0
- package/src/testing/shared-transaction-broker.js +48 -7
- 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 {
|
|
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>):
|
|
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 {
|
|
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:
|
|
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,
|
|
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 {
|
|
8
|
+
* @returns {symbol} - Opaque owner for broker-internal connection calls.
|
|
9
9
|
*/
|
|
10
10
|
export function setSharedTransactionCoordinator(connection, coordinator) {
|
|
11
|
-
|
|
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 {
|
|
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
|
|
32
|
-
if (!
|
|
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
|
-
|
|
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,
|
|
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.
|