SQLite

Check-in [b5dbd52195]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add the pause/unpause capability to the opfs-sahpool VFS, as discussed in forum thread fe8cdb8431c. Summary: this gives clients a way to eke some degree of multi-page/tab/Worker concurrency out of this VFS but requires that coordination to be implemented client-side, e.g. via a SharedWorker or WebLocks.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: b5dbd521951e129b4dec69f191a872500dbf387b34a8479ad58b053ffcccbab9
User & Date: stephan 2025-02-20 04:14:26.736
Context
2025-02-20
04:45
wasm makefile docs: make explicit that the node.js-for-node.js builds (as opposed to the node.js-for-browser builds) are both untested and unsupported. (check-in: e1f184889f user: stephan tags: trunk)
04:14
Add the pause/unpause capability to the opfs-sahpool VFS, as discussed in forum thread fe8cdb8431c. Summary: this gives clients a way to eke some degree of multi-page/tab/Worker concurrency out of this VFS but requires that coordination to be implemented client-side, e.g. via a SharedWorker or WebLocks. (check-in: b5dbd52195 user: stephan tags: trunk)
03:27
configure: when running proj-check-function-in-lib, strip -Werror from CFLAGS for the duration of the test. This enables CFLAGS='-Wall -Werror' and the like to be passed to configure without breaking these configure-time checks. (check-in: 4ae9d6c642 user: stephan tags: trunk)
2025-01-31
17:47
Minor cleanups in the opfs-sahpool pause/unpause API demo. (Closed-Leaf check-in: e205cdc468 user: stephan tags: opfs-sahpool-pause)
Changes
Unified Diff Ignore Whitespace Patch
Changes to ext/wasm/GNUmakefile.
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
      $(error Cannot make release-quality binary because wasm-strip is not available.)
    endif
    bin.wasm-strip := echo "not wasm-stripping"
  endif
  ifeq (,$(filter $(OPTIMIZED_TARGETS),$(MAKECMDGOALS)))
    $(info ==============================================================)
    $(info == Development build. Make one of (dist, snapshot) for a)
    $(info == smaller release build.)
    $(info ==============================================================)
  endif
endif
# ^^^ end of are-we-MAKING_CLEAN
maybe-wasm-strip := $(bin.wasm-strip)

########################################################################







|







99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
      $(error Cannot make release-quality binary because wasm-strip is not available.)
    endif
    bin.wasm-strip := echo "not wasm-stripping"
  endif
  ifeq (,$(filter $(OPTIMIZED_TARGETS),$(MAKECMDGOALS)))
    $(info ==============================================================)
    $(info == Development build. Make one of (dist, snapshot) for a)
    $(info == smaller and faster release build.)
    $(info ==============================================================)
  endif
endif
# ^^^ end of are-we-MAKING_CLEAN
maybe-wasm-strip := $(bin.wasm-strip)

########################################################################
Changes to ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js.
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
    /* Current number of in-use files from pool. */
    getFileCount(){return this.#mapFilenameToSAH.size}

    /* Returns an array of the names of all
       currently-opened client-specified filenames. */
    getFileNames(){
      const rc = [];
      const iter = this.#mapFilenameToSAH.keys();
      for(const n of iter) rc.push(n);
      return rc;
    }

//    #createFileObject(sah,clientName,opaqueName){
//      const f = Object.assign(Object.create(null),{
//        clientName, opaqueName
//      });
//      this.#mapSAHToMeta.set(sah, f);
//      return f;
//    }
//    #unmapFileObject(sah){
//      this.#mapSAHToMeta.delete(sah);
//    }

    /**
       Adds n files to the pool's capacity. This change is
       persistent across settings. Returns a Promise which resolves
       to the new capacity.
    */
    async addCapacity(n){
      for(let i = 0; i < n; ++i){







|
<



<
<
<
<
<
<
<
<
<
<
<







497
498
499
500
501
502
503
504

505
506
507











508
509
510
511
512
513
514
    /* Current number of in-use files from pool. */
    getFileCount(){return this.#mapFilenameToSAH.size}

    /* Returns an array of the names of all
       currently-opened client-specified filenames. */
    getFileNames(){
      const rc = [];
      for(const n of this.#mapFilenameToSAH.keys()) rc.push(n);

      return rc;
    }












    /**
       Adds n files to the pool's capacity. This change is
       persistent across settings. Returns a Promise which resolves
       to the new capacity.
    */
    async addCapacity(n){
      for(let i = 0; i < n; ++i){
553
554
555
556
557
558
559
560
561

562
563
564
565
566
567
568
569
570
571
572
573
574

575



576
577
578
579
580
581
582
583
584
585
586
587
588
        this.#availableSAH.delete(ah);
        ++nRm;
      }
      return nRm;
    }

    /**
       Releases all currently-opened SAHs. The only legal
       operation after this is acquireAccessHandles().

    */
    releaseAccessHandles(){
      for(const ah of this.#mapSAHToName.keys()) ah.close();
      this.#mapSAHToName.clear();
      this.#mapFilenameToSAH.clear();
      this.#availableSAH.clear();
    }

    /**
       Opens all files under this.vfsDir/this.#dhOpaque and acquires
       a SAH for each. returns a Promise which resolves to no value
       but completes once all SAHs are acquired. If acquiring an SAH
       throws, SAHPool.$error will contain the corresponding

       exception.




       If clearFiles is true, the client-stored state of each file is
       cleared when its handle is acquired, including its name, flags,
       and any data stored after the metadata block.
    */
    async acquireAccessHandles(clearFiles){
      const files = [];
      for await (const [name,h] of this.#dhOpaque){
        if('file'===h.kind){
          files.push([name,h]);
        }
      }
      return Promise.all(files.map(async([name,h])=>{







|
|
>









|
|
|
|
>
|
>
>
>





|







541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
        this.#availableSAH.delete(ah);
        ++nRm;
      }
      return nRm;
    }

    /**
       Releases all currently-opened SAHs. The only legal operation
       after this is acquireAccessHandles() or (if this is called from
       pauseVfs()) either of isPaused() or unpauseVfs().
    */
    releaseAccessHandles(){
      for(const ah of this.#mapSAHToName.keys()) ah.close();
      this.#mapSAHToName.clear();
      this.#mapFilenameToSAH.clear();
      this.#availableSAH.clear();
    }

    /**
       Opens all files under this.vfsDir/this.#dhOpaque and acquires a
       SAH for each. Returns a Promise which resolves to no value but
       completes once all SAHs are acquired. If acquiring an SAH
       throws, this.$error will contain the corresponding Error
       object.

       If it throws, it releases any SAHs which it may have
       acquired before the exception was thrown, leaving the VFS in a
       well-defined but unusable state.

       If clearFiles is true, the client-stored state of each file is
       cleared when its handle is acquired, including its name, flags,
       and any data stored after the metadata block.
    */
    async acquireAccessHandles(clearFiles=false){
      const files = [];
      for await (const [name,h] of this.#dhOpaque){
        if('file'===h.kind){
          files.push([name,h]);
        }
      }
      return Promise.all(files.map(async([name,h])=>{
828
829
830
831
832
833
834
835



836
837
838
839
840



841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
































































862
863
864
865
866
867
868
      return this.#mapFilenameToSAH.get(path);
    }

    /**
       Removes this object's sqlite3_vfs registration and shuts down
       this object, releasing all handles, mappings, and whatnot,
       including deleting its data directory. There is currently no
       way to "revive" the object and reaquire its resources.




       This function is intended primarily for testing.

       Resolves to true if it did its job, false if the
       VFS has already been shut down.



    */
    async removeVfs(){
      if(!this.#cVfs.pointer || !this.#dhOpaque) return false;
      capi.sqlite3_vfs_unregister(this.#cVfs.pointer);
      this.#cVfs.dispose();
      delete initPromises[this.vfsName];
      try{
        this.releaseAccessHandles();
        await this.#dhVfsRoot.removeEntry(OPAQUE_DIR_NAME, {recursive: true});
        this.#dhOpaque = undefined;
        await this.#dhVfsParent.removeEntry(
          this.#dhVfsRoot.name, {recursive: true}
        );
        this.#dhVfsRoot = this.#dhVfsParent = undefined;
      }catch(e){
        sqlite3.config.error(this.vfsName,"removeVfs() failed:",e);
        /*otherwise ignored - there is no recovery strategy*/
      }
      return true;
    }


































































    //! Documented elsewhere in this file.
    exportFile(name){
      const sah = this.#mapFilenameToSAH.get(name) || toss("File not found:",name);
      const n = sah.getSize() - HEADER_OFFSET_DATA;
      const b = new Uint8Array(n>0 ? n : 0);
      if(n>0){







|
>
>
>





>
>
>















|





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
      return this.#mapFilenameToSAH.get(path);
    }

    /**
       Removes this object's sqlite3_vfs registration and shuts down
       this object, releasing all handles, mappings, and whatnot,
       including deleting its data directory. There is currently no
       way to "revive" the object and reaquire its
       resources. Similarly, there is no recovery strategy if removal
       of any given SAH fails, so such errors are ignored by this
       function.

       This function is intended primarily for testing.

       Resolves to true if it did its job, false if the
       VFS has already been shut down.

       @see pauseVfs()
       @see unpauseVfs()
    */
    async removeVfs(){
      if(!this.#cVfs.pointer || !this.#dhOpaque) return false;
      capi.sqlite3_vfs_unregister(this.#cVfs.pointer);
      this.#cVfs.dispose();
      delete initPromises[this.vfsName];
      try{
        this.releaseAccessHandles();
        await this.#dhVfsRoot.removeEntry(OPAQUE_DIR_NAME, {recursive: true});
        this.#dhOpaque = undefined;
        await this.#dhVfsParent.removeEntry(
          this.#dhVfsRoot.name, {recursive: true}
        );
        this.#dhVfsRoot = this.#dhVfsParent = undefined;
      }catch(e){
        sqlite3.config.error(this.vfsName,"removeVfs() failed with no recovery strategy:",e);
        /*otherwise ignored - there is no recovery strategy*/
      }
      return true;
    }


    /**
       "Pauses" this VFS by unregistering it from SQLite and
       relinquishing all open SAHs, leaving the associated files
       intact. If this object is already paused, this is a
       no-op. Returns this object.

       This function throws if SQLite has any opened file handles
       hosted by this VFS, as the alternative would be to invoke
       Undefined Behavior by closing file handles out from under the
       library. Similarly, automatically closing any database handles
       opened by this VFS would invoke Undefined Behavior in
       downstream code which is holding those pointers.

       If this function throws due to open file handles then it has
       no side effects. If the OPFS API throws while closing handles
       then the VFS is left in an undefined state.

       @see isPaused()
       @see unpauseVfs()
    */
    pauseVfs(){
      if(this.#mapS3FileToOFile_.size>0){
        sqlite3.SQLite3Error.toss(
          capi.SQLITE_MISUSE, "Cannot pause VFS",
          this.vfsName,"because it has opened files."
        );
      }
      if(this.#mapSAHToName.size>0){
        capi.sqlite3_vfs_unregister(this.vfsName);
        this.releaseAccessHandles();
      }
      return this;
    }

    /**
       Returns true if this pool is currently paused else false.

       @see pauseVfs()
       @see unpauseVfs()
    */
    isPaused(){
      return 0===this.#mapSAHToName.size;
    }

    /**
       "Unpauses" this VFS, reacquiring all SAH's and (if successful)
       re-registering it with SQLite. This is a no-op if the VFS is
       not currently paused.

       The returned Promise resolves to this object. See
       acquireAccessHandles() for how it behaves if it throws due to
       SAH acquisition failure.

       @see isPaused()
       @see pauseVfs()
    */
    async unpauseVfs(){
      if(0===this.#mapSAHToName.size){
        return this.acquireAccessHandles(false).
          then(()=>capi.sqlite3_vfs_register(this.#cVfs, 0),this);
      }
      return this;
    }

    //! Documented elsewhere in this file.
    exportFile(name){
      const sah = this.#mapFilenameToSAH.get(name) || toss("File not found:",name);
      const n = sah.getSize() - HEADER_OFFSET_DATA;
      const b = new Uint8Array(n>0 ? n : 0);
      if(n>0){
979
980
981
982
983
984
985




986
987
988
989
990
991
992
    importDb(name, bytes){ return this.#p.importDb(name,bytes) }

    async wipeFiles(){ return this.#p.reset(true) }

    unlink(filename){ return this.#p.deletePath(filename) }

    async removeVfs(){ return this.#p.removeVfs() }





  }/* class OpfsSAHPoolUtil */;

  /**
     Returns a resolved Promise if the current environment
     has a "fully-sync" SAH impl, else a rejected Promise.
  */







>
>
>
>







1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
    importDb(name, bytes){ return this.#p.importDb(name,bytes) }

    async wipeFiles(){ return this.#p.reset(true) }

    unlink(filename){ return this.#p.deletePath(filename) }

    async removeVfs(){ return this.#p.removeVfs() }

    pauseVfs(){ this.#p.pauseVfs(); return this; }
    async unpauseVfs(){ return this.#p.unpauseVfs().then(()=>this); }
    isPaused(){ return this.#p.isPaused() }

  }/* class OpfsSAHPoolUtil */;

  /**
     Returns a resolved Promise if the current environment
     has a "fully-sync" SAH impl, else a rejected Promise.
  */
1213
1214
1215
1216
1217
1218
1219



































1220
1221
1222
1223
1224
1225
1226
     The SQLite VFS name under which this pool's VFS is registered.

     - [async] void wipeFiles()

     Clears all client-defined state of all SAHs and makes all of them
     available for re-use by the pool. Results are undefined if any such
     handles are currently in use, e.g. by an sqlite3 db.



































  */
  sqlite3.installOpfsSAHPoolVfs = async function(options=Object.create(null)){
    options = Object.assign(Object.create(null), optionDefaults, (options||{}));
    const vfsName = options.name;
    if(options.$testThrowPhase1){
      throw options.$testThrowPhase1;
    }







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
     The SQLite VFS name under which this pool's VFS is registered.

     - [async] void wipeFiles()

     Clears all client-defined state of all SAHs and makes all of them
     available for re-use by the pool. Results are undefined if any such
     handles are currently in use, e.g. by an sqlite3 db.

     APIs specific to the "pause" capability (added in version 3.49):

     Summary: "pausing" the VFS disassociates it from SQLite and
     relinquishes its SAHs so that they may be opened by another
     instance of this VFS (running in a separate tab/page or Worker).
     "Unpausing" it takes back control, if able.

     - pauseVfs()

     "Pauses" this VFS by unregistering it from SQLite and
     relinquishing all open SAHs, leaving the associated files intact.
     This enables pages/tabs to coordinate semi-concurrent usage of
     this VFS.  If this object is already paused, this is a
     no-op. Returns this object. Throws if SQLite has any opened file
     handles hosted by this VFS. If this function throws due to open
     file handles then it has no side effects. If the OPFS API throws
     while closing handles then the VFS is left in an undefined state.

     - isPaused()

     Returns true if this VFS is paused, else false.

     - [async] unpauseVfs()

     Restores the VFS to an active state after having called
     pauseVfs() on it.  This is a no-op if the VFS is not paused. The
     returned Promise resolves to this object on success. A rejected
     Promise means there was a problem reacquiring the SAH handles
     (possibly because they're in use by another instance or have
     since been removed). Generically speaking, there is no recovery
     strategy for that type of error, but if the problem is simply
     that the OPFS files are locked, then a later attempt to unpause
     it, made after the concurrent instance releases the SAHs, may
     recover from the situation.
  */
  sqlite3.installOpfsSAHPoolVfs = async function(options=Object.create(null)){
    options = Object.assign(Object.create(null), optionDefaults, (options||{}));
    const vfsName = options.name;
    if(options.$testThrowPhase1){
      throw options.$testThrowPhase1;
    }
Changes to ext/wasm/api/sqlite3-worker1-promiser.c-pp.js.
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
  original: sqlite3Worker1Promiser
});

//#if target=es6-module
/**
  When built as a module, we export sqlite3Worker1Promiser.v2()
  instead of sqlite3Worker1Promise() because (A) its interface is more
  conventional for ESM usage and (B) the ESM option export option for
  this API did not exist until v2 was created, so there's no backwards
  incompatibility.
*/
export default sqlite3Worker1Promiser.v2;
//#endif /* target=es6-module */
//#else
/* Built with the omit-oo1 flag. */
//#endif ifnot omit-oo1







|
|







331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
  original: sqlite3Worker1Promiser
});

//#if target=es6-module
/**
  When built as a module, we export sqlite3Worker1Promiser.v2()
  instead of sqlite3Worker1Promise() because (A) its interface is more
  conventional for ESM usage and (B) the ESM export option for this
  API did not exist until v2 was created, so there's no backwards
  incompatibility.
*/
export default sqlite3Worker1Promiser.v2;
//#endif /* target=es6-module */
//#else
/* Built with the omit-oo1 flag. */
//#endif ifnot omit-oo1
Changes to ext/wasm/index.html.
114
115
116
117
118
119
120




121
122
123
124
125
126
127
              sqlite3_vfs OPFS proxy using SharedArrayBuffer and the
              Atomics APIs to regulate communication between the
              synchronous sqlite3_vfs interface and the async OPFS
              impl.
            </li>
            <li><a href='tests/opfs/concurrency/index.html'>OPFS concurrency</a>
              tests using multiple workers.




            </li>
          </ul>
        </li>
        <li><strong>WASMFS</strong>-specific tests which require that
          the WASMFS build is available on this server (it is not by
          default) and that this server emits the COOP/COEP headers.
          <ul>







>
>
>
>







114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
              sqlite3_vfs OPFS proxy using SharedArrayBuffer and the
              Atomics APIs to regulate communication between the
              synchronous sqlite3_vfs interface and the async OPFS
              impl.
            </li>
            <li><a href='tests/opfs/concurrency/index.html'>OPFS concurrency</a>
              tests using multiple workers.
            </li>
            <li><a href='tests/opfs/sahpool/index.html'>OPFS SAHPool cooperative semi-concurrency</a>
              demonstrates usage of the OPFS SAHPool VFS's "pause" feature to coordinate
              access to a database.
            </li>
          </ul>
        </li>
        <li><strong>WASMFS</strong>-specific tests which require that
          the WASMFS build is available on this server (it is not by
          default) and that this server emits the COOP/COEP headers.
          <ul>
Changes to ext/wasm/tester1.c-pp.js.
3185
3186
3187
3188
3189
3190
3191
3192








3193









3194
3195
3196
3197
3198
3199
3200
          .assert(
            'wal'===db.selectValue('pragma journal_mode')
              || wasm.compileOptionUsed('OMIT_WAL')
          );
        db.close();
        T.assert(1 === u1.getFileCount());
        db = new u2.OpfsSAHPoolDb(dbName);
        T.assert(1 === u1.getFileCount());








        db.close();









        const fileNames = u1.getFileNames();
        T.assert(1 === fileNames.length)
          .assert(dbName === fileNames[0])
          .assert(1 === u1.getFileCount())

        if(1){ // test exportFile() and importDb()
          const dbytes = u1.exportFile(dbName);







|
>
>
>
>
>
>
>
>

>
>
>
>
>
>
>
>
>







3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
          .assert(
            'wal'===db.selectValue('pragma journal_mode')
              || wasm.compileOptionUsed('OMIT_WAL')
          );
        db.close();
        T.assert(1 === u1.getFileCount());
        db = new u2.OpfsSAHPoolDb(dbName);
        T.assert(1 === u1.getFileCount())
          .mustThrowMatching(
            ()=>u1.pauseVfs(),
            (err)=>{
              return capi.SQLITE_MISUSE===err.resultCode
                && /^SQLITE_MISUSE: Cannot pause VFS /.test(err.message);
            },
            "Cannot pause VFS with opened db."
          );
        db.close();
        T.assert( u2===u2.pauseVfs() )
          .assert( u2.isPaused() )
          .assert( 0===capi.sqlite3_vfs_find(u2.vfsName) )
          .mustThrowMatching(()=>new u2.OpfsSAHPoolDb(dbName),
                             /.+no such vfs: .+/,
                             "VFS is not available")
          .assert( u2===await u2.unpauseVfs() )
          .assert( u2===await u1.unpauseVfs(), "unpause is a no-op if the VFS is not paused" )
          .assert( 0!==capi.sqlite3_vfs_find(u2.vfsName) );
        const fileNames = u1.getFileNames();
        T.assert(1 === fileNames.length)
          .assert(dbName === fileNames[0])
          .assert(1 === u1.getFileCount())

        if(1){ // test exportFile() and importDb()
          const dbytes = u1.exportFile(dbName);
Added ext/wasm/tests/opfs/sahpool/index.html.






























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
    <link rel="stylesheet" href="../../../common/emscripten.css"/>
    <link rel="stylesheet" href="../../../common/testing.css"/>
    <title>sqlite3 tester: OpfsSAHPool Pausing</title>
    <style></style>
  </head>
  <body><h1 id='color-target'></h1>

    <p>
      This page provides a <em>very basic</em> demonstration of
      "pausing" and "unpausing" the OPFS SAHPool VFS such that
      multiple pages or workers can use it by coordinating which
      handler may have it open at any given time.
    </p>
    <div class='input-wrapper'>
      <input type='checkbox' id='cb-log-reverse'>
      <label for='cb-log-reverse'>Reverse log order?</label>
    </div>
    <div id='test-output'></div>
    <script>(function(){
      document.querySelector('h1').innerHTML =
        document.querySelector('title').innerHTML;
    })();</script>
    <script src="sahpool-pausing.js"></script>
  </body>
</html>
Added ext/wasm/tests/opfs/sahpool/sahpool-pausing.js.














































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
  2025-01-31

  The author disclaims copyright to this source code.  In place of a
  legal notice, here is a blessing:

  *   May you do good and not evil.
  *   May you find forgiveness for yourself and forgive others.
  *   May you share freely, never taking more than you give.

  ***********************************************************************

  These tests are specific to the opfs-sahpool VFS and are limited to
  demonstrating its pause/unpause capabilities.

  Most of this file is infrastructure for displaying results to the
  user. Search for runTests() to find where the work actually starts.
*/
'use strict';
(function(){
  let logClass;

  const mapToString = (v)=>{
    switch(typeof v){
      case 'number': case 'string': case 'boolean':
      case 'undefined': case 'bigint':
        return ''+v;
      default: break;
    }
    if(null===v) return 'null';
    if(v instanceof Error){
      v = {
        message: v.message,
        stack: v.stack,
        errorClass: v.name
      };
    }
    return JSON.stringify(v,undefined,2);
  };
  const normalizeArgs = (args)=>args.map(mapToString);
  const logTarget = document.querySelector('#test-output');
  logClass = function(cssClass,...args){
    const ln = document.createElement('div');
    if(cssClass){
      for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){
        ln.classList.add(c);
      }
    }
    ln.append(document.createTextNode(normalizeArgs(args).join(' ')));
    logTarget.append(ln);
  };
  const cbReverse = document.querySelector('#cb-log-reverse');
  //cbReverse.setAttribute('checked','checked');
  const cbReverseKey = 'tester1:cb-log-reverse';
  const cbReverseIt = ()=>{
    logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
    //localStorage.setItem(cbReverseKey, cbReverse.checked ? 1 : 0);
  };
  cbReverse.addEventListener('change', cbReverseIt, true);
  /*if(localStorage.getItem(cbReverseKey)){
    cbReverse.checked = !!(+localStorage.getItem(cbReverseKey));
    }*/
  cbReverseIt();

  const log = (...args)=>{
    //console.log(...args);
    logClass('',...args);
  }
  const warn = (...args)=>{
    console.warn(...args);
    logClass('warning',...args);
  }
  const error = (...args)=>{
    console.error(...args);
    logClass('error',...args);
  };

  const toss = (...args)=>{
    error(...args);
    throw new Error(args.join(' '));
  };

  const endOfWork = (passed=true)=>{
    const eH = document.querySelector('#color-target');
    const eT = document.querySelector('title');
    if(passed){
      log("End of work chain. If you made it this far, you win.");
      eH.innerText = 'PASS: '+eH.innerText;
      eH.classList.add('tests-pass');
      eT.innerText = 'PASS: '+eT.innerText;
    }else{
      eH.innerText = 'FAIL: '+eH.innerText;
      eH.classList.add('tests-fail');
      eT.innerText = 'FAIL: '+eT.innerText;
    }
  };

  const nextHandlerQueue = [];

  const nextHandler = function(workerId,...msg){
    log(workerId,...msg);
    (nextHandlerQueue.shift())();
  };

  const postThen = function(W, msgType, callback){
    nextHandlerQueue.push(callback);
    W.postMessage({type:msgType});
  };

  /**
     Run a series of operations on an sahpool db spanning two workers.
     This would arguably be more legible with Promises, but creating a
     Promise-based communication channel for this purpose is left as
     an exercise for the reader. An example of such a proxy can be
     found in the SQLite source tree:

     https://sqlite.org/src/file/ext/wasm/api/sqlite3-worker1-promiser.c-pp.js
  */
  const runPyramidOfDoom = function(W1, W2){
    postThen(W1, 'vfs-acquire', function(){
      postThen(W1, 'db-init', function(){
        postThen(W1, 'db-query', function(){
          postThen(W1, 'vfs-pause', function(){
            postThen(W2, 'vfs-acquire', function(){
              postThen(W2, 'db-query', function(){
                postThen(W2, 'vfs-remove', endOfWork);
              });
            });
          });
        });
      });
    });
  };

  const runTests = function(){
    log("Running opfs-sahpool pausing tests...");
    const wjs = 'sahpool-worker.js?sqlite3.dir=../../../jswasm';
    const W1 = new Worker(wjs+'&workerId=w1'),
          W2 = new Worker(wjs+'&workerId=w2');
    W1.workerId = 'w1';
    W2.workerId = 'w2';
    let initCount = 0;
    const onmessage = function({data}){
      //log("onmessage:",data);
      switch(data.type){
        case 'vfs-acquired':
          nextHandler(data.workerId, "VFS acquired");
          break;
        case 'vfs-paused':
          nextHandler(data.workerId, "VFS paused");
          break;
        case 'vfs-unpaused':
          nextHandler(data.workerId, 'VFS unpaused');
          break;
        case 'vfs-removed':
          nextHandler(data.workerId, 'VFS removed');
          break;
        case 'db-inited':
          nextHandler(data.workerId, 'db initialized');
          break;
        case 'query-result':
          nextHandler(data.workerId, 'query result', data.payload);
          break;
        case 'log':
          log(data.workerId, ':', ...data.payload);
          break;
        case 'error':
          error(data.workerId, ':', ...data.payload);
          endOfWork(false);
          break;
        case 'initialized':
          log(data.workerId, ': Worker initialized',...data.payload);
          if( 2===++initCount ){
            runPyramidOfDoom(W1, W2);
          }
          break;
      }
    };
    W1.onmessage = W2.onmessage = onmessage;
  };

  runTests();
})();
Added ext/wasm/tests/opfs/sahpool/sahpool-worker.js.
















































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/*
  2025-01-31

  The author disclaims copyright to this source code.  In place of a
  legal notice, here is a blessing:

  *   May you do good and not evil.
  *   May you find forgiveness for yourself and forgive others.
  *   May you share freely, never taking more than you give.

  ***********************************************************************

  This file is part of sahpool-pausing.js's demonstration of the
  pause/unpause feature of the opfs-sahpool VFS.
*/
const searchParams = new URL(self.location.href).searchParams;
const workerId = searchParams.get('workerId');
const wPost = (type,...args)=>postMessage({type, workerId, payload:args});
const log = (...args)=>wPost('log',...args);
let capi, wasm, S, poolUtil;

const sahPoolConfig = {
  name: 'opfs-sahpool-pausable',
  clearOnInit: false,
  initialCapacity: 3
};

importScripts(searchParams.get('sqlite3.dir') + '/sqlite3.js');

const sqlExec = function(sql){
  const db = new poolUtil.OpfsSAHPoolDb('/my.db');
  try{
    return db.exec(sql);
  }finally{
    db.close();
  }
};

const clog = console.log.bind(console);
globalThis.onmessage = function({data}){
  clog(workerId+": onmessage:",data);
  switch(data.type){
    case 'vfs-acquire':
      if( poolUtil ){
        poolUtil.unpauseVfs().then(()=>wPost('vfs-unpaused'));
      }else{
        S.installOpfsSAHPoolVfs(sahPoolConfig).then(pu=>{
          poolUtil = pu;
          wPost('vfs-acquired');
        });
      }
      break;
    case 'db-init':
      try{
        sqlExec([
          "DROP TABLE IF EXISTS mytable;",
          "CREATE TABLE mytable(a);",
          "INSERT INTO mytable(a) VALUES(11),(22),(33)"
        ]);
        wPost('db-inited');
      }catch(e){
        wPost('error',e.message);
      }
      break;
    case 'db-query': {
      const rc = sqlExec({
        sql: 'select * from mytable order by a',
        rowMode: 'array',
        returnValue: 'resultRows'
      });
      wPost('query-result',rc);
      break;
    }
    case 'vfs-remove':
      poolUtil.removeVfs().then(()=>wPost('vfs-removed'));
      break;
    case 'vfs-pause':
      poolUtil.pauseVfs();
      wPost('vfs-paused');
      break;
  }
};

const hasOpfs = ()=>{
  return globalThis.FileSystemHandle
    && globalThis.FileSystemDirectoryHandle
    && globalThis.FileSystemFileHandle
    && globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle
    && navigator?.storage?.getDirectory;
};
if( !hasOpfs() ){
  wPost('error',"OPFS not detected");
}else{
  globalThis.sqlite3InitModule().then(async function(sqlite3){
    S = sqlite3;
    capi = S.capi;
    wasm = S.wasm;
    log("sqlite3 version:",capi.sqlite3_libversion(),
        capi.sqlite3_sourceid());
    //return sqlite3.installOpfsSAHPoolVfs(sahPoolConfig).then(pu=>poolUtil=pu);
  }).then(()=>{
    wPost('initialized');
  });
}