snapback4 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -206,7 +206,11 @@ The horizon `last N by index` bounds what a device acquires per group;
206
206
  older rows are online-only, and a device's read past them says
207
207
  `complete: false`. Joining a group delivers its rows; leaving it delivers
208
208
  one scope tombstone, and the table's `retain` says what the device keeps.
209
- A table without `sync` is online-only: its queries run on the server.
209
+ What it keeps under `delivered-history` it can still read locally through
210
+ a query without a membership `require` (the server refuses the same query
211
+ once the membership is gone, so a "history" query answers on the device
212
+ and is empty online). A table without `sync` is online-only: its queries
213
+ run on the server.
210
214
 
211
215
  ## 4. The client
212
216
 
@@ -308,7 +312,8 @@ flips the link.
308
312
  - **`snapback4 run <op> '<json args>' --as alice [--json]`** runs one
309
313
  operation against the local store and prints the result and the meter.
310
314
  - **`snapback4 test [--fixture dir] [--as persona]`** loads a fixture
311
- (one `<table>.jsonl` per table), runs every query as each persona with
315
+ (one `<table>.jsonl` per table) into a fresh store in memory (without a
316
+ fixture, the project's dev store), runs every query as each persona with
312
317
  arguments derived from the data (a reference is the table's first row,
313
318
  a principal the persona, a text the shortest that fits), prints the
314
319
  meters beside the ceilings and the arguments it used, and names any
package/dist/local.js CHANGED
@@ -190,11 +190,27 @@ export async function createLocalClient(options) {
190
190
  return store.read(async (tx) => (await tx.side("outbox").all()).map((e) => e.value).sort((a, b) => (a.id < b.id ? -1 : 1)));
191
191
  }
192
192
  let flushing = false;
193
+ let flushAgain = false;
193
194
  async function flush() {
194
- if (flushing)
195
+ // A write queued while a flush runs is flushed by the same flush, on
196
+ // its next pass: nothing waits for the next link change.
197
+ if (flushing) {
198
+ flushAgain = true;
195
199
  return;
200
+ }
196
201
  flushing = true;
197
202
  try {
203
+ do {
204
+ flushAgain = false;
205
+ await flushOnce();
206
+ } while (flushAgain && link === "online" && !closed);
207
+ }
208
+ finally {
209
+ flushing = false;
210
+ }
211
+ }
212
+ async function flushOnce() {
213
+ {
198
214
  for (const entry of await queued()) {
199
215
  if (link !== "online")
200
216
  return;
@@ -228,9 +244,6 @@ export async function createLocalClient(options) {
228
244
  void rerun(watch);
229
245
  }
230
246
  }
231
- finally {
232
- flushing = false;
233
- }
234
247
  }
235
248
  const settled = new Map();
236
249
  const client = {
@@ -91,7 +91,12 @@ export declare class Interpreter {
91
91
  * counted. */
92
92
  private visibility;
93
93
  private ruleTrue;
94
- /** The read rule for one row, with probes over the replica. */
94
+ /** Whether the viewer may read a held row. A synced table's rows are
95
+ * the partition: the server delivered each under the read rule and
96
+ * withdrew what it revoked, so what the device holds it may read — and
97
+ * what `retain delivered-history` kept after a leave stays readable here
98
+ * even though the server now refuses it. Only an online-only table's
99
+ * rows (none, unless predicted) meet the rule on the device. */
95
100
  readable(table: string, row: Row): Promise<boolean>;
96
101
  private rule;
97
102
  private permitted;
@@ -625,7 +625,9 @@ export class Interpreter {
625
625
  this.env.set(lane.binding, source);
626
626
  await this.statements(lane.setup ?? [], merge.site);
627
627
  this.pageDepth++;
628
- const rows = await this.scan(lane.scan);
628
+ // One row past the page per lane, so a lane that alone fills the
629
+ // page still shows there is more (as on the server).
630
+ const rows = await this.scan({ ...lane.scan, take: { Literal: take + 1 } });
629
631
  this.pageDepth = depth;
630
632
  for (const row of rows) {
631
633
  if (seen.has(row.id))
@@ -703,9 +705,17 @@ export class Interpreter {
703
705
  default: return false;
704
706
  }
705
707
  }
706
- /** The read rule for one row, with probes over the replica. */
708
+ /** Whether the viewer may read a held row. A synced table's rows are
709
+ * the partition: the server delivered each under the read rule and
710
+ * withdrew what it revoked, so what the device holds it may read — and
711
+ * what `retain delivered-history` kept after a leave stays readable here
712
+ * even though the server now refuses it. Only an online-only table's
713
+ * rows (none, unless predicted) meet the rule on the device. */
707
714
  async readable(table, row) {
708
- const rule = this.schema.tables[table].rules.read;
715
+ const definition = this.schema.tables[table];
716
+ if (definition?.sync)
717
+ return true;
718
+ const rule = definition.rules.read;
709
719
  return this.rule(rule, undefined, row, false);
710
720
  }
711
721
  async rule(rule, old, next, insert) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapback4",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "description": "Snapback 4: the card (contract), the online and local-first clients (IndexedDB, SQLite), React hooks, sign-in, a labelled mock, and the `snapback4` CLI. LLP 3000.",
6
6
  "bin": {
@@ -42,7 +42,7 @@
42
42
  "README.md"
43
43
  ],
44
44
  "optionalDependencies": {
45
- "snapback4-darwin-arm64": "0.0.6"
45
+ "snapback4-darwin-arm64": "0.0.8"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.build.json",