slopkit 0.1.2 → 0.3.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.
Files changed (41) hide show
  1. package/DEPS.md +2 -1
  2. package/README.md +51 -0
  3. package/dist/client.d.ts +31 -4
  4. package/dist/client.js +25 -2
  5. package/dist/conformance/client.test.js +7 -7
  6. package/dist/conformance/fixtures.d.ts +55 -3
  7. package/dist/conformance/fixtures.js +9 -5
  8. package/dist/conformance/legacy/replica-012.d.ts +52 -0
  9. package/dist/conformance/legacy/replica-012.js +403 -0
  10. package/dist/conformance/outbox-bound.test.d.ts +1 -0
  11. package/dist/conformance/outbox-bound.test.js +268 -0
  12. package/dist/conformance/paging-budget.test.d.ts +1 -0
  13. package/dist/conformance/paging-budget.test.js +101 -0
  14. package/dist/conformance/paging-fixtures.d.ts +80 -0
  15. package/dist/conformance/paging-fixtures.js +135 -0
  16. package/dist/conformance/paging-network.test.d.ts +1 -0
  17. package/dist/conformance/paging-network.test.js +123 -0
  18. package/dist/conformance/paging-state.test.d.ts +1 -0
  19. package/dist/conformance/paging-state.test.js +180 -0
  20. package/dist/conformance/paging-storage.test.d.ts +1 -0
  21. package/dist/conformance/paging-storage.test.js +255 -0
  22. package/dist/conformance/paging.test.d.ts +1 -0
  23. package/dist/conformance/paging.test.js +209 -0
  24. package/dist/conformance/pull.d.ts +74 -0
  25. package/dist/conformance/pull.js +400 -0
  26. package/dist/conformance/push-bytes.test.d.ts +1 -0
  27. package/dist/conformance/push-bytes.test.js +174 -0
  28. package/dist/conformance/push-refusal.test.d.ts +1 -0
  29. package/dist/conformance/push-refusal.test.js +644 -0
  30. package/dist/contract.d.ts +19 -6
  31. package/dist/contract.js +76 -9
  32. package/dist/server/ledger.d.ts +13 -0
  33. package/dist/server/ledger.js +18 -0
  34. package/dist/server/pull.d.ts +85 -0
  35. package/dist/server/pull.js +324 -0
  36. package/dist/server/web.d.ts +9 -5
  37. package/dist/server/web.js +29 -12
  38. package/dist/sync.d.ts +221 -16
  39. package/dist/sync.js +565 -96
  40. package/dist/web/replica.js +93 -16
  41. package/package.json +17 -1
@@ -5,7 +5,7 @@
5
5
  * from the page's own Origin. Responses carry a strict CSP and no framing.
6
6
  *
7
7
  * Routes are contract endpoints: the request is checked against its schema before the handler runs and the reply
8
- * after, so neither side can drift from the contract. Failures answer `{ kind, message }` with the kind's status.
8
+ * after, so neither side can drift from the contract. Failures answer `{ kind, message, path? }` with the kind's status.
9
9
  * The socket is a hint channel: the app sends small messages (pokes, pings); nothing correct depends on it.
10
10
  * Every API request is logged with an id, status, time and whether a person or a service asked.
11
11
  */
@@ -32,12 +32,17 @@ const SECURITY = {
32
32
  "x-frame-options": "DENY",
33
33
  "content-security-policy": "default-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; manifest-src 'self'; worker-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
34
34
  };
35
- /** A handler's refusal, answered as a typed failure. */
35
+ /**
36
+ * A handler's refusal, answered as a typed failure: the kit's error body, so the page can tell it from a proxy's.
37
+ * `path` names where a request broke its schema (`request.commands[1].command.value`).
38
+ */
36
39
  export class Refusal extends Error {
37
40
  kind;
38
- constructor(kind, message) {
41
+ path;
42
+ constructor(kind, message, path) {
39
43
  super(message);
40
44
  this.kind = kind;
45
+ this.path = path;
41
46
  }
42
47
  }
43
48
  export function route(endpoint, handle) {
@@ -52,7 +57,7 @@ export function route(endpoint, handle) {
52
57
  }
53
58
  catch (error) {
54
59
  if (error instanceof Invalid) {
55
- throw new Refusal("invalid", error.message);
60
+ throw new Refusal("invalid", error.message, error.path);
56
61
  }
57
62
  throw error;
58
63
  }
@@ -90,18 +95,30 @@ function send(request, response, status, body, headers = {}) {
90
95
  });
91
96
  response.end(request.method === "HEAD" ? undefined : bytes);
92
97
  }
93
- function failure(request, response, kind, message) {
94
- send(request, response, STATUS[kind], JSON.stringify({ kind, message }), {
95
- "content-type": "application/json",
96
- });
98
+ function failure(request, response, kind, message, path) {
99
+ send(request, response, STATUS[kind], JSON.stringify({ kind, message, ...(path === undefined ? {} : { path }) }), { "content-type": "application/json" });
97
100
  }
101
+ /**
102
+ * The body as JSON: its bytes collected and counted against `limit`, then decoded once, so a character split between
103
+ * chunks arrives whole. Bytes that aren't UTF-8 are refused, never replaced.
104
+ */
98
105
  async function readBody(request, limit) {
99
- let text = "";
106
+ const chunks = [];
107
+ let size = 0;
100
108
  for await (const chunk of request) {
101
- text += String(chunk);
102
- if (text.length > limit) {
109
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
110
+ size += bytes.length;
111
+ if (size > limit) {
103
112
  throw new Refusal("invalid", "the request is too large");
104
113
  }
114
+ chunks.push(bytes);
115
+ }
116
+ let text;
117
+ try {
118
+ text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
119
+ }
120
+ catch {
121
+ throw new Refusal("invalid", "the request is not UTF-8");
105
122
  }
106
123
  try {
107
124
  return text ? JSON.parse(text) : {};
@@ -212,7 +229,7 @@ export async function serveWeb(options) {
212
229
  reason: message,
213
230
  });
214
231
  }
215
- failure(request, response, kind, error instanceof Refusal ? message : "the server failed; see its log");
232
+ failure(request, response, kind, error instanceof Refusal ? message : "the server failed; see its log", error instanceof Refusal ? error.path : undefined);
216
233
  }
217
234
  log(status < 400 ? "info" : "warn", "request", {
218
235
  request: caller.request,
package/dist/sync.d.ts CHANGED
@@ -5,23 +5,47 @@
5
5
  * headless simulation over real stores and in the browser alike.
6
6
  *
7
7
  * Invariants it owns:
8
- * - **The replica only moves forward.** A pull applies only if newer (or a reset for another dataset). The disk commit
9
- * is decided again against what the disk holds, inside its own transaction (`commitPlan`), so another tab or a
10
- * failed write can't make the disk go back or skip a patch.
8
+ * - **The replica only moves forward.** A delta applies only to the same dataset with `since ≤ copy.revision ≤
9
+ * reply.revision`; only a reset replaces the copy (a late one is dropped), and a reply it can't follow asks for a
10
+ * real reset instead of leaving a gap. The disk commit is decided again against what the disk holds, inside its own
11
+ * transaction (`commitPlan`), so another tab or a failed write can't make the disk go back or skip a patch.
12
+ * - **A copy never claims more history than it holds.** It is complete or partial (`History`): every row it holds is
13
+ * the server's row at its revision, and a partial copy holds every row at or above its cursor, so every row at or
14
+ * above `through`. The first reply draws the newest rows at once; the cursor moves only when a reply answers the
15
+ * copy's own `next`, a reply without a page never changes it, and a row merged below `through` never deepens it.
16
+ * The disk takes a page's history only from the cursor it moved from, in the transaction that writes its rows.
17
+ * - **A partial copy owes its next page**, back to back, until it is complete; a reply that doesn't move it on counts
18
+ * as a failed pull, for backoff. An old server's reply to a partial copy asks for its whole copy instead.
19
+ * - **Only a complete copy is checked.** No fingerprint is asked for or compared while the copy is partial.
20
+ * - **A chunk costs its own size.** Merging a reply doesn't copy the tables it lands in (`Layered`).
11
21
  * - **Observed is not saved.** `saved` is what the disk is known to hold; a failed commit is retried whole until the
12
- * disk catches up, and until then the page says so.
22
+ * disk catches up (a later page the disk takes as a delta under its own history doesn't), and until then the page
23
+ * says so.
13
24
  * - **A pull is owed while the server is ahead.** `target` is the newest revision the server is known to have (pokes,
14
25
  * which also beat every 20 s, push replies, receipts); while the replica is behind it a pull is in flight or
15
26
  * scheduled, whatever the socket says. Liveness, the known head and the last reconciliation are separate facts.
16
27
  * - **A command id is never reused.** Each page load is a new client with its own numbers; commands loaded from an
17
28
  * earlier page keep theirs.
18
29
  * - **Nothing is sent before it is kept.** An intent the device can't keep fails at once, visibly, and is never sent.
30
+ * A push is journalled before it goes: its commands are kept as `sending` (one write per push), so a page that dies
31
+ * mid-push leaves them uncertain on disk, and none goes unless that write lands.
19
32
  * - **Every command reaches an outcome, and is settled only once the replica holds its effects** (a receipt names the
20
- * revision that made it visible); until then a pull is owed.
33
+ * revision that made it visible; on a partial copy, only a pull reply that carried the receipt, with the rows it
34
+ * names, settles it); until then a pull is owed, and names the commands it waits on, any page's.
21
35
  * - **Every waiting state ends.** Pulling, pushing, retrying and saving have deadlines or backoff; storage that is slow
22
36
  * to load or keep is said (the page goes on from the server meanwhile), and its late answer still counts: a slow
23
37
  * write is not a failed one.
24
38
  * - **An intent belongs to the dataset it was made against**: the server refuses it against a replacement.
39
+ * - **A refusal proves only that attempt didn't act.** Only the kit's own error body is final; anything else may have
40
+ * acted and leaves the command unknown, kept so. A command an attempt of which may have acted never becomes a final
41
+ * failure: a refusal naming it parks it (never sent again, still named in pulls, done when a receipt comes). A schema
42
+ * refusal naming one command of a push is that command's alone; the rest return to what they were and go again.
43
+ * - **A push fits the server's bounds**: at most `pushMax` commands and `pushBytes` bytes, oldest first, stopping at
44
+ * the first command that doesn't fit. One too large even alone is answered at the tap and never kept; one already
45
+ * kept is refused (or parked) and passed over, so it never strands the rest.
46
+ * - **The outbox is bounded across tabs.** The store admits a new command inside its own write transaction, from its
47
+ * own count; one it has no room for is answered and never retained. Kept commands always update and are never
48
+ * dropped for room.
25
49
  */
26
50
  import type { Failure } from "./contract.ts";
27
51
  import type { Schema } from "./schema.ts";
@@ -65,12 +89,39 @@ export declare function table<T, P, R>(def: {
65
89
  get: (tables: T) => ReadonlyMap<Key, R>;
66
90
  set: (tables: T, rows: ReadonlyMap<Key, R>) => T;
67
91
  }): Lens<T, P>;
92
+ /**
93
+ * How much of the server's history a copy holds. Complete: every row. Partial: every row at or above the keyset cursor
94
+ * `next` (the server's opaque cursor of the next older page), so every row at or above `through` (the fully covered
95
+ * boundary, as the app formats it; null while no order group is whole); `held` of `total` is progress, not a claim.
96
+ */
97
+ export type History = {
98
+ kind: "complete";
99
+ } | {
100
+ kind: "partial";
101
+ next: string;
102
+ through: string | null;
103
+ held: number;
104
+ total: number;
105
+ };
106
+ /**
107
+ * A paged reply's place in the history: `from` is the cursor it answered (null on a reset's first page), `next` the
108
+ * next older one (null once complete), `through`, `held` and `total` as in `History`, at the reply's revision.
109
+ */
110
+ export type Page = {
111
+ from: string | null;
112
+ next: string | null;
113
+ through: string | null;
114
+ held: number;
115
+ total: number;
116
+ };
68
117
  export type Replica<T, S> = {
69
118
  dataset: string | null;
70
119
  revision: number;
71
120
  tables: T;
72
121
  /** What the server says about itself, read with every pull. */
73
122
  server: S | null;
123
+ /** How much of the history it holds; absent (a copy from before paging) is complete. */
124
+ history?: History;
74
125
  };
75
126
  /** A pull reply as sync sees it: rows changed since the pull's `since`, or everything (`reset`). */
76
127
  export type Patch<P, S> = {
@@ -80,12 +131,18 @@ export type Patch<P, S> = {
80
131
  rows: P;
81
132
  server: S;
82
133
  checksum?: string;
134
+ /** A paging server's place in the history: absent from an old server, whose reset is the whole copy. */
135
+ page?: Page;
83
136
  };
84
- /** A command's outcome as the server recorded it, and the revision at which its effects are visible. */
137
+ /**
138
+ * A command's outcome as the server recorded it, and the revision at which its effects are visible. `client` names an
139
+ * earlier page's command (absent: the pull's or push's own client).
140
+ */
85
141
  export type Receipt<O> = {
86
142
  id: number;
87
143
  outcome: O;
88
144
  revision: number;
145
+ client?: string;
89
146
  };
90
147
  export type CommandState<O> = {
91
148
  kind: "saving";
@@ -93,9 +150,16 @@ export type CommandState<O> = {
93
150
  kind: "queued";
94
151
  } | {
95
152
  kind: "sending";
96
- } | {
153
+ }
154
+ /**
155
+ * An attempt's outcome was lost: it may have acted. `parked`: the server refused it by name since, which proves only
156
+ * that attempt didn't act, so it is never sent again and waits on a pull to bring the receipt an earlier one left; it
157
+ * needs the person's attention meanwhile.
158
+ */
159
+ | {
97
160
  kind: "unknown";
98
161
  failure: string;
162
+ parked?: true;
99
163
  } | {
100
164
  kind: "done";
101
165
  outcome: O;
@@ -131,6 +195,22 @@ export type Connection = {
131
195
  kind: "incompatible";
132
196
  failure: string;
133
197
  };
198
+ /**
199
+ * The outbox's room, as a closed subject for the app's state table: full when it holds `max` commands. `waiting`
200
+ * (unsent or unknown) needs a connection; `finished` (done, kept until dismissed) needs the person to dismiss them.
201
+ */
202
+ export type OutboxState = {
203
+ kind: "ok";
204
+ } | {
205
+ kind: "full";
206
+ waiting: number;
207
+ finished: number;
208
+ max: number;
209
+ };
210
+ /** The store's answer to a new command it had no room for: its own counts, across every tab. */
211
+ export type OutboxFull = Extract<OutboxState, {
212
+ kind: "full";
213
+ }>;
134
214
  /** This device's copy: fine, failing (writes are retried), or forgotten under this page by another tab. */
135
215
  export type Storage = {
136
216
  kind: "ok";
@@ -160,8 +240,18 @@ export type Model<T, S, C, O> = {
160
240
  writing: number;
161
241
  failures: number;
162
242
  nextAt: number;
243
+ /**
244
+ * A write failed and only a whole one (or one the disk took whole) repairs it: a later page landing as a delta
245
+ * doesn't, since the disk still lacks the failed page's rows.
246
+ */
247
+ owed: boolean;
163
248
  };
164
249
  target: number;
250
+ /**
251
+ * A reset this copy asks for: `paged` after a reply it couldn't apply (another dataset without a reset, a gap),
252
+ * `whole` (no `page`, as an old server understands) after an old server answered a partial copy.
253
+ */
254
+ restart: "paged" | "whole" | null;
165
255
  /** When a pull last succeeded: freshness, apart from the socket's liveness. */
166
256
  reconciledAt: number | null;
167
257
  connection: Connection;
@@ -186,9 +276,23 @@ export type Model<T, S, C, O> = {
186
276
  kind: "pushing";
187
277
  client: string;
188
278
  ids: readonly number[];
279
+ /** Each command's state before this attempt, by position in `ids`: what a refusal restores. */
280
+ was: readonly CommandState<O>[];
189
281
  failures: number;
190
282
  };
191
283
  commands: readonly Pending<C, O>[];
284
+ /**
285
+ * The most commands the outbox admits, and the store's counts when it last refused one for room (it is shared with
286
+ * other tabs, whose commands this page doesn't hold); cleared once one is admitted or this page drops some.
287
+ */
288
+ outbox: {
289
+ max: number;
290
+ full: OutboxFull | null;
291
+ };
292
+ /** Where the next pull's list of waiting commands starts, when more wait than one pull names. */
293
+ asking: number;
294
+ /** Commands (`client:id`) whose receipt came in a pull reply this copy applied, so with the rows it names. */
295
+ carried: ReadonlySet<string>;
192
296
  /** Commands an earlier page kept that this version can't read: left on disk, and said. */
193
297
  unreadable: number;
194
298
  visible: boolean;
@@ -242,11 +346,24 @@ export type Msg<T, P, S, C, O> = {
242
346
  failure: Failure;
243
347
  now: number;
244
348
  }
245
- /** The disk holds the replica at `revision` (perhaps newer than asked, from another tab). */
349
+ /** A push's in-flight mark couldn't be kept, so it was never sent. */
350
+ | {
351
+ type: "unsent";
352
+ client: string;
353
+ ids: number[];
354
+ reason: string;
355
+ replaced: boolean;
356
+ now: number;
357
+ }
358
+ /**
359
+ * The disk holds the replica at `revision` (perhaps newer than asked, from another tab); `whole: false` when it took
360
+ * the patch as a delta under its own, other history.
361
+ */
246
362
  | {
247
363
  type: "committed";
248
364
  dataset: string;
249
365
  revision: number;
366
+ whole?: boolean;
250
367
  now: number;
251
368
  } | {
252
369
  type: "commitFailed";
@@ -265,6 +382,13 @@ export type Msg<T, P, S, C, O> = {
265
382
  reason: string;
266
383
  now: number;
267
384
  }
385
+ /** The outbox had no room for a new command: it was not kept. */
386
+ | ({
387
+ type: "full";
388
+ client: string;
389
+ id: number;
390
+ now: number;
391
+ } & Omit<OutboxFull, "kind">)
268
392
  /** Finished commands left the outbox (`failure` if not: they fade again on the next load). */
269
393
  | {
270
394
  type: "dropped";
@@ -311,6 +435,8 @@ export type Commit<T, S> = {
311
435
  reset: boolean;
312
436
  /** The revision the patch starts from: the disk takes it as a delta only if it is at least there. */
313
437
  base: number;
438
+ /** The cursor the copy held before this patch: the disk adopts the patch's history only from that same cursor. */
439
+ from?: string | null;
314
440
  /** The dataset this page last knew the disk to hold: the only one it may replace with another. */
315
441
  replaces: string | null;
316
442
  };
@@ -322,6 +448,13 @@ export type Effect<T, S, C, O> = {
322
448
  dataset: string | null;
323
449
  since: number;
324
450
  verify: boolean;
451
+ /** The older page wanted (null: none, but a paged reset is understood); absent asks an old server's whole reply. */
452
+ page?: string | null;
453
+ /** Commands still waiting on an outcome or on their effects, any page's: their receipts come with their rows. */
454
+ commands: {
455
+ client: string;
456
+ id: number;
457
+ }[];
325
458
  } | {
326
459
  kind: "push";
327
460
  client: string;
@@ -337,9 +470,12 @@ export type Effect<T, S, C, O> = {
337
470
  }[];
338
471
  } | ({
339
472
  kind: "commit";
340
- } & Commit<T, S>) | {
473
+ } & Commit<T, S>)
474
+ /** Keep a command: a new one only while the outbox holds fewer than `max` (decided by the store). */
475
+ | {
341
476
  kind: "keep";
342
477
  pending: Pending<C, O>;
478
+ max: number;
343
479
  } | {
344
480
  kind: "drop";
345
481
  keys: {
@@ -380,6 +516,13 @@ export type Spec<T, P, S, C, O> = {
380
516
  resolved?: (pending: Pending<C, O>, model: Model<T, S, C, O>) => boolean;
381
517
  /** The most commands one push carries: the push endpoint's own bound. Default `PUSH_MAX`. */
382
518
  pushMax?: number;
519
+ /**
520
+ * The most UTF-8 bytes of one push request, encoded whole: the push endpoint's body limit (`serveWeb.bodyLimit`)
521
+ * must be at least this. Default `PUSH_BYTES`.
522
+ */
523
+ pushBytes?: number;
524
+ /** The most commands the device's outbox admits, shared by every tab. Default `OUTBOX_MAX`. */
525
+ outboxMax?: number;
383
526
  };
384
527
  /** The socket must be heard this often (the server pokes every 20 s); quieter, the page polls. */
385
528
  export declare const SOCKET_DEADLINE_MS = 45000;
@@ -389,37 +532,99 @@ export declare const POLL_MS = 4000;
389
532
  export declare const STORAGE_DEADLINE_MS = 3000;
390
533
  /** Commands per push unless the spec says otherwise; the rest wait for the next push. */
391
534
  export declare const PUSH_MAX = 50;
535
+ /** UTF-8 bytes of one push request unless the spec says otherwise (48 KiB). */
536
+ export declare const PUSH_BYTES = 49152;
537
+ /** Commands the outbox admits unless the spec says otherwise; updates to kept ones always are. */
538
+ export declare const OUTBOX_MAX = 500;
392
539
  /** Settled commands stay drawn this long. */
393
540
  export declare const DONE_KEEP_MS = 60000;
541
+ /**
542
+ * Where the outbox stands: full once it holds `max` commands, by this page's count or, failing that, by the store's
543
+ * last refusal (other tabs' commands count too). Unreadable kept commands take room as well.
544
+ */
545
+ export declare function deriveOutboxState<T, S, C, O>(model: Model<T, S, C, O>): OutboxState;
394
546
  /** Whether the server can be asked now. */
395
547
  export declare function reachable(connection: Connection): boolean;
396
- /** Whether the replica holds this command's effects. */
548
+ /**
549
+ * Whether the replica holds this command's effects: it is done, and the copy is at its revision and either complete or
550
+ * holding the rows its receipt names (a pull reply carried them). One that failed on the page has none.
551
+ */
397
552
  export declare function settled<T, S, C, O>(model: Model<T, S, C, O>, pending: Pending<C, O>): boolean;
398
- /** Whether what the page shows is also on the disk. */
553
+ /**
554
+ * Whether what the page shows is also on the disk: by revision only, so tabs at different cursors never chase each
555
+ * other's writes. A failed write is owed again even at the same revision (a page that failed to land).
556
+ */
399
557
  export declare function saved<T, S, C, O>(model: Model<T, S, C, O>): boolean;
400
558
  /**
401
559
  * What a commit does to the disk, decided inside the disk's own transaction from what it holds: nothing when the disk
402
- * already has this or newer (another tab got there), the patch alone when the disk is at least at its base, else the
403
- * whole replica, so a skipped or failed patch is never covered by a later checkpoint. A page may replace the disk's
560
+ * already has this or newer (another tab got there; a complete copy at the same revision is newer than any page), the
561
+ * patch alone when the disk is at least at its base, else the whole replica, so a skipped or failed patch is never
562
+ * covered by a later checkpoint. A delta keeps the disk's own history, and takes the patch's only when the disk is at
563
+ * the cursor the patch moved from (`adopt`): coverage never lands without its rows. A page may replace the disk's
404
564
  * dataset only with one it moved to from that very dataset (`replaces`): a tab still on a dataset the server has since
405
565
  * replaced is `stale` and never writes it back.
406
566
  */
407
567
  export declare function commitPlan(stored: {
408
568
  dataset: string | null;
409
569
  revision: number;
570
+ history?: History;
410
571
  }, commit: {
411
572
  dataset: string | null;
412
573
  revision: number;
413
574
  reset: boolean;
414
575
  base: number;
415
576
  replaces: string | null;
416
- }): "skip" | "delta" | "whole" | "stale";
577
+ from?: string | null;
578
+ }): "skip" | "delta" | "adopt" | "whole" | "stale";
579
+ /** How much history the page holds, as a closed subject for the app's state table. */
580
+ export type HistoryState = {
581
+ kind: "loading";
582
+ } | {
583
+ kind: "none";
584
+ } | {
585
+ kind: "complete";
586
+ } | {
587
+ kind: "partial";
588
+ through: string | null;
589
+ held: number;
590
+ total: number;
591
+ };
592
+ /**
593
+ * The copy's history: loading while storage loads, none without a dataset, else partial or complete. Partial is a claim
594
+ * beside the numbers ("history from <through>, loading older · held of total"), and totals cover only `through` and
595
+ * above. Complete says nothing about storage: that is `deriveDurabilityState`, its own subject.
596
+ */
597
+ export declare function deriveHistoryState<T, S, C, O>(model: Model<T, S, C, O>): HistoryState;
598
+ /** Whether the page's copy is kept on this device, as a closed subject apart from how much history it holds. */
599
+ export type DurabilityState = {
600
+ kind: "loading";
601
+ } | {
602
+ kind: "unsaved";
603
+ } | {
604
+ kind: "lost";
605
+ reason: string;
606
+ } | {
607
+ kind: "failing";
608
+ reason: string;
609
+ } | {
610
+ kind: "replaced";
611
+ } | {
612
+ kind: "unreadable";
613
+ count: number;
614
+ } | {
615
+ kind: "ok";
616
+ };
617
+ /**
618
+ * The copy's durability at the model's clock: loading, replaced by another tab, a failing store (lost while the page
619
+ * shows more than the disk holds), unsaved past the storage deadline, commands kept that this version can't read, or ok.
620
+ */
621
+ export declare function deriveDurabilityState<T, S, C, O>(model: Model<T, S, C, O>): DurabilityState;
417
622
  export declare function createSync<T, P, S, C, O>(spec: Spec<T, P, S, C, O>): {
418
623
  initial: (client: string, now: number) => Model<T, S, C, O>;
419
624
  update: (model: Model<T, S, C, O>, msg: Msg<T, P, S, C, O>) => readonly [Model<T, S, C, O>, readonly Effect<T, S, C, O>[]];
420
- merge: (replica: Replica<T, S>, patch: Patch<P, S>) => {
625
+ merge: (replica: Replica<T, S>, patch: Patch<P, S>, since: number) => {
421
626
  replica: Replica<T, S>;
422
627
  ops: Op[];
423
628
  reset: boolean;
424
- } | undefined;
629
+ } | "gap" | undefined;
425
630
  };