xref: /sqlite-3.40.0/ext/wasm/demo-worker1.js (revision f45c3370)
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 ? ev.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