The Worker-specific variants of the most significant DB-class JS bindings are implemented, most notably various uses of DB.exec().

FossilOrigin-Name: 371d6f7497016ca9bf82c8524b4d701ddf1e614b3fb4e69ff63935da0d51ed05
This commit is contained in:
stephan
2022-06-01 11:20:07 +00:00
parent 645682a7c7
commit ea7a4e4fb3
6 changed files with 315 additions and 106 deletions
+202 -48
View File
@@ -278,7 +278,8 @@ Module.postRun.push(function(namespace/*the module object, the target for
image.
If the filename is provided, only the last component of the
path is used - any path prefix is stripped. If no name is
path is used - any path prefix is stripped and certain
"special" characters are replaced with `_`. If no name is
provided, a random name is generated. The resulting filename is
the one used for accessing the db file within root directory of
the emscripten-supplied virtual filesystem, and is set (with no
@@ -300,7 +301,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
arg = undefined;
}else if(arguments.length){ /*(filename[,buffer])*/
if('string'===typeof arg){
const p = arg.split('/').pop().replace(':','');
const p = arg.split('/').pop().replace(':','_');
if(p) fn = p;
if(arguments.length>1){
buffer = arguments[1];
@@ -406,7 +407,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
if('string'!==typeof out.sql) toss("Missing SQL argument.");
if(out.opt.callback || out.opt.resultRows){
switch((undefined===out.opt.rowMode)
? 'stmt' : out.opt.rowMode) {
? 'stmt' : out.opt.rowMode) {
case 'object': out.cbArg = (stmt)=>stmt.get({}); break;
case 'array': out.cbArg = (stmt)=>stmt.get([]); break;
case 'stmt': out.cbArg = (stmt)=>stmt; break;
@@ -440,9 +441,12 @@ Module.postRun.push(function(namespace/*the module object, the target for
/**
Finalizes all open statements and closes this database
connection. This is a no-op if the db has already been
closed.
closed. If the db is open and alsoUnlink is truthy then the
this.filename entry in the pseudo-filesystem will also be
removed (and any error in that attempt is silently
ignored).
*/
close: function(){
close: function(alsoUnlink){
if(this._pDb){
let s;
const that = this;
@@ -453,9 +457,15 @@ Module.postRun.push(function(namespace/*the module object, the target for
Object.values(this._udfs).forEach(SQM.removeFunction);
delete this._udfs;
delete this._statements;
delete this.filename;
api.sqlite3_close_v2(this._pDb);
delete this._pDb;
if(this.filename){
if(alsoUnlink){
try{SQM.FS.unlink(this.filename);}
catch(e){/*ignored*/}
}
delete this.filename;
}
}
},
/**
@@ -491,7 +501,7 @@ Module.postRun.push(function(namespace/*the module object, the target for
one.
This function supports the following additional options not
used by execMulti():
supported by execMulti():
- .multi: if true, this function acts as a proxy for
execMulti() and behaves identically to that function.
@@ -499,10 +509,11 @@ Module.postRun.push(function(namespace/*the module object, the target for
- .resultRows: if this is an array, each row of the result
set (if any) is appended to it in the format specified
for the `rowMode` property, with the exception that the
`rowMode` property _must_ be one of 'array' or 'object'
if this is set (else an exception is throws). It is legal
only legal values for `rowMode` in this case are 'array'
or 'object', neither of which is the default. It is legal
to use both `resultRows` and `callback`, but `resultRows`
is likely much simpler to use for small data sets.
is likely much simpler to use for small data sets and can
be used over a WebWorker-style message interface.
- .columnNames: if this is an array and the query has
result columns, the array is passed to
@@ -528,8 +539,8 @@ Module.postRun.push(function(namespace/*the module object, the target for
try {
if(Array.isArray(opt.resultRows)){
if(opt.rowMode!=='array' && opt.rowMode!=='object'){
throw new Error("Invalid rowMode for resultRows array: must "+
"be one of 'array' or 'object'.");
toss("Invalid rowMode for resultRows array: must",
"be one of 'array' or 'object'.");
}
rowTarget = opt.resultRows;
}
@@ -1316,6 +1327,13 @@ Module.postRun.push(function(namespace/*the module object, the target for
const ptr = api.sqlite3_column_blob(this._pStmt, ndx);
const rc = new Uint8Array(n);
for(let i = 0; i < n; ++i) rc[i] = HEAP8[ptr + i];
if(n && this.db._blobXfer instanceof Array){
/* This is an optimization soley for the
Worker-based API. These values will be
transfered to the main thread directly
instead of being copied. */
this.db._blobXfer.push(rc.buffer);
}
return rc;
}
default: toss("Don't know how to translate",
@@ -1462,13 +1480,9 @@ Module.postRun.push(function(namespace/*the module object, the target for
SQLite3
};
const postApiLoaded = function(){
setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
};
if(self === self.window){
/* This is running in the main window thread, so we're done. */
postApiLoaded();
setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
return;
}
/******************************************************************
@@ -1506,18 +1520,170 @@ Module.postRun.push(function(namespace/*the module object, the target for
engine's memory.
*/
/**
Helper for managing Worker-level state.
*/
const wState = {
db: undefined,
open: function(arg){
if(!arg && this.db) return this.db;
else if(this.db) this.db.close();
return this.db = (Array.isArray(arg) ? new DB(...arg) : new DB(arg));
},
close: function(){
if(this.db){
this.db.close();
this.db = undefined;
}
},
affirmOpen: function(){
return this.db || toss("DB is not opened.");
},
post: function(type,data,xferList){
if(xferList){
self.postMessage({type, data},xferList);
xferList.length = 0;
}else{
self.postMessage({type, data});
}
}
};
const wMsg = (type,data)=>self.postMessage({type, data});
/**
UNDER CONSTRUCTION:
A level of "organizational abstraction" for the Worker
API. Each method in this object must map directly to a Worker
message type key. The onmessage() dispatcher attempts to
dispatch all inbound messages to a method of this object,
passing it the event.data part of the inbound event object. All
methods must return a plain Object containing any response
state, which the dispatcher may amend. All methods must throw
on error.
*/
const wMsgHandler = {
xfer: [/*Temp holder for "transferable" postMessage() state.*/],
/**
Proxy for DB.exec() which expects a single argument of type
string (SQL to execute) or an options object in the form
expected by exec(). The notable differences from exec()
include:
- The default value for options.rowMode is 'array' because
the normal default cannot cross the window/Worker boundary.
- A function-type options.callback property cannot cross
the window/Worker boundary, so is not useful here. If
options.callback is a string then it is assumed to be a
message type key, in which case a callback function will be
applied which posts each row result via:
postMessage({type: thatKeyType, data: theRow})
And, at the end of the result set (whether or not any
result rows were produced), it will post an identical
message with data:null to alert the caller than the result
set is completed.
The callback proxy must not recurse into this interface, or
results are undefined. (It hypothetically cannot recurse
because an exec() call will be tying up the Worker thread,
causing any recursion attempt to wait until the first
exec() is completed.)
The response is the input options object (or a synthesized
one if passed only a string), noting that
options.resultRows and options.columnNames may be populated
by the call to exec().
This opens/creates the Worker's db if needed.
*/
exec: function(ev){
const opt = (
'string'===typeof ev.data
) ? {sql: ev.data} : (ev.data || {});
if(!opt.rowMode){
/* Since the default rowMode of 'stmt' is not useful
for the Worker interface, we'll default to
something else. */
opt.rowMode = 'array';
}else if('stmt'===opt.rowMode){
toss("Invalid rowMode for exec(): stmt mode",
"does not work in the Worker API.");
}
const db = wState.open();
if(opt.callback || opt.resultRows instanceof Array){
// Part of a copy-avoidance optimization for blobs
db._blobXfer = this.xfer;
}
const callbackMsgType = opt.callback;
if('string' === typeof callbackMsgType){
const that = this;
opt.callback =
(row)=>wState.post(callbackMsgType,row,this.xfer);
}
try {
db.exec(opt);
if(opt.callback instanceof Function){
opt.callback = callbackMsgType;
wState.post(callbackMsgType, null);
}
}finally{
delete db._blobXfer;
if('string'===typeof callbackMsgType){
opt.callback = callbackMsgType;
}
}
return opt;
}/*exec()*/,
/**
Proxy for DB.exportBinaryImage(). Throws if the db has not
been opened. Response is an object:
{
buffer: Uint8Array (db file contents),
filename: the current db filename,
mimetype: string
}
*/
export: function(ev){
const db = wState.affirmOpen();
const response = {
buffer: db.exportBinaryImage(),
filename: db.filename,
mimetype: 'application/x-sqlite3'
};
this.xfer.push(response.buffer.buffer);
return response;
}/*export()*/,
/**
Proxy for the DB constructor. Expects to be passed a single
object or a falsy value to use defaults. The object may
have a filename property to name the db file (see the DB
constructor for peculiarities and transformations) and/or a
buffer property (a Uint8Array holding a complete database
file's contents). The response is an object:
{
filename: db filename (possibly differing from the input)
}
If the Worker's db is currently opened, this call closes it
before proceeding.
*/
open: function(ev){
wState.close(/*true???*/);
const args = [], data = (ev.data || {});
if(data.filename) args.push(data.filename);
if(data.buffer){
args.push(data.buffer);
this.xfer.push(data.buffer.buffer);
}
const db = wState.open(args);
return {filename: db.filename};
}
}/*wMsgHandler*/;
/**
UNDER CONSTRUCTION!
A subset of the DB API is accessible via Worker messages in the form:
@@ -1534,51 +1700,39 @@ Module.postRun.push(function(namespace/*the module object, the target for
during processing result in an `error`-type event with a
payload in the form:
{message: error string,
errorClass: class name of the error type,
[, messageId: if set in the inbound message]}
{
message: error string,
errorClass: class name of the error type,
input: ev.data,
[messageId: if set in the inbound message]
}
The individual APIs will be documented as they are fleshed out.
The individual APIs are documented in the wMsgHandler object.
*/
self.onmessage = function(ev){
ev = ev.data;
let response = {}, evType = ev.type;
let response, evType = ev.type;
try {
switch(evType){
case 'open': {
const args = [], data = (ev.data || {});
if(data.filename) args.push(data.filename);
if(data.buffer) args.push(data.buffer);
const d = wState.open(args);
response.filename = d.filename;
response.messageId = data.messageId;
break;
}
case 'exec': {
const opt = (
'string'===typeof ev.data
) ? {sql: ev.data} : (ev.data || {});
if(!opt.rowMode) opt.rowMode = 'array';
wState.open().exec(opt);
response = opt;
break;
}
default:
throw new Error("Unknown db worker message type: "+ev.type);
if(wMsgHandler.hasOwnProperty(evType) &&
wMsgHandler[evType] instanceof Function){
response = wMsgHandler[evType](ev);
}else{
toss("Unknown db worker message type:",ev.type);
}
}catch(err){
evType = 'error';
response = {
message: err.message,
errorClass: err.name
errorClass: err.name,
input: ev
};
}
if(!response.messageId && ev.data
&& 'object'===typeof ev.data && ev.data.messageId){
response.messageId = ev.data.messageId;
}
wMsg(evType, response);
wState.post(evType, response, wMsgHandler.xfer);
};
postApiLoaded();
setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0);
});