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.
- package/DEPS.md +2 -1
- package/README.md +51 -0
- package/dist/client.d.ts +31 -4
- package/dist/client.js +25 -2
- package/dist/conformance/client.test.js +7 -7
- package/dist/conformance/fixtures.d.ts +55 -3
- package/dist/conformance/fixtures.js +9 -5
- package/dist/conformance/legacy/replica-012.d.ts +52 -0
- package/dist/conformance/legacy/replica-012.js +403 -0
- package/dist/conformance/outbox-bound.test.d.ts +1 -0
- package/dist/conformance/outbox-bound.test.js +268 -0
- package/dist/conformance/paging-budget.test.d.ts +1 -0
- package/dist/conformance/paging-budget.test.js +101 -0
- package/dist/conformance/paging-fixtures.d.ts +80 -0
- package/dist/conformance/paging-fixtures.js +135 -0
- package/dist/conformance/paging-network.test.d.ts +1 -0
- package/dist/conformance/paging-network.test.js +123 -0
- package/dist/conformance/paging-state.test.d.ts +1 -0
- package/dist/conformance/paging-state.test.js +180 -0
- package/dist/conformance/paging-storage.test.d.ts +1 -0
- package/dist/conformance/paging-storage.test.js +255 -0
- package/dist/conformance/paging.test.d.ts +1 -0
- package/dist/conformance/paging.test.js +209 -0
- package/dist/conformance/pull.d.ts +74 -0
- package/dist/conformance/pull.js +400 -0
- package/dist/conformance/push-bytes.test.d.ts +1 -0
- package/dist/conformance/push-bytes.test.js +174 -0
- package/dist/conformance/push-refusal.test.d.ts +1 -0
- package/dist/conformance/push-refusal.test.js +644 -0
- package/dist/contract.d.ts +19 -6
- package/dist/contract.js +76 -9
- package/dist/server/ledger.d.ts +13 -0
- package/dist/server/ledger.js +18 -0
- package/dist/server/pull.d.ts +85 -0
- package/dist/server/pull.js +324 -0
- package/dist/server/web.d.ts +9 -5
- package/dist/server/web.js +29 -12
- package/dist/sync.d.ts +221 -16
- package/dist/sync.js +565 -96
- package/dist/web/replica.js +93 -16
- package/package.json +17 -1
package/dist/server/web.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
const chunks = [];
|
|
107
|
+
let size = 0;
|
|
100
108
|
for await (const chunk of request) {
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
403
|
-
* whole replica, so a skipped or failed patch is never
|
|
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
|
-
|
|
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
|
};
|