@interop/was-client 0.48.1 → 0.50.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 +13 -3
- package/dist/sync/index.d.ts +20 -9
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +19 -8
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/port.d.ts +25 -15
- package/dist/sync/port.d.ts.map +1 -1
- package/dist/sync/port.js +135 -62
- package/dist/sync/port.js.map +1 -1
- package/dist/sync/predicates.d.ts +67 -0
- package/dist/sync/predicates.d.ts.map +1 -0
- package/dist/sync/predicates.js +85 -0
- package/dist/sync/predicates.js.map +1 -0
- package/dist/sync/types.d.ts +60 -25
- package/dist/sync/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -840,7 +840,17 @@ plugs into:
|
|
|
840
840
|
itself never touches keys. A rejected precondition throws
|
|
841
841
|
`WasSyncConflictError` (412); a delete of an already-gone resource throws
|
|
842
842
|
`WasSyncNotFoundError` (404) -- both catchable subtypes of the core
|
|
843
|
-
`PreconditionFailedError` / `NotFoundError`.
|
|
843
|
+
`PreconditionFailedError` / `NotFoundError`. Every other failure arrives as
|
|
844
|
+
the typed `WasError` subclass for its status, carrying the server's
|
|
845
|
+
`problem+json` fields.
|
|
846
|
+
- **`isSyncConflictError` / `isSyncNotFoundError` / `isSyncAuthError` /
|
|
847
|
+
`isUnknownEpochError`** classify those signals by `err.name`. Use them rather
|
|
848
|
+
than `instanceof`: the port and the `DocCipher` are seams your app injects,
|
|
849
|
+
and a tree that resolves two copies of this package makes an `instanceof`
|
|
850
|
+
check quietly false. Read what you need off the matched value, such as
|
|
851
|
+
`err.status` on an auth error to tell a 401 from the masked 404.
|
|
852
|
+
- **`SyncStatus`** (`'idle' | 'syncing' | 'synced' | 'error'`) is the closed
|
|
853
|
+
vocabulary a replication driver reports one feed's state through.
|
|
844
854
|
- **`DocCipher`** is the per-collection encrypt/decrypt seam sitting above the
|
|
845
855
|
port: it turns a JSON document into its stored body (minting the resource id)
|
|
846
856
|
and back. `createPlaintextDocCipher(...)` is the crypto-free identity
|
|
@@ -869,7 +879,7 @@ import {
|
|
|
869
879
|
createPlaintextDocCipher,
|
|
870
880
|
deriveSpaceId,
|
|
871
881
|
ensureSpaceAndCollection,
|
|
872
|
-
|
|
882
|
+
isSyncConflictError
|
|
873
883
|
} from '@interop/was-client/sync'
|
|
874
884
|
|
|
875
885
|
const spaceId = deriveSpaceId(controllerDid) // same Space on every device
|
|
@@ -889,7 +899,7 @@ const { id, envelope } = await cipher.encrypt({ data: { note: 'hello' } })
|
|
|
889
899
|
try {
|
|
890
900
|
await port.putContent({ id, data: envelope, ifNoneMatch: true })
|
|
891
901
|
} catch (err) {
|
|
892
|
-
if (!(err
|
|
902
|
+
if (!isSyncConflictError(err)) {
|
|
893
903
|
throw err
|
|
894
904
|
}
|
|
895
905
|
// 412 on a content-addressed insert: another replica already wrote this
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -16,25 +16,36 @@
|
|
|
16
16
|
* - `ensureSpaceAndCollection` / `ensureSpace` -- idempotent Space +
|
|
17
17
|
* Collection provisioning, and its Space half alone (ensure the Space once,
|
|
18
18
|
* then thread the returned description through every collection).
|
|
19
|
+
* - `isSyncConflictError` / `isSyncNotFoundError` / `isSyncAuthError` /
|
|
20
|
+
* `isUnknownEpochError` -- the classification contract for the signals
|
|
21
|
+
* below. A consumer matches by `err.name` and never with `instanceof`,
|
|
22
|
+
* because every one of these errors is raised inside a seam the app injects
|
|
23
|
+
* (the port, the caller's `DocCipher`) and that seam can resolve to a second
|
|
24
|
+
* copy of this package. Reading a property off the matched value, such as
|
|
25
|
+
* `status` on an auth error, is the intended shape.
|
|
19
26
|
*
|
|
20
27
|
* The 412 conflict / 404 not-found port signals (`WasSyncConflictError` /
|
|
21
28
|
* `WasSyncNotFoundError`), and the opt-in revoked-access signal
|
|
22
29
|
* (`WasSyncAuthError`, raised only under `mapAuthErrors`), live in the client's
|
|
23
30
|
* typed error hierarchy and are re-exported here for convenience -- as are the
|
|
24
|
-
* two decrypt-routing signals, so a crypto-free sync consumer can
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* two decrypt-routing signals, so a crypto-free sync consumer can classify them
|
|
32
|
+
* without importing the `/edv` entry that throws them: the stale-descriptor
|
|
33
|
+
* signal (`UnknownEpochError`: the envelope's epoch is not on the descriptor at
|
|
34
|
+
* all, so re-read it and rebuild the cipher) and the membership signal
|
|
35
|
+
* (`KeyUnwrapError`: the epoch is on the descriptor but this reader holds no
|
|
36
|
+
* key for it, so a refresh cannot help). `EncryptionError`, the fail-closed
|
|
37
|
+
* umbrella `KeyUnwrapError` falls under, rides along. The classes are exported
|
|
38
|
+
* for construction and for a caller inside one resolved copy; across a package
|
|
39
|
+
* boundary the predicates are the contract
|
|
40
|
+
* (`decisions/0001-cross-package-errors-match-by-name.md`).
|
|
31
41
|
*/
|
|
32
|
-
export { createWasSyncPort, KEY_EPOCH_HEADER,
|
|
42
|
+
export { createWasSyncPort, KEY_EPOCH_HEADER, parseEtag } from './port.js';
|
|
33
43
|
export { httpStatus as errorStatus, errorMessage } from '../errors.js';
|
|
34
44
|
export { contentCid, cidFrom, deriveSpaceId } from './cid.js';
|
|
35
45
|
export { isEncryptedEnvelope } from './envelope.js';
|
|
36
46
|
export { createPlaintextDocCipher } from './plaintextCipher.js';
|
|
37
47
|
export { ensureSpace, ensureSpaceAndCollection } from './provisioning.js';
|
|
48
|
+
export { isSyncAuthError, isSyncConflictError, isSyncNotFoundError, isUnknownEpochError } from './predicates.js';
|
|
38
49
|
export { EncryptionError, KeyUnwrapError, UnknownEpochError, WasSyncAuthError, WasSyncConflictError, WasSyncNotFoundError } from '../errors.js';
|
|
39
|
-
export type { Json, SyncCheckpoint, WireDoc, SyncPage, MasterState, WasSyncPort, DocCipher } from './types.js';
|
|
50
|
+
export type { Json, SyncCheckpoint, WireDoc, SyncPage, MasterState, WriteAck, SyncStatus, WasSyncPort, DocCipher } from './types.js';
|
|
40
51
|
//# 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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAI1E,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACtE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AACzE,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,IAAI,EACJ,cAAc,EACd,OAAO,EACP,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,UAAU,EACV,WAAW,EACX,SAAS,EACV,MAAM,YAAY,CAAA"}
|
package/dist/sync/index.js
CHANGED
|
@@ -16,20 +16,30 @@
|
|
|
16
16
|
* - `ensureSpaceAndCollection` / `ensureSpace` -- idempotent Space +
|
|
17
17
|
* Collection provisioning, and its Space half alone (ensure the Space once,
|
|
18
18
|
* then thread the returned description through every collection).
|
|
19
|
+
* - `isSyncConflictError` / `isSyncNotFoundError` / `isSyncAuthError` /
|
|
20
|
+
* `isUnknownEpochError` -- the classification contract for the signals
|
|
21
|
+
* below. A consumer matches by `err.name` and never with `instanceof`,
|
|
22
|
+
* because every one of these errors is raised inside a seam the app injects
|
|
23
|
+
* (the port, the caller's `DocCipher`) and that seam can resolve to a second
|
|
24
|
+
* copy of this package. Reading a property off the matched value, such as
|
|
25
|
+
* `status` on an auth error, is the intended shape.
|
|
19
26
|
*
|
|
20
27
|
* The 412 conflict / 404 not-found port signals (`WasSyncConflictError` /
|
|
21
28
|
* `WasSyncNotFoundError`), and the opt-in revoked-access signal
|
|
22
29
|
* (`WasSyncAuthError`, raised only under `mapAuthErrors`), live in the client's
|
|
23
30
|
* typed error hierarchy and are re-exported here for convenience -- as are the
|
|
24
|
-
* two decrypt-routing signals, so a crypto-free sync consumer can
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* two decrypt-routing signals, so a crypto-free sync consumer can classify them
|
|
32
|
+
* without importing the `/edv` entry that throws them: the stale-descriptor
|
|
33
|
+
* signal (`UnknownEpochError`: the envelope's epoch is not on the descriptor at
|
|
34
|
+
* all, so re-read it and rebuild the cipher) and the membership signal
|
|
35
|
+
* (`KeyUnwrapError`: the epoch is on the descriptor but this reader holds no
|
|
36
|
+
* key for it, so a refresh cannot help). `EncryptionError`, the fail-closed
|
|
37
|
+
* umbrella `KeyUnwrapError` falls under, rides along. The classes are exported
|
|
38
|
+
* for construction and for a caller inside one resolved copy; across a package
|
|
39
|
+
* boundary the predicates are the contract
|
|
40
|
+
* (`decisions/0001-cross-package-errors-match-by-name.md`).
|
|
31
41
|
*/
|
|
32
|
-
export { createWasSyncPort, KEY_EPOCH_HEADER,
|
|
42
|
+
export { createWasSyncPort, KEY_EPOCH_HEADER, parseEtag } from './port.js';
|
|
33
43
|
// The sync subpath's names for the client's own error accessors, so a
|
|
34
44
|
// sync-only consumer reads a raw ky/ezcap failure without importing the core
|
|
35
45
|
// entry.
|
|
@@ -38,5 +48,6 @@ export { contentCid, cidFrom, deriveSpaceId } from './cid.js';
|
|
|
38
48
|
export { isEncryptedEnvelope } from './envelope.js';
|
|
39
49
|
export { createPlaintextDocCipher } from './plaintextCipher.js';
|
|
40
50
|
export { ensureSpace, ensureSpaceAndCollection } from './provisioning.js';
|
|
51
|
+
export { isSyncAuthError, isSyncConflictError, isSyncNotFoundError, isUnknownEpochError } from './predicates.js';
|
|
41
52
|
export { EncryptionError, KeyUnwrapError, UnknownEpochError, WasSyncAuthError, WasSyncConflictError, WasSyncNotFoundError } from '../errors.js';
|
|
42
53
|
//# 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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1E,sEAAsE;AACtE,6EAA6E;AAC7E,SAAS;AACT,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACtE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AACzE,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,cAAc,CAAA"}
|
package/dist/sync/port.d.ts
CHANGED
|
@@ -19,11 +19,21 @@
|
|
|
19
19
|
* `POST /space/:s/:c/query` (profile `changes`) as a root invocation and, like
|
|
20
20
|
* the raw writes, ships the stored bodies verbatim without decrypting.
|
|
21
21
|
*
|
|
22
|
-
* Conditional writes ride the server's
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* Conditional writes ride the server's `ETag`: an opaque quoted string
|
|
23
|
+
* (`"<generation>.<version>"`, a per-record generation marker ahead of the
|
|
24
|
+
* monotonic content `version` so a hard-deleted-and-recreated id can never
|
|
25
|
+
* collide with its predecessor's validators), enforced uniformly for
|
|
26
|
+
* plaintext and encrypted resources, so there is no plaintext-vs-encrypted
|
|
27
|
+
* fork. `putContent`/`deleteContent` return the server-acked {@link
|
|
28
|
+
* WriteAck} -- the write's raw `etag` (re-read only if the backend sent
|
|
29
|
+
* none) plus its `version` parsed out of it -- so a caller can record acked
|
|
30
|
+
* revisions immediately and echo `etag` back verbatim as a later write's
|
|
31
|
+
* `ifMatch`.
|
|
32
|
+
*
|
|
33
|
+
* Bypassing the codec is not bypassing the error mapper. Every failure caught
|
|
34
|
+
* here goes through the client's own `mapError` first, so the port's signals
|
|
35
|
+
* carry the server's `problem+json` fields and a `cause`, and a status the
|
|
36
|
+
* port has no signal for still leaves this subpath as a typed `WasError`.
|
|
27
37
|
*/
|
|
28
38
|
import type { WasClient } from '../WasClient.js';
|
|
29
39
|
import { KEY_EPOCH_HEADER } from '../internal/conditional.js';
|
|
@@ -37,17 +47,17 @@ import type { WasSyncPort } from './types.js';
|
|
|
37
47
|
*/
|
|
38
48
|
export { KEY_EPOCH_HEADER };
|
|
39
49
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `
|
|
50
|
+
* Parses a quoted strong `ETag` into its numeric revision: the decimal
|
|
51
|
+
* integer after the LAST `.` inside the quotes (`"3mJr7AoUXx2.3"` to `3`).
|
|
52
|
+
* Returns `undefined` when the header is absent, has no `.`, or the trailing
|
|
53
|
+
* segment is anything other than a run of digits.
|
|
43
54
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* `undefined` when the header is absent or non-numeric (no such revision yet).
|
|
55
|
+
* The quoted string as a whole is opaque -- it also carries a per-record
|
|
56
|
+
* generation marker ahead of the version, minted once and kept for the
|
|
57
|
+
* record's life, so this is one-way: there is no `formatEtag` to build a
|
|
58
|
+
* validator back out of a bare revision number. Always echo the `etag` a read
|
|
59
|
+
* or write returned back verbatim for `ifMatch`/`ifNoneMatch`; this helper
|
|
60
|
+
* only reads the revision out of it for comparison or display.
|
|
51
61
|
*
|
|
52
62
|
* @param etag {string | null | undefined}
|
|
53
63
|
* @returns {number | undefined}
|
package/dist/sync/port.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../src/sync/port.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../src/sync/port.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EACL,gBAAgB,EAGjB,MAAM,4BAA4B,CAAA;AAWnC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,KAAK,EAAqB,WAAW,EAAY,MAAM,YAAY,CAAA;AAE1E;;;;;GAKG;AACH,OAAO,EAAE,gBAAgB,EAAE,CAAA;AAwG3B;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW7E;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,GAAG,EACH,OAAO,EACP,YAAY,EACZ,UAAU,EACV,aAAqB,EACtB,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,KAAK,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,GAAG,WAAW,CAiNd"}
|
package/dist/sync/port.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KEY_EPOCH_HEADER, readEtag, writeHeaders } from '../internal/conditional.js';
|
|
2
2
|
import { resourceMeta, resourcePath } from '../internal/paths.js';
|
|
3
|
-
import {
|
|
3
|
+
import { mapError, NotFoundError, PreconditionFailedError, WasSyncAuthError, WasSyncConflictError, WasSyncNotFoundError } from '../errors.js';
|
|
4
4
|
/**
|
|
5
5
|
* The request header the server reads a content write's key-epoch id from,
|
|
6
6
|
* stamping it onto the Resource's metadata. Defined next to the header
|
|
@@ -28,49 +28,87 @@ function isAuthStatus(status) {
|
|
|
28
28
|
return status === 401 || status === 403 || status === 404;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
31
|
+
* Carries a mapped error's `problem+json` fields onto the port signal built
|
|
32
|
+
* from it. The `cause` is the raw transport error `mapError` recorded, or the
|
|
33
|
+
* mapped error itself when there was none. A port signal is a narrowing of the
|
|
34
|
+
* classification `mapError` already made, so it must not lose what the server
|
|
35
|
+
* said.
|
|
36
|
+
*
|
|
37
|
+
* @param mapped {WasError} the classified error
|
|
38
|
+
* @returns {WasErrorOptions}
|
|
39
|
+
*/
|
|
40
|
+
function signalOptions(mapped) {
|
|
41
|
+
return {
|
|
42
|
+
status: mapped.status,
|
|
43
|
+
type: mapped.type,
|
|
44
|
+
title: mapped.title,
|
|
45
|
+
details: mapped.details,
|
|
46
|
+
requestUrl: mapped.requestUrl,
|
|
47
|
+
cause: mapped.cause ?? mapped
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a mapped error is already one of the port's own signals, which a
|
|
52
|
+
* nested port call (the write-ack re-read inside a write) can raise through a
|
|
53
|
+
* write's catch block. Re-wrapping one would drop its cause for no gain.
|
|
54
|
+
*
|
|
55
|
+
* @param mapped {WasError}
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
function isPortSignal(mapped) {
|
|
59
|
+
return (mapped instanceof WasSyncConflictError ||
|
|
60
|
+
mapped instanceof WasSyncNotFoundError ||
|
|
61
|
+
mapped instanceof WasSyncAuthError);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Maps a caught write error onto the port's typed signals, rethrowing anything
|
|
65
|
+
* else as the classified {@link WasError} the client's own `mapError` builds
|
|
66
|
+
* (so a status outside this list -- a `500`, a `507` quota-exceeded -- still
|
|
67
|
+
* leaves the sync subpath typed and carrying the server's problem details).
|
|
68
|
+
* A rejected precondition becomes a {@link WasSyncConflictError} for every
|
|
69
|
+
* write. `notFound` opts in to the not-found mapping, which only
|
|
70
|
+
* `deleteContent` performs (an already-gone target is a settled outcome for a
|
|
71
|
+
* delete, but a hard error for a content or metadata write). `authErrors` is
|
|
72
|
+
* the port's `mapAuthErrors` option: it maps `401` / `403` / the masked `404`
|
|
73
|
+
* to a {@link WasSyncAuthError}. `notFound` is checked first, so a port that
|
|
74
|
+
* asked for both still gets the delete-specific signal.
|
|
39
75
|
*
|
|
40
76
|
* @param err {unknown} the caught error
|
|
41
77
|
* @param [options] {object}
|
|
42
|
-
* @param [options.notFound] {boolean} map
|
|
78
|
+
* @param [options.notFound] {boolean} map a not-found to
|
|
79
|
+
* {@link WasSyncNotFoundError}
|
|
43
80
|
* @param [options.authErrors] {boolean} map `401`/`403`/`404` to
|
|
44
81
|
* {@link WasSyncAuthError}
|
|
45
82
|
* @returns {never} always throws
|
|
46
83
|
*/
|
|
47
84
|
function mapWriteError(err, { notFound = false, authErrors = false } = {}) {
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
throw
|
|
85
|
+
const mapped = mapError(err);
|
|
86
|
+
if (isPortSignal(mapped)) {
|
|
87
|
+
throw mapped;
|
|
88
|
+
}
|
|
89
|
+
if (notFound && mapped instanceof NotFoundError) {
|
|
90
|
+
throw new WasSyncNotFoundError(mapped.message, signalOptions(mapped));
|
|
51
91
|
}
|
|
52
|
-
if (
|
|
53
|
-
throw new WasSyncConflictError();
|
|
92
|
+
if (mapped instanceof PreconditionFailedError) {
|
|
93
|
+
throw new WasSyncConflictError(mapped.message, signalOptions(mapped));
|
|
54
94
|
}
|
|
55
|
-
if (authErrors && isAuthStatus(status)) {
|
|
56
|
-
throw new WasSyncAuthError(status);
|
|
95
|
+
if (authErrors && isAuthStatus(mapped.status)) {
|
|
96
|
+
throw new WasSyncAuthError(mapped.status, signalOptions(mapped));
|
|
57
97
|
}
|
|
58
|
-
throw
|
|
98
|
+
throw mapped;
|
|
59
99
|
}
|
|
60
100
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `
|
|
101
|
+
* Parses a quoted strong `ETag` into its numeric revision: the decimal
|
|
102
|
+
* integer after the LAST `.` inside the quotes (`"3mJr7AoUXx2.3"` to `3`).
|
|
103
|
+
* Returns `undefined` when the header is absent, has no `.`, or the trailing
|
|
104
|
+
* segment is anything other than a run of digits.
|
|
64
105
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Parses a quoted strong `ETag` (`"3"`) into its numeric revision, or
|
|
73
|
-
* `undefined` when the header is absent or non-numeric (no such revision yet).
|
|
106
|
+
* The quoted string as a whole is opaque -- it also carries a per-record
|
|
107
|
+
* generation marker ahead of the version, minted once and kept for the
|
|
108
|
+
* record's life, so this is one-way: there is no `formatEtag` to build a
|
|
109
|
+
* validator back out of a bare revision number. Always echo the `etag` a read
|
|
110
|
+
* or write returned back verbatim for `ifMatch`/`ifNoneMatch`; this helper
|
|
111
|
+
* only reads the revision out of it for comparison or display.
|
|
74
112
|
*
|
|
75
113
|
* @param etag {string | null | undefined}
|
|
76
114
|
* @returns {number | undefined}
|
|
@@ -79,8 +117,31 @@ export function parseEtag(etag) {
|
|
|
79
117
|
if (!etag) {
|
|
80
118
|
return undefined;
|
|
81
119
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
120
|
+
const unquoted = etag.replace(/"/g, '');
|
|
121
|
+
const lastDot = unquoted.lastIndexOf('.');
|
|
122
|
+
if (lastDot === -1) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const versionPart = unquoted.slice(lastDot + 1);
|
|
126
|
+
return /^\d+$/.test(versionPart) ? Number(versionPart) : undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Reads a response's `ETag` together with the revision parsed out of it, the
|
|
130
|
+
* one place the two are paired. `etag` is absent when the response carried no
|
|
131
|
+
* validator (a backend that does not version the resource); `version` is
|
|
132
|
+
* absent whenever `etag` is, and also when the validator does not end in a
|
|
133
|
+
* revision number. Callers decide what an absent value means for them.
|
|
134
|
+
*
|
|
135
|
+
* @param response {HttpResponse}
|
|
136
|
+
* @returns {{ etag?: string, version?: number }}
|
|
137
|
+
*/
|
|
138
|
+
function versionedEtag(response) {
|
|
139
|
+
const etag = readEtag(response);
|
|
140
|
+
const version = parseEtag(etag);
|
|
141
|
+
return {
|
|
142
|
+
...(etag !== undefined && { etag }),
|
|
143
|
+
...(version !== undefined && { version })
|
|
144
|
+
};
|
|
84
145
|
}
|
|
85
146
|
/**
|
|
86
147
|
* Builds a {@link WasSyncPort} bound to one Space + Collection, backed by the
|
|
@@ -136,31 +197,36 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
136
197
|
});
|
|
137
198
|
}
|
|
138
199
|
catch (err) {
|
|
139
|
-
const
|
|
200
|
+
const mapped = mapError(err);
|
|
140
201
|
// A read's `404` stays "absent or tombstoned" even under
|
|
141
202
|
// `mapAuthErrors`: it is a modeled outcome here, and the callers read it
|
|
142
203
|
// as deletion-wins.
|
|
143
|
-
if (
|
|
204
|
+
if (mapped instanceof NotFoundError) {
|
|
144
205
|
return null;
|
|
145
206
|
}
|
|
146
|
-
if (mapAuthErrors && isAuthStatus(status)) {
|
|
147
|
-
throw new WasSyncAuthError(status);
|
|
207
|
+
if (mapAuthErrors && isAuthStatus(mapped.status)) {
|
|
208
|
+
throw new WasSyncAuthError(mapped.status, signalOptions(mapped));
|
|
148
209
|
}
|
|
149
|
-
throw
|
|
210
|
+
throw mapped;
|
|
150
211
|
}
|
|
212
|
+
const { etag, version } = versionedEtag(response);
|
|
151
213
|
return {
|
|
152
|
-
version:
|
|
214
|
+
version: version ?? 0,
|
|
215
|
+
etag,
|
|
153
216
|
updatedAt: UNKNOWN_UPDATED_AT,
|
|
154
217
|
data: response.data
|
|
155
218
|
};
|
|
156
219
|
};
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
220
|
+
/**
|
|
221
|
+
* The acked {@link WriteAck} of a write response. Taken from the response's
|
|
222
|
+
* own `ETag` only: a re-read after the fact could return a concurrent
|
|
223
|
+
* writer's validator as this write's ack. A response with no `ETag` acks
|
|
224
|
+
* `version: 0` and no `etag`, the shape of a backend that does not version
|
|
225
|
+
* resources.
|
|
226
|
+
*/
|
|
227
|
+
const writeAck = (response) => {
|
|
228
|
+
const { etag, version } = versionedEtag(response);
|
|
229
|
+
return { version: version ?? 0, etag };
|
|
164
230
|
};
|
|
165
231
|
return {
|
|
166
232
|
async query({ checkpoint, limit }) {
|
|
@@ -171,11 +237,11 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
171
237
|
// The pull path is where revoked access surfaces reliably: unlike a
|
|
172
238
|
// read or a delete, a `404` on the collection's own query endpoint has
|
|
173
239
|
// no benign reading once the collection is known to exist.
|
|
174
|
-
const
|
|
175
|
-
if (mapAuthErrors && isAuthStatus(status)) {
|
|
176
|
-
throw new WasSyncAuthError(status);
|
|
240
|
+
const mapped = mapError(err);
|
|
241
|
+
if (mapAuthErrors && isAuthStatus(mapped.status)) {
|
|
242
|
+
throw new WasSyncAuthError(mapped.status, signalOptions(mapped));
|
|
177
243
|
}
|
|
178
|
-
throw
|
|
244
|
+
throw mapped;
|
|
179
245
|
}
|
|
180
246
|
},
|
|
181
247
|
async putContent({ id, data, ifMatch, ifNoneMatch, epoch }) {
|
|
@@ -190,7 +256,7 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
190
256
|
epoch
|
|
191
257
|
})
|
|
192
258
|
});
|
|
193
|
-
return
|
|
259
|
+
return writeAck(response);
|
|
194
260
|
}
|
|
195
261
|
catch (err) {
|
|
196
262
|
mapWriteError(err, { authErrors: mapAuthErrors });
|
|
@@ -204,7 +270,7 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
204
270
|
method: 'DELETE',
|
|
205
271
|
headers: writeHeaders({ precondition: { ifMatch } })
|
|
206
272
|
});
|
|
207
|
-
return
|
|
273
|
+
return writeAck(response);
|
|
208
274
|
}
|
|
209
275
|
catch (err) {
|
|
210
276
|
// Under `mapAuthErrors` a delete's `404` is an idempotent success: the
|
|
@@ -213,10 +279,11 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
213
279
|
// holds and the batch must not be retried forever. A masked
|
|
214
280
|
// authorization `404` is swallowed with it -- indistinguishable by
|
|
215
281
|
// design -- but revoked access still surfaces on the next `query`.
|
|
216
|
-
|
|
282
|
+
const mapped = mapError(err);
|
|
283
|
+
if (mapAuthErrors && mapped instanceof NotFoundError) {
|
|
217
284
|
return undefined;
|
|
218
285
|
}
|
|
219
|
-
mapWriteError(
|
|
286
|
+
mapWriteError(mapped, {
|
|
220
287
|
notFound: !mapAuthErrors,
|
|
221
288
|
authErrors: mapAuthErrors
|
|
222
289
|
});
|
|
@@ -236,7 +303,8 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
236
303
|
json: custom === undefined ? {} : { custom },
|
|
237
304
|
headers: writeHeaders({ precondition: { ifMatch, ifNoneMatch } })
|
|
238
305
|
});
|
|
239
|
-
|
|
306
|
+
const { etag, version } = versionedEtag(response);
|
|
307
|
+
return etag !== undefined ? { version: version ?? 0, etag } : undefined;
|
|
240
308
|
}
|
|
241
309
|
catch (err) {
|
|
242
310
|
mapWriteError(err, { authErrors: mapAuthErrors });
|
|
@@ -256,21 +324,21 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
256
324
|
}
|
|
257
325
|
// Metadata (best-effort): the `/meta` body carries the server-managed
|
|
258
326
|
// `updatedAt`, the creator DID, the key-epoch id, and the user-writable
|
|
259
|
-
// `custom`, plus its own `metaVersion` ETag. A resource with
|
|
260
|
-
// yet 404s here; only a hard error propagates.
|
|
327
|
+
// `custom`, plus its own `metaVersion`/`metaEtag` ETag. A resource with
|
|
328
|
+
// no metadata yet 404s here; only a hard error propagates.
|
|
261
329
|
const meta = await metaRead;
|
|
262
330
|
if (!meta.ok) {
|
|
263
|
-
const
|
|
331
|
+
const mapped = mapError(meta.err);
|
|
264
332
|
// A `/meta` `404` is routine (the resource has no metadata document
|
|
265
333
|
// yet), so it stays benign under `mapAuthErrors` -- only `401`/`403`
|
|
266
334
|
// map there.
|
|
267
|
-
if (
|
|
335
|
+
if (mapped instanceof NotFoundError) {
|
|
268
336
|
return master;
|
|
269
337
|
}
|
|
270
|
-
if (mapAuthErrors && isAuthStatus(status)) {
|
|
271
|
-
throw new WasSyncAuthError(status);
|
|
338
|
+
if (mapAuthErrors && isAuthStatus(mapped.status)) {
|
|
339
|
+
throw new WasSyncAuthError(mapped.status, signalOptions(mapped));
|
|
272
340
|
}
|
|
273
|
-
throw
|
|
341
|
+
throw mapped;
|
|
274
342
|
}
|
|
275
343
|
const metaBody = meta.response.data;
|
|
276
344
|
if (metaBody?.updatedAt) {
|
|
@@ -285,7 +353,12 @@ export function createWasSyncPort({ was, spaceId, collectionId, capability, mapA
|
|
|
285
353
|
if (metaBody?.custom !== undefined) {
|
|
286
354
|
master.custom = metaBody.custom;
|
|
287
355
|
}
|
|
288
|
-
|
|
356
|
+
// The validator is kept whenever the server sent one, so a later
|
|
357
|
+
// `putMeta` can pin on it even when no revision number parses out of it.
|
|
358
|
+
const { etag: metaEtag, version: metaVersion } = versionedEtag(meta.response);
|
|
359
|
+
if (metaEtag !== undefined) {
|
|
360
|
+
master.metaEtag = metaEtag;
|
|
361
|
+
}
|
|
289
362
|
if (metaVersion !== undefined) {
|
|
290
363
|
master.metaVersion = metaVersion;
|
|
291
364
|
}
|
package/dist/sync/port.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/sync/port.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/sync/port.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `err.name` predicates that classify the errors a replication path can
|
|
6
|
+
* meet: the port's two wire signals (`WasSyncConflictError` /
|
|
7
|
+
* `WasSyncNotFoundError`), its opt-in revoked-access signal
|
|
8
|
+
* (`WasSyncAuthError`), and the cipher's stale-descriptor signal
|
|
9
|
+
* (`UnknownEpochError`).
|
|
10
|
+
*
|
|
11
|
+
* They live beside the classes that assign the names they match (`errors.ts`)
|
|
12
|
+
* because every one of those errors is raised inside a seam the consuming app
|
|
13
|
+
* injects -- the port for the wire signals, the caller's `DocCipher` for the
|
|
14
|
+
* unknown epoch -- and that seam can resolve to a SECOND copy of this package
|
|
15
|
+
* (a `link:` dev setup, a dedupe miss through a dependency tree). So the match
|
|
16
|
+
* is on the `name` string alone and never `instanceof`, which cannot see across
|
|
17
|
+
* that boundary. The cost of a miss is silent and expensive: every push 412
|
|
18
|
+
* becomes a fatal cycle error, and a create-loss re-mint rethrows instead of
|
|
19
|
+
* re-minting. Each class assigns its `name` explicitly, which is what makes the
|
|
20
|
+
* string a contract (`decisions/0001-cross-package-errors-match-by-name.md`).
|
|
21
|
+
*
|
|
22
|
+
* Each returns a plain boolean rather than a type guard: the guard would narrow
|
|
23
|
+
* to this copy's class, the very identity the rule declines to depend on. A
|
|
24
|
+
* caller reading a property after the match (`err.status` on an auth error)
|
|
25
|
+
* reads it off the value itself.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Whether an error is the replication port's rejected-conditional-write signal
|
|
29
|
+
* (`WasSyncConflictError`, HTTP 412): a lost-update `If-Match` mismatch, or a
|
|
30
|
+
* create-if-absent whose target already exists. The push loop's one settle-and-
|
|
31
|
+
* reconcile branch; everything else propagates to the engine's backoff.
|
|
32
|
+
*
|
|
33
|
+
* @param err {unknown}
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
export declare function isSyncConflictError(err: unknown): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether an error is the replication port's absent-target signal
|
|
39
|
+
* (`WasSyncNotFoundError`, HTTP 404). On a delete that is a settled outcome
|
|
40
|
+
* -- already gone, or the write never reached the server -- not a conflict.
|
|
41
|
+
*
|
|
42
|
+
* @param err {unknown}
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export declare function isSyncNotFoundError(err: unknown): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether an error is the replication port's refused-authorization signal
|
|
48
|
+
* (`WasSyncAuthError`), raised only by a port built with `mapAuthErrors`. It
|
|
49
|
+
* covers `401`, `403`, and the `404` a WAS server returns when it masks an
|
|
50
|
+
* authorization failure as "not found", so a caller that needs the three apart
|
|
51
|
+
* reads `status` off the matched value as a plain property.
|
|
52
|
+
*
|
|
53
|
+
* @param err {unknown}
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export declare function isSyncAuthError(err: unknown): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether an error is the cipher's unknown-epoch signal (`UnknownEpochError`):
|
|
59
|
+
* an envelope naming recipient key ids whose epoch the descriptor this reader
|
|
60
|
+
* holds does not list at all. Distinct from a key the reader simply does not
|
|
61
|
+
* have (`KeyUnwrapError`), which re-reading the descriptor cannot fix.
|
|
62
|
+
*
|
|
63
|
+
* @param err {unknown}
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
export declare function isUnknownEpochError(err: unknown): boolean;
|
|
67
|
+
//# sourceMappingURL=predicates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"predicates.d.ts","sourceRoot":"","sources":["../../src/sync/predicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAaH;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAErD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEzD"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `err.name` predicates that classify the errors a replication path can
|
|
6
|
+
* meet: the port's two wire signals (`WasSyncConflictError` /
|
|
7
|
+
* `WasSyncNotFoundError`), its opt-in revoked-access signal
|
|
8
|
+
* (`WasSyncAuthError`), and the cipher's stale-descriptor signal
|
|
9
|
+
* (`UnknownEpochError`).
|
|
10
|
+
*
|
|
11
|
+
* They live beside the classes that assign the names they match (`errors.ts`)
|
|
12
|
+
* because every one of those errors is raised inside a seam the consuming app
|
|
13
|
+
* injects -- the port for the wire signals, the caller's `DocCipher` for the
|
|
14
|
+
* unknown epoch -- and that seam can resolve to a SECOND copy of this package
|
|
15
|
+
* (a `link:` dev setup, a dedupe miss through a dependency tree). So the match
|
|
16
|
+
* is on the `name` string alone and never `instanceof`, which cannot see across
|
|
17
|
+
* that boundary. The cost of a miss is silent and expensive: every push 412
|
|
18
|
+
* becomes a fatal cycle error, and a create-loss re-mint rethrows instead of
|
|
19
|
+
* re-minting. Each class assigns its `name` explicitly, which is what makes the
|
|
20
|
+
* string a contract (`decisions/0001-cross-package-errors-match-by-name.md`).
|
|
21
|
+
*
|
|
22
|
+
* Each returns a plain boolean rather than a type guard: the guard would narrow
|
|
23
|
+
* to this copy's class, the very identity the rule declines to depend on. A
|
|
24
|
+
* caller reading a property after the match (`err.status` on an auth error)
|
|
25
|
+
* reads it off the value itself.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Reads the `name` off an unknown caught value, without raising on a nullish
|
|
29
|
+
* or non-object rejection.
|
|
30
|
+
*
|
|
31
|
+
* @param err {unknown} the caught error
|
|
32
|
+
* @returns {unknown}
|
|
33
|
+
*/
|
|
34
|
+
function nameOf(err) {
|
|
35
|
+
return err?.name;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether an error is the replication port's rejected-conditional-write signal
|
|
39
|
+
* (`WasSyncConflictError`, HTTP 412): a lost-update `If-Match` mismatch, or a
|
|
40
|
+
* create-if-absent whose target already exists. The push loop's one settle-and-
|
|
41
|
+
* reconcile branch; everything else propagates to the engine's backoff.
|
|
42
|
+
*
|
|
43
|
+
* @param err {unknown}
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export function isSyncConflictError(err) {
|
|
47
|
+
return nameOf(err) === 'WasSyncConflictError';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether an error is the replication port's absent-target signal
|
|
51
|
+
* (`WasSyncNotFoundError`, HTTP 404). On a delete that is a settled outcome
|
|
52
|
+
* -- already gone, or the write never reached the server -- not a conflict.
|
|
53
|
+
*
|
|
54
|
+
* @param err {unknown}
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function isSyncNotFoundError(err) {
|
|
58
|
+
return nameOf(err) === 'WasSyncNotFoundError';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether an error is the replication port's refused-authorization signal
|
|
62
|
+
* (`WasSyncAuthError`), raised only by a port built with `mapAuthErrors`. It
|
|
63
|
+
* covers `401`, `403`, and the `404` a WAS server returns when it masks an
|
|
64
|
+
* authorization failure as "not found", so a caller that needs the three apart
|
|
65
|
+
* reads `status` off the matched value as a plain property.
|
|
66
|
+
*
|
|
67
|
+
* @param err {unknown}
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function isSyncAuthError(err) {
|
|
71
|
+
return nameOf(err) === 'WasSyncAuthError';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether an error is the cipher's unknown-epoch signal (`UnknownEpochError`):
|
|
75
|
+
* an envelope naming recipient key ids whose epoch the descriptor this reader
|
|
76
|
+
* holds does not list at all. Distinct from a key the reader simply does not
|
|
77
|
+
* have (`KeyUnwrapError`), which re-reading the descriptor cannot fix.
|
|
78
|
+
*
|
|
79
|
+
* @param err {unknown}
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export function isUnknownEpochError(err) {
|
|
83
|
+
return nameOf(err) === 'UnknownEpochError';
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=predicates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"predicates.js","sourceRoot":"","sources":["../../src/sync/predicates.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;GAMG;AACH,SAAS,MAAM,CAAC,GAAY;IAC1B,OAAQ,GAAiC,EAAE,IAAI,CAAA;AACjD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,sBAAsB,CAAA;AAC/C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,sBAAsB,CAAA;AAC/C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,kBAAkB,CAAA;AAC3C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,mBAAmB,CAAA;AAC5C,CAAC"}
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* here under replication-facing names so a sync consumer imports one module.
|
|
10
10
|
* `WasSyncPort` and `MasterState` are the injectable access seam the change
|
|
11
11
|
* engine depends on, and `DocCipher` is the encrypt/decrypt seam a per-
|
|
12
|
-
* collection cipher implements.
|
|
12
|
+
* collection cipher implements. `SyncStatus` is the closed status vocabulary
|
|
13
|
+
* every replication driver reports one feed's state through.
|
|
13
14
|
*/
|
|
14
15
|
import type { ChangeDocument, ChangesCheckpoint, ChangesPage } from '@interop/storage-core';
|
|
15
16
|
import type { Json } from '../types.js';
|
|
@@ -24,12 +25,17 @@ export type { Json };
|
|
|
24
25
|
export type SyncCheckpoint = ChangesCheckpoint;
|
|
25
26
|
/**
|
|
26
27
|
* One document as it travels on the `changes` feed wire: `id` is the WAS
|
|
27
|
-
* resource id, `version`
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* resource id, `version` its monotonic content revision number, and the
|
|
29
|
+
* stored body is under `data`. `version` is for comparison/ordering only and
|
|
30
|
+
* is not an `ifMatch` value on its own; the server's `ETag` is an opaque
|
|
31
|
+
* string that embeds more than the version. The feed carries the validators
|
|
32
|
+
* themselves: `etag` (content) and `metaEtag` (the `/meta` object), quoted
|
|
33
|
+
* exactly as the server emits them, so a puller passes one back verbatim as a
|
|
34
|
+
* conditional write's `ifMatch` without a {@link WasSyncPort.get} first. A
|
|
35
|
+
* tombstone carries `_deleted: true` with no `data`. This is the shared
|
|
36
|
+
* `ChangeDocument` from `@interop/storage-core`; on an encrypted collection
|
|
37
|
+
* `data`/`custom` are the opaque stored envelope, moved verbatim (decrypt is a
|
|
38
|
+
* projection-time concern the engine's `DocCipher` handles, never the port).
|
|
33
39
|
*/
|
|
34
40
|
export type WireDoc = ChangeDocument;
|
|
35
41
|
/**
|
|
@@ -42,20 +48,41 @@ export type SyncPage = ChangesPage;
|
|
|
42
48
|
* The current master state of a single resource, read back for the 412-conflict
|
|
43
49
|
* path ({@link WasSyncPort.get}). An absent or tombstoned resource surfaces as
|
|
44
50
|
* `get` resolving `null`, never as a `MasterState`. `updatedAt`, `metaVersion`,
|
|
45
|
-
* `custom`, `createdBy`, and `epoch` are populated from the
|
|
46
|
-
* document when it exists; a resource with no metadata yet
|
|
47
|
-
* epoch-zero `updatedAt` placeholder (a valid, sortable timestamp --
|
|
48
|
-
* change feed remains the authority on ordering).
|
|
51
|
+
* `metaEtag`, `custom`, `createdBy`, and `epoch` are populated from the
|
|
52
|
+
* resource's `/meta` document when it exists; a resource with no metadata yet
|
|
53
|
+
* reports an epoch-zero `updatedAt` placeholder (a valid, sortable timestamp --
|
|
54
|
+
* the change feed remains the authority on ordering).
|
|
55
|
+
*
|
|
56
|
+
* `etag` and `metaEtag` are the raw, opaque `ETag` validators the content and
|
|
57
|
+
* `/meta` reads returned (absent against a backend that does not version
|
|
58
|
+
* resources) -- pass one back verbatim as a later write's `ifMatch`. `version`
|
|
59
|
+
* and `metaVersion` are the revision numbers parsed out of them, for
|
|
60
|
+
* comparison or display only.
|
|
49
61
|
*/
|
|
50
62
|
export interface MasterState {
|
|
51
63
|
version: number;
|
|
64
|
+
etag?: string;
|
|
52
65
|
updatedAt: string;
|
|
53
66
|
metaVersion?: number;
|
|
67
|
+
metaEtag?: string;
|
|
54
68
|
data?: Json;
|
|
55
69
|
custom?: Json;
|
|
56
70
|
createdBy?: string;
|
|
57
71
|
epoch?: string;
|
|
58
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The acknowledgment a conditional write returns: the new `version` plus the
|
|
75
|
+
* opaque `etag` validator it lives behind, exactly as the server sent it.
|
|
76
|
+
* Pass `etag` back verbatim as a later write's `ifMatch` -- it can no longer
|
|
77
|
+
* be synthesized from `version` alone, since the server's `ETag` also embeds a
|
|
78
|
+
* per-record generation marker ahead of the version. `etag` is absent against
|
|
79
|
+
* a backend that does not version resources; read `version` only to compare
|
|
80
|
+
* or display a revision number.
|
|
81
|
+
*/
|
|
82
|
+
export interface WriteAck {
|
|
83
|
+
version: number;
|
|
84
|
+
etag?: string;
|
|
85
|
+
}
|
|
59
86
|
/**
|
|
60
87
|
* The injected WAS-access seam. `createWasSyncPort` implements this over
|
|
61
88
|
* `@interop/was-client`'s `was.request()` and the `Collection.changes()` feed;
|
|
@@ -77,10 +104,10 @@ export interface WasSyncPort {
|
|
|
77
104
|
}): Promise<SyncPage>;
|
|
78
105
|
/**
|
|
79
106
|
* Conditionally writes the content body verbatim (`PUT /:id`). Pass
|
|
80
|
-
* `ifNoneMatch: true` for create-if-absent, or `ifMatch` (
|
|
81
|
-
*
|
|
82
|
-
* key-epoch id the body was encrypted under
|
|
83
|
-
*
|
|
107
|
+
* `ifNoneMatch: true` for create-if-absent, or `ifMatch` (the opaque `ETag`
|
|
108
|
+
* from a prior read/write, echoed back verbatim) for update-if-unchanged.
|
|
109
|
+
* `epoch` stamps the opaque key-epoch id the body was encrypted under
|
|
110
|
+
* (absent clears any prior stamp). Returns the new {@link WriteAck}. Throws
|
|
84
111
|
* {@link WasSyncConflictError} on `412`.
|
|
85
112
|
*/
|
|
86
113
|
putContent(options: {
|
|
@@ -89,13 +116,14 @@ export interface WasSyncPort {
|
|
|
89
116
|
ifMatch?: string;
|
|
90
117
|
ifNoneMatch?: boolean;
|
|
91
118
|
epoch?: string;
|
|
92
|
-
}): Promise<
|
|
119
|
+
}): Promise<WriteAck>;
|
|
93
120
|
/**
|
|
94
121
|
* Conditionally deletes a resource (writes a tombstone; `DELETE /:id`). Pass
|
|
95
|
-
* `ifMatch` (
|
|
96
|
-
* unchanged. Returns the tombstone's new
|
|
97
|
-
* {@link
|
|
98
|
-
* (already gone -- a settled outcome
|
|
122
|
+
* `ifMatch` (the opaque `ETag` from a prior read/write, echoed back
|
|
123
|
+
* verbatim) to delete only if unchanged. Returns the tombstone's new
|
|
124
|
+
* {@link WriteAck}. Throws {@link WasSyncConflictError} on `412`,
|
|
125
|
+
* {@link WasSyncNotFoundError} on `404` (already gone -- a settled outcome
|
|
126
|
+
* for a delete).
|
|
99
127
|
*
|
|
100
128
|
* Resolves `undefined` instead when the port was built with
|
|
101
129
|
* `mapAuthErrors: true` and the target was already absent: there the delete
|
|
@@ -104,22 +132,22 @@ export interface WasSyncPort {
|
|
|
104
132
|
deleteContent(options: {
|
|
105
133
|
id: string;
|
|
106
134
|
ifMatch?: string;
|
|
107
|
-
}): Promise<
|
|
135
|
+
}): Promise<WriteAck | undefined>;
|
|
108
136
|
/**
|
|
109
137
|
* Conditionally writes the user-writable metadata `custom` (`PUT /:id/meta`).
|
|
110
138
|
* Optional -- present only on a port that syncs metadata. The write fully
|
|
111
139
|
* replaces `custom`, so omitting it writes the CLEARED state (the server
|
|
112
140
|
* clears every property the body leaves out) -- that is how a metadata clear
|
|
113
|
-
* replicates. Returns the new
|
|
114
|
-
*
|
|
115
|
-
*
|
|
141
|
+
* replicates. Returns the new metadata {@link WriteAck}, or `undefined` when
|
|
142
|
+
* the response carried no `ETag`. Throws {@link WasSyncConflictError} on
|
|
143
|
+
* `412`.
|
|
116
144
|
*/
|
|
117
145
|
putMeta?(options: {
|
|
118
146
|
id: string;
|
|
119
147
|
custom?: Json;
|
|
120
148
|
ifMatch?: string;
|
|
121
149
|
ifNoneMatch?: boolean;
|
|
122
|
-
}): Promise<
|
|
150
|
+
}): Promise<WriteAck | undefined>;
|
|
123
151
|
/**
|
|
124
152
|
* Re-reads a single resource's current master state for the 412-conflict
|
|
125
153
|
* assembler. Returns `null` when the resource is genuinely absent OR a
|
|
@@ -169,4 +197,11 @@ export interface DocCipher {
|
|
|
169
197
|
envelope: Json;
|
|
170
198
|
}): Promise<Json>;
|
|
171
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Per-feed replication status, surfaced to the app's state layer. The
|
|
202
|
+
* vocabulary is closed: a consumer may render a status through a lookup keyed
|
|
203
|
+
* on the string, so a widened value would fail at runtime rather than at
|
|
204
|
+
* compile time.
|
|
205
|
+
*/
|
|
206
|
+
export type SyncStatus = 'idle' | 'syncing' | 'synced' | 'error';
|
|
172
207
|
//# sourceMappingURL=types.d.ts.map
|
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":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interop/was-client",
|
|
3
3
|
"description": "A developer-friendly client for Wallet Attached Storage (WAS) servers.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.50.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "pnpm run clear && tsc",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@interop/ezcap": "^7.4.6",
|
|
68
68
|
"@interop/http-client": "^1.0.5",
|
|
69
69
|
"@interop/minimal-cipher": "^7.10.0",
|
|
70
|
-
"@interop/storage-core": "^0.
|
|
70
|
+
"@interop/storage-core": "^0.11.0",
|
|
71
71
|
"@interop/vh-resource-log": "^0.4.1",
|
|
72
72
|
"@interop/x25519-key-agreement-key": "^5.3.0",
|
|
73
73
|
"@noble/hashes": "^2.3.0",
|