├── index.html ├── index.js ├── readme.md ├── sqlite-wasm ├── README.txt ├── common │ ├── SqliteTestUtil.js │ ├── emscripten.css │ └── testing.css ├── demo-123-worker.html ├── demo-123.html ├── demo-123.js ├── demo-jsstorage.html ├── demo-jsstorage.js ├── demo-worker1-promiser.html ├── demo-worker1-promiser.js ├── demo-worker1.html ├── demo-worker1.js ├── index.html ├── jswasm │ ├── sqlite3-bundler-friendly.mjs │ ├── sqlite3-node.mjs │ ├── sqlite3-opfs-async-proxy.js │ ├── sqlite3-worker1-bundler-friendly.mjs │ ├── sqlite3-worker1-promiser-bundler-friendly.js │ ├── sqlite3-worker1-promiser.js │ ├── sqlite3-worker1.js │ ├── sqlite3.js │ ├── sqlite3.mjs │ └── sqlite3.wasm ├── tester1-esm.html ├── tester1-worker.html ├── tester1.html ├── tester1.js ├── tester1.mjs └── version.txt ├── test.js └── yjsSQLite.js /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello, sqlite3 8 | 18 | 19 | 20 |

yJS + sqlite3 demo

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 |

1-2-sqlite3 worker demo

21 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /sqlite-wasm/demo-123.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello, sqlite3 8 | 18 | 19 | 20 |

1-2-sqlite3 demo

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 |
28 | Options 29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |
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:
40 |
althttpd -enable-sab -page index.html
41 |
and the individual pages be started in their own tab. 42 | Warnings and Caveats: 43 | 65 |
66 |
The tests and demos... 67 | 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 |
Variants: 16 | conventional UI thread, 17 | conventional worker, 18 | ESM in UI thread, 19 | ESM worker 20 |
21 |
22 | 23 | 24 |
25 |
26 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sqlite-wasm/tester1-worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | sqlite3 tester #1: Worker thread 10 | 11 | 12 | 13 |

sqlite3 tester #1: Worker thread

14 |
Variants: 15 | conventional UI thread, 16 | conventional worker, 17 | ESM in UI thread, 18 | ESM worker 19 |
20 |
21 | 22 | 23 |
24 |
25 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /sqlite-wasm/tester1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | sqlite3 tester #1: 10 | UI thread 11 | 12 | 13 | 14 |

15 |
Variants: 16 | conventional UI thread, 17 | conventional worker, 18 | ESM in UI thread, 19 | ESM worker 20 |
21 |
22 | 23 | 24 |
25 |
26 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /sqlite-wasm/version.txt: -------------------------------------------------------------------------------- 1 | sqlite-wasm-snapshot-20230311-3420000 -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import * as yjsSQLite from "./yjsSQLite.js"; 2 | import * as Y from "https://esm.run/yjs@13.5.52"; 3 | 4 | export const test = function (sqlite3, log) { 5 | const capi = sqlite3.capi /*C-style API*/, 6 | oo = sqlite3.oo1; /*high-level OO API*/ 7 | log("sqlite3 version", capi.sqlite3_libversion(), capi.sqlite3_sourceid()); 8 | const db = new oo.DB("/mydb.sqlite3", "ct"); 9 | log("transient db =", db.filename); 10 | try { 11 | 12 | yjsSQLite.install(db); 13 | 14 | log("Create a table..."); 15 | db.exec("CREATE TABLE IF NOT EXISTS docs (id INTEGER PRIMARY KEY, doc BLOB)"); 16 | 17 | log("Create and insert new yJS document"); 18 | let doc = new Y.Doc(); 19 | let map = doc.getMap("myMap"); 20 | map.set("foo", "bar"); 21 | db.exec({ 22 | sql: "insert into docs (id, doc) values (?, ?);", 23 | bind: [1, Y.encodeStateAsUpdate(doc)], 24 | }); 25 | doc = null; 26 | map = null; 27 | 28 | function getAndLogDoc(info) { 29 | log(info); 30 | db.exec({ 31 | sql: "select doc from docs where id = ?;", 32 | bind: [1], 33 | callback: function (row) { 34 | doc = new Y.Doc(); 35 | Y.applyUpdate(doc, row[0]); 36 | log('Value of "foo":', doc.getMap("myMap").get("foo")); 37 | }, 38 | }); 39 | } 40 | getAndLogDoc("Get yJS document"); 41 | 42 | log("Update yJS document"); 43 | map = doc.getMap("myMap"); 44 | map.set("foo", "bar2"); 45 | db.exec({ 46 | sql: "update docs set doc = ? where id = ?;", 47 | bind: [Y.encodeStateAsUpdate(doc), 1], 48 | }); 49 | doc = null; 50 | map = null; 51 | 52 | getAndLogDoc("Get Updated yJS document"); 53 | 54 | log("Update yJS document using y_apply_update with whole doc"); 55 | map = doc.getMap("myMap"); 56 | map.set("foo", "bar3"); 57 | db.exec({ 58 | sql: "update docs set doc = y_apply_update(doc, ?) where id = ?;", 59 | bind: [Y.encodeStateAsUpdate(doc), 1], 60 | }); 61 | doc = null; 62 | map = null; 63 | 64 | getAndLogDoc("Get Updated yJS document"); 65 | 66 | log("Get current state vector form database using y_encode_state_vector"); 67 | let stateVector; 68 | db.exec({ 69 | sql: "select y_encode_state_vector(doc) from docs where id = ?;", 70 | bind: [1], 71 | callback: function (row) { 72 | log("State vector:", row[0]); 73 | stateVector = row[0]; 74 | }, 75 | }); 76 | 77 | log("Update yJS document using y_apply_update with diff"); 78 | map = doc.getMap("myMap"); 79 | map.set("foo", "bar4"); 80 | let update = Y.encodeStateAsUpdate(doc, stateVector); 81 | db.exec({ 82 | sql: "update docs set doc = y_apply_update(doc, ?) where id = ?;", 83 | bind: [update, 1], 84 | }); 85 | doc = null; 86 | map = null; 87 | 88 | getAndLogDoc("Get Updated yJS document"); 89 | 90 | log("Get value of 'foo' directly from database"); 91 | db.exec({ 92 | sql: "select y_get_map_json(doc, 'myMap') ->> '$.foo' from docs where id = ?;", 93 | bind: [1], 94 | callback: function (row) { 95 | log("Value of 'foo':", row[0]); 96 | }, 97 | }); 98 | 99 | log("Add a bunch of documents"); 100 | for (let i = 1; i <= 100; i++) { 101 | doc = new Y.Doc(); 102 | map = doc.getMap("myMap"); 103 | map.set("foo", "bar" + i); 104 | map.set("num", Math.floor(Math.random() * 100)); 105 | db.exec({ 106 | sql: "insert into docs (doc) values (?);", 107 | bind: [Y.encodeStateAsUpdate(doc)], 108 | }); 109 | doc = null; 110 | map = null; 111 | } 112 | 113 | log("Count all documents with 'num' below 50"); 114 | db.exec({ 115 | sql: "select count(*) from docs where y_get_map_json(doc, 'myMap') ->> '$.num' < 50;", 116 | callback: function (row) { 117 | log("Count:", row[0]); 118 | }, 119 | }); 120 | 121 | 122 | log("Create an index on the 'num' field by adding a virtual column"); 123 | db.exec("alter table docs add column num integer generated always as (y_get_map_json(doc, 'myMap') ->> '$.num') virtual;"); 124 | db.exec("create index docs_num on docs (num);"); 125 | 126 | log("Count all documents with 'num' below 50"); 127 | db.exec({ 128 | sql: "select count(*) from docs where num < 50;", 129 | callback: function (row) { 130 | log("Count:", row[0]); 131 | }, 132 | }); 133 | 134 | log("Count AGAIN all documents with 'num' below 50"); 135 | db.exec({ 136 | sql: "select count(*) from docs where num < 50;", 137 | callback: function (row) { 138 | log("Count:", row[0]); 139 | }, 140 | }); 141 | 142 | 143 | } finally { 144 | db.close(); 145 | } 146 | 147 | log("That's all, folks!"); 148 | }; -------------------------------------------------------------------------------- /yjsSQLite.js: -------------------------------------------------------------------------------- 1 | import * as Y from "https://esm.run/yjs@13.5.52"; 2 | 3 | export function install(db) { 4 | for (const [name, func] of Object.entries(functions)) { 5 | db.createFunction({ 6 | name, 7 | xFunc: func, 8 | deterministic: true, 9 | }); 10 | } 11 | } 12 | 13 | const functions = { 14 | /** 15 | * Create a new Y.Doc and return its initial state as an update. 16 | * @param {number} pCx 17 | * @returns {Uint8Array} The initial state of the new Y.Doc as an update. 18 | */ 19 | y_new_doc(pCx) { 20 | console.log("y_new_doc"); 21 | const doc = new Y.Doc(); 22 | return Y.encodeStateAsUpdate(doc); 23 | }, 24 | 25 | /** 26 | * Apply a document update on the document. 27 | * Note that this feature only merges document updates and doesn't garbage-collect 28 | * deleted content. 29 | * Use y_apply_update_gc to apply an update and garbage-collect deleted content. 30 | * @param {number} pCx 31 | * @param {Uint8Array} savedDoc 32 | * @param {Uint8Array} update 33 | * @returns {Uint8Array} The new state of the document as an update. 34 | */ 35 | y_apply_update(pCx, savedDoc, update) { 36 | console.log("y_apply_update"); 37 | return Y.mergeUpdates([savedDoc, update]); 38 | }, 39 | 40 | /** 41 | * Apply a document update on the document and garbage-collect deleted content. 42 | * @param {number} pCx 43 | * @param {Uint8Array} savedDoc 44 | * @param {Uint8Array} update 45 | * @returns {Uint8Array} The new state of the document as an update. 46 | */ 47 | y_apply_update_gc(pCx, savedDoc, update) { 48 | console.log("y_apply_update_gc"); 49 | const doc = new Y.Doc(); 50 | Y.applyUpdate(doc, savedDoc); 51 | Y.applyUpdate(doc, update); 52 | return Y.encodeStateAsUpdate(doc); 53 | }, 54 | 55 | /** 56 | * Merge several document updates into a single document update while removing 57 | * duplicate information. 58 | * Note that this feature only merges document updates and doesn't garbage-collect 59 | * deleted content. 60 | * @param {number} pCx 61 | * @param {Array} updates 62 | * @returns {Uint8Array} The merged update. 63 | */ 64 | y_merge_updates(pCx, updates) { 65 | console.log("y_merge_updates"); 66 | return Y.mergeUpdates(updates); 67 | }, 68 | 69 | /** 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 | * @param {number} pCx 73 | * @param {Uint8Array} savedDoc 74 | * @param {Uint8Array} stateVector 75 | * @returns {Uint8Array} The new state of the document as an update. 76 | */ 77 | y_diff_update(pCx, savedDoc, stateVector) { 78 | console.log("y_diff_update"); 79 | return Y.diffUpdate(savedDoc, stateVector); 80 | }, 81 | 82 | /** 83 | * Computes the state vector and encodes it into an Uint8Array 84 | * @param {number} pCx 85 | * @param {Uint8Array} savedDoc 86 | * @returns {Uint8Array} The state vector of the document. 87 | */ 88 | y_encode_state_vector(pCx, savedDoc) { 89 | console.log("y_encode_state_vector"); 90 | return Y.encodeStateVectorFromUpdate(savedDoc); 91 | }, 92 | 93 | /** 94 | * Get the map at the given key from the given savedDoc, and return it as JSON. 95 | * @param {number} pCx 96 | * @param {Uint8Array} savedDoc 97 | * @param {string} key 98 | * @returns {string} The map at the given key from the given savedDoc, as JSON. 99 | */ 100 | y_get_map_json(pCx, savedDoc, key) { 101 | console.log('y_get_map_json'); 102 | const doc = new Y.Doc(); 103 | Y.applyUpdate(doc, savedDoc); 104 | return JSON.stringify(doc.getMap(key).toJSON()); 105 | }, 106 | 107 | /** 108 | * Get the array at the given key from the given savedDoc, and return it as JSON. 109 | * @param {number} pCx 110 | * @param {Uint8Array} savedDoc 111 | * @param {string} key 112 | * @returns {string} The array at the given key from the given savedDoc, as JSON. 113 | */ 114 | y_get_array_json(pCx, savedDoc, key) { 115 | console.log('y_get_array_json'); 116 | const doc = new Y.Doc(); 117 | Y.applyUpdate(doc, savedDoc); 118 | return JSON.stringify(doc.getArray(key).toJSON()); 119 | }, 120 | 121 | /** 122 | * Get the xmlFragment at the given key from the given savedDoc, and return it as JSON. 123 | * @param {number} pCx 124 | * @param {Uint8Array} savedDoc 125 | * @param {string} key 126 | * @returns {string} The xmlFragment at the given key from the given savedDoc, as JSON. 127 | */ 128 | y_get_xml_fragment_json(pCx, savedDoc, key) { 129 | console.log('y_get_xml_fragment_json'); 130 | const doc = new Y.Doc(); 131 | Y.applyUpdate(doc, savedDoc); 132 | return JSON.stringify(doc.getXmlFragment(key).toJSON()); 133 | }, 134 | 135 | /** 136 | * Extract all text from the xmlFragment at the given key from the given savedDoc. 137 | * Useful for full text search. 138 | * @param {number} pCx 139 | * @param {Uint8Array} savedDoc 140 | * @param {string} key 141 | * @returns {string} 142 | */ 143 | y_extract_xml_fragment_text(pCx, savedDoc, key) { 144 | console.log('y_extract_xml_fragment_text'); 145 | const doc = new Y.Doc(); 146 | Y.applyUpdate(doc, savedDoc); 147 | const xml = doc.getXmlFragment(key); 148 | // TODO 149 | return ''; 150 | }, 151 | 152 | }; 153 | --------------------------------------------------------------------------------