21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { test } from "./test.js";
2 |
3 | /**
4 | Set up our output channel differently depending
5 | on whether we are running in a worker thread or
6 | the main (UI) thread.
7 | */
8 | let logHtml;
9 | if (self.window === self /* UI thread */) {
10 | console.log("Running demo from main UI thread.");
11 | logHtml = function (cssClass, ...args) {
12 | const ln = document.createElement("div");
13 | if (cssClass) ln.classList.add(cssClass);
14 | ln.append(document.createTextNode(args.join(" ")));
15 | document.body.append(ln);
16 | };
17 | } else {
18 | /* Worker thread */
19 | console.log("Running demo from Worker thread.");
20 | logHtml = function (cssClass, ...args) {
21 | postMessage({
22 | type: "log",
23 | payload: { cssClass, args },
24 | });
25 | };
26 | }
27 | const log = (...args) => logHtml("", ...args);
28 | const warn = (...args) => logHtml("warning", ...args);
29 | const error = (...args) => logHtml("error", ...args);
30 |
31 | log.warn = warn;
32 | log.error = error;
33 |
34 | log("Loading and initializing sqlite3 module...");
35 | if (self.window !== self) {
36 | let sqlite3Js = "sqlite3.js";
37 | const urlParams = new URL(self.location.href).searchParams;
38 | if (urlParams.has("sqlite3.dir")) {
39 | sqlite3Js = urlParams.get("sqlite3.dir") + "/" + sqlite3Js;
40 | }
41 | importScripts(sqlite3Js);
42 | }
43 | const sqlite3 = await self.sqlite3InitModule({
44 | // We can redirect any stdout/stderr from the module
45 | // like so...
46 | print: log,
47 | printErr: error,
48 | });
49 |
50 | //console.log('sqlite3 =',sqlite3);
51 | log("Done initializing. Running demo...");
52 | try {
53 | test(sqlite3, log);
54 | } catch (e) {
55 | error("Exception:", e.message);
56 | }
57 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Test combining [yjs](https://github.com/yjs/yjs) and [sqlite wasm](https://sqlite.org/wasm/doc/trunk/index.md)
2 |
3 | This is a test project to combine yjs and sqlite wasm, it lets you store yjs documents
4 | in a sqlite database, update them in place and query the content. Perfect for building
5 | a local first web app.
6 |
7 | Demo: http://samwillis.co.uk/yjs-sqlite-test/
8 |
9 | The current version uses javascript version of yjs, but it could be built with
10 | [y-crdt](https://github.com/y-crdt/y-crdt) the rust port and compiled into the sqlLite
11 | wasm module. This would be more efficient.
12 |
13 | ## A few things you can do
14 |
15 | - Create a new document via sql:
16 |
17 | ```sql
18 | INSERT INTO docs (id, doc) VALUES ('doc1', y_new_doc());
19 | ```
20 |
21 | - Update a document via sql, passing the update as a parameter:
22 |
23 | ```sql
24 | UPDATE docs SET doc = y_apply_update(doc, ?) WHERE id = 'doc1';
25 | ```
26 |
27 | - Get the state vector of a document via sql:
28 |
29 | ```sql
30 | SELECT y_encode_state_vector(doc) FROM docs WHERE id = 'doc1';
31 | ```
32 |
33 | - Query the content of a document via sql:
34 |
35 | ```sql
36 | SELECT doc FROM docs WHERE y_get_map_json(doc, 'myMap') ->> '$.aMapKey' = 'a value';
37 | ```
38 |
39 | - Index the content of a document by creating a virtual column:
40 |
41 | ```sql
42 | ALTER TABLE docs ADD COLUMN aMapKey INTEGER GENERATED ALWAYS AS (y_get_map_json(doc, 'myMap') ->> '$.aMapKey') VIRTUAL;
43 | CREATE INDEX docs_aMapKey ON docs (aMapKey);
44 | SELECT doc FROM docs WHERE aMapKey = 'a value';
45 | ```
46 |
47 | ## How to use
48 |
49 | Somthing a little like this:
50 |
51 | ```js
52 | import * as yjsSQLite from "./yjsSQLite.js";
53 |
54 | const db = new sqlite3.oo1.DB("/mydb.sqlite3", "ct");
55 | yjsSQLite.install(db);
56 | ```
57 |
58 | Now you have a bunch of `y_...` functions available in your database:
59 |
60 | - `y_new_doc()`
61 | Create a new Y.Doc and return its initial state as an update
62 |
63 | - `y_apply_update(savedDoc, update)`
64 | Apply a document update to the document
65 |
66 | - `y_merge_updates(updates)`
67 | Merge several document updates into a single document
68 |
69 | - `y_diff_update(savedDoc, stateVector)`
70 | Encode the missing differences to another document as a single update message
71 | that can be applied on the remote document. Specify a target state vector.
72 |
73 | - `y_encode_state_vector(savedDoc)`
74 | Computes the state vector and encodes it into an Uint8Array
75 |
76 | - `y_get_map_json(savedDoc, key)`
77 | Get the map at the given key from the given savedDoc, and return it as JSON.
78 | JSON is then queryable via the SQLite JSON operators.
79 |
80 | - `y_get_array_json(savedDoc, key)`
81 | As above but for a top level array.
82 |
83 | - `y_get_xml_fragment_json(savedDoc, key)`
84 | As above but for a top level xml fragment.
85 |
--------------------------------------------------------------------------------
/sqlite-wasm/README.txt:
--------------------------------------------------------------------------------
1 | This is the README for the sqlite3 WASM/JS distribution.
2 |
3 | Main project page: https://sqlite.org
4 |
5 | Documentation: https://sqlite.org/wasm
6 |
7 | This archive contains the following deliverables for the WASM/JS
8 | build:
9 |
10 | - jswasm/sqlite3.js is the canonical "vanilla JS" version.
11 |
12 | - jswasm/sqlite3.mjs is the same but in ES6 module form
13 |
14 | - jswasm/*-bundler-friendly.js and .mjs are variants which are
15 | intended to be compatible with "bundler" tools commonly seen in
16 | node.js-based projects. Projects using such tools should use those
17 | variants, where available, instead of files without the
18 | "-bundler-friendly" suffix. Some files do not have separate
19 | variants.
20 |
21 | - jswasm/sqlite3.wasm is the binary WASM file imported by all of the
22 | above-listed JS files.
23 |
24 | - The jswasm directory additionally contains a number of supplemental
25 | JS files which cannot be bundled directly with the main JS files
26 | but are necessary for certain usages.
27 |
28 | - The top-level directory contains various demonstration and test
29 | applications for sqlite3.js and sqlite3.mjs.
30 | sqlite3-bundler-friendly.mjs requires client-side build tools to make
31 | use of and is not demonstrated here.
32 |
33 | Browsers will not serve WASM files from file:// URLs, so the test and
34 | demonstration apps require a web server and that server must include
35 | the following headers in its response when serving the files:
36 |
37 | Cross-Origin-Opener-Policy: same-origin
38 | Cross-Origin-Embedder-Policy: require-corp
39 |
40 | The core library will function without those headers but certain
41 | features, most notably OPFS storage, will not be available.
42 |
43 | One simple way to get the demo apps up and running on Unix-style
44 | systems is to install althttpd (https://sqlite.org/althttpd) and run:
45 |
46 | althttpd --enable-sab --page index.html
47 |
--------------------------------------------------------------------------------
/sqlite-wasm/common/SqliteTestUtil.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-05-22
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | This file contains bootstrapping code used by various test scripts
14 | which live in this file's directory.
15 | */
16 | 'use strict';
17 | (function(self){
18 | /* querySelectorAll() proxy */
19 | const EAll = function(/*[element=document,] cssSelector*/){
20 | return (arguments.length>1 ? arguments[0] : document)
21 | .querySelectorAll(arguments[arguments.length-1]);
22 | };
23 | /* querySelector() proxy */
24 | const E = function(/*[element=document,] cssSelector*/){
25 | return (arguments.length>1 ? arguments[0] : document)
26 | .querySelector(arguments[arguments.length-1]);
27 | };
28 |
29 | /**
30 | Helpers for writing sqlite3-specific tests.
31 | */
32 | self.SqliteTestUtil = {
33 | /** Running total of the number of tests run via
34 | this API. */
35 | counter: 0,
36 | /**
37 | If expr is a function, it is called and its result
38 | is returned, coerced to a bool, else expr, coerced to
39 | a bool, is returned.
40 | */
41 | toBool: function(expr){
42 | return (expr instanceof Function) ? !!expr() : !!expr;
43 | },
44 | /** abort() if expr is false. If expr is a function, it
45 | is called and its result is evaluated.
46 | */
47 | assert: function f(expr, msg){
48 | if(!f._){
49 | f._ = ('undefined'===typeof abort
50 | ? (msg)=>{throw new Error(msg)}
51 | : abort);
52 | }
53 | ++this.counter;
54 | if(!this.toBool(expr)){
55 | f._(msg || "Assertion failed.");
56 | }
57 | return this;
58 | },
59 | /** Identical to assert() but throws instead of calling
60 | abort(). */
61 | affirm: function(expr, msg){
62 | ++this.counter;
63 | if(!this.toBool(expr)) throw new Error(msg || "Affirmation failed.");
64 | return this;
65 | },
66 | /** Calls f() and squelches any exception it throws. If it
67 | does not throw, this function throws. */
68 | mustThrow: function(f, msg){
69 | ++this.counter;
70 | let err;
71 | try{ f(); } catch(e){err=e;}
72 | if(!err) throw new Error(msg || "Expected exception.");
73 | return this;
74 | },
75 | /**
76 | Works like mustThrow() but expects filter to be a regex,
77 | function, or string to match/filter the resulting exception
78 | against. If f() does not throw, this test fails and an Error is
79 | thrown. If filter is a regex, the test passes if
80 | filter.test(error.message) passes. If it's a function, the test
81 | passes if filter(error) returns truthy. If it's a string, the
82 | test passes if the filter matches the exception message
83 | precisely. In all other cases the test fails, throwing an
84 | Error.
85 |
86 | If it throws, msg is used as the error report unless it's falsy,
87 | in which case a default is used.
88 | */
89 | mustThrowMatching: function(f, filter, msg){
90 | ++this.counter;
91 | let err;
92 | try{ f(); } catch(e){err=e;}
93 | if(!err) throw new Error(msg || "Expected exception.");
94 | let pass = false;
95 | if(filter instanceof RegExp) pass = filter.test(err.message);
96 | else if(filter instanceof Function) pass = filter(err);
97 | else if('string' === typeof filter) pass = (err.message === filter);
98 | if(!pass){
99 | throw new Error(msg || ("Filter rejected this exception: "+err.message));
100 | }
101 | return this;
102 | },
103 | /** Throws if expr is truthy or expr is a function and expr()
104 | returns truthy. */
105 | throwIf: function(expr, msg){
106 | ++this.counter;
107 | if(this.toBool(expr)) throw new Error(msg || "throwIf() failed");
108 | return this;
109 | },
110 | /** Throws if expr is falsy or expr is a function and expr()
111 | returns falsy. */
112 | throwUnless: function(expr, msg){
113 | ++this.counter;
114 | if(!this.toBool(expr)) throw new Error(msg || "throwUnless() failed");
115 | return this;
116 | },
117 |
118 | /**
119 | Parses window.location.search-style string into an object
120 | containing key/value pairs of URL arguments (already
121 | urldecoded). The object is created using Object.create(null),
122 | so contains only parsed-out properties and has no prototype
123 | (and thus no inherited properties).
124 |
125 | If the str argument is not passed (arguments.length==0) then
126 | window.location.search.substring(1) is used by default. If
127 | neither str is passed in nor window exists then false is returned.
128 |
129 | On success it returns an Object containing the key/value pairs
130 | parsed from the string. Keys which have no value are treated
131 | has having the boolean true value.
132 |
133 | Pedantic licensing note: this code has appeared in other source
134 | trees, but was originally written by the same person who pasted
135 | it into those trees.
136 | */
137 | processUrlArgs: function(str) {
138 | if( 0 === arguments.length ) {
139 | if( ('undefined' === typeof window) ||
140 | !window.location ||
141 | !window.location.search ) return false;
142 | else str = (''+window.location.search).substring(1);
143 | }
144 | if( ! str ) return false;
145 | str = (''+str).split(/#/,2)[0]; // remove #... to avoid it being added as part of the last value.
146 | const args = Object.create(null);
147 | const sp = str.split(/&+/);
148 | const rx = /^([^=]+)(=(.+))?/;
149 | var i, m;
150 | for( i in sp ) {
151 | m = rx.exec( sp[i] );
152 | if( ! m ) continue;
153 | args[decodeURIComponent(m[1])] = (m[3] ? decodeURIComponent(m[3]) : true);
154 | }
155 | return args;
156 | }
157 | };
158 |
159 |
160 | /**
161 | This is a module object for use with the emscripten-installed
162 | sqlite3InitModule() factory function.
163 | */
164 | self.sqlite3TestModule = {
165 | /**
166 | Array of functions to call after Emscripten has initialized the
167 | wasm module. Each gets passed the Emscripten module object
168 | (which is _this_ object).
169 | */
170 | postRun: [
171 | /* function(theModule){...} */
172 | ],
173 | //onRuntimeInitialized: function(){},
174 | /* Proxy for C-side stdout output. */
175 | print: (...args)=>{console.log(...args)},
176 | /* Proxy for C-side stderr output. */
177 | printErr: (...args)=>{console.error(...args)},
178 | /**
179 | Called by the Emscripten module init bits to report loading
180 | progress. It gets passed an empty argument when loading is done
181 | (after onRuntimeInitialized() and any this.postRun callbacks
182 | have been run).
183 | */
184 | setStatus: function f(text){
185 | if(!f.last){
186 | f.last = { text: '', step: 0 };
187 | f.ui = {
188 | status: E('#module-status'),
189 | progress: E('#module-progress'),
190 | spinner: E('#module-spinner')
191 | };
192 | }
193 | if(text === f.last.text) return;
194 | f.last.text = text;
195 | if(f.ui.progress){
196 | f.ui.progress.value = f.last.step;
197 | f.ui.progress.max = f.last.step + 1;
198 | }
199 | ++f.last.step;
200 | if(text) {
201 | f.ui.status.classList.remove('hidden');
202 | f.ui.status.innerText = text;
203 | }else{
204 | if(f.ui.progress){
205 | f.ui.progress.remove();
206 | f.ui.spinner.remove();
207 | delete f.ui.progress;
208 | delete f.ui.spinner;
209 | }
210 | f.ui.status.classList.add('hidden');
211 | }
212 | },
213 | /**
214 | Config options used by the Emscripten-dependent initialization
215 | which happens via this.initSqlite3(). This object gets
216 | (indirectly) passed to sqlite3ApiBootstrap() to configure the
217 | sqlite3 API.
218 | */
219 | sqlite3ApiConfig: {
220 | wasmfsOpfsDir: "/opfs"
221 | },
222 | /**
223 | Intended to be called by apps which need to call the
224 | Emscripten-installed sqlite3InitModule() routine. This function
225 | temporarily installs this.sqlite3ApiConfig into the self
226 | object, calls it sqlite3InitModule(), and removes
227 | self.sqlite3ApiConfig after initialization is done. Returns the
228 | promise from sqlite3InitModule(), and the next then() handler
229 | will get the sqlite3 API object as its argument.
230 | */
231 | initSqlite3: function(){
232 | self.sqlite3ApiConfig = this.sqlite3ApiConfig;
233 | return self.sqlite3InitModule(this).finally(()=>delete self.sqlite3ApiConfig);
234 | }
235 | };
236 | })(self/*window or worker*/);
237 |
--------------------------------------------------------------------------------
/sqlite-wasm/common/emscripten.css:
--------------------------------------------------------------------------------
1 | /* emscripten-related styling, used during the module load/intialization processes... */
2 | .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
3 | div.emscripten { text-align: center; }
4 | div.emscripten_border { border: 1px solid black; }
5 | #module-spinner { overflow: visible; }
6 | #module-spinner > * {
7 | margin-top: 1em;
8 | }
9 | .spinner {
10 | height: 50px;
11 | width: 50px;
12 | margin: 0px auto;
13 | animation: rotation 0.8s linear infinite;
14 | border-left: 10px solid rgb(0,150,240);
15 | border-right: 10px solid rgb(0,150,240);
16 | border-bottom: 10px solid rgb(0,150,240);
17 | border-top: 10px solid rgb(100,0,200);
18 | border-radius: 100%;
19 | background-color: rgb(200,100,250);
20 | }
21 | @keyframes rotation {
22 | from {transform: rotate(0deg);}
23 | to {transform: rotate(360deg);}
24 | }
25 |
--------------------------------------------------------------------------------
/sqlite-wasm/common/testing.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | flex-direction: column;
4 | flex-wrap: wrap;
5 | }
6 | textarea {
7 | font-family: monospace;
8 | }
9 | header {
10 | font-size: 130%;
11 | font-weight: bold;
12 | }
13 | .hidden, .initially-hidden {
14 | position: absolute !important;
15 | opacity: 0 !important;
16 | pointer-events: none !important;
17 | display: none !important;
18 | }
19 | fieldset.options {
20 | font-size: 75%;
21 | }
22 | fieldset > legend {
23 | padding: 0 0.5em;
24 | }
25 | span.labeled-input {
26 | padding: 0.25em;
27 | margin: 0.25em 0.5em;
28 | border-radius: 0.25em;
29 | white-space: nowrap;
30 | background: #0002;
31 | }
32 | .center { text-align: center; }
33 | .error {
34 | color: red;
35 | background-color: yellow;
36 | }
37 | .strong { font-weight: 700 }
38 | .warning { color: firebrick; }
39 | .green { color: darkgreen; }
40 | .tests-pass { background-color: green; color: white }
41 | .tests-fail { background-color: red; color: yellow }
42 | .faded { opacity: 0.5; }
43 | .group-start { color: blue; }
44 | .group-end { color: blue; }
45 | .input-wrapper {
46 | white-space: nowrap;
47 | display: flex;
48 | align-items: center;
49 | }
50 | #test-output {
51 | border: 1px inset;
52 | border-radius: 0.25em;
53 | padding: 0.25em;
54 | /*max-height: 30em;*/
55 | overflow: auto;
56 | white-space: break-spaces;
57 | display: flex; flex-direction: column;
58 | font-family: monospace;
59 | }
60 | #test-output.reverse {
61 | flex-direction: column-reverse;
62 | }
63 | label[for] { cursor: pointer }
64 |
65 | h1 {
66 | border-radius: 0.25em;
67 | padding: 0.15em 0.25em;
68 | }
69 | h1:first-of-type {margin: 0 0 0.5em 0;}
70 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-123-worker.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Hello, sqlite3
8 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-123.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-09-19
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | A basic demonstration of the SQLite3 "OO#1" API.
14 | */
15 | 'use strict';
16 | (function(){
17 | /**
18 | Set up our output channel differently depending
19 | on whether we are running in a worker thread or
20 | the main (UI) thread.
21 | */
22 | let logHtml;
23 | if(self.window === self /* UI thread */){
24 | console.log("Running demo from main UI thread.");
25 | logHtml = function(cssClass,...args){
26 | const ln = document.createElement('div');
27 | if(cssClass) ln.classList.add(cssClass);
28 | ln.append(document.createTextNode(args.join(' ')));
29 | document.body.append(ln);
30 | };
31 | }else{ /* Worker thread */
32 | console.log("Running demo from Worker thread.");
33 | logHtml = function(cssClass,...args){
34 | postMessage({
35 | type:'log',
36 | payload:{cssClass, args}
37 | });
38 | };
39 | }
40 | const log = (...args)=>logHtml('',...args);
41 | const warn = (...args)=>logHtml('warning',...args);
42 | const error = (...args)=>logHtml('error',...args);
43 |
44 | const demo1 = function(sqlite3){
45 | const capi = sqlite3.capi/*C-style API*/,
46 | oo = sqlite3.oo1/*high-level OO API*/;
47 | log("sqlite3 version",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
48 | const db = new oo.DB("/mydb.sqlite3",'ct');
49 | log("transient db =",db.filename);
50 | /**
51 | Never(!) rely on garbage collection to clean up DBs and
52 | (especially) prepared statements. Always wrap their lifetimes
53 | in a try/finally construct, as demonstrated below. By and
54 | large, client code can entirely avoid lifetime-related
55 | complications of prepared statement objects by using the
56 | DB.exec() method for SQL execution.
57 | */
58 | try {
59 | log("Create a table...");
60 | db.exec("CREATE TABLE IF NOT EXISTS t(a,b)");
61 | //Equivalent:
62 | db.exec({
63 | sql:"CREATE TABLE IF NOT EXISTS t(a,b)"
64 | // ... numerous other options ...
65 | });
66 | // SQL can be either a string or a byte array
67 | // or an array of strings which get concatenated
68 | // together as-is (so be sure to end each statement
69 | // with a semicolon).
70 |
71 | log("Insert some data using exec()...");
72 | let i;
73 | for( i = 20; i <= 25; ++i ){
74 | db.exec({
75 | sql: "insert into t(a,b) values (?,?)",
76 | // bind by parameter index...
77 | bind: [i, i*2]
78 | });
79 | db.exec({
80 | sql: "insert into t(a,b) values ($a,$b)",
81 | // bind by parameter name...
82 | bind: {$a: i * 10, $b: i * 20}
83 | });
84 | }
85 |
86 | log("Insert using a prepared statement...");
87 | let q = db.prepare([
88 | // SQL may be a string or array of strings
89 | // (concatenated w/o separators).
90 | "insert into t(a,b) ",
91 | "values(?,?)"
92 | ]);
93 | try {
94 | for( i = 100; i < 103; ++i ){
95 | q.bind( [i, i*2] ).step();
96 | q.reset();
97 | }
98 | // Equivalent...
99 | for( i = 103; i <= 105; ++i ){
100 | q.bind(1, i).bind(2, i*2).stepReset();
101 | }
102 | }finally{
103 | q.finalize();
104 | }
105 |
106 | log("Query data with exec() using rowMode 'array'...");
107 | db.exec({
108 | sql: "select a from t order by a limit 3",
109 | rowMode: 'array', // 'array' (default), 'object', or 'stmt'
110 | callback: function(row){
111 | log("row ",++this.counter,"=",row);
112 | }.bind({counter: 0})
113 | });
114 |
115 | log("Query data with exec() using rowMode 'object'...");
116 | db.exec({
117 | sql: "select a as aa, b as bb from t order by aa limit 3",
118 | rowMode: 'object',
119 | callback: function(row){
120 | log("row ",++this.counter,"=",JSON.stringify(row));
121 | }.bind({counter: 0})
122 | });
123 |
124 | log("Query data with exec() using rowMode 'stmt'...");
125 | db.exec({
126 | sql: "select a from t order by a limit 3",
127 | rowMode: 'stmt',
128 | callback: function(row){
129 | log("row ",++this.counter,"get(0) =",row.get(0));
130 | }.bind({counter: 0})
131 | });
132 |
133 | log("Query data with exec() using rowMode INTEGER (result column index)...");
134 | db.exec({
135 | sql: "select a, b from t order by a limit 3",
136 | rowMode: 1, // === result column 1
137 | callback: function(row){
138 | log("row ",++this.counter,"b =",row);
139 | }.bind({counter: 0})
140 | });
141 |
142 | log("Query data with exec() using rowMode $COLNAME (result column name)...");
143 | db.exec({
144 | sql: "select a a, b from t order by a limit 3",
145 | rowMode: '$a',
146 | callback: function(value){
147 | log("row ",++this.counter,"a =",value);
148 | }.bind({counter: 0})
149 | });
150 |
151 | log("Query data with exec() without a callback...");
152 | let resultRows = [];
153 | db.exec({
154 | sql: "select a, b from t order by a limit 3",
155 | rowMode: 'object',
156 | resultRows: resultRows
157 | });
158 | log("Result rows:",JSON.stringify(resultRows,undefined,2));
159 |
160 | log("Create a scalar UDF...");
161 | db.createFunction({
162 | name: 'twice',
163 | xFunc: function(pCx, arg){ // note the call arg count
164 | return arg + arg;
165 | }
166 | });
167 | log("Run scalar UDF and collect result column names...");
168 | let columnNames = [];
169 | db.exec({
170 | sql: "select a, twice(a), twice(''||a) from t order by a desc limit 3",
171 | columnNames: columnNames,
172 | rowMode: 'stmt',
173 | callback: function(row){
174 | log("a =",row.get(0), "twice(a) =", row.get(1),
175 | "twice(''||a) =",row.get(2));
176 | }
177 | });
178 | log("Result column names:",columnNames);
179 |
180 | try{
181 | log("The following use of the twice() UDF will",
182 | "fail because of incorrect arg count...");
183 | db.exec("select twice(1,2,3)");
184 | }catch(e){
185 | warn("Got expected exception:",e.message);
186 | }
187 |
188 | try {
189 | db.transaction( function(D) {
190 | D.exec("delete from t");
191 | log("In transaction: count(*) from t =",db.selectValue("select count(*) from t"));
192 | throw new sqlite3.SQLite3Error("Demonstrating transaction() rollback");
193 | });
194 | }catch(e){
195 | if(e instanceof sqlite3.SQLite3Error){
196 | log("Got expected exception from db.transaction():",e.message);
197 | log("count(*) from t =",db.selectValue("select count(*) from t"));
198 | }else{
199 | throw e;
200 | }
201 | }
202 |
203 | try {
204 | db.savepoint( function(D) {
205 | D.exec("delete from t");
206 | log("In savepoint: count(*) from t =",db.selectValue("select count(*) from t"));
207 | D.savepoint(function(DD){
208 | const rows = [];
209 | DD.exec({
210 | sql: ["insert into t(a,b) values(99,100);",
211 | "select count(*) from t"],
212 | rowMode: 0,
213 | resultRows: rows
214 | });
215 | log("In nested savepoint. Row count =",rows[0]);
216 | throw new sqlite3.SQLite3Error("Demonstrating nested savepoint() rollback");
217 | })
218 | });
219 | }catch(e){
220 | if(e instanceof sqlite3.SQLite3Error){
221 | log("Got expected exception from nested db.savepoint():",e.message);
222 | log("count(*) from t =",db.selectValue("select count(*) from t"));
223 | }else{
224 | throw e;
225 | }
226 | }
227 | }finally{
228 | db.close();
229 | }
230 |
231 | log("That's all, folks!");
232 |
233 | /**
234 | Some of the features of the OO API not demonstrated above...
235 |
236 | - get change count (total or statement-local, 32- or 64-bit)
237 | - get a DB's file name
238 |
239 | Misc. Stmt features:
240 |
241 | - Various forms of bind()
242 | - clearBindings()
243 | - reset()
244 | - Various forms of step()
245 | - Variants of get() for explicit type treatment/conversion,
246 | e.g. getInt(), getFloat(), getBlob(), getJSON()
247 | - getColumnName(ndx), getColumnNames()
248 | - getParamIndex(name)
249 | */
250 | }/*demo1()*/;
251 |
252 | log("Loading and initializing sqlite3 module...");
253 | if(self.window!==self) /*worker thread*/{
254 | /*
255 | If sqlite3.js is in a directory other than this script, in order
256 | to get sqlite3.js to resolve sqlite3.wasm properly, we have to
257 | explicitly tell it where sqlite3.js is being loaded from. We do
258 | that by passing the `sqlite3.dir=theDirName` URL argument to
259 | _this_ script. That URL argument will be seen by the JS/WASM
260 | loader and it will adjust the sqlite3.wasm path accordingly. If
261 | sqlite3.js/.wasm are in the same directory as this script then
262 | that's not needed.
263 |
264 | URL arguments passed as part of the filename via importScripts()
265 | are simply lost, and such scripts see the self.location of
266 | _this_ script.
267 | */
268 | let sqlite3Js = 'sqlite3.js';
269 | const urlParams = new URL(self.location.href).searchParams;
270 | if(urlParams.has('sqlite3.dir')){
271 | sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
272 | }
273 | importScripts(sqlite3Js);
274 | }
275 | self.sqlite3InitModule({
276 | // We can redirect any stdout/stderr from the module
277 | // like so...
278 | print: log,
279 | printErr: error
280 | }).then(function(sqlite3){
281 | //console.log('sqlite3 =',sqlite3);
282 | log("Done initializing. Running demo...");
283 | try {
284 | demo1(sqlite3);
285 | }catch(e){
286 | error("Exception:",e.message);
287 | }
288 | });
289 | })();
290 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-jsstorage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | sqlite3-kvvfs.js tests
10 |
11 |
12 | sqlite3-kvvfs.js tests
13 |
14 |
15 |
16 |
Initializing app...
17 |
18 | On a slow internet connection this may take a moment. If this
19 | message displays for "a long time", intialization may have
20 | failed and the JavaScript console may contain clues as to why.
21 |
22 |
23 |
Downloading...
24 |
25 |
26 |
27 |
37 |
38 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-jsstorage.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-09-12
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | A basic test script for sqlite3.wasm with kvvfs support. This file
14 | must be run in main JS thread and sqlite3.js must have been loaded
15 | before it.
16 | */
17 | 'use strict';
18 | (function(){
19 | const T = self.SqliteTestUtil;
20 | const toss = function(...args){throw new Error(args.join(' '))};
21 | const debug = console.debug.bind(console);
22 | const eOutput = document.querySelector('#test-output');
23 | const logC = console.log.bind(console)
24 | const logE = function(domElement){
25 | eOutput.append(domElement);
26 | };
27 | const logHtml = function(cssClass,...args){
28 | const ln = document.createElement('div');
29 | if(cssClass) ln.classList.add(cssClass);
30 | ln.append(document.createTextNode(args.join(' ')));
31 | logE(ln);
32 | }
33 | const log = function(...args){
34 | logC(...args);
35 | logHtml('',...args);
36 | };
37 | const warn = function(...args){
38 | logHtml('warning',...args);
39 | };
40 | const error = function(...args){
41 | logHtml('error',...args);
42 | };
43 |
44 | const runTests = function(sqlite3){
45 | const capi = sqlite3.capi,
46 | oo = sqlite3.oo1,
47 | wasm = sqlite3.wasm;
48 | log("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
49 | T.assert( 0 !== capi.sqlite3_vfs_find(null) );
50 | if(!capi.sqlite3_vfs_find('kvvfs')){
51 | error("This build is not kvvfs-capable.");
52 | return;
53 | }
54 |
55 | const dbStorage = 0 ? 'session' : 'local';
56 | const theStore = 's'===dbStorage[0] ? sessionStorage : localStorage;
57 | const db = new oo.JsStorageDb( dbStorage );
58 | // Or: oo.DB(dbStorage, 'c', 'kvvfs')
59 | log("db.storageSize():",db.storageSize());
60 | document.querySelector('#btn-clear-storage').addEventListener('click',function(){
61 | const sz = db.clearStorage();
62 | log("kvvfs",db.filename+"Storage cleared:",sz,"entries.");
63 | });
64 | document.querySelector('#btn-clear-log').addEventListener('click',function(){
65 | eOutput.innerText = '';
66 | });
67 | document.querySelector('#btn-init-db').addEventListener('click',function(){
68 | try{
69 | const saveSql = [];
70 | db.exec({
71 | sql: ["drop table if exists t;",
72 | "create table if not exists t(a);",
73 | "insert into t(a) values(?),(?),(?)"],
74 | bind: [performance.now() >> 0,
75 | (performance.now() * 2) >> 0,
76 | (performance.now() / 2) >> 0],
77 | saveSql
78 | });
79 | console.log("saveSql =",saveSql,theStore);
80 | log("DB (re)initialized.");
81 | }catch(e){
82 | error(e.message);
83 | }
84 | });
85 | const btnSelect = document.querySelector('#btn-select1');
86 | btnSelect.addEventListener('click',function(){
87 | log("DB rows:");
88 | try{
89 | db.exec({
90 | sql: "select * from t order by a",
91 | rowMode: 0,
92 | callback: (v)=>log(v)
93 | });
94 | }catch(e){
95 | error(e.message);
96 | }
97 | });
98 | document.querySelector('#btn-storage-size').addEventListener('click',function(){
99 | log("size.storageSize(",dbStorage,") says", db.storageSize(),
100 | "bytes");
101 | });
102 | log("Storage backend:",db.filename);
103 | if(0===db.selectValue('select count(*) from sqlite_master')){
104 | log("DB is empty. Use the init button to populate it.");
105 | }else{
106 | log("DB contains data from a previous session. Use the Clear Ctorage button to delete it.");
107 | btnSelect.click();
108 | }
109 | };
110 |
111 | sqlite3InitModule(self.sqlite3TestModule).then((sqlite3)=>{
112 | runTests(sqlite3);
113 | });
114 | })();
115 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-worker1-promiser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | worker-promise tests
10 |
11 |
12 | worker-promise tests
13 |
14 |
15 |
16 |
Initializing app...
17 |
18 | On a slow internet connection this may take a moment. If this
19 | message displays for "a long time", intialization may have
20 | failed and the JavaScript console may contain clues as to why.
21 |
22 |
23 |
Downloading...
24 |
25 |
26 |
27 |
Most stuff on this page happens in the dev console.
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-worker1-promiser.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-08-23
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | Demonstration of the sqlite3 Worker API #1 Promiser: a Promise-based
14 | proxy for for the sqlite3 Worker #1 API.
15 | */
16 | 'use strict';
17 | (function(){
18 | const T = self.SqliteTestUtil;
19 | const eOutput = document.querySelector('#test-output');
20 | const warn = console.warn.bind(console);
21 | const error = console.error.bind(console);
22 | const log = console.log.bind(console);
23 | const logHtml = async function(cssClass,...args){
24 | log.apply(this, args);
25 | const ln = document.createElement('div');
26 | if(cssClass) ln.classList.add(cssClass);
27 | ln.append(document.createTextNode(args.join(' ')));
28 | eOutput.append(ln);
29 | };
30 |
31 | let startTime;
32 | const testCount = async ()=>{
33 | logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms");
34 | };
35 |
36 | //why is this triggered even when we catch() a Promise?
37 | //window.addEventListener('unhandledrejection', function(event) {
38 | // warn('unhandledrejection',event);
39 | //});
40 |
41 | const promiserConfig = {
42 | worker: ()=>{
43 | const w = new Worker("jswasm/sqlite3-worker1.js");
44 | w.onerror = (event)=>error("worker.onerror",event);
45 | return w;
46 | },
47 | debug: 1 ? undefined : (...args)=>console.debug('worker debug',...args),
48 | onunhandled: function(ev){
49 | error("Unhandled worker message:",ev.data);
50 | },
51 | onready: function(){
52 | self.sqlite3TestModule.setStatus(null)/*hide the HTML-side is-loading spinner*/;
53 | runTests();
54 | },
55 | onerror: function(ev){
56 | error("worker1 error:",ev);
57 | }
58 | };
59 | const workerPromise = self.sqlite3Worker1Promiser(promiserConfig);
60 | delete self.sqlite3Worker1Promiser;
61 |
62 | const wtest = async function(msgType, msgArgs, callback){
63 | if(2===arguments.length && 'function'===typeof msgArgs){
64 | callback = msgArgs;
65 | msgArgs = undefined;
66 | }
67 | const p = workerPromise({type: msgType, args:msgArgs});
68 | return callback ? p.then(callback).finally(testCount) : p;
69 | };
70 |
71 | const runTests = async function(){
72 | const dbFilename = '/testing2.sqlite3';
73 | startTime = performance.now();
74 |
75 | let sqConfig;
76 | await wtest('config-get', (ev)=>{
77 | const r = ev.result;
78 | log('sqlite3.config subset:', r);
79 | T.assert('boolean' === typeof r.bigIntEnabled);
80 | sqConfig = r;
81 | });
82 | logHtml('',
83 | "Sending 'open' message and waiting for its response before continuing...");
84 |
85 | await wtest('open', {
86 | filename: dbFilename,
87 | simulateError: 0 /* if true, fail the 'open' */,
88 | }, function(ev){
89 | const r = ev.result;
90 | log("then open result",r);
91 | T.assert(ev.dbId === r.dbId)
92 | .assert(ev.messageId)
93 | .assert('string' === typeof r.vfs);
94 | promiserConfig.dbId = ev.dbId;
95 | }).then(runTests2);
96 | };
97 |
98 | const runTests2 = async function(){
99 | const mustNotReach = ()=>toss("This is not supposed to be reached.");
100 |
101 | await wtest('exec',{
102 | sql: ["create table t(a,b)",
103 | "insert into t(a,b) values(1,2),(3,4),(5,6)"
104 | ].join(';'),
105 | multi: true,
106 | resultRows: [], columnNames: []
107 | }, function(ev){
108 | ev = ev.result;
109 | T.assert(0===ev.resultRows.length)
110 | .assert(0===ev.columnNames.length);
111 | });
112 |
113 | await wtest('exec',{
114 | sql: 'select a a, b b from t order by a',
115 | resultRows: [], columnNames: [],
116 | }, function(ev){
117 | ev = ev.result;
118 | T.assert(3===ev.resultRows.length)
119 | .assert(1===ev.resultRows[0][0])
120 | .assert(6===ev.resultRows[2][1])
121 | .assert(2===ev.columnNames.length)
122 | .assert('b'===ev.columnNames[1]);
123 | });
124 |
125 | await wtest('exec',{
126 | sql: 'select a a, b b from t order by a',
127 | resultRows: [], columnNames: [],
128 | rowMode: 'object'
129 | }, function(ev){
130 | ev = ev.result;
131 | T.assert(3===ev.resultRows.length)
132 | .assert(1===ev.resultRows[0].a)
133 | .assert(6===ev.resultRows[2].b)
134 | });
135 |
136 | await wtest(
137 | 'exec',
138 | {sql:'intentional_error'},
139 | mustNotReach
140 | ).catch((e)=>{
141 | warn("Intentional error:",e);
142 | });
143 |
144 | await wtest('exec',{
145 | sql:'select 1 union all select 3',
146 | resultRows: [],
147 | }, function(ev){
148 | ev = ev.result;
149 | T.assert(2 === ev.resultRows.length)
150 | .assert(1 === ev.resultRows[0][0])
151 | .assert(3 === ev.resultRows[1][0]);
152 | });
153 |
154 | const resultRowTest1 = function f(ev){
155 | if(undefined === f.counter) f.counter = 0;
156 | if(null === ev.rowNumber){
157 | /* End of result set. */
158 | T.assert(undefined === ev.row)
159 | .assert(2===ev.columnNames.length)
160 | .assert('a'===ev.columnNames[0])
161 | .assert('B'===ev.columnNames[1]);
162 | }else{
163 | T.assert(ev.rowNumber > 0);
164 | ++f.counter;
165 | }
166 | log("exec() result row:",ev);
167 | T.assert(null === ev.rowNumber || 'number' === typeof ev.row.B);
168 | };
169 | await wtest('exec',{
170 | sql: 'select a a, b B from t order by a limit 3',
171 | callback: resultRowTest1,
172 | rowMode: 'object'
173 | }, function(ev){
174 | T.assert(3===resultRowTest1.counter);
175 | resultRowTest1.counter = 0;
176 | });
177 |
178 | const resultRowTest2 = function f(ev){
179 | if(null === ev.rowNumber){
180 | /* End of result set. */
181 | T.assert(undefined === ev.row)
182 | .assert(1===ev.columnNames.length)
183 | .assert('a'===ev.columnNames[0])
184 | }else{
185 | T.assert(ev.rowNumber > 0);
186 | f.counter = ev.rowNumber;
187 | }
188 | log("exec() result row:",ev);
189 | T.assert(null === ev.rowNumber || 'number' === typeof ev.row);
190 | };
191 | await wtest('exec',{
192 | sql: 'select a a from t limit 3',
193 | callback: resultRowTest2,
194 | rowMode: 0
195 | }, function(ev){
196 | T.assert(3===resultRowTest2.counter);
197 | });
198 |
199 | const resultRowTest3 = function f(ev){
200 | if(null === ev.rowNumber){
201 | T.assert(3===ev.columnNames.length)
202 | .assert('foo'===ev.columnNames[0])
203 | .assert('bar'===ev.columnNames[1])
204 | .assert('baz'===ev.columnNames[2]);
205 | }else{
206 | f.counter = ev.rowNumber;
207 | T.assert('number' === typeof ev.row);
208 | }
209 | };
210 | await wtest('exec',{
211 | sql: "select 'foo' foo, a bar, 'baz' baz from t limit 2",
212 | callback: resultRowTest3,
213 | columnNames: [],
214 | rowMode: '$bar'
215 | }, function(ev){
216 | log("exec() result row:",ev);
217 | T.assert(2===resultRowTest3.counter);
218 | });
219 |
220 | await wtest('exec',{
221 | multi: true,
222 | sql:[
223 | 'pragma foreign_keys=0;',
224 | // ^^^ arbitrary query with no result columns
225 | 'select a, b from t order by a desc; select a from t;'
226 | // multi-exec only honors results from the first
227 | // statement with result columns (regardless of whether)
228 | // it has any rows).
229 | ],
230 | rowMode: 1,
231 | resultRows: []
232 | },function(ev){
233 | const rows = ev.result.resultRows;
234 | T.assert(3===rows.length).
235 | assert(6===rows[0]);
236 | });
237 |
238 | await wtest('exec',{sql: 'delete from t where a>3'});
239 |
240 | await wtest('exec',{
241 | sql: 'select count(a) from t',
242 | resultRows: []
243 | },function(ev){
244 | ev = ev.result;
245 | T.assert(1===ev.resultRows.length)
246 | .assert(2===ev.resultRows[0][0]);
247 | });
248 |
249 | await wtest('export', function(ev){
250 | ev = ev.result;
251 | T.assert('string' === typeof ev.filename)
252 | .assert(ev.byteArray instanceof Uint8Array)
253 | .assert(ev.byteArray.length > 1024)
254 | .assert('application/x-sqlite3' === ev.mimetype);
255 | });
256 |
257 | /***** close() tests must come last. *****/
258 | await wtest('close',{},function(ev){
259 | T.assert('string' === typeof ev.result.filename);
260 | });
261 |
262 | await wtest('close', (ev)=>{
263 | T.assert(undefined === ev.result.filename);
264 | }).finally(()=>logHtml('',"That's all, folks!"));
265 | }/*runTests2()*/;
266 |
267 | log("Init complete, but async init bits may still be running.");
268 | })();
269 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-worker1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | sqlite3-worker1.js tests
11 |
12 |
13 | sqlite3-worker1.js tests
14 |
15 |
16 |
17 |
Initializing app...
18 |
19 | On a slow internet connection this may take a moment. If this
20 | message displays for "a long time", intialization may have
21 | failed and the JavaScript console may contain clues as to why.
22 |
23 |
24 |
Downloading...
25 |
26 |
27 |
28 |
Most stuff on this page happens in the dev console.
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/sqlite-wasm/demo-worker1.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-05-22
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | A basic test script for sqlite3-worker1.js.
14 |
15 | Note that the wrapper interface demonstrated in
16 | demo-worker1-promiser.js is much easier to use from client code, as it
17 | lacks the message-passing acrobatics demonstrated in this file.
18 | */
19 | 'use strict';
20 | (function(){
21 | const T = self.SqliteTestUtil;
22 | const SW = new Worker("jswasm/sqlite3-worker1.js");
23 | const DbState = {
24 | id: undefined
25 | };
26 | const eOutput = document.querySelector('#test-output');
27 | const log = console.log.bind(console);
28 | const logHtml = function(cssClass,...args){
29 | log.apply(this, args);
30 | const ln = document.createElement('div');
31 | if(cssClass) ln.classList.add(cssClass);
32 | ln.append(document.createTextNode(args.join(' ')));
33 | eOutput.append(ln);
34 | };
35 | const warn = console.warn.bind(console);
36 | const error = console.error.bind(console);
37 | const toss = (...args)=>{throw new Error(args.join(' '))};
38 |
39 | SW.onerror = function(event){
40 | error("onerror",event);
41 | };
42 |
43 | let startTime;
44 |
45 | /**
46 | A queue for callbacks which are to be run in response to async
47 | DB commands. See the notes in runTests() for why we need
48 | this. The event-handling plumbing of this file requires that
49 | any DB command which includes a `messageId` property also have
50 | a queued callback entry, as the existence of that property in
51 | response payloads is how it knows whether or not to shift an
52 | entry off of the queue.
53 | */
54 | const MsgHandlerQueue = {
55 | queue: [],
56 | id: 0,
57 | push: function(type,callback){
58 | this.queue.push(callback);
59 | return type + '-' + (++this.id);
60 | },
61 | shift: function(){
62 | return this.queue.shift();
63 | }
64 | };
65 |
66 | const testCount = ()=>{
67 | logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms");
68 | };
69 |
70 | const logEventResult = function(ev){
71 | const evd = ev.result;
72 | logHtml(evd.errorClass ? 'error' : '',
73 | "runOneTest",ev.messageId,"Worker time =",
74 | (ev.workerRespondTime - ev.workerReceivedTime),"ms.",
75 | "Round-trip event time =",
76 | (performance.now() - ev.departureTime),"ms.",
77 | (evd.errorClass ? evd.message : "")//, JSON.stringify(evd)
78 | );
79 | };
80 |
81 | const runOneTest = function(eventType, eventArgs, callback){
82 | T.assert(eventArgs && 'object'===typeof eventArgs);
83 | /* ^^^ that is for the testing and messageId-related code, not
84 | a hard requirement of all of the Worker-exposed APIs. */
85 | const messageId = MsgHandlerQueue.push(eventType,function(ev){
86 | logEventResult(ev);
87 | if(callback instanceof Function){
88 | callback(ev);
89 | testCount();
90 | }
91 | });
92 | const msg = {
93 | type: eventType,
94 | args: eventArgs,
95 | dbId: DbState.id,
96 | messageId: messageId,
97 | departureTime: performance.now()
98 | };
99 | log("Posting",eventType,"message to worker dbId="+(DbState.id||'default')+':',msg);
100 | SW.postMessage(msg);
101 | };
102 |
103 | /** Methods which map directly to onmessage() event.type keys.
104 | They get passed the inbound event.data. */
105 | const dbMsgHandler = {
106 | open: function(ev){
107 | DbState.id = ev.dbId;
108 | log("open result",ev);
109 | },
110 | exec: function(ev){
111 | log("exec result",ev);
112 | },
113 | export: function(ev){
114 | log("export result",ev);
115 | },
116 | error: function(ev){
117 | error("ERROR from the worker:",ev);
118 | logEventResult(ev);
119 | },
120 | resultRowTest1: function f(ev){
121 | if(undefined === f.counter) f.counter = 0;
122 | if(null === ev.rowNumber){
123 | /* End of result set. */
124 | T.assert(undefined === ev.row)
125 | .assert(Array.isArray(ev.columnNames))
126 | .assert(ev.columnNames.length);
127 | }else{
128 | T.assert(ev.rowNumber > 0);
129 | ++f.counter;
130 | }
131 | //log("exec() result row:",ev);
132 | T.assert(null === ev.rowNumber || 'number' === typeof ev.row.b);
133 | }
134 | };
135 |
136 | /**
137 | "The problem" now is that the test results are async. We
138 | know, however, that the messages posted to the worker will
139 | be processed in the order they are passed to it, so we can
140 | create a queue of callbacks to handle them. The problem
141 | with that approach is that it's not error-handling
142 | friendly, in that an error can cause us to bypass a result
143 | handler queue entry. We have to perform some extra
144 | acrobatics to account for that.
145 |
146 | Problem #2 is that we cannot simply start posting events: we
147 | first have to post an 'open' event, wait for it to respond, and
148 | collect its db ID before continuing. If we don't wait, we may
149 | well fire off 10+ messages before the open actually responds.
150 | */
151 | const runTests2 = function(){
152 | const mustNotReach = ()=>{
153 | throw new Error("This is not supposed to be reached.");
154 | };
155 | runOneTest('exec',{
156 | sql: ["create table t(a,b);",
157 | "insert into t(a,b) values(1,2),(3,4),(5,6)"
158 | ],
159 | resultRows: [], columnNames: []
160 | }, function(ev){
161 | ev = ev.result;
162 | T.assert(0===ev.resultRows.length)
163 | .assert(0===ev.columnNames.length);
164 | });
165 | runOneTest('exec',{
166 | sql: 'select a a, b b from t order by a',
167 | resultRows: [], columnNames: [], saveSql:[]
168 | }, function(ev){
169 | ev = ev.result;
170 | T.assert(3===ev.resultRows.length)
171 | .assert(1===ev.resultRows[0][0])
172 | .assert(6===ev.resultRows[2][1])
173 | .assert(2===ev.columnNames.length)
174 | .assert('b'===ev.columnNames[1]);
175 | });
176 | //if(1){ error("Returning prematurely for testing."); return; }
177 | runOneTest('exec',{
178 | sql: 'select a a, b b from t order by a',
179 | resultRows: [], columnNames: [],
180 | rowMode: 'object'
181 | }, function(ev){
182 | ev = ev.result;
183 | T.assert(3===ev.resultRows.length)
184 | .assert(1===ev.resultRows[0].a)
185 | .assert(6===ev.resultRows[2].b)
186 | });
187 | runOneTest('exec',{sql:'intentional_error'}, mustNotReach);
188 | // Ensure that the message-handler queue survives ^^^ that error...
189 | runOneTest('exec',{
190 | sql:'select 1',
191 | resultRows: [],
192 | //rowMode: 'array', // array is the default in the Worker interface
193 | }, function(ev){
194 | ev = ev.result;
195 | T.assert(1 === ev.resultRows.length)
196 | .assert(1 === ev.resultRows[0][0]);
197 | });
198 | runOneTest('exec',{
199 | sql: 'select a a, b b from t order by a',
200 | callback: 'resultRowTest1',
201 | rowMode: 'object'
202 | }, function(ev){
203 | T.assert(3===dbMsgHandler.resultRowTest1.counter);
204 | dbMsgHandler.resultRowTest1.counter = 0;
205 | });
206 | runOneTest('exec',{
207 | sql:[
208 | "pragma foreign_keys=0;",
209 | // ^^^ arbitrary query with no result columns
210 | "select a, b from t order by a desc;",
211 | "select a from t;"
212 | // multi-statement exec only honors results from the first
213 | // statement with result columns (regardless of whether)
214 | // it has any rows).
215 | ],
216 | rowMode: 1,
217 | resultRows: []
218 | },function(ev){
219 | const rows = ev.result.resultRows;
220 | T.assert(3===rows.length).
221 | assert(6===rows[0]);
222 | });
223 | runOneTest('exec',{sql: 'delete from t where a>3'});
224 | runOneTest('exec',{
225 | sql: 'select count(a) from t',
226 | resultRows: []
227 | },function(ev){
228 | ev = ev.result;
229 | T.assert(1===ev.resultRows.length)
230 | .assert(2===ev.resultRows[0][0]);
231 | });
232 | runOneTest('export',{}, function(ev){
233 | ev = ev.result;
234 | log("export result:",ev);
235 | T.assert('string' === typeof ev.filename)
236 | .assert(ev.byteArray instanceof Uint8Array)
237 | .assert(ev.byteArray.length > 1024)
238 | .assert('application/x-sqlite3' === ev.mimetype);
239 | });
240 | /***** close() tests must come last. *****/
241 | runOneTest('close',{unlink:true},function(ev){
242 | ev = ev.result;
243 | T.assert('string' === typeof ev.filename);
244 | });
245 | runOneTest('close',{unlink:true},function(ev){
246 | ev = ev.result;
247 | T.assert(undefined === ev.filename);
248 | logHtml('warning',"This is the final test.");
249 | });
250 | logHtml('warning',"Finished posting tests. Waiting on async results.");
251 | };
252 |
253 | const runTests = function(){
254 | /**
255 | Design decision time: all remaining tests depend on the 'open'
256 | command having succeeded. In order to support multiple DBs, the
257 | upcoming commands ostensibly have to know the ID of the DB they
258 | want to talk to. We have two choices:
259 |
260 | 1) We run 'open' and wait for its response, which contains the
261 | db id.
262 |
263 | 2) We have the Worker automatically use the current "default
264 | db" (the one which was most recently opened) if no db id is
265 | provided in the message. When we do this, the main thread may
266 | well fire off _all_ of the test messages before the 'open'
267 | actually responds, but because the messages are handled on a
268 | FIFO basis, those after the initial 'open' will pick up the
269 | "default" db. However, if the open fails, then all pending
270 | messages (until next next 'open', at least) except for 'close'
271 | will fail and we have no way of cancelling them once they've
272 | been posted to the worker.
273 |
274 | Which approach we use below depends on the boolean value of
275 | waitForOpen.
276 | */
277 | const waitForOpen = 1,
278 | simulateOpenError = 0 /* if true, the remaining tests will
279 | all barf if waitForOpen is
280 | false. */;
281 | logHtml('',
282 | "Sending 'open' message and",(waitForOpen ? "" : "NOT ")+
283 | "waiting for its response before continuing.");
284 | startTime = performance.now();
285 | runOneTest('open', {
286 | filename:'testing2.sqlite3',
287 | simulateError: simulateOpenError
288 | }, function(ev){
289 | log("open result",ev);
290 | T.assert('testing2.sqlite3'===ev.result.filename)
291 | .assert(ev.dbId)
292 | .assert(ev.messageId)
293 | .assert('string' === typeof ev.result.vfs);
294 | DbState.id = ev.dbId;
295 | if(waitForOpen) setTimeout(runTests2, 0);
296 | });
297 | if(!waitForOpen) runTests2();
298 | };
299 |
300 | SW.onmessage = function(ev){
301 | if(!ev.data || 'object'!==typeof ev.data){
302 | warn("Unknown sqlite3-worker message type:",ev);
303 | return;
304 | }
305 | ev = ev.data/*expecting a nested object*/;
306 | //log("main window onmessage:",ev);
307 | if(ev.result && ev.messageId){
308 | /* We're expecting a queued-up callback handler. */
309 | const f = MsgHandlerQueue.shift();
310 | if('error'===ev.type){
311 | dbMsgHandler.error(ev);
312 | return;
313 | }
314 | T.assert(f instanceof Function);
315 | f(ev);
316 | return;
317 | }
318 | switch(ev.type){
319 | case 'sqlite3-api':
320 | switch(ev.result){
321 | case 'worker1-ready':
322 | log("Message:",ev);
323 | self.sqlite3TestModule.setStatus(null);
324 | runTests();
325 | return;
326 | default:
327 | warn("Unknown sqlite3-api message type:",ev);
328 | return;
329 | }
330 | default:
331 | if(dbMsgHandler.hasOwnProperty(ev.type)){
332 | try{dbMsgHandler[ev.type](ev);}
333 | catch(err){
334 | error("Exception while handling db result message",
335 | ev,":",err);
336 | }
337 | return;
338 | }
339 | warn("Unknown sqlite3-api message type:",ev);
340 | }
341 | };
342 | log("Init complete, but async init bits may still be running.");
343 | log("Installing Worker into global scope SW for dev purposes.");
344 | self.SW = SW;
345 | })();
346 |
--------------------------------------------------------------------------------
/sqlite-wasm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | sqlite3 WASM Demo Page Index
8 |
9 |
10 |
35 | sqlite3 WASM demo pages
36 |
37 |
Below is the list of demo pages for the sqlite3 WASM
38 | builds. The intent is that this page be run
39 | using the functional equivalent of:
and the individual pages be started in their own tab.
42 | Warnings and Caveats:
43 |
44 |
All of these pages must be served via an HTTP
45 | server. Browsers do not support loading WASM files via
46 | file:// URLs.
47 |
Any OPFS-related pages or tests require:
48 |
49 |
That the web server emit the so-called
50 | COOP
51 | and
52 | COEP
53 | headers. althttpd requires the
54 | -enable-sab flag for that.
55 |
56 |
A very recent version of a Chromium-based browser
57 | (v102 at least, possibly newer). OPFS support in the
58 | other major browsers is pending. Development and testing
59 | is currently done against a dev-channel release of
60 | Chrome (v111 as of 2023-02-10).
61 |
62 |
63 |
64 |
65 |
66 |
The tests and demos...
67 |
68 |
Core-most tests
69 |
70 |
tester1: Core unit and
71 | regression tests for the various APIs and surrounding
72 | utility code.
73 |
tester1-worker: same thing
74 | but running in a Worker.
75 |
tester1-esm: same as
76 | tester1 but loads sqlite3 in the main thread via
77 | an ES6 module.
78 |
79 |
tester1-worker?esm:
80 | same as tester1-esm but loads a Worker Module which
81 | then loads the sqlite3 API via an ES6 module. Note that
82 | not all browsers permit loading modules in Worker
83 | threads.
84 |
85 |
86 |
87 |
Higher-level apps and demos...
88 |
89 |
demo-123 provides a
90 | no-nonsense example of adding sqlite3 support to a web
91 | page in the UI thread.
92 |
demo-123-worker is
93 | the same as demo-123 but loads and runs
94 | sqlite3 from a Worker thread.
95 |
demo-jsstorage: very basic
96 | demo of using the key-value VFS for storing a persistent db
97 | in JS localStorage or sessionStorage.
98 |
demo-worker1:
99 | Worker-based wrapper of the OO API #1. Its Promise-based
100 | wrapper is significantly easier to use, however.
101 |
demo-worker1-promiser:
102 | a demo of the Promise-based wrapper of the Worker1 API.
103 |
104 |
105 |
106 |
107 |
110 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-09-16
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | A Worker which manages asynchronous OPFS handles on behalf of a
14 | synchronous API which controls it via a combination of Worker
15 | messages, SharedArrayBuffer, and Atomics. It is the asynchronous
16 | counterpart of the API defined in sqlite3-vfs-opfs.js.
17 |
18 | Highly indebted to:
19 |
20 | https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js
21 |
22 | for demonstrating how to use the OPFS APIs.
23 |
24 | This file is to be loaded as a Worker. It does not have any direct
25 | access to the sqlite3 JS/WASM bits, so any bits which it needs (most
26 | notably SQLITE_xxx integer codes) have to be imported into it via an
27 | initialization process.
28 |
29 | This file represents an implementation detail of a larger piece of
30 | code, and not a public interface. Its details may change at any time
31 | and are not intended to be used by any client-level code.
32 |
33 | 2022-11-27: Chrome v108 changes some async methods to synchronous, as
34 | documented at:
35 |
36 | https://developer.chrome.com/blog/sync-methods-for-accesshandles/
37 |
38 | We cannot change to the sync forms at this point without breaking
39 | clients who use Chrome v104-ish or higher. truncate(), getSize(),
40 | flush(), and close() are now (as of v108) synchronous. Calling them
41 | with an "await", as we have to for the async forms, is still legal
42 | with the sync forms but is superfluous. Calling the async forms with
43 | theFunc().then(...) is not compatible with the change to
44 | synchronous, but we do do not use those APIs that way. i.e. we don't
45 | _need_ to change anything for this, but at some point (after Chrome
46 | versions (approximately) 104-107 are extinct) should change our
47 | usage of those methods to remove the "await".
48 | */
49 | "use strict";
50 | const wPost = (type,...args)=>postMessage({type, payload:args});
51 | const installAsyncProxy = function(self){
52 | const toss = function(...args){throw new Error(args.join(' '))};
53 | if(globalThis.window === globalThis){
54 | toss("This code cannot run from the main thread.",
55 | "Load it as a Worker from a separate Worker.");
56 | }else if(!navigator?.storage?.getDirectory){
57 | toss("This API requires navigator.storage.getDirectory.");
58 | }
59 |
60 | /**
61 | Will hold state copied to this object from the syncronous side of
62 | this API.
63 | */
64 | const state = Object.create(null);
65 |
66 | /**
67 | verbose:
68 |
69 | 0 = no logging output
70 | 1 = only errors
71 | 2 = warnings and errors
72 | 3 = debug, warnings, and errors
73 | */
74 | state.verbose = 1;
75 |
76 | const loggers = {
77 | 0:console.error.bind(console),
78 | 1:console.warn.bind(console),
79 | 2:console.log.bind(console)
80 | };
81 | const logImpl = (level,...args)=>{
82 | if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
83 | };
84 | const log = (...args)=>logImpl(2, ...args);
85 | const warn = (...args)=>logImpl(1, ...args);
86 | const error = (...args)=>logImpl(0, ...args);
87 | const metrics = Object.create(null);
88 | metrics.reset = ()=>{
89 | let k;
90 | const r = (m)=>(m.count = m.time = m.wait = 0);
91 | for(k in state.opIds){
92 | r(metrics[k] = Object.create(null));
93 | }
94 | let s = metrics.s11n = Object.create(null);
95 | s = s.serialize = Object.create(null);
96 | s.count = s.time = 0;
97 | s = metrics.s11n.deserialize = Object.create(null);
98 | s.count = s.time = 0;
99 | };
100 | metrics.dump = ()=>{
101 | let k, n = 0, t = 0, w = 0;
102 | for(k in state.opIds){
103 | const m = metrics[k];
104 | n += m.count;
105 | t += m.time;
106 | w += m.wait;
107 | m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
108 | }
109 | console.log(globalThis?.location?.href,
110 | "metrics for",globalThis?.location?.href,":\n",
111 | metrics,
112 | "\nTotal of",n,"op(s) for",t,"ms",
113 | "approx",w,"ms spent waiting on OPFS APIs.");
114 | console.log("Serialization metrics:",metrics.s11n);
115 | };
116 |
117 | /**
118 | __openFiles is a map of sqlite3_file pointers (integers) to
119 | metadata related to a given OPFS file handles. The pointers are, in
120 | this side of the interface, opaque file handle IDs provided by the
121 | synchronous part of this constellation. Each value is an object
122 | with a structure demonstrated in the xOpen() impl.
123 | */
124 | const __openFiles = Object.create(null);
125 | /**
126 | __implicitLocks is a Set of sqlite3_file pointers (integers) which were
127 | "auto-locked". i.e. those for which we obtained a sync access
128 | handle without an explicit xLock() call. Such locks will be
129 | released during db connection idle time, whereas a sync access
130 | handle obtained via xLock(), or subsequently xLock()'d after
131 | auto-acquisition, will not be released until xUnlock() is called.
132 |
133 | Maintenance reminder: if we relinquish auto-locks at the end of the
134 | operation which acquires them, we pay a massive performance
135 | penalty: speedtest1 benchmarks take up to 4x as long. By delaying
136 | the lock release until idle time, the hit is negligible.
137 | */
138 | const __implicitLocks = new Set();
139 |
140 | /**
141 | Expects an OPFS file path. It gets resolved, such that ".."
142 | components are properly expanded, and returned. If the 2nd arg is
143 | true, the result is returned as an array of path elements, else an
144 | absolute path string is returned.
145 | */
146 | const getResolvedPath = function(filename,splitIt){
147 | const p = new URL(
148 | filename, 'file://irrelevant'
149 | ).pathname;
150 | return splitIt ? p.split('/').filter((v)=>!!v) : p;
151 | };
152 |
153 | /**
154 | Takes the absolute path to a filesystem element. Returns an array
155 | of [handleOfContainingDir, filename]. If the 2nd argument is truthy
156 | then each directory element leading to the file is created along
157 | the way. Throws if any creation or resolution fails.
158 | */
159 | const getDirForFilename = async function f(absFilename, createDirs = false){
160 | const path = getResolvedPath(absFilename, true);
161 | const filename = path.pop();
162 | let dh = state.rootDir;
163 | for(const dirName of path){
164 | if(dirName){
165 | dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
166 | }
167 | }
168 | return [dh, filename];
169 | };
170 |
171 | /**
172 | If the given file-holding object has a sync handle attached to it,
173 | that handle is remove and asynchronously closed. Though it may
174 | sound sensible to continue work as soon as the close() returns
175 | (noting that it's asynchronous), doing so can cause operations
176 | performed soon afterwards, e.g. a call to getSyncHandle() to fail
177 | because they may happen out of order from the close(). OPFS does
178 | not guaranty that the actual order of operations is retained in
179 | such cases. i.e. always "await" on the result of this function.
180 | */
181 | const closeSyncHandle = async (fh)=>{
182 | if(fh.syncHandle){
183 | log("Closing sync handle for",fh.filenameAbs);
184 | const h = fh.syncHandle;
185 | delete fh.syncHandle;
186 | delete fh.xLock;
187 | __implicitLocks.delete(fh.fid);
188 | return h.close();
189 | }
190 | };
191 |
192 | /**
193 | A proxy for closeSyncHandle() which is guaranteed to not throw.
194 |
195 | This function is part of a lock/unlock step in functions which
196 | require a sync access handle but may be called without xLock()
197 | having been called first. Such calls need to release that
198 | handle to avoid locking the file for all of time. This is an
199 | _attempt_ at reducing cross-tab contention but it may prove
200 | to be more of a problem than a solution and may need to be
201 | removed.
202 | */
203 | const closeSyncHandleNoThrow = async (fh)=>{
204 | try{await closeSyncHandle(fh)}
205 | catch(e){
206 | warn("closeSyncHandleNoThrow() ignoring:",e,fh);
207 | }
208 | };
209 |
210 | /* Release all auto-locks. */
211 | const releaseImplicitLocks = async ()=>{
212 | if(__implicitLocks.size){
213 | /* Release all auto-locks. */
214 | for(const fid of __implicitLocks){
215 | const fh = __openFiles[fid];
216 | await closeSyncHandleNoThrow(fh);
217 | log("Auto-unlocked",fid,fh.filenameAbs);
218 | }
219 | }
220 | };
221 |
222 | /**
223 | An experiment in improving concurrency by freeing up implicit locks
224 | sooner. This is known to impact performance dramatically but it has
225 | also shown to improve concurrency considerably.
226 |
227 | If fh.releaseImplicitLocks is truthy and fh is in __implicitLocks,
228 | this routine returns closeSyncHandleNoThrow(), else it is a no-op.
229 | */
230 | const releaseImplicitLock = async (fh)=>{
231 | if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){
232 | return closeSyncHandleNoThrow(fh);
233 | }
234 | };
235 |
236 | /**
237 | An error class specifically for use with getSyncHandle(), the goal
238 | of which is to eventually be able to distinguish unambiguously
239 | between locking-related failures and other types, noting that we
240 | cannot currently do so because createSyncAccessHandle() does not
241 | define its exceptions in the required level of detail.
242 |
243 | 2022-11-29: according to:
244 |
245 | https://github.com/whatwg/fs/pull/21
246 |
247 | NoModificationAllowedError will be the standard exception thrown
248 | when acquisition of a sync access handle fails due to a locking
249 | error. As of this writing, that error type is not visible in the
250 | dev console in Chrome v109, nor is it documented in MDN, but an
251 | error with that "name" property is being thrown from the OPFS
252 | layer.
253 | */
254 | class GetSyncHandleError extends Error {
255 | constructor(errorObject, ...msg){
256 | super([
257 | ...msg, ': '+errorObject.name+':',
258 | errorObject.message
259 | ].join(' '), {
260 | cause: errorObject
261 | });
262 | this.name = 'GetSyncHandleError';
263 | }
264 | };
265 | GetSyncHandleError.convertRc = (e,rc)=>{
266 | if(1){
267 | return (
268 | e instanceof GetSyncHandleError
269 | && ((e.cause.name==='NoModificationAllowedError')
270 | /* Inconsistent exception.name from Chrome/ium with the
271 | same exception.message text: */
272 | || (e.cause.name==='DOMException'
273 | && 0===e.cause.message.indexOf('Access Handles cannot')))
274 | ) ? (
275 | /*console.warn("SQLITE_BUSY",e),*/
276 | state.sq3Codes.SQLITE_BUSY
277 | ) : rc;
278 | }else{
279 | return rc;
280 | }
281 | }
282 | /**
283 | Returns the sync access handle associated with the given file
284 | handle object (which must be a valid handle object, as created by
285 | xOpen()), lazily opening it if needed.
286 |
287 | In order to help alleviate cross-tab contention for a dabase, if
288 | an exception is thrown while acquiring the handle, this routine
289 | will wait briefly and try again, up to some fixed number of
290 | times. If acquisition still fails at that point it will give up
291 | and propagate the exception. Client-level code will see that as
292 | an I/O error.
293 | */
294 | const getSyncHandle = async (fh,opName)=>{
295 | if(!fh.syncHandle){
296 | const t = performance.now();
297 | log("Acquiring sync handle for",fh.filenameAbs);
298 | const maxTries = 6,
299 | msBase = state.asyncIdleWaitTime * 2;
300 | let i = 1, ms = msBase;
301 | for(; true; ms = msBase * ++i){
302 | try {
303 | //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
304 | //TODO? A config option which tells it to throw here
305 | //randomly every now and then, for testing purposes.
306 | fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
307 | break;
308 | }catch(e){
309 | if(i === maxTries){
310 | throw new GetSyncHandleError(
311 | e, "Error getting sync handle for",opName+"().",maxTries,
312 | "attempts failed.",fh.filenameAbs
313 | );
314 | }
315 | warn("Error getting sync handle for",opName+"(). Waiting",ms,
316 | "ms and trying again.",fh.filenameAbs,e);
317 | Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
318 | }
319 | }
320 | log("Got",opName+"() sync handle for",fh.filenameAbs,
321 | 'in',performance.now() - t,'ms');
322 | if(!fh.xLock){
323 | __implicitLocks.add(fh.fid);
324 | log("Acquired implicit lock for",opName+"()",fh.fid,fh.filenameAbs);
325 | }
326 | }
327 | return fh.syncHandle;
328 | };
329 |
330 | /**
331 | Stores the given value at state.sabOPView[state.opIds.rc] and then
332 | Atomics.notify()'s it.
333 | */
334 | const storeAndNotify = (opName, value)=>{
335 | log(opName+"() => notify(",value,")");
336 | Atomics.store(state.sabOPView, state.opIds.rc, value);
337 | Atomics.notify(state.sabOPView, state.opIds.rc);
338 | };
339 |
340 | /**
341 | Throws if fh is a file-holding object which is flagged as read-only.
342 | */
343 | const affirmNotRO = function(opName,fh){
344 | if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
345 | };
346 |
347 | /**
348 | We track 2 different timers: the "metrics" timer records how much
349 | time we spend performing work. The "wait" timer records how much
350 | time we spend waiting on the underlying OPFS timer. See the calls
351 | to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
352 | throughout this file to see how they're used.
353 | */
354 | const __mTimer = Object.create(null);
355 | __mTimer.op = undefined;
356 | __mTimer.start = undefined;
357 | const mTimeStart = (op)=>{
358 | __mTimer.start = performance.now();
359 | __mTimer.op = op;
360 | //metrics[op] || toss("Maintenance required: missing metrics for",op);
361 | ++metrics[op].count;
362 | };
363 | const mTimeEnd = ()=>(
364 | metrics[__mTimer.op].time += performance.now() - __mTimer.start
365 | );
366 | const __wTimer = Object.create(null);
367 | __wTimer.op = undefined;
368 | __wTimer.start = undefined;
369 | const wTimeStart = (op)=>{
370 | __wTimer.start = performance.now();
371 | __wTimer.op = op;
372 | //metrics[op] || toss("Maintenance required: missing metrics for",op);
373 | };
374 | const wTimeEnd = ()=>(
375 | metrics[__wTimer.op].wait += performance.now() - __wTimer.start
376 | );
377 |
378 | /**
379 | Gets set to true by the 'opfs-async-shutdown' command to quit the
380 | wait loop. This is only intended for debugging purposes: we cannot
381 | inspect this file's state while the tight waitLoop() is running and
382 | need a way to stop that loop for introspection purposes.
383 | */
384 | let flagAsyncShutdown = false;
385 |
386 | /**
387 | Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
388 | methods, as well as helpers like mkdir(). Maintenance reminder:
389 | members are in alphabetical order to simplify finding them.
390 | */
391 | const vfsAsyncImpls = {
392 | 'opfs-async-metrics': async ()=>{
393 | mTimeStart('opfs-async-metrics');
394 | metrics.dump();
395 | storeAndNotify('opfs-async-metrics', 0);
396 | mTimeEnd();
397 | },
398 | 'opfs-async-shutdown': async ()=>{
399 | flagAsyncShutdown = true;
400 | storeAndNotify('opfs-async-shutdown', 0);
401 | },
402 | mkdir: async (dirname)=>{
403 | mTimeStart('mkdir');
404 | let rc = 0;
405 | wTimeStart('mkdir');
406 | try {
407 | await getDirForFilename(dirname+"/filepart", true);
408 | }catch(e){
409 | state.s11n.storeException(2,e);
410 | rc = state.sq3Codes.SQLITE_IOERR;
411 | }finally{
412 | wTimeEnd();
413 | }
414 | storeAndNotify('mkdir', rc);
415 | mTimeEnd();
416 | },
417 | xAccess: async (filename)=>{
418 | mTimeStart('xAccess');
419 | /* OPFS cannot support the full range of xAccess() queries
420 | sqlite3 calls for. We can essentially just tell if the file
421 | is accessible, but if it is then it's automatically writable
422 | (unless it's locked, which we cannot(?) know without trying
423 | to open it). OPFS does not have the notion of read-only.
424 |
425 | The return semantics of this function differ from sqlite3's
426 | xAccess semantics because we are limited in what we can
427 | communicate back to our synchronous communication partner: 0 =
428 | accessible, non-0 means not accessible.
429 | */
430 | let rc = 0;
431 | wTimeStart('xAccess');
432 | try{
433 | const [dh, fn] = await getDirForFilename(filename);
434 | await dh.getFileHandle(fn);
435 | }catch(e){
436 | state.s11n.storeException(2,e);
437 | rc = state.sq3Codes.SQLITE_IOERR;
438 | }finally{
439 | wTimeEnd();
440 | }
441 | storeAndNotify('xAccess', rc);
442 | mTimeEnd();
443 | },
444 | xClose: async function(fid/*sqlite3_file pointer*/){
445 | const opName = 'xClose';
446 | mTimeStart(opName);
447 | __implicitLocks.delete(fid);
448 | const fh = __openFiles[fid];
449 | let rc = 0;
450 | wTimeStart(opName);
451 | if(fh){
452 | delete __openFiles[fid];
453 | await closeSyncHandle(fh);
454 | if(fh.deleteOnClose){
455 | try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
456 | catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
457 | }
458 | }else{
459 | state.s11n.serialize();
460 | rc = state.sq3Codes.SQLITE_NOTFOUND;
461 | }
462 | wTimeEnd();
463 | storeAndNotify(opName, rc);
464 | mTimeEnd();
465 | },
466 | xDelete: async function(...args){
467 | mTimeStart('xDelete');
468 | const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
469 | storeAndNotify('xDelete', rc);
470 | mTimeEnd();
471 | },
472 | xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
473 | /* The syncDir flag is, for purposes of the VFS API's semantics,
474 | ignored here. However, if it has the value 0x1234 then: after
475 | deleting the given file, recursively try to delete any empty
476 | directories left behind in its wake (ignoring any errors and
477 | stopping at the first failure).
478 |
479 | That said: we don't know for sure that removeEntry() fails if
480 | the dir is not empty because the API is not documented. It has,
481 | however, a "recursive" flag which defaults to false, so
482 | presumably it will fail if the dir is not empty and that flag
483 | is false.
484 | */
485 | let rc = 0;
486 | wTimeStart('xDelete');
487 | try {
488 | while(filename){
489 | const [hDir, filenamePart] = await getDirForFilename(filename, false);
490 | if(!filenamePart) break;
491 | await hDir.removeEntry(filenamePart, {recursive});
492 | if(0x1234 !== syncDir) break;
493 | recursive = false;
494 | filename = getResolvedPath(filename, true);
495 | filename.pop();
496 | filename = filename.join('/');
497 | }
498 | }catch(e){
499 | state.s11n.storeException(2,e);
500 | rc = state.sq3Codes.SQLITE_IOERR_DELETE;
501 | }
502 | wTimeEnd();
503 | return rc;
504 | },
505 | xFileSize: async function(fid/*sqlite3_file pointer*/){
506 | mTimeStart('xFileSize');
507 | const fh = __openFiles[fid];
508 | let rc = 0;
509 | wTimeStart('xFileSize');
510 | try{
511 | const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
512 | state.s11n.serialize(Number(sz));
513 | }catch(e){
514 | state.s11n.storeException(1,e);
515 | rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR);
516 | }
517 | await releaseImplicitLock(fh);
518 | wTimeEnd();
519 | storeAndNotify('xFileSize', rc);
520 | mTimeEnd();
521 | },
522 | xLock: async function(fid/*sqlite3_file pointer*/,
523 | lockType/*SQLITE_LOCK_...*/){
524 | mTimeStart('xLock');
525 | const fh = __openFiles[fid];
526 | let rc = 0;
527 | const oldLockType = fh.xLock;
528 | fh.xLock = lockType;
529 | if( !fh.syncHandle ){
530 | wTimeStart('xLock');
531 | try {
532 | await getSyncHandle(fh,'xLock');
533 | __implicitLocks.delete(fid);
534 | }catch(e){
535 | state.s11n.storeException(1,e);
536 | rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
537 | fh.xLock = oldLockType;
538 | }
539 | wTimeEnd();
540 | }
541 | storeAndNotify('xLock',rc);
542 | mTimeEnd();
543 | },
544 | xOpen: async function(fid/*sqlite3_file pointer*/, filename,
545 | flags/*SQLITE_OPEN_...*/,
546 | opfsFlags/*OPFS_...*/){
547 | const opName = 'xOpen';
548 | mTimeStart(opName);
549 | const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
550 | wTimeStart('xOpen');
551 | try{
552 | let hDir, filenamePart;
553 | try {
554 | [hDir, filenamePart] = await getDirForFilename(filename, !!create);
555 | }catch(e){
556 | state.s11n.storeException(1,e);
557 | storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
558 | mTimeEnd();
559 | wTimeEnd();
560 | return;
561 | }
562 | const hFile = await hDir.getFileHandle(filenamePart, {create});
563 | wTimeEnd();
564 | const fh = Object.assign(Object.create(null),{
565 | fid: fid,
566 | filenameAbs: filename,
567 | filenamePart: filenamePart,
568 | dirHandle: hDir,
569 | fileHandle: hFile,
570 | sabView: state.sabFileBufView,
571 | readOnly: create
572 | ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
573 | deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags)
574 | });
575 | fh.releaseImplicitLocks =
576 | (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP)
577 | || state.opfsFlags.defaultUnlockAsap;
578 | if(0 /* this block is modelled after something wa-sqlite
579 | does but it leads to immediate contention on journal files.
580 | Update: this approach reportedly only works for DELETE journal
581 | mode. */
582 | && (0===(flags & state.sq3Codes.SQLITE_OPEN_MAIN_DB))){
583 | /* sqlite does not lock these files, so go ahead and grab an OPFS
584 | lock. */
585 | fh.xLock = "xOpen"/* Truthy value to keep entry from getting
586 | flagged as auto-locked. String value so
587 | that we can easily distinguish is later
588 | if needed. */;
589 | await getSyncHandle(fh,'xOpen');
590 | }
591 | __openFiles[fid] = fh;
592 | storeAndNotify(opName, 0);
593 | }catch(e){
594 | wTimeEnd();
595 | error(opName,e);
596 | state.s11n.storeException(1,e);
597 | storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
598 | }
599 | mTimeEnd();
600 | },
601 | xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
602 | mTimeStart('xRead');
603 | let rc = 0, nRead;
604 | const fh = __openFiles[fid];
605 | try{
606 | wTimeStart('xRead');
607 | nRead = (await getSyncHandle(fh,'xRead')).read(
608 | fh.sabView.subarray(0, n),
609 | {at: Number(offset64)}
610 | );
611 | wTimeEnd();
612 | if(nRead < n){/* Zero-fill remaining bytes */
613 | fh.sabView.fill(0, nRead, n);
614 | rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
615 | }
616 | }catch(e){
617 | if(undefined===nRead) wTimeEnd();
618 | error("xRead() failed",e,fh);
619 | state.s11n.storeException(1,e);
620 | rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ);
621 | }
622 | await releaseImplicitLock(fh);
623 | storeAndNotify('xRead',rc);
624 | mTimeEnd();
625 | },
626 | xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
627 | mTimeStart('xSync');
628 | const fh = __openFiles[fid];
629 | let rc = 0;
630 | if(!fh.readOnly && fh.syncHandle){
631 | try {
632 | wTimeStart('xSync');
633 | await fh.syncHandle.flush();
634 | }catch(e){
635 | state.s11n.storeException(2,e);
636 | rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
637 | }
638 | wTimeEnd();
639 | }
640 | storeAndNotify('xSync',rc);
641 | mTimeEnd();
642 | },
643 | xTruncate: async function(fid/*sqlite3_file pointer*/,size){
644 | mTimeStart('xTruncate');
645 | let rc = 0;
646 | const fh = __openFiles[fid];
647 | wTimeStart('xTruncate');
648 | try{
649 | affirmNotRO('xTruncate', fh);
650 | await (await getSyncHandle(fh,'xTruncate')).truncate(size);
651 | }catch(e){
652 | error("xTruncate():",e,fh);
653 | state.s11n.storeException(2,e);
654 | rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE);
655 | }
656 | await releaseImplicitLock(fh);
657 | wTimeEnd();
658 | storeAndNotify('xTruncate',rc);
659 | mTimeEnd();
660 | },
661 | xUnlock: async function(fid/*sqlite3_file pointer*/,
662 | lockType/*SQLITE_LOCK_...*/){
663 | mTimeStart('xUnlock');
664 | let rc = 0;
665 | const fh = __openFiles[fid];
666 | if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
667 | && fh.syncHandle ){
668 | wTimeStart('xUnlock');
669 | try { await closeSyncHandle(fh) }
670 | catch(e){
671 | state.s11n.storeException(1,e);
672 | rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
673 | }
674 | wTimeEnd();
675 | }
676 | storeAndNotify('xUnlock',rc);
677 | mTimeEnd();
678 | },
679 | xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
680 | mTimeStart('xWrite');
681 | let rc;
682 | const fh = __openFiles[fid];
683 | wTimeStart('xWrite');
684 | try{
685 | affirmNotRO('xWrite', fh);
686 | rc = (
687 | n === (await getSyncHandle(fh,'xWrite'))
688 | .write(fh.sabView.subarray(0, n),
689 | {at: Number(offset64)})
690 | ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
691 | }catch(e){
692 | error("xWrite():",e,fh);
693 | state.s11n.storeException(1,e);
694 | rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE);
695 | }
696 | await releaseImplicitLock(fh);
697 | wTimeEnd();
698 | storeAndNotify('xWrite',rc);
699 | mTimeEnd();
700 | }
701 | }/*vfsAsyncImpls*/;
702 |
703 | const initS11n = ()=>{
704 | /**
705 | ACHTUNG: this code is 100% duplicated in the other half of this
706 | proxy! The documentation is maintained in the "synchronous half".
707 | */
708 | if(state.s11n) return state.s11n;
709 | const textDecoder = new TextDecoder(),
710 | textEncoder = new TextEncoder('utf-8'),
711 | viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
712 | viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
713 | state.s11n = Object.create(null);
714 | const TypeIds = Object.create(null);
715 | TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
716 | TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
717 | TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
718 | TypeIds.string = { id: 4 };
719 | const getTypeId = (v)=>(
720 | TypeIds[typeof v]
721 | || toss("Maintenance required: this value type cannot be serialized.",v)
722 | );
723 | const getTypeIdById = (tid)=>{
724 | switch(tid){
725 | case TypeIds.number.id: return TypeIds.number;
726 | case TypeIds.bigint.id: return TypeIds.bigint;
727 | case TypeIds.boolean.id: return TypeIds.boolean;
728 | case TypeIds.string.id: return TypeIds.string;
729 | default: toss("Invalid type ID:",tid);
730 | }
731 | };
732 | state.s11n.deserialize = function(clear=false){
733 | ++metrics.s11n.deserialize.count;
734 | const t = performance.now();
735 | const argc = viewU8[0];
736 | const rc = argc ? [] : null;
737 | if(argc){
738 | const typeIds = [];
739 | let offset = 1, i, n, v;
740 | for(i = 0; i < argc; ++i, ++offset){
741 | typeIds.push(getTypeIdById(viewU8[offset]));
742 | }
743 | for(i = 0; i < argc; ++i){
744 | const t = typeIds[i];
745 | if(t.getter){
746 | v = viewDV[t.getter](offset, state.littleEndian);
747 | offset += t.size;
748 | }else{/*String*/
749 | n = viewDV.getInt32(offset, state.littleEndian);
750 | offset += 4;
751 | v = textDecoder.decode(viewU8.slice(offset, offset+n));
752 | offset += n;
753 | }
754 | rc.push(v);
755 | }
756 | }
757 | if(clear) viewU8[0] = 0;
758 | //log("deserialize:",argc, rc);
759 | metrics.s11n.deserialize.time += performance.now() - t;
760 | return rc;
761 | };
762 | state.s11n.serialize = function(...args){
763 | const t = performance.now();
764 | ++metrics.s11n.serialize.count;
765 | if(args.length){
766 | //log("serialize():",args);
767 | const typeIds = [];
768 | let i = 0, offset = 1;
769 | viewU8[0] = args.length & 0xff /* header = # of args */;
770 | for(; i < args.length; ++i, ++offset){
771 | /* Write the TypeIds.id value into the next args.length
772 | bytes. */
773 | typeIds.push(getTypeId(args[i]));
774 | viewU8[offset] = typeIds[i].id;
775 | }
776 | for(i = 0; i < args.length; ++i) {
777 | /* Deserialize the following bytes based on their
778 | corresponding TypeIds.id from the header. */
779 | const t = typeIds[i];
780 | if(t.setter){
781 | viewDV[t.setter](offset, args[i], state.littleEndian);
782 | offset += t.size;
783 | }else{/*String*/
784 | const s = textEncoder.encode(args[i]);
785 | viewDV.setInt32(offset, s.byteLength, state.littleEndian);
786 | offset += 4;
787 | viewU8.set(s, offset);
788 | offset += s.byteLength;
789 | }
790 | }
791 | //log("serialize() result:",viewU8.slice(0,offset));
792 | }else{
793 | viewU8[0] = 0;
794 | }
795 | metrics.s11n.serialize.time += performance.now() - t;
796 | };
797 |
798 | state.s11n.storeException = state.asyncS11nExceptions
799 | ? ((priority,e)=>{
800 | if(priority<=state.asyncS11nExceptions){
801 | state.s11n.serialize([e.name,': ',e.message].join(""));
802 | }
803 | })
804 | : ()=>{};
805 |
806 | return state.s11n;
807 | }/*initS11n()*/;
808 |
809 | const waitLoop = async function f(){
810 | const opHandlers = Object.create(null);
811 | for(let k of Object.keys(state.opIds)){
812 | const vi = vfsAsyncImpls[k];
813 | if(!vi) continue;
814 | const o = Object.create(null);
815 | opHandlers[state.opIds[k]] = o;
816 | o.key = k;
817 | o.f = vi;
818 | }
819 | while(!flagAsyncShutdown){
820 | try {
821 | if('timed-out'===Atomics.wait(
822 | state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime
823 | )){
824 | await releaseImplicitLocks();
825 | continue;
826 | }
827 | const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
828 | Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
829 | const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
830 | const args = state.s11n.deserialize(
831 | true /* clear s11n to keep the caller from confusing this with
832 | an exception string written by the upcoming
833 | operation */
834 | ) || [];
835 | //warn("waitLoop() whichOp =",opId, hnd, args);
836 | if(hnd.f) await hnd.f(...args);
837 | else error("Missing callback for opId",opId);
838 | }catch(e){
839 | error('in waitLoop():',e);
840 | }
841 | }
842 | };
843 |
844 | navigator.storage.getDirectory().then(function(d){
845 | state.rootDir = d;
846 | globalThis.onmessage = function({data}){
847 | switch(data.type){
848 | case 'opfs-async-init':{
849 | /* Receive shared state from synchronous partner */
850 | const opt = data.args;
851 | for(const k in opt) state[k] = opt[k];
852 | state.verbose = opt.verbose ?? 1;
853 | state.sabOPView = new Int32Array(state.sabOP);
854 | state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
855 | state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
856 | Object.keys(vfsAsyncImpls).forEach((k)=>{
857 | if(!Number.isFinite(state.opIds[k])){
858 | toss("Maintenance required: missing state.opIds[",k,"]");
859 | }
860 | });
861 | initS11n();
862 | metrics.reset();
863 | log("init state",state);
864 | wPost('opfs-async-inited');
865 | waitLoop();
866 | break;
867 | }
868 | case 'opfs-async-restart':
869 | if(flagAsyncShutdown){
870 | warn("Restarting after opfs-async-shutdown. Might or might not work.");
871 | flagAsyncShutdown = false;
872 | waitLoop();
873 | }
874 | break;
875 | case 'opfs-async-metrics':
876 | metrics.dump();
877 | break;
878 | }
879 | };
880 | wPost('opfs-async-loaded');
881 | }).catch((e)=>error("error initializing OPFS asyncer:",e));
882 | }/*installAsyncProxy()*/;
883 | if(!globalThis.SharedArrayBuffer){
884 | wPost('opfs-unavailable', "Missing SharedArrayBuffer API.",
885 | "The server must emit the COOP/COEP response headers to enable that.");
886 | }else if(!globalThis.Atomics){
887 | wPost('opfs-unavailable', "Missing Atomics API.",
888 | "The server must emit the COOP/COEP response headers to enable that.");
889 | }else if(!globalThis.FileSystemHandle ||
890 | !globalThis.FileSystemDirectoryHandle ||
891 | !globalThis.FileSystemFileHandle ||
892 | !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle ||
893 | !navigator?.storage?.getDirectory){
894 | wPost('opfs-unavailable',"Missing required OPFS APIs.");
895 | }else{
896 | installAsyncProxy(self);
897 | }
898 |
--------------------------------------------------------------------------------
/sqlite-wasm/jswasm/sqlite3-worker1-bundler-friendly.mjs:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-05-23
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | This is a JS Worker file for the main sqlite3 api. It loads
14 | sqlite3.js, initializes the module, and postMessage()'s a message
15 | after the module is initialized:
16 |
17 | {type: 'sqlite3-api', result: 'worker1-ready'}
18 |
19 | This seemingly superfluous level of indirection is necessary when
20 | loading sqlite3.js via a Worker. Instantiating a worker with new
21 | Worker("sqlite.js") will not (cannot) call sqlite3InitModule() to
22 | initialize the module due to a timing/order-of-operations conflict
23 | (and that symbol is not exported in a way that a Worker loading it
24 | that way can see it). Thus JS code wanting to load the sqlite3
25 | Worker-specific API needs to pass _this_ file (or equivalent) to the
26 | Worker constructor and then listen for an event in the form shown
27 | above in order to know when the module has completed initialization.
28 |
29 | This file accepts a URL arguments to adjust how it loads sqlite3.js:
30 |
31 | - `sqlite3.dir`, if set, treats the given directory name as the
32 | directory from which `sqlite3.js` will be loaded.
33 | */
34 | import {default as sqlite3InitModule} from './sqlite3-bundler-friendly.mjs';
35 | sqlite3InitModule().then(sqlite3 => sqlite3.initWorker1API());
36 |
--------------------------------------------------------------------------------
/sqlite-wasm/jswasm/sqlite3-worker1-promiser-bundler-friendly.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-08-24
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | This file implements a Promise-based proxy for the sqlite3 Worker
14 | API #1. It is intended to be included either from the main thread or
15 | a Worker, but only if (A) the environment supports nested Workers
16 | and (B) it's _not_ a Worker which loads the sqlite3 WASM/JS
17 | module. This file's features will load that module and provide a
18 | slightly simpler client-side interface than the slightly-lower-level
19 | Worker API does.
20 |
21 | This script necessarily exposes one global symbol, but clients may
22 | freely `delete` that symbol after calling it.
23 | */
24 | 'use strict';
25 |
26 | globalThis.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){
27 |
28 | if(1===arguments.length && 'function'===typeof arguments[0]){
29 | const f = config;
30 | config = Object.assign(Object.create(null), callee.defaultConfig);
31 | config.onready = f;
32 | }else{
33 | config = Object.assign(Object.create(null), callee.defaultConfig, config);
34 | }
35 | const handlerMap = Object.create(null);
36 | const noop = function(){};
37 | const err = config.onerror
38 | || noop ;
39 | const debug = config.debug || noop;
40 | const idTypeMap = config.generateMessageId ? undefined : Object.create(null);
41 | const genMsgId = config.generateMessageId || function(msg){
42 | return msg.type+'#'+(idTypeMap[msg.type] = (idTypeMap[msg.type]||0) + 1);
43 | };
44 | const toss = (...args)=>{throw new Error(args.join(' '))};
45 | if(!config.worker) config.worker = callee.defaultConfig.worker;
46 | if('function'===typeof config.worker) config.worker = config.worker();
47 | let dbId;
48 | config.worker.onmessage = function(ev){
49 | ev = ev.data;
50 | debug('worker1.onmessage',ev);
51 | let msgHandler = handlerMap[ev.messageId];
52 | if(!msgHandler){
53 | if(ev && 'sqlite3-api'===ev.type && 'worker1-ready'===ev.result) {
54 |
55 | if(config.onready) config.onready();
56 | return;
57 | }
58 | msgHandler = handlerMap[ev.type] ;
59 | if(msgHandler && msgHandler.onrow){
60 | msgHandler.onrow(ev);
61 | return;
62 | }
63 | if(config.onunhandled) config.onunhandled(arguments[0]);
64 | else err("sqlite3Worker1Promiser() unhandled worker message:",ev);
65 | return;
66 | }
67 | delete handlerMap[ev.messageId];
68 | switch(ev.type){
69 | case 'error':
70 | msgHandler.reject(ev);
71 | return;
72 | case 'open':
73 | if(!dbId) dbId = ev.dbId;
74 | break;
75 | case 'close':
76 | if(ev.dbId===dbId) dbId = undefined;
77 | break;
78 | default:
79 | break;
80 | }
81 | try {msgHandler.resolve(ev)}
82 | catch(e){msgHandler.reject(e)}
83 | };
84 | return function(){
85 | let msg;
86 | if(1===arguments.length){
87 | msg = arguments[0];
88 | }else if(2===arguments.length){
89 | msg = {
90 | type: arguments[0],
91 | args: arguments[1]
92 | };
93 | }else{
94 | toss("Invalid arugments for sqlite3Worker1Promiser()-created factory.");
95 | }
96 | if(!msg.dbId) msg.dbId = dbId;
97 | msg.messageId = genMsgId(msg);
98 | msg.departureTime = performance.now();
99 | const proxy = Object.create(null);
100 | proxy.message = msg;
101 | let rowCallbackId ;
102 | if('exec'===msg.type && msg.args){
103 | if('function'===typeof msg.args.callback){
104 | rowCallbackId = msg.messageId+':row';
105 | proxy.onrow = msg.args.callback;
106 | msg.args.callback = rowCallbackId;
107 | handlerMap[rowCallbackId] = proxy;
108 | }else if('string' === typeof msg.args.callback){
109 | toss("exec callback may not be a string when using the Promise interface.");
110 |
111 | }
112 | }
113 |
114 | let p = new Promise(function(resolve, reject){
115 | proxy.resolve = resolve;
116 | proxy.reject = reject;
117 | handlerMap[msg.messageId] = proxy;
118 | debug("Posting",msg.type,"message to Worker dbId="+(dbId||'default')+':',msg);
119 | config.worker.postMessage(msg);
120 | });
121 | if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]);
122 | return p;
123 | };
124 | };
125 | globalThis.sqlite3Worker1Promiser.defaultConfig = {
126 | worker: function(){
127 | return new Worker("sqlite3-worker1-bundler-friendly.mjs",{
128 | type: 'module'
129 | });
130 | }.bind({
131 | currentScript: globalThis?.document?.currentScript
132 | }),
133 | onerror: (...args)=>console.error('worker1 promiser error',...args)
134 | };
135 |
--------------------------------------------------------------------------------
/sqlite-wasm/jswasm/sqlite3-worker1-promiser.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-08-24
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | This file implements a Promise-based proxy for the sqlite3 Worker
14 | API #1. It is intended to be included either from the main thread or
15 | a Worker, but only if (A) the environment supports nested Workers
16 | and (B) it's _not_ a Worker which loads the sqlite3 WASM/JS
17 | module. This file's features will load that module and provide a
18 | slightly simpler client-side interface than the slightly-lower-level
19 | Worker API does.
20 |
21 | This script necessarily exposes one global symbol, but clients may
22 | freely `delete` that symbol after calling it.
23 | */
24 | 'use strict';
25 |
26 | globalThis.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){
27 |
28 | if(1===arguments.length && 'function'===typeof arguments[0]){
29 | const f = config;
30 | config = Object.assign(Object.create(null), callee.defaultConfig);
31 | config.onready = f;
32 | }else{
33 | config = Object.assign(Object.create(null), callee.defaultConfig, config);
34 | }
35 | const handlerMap = Object.create(null);
36 | const noop = function(){};
37 | const err = config.onerror
38 | || noop ;
39 | const debug = config.debug || noop;
40 | const idTypeMap = config.generateMessageId ? undefined : Object.create(null);
41 | const genMsgId = config.generateMessageId || function(msg){
42 | return msg.type+'#'+(idTypeMap[msg.type] = (idTypeMap[msg.type]||0) + 1);
43 | };
44 | const toss = (...args)=>{throw new Error(args.join(' '))};
45 | if(!config.worker) config.worker = callee.defaultConfig.worker;
46 | if('function'===typeof config.worker) config.worker = config.worker();
47 | let dbId;
48 | config.worker.onmessage = function(ev){
49 | ev = ev.data;
50 | debug('worker1.onmessage',ev);
51 | let msgHandler = handlerMap[ev.messageId];
52 | if(!msgHandler){
53 | if(ev && 'sqlite3-api'===ev.type && 'worker1-ready'===ev.result) {
54 |
55 | if(config.onready) config.onready();
56 | return;
57 | }
58 | msgHandler = handlerMap[ev.type] ;
59 | if(msgHandler && msgHandler.onrow){
60 | msgHandler.onrow(ev);
61 | return;
62 | }
63 | if(config.onunhandled) config.onunhandled(arguments[0]);
64 | else err("sqlite3Worker1Promiser() unhandled worker message:",ev);
65 | return;
66 | }
67 | delete handlerMap[ev.messageId];
68 | switch(ev.type){
69 | case 'error':
70 | msgHandler.reject(ev);
71 | return;
72 | case 'open':
73 | if(!dbId) dbId = ev.dbId;
74 | break;
75 | case 'close':
76 | if(ev.dbId===dbId) dbId = undefined;
77 | break;
78 | default:
79 | break;
80 | }
81 | try {msgHandler.resolve(ev)}
82 | catch(e){msgHandler.reject(e)}
83 | };
84 | return function(){
85 | let msg;
86 | if(1===arguments.length){
87 | msg = arguments[0];
88 | }else if(2===arguments.length){
89 | msg = {
90 | type: arguments[0],
91 | args: arguments[1]
92 | };
93 | }else{
94 | toss("Invalid arugments for sqlite3Worker1Promiser()-created factory.");
95 | }
96 | if(!msg.dbId) msg.dbId = dbId;
97 | msg.messageId = genMsgId(msg);
98 | msg.departureTime = performance.now();
99 | const proxy = Object.create(null);
100 | proxy.message = msg;
101 | let rowCallbackId ;
102 | if('exec'===msg.type && msg.args){
103 | if('function'===typeof msg.args.callback){
104 | rowCallbackId = msg.messageId+':row';
105 | proxy.onrow = msg.args.callback;
106 | msg.args.callback = rowCallbackId;
107 | handlerMap[rowCallbackId] = proxy;
108 | }else if('string' === typeof msg.args.callback){
109 | toss("exec callback may not be a string when using the Promise interface.");
110 |
111 | }
112 | }
113 |
114 | let p = new Promise(function(resolve, reject){
115 | proxy.resolve = resolve;
116 | proxy.reject = reject;
117 | handlerMap[msg.messageId] = proxy;
118 | debug("Posting",msg.type,"message to Worker dbId="+(dbId||'default')+':',msg);
119 | config.worker.postMessage(msg);
120 | });
121 | if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]);
122 | return p;
123 | };
124 | };
125 | globalThis.sqlite3Worker1Promiser.defaultConfig = {
126 | worker: function(){
127 | let theJs = "sqlite3-worker1.js";
128 | if(this.currentScript){
129 | const src = this.currentScript.src.split('/');
130 | src.pop();
131 | theJs = src.join('/')+'/' + theJs;
132 |
133 | }else if(globalThis.location){
134 |
135 | const urlParams = new URL(globalThis.location.href).searchParams;
136 | if(urlParams.has('sqlite3.dir')){
137 | theJs = urlParams.get('sqlite3.dir') + '/' + theJs;
138 | }
139 | }
140 | return new Worker(theJs + globalThis.location.search);
141 | }.bind({
142 | currentScript: globalThis?.document?.currentScript
143 | }),
144 | onerror: (...args)=>console.error('worker1 promiser error',...args)
145 | };
146 |
--------------------------------------------------------------------------------
/sqlite-wasm/jswasm/sqlite3-worker1.js:
--------------------------------------------------------------------------------
1 | /*
2 | 2022-05-23
3 |
4 | The author disclaims copyright to this source code. In place of a
5 | legal notice, here is a blessing:
6 |
7 | * May you do good and not evil.
8 | * May you find forgiveness for yourself and forgive others.
9 | * May you share freely, never taking more than you give.
10 |
11 | ***********************************************************************
12 |
13 | This is a JS Worker file for the main sqlite3 api. It loads
14 | sqlite3.js, initializes the module, and postMessage()'s a message
15 | after the module is initialized:
16 |
17 | {type: 'sqlite3-api', result: 'worker1-ready'}
18 |
19 | This seemingly superfluous level of indirection is necessary when
20 | loading sqlite3.js via a Worker. Instantiating a worker with new
21 | Worker("sqlite.js") will not (cannot) call sqlite3InitModule() to
22 | initialize the module due to a timing/order-of-operations conflict
23 | (and that symbol is not exported in a way that a Worker loading it
24 | that way can see it). Thus JS code wanting to load the sqlite3
25 | Worker-specific API needs to pass _this_ file (or equivalent) to the
26 | Worker constructor and then listen for an event in the form shown
27 | above in order to know when the module has completed initialization.
28 |
29 | This file accepts a URL arguments to adjust how it loads sqlite3.js:
30 |
31 | - `sqlite3.dir`, if set, treats the given directory name as the
32 | directory from which `sqlite3.js` will be loaded.
33 | */
34 | "use strict";
35 | {
36 | const urlParams = globalThis.location
37 | ? new URL(self.location.href).searchParams
38 | : new URLSearchParams();
39 | let theJs = 'sqlite3.js';
40 | if(urlParams.has('sqlite3.dir')){
41 | theJs = urlParams.get('sqlite3.dir') + '/' + theJs;
42 | }
43 |
44 | importScripts(theJs);
45 | }
46 | sqlite3InitModule().then(sqlite3 => sqlite3.initWorker1API());
47 |
--------------------------------------------------------------------------------
/sqlite-wasm/jswasm/sqlite3.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samwillis/yjs-sqlite-test/056845d4394354103fb132c54bf66ae4cf1f6439/sqlite-wasm/jswasm/sqlite3.wasm
--------------------------------------------------------------------------------
/sqlite-wasm/tester1-esm.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | sqlite3 tester #1:
10 | ES6 Module in UI thread
11 |
12 |
13 |
14 |
15 |