snapback4 0.0.3 → 0.0.4

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
@@ -122,7 +122,8 @@ mutation edit(messageId: messages, body: text <=4000):
122
122
  exist by the end of the mutation that inserted this one).
123
123
  - **Rules:** `read`, `insert`, `update`, `delete`, each `<-` one
124
124
  expression over the row (`.col`), `viewer`, and literals: `allow`,
125
- `deny`, `public 'reason'`, `.col = viewer`, `.col != 'x'`,
125
+ `deny`, `public` (or the table line `public 'reason'`, which also
126
+ documents why), `.col = viewer`, `.col != 'x'`,
126
127
  `exists t[.a, viewer, 'literal']`, `created t[..]` (insert rules only:
127
128
  true when this mutation inserted the probed row), `and`, `or`, `not`.
128
129
  `update (old, next) <- old.x = next.x` names both images. An `exists`
@@ -320,8 +321,11 @@ seq, next }` or `{ denied }`; `POST /m/<op>` with `{ "id", "args",
320
321
  "failed", why }` (a client-minted `id` makes the write apply at most
321
322
  once); `GET /sync?from=W` streams the partition; `GET /changes?since=S`
322
323
  long-polls for commits; `GET /schema` is the compiled backend;
323
- `POST /auth/guest|signup|login|logout`. Authorization: `x-snapback-persona`
324
- (dev, same machine) or `Authorization: Bearer <token>`.
324
+ `POST /auth/guest` (`{}`), `POST /auth/signup` and `/auth/login`
325
+ (`{ "email", "password" }`) answer `{ "session": { principal, kind, token,
326
+ expiresAt } }` or `{ "denied": Refusal }`; `POST /auth/logout` with the
327
+ bearer token ends it. Authorization: `x-snapback-persona` (dev, same
328
+ machine) or `Authorization: Bearer <token>`.
325
329
 
326
330
  ## 6. Release
327
331
 
package/bin/snapback4.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // `npx snapback4 <verb>`: the Rust binary, found in the platform package
3
3
  // installed beside this one, or named by SNAPBACK4_BIN, or built in this
4
- // repository. The JavaScript here only finds and runs it.
5
- import { spawnSync } from "node:child_process";
4
+ // repository. The JavaScript here only finds and runs it, and dies with it:
5
+ // a signal to this wrapper reaches the binary, so a recorded PID of either
6
+ // stops the server.
7
+ import { spawn } from "node:child_process";
6
8
  import { existsSync } from "node:fs";
7
9
  import { createRequire } from "node:module";
8
10
  import { dirname, join, resolve } from "node:path";
@@ -20,5 +22,9 @@ if (!binary) {
20
22
  console.error(`snapback4: no binary for ${process.platform}-${process.arch}. Packages exist for darwin-arm64; elsewhere, build with \`cargo build --release -p snapback4\` and set SNAPBACK4_BIN.`);
21
23
  process.exit(2);
22
24
  }
23
- const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
24
- process.exit(result.status ?? 1);
25
+ const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
26
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => { try { child.kill(signal); } catch { /* gone */ } });
27
+ child.on("exit", (code, signal) => {
28
+ if (signal) { process.kill(process.pid, signal); return; }
29
+ process.exit(code ?? 1);
30
+ });
@@ -54,5 +54,7 @@ export declare class Replica {
54
54
  /** Follow the stream until closed: sync, wait for the transport's next
55
55
  * change signal, sync again. */
56
56
  follow(wait: () => Promise<void>): void;
57
+ /** Stop following, let a sync in flight finish on the open store, then
58
+ * close it: nothing touches the store after this returns. */
57
59
  close(): Promise<void>;
58
60
  }
@@ -44,6 +44,8 @@ export class Replica {
44
44
  * applies as one transaction. Returns the tables that changed. */
45
45
  async syncOnce() {
46
46
  const touched = new Set();
47
+ if (this.closed)
48
+ return { touched, ok: false };
47
49
  let { watermark, generation } = await this.state();
48
50
  if (generation !== this.generation) {
49
51
  // A new generation re-derives the partition: drop rows, keep the outbox.
@@ -53,6 +55,8 @@ export class Replica {
53
55
  }
54
56
  for (let pages = 0; pages < 1_000; pages++) {
55
57
  const page = await this.transport.sync(watermark, PAGE);
58
+ if (this.closed)
59
+ return { touched, ok: false };
56
60
  if ("denied" in page)
57
61
  return { touched, ok: false };
58
62
  if (page.watermark < watermark)
@@ -130,7 +134,16 @@ export class Replica {
130
134
  return;
131
135
  this.following = (async () => {
132
136
  while (!this.closed) {
133
- const { ok } = await this.syncOnce();
137
+ let ok = false;
138
+ try {
139
+ ({ ok } = await this.syncOnce());
140
+ }
141
+ catch {
142
+ // A store that closed under a sync, or a transport that threw:
143
+ // nothing escapes the loop; the next round decides.
144
+ if (this.closed)
145
+ break;
146
+ }
134
147
  if (this.closed)
135
148
  break;
136
149
  if (!ok)
@@ -140,8 +153,11 @@ export class Replica {
140
153
  }
141
154
  })();
142
155
  }
156
+ /** Stop following, let a sync in flight finish on the open store, then
157
+ * close it: nothing touches the store after this returns. */
143
158
  async close() {
144
159
  this.closed = true;
160
+ await this.following?.catch(() => { });
145
161
  await this.store.close();
146
162
  }
147
163
  }
@@ -235,6 +235,10 @@ class IndexedDbStore {
235
235
  transaction.onerror = () => reject(transaction.error);
236
236
  transaction.onabort = () => reject(transaction.error ?? new Error("aborted"));
237
237
  });
238
+ // A body that throws (a refused prediction) aborts the transaction on
239
+ // purpose; the abort must not surface a second time as an unhandled
240
+ // rejection in the page. The caller still sees the body's own error.
241
+ done.catch(() => { });
238
242
  const rows = transaction.objectStore("rows");
239
243
  const idx = transaction.objectStore("idx");
240
244
  const meta = transaction.objectStore("meta");
@@ -331,10 +335,19 @@ class IndexedDbStore {
331
335
  return { transaction, api, done };
332
336
  }
333
337
  async read(body) {
334
- const { api, done } = this.tx("readonly");
335
- const result = await body(api);
336
- await done;
337
- return result;
338
+ const { transaction, api, done } = this.tx("readonly");
339
+ try {
340
+ const result = await body(api);
341
+ await done;
342
+ return result;
343
+ }
344
+ catch (error) {
345
+ try {
346
+ transaction.abort();
347
+ }
348
+ catch { /* already done */ }
349
+ throw error;
350
+ }
338
351
  }
339
352
  async write(body) {
340
353
  const { transaction, api, done } = this.tx("readwrite");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapback4",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
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.3"
45
+ "snapback4-darwin-arm64": "0.0.4"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.build.json",