@interop/was-react 0.8.2 → 0.9.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/README.md +55 -44
- package/dist/auth/chapi.d.ts.map +1 -1
- package/dist/auth/chapi.js +16 -5
- package/dist/auth/chapi.js.map +1 -1
- package/dist/auth/loginFlow.d.ts.map +1 -1
- package/dist/auth/loginFlow.js +55 -49
- package/dist/auth/loginFlow.js.map +1 -1
- package/dist/auth/verifyResponse.d.ts.map +1 -1
- package/dist/auth/verifyResponse.js +27 -10
- package/dist/auth/verifyResponse.js.map +1 -1
- package/dist/auth/walletRequestTypes.d.ts +9 -90
- package/dist/auth/walletRequestTypes.d.ts.map +1 -1
- package/dist/config.d.ts +9 -9
- package/dist/config.js +0 -0
- package/dist/dev/provisionDevGrants.d.ts.map +1 -1
- package/dist/dev/provisionDevGrants.js +18 -19
- package/dist/dev/provisionDevGrants.js.map +1 -1
- package/dist/grants.d.ts +19 -9
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +17 -2
- package/dist/grants.js.map +1 -1
- package/dist/identity/agents.d.ts +1 -1
- package/dist/identity/agents.d.ts.map +1 -1
- package/dist/identity/agents.js +10 -25
- package/dist/identity/agents.js.map +1 -1
- package/dist/identity/appSession.d.ts +27 -0
- package/dist/identity/appSession.d.ts.map +1 -1
- package/dist/identity/appSession.js +51 -13
- package/dist/identity/appSession.js.map +1 -1
- package/dist/identity/seedCredential.d.ts +7 -0
- package/dist/identity/seedCredential.d.ts.map +1 -1
- package/dist/identity/seedCredential.js +11 -14
- package/dist/identity/seedCredential.js.map +1 -1
- package/dist/identity/seedStore.d.ts +19 -0
- package/dist/identity/seedStore.d.ts.map +1 -1
- package/dist/identity/seedStore.js +40 -13
- package/dist/identity/seedStore.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/jsonLd.d.ts +19 -0
- package/dist/jsonLd.d.ts.map +1 -0
- package/dist/jsonLd.js +24 -0
- package/dist/jsonLd.js.map +1 -0
- package/dist/mui/AdoptDialog.d.ts.map +1 -1
- package/dist/mui/AdoptDialog.js +7 -3
- package/dist/mui/AdoptDialog.js.map +1 -1
- package/dist/mui/ClearDataDialog.d.ts.map +1 -1
- package/dist/mui/ClearDataDialog.js +12 -11
- package/dist/mui/ClearDataDialog.js.map +1 -1
- package/dist/mui/ConfirmDialog.d.ts +35 -0
- package/dist/mui/ConfirmDialog.d.ts.map +1 -0
- package/dist/mui/ConfirmDialog.js +18 -0
- package/dist/mui/ConfirmDialog.js.map +1 -0
- package/dist/mui/LogoutDialog.d.ts.map +1 -1
- package/dist/mui/LogoutDialog.js +7 -3
- package/dist/mui/LogoutDialog.js.map +1 -1
- package/dist/react/documentApp.d.ts +29 -0
- package/dist/react/documentApp.d.ts.map +1 -1
- package/dist/react/documentApp.js +9 -38
- package/dist/react/documentApp.js.map +1 -1
- package/dist/react/hooks.d.ts +13 -7
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/hooks.js +17 -11
- package/dist/react/hooks.js.map +1 -1
- package/dist/session/authStore.d.ts +9 -9
- package/dist/session/authStore.d.ts.map +1 -1
- package/dist/session/authStore.js +203 -112
- package/dist/session/authStore.js.map +1 -1
- package/dist/storage/adopt.d.ts +5 -1
- package/dist/storage/adopt.d.ts.map +1 -1
- package/dist/storage/adopt.js +8 -24
- package/dist/storage/adopt.js.map +1 -1
- package/dist/storage/entityStore.d.ts.map +1 -1
- package/dist/storage/entityStore.js +27 -20
- package/dist/storage/entityStore.js.map +1 -1
- package/dist/storage/localStore.d.ts +12 -6
- package/dist/storage/localStore.d.ts.map +1 -1
- package/dist/storage/localStore.js +146 -77
- package/dist/storage/localStore.js.map +1 -1
- package/dist/storage/rehydrate.d.ts.map +1 -1
- package/dist/storage/rehydrate.js +20 -4
- package/dist/storage/rehydrate.js.map +1 -1
- package/dist/storage/sharedCollectionReader.d.ts +1 -1
- package/dist/storage/sharedCollectionReader.d.ts.map +1 -1
- package/dist/storage/sharedCollectionReader.js +79 -64
- package/dist/storage/sharedCollectionReader.js.map +1 -1
- package/dist/storage/storageManager.d.ts +3 -1
- package/dist/storage/storageManager.d.ts.map +1 -1
- package/dist/storage/storageManager.js +20 -6
- package/dist/storage/storageManager.js.map +1 -1
- package/dist/storage/syncController.d.ts +1 -16
- package/dist/storage/syncController.d.ts.map +1 -1
- package/dist/storage/syncController.js +48 -37
- package/dist/storage/syncController.js.map +1 -1
- package/dist/storage/syncStatusStore.d.ts +8 -6
- package/dist/storage/syncStatusStore.d.ts.map +1 -1
- package/dist/storage/syncStatusStore.js +6 -6
- package/dist/storage/syncStatusStore.js.map +1 -1
- package/dist/storage/wasRemoteStore.d.ts +34 -2
- package/dist/storage/wasRemoteStore.d.ts.map +1 -1
- package/dist/storage/wasRemoteStore.js +45 -7
- package/dist/storage/wasRemoteStore.js.map +1 -1
- package/dist/storage/wasSync.d.ts.map +1 -1
- package/dist/storage/wasSync.js +47 -38
- package/dist/storage/wasSync.js.map +1 -1
- package/dist/sync/changesQuery.d.ts.map +1 -1
- package/dist/sync/changesQuery.js +2 -9
- package/dist/sync/changesQuery.js.map +1 -1
- package/dist/sync/docCipher.d.ts +23 -10
- package/dist/sync/docCipher.d.ts.map +1 -1
- package/dist/sync/docCipher.js +17 -11
- package/dist/sync/docCipher.js.map +1 -1
- package/dist/sync/feedMasterPort.d.ts +6 -0
- package/dist/sync/feedMasterPort.d.ts.map +1 -1
- package/dist/sync/feedMasterPort.js +82 -37
- package/dist/sync/feedMasterPort.js.map +1 -1
- package/dist/sync/index.d.ts +6 -5
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +6 -3
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/lww.d.ts +27 -18
- package/dist/sync/lww.d.ts.map +1 -1
- package/dist/sync/lww.js +35 -8
- package/dist/sync/lww.js.map +1 -1
- package/dist/sync/lwwConflictHandler.d.ts +15 -14
- package/dist/sync/lwwConflictHandler.d.ts.map +1 -1
- package/dist/sync/lwwConflictHandler.js +37 -26
- package/dist/sync/lwwConflictHandler.js.map +1 -1
- package/dist/sync/pushWrites.d.ts +9 -9
- package/dist/sync/pushWrites.d.ts.map +1 -1
- package/dist/sync/pushWrites.js +102 -68
- package/dist/sync/pushWrites.js.map +1 -1
- package/dist/sync/syncedDocSchema.d.ts.map +1 -1
- package/dist/sync/syncedDocSchema.js +3 -0
- package/dist/sync/syncedDocSchema.js.map +1 -1
- package/dist/sync/types.d.ts +115 -15
- package/dist/sync/types.d.ts.map +1 -1
- package/dist/sync/types.js +36 -18
- package/dist/sync/types.js.map +1 -1
- package/dist/sync/wasSyncPort.d.ts +0 -11
- package/dist/sync/wasSyncPort.d.ts.map +1 -1
- package/dist/sync/wasSyncPort.js +20 -36
- package/dist/sync/wasSyncPort.js.map +1 -1
- package/package.json +6 -6
- package/dist/identity/initAppSession.d.ts +0 -27
- package/dist/identity/initAppSession.d.ts.map +0 -1
- package/dist/identity/initAppSession.js +0 -30
- package/dist/identity/initAppSession.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { copyOptionalBodyFields } from './types.js';
|
|
1
2
|
// A generous page cap so a pathological feed cannot spin forever; conflicts are
|
|
2
3
|
// rare and the dev/e2e feed is small, so a full scan is acceptable.
|
|
3
4
|
const MAX_PAGES = 50;
|
|
@@ -11,22 +12,71 @@ function toMasterState(doc) {
|
|
|
11
12
|
updatedAt: doc.updatedAt,
|
|
12
13
|
deleted: doc._deleted
|
|
13
14
|
};
|
|
14
|
-
|
|
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
|
-
}
|
|
15
|
+
copyOptionalBodyFields({ source: doc, target: master });
|
|
23
16
|
return master;
|
|
24
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Walks the changes feed from its origin looking for one resource, memoizing
|
|
20
|
+
* every document it pages past when the caller supplied a batch `cache`.
|
|
21
|
+
*
|
|
22
|
+
* @param options {object}
|
|
23
|
+
* @param options.base {WasSyncBasePort}
|
|
24
|
+
* @param options.id {string}
|
|
25
|
+
* @param [options.cache] {MasterReadCache}
|
|
26
|
+
* @returns {Promise<MasterState | null>}
|
|
27
|
+
*/
|
|
28
|
+
async function walkFeedFor({ base, id, cache }) {
|
|
29
|
+
let checkpoint;
|
|
30
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
31
|
+
const { documents, checkpoint: next } = await base.query({
|
|
32
|
+
...(checkpoint !== undefined && { checkpoint }),
|
|
33
|
+
limit: PAGE_SIZE
|
|
34
|
+
});
|
|
35
|
+
const byId = new Map(documents.map(doc => [doc.id, doc]));
|
|
36
|
+
if (cache !== undefined) {
|
|
37
|
+
for (const [docId, doc] of byId) {
|
|
38
|
+
cache.byId.set(docId, toMasterState(doc));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const found = byId.get(id);
|
|
42
|
+
if (found) {
|
|
43
|
+
return toMasterState(found);
|
|
44
|
+
}
|
|
45
|
+
// Reached the end of the feed without finding it: the resource is
|
|
46
|
+
// genuinely absent (a delete/delete race), so report `null` and let the
|
|
47
|
+
// conflict assembler synthesize a tombstone. Only THIS id is memoized as
|
|
48
|
+
// absent, never "every id this completed walk did not see": a row of the
|
|
49
|
+
// same batch may have created its resource after this walk read the feed,
|
|
50
|
+
// and answering `null` for it would fabricate a tombstone and drop a live
|
|
51
|
+
// payload -- exactly what the scan-budget throw below guards against.
|
|
52
|
+
if (next === null || documents.length === 0) {
|
|
53
|
+
cache?.byId.set(id, null);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
checkpoint = next;
|
|
57
|
+
}
|
|
58
|
+
// The page budget ran out before the feed's end. The feed is keyset-ordered
|
|
59
|
+
// ascending on `updatedAt`, so a just-conflicted resource sits at the tail --
|
|
60
|
+
// the least-reachable position -- and may well be past the cap. Reporting
|
|
61
|
+
// `null` here would fabricate a false tombstone in the conflict assembler and
|
|
62
|
+
// drop the winner's payload. Throw instead: RxDB treats a thrown push-handler
|
|
63
|
+
// error as retryable, so the whole replication cycle retries rather than
|
|
64
|
+
// diverging.
|
|
65
|
+
throw new Error(`Feed master re-read for resource "${id}" exhausted its ` +
|
|
66
|
+
`${MAX_PAGES}-page scan budget without reaching the end of the ` +
|
|
67
|
+
`changes feed; retrying.`);
|
|
68
|
+
}
|
|
25
69
|
/**
|
|
26
70
|
* Wraps a base port with a `get` that resolves the master state from the changes
|
|
27
71
|
* feed. `query`, `putContent`, `deleteContent`, and `putMeta` pass straight
|
|
28
72
|
* through.
|
|
29
73
|
*
|
|
74
|
+
* A walk starts at the feed's origin, so resolving one resource pages past many
|
|
75
|
+
* others. With a per-push-batch `cache` the walk memoizes every one of them, and
|
|
76
|
+
* a row that finds a walk already running waits for it rather than starting a
|
|
77
|
+
* second -- so a batch of k conflicting rows costs one feed scan instead of k
|
|
78
|
+
* concurrent ones. Without a `cache` the behavior is exactly as before.
|
|
79
|
+
*
|
|
30
80
|
* @param base {WasSyncBasePort}
|
|
31
81
|
* @returns {WasSyncPort}
|
|
32
82
|
*/
|
|
@@ -36,35 +86,30 @@ export function withFeedMasterRead(base) {
|
|
|
36
86
|
putContent: base.putContent.bind(base),
|
|
37
87
|
deleteContent: base.deleteContent.bind(base),
|
|
38
88
|
putMeta: base.putMeta.bind(base),
|
|
39
|
-
async get({ id }) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// conflict assembler synthesize a tombstone.
|
|
53
|
-
if (next === null || documents.length === 0) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
checkpoint = next;
|
|
89
|
+
async get({ id, cache }) {
|
|
90
|
+
if (cache === undefined) {
|
|
91
|
+
return walkFeedFor({ base, id });
|
|
92
|
+
}
|
|
93
|
+
// Wait out whatever walk this batch already has running -- it pages past
|
|
94
|
+
// every resource, so it may well answer this read -- then re-check the
|
|
95
|
+
// memo. Looped because the walk we waited for can be followed by another.
|
|
96
|
+
while (cache.inFlight !== null) {
|
|
97
|
+
await cache.inFlight;
|
|
98
|
+
}
|
|
99
|
+
const memoized = cache.byId.get(id);
|
|
100
|
+
if (memoized !== undefined) {
|
|
101
|
+
return memoized;
|
|
57
102
|
}
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
// Publish this walk for the batch's other rows. Nothing above awaits
|
|
104
|
+
// between the memo miss and here, so two rows can never both get past it.
|
|
105
|
+
// The published promise SETTLES either way: a walk that threw is this
|
|
106
|
+
// row's error to propagate, not a waiting row's.
|
|
107
|
+
const walk = walkFeedFor({ base, id, cache });
|
|
108
|
+
const clear = () => {
|
|
109
|
+
cache.inFlight = null;
|
|
110
|
+
};
|
|
111
|
+
cache.inFlight = walk.then(clear, clear);
|
|
112
|
+
return walk;
|
|
68
113
|
}
|
|
69
114
|
};
|
|
70
115
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feedMasterPort.js","sourceRoot":"","sources":["../../src/sync/feedMasterPort.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"feedMasterPort.js","sourceRoot":"","sources":["../../src/sync/feedMasterPort.ts"],"names":[],"mappings":"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"}
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
export { createWasReplication } from './wasReplication.js';
|
|
12
12
|
export { syncedDocSchema } from './syncedDocSchema.js';
|
|
13
13
|
export { createPullHandler, wireDocToRxDoc } from './changesQuery.js';
|
|
14
|
-
export { createPushHandler,
|
|
15
|
-
export { WasSyncConflictError, WasSyncAuthError, type Json, type SyncCheckpoint, type WireDoc, type SyncedDoc, type MasterState, type WasSyncBasePort, type WasSyncPort } from './types.js';
|
|
16
|
-
export { createWasSyncPort,
|
|
14
|
+
export { createPushHandler, type PushWriteAck } from './pushWrites.js';
|
|
15
|
+
export { WasSyncConflictError, WasSyncAuthError, type Json, type SyncCheckpoint, type WireDoc, type SyncedDoc, type MasterState, type MasterReadCache, type WasSyncBasePort, type WasSyncPort } from './types.js';
|
|
16
|
+
export { createWasSyncPort, errorMessage } from './wasSyncPort.js';
|
|
17
17
|
export { withFeedMasterRead } from './feedMasterPort.js';
|
|
18
|
-
export { createDocCipher, createPlaintextDocCodec,
|
|
18
|
+
export { createDocCipher, createPlaintextDocCodec, isUnknownEpochError, type DocCipher } from './docCipher.js';
|
|
19
19
|
export { makeLwwConflictHandler } from './lwwConflictHandler.js';
|
|
20
|
-
export { lwwFields, remotePayloadWins } from './lww.js';
|
|
20
|
+
export { lwwFields, remotePayloadWins, type LwwFields } from './lww.js';
|
|
21
|
+
export { errorStatus, formatEtag, isEncryptedEnvelope } from '@interop/was-client/sync';
|
|
21
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/sync/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,
|
|
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,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACtE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,WAAW,EACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EACnB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AAGvE,OAAO,EACL,WAAW,EACX,UAAU,EACV,mBAAmB,EACpB,MAAM,0BAA0B,CAAA"}
|
package/dist/sync/index.js
CHANGED
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
export { createWasReplication } from './wasReplication.js';
|
|
12
12
|
export { syncedDocSchema } from './syncedDocSchema.js';
|
|
13
13
|
export { createPullHandler, wireDocToRxDoc } from './changesQuery.js';
|
|
14
|
-
export { createPushHandler
|
|
14
|
+
export { createPushHandler } from './pushWrites.js';
|
|
15
15
|
export { WasSyncConflictError, WasSyncAuthError } from './types.js';
|
|
16
|
-
export { createWasSyncPort,
|
|
16
|
+
export { createWasSyncPort, errorMessage } from './wasSyncPort.js';
|
|
17
17
|
export { withFeedMasterRead } from './feedMasterPort.js';
|
|
18
|
-
export { createDocCipher, createPlaintextDocCodec,
|
|
18
|
+
export { createDocCipher, createPlaintextDocCodec, isUnknownEpochError } from './docCipher.js';
|
|
19
19
|
export { makeLwwConflictHandler } from './lwwConflictHandler.js';
|
|
20
20
|
export { lwwFields, remotePayloadWins } from './lww.js';
|
|
21
|
+
// Owned by the client's sync subpath and re-exported verbatim, so the names this
|
|
22
|
+
// package's consumers already import stay put.
|
|
23
|
+
export { errorStatus, formatEtag, isEncryptedEnvelope } from '@interop/was-client/sync';
|
|
21
24
|
//# sourceMappingURL=index.js.map
|
package/dist/sync/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,
|
|
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,EAAqB,MAAM,iBAAiB,CAAA;AACtE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EASjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,mBAAmB,EAEpB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAkB,MAAM,UAAU,CAAA;AACvE,iFAAiF;AACjF,+CAA+C;AAC/C,OAAO,EACL,WAAW,EACX,UAAU,EACV,mBAAmB,EACpB,MAAM,0BAA0B,CAAA"}
|
package/dist/sync/lww.d.ts
CHANGED
|
@@ -6,36 +6,45 @@
|
|
|
6
6
|
* storage imports -- so both replicas run this identical rule against the same
|
|
7
7
|
* two payloads and converge on the same winner with no coordination.
|
|
8
8
|
*
|
|
9
|
-
* `updatedAt` (an ISO-8601 string,
|
|
10
|
-
*
|
|
9
|
+
* `updatedAt` (an ISO-8601 string) decides, compared CHRONOLOGICALLY (parsed to
|
|
10
|
+
* epoch ms, never lexically -- `updatedAt` is app-owned, so mixed precision like
|
|
11
|
+
* `...05Z` vs `...05.400Z`, or a `+00:00` offset, must still order by instant);
|
|
12
|
+
* `clientId` breaks an exact-instant tie deterministically. Both fields live in
|
|
11
13
|
* the payload (never the envelope-level checkpoint `updatedAt`).
|
|
12
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* The two fields the last-write-wins rule reads off a payload: the app-owned
|
|
17
|
+
* ISO-8601 edit stamp and the writing client's id (the exact-instant
|
|
18
|
+
* tiebreaker). Read off a doc with {@link lwwFields}; compared with
|
|
19
|
+
* {@link remotePayloadWins}.
|
|
20
|
+
*/
|
|
21
|
+
export interface LwwFields {
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
clientId: string;
|
|
24
|
+
}
|
|
13
25
|
/**
|
|
14
26
|
* Whether the remote payload wins over the local one under last-write-wins.
|
|
15
|
-
*
|
|
16
|
-
* `clientId` wins (an arbitrary but deterministic,
|
|
27
|
+
* The chronologically later `updatedAt` wins; on an exact-instant tie the
|
|
28
|
+
* lexically greater `clientId` wins (an arbitrary but deterministic,
|
|
29
|
+
* replica-independent choice).
|
|
30
|
+
*
|
|
31
|
+
* Unparseable `updatedAt` values are rejected as losers: a side whose stamp
|
|
32
|
+
* does not parse loses to one whose stamp does (a replica-independent rule, so
|
|
33
|
+
* every device converges on the same winner). When neither side parses, the
|
|
34
|
+
* lexical compare of the raw strings is the deterministic fallback.
|
|
17
35
|
*
|
|
18
|
-
* @param remote {
|
|
19
|
-
* @param local {
|
|
36
|
+
* @param remote {LwwFields}
|
|
37
|
+
* @param local {LwwFields}
|
|
20
38
|
* @returns {boolean} true if the remote payload should replace the local one
|
|
21
39
|
*/
|
|
22
|
-
export declare function remotePayloadWins(remote:
|
|
23
|
-
updatedAt: string;
|
|
24
|
-
clientId: string;
|
|
25
|
-
}, local: {
|
|
26
|
-
updatedAt: string;
|
|
27
|
-
clientId: string;
|
|
28
|
-
}): boolean;
|
|
40
|
+
export declare function remotePayloadWins(remote: LwwFields, local: LwwFields): boolean;
|
|
29
41
|
/**
|
|
30
42
|
* Reads the LWW fields off a doc when it carries them. Storage payloads are
|
|
31
43
|
* generic over `{ id: string }`, so docs without `updatedAt`/`clientId` are
|
|
32
44
|
* legal; callers fall back to their own rule for those.
|
|
33
45
|
*
|
|
34
46
|
* @param doc {unknown}
|
|
35
|
-
* @returns {
|
|
47
|
+
* @returns {LwwFields | null}
|
|
36
48
|
*/
|
|
37
|
-
export declare function lwwFields(doc: unknown):
|
|
38
|
-
updatedAt: string;
|
|
39
|
-
clientId: string;
|
|
40
|
-
} | null;
|
|
49
|
+
export declare function lwwFields(doc: unknown): LwwFields | null;
|
|
41
50
|
//# sourceMappingURL=lww.d.ts.map
|
package/dist/sync/lww.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lww.d.ts","sourceRoot":"","sources":["../../src/sync/lww.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"lww.d.ts","sourceRoot":"","sources":["../../src/sync/lww.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;GAUG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,SAAS,GACf,OAAO,CAeT;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAQxD"}
|
package/dist/sync/lww.js
CHANGED
|
@@ -6,21 +6,48 @@
|
|
|
6
6
|
* storage imports -- so both replicas run this identical rule against the same
|
|
7
7
|
* two payloads and converge on the same winner with no coordination.
|
|
8
8
|
*
|
|
9
|
-
* `updatedAt` (an ISO-8601 string,
|
|
10
|
-
*
|
|
9
|
+
* `updatedAt` (an ISO-8601 string) decides, compared CHRONOLOGICALLY (parsed to
|
|
10
|
+
* epoch ms, never lexically -- `updatedAt` is app-owned, so mixed precision like
|
|
11
|
+
* `...05Z` vs `...05.400Z`, or a `+00:00` offset, must still order by instant);
|
|
12
|
+
* `clientId` breaks an exact-instant tie deterministically. Both fields live in
|
|
11
13
|
* the payload (never the envelope-level checkpoint `updatedAt`).
|
|
12
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Parses an ISO-8601 `updatedAt` into epoch ms, or `null` when unparseable.
|
|
17
|
+
*/
|
|
18
|
+
function parseInstant(updatedAt) {
|
|
19
|
+
const ms = Date.parse(updatedAt);
|
|
20
|
+
return Number.isNaN(ms) ? null : ms;
|
|
21
|
+
}
|
|
13
22
|
/**
|
|
14
23
|
* Whether the remote payload wins over the local one under last-write-wins.
|
|
15
|
-
*
|
|
16
|
-
* `clientId` wins (an arbitrary but deterministic,
|
|
24
|
+
* The chronologically later `updatedAt` wins; on an exact-instant tie the
|
|
25
|
+
* lexically greater `clientId` wins (an arbitrary but deterministic,
|
|
26
|
+
* replica-independent choice).
|
|
17
27
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
28
|
+
* Unparseable `updatedAt` values are rejected as losers: a side whose stamp
|
|
29
|
+
* does not parse loses to one whose stamp does (a replica-independent rule, so
|
|
30
|
+
* every device converges on the same winner). When neither side parses, the
|
|
31
|
+
* lexical compare of the raw strings is the deterministic fallback.
|
|
32
|
+
*
|
|
33
|
+
* @param remote {LwwFields}
|
|
34
|
+
* @param local {LwwFields}
|
|
20
35
|
* @returns {boolean} true if the remote payload should replace the local one
|
|
21
36
|
*/
|
|
22
37
|
export function remotePayloadWins(remote, local) {
|
|
23
|
-
|
|
38
|
+
const remoteMs = parseInstant(remote.updatedAt);
|
|
39
|
+
const localMs = parseInstant(local.updatedAt);
|
|
40
|
+
if (remoteMs !== null && localMs !== null) {
|
|
41
|
+
if (remoteMs !== localMs) {
|
|
42
|
+
return remoteMs > localMs;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (remoteMs !== null || localMs !== null) {
|
|
46
|
+
// Exactly one side parses: the parseable stamp wins.
|
|
47
|
+
return remoteMs !== null;
|
|
48
|
+
}
|
|
49
|
+
else if (remote.updatedAt !== local.updatedAt) {
|
|
50
|
+
// Neither parses: fall back to the deterministic lexical compare.
|
|
24
51
|
return remote.updatedAt > local.updatedAt;
|
|
25
52
|
}
|
|
26
53
|
return remote.clientId > local.clientId;
|
|
@@ -31,7 +58,7 @@ export function remotePayloadWins(remote, local) {
|
|
|
31
58
|
* legal; callers fall back to their own rule for those.
|
|
32
59
|
*
|
|
33
60
|
* @param doc {unknown}
|
|
34
|
-
* @returns {
|
|
61
|
+
* @returns {LwwFields | null}
|
|
35
62
|
*/
|
|
36
63
|
export function lwwFields(doc) {
|
|
37
64
|
const { updatedAt, clientId } = doc;
|
package/dist/sync/lww.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lww.js","sourceRoot":"","sources":["../../src/sync/lww.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"lww.js","sourceRoot":"","sources":["../../src/sync/lww.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;GAUG;AAaH;;GAEG;AACH,SAAS,YAAY,CAAC,SAAiB;IACrC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAChC,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;AACrC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAiB,EACjB,KAAgB;IAEhB,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAC/C,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAC7C,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QAC1C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YACzB,OAAO,QAAQ,GAAG,OAAO,CAAA;QAC3B,CAAC;IACH,CAAC;SAAM,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACjD,qDAAqD;QACrD,OAAO,QAAQ,KAAK,IAAI,CAAA;IAC1B,CAAC;SAAM,IAAI,MAAM,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,EAAE,CAAC;QAChD,kEAAkE;QAClE,OAAO,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;IAC3C,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;AACzC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,GAAY;IACpC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,GAG/B,CAAA;IACD,OAAO,OAAO,SAAS,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAClE,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE;QACzB,CAAC,CAAC,IAAI,CAAA;AACV,CAAC"}
|
|
@@ -41,36 +41,38 @@
|
|
|
41
41
|
* Without this rule the equal-`data` case would fall through to rule 3, where
|
|
42
42
|
* the two payloads compare equal and the tie keeps the local (stale) `custom`,
|
|
43
43
|
* silently clobbering the committed metadata.
|
|
44
|
-
* 3.
|
|
45
|
-
*
|
|
44
|
+
* 3. An UNDECRYPTABLE side (the decrypt threw -- e.g. an envelope written under
|
|
45
|
+
* a key epoch this device has not seen) is never scored as the loser: it is
|
|
46
|
+
* presumed newer, not absent. An undecryptable master is adopted (never
|
|
47
|
+
* re-pushed over with the possibly-older local payload); an undecryptable
|
|
48
|
+
* local row is re-asserted (the user's edit is not silently dropped); both
|
|
49
|
+
* undecryptable adopts the master (deterministic and convergent). Each case
|
|
50
|
+
* is logged -- distinguishable from the intended tombstone/absent-body
|
|
51
|
+
* `null` the remaining rules were written for.
|
|
52
|
+
* 4. Both sides carry an LWW payload: pure payload LWW via `payloadWins`.
|
|
53
|
+
* 5. A live local edit vs an incomparable remote (e.g. a remote tombstone):
|
|
46
54
|
* the edit wins and is re-pushed (resurrection).
|
|
47
|
-
*
|
|
48
|
-
* both sides incomparable: the master wins. Together with rule
|
|
55
|
+
* 6. Everything else -- a local tombstone vs a REAL remote content change, or
|
|
56
|
+
* both sides incomparable: the master wins. Together with rule 5 this makes
|
|
49
57
|
* the delete-vs-concurrent-edit rule "the edit wins" on every replica: a
|
|
50
58
|
* tombstone carries no LWW payload of its own, so a genuine racing edit
|
|
51
59
|
* deterministically survives, whichever write reached the server first.
|
|
52
60
|
*/
|
|
53
61
|
import type { WithDeleted } from 'rxdb/plugins/core';
|
|
54
62
|
import type { Json, SyncedDoc } from './types.js';
|
|
55
|
-
|
|
56
|
-
* The LWW fields read out of a decrypted entity payload.
|
|
57
|
-
*/
|
|
58
|
-
interface LwwPayload {
|
|
59
|
-
updatedAt: string;
|
|
60
|
-
clientId: string;
|
|
61
|
-
}
|
|
63
|
+
import { type LwwFields } from './lww.js';
|
|
62
64
|
/**
|
|
63
65
|
* Builds an RxDB conflict handler that settles content conflicts by payload LWW,
|
|
64
66
|
* decrypting through the supplied per-collection `decrypt`.
|
|
65
67
|
*
|
|
66
68
|
* @param decrypt {(envelope: Json) => Promise<Json>} this collection's decrypt
|
|
67
|
-
* @param [payloadWins] {(remote:
|
|
69
|
+
* @param [payloadWins] {(remote: LwwFields, local: LwwFields) => boolean}
|
|
68
70
|
* the total-order comparator deciding whether the remote payload replaces the
|
|
69
71
|
* local one; defaults to {@link remotePayloadWins} (later `updatedAt` wins,
|
|
70
72
|
* `clientId` breaks a tie)
|
|
71
73
|
* @returns {import('rxdb/plugins/core').RxConflictHandler<SyncedDoc>}
|
|
72
74
|
*/
|
|
73
|
-
export declare function makeLwwConflictHandler(decrypt: (envelope: Json) => Promise<Json>, payloadWins?: (remote:
|
|
75
|
+
export declare function makeLwwConflictHandler(decrypt: (envelope: Json) => Promise<Json>, payloadWins?: (remote: LwwFields, local: LwwFields) => boolean): {
|
|
74
76
|
isEqual(a: WithDeleted<SyncedDoc>, b: WithDeleted<SyncedDoc>): boolean;
|
|
75
77
|
resolve({ realMasterState, newDocumentState, assumedMasterState }: {
|
|
76
78
|
realMasterState: WithDeleted<SyncedDoc>;
|
|
@@ -78,5 +80,4 @@ export declare function makeLwwConflictHandler(decrypt: (envelope: Json) => Prom
|
|
|
78
80
|
assumedMasterState?: WithDeleted<SyncedDoc>;
|
|
79
81
|
}): Promise<WithDeleted<SyncedDoc>>;
|
|
80
82
|
};
|
|
81
|
-
export {};
|
|
82
83
|
//# sourceMappingURL=lwwConflictHandler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lwwConflictHandler.d.ts","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"lwwConflictHandler.d.ts","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AAoCvE;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,EAC1C,WAAW,GAAE,CACX,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,SAAS,KACb,OAA2B;eAQnB,WAAW,CAAC,SAAS,CAAC,KAAK,WAAW,CAAC,SAAS,CAAC,GAAG,OAAO;uEAcnE;QACD,eAAe,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;QACvC,gBAAgB,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;QACxC,kBAAkB,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;KAC5C,GAAG,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;EAiFtC"}
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bodiesEqual } from './types.js';
|
|
2
|
+
import { lwwFields, remotePayloadWins } from './lww.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Decrypts one side's envelope into its `{ updatedAt, clientId }`, or `null`
|
|
10
|
-
* when the side has no decryptable payload (a tombstone, or an absent/corrupt
|
|
11
|
-
* body). `null` means "no comparable timestamp", handled by the caller.
|
|
4
|
+
* Decrypts one side's envelope into its LWW comparability (see {@link LwwSide}).
|
|
5
|
+
* A payload carrying no LWW stamp reads as `none`, the same as a tombstone or an
|
|
6
|
+
* absent body: nothing there to compare.
|
|
12
7
|
*/
|
|
13
8
|
async function lwwFieldsOf(doc, decrypt) {
|
|
14
9
|
if (doc._deleted || doc.data === undefined) {
|
|
15
|
-
return
|
|
10
|
+
return { kind: 'none' };
|
|
16
11
|
}
|
|
17
12
|
try {
|
|
18
|
-
const payload = (await decrypt(doc.data));
|
|
19
|
-
|
|
20
|
-
typeof payload.clientId === 'string') {
|
|
21
|
-
return { updatedAt: payload.updatedAt, clientId: payload.clientId };
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
13
|
+
const payload = lwwFields(await decrypt(doc.data));
|
|
14
|
+
return payload === null ? { kind: 'none' } : { kind: 'payload', payload };
|
|
24
15
|
}
|
|
25
|
-
catch {
|
|
26
|
-
return
|
|
16
|
+
catch (err) {
|
|
17
|
+
return { kind: 'undecryptable', err };
|
|
27
18
|
}
|
|
28
19
|
}
|
|
29
20
|
/**
|
|
@@ -31,7 +22,7 @@ async function lwwFieldsOf(doc, decrypt) {
|
|
|
31
22
|
* decrypting through the supplied per-collection `decrypt`.
|
|
32
23
|
*
|
|
33
24
|
* @param decrypt {(envelope: Json) => Promise<Json>} this collection's decrypt
|
|
34
|
-
* @param [payloadWins] {(remote:
|
|
25
|
+
* @param [payloadWins] {(remote: LwwFields, local: LwwFields) => boolean}
|
|
35
26
|
* the total-order comparator deciding whether the remote payload replaces the
|
|
36
27
|
* local one; defaults to {@link remotePayloadWins} (later `updatedAt` wins,
|
|
37
28
|
* `clientId` breaks a tie)
|
|
@@ -83,16 +74,36 @@ export function makeLwwConflictHandler(decrypt, payloadWins = remotePayloadWins)
|
|
|
83
74
|
lwwFieldsOf(realMasterState, decrypt),
|
|
84
75
|
lwwFieldsOf(newDocumentState, decrypt)
|
|
85
76
|
]);
|
|
86
|
-
// Rule 3 --
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
// Rule 3 -- an undecryptable side is never scored as the loser: it is
|
|
78
|
+
// presumed newer (typically an envelope under an unseen key epoch), not
|
|
79
|
+
// absent. Logged so it is distinguishable from a genuine tombstone.
|
|
80
|
+
if (remote.kind === 'undecryptable' || local.kind === 'undecryptable') {
|
|
81
|
+
if (remote.kind === 'undecryptable' && local.kind !== 'undecryptable') {
|
|
82
|
+
console.warn('LWW conflict: the remote master did not decrypt; adopting it ' +
|
|
83
|
+
'rather than re-pushing the local payload over it.', remote.err);
|
|
84
|
+
return realMasterState;
|
|
85
|
+
}
|
|
86
|
+
if (local.kind === 'undecryptable' && remote.kind !== 'undecryptable') {
|
|
87
|
+
console.warn('LWW conflict: the local row did not decrypt; re-asserting it ' +
|
|
88
|
+
'rather than dropping the local edit for the master.', local.err);
|
|
89
|
+
return newDocumentState;
|
|
90
|
+
}
|
|
91
|
+
console.warn('LWW conflict: neither side decrypted; adopting the master ' +
|
|
92
|
+
'(deterministic and convergent).', remote.kind === 'undecryptable' ? remote.err : undefined);
|
|
93
|
+
return realMasterState;
|
|
94
|
+
}
|
|
95
|
+
// Rule 4 -- both sides comparable: pure payload LWW.
|
|
96
|
+
if (remote.kind === 'payload' && local.kind === 'payload') {
|
|
97
|
+
return payloadWins(remote.payload, local.payload)
|
|
98
|
+
? realMasterState
|
|
99
|
+
: newDocumentState;
|
|
89
100
|
}
|
|
90
|
-
// Rule
|
|
101
|
+
// Rule 5 -- a live local edit against a remote tombstone (or otherwise
|
|
91
102
|
// incomparable remote): keep the edit (resurrect / re-assert the write).
|
|
92
|
-
if (local &&
|
|
103
|
+
if (local.kind === 'payload' && remote.kind === 'none') {
|
|
93
104
|
return newDocumentState;
|
|
94
105
|
}
|
|
95
|
-
// Rule
|
|
106
|
+
// Rule 6 -- everything else (a local tombstone racing a REAL remote
|
|
96
107
|
// content change, or both incomparable): the master wins, which is
|
|
97
108
|
// deterministic and convergent across replicas (the edit survives the
|
|
98
109
|
// delete on every device).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lwwConflictHandler.js","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lwwConflictHandler.js","sourceRoot":"","sources":["../../src/sync/lwwConflictHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* - content changed -> `PUT /:id` (`If-Match: "<version>"`) or, on create,
|
|
15
15
|
* `PUT /:id` (`If-None-Match: *`); a delete -> `DELETE /:id`.
|
|
16
16
|
* - metadata changed -> `PUT /:id/meta` (`If-Match: "<metaVersion>"`, or
|
|
17
|
-
* `If-None-Match: *` when the resource has no metadata yet)
|
|
17
|
+
* `If-None-Match: *` when the resource has no metadata yet); a metadata
|
|
18
|
+
* CLEAR (the new state carries no `custom`) writes the cleared state rather
|
|
19
|
+
* than being skipped.
|
|
18
20
|
*
|
|
19
21
|
* Content is written before metadata on a create, because the server rejects a
|
|
20
22
|
* `/meta` write to a resource that does not yet exist.
|
|
@@ -43,14 +45,6 @@ export interface PushWriteAck {
|
|
|
43
45
|
version?: number;
|
|
44
46
|
metaVersion?: number;
|
|
45
47
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Formats a master revision (`version` or `metaVersion`) as the quoted strong
|
|
48
|
-
* ETag the server compares `If-Match` against (revision `3` becomes `"3"`).
|
|
49
|
-
*
|
|
50
|
-
* @param revision {number}
|
|
51
|
-
* @returns {string}
|
|
52
|
-
*/
|
|
53
|
-
export declare function formatEtag(revision: number): string;
|
|
54
48
|
/**
|
|
55
49
|
* Builds the RxDB push handler that fans a batch of local changes out to
|
|
56
50
|
* conditional WAS writes and returns the conflicting rows' master states.
|
|
@@ -63,6 +57,12 @@ export declare function formatEtag(revision: number): string;
|
|
|
63
57
|
* caller can write the new `version` / `metaVersion` back into the local row
|
|
64
58
|
* and keep subsequent conditional writes' `If-Match` in step with the server.
|
|
65
59
|
*
|
|
60
|
+
* Each batch gets one short-lived master-read memo, shared by its rows and
|
|
61
|
+
* discarded with the batch (never held across batches, where it would go
|
|
62
|
+
* stale). A conflict re-read walks the changes feed from its origin and so pages
|
|
63
|
+
* past every other conflicting row's master on the way; without the memo a batch
|
|
64
|
+
* with k conflicts would run k concurrent full-feed walks.
|
|
65
|
+
*
|
|
66
66
|
* @param port {WasSyncPort}
|
|
67
67
|
* @param [onWriteAccepted] {(ack: PushWriteAck) => Promise<void>}
|
|
68
68
|
* @returns {(rows: Array<{ newDocumentState: WithDeleted<SyncedDoc>,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pushWrites.d.ts","sourceRoot":"","sources":["../../src/sync/pushWrites.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"pushWrites.d.ts","sourceRoot":"","sources":["../../src/sync/pushWrites.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,KAAK,EAGV,SAAS,EACT,WAAW,EACZ,MAAM,YAAY,CAAA;AAQnB;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAqND;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,WAAW,EACjB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,IAGpD,MAAM,KAAK,CAAC;IACV,gBAAgB,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;IACxC,kBAAkB,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;CAC5C,CAAC,KACD,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC,CAsBrC"}
|