@auersperg/flare 0.0.0-stage → 0.3.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.
@@ -0,0 +1,83 @@
1
+ import { boundedJson, FlareError, stableJson } from "./protocol.js";
2
+ import {
3
+ documentValue,
4
+ quote,
5
+ schemaContract
6
+ } from "./schema.js";
7
+ export const SNAPSHOT_BYTES = 4 * 1048576;
8
+ export const SNAPSHOT_ROWS = 1e4;
9
+ const reject = (code) => {
10
+ throw new FlareError(code, code);
11
+ };
12
+ export function emptyData(storage, schema) {
13
+ for (const name of Object.keys(schema))
14
+ if (storage.sql.exec(`SELECT 1 FROM ${quote(name)} LIMIT 1`).toArray().length)
15
+ return false;
16
+ return !storage.sql.exec("SELECT 1 FROM _flared_outbox LIMIT 1").toArray().length;
17
+ }
18
+ export function exportData(storage, schema) {
19
+ if (storage.sql.exec("SELECT 1 FROM _flared_outbox LIMIT 1").toArray().length)
20
+ reject("BACKUP_BUSY");
21
+ const snapshot = {
22
+ format: 1,
23
+ schema: schemaContract(schema),
24
+ revision: Number(storage.sql.exec("SELECT value FROM _flared_revision WHERE id=1").one().value),
25
+ tables: Object.create(null)
26
+ };
27
+ let count = 0, bytes = 0;
28
+ for (const name of Object.keys(schema)) {
29
+ const docs = [];
30
+ snapshot.tables[name] = docs;
31
+ let cursor = "";
32
+ for (;; ) {
33
+ const page = storage.sql.exec(`SELECT * FROM ${quote(name)} WHERE _id>? ORDER BY _id LIMIT 100`, cursor).toArray();
34
+ for (const row of page) {
35
+ const doc = {
36
+ _id: String(row._id),
37
+ _creationTime: Number(row._creationTime),
38
+ value: JSON.parse(String(row._data))
39
+ };
40
+ bytes += new TextEncoder().encode(stableJson(doc)).length;
41
+ if (++count > SNAPSHOT_ROWS || bytes > SNAPSHOT_BYTES)
42
+ reject("BACKUP_TOO_LARGE");
43
+ docs.push(doc);
44
+ }
45
+ if (page.length < 100)
46
+ break;
47
+ cursor = String(page.at(-1)._id);
48
+ }
49
+ }
50
+ return JSON.parse(boundedJson(snapshot, SNAPSHOT_BYTES));
51
+ }
52
+ export async function restoreData(storage, schema, id, snapshot) {
53
+ boundedJson(snapshot, SNAPSHOT_BYTES);
54
+ if (!/^[0-9a-f-]{36}$/.test(id) || snapshot?.format !== 1 || !Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0 || stableJson(snapshot.schema) !== stableJson(schemaContract(schema)) || !snapshot.tables || Array.isArray(snapshot.tables) || Object.keys(snapshot.tables).sort().join() !== Object.keys(schema).sort().join())
55
+ reject("INVALID_BACKUP");
56
+ await storage.transaction(async () => {
57
+ storage.sql.exec("CREATE TABLE IF NOT EXISTS _flared_restore (id TEXT PRIMARY KEY)");
58
+ if (storage.sql.exec("SELECT id FROM _flared_restore WHERE id=?", id).toArray().length)
59
+ return;
60
+ if (!emptyData(storage, schema))
61
+ reject("RESTORE_NOT_EMPTY");
62
+ let count = 0;
63
+ for (const [name, docs] of Object.entries(snapshot.tables)) {
64
+ if (!Array.isArray(docs))
65
+ reject("INVALID_BACKUP");
66
+ for (const doc of docs) {
67
+ if (++count > SNAPSHOT_ROWS || !doc || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(doc._id) || !Number.isFinite(doc._creationTime) || doc._creationTime < 0)
68
+ reject("INVALID_BACKUP");
69
+ const value = await documentValue(schema[name], doc.value);
70
+ if (stableJson(value) !== stableJson(doc.value))
71
+ reject("INVALID_BACKUP");
72
+ storage.sql.exec(`INSERT INTO ${quote(name)} VALUES (?,?,?)`, doc._id, doc._creationTime, stableJson(value));
73
+ }
74
+ }
75
+ storage.sql.exec("UPDATE _flared_revision SET value=? WHERE id=1", snapshot.revision);
76
+ storage.sql.exec("DELETE FROM _flared_changes");
77
+ for (const name of Object.keys(schema))
78
+ storage.sql.exec("INSERT INTO _flared_changes VALUES (?,?)", name, snapshot.revision);
79
+ storage.sql.exec("INSERT INTO _flared_restore VALUES (?)", id);
80
+ });
81
+ await storage.sync();
82
+ return { restored: true, revision: snapshot.revision };
83
+ }
@@ -0,0 +1,80 @@
1
+ import { FlareError, type FunctionRef } from "./protocol.js";
2
+ export { reference, FlareError } from "./protocol.js";
3
+ export type { FunctionRef, Json } from "./protocol.js";
4
+ /** Unknown formats are stored as binary and always downloaded as attachments. */
5
+ export declare function fileMetadata(file: Blob & {
6
+ name: string;
7
+ }): {
8
+ name: string;
9
+ size: number;
10
+ type: string;
11
+ };
12
+ /** A grant is single-use. Never retry automatically after an ambiguous failure. */
13
+ export declare function uploadFile(grant: {
14
+ url: string;
15
+ }, file: Blob, options?: {
16
+ fetch?: (input: string, init: RequestInit) => Promise<Response>;
17
+ signal?: AbortSignal;
18
+ timeoutMs?: number;
19
+ }): Promise<{
20
+ id: string;
21
+ }>;
22
+ export interface Socket {
23
+ readonly readyState: number;
24
+ send(data: string): void;
25
+ close(code?: number, reason?: string): void;
26
+ addEventListener(type: string, listener: (event: any) => void): void;
27
+ removeEventListener(type: string, listener: (event: any) => void): void;
28
+ }
29
+ export interface ClientOptions {
30
+ url: string;
31
+ getToken?: () => string | null | Promise<string | null>;
32
+ fetch?: (input: string, init?: RequestInit) => Promise<Response>;
33
+ createWebSocket?: (url: string) => Socket;
34
+ requestTimeoutMs?: number;
35
+ }
36
+ export interface Observer<O> {
37
+ next(value: O): void;
38
+ error?(error: FlareError): void;
39
+ }
40
+ export declare function createClient(options: ClientOptions): {
41
+ query: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
42
+ mutation: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
43
+ action: <I, O>(ref: FunctionRef<I, O>, args: I) => Promise<O>;
44
+ /** One subscription delivers the initial snapshot and subsequent updates. */
45
+ onUpdate<I, O>(ref: FunctionRef<I, O>, args: I, next: (value: O) => void, error?: (error: FlareError) => void): () => void;
46
+ /** Call on login/logout/account switch; never reuse cached values across identities. */
47
+ refreshAuth(): void;
48
+ dispose(): void;
49
+ };
50
+ export type FlareClient = ReturnType<typeof createClient>;
51
+ export type PasswordUser = {
52
+ id: string;
53
+ login: string;
54
+ disabled: boolean;
55
+ createdAt: number;
56
+ };
57
+ export type PasswordSession = {
58
+ token: string;
59
+ expiresAt: number;
60
+ user: PasswordUser;
61
+ };
62
+ /** Browser persistence is scoped to the full deployment URL. Passwords are never stored.
63
+ * Server/mobile callers can provide storage explicitly, or use null for memory-only auth.
64
+ * Connect onChange to client.refreshAuth() and call restore() before rendering protected UI. */
65
+ export type PasswordAuthStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
66
+ export declare function createPasswordAuth(options: {
67
+ url: string;
68
+ storage?: PasswordAuthStorage | null;
69
+ }): {
70
+ getToken: () => string | null;
71
+ getUser: () => PasswordUser | null;
72
+ restore: () => Promise<PasswordUser | null>;
73
+ onChange(listener: () => void): () => boolean;
74
+ register: (login: string, password: string) => Promise<PasswordUser>;
75
+ login: (login: string, password: string) => Promise<PasswordUser>;
76
+ me(): Promise<PasswordUser>;
77
+ logout(): Promise<void>;
78
+ changePassword(currentPassword: string, password: string): Promise<void>;
79
+ dispose(): void;
80
+ };