snapback4 0.0.1 → 0.0.2
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 +360 -15
- package/bin/snapback4.js +24 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.js +90 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +262 -0
- package/dist/local.d.ts +34 -0
- package/dist/local.js +358 -0
- package/dist/replica/cursor.d.ts +15 -0
- package/dist/replica/cursor.js +81 -0
- package/dist/replica/index.d.ts +3 -0
- package/dist/replica/index.js +6 -0
- package/dist/replica/interpreter.d.ts +106 -0
- package/dist/replica/interpreter.js +906 -0
- package/dist/replica/key.d.ts +9 -0
- package/dist/replica/key.js +107 -0
- package/dist/replica/replica.d.ts +58 -0
- package/dist/replica/replica.js +147 -0
- package/dist/replica/sqlite.d.ts +29 -0
- package/dist/replica/sqlite.js +151 -0
- package/dist/replica/store.d.ts +91 -0
- package/dist/replica/store.js +359 -0
- package/package.json +29 -6
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Opaque cursors, byte-for-byte the server's (`cursor.rs`): base64url over
|
|
2
|
+
// canonical JSON of `[1, table, index, order, suffix, values]`, values in
|
|
3
|
+
// the tagged form. A page position minted here resumes on the server and
|
|
4
|
+
// vice versa.
|
|
5
|
+
/** Canonical JSON: compact, object keys sorted, as serde_json writes a BTreeMap. */
|
|
6
|
+
export function canonicalJson(value) {
|
|
7
|
+
if (value === null || value === undefined)
|
|
8
|
+
return "null";
|
|
9
|
+
if (typeof value === "number")
|
|
10
|
+
return Number.isInteger(value) ? String(value) : JSON.stringify(value);
|
|
11
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
15
|
+
const keys = Object.keys(value).sort();
|
|
16
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(",")}}`;
|
|
17
|
+
}
|
|
18
|
+
function tagFor(kind, value) {
|
|
19
|
+
if (value === null || value === undefined)
|
|
20
|
+
return "Null";
|
|
21
|
+
const name = typeof kind === "string" ? kind : Object.keys(kind)[0];
|
|
22
|
+
const inner = typeof kind === "object" && kind !== null ? kind[name] : undefined;
|
|
23
|
+
switch (name) {
|
|
24
|
+
case "Optional": return tagFor(inner, value);
|
|
25
|
+
case "Id":
|
|
26
|
+
case "Principal":
|
|
27
|
+
case "Ref": return { Id: value };
|
|
28
|
+
case "Text":
|
|
29
|
+
case "Enum": return { String: value };
|
|
30
|
+
case "Int":
|
|
31
|
+
case "Time":
|
|
32
|
+
case "Decimal":
|
|
33
|
+
case "Money": return { Int: value };
|
|
34
|
+
case "Bool": return { Bool: value };
|
|
35
|
+
case "Bytes": return { Bytes: value };
|
|
36
|
+
default: return typeof value === "string" ? { String: value } : typeof value === "number" ? { Int: value } : typeof value === "boolean" ? { Bool: value } : "Null";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function untag(tagged) {
|
|
40
|
+
if (tagged === "Null")
|
|
41
|
+
return null;
|
|
42
|
+
if (typeof tagged !== "object" || tagged === null)
|
|
43
|
+
return tagged;
|
|
44
|
+
const [k, v] = Object.entries(tagged)[0];
|
|
45
|
+
return k === "Array" ? v.map(untag) : v;
|
|
46
|
+
}
|
|
47
|
+
const base64url = (bytes) => {
|
|
48
|
+
let binary = "";
|
|
49
|
+
for (const b of bytes)
|
|
50
|
+
binary += String.fromCharCode(b);
|
|
51
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
52
|
+
};
|
|
53
|
+
const fromBase64url = (text) => {
|
|
54
|
+
try {
|
|
55
|
+
const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (text.length % 4)) % 4);
|
|
56
|
+
const binary = atob(padded);
|
|
57
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
export function encodeCursor(position) {
|
|
64
|
+
const tagged = [1, position.table, position.index, position.order, position.suffix, position.values.map((v, i) => tagFor(position.suffix[i]?.kind, v))];
|
|
65
|
+
return base64url(new TextEncoder().encode(canonicalJson(tagged)));
|
|
66
|
+
}
|
|
67
|
+
export function decodeCursor(token) {
|
|
68
|
+
const bytes = fromBase64url(token);
|
|
69
|
+
if (!bytes)
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
73
|
+
if (!Array.isArray(parsed) || parsed[0] !== 1)
|
|
74
|
+
return null;
|
|
75
|
+
const suffix = parsed[4];
|
|
76
|
+
return { table: parsed[1], index: parsed[2], order: parsed[3], suffix, values: parsed[5].map(untag) };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { openSqlite, expoSqliteDriver, nodeSqliteDriver, type SqliteDriver } from "./sqlite.ts";
|
|
2
|
+
export { MemoryStore, openIndexedDb, type Store, type StoreTx, type SchemaJson, type Row } from "./store.ts";
|
|
3
|
+
export { encodeKey, upperBound, compareKeys, type Kind } from "./key.ts";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The replica's parts, for hosts that bring their own store: the SQLite
|
|
2
|
+
// store behind a four-method driver (`expo-sqlite`, `node:sqlite`), the
|
|
3
|
+
// memory store, and the store contract itself.
|
|
4
|
+
export { openSqlite, expoSqliteDriver, nodeSqliteDriver } from "./sqlite.js";
|
|
5
|
+
export { MemoryStore, openIndexedDb } from "./store.js";
|
|
6
|
+
export { encodeKey, upperBound, compareKeys } from "./key.js";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Row, SchemaJson, StoreTx } from "./store.ts";
|
|
2
|
+
export type Json = unknown;
|
|
3
|
+
export interface ProgramJson {
|
|
4
|
+
name: string;
|
|
5
|
+
kind: "Query" | "Mutation" | "job";
|
|
6
|
+
args: Record<string, unknown>;
|
|
7
|
+
body: unknown[];
|
|
8
|
+
}
|
|
9
|
+
export interface Refusal {
|
|
10
|
+
code: string;
|
|
11
|
+
family: string;
|
|
12
|
+
message: string;
|
|
13
|
+
site?: string;
|
|
14
|
+
rule?: string;
|
|
15
|
+
retryable?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class Refused extends Error {
|
|
18
|
+
readonly refusal: Refusal;
|
|
19
|
+
constructor(refusal: Refusal);
|
|
20
|
+
}
|
|
21
|
+
/** A query's outcome on the device. */
|
|
22
|
+
export interface LocalRead {
|
|
23
|
+
data: unknown;
|
|
24
|
+
/** False beyond a horizon, at an online-only site, or past the meter. */
|
|
25
|
+
complete: boolean;
|
|
26
|
+
next: string | null;
|
|
27
|
+
tables: Set<string>;
|
|
28
|
+
}
|
|
29
|
+
export interface Ceilings {
|
|
30
|
+
examined: number;
|
|
31
|
+
ruleProbes: number;
|
|
32
|
+
steps: number;
|
|
33
|
+
rowsWritten: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const CEILINGS: Ceilings;
|
|
36
|
+
export declare function literal(v: unknown): unknown;
|
|
37
|
+
/** The value model's total order over plain values. */
|
|
38
|
+
export declare function totalCompare(a: unknown, b: unknown): number;
|
|
39
|
+
export declare function equal(a: unknown, b: unknown): boolean;
|
|
40
|
+
export interface Context {
|
|
41
|
+
viewer: string;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
now: number;
|
|
44
|
+
newIds: string[];
|
|
45
|
+
mint: () => string;
|
|
46
|
+
}
|
|
47
|
+
/** Runs programs over one store transaction. */
|
|
48
|
+
export declare class Interpreter {
|
|
49
|
+
private tx;
|
|
50
|
+
private schema;
|
|
51
|
+
private ctx;
|
|
52
|
+
private mode;
|
|
53
|
+
private ceilings;
|
|
54
|
+
private env;
|
|
55
|
+
private examined;
|
|
56
|
+
private probes;
|
|
57
|
+
private steps;
|
|
58
|
+
private written;
|
|
59
|
+
private capped;
|
|
60
|
+
private beyondHorizon;
|
|
61
|
+
private page;
|
|
62
|
+
private pageDepth;
|
|
63
|
+
private created;
|
|
64
|
+
readonly tables: Set<string>;
|
|
65
|
+
readonly writes: {
|
|
66
|
+
table: string;
|
|
67
|
+
row: Row;
|
|
68
|
+
old?: Row;
|
|
69
|
+
}[];
|
|
70
|
+
private site;
|
|
71
|
+
constructor(tx: StoreTx, schema: SchemaJson, ctx: Context, mode: "query" | "mutation", ceilings?: Ceilings);
|
|
72
|
+
private charge;
|
|
73
|
+
private step;
|
|
74
|
+
run(program: ProgramJson): Promise<LocalRead>;
|
|
75
|
+
private nextCursor;
|
|
76
|
+
private statements;
|
|
77
|
+
private domain;
|
|
78
|
+
private recurse;
|
|
79
|
+
expr(e: unknown): Promise<unknown>;
|
|
80
|
+
private builtin;
|
|
81
|
+
private get;
|
|
82
|
+
private scan;
|
|
83
|
+
private merge;
|
|
84
|
+
/** The server's `scan_visibility`, on the device: a read rule whose every
|
|
85
|
+
* field the prefix pins is decided once for the whole range (`uniform`);
|
|
86
|
+
* one the prefix proves is `decidedTrue` before any row is read. A
|
|
87
|
+
* uniform-false range halts after one row at the cost of an empty one,
|
|
88
|
+
* so the rows the viewer may no longer read (delivered history) are never
|
|
89
|
+
* counted. */
|
|
90
|
+
private visibility;
|
|
91
|
+
private ruleTrue;
|
|
92
|
+
/** The read rule for one row, with probes over the replica. */
|
|
93
|
+
readable(table: string, row: Row): Promise<boolean>;
|
|
94
|
+
private rule;
|
|
95
|
+
private permitted;
|
|
96
|
+
private constraints;
|
|
97
|
+
private insert;
|
|
98
|
+
private update;
|
|
99
|
+
private upsert;
|
|
100
|
+
private delete;
|
|
101
|
+
private put;
|
|
102
|
+
/** A device keeps its counts honest while a prediction stands. */
|
|
103
|
+
private maintain;
|
|
104
|
+
}
|
|
105
|
+
/** A plain value as the server's tagged literal (for canonical ids). */
|
|
106
|
+
export declare function tag(v: unknown): unknown;
|