@interop/was-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +20 -0
- package/README.md +410 -0
- package/dist/auth/chapi.d.ts +43 -0
- package/dist/auth/chapi.d.ts.map +1 -0
- package/dist/auth/chapi.js +107 -0
- package/dist/auth/chapi.js.map +1 -0
- package/dist/auth/loginFlow.d.ts +93 -0
- package/dist/auth/loginFlow.d.ts.map +1 -0
- package/dist/auth/loginFlow.js +149 -0
- package/dist/auth/loginFlow.js.map +1 -0
- package/dist/auth/loginRequest.d.ts +66 -0
- package/dist/auth/loginRequest.d.ts.map +1 -0
- package/dist/auth/loginRequest.js +83 -0
- package/dist/auth/loginRequest.js.map +1 -0
- package/dist/auth/verifyResponse.d.ts +52 -0
- package/dist/auth/verifyResponse.d.ts.map +1 -0
- package/dist/auth/verifyResponse.js +138 -0
- package/dist/auth/verifyResponse.js.map +1 -0
- package/dist/auth/verifyResponse.test.d.ts +2 -0
- package/dist/auth/verifyResponse.test.d.ts.map +1 -0
- package/dist/auth/verifyResponse.test.js +282 -0
- package/dist/auth/verifyResponse.test.js.map +1 -0
- package/dist/auth/walletRequestTypes.d.ts +143 -0
- package/dist/auth/walletRequestTypes.d.ts.map +1 -0
- package/dist/auth/walletRequestTypes.js +2 -0
- package/dist/auth/walletRequestTypes.js.map +1 -0
- package/dist/config.d.ts +150 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/dev/cli.d.ts +16 -0
- package/dist/dev/cli.d.ts.map +1 -0
- package/dist/dev/cli.js +122 -0
- package/dist/dev/cli.js.map +1 -0
- package/dist/dev/cli.test.d.ts +2 -0
- package/dist/dev/cli.test.d.ts.map +1 -0
- package/dist/dev/cli.test.js +39 -0
- package/dist/dev/cli.test.js.map +1 -0
- package/dist/dev/index.d.ts +10 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +10 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/provisionDevGrants.d.ts +73 -0
- package/dist/dev/provisionDevGrants.d.ts.map +1 -0
- package/dist/dev/provisionDevGrants.js +176 -0
- package/dist/dev/provisionDevGrants.js.map +1 -0
- package/dist/grants.d.ts +59 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +82 -0
- package/dist/grants.js.map +1 -0
- package/dist/grants.test.d.ts +2 -0
- package/dist/grants.test.d.ts.map +1 -0
- package/dist/grants.test.js +79 -0
- package/dist/grants.test.js.map +1 -0
- package/dist/identity/agents.d.ts +99 -0
- package/dist/identity/agents.d.ts.map +1 -0
- package/dist/identity/agents.js +132 -0
- package/dist/identity/agents.js.map +1 -0
- package/dist/identity/appSession.d.ts +78 -0
- package/dist/identity/appSession.d.ts.map +1 -0
- package/dist/identity/appSession.js +93 -0
- package/dist/identity/appSession.js.map +1 -0
- package/dist/identity/documentLoader.d.ts +20 -0
- package/dist/identity/documentLoader.d.ts.map +1 -0
- package/dist/identity/documentLoader.js +99 -0
- package/dist/identity/documentLoader.js.map +1 -0
- package/dist/identity/initAppSession.d.ts +27 -0
- package/dist/identity/initAppSession.d.ts.map +1 -0
- package/dist/identity/initAppSession.js +30 -0
- package/dist/identity/initAppSession.js.map +1 -0
- package/dist/identity/seedCredential.d.ts +74 -0
- package/dist/identity/seedCredential.d.ts.map +1 -0
- package/dist/identity/seedCredential.js +165 -0
- package/dist/identity/seedCredential.js.map +1 -0
- package/dist/identity/seedCredential.test.d.ts +2 -0
- package/dist/identity/seedCredential.test.d.ts.map +1 -0
- package/dist/identity/seedCredential.test.js +205 -0
- package/dist/identity/seedCredential.test.js.map +1 -0
- package/dist/identity/seedStore.d.ts +27 -0
- package/dist/identity/seedStore.d.ts.map +1 -0
- package/dist/identity/seedStore.js +74 -0
- package/dist/identity/seedStore.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/mui/ProtectedRoute.d.ts +10 -0
- package/dist/mui/ProtectedRoute.d.ts.map +1 -0
- package/dist/mui/ProtectedRoute.js +54 -0
- package/dist/mui/ProtectedRoute.js.map +1 -0
- package/dist/mui/ReconnectBanner.d.ts +2 -0
- package/dist/mui/ReconnectBanner.d.ts.map +1 -0
- package/dist/mui/ReconnectBanner.js +20 -0
- package/dist/mui/ReconnectBanner.js.map +1 -0
- package/dist/mui/SyncStatusChip.d.ts +2 -0
- package/dist/mui/SyncStatusChip.d.ts.map +1 -0
- package/dist/mui/SyncStatusChip.js +27 -0
- package/dist/mui/SyncStatusChip.js.map +1 -0
- package/dist/mui/index.d.ts +12 -0
- package/dist/mui/index.d.ts.map +1 -0
- package/dist/mui/index.js +12 -0
- package/dist/mui/index.js.map +1 -0
- package/dist/react/WasSessionProvider.d.ts +36 -0
- package/dist/react/WasSessionProvider.d.ts.map +1 -0
- package/dist/react/WasSessionProvider.js +35 -0
- package/dist/react/WasSessionProvider.js.map +1 -0
- package/dist/react/hooks.d.ts +66 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +121 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +11 -0
- package/dist/react/index.js.map +1 -0
- package/dist/session/appReadyStore.d.ts +10 -0
- package/dist/session/appReadyStore.d.ts.map +1 -0
- package/dist/session/appReadyStore.js +22 -0
- package/dist/session/appReadyStore.js.map +1 -0
- package/dist/session/authStore.d.ts +71 -0
- package/dist/session/authStore.d.ts.map +1 -0
- package/dist/session/authStore.js +347 -0
- package/dist/session/authStore.js.map +1 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +10 -0
- package/dist/session/index.js.map +1 -0
- package/dist/storage/entityStore.d.ts +51 -0
- package/dist/storage/entityStore.d.ts.map +1 -0
- package/dist/storage/entityStore.js +78 -0
- package/dist/storage/entityStore.js.map +1 -0
- package/dist/storage/localStore.d.ts +144 -0
- package/dist/storage/localStore.d.ts.map +1 -0
- package/dist/storage/localStore.js +289 -0
- package/dist/storage/localStore.js.map +1 -0
- package/dist/storage/rehydrate.d.ts +56 -0
- package/dist/storage/rehydrate.d.ts.map +1 -0
- package/dist/storage/rehydrate.js +87 -0
- package/dist/storage/rehydrate.js.map +1 -0
- package/dist/storage/rehydrate.test.d.ts +2 -0
- package/dist/storage/rehydrate.test.d.ts.map +1 -0
- package/dist/storage/rehydrate.test.js +172 -0
- package/dist/storage/rehydrate.test.js.map +1 -0
- package/dist/storage/storageManager.d.ts +39 -0
- package/dist/storage/storageManager.d.ts.map +1 -0
- package/dist/storage/storageManager.js +68 -0
- package/dist/storage/storageManager.js.map +1 -0
- package/dist/storage/syncController.d.ts +79 -0
- package/dist/storage/syncController.d.ts.map +1 -0
- package/dist/storage/syncController.js +189 -0
- package/dist/storage/syncController.js.map +1 -0
- package/dist/storage/syncStatusStore.d.ts +17 -0
- package/dist/storage/syncStatusStore.d.ts.map +1 -0
- package/dist/storage/syncStatusStore.js +19 -0
- package/dist/storage/syncStatusStore.js.map +1 -0
- package/dist/storage/wasRemoteStore.d.ts +73 -0
- package/dist/storage/wasRemoteStore.d.ts.map +1 -0
- package/dist/storage/wasRemoteStore.js +76 -0
- package/dist/storage/wasRemoteStore.js.map +1 -0
- package/dist/storage/wasSync.d.ts +42 -0
- package/dist/storage/wasSync.d.ts.map +1 -0
- package/dist/storage/wasSync.js +34 -0
- package/dist/storage/wasSync.js.map +1 -0
- package/dist/sync/changesQuery.d.ts +44 -0
- package/dist/sync/changesQuery.d.ts.map +1 -0
- package/dist/sync/changesQuery.js +61 -0
- package/dist/sync/changesQuery.js.map +1 -0
- package/dist/sync/changesQuery.test.d.ts +2 -0
- package/dist/sync/changesQuery.test.d.ts.map +1 -0
- package/dist/sync/changesQuery.test.js +145 -0
- package/dist/sync/changesQuery.test.js.map +1 -0
- package/dist/sync/docCipher.d.ts +68 -0
- package/dist/sync/docCipher.d.ts.map +1 -0
- package/dist/sync/docCipher.js +89 -0
- package/dist/sync/docCipher.js.map +1 -0
- package/dist/sync/feedMasterPort.d.ts +30 -0
- package/dist/sync/feedMasterPort.d.ts.map +1 -0
- package/dist/sync/feedMasterPort.js +58 -0
- package/dist/sync/feedMasterPort.js.map +1 -0
- package/dist/sync/index.d.ts +21 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +21 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/lww.d.ts +29 -0
- package/dist/sync/lww.d.ts.map +1 -0
- package/dist/sync/lww.js +28 -0
- package/dist/sync/lww.js.map +1 -0
- package/dist/sync/lww.test.d.ts +2 -0
- package/dist/sync/lww.test.d.ts.map +1 -0
- package/dist/sync/lww.test.js +28 -0
- package/dist/sync/lww.test.js.map +1 -0
- package/dist/sync/lwwConflictHandler.d.ts +49 -0
- package/dist/sync/lwwConflictHandler.d.ts.map +1 -0
- package/dist/sync/lwwConflictHandler.js +65 -0
- package/dist/sync/lwwConflictHandler.js.map +1 -0
- package/dist/sync/lwwConflictHandler.test.d.ts +2 -0
- package/dist/sync/lwwConflictHandler.test.d.ts.map +1 -0
- package/dist/sync/lwwConflictHandler.test.js +78 -0
- package/dist/sync/lwwConflictHandler.test.js.map +1 -0
- package/dist/sync/pushWrites.d.ts +57 -0
- package/dist/sync/pushWrites.d.ts.map +1 -0
- package/dist/sync/pushWrites.js +159 -0
- package/dist/sync/pushWrites.js.map +1 -0
- package/dist/sync/pushWrites.test.d.ts +2 -0
- package/dist/sync/pushWrites.test.d.ts.map +1 -0
- package/dist/sync/pushWrites.test.js +287 -0
- package/dist/sync/pushWrites.test.js.map +1 -0
- package/dist/sync/syncedDocSchema.d.ts +22 -0
- package/dist/sync/syncedDocSchema.d.ts.map +1 -0
- package/dist/sync/syncedDocSchema.js +26 -0
- package/dist/sync/syncedDocSchema.js.map +1 -0
- package/dist/sync/types.d.ts +194 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +36 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/wasReplication.d.ts +47 -0
- package/dist/sync/wasReplication.d.ts.map +1 -0
- package/dist/sync/wasReplication.js +55 -0
- package/dist/sync/wasReplication.js.map +1 -0
- package/dist/sync/wasSyncPort.d.ts +45 -0
- package/dist/sync/wasSyncPort.d.ts.map +1 -0
- package/dist/sync/wasSyncPort.js +170 -0
- package/dist/sync/wasSyncPort.js.map +1 -0
- package/package.json +142 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createEdvEncryption } from '@interop/was-client/edv';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a stored body is an EDV encryption envelope (carries an object `jwe`)
|
|
4
|
+
* rather than plaintext. Lets read paths stay tolerant of any legacy plaintext
|
|
5
|
+
* row.
|
|
6
|
+
*
|
|
7
|
+
* @param data {Json | undefined}
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isEncryptedEnvelope(data) {
|
|
11
|
+
if (data === undefined || data === null || typeof data !== 'object') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const jwe = data.jwe;
|
|
15
|
+
return jwe !== null && typeof jwe === 'object';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds a {@link DocCipher} for one collection from its derived key material
|
|
19
|
+
* (the per-collection X25519 key agreement key). Keys are supplied directly (no
|
|
20
|
+
* keystore lookup). `idDerivation: 'random'` mints a stable random id updated in
|
|
21
|
+
* place via `sequence` -- the mutable head-document model every entity here uses
|
|
22
|
+
* (constant bump / toggle / re-categorize edits).
|
|
23
|
+
*
|
|
24
|
+
* @param options {object}
|
|
25
|
+
* @param options.keyAgreementKey {IKeyAgreementKey}
|
|
26
|
+
* @param options.keyResolver {IKeyResolver}
|
|
27
|
+
* @param options.collectionId {string} labels errors; the codec is agnostic
|
|
28
|
+
* @returns {Promise<DocCipher>}
|
|
29
|
+
*/
|
|
30
|
+
export async function createDocCipher({ keyAgreementKey, keyResolver, collectionId }) {
|
|
31
|
+
const provider = createEdvEncryption({
|
|
32
|
+
resolveKeys: async () => null,
|
|
33
|
+
idDerivation: 'random'
|
|
34
|
+
});
|
|
35
|
+
const codec = await provider.codecFor({
|
|
36
|
+
spaceId: 'local',
|
|
37
|
+
collectionId,
|
|
38
|
+
scheme: 'edv',
|
|
39
|
+
keys: { keyAgreementKey, keyResolver }
|
|
40
|
+
});
|
|
41
|
+
if (!codec) {
|
|
42
|
+
throw new Error(`Could not build the EDV cipher for collection "${collectionId}".`);
|
|
43
|
+
}
|
|
44
|
+
// Parses the codec's `EncodedWrite` (id + envelope body bytes) into the
|
|
45
|
+
// stored `{ id, envelope }` shape. Shared by the create and update paths.
|
|
46
|
+
const readEncoded = (encoded) => {
|
|
47
|
+
if (typeof encoded.id !== 'string' ||
|
|
48
|
+
!(encoded.body instanceof Uint8Array)) {
|
|
49
|
+
throw new Error(`EDV encrypt for collection "${collectionId}" returned no id/envelope body.`);
|
|
50
|
+
}
|
|
51
|
+
const envelope = JSON.parse(new TextDecoder().decode(encoded.body));
|
|
52
|
+
return { id: encoded.id, envelope };
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
async encrypt({ data }) {
|
|
56
|
+
// `encode` with no caller id is the create path: encrypt, then use the
|
|
57
|
+
// minted random id.
|
|
58
|
+
const encoded = await codec.encode({
|
|
59
|
+
data: data
|
|
60
|
+
});
|
|
61
|
+
return readEncoded(encoded);
|
|
62
|
+
},
|
|
63
|
+
async encryptUpdate({ id, data, current }) {
|
|
64
|
+
// The update path: hand the codec the prior stored envelope so it advances
|
|
65
|
+
// `sequence` from it and re-encrypts under the same id. The codec reads
|
|
66
|
+
// `current.data` (the prior envelope) and its `etag` header (unused here;
|
|
67
|
+
// the wire `If-Match` is derived from the synced-doc `version`, not this).
|
|
68
|
+
const priorResponse = {
|
|
69
|
+
data: current,
|
|
70
|
+
json: async () => current,
|
|
71
|
+
headers: { get: () => null }
|
|
72
|
+
};
|
|
73
|
+
const encoded = await codec.encode({
|
|
74
|
+
id,
|
|
75
|
+
data: data,
|
|
76
|
+
current: priorResponse
|
|
77
|
+
});
|
|
78
|
+
return readEncoded(encoded);
|
|
79
|
+
},
|
|
80
|
+
async decrypt({ envelope }) {
|
|
81
|
+
const response = {
|
|
82
|
+
data: envelope,
|
|
83
|
+
json: async () => envelope
|
|
84
|
+
};
|
|
85
|
+
return (await codec.decode(response));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=docCipher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docCipher.js","sourceRoot":"","sources":["../../src/sync/docCipher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
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.
|
|
8
|
+
*
|
|
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.
|
|
20
|
+
*/
|
|
21
|
+
import type { WasSyncPort } from './types.js';
|
|
22
|
+
/**
|
|
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.
|
|
25
|
+
*
|
|
26
|
+
* @param base {WasSyncPort}
|
|
27
|
+
* @returns {WasSyncPort}
|
|
28
|
+
*/
|
|
29
|
+
export declare function withFeedMasterRead(base: WasSyncPort): WasSyncPort;
|
|
30
|
+
//# sourceMappingURL=feedMasterPort.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// A generous page cap so a pathological feed cannot spin forever; conflicts are
|
|
2
|
+
// rare and the dev/e2e feed is small, so a full scan is acceptable.
|
|
3
|
+
const MAX_PAGES = 50;
|
|
4
|
+
const PAGE_SIZE = 500;
|
|
5
|
+
/**
|
|
6
|
+
* Builds a `MasterState` from a `changes`-feed document (all fields in-body).
|
|
7
|
+
*/
|
|
8
|
+
function toMasterState(doc) {
|
|
9
|
+
const master = {
|
|
10
|
+
version: doc.version,
|
|
11
|
+
updatedAt: doc.updatedAt,
|
|
12
|
+
deleted: doc._deleted
|
|
13
|
+
};
|
|
14
|
+
if (doc.data !== undefined) {
|
|
15
|
+
master.data = doc.data;
|
|
16
|
+
}
|
|
17
|
+
if (doc.metaVersion !== undefined) {
|
|
18
|
+
master.metaVersion = doc.metaVersion;
|
|
19
|
+
}
|
|
20
|
+
if (doc.custom !== undefined) {
|
|
21
|
+
master.custom = doc.custom;
|
|
22
|
+
}
|
|
23
|
+
return master;
|
|
24
|
+
}
|
|
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.
|
|
28
|
+
*
|
|
29
|
+
* @param base {WasSyncPort}
|
|
30
|
+
* @returns {WasSyncPort}
|
|
31
|
+
*/
|
|
32
|
+
export function withFeedMasterRead(base) {
|
|
33
|
+
return {
|
|
34
|
+
query: base.query.bind(base),
|
|
35
|
+
putContent: base.putContent.bind(base),
|
|
36
|
+
deleteContent: base.deleteContent.bind(base),
|
|
37
|
+
putMeta: base.putMeta.bind(base),
|
|
38
|
+
async get({ id }) {
|
|
39
|
+
let checkpoint;
|
|
40
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
41
|
+
const { documents, checkpoint: next } = await base.query({
|
|
42
|
+
...(checkpoint !== undefined && { checkpoint }),
|
|
43
|
+
limit: PAGE_SIZE
|
|
44
|
+
});
|
|
45
|
+
const found = documents.find(doc => doc.id === id);
|
|
46
|
+
if (found) {
|
|
47
|
+
return toMasterState(found);
|
|
48
|
+
}
|
|
49
|
+
if (next === null || documents.length === 0) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
checkpoint = next;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=feedMasterPort.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Public surface of the collection-agnostic WAS replication layer. Framework-
|
|
6
|
+
* agnostic: consumers supply an RxDB collection and a {@link WasSyncPort};
|
|
7
|
+
* nothing in the core adapter imports React. The port adapter
|
|
8
|
+
* (`createWasSyncPort`) and the document cipher (`createDocCipher`) bridge to
|
|
9
|
+
* `@interop/was-client`.
|
|
10
|
+
*/
|
|
11
|
+
export { createWasReplication } from './wasReplication.js';
|
|
12
|
+
export { syncedDocSchema } from './syncedDocSchema.js';
|
|
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';
|
|
17
|
+
export { withFeedMasterRead } from './feedMasterPort.js';
|
|
18
|
+
export { createDocCipher, isEncryptedEnvelope, type DocCipher } from './docCipher.js';
|
|
19
|
+
export { makeLwwConflictHandler } from './lwwConflictHandler.js';
|
|
20
|
+
export { remotePayloadWins } from './lww.js';
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Public surface of the collection-agnostic WAS replication layer. Framework-
|
|
6
|
+
* agnostic: consumers supply an RxDB collection and a {@link WasSyncPort};
|
|
7
|
+
* nothing in the core adapter imports React. The port adapter
|
|
8
|
+
* (`createWasSyncPort`) and the document cipher (`createDocCipher`) bridge to
|
|
9
|
+
* `@interop/was-client`.
|
|
10
|
+
*/
|
|
11
|
+
export { createWasReplication } from './wasReplication.js';
|
|
12
|
+
export { syncedDocSchema } from './syncedDocSchema.js';
|
|
13
|
+
export { createPullHandler, wireDocToRxDoc } from './changesQuery.js';
|
|
14
|
+
export { createPushHandler, formatEtag } from './pushWrites.js';
|
|
15
|
+
export { WasSyncConflictError } from './types.js';
|
|
16
|
+
export { createWasSyncPort } from './wasSyncPort.js';
|
|
17
|
+
export { withFeedMasterRead } from './feedMasterPort.js';
|
|
18
|
+
export { createDocCipher, isEncryptedEnvelope } from './docCipher.js';
|
|
19
|
+
export { makeLwwConflictHandler } from './lwwConflictHandler.js';
|
|
20
|
+
export { remotePayloadWins } from './lww.js';
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The last-write-wins tiebreak for a mutable head document. Pure -- no React, no
|
|
6
|
+
* storage imports -- so both replicas run this identical rule against the same
|
|
7
|
+
* two payloads and converge on the same winner with no coordination.
|
|
8
|
+
*
|
|
9
|
+
* `updatedAt` (an ISO-8601 string, so lexical compare == chronological compare)
|
|
10
|
+
* decides; `deviceId` breaks an exact tie deterministically. Both fields live in
|
|
11
|
+
* the payload (never the envelope-level checkpoint `updatedAt`).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Whether the remote payload wins over the local one under last-write-wins.
|
|
15
|
+
* Later `updatedAt` wins; on an exact `updatedAt` tie the lexically greater
|
|
16
|
+
* `deviceId` wins (an arbitrary but deterministic, replica-independent choice).
|
|
17
|
+
*
|
|
18
|
+
* @param remote {{ updatedAt: string; deviceId: string }}
|
|
19
|
+
* @param local {{ updatedAt: string; deviceId: string }}
|
|
20
|
+
* @returns {boolean} true if the remote payload should replace the local one
|
|
21
|
+
*/
|
|
22
|
+
export declare function remotePayloadWins(remote: {
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
deviceId: string;
|
|
25
|
+
}, local: {
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
deviceId: string;
|
|
28
|
+
}): boolean;
|
|
29
|
+
//# sourceMappingURL=lww.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lww.d.ts","sourceRoot":"","sources":["../../src/sync/lww.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAC/C,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC7C,OAAO,CAKT"}
|
package/dist/sync/lww.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The last-write-wins tiebreak for a mutable head document. Pure -- no React, no
|
|
6
|
+
* storage imports -- so both replicas run this identical rule against the same
|
|
7
|
+
* two payloads and converge on the same winner with no coordination.
|
|
8
|
+
*
|
|
9
|
+
* `updatedAt` (an ISO-8601 string, so lexical compare == chronological compare)
|
|
10
|
+
* decides; `deviceId` breaks an exact tie deterministically. Both fields live in
|
|
11
|
+
* the payload (never the envelope-level checkpoint `updatedAt`).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Whether the remote payload wins over the local one under last-write-wins.
|
|
15
|
+
* Later `updatedAt` wins; on an exact `updatedAt` tie the lexically greater
|
|
16
|
+
* `deviceId` wins (an arbitrary but deterministic, replica-independent choice).
|
|
17
|
+
*
|
|
18
|
+
* @param remote {{ updatedAt: string; deviceId: string }}
|
|
19
|
+
* @param local {{ updatedAt: string; deviceId: string }}
|
|
20
|
+
* @returns {boolean} true if the remote payload should replace the local one
|
|
21
|
+
*/
|
|
22
|
+
export function remotePayloadWins(remote, local) {
|
|
23
|
+
if (remote.updatedAt !== local.updatedAt) {
|
|
24
|
+
return remote.updatedAt > local.updatedAt;
|
|
25
|
+
}
|
|
26
|
+
return remote.deviceId > local.deviceId;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=lww.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lww.js","sourceRoot":"","sources":["../../src/sync/lww.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAA+C,EAC/C,KAA8C;IAE9C,IAAI,MAAM,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,EAAE,CAAC;QACzC,OAAO,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;IAC3C,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;AACzC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lww.test.d.ts","sourceRoot":"","sources":["../../src/sync/lww.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Unit tests for the last-write-wins tiebreak.
|
|
6
|
+
*
|
|
7
|
+
* @vitest-environment node
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { remotePayloadWins } from './lww.js';
|
|
11
|
+
describe('remotePayloadWins', () => {
|
|
12
|
+
it('later updatedAt wins (remote newer)', () => {
|
|
13
|
+
expect(remotePayloadWins({ updatedAt: '2026-01-02T00:00:00.000Z', deviceId: 'a' }, { updatedAt: '2026-01-01T00:00:00.000Z', deviceId: 'z' })).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('earlier updatedAt loses (local newer)', () => {
|
|
16
|
+
expect(remotePayloadWins({ updatedAt: '2026-01-01T00:00:00.000Z', deviceId: 'z' }, { updatedAt: '2026-01-02T00:00:00.000Z', deviceId: 'a' })).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it('breaks an exact updatedAt tie by greater deviceId', () => {
|
|
19
|
+
const at = '2026-01-01T00:00:00.000Z';
|
|
20
|
+
expect(remotePayloadWins({ updatedAt: at, deviceId: 'b' }, { updatedAt: at, deviceId: 'a' })).toBe(true);
|
|
21
|
+
expect(remotePayloadWins({ updatedAt: at, deviceId: 'a' }, { updatedAt: at, deviceId: 'b' })).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it('a fully identical payload does not let remote win', () => {
|
|
24
|
+
const same = { updatedAt: '2026-01-01T00:00:00.000Z', deviceId: 'a' };
|
|
25
|
+
expect(remotePayloadWins(same, same)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=lww.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lww.test.js","sourceRoot":"","sources":["../../src/sync/lww.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAE5C,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CACJ,iBAAiB,CACf,EAAE,SAAS,EAAE,0BAA0B,EAAE,QAAQ,EAAE,GAAG,EAAE,EACxD,EAAE,SAAS,EAAE,0BAA0B,EAAE,QAAQ,EAAE,GAAG,EAAE,CACzD,CACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACd,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CACJ,iBAAiB,CACf,EAAE,SAAS,EAAE,0BAA0B,EAAE,QAAQ,EAAE,GAAG,EAAE,EACxD,EAAE,SAAS,EAAE,0BAA0B,EAAE,QAAQ,EAAE,GAAG,EAAE,CACzD,CACF,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACf,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,EAAE,GAAG,0BAA0B,CAAA;QACrC,MAAM,CACJ,iBAAiB,CACf,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,EAChC,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CACjC,CACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACZ,MAAM,CACJ,iBAAiB,CACf,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,EAChC,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CACjC,CACF,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACf,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,IAAI,GAAG,EAAE,SAAS,EAAE,0BAA0B,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAA;QACrE,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The RxDB conflict handler for a mutable-head (LWW) collection. RxDB's default
|
|
6
|
+
* handler always drops the local fork and keeps the remote master, which is
|
|
7
|
+
* correct for content-addressed (immutable-per-id) collections but wrong here:
|
|
8
|
+
* every entity is a mutable head that two devices can edit concurrently, so a
|
|
9
|
+
* genuine content conflict must be settled by last-write-wins on the payload's
|
|
10
|
+
* own `updatedAt` (deviceId tiebreak) -- exactly the rule two offline replicas
|
|
11
|
+
* apply independently to converge.
|
|
12
|
+
*
|
|
13
|
+
* The wrinkle is that the conflicting bodies are EDV envelopes (ciphertext), so
|
|
14
|
+
* `resolve` must decrypt both sides through this collection's cipher before it
|
|
15
|
+
* can compare the plaintext `updatedAt` / `deviceId`. `isEqual` stays cheap and
|
|
16
|
+
* synchronous (a structural compare of the opaque bodies), as RxDB requires.
|
|
17
|
+
*
|
|
18
|
+
* Convergence: the server holds ONE winner of the push race as `realMasterState`;
|
|
19
|
+
* every replica compares that same master against its own local edit, and the
|
|
20
|
+
* `payloadWins` comparator is a total order over `(updatedAt, deviceId)`, so the
|
|
21
|
+
* globally-latest payload wins on every replica with no coordination.
|
|
22
|
+
*/
|
|
23
|
+
import type { WithDeleted } from 'rxdb/plugins/core';
|
|
24
|
+
import type { Json, SyncedDoc } from './types.js';
|
|
25
|
+
/** The LWW fields read out of a decrypted entity payload. */
|
|
26
|
+
interface LwwPayload {
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
deviceId: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Builds an RxDB conflict handler that settles content conflicts by payload LWW,
|
|
32
|
+
* decrypting through the supplied per-collection `decrypt`.
|
|
33
|
+
*
|
|
34
|
+
* @param decrypt {(envelope: Json) => Promise<Json>} this collection's decrypt
|
|
35
|
+
* @param [payloadWins] {(remote: LwwPayload, local: LwwPayload) => boolean}
|
|
36
|
+
* the total-order comparator deciding whether the remote payload replaces the
|
|
37
|
+
* local one; defaults to {@link remotePayloadWins} (later `updatedAt` wins,
|
|
38
|
+
* `deviceId` breaks a tie)
|
|
39
|
+
* @returns {import('rxdb/plugins/core').RxConflictHandler<SyncedDoc>}
|
|
40
|
+
*/
|
|
41
|
+
export declare function makeLwwConflictHandler(decrypt: (envelope: Json) => Promise<Json>, payloadWins?: (remote: LwwPayload, local: LwwPayload) => boolean): {
|
|
42
|
+
isEqual(a: WithDeleted<SyncedDoc>, b: WithDeleted<SyncedDoc>): boolean;
|
|
43
|
+
resolve({ realMasterState, newDocumentState }: {
|
|
44
|
+
realMasterState: WithDeleted<SyncedDoc>;
|
|
45
|
+
newDocumentState: WithDeleted<SyncedDoc>;
|
|
46
|
+
}): Promise<WithDeleted<SyncedDoc>>;
|
|
47
|
+
};
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=lwwConflictHandler.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { remotePayloadWins } from './lww.js';
|
|
2
|
+
/** Structural equality of two opaque bodies (used for the fast `isEqual`). */
|
|
3
|
+
function bodiesEqual(a, b) {
|
|
4
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Decrypts one side's envelope into its `{ updatedAt, deviceId }`, or `null`
|
|
8
|
+
* when the side has no decryptable payload (a tombstone, or an absent/corrupt
|
|
9
|
+
* body). `null` means "no comparable timestamp", handled by the caller.
|
|
10
|
+
*/
|
|
11
|
+
async function lwwFieldsOf(doc, decrypt) {
|
|
12
|
+
if (doc._deleted || doc.data === undefined) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const payload = (await decrypt(doc.data));
|
|
17
|
+
if (typeof payload.updatedAt === 'string' &&
|
|
18
|
+
typeof payload.deviceId === 'string') {
|
|
19
|
+
return { updatedAt: payload.updatedAt, deviceId: payload.deviceId };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Builds an RxDB conflict handler that settles content conflicts by payload LWW,
|
|
29
|
+
* decrypting through the supplied per-collection `decrypt`.
|
|
30
|
+
*
|
|
31
|
+
* @param decrypt {(envelope: Json) => Promise<Json>} this collection's decrypt
|
|
32
|
+
* @param [payloadWins] {(remote: LwwPayload, local: LwwPayload) => boolean}
|
|
33
|
+
* the total-order comparator deciding whether the remote payload replaces the
|
|
34
|
+
* local one; defaults to {@link remotePayloadWins} (later `updatedAt` wins,
|
|
35
|
+
* `deviceId` breaks a tie)
|
|
36
|
+
* @returns {import('rxdb/plugins/core').RxConflictHandler<SyncedDoc>}
|
|
37
|
+
*/
|
|
38
|
+
export function makeLwwConflictHandler(decrypt, payloadWins = remotePayloadWins) {
|
|
39
|
+
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
|
+
isEqual(a, b) {
|
|
43
|
+
return a._deleted === b._deleted && bodiesEqual(a.data, b.data);
|
|
44
|
+
},
|
|
45
|
+
async resolve({ realMasterState, newDocumentState }) {
|
|
46
|
+
const [remote, local] = await Promise.all([
|
|
47
|
+
lwwFieldsOf(realMasterState, decrypt),
|
|
48
|
+
lwwFieldsOf(newDocumentState, decrypt)
|
|
49
|
+
]);
|
|
50
|
+
// Both sides comparable: pure payload LWW.
|
|
51
|
+
if (remote && local) {
|
|
52
|
+
return payloadWins(remote, local) ? realMasterState : newDocumentState;
|
|
53
|
+
}
|
|
54
|
+
// A live local edit against a remote tombstone (or otherwise
|
|
55
|
+
// incomparable remote): keep the edit (resurrect / re-assert the write).
|
|
56
|
+
if (local && !remote) {
|
|
57
|
+
return newDocumentState;
|
|
58
|
+
}
|
|
59
|
+
// Everything else (local tombstone, both incomparable): the master wins,
|
|
60
|
+
// which is deterministic and convergent across replicas.
|
|
61
|
+
return realMasterState;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=lwwConflictHandler.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lwwConflictHandler.test.d.ts","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { makeLwwConflictHandler } from './lwwConflictHandler.js';
|
|
6
|
+
// A fake cipher whose "envelope" is just the plaintext payload wrapped in
|
|
7
|
+
// `{ jwe: payload }`; decrypt unwraps it. Lets us drive the handler's LWW
|
|
8
|
+
// decision without real crypto.
|
|
9
|
+
const decrypt = async (envelope) => envelope.jwe;
|
|
10
|
+
function row(payload, { deleted = false, version = 0 } = {}) {
|
|
11
|
+
const doc = {
|
|
12
|
+
id: 'r1',
|
|
13
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
14
|
+
version,
|
|
15
|
+
_deleted: deleted
|
|
16
|
+
};
|
|
17
|
+
if (payload !== null) {
|
|
18
|
+
doc.data = { jwe: payload };
|
|
19
|
+
}
|
|
20
|
+
return doc;
|
|
21
|
+
}
|
|
22
|
+
const handler = makeLwwConflictHandler(decrypt);
|
|
23
|
+
describe('makeLwwConflictHandler', () => {
|
|
24
|
+
it('isEqual is true only when body + deletion agree', () => {
|
|
25
|
+
const a = row({ updatedAt: 't1', deviceId: 'd1' });
|
|
26
|
+
const b = row({ updatedAt: 't1', deviceId: 'd1' });
|
|
27
|
+
const c = row({ updatedAt: 't2', deviceId: 'd1' });
|
|
28
|
+
expect(handler.isEqual(a, b)).toBe(true);
|
|
29
|
+
expect(handler.isEqual(a, c)).toBe(false);
|
|
30
|
+
expect(handler.isEqual(a, { ...b, _deleted: true })).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
it('resolves to the later payload (remote wins)', async () => {
|
|
33
|
+
const remote = row({ updatedAt: '2026-02-02T00:00:00Z', deviceId: 'dB' });
|
|
34
|
+
const local = row({ updatedAt: '2026-01-01T00:00:00Z', deviceId: 'dA' });
|
|
35
|
+
const winner = await handler.resolve({
|
|
36
|
+
realMasterState: remote,
|
|
37
|
+
newDocumentState: local
|
|
38
|
+
});
|
|
39
|
+
expect(winner).toBe(remote);
|
|
40
|
+
});
|
|
41
|
+
it('resolves to the later payload (local wins)', async () => {
|
|
42
|
+
const remote = row({ updatedAt: '2026-01-01T00:00:00Z', deviceId: 'dB' });
|
|
43
|
+
const local = row({ updatedAt: '2026-02-02T00:00:00Z', deviceId: 'dA' });
|
|
44
|
+
const winner = await handler.resolve({
|
|
45
|
+
realMasterState: remote,
|
|
46
|
+
newDocumentState: local
|
|
47
|
+
});
|
|
48
|
+
expect(winner).toBe(local);
|
|
49
|
+
});
|
|
50
|
+
it('breaks an exact updatedAt tie by greater deviceId', async () => {
|
|
51
|
+
const remote = row({ updatedAt: 'T', deviceId: 'dZ' });
|
|
52
|
+
const local = row({ updatedAt: 'T', deviceId: 'dA' });
|
|
53
|
+
const winner = await handler.resolve({
|
|
54
|
+
realMasterState: remote,
|
|
55
|
+
newDocumentState: local
|
|
56
|
+
});
|
|
57
|
+
expect(winner).toBe(remote);
|
|
58
|
+
});
|
|
59
|
+
it('keeps a live local edit over a remote tombstone', async () => {
|
|
60
|
+
const remote = row(null, { deleted: true, version: 3 });
|
|
61
|
+
const local = row({ updatedAt: 'T', deviceId: 'dA' });
|
|
62
|
+
const winner = await handler.resolve({
|
|
63
|
+
realMasterState: remote,
|
|
64
|
+
newDocumentState: local
|
|
65
|
+
});
|
|
66
|
+
expect(winner).toBe(local);
|
|
67
|
+
});
|
|
68
|
+
it('defaults to the master when incomparable (local tombstone)', async () => {
|
|
69
|
+
const remote = row({ updatedAt: 'T', deviceId: 'dB' }, { version: 2 });
|
|
70
|
+
const local = row(null, { deleted: true });
|
|
71
|
+
const winner = await handler.resolve({
|
|
72
|
+
realMasterState: remote,
|
|
73
|
+
newDocumentState: local
|
|
74
|
+
});
|
|
75
|
+
expect(winner).toBe(remote);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
//# sourceMappingURL=lwwConflictHandler.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lwwConflictHandler.test.js","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The push side of the WAS replication adapter: fans each local change out to
|
|
6
|
+
* conditional WAS writes and assembles the RxDB conflict entry when the server
|
|
7
|
+
* rejects a write with `412`.
|
|
8
|
+
*
|
|
9
|
+
* A single RxDB document spans two independently-versioned sub-resources: the
|
|
10
|
+
* content (`data` / `version`, at `PUT/DELETE /:id`) and the metadata (`custom`
|
|
11
|
+
* / `metaVersion`, at `PUT /:id/meta`). This handler diffs the new local state
|
|
12
|
+
* against the assumed master to route each half:
|
|
13
|
+
*
|
|
14
|
+
* - content changed -> `PUT /:id` (`If-Match: "<version>"`) or, on create,
|
|
15
|
+
* `PUT /:id` (`If-None-Match: *`); a delete -> `DELETE /:id`.
|
|
16
|
+
* - metadata changed -> `PUT /:id/meta` (`If-Match: "<metaVersion>"`, or
|
|
17
|
+
* `If-None-Match: *` when the resource has no metadata yet).
|
|
18
|
+
*
|
|
19
|
+
* Content is written before metadata on a create, because the server rejects a
|
|
20
|
+
* `/meta` write to a resource that does not yet exist.
|
|
21
|
+
*
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
import type { WithDeleted } from 'rxdb/plugins/core';
|
|
32
|
+
import type { SyncedDoc, WasSyncPort } from './types.js';
|
|
33
|
+
/**
|
|
34
|
+
* Formats a master revision (`version` or `metaVersion`) as the quoted strong
|
|
35
|
+
* ETag the server compares `If-Match` against (revision `3` becomes `"3"`).
|
|
36
|
+
*
|
|
37
|
+
* @param revision {number}
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatEtag(revision: number): string;
|
|
41
|
+
/**
|
|
42
|
+
* Builds the RxDB push handler that fans a batch of local changes out to
|
|
43
|
+
* conditional WAS writes and returns the conflicting rows' master states.
|
|
44
|
+
*
|
|
45
|
+
* Rows are pushed concurrently; if any non-conflict error is thrown the whole
|
|
46
|
+
* batch rejects (RxDB re-sends it later), matching RxDB's all-or-nothing retry.
|
|
47
|
+
*
|
|
48
|
+
* @param port {WasSyncPort}
|
|
49
|
+
* @returns {(rows: Array<{ newDocumentState: WithDeleted<SyncedDoc>,
|
|
50
|
+
* assumedMasterState?: WithDeleted<SyncedDoc> }>) =>
|
|
51
|
+
* Promise<WithDeleted<SyncedDoc>[]>}
|
|
52
|
+
*/
|
|
53
|
+
export declare function createPushHandler(port: WasSyncPort): (rows: Array<{
|
|
54
|
+
newDocumentState: WithDeleted<SyncedDoc>;
|
|
55
|
+
assumedMasterState?: WithDeleted<SyncedDoc>;
|
|
56
|
+
}>) => Promise<WithDeleted<SyncedDoc>[]>;
|
|
57
|
+
//# sourceMappingURL=pushWrites.d.ts.map
|
|
@@ -0,0 +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"}
|