@interop/was-client 0.92.0 → 0.93.1
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 +176 -13
- package/dist/Collection.d.ts +43 -6
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +96 -21
- package/dist/Collection.js.map +1 -1
- package/dist/Resource.d.ts +35 -2
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +49 -5
- package/dist/Resource.js.map +1 -1
- package/dist/Space.d.ts +85 -1
- package/dist/Space.d.ts.map +1 -1
- package/dist/Space.js +160 -1
- package/dist/Space.js.map +1 -1
- package/dist/codec.d.ts +26 -0
- package/dist/codec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.d.ts +20 -1
- package/dist/edv/EdvCodec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.js +161 -85
- package/dist/edv/EdvCodec.js.map +1 -1
- package/dist/edv/WasTransport.d.ts.map +1 -1
- package/dist/edv/WasTransport.js +10 -7
- package/dist/edv/WasTransport.js.map +1 -1
- package/dist/errors.d.ts +13 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +98 -49
- package/dist/errors.js.map +1 -1
- package/dist/identity/agents.d.ts +8 -0
- package/dist/identity/agents.d.ts.map +1 -1
- package/dist/identity/agents.js +13 -1
- package/dist/identity/agents.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/blob.d.ts +16 -1
- package/dist/internal/blob.d.ts.map +1 -1
- package/dist/internal/blob.js +34 -1
- package/dist/internal/blob.js.map +1 -1
- package/dist/internal/codec.d.ts +3 -2
- package/dist/internal/codec.d.ts.map +1 -1
- package/dist/internal/codec.js +26 -3
- package/dist/internal/codec.js.map +1 -1
- package/dist/internal/content.d.ts +65 -10
- package/dist/internal/content.d.ts.map +1 -1
- package/dist/internal/content.js +88 -11
- package/dist/internal/content.js.map +1 -1
- package/dist/internal/describe.d.ts.map +1 -1
- package/dist/internal/describe.js +6 -1
- package/dist/internal/describe.js.map +1 -1
- package/dist/internal/paths.d.ts +17 -11
- package/dist/internal/paths.d.ts.map +1 -1
- package/dist/internal/paths.js +23 -11
- package/dist/internal/paths.js.map +1 -1
- package/dist/sync/envelope.d.ts +11 -3
- package/dist/sync/envelope.d.ts.map +1 -1
- package/dist/sync/envelope.js +11 -3
- package/dist/sync/envelope.js.map +1 -1
- package/dist/sync/port.d.ts.map +1 -1
- package/dist/sync/port.js +6 -10
- package/dist/sync/port.js.map +1 -1
- package/dist/sync/types.d.ts +5 -8
- package/dist/sync/types.d.ts.map +1 -1
- package/dist/types.d.ts +12 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- [Conditional writes (optimistic concurrency)](#conditional-writes-optimistic-concurrency)
|
|
27
27
|
- [Storage introspection: backends and quotas](#storage-introspection-backends-and-quotas)
|
|
28
28
|
- [Registering a Bring-Your-Own-Storage backend](#registering-a-bring-your-own-storage-backend)
|
|
29
|
+
- [Replica registration](#replica-registration)
|
|
29
30
|
- [Encrypted collections (EDV-over-WAS): pass-through encryption via the WAS client (recommended)](#encrypted-collections-edv-over-was-pass-through-encryption-via-the-was-client-recommended)
|
|
30
31
|
- [Cross-replica sync](#cross-replica-sync)
|
|
31
32
|
- [Resource logs (co-managed key resources)](#resource-logs-co-managed-key-resources)
|
|
@@ -432,7 +433,12 @@ await collection.resource('vc-1').delete() // idempotent
|
|
|
432
433
|
|
|
433
434
|
Writes detect the payload: a plain object/array is sent as JSON; a
|
|
434
435
|
`Blob`/`Uint8Array`/`Buffer` is sent as binary, with the content-type taken from
|
|
435
|
-
`options.contentType`, the `Blob.type`, or `application/octet-stream`.
|
|
436
|
+
`options.contentType`, the `Blob.type`, or `application/octet-stream`. A
|
|
437
|
+
`ReadableStream<Uint8Array>` is accepted only by an encrypted collection (see
|
|
438
|
+
[Encrypted collections](#encrypted-collections-edv-over-was-pass-through-encryption-via-the-was-client-recommended)).
|
|
439
|
+
A plaintext collection refuses it with `ValidationError`. A plaintext resource
|
|
440
|
+
has no chunked representation, and a signed request carries a `Digest` of its
|
|
441
|
+
whole body. Buffer the stream into a `Blob` there.
|
|
436
442
|
|
|
437
443
|
```ts
|
|
438
444
|
// JSON
|
|
@@ -452,13 +458,17 @@ Reads auto-parse: `get()` returns a parsed object for a JSON content-type and a
|
|
|
452
458
|
`Blob` otherwise; `getText()` / `getBytes()` are explicit escape hatches.
|
|
453
459
|
`getWithEtag()` pairs the decoded value with its ETag validator, and
|
|
454
460
|
`getWithEtag({ as: 'text' })` projects it to text (decrypting where `getText()`
|
|
455
|
-
would not).
|
|
456
|
-
|
|
457
|
-
A
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
461
|
+
would not). `getStream()` resolves `{ stream, contentType?, etag? }`, or `null`
|
|
462
|
+
on a miss. It runs the codec like `get()`, so it decrypts on an encrypted
|
|
463
|
+
collection. A JSON body is re-serialized, so it is semantically identical JSON
|
|
464
|
+
but not guaranteed byte-identical.
|
|
465
|
+
|
|
466
|
+
A write value is a JSON object/array or binary (`Blob`/`Uint8Array`, or a
|
|
467
|
+
`ReadableStream<Uint8Array>` on an encrypted collection) -- the `ResourceData`
|
|
468
|
+
type. A top-level JSON primitive (a bare `string`, `number`, `boolean`, or
|
|
469
|
+
`null`) is **not** accepted; it is a compile-time error. To store one, either
|
|
470
|
+
wrap it in an object (`put('greeting', { value: 'hello' })`) or write it as
|
|
471
|
+
binary via a `Blob`:
|
|
462
472
|
|
|
463
473
|
```ts
|
|
464
474
|
await collection.put('greeting', new Blob(['hello'], { type: 'text/plain' }))
|
|
@@ -627,11 +637,12 @@ collection listings; updating one updates the other.
|
|
|
627
637
|
A Collection's configuration (`name`, `backend`, `encryption`, `generator`, read
|
|
628
638
|
by [`describe()`](#collections)) and its annotations live in one object at the
|
|
629
639
|
Collection's reserved `/meta` path: server-managed properties (`createdAt` /
|
|
630
|
-
`updatedAt` / `createdBy`) plus a user-writable `custom` object
|
|
631
|
-
`tags`), under one `ETag` validator. `meta()` is that same object
|
|
632
|
-
decoded through the codec; `describe()` does not resolve the codec
|
|
633
|
-
an encrypted collection it reports `custom` as the opaque
|
|
634
|
-
without the endpoint surfaces its 501 as
|
|
640
|
+
`updatedAt` / `createdBy` / `created`) plus a user-writable `custom` object
|
|
641
|
+
(`name` and `tags`), under one `ETag` validator. `meta()` is that same object
|
|
642
|
+
with `custom` decoded through the codec; `describe()` does not resolve the codec
|
|
643
|
+
at all, so on an encrypted collection it reports `custom` as the opaque
|
|
644
|
+
envelope. A server without the endpoint surfaces its 501 as
|
|
645
|
+
`NotImplementedError`.
|
|
635
646
|
|
|
636
647
|
```ts
|
|
637
648
|
const meta = await collection.meta() // CollectionMetadata | null (null on a miss)
|
|
@@ -649,6 +660,12 @@ await collection.setMeta({ custom: { name: 'Vault' } }, { ifMatch: meta?.etag })
|
|
|
649
660
|
await collection.setMeta({ custom: { name: 'Vault' } }, { ifNoneMatch: true })
|
|
650
661
|
```
|
|
651
662
|
|
|
663
|
+
`created` is the write stamp of the write that created the Collection
|
|
664
|
+
(`{ updatedAt, updatedAtCounter, originId }`), kept for the Collection's life. A
|
|
665
|
+
Collection deleted and created again carries the stamp of the later create. No
|
|
666
|
+
write sends it back. A Collection created before the server supported the stamp
|
|
667
|
+
may lack it.
|
|
668
|
+
|
|
652
669
|
A failed precondition throws `PreconditionFailedError` (412). This `/meta` ETag
|
|
653
670
|
is independent of every Resource's versions, but not of the configuration
|
|
654
671
|
members: one validator covers the whole Collection Metadata object, so
|
|
@@ -844,6 +861,125 @@ sends no body on an in-place replace).
|
|
|
844
861
|
> can actually serve bytes depends on the server having a live provider adapter
|
|
845
862
|
> for it. Until then it is registered but inert (`status: 'registered'`).
|
|
846
863
|
|
|
864
|
+
### Replica registration
|
|
865
|
+
|
|
866
|
+
A Space can pull from a copy of itself on another server. The Space controller
|
|
867
|
+
registers that peer Space with this server, and this server then runs a pull
|
|
868
|
+
loop against the peer. Replication is one-way. The peer receives nothing unless
|
|
869
|
+
its own server holds a registration pointing back.
|
|
870
|
+
|
|
871
|
+
A registration is a directed edge. `fromSpace` is the peer Space's canonical
|
|
872
|
+
trailing-slash URL, and `toSpace` is this Space's. `role` is always `source`.
|
|
873
|
+
`capability` is the pull capability: a zcap the controller delegates to this
|
|
874
|
+
server's DID, rooted in the peer Space, with `allowedAction` within `GET` and
|
|
875
|
+
`HEAD`. `collections` lists the Collections to pull. Leave it out to pull every
|
|
876
|
+
Collection.
|
|
877
|
+
|
|
878
|
+
```ts
|
|
879
|
+
// The DID of the server that will pull: the reference server discloses it
|
|
880
|
+
// as `instance.serverDid` in its service description. `instance` is an
|
|
881
|
+
// operator disclosure, so a deployment may leave it out; ask the operator
|
|
882
|
+
// for the DID then.
|
|
883
|
+
const { description } = await was.service()
|
|
884
|
+
const serverDid = description.instance?.serverDid
|
|
885
|
+
if (serverDid === undefined) {
|
|
886
|
+
throw new Error('This server does not disclose its DID.')
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// The pull capability: GET-only, rooted in the peer Space, delegated to that
|
|
890
|
+
// DID.
|
|
891
|
+
const capability = await peerWas.space(peerSpaceId).grant({
|
|
892
|
+
to: serverDid,
|
|
893
|
+
actions: ['GET']
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
const registration = await space.registerReplica({
|
|
897
|
+
id: 'peer-1', // client-chosen and URL-safe
|
|
898
|
+
fromSpace: `https://peer.example/space/${peerSpaceId}/`,
|
|
899
|
+
toSpace: `https://was.example/space/${space.id}/`,
|
|
900
|
+
capability,
|
|
901
|
+
collections: [{ id: 'credentials' }], // omit to pull every Collection
|
|
902
|
+
role: 'source'
|
|
903
|
+
}) // the stored ReplicaRegistration
|
|
904
|
+
|
|
905
|
+
const listing = await space.replicas() // { url, totalItems, items } | null
|
|
906
|
+
const one = await space.replica('peer-1') // ReplicaRegistration | null
|
|
907
|
+
|
|
908
|
+
const status = await space.replicaStatus('peer-1') // ReplicaStatus | null
|
|
909
|
+
// status.state: 'idle' | 'pulling' | 'backing-off' | 'stalled'
|
|
910
|
+
for (const entry of status?.collections ?? []) {
|
|
911
|
+
if (entry.state === 'stalled') {
|
|
912
|
+
// entry.stall: { reason, since, detail? }
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// The data already pulled under the registration stays. 'not-found' is the
|
|
917
|
+
// server's masked 404: absent, or the caller is not the controller.
|
|
918
|
+
const { outcome } = await space.deregisterReplica('peer-1') // 'deleted' | 'not-found'
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
`replicaStatus()` reports the pull loop's runtime state. The response is
|
|
922
|
+
uncacheable and is not part of the registration record. Each Collection entry
|
|
923
|
+
has its own `state` (`synced`, `syncing`, `stalled`, or `skipped`). A stalled
|
|
924
|
+
Collection keeps its checkpoint until the cause is removed, and the other
|
|
925
|
+
Collections continue. Its `stall.reason` is one of:
|
|
926
|
+
|
|
927
|
+
- `clock-bound` -- a received write stamp is dated further ahead of local time
|
|
928
|
+
than the server allows. It clears as local time catches up.
|
|
929
|
+
- `fork` -- a history log or an immutable Collection member differs from the
|
|
930
|
+
local one in a way no write order resolves.
|
|
931
|
+
- `quota-exceeded` -- applying the change would exceed a local quota.
|
|
932
|
+
- `unsupported-backend` -- the Collection names a `backend` this server has no
|
|
933
|
+
registration for.
|
|
934
|
+
- `container-refused` -- the local Space or Collection refused the write.
|
|
935
|
+
|
|
936
|
+
Every replica method is controller-only on the server, the reads included, since
|
|
937
|
+
a registration holds the pull capability. A delegated capability gets the
|
|
938
|
+
masked 404. `replicas()`, `replica()`, and `replicaStatus()` then return `null`,
|
|
939
|
+
and `registerReplica()` throws `NotFoundError`. `deregisterReplica()` reports it
|
|
940
|
+
as `{ outcome: 'not-found' }` rather than swallowing it, so a delegated caller
|
|
941
|
+
can tell that nothing was removed. The three reads reserve `null` for that 404.
|
|
942
|
+
A 2xx with no JSON body is a server fault and throws `WasServerError`.
|
|
943
|
+
|
|
944
|
+
`registerReplica()` throws `ValidationError` (400 `invalid-request-body`) for a
|
|
945
|
+
malformed body. Each entry of the error's `problems` carries a `pointer` naming
|
|
946
|
+
the offending member. It throws `ConflictError` (409) for two refusals, told
|
|
947
|
+
apart by the error's `type` URI:
|
|
948
|
+
|
|
949
|
+
- `replica-refused` (`ProblemTypes.REPLICA_REFUSED`) -- the server refuses the
|
|
950
|
+
peer. For example, the peer Space's `controller` or `type` differs from this
|
|
951
|
+
one, the peer advertises no replication, the capability's controller is not
|
|
952
|
+
this server, or a listed Collection's immutable members differ. The cause is
|
|
953
|
+
in the error's `details`.
|
|
954
|
+
- `id-conflict` (`ProblemTypes.ID_CONFLICT`) -- a registration with this `id`
|
|
955
|
+
already exists.
|
|
956
|
+
|
|
957
|
+
```ts
|
|
958
|
+
import { ProblemTypes } from '@interop/was-client'
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
await space.registerReplica(registration)
|
|
962
|
+
} catch (err) {
|
|
963
|
+
if (err instanceof ConflictError && err.type === ProblemTypes.ID_CONFLICT) {
|
|
964
|
+
// already registered under this id
|
|
965
|
+
} else {
|
|
966
|
+
throw err
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
`deregisterReplica()` throws `ConflictError` (`replica-refused`) when the
|
|
972
|
+
registration is the one that maps this Space's did:webvh controller. Removing it
|
|
973
|
+
would leave the Space with no way to follow its controller's log.
|
|
974
|
+
|
|
975
|
+
A peer advertises replication in its service description, under
|
|
976
|
+
`specs['https://w3id.org/pws/replication']` (version `0.1` on the reference
|
|
977
|
+
server). A registration is per-server state. It is not replicated, it is not
|
|
978
|
+
part of an export, and deleting the Space removes it. `space.describe()`
|
|
979
|
+
surfaces the Space's registrations as the read-only `replicas` member, one
|
|
980
|
+
`{ fromSpace, toSpace, role }` summary per peer, with no registration id and no
|
|
981
|
+
capability. The server ignores `replicas` in a write body.
|
|
982
|
+
|
|
847
983
|
### Encrypted collections (EDV-over-WAS): pass-through encryption via the WAS client (recommended)
|
|
848
984
|
|
|
849
985
|
This is the recommended way to use encrypted collections. For the low-level
|
|
@@ -966,6 +1102,21 @@ scope for now):
|
|
|
966
1102
|
`isPendingStub` recognizes it, and `delete()` removes it with its chunks. Tune
|
|
967
1103
|
the threshold and the chunk size with
|
|
968
1104
|
`createEdvEncryption({ maxBlobBytes, chunkSize })`.
|
|
1105
|
+
- **Streams.** A `ReadableStream<Uint8Array>` has no known size, so `add()`
|
|
1106
|
+
always routes it to the chunked-stream path, however short it is. It is
|
|
1107
|
+
encrypted and written as it is read, without buffering. `put(id, stream)`
|
|
1108
|
+
takes the same route where no document is stored at `id` yet. Over an existing
|
|
1109
|
+
document it is refused with `ValidationError` before the stream is read. A
|
|
1110
|
+
content-addressed collection refuses a stream for the reason it refuses a
|
|
1111
|
+
large blob. A write that fails midway cancels the stream, so the source behind
|
|
1112
|
+
it is released. The content type is `options.contentType`, then a guess from
|
|
1113
|
+
the id's extension, then `application/octet-stream`. To read without
|
|
1114
|
+
buffering, call `resource.getStream()` or `collection.getStream(id)`. Its
|
|
1115
|
+
`contentType` is the bare media type, with no parameters, on every collection.
|
|
1116
|
+
For a chunked document the stream fetches and decrypts each chunk as it is
|
|
1117
|
+
read. A missing chunk therefore errors the stream, with `NotFoundError`,
|
|
1118
|
+
rather than the `getStream()` call; a tampered one errors it with
|
|
1119
|
+
`EncryptionError`. `get()` still returns a `Blob`.
|
|
969
1120
|
- **Raw reads.** `get()` decrypts; the `getText()` / `getBytes()` escape hatches
|
|
970
1121
|
do not (they return the stored representation).
|
|
971
1122
|
|
|
@@ -1224,6 +1375,17 @@ carries a blinded-index key, pass the cipher the stored `/meta` value
|
|
|
1224
1375
|
Collection-handle write does; a cipher built without it writes documents
|
|
1225
1376
|
`find()` cannot see until they are rewritten.
|
|
1226
1377
|
|
|
1378
|
+
`collection.changes()` reads one page of the raw feed, and sends the query as a
|
|
1379
|
+
`POST` by default. Pass `method: 'GET'` for the read-only form,
|
|
1380
|
+
`GET .../query?profile=changes&checkpoint=...&limit=...`. The server verifies it
|
|
1381
|
+
under the `GET` action and answers the same `ChangesPage`. A caller holding a
|
|
1382
|
+
GET-only capability, such as a pull capability, reads the feed this way. The
|
|
1383
|
+
`POST` form refuses that capability with `NotFoundError`.
|
|
1384
|
+
|
|
1385
|
+
```ts
|
|
1386
|
+
const page = await collection.changes({ method: 'GET', checkpoint, limit: 100 })
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1227
1389
|
### Resource logs (co-managed key resources)
|
|
1228
1390
|
|
|
1229
1391
|
The opt-in `@interop/was-client/log` subpath is the WAS binding of
|
|
@@ -1303,6 +1465,7 @@ not exist". Write/delete methods throw a typed error instead.
|
|
|
1303
1465
|
| 400 | `ValidationError` | `ValidationError` |
|
|
1304
1466
|
| 401 | `AuthRequiredError` | `AuthRequiredError` |
|
|
1305
1467
|
| 409 | `ConflictError` | `ConflictError` |
|
|
1468
|
+
| 422 | `ValidationError` | `ValidationError` |
|
|
1306
1469
|
| 413 | `PayloadTooLargeError` | `PayloadTooLargeError` |
|
|
1307
1470
|
| 501 | `NotImplementedError` | `NotImplementedError` |
|
|
1308
1471
|
| 507 | `QuotaExceededError` | `QuotaExceededError` |
|
package/dist/Collection.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ClientContext } from './internal/request.js';
|
|
2
2
|
import type { WritePrecondition } from './internal/conditional.js';
|
|
3
|
-
import type { CodecRequestContext, IndexDeclaration, IndexSchema } from './codec.js';
|
|
3
|
+
import type { CodecRequestContext, DecodedStream, IndexDeclaration, IndexSchema } from './codec.js';
|
|
4
4
|
import { Resource } from './Resource.js';
|
|
5
5
|
import type { ChangesCheckpoint, ChangesPage, ResourceChangeDocument } from '@interop/storage-core';
|
|
6
6
|
import type { AddResult, BackendDescriptor, BackendUsage, CollectionMetadata, CollectionWritableFields, EncryptionOverride, FindPage, GrantOptions, HandleOptions, IDelegatedZcap, IZcap, Json, ResourceData, LinkSet, PolicyDocument, CollectionResourcesList, ResourceMetadataCustomInput, ResourceSummary } from './types.js';
|
|
@@ -458,12 +458,21 @@ export declare class Collection {
|
|
|
458
458
|
resource(resourceId: string, options?: HandleOptions): Resource;
|
|
459
459
|
/**
|
|
460
460
|
* Adds a resource with a server-generated id. JSON for plain objects/arrays,
|
|
461
|
-
* binary for `Blob`/`Uint8Array
|
|
462
|
-
* does not exist (WAS does not
|
|
461
|
+
* binary for `Blob`/`Uint8Array` or `ReadableStream<Uint8Array>`. Throws
|
|
462
|
+
* `NotFoundError` if the collection does not exist (WAS does not
|
|
463
|
+
* auto-create parents).
|
|
463
464
|
*
|
|
464
465
|
* On an encrypted collection a binary payload above the codec's
|
|
465
466
|
* single-document threshold is auto-routed to the chunked-stream path, which
|
|
466
|
-
* a server that does not serve the chunk endpoints answers with `501`.
|
|
467
|
+
* a server that does not serve the chunk endpoints answers with `501`. A
|
|
468
|
+
* `ReadableStream<Uint8Array>` is always written through that path, without
|
|
469
|
+
* buffering, since its size is unknown. A content-addressed collection
|
|
470
|
+
* (`idDerivation: 'content'`) cannot store a chunked payload, so it refuses
|
|
471
|
+
* a stream with `ValidationError` before reading it.
|
|
472
|
+
*
|
|
473
|
+
* On a plaintext collection a stream is refused with `ValidationError`: a
|
|
474
|
+
* plaintext resource has no chunked representation, and a signed request
|
|
475
|
+
* carries a `Digest` of its whole body. Buffer it into a `Blob` there.
|
|
467
476
|
*
|
|
468
477
|
* @param data {ResourceData}
|
|
469
478
|
* @param options {object}
|
|
@@ -482,11 +491,25 @@ export declare class Collection {
|
|
|
482
491
|
* @returns {Promise<Json | Blob | null>}
|
|
483
492
|
*/
|
|
484
493
|
get(resourceId: string): Promise<Json | Blob | null>;
|
|
494
|
+
/**
|
|
495
|
+
* Reads a resource by id as a byte stream, decrypting on an encrypted
|
|
496
|
+
* collection -- the read for a caller that cannot buffer a large chunked
|
|
497
|
+
* blob. Delegates to `Resource.getStream`; see it for the details. The
|
|
498
|
+
* ordinary {@link get} still returns a `Blob`. Returns `null` on a
|
|
499
|
+
* missing/unauthorized resource (404 conflation caveat).
|
|
500
|
+
*
|
|
501
|
+
* @param resourceId {string}
|
|
502
|
+
* @returns {Promise<(DecodedStream & { etag?: string }) | null>}
|
|
503
|
+
*/
|
|
504
|
+
getStream(resourceId: string): Promise<(DecodedStream & {
|
|
505
|
+
etag?: string;
|
|
506
|
+
}) | null>;
|
|
485
507
|
/**
|
|
486
508
|
* Creates or replaces a resource by id (upsert). Forwards the
|
|
487
509
|
* conditional-write options (`ifMatch` / `ifNoneMatch`) to `Resource.put`;
|
|
488
|
-
* see it for the conditional-write semantics
|
|
489
|
-
*
|
|
510
|
+
* see it for the conditional-write semantics and for which payloads
|
|
511
|
+
* (including a `ReadableStream<Uint8Array>`) each kind of collection
|
|
512
|
+
* accepts. Returns the stored resource's new `etag`.
|
|
490
513
|
*
|
|
491
514
|
* @param resourceId {string}
|
|
492
515
|
* @param data {ResourceData}
|
|
@@ -579,14 +602,25 @@ export declare class Collection {
|
|
|
579
602
|
* parse that resource's body). Entries of any other kind pass through
|
|
580
603
|
* unchecked.
|
|
581
604
|
*
|
|
605
|
+
* The query is sent as a `POST` whose body carries the parameters, or in
|
|
606
|
+
* the read-only `GET` form with the parameters in the query string, which
|
|
607
|
+
* the server verifies under the `GET` action and answers with the same page.
|
|
608
|
+
* A capability whose `allowedAction` excludes `POST` (a GET-only pull
|
|
609
|
+
* capability, say) reads the feed with `GET`, since the `POST` form refuses
|
|
610
|
+
* it with `NotFoundError`; every other handle sends `POST`. `method`
|
|
611
|
+
* overrides the derived form.
|
|
612
|
+
*
|
|
582
613
|
* @param [options] {object}
|
|
583
614
|
* @param [options.checkpoint] {ChangesCheckpoint} opaque checkpoint from a prior page; resume strictly after it
|
|
584
615
|
* @param [options.limit] {number} max documents; the server reduces it to its own maximum
|
|
616
|
+
* @param [options.method] {'POST' | 'GET'} the request form; derived from
|
|
617
|
+
* the bound capability by default
|
|
585
618
|
* @returns {Promise<ChangesPage>}
|
|
586
619
|
*/
|
|
587
620
|
changes(options?: {
|
|
588
621
|
checkpoint?: ChangesCheckpoint;
|
|
589
622
|
limit?: number;
|
|
623
|
+
method?: 'POST' | 'GET';
|
|
590
624
|
}): Promise<ChangesPage>;
|
|
591
625
|
/**
|
|
592
626
|
* Walks the `changes` feed page by page from `checkpoint` (or the beginning)
|
|
@@ -608,11 +642,14 @@ export declare class Collection {
|
|
|
608
642
|
* @param [options] {object}
|
|
609
643
|
* @param [options.checkpoint] {ChangesCheckpoint} resume strictly after it
|
|
610
644
|
* @param [options.limit] {number} max entries per request, every kind counted
|
|
645
|
+
* @param [options.method] {'POST' | 'GET'} the request form of each page,
|
|
646
|
+
* as for {@link changes}
|
|
611
647
|
* @returns {AsyncGenerator<{ documents: ResourceChangeDocument[], checkpoint: ChangesCheckpoint | null }>}
|
|
612
648
|
*/
|
|
613
649
|
resourceChanges(options?: {
|
|
614
650
|
checkpoint?: ChangesCheckpoint;
|
|
615
651
|
limit?: number;
|
|
652
|
+
method?: 'POST' | 'GET';
|
|
616
653
|
}): AsyncGenerator<{
|
|
617
654
|
documents: ResourceChangeDocument[];
|
|
618
655
|
checkpoint: ChangesCheckpoint | null;
|
package/dist/Collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collection.d.ts","sourceRoot":"","sources":["../src/Collection.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AA+B1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AA8BlE,OAAO,KAAK,EAEV,mBAAmB,EACnB,gBAAgB,EAChB,WAAW,EAEZ,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,OAAO,KAAK,EACV,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EACV,SAAS,EACT,iBAAiB,EACjB,YAAY,EAEZ,kBAAkB,EAClB,wBAAwB,EACxB,kBAAkB,EAClB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,cAAc,EACd,KAAK,EACL,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,cAAc,EACd,uBAAuB,EACvB,2BAA2B,EAC3B,eAAe,EAChB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"Collection.d.ts","sourceRoot":"","sources":["../src/Collection.ts"],"names":[],"mappings":"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"}
|
package/dist/Collection.js
CHANGED
|
@@ -71,6 +71,47 @@ function mergedConfiguration(desc, current) {
|
|
|
71
71
|
plaintext: desc.plaintext ?? current?.plaintext
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The request form of the `changes` query a bound capability admits: `GET`
|
|
76
|
+
* when the capability's `allowedAction` is stated and excludes `POST` (a
|
|
77
|
+
* GET-only pull capability), `POST` otherwise (no capability, a root one, or
|
|
78
|
+
* one whose actions are unrestricted or include `POST`).
|
|
79
|
+
*/
|
|
80
|
+
function changesMethodFor(capability) {
|
|
81
|
+
const allowed = capability?.allowedAction ?? 'POST';
|
|
82
|
+
const actions = Array.isArray(allowed) ? allowed : [allowed];
|
|
83
|
+
return actions.some(action => action.toUpperCase() === 'POST')
|
|
84
|
+
? 'POST'
|
|
85
|
+
: 'GET';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The path and body of a `changes` query in the given form. The `POST` form
|
|
89
|
+
* carries the parameters in its JSON body; the read-only `GET` form carries
|
|
90
|
+
* them in the query string, each percent-encoded by `URLSearchParams` so an
|
|
91
|
+
* opaque checkpoint never breaks the URL.
|
|
92
|
+
*
|
|
93
|
+
* @param options {object}
|
|
94
|
+
* @param options.queryPath {string} the collection's `/query` path
|
|
95
|
+
* @param options.method {'POST' | 'GET'}
|
|
96
|
+
* @param [options.checkpoint] {ChangesCheckpoint}
|
|
97
|
+
* @param [options.limit] {number}
|
|
98
|
+
* @returns {{ path: string, method: 'POST' | 'GET', json?: object }}
|
|
99
|
+
*/
|
|
100
|
+
function changesRequest({ queryPath, method, checkpoint, limit }) {
|
|
101
|
+
const params = {
|
|
102
|
+
profile: 'changes',
|
|
103
|
+
...(checkpoint !== undefined && { checkpoint }),
|
|
104
|
+
...(limit !== undefined && { limit })
|
|
105
|
+
};
|
|
106
|
+
if (method === 'POST') {
|
|
107
|
+
return { path: queryPath, method, json: params };
|
|
108
|
+
}
|
|
109
|
+
const search = new URLSearchParams();
|
|
110
|
+
for (const [name, value] of Object.entries(params)) {
|
|
111
|
+
search.set(name, String(value));
|
|
112
|
+
}
|
|
113
|
+
return { path: `${queryPath}?${search.toString()}`, method };
|
|
114
|
+
}
|
|
74
115
|
export class Collection {
|
|
75
116
|
spaceId;
|
|
76
117
|
id;
|
|
@@ -1208,12 +1249,21 @@ export class Collection {
|
|
|
1208
1249
|
}
|
|
1209
1250
|
/**
|
|
1210
1251
|
* Adds a resource with a server-generated id. JSON for plain objects/arrays,
|
|
1211
|
-
* binary for `Blob`/`Uint8Array
|
|
1212
|
-
* does not exist (WAS does not
|
|
1252
|
+
* binary for `Blob`/`Uint8Array` or `ReadableStream<Uint8Array>`. Throws
|
|
1253
|
+
* `NotFoundError` if the collection does not exist (WAS does not
|
|
1254
|
+
* auto-create parents).
|
|
1213
1255
|
*
|
|
1214
1256
|
* On an encrypted collection a binary payload above the codec's
|
|
1215
1257
|
* single-document threshold is auto-routed to the chunked-stream path, which
|
|
1216
|
-
* a server that does not serve the chunk endpoints answers with `501`.
|
|
1258
|
+
* a server that does not serve the chunk endpoints answers with `501`. A
|
|
1259
|
+
* `ReadableStream<Uint8Array>` is always written through that path, without
|
|
1260
|
+
* buffering, since its size is unknown. A content-addressed collection
|
|
1261
|
+
* (`idDerivation: 'content'`) cannot store a chunked payload, so it refuses
|
|
1262
|
+
* a stream with `ValidationError` before reading it.
|
|
1263
|
+
*
|
|
1264
|
+
* On a plaintext collection a stream is refused with `ValidationError`: a
|
|
1265
|
+
* plaintext resource has no chunked representation, and a signed request
|
|
1266
|
+
* carries a `Digest` of its whole body. Buffer it into a `Blob` there.
|
|
1217
1267
|
*
|
|
1218
1268
|
* @param data {ResourceData}
|
|
1219
1269
|
* @param options {object}
|
|
@@ -1292,11 +1342,25 @@ export class Collection {
|
|
|
1292
1342
|
// guard in the `Resource` constructor applies to reads and writes alike.
|
|
1293
1343
|
return this.resource(resourceId).get();
|
|
1294
1344
|
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Reads a resource by id as a byte stream, decrypting on an encrypted
|
|
1347
|
+
* collection -- the read for a caller that cannot buffer a large chunked
|
|
1348
|
+
* blob. Delegates to `Resource.getStream`; see it for the details. The
|
|
1349
|
+
* ordinary {@link get} still returns a `Blob`. Returns `null` on a
|
|
1350
|
+
* missing/unauthorized resource (404 conflation caveat).
|
|
1351
|
+
*
|
|
1352
|
+
* @param resourceId {string}
|
|
1353
|
+
* @returns {Promise<(DecodedStream & { etag?: string }) | null>}
|
|
1354
|
+
*/
|
|
1355
|
+
async getStream(resourceId) {
|
|
1356
|
+
return this.resource(resourceId).getStream();
|
|
1357
|
+
}
|
|
1295
1358
|
/**
|
|
1296
1359
|
* Creates or replaces a resource by id (upsert). Forwards the
|
|
1297
1360
|
* conditional-write options (`ifMatch` / `ifNoneMatch`) to `Resource.put`;
|
|
1298
|
-
* see it for the conditional-write semantics
|
|
1299
|
-
*
|
|
1361
|
+
* see it for the conditional-write semantics and for which payloads
|
|
1362
|
+
* (including a `ReadableStream<Uint8Array>`) each kind of collection
|
|
1363
|
+
* accepts. Returns the stored resource's new `etag`.
|
|
1300
1364
|
*
|
|
1301
1365
|
* @param resourceId {string}
|
|
1302
1366
|
* @param data {ResourceData}
|
|
@@ -1407,28 +1471,37 @@ export class Collection {
|
|
|
1407
1471
|
* parse that resource's body). Entries of any other kind pass through
|
|
1408
1472
|
* unchecked.
|
|
1409
1473
|
*
|
|
1474
|
+
* The query is sent as a `POST` whose body carries the parameters, or in
|
|
1475
|
+
* the read-only `GET` form with the parameters in the query string, which
|
|
1476
|
+
* the server verifies under the `GET` action and answers with the same page.
|
|
1477
|
+
* A capability whose `allowedAction` excludes `POST` (a GET-only pull
|
|
1478
|
+
* capability, say) reads the feed with `GET`, since the `POST` form refuses
|
|
1479
|
+
* it with `NotFoundError`; every other handle sends `POST`. `method`
|
|
1480
|
+
* overrides the derived form.
|
|
1481
|
+
*
|
|
1410
1482
|
* @param [options] {object}
|
|
1411
1483
|
* @param [options.checkpoint] {ChangesCheckpoint} opaque checkpoint from a prior page; resume strictly after it
|
|
1412
1484
|
* @param [options.limit] {number} max documents; the server reduces it to its own maximum
|
|
1485
|
+
* @param [options.method] {'POST' | 'GET'} the request form; derived from
|
|
1486
|
+
* the bound capability by default
|
|
1413
1487
|
* @returns {Promise<ChangesPage>}
|
|
1414
1488
|
*/
|
|
1415
1489
|
async changes(options = {}) {
|
|
1416
|
-
const { checkpoint, limit } = options;
|
|
1490
|
+
const { checkpoint, limit, method = changesMethodFor(this.#capability) } = options;
|
|
1417
1491
|
const response = await send(this.#context, {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
}
|
|
1492
|
+
...changesRequest({
|
|
1493
|
+
queryPath: collectionQuery(this.spaceId, this.id),
|
|
1494
|
+
method,
|
|
1495
|
+
checkpoint,
|
|
1496
|
+
limit
|
|
1497
|
+
}),
|
|
1498
|
+
capability: this.#capability
|
|
1426
1499
|
});
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1500
|
+
// Neither form carries the null-on-404 `read` flag: a missing or
|
|
1501
|
+
// unauthorized collection throws, as every other write-shaped call on this
|
|
1502
|
+
// handle does. A `null` here is therefore a 2xx whose body did not parse
|
|
1503
|
+
// as JSON, which must not masquerade as the end-of-feed page
|
|
1504
|
+
// `{ documents: [], checkpoint: null }`.
|
|
1432
1505
|
const page = dataOrNull(response);
|
|
1433
1506
|
if (page === null) {
|
|
1434
1507
|
throw new WasServerError(`The changes feed of collection "${this.id}" answered with no JSON ` +
|
|
@@ -1487,14 +1560,16 @@ export class Collection {
|
|
|
1487
1560
|
* @param [options] {object}
|
|
1488
1561
|
* @param [options.checkpoint] {ChangesCheckpoint} resume strictly after it
|
|
1489
1562
|
* @param [options.limit] {number} max entries per request, every kind counted
|
|
1563
|
+
* @param [options.method] {'POST' | 'GET'} the request form of each page,
|
|
1564
|
+
* as for {@link changes}
|
|
1490
1565
|
* @returns {AsyncGenerator<{ documents: ResourceChangeDocument[], checkpoint: ChangesCheckpoint | null }>}
|
|
1491
1566
|
*/
|
|
1492
1567
|
async *resourceChanges(options = {}) {
|
|
1493
|
-
const { limit } = options;
|
|
1568
|
+
const { limit, method } = options;
|
|
1494
1569
|
let checkpoint = options.checkpoint;
|
|
1495
1570
|
const seen = new Set(checkpoint === undefined ? [] : [checkpoint]);
|
|
1496
1571
|
for (;;) {
|
|
1497
|
-
const page = await this.changes({ checkpoint, limit });
|
|
1572
|
+
const page = await this.changes({ checkpoint, limit, method });
|
|
1498
1573
|
yield {
|
|
1499
1574
|
documents: page.documents.filter(isResourceChange),
|
|
1500
1575
|
checkpoint: page.checkpoint ?? null
|