snapback4 0.0.12 → 0.0.14
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 +4 -1
- package/dist/local.js +23 -2
- package/dist/replica/interpreter.d.ts +2 -0
- package/dist/replica/interpreter.js +11 -0
- package/dist/replica/sqlite.js +1 -0
- package/dist/replica/store.d.ts +6 -1
- package/dist/replica/store.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -228,7 +228,10 @@ The local-first client opens the device's replica (IndexedDB on the web,
|
|
|
228
228
|
memory with `store: "memory"`, SQLite on Expo — §7), syncs the viewer's
|
|
229
229
|
partition, runs every query locally over it, queues every mutation
|
|
230
230
|
durably and predicts its rows, and follows the server's stream for the
|
|
231
|
-
rest.
|
|
231
|
+
rest. It keeps the compiled backend in the replica too, so a device that
|
|
232
|
+
starts with no network opens on what it holds (`link()` is `"offline"`,
|
|
233
|
+
reads carry `fresh: false` and `since`) and catches up when the link
|
|
234
|
+
returns; only a device that has never synced needs the server to open. Its options: `url`; one of `persona` (development), `session` (from
|
|
232
235
|
`snapback4/auth`, kept and restored by the client), or `guest: true`
|
|
233
236
|
(mint one when none is kept); `store`; `sessionStore`; `fetch`;
|
|
234
237
|
`waitSeconds`. It has `query(op, args) → Read`, `observe(op, args) →
|
package/dist/local.js
CHANGED
|
@@ -9,6 +9,8 @@ import { Interpreter, Refused } from "./replica/interpreter.js";
|
|
|
9
9
|
import { Replica } from "./replica/replica.js";
|
|
10
10
|
import { MemoryStore, openIndexedDb } from "./replica/store.js";
|
|
11
11
|
import { defaultSessionStore, forgetSession, guest, keepSession, logout, restoreSession } from "./auth.js";
|
|
12
|
+
/** The meta key under which a device keeps the backend it last loaded. */
|
|
13
|
+
const BACKEND = "backend";
|
|
12
14
|
export async function createLocalClient(options) {
|
|
13
15
|
const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
14
16
|
const base = options.url.replace(/\/$/, "");
|
|
@@ -47,11 +49,28 @@ export async function createLocalClient(options) {
|
|
|
47
49
|
await forgetSession(base, sessions);
|
|
48
50
|
}
|
|
49
51
|
};
|
|
50
|
-
|
|
52
|
+
// The store opens before the network is asked: a device that starts
|
|
53
|
+
// offline runs on the backend it cached last time, on the rows it holds.
|
|
51
54
|
const storeName = `snapback4:${base}:${viewer}`;
|
|
52
55
|
const useIdb = options.store === "indexeddb" || (options.store === undefined && typeof indexedDB !== "undefined");
|
|
53
|
-
const
|
|
56
|
+
const unknown = { tables: {} };
|
|
57
|
+
const store = typeof options.store === "object" ? await options.store.open(storeName, unknown) : useIdb ? await openIndexedDb(storeName, unknown) : new MemoryStore(unknown);
|
|
58
|
+
const cached = (await store.read((tx) => tx.getMeta(BACKEND)));
|
|
54
59
|
let link = "online";
|
|
60
|
+
let backend;
|
|
61
|
+
try {
|
|
62
|
+
backend = await loadBackend(doFetch, base, headers);
|
|
63
|
+
await store.write((tx) => tx.setMeta(BACKEND, backend));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!cached) {
|
|
67
|
+
await store.close();
|
|
68
|
+
throw new Error(`createLocalClient: cannot reach ${base} and this device holds no backend yet: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
backend = cached;
|
|
71
|
+
link = "offline";
|
|
72
|
+
}
|
|
73
|
+
store.setSchema(backend.schema);
|
|
55
74
|
let closed = false;
|
|
56
75
|
let seq = 0;
|
|
57
76
|
const aborter = new AbortController();
|
|
@@ -117,6 +136,8 @@ export async function createLocalClient(options) {
|
|
|
117
136
|
backend.generation = next.generation;
|
|
118
137
|
backend.schema = next.schema;
|
|
119
138
|
backend.programs = next.programs;
|
|
139
|
+
store.setSchema(next.schema);
|
|
140
|
+
await store.write((tx) => tx.setMeta(BACKEND, next));
|
|
120
141
|
replica.adopt(next.schema, next.generation);
|
|
121
142
|
return { denied: { code: "E_GENERATION", family: "generation", message: "the backend changed; re-deriving the partition", retryable: true } };
|
|
122
143
|
}
|
|
@@ -60,6 +60,8 @@ export declare class Interpreter {
|
|
|
60
60
|
private beyondHorizon;
|
|
61
61
|
/** The program's page had more rows and no cursor to continue on. */
|
|
62
62
|
private pageOverflow;
|
|
63
|
+
/** A field of a pending row was read while building an object. */
|
|
64
|
+
private pendingTouched;
|
|
63
65
|
private page;
|
|
64
66
|
private pageDepth;
|
|
65
67
|
private created;
|
|
@@ -137,6 +137,8 @@ export class Interpreter {
|
|
|
137
137
|
beyondHorizon = false;
|
|
138
138
|
/** The program's page had more rows and no cursor to continue on. */
|
|
139
139
|
pageOverflow = false;
|
|
140
|
+
/** A field of a pending row was read while building an object. */
|
|
141
|
+
pendingTouched = false;
|
|
140
142
|
page = null;
|
|
141
143
|
pageDepth = 0;
|
|
142
144
|
created = new Set();
|
|
@@ -361,6 +363,8 @@ export class Interpreter {
|
|
|
361
363
|
return null;
|
|
362
364
|
if (typeof base !== "object")
|
|
363
365
|
throw new Refused({ code: "E_FIELD", family: "input", message: `no field ${b.name}` });
|
|
366
|
+
if (base.pending === true)
|
|
367
|
+
this.pendingTouched = true;
|
|
364
368
|
return base[b.name] ?? null;
|
|
365
369
|
}
|
|
366
370
|
case "Array": {
|
|
@@ -370,9 +374,16 @@ export class Interpreter {
|
|
|
370
374
|
return out;
|
|
371
375
|
}
|
|
372
376
|
case "Object": {
|
|
377
|
+
// A shape over a pending row is pending: the card's word survives
|
|
378
|
+
// the field selection, so a screen shows "Sending…" on shaped rows too.
|
|
379
|
+
const outer = this.pendingTouched;
|
|
380
|
+
this.pendingTouched = false;
|
|
373
381
|
const out = {};
|
|
374
382
|
for (const [k, v] of Object.entries(body))
|
|
375
383
|
out[k] = await this.expr(v);
|
|
384
|
+
if (this.pendingTouched && !("pending" in out))
|
|
385
|
+
out.pending = true;
|
|
386
|
+
this.pendingTouched = outer || this.pendingTouched;
|
|
376
387
|
return out;
|
|
377
388
|
}
|
|
378
389
|
case "Binary": {
|
package/dist/replica/sqlite.js
CHANGED
package/dist/replica/store.d.ts
CHANGED
|
@@ -60,6 +60,10 @@ export interface SideTx {
|
|
|
60
60
|
clear(): Promise<void>;
|
|
61
61
|
}
|
|
62
62
|
export interface Store {
|
|
63
|
+
/** The compiled backend's schema, for key encoding; set once it is known
|
|
64
|
+
* (a device that starts offline reads it from its own store first). */
|
|
65
|
+
readonly schema: SchemaJson;
|
|
66
|
+
setSchema(schema: SchemaJson): void;
|
|
63
67
|
read<T>(body: (tx: StoreTx) => Promise<T>): Promise<T>;
|
|
64
68
|
write<T>(body: (tx: StoreTx) => Promise<T>): Promise<T>;
|
|
65
69
|
/** Drop every row and index entry (a re-bootstrap); meta and side tables stay. */
|
|
@@ -76,12 +80,13 @@ export declare function indexKey(schema: SchemaJson, table: string, index: strin
|
|
|
76
80
|
/** The byte range of a scan: `[lo, hi)`, or null when empty. */
|
|
77
81
|
export declare function scanRange(schema: SchemaJson, table: string, index: string, prefix: readonly unknown[], bounds: Bounds, dir: "asc" | "desc"): [Uint8Array, Uint8Array] | null;
|
|
78
82
|
export declare class MemoryStore implements Store {
|
|
79
|
-
|
|
83
|
+
schema: SchemaJson;
|
|
80
84
|
private rows;
|
|
81
85
|
private indexes;
|
|
82
86
|
private meta;
|
|
83
87
|
private sides;
|
|
84
88
|
constructor(schema: SchemaJson);
|
|
89
|
+
setSchema(schema: SchemaJson): void;
|
|
85
90
|
private tx;
|
|
86
91
|
read<T>(body: (tx: StoreTx) => Promise<T>): Promise<T>;
|
|
87
92
|
write<T>(body: (tx: StoreTx) => Promise<T>): Promise<T>;
|
package/dist/replica/store.js
CHANGED
|
@@ -77,6 +77,7 @@ export class MemoryStore {
|
|
|
77
77
|
constructor(schema) {
|
|
78
78
|
this.schema = schema;
|
|
79
79
|
}
|
|
80
|
+
setSchema(schema) { this.schema = schema; }
|
|
80
81
|
tx() {
|
|
81
82
|
const self = this;
|
|
82
83
|
const entries = (table, index) => {
|
|
@@ -228,6 +229,7 @@ class IndexedDbStore {
|
|
|
228
229
|
this.db = db;
|
|
229
230
|
this.schema = schema;
|
|
230
231
|
}
|
|
232
|
+
setSchema(schema) { this.schema = schema; }
|
|
231
233
|
tx(mode) {
|
|
232
234
|
const transaction = this.db.transaction([...STORES], mode);
|
|
233
235
|
const done = new Promise((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapback4",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
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.14"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc -p tsconfig.build.json",
|