snapback4 0.0.2 → 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 +7 -3
- package/bin/snapback4.js +10 -4
- package/dist/replica/replica.d.ts +2 -0
- package/dist/replica/replica.js +17 -1
- package/dist/replica/store.js +17 -4
- package/package.json +2 -2
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
|
|
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
|
|
324
|
-
(
|
|
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
|
-
|
|
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
|
|
24
|
-
process.
|
|
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
|
}
|
package/dist/replica/replica.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/replica/store.js
CHANGED
|
@@ -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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
"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.
|
|
45
|
+
"snapback4-darwin-arm64": "0.0.4"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc -p tsconfig.build.json",
|