@sqlite.org/sqlite-wasm
Advanced tools
+360
-150
@@ -52,5 +52,15 @@ "use strict"; | ||
| _need_ to change anything for this, but at some point (after Chrome | ||
| versions (approximately) 104-107 are extinct) should change our | ||
| versions (approximately) 104-107 are extinct) we should change our | ||
| usage of those methods to remove the "await". | ||
| */ | ||
| const urlParams = new URL(globalThis.location.href).searchParams; | ||
| const vfsName = urlParams.get("vfs"); | ||
| if (!vfsName) throw new Error("Expecting vfs=opfs|opfs-wl URL argument for this worker"); | ||
| /** | ||
| We use this to allow us to differentiate debug output from | ||
| multiple instances, e.g. multiple Workers to the "opfs" | ||
| VFS or both the "opfs" and "opfs-wl" VFSes. | ||
| */ | ||
| const workerId = Math.random() * 1e7 | 0; | ||
| const isWebLocker = "opfs-wl" === urlParams.get("vfs"); | ||
| const wPost = (type, ...args) => postMessage({ | ||
@@ -71,2 +81,156 @@ type, | ||
| const state = Object.create(null); | ||
| const initS11n = function() { | ||
| /** | ||
| This proxy de/serializes cross-thread function arguments and | ||
| output-pointer values via the state.sabIO SharedArrayBuffer, | ||
| using the region defined by (state.sabS11nOffset, | ||
| state.sabS11nOffset + state.sabS11nSize]. Only one dataset is | ||
| recorded at a time. | ||
| This is not a general-purpose format. It only supports the | ||
| range of operations, and data sizes, needed by the | ||
| sqlite3_vfs and sqlite3_io_methods operations. Serialized | ||
| data are transient and this serialization algorithm may | ||
| change at any time. | ||
| The data format can be succinctly summarized as: | ||
| Nt...Td...D | ||
| Where: | ||
| - N = number of entries (1 byte) | ||
| - t = type ID of first argument (1 byte) | ||
| - ...T = type IDs of the 2nd and subsequent arguments (1 byte | ||
| each). | ||
| - d = raw bytes of first argument (per-type size). | ||
| - ...D = raw bytes of the 2nd and subsequent arguments (per-type | ||
| size). | ||
| All types except strings have fixed sizes. Strings are stored | ||
| using their TextEncoder/TextDecoder representations. It would | ||
| arguably make more sense to store them as Int16Arrays of | ||
| their JS character values, but how best/fastest to get that | ||
| in and out of string form is an open point. Initial | ||
| experimentation with that approach did not gain us any speed. | ||
| Historical note: this impl was initially about 1% this size by | ||
| using using JSON.stringify/parse(), but using fit-to-purpose | ||
| serialization saves considerable runtime. | ||
| */ | ||
| if (state.s11n) return state.s11n; | ||
| const textDecoder = new TextDecoder(), textEncoder = new TextEncoder("utf-8"), viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); | ||
| state.s11n = Object.create(null); | ||
| const TypeIds = Object.create(null); | ||
| TypeIds.number = { | ||
| id: 1, | ||
| size: 8, | ||
| getter: "getFloat64", | ||
| setter: "setFloat64" | ||
| }; | ||
| TypeIds.bigint = { | ||
| id: 2, | ||
| size: 8, | ||
| getter: "getBigInt64", | ||
| setter: "setBigInt64" | ||
| }; | ||
| TypeIds.boolean = { | ||
| id: 3, | ||
| size: 4, | ||
| getter: "getInt32", | ||
| setter: "setInt32" | ||
| }; | ||
| TypeIds.string = { id: 4 }; | ||
| const getTypeId = (v) => TypeIds[typeof v] || toss("Maintenance required: this value type cannot be serialized.", v); | ||
| const getTypeIdById = (tid) => { | ||
| switch (tid) { | ||
| case TypeIds.number.id: return TypeIds.number; | ||
| case TypeIds.bigint.id: return TypeIds.bigint; | ||
| case TypeIds.boolean.id: return TypeIds.boolean; | ||
| case TypeIds.string.id: return TypeIds.string; | ||
| default: toss("Invalid type ID:", tid); | ||
| } | ||
| }; | ||
| /** | ||
| Returns an array of the deserialized state stored by the most | ||
| recent serialize() operation (from this thread or the | ||
| counterpart thread), or null if the serialization buffer is | ||
| empty. If passed a truthy argument, the serialization buffer | ||
| is cleared after deserialization. | ||
| */ | ||
| state.s11n.deserialize = function(clear = false) { | ||
| performance.now(); | ||
| const argc = viewU8[0]; | ||
| const rc = argc ? [] : null; | ||
| if (argc) { | ||
| const typeIds = []; | ||
| let offset = 1, i, n, v; | ||
| for (i = 0; i < argc; ++i, ++offset) typeIds.push(getTypeIdById(viewU8[offset])); | ||
| for (i = 0; i < argc; ++i) { | ||
| const t = typeIds[i]; | ||
| if (t.getter) { | ||
| v = viewDV[t.getter](offset, state.littleEndian); | ||
| offset += t.size; | ||
| } else { | ||
| n = viewDV.getInt32(offset, state.littleEndian); | ||
| offset += 4; | ||
| v = textDecoder.decode(viewU8.slice(offset, offset + n)); | ||
| offset += n; | ||
| } | ||
| rc.push(v); | ||
| } | ||
| } | ||
| if (clear) viewU8[0] = 0; | ||
| return rc; | ||
| }; | ||
| /** | ||
| Serializes all arguments to the shared buffer for consumption | ||
| by the counterpart thread. | ||
| This routine is only intended for serializing OPFS VFS | ||
| arguments and (in at least one special case) result values, | ||
| and the buffer is sized to be able to comfortably handle | ||
| those. | ||
| If passed no arguments then it zeroes out the serialization | ||
| state. | ||
| */ | ||
| state.s11n.serialize = function(...args) { | ||
| performance.now(); | ||
| if (args.length) { | ||
| const typeIds = []; | ||
| let i = 0, offset = 1; | ||
| viewU8[0] = args.length & 255; | ||
| for (; i < args.length; ++i, ++offset) { | ||
| typeIds.push(getTypeId(args[i])); | ||
| viewU8[offset] = typeIds[i].id; | ||
| } | ||
| for (i = 0; i < args.length; ++i) { | ||
| const t = typeIds[i]; | ||
| if (t.setter) { | ||
| viewDV[t.setter](offset, args[i], state.littleEndian); | ||
| offset += t.size; | ||
| } else { | ||
| const s = textEncoder.encode(args[i]); | ||
| viewDV.setInt32(offset, s.byteLength, state.littleEndian); | ||
| offset += 4; | ||
| viewU8.set(s, offset); | ||
| offset += s.byteLength; | ||
| } | ||
| } | ||
| } else viewU8[0] = 0; | ||
| }; | ||
| state.s11n.storeException = state.asyncS11nExceptions ? ((priority, e) => { | ||
| if (priority <= state.asyncS11nExceptions) state.s11n.serialize([ | ||
| e.name, | ||
| ": ", | ||
| e.message | ||
| ].join("")); | ||
| }) : () => {}; | ||
| return state.s11n; | ||
| }; | ||
| /** | ||
@@ -87,3 +251,3 @@ verbose: | ||
| const logImpl = (level, ...args) => { | ||
| if (state.verbose > level) loggers[level]("OPFS asyncer:", ...args); | ||
| if (state.verbose > level) loggers[level](vfsName + " async-proxy", workerId + ":", ...args); | ||
| }; | ||
@@ -102,8 +266,9 @@ const log = (...args) => logImpl(2, ...args); | ||
| /** | ||
| __implicitLocks is a Set of sqlite3_file pointers (integers) which were | ||
| "auto-locked". i.e. those for which we obtained a sync access | ||
| handle without an explicit xLock() call. Such locks will be | ||
| released during db connection idle time, whereas a sync access | ||
| handle obtained via xLock(), or subsequently xLock()'d after | ||
| auto-acquisition, will not be released until xUnlock() is called. | ||
| __implicitLocks is a Set of sqlite3_file pointers (integers) | ||
| which were "auto-locked". i.e. those for which we necessarily | ||
| obtain a sync access handle without an explicit xLock() call | ||
| guarding access. Such locks will be released during | ||
| `waitLoop()`'s idle time, whereas a sync access handle obtained | ||
| via xLock(), or subsequently xLock()'d after auto-acquisition, | ||
| will not be released until xUnlock() is called. | ||
@@ -248,6 +413,7 @@ Maintenance reminder: if we relinquish auto-locks at the end of the | ||
| an exception is thrown while acquiring the handle, this routine | ||
| will wait briefly and try again, up to some fixed number of | ||
| times. If acquisition still fails at that point it will give up | ||
| and propagate the exception. Client-level code will see that as | ||
| an I/O error. | ||
| will wait briefly and try again, up to `maxTries` of times. If | ||
| acquisition still fails at that point it will give up and | ||
| propagate the exception. Client-level code will see that either | ||
| as an I/O error or SQLITE_BUSY, depending on the exception and | ||
| the context. | ||
@@ -267,8 +433,27 @@ 2024-06-12: there is a rare race condition here which has been | ||
| so far. | ||
| Interface quirk: if fh.xLock is falsy and the handle is acquired | ||
| then fh.fid is added to __implicitLocks(). If fh.xLock is truthy, | ||
| it is not added as an implicit lock. i.e. xLock() impls must set | ||
| fh.xLock immediately _before_ calling this and must arrange to | ||
| restore it to its previous value if this function throws. | ||
| 2026-03-06: | ||
| - baseWaitTime is the number of milliseconds to wait for the | ||
| first retry, increasing by one factor for each retry. It defaults | ||
| to (state.asyncIdleWaitTime*2). | ||
| - maxTries is the number of attempt to make, each one spaced out | ||
| by one additional factor of the baseWaitTime (e.g. 300, then 600, | ||
| then 900, the 1200...). This MUST be an integer >0. | ||
| Only the Web Locks impl should use the 3rd and 4th parameters. | ||
| */ | ||
| const getSyncHandle = async (fh, opName) => { | ||
| const getSyncHandle = async (fh, opName, baseWaitTime, maxTries = 6) => { | ||
| if (!fh.syncHandle) { | ||
| const t = performance.now(); | ||
| log("Acquiring sync handle for", fh.filenameAbs); | ||
| const maxTries = 6, msBase = state.asyncIdleWaitTime * 2; | ||
| const msBase = baseWaitTime ?? state.asyncIdleWaitTime * 2; | ||
| maxTries ??= 6; | ||
| let i = 1, ms = msBase; | ||
@@ -294,2 +479,5 @@ for (;; ms = msBase * ++i) try { | ||
| Atomics.notify()'s it. | ||
| The opName is only used for logging and debugging - all result | ||
| codes are expected on the same state.sabOPView slot. | ||
| */ | ||
@@ -398,17 +586,2 @@ const storeAndNotify = (opName, value) => { | ||
| }, | ||
| xLock: async function(fid, lockType) { | ||
| const fh = __openFiles[fid]; | ||
| let rc = 0; | ||
| const oldLockType = fh.xLock; | ||
| fh.xLock = lockType; | ||
| if (!fh.syncHandle) try { | ||
| await getSyncHandle(fh, "xLock"); | ||
| __implicitLocks.delete(fid); | ||
| } catch (e) { | ||
| state.s11n.storeException(1, e); | ||
| rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_LOCK); | ||
| fh.xLock = oldLockType; | ||
| } | ||
| storeAndNotify("xLock", rc); | ||
| }, | ||
| xOpen: async function(fid, filename, flags, opfsFlags) { | ||
@@ -459,3 +632,2 @@ const opName = "xOpen"; | ||
| } catch (e) { | ||
| error("xRead() failed", e, fh); | ||
| state.s11n.storeException(1, e); | ||
@@ -485,3 +657,2 @@ rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_READ); | ||
| } catch (e) { | ||
| error("xTruncate():", e, fh); | ||
| state.s11n.storeException(2, e); | ||
@@ -493,13 +664,2 @@ rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_TRUNCATE); | ||
| }, | ||
| xUnlock: async function(fid, lockType) { | ||
| let rc = 0; | ||
| const fh = __openFiles[fid]; | ||
| if (fh.syncHandle && state.sq3Codes.SQLITE_LOCK_NONE === lockType) try { | ||
| await closeSyncHandle(fh); | ||
| } catch (e) { | ||
| state.s11n.storeException(1, e); | ||
| rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; | ||
| } | ||
| storeAndNotify("xUnlock", rc); | ||
| }, | ||
| xWrite: async function(fid, n, offset64) { | ||
@@ -512,3 +672,2 @@ let rc; | ||
| } catch (e) { | ||
| error("xWrite():", e, fh); | ||
| state.s11n.storeException(1, e); | ||
@@ -521,118 +680,167 @@ rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_WRITE); | ||
| }; | ||
| const initS11n = () => { | ||
| /** | ||
| ACHTUNG: this code is 100% duplicated in the other half of this | ||
| proxy! The documentation is maintained in the "synchronous half". | ||
| */ | ||
| if (state.s11n) return state.s11n; | ||
| const textDecoder = new TextDecoder(), textEncoder = new TextEncoder("utf-8"), viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); | ||
| state.s11n = Object.create(null); | ||
| const TypeIds = Object.create(null); | ||
| TypeIds.number = { | ||
| id: 1, | ||
| size: 8, | ||
| getter: "getFloat64", | ||
| setter: "setFloat64" | ||
| if (isWebLocker) { | ||
| /** Registry of active Web Locks: fid -> { mode, resolveRelease } */ | ||
| const __activeWebLocks = Object.create(null); | ||
| vfsAsyncImpls.xLock = async function(fid, lockType, isFromUnlock) { | ||
| const whichOp = isFromUnlock ? "xUnlock" : "xLock"; | ||
| const fh = __openFiles[fid]; | ||
| const requestedMode = lockType >= state.sq3Codes.SQLITE_LOCK_RESERVED ? "exclusive" : "shared"; | ||
| const existing = __activeWebLocks[fid]; | ||
| if (existing) { | ||
| if (existing.mode === requestedMode || existing.mode === "exclusive" && requestedMode === "shared") { | ||
| fh.xLock = lockType; | ||
| storeAndNotify(whichOp, 0); | ||
| return 0; | ||
| } | ||
| await closeSyncHandle(fh); | ||
| existing.resolveRelease(); | ||
| delete __activeWebLocks[fid]; | ||
| } | ||
| const lockName = "sqlite3-vfs-opfs:" + fh.filenameAbs; | ||
| const oldLockType = fh.xLock; | ||
| return new Promise((resolveWaitLoop) => { | ||
| navigator.locks.request(lockName, { mode: requestedMode }, async (lock) => { | ||
| __implicitLocks.delete(fid); | ||
| let rc = 0; | ||
| try { | ||
| fh.xLock = lockType; | ||
| await getSyncHandle(fh, "xLock", state.asyncIdleWaitTime, 5); | ||
| } catch (e) { | ||
| fh.xLock = oldLockType; | ||
| state.s11n.storeException(1, e); | ||
| rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_BUSY); | ||
| } | ||
| const releasePromise = rc ? void 0 : new Promise((resolveRelease) => { | ||
| __activeWebLocks[fid] = { | ||
| mode: requestedMode, | ||
| resolveRelease | ||
| }; | ||
| }); | ||
| storeAndNotify(whichOp, rc); | ||
| resolveWaitLoop(0); | ||
| await releasePromise; | ||
| }); | ||
| }); | ||
| }; | ||
| TypeIds.bigint = { | ||
| id: 2, | ||
| size: 8, | ||
| getter: "getBigInt64", | ||
| setter: "setBigInt64" | ||
| /** Internal helper for the opfs-wl xUnlock() */ | ||
| const wlCloseHandle = async (fh) => { | ||
| let rc = 0; | ||
| try { | ||
| await closeSyncHandle(fh); | ||
| } catch (e) { | ||
| state.s11n.storeException(1, e); | ||
| rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; | ||
| } | ||
| return rc; | ||
| }; | ||
| TypeIds.boolean = { | ||
| id: 3, | ||
| size: 4, | ||
| getter: "getInt32", | ||
| setter: "setInt32" | ||
| vfsAsyncImpls.xUnlock = async function(fid, lockType) { | ||
| const fh = __openFiles[fid]; | ||
| const existing = __activeWebLocks[fid]; | ||
| if (!existing) { | ||
| const rc = await wlCloseHandle(fh); | ||
| storeAndNotify("xUnlock", rc); | ||
| return rc; | ||
| } | ||
| let rc = 0; | ||
| if (lockType === state.sq3Codes.SQLITE_LOCK_NONE) { | ||
| rc = await wlCloseHandle(fh); | ||
| existing.resolveRelease(); | ||
| delete __activeWebLocks[fid]; | ||
| fh.xLock = lockType; | ||
| } else if (lockType === state.sq3Codes.SQLITE_LOCK_SHARED && existing.mode === "exclusive") { | ||
| rc = await wlCloseHandle(fh); | ||
| if (0 === rc) { | ||
| fh.xLock = lockType; | ||
| existing.resolveRelease(); | ||
| delete __activeWebLocks[fid]; | ||
| return vfsAsyncImpls.xLock(fid, lockType, true); | ||
| } | ||
| } else error("xUnlock() unhandled condition", fh); | ||
| storeAndNotify("xUnlock", rc); | ||
| return 0; | ||
| }; | ||
| TypeIds.string = { id: 4 }; | ||
| const getTypeId = (v) => TypeIds[typeof v] || toss("Maintenance required: this value type cannot be serialized.", v); | ||
| const getTypeIdById = (tid) => { | ||
| switch (tid) { | ||
| case TypeIds.number.id: return TypeIds.number; | ||
| case TypeIds.bigint.id: return TypeIds.bigint; | ||
| case TypeIds.boolean.id: return TypeIds.boolean; | ||
| case TypeIds.string.id: return TypeIds.string; | ||
| default: toss("Invalid type ID:", tid); | ||
| } else { | ||
| vfsAsyncImpls.xLock = async function(fid, lockType) { | ||
| const fh = __openFiles[fid]; | ||
| let rc = 0; | ||
| const oldLockType = fh.xLock; | ||
| fh.xLock = lockType; | ||
| if (!fh.syncHandle) try { | ||
| await getSyncHandle(fh, "xLock"); | ||
| __implicitLocks.delete(fid); | ||
| } catch (e) { | ||
| state.s11n.storeException(1, e); | ||
| rc = GetSyncHandleError.convertRc(e, state.sq3Codes.SQLITE_IOERR_LOCK); | ||
| fh.xLock = oldLockType; | ||
| } | ||
| storeAndNotify("xLock", rc); | ||
| }; | ||
| state.s11n.deserialize = function(clear = false) { | ||
| const argc = viewU8[0]; | ||
| const rc = argc ? [] : null; | ||
| if (argc) { | ||
| const typeIds = []; | ||
| let offset = 1, i, n, v; | ||
| for (i = 0; i < argc; ++i, ++offset) typeIds.push(getTypeIdById(viewU8[offset])); | ||
| for (i = 0; i < argc; ++i) { | ||
| const t = typeIds[i]; | ||
| if (t.getter) { | ||
| v = viewDV[t.getter](offset, state.littleEndian); | ||
| offset += t.size; | ||
| } else { | ||
| n = viewDV.getInt32(offset, state.littleEndian); | ||
| offset += 4; | ||
| v = textDecoder.decode(viewU8.slice(offset, offset + n)); | ||
| offset += n; | ||
| } | ||
| rc.push(v); | ||
| } | ||
| vfsAsyncImpls.xUnlock = async function(fid, lockType) { | ||
| let rc = 0; | ||
| const fh = __openFiles[fid]; | ||
| if (fh.syncHandle && state.sq3Codes.SQLITE_LOCK_NONE === lockType) try { | ||
| await closeSyncHandle(fh); | ||
| } catch (e) { | ||
| state.s11n.storeException(1, e); | ||
| rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; | ||
| } | ||
| if (clear) viewU8[0] = 0; | ||
| return rc; | ||
| storeAndNotify("xUnlock", rc); | ||
| }; | ||
| state.s11n.serialize = function(...args) { | ||
| if (args.length) { | ||
| const typeIds = []; | ||
| let i = 0, offset = 1; | ||
| viewU8[0] = args.length & 255; | ||
| for (; i < args.length; ++i, ++offset) { | ||
| typeIds.push(getTypeId(args[i])); | ||
| viewU8[offset] = typeIds[i].id; | ||
| } | ||
| for (i = 0; i < args.length; ++i) { | ||
| const t = typeIds[i]; | ||
| if (t.setter) { | ||
| viewDV[t.setter](offset, args[i], state.littleEndian); | ||
| offset += t.size; | ||
| } else { | ||
| const s = textEncoder.encode(args[i]); | ||
| viewDV.setInt32(offset, s.byteLength, state.littleEndian); | ||
| offset += 4; | ||
| viewU8.set(s, offset); | ||
| offset += s.byteLength; | ||
| } | ||
| } | ||
| } else viewU8[0] = 0; | ||
| }; | ||
| state.s11n.storeException = state.asyncS11nExceptions ? ((priority, e) => { | ||
| if (priority <= state.asyncS11nExceptions) state.s11n.serialize([ | ||
| e.name, | ||
| ": ", | ||
| e.message | ||
| ].join("")); | ||
| }) : () => {}; | ||
| return state.s11n; | ||
| }; | ||
| } | ||
| const waitLoop = async function f() { | ||
| const opHandlers = Object.create(null); | ||
| for (let k of Object.keys(state.opIds)) { | ||
| const vi = vfsAsyncImpls[k]; | ||
| if (!vi) continue; | ||
| const o = Object.create(null); | ||
| opHandlers[state.opIds[k]] = o; | ||
| o.key = k; | ||
| o.f = vi; | ||
| if (!f.inited) { | ||
| f.inited = true; | ||
| f.opHandlers = Object.create(null); | ||
| for (let k of Object.keys(state.opIds)) { | ||
| const vi = vfsAsyncImpls[k]; | ||
| if (!vi) continue; | ||
| const o = Object.create(null); | ||
| f.opHandlers[state.opIds[k]] = o; | ||
| o.key = k; | ||
| o.f = vi; | ||
| } | ||
| } | ||
| const opIds = state.opIds; | ||
| const opView = state.sabOPView; | ||
| const slotWhichOp = opIds.whichOp; | ||
| const idleWaitTime = state.asyncIdleWaitTime; | ||
| const hasWaitAsync = !!Atomics.waitAsync; | ||
| while (!flagAsyncShutdown) try { | ||
| if ("not-equal" !== Atomics.wait(state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime)) { | ||
| await releaseImplicitLocks(); | ||
| continue; | ||
| let opId; | ||
| if (hasWaitAsync) { | ||
| opId = Atomics.load(opView, slotWhichOp); | ||
| if (0 === opId) { | ||
| const rv = Atomics.waitAsync(opView, slotWhichOp, 0, idleWaitTime); | ||
| if (rv.async) await rv.value; | ||
| await releaseImplicitLocks(); | ||
| continue; | ||
| } | ||
| } else { | ||
| /** | ||
| For browsers without Atomics.waitAsync(), we require | ||
| the legacy implementation. Browser versions where | ||
| waitAsync() arrived: | ||
| Chrome: 90 (2021-04-13) | ||
| Firefox: 145 (2025-11-11) | ||
| Safari: 16.4 (2023-03-27) | ||
| The "opfs" VFS was not born until Chrome was somewhere in | ||
| the v104-108 range (Summer/Autumn 2022) and did not work | ||
| with Safari < v17 (2023-09-18) due to a WebKit bug which | ||
| restricted OPFS access from sub-Workers. | ||
| The waitAsync() counterpart of this block can be used by | ||
| both "opfs" and "opfs-wl", whereas this block can only be | ||
| used by "opfs". Performance comparisons between the two | ||
| in high-contention tests have been indecisive. | ||
| */ | ||
| if ("not-equal" !== Atomics.wait(state.sabOPView, slotWhichOp, 0, state.asyncIdleWaitTime)) { | ||
| await releaseImplicitLocks(); | ||
| continue; | ||
| } | ||
| opId = Atomics.load(state.sabOPView, slotWhichOp); | ||
| } | ||
| const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); | ||
| Atomics.store(state.sabOPView, state.opIds.whichOp, 0); | ||
| const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #", opId); | ||
| const args = state.s11n.deserialize(true) || []; | ||
| if (hnd.f) await hnd.f(...args); | ||
| else error("Missing callback for opId", opId); | ||
| Atomics.store(opView, slotWhichOp, 0); | ||
| await (f.opHandlers[opId]?.f ?? toss("No waitLoop handler for whichOp #", opId))(...state.s11n.deserialize(true) || []); | ||
| } catch (e) { | ||
@@ -674,7 +882,9 @@ error("in waitLoop():", e); | ||
| }; | ||
| if (!globalThis.SharedArrayBuffer) wPost("opfs-unavailable", "Missing SharedArrayBuffer API.", "The server must emit the COOP/COEP response headers to enable that."); | ||
| if (globalThis.window === globalThis) wPost("opfs-unavailable", "This code cannot run from the main thread.", "Load it as a Worker from a separate Worker."); | ||
| else if (!globalThis.SharedArrayBuffer) wPost("opfs-unavailable", "Missing SharedArrayBuffer API.", "The server must emit the COOP/COEP response headers to enable that."); | ||
| else if (!globalThis.Atomics) wPost("opfs-unavailable", "Missing Atomics API.", "The server must emit the COOP/COEP response headers to enable that."); | ||
| else if (!globalThis.FileSystemHandle || !globalThis.FileSystemDirectoryHandle || !globalThis.FileSystemFileHandle || !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle || !navigator?.storage?.getDirectory) wPost("opfs-unavailable", "Missing required OPFS APIs."); | ||
| else if (isWebLocker && !globalThis.Atomics.waitAsync) wPost("opfs-unavailable", "Missing required Atomics.waitSync() for " + vfsName); | ||
| else if (!globalThis.FileSystemHandle || !globalThis.FileSystemDirectoryHandle || !globalThis.FileSystemFileHandle?.prototype?.createSyncAccessHandle || !navigator?.storage?.getDirectory) wPost("opfs-unavailable", "Missing required OPFS APIs."); | ||
| else installAsyncProxy(); | ||
| //#endregion | ||
| })(); |
+24
-13
| { | ||
| "name": "@sqlite.org/sqlite-wasm", | ||
| "version": "3.51.2-build9", | ||
| "version": "3.53.0-build1", | ||
| "description": "SQLite Wasm conveniently wrapped as an ES Module.", | ||
@@ -44,2 +44,5 @@ "type": "module", | ||
| }, | ||
| "workspaces": [ | ||
| "demos/*" | ||
| ], | ||
| "files": [ | ||
@@ -56,7 +59,17 @@ "dist", | ||
| "build": "tsdown", | ||
| "dev:kvvfs": "npm run dev -w demos/kvvfs-demo", | ||
| "dev:opfs": "npm run dev -w demos/opfs-demo", | ||
| "dev:opfs-wl": "npm run dev -w demos/opfs-wl-demo", | ||
| "dev:sahpool": "npm run dev -w demos/sahpool-demo", | ||
| "dev:sahpool-webpack": "npm run dev -w demos/sahpool-webpack-demo", | ||
| "dev:sahpool-parcel": "npm run dev -w demos/sahpool-parcel-demo", | ||
| "dev:sahpool-rsbuild": "npm run dev -w demos/sahpool-rsbuild-demo", | ||
| "dev:main-thread": "npm run dev -w demos/main-thread-demo", | ||
| "dev:in-worker": "npm run dev -w demos/in-worker-demo", | ||
| "start": "npx http-server --coop", | ||
| "start:node": "cd demo && node node.mjs", | ||
| "fix": "npx prettier . --write", | ||
| "format": "oxfmt", | ||
| "format:check": "oxfmt --check", | ||
| "prepare": "lefthook install", | ||
| "prepublishOnly": "npm run build && npm run fix && npm run publint && npm run check-types", | ||
| "prepublishOnly": "npm run build && npm run format && npm run publint && npm run check-types", | ||
| "deploy": "npm run prepublishOnly && git add . && git commit -am 'New release' && git push && npm publish --tag=latest" | ||
@@ -69,16 +82,14 @@ }, | ||
| "@types/node": "^25.6.0", | ||
| "@typescript/native-preview": "^7.0.0-dev.20260414.1", | ||
| "@vitest/browser": "^4.1.4", | ||
| "@vitest/browser-playwright": "^4.1.4", | ||
| "@typescript/native-preview": "^7.0.0-dev.20260421.2", | ||
| "@vitest/browser": "^4.1.5", | ||
| "@vitest/browser-playwright": "^4.1.5", | ||
| "happy-dom": "20.9.0", | ||
| "http-server": "^14.1.1", | ||
| "lefthook": "2.1.5", | ||
| "lefthook": "2.1.6", | ||
| "oxfmt": "^0.46.0", | ||
| "playwright": "^1.59.1", | ||
| "prettier": "^3.8.2", | ||
| "prettier-plugin-jsdoc": "^1.8.0", | ||
| "publint": "^0.3.18", | ||
| "tsdown": "^0.21.8", | ||
| "typescript": "^6.0.2", | ||
| "vitest": "^4.1.4" | ||
| "tsdown": "^0.21.9", | ||
| "typescript": "^6.0.3", | ||
| "vitest": "^4.1.5" | ||
| } | ||
| } |
+94
-154
@@ -5,2 +5,8 @@ # SQLite Wasm | ||
| ## Installation | ||
| ```bash | ||
| npm install @sqlite.org/sqlite-wasm | ||
| ``` | ||
| ## Bug reports | ||
@@ -10,9 +16,7 @@ | ||
| > | ||
| > This project wraps the code of | ||
| > [SQLite Wasm](https://sqlite.org/wasm/doc/trunk/index.md) with _no_ changes, | ||
| > apart from added TypeScript types. Please do _not_ file issues or feature | ||
| > requests regarding the underlying SQLite Wasm code here. Instead, please | ||
| > follow the | ||
| > [SQLite bug filing instructions](https://www.sqlite.org/src/wiki?name=Bug+Reports). | ||
| > Filing TypeScript type related issues and feature requests is fine. | ||
| > This project wraps the code of [SQLite Wasm](https://sqlite.org/wasm/doc/trunk/index.md) with _no_ | ||
| > changes, apart from added TypeScript types. Please do _not_ file issues or feature requests | ||
| > regarding the underlying SQLite Wasm code here. Instead, please follow the | ||
| > [SQLite bug filing instructions](https://www.sqlite.org/src/wiki?name=Bug+Reports). Filing | ||
| > TypeScript type related issues and feature requests is fine. | ||
@@ -23,72 +27,13 @@ ## Node.js support | ||
| > | ||
| > Node.js is currently only supported for in-memory databases without | ||
| > persistence. | ||
| > Node.js is currently only supported for in-memory databases without persistence. | ||
| ## Installation | ||
| ```bash | ||
| npm install @sqlite.org/sqlite-wasm | ||
| ``` | ||
| ## Usage | ||
| There are three ways to use SQLite Wasm: | ||
| There are two ways to use SQLite Wasm: | ||
| - [in the main thread with a wrapped worker](#in-a-wrapped-worker-with-opfs-if-available) | ||
| (🏆 preferred option) | ||
| - [in a worker](#in-a-worker-with-opfs-if-available) | ||
| - [in the main thread](#in-the-main-thread-without-opfs) | ||
| Only the worker versions allow you to use the origin private file system (OPFS) | ||
| storage back-end. | ||
| Only the worker versions allow you to use the origin private file system (OPFS) storage back-end. | ||
| ### In a wrapped worker (with OPFS if available): | ||
| > [!Warning] | ||
| > | ||
| > For this to work, you need to set the following headers on your server: | ||
| > | ||
| > `Cross-Origin-Opener-Policy: same-origin` | ||
| > | ||
| > `Cross-Origin-Embedder-Policy: require-corp` | ||
| ```ts | ||
| import { | ||
| sqlite3Worker1Promiser, | ||
| type Worker1Promiser, | ||
| } from '@sqlite.org/sqlite-wasm'; | ||
| const initializeSQLite = async () => { | ||
| try { | ||
| const promiser = await sqlite3Worker1Promiser.v2(); | ||
| const configResponse = await promiser('config-get', {}); | ||
| console.log( | ||
| 'Running SQLite3 version', | ||
| configResponse.result.version.libVersion, | ||
| ); | ||
| const openResponse = await promiser('open', { | ||
| filename: 'file:mydb-v2.sqlite3?vfs=opfs', | ||
| }); | ||
| console.log( | ||
| 'OPFS is available, created persisted database at', | ||
| openResponse.result.filename.replace(/^file:(.*?)\?vfs=opfs$/, '$1'), | ||
| ); | ||
| } catch (err: any) { | ||
| if (!(err instanceof Error)) { | ||
| err = new Error(err.result.message || 'Unknown error'); | ||
| } | ||
| console.error(err.name, err.message); | ||
| } | ||
| }; | ||
| await initializeSQLite(); | ||
| ``` | ||
| The `promiser` object above implements the | ||
| [Worker1 API](https://sqlite.org/wasm/doc/trunk/api-worker1.md#worker1-methods). | ||
| ### In a worker (with OPFS if available): | ||
@@ -113,7 +58,4 @@ | ||
| const log = console.log; | ||
| const error = console.error; | ||
| const start = (sqlite3) => { | ||
| log('Running SQLite3 version', sqlite3.version.libVersion); | ||
| console.log('Running SQLite3 version', sqlite3.version.libVersion); | ||
| const db = | ||
@@ -123,3 +65,3 @@ 'opfs' in sqlite3 | ||
| : new sqlite3.oo1.DB('/mydb.sqlite3', 'ct'); | ||
| log( | ||
| console.log( | ||
| 'opfs' in sqlite3 | ||
@@ -134,8 +76,8 @@ ? `OPFS is available, created persisted database at ${db.filename}` | ||
| try { | ||
| log('Loading and initializing SQLite3 module...'); | ||
| console.log('Loading and initializing SQLite3 module...'); | ||
| const sqlite3 = await sqlite3InitModule(); | ||
| log('Done initializing. Running demo...'); | ||
| console.log('Done initializing. Running demo...'); | ||
| start(sqlite3); | ||
| } catch (err) { | ||
| error('Initialization error:', err.name, err.message); | ||
| console.error('Initialization error:', err.name, err.message); | ||
| } | ||
@@ -148,3 +90,3 @@ }; | ||
| The `db` object above implements the | ||
| [Object Oriented API #1](https://sqlite.org/wasm/doc/trunk/api-oo1.md). | ||
| [Object-Oriented API #1](https://sqlite.org/wasm/doc/trunk/api-oo1.md). | ||
@@ -156,5 +98,2 @@ ### In the main thread (without OPFS): | ||
| const log = console.log; | ||
| const error = console.error; | ||
| const start = (sqlite3) => { | ||
@@ -165,11 +104,10 @@ log('Running SQLite3 version', sqlite3.version.libVersion); | ||
| }; | ||
| const initializeSQLite = async () => { | ||
| try { | ||
| log('Loading and initializing SQLite3 module...'); | ||
| console.log('Loading and initializing SQLite3 module...'); | ||
| const sqlite3 = await sqlite3InitModule(); | ||
| log('Done initializing. Running demo...'); | ||
| console.log('Done initializing. Running demo...'); | ||
| start(sqlite3); | ||
| } catch (err) { | ||
| error('Initialization error:', err.name, err.message); | ||
| console.error('Initialization error:', err.name, err.message); | ||
| } | ||
@@ -182,8 +120,8 @@ }; | ||
| The `db` object above implements the | ||
| [Object Oriented API #1](https://sqlite.org/wasm/doc/trunk/api-oo1.md). | ||
| [Object-Oriented API #1](https://sqlite.org/wasm/doc/trunk/api-oo1.md). | ||
| ## Usage with vite | ||
| If you are using [vite](https://vitejs.dev/), you need to add the following | ||
| config option in `vite.config.js`: | ||
| If you are using [vite](https://vitejs.dev/), you need to add the following config option in | ||
| `vite.config.js`: | ||
@@ -206,19 +144,16 @@ ```js | ||
| Check out a | ||
| [sample project](https://stackblitz.com/edit/vitejs-vite-ttrbwh?file=main.js) | ||
| that shows this in action. | ||
| Check out a [sample project](https://stackblitz.com/edit/vitejs-vite-ttrbwh?file=main.js) that shows | ||
| this in action. | ||
| ## Demo | ||
| See the [demo](https://github.com/sqlite/sqlite-wasm/tree/main/demo) folder for | ||
| examples of how to use this in the main thread and in a worker. (Note that the | ||
| worker variant requires special HTTP headers, so it can't be hosted on GitHub | ||
| Pages.) An example that shows how to use this with vite is available on | ||
| [StackBlitz](https://stackblitz.com/edit/vitejs-vite-ttrbwh?file=main.js). | ||
| See the [demo](https://github.com/sqlite/sqlite-wasm/tree/main/demo) folder for examples of how to | ||
| use this in the main thread and in a worker. (Note that the worker variant requires special HTTP | ||
| headers, so it can't be hosted on GitHub Pages.) An example that shows how to use this with vite is | ||
| available on [StackBlitz](https://stackblitz.com/edit/vitejs-vite-ttrbwh?file=main.js). | ||
| ## Projects using this package | ||
| See the list of | ||
| [npm dependents](https://www.npmjs.com/browse/depended/@sqlite.org/sqlite-wasm) | ||
| for this package. | ||
| See the list of [npm dependents](https://www.npmjs.com/browse/depended/@sqlite.org/sqlite-wasm) for | ||
| this package. | ||
@@ -229,77 +164,82 @@ ## Deploying a new version | ||
| 1. Manually trigger the | ||
| [GitHub Actions workflow](../../actions/workflows/build-wasm.yml). By | ||
| default, it uses the latest SQLite tag. This pull request will contain the | ||
| latest `sqlite3.wasm` and related bindings. | ||
| 1. Manually trigger the [GitHub Actions workflow](../../actions/workflows/build-wasm.yml). By | ||
| default, it uses the latest SQLite tag. This pull request will contain the latest `sqlite3.wasm` | ||
| and related bindings. | ||
| 2. Once the above pull request is validated and merged, update the version | ||
| number in `package.json`, reflecting the current | ||
| [SQLite version number](https://sqlite.org/download.html) and add a build | ||
| identifier suffix like `-build1`. The complete version number should read | ||
| something like `3.41.2-build1`. | ||
| 2. Once the above pull request is validated and merged, update the version number in `package.json`, | ||
| reflecting the current [SQLite version number](https://sqlite.org/download.html) and add a build | ||
| identifier suffix like `-build1`. The complete version number should read something like | ||
| `3.41.2-build1`. | ||
| ## Building the SQLite Wasm locally | ||
| 1. Build the Docker image: | ||
| 1. Build the Docker image: | ||
| ```bash | ||
| docker build -t sqlite-wasm-builder:env . | ||
| ``` | ||
| ```bash | ||
| docker build -t sqlite-wasm-builder:env . | ||
| ``` | ||
| 2. Run the build: | ||
| 2. Run the build: | ||
| **Unix (Linux/macOS):** | ||
| **Unix (Linux/macOS):** | ||
| ```bash | ||
| docker run --rm \ | ||
| -e SQLITE_REF="master" \ | ||
| -v "$(pwd)/out":/out \ | ||
| -v "$(pwd)/src/bin":/src/bin \ | ||
| sqlite-wasm-builder:env build | ||
| ``` | ||
| ```bash | ||
| docker run --rm \ | ||
| -e SQLITE_REF="master" \ | ||
| -v "$(pwd)/out":/out \ | ||
| -v "$(pwd)/src/bin":/src/bin \ | ||
| sqlite-wasm-builder:env build | ||
| ``` | ||
| **Windows (PowerShell):** | ||
| **Windows (PowerShell):** | ||
| ```powershell | ||
| docker run --rm ` | ||
| -e SQLITE_REF="master" ` | ||
| -v "${PWD}/out:/out" ` | ||
| -v "${PWD}/src/bin:/src/bin" ` | ||
| sqlite-wasm-builder:env build | ||
| ``` | ||
| ```powershell | ||
| docker run --rm ` | ||
| -e SQLITE_REF="master" ` | ||
| -v "${PWD}/out:/out" ` | ||
| -v "${PWD}/src/bin:/src/bin" ` | ||
| sqlite-wasm-builder:env build | ||
| ``` | ||
| **Windows (Command Prompt):** | ||
| **Windows (Command Prompt):** | ||
| ```cmd | ||
| docker run --rm ^ | ||
| -e SQLITE_REF="master" ^ | ||
| -v "%cd%/out:/out" ^ | ||
| -v "%cd%/src/bin:/src/bin" ^ | ||
| sqlite-wasm-builder:env build | ||
| ``` | ||
| ```cmd | ||
| docker run --rm ^ | ||
| -e SQLITE_REF="master" ^ | ||
| -v "%cd%/out:/out" ^ | ||
| -v "%cd%/src/bin:/src/bin" ^ | ||
| sqlite-wasm-builder:env build | ||
| ``` | ||
| ## Running tests | ||
| The test suite consists of Node.js tests and browser-based tests (using Vitest | ||
| Browser Mode). Tests aim to sanity-check the exported scripts. We test for | ||
| correct exports and **very** basic functionality. | ||
| The test suite consists of Node.js tests and browser-based tests (using Vitest Browser Mode). Tests | ||
| aim to sanity-check the exported scripts. We test for correct exports and **very** basic | ||
| functionality. | ||
| 1. Install dependencies: | ||
| 1. Install dependencies: | ||
| ```bash | ||
| npm install | ||
| ``` | ||
| ```bash | ||
| npm install | ||
| ``` | ||
| 2. Install Playwright browsers (required for browser tests): | ||
| 2. Install Playwright browsers (required for browser tests): | ||
| ```bash | ||
| npx playwright install chromium --with-deps --no-shell | ||
| ``` | ||
| ```bash | ||
| npx playwright install chromium --with-deps --no-shell | ||
| ``` | ||
| 3. Run all tests: | ||
| 3. Run all tests: | ||
| ```bash | ||
| npm test | ||
| ``` | ||
| ```bash | ||
| npm test | ||
| ``` | ||
| ## Deprecations | ||
| The Worker1 and Promiser1 APIs are, as of 2026-04-15, deprecated. They _will not be removed_, but | ||
| they also will not be extended further. It is their author's considered opinion that they are too | ||
| fragile, too imperformant, and too limited for any non-toy software, and their use is _actively | ||
| discouraged_. The "correct" way to use this library is documented in [Usage](#usage) section above. | ||
| ## License | ||
@@ -311,4 +251,4 @@ | ||
| This project is based on [SQLite Wasm](https://sqlite.org/wasm), which it | ||
| conveniently wraps as an ES Module and publishes to npm as | ||
| This project is based on [SQLite Wasm](https://sqlite.org/wasm), which it conveniently wraps as an | ||
| ES Module and publishes to npm as | ||
| [`@sqlite.org/sqlite-wasm`](https://www.npmjs.com/package/@sqlite.org/sqlite-wasm). |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
2829040
0.87%12
-14.29%43412
1.35%242
-19.87%