@statewalker/webrun-files-sqlite 0.10.0

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,132 @@
1
+ /**
2
+ * A streaming compression format. Pulled, never pushed: `compress` and
3
+ * `decompress` read their input only as their output is consumed.
4
+ */
5
+ export interface StreamCodec {
6
+ /** Stored in `fs_files.compression`. Implementations of one format share the name. */
7
+ name: string;
8
+ compress(input: AsyncIterable<Uint8Array>): AsyncIterable<Uint8Array>;
9
+ decompress(input: AsyncIterable<Uint8Array>): AsyncIterable<Uint8Array>;
10
+ }
11
+
12
+ /**
13
+ * zlib-wrapped Deflate over Compression Streams — browsers, Node, Deno, Bun,
14
+ * Workers. The source is wrapped in a pull-only ReadableStream, so the
15
+ * transform asks for input only when it has room, and cancelling the output
16
+ * closes the source.
17
+ */
18
+ export function webDeflateCodec(): StreamCodec {
19
+ if (typeof CompressionStream === "undefined") {
20
+ throw new Error("webDeflateCodec: CompressionStream is not available in this runtime");
21
+ }
22
+ return {
23
+ name: "deflate",
24
+ compress: (input) =>
25
+ iterate(pullStream(input).pipeThrough(asPair(new CompressionStream("deflate")))),
26
+ decompress: (input) =>
27
+ iterate(pullStream(input).pipeThrough(asPair(new DecompressionStream("deflate")))),
28
+ };
29
+ }
30
+
31
+ /** The part of the `pako` module the streaming codec uses. */
32
+ export interface PakoStreamModule {
33
+ Deflate: new (options?: { level?: number }) => PakoStream;
34
+ Inflate: new () => PakoStream;
35
+ }
36
+
37
+ interface PakoStream {
38
+ push(data: Uint8Array, flush?: boolean): boolean;
39
+ onData(chunk: Uint8Array): void;
40
+ err: number;
41
+ msg: string;
42
+ ended?: boolean;
43
+ }
44
+
45
+ /**
46
+ * zlib-wrapped Deflate through pako's incremental classes, for runtimes without
47
+ * Compression Streams. The module is injected, not imported.
48
+ */
49
+ export function pakoDeflateCodec(
50
+ pako: PakoStreamModule,
51
+ opts: { level?: number } = {},
52
+ ): StreamCodec {
53
+ const options = opts.level === undefined ? {} : { level: opts.level };
54
+ return {
55
+ name: "deflate",
56
+ compress: (input) => pakoTransform(new pako.Deflate(options), input, true),
57
+ decompress: (input) => pakoTransform(new pako.Inflate(), input, false),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Feed one input chunk at a time and hand on whatever that produced before
63
+ * pulling again. pako's `push` is synchronous, so its output per chunk is
64
+ * bounded by the chunk.
65
+ */
66
+ async function* pakoTransform(
67
+ stream: PakoStream,
68
+ input: AsyncIterable<Uint8Array>,
69
+ compressing: boolean,
70
+ ): AsyncGenerator<Uint8Array> {
71
+ let pending: Uint8Array[] = [];
72
+ stream.onData = (chunk) => pending.push(chunk);
73
+ const check = () => {
74
+ if (stream.err) throw new Error(`pako: ${stream.msg || `error ${stream.err}`}`);
75
+ };
76
+ for await (const chunk of input) {
77
+ if (chunk.length === 0) continue;
78
+ stream.push(chunk, false);
79
+ check();
80
+ const out = pending;
81
+ pending = [];
82
+ yield* out;
83
+ }
84
+ if (compressing) {
85
+ stream.push(new Uint8Array(0), true);
86
+ check();
87
+ } else if (!stream.ended) {
88
+ throw new Error("pako: truncated deflate stream");
89
+ }
90
+ yield* pending;
91
+ }
92
+
93
+ /** Compression streams accept any BufferSource; this pipe only ever writes Uint8Array. */
94
+ function asPair(transform: CompressionStream | DecompressionStream) {
95
+ return transform as unknown as ReadableWritablePair<Uint8Array, Uint8Array>;
96
+ }
97
+
98
+ function pullStream(source: AsyncIterable<Uint8Array>): ReadableStream<Uint8Array> {
99
+ const iterator = source[Symbol.asyncIterator]();
100
+ return new ReadableStream<Uint8Array>(
101
+ {
102
+ async pull(controller) {
103
+ const next = await iterator.next();
104
+ if (next.done) controller.close();
105
+ else controller.enqueue(next.value);
106
+ },
107
+ async cancel(reason) {
108
+ await iterator.return?.(reason);
109
+ },
110
+ },
111
+ { highWaterMark: 0 },
112
+ );
113
+ }
114
+
115
+ async function* iterate(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
116
+ const reader = stream.getReader();
117
+ let finished = false;
118
+ try {
119
+ for (;;) {
120
+ const { done, value } = await reader.read();
121
+ if (done) {
122
+ finished = true;
123
+ return;
124
+ }
125
+ yield value;
126
+ }
127
+ } finally {
128
+ // An early exit cancels the pipeline, which closes the source.
129
+ if (!finished) await reader.cancel().catch(() => {});
130
+ reader.releaseLock();
131
+ }
132
+ }
package/src/bytes.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** Blobs arrive as Uint8Array (node:sqlite), ArrayBuffer (Durable Objects) or number[] (D1). */
2
+ export function toBytes(value: unknown): Uint8Array {
3
+ if (value instanceof Uint8Array) return value;
4
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
5
+ if (Array.isArray(value)) return Uint8Array.from(value);
6
+ if (typeof value === "string") return new TextEncoder().encode(value);
7
+ return new Uint8Array(0);
8
+ }
package/src/codec.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * SQLAR compression.
3
+ *
4
+ * The format mandates zlib-wrapped Deflate (RFC 1950, the `78 xx` header) —
5
+ * what zlib's `compress()` writes. Every codec here writes exactly that, so an
6
+ * archive written by one is read by any other, and by other SQLAR tools.
7
+ */
8
+ export interface Codec {
9
+ name: string;
10
+ /**
11
+ * Inputs shorter than this are stored plaintext without a deflate attempt:
12
+ * deflating a short input makes it longer.
13
+ */
14
+ minSize: number;
15
+ deflate(bytes: Uint8Array): Uint8Array | Promise<Uint8Array>;
16
+ inflate(data: Uint8Array): Uint8Array | Promise<Uint8Array>;
17
+ }
18
+
19
+ const MIN_SIZE = 64;
20
+
21
+ /** Compression Streams — browsers, Node, Workers. No dependency. */
22
+ export function webCodec(opts: { minSize?: number } = {}): Codec {
23
+ if (typeof CompressionStream === "undefined") {
24
+ throw new Error("webCodec: CompressionStream is not available in this runtime");
25
+ }
26
+ return {
27
+ name: "web",
28
+ minSize: opts.minSize ?? MIN_SIZE,
29
+ // "deflate" is the zlib-wrapped format; "deflate-raw" would not be SQLAR.
30
+ deflate: (bytes) => pipe(bytes, new CompressionStream("deflate")),
31
+ inflate: (data) => pipe(data, new DecompressionStream("deflate")),
32
+ };
33
+ }
34
+
35
+ /** The part of the `pako` module this codec uses. */
36
+ export interface PakoModule {
37
+ deflate(data: Uint8Array, opts?: { level?: number }): Uint8Array;
38
+ inflate(data: Uint8Array): Uint8Array;
39
+ }
40
+
41
+ /**
42
+ * pako — synchronous, for runtimes without a usable CompressionStream. The
43
+ * module is injected so this package does not depend on it:
44
+ *
45
+ * import * as pako from "pako";
46
+ * new SqlarFilesApi(driver, { codec: pakoCodec(pako) });
47
+ */
48
+ export function pakoCodec(
49
+ pako: PakoModule,
50
+ opts: { minSize?: number; level?: number } = {},
51
+ ): Codec {
52
+ const deflateOptions = opts.level === undefined ? {} : { level: opts.level };
53
+ return {
54
+ name: "pako",
55
+ minSize: opts.minSize ?? MIN_SIZE,
56
+ // pako.deflate is zlib-wrapped by default; deflateRaw is not.
57
+ deflate: (bytes) => pako.deflate(bytes, deflateOptions),
58
+ inflate: (data) => pako.inflate(data),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * No compression — still a valid archive. Reading a compressed row through it
64
+ * throws rather than returning deflate bytes as file content.
65
+ */
66
+ export function rawCodec(): Codec {
67
+ return {
68
+ name: "raw",
69
+ minSize: Number.POSITIVE_INFINITY,
70
+ deflate: (bytes) => bytes,
71
+ inflate() {
72
+ throw new Error("rawCodec cannot inflate: this archive holds compressed rows");
73
+ },
74
+ };
75
+ }
76
+
77
+ /** webCodec where CompressionStream exists, rawCodec elsewhere. */
78
+ export function defaultCodec(): Codec {
79
+ return typeof CompressionStream === "undefined" ? rawCodec() : webCodec();
80
+ }
81
+
82
+ async function pipe(
83
+ bytes: Uint8Array,
84
+ transform: TransformStream<BufferSource, Uint8Array>,
85
+ ): Promise<Uint8Array> {
86
+ const stream = new Blob([bytes as BlobPart]).stream().pipeThrough(transform);
87
+ return new Uint8Array(await new Response(stream).arrayBuffer());
88
+ }
package/src/drivers.ts ADDED
@@ -0,0 +1,133 @@
1
+ import type { SqlDriver, SqlStatement } from "./sql.types.js";
2
+
3
+ /** The subset of node:sqlite's `DatabaseSync` this driver uses. */
4
+ export interface NodeSqliteDatabase {
5
+ exec(sql: string): void;
6
+ prepare(sql: string): {
7
+ all(...params: never[]): unknown[];
8
+ run(...params: never[]): { changes: number | bigint };
9
+ };
10
+ }
11
+
12
+ /** node:sqlite — local development, tests and any Node deployment. */
13
+ export class NodeSqlDriver implements SqlDriver {
14
+ readonly #db: NodeSqliteDatabase;
15
+
16
+ constructor(db: NodeSqliteDatabase) {
17
+ this.#db = db;
18
+ }
19
+
20
+ all<T>(sql: string, ...params: unknown[]): T[] {
21
+ return this.#db.prepare(sql).all(...(params as never[])) as T[];
22
+ }
23
+
24
+ run(sql: string, ...params: unknown[]): void {
25
+ this.#db.prepare(sql).run(...(params as never[]));
26
+ }
27
+
28
+ /** Synchronous from BEGIN to COMMIT, so no other call can interleave on the connection. */
29
+ transaction(statements: SqlStatement[]): number[] {
30
+ this.#db.exec("BEGIN IMMEDIATE");
31
+ try {
32
+ const counts = statements.map(({ sql, params = [] }) =>
33
+ Number(this.#db.prepare(sql).run(...(params as never[])).changes),
34
+ );
35
+ this.#db.exec("COMMIT");
36
+ return counts;
37
+ } catch (error) {
38
+ this.#db.exec("ROLLBACK");
39
+ throw error;
40
+ }
41
+ }
42
+ }
43
+
44
+ /** The subset of a Durable Object's `ctx.storage` this driver uses. */
45
+ export interface DoStorage {
46
+ sql: {
47
+ exec(query: string, ...bindings: unknown[]): { toArray(): unknown[] };
48
+ };
49
+ transactionSync<T>(fn: () => T): T;
50
+ }
51
+
52
+ /**
53
+ * Durable Object SQLite storage: pass `ctx.storage`. Synchronous; BLOBs come
54
+ * back as ArrayBuffer. Transactions use `transactionSync`, since `sql.exec`
55
+ * rejects `BEGIN`.
56
+ */
57
+ export class DoSqlDriver implements SqlDriver {
58
+ readonly #storage: DoStorage;
59
+
60
+ constructor(storage: DoStorage) {
61
+ this.#storage = storage;
62
+ }
63
+
64
+ all<T>(sql: string, ...params: unknown[]): T[] {
65
+ return this.#storage.sql.exec(sql, ...params).toArray() as T[];
66
+ }
67
+
68
+ run(sql: string, ...params: unknown[]): void {
69
+ // The cursor is lazy; draining it makes sure the statement has run.
70
+ this.#storage.sql.exec(sql, ...params).toArray();
71
+ }
72
+
73
+ /**
74
+ * Counts come from SQLite's `changes()` on the same connection, not from the
75
+ * cursor's `rowsWritten`, which also counts every index row a write touches.
76
+ */
77
+ transaction(statements: SqlStatement[]): number[] {
78
+ return this.#storage.transactionSync(() =>
79
+ statements.map(({ sql, params = [] }) => {
80
+ this.#storage.sql.exec(sql, ...params).toArray();
81
+ const [row] = this.#storage.sql.exec("SELECT changes() AS n").toArray() as { n: number }[];
82
+ return row.n;
83
+ }),
84
+ );
85
+ }
86
+ }
87
+
88
+ /** The subset of a `D1Database` this driver uses. */
89
+ export interface D1Database {
90
+ prepare(query: string): D1PreparedStatement;
91
+ batch(statements: D1BoundStatement[]): Promise<{ meta: { changes: number } }[]>;
92
+ }
93
+
94
+ interface D1PreparedStatement {
95
+ bind(...values: unknown[]): D1BoundStatement;
96
+ }
97
+
98
+ interface D1BoundStatement {
99
+ all(): Promise<{ results: unknown[] }>;
100
+ run(): Promise<unknown>;
101
+ }
102
+
103
+ /** Cloudflare D1. Asynchronous; BLOBs come back as number[]; transactions are `batch()`. */
104
+ export class D1SqlDriver implements SqlDriver {
105
+ readonly #db: D1Database;
106
+
107
+ constructor(db: D1Database) {
108
+ this.#db = db;
109
+ }
110
+
111
+ async all<T>(sql: string, ...params: unknown[]): Promise<T[]> {
112
+ const { results } = await this.#db
113
+ .prepare(sql)
114
+ .bind(...params)
115
+ .all();
116
+ return results as T[];
117
+ }
118
+
119
+ async run(sql: string, ...params: unknown[]): Promise<void> {
120
+ await this.#db
121
+ .prepare(sql)
122
+ .bind(...params)
123
+ .run();
124
+ }
125
+
126
+ async transaction(statements: SqlStatement[]): Promise<number[]> {
127
+ if (statements.length === 0) return [];
128
+ const results = await this.#db.batch(
129
+ statements.map(({ sql, params = [] }) => this.#db.prepare(sql).bind(...params)),
130
+ );
131
+ return results.map((result) => result.meta.changes);
132
+ }
133
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export { SqliteFilesApi, type SqliteFilesApiOptions } from "./blocks/sqlite-files-api.js";
2
+ export {
3
+ type PakoStreamModule,
4
+ pakoDeflateCodec,
5
+ type StreamCodec,
6
+ webDeflateCodec,
7
+ } from "./blocks/stream-codec.js";
8
+ export {
9
+ type Codec,
10
+ defaultCodec,
11
+ type PakoModule,
12
+ pakoCodec,
13
+ rawCodec,
14
+ webCodec,
15
+ } from "./codec.js";
16
+ export {
17
+ type D1Database,
18
+ D1SqlDriver,
19
+ DoSqlDriver,
20
+ type DoStorage,
21
+ NodeSqlDriver,
22
+ type NodeSqliteDatabase,
23
+ } from "./drivers.js";
24
+ export type { SqlDriver, SqlStatement } from "./sql.types.js";
25
+ export { SqlarFilesApi, type SqlarFilesApiOptions } from "./sqlar-files-api.js";
@@ -0,0 +1,25 @@
1
+ /** One statement of a transaction. */
2
+ export interface SqlStatement {
3
+ sql: string;
4
+ params?: unknown[];
5
+ }
6
+
7
+ /**
8
+ * The SQL port. Each method may be sync or async, so node:sqlite, a Durable
9
+ * Object's storage and D1 all satisfy it through a thin driver.
10
+ */
11
+ export interface SqlDriver {
12
+ /** Rows as objects keyed by column name. */
13
+ all<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[] | Promise<T[]>;
14
+ /** Execute one statement for its effect. */
15
+ run(sql: string, ...params: unknown[]): void | Promise<void>;
16
+ /**
17
+ * Run the statements in order as one transaction: all apply or none do, and
18
+ * a failure rethrows after rolling back. Returns, per statement, a count that
19
+ * is zero exactly when that statement changed nothing.
20
+ *
21
+ * No statement may depend on reading another's result — D1's `batch()` cannot
22
+ * pass results along — so conditions belong in SQL (`WHERE EXISTS …`).
23
+ */
24
+ transaction(statements: SqlStatement[]): number[] | Promise<number[]>;
25
+ }