snapback4 0.0.7 → 0.0.9

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
@@ -300,8 +300,11 @@ flips the link.
300
300
 
301
301
  ## 5. Development
302
302
 
303
- - **`snapback4 dev [--port N] [--no-watch]`** serves `snapback/*.q` on
304
- `127.0.0.1:4400`, recompiling and re-adopting on every save. Personas
303
+ - **`snapback4 dev [--port N] [--no-watch] [--fresh]`** serves
304
+ `snapback/*.q` on `127.0.0.1:4400`, recompiling and re-adopting on every
305
+ save; the store lives in `.snapback4/` and persists across runs, and
306
+ `--fresh` uses one in memory that dies with the process (acceptance
307
+ scripts start from nothing). Personas
305
308
  (`use personas ..`) sign requests from the same machine with the
306
309
  `x-snapback-persona: alice` header (the client's `persona: "alice"`),
307
310
  acting as `dev:alice`.
package/dist/local.js CHANGED
@@ -40,6 +40,7 @@ export async function createLocalClient(options) {
40
40
  const viewer = (options.persona ? `dev:${options.persona}` : session.principal);
41
41
  const principal = viewer;
42
42
  let sessionDead = false;
43
+ let partitionDenied = null;
43
44
  const refusedSession = async (denied) => {
44
45
  if (session && !sessionDead && denied?.code === "E_AUTH") {
45
46
  sessionDead = true;
@@ -87,13 +88,24 @@ export async function createLocalClient(options) {
87
88
  async sync(from, limit) {
88
89
  try {
89
90
  const response = await doFetch(`${base}/sync?from=${from}&limit=${limit}`, { headers });
90
- if (response.status >= 500)
91
+ if (response.status === 503)
91
92
  return { denied: offline() };
92
93
  const page = (await response.json());
93
94
  if (page.denied) {
94
95
  await refusedSession(page.denied);
96
+ if (!page.denied.retryable) {
97
+ // The server refuses this device's partition (a schema the store
98
+ // cannot serve, a dead session): every read says so, once
99
+ // loudly here, and until the next successful sync.
100
+ if (!partitionDenied)
101
+ console.error(`snapback4: the server refused the partition: ${page.denied.code}: ${page.denied.message}`);
102
+ partitionDenied = page.denied;
103
+ for (const watch of watches.values())
104
+ void rerun(watch);
105
+ }
95
106
  return { denied: page.denied };
96
107
  }
108
+ partitionDenied = null;
97
109
  if (link !== "online")
98
110
  setLink("online");
99
111
  if (page.watermark > seq)
@@ -147,6 +159,8 @@ export async function createLocalClient(options) {
147
159
  const program = backend.programs[op.name];
148
160
  if (!program)
149
161
  return { read: { denied: { code: "E_OP", family: "op", message: `unknown query ${op.name}` } }, tables: new Set() };
162
+ if (partitionDenied)
163
+ return { read: { denied: partitionDenied }, tables: new Set(Object.keys(backend.schema.tables)) };
150
164
  try {
151
165
  const outcome = await store.read((tx) => new Interpreter(tx, backend.schema, { viewer: principal, args: args, now: Date.now(), newIds: [], mint: mintId }, "query").run(program));
152
166
  const read = { data: outcome.data, complete: outcome.complete, fresh: link === "online" && replica.isCaughtUp, next: outcome.next, ...(link === "online" && replica.isCaughtUp ? {} : { since: lastSyncAt }) };
@@ -190,11 +204,27 @@ export async function createLocalClient(options) {
190
204
  return store.read(async (tx) => (await tx.side("outbox").all()).map((e) => e.value).sort((a, b) => (a.id < b.id ? -1 : 1)));
191
205
  }
192
206
  let flushing = false;
207
+ let flushAgain = false;
193
208
  async function flush() {
194
- if (flushing)
209
+ // A write queued while a flush runs is flushed by the same flush, on
210
+ // its next pass: nothing waits for the next link change.
211
+ if (flushing) {
212
+ flushAgain = true;
195
213
  return;
214
+ }
196
215
  flushing = true;
197
216
  try {
217
+ do {
218
+ flushAgain = false;
219
+ await flushOnce();
220
+ } while (flushAgain && link === "online" && !closed);
221
+ }
222
+ finally {
223
+ flushing = false;
224
+ }
225
+ }
226
+ async function flushOnce() {
227
+ {
198
228
  for (const entry of await queued()) {
199
229
  if (link !== "online")
200
230
  return;
@@ -228,9 +258,6 @@ export async function createLocalClient(options) {
228
258
  void rerun(watch);
229
259
  }
230
260
  }
231
- finally {
232
- flushing = false;
233
- }
234
261
  }
235
262
  const settled = new Map();
236
263
  const client = {
@@ -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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapback4",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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.7"
45
+ "snapback4-darwin-arm64": "0.0.9"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.build.json",