@powersync/web 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/fe5d98632ac68b2022d7.wasm +0 -0
  2. package/dist/index.umd.js +40347 -0
  3. package/dist/index.umd.js.map +1 -0
  4. package/dist/worker/SharedSyncImplementation.umd.js +4174 -0
  5. package/dist/worker/SharedSyncImplementation.umd.js.map +1 -0
  6. package/dist/worker/WASQLiteDB.umd.js +2780 -0
  7. package/dist/worker/WASQLiteDB.umd.js.map +1 -0
  8. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js +4437 -0
  9. package/dist/worker/node_modules_bson_lib_bson_mjs.umd.js.map +1 -0
  10. package/dist/worker/node_modules_crypto-browserify_index_js.umd.js +33593 -0
  11. package/dist/worker/node_modules_crypto-browserify_index_js.umd.js.map +1 -0
  12. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js +175 -0
  13. package/dist/worker/node_modules_journeyapps_wa-sqlite_dist_wa-sqlite-async_mjs.umd.js.map +1 -0
  14. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js +1926 -0
  15. package/dist/worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js.map +1 -0
  16. package/lib/package.json +36 -13
  17. package/lib/src/db/PowerSyncDatabase.d.ts +17 -5
  18. package/lib/src/db/adapters/AbstractWebSQLOpenFactory.d.ts +2 -2
  19. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.d.ts +3 -2
  20. package/lib/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.js +10 -3
  21. package/lib/src/db/adapters/web-sql-flags.d.ts +13 -2
  22. package/lib/src/db/sync/SharedWebStreamingSyncImplementation.js +32 -9
  23. package/lib/src/db/sync/WebStreamingSyncImplementation.d.ts +9 -2
  24. package/lib/src/worker/db/WASQLiteDB.worker.d.ts +4 -1
  25. package/lib/src/worker/db/WASQLiteDB.worker.js +60 -1
  26. package/lib/src/worker/db/open-worker-database.d.ts +4 -2
  27. package/lib/src/worker/db/open-worker-database.js +40 -20
  28. package/lib/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +36 -13
  30. package/lib/src/worker/db/SharedWASQLiteDB.worker.d.ts +0 -1
  31. package/lib/src/worker/db/SharedWASQLiteDB.worker.js +0 -49
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker/node_modules_journeyapps_wa-sqlite_src_examples_IDBBatchAtomicVFS_js.umd.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC3KA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;AC52BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;AC3PA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://sdk_web/../../node_modules/@journeyapps/wa-sqlite/src/VFS.js","webpack://sdk_web/../../node_modules/@journeyapps/wa-sqlite/src/examples/IDBBatchAtomicVFS.js","webpack://sdk_web/../../node_modules/@journeyapps/wa-sqlite/src/examples/IDBContext.js","webpack://sdk_web/../../node_modules/@journeyapps/wa-sqlite/src/examples/WebLocks.js"],"sourcesContent":["// Copyright 2022 Roy T. Hashimoto. All Rights Reserved.\nimport * as VFS from './sqlite-constants.js';\nexport * from './sqlite-constants.js';\n\n// Base class for a VFS.\nexport class Base {\n mxPathName = 64;\n\n /**\n * @param {number} fileId \n * @returns {number}\n */\n xClose(fileId) {\n return VFS.SQLITE_IOERR;\n }\n\n /**\n * @param {number} fileId \n * @param {Uint8Array} pData \n * @param {number} iOffset\n * @returns {number}\n */\n xRead(fileId, pData, iOffset) {\n return VFS.SQLITE_IOERR;\n }\n\n /**\n * @param {number} fileId \n * @param {Uint8Array} pData \n * @param {number} iOffset\n * @returns {number}\n */\n xWrite(fileId, pData, iOffset) {\n return VFS.SQLITE_IOERR;\n }\n\n /**\n * @param {number} fileId \n * @param {number} iSize \n * @returns {number}\n */\n xTruncate(fileId, iSize) {\n return VFS.SQLITE_IOERR;\n }\n\n /**\n * @param {number} fileId \n * @param {*} flags \n * @returns {number}\n */\n xSync(fileId, flags) {\n return VFS.SQLITE_OK;\n }\n\n /**\n * @param {number} fileId \n * @param {DataView} pSize64 \n * @returns {number}\n */\n xFileSize(fileId, pSize64) {\n return VFS.SQLITE_IOERR;\n }\n\n /**\n * @param {number} fileId \n * @param {number} flags \n * @returns {number}\n */\n xLock(fileId, flags) {\n return VFS.SQLITE_OK;\n }\n\n /**\n * @param {number} fileId \n * @param {number} flags \n * @returns {number}\n */\n xUnlock(fileId, flags) {\n return VFS.SQLITE_OK;\n }\n\n /**\n * @param {number} fileId \n * @param {DataView} pResOut \n * @returns {number}\n */\n xCheckReservedLock(fileId, pResOut) {\n pResOut.setInt32(0, 0, true);\n return VFS.SQLITE_OK;\n }\n\n /**\n * @param {number} fileId \n * @param {number} op \n * @param {DataView} pArg \n * @returns {number}\n */\n xFileControl(fileId, op, pArg) {\n return VFS.SQLITE_NOTFOUND;\n }\n\n /**\n * @param {number} fileId \n * @returns {number}\n */\n xSectorSize(fileId) {\n return 512;\n }\n\n /**\n * @param {number} fileId \n * @returns {number}\n */\n xDeviceCharacteristics(fileId) {\n return 0;\n }\n\n /**\n * @param {string?} name \n * @param {number} fileId \n * @param {number} flags \n * @param {DataView} pOutFlags \n * @returns {number}\n */\n xOpen(name, fileId, flags, pOutFlags) {\n return VFS.SQLITE_CANTOPEN;\n }\n\n /**\n * @param {string} name \n * @param {number} syncDir \n * @returns {number}\n */\n xDelete(name, syncDir) {\n return VFS.SQLITE_IOERR;\n }\n\n /**\n * @param {string} name \n * @param {number} flags \n * @param {DataView} pResOut \n * @returns {number}\n */\n xAccess(name, flags, pResOut) {\n return VFS.SQLITE_IOERR;\n }\n\n /**\n * Handle asynchronous operation. This implementation will be overriden on\n * registration by an Asyncify build.\n * @param {function(): Promise<number>} f \n * @returns {number}\n */\n handleAsync(f) {\n // This default implementation deliberately does not match the\n // declared signature. It will be used in testing VFS classes\n // separately from SQLite. This will work acceptably for methods\n // that simply return the handleAsync() result without using it.\n // @ts-ignore\n return f();\n }\n}\n\nexport const FILE_TYPE_MASK = [\n VFS.SQLITE_OPEN_MAIN_DB,\n VFS.SQLITE_OPEN_MAIN_JOURNAL,\n VFS.SQLITE_OPEN_TEMP_DB,\n VFS.SQLITE_OPEN_TEMP_JOURNAL,\n VFS.SQLITE_OPEN_TRANSIENT_DB,\n VFS.SQLITE_OPEN_SUBJOURNAL,\n VFS.SQLITE_OPEN_SUPER_JOURNAL\n].reduce((mask, element) => mask | element);","// Copyright 2022 Roy T. Hashimoto. All Rights Reserved.\nimport * as VFS from '../VFS.js';\nimport { WebLocksExclusive as WebLocks } from './WebLocks.js';\nimport { IDBContext } from './IDBContext.js';\n\nconst SECTOR_SIZE = 512;\nconst MAX_TASK_MILLIS = 3000;\n\n/**\n * @typedef VFSOptions\n * @property {\"default\"|\"strict\"|\"relaxed\"} [durability]\n * @property {\"deferred\"|\"manual\"} [purge]\n * @property {number} [purgeAtLeast]\n */\n\n/** @type {VFSOptions} */\nconst DEFAULT_OPTIONS = {\n durability: \"default\",\n purge: \"deferred\",\n purgeAtLeast: 16\n};\n\nfunction log(...args) {\n // console.debug(...args);\n}\n\n/**\n * @typedef FileBlock IndexedDB object with key [path, offset, version]\n * @property {string} path\n * @property {number} offset negative of position in file\n * @property {number} version\n * @property {Uint8Array} data\n *\n * @property {number} [fileSize] Only present on block 0\n*/\n\n/**\n * @typedef OpenedFileEntry\n * @property {string} path\n * @property {number} flags\n * @property {FileBlock} block0\n * @property {boolean} isMetadataChanged\n * @property {WebLocks} locks\n * \n * @property {Set<number>} [changedPages]\n * @property {boolean} [overwrite]\n */\n\n// This sample VFS stores optionally versioned writes to IndexedDB, which\n// it uses with the SQLite xFileControl() batch atomic write feature.\nexport class IDBBatchAtomicVFS extends VFS.Base {\n #options;\n /** @type {Map<number, OpenedFileEntry>} */ #mapIdToFile = new Map();\n\n /** @type {IDBContext} */ #idb;\n /** @type {Set<string>} */ #pendingPurges = new Set();\n\n #taskTimestamp = performance.now();\n #pendingAsync = new Set();\n\n // Asyncify can grow WebAssembly memory during an asynchronous call.\n // If this happens, then any array buffer arguments will be detached.\n // The workaround is when finding a detached buffer, set this handler\n // function to process the new buffer outside handlerAsync().\n #growthHandler = null;\n\n constructor(idbDatabaseName = 'wa-sqlite', options = DEFAULT_OPTIONS) {\n super();\n this.name = idbDatabaseName;\n this.#options = Object.assign({}, DEFAULT_OPTIONS, options);\n this.#idb = new IDBContext(openDatabase(idbDatabaseName), {\n durability: this.#options.durability\n });\n }\n\n async close() {\n for (const fileId of this.#mapIdToFile.keys()) {\n await this.xClose(fileId);\n }\n\n await this.#idb?.close();\n this.#idb = null;\n }\n\n /**\n * @param {string?} name \n * @param {number} fileId \n * @param {number} flags \n * @param {DataView} pOutFlags \n * @returns {number}\n */\n xOpen(name, fileId, flags, pOutFlags) {\n const result = this.handleAsync(async () => {\n if (name === null) name = `null_${fileId}`;\n log(`xOpen ${name} 0x${fileId.toString(16)} 0x${flags.toString(16)}`);\n\n try {\n // Filenames can be URLs, possibly with query parameters.\n const url = new URL(name, 'http://localhost/');\n /** @type {OpenedFileEntry} */ const file = {\n path: url.pathname,\n flags,\n block0: null,\n isMetadataChanged: true,\n locks: new WebLocks(url.pathname)\n };\n this.#mapIdToFile.set(fileId, file);\n\n // Read the first block, which also contains the file metadata.\n await this.#idb.run('readwrite', async ({blocks}) => {\n file.block0 = await blocks.get(this.#bound(file, 0));\n if (!file.block0) {\n if (flags & VFS.SQLITE_OPEN_CREATE) {\n file.block0 = {\n path: file.path,\n offset: 0,\n version: 0,\n data: new Uint8Array(0),\n fileSize: 0\n };\n blocks.put(file.block0);\n } else {\n throw new Error(`file not found: ${file.path}`);\n }\n }\n });\n\n // @ts-ignore\n if (pOutFlags.buffer.detached || !pOutFlags.buffer.byteLength) {\n pOutFlags = new DataView(new ArrayBuffer(4));\n this.#growthHandler = (pOutFlagsNew) => {\n pOutFlagsNew.setInt32(0, pOutFlags.getInt32(0, true), true);\n };\n }\n pOutFlags.setInt32(0, flags & VFS.SQLITE_OPEN_READONLY, true);\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_CANTOPEN;\n }\n });\n\n this.#growthHandler?.(pOutFlags);\n this.#growthHandler = null;\n return result;\n }\n\n /**\n * @param {number} fileId \n * @returns {number}\n */\n xClose(fileId) {\n return this.handleAsync(async () => {\n try {\n const file = this.#mapIdToFile.get(fileId);\n if (file) {\n log(`xClose ${file.path}`);\n\n this.#mapIdToFile.delete(fileId);\n if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {\n this.#idb.run('readwrite', ({blocks}) => {\n blocks.delete(IDBKeyRange.bound([file.path], [file.path, []]));\n });\n }\n }\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n }\n\n /**\n * @param {number} fileId \n * @param {Uint8Array} pData \n * @param {number} iOffset\n * @returns {number}\n */\n xRead(fileId, pData, iOffset) {\n const byteLength = pData.byteLength;\n const result = this.handleAsync(async () => {\n const file = this.#mapIdToFile.get(fileId);\n log(`xRead ${file.path} ${pData.byteLength} ${iOffset}`);\n\n try {\n // Read as many blocks as necessary to satisfy the read request.\n // Usually a read fits within a single write but there is at least\n // one case - rollback after journal spill - where reads cross\n // write boundaries so we have to allow for that.\n const result = await this.#idb.run('readonly', async ({blocks}) => {\n // @ts-ignore\n if (pData.buffer.detached || !pData.buffer.byteLength) {\n // WebAssembly memory has grown, invalidating our buffer. Use\n // a temporary buffer and copy after this asynchronous call\n // completes.\n pData = new Uint8Array(byteLength);\n this.#growthHandler = (pDataNew) => pDataNew.set(pData);\n }\n\n let pDataOffset = 0;\n while (pDataOffset < pData.byteLength) {\n // Fetch the IndexedDB block for this file location.\n const fileOffset = iOffset + pDataOffset;\n /** @type {FileBlock} */\n const block = fileOffset < file.block0.data.byteLength ?\n file.block0 :\n await blocks.get(this.#bound(file, -fileOffset));\n\n if (!block || block.data.byteLength - block.offset <= fileOffset) {\n pData.fill(0, pDataOffset);\n return VFS.SQLITE_IOERR_SHORT_READ;\n }\n\n const buffer = pData.subarray(pDataOffset);\n const blockOffset = fileOffset + block.offset;\n const nBytesToCopy = Math.min(\n Math.max(block.data.byteLength - blockOffset, 0), // source bytes\n buffer.byteLength); // destination bytes\n buffer.set(block.data.subarray(blockOffset, blockOffset + nBytesToCopy));\n pDataOffset += nBytesToCopy;\n }\n return VFS.SQLITE_OK;\n });\n return result;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n\n this.#growthHandler?.(pData);\n this.#growthHandler = null;\n return result;\n }\n\n /**\n * @param {number} fileId \n * @param {Uint8Array} pData \n * @param {number} iOffset\n * @returns {number}\n */\n xWrite(fileId, pData, iOffset) {\n // Handle asynchronously every MAX_TASK_MILLIS milliseconds. This is\n // tricky because Asyncify calls asynchronous methods twice: once\n // to initiate the call and unwinds the stack, then rewinds the\n // stack and calls again to retrieve the completed result.\n const rewound = this.#pendingAsync.has(fileId);\n if (rewound || performance.now() - this.#taskTimestamp > MAX_TASK_MILLIS) {\n const result = this.handleAsync(async () => {\n if (this.handleAsync !== super.handleAsync) {\n this.#pendingAsync.add(fileId);\n }\n await new Promise(resolve => setTimeout(resolve));\n\n const result = this.#xWriteHelper(fileId, pData.slice(), iOffset);\n this.#taskTimestamp = performance.now();\n return result;\n });\n\n if (rewound) this.#pendingAsync.delete(fileId);\n return result;\n }\n return this.#xWriteHelper(fileId, pData, iOffset);\n }\n\n /**\n * @param {number} fileId \n * @param {Uint8Array} pData \n * @param {number} iOffset\n * @returns {number}\n */\n #xWriteHelper(fileId, pData, iOffset) {\n const file = this.#mapIdToFile.get(fileId);\n log(`xWrite ${file.path} ${pData.byteLength} ${iOffset}`);\n\n try {\n // Update file size if appending.\n const prevFileSize = file.block0.fileSize;\n if (file.block0.fileSize < iOffset + pData.byteLength) {\n file.block0.fileSize = iOffset + pData.byteLength;\n file.isMetadataChanged = true;\n }\n\n // Convert the write directly into an IndexedDB object. Our assumption\n // is that SQLite will only overwrite data with an xWrite of the same\n // offset and size unless the database page size changes, except when\n // changing database page size which is handled by #reblockIfNeeded().\n const block = iOffset === 0 ? file.block0 : {\n path: file.path,\n offset: -iOffset,\n version: file.block0.version,\n data: null\n };\n block.data = pData.slice();\n\n if (file.changedPages) {\n // This write is part of a batch atomic write. All writes in the\n // batch have a new version, so update the changed list to allow\n // old versions to be eventually deleted.\n if (prevFileSize === file.block0.fileSize) {\n file.changedPages.add(-iOffset);\n }\n\n // Defer writing block 0 to IndexedDB until batch commit.\n if (iOffset !== 0) {\n this.#idb.run('readwrite', ({blocks}) => blocks.put(block));\n }\n } else {\n // Not a batch atomic write so write through.\n this.#idb.run('readwrite', ({blocks}) => blocks.put(block));\n }\n\n // Clear dirty flag if page 0 was written.\n file.isMetadataChanged = iOffset === 0 ? false : file.isMetadataChanged;\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n }\n\n /**\n * @param {number} fileId \n * @param {number} iSize \n * @returns {number}\n */\n xTruncate(fileId, iSize) {\n const file = this.#mapIdToFile.get(fileId);\n log(`xTruncate ${file.path} ${iSize}`);\n\n try {\n Object.assign(file.block0, {\n fileSize: iSize,\n data: file.block0.data.slice(0, iSize)\n });\n\n // Delete all blocks beyond the file size and update metadata.\n // This is never called within a transaction.\n const block0 = Object.assign({}, file.block0);\n this.#idb.run('readwrite', ({blocks})=> {\n blocks.delete(this.#bound(file, -Infinity, -iSize));\n blocks.put(block0);\n });\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n }\n\n /**\n * @param {number} fileId \n * @param {number} flags \n * @returns {number}\n */\n xSync(fileId, flags) {\n // Skip IndexedDB sync if durability is relaxed and the last\n // sync was recent enough.\n const rewound = this.#pendingAsync.has(fileId);\n if (rewound || this.#options.durability !== 'relaxed' ||\n performance.now() - this.#taskTimestamp > MAX_TASK_MILLIS) {\n const result = this.handleAsync(async () => {\n if (this.handleAsync !== super.handleAsync) {\n this.#pendingAsync.add(fileId);\n }\n\n const result = await this.#xSyncHelper(fileId, flags);\n this.#taskTimestamp = performance.now();\n return result;\n });\n\n if (rewound) this.#pendingAsync.delete(fileId);\n return result;\n }\n\n const file = this.#mapIdToFile.get(fileId);\n log(`xSync ${file.path} ${flags}`);\n return VFS.SQLITE_OK;\n }\n\n /**\n * @param {number} fileId \n * @param {number} flags \n * @returns {Promise<number>}\n */\n async #xSyncHelper(fileId, flags) {\n const file = this.#mapIdToFile.get(fileId);\n log(`xSync ${file.path} ${flags}`);\n try {\n if (file.isMetadataChanged) {\n // Metadata has changed so write block 0 to IndexedDB.\n this.#idb.run('readwrite', async ({blocks}) => {\n await blocks.put(file.block0);\n }); \n file.isMetadataChanged = false;\n }\n await this.#idb.sync();\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n return VFS.SQLITE_OK;\n }\n\n /**\n * @param {number} fileId \n * @param {DataView} pSize64 \n * @returns {number}\n */\n xFileSize(fileId, pSize64) {\n const file = this.#mapIdToFile.get(fileId);\n log(`xFileSize ${file.path}`);\n\n pSize64.setBigInt64(0, BigInt(file.block0.fileSize), true)\n return VFS.SQLITE_OK;\n }\n\n /**\n * @param {number} fileId \n * @param {number} flags \n * @returns {number}\n */\n xLock(fileId, flags) {\n return this.handleAsync(async () => {\n const file = this.#mapIdToFile.get(fileId);\n log(`xLock ${file.path} ${flags}`);\n\n try {\n // Acquire the lock.\n const result = await file.locks.lock(flags);\n if (result === VFS.SQLITE_OK && file.locks.state === VFS.SQLITE_LOCK_SHARED) {\n // Update block 0 in case another connection changed it.\n file.block0 = await this.#idb.run('readonly', ({blocks}) => {\n return blocks.get(this.#bound(file, 0));\n });\n }\n return result;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n }\n\n /**\n * @param {number} fileId \n * @param {number} flags \n * @returns {number}\n */\n xUnlock(fileId, flags) {\n return this.handleAsync(async () => {\n const file = this.#mapIdToFile.get(fileId);\n log(`xUnlock ${file.path} ${flags}`);\n \n try {\n return file.locks.unlock(flags);\n } catch(e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n }\n\n /**\n * @param {number} fileId \n * @param {DataView} pResOut \n * @returns {number}\n */\n xCheckReservedLock(fileId, pResOut) {\n const result = this.handleAsync(async () => {\n const file = this.#mapIdToFile.get(fileId);\n log(`xCheckReservedLock ${file.path}`);\n\n const isReserved = await file.locks.isSomewhereReserved();\n function setOutput(pResOut) {\n };\n\n // @ts-ignore\n if (pResOut.buffer.detached || !pResOut.buffer.byteLength) {\n pResOut = new DataView(new ArrayBuffer(4));\n this.#growthHandler = (pResOutNew) => {\n pResOutNew.setInt32(0, pResOut.getInt32(0, true), true);\n };\n }\n pResOut.setInt32(0, isReserved ? 1 : 0, true);\n return VFS.SQLITE_OK;\n });\n\n this.#growthHandler?.(pResOut);\n this.#growthHandler = null;\n return result;\n }\n\n /**\n * @param {number} fileId \n * @returns {number}\n */\n xSectorSize(fileId) {\n log('xSectorSize');\n return SECTOR_SIZE;\n }\n\n /**\n * @param {number} fileId \n * @returns {number}\n */\n xDeviceCharacteristics(fileId) {\n log('xDeviceCharacteristics');\n return VFS.SQLITE_IOCAP_BATCH_ATOMIC |\n VFS.SQLITE_IOCAP_SAFE_APPEND |\n VFS.SQLITE_IOCAP_SEQUENTIAL |\n VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;\n }\n\n /**\n * @param {number} fileId \n * @param {number} op \n * @param {DataView} pArg \n * @returns {number}\n */\n xFileControl(fileId, op, pArg) {\n const file = this.#mapIdToFile.get(fileId);\n log(`xFileControl ${file.path} ${op}`);\n\n switch (op) {\n case 11: //SQLITE_FCNTL_OVERWRITE\n // This called on VACUUM. Set a flag so we know whether to check\n // later if the page size changed.\n file.overwrite = true;\n return VFS.SQLITE_OK;\n\n case 21: // SQLITE_FCNTL_SYNC\n // This is called at the end of each database transaction, whether\n // it is batch atomic or not. Handle page size changes here.\n if (file.overwrite) {\n // As an optimization we only check for and handle a page file\n // changes if we know a VACUUM has been done because handleAsync()\n // has to unwind and rewind the stack. We must be sure to follow\n // the same conditional path in both calls.\n try {\n return this.handleAsync(async () => {\n await this.#reblockIfNeeded(file);\n return VFS.SQLITE_OK;\n });\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n }\n\n if (file.isMetadataChanged) {\n // Metadata has changed so write block 0 to IndexedDB.\n try {\n this.#idb.run('readwrite', async ({blocks}) => {\n await blocks.put(file.block0);\n });\n file.isMetadataChanged = false;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n }\n return VFS.SQLITE_OK;\n\n case 22: // SQLITE_FCNTL_COMMIT_PHASETWO\n // This is called after a commit is completed.\n file.overwrite = false;\n return VFS.SQLITE_OK;\n\n case 31: // SQLITE_FCNTL_BEGIN_ATOMIC_WRITE\n return this.handleAsync(async () => {\n try {\n // Prepare a new version for IndexedDB blocks.\n file.block0.version--;\n file.changedPages = new Set();\n\n // Clear blocks from abandoned transactions that would conflict\n // with the new transaction.\n this.#idb.run('readwrite', async ({blocks}) => {\n const keys = await blocks.index('version').getAllKeys(IDBKeyRange.bound(\n [file.path],\n [file.path, file.block0.version]));\n for (const key of keys) {\n blocks.delete(key);\n }\n });\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n\n case 32: // SQLITE_FCNTL_COMMIT_ATOMIC_WRITE\n try {\n const block0 = Object.assign({}, file.block0);\n block0.data = block0.data.slice();\n const changedPages = file.changedPages;\n file.changedPages = null;\n file.isMetadataChanged = false;\n this.#idb.run('readwrite', async ({blocks})=> {\n // Write block 0 to commit the new version.\n blocks.put(block0);\n\n // Blocks to purge are saved in a special IndexedDB object with\n // an \"index\" of \"purge\". Add pages changed by this transaction.\n const purgeBlock = await blocks.get([file.path, 'purge', 0]) ?? {\n path: file.path,\n offset: 'purge',\n version: 0,\n data: new Map(),\n count: 0\n };\n\n purgeBlock.count += changedPages.size;\n for (const pageIndex of changedPages) {\n purgeBlock.data.set(pageIndex, block0.version);\n }\n\n blocks.put(purgeBlock);\n this.#maybePurge(file.path, purgeBlock.count);\n });\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n\n case 33: // SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE\n return this.handleAsync(async () => {\n try {\n // Restore original state. Objects for the abandoned version will\n // be left in IndexedDB to be removed by the next atomic write\n // transaction.\n file.changedPages = null;\n file.isMetadataChanged = false;\n file.block0 = await this.#idb.run('readonly', ({blocks}) => {\n return blocks.get([file.path, 0, file.block0.version + 1]);\n });\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n\n default:\n return VFS.SQLITE_NOTFOUND;\n }\n }\n\n /**\n * @param {string} name \n * @param {number} flags \n * @param {DataView} pResOut \n * @returns {number}\n */\n xAccess(name, flags, pResOut) {\n const result = this.handleAsync(async () => {\n try {\n const path = new URL(name, 'file://localhost/').pathname;\n log(`xAccess ${path} ${flags}`);\n\n // Check if block 0 exists.\n const key = await this.#idb.run('readonly', ({blocks}) => {\n return blocks.getKey(this.#bound({path}, 0));\n });\n\n // @ts-ignore\n if (pResOut.buffer.detached || !pResOut.buffer.byteLength) {\n pResOut = new DataView(new ArrayBuffer(4));\n this.#growthHandler = (pResOutNew) => {\n pResOutNew.setInt32(0, pResOut.getInt32(0, true), true);\n }\n }\n pResOut.setInt32(0, key ? 1 : 0, true);\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n\n this.#growthHandler?.(pResOut);\n this.#growthHandler = null;\n return result;\n }\n\n /**\n * @param {string} name \n * @param {number} syncDir \n * @returns {number}\n */\n xDelete(name, syncDir) {\n return this.handleAsync(async () => {\n const path = new URL(name, 'file://localhost/').pathname;\n log(`xDelete ${path} ${syncDir}`);\n\n try {\n this.#idb.run('readwrite', ({blocks}) => {\n return blocks.delete(IDBKeyRange.bound([path], [path, []]));\n });\n if (syncDir) {\n await this.#idb.sync();\n }\n return VFS.SQLITE_OK;\n } catch (e) {\n console.error(e);\n return VFS.SQLITE_IOERR;\n }\n });\n }\n\n /**\n * Purge obsolete blocks from a database file.\n * @param {string} path \n */\n async purge(path) {\n const start = Date.now();\n await this.#idb.run('readwrite', async ({blocks}) => {\n const purgeBlock = await blocks.get([path, 'purge', 0]);\n if (purgeBlock) {\n for (const [pageOffset, version] of purgeBlock.data) {\n blocks.delete(IDBKeyRange.bound(\n [path, pageOffset, version],\n [path, pageOffset, Infinity],\n true, false));\n }\n await blocks.delete([path, 'purge', 0]);\n }\n log(`purge ${path} ${purgeBlock?.data.size ?? 0} pages in ${Date.now() - start} ms`);\n });\n }\n\n /**\n * Conditionally schedule a purge task.\n * @param {string} path \n * @param {number} nPages \n */\n #maybePurge(path, nPages) {\n if (this.#options.purge === 'manual' ||\n this.#pendingPurges.has(path) ||\n nPages < this.#options.purgeAtLeast) {\n // No purge needed.\n return;\n }\n \n if (globalThis.requestIdleCallback) {\n globalThis.requestIdleCallback(() => {\n this.purge(path);\n this.#pendingPurges.delete(path)\n });\n } else {\n setTimeout(() => {\n this.purge(path);\n this.#pendingPurges.delete(path)\n });\n }\n this.#pendingPurges.add(path);\n }\n\n #bound(file, begin, end = 0) {\n // Fetch newest block 0. For other blocks, use block 0 version.\n const version = !begin || -begin < file.block0.data.length ?\n -Infinity :\n file.block0.version;\n return IDBKeyRange.bound(\n [file.path, begin, version],\n [file.path, end, Infinity]);\n }\n\n // The database page size can be changed with PRAGMA page_size and VACUUM.\n // The updated file will be overwritten with a regular transaction using\n // the old page size. After that it will be read and written using the\n // new page size, so the IndexedDB objects must be combined or split\n // appropriately.\n async #reblockIfNeeded(file) {\n const oldPageSize = file.block0.data.length;\n if (oldPageSize < 18) return; // no page size defined\n\n const view = new DataView(file.block0.data.buffer, file.block0.data.byteOffset);\n let newPageSize = view.getUint16(16);\n if (newPageSize === 1) newPageSize = 65536;\n if (newPageSize === oldPageSize) return; // no page size change\n\n const maxPageSize = Math.max(oldPageSize, newPageSize);\n const nOldPages = maxPageSize / oldPageSize;\n const nNewPages = maxPageSize / newPageSize;\n\n const newPageCount = view.getUint32(28);\n const fileSize = newPageCount * newPageSize;\n\n const version = file.block0.version;\n await this.#idb.run('readwrite', async ({blocks}) => {\n // When the block size changes, the entire file is rewritten. Delete\n // all blocks older than block 0 to leave a single version at every\n // offset.\n const keys = await blocks.index('version').getAllKeys(IDBKeyRange.bound(\n [file.path, version + 1],\n [file.path, Infinity]\n ));\n for (const key of keys) {\n blocks.delete(key);\n }\n blocks.delete([file.path, 'purge', 0]);\n\n // Do the conversion in chunks of the larger of the page sizes.\n for (let iOffset = 0; iOffset < fileSize; iOffset += maxPageSize) {\n // Fetch nOldPages. They can be fetched in one request because\n // there is now a single version in the file.\n const oldPages = await blocks.getAll(\n IDBKeyRange.lowerBound([file.path, -(iOffset + maxPageSize), Infinity]),\n nOldPages);\n for (const oldPage of oldPages) {\n blocks.delete([oldPage.path, oldPage.offset, oldPage.version]);\n }\n\n // Convert to new pages.\n if (nNewPages === 1) {\n // Combine nOldPages old pages into a new page.\n const buffer = new Uint8Array(newPageSize);\n for (const oldPage of oldPages) {\n buffer.set(oldPage.data, -(iOffset + oldPage.offset));\n }\n const newPage = {\n path: file.path,\n offset: -iOffset,\n version,\n data: buffer\n };\n if (newPage.offset === 0) {\n newPage.fileSize = fileSize;\n file.block0 = newPage;\n }\n blocks.put(newPage);\n } else {\n // Split an old page into nNewPages new pages.\n const oldPage = oldPages[0];\n for (let i = 0; i < nNewPages; ++i) {\n const offset = -(iOffset + i * newPageSize);\n if (-offset >= fileSize) break;\n const newPage = {\n path: oldPage.path,\n offset,\n version,\n data: oldPage.data.subarray(i * newPageSize, (i + 1) * newPageSize)\n }\n if (newPage.offset === 0) {\n newPage.fileSize = fileSize;\n file.block0 = newPage;\n }\n blocks.put(newPage);\n }\n }\n }\n });\n }\n}\n\nfunction openDatabase(idbDatabaseName) {\n return new Promise((resolve, reject) => {\n const request = globalThis.indexedDB.open(idbDatabaseName, 5);\n request.addEventListener('upgradeneeded', function() {\n const blocks = request.result.createObjectStore('blocks', {\n keyPath: ['path', 'offset', 'version']\n });\n blocks.createIndex('version', ['path', 'version']);\n });\n request.addEventListener('success', () => {\n resolve(request.result);\n });\n request.addEventListener('error', () => {\n reject(request.error);\n });\n });\n}","// Copyright 2022 Roy T. Hashimoto. All Rights Reserved.\n\n// IndexedDB transactions older than this will be replaced.\nconst MAX_TRANSACTION_LIFETIME_MILLIS = 5_000;\n\n// For debugging.\nlet nextTxId = 0;\nconst mapTxToId = new WeakMap();\nfunction log(...args) {\n // console.debug(...args);\n}\n\n// This class manages IDBTransaction and IDBRequest instances. It tries\n// to reuse transactions to minimize transaction overhead.\nexport class IDBContext {\n /** @type {IDBDatabase} */ #db;\n /** @type {Promise<IDBDatabase>} */ #dbReady;\n #txOptions;\n\n /** @type {IDBTransaction} */ #tx = null;\n #txTimestamp = 0;\n #runChain = Promise.resolve();\n #putChain = Promise.resolve();\n\n /**\n * @param {IDBDatabase|Promise<IDBDatabase>} idbDatabase\n */\n constructor(idbDatabase, txOptions = { durability: 'default' }) {\n this.#dbReady = Promise.resolve(idbDatabase).then(db => this.#db = db);\n this.#txOptions = txOptions;\n }\n\n async close() {\n const db = this.#db ?? await this.#dbReady;\n await this.#runChain;\n await this.sync();\n db.close();\n }\n \n /**\n * Run a function with the provided object stores. The function\n * should be idempotent in case it is passed an expired transaction.\n * @param {IDBTransactionMode} mode\n * @param {(stores: Object.<string, ObjectStore>) => any} f \n */\n async run(mode, f) {\n // Ensure that functions run sequentially.\n const result = this.#runChain.then(() => this.#run(mode, f));\n this.#runChain = result.catch(() => {});\n return result;\n }\n\n /**\n * @param {IDBTransactionMode} mode\n * @param {(stores: Object.<string, ObjectStore>) => any} f \n * @returns \n */\n async #run(mode, f) {\n const db = this.#db ?? await this.#dbReady;\n if (mode === 'readwrite' && this.#tx?.mode === 'readonly') {\n // Mode requires a new transaction.\n this.#tx = null;\n } else if (performance.now() - this.#txTimestamp > MAX_TRANSACTION_LIFETIME_MILLIS) {\n // Chrome times out transactions after 60 seconds so refresh preemptively.\n try {\n this.#tx?.commit();\n } catch (e) {\n // Explicit commit can fail but this can be ignored if it will\n // auto-commit anyway.\n if (e.name !== 'InvalidStateError') throw e;\n }\n\n // Skip to the next task to allow processing.\n await new Promise(resolve => setTimeout(resolve));\n this.#tx = null;\n }\n\n // Run the user function with a retry in case the transaction is invalid.\n for (let i = 0; i < 2; ++i) {\n if (!this.#tx) {\n // @ts-ignore\n this.#tx = db.transaction(db.objectStoreNames, mode, this.#txOptions);\n const timestamp = this.#txTimestamp = performance.now();\n\n // Chain the result of every transaction. If any transaction is\n // aborted then the next sync() call will throw.\n this.#putChain = this.#putChain.then(() => {\n return new Promise((resolve, reject) => {\n this.#tx.addEventListener('complete', event => {\n resolve();\n if (this.#tx === event.target) {\n this.#tx = null;\n }\n log(`transaction ${mapTxToId.get(event.target)} complete`);\n });\n this.#tx.addEventListener('abort', event => {\n console.warn('tx abort', (performance.now() - timestamp)/1000);\n // @ts-ignore\n const e = event.target.error;\n reject(e);\n if (this.#tx === event.target) {\n this.#tx = null;\n }\n log(`transaction ${mapTxToId.get(event.target)} aborted`, e);\n });\n });\n });\n\n log(`new transaction ${nextTxId} ${mode}`);\n mapTxToId.set(this.#tx, nextTxId++);\n }\n\n try {\n const stores = Object.fromEntries(Array.from(db.objectStoreNames, name => {\n return [name, new ObjectStore(this.#tx.objectStore(name))];\n }));\n return await f(stores);\n } catch (e) {\n this.#tx = null;\n if (i) throw e;\n // console.warn('retrying with new transaction');\n }\n }\n }\n\n async sync() {\n // Wait until all transactions since the previous sync have committed.\n // Throw if any transaction failed.\n await this.#runChain;\n await this.#putChain;\n this.#putChain = Promise.resolve();\n }\n}\n\n/**\n * Helper to convert IDBRequest to Promise.\n * @param {IDBRequest} request \n * @returns {Promise}\n */\nfunction wrapRequest(request) {\n return new Promise((resolve, reject) => {\n request.addEventListener('success', () => resolve(request.result));\n request.addEventListener('error', () => reject(request.error));\n });\n}\n\n// IDBObjectStore wrapper passed to IDBContext run functions.\nclass ObjectStore {\n #objectStore;\n\n /**\n * @param {IDBObjectStore} objectStore \n */\n constructor(objectStore) {\n this.#objectStore = objectStore;\n }\n\n /**\n * @param {IDBValidKey|IDBKeyRange} query \n * @returns {Promise}\n */\n get(query) {\n log(`get ${this.#objectStore.name}`, query);\n const request = this.#objectStore.get(query);\n return wrapRequest(request);\n }\n\n /**\n * @param {IDBValidKey|IDBKeyRange} query \n * @param {number} [count]\n * @returns {Promise}\n */\n getAll(query, count) {\n log(`getAll ${this.#objectStore.name}`, query, count);\n const request = this.#objectStore.getAll(query, count);\n return wrapRequest(request);\n }\n\n /**\n * @param {IDBValidKey|IDBKeyRange} query \n * @returns {Promise<IDBValidKey>}\n */\n getKey(query) {\n log(`getKey ${this.#objectStore.name}`, query);\n const request = this.#objectStore.getKey(query);\n return wrapRequest(request);\n }\n\n /**\n * @param {IDBValidKey|IDBKeyRange} query \n * @param {number} [count]\n * @returns {Promise}\n */\n getAllKeys(query, count) {\n log(`getAllKeys ${this.#objectStore.name}`, query, count);\n const request = this.#objectStore.getAllKeys(query, count);\n return wrapRequest(request);\n }\n\n /**\n * @param {any} value\n * @param {IDBValidKey} [key] \n * @returns {Promise}\n */\n put(value, key) {\n log(`put ${this.#objectStore.name}`, value, key);\n const request = this.#objectStore.put(value, key);\n return wrapRequest(request);\n }\n\n /**\n * @param {IDBValidKey|IDBKeyRange} query \n * @returns {Promise}\n */\n delete(query) {\n log(`delete ${this.#objectStore.name}`, query);\n const request = this.#objectStore.delete(query);\n return wrapRequest(request);\n }\n\n clear() {\n log(`clear ${this.#objectStore.name}`);\n const request = this.#objectStore.clear();\n return wrapRequest(request);\n }\n\n index(name) {\n return new Index(this.#objectStore.index(name));\n }\n}\n\nclass Index {\n /** @type {IDBIndex} */ #index;\n\n /**\n * @param {IDBIndex} index \n */\n constructor(index) {\n this.#index = index;\n }\n\n /**\n * @param {IDBValidKey|IDBKeyRange} query \n * @param {number} [count]\n * @returns {Promise<IDBValidKey[]>}\n */\n getAllKeys(query, count) {\n log(`IDBIndex.getAllKeys ${this.#index.objectStore.name}<${this.#index.name}>`, query, count);\n const request = this.#index.getAllKeys(query, count);\n return wrapRequest(request);\n }\n}","// Copyright 2022 Roy T. Hashimoto. All Rights Reserved.\nimport * as VFS from '../VFS.js';\n\nconst LOCK_TYPE_MASK =\n VFS.SQLITE_LOCK_NONE |\n VFS.SQLITE_LOCK_SHARED |\n VFS.SQLITE_LOCK_RESERVED |\n VFS.SQLITE_LOCK_PENDING |\n VFS.SQLITE_LOCK_EXCLUSIVE;\n\nexport class WebLocksBase {\n get state() { return this.#state; }\n #state = VFS.SQLITE_LOCK_NONE;\n\n timeoutMillis = 0;\n\n /** @type {Map<string, (value: any) => void>} */ #releasers = new Map();\n /** @type {Promise<0|5|3850>} */ #pending = Promise.resolve(0);\n\n /**\n * @param {number} flags \n * @returns {Promise<0|5|3850>} SQLITE_OK, SQLITE_BUSY, SQLITE_IOERR_LOCK\n */\n async lock(flags) {\n return this.#apply(this.#lock, flags);\n }\n\n /**\n * @param {number} flags \n * @returns {Promise<0|5|3850>} SQLITE_OK, SQLITE_IOERR_LOCK\n */\n async unlock(flags) {\n return this.#apply(this.#unlock, flags);\n }\n\n /**\n * @returns {Promise<boolean>}\n */\n async isSomewhereReserved() {\n throw new Error('unimplemented');\n }\n\n /**\n * \n * @param {(targetState: number) => void} method \n * @param {number} flags \n */\n async #apply(method, flags) {\n const targetState = flags & LOCK_TYPE_MASK;\n try {\n // Force locks and unlocks to run sequentially. This allows not\n // waiting for unlocks to complete.\n const call = () => method.call(this, targetState);\n await (this.#pending = this.#pending.then(call, call));\n this.#state = targetState;\n return VFS.SQLITE_OK;\n } catch (e) {\n if (e.name === 'AbortError') {\n return VFS.SQLITE_BUSY;\n }\n console.error(e);\n return VFS.SQLITE_IOERR_LOCK;\n }\n }\n\n async #lock(targetState) {\n if (targetState === this.#state) return VFS.SQLITE_OK;\n switch (this.#state) {\n case VFS.SQLITE_LOCK_NONE:\n switch (targetState) {\n case VFS.SQLITE_LOCK_SHARED:\n return this._NONEtoSHARED();\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n\n case VFS.SQLITE_LOCK_SHARED:\n switch (targetState) {\n case VFS.SQLITE_LOCK_RESERVED:\n return this._SHAREDtoRESERVED();\n case VFS.SQLITE_LOCK_EXCLUSIVE:\n return this._SHAREDtoEXCLUSIVE();\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n \n case VFS.SQLITE_LOCK_RESERVED:\n switch (targetState) {\n case VFS.SQLITE_LOCK_EXCLUSIVE:\n return this._RESERVEDtoEXCLUSIVE();\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n }\n\n async #unlock(targetState) {\n if (targetState === this.#state) return VFS.SQLITE_OK;\n switch (this.#state) {\n case VFS.SQLITE_LOCK_EXCLUSIVE:\n switch (targetState) {\n case VFS.SQLITE_LOCK_SHARED:\n return this._EXCLUSIVEtoSHARED();\n case VFS.SQLITE_LOCK_NONE:\n return this._EXCLUSIVEtoNONE();\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n \n case VFS.SQLITE_LOCK_RESERVED:\n switch (targetState) {\n case VFS.SQLITE_LOCK_SHARED:\n return this._RESERVEDtoSHARED();\n case VFS.SQLITE_LOCK_NONE:\n return this._RESERVEDtoNONE();\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n\n case VFS.SQLITE_LOCK_SHARED:\n switch (targetState) {\n case VFS.SQLITE_LOCK_NONE:\n return this._SHAREDtoNONE();\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n\n default:\n throw new Error(`unexpected transition ${this.#state} -> ${targetState}`);\n }\n }\n\n async _NONEtoSHARED() {\n }\n\n async _SHAREDtoEXCLUSIVE() {\n await this._SHAREDtoRESERVED();\n await this._RESERVEDtoEXCLUSIVE();\n }\n\n async _SHAREDtoRESERVED() {\n }\n\n async _RESERVEDtoEXCLUSIVE() {\n }\n\n async _EXCLUSIVEtoRESERVED() {\n }\n\n async _EXCLUSIVEtoSHARED() {\n await this._EXCLUSIVEtoRESERVED();\n await this._RESERVEDtoSHARED();\n }\n\n async _EXCLUSIVEtoNONE() {\n await this._EXCLUSIVEtoRESERVED();\n await this._RESERVEDtoSHARED();\n await this._SHAREDtoNONE();\n }\n\n async _RESERVEDtoSHARED() {\n }\n\n async _RESERVEDtoNONE() {\n await this._RESERVEDtoSHARED();\n await this._SHAREDtoNONE();\n }\n\n async _SHAREDtoNONE() {\n }\n\n /**\n * @param {string} lockName \n * @param {LockOptions} options \n * @returns {Promise<?Lock>}\n */\n _acquireWebLock(lockName, options) {\n return new Promise(async (resolve, reject) => {\n try {\n await navigator.locks.request(lockName, options, lock => {\n resolve(lock);\n if (lock) {\n return new Promise(release => this.#releasers.set(lockName, release));\n }\n });\n } catch(e) {\n reject(e);\n }\n });\n }\n\n /**\n * @param {string} lockName \n */\n _releaseWebLock(lockName) {\n this.#releasers.get(lockName)?.();\n this.#releasers.delete(lockName);\n }\n\n /**\n * @param {string} lockName \n */\n async _pollWebLock(lockName) {\n const query = await navigator.locks.query();\n return query.held.find(({name}) => name === lockName)?.mode;\n }\n\n /**\n * @returns {?AbortSignal}\n */\n _getTimeoutSignal() {\n if (this.timeoutMillis) {\n const abortController = new AbortController();\n setTimeout(() => abortController.abort(), this.timeoutMillis);\n return abortController.signal;\n }\n return undefined;\n }\n}\n\nexport class WebLocksExclusive extends WebLocksBase {\n /**\n * @param {string} name \n */\n constructor(name) {\n super();\n this._lockName = name + '-outer';\n this._reservedName = name + '-reserved';\n }\n\n async isSomewhereReserved() {\n const mode = await this._pollWebLock(this._reservedName);\n return mode === 'exclusive';\n }\n\n async _NONEtoSHARED() {\n await this._acquireWebLock(this._lockName, {\n mode: 'exclusive',\n signal: this._getTimeoutSignal()\n });\n }\n\n async _SHAREDtoRESERVED() {\n await this._acquireWebLock(this._reservedName, {\n mode: 'exclusive',\n signal: this._getTimeoutSignal()\n });\n }\n\n async _RESERVEDtoSHARED() {\n this._releaseWebLock(this._reservedName);\n }\n\n async _SHAREDtoNONE() {\n this._releaseWebLock(this._lockName);\n }\n}\n\nexport class WebLocksShared extends WebLocksBase {\n maxRetryMillis = 1000;\n\n /**\n * @param {string} name \n */\n constructor(name) {\n super();\n this._outerName = name + '-outer';\n this._innerName = name + '-inner';\n }\n\n async isSomewhereReserved() {\n const mode = await this._pollWebLock(this._outerName);\n return mode === 'exclusive';\n }\n\n async _NONEtoSHARED() {\n await this._acquireWebLock(this._outerName, {\n mode: 'shared',\n signal: this._getTimeoutSignal()\n });\n await this._acquireWebLock(this._innerName, {\n mode: 'shared',\n signal: this._getTimeoutSignal()\n });\n this._releaseWebLock(this._outerName);\n }\n\n async _SHAREDtoRESERVED() {\n let timeoutMillis = 1;\n while (true) {\n // Attempt to get the outer lock without blocking.\n const isLocked = await this._acquireWebLock(this._outerName, {\n mode: 'exclusive',\n ifAvailable: true\n });\n if (isLocked) break;\n\n if (await this.isSomewhereReserved()) {\n // Someone else has a reserved lock so retry cannot succeed.\n throw new DOMException('', 'AbortError');\n }\n\n await new Promise(resolve => setTimeout(resolve, timeoutMillis));\n timeoutMillis = Math.min(2 * timeoutMillis, this.maxRetryMillis);\n }\n this._releaseWebLock(this._innerName);\n }\n\n async _RESERVEDtoEXCLUSIVE() {\n await this._acquireWebLock(this._innerName, {\n mode: 'exclusive',\n signal: this._getTimeoutSignal()\n });\n }\n\n async _EXCLUSIVEtoRESERVED() {\n this._releaseWebLock(this._innerName);\n }\n\n async _RESERVEDtoSHARED() {\n await this._acquireWebLock(this._innerName, { mode: 'shared' });\n this._releaseWebLock(this._outerName);\n }\n\n async _SHAREDtoNONE() {\n this._releaseWebLock(this._innerName);\n }\n}"],"names":[],"sourceRoot":""}
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/web",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "A Web SDK for JourneyApps PowerSync",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -9,6 +9,24 @@
9
9
  "!lib/tests",
10
10
  "dist"
11
11
  ],
12
+ "exports": {
13
+ ".": "./lib/src/index.js",
14
+ "./umd": {
15
+ "import": "./dist/index.umd.js",
16
+ "require": "./dist/index.umd.js",
17
+ "types": "./lib/src/index.d.ts"
18
+ },
19
+ "./umd/worker/db": {
20
+ "import": "./dist/worker/WASQLiteDB.umd.js",
21
+ "require": "./dist/worker/WASQLiteDB.umd.js",
22
+ "types": "./lib/src/index.d.ts"
23
+ },
24
+ "./umd/worker/sync": {
25
+ "import": "./dist/worker/SharedSyncImplementation.umd.js",
26
+ "require": "./dist/worker/SharedSyncImplementation.umd.js",
27
+ "types": "./lib/src/index.d.ts"
28
+ }
29
+ },
12
30
  "repository": "https://github.com/powersync-ja/powersync-js",
13
31
  "bugs": {
14
32
  "url": "https://github.com/powersync-ja/powersync-js/issues"
@@ -19,8 +37,12 @@
19
37
  },
20
38
  "homepage": "https://docs.powersync.com",
21
39
  "scripts": {
22
- "build": "tsc --build",
23
- "clean": "rm -rf lib tsconfig.tsbuildinfo",
40
+ "build:tsc": "tsc --build",
41
+ "build:webpack-main": "webpack",
42
+ "build:webpack-workers": "webpack --config webpack.workers.config.js",
43
+ "build": "pnpm run build:tsc && pnpm run build:webpack-main && pnpm run build:webpack-workers",
44
+ "build:prod": "pnpm run build:tsc --sourceMap false && pnpm run build:webpack-main && pnpm run build:webpack-workers",
45
+ "clean": "rm -rf lib dist tsconfig.tsbuildinfo",
24
46
  "watch": "tsc --build -w",
25
47
  "test": "pnpm build && vitest"
26
48
  },
@@ -35,33 +57,34 @@
35
57
  "license": "Apache-2.0",
36
58
  "peerDependencies": {
37
59
  "@journeyapps/wa-sqlite": "^0.3.0",
38
- "@powersync/common": "workspace:^1.17.0"
60
+ "@powersync/common": "workspace:^1.18.1"
39
61
  },
40
62
  "dependencies": {
41
63
  "@powersync/common": "workspace:*",
42
64
  "async-mutex": "^0.4.0",
43
65
  "bson": "^6.6.0",
44
66
  "comlink": "^4.4.1",
45
- "js-logger": "^1.6.1",
46
- "lodash": "^4.17.21"
67
+ "js-logger": "^1.6.1"
47
68
  },
48
69
  "devDependencies": {
49
70
  "@journeyapps/wa-sqlite": "^0.3.0",
50
- "@rollup/plugin-commonjs": "^25.0.7",
51
- "@rollup/plugin-inject": "^5.0.5",
52
- "@rollup/plugin-json": "^6.1.0",
53
- "@rollup/plugin-node-resolve": "15.2.3",
54
- "@types/lodash": "^4.14.200",
55
71
  "@types/uuid": "^9.0.6",
56
72
  "@vitest/browser": "^1.3.1",
73
+ "crypto-browserify": "^3.12.0",
57
74
  "p-defer": "^4.0.1",
58
- "rollup": "4.14.3",
75
+ "source-map-loader": "^5.0.0",
76
+ "stream-browserify": "^3.0.0",
77
+ "terser-webpack-plugin": "^5.3.9",
59
78
  "typescript": "^5.5.3",
60
79
  "uuid": "^9.0.1",
61
80
  "vite": "^5.1.1",
62
81
  "vite-plugin-top-level-await": "^1.4.1",
63
82
  "vite-plugin-wasm": "^3.3.0",
64
83
  "vitest": "^1.3.1",
65
- "webdriverio": "^8.32.3"
84
+ "vm-browserify": "^1.1.2",
85
+ "webdriverio": "^8.32.3",
86
+ "webpack": "^5.90.1",
87
+ "webpack-cli": "^5.1.4",
88
+ "webpack-node-externals": "^3.0.0"
66
89
  }
67
90
  }
@@ -1,6 +1,6 @@
1
1
  import { type BucketStorageAdapter, type PowerSyncBackendConnector, type PowerSyncCloseOptions, type PowerSyncConnectionOptions, AbstractPowerSyncDatabase, DBAdapter, PowerSyncDatabaseOptions, PowerSyncDatabaseOptionsWithDBAdapter, PowerSyncDatabaseOptionsWithOpenFactory, PowerSyncDatabaseOptionsWithSettings, StreamingSyncImplementation } from '@powersync/common';
2
2
  import { Mutex } from 'async-mutex';
3
- import { WebSQLFlags } from './adapters/web-sql-flags';
3
+ import { ResolvedWebSQLOpenOptions, WebSQLFlags } from './adapters/web-sql-flags';
4
4
  export interface WebPowerSyncFlags extends WebSQLFlags {
5
5
  /**
6
6
  * Externally unload open PowerSync database instances when the window closes.
@@ -12,10 +12,22 @@ export interface WebPowerSyncFlags extends WebSQLFlags {
12
12
  type WithWebFlags<Base> = Base & {
13
13
  flags?: WebPowerSyncFlags;
14
14
  };
15
- export type WebPowerSyncDatabaseOptionsWithAdapter = WithWebFlags<PowerSyncDatabaseOptionsWithDBAdapter>;
16
- export type WebPowerSyncDatabaseOptionsWithOpenFactory = WithWebFlags<PowerSyncDatabaseOptionsWithOpenFactory>;
17
- export type WebPowerSyncDatabaseOptionsWithSettings = WithWebFlags<PowerSyncDatabaseOptionsWithSettings>;
18
- export type WebPowerSyncDatabaseOptions = WithWebFlags<PowerSyncDatabaseOptions>;
15
+ export interface WebSyncOptions {
16
+ /**
17
+ * Allows you to override the default sync worker.
18
+ *
19
+ * You can either provide a path to the worker script
20
+ * or a factory method that returns a worker.
21
+ */
22
+ worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => SharedWorker);
23
+ }
24
+ type WithWebSyncOptions<Base> = Base & {
25
+ sync?: WebSyncOptions;
26
+ };
27
+ export type WebPowerSyncDatabaseOptionsWithAdapter = WithWebSyncOptions<WithWebFlags<PowerSyncDatabaseOptionsWithDBAdapter>>;
28
+ export type WebPowerSyncDatabaseOptionsWithOpenFactory = WithWebSyncOptions<WithWebFlags<PowerSyncDatabaseOptionsWithOpenFactory>>;
29
+ export type WebPowerSyncDatabaseOptionsWithSettings = WithWebSyncOptions<WithWebFlags<PowerSyncDatabaseOptionsWithSettings>>;
30
+ export type WebPowerSyncDatabaseOptions = WithWebSyncOptions<WithWebFlags<PowerSyncDatabaseOptions>>;
19
31
  export declare const DEFAULT_POWERSYNC_FLAGS: Required<WebPowerSyncFlags>;
20
32
  export declare const resolveWebPowerSyncFlags: (flags?: WebPowerSyncFlags) => WebPowerSyncFlags;
21
33
  /**
@@ -1,8 +1,8 @@
1
1
  import { DBAdapter, SQLOpenFactory } from '@powersync/common';
2
- import { WebSQLFlags, WebSQLOpenFactoryOptions } from './web-sql-flags';
2
+ import { ResolvedWebSQLFlags, WebSQLOpenFactoryOptions } from './web-sql-flags';
3
3
  export declare abstract class AbstractWebSQLOpenFactory implements SQLOpenFactory {
4
4
  protected options: WebSQLOpenFactoryOptions;
5
- protected resolvedFlags: WebSQLFlags;
5
+ protected resolvedFlags: ResolvedWebSQLFlags;
6
6
  constructor(options: WebSQLOpenFactoryOptions);
7
7
  /**
8
8
  * Opens a DBAdapter if not in SSR mode
@@ -1,5 +1,5 @@
1
1
  import { type DBAdapter, type DBAdapterListener, type DBLockOptions, type LockContext, type PowerSyncOpenFactoryOptions, type QueryResult, type Transaction, BaseObserver } from '@powersync/common';
2
- import { WebSQLFlags } from '../web-sql-flags';
2
+ import { ResolvedWebSQLOpenOptions, WebSQLFlags } from '../web-sql-flags';
3
3
  /**
4
4
  * These flags are the same as {@link WebSQLFlags}.
5
5
  * This export is maintained only for API consistency
@@ -12,6 +12,7 @@ export interface WASQLiteDBAdapterOptions extends Omit<PowerSyncOpenFactoryOptio
12
12
  * A worker will be initialized if none is provided
13
13
  */
14
14
  workerPort?: MessagePort;
15
+ worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => Worker | SharedWorker);
15
16
  }
16
17
  /**
17
18
  * Adapter for WA-SQLite SQLite connections.
@@ -25,7 +26,7 @@ export declare class WASQLiteDBAdapter extends BaseObserver<DBAdapterListener> i
25
26
  private debugMode;
26
27
  constructor(options: WASQLiteDBAdapterOptions);
27
28
  get name(): string;
28
- protected get flags(): WASQLiteFlags;
29
+ protected get flags(): Required<WASQLiteFlags>;
29
30
  getWorker(): void;
30
31
  protected init(): Promise<void>;
31
32
  execute(query: string, params?: any[] | undefined): Promise<QueryResult>;
@@ -2,7 +2,8 @@ import { BaseObserver } from '@powersync/common';
2
2
  import * as Comlink from 'comlink';
3
3
  import Logger from 'js-logger';
4
4
  import { _openDB } from '../../../shared/open-db';
5
- import { getWorkerDatabaseOpener } from '../../../worker/db/open-worker-database';
5
+ import { getWorkerDatabaseOpener, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database';
6
+ import { resolveWebSQLFlags } from '../web-sql-flags';
6
7
  /**
7
8
  * Adapter for WA-SQLite SQLite connections.
8
9
  */
@@ -44,7 +45,7 @@ export class WASQLiteDBAdapter extends BaseObserver {
44
45
  return this.options.dbFilename;
45
46
  }
46
47
  get flags() {
47
- return this.options.flags ?? {};
48
+ return resolveWebSQLFlags(this.options.flags ?? {});
48
49
  }
49
50
  getWorker() { }
50
51
  async init() {
@@ -53,9 +54,15 @@ export class WASQLiteDBAdapter extends BaseObserver {
53
54
  this.logger.warn('Multiple tabs are not enabled in this browser');
54
55
  }
55
56
  if (useWebWorker) {
57
+ const optionsDbWorker = this.options.worker;
56
58
  const dbOpener = this.options.workerPort
57
59
  ? Comlink.wrap(this.options.workerPort)
58
- : getWorkerDatabaseOpener(this.options.dbFilename, enableMultiTabs);
60
+ : typeof optionsDbWorker === 'function'
61
+ ? Comlink.wrap(resolveWorkerDatabasePortFactory(() => optionsDbWorker({
62
+ ...this.options,
63
+ flags: this.flags
64
+ })))
65
+ : getWorkerDatabaseOpener(this.options.dbFilename, enableMultiTabs, optionsDbWorker);
59
66
  this.methods = await dbOpener(this.options.dbFilename);
60
67
  this.methods.registerOnTableChange(Comlink.proxy((event) => {
61
68
  this.iterateListeners((cb) => cb.tablesUpdated?.(event));
@@ -30,12 +30,23 @@ export interface WebSQLFlags {
30
30
  */
31
31
  ssrMode?: boolean;
32
32
  }
33
+ export type ResolvedWebSQLFlags = Required<WebSQLFlags>;
34
+ export interface ResolvedWebSQLOpenOptions extends SQLOpenOptions {
35
+ flags: ResolvedWebSQLFlags;
36
+ }
33
37
  /**
34
38
  * Options for opening a Web SQL connection
35
39
  */
36
40
  export interface WebSQLOpenFactoryOptions extends SQLOpenOptions {
37
41
  flags?: WebSQLFlags;
42
+ /**
43
+ * Allows you to override the default wasqlite db worker.
44
+ *
45
+ * You can either provide a path to the worker script
46
+ * or a factory method that returns a worker.
47
+ */
48
+ worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => Worker | SharedWorker);
38
49
  }
39
50
  export declare function isServerSide(): boolean;
40
- export declare const DEFAULT_WEB_SQL_FLAGS: Required<WebSQLFlags>;
41
- export declare function resolveWebSQLFlags(flags?: WebSQLFlags): WebSQLFlags;
51
+ export declare const DEFAULT_WEB_SQL_FLAGS: ResolvedWebSQLFlags;
52
+ export declare function resolveWebSQLFlags(flags?: WebSQLFlags): ResolvedWebSQLFlags;
@@ -1,8 +1,9 @@
1
1
  import * as Comlink from 'comlink';
2
- import { openWorkerDatabasePort } from '../../worker/db/open-worker-database';
2
+ import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../worker/db/open-worker-database';
3
3
  import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider';
4
4
  import { SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation';
5
5
  import { WebStreamingSyncImplementation } from './WebStreamingSyncImplementation';
6
+ import { resolveWebSQLFlags } from '../adapters/web-sql-flags';
6
7
  /**
7
8
  * The shared worker will trigger methods on this side of the message port
8
9
  * via this client provider.
@@ -78,12 +79,30 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
78
79
  * Configure or connect to the shared sync worker.
79
80
  * This worker will manage all syncing operations remotely.
80
81
  */
81
- const syncWorker = new SharedWorker(new URL('../../worker/sync/SharedSyncImplementation.worker.js', import.meta.url), {
82
- /* @vite-ignore */
83
- name: `shared-sync-${this.webOptions.identifier}`,
84
- type: 'module'
85
- });
86
- this.messagePort = syncWorker.port;
82
+ const resolvedWorkerOptions = {
83
+ ...options,
84
+ dbFilename: this.options.identifier,
85
+ flags: resolveWebSQLFlags(options.flags)
86
+ };
87
+ const syncWorker = options.sync?.worker;
88
+ if (syncWorker) {
89
+ if (typeof syncWorker === 'function') {
90
+ this.messagePort = syncWorker(resolvedWorkerOptions).port;
91
+ }
92
+ else {
93
+ this.messagePort = new SharedWorker(`${syncWorker}`, {
94
+ /* @vite-ignore */
95
+ name: `shared-sync-${this.webOptions.identifier}`
96
+ }).port;
97
+ }
98
+ }
99
+ else {
100
+ this.messagePort = new SharedWorker(new URL('../../worker/sync/SharedSyncImplementation.worker.js', import.meta.url), {
101
+ /* @vite-ignore */
102
+ name: `shared-sync-${this.webOptions.identifier}`,
103
+ type: 'module'
104
+ }).port;
105
+ }
87
106
  this.syncManager = Comlink.wrap(this.messagePort);
88
107
  this.triggerCrudUpload = this.syncManager.triggerCrudUpload;
89
108
  /**
@@ -93,14 +112,18 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem
93
112
  * sync worker.
94
113
  */
95
114
  const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options;
96
- const dbOpenerPort = openWorkerDatabasePort(this.options.identifier, true);
115
+ const dbWorker = options.database?.options?.worker;
116
+ const dbOpenerPort = typeof dbWorker === 'function'
117
+ ? resolveWorkerDatabasePortFactory(() => dbWorker(resolvedWorkerOptions))
118
+ : openWorkerDatabasePort(this.options.identifier, true, dbWorker);
119
+ const flags = { ...this.webOptions.flags, workers: undefined };
97
120
  this.isInitialized = this.syncManager.init(Comlink.transfer(dbOpenerPort, [dbOpenerPort]), {
98
121
  dbName: this.options.identifier,
99
122
  streamOptions: {
100
123
  crudUploadThrottleMs,
101
124
  identifier,
102
125
  retryDelayMs,
103
- flags: this.webOptions.flags
126
+ flags: flags
104
127
  }
105
128
  });
106
129
  /**
@@ -1,7 +1,14 @@
1
1
  import { AbstractStreamingSyncImplementation, AbstractStreamingSyncImplementationOptions, LockOptions } from '@powersync/common';
2
+ import { ResolvedWebSQLOpenOptions, WebSQLFlags } from '../adapters/web-sql-flags';
2
3
  export interface WebStreamingSyncImplementationOptions extends AbstractStreamingSyncImplementationOptions {
3
- flags?: {
4
- broadcastLogs?: boolean;
4
+ flags?: WebSQLFlags;
5
+ database?: {
6
+ options: {
7
+ worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => Worker | SharedWorker);
8
+ };
9
+ };
10
+ sync?: {
11
+ worker?: string | URL | ((options: ResolvedWebSQLOpenOptions) => SharedWorker);
5
12
  };
6
13
  }
7
14
  export declare class WebStreamingSyncImplementation extends AbstractStreamingSyncImplementation {
@@ -1 +1,4 @@
1
- export {};
1
+ /**
2
+ * Supports both shared and dedicated workers, based on how the worker is constructed (new SharedWorker vs new Worker()).
3
+ */
4
+ import '@journeyapps/wa-sqlite';
@@ -1,3 +1,62 @@
1
+ /**
2
+ * Supports both shared and dedicated workers, based on how the worker is constructed (new SharedWorker vs new Worker()).
3
+ */
4
+ import '@journeyapps/wa-sqlite';
1
5
  import * as Comlink from 'comlink';
2
6
  import { _openDB } from '../../shared/open-db';
3
- Comlink.expose(async (dbFileName) => Comlink.proxy(await _openDB(dbFileName)));
7
+ const DBMap = new Map();
8
+ const OPEN_DB_LOCK = 'open-wasqlite-db';
9
+ let nextClientId = 1;
10
+ const openDBShared = async (dbFileName) => {
11
+ // Prevent multiple simultaneous opens from causing race conditions
12
+ return navigator.locks.request(OPEN_DB_LOCK, async () => {
13
+ const clientId = nextClientId++;
14
+ if (!DBMap.has(dbFileName)) {
15
+ const clientIds = new Set();
16
+ const connection = await _openDB(dbFileName);
17
+ DBMap.set(dbFileName, {
18
+ clientIds,
19
+ db: connection
20
+ });
21
+ }
22
+ const dbEntry = DBMap.get(dbFileName);
23
+ dbEntry.clientIds.add(clientId);
24
+ const { db } = dbEntry;
25
+ const wrappedConnection = {
26
+ ...db,
27
+ close: Comlink.proxy(() => {
28
+ const { clientIds } = dbEntry;
29
+ clientIds.delete(clientId);
30
+ if (clientIds.size == 0) {
31
+ console.debug(`Closing connection to ${dbFileName}.`);
32
+ DBMap.delete(dbFileName);
33
+ return db.close?.();
34
+ }
35
+ console.debug(`Connection to ${dbFileName} not closed yet due to active clients.`);
36
+ })
37
+ };
38
+ return Comlink.proxy(wrappedConnection);
39
+ });
40
+ };
41
+ const openDBDedicated = async (dbFileName) => {
42
+ const connection = await _openDB(dbFileName);
43
+ return Comlink.proxy(connection);
44
+ };
45
+ // Check if we're in a SharedWorker context
46
+ if (typeof SharedWorkerGlobalScope !== 'undefined') {
47
+ const _self = self;
48
+ _self.onconnect = function (event) {
49
+ const port = event.ports[0];
50
+ console.debug('Exposing shared db on port', port);
51
+ Comlink.expose(openDBShared, port);
52
+ };
53
+ addEventListener('unload', () => {
54
+ Array.from(DBMap.values()).forEach(async (dbConnection) => {
55
+ const db = await dbConnection.db;
56
+ db.close?.();
57
+ });
58
+ });
59
+ }
60
+ else {
61
+ Comlink.expose(openDBDedicated);
62
+ }
@@ -3,9 +3,11 @@ import type { OpenDB } from '../../shared/types';
3
3
  /**
4
4
  * Opens a shared or dedicated worker which exposes opening of database connections
5
5
  */
6
- export declare function openWorkerDatabasePort(workerIdentifier: string, multipleTabs?: boolean): MessagePort | Worker;
6
+ export declare function openWorkerDatabasePort(workerIdentifier: string, multipleTabs?: boolean, worker?: string | URL): MessagePort | Worker;
7
7
  /**
8
8
  * @returns A function which allows for opening database connections inside
9
9
  * a worker.
10
10
  */
11
- export declare function getWorkerDatabaseOpener(workerIdentifier: string, multipleTabs?: boolean): Comlink.Remote<OpenDB>;
11
+ export declare function getWorkerDatabaseOpener(workerIdentifier: string, multipleTabs?: boolean, worker?: string | URL): Comlink.Remote<OpenDB>;
12
+ export declare function resolveWorkerDatabasePortFactory(worker: () => Worker | SharedWorker): MessagePort | Worker;
13
+ export declare function isSharedWorker(worker: Worker | SharedWorker): worker is SharedWorker;
@@ -2,29 +2,49 @@ import * as Comlink from 'comlink';
2
2
  /**
3
3
  * Opens a shared or dedicated worker which exposes opening of database connections
4
4
  */
5
- export function openWorkerDatabasePort(workerIdentifier, multipleTabs = true) {
6
- /**
7
- * Webpack V5 can bundle the worker automatically if the full Worker constructor syntax is used
8
- * https://webpack.js.org/guides/web-workers/
9
- * This enables multi tab support by default, but falls back if SharedWorker is not available
10
- * (in the case of Android)
11
- */
12
- return multipleTabs
13
- ? new SharedWorker(new URL('./SharedWASQLiteDB.worker.js', import.meta.url), {
14
- /* @vite-ignore */
15
- name: `shared-DB-worker-${workerIdentifier}`,
16
- type: 'module'
17
- }).port
18
- : new Worker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
19
- /* @vite-ignore */
20
- name: `DB-worker-${workerIdentifier}`,
21
- type: 'module'
22
- });
5
+ export function openWorkerDatabasePort(workerIdentifier, multipleTabs = true, worker = '') {
6
+ if (worker) {
7
+ return multipleTabs
8
+ ? new SharedWorker(`${worker}`, {
9
+ /* @vite-ignore */
10
+ name: `shared-DB-worker-${workerIdentifier}`
11
+ }).port
12
+ : new Worker(`${worker}`, {
13
+ /* @vite-ignore */
14
+ name: `DB-worker-${workerIdentifier}`
15
+ });
16
+ }
17
+ else {
18
+ /**
19
+ * Webpack V5 can bundle the worker automatically if the full Worker constructor syntax is used
20
+ * https://webpack.js.org/guides/web-workers/
21
+ * This enables multi tab support by default, but falls back if SharedWorker is not available
22
+ * (in the case of Android)
23
+ */
24
+ return multipleTabs
25
+ ? new SharedWorker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
26
+ /* @vite-ignore */
27
+ name: `shared-DB-worker-${workerIdentifier}`,
28
+ type: 'module'
29
+ }).port
30
+ : new Worker(new URL('./WASQLiteDB.worker.js', import.meta.url), {
31
+ /* @vite-ignore */
32
+ name: `DB-worker-${workerIdentifier}`,
33
+ type: 'module'
34
+ });
35
+ }
23
36
  }
24
37
  /**
25
38
  * @returns A function which allows for opening database connections inside
26
39
  * a worker.
27
40
  */
28
- export function getWorkerDatabaseOpener(workerIdentifier, multipleTabs = true) {
29
- return Comlink.wrap(openWorkerDatabasePort(workerIdentifier, multipleTabs));
41
+ export function getWorkerDatabaseOpener(workerIdentifier, multipleTabs = true, worker = '') {
42
+ return Comlink.wrap(openWorkerDatabasePort(workerIdentifier, multipleTabs, worker));
43
+ }
44
+ export function resolveWorkerDatabasePortFactory(worker) {
45
+ const workerInstance = worker();
46
+ return isSharedWorker(workerInstance) ? workerInstance.port : workerInstance;
47
+ }
48
+ export function isSharedWorker(worker) {
49
+ return 'port' in worker;
30
50
  }