snapback4 0.0.13 → 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 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. Its options: `url`; one of `persona` (development), `session` (from
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
- const backend = await loadBackend(doFetch, base, headers);
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 store = typeof options.store === "object" ? await options.store.open(storeName, backend.schema) : useIdb ? await openIndexedDb(storeName, backend.schema) : new MemoryStore(backend.schema);
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
  }
@@ -24,6 +24,7 @@ class SqliteStore {
24
24
  this.db = db;
25
25
  this.schema = schema;
26
26
  }
27
+ setSchema(schema) { this.schema = schema; }
27
28
  tx() {
28
29
  const db = this.db;
29
30
  const schema = this.schema;
@@ -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
- readonly schema: SchemaJson;
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>;
@@ -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.13",
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.13"
45
+ "snapback4-darwin-arm64": "0.0.14"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsc -p tsconfig.build.json",