stratum-sqlite 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YOUR_NAME
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # stratum-sqlite
2
+
3
+ Load and query a **read-only SQLite database on any static website** — plain HTML,
4
+ Quarto, Jekyll, Hugo, and more. No server. No backend.
5
+
6
+ The database file is fetched once from any public URL (GitHub Releases, Zenodo,
7
+ your own GitHub Pages, Cloudflare R2, …) and stored in the browser's **Cache API**,
8
+ so every page on your site after the first one loads it instantly from local cache.
9
+
10
+ ---
11
+
12
+ ## How it works
13
+
14
+ ```
15
+ Your browser Your static site server
16
+ │ │
17
+ │ first visit │
18
+ ├─────── GET /libs/sqljs/sql-wasm.js ────►│
19
+ ├─────── GET /libs/sqljs/sql-wasm.wasm ──►│
20
+ ├─────── GET /data/mydb.sqlite ──────────►│
21
+ │ │
22
+ │ second visit (and all other pages) │
23
+ │ ◄── served from browser Cache API ─────┤ (no network request!)
24
+ │ │
25
+ ```
26
+
27
+ sql.js compiles SQLite to WebAssembly and runs it entirely in the browser.
28
+ `stratum-sqlite` wraps sql.js with a simple `open()` / `query()` API and adds
29
+ transparent caching.
30
+
31
+ ---
32
+
33
+ ## Quick start
34
+
35
+ ### For the demo site (local preview)
36
+
37
+ The fastest way to get the demo running locally:
38
+
39
+ ```bash
40
+ git clone https://github.com/stratum-toolkit/stratum-sqlite
41
+ cd stratum-sqlite
42
+ node build.mjs # build the library
43
+ bash setup.sh # download sql.js binaries into docs/libs/sqljs/
44
+ python3 -m http.server 8000 --directory docs
45
+ # open http://localhost:8000
46
+ ```
47
+
48
+ ### Option A — Self-hosted (for your own project)
49
+
50
+ 1. **Download sql.js and the library** using `setup.sh` (if you have the repo),
51
+ or manually:
52
+
53
+ ```bash
54
+ mkdir -p libs/sqljs
55
+ curl -sSfL https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js \
56
+ -o libs/sqljs/sql-wasm.js
57
+ curl -sSfL https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.wasm \
58
+ -o libs/sqljs/sql-wasm.wasm
59
+ ```
60
+
61
+ Then download `stratum-sqlite.umd.js` from the
62
+ [Releases page](https://github.com/stratum-toolkit/stratum-sqlite/releases) and
63
+ place it alongside sql.js:
64
+
65
+ ```
66
+ libs/
67
+ └── sqljs/
68
+ ├── sql-wasm.js
69
+ ├── sql-wasm.wasm
70
+ └── stratum-sqlite.umd.js
71
+ ```
72
+
73
+ 2. **Use it in your HTML**:
74
+
75
+ ```html
76
+ <script src="libs/sqljs/sql-wasm.js"></script>
77
+ <script src="libs/sqljs/stratum-sqlite.umd.js"></script>
78
+
79
+ <script type="module">
80
+ const db = await StratumSQLite.open("data/mydb.sqlite", {
81
+ sqlJsPath: "libs/sqljs/",
82
+ cacheKey: "mydb@v1", // bump version when you publish a new DB
83
+ });
84
+
85
+ const rows = db.query("SELECT * FROM countries WHERE region = ?", ["Europe"]);
86
+ console.log(rows);
87
+ </script>
88
+ ```
89
+
90
+ ### Option B — npm + bundler
91
+
92
+ ```bash
93
+ npm install stratum-sqlite
94
+ ```
95
+
96
+ ```js
97
+ import StratumSQLite from 'stratum-sqlite';
98
+
99
+ const db = await StratumSQLite.open("/data/mydb.sqlite", {
100
+ sqlJsPath: "/libs/sqljs/",
101
+ cacheKey: "mydb@v1",
102
+ });
103
+
104
+ const rows = db.query("SELECT * FROM countries");
105
+ ```
106
+
107
+ ---
108
+
109
+ ## API
110
+
111
+ ### `StratumSQLite.open(url, options)` → `Promise<Database>`
112
+
113
+ Fetches the SQLite file at `url` and returns a `Database` instance.
114
+
115
+ | Option | Type | Default | Description |
116
+ |--------|------|---------|-------------|
117
+ | `sqlJsPath` | `string` | cdnjs URL | Path (relative or absolute) to the folder containing `sql-wasm.js` and `sql-wasm.wasm`. |
118
+ | `cacheKey` | `string` | `"stratum-sqlite:<url>@1"` | Browser Cache API bucket name. Bump the suffix (e.g. `@v2`) to force a re-download on your next publish. |
119
+ | `onProgress` | `function(loaded, total)` | — | Called during the first download to update a loading bar. |
120
+
121
+ ### `db.query(sql, params?)` → `Array<Object>`
122
+
123
+ Runs a SQL statement and returns rows as plain objects.
124
+
125
+ ```js
126
+ db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
127
+ // → [{ name: "Norway", capital: "Oslo" }, …]
128
+ ```
129
+
130
+ ### `db.tables()` → `string[]`
131
+
132
+ Returns the names of all user tables.
133
+
134
+ ### `db.columns(tableName)` → `Array<Object>`
135
+
136
+ Returns column definitions from `PRAGMA table_info`.
137
+
138
+ ### `db.count(tableName)` → `number`
139
+
140
+ Returns the total row count of a table.
141
+
142
+ ---
143
+
144
+ ## Quarto / ObservableJS integration
145
+
146
+ Add `_sqljs-init.html` to your project root:
147
+
148
+ ```html
149
+ <!-- _sqljs-init.html -->
150
+ <script>
151
+ (function () {
152
+ // Detect page depth by reading the relative path of any Quarto site_libs script.
153
+ var siteLibScript = document.querySelector('script[src*="site_libs/"]');
154
+ var root = siteLibScript
155
+ ? siteLibScript.getAttribute('src').replace(/site_libs\/.*$/, '')
156
+ : '';
157
+
158
+ window._dbPath = root + 'data/mydb.sqlite';
159
+ window._sqljsBase = root + 'libs/sqljs/';
160
+
161
+ window._sqlJsReady = new Promise(function (resolve, reject) {
162
+ var s = document.createElement('script');
163
+ s.src = window._sqljsBase + 'sql-wasm.js';
164
+ s.onload = function () {
165
+ initSqlJs({ locateFile: function () { return window._sqljsBase + 'sql-wasm.wasm'; } })
166
+ .then(resolve).catch(reject);
167
+ };
168
+ s.onerror = function () { reject(new Error('sql.js load failed: ' + s.src)); };
169
+ document.head.appendChild(s);
170
+ });
171
+ }());
172
+ </script>
173
+ ```
174
+
175
+ In `_quarto.yml`:
176
+
177
+ ```yaml
178
+ format:
179
+ html:
180
+ include-in-header: _sqljs-init.html
181
+ ```
182
+
183
+ In any `.qmd` file:
184
+
185
+ ```{ojs}
186
+ db = {
187
+ const SqlJs = await window._sqlJsReady;
188
+ const r = await fetch(window._dbPath);
189
+ const raw = new SqlJs.Database(new Uint8Array(await r.arrayBuffer()));
190
+ return {
191
+ query: sql => {
192
+ const res = raw.exec(sql);
193
+ if (!res.length) return [];
194
+ const { columns, values } = res[0];
195
+ return values.map(row =>
196
+ Object.fromEntries(columns.map((c, i) => [c, row[i]])));
197
+ }
198
+ };
199
+ }
200
+
201
+ rows = (await db).query("SELECT * FROM countries")
202
+ Inputs.table(rows)
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Hosting your database
208
+
209
+ Your SQLite file can be hosted anywhere with public HTTPS and
210
+ `Access-Control-Allow-Origin: *`:
211
+
212
+ | Host | Free | Max size | CORS |
213
+ |------|------|----------|------|
214
+ | **GitHub Pages** (same origin) | ✓ | 1 GB repo limit | same-origin |
215
+ | **GitHub Releases** | ✓ | 2 GB per file | ✓ automatic |
216
+ | **Zenodo** | ✓ | 50 GB | ✓ |
217
+ | **Figshare** | ✓ | 20 GB | ✓ |
218
+ | **Cloudflare R2** | free tier | unlimited | configure bucket policy |
219
+ | **AWS S3** | free tier | unlimited | configure bucket policy |
220
+
221
+ ---
222
+
223
+ ## Updating the database
224
+
225
+ 1. Publish the new `.sqlite` file to your chosen host.
226
+ 2. Bump the `cacheKey` option: `"mydb@v1"` → `"mydb@v2"`.
227
+ `stratum-sqlite` automatically evicts the old cached file on next page load.
228
+
229
+ ---
230
+
231
+ ## Developing this library
232
+
233
+ ```bash
234
+ git clone https://github.com/stratum-toolkit/stratum-sqlite
235
+ cd stratum-sqlite
236
+
237
+ # 1. Build dist bundles
238
+ node build.mjs
239
+
240
+ # 2. Download sql.js binaries and copy the built library (one command)
241
+ bash setup.sh
242
+
243
+ # 3. Create sample database if needed
244
+ python3 scripts/create_demo_db.py # Python
245
+ Rscript scripts/create_demo_db.R # R
246
+
247
+ # 4. Serve demo site
248
+ python3 -m http.server 8000 --directory docs
249
+ # open http://localhost:8000
250
+ ```
251
+
252
+ `setup.sh` downloads `sql-wasm.js` and `sql-wasm.wasm` from cdnjs into
253
+ `docs/libs/sqljs/` and copies the built library there. These binary files
254
+ are in `.gitignore` — the CI pipeline downloads them fresh on every deploy.
255
+
256
+ ---
257
+
258
+ ## License
259
+
260
+ MIT
@@ -0,0 +1,240 @@
1
+ /* stratum-sqlite v0.1.2 | MIT license | https://github.com/stratum-toolkit/stratum-sqlite */
2
+ /**
3
+ * stratum-sqlite
4
+ * Load and query a read-only SQLite database on any static website.
5
+ *
6
+ * Works with plain HTML, Quarto / ObservableJS, Jekyll, Hugo, and any other
7
+ * static site generator. The database is fetched once and cached in the
8
+ * browser's Cache API so repeat visits (and other pages on the same site)
9
+ * skip the network entirely.
10
+ *
11
+ * @license MIT
12
+ */
13
+
14
+ // ─── Default sql.js location ────────────────────────────────────────────────
15
+ // Users can override this with the sqlJsPath option when calling open().
16
+ // The default points to cdnjs, but for offline / restricted environments you
17
+ // should download sql-wasm.js and sql-wasm.wasm and serve them yourself.
18
+ const DEFAULT_SQLJS_CDN =
19
+ 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/';
20
+
21
+ // ─── Internal helpers ────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Inject a <script> tag and return a promise that resolves when it loads.
25
+ * Skips injection if a tag with the same src already exists.
26
+ */
27
+ function loadScript(src) {
28
+ return new Promise((resolve, reject) => {
29
+ if (document.querySelector(`script[src="${src}"]`)) {
30
+ resolve();
31
+ return;
32
+ }
33
+ const s = document.createElement('script');
34
+ s.src = src;
35
+ s.crossOrigin = 'anonymous';
36
+ s.onload = resolve;
37
+ s.onerror = () => reject(new Error(`stratum-sqlite: failed to load script: ${src}`));
38
+ document.head.appendChild(s);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Fetch a URL as a Uint8Array, using the browser Cache API when available.
44
+ *
45
+ * @param {string} url - URL of the resource to fetch
46
+ * @param {string} cacheKey - Cache storage name (bump to force re-download)
47
+ * @param {function} onProgress - Optional callback(loaded, total) for progress
48
+ */
49
+ async function fetchWithCache(url, cacheKey, onProgress) {
50
+ // Cache API requires a secure context (HTTPS or localhost).
51
+ // Fall back to a plain fetch when unavailable (e.g. file://).
52
+ if (!('caches' in window)) {
53
+ return plainFetch(url, onProgress);
54
+ }
55
+
56
+ const cache = await caches.open(cacheKey);
57
+
58
+ // Evict caches from older versions of the same key prefix.
59
+ // Convention: keys are "stratum-sqlite:<name>@<version>"
60
+ const prefix = cacheKey.replace(/@[^@]*$/, '@');
61
+ const allKeys = await caches.keys();
62
+ for (const key of allKeys) {
63
+ if (key.startsWith(prefix) && key !== cacheKey) {
64
+ await caches.delete(key);
65
+ }
66
+ }
67
+
68
+ let cached = await cache.match(url);
69
+ if (!cached) {
70
+ const response = await fetch(url);
71
+ if (!response.ok) {
72
+ throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
73
+ }
74
+ await cache.put(url, response.clone());
75
+ cached = await cache.match(url);
76
+ }
77
+
78
+ return streamToUint8Array(cached, onProgress);
79
+ }
80
+
81
+ async function plainFetch(url, onProgress) {
82
+ const response = await fetch(url);
83
+ if (!response.ok) {
84
+ throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
85
+ }
86
+ return streamToUint8Array(response, onProgress);
87
+ }
88
+
89
+ /**
90
+ * Read a Response body as Uint8Array, reporting progress if the callback and
91
+ * Content-Length header are both available.
92
+ */
93
+ async function streamToUint8Array(response, onProgress) {
94
+ const total = Number(response.headers.get('content-length')) || 0;
95
+
96
+ if (!onProgress || !total || !response.body) {
97
+ return new Uint8Array(await response.arrayBuffer());
98
+ }
99
+
100
+ const reader = response.body.getReader();
101
+ const chunks = [];
102
+ let loaded = 0;
103
+
104
+ while (true) {
105
+ const { done, value } = await reader.read();
106
+ if (done) break;
107
+ chunks.push(value);
108
+ loaded += value.length;
109
+ onProgress(loaded, total);
110
+ }
111
+
112
+ const result = new Uint8Array(loaded);
113
+ let offset = 0;
114
+ for (const chunk of chunks) {
115
+ result.set(chunk, offset);
116
+ offset += chunk.length;
117
+ }
118
+ return result;
119
+ }
120
+
121
+ // ─── Database wrapper ────────────────────────────────────────────────────────
122
+
123
+ class Database {
124
+ /** @param {object} sqlJsDb - raw sql.js Database instance */
125
+ constructor(sqlJsDb) {
126
+ this._db = sqlJsDb;
127
+ }
128
+
129
+ /**
130
+ * Run a SQL query and return results as an array of plain objects.
131
+ *
132
+ * @param {string} sql - SQL statement (SELECT …)
133
+ * @param {Array} params - Optional positional parameters (? placeholders)
134
+ * @returns {Array<Object>}
135
+ *
136
+ * @example
137
+ * db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
138
+ */
139
+ query(sql, params) {
140
+ const results = this._db.exec(sql, params);
141
+ if (!results.length) return [];
142
+ const { columns, values } = results[0];
143
+ return values.map(row =>
144
+ Object.fromEntries(columns.map((col, i) => [col, row[i]]))
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Return the names of all user tables in the database.
150
+ * @returns {string[]}
151
+ */
152
+ tables() {
153
+ return this.query(
154
+ "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
155
+ ).map(r => r.name);
156
+ }
157
+
158
+ /**
159
+ * Return column definitions for a table.
160
+ * @param {string} tableName
161
+ * @returns {Array<{cid, name, type, notnull, dflt_value, pk}>}
162
+ */
163
+ columns(tableName) {
164
+ return this.query(`PRAGMA table_info("${tableName}")`);
165
+ }
166
+
167
+ /**
168
+ * Count rows in a table.
169
+ * @param {string} tableName
170
+ * @returns {number}
171
+ */
172
+ count(tableName) {
173
+ return this.query(`SELECT COUNT(*) as n FROM "${tableName}"`)[0]?.n ?? 0;
174
+ }
175
+ }
176
+
177
+ // ─── Public API ──────────────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * @typedef {Object} OpenOptions
181
+ * @property {string} [sqlJsPath] - Base path/URL for sql-wasm.js and sql-wasm.wasm.
182
+ * Defaults to cdnjs. Set to a local path like
183
+ * "/libs/sqljs/" when serving sql.js yourself.
184
+ * @property {string} [cacheKey] - Browser Cache API key.
185
+ * Defaults to "stratum-sqlite:<url>@1".
186
+ * Bump the version suffix to force a re-download
187
+ * when you publish a new database release.
188
+ * @property {function} [onProgress] - Called with (bytesLoaded, bytesTotal) during
189
+ * the first download. Not called on cache hits.
190
+ */
191
+
192
+ /**
193
+ * Fetch a SQLite database from `url` and return a Database instance ready to
194
+ * query. On first call the file is downloaded and stored in the browser's
195
+ * Cache API. Subsequent calls (including from other pages on the same origin)
196
+ * are served instantly from cache.
197
+ *
198
+ * @param {string} url - URL of the .sqlite file (absolute or relative)
199
+ * @param {OpenOptions} options
200
+ * @returns {Promise<Database>}
201
+ *
202
+ * @example <caption>Plain HTML</caption>
203
+ * const db = await StratumSQLite.open("data/mydb.sqlite", {
204
+ * sqlJsPath: "libs/sqljs/",
205
+ * cacheKey: "mydb@v1.2",
206
+ * });
207
+ * const rows = db.query("SELECT * FROM countries");
208
+ *
209
+ * @example <caption>Quarto / ObservableJS cell</caption>
210
+ * db = StratumSQLite.open(window._dbPath, { sqlJsPath: window._sqljsBase })
211
+ * rows = (await db).query("SELECT * FROM countries")
212
+ */
213
+ async function open(url, options = {}) {
214
+ const sqlJsBase = options.sqlJsPath || DEFAULT_SQLJS_CDN;
215
+ const cacheKey = options.cacheKey || `stratum-sqlite:${url}@1`;
216
+ const onProgress = options.onProgress || null;
217
+
218
+ // Ensure trailing slash on sqlJsBase
219
+ const base = sqlJsBase.endsWith('/') ? sqlJsBase : sqlJsBase + '/';
220
+
221
+ // Load the sql.js bootstrap script (sets global `initSqlJs`)
222
+ await loadScript(base + 'sql-wasm.js');
223
+
224
+ // Initialise the WASM module
225
+ const SQL = await initSqlJs({ // eslint-disable-line no-undef
226
+ locateFile: () => base + 'sql-wasm.wasm',
227
+ });
228
+
229
+ // Fetch the database (cache hit after first load)
230
+ const bytes = await fetchWithCache(url, cacheKey, onProgress);
231
+
232
+ return new Database(new SQL.Database(bytes));
233
+ }
234
+
235
+ const StratumSQLite = { open, Database };
236
+
237
+
238
+
239
+ export { open, Database };
240
+ export default StratumSQLite;
@@ -0,0 +1,247 @@
1
+ /* stratum-sqlite v0.1.2 | MIT license | https://github.com/stratum-toolkit/stratum-sqlite */
2
+ (function (global, factory) {
3
+ typeof exports === 'object' && typeof module !== 'undefined'
4
+ ? module.exports = factory()
5
+ : (global = typeof globalThis !== 'undefined' ? globalThis : global || self,
6
+ global.StratumSQLite = factory());
7
+ })(this, function () {
8
+ 'use strict';
9
+ /**
10
+ * stratum-sqlite
11
+ * Load and query a read-only SQLite database on any static website.
12
+ *
13
+ * Works with plain HTML, Quarto / ObservableJS, Jekyll, Hugo, and any other
14
+ * static site generator. The database is fetched once and cached in the
15
+ * browser's Cache API so repeat visits (and other pages on the same site)
16
+ * skip the network entirely.
17
+ *
18
+ * @license MIT
19
+ */
20
+
21
+ // ─── Default sql.js location ────────────────────────────────────────────────
22
+ // Users can override this with the sqlJsPath option when calling open().
23
+ // The default points to cdnjs, but for offline / restricted environments you
24
+ // should download sql-wasm.js and sql-wasm.wasm and serve them yourself.
25
+ const DEFAULT_SQLJS_CDN =
26
+ 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/';
27
+
28
+ // ─── Internal helpers ────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Inject a <script> tag and return a promise that resolves when it loads.
32
+ * Skips injection if a tag with the same src already exists.
33
+ */
34
+ function loadScript(src) {
35
+ return new Promise((resolve, reject) => {
36
+ if (document.querySelector(`script[src="${src}"]`)) {
37
+ resolve();
38
+ return;
39
+ }
40
+ const s = document.createElement('script');
41
+ s.src = src;
42
+ s.crossOrigin = 'anonymous';
43
+ s.onload = resolve;
44
+ s.onerror = () => reject(new Error(`stratum-sqlite: failed to load script: ${src}`));
45
+ document.head.appendChild(s);
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Fetch a URL as a Uint8Array, using the browser Cache API when available.
51
+ *
52
+ * @param {string} url - URL of the resource to fetch
53
+ * @param {string} cacheKey - Cache storage name (bump to force re-download)
54
+ * @param {function} onProgress - Optional callback(loaded, total) for progress
55
+ */
56
+ async function fetchWithCache(url, cacheKey, onProgress) {
57
+ // Cache API requires a secure context (HTTPS or localhost).
58
+ // Fall back to a plain fetch when unavailable (e.g. file://).
59
+ if (!('caches' in window)) {
60
+ return plainFetch(url, onProgress);
61
+ }
62
+
63
+ const cache = await caches.open(cacheKey);
64
+
65
+ // Evict caches from older versions of the same key prefix.
66
+ // Convention: keys are "stratum-sqlite:<name>@<version>"
67
+ const prefix = cacheKey.replace(/@[^@]*$/, '@');
68
+ const allKeys = await caches.keys();
69
+ for (const key of allKeys) {
70
+ if (key.startsWith(prefix) && key !== cacheKey) {
71
+ await caches.delete(key);
72
+ }
73
+ }
74
+
75
+ let cached = await cache.match(url);
76
+ if (!cached) {
77
+ const response = await fetch(url);
78
+ if (!response.ok) {
79
+ throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
80
+ }
81
+ await cache.put(url, response.clone());
82
+ cached = await cache.match(url);
83
+ }
84
+
85
+ return streamToUint8Array(cached, onProgress);
86
+ }
87
+
88
+ async function plainFetch(url, onProgress) {
89
+ const response = await fetch(url);
90
+ if (!response.ok) {
91
+ throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
92
+ }
93
+ return streamToUint8Array(response, onProgress);
94
+ }
95
+
96
+ /**
97
+ * Read a Response body as Uint8Array, reporting progress if the callback and
98
+ * Content-Length header are both available.
99
+ */
100
+ async function streamToUint8Array(response, onProgress) {
101
+ const total = Number(response.headers.get('content-length')) || 0;
102
+
103
+ if (!onProgress || !total || !response.body) {
104
+ return new Uint8Array(await response.arrayBuffer());
105
+ }
106
+
107
+ const reader = response.body.getReader();
108
+ const chunks = [];
109
+ let loaded = 0;
110
+
111
+ while (true) {
112
+ const { done, value } = await reader.read();
113
+ if (done) break;
114
+ chunks.push(value);
115
+ loaded += value.length;
116
+ onProgress(loaded, total);
117
+ }
118
+
119
+ const result = new Uint8Array(loaded);
120
+ let offset = 0;
121
+ for (const chunk of chunks) {
122
+ result.set(chunk, offset);
123
+ offset += chunk.length;
124
+ }
125
+ return result;
126
+ }
127
+
128
+ // ─── Database wrapper ────────────────────────────────────────────────────────
129
+
130
+ class Database {
131
+ /** @param {object} sqlJsDb - raw sql.js Database instance */
132
+ constructor(sqlJsDb) {
133
+ this._db = sqlJsDb;
134
+ }
135
+
136
+ /**
137
+ * Run a SQL query and return results as an array of plain objects.
138
+ *
139
+ * @param {string} sql - SQL statement (SELECT …)
140
+ * @param {Array} params - Optional positional parameters (? placeholders)
141
+ * @returns {Array<Object>}
142
+ *
143
+ * @example
144
+ * db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
145
+ */
146
+ query(sql, params) {
147
+ const results = this._db.exec(sql, params);
148
+ if (!results.length) return [];
149
+ const { columns, values } = results[0];
150
+ return values.map(row =>
151
+ Object.fromEntries(columns.map((col, i) => [col, row[i]]))
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Return the names of all user tables in the database.
157
+ * @returns {string[]}
158
+ */
159
+ tables() {
160
+ return this.query(
161
+ "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
162
+ ).map(r => r.name);
163
+ }
164
+
165
+ /**
166
+ * Return column definitions for a table.
167
+ * @param {string} tableName
168
+ * @returns {Array<{cid, name, type, notnull, dflt_value, pk}>}
169
+ */
170
+ columns(tableName) {
171
+ return this.query(`PRAGMA table_info("${tableName}")`);
172
+ }
173
+
174
+ /**
175
+ * Count rows in a table.
176
+ * @param {string} tableName
177
+ * @returns {number}
178
+ */
179
+ count(tableName) {
180
+ return this.query(`SELECT COUNT(*) as n FROM "${tableName}"`)[0]?.n ?? 0;
181
+ }
182
+ }
183
+
184
+ // ─── Public API ──────────────────────────────────────────────────────────────
185
+
186
+ /**
187
+ * @typedef {Object} OpenOptions
188
+ * @property {string} [sqlJsPath] - Base path/URL for sql-wasm.js and sql-wasm.wasm.
189
+ * Defaults to cdnjs. Set to a local path like
190
+ * "/libs/sqljs/" when serving sql.js yourself.
191
+ * @property {string} [cacheKey] - Browser Cache API key.
192
+ * Defaults to "stratum-sqlite:<url>@1".
193
+ * Bump the version suffix to force a re-download
194
+ * when you publish a new database release.
195
+ * @property {function} [onProgress] - Called with (bytesLoaded, bytesTotal) during
196
+ * the first download. Not called on cache hits.
197
+ */
198
+
199
+ /**
200
+ * Fetch a SQLite database from `url` and return a Database instance ready to
201
+ * query. On first call the file is downloaded and stored in the browser's
202
+ * Cache API. Subsequent calls (including from other pages on the same origin)
203
+ * are served instantly from cache.
204
+ *
205
+ * @param {string} url - URL of the .sqlite file (absolute or relative)
206
+ * @param {OpenOptions} options
207
+ * @returns {Promise<Database>}
208
+ *
209
+ * @example <caption>Plain HTML</caption>
210
+ * const db = await StratumSQLite.open("data/mydb.sqlite", {
211
+ * sqlJsPath: "libs/sqljs/",
212
+ * cacheKey: "mydb@v1.2",
213
+ * });
214
+ * const rows = db.query("SELECT * FROM countries");
215
+ *
216
+ * @example <caption>Quarto / ObservableJS cell</caption>
217
+ * db = StratumSQLite.open(window._dbPath, { sqlJsPath: window._sqljsBase })
218
+ * rows = (await db).query("SELECT * FROM countries")
219
+ */
220
+ async function open(url, options = {}) {
221
+ const sqlJsBase = options.sqlJsPath || DEFAULT_SQLJS_CDN;
222
+ const cacheKey = options.cacheKey || `stratum-sqlite:${url}@1`;
223
+ const onProgress = options.onProgress || null;
224
+
225
+ // Ensure trailing slash on sqlJsBase
226
+ const base = sqlJsBase.endsWith('/') ? sqlJsBase : sqlJsBase + '/';
227
+
228
+ // Load the sql.js bootstrap script (sets global `initSqlJs`)
229
+ await loadScript(base + 'sql-wasm.js');
230
+
231
+ // Initialise the WASM module
232
+ const SQL = await initSqlJs({ // eslint-disable-line no-undef
233
+ locateFile: () => base + 'sql-wasm.wasm',
234
+ });
235
+
236
+ // Fetch the database (cache hit after first load)
237
+ const bytes = await fetchWithCache(url, cacheKey, onProgress);
238
+
239
+ return new Database(new SQL.Database(bytes));
240
+ }
241
+
242
+ const StratumSQLite = { open, Database };
243
+
244
+
245
+
246
+ return StratumSQLite;
247
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "stratum-sqlite",
3
+ "version": "0.1.2",
4
+ "description": "Load and query a read-only SQLite database on any static website (plain HTML, Quarto, Jekyll, Hugo, …). Zero server required.",
5
+ "keywords": [
6
+ "sqlite",
7
+ "static-site",
8
+ "quarto",
9
+ "observablejs",
10
+ "indexeddb",
11
+ "sql"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Your Name <you@example.com>",
15
+ "homepage": "https://github.com/stratum-toolkit/stratum-sqlite#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/stratum-toolkit/stratum-sqlite.git"
19
+ },
20
+ "type": "module",
21
+ "main": "dist/stratum-sqlite.umd.js",
22
+ "module": "dist/stratum-sqlite.esm.js",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/stratum-sqlite.esm.js",
26
+ "require": "./dist/stratum-sqlite.umd.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "node build.mjs",
37
+ "dev": "node build.mjs --watch",
38
+ "prepare": "npm run build"
39
+ },
40
+ "devDependencies": {
41
+ "esbuild": "^0.28.0"
42
+ },
43
+ "peerDependencies": {
44
+ "sql.js": ">=1.10.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "sql.js": {
48
+ "optional": true
49
+ }
50
+ }
51
+ }
package/src/index.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * stratum-sqlite
3
+ * Load and query a read-only SQLite database on any static website.
4
+ *
5
+ * Works with plain HTML, Quarto / ObservableJS, Jekyll, Hugo, and any other
6
+ * static site generator. The database is fetched once and cached in the
7
+ * browser's Cache API so repeat visits (and other pages on the same site)
8
+ * skip the network entirely.
9
+ *
10
+ * @license MIT
11
+ */
12
+
13
+ // ─── Default sql.js location ────────────────────────────────────────────────
14
+ // Users can override this with the sqlJsPath option when calling open().
15
+ // The default points to cdnjs, but for offline / restricted environments you
16
+ // should download sql-wasm.js and sql-wasm.wasm and serve them yourself.
17
+ const DEFAULT_SQLJS_CDN =
18
+ 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/';
19
+
20
+ // ─── Internal helpers ────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Inject a <script> tag and return a promise that resolves when it loads.
24
+ * Skips injection if a tag with the same src already exists.
25
+ */
26
+ function loadScript(src) {
27
+ return new Promise((resolve, reject) => {
28
+ if (document.querySelector(`script[src="${src}"]`)) {
29
+ resolve();
30
+ return;
31
+ }
32
+ const s = document.createElement('script');
33
+ s.src = src;
34
+ s.crossOrigin = 'anonymous';
35
+ s.onload = resolve;
36
+ s.onerror = () => reject(new Error(`stratum-sqlite: failed to load script: ${src}`));
37
+ document.head.appendChild(s);
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Fetch a URL as a Uint8Array, using the browser Cache API when available.
43
+ *
44
+ * @param {string} url - URL of the resource to fetch
45
+ * @param {string} cacheKey - Cache storage name (bump to force re-download)
46
+ * @param {function} onProgress - Optional callback(loaded, total) for progress
47
+ */
48
+ async function fetchWithCache(url, cacheKey, onProgress) {
49
+ // Cache API requires a secure context (HTTPS or localhost).
50
+ // Fall back to a plain fetch when unavailable (e.g. file://).
51
+ if (!('caches' in window)) {
52
+ return plainFetch(url, onProgress);
53
+ }
54
+
55
+ const cache = await caches.open(cacheKey);
56
+
57
+ // Evict caches from older versions of the same key prefix.
58
+ // Convention: keys are "stratum-sqlite:<name>@<version>"
59
+ const prefix = cacheKey.replace(/@[^@]*$/, '@');
60
+ const allKeys = await caches.keys();
61
+ for (const key of allKeys) {
62
+ if (key.startsWith(prefix) && key !== cacheKey) {
63
+ await caches.delete(key);
64
+ }
65
+ }
66
+
67
+ let cached = await cache.match(url);
68
+ if (!cached) {
69
+ const response = await fetch(url);
70
+ if (!response.ok) {
71
+ throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
72
+ }
73
+ await cache.put(url, response.clone());
74
+ cached = await cache.match(url);
75
+ }
76
+
77
+ return streamToUint8Array(cached, onProgress);
78
+ }
79
+
80
+ async function plainFetch(url, onProgress) {
81
+ const response = await fetch(url);
82
+ if (!response.ok) {
83
+ throw new Error(`stratum-sqlite: fetch failed (${response.status}) ${url}`);
84
+ }
85
+ return streamToUint8Array(response, onProgress);
86
+ }
87
+
88
+ /**
89
+ * Read a Response body as Uint8Array, reporting progress if the callback and
90
+ * Content-Length header are both available.
91
+ */
92
+ async function streamToUint8Array(response, onProgress) {
93
+ const total = Number(response.headers.get('content-length')) || 0;
94
+
95
+ if (!onProgress || !total || !response.body) {
96
+ return new Uint8Array(await response.arrayBuffer());
97
+ }
98
+
99
+ const reader = response.body.getReader();
100
+ const chunks = [];
101
+ let loaded = 0;
102
+
103
+ while (true) {
104
+ const { done, value } = await reader.read();
105
+ if (done) break;
106
+ chunks.push(value);
107
+ loaded += value.length;
108
+ onProgress(loaded, total);
109
+ }
110
+
111
+ const result = new Uint8Array(loaded);
112
+ let offset = 0;
113
+ for (const chunk of chunks) {
114
+ result.set(chunk, offset);
115
+ offset += chunk.length;
116
+ }
117
+ return result;
118
+ }
119
+
120
+ // ─── Database wrapper ────────────────────────────────────────────────────────
121
+
122
+ class Database {
123
+ /** @param {object} sqlJsDb - raw sql.js Database instance */
124
+ constructor(sqlJsDb) {
125
+ this._db = sqlJsDb;
126
+ }
127
+
128
+ /**
129
+ * Run a SQL query and return results as an array of plain objects.
130
+ *
131
+ * @param {string} sql - SQL statement (SELECT …)
132
+ * @param {Array} params - Optional positional parameters (? placeholders)
133
+ * @returns {Array<Object>}
134
+ *
135
+ * @example
136
+ * db.query("SELECT name, capital FROM countries WHERE region = ?", ["Europe"])
137
+ */
138
+ query(sql, params) {
139
+ const results = this._db.exec(sql, params);
140
+ if (!results.length) return [];
141
+ const { columns, values } = results[0];
142
+ return values.map(row =>
143
+ Object.fromEntries(columns.map((col, i) => [col, row[i]]))
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Return the names of all user tables in the database.
149
+ * @returns {string[]}
150
+ */
151
+ tables() {
152
+ return this.query(
153
+ "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
154
+ ).map(r => r.name);
155
+ }
156
+
157
+ /**
158
+ * Return column definitions for a table.
159
+ * @param {string} tableName
160
+ * @returns {Array<{cid, name, type, notnull, dflt_value, pk}>}
161
+ */
162
+ columns(tableName) {
163
+ return this.query(`PRAGMA table_info("${tableName}")`);
164
+ }
165
+
166
+ /**
167
+ * Count rows in a table.
168
+ * @param {string} tableName
169
+ * @returns {number}
170
+ */
171
+ count(tableName) {
172
+ return this.query(`SELECT COUNT(*) as n FROM "${tableName}"`)[0]?.n ?? 0;
173
+ }
174
+ }
175
+
176
+ // ─── Public API ──────────────────────────────────────────────────────────────
177
+
178
+ /**
179
+ * @typedef {Object} OpenOptions
180
+ * @property {string} [sqlJsPath] - Base path/URL for sql-wasm.js and sql-wasm.wasm.
181
+ * Defaults to cdnjs. Set to a local path like
182
+ * "/libs/sqljs/" when serving sql.js yourself.
183
+ * @property {string} [cacheKey] - Browser Cache API key.
184
+ * Defaults to "stratum-sqlite:<url>@1".
185
+ * Bump the version suffix to force a re-download
186
+ * when you publish a new database release.
187
+ * @property {function} [onProgress] - Called with (bytesLoaded, bytesTotal) during
188
+ * the first download. Not called on cache hits.
189
+ */
190
+
191
+ /**
192
+ * Fetch a SQLite database from `url` and return a Database instance ready to
193
+ * query. On first call the file is downloaded and stored in the browser's
194
+ * Cache API. Subsequent calls (including from other pages on the same origin)
195
+ * are served instantly from cache.
196
+ *
197
+ * @param {string} url - URL of the .sqlite file (absolute or relative)
198
+ * @param {OpenOptions} options
199
+ * @returns {Promise<Database>}
200
+ *
201
+ * @example <caption>Plain HTML</caption>
202
+ * const db = await StratumSQLite.open("data/mydb.sqlite", {
203
+ * sqlJsPath: "libs/sqljs/",
204
+ * cacheKey: "mydb@v1.2",
205
+ * });
206
+ * const rows = db.query("SELECT * FROM countries");
207
+ *
208
+ * @example <caption>Quarto / ObservableJS cell</caption>
209
+ * db = StratumSQLite.open(window._dbPath, { sqlJsPath: window._sqljsBase })
210
+ * rows = (await db).query("SELECT * FROM countries")
211
+ */
212
+ async function open(url, options = {}) {
213
+ const sqlJsBase = options.sqlJsPath || DEFAULT_SQLJS_CDN;
214
+ const cacheKey = options.cacheKey || `stratum-sqlite:${url}@1`;
215
+ const onProgress = options.onProgress || null;
216
+
217
+ // Ensure trailing slash on sqlJsBase
218
+ const base = sqlJsBase.endsWith('/') ? sqlJsBase : sqlJsBase + '/';
219
+
220
+ // Load the sql.js bootstrap script (sets global `initSqlJs`)
221
+ await loadScript(base + 'sql-wasm.js');
222
+
223
+ // Initialise the WASM module
224
+ const SQL = await initSqlJs({ // eslint-disable-line no-undef
225
+ locateFile: () => base + 'sql-wasm.wasm',
226
+ });
227
+
228
+ // Fetch the database (cache hit after first load)
229
+ const bytes = await fetchWithCache(url, cacheKey, onProgress);
230
+
231
+ return new Database(new SQL.Database(bytes));
232
+ }
233
+
234
+ const StratumSQLite = { open, Database };
235
+
236
+ export { open, Database };
237
+ export default StratumSQLite;