@superbee/core 0.2.0-pre.1 → 0.2.0-pre.2

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.
Files changed (43) hide show
  1. package/dist/backend.d.ts.map +1 -1
  2. package/dist/backend.js +9 -6
  3. package/dist/backend.js.map +1 -1
  4. package/dist/heads-digest.d.ts +32 -0
  5. package/dist/heads-digest.d.ts.map +1 -0
  6. package/dist/heads-digest.js +37 -0
  7. package/dist/heads-digest.js.map +1 -0
  8. package/dist/index.d.ts +5 -3
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/indexeddb-backend.d.ts +12 -2
  13. package/dist/indexeddb-backend.d.ts.map +1 -1
  14. package/dist/indexeddb-backend.js +91 -5
  15. package/dist/indexeddb-backend.js.map +1 -1
  16. package/dist/journaled-backend.d.ts +61 -1
  17. package/dist/journaled-backend.d.ts.map +1 -1
  18. package/dist/journaled-backend.js +11 -0
  19. package/dist/journaled-backend.js.map +1 -1
  20. package/dist/memory-backend.d.ts.map +1 -1
  21. package/dist/memory-backend.js +6 -5
  22. package/dist/memory-backend.js.map +1 -1
  23. package/dist/paths.d.ts +6 -2
  24. package/dist/paths.d.ts.map +1 -1
  25. package/dist/paths.js +35 -33
  26. package/dist/paths.js.map +1 -1
  27. package/dist/platform.d.ts +77 -6
  28. package/dist/platform.d.ts.map +1 -1
  29. package/dist/platform.js +9 -4
  30. package/dist/platform.js.map +1 -1
  31. package/dist/remote-backend.d.ts +66 -2
  32. package/dist/remote-backend.d.ts.map +1 -1
  33. package/dist/remote-backend.js +205 -5
  34. package/dist/remote-backend.js.map +1 -1
  35. package/dist/remote.d.ts +3 -1
  36. package/dist/remote.d.ts.map +1 -1
  37. package/dist/remote.js +1 -0
  38. package/dist/remote.js.map +1 -1
  39. package/dist/storage.d.ts +4 -2
  40. package/dist/storage.d.ts.map +1 -1
  41. package/dist/storage.js +4 -1
  42. package/dist/storage.js.map +1 -1
  43. package/package.json +1 -1
@@ -17,10 +17,15 @@
17
17
  * presentation can report shared confirmation without an acknowledgement. It says nothing
18
18
  * about what the authority holds now: request-driven's last exchange is the request itself,
19
19
  * browser-local's is its last sync, and the authority may have moved (or deleted the document)
20
- * since. One limit of "last exchange" today: a browser-local sync pulls what the authority
21
- * lists, so a document the authority deleted is not visited and stays `shared-confirmed` at
22
- * its last served version until pull reconciles deletions; request-driven answers absence
23
- * directly. A presentation must not read `shared-confirmed` as proof of present existence.
20
+ * since. A document the authority deleted disappears from a browser-local working copy at
21
+ * its next sync (pull reconciles deletions against the authority's heads) unless a local
22
+ * edit holds it, in which case pull retains the local content and rewrites its base to an
23
+ * absent shared version, and the document reads `local-pending` until push delivers the edit
24
+ * and settles the authority's conflict answer (a 412 whose actual version is `null`) as a
25
+ * conflict against an absent remote, from which point it reads `local-conflict` with
26
+ * `remote: null`; request-driven answers absence directly. Between syncs a deleted document
27
+ * still reads `shared-confirmed` at its last served version, so a presentation must not read
28
+ * `shared-confirmed` as proof of present existence.
24
29
  * - `local-pending`: the working copy holds a local edit the authority has not accepted;
25
30
  * `requestId` names the journaled intent that will deliver it and `base` the shared version
26
31
  * the edit was made against.
@@ -111,6 +116,62 @@ export interface PlatformCommit {
111
116
  changed: boolean;
112
117
  provenance: Provenance;
113
118
  }
119
+ /**
120
+ * Why a browser-local sync applied none of the deletions a verified listing implied:
121
+ * `empty-listing` names no document while the working copy holds some, `over-half` names so
122
+ * few that more than half of the working copy would go. Only a listing that would remove at
123
+ * least eight documents is bounded; fewer are applied on the authority's word.
124
+ */
125
+ export type RefusedDeletionsReason = "empty-listing" | "over-half";
126
+ /**
127
+ * The deletions a browser-local sync refused to apply, exactly as recorded on its last pull.
128
+ * `digest` names the listing that implied them; a caller that has judged the shrink genuine
129
+ * passes the record back as {@link PlatformSyncOptions.acceptRefusedDeletions}.
130
+ */
131
+ export interface RefusedDeletions {
132
+ /** How many documents the listing would have removed from the working copy. */
133
+ deletions: number;
134
+ reason: RefusedDeletionsReason;
135
+ /** The heads digest of the listing that was refused. */
136
+ digest: string;
137
+ }
138
+ /** How the runtime's last sync ended; see {@link PlatformSyncStatus.lastSync}. */
139
+ export interface PlatformSyncOutcome {
140
+ /** False when the sync rejected (a carrier failure, or an authority answer the runtime could not apply). */
141
+ ok: boolean;
142
+ /** The rejection's `name: message` when `ok` is false. */
143
+ error?: string;
144
+ /** The deletions the last completed pull refused; present until a pull applies or clears them. */
145
+ refusedDeletions?: RefusedDeletions;
146
+ }
147
+ export interface PlatformSyncOptions {
148
+ /**
149
+ * The refusal the last sync reported (`lastSync.refusedDeletions`), passed back to say the
150
+ * shrink it describes is genuine. Browser-local applies the deletions only when the
151
+ * authority's current listing still carries the same digest, count and reason; a listing
152
+ * that has moved since is refused afresh. Request-driven ignores it: nothing is ever
153
+ * refused there.
154
+ */
155
+ acceptRefusedDeletions?: RefusedDeletions;
156
+ }
157
+ /**
158
+ * The runtime's relation to the authority as of its last exchange. `online` reports the
159
+ * carrier: it turns false only when a request could not reach the authority, never on an
160
+ * answer the authority gave. `lastSync` reports the sync verb itself, and is the field a
161
+ * presentation must show to distinguish "synchronized" from "tried and failed": `ok` is false
162
+ * whenever `sync` rejected, whatever the cause, with the rejection's text in `error`.
163
+ *
164
+ * Browser-local applies a verified heads listing on the authority's word, including the
165
+ * documents it no longer names, with one bound: a listing that would remove at least eight
166
+ * documents and more than half of the working copy (or every document) is refused as a whole.
167
+ * The refreshes in that sync still apply, nothing is removed, `lastSync.ok` stays true, and
168
+ * `lastSync.refusedDeletions` carries the refusal until a later pull applies or clears it. A
169
+ * genuine shrink of that size therefore never reconciles on its own; the recovery path is to
170
+ * pass the recorded refusal back as `sync({ acceptRefusedDeletions })`, which applies the
171
+ * deletions if the authority still lists the same state and refuses afresh if it has moved.
172
+ * Request-driven has no working copy and leaves `lastSync` undefined: the status is the answer
173
+ * to the request that produced it.
174
+ */
114
175
  export interface PlatformSyncStatus {
115
176
  mode: ExecutionMode;
116
177
  /** Whether the last request to the authority succeeded; `null` before any was made. */
@@ -131,6 +192,12 @@ export interface PlatformSyncStatus {
131
192
  pausedReason?: string;
132
193
  /** The runtime's working set is complete: the authority's for request-driven, a finished bootstrap for browser-local. */
133
194
  complete: boolean;
195
+ /**
196
+ * How the last sync ended, and any deletions its pull refused. Browser-local fills it from
197
+ * the sync's own outcome and the pull marker (so a refusal recorded before this runtime was
198
+ * created still shows); request-driven leaves it undefined. Absent before any sync.
199
+ */
200
+ lastSync?: PlatformSyncOutcome;
134
201
  }
135
202
  export interface PlatformRuntime {
136
203
  capabilities(): PlatformCapabilities;
@@ -141,8 +208,12 @@ export interface PlatformRuntime {
141
208
  validate(id: ConceptId): Promise<PlatformValidation>;
142
209
  commit(id: ConceptId, edit: PlatformEdit): Promise<PlatformCommit>;
143
210
  syncStatus(): Promise<PlatformSyncStatus>;
144
- /** Push pending work and pull the authority's changes; a no-op returning the status in request-driven mode. */
145
- sync(): Promise<PlatformSyncStatus>;
211
+ /**
212
+ * Push pending work and pull the authority's changes; a no-op returning the status in
213
+ * request-driven mode. `options` carries the recovery path for refused deletions; see
214
+ * {@link PlatformSyncOptions}.
215
+ */
216
+ sync(options?: PlatformSyncOptions): Promise<PlatformSyncStatus>;
146
217
  }
147
218
  /** The word a presentation shows for a provenance. */
148
219
  export declare function provenanceLabel(provenance: Provenance): ProvenanceLabel;
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
package/dist/platform.js CHANGED
@@ -17,10 +17,15 @@
17
17
  * presentation can report shared confirmation without an acknowledgement. It says nothing
18
18
  * about what the authority holds now: request-driven's last exchange is the request itself,
19
19
  * browser-local's is its last sync, and the authority may have moved (or deleted the document)
20
- * since. One limit of "last exchange" today: a browser-local sync pulls what the authority
21
- * lists, so a document the authority deleted is not visited and stays `shared-confirmed` at
22
- * its last served version until pull reconciles deletions; request-driven answers absence
23
- * directly. A presentation must not read `shared-confirmed` as proof of present existence.
20
+ * since. A document the authority deleted disappears from a browser-local working copy at
21
+ * its next sync (pull reconciles deletions against the authority's heads) unless a local
22
+ * edit holds it, in which case pull retains the local content and rewrites its base to an
23
+ * absent shared version, and the document reads `local-pending` until push delivers the edit
24
+ * and settles the authority's conflict answer (a 412 whose actual version is `null`) as a
25
+ * conflict against an absent remote, from which point it reads `local-conflict` with
26
+ * `remote: null`; request-driven answers absence directly. Between syncs a deleted document
27
+ * still reads `shared-confirmed` at its last served version, so a presentation must not read
28
+ * `shared-confirmed` as proof of present existence.
24
29
  * - `local-pending`: the working copy holds a local edit the authority has not accepted;
25
30
  * `requestId` names the journaled intent that will deliver it and `base` the shared version
26
31
  * the edit was made against.
@@ -1 +1 @@
1
- {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAqGH,sDAAsD;AACtD,MAAM,UAAU,eAAe,CAAC,UAAsB;IACpD,QAAQ,UAAU,CAAC,KAAK,EAAE,CAAC;QACzB,KAAK,kBAAkB;YACrB,OAAO,QAAQ,CAAC;QAClB,KAAK,eAAe;YAClB,OAAO,SAAS,CAAC;QACnB,KAAK,gBAAgB;YACnB,OAAO,UAAU,CAAC;IACtB,CAAC;AACH,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,iBAAiB,CAAC,UAAsB;IACtD,OAAO,UAAU,CAAC,KAAK,KAAK,kBAAkB,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAgB,EAAE,eAAwB,OAAO;IAC/E,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,IAAoB,EAAE,SAAiB;IACpF,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAgB,EAAE,IAAoB,EAAE,MAAsB,EAAE,SAAiB;IAC7G,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACvE,CAAC"}
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AA2KH,sDAAsD;AACtD,MAAM,UAAU,eAAe,CAAC,UAAsB;IACpD,QAAQ,UAAU,CAAC,KAAK,EAAE,CAAC;QACzB,KAAK,kBAAkB;YACrB,OAAO,QAAQ,CAAC;QAClB,KAAK,eAAe;YAClB,OAAO,SAAS,CAAC;QACnB,KAAK,gBAAgB;YACnB,OAAO,UAAU,CAAC;IACtB,CAAC;AACH,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,iBAAiB,CAAC,UAAsB;IACtD,OAAO,UAAU,CAAC,KAAK,KAAK,kBAAkB,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAgB,EAAE,eAAwB,OAAO;IAC/E,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,IAAoB,EAAE,SAAiB;IACpF,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAgB,EAAE,IAAoB,EAAE,MAAsB,EAAE,SAAiB;IAC7G,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACvE,CAAC"}
@@ -40,8 +40,9 @@
40
40
  * This module touches NO filesystem and parses NO markdown — like {@link MemoryBackend},
41
41
  * it proves the engine leaks no assumptions beyond the {@link StorageBackend} contract.
42
42
  */
43
+ import { type DocumentHead } from "./heads-digest.js";
43
44
  import { type Outcome } from "./uncertain-write.js";
44
- import type { BlobKey, ConceptId, DeleteOptions, HeadResult, OkfDocument, QueryFilter, ReadBlobResult, ReadResult, ReservedFilename, ReservedReadResult, StorageBackend, Version, VersionInfo, WriteOptions } from "./types.js";
45
+ import type { BlobKey, ConceptId, DeleteOptions, Frontmatter, HeadResult, OkfDocument, QueryFilter, ReadBlobResult, ReadResult, ReservedFilename, ReservedReadResult, StorageBackend, Version, VersionInfo, WriteOptions } from "./types.js";
45
46
  /**
46
47
  * An injectable fetch-like transport: given a `Request`, resolve a `Response`. This
47
48
  * is exactly the shape {@link createRouter} (in `@superbee/server`) returns,
@@ -86,7 +87,7 @@ export declare class RemoteError extends Error {
86
87
  readonly code: string;
87
88
  /** The raw HTTP status that produced this error. */
88
89
  readonly status: number;
89
- constructor(message: string, code: string, status: number);
90
+ constructor(message: string, code: string, status: number, cause?: unknown);
90
91
  }
91
92
  /** The deployment-scoped capability booleans a wire authority reports (`docs/WIRE-PROTOCOL.md`). */
92
93
  export interface WireCapabilities {
@@ -97,6 +98,47 @@ export interface WireCapabilities {
97
98
  blobs: boolean;
98
99
  /** Whether document writes carrying `Idempotency-Key` are recorded and readable by lookup. */
99
100
  operations: boolean;
101
+ /** Whether `GET /heads` answers every document id and version under one digest, with `304`. */
102
+ heads: boolean;
103
+ /** Whether `GET /snapshot` streams every document as terminated NDJSON. */
104
+ snapshot: boolean;
105
+ }
106
+ /** Options for {@link RemoteBackend.heads}. */
107
+ export interface HeadsOptions {
108
+ /** A digest from an earlier `heads` or `snapshot` answer; the authority answers `304` when it still holds it. */
109
+ ifNoneMatch?: string;
110
+ }
111
+ /** A `200` from `GET /heads`: every document id and version, and the digest over them. */
112
+ export interface HeadsResult {
113
+ digest: string;
114
+ heads: DocumentHead[];
115
+ }
116
+ /** The snapshot's first line: how many documents follow and the digest `heads` would return for this state. */
117
+ export interface SnapshotHeader {
118
+ count: number;
119
+ digest: string;
120
+ }
121
+ /** One `doc` line of a snapshot. */
122
+ export interface SnapshotDocument {
123
+ id: ConceptId;
124
+ version: Version;
125
+ frontmatter: Frontmatter;
126
+ body: string;
127
+ }
128
+ /**
129
+ * A snapshot as {@link RemoteBackend.snapshot} hands it over: the header, already parsed, and
130
+ * the documents as they stream. Iterating `docs` to completion is the completeness signal: the
131
+ * loop ends only after the `end` line arrived with the announced count and the digest recomputed
132
+ * over the received `{ id, version }` rows equals the header's, and it throws
133
+ * `SNAPSHOT_TRUNCATED` (the body ended or failed first) or `SNAPSHOT_DIGEST_MISMATCH` (the body
134
+ * was whole but describes a state the header did not announce) otherwise, so a consumer that
135
+ * writes batches as they arrive has one control path and never needs to inspect a terminator
136
+ * or a digest itself. A consumer records the header digest as matched only after the loop
137
+ * ended normally.
138
+ */
139
+ export interface RemoteSnapshot {
140
+ header: SnapshotHeader;
141
+ docs: AsyncIterable<SnapshotDocument>;
100
142
  }
101
143
  /**
102
144
  * The client half of the wire-protocol v0 contract: implements {@link StorageBackend}
@@ -128,6 +170,28 @@ export declare class RemoteBackend implements StorageBackend {
128
170
  * sends any intent. Missing booleans read as `false`.
129
171
  */
130
172
  wireCapabilities(): Promise<WireCapabilities>;
173
+ /**
174
+ * `GET /heads`: every document id and version the authority holds, under one digest, or
175
+ * `null` when `ifNoneMatch` named the digest it still holds (a `304`: nothing changed). A
176
+ * `200` is diffed against the caller's own copy: an id missing from `heads` was deleted, a
177
+ * differing version changed. A `200` without a well-formed digest, whose `count` and rows
178
+ * disagree, or whose rows digest by the documented recipe to something other than the served
179
+ * digest, is rejected rather than trusted, as is a `304` to a request that sent no
180
+ * `ifNoneMatch`. The recomputation is what stops a listing that is whole by its own count
181
+ * but not the state its digest names (a shortened listing under the real digest) from being
182
+ * diffed as a mass deletion. Not part of the {@link StorageBackend} seam.
183
+ */
184
+ heads(options?: HeadsOptions): Promise<HeadsResult | null>;
185
+ /**
186
+ * `GET /snapshot`: the whole bundle in one response. The header line is parsed before this
187
+ * resolves, so a non-2xx or a body without a header rejects here with the usual typed error;
188
+ * the documents then stream through {@link RemoteSnapshot.docs}. Transient retry applies only
189
+ * to obtaining the response: a body cut mid-stream is reported to the consumer as
190
+ * `SNAPSHOT_TRUNCATED`, a whole body whose heads do not digest to the header's announcement as
191
+ * `SNAPSHOT_DIGEST_MISMATCH`, and re-requesting is the consumer's decision. Reserved files are
192
+ * not part of a snapshot. Not part of the {@link StorageBackend} seam.
193
+ */
194
+ snapshot(): Promise<RemoteSnapshot>;
131
195
  /**
132
196
  * `GET /operations/{requestId}`: the authority's recorded outcome for an identified write, or
133
197
  * `null` when it holds nothing under that identity (never recorded, or recorded and expired;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-backend.d.ts","sourceRoot":"","sources":["../src/remote-backend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"remote-backend.d.ts","sourceRoot":"","sources":["../src/remote-backend.ts"],"names":[],"mappings":"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"}
@@ -42,7 +42,8 @@
42
42
  */
43
43
  import { DEFAULT_BLOB_CONTENT_TYPE } from "./content-type.js";
44
44
  import { InvalidInputError } from "./errors.js";
45
- import { assertSafeBlobKey, assertSafeConceptId } from "./paths.js";
45
+ import { headsDigest, isHeadsDigest } from "./heads-digest.js";
46
+ import { assertSafeBlobKey, assertSafeConceptId, assertSafeReservedDir, assertSafeReservedFilename, compareStorageKeys } from "./paths.js";
46
47
  import { isRequestIdentity } from "./uncertain-write.js";
47
48
  import { VersionConflict, stripETagWrapper } from "./version-transport.js";
48
49
  /**
@@ -57,8 +58,8 @@ export class RemoteError extends Error {
57
58
  code;
58
59
  /** The raw HTTP status that produced this error. */
59
60
  status;
60
- constructor(message, code, status) {
61
- super(message);
61
+ constructor(message, code, status, cause) {
62
+ super(message, cause === undefined ? undefined : { cause });
62
63
  this.name = "RemoteError";
63
64
  this.code = code;
64
65
  this.status = status;
@@ -122,6 +123,129 @@ function assertRequestIdentity(requestId) {
122
123
  }
123
124
  /** The lookup route's `200` body: a recorded outcome, never `unknown`. */
124
125
  const RECORDED_OUTCOME_KINDS = new Set(["committed", "conflict", "refused"]);
126
+ /** A snapshot body that ended, or failed, before its terminator arrived with the announced count. */
127
+ const SNAPSHOT_TRUNCATED = "SNAPSHOT_TRUNCATED";
128
+ /**
129
+ * A snapshot whose body arrived whole, with its terminator and the announced count, but whose
130
+ * rows digest to something other than the header announced. Not truncation: re-requesting will
131
+ * not necessarily repair it, since the authority contradicted itself. A consumer discards the
132
+ * documents' claim to the header digest either way.
133
+ */
134
+ const SNAPSHOT_DIGEST_MISMATCH = "SNAPSHOT_DIGEST_MISMATCH";
135
+ function isDocumentHead(value) {
136
+ if (typeof value !== "object" || value === null)
137
+ return false;
138
+ const head = value;
139
+ return typeof head.id === "string" && typeof head.version === "string";
140
+ }
141
+ /** A wire payload the authority produced but the contract does not admit: not retried, not truncation. */
142
+ function malformed(message) {
143
+ return new RemoteError(message, "RUNTIME", 502);
144
+ }
145
+ /** Quote a bare digest for `If-None-Match`; an already quoted or weak form passes through. */
146
+ function etagForm(token) {
147
+ return token.startsWith('"') || token.startsWith("W/") ? token : `"${token}"`;
148
+ }
149
+ /**
150
+ * Split a response body into its NDJSON lines as they arrive, decoding UTF-8 across chunk
151
+ * boundaries. Only `TextDecoder` and the Web Streams reader are used, so the same code runs in a
152
+ * browser. A partial trailing line at the end of the body is a cut line and is not yielded; a
153
+ * transport failure while reading is reported as truncation, since either way the terminator
154
+ * never arrived. Leaving the loop early cancels the reader so the connection is released.
155
+ */
156
+ async function* ndjsonLines(body, status) {
157
+ const reader = body.getReader();
158
+ const decoder = new TextDecoder();
159
+ let buffered = "";
160
+ try {
161
+ for (;;) {
162
+ let chunk;
163
+ try {
164
+ chunk = await reader.read();
165
+ }
166
+ catch (cause) {
167
+ throw new RemoteError("snapshot body failed before its end line arrived", SNAPSHOT_TRUNCATED, status, cause);
168
+ }
169
+ buffered += decoder.decode(chunk.value, { stream: !chunk.done });
170
+ let start = 0;
171
+ for (let newline = buffered.indexOf("\n"); newline !== -1; newline = buffered.indexOf("\n", start)) {
172
+ yield buffered.slice(start, newline);
173
+ start = newline + 1;
174
+ }
175
+ buffered = buffered.slice(start);
176
+ if (chunk.done)
177
+ return;
178
+ }
179
+ }
180
+ finally {
181
+ await reader.cancel().catch(() => undefined);
182
+ }
183
+ }
184
+ function parseSnapshotLine(line) {
185
+ let record;
186
+ try {
187
+ record = JSON.parse(line);
188
+ }
189
+ catch {
190
+ throw malformed("snapshot line is not JSON");
191
+ }
192
+ if (typeof record !== "object" || record === null)
193
+ throw malformed("snapshot line is not a JSON object");
194
+ return record;
195
+ }
196
+ function parseSnapshotHeader(line) {
197
+ const record = parseSnapshotLine(line);
198
+ if (record.kind !== "snapshot")
199
+ throw malformed("snapshot did not begin with its header line");
200
+ if (typeof record.count !== "number" || !Number.isInteger(record.count) || record.count < 0) {
201
+ throw malformed("snapshot header carries no document count");
202
+ }
203
+ if (!isHeadsDigest(record.digest))
204
+ throw malformed("snapshot header carries no well-formed digest");
205
+ return { count: record.count, digest: record.digest };
206
+ }
207
+ /**
208
+ * The document lines of a snapshot, ending normally only at an `end` line whose count equals
209
+ * both the header's announcement and the documents actually seen, and only when the digest
210
+ * recomputed over the received heads equals the header's. Anything else is a rejection: a body
211
+ * that ends first is `SNAPSHOT_TRUNCATED`; a whole body whose heads digest differently is
212
+ * `SNAPSHOT_DIGEST_MISMATCH`; a line the contract does not admit is malformed. The digest is the
213
+ * client's own check of the listing it is about to trust: a count and a terminator say the body
214
+ * is whole, only the recipe says it is the state the header named.
215
+ */
216
+ async function* snapshotDocuments(lines, header, status) {
217
+ let seen = 0;
218
+ const received = [];
219
+ for await (const line of lines) {
220
+ const record = parseSnapshotLine(line);
221
+ if (record.kind === "doc") {
222
+ if (typeof record.id !== "string" || typeof record.version !== "string" || typeof record.body !== "string") {
223
+ throw malformed("snapshot doc line lacks id, version, or body");
224
+ }
225
+ if (typeof record.frontmatter !== "object" || record.frontmatter === null || Array.isArray(record.frontmatter)) {
226
+ throw malformed("snapshot doc line lacks a frontmatter object");
227
+ }
228
+ seen += 1;
229
+ if (seen > header.count)
230
+ throw malformed(`snapshot delivered more than the ${header.count} announced document(s)`);
231
+ received.push({ id: record.id, version: record.version });
232
+ yield { id: record.id, version: record.version, frontmatter: record.frontmatter, body: record.body };
233
+ continue;
234
+ }
235
+ if (record.kind === "end") {
236
+ if (record.count !== seen || seen !== header.count) {
237
+ throw new RemoteError(`snapshot end line counts ${String(record.count)} document(s), but ${seen} of ${header.count} announced arrived`, SNAPSHOT_TRUNCATED, status);
238
+ }
239
+ const recomputed = headsDigest(received);
240
+ if (recomputed !== header.digest) {
241
+ throw new RemoteError(`snapshot header announced digest ${header.digest}, but its ${seen} document(s) digest to ${recomputed}`, SNAPSHOT_DIGEST_MISMATCH, status);
242
+ }
243
+ return;
244
+ }
245
+ throw malformed(`snapshot line has unexpected kind ${JSON.stringify(record.kind)}`);
246
+ }
247
+ throw new RemoteError(`snapshot ended after ${seen} of ${header.count} document(s) without its end line`, SNAPSHOT_TRUNCATED, status);
248
+ }
125
249
  /**
126
250
  * Transient HTTP statuses worth retrying: 500 (a Cloudflare D1 cold-start surfaces as a 500
127
251
  * "storage caused object to be reset" the first time a hibernated database is hit), plus the edge
@@ -343,8 +467,80 @@ export class RemoteBackend {
343
467
  backlinks: flag("backlinks"),
344
468
  blobs: flag("blobs"),
345
469
  operations: flag("operations"),
470
+ heads: flag("heads"),
471
+ snapshot: flag("snapshot"),
346
472
  };
347
473
  }
474
+ /**
475
+ * `GET /heads`: every document id and version the authority holds, under one digest, or
476
+ * `null` when `ifNoneMatch` named the digest it still holds (a `304`: nothing changed). A
477
+ * `200` is diffed against the caller's own copy: an id missing from `heads` was deleted, a
478
+ * differing version changed. A `200` without a well-formed digest, whose `count` and rows
479
+ * disagree, or whose rows digest by the documented recipe to something other than the served
480
+ * digest, is rejected rather than trusted, as is a `304` to a request that sent no
481
+ * `ifNoneMatch`. The recomputation is what stops a listing that is whole by its own count
482
+ * but not the state its digest names (a shortened listing under the real digest) from being
483
+ * diffed as a mass deletion. Not part of the {@link StorageBackend} seam.
484
+ */
485
+ async heads(options = {}) {
486
+ const headers = {};
487
+ if (options.ifNoneMatch !== undefined)
488
+ headers["If-None-Match"] = etagForm(options.ifNoneMatch);
489
+ const res = await this.send("/heads", { method: "GET", headers });
490
+ if (res.status === 304) {
491
+ // Only a conditional request can be answered `304`; to an unconditional one it is a
492
+ // malformed answer, not "nothing changed", since there is no digest it could be relative to.
493
+ if (options.ifNoneMatch === undefined)
494
+ throw malformed("wire heads answered 304 to a request that sent no If-None-Match");
495
+ return null;
496
+ }
497
+ if (!res.ok)
498
+ throw await this.toError(res, "heads");
499
+ const payload = (await res.json());
500
+ if (!isHeadsDigest(payload?.digest))
501
+ throw malformed("wire heads answered without a well-formed digest");
502
+ if (!Array.isArray(payload.heads) || !payload.heads.every(isDocumentHead)) {
503
+ throw malformed("wire heads answered without a heads array of { id, version } rows");
504
+ }
505
+ if (payload.count !== payload.heads.length) {
506
+ throw malformed(`wire heads count ${String(payload.count)} disagrees with its ${payload.heads.length} row(s)`);
507
+ }
508
+ const heads = payload.heads.map(({ id, version }) => ({ id, version }));
509
+ const recomputed = headsDigest(heads);
510
+ if (recomputed !== payload.digest) {
511
+ throw malformed(`wire heads served digest ${payload.digest}, but its ${heads.length} row(s) digest to ${recomputed}`);
512
+ }
513
+ return { digest: payload.digest, heads };
514
+ }
515
+ /**
516
+ * `GET /snapshot`: the whole bundle in one response. The header line is parsed before this
517
+ * resolves, so a non-2xx or a body without a header rejects here with the usual typed error;
518
+ * the documents then stream through {@link RemoteSnapshot.docs}. Transient retry applies only
519
+ * to obtaining the response: a body cut mid-stream is reported to the consumer as
520
+ * `SNAPSHOT_TRUNCATED`, a whole body whose heads do not digest to the header's announcement as
521
+ * `SNAPSHOT_DIGEST_MISMATCH`, and re-requesting is the consumer's decision. Reserved files are
522
+ * not part of a snapshot. Not part of the {@link StorageBackend} seam.
523
+ */
524
+ async snapshot() {
525
+ const res = await this.send("/snapshot", { method: "GET" });
526
+ if (!res.ok)
527
+ throw await this.toError(res, "snapshot");
528
+ if (!res.body)
529
+ throw new RemoteError("snapshot response carried no body", SNAPSHOT_TRUNCATED, res.status);
530
+ const lines = ndjsonLines(res.body, res.status);
531
+ const first = await lines.next();
532
+ if (first.done)
533
+ throw new RemoteError("snapshot ended before its header line", SNAPSHOT_TRUNCATED, res.status);
534
+ let header;
535
+ try {
536
+ header = parseSnapshotHeader(first.value);
537
+ }
538
+ catch (err) {
539
+ await lines.return(undefined);
540
+ throw err;
541
+ }
542
+ return { header, docs: snapshotDocuments(lines, header, res.status) };
543
+ }
348
544
  /**
349
545
  * `GET /operations/{requestId}`: the authority's recorded outcome for an identified write, or
350
546
  * `null` when it holds nothing under that identity (never recorded, or recorded and expired;
@@ -403,7 +599,7 @@ export class RemoteBackend {
403
599
  const params = new URLSearchParams();
404
600
  if (prefix)
405
601
  params.set("prefix", prefix);
406
- return this.pageDocs(params, (row) => row.id, prefix ?? "");
602
+ return (await this.pageDocs(params, (row) => row.id, prefix ?? "")).sort(compareStorageKeys);
407
603
  }
408
604
  /**
409
605
  * The seam's OPTIONAL head-projection push-down, over the SAME `GET /docs` route
@@ -441,6 +637,8 @@ export class RemoteBackend {
441
637
  : { version: v.version, actor: v.actor, timestamp: v.timestamp, agent: v.agent });
442
638
  }
443
639
  async readReserved(dir, name) {
640
+ assertSafeReservedDir(dir);
641
+ assertSafeReservedFilename(name);
444
642
  const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
445
643
  const res = await this.send(`/reserved/${name}${qs}`, { method: "GET" });
446
644
  if (res.status === 404)
@@ -452,6 +650,8 @@ export class RemoteBackend {
452
650
  return { content: payload.content, version };
453
651
  }
454
652
  async writeReserved(dir, name, content, options = {}) {
653
+ assertSafeReservedDir(dir);
654
+ assertSafeReservedFilename(name);
455
655
  assertValidExpectedVersion(options.expectedVersion);
456
656
  const headers = { "content-type": "application/json" };
457
657
  if (options.expectedVersion === null)
@@ -579,7 +779,7 @@ export class RemoteBackend {
579
779
  break;
580
780
  cursor = payload.next_cursor;
581
781
  }
582
- return keys;
782
+ return keys.sort(compareStorageKeys);
583
783
  }
584
784
  }
585
785
  //# sourceMappingURL=remote-backend.js.map