snapback4 0.0.5 → 0.0.7

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
 
@@ -227,10 +231,12 @@ durably and predicts its rows, and follows the server's stream for the
227
231
  rest. Its options: `url`; one of `persona` (development), `session` (from
228
232
  `snapback4/auth`, kept and restored by the client), or `guest: true`
229
233
  (mint one when none is kept); `store`; `sessionStore`; `fetch`;
230
- `waitSeconds`. It has `query`, `observe`, `mutate`, `link()`, `onLink`,
231
- `viewer()`, `session()`, `signOut()`, `sync()`, `replica`, `close()`. The
232
- online client takes `url` with `persona`, `session` or `token`, and has
233
- the same card without a store.
234
+ `waitSeconds`. It has `query(op, args) → Read`, `observe(op, args)
235
+ { getSnapshot(), subscribe(listener) → unsubscribe, close() }` (a live
236
+ read; React's `useSyncExternalStore` shape), `mutate(op, args) Write`,
237
+ `link()`, `onLink`, `viewer()`, `session()`, `signOut()`, `sync()`,
238
+ `replica`, `close()`. The online client takes `url` with `persona`,
239
+ `session` or `token`, and has the same card without a store.
234
240
 
235
241
  ### The card
236
242
 
@@ -245,10 +251,14 @@ type Write = { state: "pending"; id } | { state: "sent"; id; seq } | { state: "f
245
251
  type Refusal = { code: string; family: "input" | "schema" | "auth" | "rule" | "constraint" | "bound" | "op" | "link" | "generation"; message: string; site?: string; rewrite?: string; guide?: string; retryable?: boolean };
246
252
  ```
247
253
 
248
- `complete` says the page is whole; `fresh` says the device has everything
249
- the server had at the last sync (`since` is the last sync's time when it
250
- does not); `next` is the cursor for the next page (pass it as the query's
251
- `cursor ?` argument). A row this device wrote and the server has not yet
254
+ `complete` says this is everything the query can answer: false when the
255
+ meter cut the page, when a scan's read rule is not decided by its prefix
256
+ (§2, the meter), or when a page declared without a cursor had more rows
257
+ than it holds (declare `c: cursor ?` and page with `after c`; `check`
258
+ notes it). `fresh` says the device has everything the server had at the
259
+ last sync (`since` is the last sync's time when it does not); `next` is
260
+ the cursor for the next page (pass it as the query's `cursor ?` argument);
261
+ a page with a cursor and more rows is `complete: true` with `next`. A row this device wrote and the server has not yet
252
262
  confirmed carries `pending: true`. A write is `pending` while the link is
253
263
  down (it lands when it returns), `sent` with the server's sequence, or
254
264
  `failed` with the refusal and its predicted rows withdrawn. `isReady`,
@@ -302,8 +312,12 @@ flips the link.
302
312
  - **`snapback4 run <op> '<json args>' --as alice [--json]`** runs one
303
313
  operation against the local store and prints the result and the meter.
304
314
  - **`snapback4 test [--fixture dir] [--as persona]`** loads a fixture
305
- (one `<table>.jsonl` per table), runs every query, and prints the
306
- meters beside the ceilings.
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
317
+ arguments derived from the data (a reference is the table's first row,
318
+ a principal the persona, a text the shortest that fits), prints the
319
+ meters beside the ceilings and the arguments it used, and names any
320
+ query it could not run.
307
321
  - **`snapback4 check [--cost] [--json]`** compiles, writes
308
322
  `snapback/generated/api.ts` (`Rows`, `Ops`, `api`), and prints every
309
323
  diagnostic with its site and rewrite; `--cost` adds each site's
@@ -58,6 +58,8 @@ export declare class Interpreter {
58
58
  private written;
59
59
  private capped;
60
60
  private beyondHorizon;
61
+ /** The program's page had more rows and no cursor to continue on. */
62
+ private pageOverflow;
61
63
  private page;
62
64
  private pageDepth;
63
65
  private created;
@@ -89,7 +91,12 @@ export declare class Interpreter {
89
91
  * counted. */
90
92
  private visibility;
91
93
  private ruleTrue;
92
- /** 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. */
93
100
  readable(table: string, row: Row): Promise<boolean>;
94
101
  private rule;
95
102
  private permitted;
@@ -135,6 +135,8 @@ export class Interpreter {
135
135
  written = 0;
136
136
  capped = false;
137
137
  beyondHorizon = false;
138
+ /** The program's page had more rows and no cursor to continue on. */
139
+ pageOverflow = false;
138
140
  page = null;
139
141
  pageDepth = 0;
140
142
  created = new Set();
@@ -176,7 +178,7 @@ export class Interpreter {
176
178
  }
177
179
  const flow = await this.statements(program.body, program.name);
178
180
  const data = flow instanceof Flow && flow.kind === "return" ? flow.value : null;
179
- return { data, complete: !this.capped && !this.beyondHorizon, next: this.capped ? null : this.nextCursor(), tables: this.tables };
181
+ return { data, complete: !this.capped && !this.beyondHorizon && !this.pageOverflow, next: this.capped ? null : this.nextCursor(), tables: this.tables };
180
182
  }
181
183
  nextCursor() {
182
184
  if (!this.page || !this.page.next)
@@ -595,6 +597,8 @@ export class Interpreter {
595
597
  if (held >= horizon.last)
596
598
  this.beyondHorizon = true;
597
599
  }
600
+ if (this.pageDepth === 0 && !scan.opaque_cursor && hasMore)
601
+ this.pageOverflow = true;
598
602
  if (this.pageDepth === 0 && scan.opaque_cursor) {
599
603
  const columns = definition.indexes[index].components.length;
600
604
  this.page = { table, index, order, prefixLen: prefix.length, next: hasMore && lastKey && visible.length ? suffixOf(this.schema, table, index, visible[visible.length - 1], prefix.length).slice(0, columns - prefix.length) : null };
@@ -634,6 +638,8 @@ export class Interpreter {
634
638
  candidates.sort((a, b) => { const c = totalCompare(a.key, b.key); return order === "Asc" ? c : -c; });
635
639
  const hasMore = candidates.length > take;
636
640
  const page = candidates.slice(0, take);
641
+ if (this.pageDepth === 0 && !first.opaque_cursor && hasMore)
642
+ this.pageOverflow = true;
637
643
  if (this.pageDepth === 0 && first.opaque_cursor)
638
644
  this.page = { table, index, order, prefixLen, next: hasMore && page.length ? page[page.length - 1].key : null };
639
645
  return page.map((c) => c.row);
@@ -697,9 +703,17 @@ export class Interpreter {
697
703
  default: return false;
698
704
  }
699
705
  }
700
- /** The read rule for one row, with probes over the replica. */
706
+ /** Whether the viewer may read a held row. A synced table's rows are
707
+ * the partition: the server delivered each under the read rule and
708
+ * withdrew what it revoked, so what the device holds it may read — and
709
+ * what `retain delivered-history` kept after a leave stays readable here
710
+ * even though the server now refuses it. Only an online-only table's
711
+ * rows (none, unless predicted) meet the rule on the device. */
701
712
  async readable(table, row) {
702
- const rule = this.schema.tables[table].rules.read;
713
+ const definition = this.schema.tables[table];
714
+ if (definition?.sync)
715
+ return true;
716
+ const rule = definition.rules.read;
703
717
  return this.rule(rule, undefined, row, false);
704
718
  }
705
719
  async rule(rule, old, next, insert) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapback4",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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.5"
45
+ "snapback4-darwin-arm64": "0.0.7"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.build.json",