slopkit 0.1.1 → 0.2.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.
@@ -11,7 +11,11 @@
11
11
  * - Rows are read back only under the row shapes that wrote them (each table's name and schema `shape`, kept with the
12
12
  * checkpoint): any change to a table or its schema starts the rows over, with no number to bump by hand. The next
13
13
  * commit then writes the whole replica, so the disk is repaired, not just the memory. Rows that don't parse are
14
- * treated the same. The server's facts are not rows: facts an older page kept that no longer fit load as null and
14
+ * treated the same.
15
+ * - A partial copy (a first copy still paging in) keeps its history in the checkpoint, written in the same transaction
16
+ * as its rows, and marks its row shapes as partial: a release from before paging reads it as another release's copy,
17
+ * so it never takes part of a history for all of it. A delta takes the patch's history only when the disk is at the
18
+ * cursor the patch moved from, and a complete copy at a revision is never narrowed to a partial one. The server's facts are not rows: facts an older page kept that no longer fit load as null and
15
19
  * never cost the copy; facts that change at the same revision are written too.
16
20
  * - A version upgrade starts the replica over (it comes back from the server) but keeps the outbox and the generation:
17
21
  * an intent on disk is never erased by an upgrade. One this version can't read stays on disk and is counted.
@@ -21,12 +25,24 @@
21
25
  * in-memory one). `interfere` is for tests: it sees each write transaction before it commits and may abort it.
22
26
  */
23
27
  import { Replaced } from "../client.js";
24
- import { check } from "../schema.js";
28
+ import { check, literal, nullable, number, object, string } from "../schema.js";
25
29
  import { commitPlan } from "../sync.js";
26
30
  import { committed, openDatabase, request } from "./idb.js";
31
+ const COMPLETE = { kind: "complete" };
32
+ const count = () => number({ integer: true, min: 0 });
33
+ const partialSchema = object({
34
+ kind: literal("partial"),
35
+ next: string(),
36
+ through: nullable(string()),
37
+ held: count(),
38
+ total: count(),
39
+ });
27
40
  export function indexedReplica(options) {
28
41
  const names = options.tables.map((lens) => lens.name);
29
42
  const shapes = options.tables.map((lens) => lens.shape).join("\n");
43
+ /** A partial copy's row shapes: foreign to a release from before paging. */
44
+ const partialShapes = `${shapes}\npartial`;
45
+ const ours = (written) => written === shapes || written === partialShapes;
30
46
  /** The checkpoint a load found whose rows didn't parse: the next commit at it rewrites the whole replica. */
31
47
  let damaged;
32
48
  /** The dataset of a copy this page loaded but couldn't read (other row shapes, rows that don't parse): its own to replace. */
@@ -142,22 +158,26 @@ export function indexedReplica(options) {
142
158
  }
143
159
  /** The checkpoint on disk as this version may trust it: one of other row shapes (or none) holds nothing. */
144
160
  const checkpointOf = (value) => {
161
+ const none = { dataset: null, revision: 0, history: COMPLETE };
145
162
  if (typeof value !== "object" || value === null) {
146
- return { dataset: null, revision: 0 };
163
+ return none;
147
164
  }
148
165
  const dataset = Reflect.get(value, "dataset");
149
166
  const revision = Reflect.get(value, "revision");
150
- if (Reflect.get(value, "shapes") !== shapes) {
151
- return { dataset: null, revision: 0 };
167
+ const written = Reflect.get(value, "shapes");
168
+ const partial = check(partialSchema, Reflect.get(value, "history"));
169
+ if (written !== shapes && (written !== partialShapes || !partial.ok)) {
170
+ return none;
152
171
  }
153
172
  const at = {
154
173
  dataset: typeof dataset === "string" ? dataset : null,
155
174
  revision: typeof revision === "number" ? revision : 0,
175
+ history: written === partialShapes && partial.ok ? partial.value : COMPLETE,
156
176
  };
157
177
  if (damaged !== undefined &&
158
178
  damaged.dataset === at.dataset &&
159
179
  damaged.revision === at.revision) {
160
- return { dataset: null, revision: 0 };
180
+ return none;
161
181
  }
162
182
  return at;
163
183
  };
@@ -197,6 +217,7 @@ export function indexedReplica(options) {
197
217
  revision: 0,
198
218
  tables: options.empty,
199
219
  server: null,
220
+ history: COMPLETE,
200
221
  };
201
222
  const at = checkpointOf(point);
202
223
  const raw = typeof point === "object" && point !== null ? Reflect.get(point, "dataset") : null;
@@ -232,6 +253,7 @@ export function indexedReplica(options) {
232
253
  revision: at.revision,
233
254
  tables: restored,
234
255
  server: server?.ok === true ? server.value : null,
256
+ history: at.history,
235
257
  },
236
258
  commands,
237
259
  unreadable,
@@ -246,7 +268,6 @@ export function indexedReplica(options) {
246
268
  dataset: commit.replica.dataset,
247
269
  revision: commit.replica.revision,
248
270
  server: commit.replica.server,
249
- shapes,
250
271
  };
251
272
  try {
252
273
  await guard(transaction);
@@ -267,6 +288,7 @@ export function indexedReplica(options) {
267
288
  reset: commit.reset,
268
289
  base: commit.base,
269
290
  replaces: commit.replaces,
291
+ ...(commit.from === undefined ? {} : { from: commit.from }),
270
292
  });
271
293
  // The disk holds a dataset this page never saw: the server replaced the one this page is on.
272
294
  if (plan === "stale") {
@@ -278,7 +300,7 @@ export function indexedReplica(options) {
278
300
  // Another release's tab wrote this dataset under its own row shapes since this page loaded: that release owns
279
301
  // the copy now, at any revision, and this page stops writing until it reloads.
280
302
  if (held === commit.replica.dataset &&
281
- written !== shapes &&
303
+ !ours(written) &&
282
304
  (loaded === undefined ||
283
305
  loaded.dataset !== held ||
284
306
  loaded.revision !== revision ||
@@ -305,7 +327,7 @@ export function indexedReplica(options) {
305
327
  }
306
328
  }
307
329
  };
308
- if (plan === "delta") {
330
+ if (plan === "delta" || plan === "adopt") {
309
331
  write(commit.ops);
310
332
  }
311
333
  if (plan === "whole") {
@@ -318,8 +340,16 @@ export function indexedReplica(options) {
318
340
  }
319
341
  // At the same revision the rows are already there, but the server's facts may have moved: they are kept too.
320
342
  const facts = plan === "skip" && stored.revision === target.revision;
343
+ // A delta keeps the disk's history; a whole write or a delta from the disk's own cursor takes the page's.
344
+ const history = plan === "whole" || plan === "adopt"
345
+ ? (commit.replica.history ?? COMPLETE)
346
+ : stored.history;
347
+ const checkpoint = {
348
+ ...target,
349
+ ...(history.kind === "partial" ? { history, shapes: partialShapes } : { shapes }),
350
+ };
321
351
  if (plan !== "skip" || facts) {
322
- meta.put(target, "checkpoint");
352
+ meta.put(checkpoint, "checkpoint");
323
353
  }
324
354
  options.interfere?.("commit", transaction);
325
355
  await done;
@@ -328,11 +358,16 @@ export function indexedReplica(options) {
328
358
  discarded = null;
329
359
  loaded = undefined;
330
360
  }
331
- const now = plan === "skip" ? stored : target;
361
+ const now = plan === "skip" ? stored : checkpoint;
332
362
  if (now.dataset === null) {
333
363
  throw new Error("the replica has no dataset");
334
364
  }
335
- return { dataset: now.dataset, revision: now.revision };
365
+ // A delta under the disk's own history leaves out rows of this page's copy: said, so a failed write stays owed.
366
+ return {
367
+ dataset: now.dataset,
368
+ revision: now.revision,
369
+ ...(plan === "delta" && stored.history.kind === "partial" ? { whole: false } : {}),
370
+ };
336
371
  }
337
372
  catch (error) {
338
373
  await done.catch(() => undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopkit",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Opinionated standards and libraries for fast, local-first personal software: state machines, exhaustive state tables, sync, a command ledger, jobs, a guarded server, and the conformance suite that proves them",
5
5
  "keywords": [
6
6
  "conformance",
@@ -82,6 +82,10 @@
82
82
  "types": "./dist/server/jobs.d.ts",
83
83
  "default": "./dist/server/jobs.js"
84
84
  },
85
+ "./server/pull": {
86
+ "types": "./dist/server/pull.d.ts",
87
+ "default": "./dist/server/pull.js"
88
+ },
85
89
  "./server/web": {
86
90
  "types": "./dist/server/web.d.ts",
87
91
  "default": "./dist/server/web.js"
@@ -118,6 +122,18 @@
118
122
  "types": "./dist/conformance/fixtures.d.ts",
119
123
  "default": "./dist/conformance/fixtures.js"
120
124
  },
125
+ "./conformance/legacy/replica-012": {
126
+ "types": "./dist/conformance/legacy/replica-012.d.ts",
127
+ "default": "./dist/conformance/legacy/replica-012.js"
128
+ },
129
+ "./conformance/paging-fixtures": {
130
+ "types": "./dist/conformance/paging-fixtures.d.ts",
131
+ "default": "./dist/conformance/paging-fixtures.js"
132
+ },
133
+ "./conformance/pull": {
134
+ "types": "./dist/conformance/pull.d.ts",
135
+ "default": "./dist/conformance/pull.js"
136
+ },
121
137
  "./package.json": "./package.json"
122
138
  },
123
139
  "dependencies": {