@interop/was-react 0.20.0 → 0.21.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 +8 -2
- package/dist/dev/provisionDevGrants.js +1 -1
- package/dist/dev/provisionDevGrants.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/session/authStore.d.ts.map +1 -1
- package/dist/session/authStore.js +48 -11
- package/dist/session/authStore.js.map +1 -1
- package/dist/storage/adopt.d.ts.map +1 -1
- package/dist/storage/adopt.js +2 -1
- package/dist/storage/adopt.js.map +1 -1
- package/dist/storage/descriptorManager.d.ts +1 -0
- package/dist/storage/descriptorManager.d.ts.map +1 -1
- package/dist/storage/descriptorManager.js +39 -21
- package/dist/storage/descriptorManager.js.map +1 -1
- package/dist/{sync → storage}/docCipher.d.ts +3 -29
- package/dist/storage/docCipher.d.ts.map +1 -0
- package/dist/{sync → storage}/docCipher.js +0 -20
- package/dist/storage/docCipher.js.map +1 -0
- package/dist/storage/entityStore.d.ts.map +1 -1
- package/dist/storage/entityStore.js +2 -1
- package/dist/storage/entityStore.js.map +1 -1
- package/dist/storage/localStore.d.ts +1 -1
- package/dist/storage/localStore.d.ts.map +1 -1
- package/dist/storage/localStore.js +14 -2
- 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 +1 -1
- package/dist/storage/sharedCollectionReader.js.map +1 -1
- package/dist/storage/storageContext.d.ts +7 -5
- package/dist/storage/storageContext.d.ts.map +1 -1
- package/dist/storage/storageContext.js +8 -4
- package/dist/storage/storageContext.js.map +1 -1
- package/dist/storage/storageManager.d.ts +7 -3
- package/dist/storage/storageManager.d.ts.map +1 -1
- package/dist/storage/storageManager.js +6 -2
- package/dist/storage/storageManager.js.map +1 -1
- package/dist/storage/syncController.d.ts +30 -19
- package/dist/storage/syncController.d.ts.map +1 -1
- package/dist/storage/syncController.js +84 -157
- package/dist/storage/syncController.js.map +1 -1
- package/dist/storage/syncStatusStore.d.ts +8 -7
- package/dist/storage/syncStatusStore.d.ts.map +1 -1
- package/dist/storage/syncStatusStore.js.map +1 -1
- package/dist/storage/wasRemoteStore.d.ts +24 -10
- package/dist/storage/wasRemoteStore.d.ts.map +1 -1
- package/dist/storage/wasRemoteStore.js +92 -37
- package/dist/storage/wasRemoteStore.js.map +1 -1
- package/dist/storage/wasSync.d.ts +25 -10
- package/dist/storage/wasSync.d.ts.map +1 -1
- package/dist/storage/wasSync.js +53 -22
- package/dist/storage/wasSync.js.map +1 -1
- package/dist/{sync → storage}/wasSyncPort.d.ts +14 -8
- package/dist/storage/wasSyncPort.d.ts.map +1 -0
- package/dist/storage/wasSyncPort.js.map +1 -0
- package/dist/storage/writerId.d.ts +7 -13
- package/dist/storage/writerId.d.ts.map +1 -1
- package/dist/storage/writerId.js +30 -46
- package/dist/storage/writerId.js.map +1 -1
- package/package.json +13 -10
- package/dist/sync/changesQuery.d.ts +0 -44
- package/dist/sync/changesQuery.d.ts.map +0 -1
- package/dist/sync/changesQuery.js +0 -54
- package/dist/sync/changesQuery.js.map +0 -1
- package/dist/sync/docCipher.d.ts.map +0 -1
- package/dist/sync/docCipher.js.map +0 -1
- package/dist/sync/feedMasterPort.d.ts +0 -38
- package/dist/sync/feedMasterPort.d.ts.map +0 -1
- package/dist/sync/feedMasterPort.js +0 -116
- package/dist/sync/feedMasterPort.js.map +0 -1
- package/dist/sync/index.d.ts +0 -23
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/index.js +0 -27
- package/dist/sync/index.js.map +0 -1
- package/dist/sync/lww.d.ts +0 -50
- package/dist/sync/lww.d.ts.map +0 -1
- package/dist/sync/lww.js +0 -69
- package/dist/sync/lww.js.map +0 -1
- package/dist/sync/lwwConflictHandler.d.ts +0 -83
- package/dist/sync/lwwConflictHandler.d.ts.map +0 -1
- package/dist/sync/lwwConflictHandler.js +0 -114
- package/dist/sync/lwwConflictHandler.js.map +0 -1
- package/dist/sync/pushWrites.d.ts +0 -76
- package/dist/sync/pushWrites.d.ts.map +0 -1
- package/dist/sync/pushWrites.js +0 -221
- package/dist/sync/pushWrites.js.map +0 -1
- package/dist/sync/syncedDocSchema.d.ts +0 -22
- package/dist/sync/syncedDocSchema.d.ts.map +0 -1
- package/dist/sync/syncedDocSchema.js +0 -29
- package/dist/sync/syncedDocSchema.js.map +0 -1
- package/dist/sync/types.d.ts +0 -319
- package/dist/sync/types.d.ts.map +0 -1
- package/dist/sync/types.js +0 -58
- package/dist/sync/types.js.map +0 -1
- package/dist/sync/wasReplication.d.ts +0 -47
- package/dist/sync/wasReplication.d.ts.map +0 -1
- package/dist/sync/wasReplication.js +0 -92
- package/dist/sync/wasReplication.js.map +0 -1
- package/dist/sync/wasSyncPort.d.ts.map +0 -1
- package/dist/sync/wasSyncPort.js.map +0 -1
- /package/dist/{sync → storage}/wasSyncPort.js +0 -0
|
@@ -1,45 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSyncController } from '@interop/was-sync/rxdb';
|
|
2
2
|
import { DEFAULT_SYNC_POLL_MS } from '../config.js';
|
|
3
|
+
export { isAuthError } from '@interop/was-sync/rxdb';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* wraps that thrown error inside an RxError (nested under `cause` / `errors` /
|
|
8
|
-
* `parameters.errors`), so this walks the error graph looking for a
|
|
9
|
-
* `WasSyncAuthError` rather than re-extracting raw status codes. RxDB's
|
|
10
|
-
* wrapping serializes the handler's thrown error to plain JSON (name, message,
|
|
11
|
-
* stack -- `errorToPlainJson`), so the walk matches the serialized `name`
|
|
12
|
-
* alongside a live instance; an instance only ever appears when a caller hands
|
|
13
|
-
* the port error over directly.
|
|
5
|
+
* The browser reachability source the core polls and subscribes through. An
|
|
6
|
+
* `isOnline` that cannot answer reports `true`, since a platform with no
|
|
7
|
+
* reachability signal must still poll.
|
|
14
8
|
*
|
|
15
|
-
* @
|
|
16
|
-
* @returns {boolean}
|
|
9
|
+
* @returns {object} the core's `onlineSource` port
|
|
17
10
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (current instanceof WasSyncAuthError ||
|
|
28
|
-
current.name === 'WasSyncAuthError') {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
const candidate = current;
|
|
32
|
-
if (candidate.cause) {
|
|
33
|
-
queue.push(candidate.cause);
|
|
34
|
-
}
|
|
35
|
-
if (Array.isArray(candidate.errors)) {
|
|
36
|
-
queue.push(...candidate.errors);
|
|
37
|
-
}
|
|
38
|
-
if (Array.isArray(candidate.parameters?.errors)) {
|
|
39
|
-
queue.push(...candidate.parameters.errors);
|
|
11
|
+
function browserOnlineSource() {
|
|
12
|
+
return {
|
|
13
|
+
isOnline: () => typeof navigator === 'undefined' || navigator.onLine !== false,
|
|
14
|
+
subscribe: onOnline => {
|
|
15
|
+
if (typeof window === 'undefined') {
|
|
16
|
+
return () => { };
|
|
17
|
+
}
|
|
18
|
+
window.addEventListener('online', onOnline);
|
|
19
|
+
return () => window.removeEventListener('online', onOnline);
|
|
40
20
|
}
|
|
41
|
-
}
|
|
42
|
-
return false;
|
|
21
|
+
};
|
|
43
22
|
}
|
|
44
23
|
/**
|
|
45
24
|
* A per-session controller around background replication.
|
|
@@ -48,10 +27,7 @@ export class SyncController {
|
|
|
48
27
|
#collections;
|
|
49
28
|
#sync;
|
|
50
29
|
#syncStatus;
|
|
51
|
-
#
|
|
52
|
-
#onlineHandler;
|
|
53
|
-
#pollTimer;
|
|
54
|
-
#started = false;
|
|
30
|
+
#core;
|
|
55
31
|
#stopped = false;
|
|
56
32
|
/**
|
|
57
33
|
* @param options {object}
|
|
@@ -67,7 +43,12 @@ export class SyncController {
|
|
|
67
43
|
}
|
|
68
44
|
/**
|
|
69
45
|
* Starts background replication for every entity collection covered by the
|
|
70
|
-
* grant set. Idempotent (a no-op if already running)
|
|
46
|
+
* grant set. Idempotent (a no-op if already running, or if `stop()` has
|
|
47
|
+
* already run: `stop()` is terminal, so a logout that raced an in-flight
|
|
48
|
+
* session bootstrap never spins up replications against a closed database).
|
|
49
|
+
*
|
|
50
|
+
* A failed bring-up rethrows, with every collection's status left at `error`,
|
|
51
|
+
* so the caller's bootstrap can surface it.
|
|
71
52
|
*
|
|
72
53
|
* @param options {object}
|
|
73
54
|
* @param options.remoteStore {WasRemoteStore}
|
|
@@ -80,123 +61,72 @@ export class SyncController {
|
|
|
80
61
|
* @returns {Promise<void>}
|
|
81
62
|
*/
|
|
82
63
|
async start({ remoteStore, localStore, onRemoteChange, onAuthError }) {
|
|
83
|
-
|
|
84
|
-
// that raced an in-flight session bootstrap) must never spin up
|
|
85
|
-
// replications against the now-closed database.
|
|
86
|
-
if (this.#started || this.#stopped) {
|
|
64
|
+
if (this.#core || this.#stopped) {
|
|
87
65
|
return;
|
|
88
66
|
}
|
|
89
|
-
this.#started = true;
|
|
90
67
|
const setStatus = this.#syncStatus.getState().setStatus;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
setStatus(key, 'idle');
|
|
107
|
-
// Wrap the verbatim port so the 412 conflict re-read resolves `version`
|
|
108
|
-
// from the changes-feed body (CORS hides the `GET` ETag cross-origin).
|
|
109
|
-
const wasPort = withFeedMasterRead(createWasSyncPort({
|
|
110
|
-
was: remoteStore.was,
|
|
111
|
-
spaceId: remoteStore.spaceId,
|
|
112
|
-
collectionId: id,
|
|
113
|
-
capability
|
|
114
|
-
}));
|
|
115
|
-
const state = createWasReplication({
|
|
116
|
-
rxCollection: localStore.rxCollection(key),
|
|
117
|
-
wasPort,
|
|
118
|
-
replicationIdentifier: `was-sync:${remoteStore.serverUrl}:${remoteStore.spaceId}:${id}`,
|
|
119
|
-
...(batchSize !== undefined && { batchSize }),
|
|
120
|
-
...(retryMs !== undefined && { retryTime: retryMs })
|
|
121
|
-
});
|
|
122
|
-
const subscriptions = [
|
|
123
|
-
state.active$.subscribe(active => {
|
|
124
|
-
setStatus(key, active ? 'syncing' : 'synced');
|
|
125
|
-
}),
|
|
126
|
-
state.error$.subscribe(err => {
|
|
127
|
-
console.error(`Sync error for "${id}":`, err);
|
|
128
|
-
setStatus(key, 'error');
|
|
129
|
-
if (onAuthError && isAuthError(err)) {
|
|
130
|
-
onAuthError();
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
];
|
|
134
|
-
if (onRemoteChange) {
|
|
135
|
-
subscriptions.push(localStore
|
|
136
|
-
.rxCollection(key)
|
|
137
|
-
.$.subscribe(event => onRemoteChange(key, event)));
|
|
138
|
-
}
|
|
139
|
-
this.#replications.push({ state, subscriptions });
|
|
68
|
+
// A collection the grant set does not cover would otherwise sync with no
|
|
69
|
+
// capability and draw a fail-closed 403, tripping the session-wide
|
|
70
|
+
// "storage access expired" banner. Flag it and keep it out of the port
|
|
71
|
+
// instead, so an uncovered collection never masquerades as expired access.
|
|
72
|
+
// The core makes the same call, but only for a port whose OTHER entries
|
|
73
|
+
// carry a capability: it cannot tell an all-uncovered delegated port from
|
|
74
|
+
// a wallet's root-capability port, and every session here is delegated.
|
|
75
|
+
const covered = [];
|
|
76
|
+
for (const { key, id } of this.#collections) {
|
|
77
|
+
const capability = remoteStore.collectionCapability(id);
|
|
78
|
+
if (!capability) {
|
|
79
|
+
console.warn(`Skipping sync for "${id}": no delegated capability covers it.`);
|
|
80
|
+
setStatus(key, 'error');
|
|
81
|
+
continue;
|
|
140
82
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
83
|
+
covered.push({ key, id, capability });
|
|
84
|
+
}
|
|
85
|
+
this.#core = createSyncController({
|
|
86
|
+
port: {
|
|
87
|
+
wasClient: remoteStore.was,
|
|
88
|
+
spaceId: remoteStore.spaceId,
|
|
89
|
+
serverUrl: remoteStore.serverUrl,
|
|
90
|
+
collections: covered,
|
|
91
|
+
rxCollection: key => localStore.rxCollection(key),
|
|
92
|
+
// The 412 conflict re-read resolves `version` from the changes-feed
|
|
93
|
+
// body: CORS hides the `GET` ETag cross-origin, which is this
|
|
94
|
+
// library's deployment shape.
|
|
95
|
+
feedPrimaryRead: true,
|
|
96
|
+
// A `401` / `403` / the server's masked `404` must arrive typed, or
|
|
97
|
+
// `onAuthError` could never fire.
|
|
98
|
+
mapAuthErrors: true,
|
|
99
|
+
...(this.#sync.batchSize !== undefined && {
|
|
100
|
+
batchSize: this.#sync.batchSize
|
|
101
|
+
}),
|
|
102
|
+
...(this.#sync.retryMs !== undefined && {
|
|
103
|
+
retryTime: this.#sync.retryMs
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
// The core delivers the logical key first and the WAS id second; the
|
|
107
|
+
// status store is keyed on the logical key, since two registry entries
|
|
108
|
+
// may share one WAS id.
|
|
109
|
+
onStatus: (key, _collectionId, status) => setStatus(key, status),
|
|
110
|
+
...(onAuthError && { onAuthError }),
|
|
111
|
+
...(onRemoteChange && { onRemoteChange }),
|
|
112
|
+
onlineSource: browserOnlineSource(),
|
|
113
|
+
pollMs: this.#sync.pollMs ?? DEFAULT_SYNC_POLL_MS,
|
|
114
|
+
log: {
|
|
115
|
+
warn: (message, meta) => console.warn(message, meta),
|
|
116
|
+
error: (message, meta) => console.error(message, meta)
|
|
149
117
|
}
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
await this.#core.start();
|
|
150
121
|
}
|
|
151
122
|
catch (err) {
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
// wallet-connected session that failed to start replicating. Instead the
|
|
157
|
-
// per-collection statuses are left at `error` (visible in the rollup)
|
|
158
|
-
// and the error RETHROWS so the caller's bootstrap promise rejects and
|
|
159
|
-
// the session can surface it.
|
|
160
|
-
await this.#teardownReplications();
|
|
161
|
-
this.#started = false;
|
|
162
|
-
for (const { key } of this.#collections) {
|
|
163
|
-
setStatus(key, 'error');
|
|
164
|
-
}
|
|
123
|
+
// The core already unwound and flagged every collection; drop the
|
|
124
|
+
// instance so this session's controller is not left holding a core it
|
|
125
|
+
// can neither start nor usefully stop, and rethrow.
|
|
126
|
+
this.#core = undefined;
|
|
165
127
|
throw err;
|
|
166
128
|
}
|
|
167
129
|
}
|
|
168
|
-
/**
|
|
169
|
-
* The shared release path behind the failed-bring-up unwind and {@link stop}:
|
|
170
|
-
* drops the `online` listener, clears the poll timer, unsubscribes every
|
|
171
|
-
* per-collection subscription, and cancels every replication. What each caller
|
|
172
|
-
* does AFTER this differs (the unwind leaves the per-collection statuses at
|
|
173
|
-
* `error` and rethrows; `stop()` resets the status store behind its terminal
|
|
174
|
-
* latch), so the divergence stays at the call sites.
|
|
175
|
-
*
|
|
176
|
-
* @returns {Promise<void>}
|
|
177
|
-
*/
|
|
178
|
-
async #teardownReplications() {
|
|
179
|
-
if (this.#onlineHandler) {
|
|
180
|
-
window.removeEventListener('online', this.#onlineHandler);
|
|
181
|
-
this.#onlineHandler = undefined;
|
|
182
|
-
}
|
|
183
|
-
if (this.#pollTimer) {
|
|
184
|
-
clearInterval(this.#pollTimer);
|
|
185
|
-
this.#pollTimer = undefined;
|
|
186
|
-
}
|
|
187
|
-
for (const { state, subscriptions } of this.#replications) {
|
|
188
|
-
for (const subscription of subscriptions) {
|
|
189
|
-
subscription.unsubscribe();
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
await state.cancel();
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
console.error('Error cancelling replication:', err);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
this.#replications = [];
|
|
199
|
-
}
|
|
200
130
|
/**
|
|
201
131
|
* Triggers an immediate replication cycle on every running collection, rather
|
|
202
132
|
* than waiting for RxDB's next scheduled tick. Fire-and-forget.
|
|
@@ -204,23 +134,20 @@ export class SyncController {
|
|
|
204
134
|
* @returns {void}
|
|
205
135
|
*/
|
|
206
136
|
reSync() {
|
|
207
|
-
|
|
208
|
-
state.reSync();
|
|
209
|
-
}
|
|
137
|
+
this.#core?.reSync();
|
|
210
138
|
}
|
|
211
139
|
/**
|
|
212
140
|
* Stops replication and releases resources (the database is owned by the
|
|
213
|
-
* caller). Idempotent.
|
|
141
|
+
* caller). Idempotent, and terminal: a later `start()` is refused.
|
|
214
142
|
*
|
|
215
143
|
* @returns {Promise<void>}
|
|
216
144
|
*/
|
|
217
145
|
async stop() {
|
|
218
|
-
// Latch first so a concurrent `start()` (a logout racing the session
|
|
219
|
-
// bootstrap) sees the controller as terminally stopped and bails.
|
|
220
146
|
this.#stopped = true;
|
|
221
|
-
|
|
147
|
+
const core = this.#core;
|
|
148
|
+
this.#core = undefined;
|
|
149
|
+
await core?.stop();
|
|
222
150
|
this.#syncStatus.getState().reset();
|
|
223
|
-
this.#started = false;
|
|
224
151
|
}
|
|
225
152
|
}
|
|
226
153
|
//# sourceMappingURL=syncController.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncController.js","sourceRoot":"","sources":["../../src/storage/syncController.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"syncController.js","sourceRoot":"","sources":["../../src/storage/syncController.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
* provider. In-memory only, like the session -- reset on logout.
|
|
11
11
|
*/
|
|
12
12
|
import { type StoreApi } from 'zustand/vanilla';
|
|
13
|
+
import type { SyncStatus } from '@interop/was-client/sync';
|
|
13
14
|
/**
|
|
14
|
-
* A single collection's replication status
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* A single collection's replication status, re-exported from the client's
|
|
16
|
+
* closed vocabulary (`idle` -- configured but no cycle has run yet; `syncing`
|
|
17
|
+
* -- a pull/push cycle is in flight; `synced` -- last cycle completed without
|
|
18
|
+
* error; `error` -- last cycle failed and RxDB is backing off). The client owns
|
|
19
|
+
* the type so the replication driver and the wallets' own sync engine report
|
|
20
|
+
* the same four strings.
|
|
19
21
|
*/
|
|
20
|
-
export type SyncStatus
|
|
22
|
+
export type { SyncStatus };
|
|
21
23
|
interface SyncStatusState {
|
|
22
24
|
/**
|
|
23
25
|
* Keyed by the registry's LOGICAL collection key (`WasCollectionConfig.key`,
|
|
@@ -61,5 +63,4 @@ export declare function deriveSyncRollup(statuses: SyncStatus[]): {
|
|
|
61
63
|
label: string;
|
|
62
64
|
title: string;
|
|
63
65
|
};
|
|
64
|
-
export {};
|
|
65
66
|
//# sourceMappingURL=syncStatusStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncStatusStore.d.ts","sourceRoot":"","sources":["../../src/storage/syncStatusStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;GAOG;AACH,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"syncStatusStore.d.ts","sourceRoot":"","sources":["../../src/storage/syncStatusStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;GAOG;AACH,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAE1D;;;;;;;GAOG;AACH,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B,UAAU,eAAe;IACvB;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACpC,SAAS,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAA;IAC9D,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAA;AAEvD;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CASvD;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEnE;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG;IACxD,KAAK,EAAE,UAAU,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CA2BA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncStatusStore.js","sourceRoot":"","sources":["../../src/storage/syncStatusStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;GAOG;AACH,OAAO,EAAE,WAAW,EAAiB,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"syncStatusStore.js","sourceRoot":"","sources":["../../src/storage/syncStatusStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;GAOG;AACH,OAAO,EAAE,WAAW,EAAiB,MAAM,iBAAiB,CAAA;AA4B5D;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,WAAW,EAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5C,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,EAAE,CACnC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACZ,QAAQ,EAAE,EAAE,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE;SACzD,CAAC,CAAC;QACL,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;KACnC,CAAC,CAAC,CAAA;AACL,CAAC;AASD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAsB;IAKrD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO;YACL,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,mDAAmD;SAC3D,CAAA;IACH,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,OAAO;YACL,KAAK,EAAE,OAAO;YACd,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,+DAA+D;SACvE,CAAA;IACH,CAAC;IACD,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9D,OAAO;YACL,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,sDAAsD;SAC9D,CAAA;IACH,CAAC;IACD,OAAO;QACL,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,QAAQ;QACf,KAAK,EAAE,oDAAoD;KAC5D,CAAA;AACH,CAAC"}
|
|
@@ -120,10 +120,16 @@ export declare class WasRemoteStore {
|
|
|
120
120
|
* Reads one collection's `encryption` descriptor from its Collection Description,
|
|
121
121
|
* invoked with that collection's delegated zcap. Returns the
|
|
122
122
|
* {@link CollectionEncryption} block (a multi-recipient descriptor carries key
|
|
123
|
-
* epochs) or `undefined` when the
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
123
|
+
* epochs), or `undefined` only when the answer is genuinely "no descriptor":
|
|
124
|
+
* no capability covers the collection, the collection is not found (which is
|
|
125
|
+
* also how WAS answers an unauthorized read), or the description carries no
|
|
126
|
+
* `encryption` member.
|
|
127
|
+
*
|
|
128
|
+
* Any other failure (a dropped connection, a 5xx) is thrown, wrapped with
|
|
129
|
+
* the collection id. It must NOT read as "no descriptor": a caller that takes
|
|
130
|
+
* it for one opens the collection under the fail-closed placeholder cipher,
|
|
131
|
+
* and a correctly provisioned collection then fails its first write or
|
|
132
|
+
* decrypt for a network hiccup.
|
|
127
133
|
*
|
|
128
134
|
* @param collectionId {string} the WAS collection id
|
|
129
135
|
* @returns {Promise<CollectionEncryption | undefined>}
|
|
@@ -138,9 +144,13 @@ export declare class WasRemoteStore {
|
|
|
138
144
|
* NOT `Collection.meta()`: that one DECODES `custom` to plaintext, which
|
|
139
145
|
* `applyMeta` cannot consume.
|
|
140
146
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
147
|
+
* Answers `undefined` for exactly the outcomes that mean "no metadata": no
|
|
148
|
+
* capability covers the collection, the read answered not-found (which is
|
|
149
|
+
* also how WAS answers an unauthorized read), or the backend has no metadata
|
|
150
|
+
* support (`501`). Any other failure is thrown, wrapped with the collection
|
|
151
|
+
* id, like {@link readCollectionEncryption}: a dropped connection is an
|
|
152
|
+
* unknown answer, not an absent one, and a caller that took it for "no
|
|
153
|
+
* schema" would skip the blinded-index install over a guess.
|
|
144
154
|
*
|
|
145
155
|
* @param collectionId {string} the WAS collection id
|
|
146
156
|
* @returns {Promise<{ custom?: unknown } | undefined>}
|
|
@@ -301,9 +311,13 @@ export declare class WasRemoteStore {
|
|
|
301
311
|
* Deliberately NOT `wasDescriptorSource` from the same subpath: that one
|
|
302
312
|
* describes through the client's root capability, which a relying-party app
|
|
303
313
|
* never holds. Every read here must invoke the per-collection grant, which is
|
|
304
|
-
* exactly what {@link WasRemoteStore.readCollectionEncryption} does
|
|
305
|
-
*
|
|
306
|
-
*
|
|
314
|
+
* exactly what {@link WasRemoteStore.readCollectionEncryption} does.
|
|
315
|
+
*
|
|
316
|
+
* A read that fails for a transient reason is warned about and answered as
|
|
317
|
+
* `undefined` here: the refresh guard is already spent by the time the source
|
|
318
|
+
* is consulted, so the decrypt retry fails the same way it would under no
|
|
319
|
+
* refresh at all, and the caller sees the envelope's own unknown-epoch error
|
|
320
|
+
* rather than a network one.
|
|
307
321
|
*
|
|
308
322
|
* @param options {object}
|
|
309
323
|
* @param options.remoteStore {WasRemoteStore}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wasRemoteStore.d.ts","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACZ,KAAK,EACN,MAAM,8BAA8B,CAAA;AACrC,OAAO,
|
|
1
|
+
{"version":3,"file":"wasRemoteStore.d.ts","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { WasClient } from '@interop/was-client';
|
|
1
|
+
import { NotFoundError, NotImplementedError, WasClient, mapError } from '@interop/was-client';
|
|
2
2
|
import { createEdvEncryption } from '@interop/was-client/edv';
|
|
3
3
|
import { collectionItems, collectionMeta, collectionPath, resourcePath, toUrl } from '@interop/was-client/paths';
|
|
4
|
-
import { errorStatus, errorMessage } from '
|
|
4
|
+
import { errorStatus, errorMessage } from '@interop/was-client/sync';
|
|
5
5
|
/**
|
|
6
6
|
* Maps one configured index attribute name to the blinded-index attribute path
|
|
7
7
|
* the EDV codec addresses it by. The codec walks the path from the stored
|
|
@@ -14,6 +14,36 @@ import { errorStatus, errorMessage } from '../sync/index.js';
|
|
|
14
14
|
function blindedAttribute(name) {
|
|
15
15
|
return `content.${name}`;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs one read whose failure may mean "absent" rather than "unknown". An
|
|
19
|
+
* error `isAbsent` accepts answers `undefined`; any other failure is rethrown,
|
|
20
|
+
* wrapped with `description`, so the caller cannot mistake an unknown answer
|
|
21
|
+
* (a dropped connection, a 5xx) for an absent one. Retrying is not this
|
|
22
|
+
* layer's job: the HTTP client underneath already retries the transient
|
|
23
|
+
* status codes and network errors on every GET.
|
|
24
|
+
*
|
|
25
|
+
* @param options {object}
|
|
26
|
+
* @param options.read {() => Promise<T>}
|
|
27
|
+
* @param options.isAbsent {(err: Error) => boolean} which mapped errors mean
|
|
28
|
+
* "absent"
|
|
29
|
+
* @param options.description {string} what was being read, for the wrap
|
|
30
|
+
* @returns {Promise<T | undefined>}
|
|
31
|
+
*/
|
|
32
|
+
async function readOrAbsent({ read, isAbsent, description }) {
|
|
33
|
+
try {
|
|
34
|
+
return await read();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const mapped = mapError(err);
|
|
38
|
+
if (isAbsent(mapped)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
// The mapped (typed) error carries the caught one as its own `cause`, so
|
|
42
|
+
// the chain is intact; the typed one is what callers match on.
|
|
43
|
+
// eslint-disable-next-line preserve-caught-error
|
|
44
|
+
throw new Error(`Failed to read ${description}.`, { cause: mapped });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
17
47
|
export class WasRemoteStore {
|
|
18
48
|
was;
|
|
19
49
|
serverUrl;
|
|
@@ -95,10 +125,16 @@ export class WasRemoteStore {
|
|
|
95
125
|
* Reads one collection's `encryption` descriptor from its Collection Description,
|
|
96
126
|
* invoked with that collection's delegated zcap. Returns the
|
|
97
127
|
* {@link CollectionEncryption} block (a multi-recipient descriptor carries key
|
|
98
|
-
* epochs) or `undefined` when the
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
128
|
+
* epochs), or `undefined` only when the answer is genuinely "no descriptor":
|
|
129
|
+
* no capability covers the collection, the collection is not found (which is
|
|
130
|
+
* also how WAS answers an unauthorized read), or the description carries no
|
|
131
|
+
* `encryption` member.
|
|
132
|
+
*
|
|
133
|
+
* Any other failure (a dropped connection, a 5xx) is thrown, wrapped with
|
|
134
|
+
* the collection id. It must NOT read as "no descriptor": a caller that takes
|
|
135
|
+
* it for one opens the collection under the fail-closed placeholder cipher,
|
|
136
|
+
* and a correctly provisioned collection then fails its first write or
|
|
137
|
+
* decrypt for a network hiccup.
|
|
102
138
|
*
|
|
103
139
|
* @param collectionId {string} the WAS collection id
|
|
104
140
|
* @returns {Promise<CollectionEncryption | undefined>}
|
|
@@ -108,18 +144,22 @@ export class WasRemoteStore {
|
|
|
108
144
|
if (!capability) {
|
|
109
145
|
return undefined;
|
|
110
146
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
return await readOrAbsent({
|
|
148
|
+
read: async () => {
|
|
149
|
+
const response = await this.was.request({
|
|
150
|
+
capability,
|
|
151
|
+
path: collectionPath(this.spaceId, collectionId),
|
|
152
|
+
method: 'GET'
|
|
153
|
+
});
|
|
154
|
+
const description = response.data;
|
|
155
|
+
return description?.encryption;
|
|
156
|
+
},
|
|
157
|
+
// A not-found (which is also how WAS answers an unauthorized read) is
|
|
158
|
+
// the one outcome that genuinely means "no descriptor visible to this
|
|
159
|
+
// app".
|
|
160
|
+
isAbsent: err => err instanceof NotFoundError,
|
|
161
|
+
description: `the encryption descriptor of collection "${collectionId}"`
|
|
162
|
+
});
|
|
123
163
|
}
|
|
124
164
|
/**
|
|
125
165
|
* Reads one collection's stored `/meta` value RAW, invoked with that
|
|
@@ -130,9 +170,13 @@ export class WasRemoteStore {
|
|
|
130
170
|
* NOT `Collection.meta()`: that one DECODES `custom` to plaintext, which
|
|
131
171
|
* `applyMeta` cannot consume.
|
|
132
172
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
173
|
+
* Answers `undefined` for exactly the outcomes that mean "no metadata": no
|
|
174
|
+
* capability covers the collection, the read answered not-found (which is
|
|
175
|
+
* also how WAS answers an unauthorized read), or the backend has no metadata
|
|
176
|
+
* support (`501`). Any other failure is thrown, wrapped with the collection
|
|
177
|
+
* id, like {@link readCollectionEncryption}: a dropped connection is an
|
|
178
|
+
* unknown answer, not an absent one, and a caller that took it for "no
|
|
179
|
+
* schema" would skip the blinded-index install over a guess.
|
|
136
180
|
*
|
|
137
181
|
* @param collectionId {string} the WAS collection id
|
|
138
182
|
* @returns {Promise<{ custom?: unknown } | undefined>}
|
|
@@ -142,18 +186,19 @@ export class WasRemoteStore {
|
|
|
142
186
|
if (!capability) {
|
|
143
187
|
return undefined;
|
|
144
188
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
return await readOrAbsent({
|
|
190
|
+
read: async () => {
|
|
191
|
+
const response = await this.was.request({
|
|
192
|
+
capability,
|
|
193
|
+
path: collectionMeta(this.spaceId, collectionId),
|
|
194
|
+
method: 'GET'
|
|
195
|
+
});
|
|
196
|
+
const stored = response.data;
|
|
197
|
+
return { custom: stored?.custom };
|
|
198
|
+
},
|
|
199
|
+
isAbsent: err => err instanceof NotFoundError || err instanceof NotImplementedError,
|
|
200
|
+
description: `the metadata of collection "${collectionId}"`
|
|
201
|
+
});
|
|
157
202
|
}
|
|
158
203
|
/**
|
|
159
204
|
* Best-effort declaration of the `{ encryption: { scheme: 'edv' } }` descriptor on
|
|
@@ -508,9 +553,13 @@ export class WasRemoteStore {
|
|
|
508
553
|
* Deliberately NOT `wasDescriptorSource` from the same subpath: that one
|
|
509
554
|
* describes through the client's root capability, which a relying-party app
|
|
510
555
|
* never holds. Every read here must invoke the per-collection grant, which is
|
|
511
|
-
* exactly what {@link WasRemoteStore.readCollectionEncryption} does
|
|
512
|
-
*
|
|
513
|
-
*
|
|
556
|
+
* exactly what {@link WasRemoteStore.readCollectionEncryption} does.
|
|
557
|
+
*
|
|
558
|
+
* A read that fails for a transient reason is warned about and answered as
|
|
559
|
+
* `undefined` here: the refresh guard is already spent by the time the source
|
|
560
|
+
* is consulted, so the decrypt retry fails the same way it would under no
|
|
561
|
+
* refresh at all, and the caller sees the envelope's own unknown-epoch error
|
|
562
|
+
* rather than a network one.
|
|
514
563
|
*
|
|
515
564
|
* @param options {object}
|
|
516
565
|
* @param options.remoteStore {WasRemoteStore}
|
|
@@ -519,7 +568,13 @@ export class WasRemoteStore {
|
|
|
519
568
|
export function remoteDescriptorSource({ remoteStore }) {
|
|
520
569
|
return {
|
|
521
570
|
async collectionEncryption({ collectionId }) {
|
|
522
|
-
|
|
571
|
+
try {
|
|
572
|
+
return await remoteStore.readCollectionEncryption(collectionId);
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
console.warn(`Descriptor refresh for collection "${collectionId}" failed:`, err);
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
523
578
|
}
|
|
524
579
|
};
|
|
525
580
|
}
|