@interop/was-react 0.8.3 → 0.10.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 +50 -44
- package/dist/auth/chapi.d.ts.map +1 -1
- package/dist/auth/chapi.js +5 -4
- package/dist/auth/chapi.js.map +1 -1
- package/dist/auth/loginFlow.d.ts.map +1 -1
- package/dist/auth/loginFlow.js +48 -50
- package/dist/auth/loginFlow.js.map +1 -1
- package/dist/auth/loginRequest.d.ts +38 -5
- package/dist/auth/loginRequest.d.ts.map +1 -1
- package/dist/auth/loginRequest.js +66 -5
- package/dist/auth/loginRequest.js.map +1 -1
- package/dist/auth/verifyResponse.d.ts +7 -4
- package/dist/auth/verifyResponse.d.ts.map +1 -1
- package/dist/auth/verifyResponse.js +23 -13
- 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 +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -5
- 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 +100 -96
- 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 +118 -74
- package/dist/storage/localStore.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 -81
- 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 +38 -58
- 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 -12
- package/dist/sync/changesQuery.js.map +1 -1
- package/dist/sync/docCipher.d.ts +17 -9
- 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 -40
- 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 +15 -14
- package/dist/sync/lww.d.ts.map +1 -1
- package/dist/sync/lww.js +3 -3
- package/dist/sync/lww.js.map +1 -1
- package/dist/sync/lwwConflictHandler.d.ts +3 -10
- package/dist/sync/lwwConflictHandler.d.ts.map +1 -1
- package/dist/sync/lwwConflictHandler.js +7 -17
- package/dist/sync/lwwConflictHandler.js.map +1 -1
- package/dist/sync/pushWrites.d.ts +6 -8
- package/dist/sync/pushWrites.d.ts.map +1 -1
- package/dist/sync/pushWrites.js +56 -83
- package/dist/sync/pushWrites.js.map +1 -1
- package/dist/sync/types.d.ts +80 -4
- 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 +8 -33
- package/dist/sync/wasSyncPort.js.map +1 -1
- package/package.json +5 -5
- 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
package/dist/sync/pushWrites.js
CHANGED
|
@@ -1,68 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatEtag } from '@interop/was-client/sync';
|
|
2
|
+
import { bodiesEqual, copyOptionalBodyFields, WasSyncAuthError, WasSyncConflictError } from './types.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*/
|
|
9
|
-
export function formatEtag(revision) {
|
|
10
|
-
return `"${revision}"`;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Structural equality over two opaque bodies, by canonical-free JSON string.
|
|
14
|
-
* Used only to decide whether the content or the metadata half changed and thus
|
|
15
|
-
* which endpoint(s) to write. Content-addressed collections never mutate `data`
|
|
16
|
-
* for a given id (so the content half only fires on create/delete), and a real
|
|
17
|
-
* metadata edit re-encrypts to fresh bytes, so this coarse comparison is
|
|
18
|
-
* sufficient for routing.
|
|
19
|
-
*
|
|
20
|
-
* @param left {Json | undefined}
|
|
21
|
-
* @param right {Json | undefined}
|
|
22
|
-
* @returns {boolean}
|
|
23
|
-
*/
|
|
24
|
-
function bodiesEqual(left, right) {
|
|
25
|
-
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Maps a re-read master state into the RxDB conflict entry for one row.
|
|
29
|
-
*/
|
|
30
|
-
function masterToConflict(id, master) {
|
|
31
|
-
const conflict = {
|
|
32
|
-
id,
|
|
33
|
-
updatedAt: master.updatedAt,
|
|
34
|
-
version: master.version,
|
|
35
|
-
_deleted: master.deleted
|
|
36
|
-
};
|
|
37
|
-
if (master.data !== undefined) {
|
|
38
|
-
conflict.data = master.data;
|
|
39
|
-
}
|
|
40
|
-
if (master.metaVersion !== undefined) {
|
|
41
|
-
conflict.metaVersion = master.metaVersion;
|
|
42
|
-
}
|
|
43
|
-
if (master.custom !== undefined) {
|
|
44
|
-
conflict.custom = master.custom;
|
|
45
|
-
}
|
|
46
|
-
if (master.epoch !== undefined) {
|
|
47
|
-
conflict.epoch = master.epoch;
|
|
48
|
-
}
|
|
49
|
-
return conflict;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Builds the RxDB conflict entry (the real current master state) for a row whose
|
|
53
|
-
* conditional write was rejected with `412`. Re-reads the resource; when it is
|
|
54
|
-
* genuinely absent (a delete/delete race) the master is a tombstone synthesized
|
|
55
|
-
* from what we know locally.
|
|
4
|
+
* Maps a re-read master state into the RxDB conflict entry for one row, or --
|
|
5
|
+
* when the re-read found the resource genuinely absent (`master === null`, a
|
|
6
|
+
* delete/delete race) -- synthesizes the tombstone conflict entry from what we
|
|
7
|
+
* know locally. Shared by the `412` assembler and the `/meta` 404 recovery, so
|
|
8
|
+
* both report an absent master identically.
|
|
56
9
|
*
|
|
57
10
|
* @param options {object}
|
|
58
|
-
* @param options.port {WasSyncPort}
|
|
59
11
|
* @param options.id {string}
|
|
12
|
+
* @param options.master {MasterState | null}
|
|
60
13
|
* @param options.fallbackUpdatedAt {string} used if the resource is now absent
|
|
61
14
|
* @param options.fallbackVersion {number} used if the resource is now absent
|
|
62
|
-
* @returns {
|
|
15
|
+
* @returns {WithDeleted<SyncedDoc>}
|
|
63
16
|
*/
|
|
64
|
-
|
|
65
|
-
const master = await port.get({ id });
|
|
17
|
+
function masterOrTombstone({ id, master, fallbackUpdatedAt, fallbackVersion }) {
|
|
66
18
|
if (master === null) {
|
|
67
19
|
return {
|
|
68
20
|
id,
|
|
@@ -71,7 +23,14 @@ async function assembleConflict({ port, id, fallbackUpdatedAt, fallbackVersion }
|
|
|
71
23
|
_deleted: true
|
|
72
24
|
};
|
|
73
25
|
}
|
|
74
|
-
|
|
26
|
+
const conflict = {
|
|
27
|
+
id,
|
|
28
|
+
updatedAt: master.updatedAt,
|
|
29
|
+
version: master.version,
|
|
30
|
+
_deleted: master.deleted
|
|
31
|
+
};
|
|
32
|
+
copyOptionalBodyFields({ source: master, target: conflict });
|
|
33
|
+
return conflict;
|
|
75
34
|
}
|
|
76
35
|
/**
|
|
77
36
|
* Sends one local change to the remote Collection as up to two conditional
|
|
@@ -85,29 +44,43 @@ async function assembleConflict({ port, id, fallbackUpdatedAt, fallbackVersion }
|
|
|
85
44
|
* @param options.port {WasSyncPort}
|
|
86
45
|
* @param options.newDocumentState {WithDeleted<SyncedDoc>}
|
|
87
46
|
* @param [options.assumedMasterState] {WithDeleted<SyncedDoc>}
|
|
47
|
+
* @param [options.cache] {MasterReadCache} the push batch's shared
|
|
48
|
+
* master-read memo
|
|
88
49
|
* @returns {Promise<{ conflict: WithDeleted<SyncedDoc> | null,
|
|
89
50
|
* ack: PushWriteAck | null }>}
|
|
90
51
|
*/
|
|
91
|
-
async function pushRow({ port, newDocumentState, assumedMasterState }) {
|
|
52
|
+
async function pushRow({ port, newDocumentState, assumedMasterState, cache }) {
|
|
92
53
|
const { id } = newDocumentState;
|
|
93
54
|
const assumedVersion = assumedMasterState?.version;
|
|
94
55
|
const isCreate = assumedMasterState === undefined;
|
|
95
56
|
const ack = { id };
|
|
96
57
|
const hasAck = () => ack.version !== undefined || ack.metaVersion !== undefined;
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
58
|
+
const fallbackVersion = () => ack.version ?? assumedVersion ?? newDocumentState.version;
|
|
59
|
+
// Re-reads this row's master. A row that has ALREADY written this batch
|
|
60
|
+
// (a content write accepted before a `/meta` rejection) bypasses the batch
|
|
61
|
+
// memo: a sibling's feed read may have paged past this resource before our
|
|
62
|
+
// own write landed, and the conflict entry must carry the revision that write
|
|
63
|
+
// produced. Rows that wrote nothing -- the ordinary content 412, the common
|
|
64
|
+
// case -- are exactly what the memo is for.
|
|
65
|
+
const readMaster = async () => hasAck()
|
|
66
|
+
? port.get({ id })
|
|
67
|
+
: port.get({ id, ...(cache !== undefined && { cache }) });
|
|
68
|
+
// Builds the conflict outcome from a re-read master (or from its absence, a
|
|
69
|
+
// delete/delete race), PRESERVING any ack already earned: a content write
|
|
70
|
+
// accepted before a `/meta` 412 must keep its acked `version` (and feed it to
|
|
71
|
+
// the absent-master fallback), or the local row keeps the pre-write version
|
|
72
|
+
// and every later conditional write sends a stale `If-Match`.
|
|
73
|
+
const conflictOutcome = (master) => ({
|
|
74
|
+
conflict: masterOrTombstone({
|
|
105
75
|
id,
|
|
76
|
+
master,
|
|
106
77
|
fallbackUpdatedAt: newDocumentState.updatedAt,
|
|
107
|
-
fallbackVersion:
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
};
|
|
78
|
+
fallbackVersion: fallbackVersion()
|
|
79
|
+
}),
|
|
80
|
+
ack: hasAck() ? ack : null
|
|
81
|
+
});
|
|
82
|
+
// The 412 path: re-read the resource, then report its real master state.
|
|
83
|
+
const conflictResult = async () => conflictOutcome(await readMaster());
|
|
111
84
|
try {
|
|
112
85
|
if (newDocumentState._deleted) {
|
|
113
86
|
// Delete supersedes any metadata write: drop the content, tombstone wins.
|
|
@@ -189,17 +162,9 @@ async function pushRow({ port, newDocumentState, assumedMasterState }) {
|
|
|
189
162
|
// (the conflict handler reconciles it) instead of flipping the whole
|
|
190
163
|
// session to "access expired" and wedging the batch in RxDB's retries.
|
|
191
164
|
if (err instanceof WasSyncAuthError && err.status === 404) {
|
|
192
|
-
const master = await
|
|
165
|
+
const master = await readMaster();
|
|
193
166
|
if (master === null || master.deleted) {
|
|
194
|
-
|
|
195
|
-
? masterToConflict(id, master)
|
|
196
|
-
: {
|
|
197
|
-
id,
|
|
198
|
-
updatedAt: newDocumentState.updatedAt,
|
|
199
|
-
version: ack.version ?? assumedVersion ?? newDocumentState.version,
|
|
200
|
-
_deleted: true
|
|
201
|
-
};
|
|
202
|
-
return { conflict, ack: hasAck() ? ack : null };
|
|
167
|
+
return conflictOutcome(master);
|
|
203
168
|
}
|
|
204
169
|
// The resource is alive and readable while its `/meta` write 404s:
|
|
205
170
|
// the write itself was rejected, so the auth signal stands.
|
|
@@ -221,6 +186,12 @@ async function pushRow({ port, newDocumentState, assumedMasterState }) {
|
|
|
221
186
|
* caller can write the new `version` / `metaVersion` back into the local row
|
|
222
187
|
* and keep subsequent conditional writes' `If-Match` in step with the server.
|
|
223
188
|
*
|
|
189
|
+
* Each batch gets one short-lived master-read memo, shared by its rows and
|
|
190
|
+
* discarded with the batch (never held across batches, where it would go
|
|
191
|
+
* stale). A conflict re-read walks the changes feed from its origin and so pages
|
|
192
|
+
* past every other conflicting row's master on the way; without the memo a batch
|
|
193
|
+
* with k conflicts would run k concurrent full-feed walks.
|
|
194
|
+
*
|
|
224
195
|
* @param port {WasSyncPort}
|
|
225
196
|
* @param [onWriteAccepted] {(ack: PushWriteAck) => Promise<void>}
|
|
226
197
|
* @returns {(rows: Array<{ newDocumentState: WithDeleted<SyncedDoc>,
|
|
@@ -229,11 +200,13 @@ async function pushRow({ port, newDocumentState, assumedMasterState }) {
|
|
|
229
200
|
*/
|
|
230
201
|
export function createPushHandler(port, onWriteAccepted) {
|
|
231
202
|
return async function push(rows) {
|
|
203
|
+
const cache = { byId: new Map(), inFlight: null };
|
|
232
204
|
const results = await Promise.all(rows.map(async (row) => {
|
|
233
205
|
const result = await pushRow({
|
|
234
206
|
port,
|
|
235
207
|
newDocumentState: row.newDocumentState,
|
|
236
|
-
assumedMasterState: row.assumedMasterState
|
|
208
|
+
assumedMasterState: row.assumedMasterState,
|
|
209
|
+
cache
|
|
237
210
|
});
|
|
238
211
|
if (result.ack !== null && onWriteAccepted !== undefined) {
|
|
239
212
|
await onWriteAccepted(result.ack);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pushWrites.js","sourceRoot":"","sources":["../../src/sync/pushWrites.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pushWrites.js","sourceRoot":"","sources":["../../src/sync/pushWrites.ts"],"names":[],"mappings":"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"}
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -19,15 +19,63 @@
|
|
|
19
19
|
* or the EDV envelope on an encrypted collection) and `custom` is the stored
|
|
20
20
|
* metadata body (an opaque envelope on an encrypted collection); encrypt/decrypt
|
|
21
21
|
* stays a read/write-time concern above this layer.
|
|
22
|
+
*
|
|
23
|
+
* The two small helpers that travel with these shapes live here as well: the
|
|
24
|
+
* opaque-body equality every routing decision is made on, and the optional-field
|
|
25
|
+
* copy every wire-to-local mapping runs.
|
|
22
26
|
*/
|
|
27
|
+
import type { Json } from '@interop/was-client';
|
|
23
28
|
/**
|
|
24
29
|
* A JSON value -- the opaque stored resource body the sync layer moves verbatim.
|
|
25
30
|
* For a plaintext collection this is the user document; for an encrypted one it
|
|
26
|
-
* is the EDV envelope. The adapter never inspects or transforms it.
|
|
31
|
+
* is the EDV envelope. The adapter never inspects or transforms it. Re-exported
|
|
32
|
+
* from `@interop/was-client`, which owns the wire model, so a body crosses the
|
|
33
|
+
* port boundary without a cast.
|
|
34
|
+
*/
|
|
35
|
+
export type { Json };
|
|
36
|
+
/**
|
|
37
|
+
* The optional half of a synced document, shared verbatim by every shape it
|
|
38
|
+
* travels in ({@link WireDoc} on the feed, {@link SyncedDoc} locally,
|
|
39
|
+
* {@link MasterState} on the conflict re-read): the independently-versioned
|
|
40
|
+
* metadata revision and body, the content body, and the content body's
|
|
41
|
+
* key-epoch stamp. Each is genuinely absent rather than `undefined` when the
|
|
42
|
+
* server has nothing for it, so every mapping between the shapes copies them
|
|
43
|
+
* conditionally -- see {@link copyOptionalBodyFields}.
|
|
44
|
+
*/
|
|
45
|
+
export interface OptionalBodyFields {
|
|
46
|
+
metaVersion?: number;
|
|
47
|
+
data?: Json;
|
|
48
|
+
custom?: Json;
|
|
49
|
+
epoch?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Structural equality over two opaque bodies, by canonical-free JSON string.
|
|
53
|
+
* Decides whether the content or the metadata half changed -- which endpoint(s)
|
|
54
|
+
* a push writes, and whether two states compare equal for conflict resolution.
|
|
55
|
+
* Content-addressed collections never mutate `data` for a given id, and a real
|
|
56
|
+
* metadata edit re-encrypts to fresh bytes, so this coarse comparison suffices.
|
|
57
|
+
*
|
|
58
|
+
* @param left {Json | undefined}
|
|
59
|
+
* @param right {Json | undefined}
|
|
60
|
+
* @returns {boolean}
|
|
27
61
|
*/
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
}
|
|
62
|
+
export declare function bodiesEqual(left: Json | undefined, right: Json | undefined): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Copies the {@link OptionalBodyFields} that are present on `source` onto
|
|
65
|
+
* `target`, leaving an absent field absent (never writing an explicit
|
|
66
|
+
* `undefined`, which would surface the key in a serialized body). The single
|
|
67
|
+
* place every wire-to-local, feed-to-master, and master-to-conflict mapping
|
|
68
|
+
* shares, so a new optional wire field is added once.
|
|
69
|
+
*
|
|
70
|
+
* @param options {object}
|
|
71
|
+
* @param options.source {OptionalBodyFields}
|
|
72
|
+
* @param options.target {OptionalBodyFields} mutated in place
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
export declare function copyOptionalBodyFields({ source, target }: {
|
|
76
|
+
source: OptionalBodyFields;
|
|
77
|
+
target: OptionalBodyFields;
|
|
78
|
+
}): void;
|
|
31
79
|
/**
|
|
32
80
|
* The keyset position in the change feed: the `{ id, updatedAt }` of the last
|
|
33
81
|
* document a pull returned. Passed back verbatim to resume, and used as the
|
|
@@ -103,6 +151,26 @@ export interface MasterState {
|
|
|
103
151
|
*/
|
|
104
152
|
epoch?: string;
|
|
105
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The short-lived master-read memo the rows of ONE push batch share. A
|
|
156
|
+
* resolving `get` implementation may answer from `byId` and MUST record every
|
|
157
|
+
* master it resolved on the way there; a feed-walking implementation pages past
|
|
158
|
+
* all of them anyway, so a batch of k conflicts costs one walk instead of k.
|
|
159
|
+
*
|
|
160
|
+
* `inFlight` is what makes that hold under concurrency: the batch's rows push in
|
|
161
|
+
* parallel, so without it every row would start its own walk before the first
|
|
162
|
+
* one finished and the memo would never be read. A `get` that must walk
|
|
163
|
+
* publishes its walk here; a `get` that finds a walk already running awaits it
|
|
164
|
+
* and re-checks `byId` first. It settles (never rejects) so one row's failed
|
|
165
|
+
* read is not another row's, and is `null` whenever no walk is running.
|
|
166
|
+
*
|
|
167
|
+
* The whole object is created per batch invocation and dropped with it: a memo
|
|
168
|
+
* held across batches would go stale.
|
|
169
|
+
*/
|
|
170
|
+
export interface MasterReadCache {
|
|
171
|
+
byId: Map<string, MasterState | null>;
|
|
172
|
+
inFlight: Promise<void> | null;
|
|
173
|
+
}
|
|
106
174
|
/**
|
|
107
175
|
* The write/query half of the injected WAS-access seam. An adapter (the app-side
|
|
108
176
|
* `createWasSyncPort`) implements this over `@interop/was-client`; the core
|
|
@@ -221,12 +289,20 @@ export interface WasSyncPort extends WasSyncBasePort {
|
|
|
221
289
|
* cannot be resolved (e.g. a feed re-read that exhausts its scan budget), so
|
|
222
290
|
* the replication cycle retries rather than fabricating a false tombstone.
|
|
223
291
|
*
|
|
292
|
+
* `cache` is an OPTIONAL {@link MasterReadCache}, shared by the rows of one
|
|
293
|
+
* push batch (see its own docs for the contract). A hit is no staler than a
|
|
294
|
+
* read issued at the same moment, since the memo lives only for that batch. An
|
|
295
|
+
* implementation that ignores it is fully conformant, and a caller that omits
|
|
296
|
+
* it gets an uncached read.
|
|
297
|
+
*
|
|
224
298
|
* @param options {object}
|
|
225
299
|
* @param options.id {string}
|
|
300
|
+
* @param [options.cache] {MasterReadCache}
|
|
226
301
|
* @returns {Promise<MasterState | null>}
|
|
227
302
|
*/
|
|
228
303
|
get(options: {
|
|
229
304
|
id: string;
|
|
305
|
+
cache?: MasterReadCache;
|
|
230
306
|
}): Promise<MasterState | null>;
|
|
231
307
|
}
|
|
232
308
|
/**
|
package/dist/sync/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAE/C;;;;;;GAMG;AACH,YAAY,EAAE,IAAI,EAAE,CAAA;AAEpB;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,MAAM,CAAC,EAAE,IAAI,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,KAAK,EAAE,IAAI,GAAG,SAAS,GACtB,OAAO,CAET;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACP,EAAE;IACD,MAAM,EAAE,kBAAkB,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;CAC3B,GAAG,IAAI,CAaP;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,MAAM,CAAC,EAAE,IAAI,CAAA;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,MAAM,CAAC,EAAE,IAAI,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,MAAM,CAAC,EAAE,IAAI,CAAA;IACb;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC,CAAA;IACrC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;OASG;IACH,KAAK,CAAC,OAAO,EAAE;QAAE,UAAU,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QACtE,SAAS,EAAE,OAAO,EAAE,CAAA;QACpB,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;KAClC,CAAC,CAAA;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,CAAC,OAAO,EAAE;QAClB,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,IAAI,CAAA;QACV,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,OAAO,CAAA;QACrB,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAE/B;;;;;;;;;;;;;;OAcG;IACH,aAAa,CAAC,OAAO,EAAE;QACrB,EAAE,EAAE,MAAM,CAAA;QACV,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAE/B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,OAAO,EAAE;QACf,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,CAAC,EAAE,IAAI,CAAA;QACb,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,OAAO,CAAA;KACtB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAChC;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD;;;;;;;;;;;;;;;;;OAiBG;IACH,GAAG,CAAC,OAAO,EAAE;QACX,EAAE,EAAE,MAAM,CAAA;QACV,KAAK,CAAC,EAAE,eAAe,CAAA;KACxB,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;CAChC;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,SAA+C;CAInE;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;gBACX,MAAM,EAAE,MAAM;CAK3B"}
|
package/dist/sync/types.js
CHANGED
|
@@ -1,25 +1,43 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
-
*/
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
2
|
+
* Structural equality over two opaque bodies, by canonical-free JSON string.
|
|
3
|
+
* Decides whether the content or the metadata half changed -- which endpoint(s)
|
|
4
|
+
* a push writes, and whether two states compare equal for conflict resolution.
|
|
5
|
+
* Content-addressed collections never mutate `data` for a given id, and a real
|
|
6
|
+
* metadata edit re-encrypts to fresh bytes, so this coarse comparison suffices.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
* @param left {Json | undefined}
|
|
9
|
+
* @param right {Json | undefined}
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function bodiesEqual(left, right) {
|
|
13
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Copies the {@link OptionalBodyFields} that are present on `source` onto
|
|
17
|
+
* `target`, leaving an absent field absent (never writing an explicit
|
|
18
|
+
* `undefined`, which would surface the key in a serialized body). The single
|
|
19
|
+
* place every wire-to-local, feed-to-master, and master-to-conflict mapping
|
|
20
|
+
* shares, so a new optional wire field is added once.
|
|
12
21
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `updatedAt` / `metaVersion` but unchanged `version` / `data`. The sync layer
|
|
18
|
-
* moves both bodies opaquely: `data` is the stored content body (plaintext JSON,
|
|
19
|
-
* or the EDV envelope on an encrypted collection) and `custom` is the stored
|
|
20
|
-
* metadata body (an opaque envelope on an encrypted collection); encrypt/decrypt
|
|
21
|
-
* stays a read/write-time concern above this layer.
|
|
22
|
+
* @param options {object}
|
|
23
|
+
* @param options.source {OptionalBodyFields}
|
|
24
|
+
* @param options.target {OptionalBodyFields} mutated in place
|
|
25
|
+
* @returns {void}
|
|
22
26
|
*/
|
|
27
|
+
export function copyOptionalBodyFields({ source, target }) {
|
|
28
|
+
if (source.data !== undefined) {
|
|
29
|
+
target.data = source.data;
|
|
30
|
+
}
|
|
31
|
+
if (source.metaVersion !== undefined) {
|
|
32
|
+
target.metaVersion = source.metaVersion;
|
|
33
|
+
}
|
|
34
|
+
if (source.custom !== undefined) {
|
|
35
|
+
target.custom = source.custom;
|
|
36
|
+
}
|
|
37
|
+
if (source.epoch !== undefined) {
|
|
38
|
+
target.epoch = source.epoch;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
23
41
|
/**
|
|
24
42
|
* Thrown by a {@link WasSyncPort} implementation when a conditional write is
|
|
25
43
|
* rejected with `412 precondition-failed` (a lost-update conflict, or a
|
package/dist/sync/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAqDA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CACzB,IAAsB,EACtB,KAAuB;IAEvB,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAA;AACvE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EAIP;IACC,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;IAC3B,CAAC;IACD,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAA;IACzC,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;IAC7B,CAAC;AACH,CAAC;AA8OD;;;;;;GAMG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAO,GAAG,4CAA4C;QAChE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAA;IACpC,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAChC,MAAM,CAAQ;IACvB,YAAY,MAAc;QACxB,KAAK,CAAC,mCAAmC,MAAM,IAAI,CAAC,CAAA;QACpD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;QAC9B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;CACF"}
|
|
@@ -26,17 +26,6 @@
|
|
|
26
26
|
import { type WasClient } from '@interop/was-client';
|
|
27
27
|
import type { IZcap } from '@interop/data-integrity-core';
|
|
28
28
|
import { type WasSyncBasePort } from './types.js';
|
|
29
|
-
/**
|
|
30
|
-
* Extracts an HTTP status from a raw ky/ezcap error. `was.request()` rejects on
|
|
31
|
-
* any non-2xx with `err.status` set (see `@interop/http-client`'s error
|
|
32
|
-
* normaliser); this reads it defensively from either location. Shared with the
|
|
33
|
-
* remote store's descriptor PUT and the dev-grant provisioner, which need the raw
|
|
34
|
-
* status for diagnostics rather than the mapped sync error.
|
|
35
|
-
*
|
|
36
|
-
* @param err {unknown}
|
|
37
|
-
* @returns {number | undefined}
|
|
38
|
-
*/
|
|
39
|
-
export declare function errorStatus(err: unknown): number | undefined;
|
|
40
29
|
/**
|
|
41
30
|
* Normalizes an unknown caught error into a display string: the `Error`'s
|
|
42
31
|
* `message` when it is one, else its `String(...)` coercion. Shared by the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasSyncPort.d.ts","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"wasSyncPort.d.ts","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE5E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AAEzD,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,YAAY,CAAA;AAEnB;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEjD;AAgCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,GAAG,EACH,OAAO,EACP,YAAY,EACZ,UAAU,EACX,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,KAAK,CAAA;CACnB,GAAG,eAAe,CAkHlB"}
|
package/dist/sync/wasSyncPort.js
CHANGED
|
@@ -24,21 +24,9 @@
|
|
|
24
24
|
* encrypted fork here.
|
|
25
25
|
*/
|
|
26
26
|
import { readEtag, writeHeaders } from '@interop/was-client';
|
|
27
|
+
import { errorStatus, parseEtag } from '@interop/was-client/sync';
|
|
28
|
+
import { collectionPath } from '../grants.js';
|
|
27
29
|
import { WasSyncAuthError, WasSyncConflictError } from './types.js';
|
|
28
|
-
/**
|
|
29
|
-
* Extracts an HTTP status from a raw ky/ezcap error. `was.request()` rejects on
|
|
30
|
-
* any non-2xx with `err.status` set (see `@interop/http-client`'s error
|
|
31
|
-
* normaliser); this reads it defensively from either location. Shared with the
|
|
32
|
-
* remote store's descriptor PUT and the dev-grant provisioner, which need the raw
|
|
33
|
-
* status for diagnostics rather than the mapped sync error.
|
|
34
|
-
*
|
|
35
|
-
* @param err {unknown}
|
|
36
|
-
* @returns {number | undefined}
|
|
37
|
-
*/
|
|
38
|
-
export function errorStatus(err) {
|
|
39
|
-
return (err.status ??
|
|
40
|
-
err.response?.status);
|
|
41
|
-
}
|
|
42
30
|
/**
|
|
43
31
|
* Normalizes an unknown caught error into a display string: the `Error`'s
|
|
44
32
|
* `message` when it is one, else its `String(...)` coercion. Shared by the
|
|
@@ -79,21 +67,6 @@ function toPortError(err) {
|
|
|
79
67
|
}
|
|
80
68
|
return err;
|
|
81
69
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Parses a quoted strong ETag (`"3"`, as read by `readEtag`) into its numeric
|
|
84
|
-
* revision, or `undefined` when the header was absent (the resource has no such
|
|
85
|
-
* revision yet).
|
|
86
|
-
*
|
|
87
|
-
* @param etag {string | undefined}
|
|
88
|
-
* @returns {number | undefined}
|
|
89
|
-
*/
|
|
90
|
-
function parseEtag(etag) {
|
|
91
|
-
if (etag === undefined) {
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
const revision = Number(etag.replace(/"/g, ''));
|
|
95
|
-
return Number.isFinite(revision) ? revision : undefined;
|
|
96
|
-
}
|
|
97
70
|
/**
|
|
98
71
|
* Builds a `WasSyncPort` bound to a single Space + Collection on the remote WAS
|
|
99
72
|
* server, backed by the session's signed `WasClient`.
|
|
@@ -108,8 +81,8 @@ function parseEtag(etag) {
|
|
|
108
81
|
* @returns {WasSyncBasePort}
|
|
109
82
|
*/
|
|
110
83
|
export function createWasSyncPort({ was, spaceId, collectionId, capability }) {
|
|
111
|
-
const
|
|
112
|
-
const resourcePath = (id) => `${
|
|
84
|
+
const basePath = collectionPath({ spaceId, collectionId });
|
|
85
|
+
const resourcePath = (id) => `${basePath}/${encodeURIComponent(id)}`;
|
|
113
86
|
/**
|
|
114
87
|
* Runs a conditional write, mapping the server's `412 precondition-failed`
|
|
115
88
|
* into the core's `WasSyncConflictError` and a `401` / `403` into
|
|
@@ -125,7 +98,9 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability }) {
|
|
|
125
98
|
run) => {
|
|
126
99
|
try {
|
|
127
100
|
const response = await run();
|
|
128
|
-
|
|
101
|
+
// `readEtag` reports an absent header as `undefined`; `parseEtag` reads
|
|
102
|
+
// the `null` spelling of the same thing.
|
|
103
|
+
return parseEtag(readEtag(response) ?? null);
|
|
129
104
|
}
|
|
130
105
|
catch (err) {
|
|
131
106
|
throw toPortError(err);
|
|
@@ -136,7 +111,7 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability }) {
|
|
|
136
111
|
try {
|
|
137
112
|
const response = await was.request({
|
|
138
113
|
capability,
|
|
139
|
-
path: `${
|
|
114
|
+
path: `${basePath}/query`,
|
|
140
115
|
method: 'POST',
|
|
141
116
|
json: {
|
|
142
117
|
profile: 'changes',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasSyncPort.js","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAkB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"wasSyncPort.js","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interop/was-react",
|
|
3
3
|
"description": "React library for building 'Bring Your Own Everything' (BYOE) apps on Wallet Attached Storage: DID Auth login via CHAPI wallet, local-first encrypted storage, and background sync to a WAS server.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "pnpm run clear && tsc",
|
|
@@ -53,10 +53,9 @@
|
|
|
53
53
|
"@interop/security-document-loader": "^9.5.1",
|
|
54
54
|
"@interop/vc": "^11.0.6",
|
|
55
55
|
"@interop/verifier-core": "^3.5.3",
|
|
56
|
-
"@interop/wallet-core": "^0.
|
|
57
|
-
"@interop/was-client": "^0.
|
|
56
|
+
"@interop/wallet-core": "^0.20.0",
|
|
57
|
+
"@interop/was-client": "^0.27.0",
|
|
58
58
|
"@interop/webkms-client": "^14.7.1",
|
|
59
|
-
"@interop/x25519-key-agreement-key": "^5.2.1",
|
|
60
59
|
"@scure/base": "^2.2.0",
|
|
61
60
|
"byoe-context": "^0.2.0",
|
|
62
61
|
"credential-handler-polyfill": "^4.1.0",
|
|
@@ -85,6 +84,7 @@
|
|
|
85
84
|
"@emotion/react": "^11.14.0",
|
|
86
85
|
"@emotion/styled": "^11.14.1",
|
|
87
86
|
"@eslint/js": "^10.0.1",
|
|
87
|
+
"@interop/x25519-key-agreement-key": "^5.2.1",
|
|
88
88
|
"@mui/icons-material": "^9.0.0",
|
|
89
89
|
"@mui/material": "^9.0.0",
|
|
90
90
|
"@playwright/test": "^1.60.0",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"typescript-eslint": "^8.59.4",
|
|
112
112
|
"vite": "^8.0.14",
|
|
113
113
|
"vitest": "^4.1.7",
|
|
114
|
-
"was-teaching-server": "^0.
|
|
114
|
+
"was-teaching-server": "^0.17.1",
|
|
115
115
|
"zustand": "^5.0.12"
|
|
116
116
|
},
|
|
117
117
|
"publishConfig": {
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* App-session identity bootstrap: everything derivable from the master seed in
|
|
6
|
-
* one call. Thin composition over the pinned derivation in `agents.ts` (which
|
|
7
|
-
* is the shared-key contract: `CapabilityAgent.fromSeed` on raw bytes, keyName
|
|
8
|
-
* `app-key` -- never change it without a migration plan).
|
|
9
|
-
*
|
|
10
|
-
* The agents it returns carry the app's identity key-agreement key, which is
|
|
11
|
-
* the ONE key every encrypted collection is read with: the local replica's
|
|
12
|
-
* ciphers (`LocalStore.init`) and every key-epoch roster entry the wallet
|
|
13
|
-
* writes for this app alike.
|
|
14
|
-
*/
|
|
15
|
-
import { type IdentityAgents } from './agents.js';
|
|
16
|
-
/**
|
|
17
|
-
* Derives the app's identity agents (stable did:key controller, signer,
|
|
18
|
-
* ZcapClient) from the master seed.
|
|
19
|
-
*
|
|
20
|
-
* @param options {object}
|
|
21
|
-
* @param options.seed {Uint8Array} the 32-byte master seed
|
|
22
|
-
* @returns {Promise<IdentityAgents>}
|
|
23
|
-
*/
|
|
24
|
-
export declare function initAppSession({ seed }: {
|
|
25
|
-
seed: Uint8Array;
|
|
26
|
-
}): Promise<IdentityAgents>;
|
|
27
|
-
//# sourceMappingURL=initAppSession.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"initAppSession.d.ts","sourceRoot":"","sources":["../../src/identity/initAppSession.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;GAUG;AACH,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjE;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,EACnC,IAAI,EACL,EAAE;IACD,IAAI,EAAE,UAAU,CAAA;CACjB,GAAG,OAAO,CAAC,cAAc,CAAC,CAK1B"}
|