summaryrefslogtreecommitdiffstats
path: root/ext/wasm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:28:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 17:28:19 +0000
commit18657a960e125336f704ea058e25c27bd3900dcb (patch)
tree17b438b680ed45a996d7b59951e6aa34023783f2 /ext/wasm
parentInitial commit. (diff)
downloadsqlite3-upstream.tar.xz
sqlite3-upstream.zip
Adding upstream version 3.40.1.upstream/3.40.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ext/wasm')
-rw-r--r--ext/wasm/EXPORTED_FUNCTIONS.fiddle.in10
-rw-r--r--ext/wasm/GNUmakefile655
-rw-r--r--ext/wasm/README-dist.txt23
-rw-r--r--ext/wasm/README.md105
-rw-r--r--ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api94
-rw-r--r--ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api3
-rw-r--r--ext/wasm/api/README.md133
-rw-r--r--ext/wasm/api/extern-post-js.js103
-rw-r--r--ext/wasm/api/extern-pre-js.js7
-rw-r--r--ext/wasm/api/post-js-footer.js4
-rw-r--r--ext/wasm/api/post-js-header.js25
-rw-r--r--ext/wasm/api/pre-js.js100
-rw-r--r--ext/wasm/api/sqlite3-api-cleanup.js70
-rw-r--r--ext/wasm/api/sqlite3-api-glue.js720
-rw-r--r--ext/wasm/api/sqlite3-api-oo1.js1800
-rw-r--r--ext/wasm/api/sqlite3-api-opfs.js1311
-rw-r--r--ext/wasm/api/sqlite3-api-prologue.js1602
-rw-r--r--ext/wasm/api/sqlite3-api-worker1.js654
-rw-r--r--ext/wasm/api/sqlite3-license-version-header.js25
-rw-r--r--ext/wasm/api/sqlite3-opfs-async-proxy.js830
-rw-r--r--ext/wasm/api/sqlite3-wasi.h69
-rw-r--r--ext/wasm/api/sqlite3-wasm.c1181
-rw-r--r--ext/wasm/api/sqlite3-worker1-promiser.js259
-rw-r--r--ext/wasm/api/sqlite3-worker1.js49
-rw-r--r--ext/wasm/batch-runner.html90
-rw-r--r--ext/wasm/batch-runner.js588
-rw-r--r--ext/wasm/common/SqliteTestUtil.js236
-rw-r--r--ext/wasm/common/emscripten.css24
-rw-r--r--ext/wasm/common/testing.css63
-rw-r--r--ext/wasm/common/whwasmutil.js1706
-rw-r--r--ext/wasm/demo-123-worker.html44
-rw-r--r--ext/wasm/demo-123.html24
-rw-r--r--ext/wasm/demo-123.js289
-rw-r--r--ext/wasm/demo-jsstorage.html49
-rw-r--r--ext/wasm/demo-jsstorage.js114
-rw-r--r--ext/wasm/demo-worker1-promiser.html34
-rw-r--r--ext/wasm/demo-worker1-promiser.js270
-rw-r--r--ext/wasm/demo-worker1.html34
-rw-r--r--ext/wasm/demo-worker1.js345
-rw-r--r--ext/wasm/dist.make101
-rw-r--r--ext/wasm/fiddle.make194
-rw-r--r--ext/wasm/fiddle/emscripten.css24
-rw-r--r--ext/wasm/fiddle/fiddle-worker.js379
-rw-r--r--ext/wasm/fiddle/fiddle.js815
-rw-r--r--ext/wasm/fiddle/index.html278
-rw-r--r--ext/wasm/index-dist.html90
-rw-r--r--ext/wasm/index.html115
-rw-r--r--ext/wasm/jaccwabyt/jaccwabyt.js746
-rw-r--r--ext/wasm/jaccwabyt/jaccwabyt.md1076
-rw-r--r--ext/wasm/module-symbols.html333
-rw-r--r--ext/wasm/scratchpad-wasmfs-main.html40
-rw-r--r--ext/wasm/scratchpad-wasmfs-main.js70
-rw-r--r--ext/wasm/speedtest1-wasmfs.html149
-rw-r--r--ext/wasm/speedtest1-worker.html372
-rw-r--r--ext/wasm/speedtest1-worker.js99
-rw-r--r--ext/wasm/speedtest1.html174
-rwxr-xr-xext/wasm/split-speedtest1-script.sh17
-rw-r--r--ext/wasm/sql/000-mandelbrot.sql17
-rw-r--r--ext/wasm/sql/001-sudoku.sql28
-rw-r--r--ext/wasm/test-opfs-vfs.html26
-rw-r--r--ext/wasm/test-opfs-vfs.js85
-rw-r--r--ext/wasm/tester1-worker.html63
-rw-r--r--ext/wasm/tester1.html28
-rw-r--r--ext/wasm/tester1.js1864
-rw-r--r--ext/wasm/version-info.c106
-rw-r--r--ext/wasm/wasmfs.make113
66 files changed, 21144 insertions, 0 deletions
diff --git a/ext/wasm/EXPORTED_FUNCTIONS.fiddle.in b/ext/wasm/EXPORTED_FUNCTIONS.fiddle.in
new file mode 100644
index 0000000..103704d
--- /dev/null
+++ b/ext/wasm/EXPORTED_FUNCTIONS.fiddle.in
@@ -0,0 +1,10 @@
+_fiddle_db_arg
+_fiddle_db_filename
+_fiddle_exec
+_fiddle_experiment
+_fiddle_interrupt
+_fiddle_main
+_fiddle_reset_db
+_fiddle_db_handle
+_fiddle_db_vfs
+_fiddle_export_db
diff --git a/ext/wasm/GNUmakefile b/ext/wasm/GNUmakefile
new file mode 100644
index 0000000..039dff4
--- /dev/null
+++ b/ext/wasm/GNUmakefile
@@ -0,0 +1,655 @@
+#######################################################################
+# This GNU makefile drives the build of the sqlite3 WASM
+# components. It is not part of the canonical build process.
+#
+# This build assumes a Linux platform and is not intended for
+# general-purpose client-level use, except for creating builds with
+# custom configurations. It is primarily intended for the sqlite
+# project's own development of the JS/WASM components.
+#
+# Primary targets:
+#
+# default, all = build in dev mode
+#
+# o0, o1, o2, o3, os, oz = full clean/rebuild with the -Ox level indicated
+# by the target name. Rebuild is necessary for all components to get
+# the desired optimization level.
+#
+# dist = create end user deliverables. Add dist.build=oX to build
+# with a specific optimization level, where oX is one of the
+# above-listed o? target names.
+#
+# clean = clean up
+########################################################################
+SHELL := $(shell which bash 2>/dev/null)
+MAKEFILE := $(lastword $(MAKEFILE_LIST))
+CLEAN_FILES :=
+DISTCLEAN_FILES := ./--dummy--
+default: all
+release: oz
+
+# Emscripten SDK home dir and related binaries...
+EMSDK_HOME ?= $(word 1,$(wildcard $(HOME)/emsdk $(HOME)/src/emsdk))
+emcc.bin ?= $(word 1,$(wildcard $(EMSDK_HOME)/upstream/emscripten/emcc) $(shell which emcc))
+ifeq (,$(emcc.bin))
+ $(error Cannot find emcc.)
+endif
+
+wasm-strip ?= $(shell which wasm-strip 2>/dev/null)
+ifeq (,$(filter clean,$(MAKECMDGOALS)))
+ifeq (,$(wasm-strip))
+ $(info WARNING: *******************************************************************)
+ $(info WARNING: builds using -O2/-O3/-Os/-Oz will minify WASM-exported names,)
+ $(info WARNING: breaking _All The Things_. The workaround for that is to build)
+ $(info WARNING: with -g3 (which explodes the file size) and then strip the debug)
+ $(info WARNING: info after compilation, using wasm-strip, to shrink the wasm file.)
+ $(info WARNING: wasm-strip was not found in the PATH so we cannot strip those.)
+ $(info WARNING: If this build uses any optimization level higher than -O1 then)
+ $(info WARNING: the ***resulting JS code WILL NOT BE USABLE***.)
+ $(info WARNING: wasm-strip is part of the wabt package:)
+ $(info WARNING: https://github.com/WebAssembly/wabt)
+ $(info WARNING: on Ubuntu-like systems it can be installed with:)
+ $(info WARNING: sudo apt install wabt)
+ $(info WARNING: *******************************************************************)
+endif
+endif # 'make clean' check
+
+ifeq (,$(wasm-strip))
+ maybe-wasm-strip = echo "not wasm-stripping"
+else
+ maybe-wasm-strip = $(wasm-strip)
+endif
+
+dir.top := ../..
+# Reminder: some Emscripten flags require absolute paths but we want
+# relative paths for most stuff simply to reduce noise. The
+# $(abspath...) GNU make function can transform relative paths to
+# absolute.
+dir.wasm := $(patsubst %/,%,$(dir $(MAKEFILE)))
+dir.api := api
+dir.jacc := jaccwabyt
+dir.common := common
+dir.fiddle := fiddle
+dir.tool := $(dir.top)/tool
+########################################################################
+# dir.dout = output dir for deliverables.
+#
+# MAINTENANCE REMINDER: the output .js and .wasm files of emcc must be
+# in _this_ dir, rather than a subdir, or else parts of the generated
+# code get confused and cannot load property. Specifically, when X.js
+# loads X.wasm, whether or not X.js uses the correct path for X.wasm
+# depends on how it's loaded: an HTML script tag will resolve it
+# intuitively, whereas a Worker's call to importScripts() will not.
+# That's a fundamental incompatibility with how URL resolution in
+# JS happens between those two contexts. See:
+#
+# https://zzz.buzz/2017/03/14/relative-uris-in-web-development/
+#
+# We unfortunately have no way, from Worker-initiated code, to
+# automatically resolve the path from X.js to X.wasm.
+#
+# We have an "only slightly unsightly" solution for our main builds
+# but it does not work for the WASMFS builds, so those builds have to
+# be built to _this_ directory and can only run when the client app is
+# loaded from the same directory.
+dir.dout := $(dir.wasm)/jswasm
+# dir.tmp = output dir for intermediary build files, as opposed to
+# end-user deliverables.
+dir.tmp := $(dir.wasm)/bld
+CLEAN_FILES += $(dir.tmp)/* $(dir.dout)/*
+ifeq (,$(wildcard $(dir.dout)))
+ dir._tmp := $(shell mkdir -p $(dir.dout))
+endif
+ifeq (,$(wildcard $(dir.tmp)))
+ dir._tmp := $(shell mkdir -p $(dir.tmp))
+endif
+
+cflags.common := -I. -I.. -I$(dir.top)
+CLEAN_FILES += *~ $(dir.jacc)/*~ $(dir.api)/*~ $(dir.common)/*~
+emcc.WASM_BIGINT ?= 1
+sqlite3.c := $(dir.top)/sqlite3.c
+sqlite3.h := $(dir.top)/sqlite3.h
+SQLITE_OPT = \
+ -DSQLITE_ENABLE_FTS4 \
+ -DSQLITE_ENABLE_RTREE \
+ -DSQLITE_ENABLE_EXPLAIN_COMMENTS \
+ -DSQLITE_ENABLE_UNKNOWN_SQL_FUNCTION \
+ -DSQLITE_ENABLE_STMTVTAB \
+ -DSQLITE_ENABLE_DBPAGE_VTAB \
+ -DSQLITE_ENABLE_DBSTAT_VTAB \
+ -DSQLITE_ENABLE_BYTECODE_VTAB \
+ -DSQLITE_ENABLE_OFFSET_SQL_FUNC \
+ -DSQLITE_OMIT_LOAD_EXTENSION \
+ -DSQLITE_OMIT_DEPRECATED \
+ -DSQLITE_OMIT_UTF16 \
+ -DSQLITE_OMIT_SHARED_CACHE \
+ -DSQLITE_OMIT_WAL \
+ -DSQLITE_THREADSAFE=0 \
+ -DSQLITE_TEMP_STORE=3 \
+ -DSQLITE_OS_KV_OPTIONAL=1 \
+ '-DSQLITE_DEFAULT_UNIX_VFS="unix-none"' \
+ -DSQLITE_USE_URI=1 \
+ -DSQLITE_WASM_ENABLE_C_TESTS
+# ^^^ most flags are set in sqlite3-wasm.c but we need them
+# made explicit here for building speedtest1.c.
+
+ifneq (,$(filter release,$(MAKECMDGOALS)))
+emcc_opt ?= -Oz -flto
+else
+emcc_opt ?= -O0
+# ^^^^ build times for -O levels higher than 0 are painful at
+# dev-time.
+endif
+# When passing emcc_opt from the CLI, += and re-assignment have no
+# effect, so emcc_opt+=-g3 doesn't work. So...
+emcc_opt_full := $(emcc_opt) -g3
+# ^^^ ALWAYS use -g3. See below for why.
+#
+# ^^^ -flto improves runtime speed at -O0 considerably but doubles
+# build time.
+#
+# ^^^^ -O3, -Oz, -Os minify symbol names and there appears to be no
+# way around that except to use -g3, but -g3 causes the binary file
+# size to absolutely explode (approx. 5x larger). This minification
+# utterly breaks the resulting module, making it unsable except as
+# self-contained/self-referential-only code, as ALL of the exported
+# symbols get minified names.
+#
+# However, we have an option for using -Oz or -Os:
+#
+# Build with (-Os -g3) or (-Oz -g3) then use wasm-strip, from the wabt
+# tools package (https://github.com/WebAssembly/wabt), to strip the
+# debugging symbols. That results in a small build with unmangled
+# symbol names. -Oz gives ever-so-slightly better compression than
+# -Os: not quite 1% in some completely unscientific tests. Runtime
+# speed for the unit tests is all over the place either way so it's
+# difficult to say whether -Os gives any speed benefit over -Oz.
+#
+# (Much later: -O2 consistently gives the best speeds.)
+########################################################################
+
+
+$(sqlite3.c) $(sqlite3.h):
+ $(MAKE) -C $(dir.top) sqlite3.c
+
+.PHONY: clean distclean
+clean:
+ -rm -f $(CLEAN_FILES)
+distclean: clean
+ -rm -f $(DISTCLEAN_FILES)
+
+ifeq (release,$(filter release,$(MAKECMDGOALS)))
+ ifeq (,$(wasm-strip))
+ $(error Cannot make release-quality binary because wasm-strip is not available. \
+ See notes in the warning above)
+ endif
+else
+ $(info Development build. Use '$(MAKE) release' for a smaller release build.)
+endif
+
+bin.version-info := $(dir.wasm)/version-info
+# ^^^^ NOT in $(dir.tmp) because we need it to survive the cleanup
+# process for the dist build to work properly.
+$(bin.version-info): $(dir.wasm)/version-info.c $(sqlite3.h) $(MAKEFILE)
+ $(CC) -O0 -I$(dir.top) -o $@ $<
+DISTCLEAN_FILES += $(bin.version-info)
+
+bin.stripccomments := $(dir.tool)/stripccomments
+$(bin.stripccomments): $(bin.stripccomments).c $(MAKEFILE)
+ $(CC) -o $@ $<
+DISTCLEAN_FILES += $(bin.stripccomments)
+
+EXPORTED_FUNCTIONS.api.in := $(abspath $(dir.api)/EXPORTED_FUNCTIONS.sqlite3-api)
+EXPORTED_FUNCTIONS.api := $(dir.tmp)/EXPORTED_FUNCTIONS.api
+$(EXPORTED_FUNCTIONS.api): $(EXPORTED_FUNCTIONS.api.in) $(MAKEFILE)
+ cat $(EXPORTED_FUNCTIONS.api.in) > $@
+
+sqlite3-license-version.js := $(dir.tmp)/sqlite3-license-version.js
+sqlite3-license-version-header.js := $(dir.api)/sqlite3-license-version-header.js
+sqlite3-api-build-version.js := $(dir.tmp)/sqlite3-api-build-version.js
+# sqlite3-api.jses = the list of JS files which make up $(sqlite3-api.js), in
+# the order they need to be assembled.
+sqlite3-api.jses := $(sqlite3-license-version.js)
+sqlite3-api.jses += $(dir.api)/sqlite3-api-prologue.js
+sqlite3-api.jses += $(dir.common)/whwasmutil.js
+sqlite3-api.jses += $(dir.jacc)/jaccwabyt.js
+sqlite3-api.jses += $(dir.api)/sqlite3-api-glue.js
+sqlite3-api.jses += $(sqlite3-api-build-version.js)
+sqlite3-api.jses += $(dir.api)/sqlite3-api-oo1.js
+sqlite3-api.jses += $(dir.api)/sqlite3-api-worker1.js
+sqlite3-api.jses += $(dir.api)/sqlite3-api-opfs.js
+sqlite3-api.jses += $(dir.api)/sqlite3-api-cleanup.js
+
+# "External" API files which are part of our distribution
+# but not part of the sqlite3-api.js amalgamation.
+SOAP.js := $(dir.api)/sqlite3-opfs-async-proxy.js
+sqlite3-worker1.js := $(dir.api)/sqlite3-worker1.js
+sqlite3-worker1-promiser.js := $(dir.api)/sqlite3-worker1-promiser.js
+define CP_XAPI
+sqlite3-api.ext.jses += $$(dir.dout)/$$(notdir $(1))
+$$(dir.dout)/$$(notdir $(1)): $(1) $$(MAKEFILE)
+ cp $$< $$@
+endef
+$(foreach X,$(SOAP.js) $(sqlite3-worker1.js) $(sqlite3-worker1-promiser.js),\
+ $(eval $(call CP_XAPI,$(X))))
+all: $(sqlite3-api.ext.jses)
+
+sqlite3-api.js := $(dir.tmp)/sqlite3-api.js
+$(sqlite3-api.js): $(sqlite3-api.jses) $(MAKEFILE)
+ @echo "Making $@..."
+ @for i in $(sqlite3-api.jses); do \
+ echo "/* BEGIN FILE: $$i */"; \
+ cat $$i; \
+ echo "/* END FILE: $$i */"; \
+ done > $@
+
+$(sqlite3-api-build-version.js): $(bin.version-info) $(MAKEFILE)
+ @echo "Making $@..."
+ @{ \
+ echo 'self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){'; \
+ echo -n ' sqlite3.version = '; \
+ $(bin.version-info) --json; \
+ echo ';'; \
+ echo '});'; \
+ } > $@
+
+########################################################################
+# --post-js and --pre-js are emcc flags we use to append/prepend JS to
+# the generated emscripten module file.
+pre-js.js := $(dir.api)/pre-js.js
+post-js.js := $(dir.tmp)/post-js.js
+post-jses := \
+ $(dir.api)/post-js-header.js \
+ $(sqlite3-api.js) \
+ $(dir.api)/post-js-footer.js
+$(post-js.js): $(post-jses) $(MAKEFILE)
+ @echo "Making $@..."
+ @for i in $(post-jses); do \
+ echo "/* BEGIN FILE: $$i */"; \
+ cat $$i; \
+ echo "/* END FILE: $$i */"; \
+ done > $@
+extern-post-js.js := $(dir.api)/extern-post-js.js
+extern-pre-js.js := $(dir.api)/extern-pre-js.js
+pre-post-common.flags := \
+ --post-js=$(post-js.js) \
+ --extern-post-js=$(extern-post-js.js) \
+ --extern-pre-js=$(sqlite3-license-version.js)
+pre-post-jses.deps := $(post-js.js) \
+ $(extern-post-js.js) $(extern-pre-js.js) $(sqlite3-license-version.js)
+$(sqlite3-license-version.js): $(sqlite3.h) $(sqlite3-license-version-header.js) $(MAKEFILE)
+ @echo "Making $@..."; { \
+ cat $(sqlite3-license-version-header.js); \
+ echo '/*'; \
+ echo '** This code was built from sqlite3 version...'; \
+ echo "** "; \
+ awk -e '/define SQLITE_VERSION/{$$1=""; print "**" $$0}' \
+ -e '/define SQLITE_SOURCE_ID/{$$1=""; print "**" $$0}' $(sqlite3.h); \
+ echo '*/'; \
+ } > $@
+
+########################################################################
+# call-make-pre-js creates rules for pre-js-$(1).js. $1 = the base
+# name of the JS file on whose behalf this pre-js is for.
+define call-make-pre-js
+pre-post-$(1).flags ?=
+$$(dir.tmp)/pre-js-$(1).js: $$(pre-js.js) $$(MAKEFILE)
+ cp $$(pre-js.js) $$@
+ @if [ sqlite3-wasmfs = $(1) ]; then \
+ echo "delete Module[xNameOfInstantiateWasm] /*for WASMFS build*/;"; \
+ elif [ sqlite3 != $(1) ]; then \
+ echo "Module[xNameOfInstantiateWasm].uri = '$(1).wasm';"; \
+ fi >> $$@
+pre-post-$(1).deps := $$(pre-post-jses.deps) $$(dir.tmp)/pre-js-$(1).js
+pre-post-$(1).flags += --pre-js=$$(dir.tmp)/pre-js-$(1).js
+endef
+#$(error $(call call-make-pre-js,sqlite3-wasmfs))
+# /post-js and pre-js
+########################################################################
+
+########################################################################
+# emcc flags for .c/.o/.wasm/.js.
+emcc.flags :=
+#emcc.flags += -v # _very_ loud but also informative about what it's doing
+# -g3 is needed to keep -O2 and higher from creating broken JS via
+# minification.
+
+########################################################################
+# emcc flags for .c/.o.
+emcc.cflags :=
+emcc.cflags += -std=c99 -fPIC
+# -------------^^^^^^^^ we currently need c99 for WASM-specific sqlite3 APIs.
+emcc.cflags += -I. -I$(dir.top)
+
+########################################################################
+# emcc flags specific to building the final .js/.wasm file...
+emcc.jsflags := -fPIC
+emcc.jsflags += --minify 0
+emcc.jsflags += --no-entry
+emcc.jsflags += -sMODULARIZE
+emcc.jsflags += -sSTRICT_JS
+emcc.jsflags += -sDYNAMIC_EXECUTION=0
+emcc.jsflags += -sNO_POLYFILL
+emcc.jsflags += -sEXPORTED_FUNCTIONS=@$(EXPORTED_FUNCTIONS.api)
+emcc.exportedRuntimeMethods := \
+ -sEXPORTED_RUNTIME_METHODS=FS,wasmMemory
+ # FS ==> stdio/POSIX I/O proxies
+ # wasmMemory ==> required by our code for use with -sIMPORTED_MEMORY
+emcc.jsflags += $(emcc.exportedRuntimeMethods)
+emcc.jsflags += -sUSE_CLOSURE_COMPILER=0
+emcc.jsflags += -sIMPORTED_MEMORY
+emcc.environment := -sENVIRONMENT=web,worker
+########################################################################
+# -sINITIAL_MEMORY: How much memory we need to start with is governed
+# at least in part by whether -sALLOW_MEMORY_GROWTH is enabled. If so,
+# we can start with less. If not, we need as much as we'll ever
+# possibly use (which, of course, we can't know for sure). Note,
+# however, that speedtest1 shows that performance for even moderate
+# workloads MAY suffer considerably if we start small and have to grow
+# at runtime. e.g. OPFS-backed (speedtest1 --size 75) take MAY take X
+# time with 16mb+ memory and 3X time when starting with 8MB. However,
+# such test results are inconsistent due to browser internals which
+# are opaque to us.
+emcc.jsflags += -sALLOW_MEMORY_GROWTH
+emcc.INITIAL_MEMORY.128 := 13107200
+emcc.INITIAL_MEMORY.96 := 100663296
+emcc.INITIAL_MEMORY.64 := 64225280
+emcc.INITIAL_MEMORY.32 := 33554432
+emcc.INITIAL_MEMORY.16 := 16777216
+emcc.INITIAL_MEMORY.8 := 8388608
+emcc.INITIAL_MEMORY ?= 16
+ifeq (,$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY)))
+$(error emcc.INITIAL_MEMORY must be one of: 8, 16, 32, 64, 96, 128 (megabytes))
+endif
+emcc.jsflags += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY))
+# /INITIAL_MEMORY
+########################################################################
+
+emcc.jsflags += $(emcc.environment)
+#emcc.jsflags += -sTOTAL_STACK=4194304
+
+sqlite3.js.init-func := sqlite3InitModule
+# ^^^^ $(sqlite3.js.init-func) symbol name is hard-coded in
+# $(extern-post-js.js) as well as in numerous docs. If changed, it
+# needs to be globally modified in *.js and all related documentation.
+
+emcc.jsflags += -sEXPORT_NAME=$(sqlite3.js.init-func)
+emcc.jsflags += -sGLOBAL_BASE=4096 # HYPOTHETICALLY keep func table indexes from overlapping w/ heap addr.
+#emcc.jsflags += -sSTRICT # fails due to missing __syscall_...()
+#emcc.jsflags += -sALLOW_UNIMPLEMENTED_SYSCALLS
+#emcc.jsflags += -sFILESYSTEM=0 # only for experimentation. sqlite3 needs the FS API
+#emcc.jsflags += -sABORTING_MALLOC
+emcc.jsflags += -sALLOW_TABLE_GROWTH
+# -sALLOW_TABLE_GROWTH is required for installing new SQL UDFs
+emcc.jsflags += -Wno-limited-postlink-optimizations
+# ^^^^^ it likes to warn when we have "limited optimizations" via the -g3 flag.
+#emcc.jsflags += -sSTANDALONE_WASM # causes OOM errors, not sure why
+# https://lld.llvm.org/WebAssembly.html
+emcc.jsflags += -sERROR_ON_UNDEFINED_SYMBOLS=0
+emcc.jsflags += -sLLD_REPORT_UNDEFINED
+#emcc.jsflags += --allow-undefined
+#emcc.jsflags += --import-undefined
+#emcc.jsflags += --unresolved-symbols=import-dynamic --experimental-pic
+#emcc.jsflags += --experimental-pic --unresolved-symbols=ingore-all --import-undefined
+#emcc.jsflags += --unresolved-symbols=ignore-all
+emcc.jsflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT)
+
+########################################################################
+# -sMEMORY64=1 fails to load, erroring with:
+# invalid memory limits flags 0x5
+# (enable via --experimental-wasm-memory64)
+#
+# ^^^^ MEMORY64=2 builds and loads but dies when we do things like:
+#
+# new Uint8Array(wasm.heap8u().buffer, ptr, n)
+#
+# because ptr is now a BigInt, so is invalid for passing to arguments
+# which have strict must-be-a-Number requirements.
+########################################################################
+
+
+########################################################################
+# -sSINGLE_FILE:
+# https://github.com/emscripten-core/emscripten/blob/main/src/settings.js#L1704
+# -sSINGLE_FILE=1 would be really nice but we have to build with -g3
+# for -O2 and higher to work (else minification breaks the code) and
+# cannot wasm-strip the binary before it gets encoded into the JS
+# file. The result is that the generated JS file is, because of the -g3
+# debugging info, _huge_.
+########################################################################
+
+########################################################################
+# AN EXPERIMENT: undocumented Emscripten feature: if the target file
+# extension is "mjs", it defaults to ES6 module builds:
+# https://github.com/emscripten-core/emscripten/issues/14383
+ifeq (,$(filter esm,$(MAKECMDGOALS)))
+sqlite3.js.ext := js
+else
+esm.deps := $(filter-out esm,$(MAKECMDGOALS))
+esm: $(if $(esm.deps),$(esm.deps),all)
+sqlite3.js.ext := mjs
+endif
+# /esm
+########################################################################
+sqlite3.js := $(dir.dout)/sqlite3.$(sqlite3.js.ext)
+sqlite3.wasm := $(dir.dout)/sqlite3.wasm
+sqlite3-wasm.c := $(dir.api)/sqlite3-wasm.c
+# sqlite3-wasm.o vs sqlite3-wasm.c: building against the latter
+# (predictably) results in a slightly faster binary, but we're close
+# enough to the target speed requirements that the 500ms makes a
+# difference. Thus we build all binaries against sqlite3-wasm.c
+# instead of building a shared copy of sqlite3-wasm.o.
+$(eval $(call call-make-pre-js,sqlite3))
+$(sqlite3.js):
+$(sqlite3.js): $(MAKEFILE) $(sqlite3.wasm.obj) \
+ $(EXPORTED_FUNCTIONS.api) \
+ $(pre-post-sqlite3.deps)
+ @echo "Building $@ ..."
+ $(emcc.bin) -o $@ $(emcc_opt_full) $(emcc.flags) \
+ $(emcc.jsflags) $(pre-post-common.flags) $(pre-post-sqlite3.flags) \
+ $(cflags.common) $(SQLITE_OPT) $(sqlite3-wasm.c)
+ chmod -x $(sqlite3.wasm)
+ $(maybe-wasm-strip) $(sqlite3.wasm)
+ @ls -la $@ $(sqlite3.wasm)
+$(sqlite3.wasm): $(sqlite3.js)
+CLEAN_FILES += $(sqlite3.js) $(sqlite3.wasm)
+all: $(sqlite3.js)
+wasm: $(sqlite3.js)
+# End main Emscripten-based module build
+########################################################################
+
+########################################################################
+# batch-runner.js...
+dir.sql := sql
+speedtest1 := ../../speedtest1
+speedtest1.c := ../../test/speedtest1.c
+speedtest1.sql := $(dir.sql)/speedtest1.sql
+speedtest1.cliflags := --size 25 --big-transactions
+$(speedtest1):
+ $(MAKE) -C ../.. speedtest1
+$(speedtest1.sql): $(speedtest1) $(MAKEFILE)
+ $(speedtest1) $(speedtest1.cliflags) --script $@
+batch-runner.list: $(MAKEFILE) $(speedtest1.sql) $(dir.sql)/000-mandelbrot.sql
+ bash split-speedtest1-script.sh $(dir.sql)/speedtest1.sql
+ ls -1 $(dir.sql)/*.sql | grep -v speedtest1.sql | sort > $@
+clean-batch:
+ rm -f batch-runner.list $(dir.sql)/speedtest1*.sql
+# ^^^ we don't do this along with 'clean' because we clean/rebuild on
+# a regular basis with different -Ox flags and rebuilding the batch
+# pieces each time is an unnecessary time sink.
+batch: batch-runner.list
+all: batch
+# end batch-runner.js
+########################################################################
+# speedtest1.js...
+# speedtest1-common.eflags = emcc flags used by multiple builds of speedtest1
+# speedtest1.eflags = emcc flags used by main build of speedtest1
+speedtest1-common.eflags := $(emcc_opt_full)
+speedtest1.eflags :=
+speedtest1.eflags += -sENVIRONMENT=web
+speedtest1.eflags += -sALLOW_MEMORY_GROWTH
+speedtest1.eflags += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.$(emcc.INITIAL_MEMORY))
+speedtest1-common.eflags += -sINVOKE_RUN=0
+speedtest1-common.eflags += --no-entry
+#speedtest1-common.eflags += -flto
+speedtest1-common.eflags += -sABORTING_MALLOC
+speedtest1-common.eflags += -sSTRICT_JS
+speedtest1-common.eflags += -sMODULARIZE
+speedtest1-common.eflags += -Wno-limited-postlink-optimizations
+EXPORTED_FUNCTIONS.speedtest1 := $(abspath $(dir.tmp)/EXPORTED_FUNCTIONS.speedtest1)
+speedtest1-common.eflags += -sEXPORTED_FUNCTIONS=@$(EXPORTED_FUNCTIONS.speedtest1)
+speedtest1-common.eflags += $(emcc.exportedRuntimeMethods)
+speedtest1-common.eflags += -sALLOW_TABLE_GROWTH
+speedtest1-common.eflags += -sDYNAMIC_EXECUTION=0
+speedtest1-common.eflags += --minify 0
+speedtest1-common.eflags += -sEXPORT_NAME=$(sqlite3.js.init-func)
+speedtest1-common.eflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT)
+speedtest1-common.eflags += $(pre-post-common.flags)
+speedtest1.exit-runtime0 := -sEXIT_RUNTIME=0
+speedtest1.exit-runtime1 := -sEXIT_RUNTIME=1
+# Re -sEXIT_RUNTIME=1 vs 0: if it's 1 and speedtest1 crashes, we get
+# this error from emscripten:
+#
+# > native function `free` called after runtime exit (use
+# NO_EXIT_RUNTIME to keep it alive after main() exits))
+#
+# If it's 0 and it crashes, we get:
+#
+# > stdio streams had content in them that was not flushed. you should
+# set EXIT_RUNTIME to 1 (see the FAQ), or make sure to emit a newline
+# when you printf etc.
+#
+# and pending output is not flushed because it didn't end with a
+# newline (by design). The lesser of the two evils seems to be
+# -sEXIT_RUNTIME=1 but we need EXIT_RUNTIME=0 for the worker-based app
+# which runs speedtest1 multiple times.
+
+$(EXPORTED_FUNCTIONS.speedtest1): $(EXPORTED_FUNCTIONS.api)
+ @echo "Making $@ ..."
+ @{ echo _wasm_main; cat $(EXPORTED_FUNCTIONS.api); } > $@
+speedtest1.js := $(dir.dout)/speedtest1.js
+speedtest1.wasm := $(subst .js,.wasm,$(speedtest1.js))
+speedtest1.cflags := $(cflags.common) -DSQLITE_SPEEDTEST1_WASM
+speedtest1.cses := $(speedtest1.c) $(sqlite3-wasm.c)
+$(eval $(call call-make-pre-js,speedtest1))
+$(speedtest1.js): $(MAKEFILE) $(speedtest1.cses) \
+ $(pre-post-speedtest1.deps) \
+ $(EXPORTED_FUNCTIONS.speedtest1)
+ @echo "Building $@ ..."
+ $(emcc.bin) \
+ $(speedtest1.eflags) $(speedtest1-common.eflags) $(speedtest1.cflags) \
+ $(pre-post-speedtest1.flags) \
+ $(SQLITE_OPT) \
+ $(speedtest1.exit-runtime0) \
+ -o $@ $(speedtest1.cses) -lm
+ $(maybe-wasm-strip) $(speedtest1.wasm)
+ ls -la $@ $(speedtest1.wasm)
+
+speedtest1: $(speedtest1.js)
+all: speedtest1
+CLEAN_FILES += $(speedtest1.js) $(speedtest1.wasm)
+# end speedtest1.js
+########################################################################
+
+########################################################################
+# Convenience rules to rebuild with various -Ox levels. Much
+# experimentation shows -O2 to be the clear winner in terms of speed.
+# Note that build times with anything higher than -O0 are somewhat
+# painful.
+
+.PHONY: o0 o1 o2 o3 os oz
+o-xtra := -flto
+# ^^^^ -flto can have a considerably performance boost at -O0 but
+# doubles the build time and seems to have negligible effect on
+# higher optimization levels.
+o0: clean
+ $(MAKE) -e "emcc_opt=-O0"
+o1: clean
+ $(MAKE) -e "emcc_opt=-O1 $(o-xtra)"
+o2: clean
+ $(MAKE) -e "emcc_opt=-O2 $(o-xtra)"
+o3: clean
+ $(MAKE) -e "emcc_opt=-O3 $(o-xtra)"
+os: clean
+ @echo "WARNING: -Os can result in a build with mysteriously missing pieces!"
+ $(MAKE) -e "emcc_opt=-Os $(o-xtra)"
+oz: clean
+ $(MAKE) -e "emcc_opt=-Oz $(o-xtra)"
+
+########################################################################
+# Sub-makes...
+
+include fiddle.make
+
+# Only add wasmfs if wasmfs.enable=1 or we're running (dist)clean
+wasmfs.enable ?= $(if $(filter %clean,$(MAKECMDGOALS)),1,0)
+ifeq (1,$(wasmfs.enable))
+# wasmfs build disabled 2022-10-19 per /chat discussion.
+# OPFS-over-wasmfs was initially a stopgap measure and a convenient
+# point of comparison for the OPFS sqlite3_vfs's performance, but it
+# currently doubles our deliverables and build maintenance burden for
+# little, if any, benefit.
+#
+########################################################################
+# Some platforms do not support the WASMFS build. Raspberry Pi OS is one
+# of them. As such platforms are discovered, add their (uname -m) name
+# to PLATFORMS_WITH_NO_WASMFS to exclude the wasmfs build parts.
+PLATFORMS_WITH_NO_WASMFS := aarch64 # add any others here
+THIS_ARCH := $(shell /usr/bin/uname -m)
+ifneq (,$(filter $(THIS_ARCH),$(PLATFORMS_WITH_NO_WASMFS)))
+$(info This platform does not support the WASMFS build.)
+HAVE_WASMFS := 0
+else
+HAVE_WASMFS := 1
+include wasmfs.make
+endif
+endif
+# /wasmfs
+########################################################################
+
+########################################################################
+# Create deliverables:
+ifneq (,$(filter dist,$(MAKECMDGOALS)))
+include dist.make
+endif
+
+########################################################################
+# Push files to public wasm-testing.sqlite.org server
+wasm-testing.include = $(dir.dout) *.js *.html \
+ batch-runner.list $(dir.sql) $(dir.common) $(dir.fiddle) $(dir.jacc)
+wasm-testing.exclude = sql/speedtest1.sql
+wasm-testing.dir = /jail/sites/wasm-testing
+wasm-testing.dest ?= wasm-testing:$(wasm-testing.dir)
+# ---------------------^^^^^^^^^^^^ ssh alias
+.PHONY: push-testing
+push-testing:
+ rsync -z -e ssh --ignore-times --chown=stephan:www-data --group -r \
+ $(patsubst %,--exclude=%,$(wasm-testing.exclude)) \
+ $(wasm-testing.include) $(wasm-testing.dest)
+ @echo "Updating gzipped copies..."; \
+ ssh wasm-testing 'cd $(wasm-testing.dir) && bash .gzip' || \
+ echo "SSH failed: it's likely that stale content will be served via old gzip files."
+
+########################################################################
+# If we find a copy of the sqlite.org/wasm docs checked out, copy
+# certain files over to it, noting that some need automatable edits...
+WDOCS.home ?= ../../../wdoc
+.PHONY: update-docs
+ifneq (,$(wildcard $(WDOCS.home)/api-index.md))
+WDOCS.jswasm := $(WDOCS.home)/jswasm
+update-docs: $(bin.stripccomments) $(sqlite3.js) $(sqlite3.wasm)
+ @echo "Copying files to the /wasm docs. Be sure to use an -Oz build for this!"
+ cp $(sqlite3.wasm) $(WDOCS.jswasm)/.
+ $(bin.stripccomments) -k -k < $(sqlite3.js) \
+ | sed -e '/^[ \t]*$$/d' > $(WDOCS.jswasm)/sqlite3.js
+ cp demo-123.js demo-123.html demo-123-worker.html $(WDOCS.home)
+ sed -n -e '/EXTRACT_BEGIN/,/EXTRACT_END/p' \
+ module-symbols.html > $(WDOCS.home)/module-symbols.html
+else
+update-docs:
+ @echo "Cannot find wasm docs checkout."; \
+ echo "Pass WDOCS.home=/path/to/wasm/docs/checkout or edit this makefile to suit."; \
+ exit 127
+endif
+# end /wasm docs
+########################################################################
diff --git a/ext/wasm/README-dist.txt b/ext/wasm/README-dist.txt
new file mode 100644
index 0000000..ca6bef9
--- /dev/null
+++ b/ext/wasm/README-dist.txt
@@ -0,0 +1,23 @@
+This is the README for the sqlite3 WASM/JS distribution.
+
+Main project page: https://sqlite.org
+
+Documentation: https://sqlite.org/wasm
+
+This archive contains the sqlite3.js and sqlite3.wasm file which make
+up the sqlite3 WASM/JS build.
+
+The jswasm directory contains the core sqlite3 deliverables and the
+top-level directory contains demonstration and test apps. Browsers
+will not serve WASM files from file:// URLs, so the demo/test apps
+require a web server and that server must include the following
+headers in its response when serving the files:
+
+ Cross-Origin-Opener-Policy: same-origin
+ Cross-Origin-Embedder-Policy: require-corp
+
+One simple way to get the demo apps up and running on Unix-style
+systems is to install althttpd (https://sqlite.org/althttpd) and run:
+
+ althttpd --enable-sab --page index.html
+
diff --git a/ext/wasm/README.md b/ext/wasm/README.md
new file mode 100644
index 0000000..e8d6686
--- /dev/null
+++ b/ext/wasm/README.md
@@ -0,0 +1,105 @@
+This directory houses the [Web Assembly (WASM)](https://en.wikipedia.org/wiki/WebAssembly)
+parts of the sqlite3 build.
+
+It requires [emscripten][] and that the build environment be set up for
+emscripten. A mini-HOWTO for setting that up follows...
+
+First, install the Emscripten SDK, as documented
+[here](https://emscripten.org/docs/getting_started/downloads.html) and summarized
+below for Linux environments:
+
+```
+# Clone the emscripten repository:
+$ sudo apt install git
+$ git clone https://github.com/emscripten-core/emsdk.git
+$ cd emsdk
+
+# Download and install the latest SDK tools:
+$ ./emsdk install latest
+
+# Make the "latest" SDK "active" for the current user:
+$ ./emsdk activate latest
+```
+
+Those parts only need to be run once, but the SDK can be updated using:
+
+```
+$ git pull
+$ ./emsdk install latest
+$ ./emsdk activate latest
+```
+
+The following needs to be run for each shell instance which needs the
+`emcc` compiler:
+
+```
+# Activate PATH and other environment variables in the current terminal:
+$ source ./emsdk_env.sh
+
+$ which emcc
+/path/to/emsdk/upstream/emscripten/emcc
+```
+
+Optionally, add that to your login shell's resource file (`~/.bashrc`
+or equivalent).
+
+That `env` script needs to be sourced for building this application
+from the top of the sqlite3 build tree:
+
+```
+$ make fiddle
+```
+
+Or:
+
+```
+$ cd ext/wasm
+$ make
+```
+
+That will generate the a number of files required for a handful of
+test and demo applications which can be accessed via
+`index.html`. WASM content cannot, due to XMLHttpRequest security
+limitations, be loaded if the containing HTML file is opened directly
+in the browser (i.e. if it is opened using a `file://` URL), so it
+needs to be served via an HTTP server. For example, using
+[althttpd][]:
+
+```
+$ cd ext/wasm
+$ althttpd --enable-sab --max-age 1 --page index.html
+```
+
+That will open the system's browser and run the index page, from which
+all of the test and demo applications can be accessed.
+
+Note that when serving this app via [althttpd][], it must be a version
+from 2022-09-26 or newer so that it recognizes the `--enable-sab`
+flag, which causes althttpd to emit two HTTP response headers which
+are required to enable JavaScript's `SharedArrayBuffer` and `Atomics`
+APIs. Those APIs are required in order to enable the OPFS-related
+features in the apps which use them.
+
+# Testing on a remote machine that is accessed via SSH
+
+*NB: The following are developer notes, last validated on 2022-08-18*
+
+ * Remote: Install git, emsdk, and althttpd
+ * Use a [version of althttpd][althttpd] from
+ September 26, 2022 or newer.
+ * Remote: Install the SQLite source tree. CD to ext/wasm
+ * Remote: "`make`" to build WASM
+ * Remote: `althttpd --enable-sab --port 8080 --popup`
+ * Local: `ssh -L 8180:localhost:8080 remote`
+ * Local: Point your web-browser at http://localhost:8180/index.html
+
+In order to enable [SharedArrayBuffers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer),
+the web-browser requires that the two extra Cross-Origin lines be present
+in HTTP reply headers and that the request must come from "localhost".
+Since the web-server is on a different machine from
+the web-broser, the localhost requirement means that the connection must be tunneled
+using SSH.
+
+
+[emscripten]: https://emscripten.org
+[althttpd]: https://sqlite.org/althttpd
diff --git a/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
new file mode 100644
index 0000000..b903bed
--- /dev/null
+++ b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
@@ -0,0 +1,94 @@
+_sqlite3_aggregate_context
+_sqlite3_bind_blob
+_sqlite3_bind_double
+_sqlite3_bind_int
+_sqlite3_bind_int64
+_sqlite3_bind_null
+_sqlite3_bind_parameter_count
+_sqlite3_bind_parameter_index
+_sqlite3_bind_text
+_sqlite3_changes
+_sqlite3_changes64
+_sqlite3_clear_bindings
+_sqlite3_close_v2
+_sqlite3_column_blob
+_sqlite3_column_bytes
+_sqlite3_column_count
+_sqlite3_column_count
+_sqlite3_column_double
+_sqlite3_column_int
+_sqlite3_column_int64
+_sqlite3_column_name
+_sqlite3_column_text
+_sqlite3_column_type
+_sqlite3_compileoption_get
+_sqlite3_compileoption_used
+_sqlite3_create_function
+_sqlite3_create_function_v2
+_sqlite3_create_window_function
+_sqlite3_data_count
+_sqlite3_db_filename
+_sqlite3_db_handle
+_sqlite3_db_name
+_sqlite3_deserialize
+_sqlite3_errmsg
+_sqlite3_error_offset
+_sqlite3_errstr
+_sqlite3_exec
+_sqlite3_expanded_sql
+_sqlite3_extended_errcode
+_sqlite3_extended_result_codes
+_sqlite3_file_control
+_sqlite3_finalize
+_sqlite3_free
+_sqlite3_initialize
+_sqlite3_libversion
+_sqlite3_libversion_number
+_sqlite3_malloc
+_sqlite3_malloc64
+_sqlite3_msize
+_sqlite3_open
+_sqlite3_open_v2
+_sqlite3_prepare_v2
+_sqlite3_prepare_v3
+_sqlite3_randomness
+_sqlite3_realloc
+_sqlite3_realloc64
+_sqlite3_reset
+_sqlite3_result_blob
+_sqlite3_result_double
+_sqlite3_result_error
+_sqlite3_result_error_code
+_sqlite3_result_error_nomem
+_sqlite3_result_error_toobig
+_sqlite3_result_int
+_sqlite3_result_int64
+_sqlite3_result_null
+_sqlite3_result_text
+_sqlite3_serialize
+_sqlite3_shutdown
+_sqlite3_sourceid
+_sqlite3_sql
+_sqlite3_step
+_sqlite3_strglob
+_sqlite3_strlike
+_sqlite3_total_changes
+_sqlite3_total_changes64
+_sqlite3_trace_v2
+_sqlite3_uri_boolean
+_sqlite3_uri_int64
+_sqlite3_uri_key
+_sqlite3_uri_parameter
+_sqlite3_user_data
+_sqlite3_value_blob
+_sqlite3_value_bytes
+_sqlite3_value_double
+_sqlite3_value_int
+_sqlite3_value_int64
+_sqlite3_value_text
+_sqlite3_value_type
+_sqlite3_vfs_find
+_sqlite3_vfs_register
+_sqlite3_vfs_unregister
+_malloc
+_free
diff --git a/ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api b/ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api
new file mode 100644
index 0000000..aab1d8b
--- /dev/null
+++ b/ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api
@@ -0,0 +1,3 @@
+FS
+wasmMemory
+
diff --git a/ext/wasm/api/README.md b/ext/wasm/api/README.md
new file mode 100644
index 0000000..6440eba
--- /dev/null
+++ b/ext/wasm/api/README.md
@@ -0,0 +1,133 @@
+# sqlite3-api.js And Friends
+
+This is the README for the files `sqlite3-*.js` and
+`sqlite3-wasm.c`. This collection of files is used to build a
+single-file distribution of the sqlite3 WASM API. It is broken into
+multiple JS files because:
+
+1. To facilitate including or excluding certain components for
+ specific use cases. e.g. by removing `sqlite3-api-oo1.js` if the
+ OO#1 API is not needed.
+
+2. To facilitate modularizing the pieces for use in different WASM
+ build environments. e.g. the files `post-js-*.js` are for use with
+ Emscripten's `--post-js` feature, and nowhere else.
+
+3. Certain components must be in their own standalone files in order
+ to be loaded as JS Workers.
+
+Note that the structure described here is the current state of things,
+not necessarily the "final" state.
+
+The overall idea is that the following files get concatenated
+together, in the listed order, the resulting file is loaded by a
+browser client:
+
+- `sqlite3-api-prologue.js`\
+ Contains the initial bootstrap setup of the sqlite3 API
+ objects. This is exposed as a function, rather than objects, so that
+ the next step can pass in a config object which abstracts away parts
+ of the WASM environment, to facilitate plugging it in to arbitrary
+ WASM toolchains.
+- `../common/whwasmutil.js`\
+ A semi-third-party collection of JS/WASM utility code intended to
+ replace much of the Emscripten glue. The sqlite3 APIs internally use
+ these APIs instead of their Emscripten counterparts, in order to be
+ more portable to arbitrary WASM toolchains. This API is
+ configurable, in principle, for use with arbitrary WASM
+ toolchains. It is "semi-third-party" in that it was created in order
+ to support this tree but is standalone and maintained together
+ with...
+- `../jaccwabyt/jaccwabyt.js`\
+ Another semi-third-party API which creates bindings between JS
+ and C structs, such that changes to the struct state from either JS
+ or C are visible to the other end of the connection. This is also an
+ independent spinoff project, conceived for the sqlite3 project but
+ maintained separately.
+- `sqlite3-api-glue.js`\
+ Invokes functionality exposed by the previous two files to
+ flesh out low-level parts of `sqlite3-api-prologue.js`. Most of
+ these pieces related to the `sqlite3.capi.wasm` object.
+- `sqlite3-api-build-version.js`\
+ Gets created by the build process and populates the
+ `sqlite3.version` object. This part is not critical, but records the
+ version of the library against which this module was built.
+- `sqlite3-api-oo1.js`\
+ Provides a high-level object-oriented wrapper to the lower-level C
+ API, colloquially known as OO API #1. Its API is similar to other
+ high-level sqlite3 JS wrappers and should feel relatively familiar
+ to anyone familiar with such APIs. That said, it is not a "required
+ component" and can be elided from builds which do not want it.
+- `sqlite3-api-worker1.js`\
+ A Worker-thread-based API which uses OO API #1 to provide an
+ interface to a database which can be driven from the main Window
+ thread via the Worker message-passing interface. Like OO API #1,
+ this is an optional component, offering one of any number of
+ potential implementations for such an API.
+ - `sqlite3-worker1.js`\
+ Is not part of the amalgamated sources and is intended to be
+ loaded by a client Worker thread. It loads the sqlite3 module
+ and runs the Worker #1 API which is implemented in
+ `sqlite3-api-worker1.js`.
+ - `sqlite3-worker1-promiser.js`\
+ Is likewise not part of the amalgamated sources and provides
+ a Promise-based interface into the Worker #1 API. This is
+ a far user-friendlier way to interface with databases running
+ in a Worker thread.
+- `sqlite3-api-opfs.js`\
+ is an sqlite3 VFS implementation which supports Google Chrome's
+ Origin-Private FileSystem (OPFS) as a storage layer to provide
+ persistent storage for database files in a browser. It requires...
+ - `sqlite3-opfs-async-proxy.js`\
+ is the asynchronous backend part of the OPFS proxy. It speaks
+ directly to the (async) OPFS API and channels those results back
+ to its synchronous counterpart. This file, because it must be
+ started in its own Worker, is not part of the amalgamation.
+- **`api/sqlite3-api-cleanup.js`**\
+ The previous files do not immediately extend the library. Instead
+ they add callback functions to be called during its
+ bootstrapping. Some also temporarily create global objects in order
+ to communicate their state to the files which follow them. This file
+ cleans up any dangling globals and runs the API bootstrapping
+ process, which is what finally executes the initialization code
+ installed by the previous files. As of this writing, this code
+ ensures that the previous files leave no more than a single global
+ symbol installed. When adapting the API for non-Emscripten
+ toolchains, this "should" be the only file where changes are needed.
+
+The build process glues those files together, resulting in
+`sqlite3-api.js`, which is everything except for the `post-js-*.js`
+files, and `sqlite3.js`, which is the Emscripten-generated amalgamated
+output and includes the `post-js-*.js` parts, as well as the
+Emscripten-provided module loading pieces.
+
+The non-JS outlier file is `sqlite3-wasm.c`: it is a proxy for
+`sqlite3.c` which `#include`'s that file and adds a couple more
+WASM-specific helper functions, at least one of which requires access
+to private/static `sqlite3.c` internals. `sqlite3.wasm` is compiled
+from this file rather than `sqlite3.c`.
+
+The following files are part of the build process but are injected
+into the build-generated `sqlite3.js` along with `sqlite3-api.js`.
+
+- `extern-pre-js.js`\
+ Emscripten-specific header for Emscripten's `--extern-pre-js`
+ flag. As of this writing, that file is only used for experimentation
+ purposes and holds no code relevant to the production deliverables.
+- `pre-js.js`\
+ Emscripten-specific header for Emscripten's `--pre-js` flag. This
+ file is intended as a place to override certain Emscripten behavior
+ before it starts up, but corner-case Emscripten bugs keep that from
+ being a reality.
+- `post-js-header.js`\
+ Emscripten-specific header for the `--post-js` input. It opens up
+ a lexical scope by starting a post-run handler for Emscripten.
+- `post-js-footer.js`\
+ Emscripten-specific footer for the `--post-js` input. This closes
+ off the lexical scope opened by `post-js-header.js`.
+- `extern-post-js.js`\
+ Emscripten-specific header for Emscripten's `--extern-post-js`
+ flag. This file overwrites the Emscripten-installed
+ `sqlite3InitModule()` function with one which, after the module is
+ loaded, also initializes the asynchronous parts of the sqlite3
+ module. For example, the OPFS VFS support.
diff --git a/ext/wasm/api/extern-post-js.js b/ext/wasm/api/extern-post-js.js
new file mode 100644
index 0000000..84b99b5
--- /dev/null
+++ b/ext/wasm/api/extern-post-js.js
@@ -0,0 +1,103 @@
+/* extern-post-js.js must be appended to the resulting sqlite3.js
+ file. It gets its name from being used as the value for the
+ --extern-post-js=... Emscripten flag. Note that this code, unlike
+ most of the associated JS code, runs outside of the
+ Emscripten-generated module init scope, in the current
+ global scope. */
+(function(){
+ /**
+ In order to hide the sqlite3InitModule()'s resulting Emscripten
+ module from downstream clients (and simplify our documentation by
+ being able to elide those details), we rewrite
+ sqlite3InitModule() to return the sqlite3 object.
+
+ Unfortunately, we cannot modify the module-loader/exporter-based
+ impls which Emscripten installs at some point in the file above
+ this.
+ */
+ const originalInit = self.sqlite3InitModule;
+ if(!originalInit){
+ throw new Error("Expecting self.sqlite3InitModule to be defined by the Emscripten build.");
+ }
+ /**
+ We need to add some state which our custom Module.locateFile()
+ can see, but an Emscripten limitation currently prevents us from
+ attaching it to the sqlite3InitModule function object:
+
+ https://github.com/emscripten-core/emscripten/issues/18071
+
+ The only(?) current workaround is to temporarily stash this state
+ into the global scope and delete it when sqlite3InitModule()
+ is called.
+ */
+ const initModuleState = self.sqlite3InitModuleState = Object.assign(Object.create(null),{
+ moduleScript: self?.document?.currentScript,
+ isWorker: ('undefined' !== typeof WorkerGlobalScope),
+ location: self.location,
+ urlParams: new URL(self.location.href).searchParams
+ });
+ initModuleState.debugModule =
+ (new URL(self.location.href).searchParams).has('sqlite3.debugModule')
+ ? (...args)=>console.warn('sqlite3.debugModule:',...args)
+ : ()=>{};
+
+ if(initModuleState.urlParams.has('sqlite3.dir')){
+ initModuleState.sqlite3Dir = initModuleState.urlParams.get('sqlite3.dir') +'/';
+ }else if(initModuleState.moduleScript){
+ const li = initModuleState.moduleScript.src.split('/');
+ li.pop();
+ initModuleState.sqlite3Dir = li.join('/') + '/';
+ }
+
+ self.sqlite3InitModule = (...args)=>{
+ //console.warn("Using replaced sqlite3InitModule()",self.location);
+ return originalInit(...args).then((EmscriptenModule)=>{
+ if(self.window!==self &&
+ (EmscriptenModule['ENVIRONMENT_IS_PTHREAD']
+ || EmscriptenModule['_pthread_self']
+ || 'function'===typeof threadAlert
+ || self.location.pathname.endsWith('.worker.js')
+ )){
+ /** Workaround for wasmfs-generated worker, which calls this
+ routine from each individual thread and requires that its
+ argument be returned. All of the criteria above are fragile,
+ based solely on inspection of the offending code, not public
+ Emscripten details. */
+ return EmscriptenModule;
+ }
+ EmscriptenModule.sqlite3.scriptInfo = initModuleState;
+ //console.warn("sqlite3.scriptInfo =",EmscriptenModule.sqlite3.scriptInfo);
+ const f = EmscriptenModule.sqlite3.asyncPostInit;
+ delete EmscriptenModule.sqlite3.asyncPostInit;
+ return f();
+ }).catch((e)=>{
+ console.error("Exception loading sqlite3 module:",e);
+ throw e;
+ });
+ };
+ self.sqlite3InitModule.ready = originalInit.ready;
+
+ if(self.sqlite3InitModuleState.moduleScript){
+ const sim = self.sqlite3InitModuleState;
+ let src = sim.moduleScript.src.split('/');
+ src.pop();
+ sim.scriptDir = src.join('/') + '/';
+ }
+ initModuleState.debugModule('sqlite3InitModuleState =',initModuleState);
+ if(0){
+ console.warn("Replaced sqlite3InitModule()");
+ console.warn("self.location.href =",self.location.href);
+ if('undefined' !== typeof document){
+ console.warn("document.currentScript.src =",
+ document?.currentScript?.src);
+ }
+ }
+ /* Replace the various module exports performed by the Emscripten
+ glue... */
+ if (typeof exports === 'object' && typeof module === 'object')
+ module.exports = sqlite3InitModule;
+ else if (typeof exports === 'object')
+ exports["sqlite3InitModule"] = sqlite3InitModule;
+ /* AMD modules get injected in a way we cannot override,
+ so we can't handle those here. */
+})();
diff --git a/ext/wasm/api/extern-pre-js.js b/ext/wasm/api/extern-pre-js.js
new file mode 100644
index 0000000..7d47d33
--- /dev/null
+++ b/ext/wasm/api/extern-pre-js.js
@@ -0,0 +1,7 @@
+/* extern-pre-js.js must be prepended to the resulting sqlite3.js
+ file. This file is currently only used for holding snippets during
+ test and development.
+
+ It gets its name from being used as the value for the
+ --extern-pre-js=... Emscripten flag.
+*/
diff --git a/ext/wasm/api/post-js-footer.js b/ext/wasm/api/post-js-footer.js
new file mode 100644
index 0000000..58882cb
--- /dev/null
+++ b/ext/wasm/api/post-js-footer.js
@@ -0,0 +1,4 @@
+/* The current function scope was opened via post-js-header.js, which
+ gets prepended to this at build-time. This file closes that
+ scope. */
+})/*postRun.push(...)*/;
diff --git a/ext/wasm/api/post-js-header.js b/ext/wasm/api/post-js-header.js
new file mode 100644
index 0000000..82a80e5
--- /dev/null
+++ b/ext/wasm/api/post-js-header.js
@@ -0,0 +1,25 @@
+/**
+ post-js-header.js is to be prepended to other code to create
+ post-js.js for use with Emscripten's --post-js flag. This code
+ requires that it be running in that context. The Emscripten
+ environment must have been set up already but it will not have
+ loaded its WASM when the code in this file is run. The function it
+ installs will be run after the WASM module is loaded, at which
+ point the sqlite3 JS API bits will get set up.
+*/
+if(!Module.postRun) Module.postRun = [];
+Module.postRun.push(function(Module/*the Emscripten-style module object*/){
+ 'use strict';
+ /* This function will contain at least the following:
+
+ - post-js-header.js (this file)
+ - sqlite3-api-prologue.js => Bootstrapping bits to attach the rest to
+ - common/whwasmutil.js => Replacements for much of Emscripten's glue
+ - jaccwaby/jaccwabyt.js => Jaccwabyt (C/JS struct binding)
+ - sqlite3-api-glue.js => glues previous parts together
+ - sqlite3-api-oo.js => SQLite3 OO API #1
+ - sqlite3-api-worker1.js => Worker-based API
+ - sqlite3-api-opfs.js => OPFS VFS
+ - sqlite3-api-cleanup.js => final API cleanup
+ - post-js-footer.js => closes this postRun() function
+ */
diff --git a/ext/wasm/api/pre-js.js b/ext/wasm/api/pre-js.js
new file mode 100644
index 0000000..f31dea1
--- /dev/null
+++ b/ext/wasm/api/pre-js.js
@@ -0,0 +1,100 @@
+/**
+ BEGIN FILE: api/pre-js.js
+
+ This file is intended to be prepended to the sqlite3.js build using
+ Emscripten's --pre-js=THIS_FILE flag (or equivalent).
+*/
+
+// See notes in extern-post-js.js
+const sqlite3InitModuleState = self.sqlite3InitModuleState || Object.create(null);
+delete self.sqlite3InitModuleState;
+sqlite3InitModuleState.debugModule('self.location =',self.location);
+
+/**
+ This custom locateFile() tries to figure out where to load `path`
+ from. The intent is to provide a way for foo/bar/X.js loaded from a
+ Worker constructor or importScripts() to be able to resolve
+ foo/bar/X.wasm (in the latter case, with some help):
+
+ 1) If URL param named the same as `path` is set, it is returned.
+
+ 2) If sqlite3InitModuleState.sqlite3Dir is set, then (thatName + path)
+ is returned (note that it's assumed to end with '/').
+
+ 3) If this code is running in the main UI thread AND it was loaded
+ from a SCRIPT tag, the directory part of that URL is used
+ as the prefix. (This form of resolution unfortunately does not
+ function for scripts loaded via importScripts().)
+
+ 4) If none of the above apply, (prefix+path) is returned.
+*/
+Module['locateFile'] = function(path, prefix) {
+ let theFile;
+ const up = this.urlParams;
+ if(up.has(path)){
+ theFile = up.get(path);
+ }else if(this.sqlite3Dir){
+ theFile = this.sqlite3Dir + path;
+ }else if(this.scriptDir){
+ theFile = this.scriptDir + path;
+ }else{
+ theFile = prefix + path;
+ }
+ sqlite3InitModuleState.debugModule(
+ "locateFile(",arguments[0], ',', arguments[1],")",
+ 'sqlite3InitModuleState.scriptDir =',this.scriptDir,
+ 'up.entries() =',Array.from(up.entries()),
+ "result =", theFile
+ );
+ return theFile;
+}.bind(sqlite3InitModuleState);
+
+/**
+ Bug warning: a custom Module.instantiateWasm() does not work
+ in WASMFS builds:
+
+ https://github.com/emscripten-core/emscripten/issues/17951
+
+ In such builds we must disable this.
+*/
+const xNameOfInstantiateWasm = true
+ ? 'instantiateWasm'
+ : 'emscripten-bug-17951';
+Module[xNameOfInstantiateWasm] = function callee(imports,onSuccess){
+ imports.env.foo = function(){};
+ const uri = Module.locateFile(
+ callee.uri, (
+ ('undefined'===typeof scriptDirectory/*var defined by Emscripten glue*/)
+ ? '' : scriptDirectory)
+ );
+ sqlite3InitModuleState.debugModule(
+ "instantiateWasm() uri =", uri
+ );
+ const wfetch = ()=>fetch(uri, {credentials: 'same-origin'});
+ const loadWasm = WebAssembly.instantiateStreaming
+ ? async ()=>{
+ return WebAssembly.instantiateStreaming(wfetch(), imports)
+ .then((arg)=>onSuccess(arg.instance, arg.module));
+ }
+ : async ()=>{ // Safari < v15
+ return wfetch()
+ .then(response => response.arrayBuffer())
+ .then(bytes => WebAssembly.instantiate(bytes, imports))
+ .then((arg)=>onSuccess(arg.instance, arg.module));
+ };
+ loadWasm();
+ return {};
+};
+/*
+ It is literally impossible to reliably get the name of _this_ script
+ at runtime, so impossible to derive X.wasm from script name
+ X.js. Thus we need, at build-time, to redefine
+ Module[xNameOfInstantiateWasm].uri by appending it to a build-specific
+ copy of this file with the name of the wasm file. This is apparently
+ why Emscripten hard-codes the name of the wasm file into their glue
+ scripts.
+*/
+Module[xNameOfInstantiateWasm].uri = 'sqlite3.wasm';
+/* END FILE: api/pre-js.js, noting that the build process may add a
+ line after this one to change the above .uri to a build-specific
+ one. */
diff --git a/ext/wasm/api/sqlite3-api-cleanup.js b/ext/wasm/api/sqlite3-api-cleanup.js
new file mode 100644
index 0000000..bef4d91
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-cleanup.js
@@ -0,0 +1,70 @@
+/*
+ 2022-07-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file is the tail end of the sqlite3-api.js constellation,
+ intended to be appended after all other sqlite3-api-*.js files so
+ that it can finalize any setup and clean up any global symbols
+ temporarily used for setting up the API's various subsystems.
+*/
+'use strict';
+if('undefined' !== typeof Module){ // presumably an Emscripten build
+ /**
+ Install a suitable default configuration for sqlite3ApiBootstrap().
+ */
+ const SABC = Object.assign(
+ Object.create(null), {
+ Module: Module /* ==> Currently needs to be exposed here for
+ test code. NOT part of the public API. */,
+ exports: Module['asm'],
+ memory: Module.wasmMemory /* gets set if built with -sIMPORT_MEMORY */
+ },
+ self.sqlite3ApiConfig || Object.create(null)
+ );
+
+ /**
+ For current (2022-08-22) purposes, automatically call
+ sqlite3ApiBootstrap(). That decision will be revisited at some
+ point, as we really want client code to be able to call this to
+ configure certain parts. Clients may modify
+ self.sqlite3ApiBootstrap.defaultConfig to tweak the default
+ configuration used by a no-args call to sqlite3ApiBootstrap(),
+ but must have first loaded their WASM module in order to be
+ able to provide the necessary configuration state.
+ */
+ //console.warn("self.sqlite3ApiConfig = ",self.sqlite3ApiConfig);
+ self.sqlite3ApiConfig = SABC;
+ let sqlite3;
+ try{
+ sqlite3 = self.sqlite3ApiBootstrap();
+ }catch(e){
+ console.error("sqlite3ApiBootstrap() error:",e);
+ throw e;
+ }finally{
+ delete self.sqlite3ApiBootstrap;
+ delete self.sqlite3ApiConfig;
+ }
+
+ if(self.location && +self.location.port > 1024){
+ console.warn("Installing sqlite3 bits as global S for local dev/test purposes.");
+ self.S = sqlite3;
+ }
+
+ /* Clean up temporary references to our APIs... */
+ delete sqlite3.util /* arguable, but these are (currently) internal-use APIs */;
+ Module.sqlite3 = sqlite3 /* Needed for customized sqlite3InitModule() to be able to
+ pass the sqlite3 object off to the client. */;
+}else{
+ console.warn("This is not running in an Emscripten module context, so",
+ "self.sqlite3ApiBootstrap() is _not_ being called due to lack",
+ "of config info for the WASM environment.",
+ "It must be called manually.");
+}
diff --git a/ext/wasm/api/sqlite3-api-glue.js b/ext/wasm/api/sqlite3-api-glue.js
new file mode 100644
index 0000000..86aa1d1
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-glue.js
@@ -0,0 +1,720 @@
+/*
+ 2022-07-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file glues together disparate pieces of JS which are loaded in
+ previous steps of the sqlite3-api.js bootstrapping process:
+ sqlite3-api-prologue.js, whwasmutil.js, and jaccwabyt.js. It
+ initializes the main API pieces so that the downstream components
+ (e.g. sqlite3-api-oo1.js) have all that they need.
+*/
+self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+ 'use strict';
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ const toss3 = sqlite3.SQLite3Error.toss;
+ const capi = sqlite3.capi, wasm = sqlite3.wasm, util = sqlite3.util;
+ self.WhWasmUtilInstaller(wasm);
+ delete self.WhWasmUtilInstaller;
+
+ /**
+ Install JS<->C struct bindings for the non-opaque struct types we
+ need... */
+ sqlite3.StructBinder = self.Jaccwabyt({
+ heap: 0 ? wasm.memory : wasm.heap8u,
+ alloc: wasm.alloc,
+ dealloc: wasm.dealloc,
+ functionTable: wasm.functionTable,
+ bigIntEnabled: wasm.bigIntEnabled,
+ memberPrefix: '$'
+ });
+ delete self.Jaccwabyt;
+
+ if(0){
+ /* "The problem" is that the following isn't even remotely
+ type-safe. OTOH, nothing about WASM pointers is. */
+ const argPointer = wasm.xWrap.argAdapter('*');
+ wasm.xWrap.argAdapter('StructType', (v)=>{
+ if(v && v.constructor && v instanceof StructBinder.StructType){
+ v = v.pointer;
+ }
+ return wasm.isPtr(v)
+ ? argPointer(v)
+ : toss("Invalid (object) type for StructType-type argument.");
+ });
+ }
+
+ {/* Convert Arrays and certain TypedArrays to strings for
+ 'flexible-string'-type arguments */
+ const xString = wasm.xWrap.argAdapter('string');
+ wasm.xWrap.argAdapter(
+ 'flexible-string', (v)=>xString(util.flexibleString(v))
+ );
+ }
+
+ if(1){// WhWasmUtil.xWrap() bindings...
+ /**
+ Add some descriptive xWrap() aliases for '*' intended to (A)
+ initially improve readability/correctness of capi.signatures
+ and (B) eventually perhaps provide automatic conversion from
+ higher-level representations, e.g. capi.sqlite3_vfs to
+ `sqlite3_vfs*` via capi.sqlite3_vfs.pointer.
+ */
+ const aPtr = wasm.xWrap.argAdapter('*');
+ wasm.xWrap.argAdapter('sqlite3*', aPtr)
+ ('sqlite3_stmt*', aPtr)
+ ('sqlite3_context*', aPtr)
+ ('sqlite3_value*', aPtr)
+ ('sqlite3_vfs*', aPtr)
+ ('void*', aPtr);
+ wasm.xWrap.resultAdapter('sqlite3*', aPtr)
+ ('sqlite3_context*', aPtr)
+ ('sqlite3_stmt*', aPtr)
+ ('sqlite3_vfs*', aPtr)
+ ('void*', aPtr);
+
+ /**
+ Populate api object with sqlite3_...() by binding the "raw" wasm
+ exports into type-converting proxies using wasm.xWrap().
+ */
+ for(const e of wasm.bindingSignatures){
+ capi[e[0]] = wasm.xWrap.apply(null, e);
+ }
+ for(const e of wasm.bindingSignatures.wasm){
+ wasm[e[0]] = wasm.xWrap.apply(null, e);
+ }
+
+ /* For C API functions which cannot work properly unless
+ wasm.bigIntEnabled is true, install a bogus impl which
+ throws if called when bigIntEnabled is false. */
+ const fI64Disabled = function(fname){
+ return ()=>toss(fname+"() disabled due to lack",
+ "of BigInt support in this build.");
+ };
+ for(const e of wasm.bindingSignatures.int64){
+ capi[e[0]] = wasm.bigIntEnabled
+ ? wasm.xWrap.apply(null, e)
+ : fI64Disabled(e[0]);
+ }
+
+ /* There's no(?) need to expose bindingSignatures to clients,
+ implicitly making it part of the public interface. */
+ delete wasm.bindingSignatures;
+
+ if(wasm.exports.sqlite3_wasm_db_error){
+ util.sqlite3_wasm_db_error = wasm.xWrap(
+ 'sqlite3_wasm_db_error', 'int', 'sqlite3*', 'int', 'string'
+ );
+ }else{
+ util.sqlite3_wasm_db_error = function(pDb,errCode,msg){
+ console.warn("sqlite3_wasm_db_error() is not exported.",arguments);
+ return errCode;
+ };
+ }
+
+ }/*xWrap() bindings*/;
+
+ /**
+ When registering a VFS and its related components it may be
+ necessary to ensure that JS keeps a reference to them to keep
+ them from getting garbage collected. Simply pass each such value
+ to this function and a reference will be held to it for the life
+ of the app.
+ */
+ capi.sqlite3_vfs_register.addReference = function f(...args){
+ if(!f._) f._ = [];
+ f._.push(...args);
+ };
+
+ /**
+ Internal helper to assist in validating call argument counts in
+ the hand-written sqlite3_xyz() wrappers. We do this only for
+ consistency with non-special-case wrappings.
+ */
+ const __dbArgcMismatch = (pDb,f,n)=>{
+ return sqlite3.util.sqlite3_wasm_db_error(pDb, capi.SQLITE_MISUSE,
+ f+"() requires "+n+" argument"+
+ (1===n?"":'s')+".");
+ };
+
+ /**
+ Helper for flexible-string conversions which require a
+ byte-length counterpart argument. Passed a value and its
+ ostensible length, this function returns [V,N], where V
+ is either v or a transformed copy of v and N is either n,
+ -1, or the byte length of v (if it's a byte array).
+ */
+ const __flexiString = function(v,n){
+ if('string'===typeof v){
+ n = -1;
+ }else if(util.isSQLableTypedArray(v)){
+ n = v.byteLength;
+ v = util.typedArrayToString(v);
+ }else if(Array.isArray(v)){
+ v = v.join("");
+ n = -1;
+ }
+ return [v, n];
+ };
+
+ if(1){/* Special-case handling of sqlite3_exec() */
+ const __exec = wasm.xWrap("sqlite3_exec", "int",
+ ["sqlite3*", "flexible-string", "*", "*", "**"]);
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_exec = function f(pDb, sql, callback, pVoid, pErrMsg){
+ if(f.length!==arguments.length){
+ return __dbArgcMismatch(pDb,"sqlite3_exec",f.length);
+ }else if('function' !== typeof callback){
+ return __exec(pDb, sql, callback, pVoid, pErrMsg);
+ }
+ /* Wrap the callback in a WASM-bound function and convert the callback's
+ `(char**)` arguments to arrays of strings... */
+ const cbwrap = function(pVoid, nCols, pColVals, pColNames){
+ let rc = capi.SQLITE_ERROR;
+ try {
+ let aVals = [], aNames = [], i = 0, offset = 0;
+ for( ; i < nCols; offset += (wasm.ptrSizeof * ++i) ){
+ aVals.push( wasm.cstringToJs(wasm.getPtrValue(pColVals + offset)) );
+ aNames.push( wasm.cstringToJs(wasm.getPtrValue(pColNames + offset)) );
+ }
+ rc = callback(pVoid, nCols, aVals, aNames) | 0;
+ /* The first 2 args of the callback are useless for JS but
+ we want the JS mapping of the C API to be as close to the
+ C API as possible. */
+ }catch(e){
+ /* If we set the db error state here, the higher-level exec() call
+ replaces it with its own, so we have no way of reporting the
+ exception message except the console. We must not propagate
+ exceptions through the C API. */
+ }
+ return rc;
+ };
+ let pFunc, rc;
+ try{
+ pFunc = wasm.installFunction("ipipp", cbwrap);
+ rc = __exec(pDb, sql, pFunc, pVoid, pErrMsg);
+ }catch(e){
+ rc = util.sqlite3_wasm_db_error(pDb, capi.SQLITE_ERROR,
+ "Error running exec(): "+e.message);
+ }finally{
+ if(pFunc) wasm.uninstallFunction(pFunc);
+ }
+ return rc;
+ };
+ }/*sqlite3_exec() proxy*/;
+
+ if(1){/* Special-case handling of sqlite3_create_function_v2()
+ and sqlite3_create_window_function() */
+ const sqlite3CreateFunction = wasm.xWrap(
+ "sqlite3_create_function_v2", "int",
+ ["sqlite3*", "string"/*funcName*/, "int"/*nArg*/,
+ "int"/*eTextRep*/, "*"/*pApp*/,
+ "*"/*xStep*/,"*"/*xFinal*/, "*"/*xValue*/, "*"/*xDestroy*/]
+ );
+ const sqlite3CreateWindowFunction = wasm.xWrap(
+ "sqlite3_create_window_function", "int",
+ ["sqlite3*", "string"/*funcName*/, "int"/*nArg*/,
+ "int"/*eTextRep*/, "*"/*pApp*/,
+ "*"/*xStep*/,"*"/*xFinal*/, "*"/*xValue*/,
+ "*"/*xInverse*/, "*"/*xDestroy*/]
+ );
+
+ const __udfSetResult = function(pCtx, val){
+ //console.warn("udfSetResult",typeof val, val);
+ switch(typeof val) {
+ case 'undefined':
+ /* Assume that the client already called sqlite3_result_xxx(). */
+ break;
+ case 'boolean':
+ capi.sqlite3_result_int(pCtx, val ? 1 : 0);
+ break;
+ case 'bigint':
+ if(wasm.bigIntEnabled){
+ if(util.bigIntFits64(val)) capi.sqlite3_result_int64(pCtx, val);
+ else toss3("BigInt value",val.toString(),"is too BigInt for int64.");
+ }else if(util.bigIntFits32(val)){
+ capi.sqlite3_result_int(pCtx, Number(val));
+ }else if(util.bigIntFitsDouble(val)){
+ capi.sqlite3_result_double(pCtx, Number(val));
+ }else{
+ toss3("BigInt value",val.toString(),"is too BigInt.");
+ }
+ break;
+ case 'number': {
+ (util.isInt32(val)
+ ? capi.sqlite3_result_int
+ : capi.sqlite3_result_double)(pCtx, val);
+ break;
+ }
+ case 'string':
+ capi.sqlite3_result_text(pCtx, val, -1, capi.SQLITE_TRANSIENT);
+ break;
+ case 'object':
+ if(null===val/*yes, typeof null === 'object'*/) {
+ capi.sqlite3_result_null(pCtx);
+ break;
+ }else if(util.isBindableTypedArray(val)){
+ const pBlob = wasm.allocFromTypedArray(val);
+ capi.sqlite3_result_blob(
+ pCtx, pBlob, val.byteLength,
+ wasm.exports[sqlite3.config.deallocExportName]
+ );
+ break;
+ }
+ // else fall through
+ default:
+ toss3("Don't not how to handle this UDF result value:",(typeof val), val);
+ };
+ }/*__udfSetResult()*/;
+
+ const __udfConvertArgs = function(argc, pArgv){
+ let i, pVal, valType, arg;
+ const tgt = [];
+ for(i = 0; i < argc; ++i){
+ pVal = wasm.getPtrValue(pArgv + (wasm.ptrSizeof * i));
+ /**
+ Curiously: despite ostensibly requiring 8-byte
+ alignment, the pArgv array is parcelled into chunks of
+ 4 bytes (1 pointer each). The values those point to
+ have 8-byte alignment but the individual argv entries
+ do not.
+ */
+ valType = capi.sqlite3_value_type(pVal);
+ switch(valType){
+ case capi.SQLITE_INTEGER:
+ if(wasm.bigIntEnabled){
+ arg = capi.sqlite3_value_int64(pVal);
+ if(util.bigIntFitsDouble(arg)) arg = Number(arg);
+ }
+ else arg = capi.sqlite3_value_double(pVal)/*yes, double, for larger integers*/;
+ break;
+ case capi.SQLITE_FLOAT:
+ arg = capi.sqlite3_value_double(pVal);
+ break;
+ case capi.SQLITE_TEXT:
+ arg = capi.sqlite3_value_text(pVal);
+ break;
+ case capi.SQLITE_BLOB:{
+ const n = capi.sqlite3_value_bytes(pVal);
+ const pBlob = capi.sqlite3_value_blob(pVal);
+ if(n && !pBlob) sqlite3.WasmAllocError.toss(
+ "Cannot allocate memory for blob argument of",n,"byte(s)"
+ );
+ arg = n ? wasm.heap8u().slice(pBlob, pBlob + Number(n)) : null;
+ break;
+ }
+ case capi.SQLITE_NULL:
+ arg = null; break;
+ default:
+ toss3("Unhandled sqlite3_value_type()",valType,
+ "is possibly indicative of incorrect",
+ "pointer size assumption.");
+ }
+ tgt.push(arg);
+ }
+ return tgt;
+ }/*__udfConvertArgs()*/;
+
+ const __udfSetError = (pCtx, e)=>{
+ if(e instanceof sqlite3.WasmAllocError){
+ capi.sqlite3_result_error_nomem(pCtx);
+ }else{
+ const msg = ('string'===typeof e) ? e : e.message;
+ capi.sqlite3_result_error(pCtx, msg, -1);
+ }
+ };
+
+ const __xFunc = function(callback){
+ return function(pCtx, argc, pArgv){
+ try{ __udfSetResult(pCtx, callback(pCtx, ...__udfConvertArgs(argc, pArgv))) }
+ catch(e){
+ //console.error('xFunc() caught:',e);
+ __udfSetError(pCtx, e);
+ }
+ };
+ };
+
+ const __xInverseAndStep = function(callback){
+ return function(pCtx, argc, pArgv){
+ try{ callback(pCtx, ...__udfConvertArgs(argc, pArgv)) }
+ catch(e){ __udfSetError(pCtx, e) }
+ };
+ };
+
+ const __xFinalAndValue = function(callback){
+ return function(pCtx){
+ try{ __udfSetResult(pCtx, callback(pCtx)) }
+ catch(e){ __udfSetError(pCtx, e) }
+ };
+ };
+
+ const __xDestroy = function(callback){
+ return function(pVoid){
+ try{ callback(pVoid) }
+ catch(e){ console.error("UDF xDestroy method threw:",e) }
+ };
+ };
+
+ const __xMap = Object.assign(Object.create(null), {
+ xFunc: {sig:'v(pip)', f:__xFunc},
+ xStep: {sig:'v(pip)', f:__xInverseAndStep},
+ xInverse: {sig:'v(pip)', f:__xInverseAndStep},
+ xFinal: {sig:'v(p)', f:__xFinalAndValue},
+ xValue: {sig:'v(p)', f:__xFinalAndValue},
+ xDestroy: {sig:'v(p)', f:__xDestroy}
+ });
+
+ const __xWrapFuncs = function(theFuncs, tgtUninst){
+ const rc = []
+ let k;
+ for(k in theFuncs){
+ let fArg = theFuncs[k];
+ if('function'===typeof fArg){
+ const w = __xMap[k];
+ fArg = wasm.installFunction(w.sig, w.f(fArg));
+ tgtUninst.push(fArg);
+ }
+ rc.push(fArg);
+ }
+ return rc;
+ };
+
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_create_function_v2 = function f(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, //void (*xFunc)(sqlite3_context*,int,sqlite3_value**)
+ xStep, //void (*xStep)(sqlite3_context*,int,sqlite3_value**)
+ xFinal, //void (*xFinal)(sqlite3_context*)
+ xDestroy //void (*xDestroy)(void*)
+ ){
+ if(f.length!==arguments.length){
+ return __dbArgcMismatch(pDb,"sqlite3_create_function_v2",f.length);
+ }
+ /* Wrap the callbacks in a WASM-bound functions... */
+ const uninstall = [/*funcs to uninstall on error*/];
+ let rc;
+ try{
+ const funcArgs = __xWrapFuncs({xFunc, xStep, xFinal, xDestroy},
+ uninstall);
+ rc = sqlite3CreateFunction(pDb, funcName, nArg, eTextRep,
+ pApp, ...funcArgs);
+ }catch(e){
+ console.error("sqlite3_create_function_v2() setup threw:",e);
+ for(let v of uninstall){
+ wasm.uninstallFunction(v);
+ }
+ rc = util.sqlite3_wasm_db_error(pDb, capi.SQLITE_ERROR,
+ "Creation of UDF threw: "+e.message);
+ }
+ return rc;
+ };
+
+ capi.sqlite3_create_function = function f(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, xStep, xFinal
+ ){
+ return (f.length===arguments.length)
+ ? capi.sqlite3_create_function_v2(pDb, funcName, nArg, eTextRep,
+ pApp, xFunc, xStep, xFinal, 0)
+ : __dbArgcMismatch(pDb,"sqlite3_create_function",f.length);
+ };
+
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_create_window_function = function f(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xStep, //void (*xStep)(sqlite3_context*,int,sqlite3_value**)
+ xFinal, //void (*xFinal)(sqlite3_context*)
+ xValue, //void (*xFinal)(sqlite3_context*)
+ xInverse,//void (*xStep)(sqlite3_context*,int,sqlite3_value**)
+ xDestroy //void (*xDestroy)(void*)
+ ){
+ if(f.length!==arguments.length){
+ return __dbArgcMismatch(pDb,"sqlite3_create_window_function",f.length);
+ }
+ /* Wrap the callbacks in a WASM-bound functions... */
+ const uninstall = [/*funcs to uninstall on error*/];
+ let rc;
+ try{
+ const funcArgs = __xWrapFuncs({xStep, xFinal, xValue, xInverse, xDestroy},
+ uninstall);
+ rc = sqlite3CreateWindowFunction(pDb, funcName, nArg, eTextRep,
+ pApp, ...funcArgs);
+ }catch(e){
+ console.error("sqlite3_create_window_function() setup threw:",e);
+ for(let v of uninstall){
+ wasm.uninstallFunction(v);
+ }
+ rc = util.sqlite3_wasm_db_error(pDb, capi.SQLITE_ERROR,
+ "Creation of UDF threw: "+e.message);
+ }
+ return rc;
+ };
+ /**
+ A helper for UDFs implemented in JS and bound to WASM by the
+ client. Given a JS value, udfSetResult(pCtx,X) calls one of the
+ sqlite3_result_xyz(pCtx,...) routines, depending on X's data
+ type:
+
+ - `null`: sqlite3_result_null()
+ - `boolean`: sqlite3_result_int()
+ - `number`: sqlite3_result_int() or sqlite3_result_double()
+ - `string`: sqlite3_result_text()
+ - Uint8Array or Int8Array: sqlite3_result_blob()
+ - `undefined`: indicates that the UDF called one of the
+ `sqlite3_result_xyz()` routines on its own, making this
+ function a no-op. Results are _undefined_ if this function is
+ passed the `undefined` value but did _not_ call one of the
+ `sqlite3_result_xyz()` routines.
+
+ Anything else triggers sqlite3_result_error().
+ */
+ capi.sqlite3_create_function_v2.udfSetResult =
+ capi.sqlite3_create_function.udfSetResult =
+ capi.sqlite3_create_window_function.udfSetResult = __udfSetResult;
+
+ /**
+ A helper for UDFs implemented in JS and bound to WASM by the
+ client. When passed the
+ (argc,argv) values from the UDF-related functions which receive
+ them (xFunc, xStep, xInverse), it creates a JS array
+ representing those arguments, converting each to JS in a manner
+ appropriate to its data type: numeric, text, blob
+ (Uint8Array), or null.
+
+ Results are undefined if it's passed anything other than those
+ two arguments from those specific contexts.
+
+ Thus an argc of 4 will result in a length-4 array containing
+ the converted values from the corresponding argv.
+
+ The conversion will throw only on allocation error or an internal
+ error.
+ */
+ capi.sqlite3_create_function_v2.udfConvertArgs =
+ capi.sqlite3_create_function.udfConvertArgs =
+ capi.sqlite3_create_window_function.udfConvertArgs = __udfConvertArgs;
+
+ /**
+ A helper for UDFs implemented in JS and bound to WASM by the
+ client. It expects to be a passed `(sqlite3_context*, Error)`
+ (an exception object or message string). And it sets the
+ current UDF's result to sqlite3_result_error_nomem() or
+ sqlite3_result_error(), depending on whether the 2nd argument
+ is a sqlite3.WasmAllocError object or not.
+ */
+ capi.sqlite3_create_function_v2.udfSetError =
+ capi.sqlite3_create_function.udfSetError =
+ capi.sqlite3_create_window_function.udfSetError = __udfSetError;
+
+ }/*sqlite3_create_function_v2() and sqlite3_create_window_function() proxies*/;
+
+ if(1){/* Special-case handling of sqlite3_prepare_v2() and
+ sqlite3_prepare_v3() */
+ /**
+ Scope-local holder of the two impls of sqlite3_prepare_v2/v3().
+ */
+ const __prepare = Object.create(null);
+ /**
+ This binding expects a JS string as its 2nd argument and
+ null as its final argument. In order to compile multiple
+ statements from a single string, the "full" impl (see
+ below) must be used.
+ */
+ __prepare.basic = wasm.xWrap('sqlite3_prepare_v3',
+ "int", ["sqlite3*", "string",
+ "int"/*ignored for this impl!*/,
+ "int", "**",
+ "**"/*MUST be 0 or null or undefined!*/]);
+ /**
+ Impl which requires that the 2nd argument be a pointer
+ to the SQL string, instead of being converted to a
+ string. This variant is necessary for cases where we
+ require a non-NULL value for the final argument
+ (exec()'ing multiple statements from one input
+ string). For simpler cases, where only the first
+ statement in the SQL string is required, the wrapper
+ named sqlite3_prepare_v2() is sufficient and easier to
+ use because it doesn't require dealing with pointers.
+ */
+ __prepare.full = wasm.xWrap('sqlite3_prepare_v3',
+ "int", ["sqlite3*", "*", "int", "int",
+ "**", "**"]);
+
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_prepare_v3 = function f(pDb, sql, sqlLen, prepFlags, ppStmt, pzTail){
+ if(f.length!==arguments.length){
+ return __dbArgcMismatch(pDb,"sqlite3_prepare_v3",f.length);
+ }
+ const [xSql, xSqlLen] = __flexiString(sql, sqlLen);
+ switch(typeof xSql){
+ case 'string': return __prepare.basic(pDb, xSql, xSqlLen, prepFlags, ppStmt, null);
+ case 'number': return __prepare.full(pDb, xSql, xSqlLen, prepFlags, ppStmt, pzTail);
+ default:
+ return util.sqlite3_wasm_db_error(
+ pDb, capi.SQLITE_MISUSE,
+ "Invalid SQL argument type for sqlite3_prepare_v2/v3()."
+ );
+ }
+ };
+
+ /* Documented in the api object's initializer. */
+ capi.sqlite3_prepare_v2 = function f(pDb, sql, sqlLen, ppStmt, pzTail){
+ return (f.length===arguments.length)
+ ? capi.sqlite3_prepare_v3(pDb, sql, sqlLen, 0, ppStmt, pzTail)
+ : __dbArgcMismatch(pDb,"sqlite3_prepare_v2",f.length);
+ };
+ }/*sqlite3_prepare_v2/v3()*/;
+
+ {/* Import C-level constants and structs... */
+ const cJson = wasm.xCall('sqlite3_wasm_enum_json');
+ if(!cJson){
+ toss("Maintenance required: increase sqlite3_wasm_enum_json()'s",
+ "static buffer size!");
+ }
+ wasm.ctype = JSON.parse(wasm.cstringToJs(cJson));
+ //console.debug('wasm.ctype length =',wasm.cstrlen(cJson));
+ for(const t of ['access', 'blobFinalizers', 'dataTypes',
+ 'encodings', 'fcntl', 'flock', 'ioCap',
+ 'openFlags', 'prepareFlags', 'resultCodes',
+ 'serialize', 'syncFlags', 'trace', 'udfFlags',
+ 'version'
+ ]){
+ for(const e of Object.entries(wasm.ctype[t])){
+ // ^^^ [k,v] there triggers a buggy code transormation via one
+ // of the Emscripten-driven optimizers.
+ capi[e[0]] = e[1];
+ }
+ }
+ const __rcMap = Object.create(null);
+ for(const t of ['resultCodes']){
+ for(const e of Object.entries(wasm.ctype[t])){
+ __rcMap[e[1]] = e[0];
+ }
+ }
+ /**
+ For the given integer, returns the SQLITE_xxx result code as a
+ string, or undefined if no such mapping is found.
+ */
+ capi.sqlite3_js_rc_str = (rc)=>__rcMap[rc];
+ /* Bind all registered C-side structs... */
+ const notThese = Object.assign(Object.create(null),{
+ // Structs NOT to register
+ WasmTestStruct: true
+ });
+ if(!util.isUIThread()){
+ /* We remove the kvvfs VFS from Worker threads below. */
+ notThese.sqlite3_kvvfs_methods = true;
+ }
+ for(const s of wasm.ctype.structs){
+ if(!notThese[s.name]){
+ capi[s.name] = sqlite3.StructBinder(s);
+ }
+ }
+ }/*end C constant imports*/
+
+ const pKvvfs = capi.sqlite3_vfs_find("kvvfs");
+ if( pKvvfs ){/* kvvfs-specific glue */
+ if(util.isUIThread()){
+ const kvvfsMethods = new capi.sqlite3_kvvfs_methods(
+ wasm.exports.sqlite3_wasm_kvvfs_methods()
+ );
+ delete capi.sqlite3_kvvfs_methods;
+
+ const kvvfsMakeKey = wasm.exports.sqlite3_wasm_kvvfsMakeKeyOnPstack,
+ pstack = wasm.pstack,
+ pAllocRaw = wasm.exports.sqlite3_wasm_pstack_alloc;
+
+ const kvvfsStorage = (zClass)=>
+ ((115/*=='s'*/===wasm.getMemValue(zClass))
+ ? sessionStorage : localStorage);
+
+ /**
+ Implementations for members of the object referred to by
+ sqlite3_wasm_kvvfs_methods(). We swap out the native
+ implementations with these, which use localStorage or
+ sessionStorage for their backing store.
+ */
+ const kvvfsImpls = {
+ xRead: (zClass, zKey, zBuf, nBuf)=>{
+ const stack = pstack.pointer,
+ astack = wasm.scopedAllocPush();
+ try {
+ const zXKey = kvvfsMakeKey(zClass,zKey);
+ if(!zXKey) return -3/*OOM*/;
+ const jKey = wasm.cstringToJs(zXKey);
+ const jV = kvvfsStorage(zClass).getItem(jKey);
+ if(!jV) return -1;
+ const nV = jV.length /* Note that we are relying 100% on v being
+ ASCII so that jV.length is equal to the
+ C-string's byte length. */;
+ if(nBuf<=0) return nV;
+ else if(1===nBuf){
+ wasm.setMemValue(zBuf, 0);
+ return nV;
+ }
+ const zV = wasm.scopedAllocCString(jV);
+ if(nBuf > nV + 1) nBuf = nV + 1;
+ wasm.heap8u().copyWithin(zBuf, zV, zV + nBuf - 1);
+ wasm.setMemValue(zBuf + nBuf - 1, 0);
+ return nBuf - 1;
+ }catch(e){
+ console.error("kvstorageRead()",e);
+ return -2;
+ }finally{
+ pstack.restore(stack);
+ wasm.scopedAllocPop(astack);
+ }
+ },
+ xWrite: (zClass, zKey, zData)=>{
+ const stack = pstack.pointer;
+ try {
+ const zXKey = kvvfsMakeKey(zClass,zKey);
+ if(!zXKey) return 1/*OOM*/;
+ const jKey = wasm.cstringToJs(zXKey);
+ kvvfsStorage(zClass).setItem(jKey, wasm.cstringToJs(zData));
+ return 0;
+ }catch(e){
+ console.error("kvstorageWrite()",e);
+ return capi.SQLITE_IOERR;
+ }finally{
+ pstack.restore(stack);
+ }
+ },
+ xDelete: (zClass, zKey)=>{
+ const stack = pstack.pointer;
+ try {
+ const zXKey = kvvfsMakeKey(zClass,zKey);
+ if(!zXKey) return 1/*OOM*/;
+ kvvfsStorage(zClass).removeItem(wasm.cstringToJs(zXKey));
+ return 0;
+ }catch(e){
+ console.error("kvstorageDelete()",e);
+ return capi.SQLITE_IOERR;
+ }finally{
+ pstack.restore(stack);
+ }
+ }
+ }/*kvvfsImpls*/;
+ for(const k of Object.keys(kvvfsImpls)){
+ kvvfsMethods[kvvfsMethods.memberKey(k)] =
+ wasm.installFunction(
+ kvvfsMethods.memberSignature(k),
+ kvvfsImpls[k]
+ );
+ }
+ }else{
+ /* Worker thread: unregister kvvfs to avoid it being used
+ for anything other than local/sessionStorage. It "can"
+ be used that way but it's not really intended to be. */
+ capi.sqlite3_vfs_unregister(pKvvfs);
+ }
+ }/*pKvvfs*/
+
+});
diff --git a/ext/wasm/api/sqlite3-api-oo1.js b/ext/wasm/api/sqlite3-api-oo1.js
new file mode 100644
index 0000000..02ce9c0
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-oo1.js
@@ -0,0 +1,1800 @@
+/*
+ 2022-07-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file contains the so-called OO #1 API wrapper for the sqlite3
+ WASM build. It requires that sqlite3-api-glue.js has already run
+ and it installs its deliverable as self.sqlite3.oo1.
+*/
+self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ const toss3 = (...args)=>{throw new sqlite3.SQLite3Error(...args)};
+
+ const capi = sqlite3.capi, wasm = sqlite3.wasm, util = sqlite3.util;
+ /* What follows is colloquially known as "OO API #1". It is a
+ binding of the sqlite3 API which is designed to be run within
+ the same thread (main or worker) as the one in which the
+ sqlite3 WASM binding was initialized. This wrapper cannot use
+ the sqlite3 binding if, e.g., the wrapper is in the main thread
+ and the sqlite3 API is in a worker. */
+
+ /**
+ In order to keep clients from manipulating, perhaps
+ inadvertently, the underlying pointer values of DB and Stmt
+ instances, we'll gate access to them via the `pointer` property
+ accessor and store their real values in this map. Keys = DB/Stmt
+ objects, values = pointer values. This also unifies how those are
+ accessed, for potential use downstream via custom
+ wasm.xWrap() function signatures which know how to extract
+ it.
+ */
+ const __ptrMap = new WeakMap();
+ /**
+ Map of DB instances to objects, each object being a map of Stmt
+ wasm pointers to Stmt objects.
+ */
+ const __stmtMap = new WeakMap();
+
+ /** If object opts has _its own_ property named p then that
+ property's value is returned, else dflt is returned. */
+ const getOwnOption = (opts, p, dflt)=>{
+ const d = Object.getOwnPropertyDescriptor(opts,p);
+ return d ? d.value : dflt;
+ };
+
+ // Documented in DB.checkRc()
+ const checkSqlite3Rc = function(dbPtr, sqliteResultCode){
+ if(sqliteResultCode){
+ if(dbPtr instanceof DB) dbPtr = dbPtr.pointer;
+ toss3(
+ "sqlite result code",sqliteResultCode+":",
+ (dbPtr
+ ? capi.sqlite3_errmsg(dbPtr)
+ : capi.sqlite3_errstr(sqliteResultCode))
+ );
+ }
+ };
+
+ /**
+ sqlite3_trace_v2() callback which gets installed by the DB ctor
+ if its open-flags contain "t".
+ */
+ const __dbTraceToConsole =
+ wasm.installFunction('i(ippp)', function(t,c,p,x){
+ if(capi.SQLITE_TRACE_STMT===t){
+ // x == SQL, p == sqlite3_stmt*
+ console.log("SQL TRACE #"+(++this.counter),
+ wasm.cstringToJs(x));
+ }
+ }.bind({counter: 0}));
+
+ /**
+ A map of sqlite3_vfs pointers to SQL code to run when the DB
+ constructor opens a database with the given VFS.
+ */
+ const __vfsPostOpenSql = Object.create(null);
+
+ /**
+ A proxy for DB class constructors. It must be called with the
+ being-construct DB object as its "this". See the DB constructor
+ for the argument docs. This is split into a separate function
+ in order to enable simple creation of special-case DB constructors,
+ e.g. JsStorageDb and OpfsDb.
+
+ Expects to be passed a configuration object with the following
+ properties:
+
+ - `.filename`: the db filename. It may be a special name like ":memory:"
+ or "".
+
+ - `.flags`: as documented in the DB constructor.
+
+ - `.vfs`: as documented in the DB constructor.
+
+ It also accepts those as the first 3 arguments.
+ */
+ const dbCtorHelper = function ctor(...args){
+ if(!ctor._name2vfs){
+ /**
+ Map special filenames which we handle here (instead of in C)
+ to some helpful metadata...
+
+ As of 2022-09-20, the C API supports the names :localStorage:
+ and :sessionStorage: for kvvfs. However, C code cannot
+ determine (without embedded JS code, e.g. via Emscripten's
+ EM_JS()) whether the kvvfs is legal in the current browser
+ context (namely the main UI thread). In order to help client
+ code fail early on, instead of it being delayed until they
+ try to read or write a kvvfs-backed db, we'll check for those
+ names here and throw if they're not legal in the current
+ context.
+ */
+ ctor._name2vfs = Object.create(null);
+ const isWorkerThread = ('function'===typeof importScripts/*===running in worker thread*/)
+ ? (n)=>toss3("The VFS for",n,"is only available in the main window thread.")
+ : false;
+ ctor._name2vfs[':localStorage:'] = {
+ vfs: 'kvvfs', filename: isWorkerThread || (()=>'local')
+ };
+ ctor._name2vfs[':sessionStorage:'] = {
+ vfs: 'kvvfs', filename: isWorkerThread || (()=>'session')
+ };
+ }
+ const opt = ctor.normalizeArgs(...args);
+ let fn = opt.filename, vfsName = opt.vfs, flagsStr = opt.flags;
+ if(('string'!==typeof fn && 'number'!==typeof fn)
+ || 'string'!==typeof flagsStr
+ || (vfsName && ('string'!==typeof vfsName && 'number'!==typeof vfsName))){
+ console.error("Invalid DB ctor args",opt,arguments);
+ toss3("Invalid arguments for DB constructor.");
+ }
+ let fnJs = ('number'===typeof fn) ? wasm.cstringToJs(fn) : fn;
+ const vfsCheck = ctor._name2vfs[fnJs];
+ if(vfsCheck){
+ vfsName = vfsCheck.vfs;
+ fn = fnJs = vfsCheck.filename(fnJs);
+ }
+ let pDb, oflags = 0;
+ if( flagsStr.indexOf('c')>=0 ){
+ oflags |= capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
+ }
+ if( flagsStr.indexOf('w')>=0 ) oflags |= capi.SQLITE_OPEN_READWRITE;
+ if( 0===oflags ) oflags |= capi.SQLITE_OPEN_READONLY;
+ oflags |= capi.SQLITE_OPEN_EXRESCODE;
+ const stack = wasm.pstack.pointer;
+ try {
+ const pPtr = wasm.pstack.allocPtr() /* output (sqlite3**) arg */;
+ let rc = capi.sqlite3_open_v2(fn, pPtr, oflags, vfsName || 0);
+ pDb = wasm.getPtrValue(pPtr);
+ checkSqlite3Rc(pDb, rc);
+ if(flagsStr.indexOf('t')>=0){
+ capi.sqlite3_trace_v2(pDb, capi.SQLITE_TRACE_STMT,
+ __dbTraceToConsole, 0);
+ }
+ // Check for per-VFS post-open SQL...
+ const pVfs = capi.sqlite3_js_db_vfs(pDb);
+ //console.warn("Opened db",fn,"with vfs",vfsName,pVfs);
+ if(!pVfs) toss3("Internal error: cannot get VFS for new db handle.");
+ const postInitSql = __vfsPostOpenSql[pVfs];
+ if(postInitSql){
+ rc = capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0);
+ checkSqlite3Rc(pDb, rc);
+ }
+ }catch( e ){
+ if( pDb ) capi.sqlite3_close_v2(pDb);
+ throw e;
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ this.filename = fnJs;
+ __ptrMap.set(this, pDb);
+ __stmtMap.set(this, Object.create(null));
+ };
+
+ /**
+ Sets SQL which should be exec()'d on a DB instance after it is
+ opened with the given VFS pointer. This is intended only for use
+ by DB subclasses or sqlite3_vfs implementations.
+ */
+ dbCtorHelper.setVfsPostOpenSql = function(pVfs, sql){
+ __vfsPostOpenSql[pVfs] = sql;
+ };
+
+ /**
+ A helper for DB constructors. It accepts either a single
+ config-style object or up to 3 arguments (filename, dbOpenFlags,
+ dbVfsName). It returns a new object containing:
+
+ { filename: ..., flags: ..., vfs: ... }
+
+ If passed an object, any additional properties it has are copied
+ as-is into the new object.
+ */
+ dbCtorHelper.normalizeArgs = function(filename=':memory:',flags = 'c',vfs = null){
+ const arg = {};
+ if(1===arguments.length && 'object'===typeof arguments[0]){
+ const x = arguments[0];
+ Object.keys(x).forEach((k)=>arg[k] = x[k]);
+ if(undefined===arg.flags) arg.flags = 'c';
+ if(undefined===arg.vfs) arg.vfs = null;
+ if(undefined===arg.filename) arg.filename = ':memory:';
+ }else{
+ arg.filename = filename;
+ arg.flags = flags;
+ arg.vfs = vfs;
+ }
+ return arg;
+ };
+ /**
+ The DB class provides a high-level OO wrapper around an sqlite3
+ db handle.
+
+ The given db filename must be resolvable using whatever
+ filesystem layer (virtual or otherwise) is set up for the default
+ sqlite3 VFS.
+
+ Note that the special sqlite3 db names ":memory:" and ""
+ (temporary db) have their normal special meanings here and need
+ not resolve to real filenames, but "" uses an on-storage
+ temporary database and requires that the VFS support that.
+
+ The second argument specifies the open/create mode for the
+ database. It must be string containing a sequence of letters (in
+ any order, but case sensitive) specifying the mode:
+
+ - "c": create if it does not exist, else fail if it does not
+ exist. Implies the "w" flag.
+
+ - "w": write. Implies "r": a db cannot be write-only.
+
+ - "r": read-only if neither "w" nor "c" are provided, else it
+ is ignored.
+
+ - "t": enable tracing of SQL executed on this database handle,
+ sending it to `console.log()`. To disable it later, call
+ `sqlite3.capi.sqlite3_trace_v2(thisDb.pointer, 0, 0, 0)`.
+
+ If "w" is not provided, the db is implicitly read-only, noting
+ that "rc" is meaningless
+
+ Any other letters are currently ignored. The default is
+ "c". These modes are ignored for the special ":memory:" and ""
+ names and _may_ be ignored altogether for certain VFSes.
+
+ The final argument is analogous to the final argument of
+ sqlite3_open_v2(): the name of an sqlite3 VFS. Pass a falsy value,
+ or none at all, to use the default. If passed a value, it must
+ be the string name of a VFS.
+
+ The constructor optionally (and preferably) takes its arguments
+ in the form of a single configuration object with the following
+ properties:
+
+ - `filename`: database file name
+ - `flags`: open-mode flags
+ - `vfs`: the VFS fname
+
+ The `filename` and `vfs` arguments may be either JS strings or
+ C-strings allocated via WASM. `flags` is required to be a JS
+ string (because it's specific to this API, which is specific
+ to JS).
+
+ For purposes of passing a DB instance to C-style sqlite3
+ functions, the DB object's read-only `pointer` property holds its
+ `sqlite3*` pointer value. That property can also be used to check
+ whether this DB instance is still open.
+
+ In the main window thread, the filenames `":localStorage:"` and
+ `":sessionStorage:"` are special: they cause the db to use either
+ localStorage or sessionStorage for storing the database using
+ the kvvfs. If one of these names are used, they trump
+ any vfs name set in the arguments.
+ */
+ const DB = function(...args){
+ dbCtorHelper.apply(this, args);
+ };
+ DB.dbCtorHelper = dbCtorHelper;
+
+ /**
+ Internal-use enum for mapping JS types to DB-bindable types.
+ These do not (and need not) line up with the SQLITE_type
+ values. All values in this enum must be truthy and distinct
+ but they need not be numbers.
+ */
+ const BindTypes = {
+ null: 1,
+ number: 2,
+ string: 3,
+ boolean: 4,
+ blob: 5
+ };
+ BindTypes['undefined'] == BindTypes.null;
+ if(wasm.bigIntEnabled){
+ BindTypes.bigint = BindTypes.number;
+ }
+
+ /**
+ This class wraps sqlite3_stmt. Calling this constructor
+ directly will trigger an exception. Use DB.prepare() to create
+ new instances.
+
+ For purposes of passing a Stmt instance to C-style sqlite3
+ functions, its read-only `pointer` property holds its `sqlite3_stmt*`
+ pointer value.
+
+ Other non-function properties include:
+
+ - `db`: the DB object which created the statement.
+
+ - `columnCount`: the number of result columns in the query, or 0 for
+ queries which cannot return results.
+
+ - `parameterCount`: the number of bindable paramters in the query.
+ */
+ const Stmt = function(){
+ if(BindTypes!==arguments[2]){
+ toss3("Do not call the Stmt constructor directly. Use DB.prepare().");
+ }
+ this.db = arguments[0];
+ __ptrMap.set(this, arguments[1]);
+ this.columnCount = capi.sqlite3_column_count(this.pointer);
+ this.parameterCount = capi.sqlite3_bind_parameter_count(this.pointer);
+ };
+
+ /** Throws if the given DB has been closed, else it is returned. */
+ const affirmDbOpen = function(db){
+ if(!db.pointer) toss3("DB has been closed.");
+ return db;
+ };
+
+ /** Throws if ndx is not an integer or if it is out of range
+ for stmt.columnCount, else returns stmt.
+
+ Reminder: this will also fail after the statement is finalized
+ but the resulting error will be about an out-of-bounds column
+ index rather than a statement-is-finalized error.
+ */
+ const affirmColIndex = function(stmt,ndx){
+ if((ndx !== (ndx|0)) || ndx<0 || ndx>=stmt.columnCount){
+ toss3("Column index",ndx,"is out of range.");
+ }
+ return stmt;
+ };
+
+ /**
+ Expects to be passed the `arguments` object from DB.exec(). Does
+ the argument processing/validation, throws on error, and returns
+ a new object on success:
+
+ { sql: the SQL, opt: optionsObj, cbArg: function}
+
+ The opt object is a normalized copy of any passed to this
+ function. The sql will be converted to a string if it is provided
+ in one of the supported non-string formats.
+
+ cbArg is only set if the opt.callback or opt.resultRows are set,
+ in which case it's a function which expects to be passed the
+ current Stmt and returns the callback argument of the type
+ indicated by the input arguments.
+ */
+ const parseExecArgs = function(db, args){
+ const out = Object.create(null);
+ out.opt = Object.create(null);
+ switch(args.length){
+ case 1:
+ if('string'===typeof args[0] || util.isSQLableTypedArray(args[0])){
+ out.sql = args[0];
+ }else if(Array.isArray(args[0])){
+ out.sql = args[0];
+ }else if(args[0] && 'object'===typeof args[0]){
+ out.opt = args[0];
+ out.sql = out.opt.sql;
+ }
+ break;
+ case 2:
+ out.sql = args[0];
+ out.opt = args[1];
+ break;
+ default: toss3("Invalid argument count for exec().");
+ };
+ out.sql = util.flexibleString(out.sql);
+ if('string'!==typeof out.sql){
+ toss3("Missing SQL argument or unsupported SQL value type.");
+ }
+ const opt = out.opt;
+ switch(opt.returnValue){
+ case 'resultRows':
+ if(!opt.resultRows) opt.resultRows = [];
+ out.returnVal = ()=>opt.resultRows;
+ break;
+ case 'saveSql':
+ if(!opt.saveSql) opt.saveSql = [];
+ out.returnVal = ()=>opt.saveSql;
+ break;
+ case undefined:
+ case 'this':
+ out.returnVal = ()=>db;
+ break;
+ default:
+ toss3("Invalid returnValue value:",opt.returnValue);
+ }
+ if(opt.callback || opt.resultRows){
+ switch((undefined===opt.rowMode)
+ ? 'array' : opt.rowMode) {
+ case 'object': out.cbArg = (stmt)=>stmt.get(Object.create(null)); break;
+ case 'array': out.cbArg = (stmt)=>stmt.get([]); break;
+ case 'stmt':
+ if(Array.isArray(opt.resultRows)){
+ toss3("exec(): invalid rowMode for a resultRows array: must",
+ "be one of 'array', 'object',",
+ "a result column number, or column name reference.");
+ }
+ out.cbArg = (stmt)=>stmt;
+ break;
+ default:
+ if(util.isInt32(opt.rowMode)){
+ out.cbArg = (stmt)=>stmt.get(opt.rowMode);
+ break;
+ }else if('string'===typeof opt.rowMode && opt.rowMode.length>1){
+ /* "$X", ":X", and "@X" fetch column named "X" (case-sensitive!) */
+ const prefix = opt.rowMode[0];
+ if(':'===prefix || '@'===prefix || '$'===prefix){
+ out.cbArg = function(stmt){
+ const rc = stmt.get(this.obj)[this.colName];
+ return (undefined===rc) ? toss3("exec(): unknown result column:",this.colName) : rc;
+ }.bind({
+ obj:Object.create(null),
+ colName: opt.rowMode.substr(1)
+ });
+ break;
+ }
+ }
+ toss3("Invalid rowMode:",opt.rowMode);
+ }
+ }
+ return out;
+ };
+
+ /**
+ Internal impl of the DB.selectArray() and
+ selectObject() methods.
+ */
+ const __selectFirstRow = (db, sql, bind, getArg)=>{
+ let stmt, rc;
+ try {
+ stmt = db.prepare(sql).bind(bind);
+ if(stmt.step()) rc = stmt.get(getArg);
+ }finally{
+ if(stmt) stmt.finalize();
+ }
+ return rc;
+ };
+
+ /**
+ Expects to be given a DB instance or an `sqlite3*` pointer (may
+ be null) and an sqlite3 API result code. If the result code is
+ not falsy, this function throws an SQLite3Error with an error
+ message from sqlite3_errmsg(), using dbPtr as the db handle, or
+ sqlite3_errstr() if dbPtr is falsy. Note that if it's passed a
+ non-error code like SQLITE_ROW or SQLITE_DONE, it will still
+ throw but the error string might be "Not an error." The various
+ non-0 non-error codes need to be checked for in
+ client code where they are expected.
+ */
+ DB.checkRc = checkSqlite3Rc;
+
+ DB.prototype = {
+ /** Returns true if this db handle is open, else false. */
+ isOpen: function(){
+ return !!this.pointer;
+ },
+ /** Throws if this given DB has been closed, else returns `this`. */
+ affirmOpen: function(){
+ return affirmDbOpen(this);
+ },
+ /**
+ Finalizes all open statements and closes this database
+ connection. This is a no-op if the db has already been
+ closed. After calling close(), `this.pointer` will resolve to
+ `undefined`, so that can be used to check whether the db
+ instance is still opened.
+
+ If this.onclose.before is a function then it is called before
+ any close-related cleanup.
+
+ If this.onclose.after is a function then it is called after the
+ db is closed but before auxiliary state like this.filename is
+ cleared.
+
+ Both onclose handlers are passed this object. If this db is not
+ opened, neither of the handlers are called. Any exceptions the
+ handlers throw are ignored because "destructors must not
+ throw."
+
+ Note that garbage collection of a db handle, if it happens at
+ all, will never trigger close(), so onclose handlers are not a
+ reliable way to implement close-time cleanup or maintenance of
+ a db.
+ */
+ close: function(){
+ if(this.pointer){
+ if(this.onclose && (this.onclose.before instanceof Function)){
+ try{this.onclose.before(this)}
+ catch(e){/*ignore*/}
+ }
+ const pDb = this.pointer;
+ Object.keys(__stmtMap.get(this)).forEach((k,s)=>{
+ if(s && s.pointer) s.finalize();
+ });
+ __ptrMap.delete(this);
+ __stmtMap.delete(this);
+ capi.sqlite3_close_v2(pDb);
+ if(this.onclose && (this.onclose.after instanceof Function)){
+ try{this.onclose.after(this)}
+ catch(e){/*ignore*/}
+ }
+ delete this.filename;
+ }
+ },
+ /**
+ Returns the number of changes, as per sqlite3_changes()
+ (if the first argument is false) or sqlite3_total_changes()
+ (if it's true). If the 2nd argument is true, it uses
+ sqlite3_changes64() or sqlite3_total_changes64(), which
+ will trigger an exception if this build does not have
+ BigInt support enabled.
+ */
+ changes: function(total=false,sixtyFour=false){
+ const p = affirmDbOpen(this).pointer;
+ if(total){
+ return sixtyFour
+ ? capi.sqlite3_total_changes64(p)
+ : capi.sqlite3_total_changes(p);
+ }else{
+ return sixtyFour
+ ? capi.sqlite3_changes64(p)
+ : capi.sqlite3_changes(p);
+ }
+ },
+ /**
+ Similar to the this.filename but returns the
+ sqlite3_db_filename() value for the given database name,
+ defaulting to "main". The argument may be either a JS string
+ or a pointer to a WASM-allocated C-string.
+ */
+ dbFilename: function(dbName='main'){
+ return capi.sqlite3_db_filename(affirmDbOpen(this).pointer, dbName);
+ },
+ /**
+ Returns the name of the given 0-based db number, as documented
+ for sqlite3_db_name().
+ */
+ dbName: function(dbNumber=0){
+ return capi.sqlite3_db_name(affirmDbOpen(this).pointer, dbNumber);
+ },
+ /**
+ Returns the name of the sqlite3_vfs used by the given database
+ of this connection (defaulting to 'main'). The argument may be
+ either a JS string or a WASM C-string. Returns undefined if the
+ given db name is invalid. Throws if this object has been
+ close()d.
+ */
+ dbVfsName: function(dbName=0){
+ let rc;
+ const pVfs = capi.sqlite3_js_db_vfs(
+ affirmDbOpen(this).pointer, dbName
+ );
+ if(pVfs){
+ const v = new capi.sqlite3_vfs(pVfs);
+ try{ rc = wasm.cstringToJs(v.$zName) }
+ finally { v.dispose() }
+ }
+ return rc;
+ },
+ /**
+ Compiles the given SQL and returns a prepared Stmt. This is
+ the only way to create new Stmt objects. Throws on error.
+
+ The given SQL must be a string, a Uint8Array holding SQL, a
+ WASM pointer to memory holding the NUL-terminated SQL string,
+ or an array of strings. In the latter case, the array is
+ concatenated together, with no separators, to form the SQL
+ string (arrays are often a convenient way to formulate long
+ statements). If the SQL contains no statements, an
+ SQLite3Error is thrown.
+
+ Design note: the C API permits empty SQL, reporting it as a 0
+ result code and a NULL stmt pointer. Supporting that case here
+ would cause extra work for all clients: any use of the Stmt API
+ on such a statement will necessarily throw, so clients would be
+ required to check `stmt.pointer` after calling `prepare()` in
+ order to determine whether the Stmt instance is empty or not.
+ Long-time practice (with other sqlite3 script bindings)
+ suggests that the empty-prepare case is sufficiently rare that
+ supporting it here would simply hurt overall usability.
+ */
+ prepare: function(sql){
+ affirmDbOpen(this);
+ const stack = wasm.pstack.pointer;
+ let ppStmt, pStmt;
+ try{
+ ppStmt = wasm.pstack.alloc(8)/* output (sqlite3_stmt**) arg */;
+ DB.checkRc(this, capi.sqlite3_prepare_v2(this.pointer, sql, -1, ppStmt, null));
+ pStmt = wasm.getPtrValue(ppStmt);
+ }
+ finally {
+ wasm.pstack.restore(stack);
+ }
+ if(!pStmt) toss3("Cannot prepare empty SQL.");
+ const stmt = new Stmt(this, pStmt, BindTypes);
+ __stmtMap.get(this)[pStmt] = stmt;
+ return stmt;
+ },
+ /**
+ Executes one or more SQL statements in the form of a single
+ string. Its arguments must be either (sql,optionsObject) or
+ (optionsObject). In the latter case, optionsObject.sql must
+ contain the SQL to execute. By default it returns this object
+ but that can be changed via the `returnValue` option as
+ described below. Throws on error.
+
+ If no SQL is provided, or a non-string is provided, an
+ exception is triggered. Empty SQL, on the other hand, is
+ simply a no-op.
+
+ The optional options object may contain any of the following
+ properties:
+
+ - `sql` = the SQL to run (unless it's provided as the first
+ argument). This must be of type string, Uint8Array, or an array
+ of strings. In the latter case they're concatenated together
+ as-is, _with no separator_ between elements, before evaluation.
+ The array form is often simpler for long hand-written queries.
+
+ - `bind` = a single value valid as an argument for
+ Stmt.bind(). This is _only_ applied to the _first_ non-empty
+ statement in the SQL which has any bindable parameters. (Empty
+ statements are skipped entirely.)
+
+ - `saveSql` = an optional array. If set, the SQL of each
+ executed statement is appended to this array before the
+ statement is executed (but after it is prepared - we don't have
+ the string until after that). Empty SQL statements are elided
+ but can have odd effects in the output. e.g. SQL of: `"select
+ 1; -- empty\n; select 2"` will result in an array containing
+ `["select 1;", "--empty \n; select 2"]`. That's simply how
+ sqlite3 records the SQL for the 2nd statement.
+
+ ==================================================================
+ The following options apply _only_ to the _first_ statement
+ which has a non-zero result column count, regardless of whether
+ the statement actually produces any result rows.
+ ==================================================================
+
+ - `columnNames`: if this is an array, the column names of the
+ result set are stored in this array before the callback (if
+ any) is triggered (regardless of whether the query produces any
+ result rows). If no statement has result columns, this value is
+ unchanged. Achtung: an SQL result may have multiple columns
+ with identical names.
+
+ - `callback` = a function which gets called for each row of
+ the result set, but only if that statement has any result
+ _rows_. The callback's "this" is the options object, noting
+ that this function synthesizes one if the caller does not pass
+ one to exec(). The second argument passed to the callback is
+ always the current Stmt object, as it's needed if the caller
+ wants to fetch the column names or some such (noting that they
+ could also be fetched via `this.columnNames`, if the client
+ provides the `columnNames` option).
+
+ ACHTUNG: The callback MUST NOT modify the Stmt object. Calling
+ any of the Stmt.get() variants, Stmt.getColumnName(), or
+ similar, is legal, but calling step() or finalize() is
+ not. Member methods which are illegal in this context will
+ trigger an exception.
+
+ The first argument passed to the callback defaults to an array of
+ values from the current result row but may be changed with ...
+
+ - `rowMode` = specifies the type of he callback's first argument.
+ It may be any of...
+
+ A) A string describing what type of argument should be passed
+ as the first argument to the callback:
+
+ A.1) `'array'` (the default) causes the results of
+ `stmt.get([])` to be passed to the `callback` and/or appended
+ to `resultRows`
+
+ A.2) `'object'` causes the results of
+ `stmt.get(Object.create(null))` to be passed to the
+ `callback` and/or appended to `resultRows`. Achtung: an SQL
+ result may have multiple columns with identical names. In
+ that case, the right-most column will be the one set in this
+ object!
+
+ A.3) `'stmt'` causes the current Stmt to be passed to the
+ callback, but this mode will trigger an exception if
+ `resultRows` is an array because appending the statement to
+ the array would be downright unhelpful.
+
+ B) An integer, indicating a zero-based column in the result
+ row. Only that one single value will be passed on.
+
+ C) A string with a minimum length of 2 and leading character of
+ ':', '$', or '@' will fetch the row as an object, extract that
+ one field, and pass that field's value to the callback. Note
+ that these keys are case-sensitive so must match the case used
+ in the SQL. e.g. `"select a A from t"` with a `rowMode` of
+ `'$A'` would work but `'$a'` would not. A reference to a column
+ not in the result set will trigger an exception on the first
+ row (as the check is not performed until rows are fetched).
+ Note also that `$` is a legal identifier character in JS so
+ need not be quoted. (Design note: those 3 characters were
+ chosen because they are the characters support for naming bound
+ parameters.)
+
+ Any other `rowMode` value triggers an exception.
+
+ - `resultRows`: if this is an array, it functions similarly to
+ the `callback` option: each row of the result set (if any),
+ with the exception that the `rowMode` 'stmt' is not legal. It
+ is legal to use both `resultRows` and `callback`, but
+ `resultRows` is likely much simpler to use for small data sets
+ and can be used over a WebWorker-style message interface.
+ exec() throws if `resultRows` is set and `rowMode` is 'stmt'.
+
+ - `returnValue`: is a string specifying what this function
+ should return:
+
+ A) The default value is `"this"`, meaning that the
+ DB object itself should be returned.
+
+ B) `"resultRows"` means to return the value of the
+ `resultRows` option. If `resultRows` is not set, this
+ function behaves as if it were set to an empty array.
+
+ C) `"saveSql"` means to return the value of the
+ `saveSql` option. If `saveSql` is not set, this
+ function behaves as if it were set to an empty array.
+
+ Potential TODOs:
+
+ - `bind`: permit an array of arrays/objects to bind. The first
+ sub-array would act on the first statement which has bindable
+ parameters (as it does now). The 2nd would act on the next such
+ statement, etc.
+
+ - `callback` and `resultRows`: permit an array entries with
+ semantics similar to those described for `bind` above.
+
+ */
+ exec: function(/*(sql [,obj]) || (obj)*/){
+ affirmDbOpen(this);
+ const arg = parseExecArgs(this, arguments);
+ if(!arg.sql){
+ return toss3("exec() requires an SQL string.");
+ }
+ const opt = arg.opt;
+ const callback = opt.callback;
+ const resultRows =
+ Array.isArray(opt.resultRows) ? opt.resultRows : undefined;
+ let stmt;
+ let bind = opt.bind;
+ let evalFirstResult = !!(arg.cbArg || opt.columnNames) /* true to evaluate the first result-returning query */;
+ const stack = wasm.scopedAllocPush();
+ try{
+ const isTA = util.isSQLableTypedArray(arg.sql)
+ /* Optimization: if the SQL is a TypedArray we can save some string
+ conversion costs. */;
+ /* Allocate the two output pointers (ppStmt, pzTail) and heap
+ space for the SQL (pSql). When prepare_v2() returns, pzTail
+ will point to somewhere in pSql. */
+ let sqlByteLen = isTA ? arg.sql.byteLength : wasm.jstrlen(arg.sql);
+ const ppStmt = wasm.scopedAlloc(/* output (sqlite3_stmt**) arg and pzTail */
+ (2 * wasm.ptrSizeof)
+ + (sqlByteLen + 1/* SQL + NUL */));
+ const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
+ let pSql = pzTail + wasm.ptrSizeof;
+ const pSqlEnd = pSql + sqlByteLen;
+ if(isTA) wasm.heap8().set(arg.sql, pSql);
+ else wasm.jstrcpy(arg.sql, wasm.heap8(), pSql, sqlByteLen, false);
+ wasm.setMemValue(pSql + sqlByteLen, 0/*NUL terminator*/);
+ while(pSql && wasm.getMemValue(pSql, 'i8')
+ /* Maintenance reminder:^^^ _must_ be 'i8' or else we
+ will very likely cause an endless loop. What that's
+ doing is checking for a terminating NUL byte. If we
+ use i32 or similar then we read 4 bytes, read stuff
+ around the NUL terminator, and get stuck in and
+ endless loop at the end of the SQL, endlessly
+ re-preparing an empty statement. */ ){
+ wasm.setPtrValue(ppStmt, 0);
+ wasm.setPtrValue(pzTail, 0);
+ DB.checkRc(this, capi.sqlite3_prepare_v3(
+ this.pointer, pSql, sqlByteLen, 0, ppStmt, pzTail
+ ));
+ const pStmt = wasm.getPtrValue(ppStmt);
+ pSql = wasm.getPtrValue(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue;
+ if(Array.isArray(opt.saveSql)){
+ opt.saveSql.push(capi.sqlite3_sql(pStmt).trim());
+ }
+ stmt = new Stmt(this, pStmt, BindTypes);
+ if(bind && stmt.parameterCount){
+ stmt.bind(bind);
+ bind = null;
+ }
+ if(evalFirstResult && stmt.columnCount){
+ /* Only forward SELECT results for the FIRST query
+ in the SQL which potentially has them. */
+ evalFirstResult = false;
+ if(Array.isArray(opt.columnNames)){
+ stmt.getColumnNames(opt.columnNames);
+ }
+ while(!!arg.cbArg && stmt.step()){
+ stmt._isLocked = true;
+ const row = arg.cbArg(stmt);
+ if(resultRows) resultRows.push(row);
+ if(callback) callback.call(opt, row, stmt);
+ stmt._isLocked = false;
+ }
+ }else{
+ stmt.step();
+ }
+ stmt.finalize();
+ stmt = null;
+ }
+ }/*catch(e){
+ console.warn("DB.exec() is propagating exception",opt,e);
+ throw e;
+ }*/finally{
+ if(stmt){
+ delete stmt._isLocked;
+ stmt.finalize();
+ }
+ wasm.scopedAllocPop(stack);
+ }
+ return arg.returnVal();
+ }/*exec()*/,
+ /**
+ Creates a new scalar UDF (User-Defined Function) which is
+ accessible via SQL code. This function may be called in any
+ of the following forms:
+
+ - (name, function)
+ - (name, function, optionsObject)
+ - (name, optionsObject)
+ - (optionsObject)
+
+ In the final two cases, the function must be defined as the
+ `callback` property of the options object (optionally called
+ `xFunc` to align with the C API documentation). In the final
+ case, the function's name must be the 'name' property.
+
+ The first two call forms can only be used for creating scalar
+ functions. Creating an aggregate or window function requires
+ the options-object form (see below for details).
+
+ UDFs cannot currently be removed from a DB handle after they're
+ added. More correctly, they can be removed as documented for
+ sqlite3_create_function_v2(), but doing so will "leak" the
+ JS-created WASM binding of those functions.
+
+ On success, returns this object. Throws on error.
+
+ When called from SQL arguments to the UDF, and its result,
+ will be converted between JS and SQL with as much fidelity as
+ is feasible, triggering an exception if a type conversion
+ cannot be determined. The docs for sqlite3_create_function_v2()
+ describe the conversions in more detail.
+
+ The values set in the options object differ for scalar and
+ aggregate functions:
+
+ - Scalar: set the `xFunc` function-type property to the UDF
+ function.
+
+ - Aggregate: set the `xStep` and `xFinal` function-type
+ properties to the "step" and "final" callbacks for the
+ aggregate. Do not set the `xFunc` property.
+
+ - Window: set the `xStep`, `xFinal`, `xValue`, and `xInverse`
+ function-type properties. Do not set the `xFunc` property.
+
+ The options object may optionally have an `xDestroy`
+ function-type property, as per sqlite3_create_function_v2().
+ Its argument will be the WASM-pointer-type value of the `pApp`
+ property, and this function will throw if `pApp` is defined but
+ is not null, undefined, or a numeric (WASM pointer)
+ value. i.e. `pApp`, if set, must be value suitable for use as a
+ WASM pointer argument, noting that `null` or `undefined` will
+ translate to 0 for that purpose.
+
+ The options object may contain flags to modify how
+ the function is defined:
+
+ - `arity`: the number of arguments which SQL calls to this
+ function expect or require. The default value is `xFunc.length`
+ or `xStep.length` (i.e. the number of declared parameters it
+ has) **MINUS 1** (see below for why). As a special case, if the
+ `length` is 0, its arity is also 0 instead of -1. A negative
+ arity value means that the function is variadic and may accept
+ any number of arguments, up to sqlite3's compile-time
+ limits. sqlite3 will enforce the argument count if is zero or
+ greater. The callback always receives a pointer to an
+ `sqlite3_context` object as its first argument. Any arguments
+ after that are from SQL code. The leading context argument does
+ _not_ count towards the function's arity. See the docs for
+ sqlite3.capi.sqlite3_create_function_v2() for why that argument
+ is needed in the interface.
+
+ The following options-object properties correspond to flags
+ documented at:
+
+ https://sqlite.org/c3ref/create_function.html
+
+ - `deterministic` = sqlite3.capi.SQLITE_DETERMINISTIC
+ - `directOnly` = sqlite3.capi.SQLITE_DIRECTONLY
+ - `innocuous` = sqlite3.capi.SQLITE_INNOCUOUS
+
+ Sidebar: the ability to add new WASM-accessible functions to
+ the runtime requires that the WASM build is compiled with the
+ equivalent functionality as that provided by Emscripten's
+ `-sALLOW_TABLE_GROWTH` flag.
+ */
+ createFunction: function f(name, xFunc, opt){
+ const isFunc = (f)=>(f instanceof Function);
+ switch(arguments.length){
+ case 1: /* (optionsObject) */
+ opt = name;
+ name = opt.name;
+ xFunc = opt.xFunc || 0;
+ break;
+ case 2: /* (name, callback|optionsObject) */
+ if(!isFunc(xFunc)){
+ opt = xFunc;
+ xFunc = opt.xFunc || 0;
+ }
+ break;
+ case 3: /* name, xFunc, opt */
+ break;
+ default: break;
+ }
+ if(!opt) opt = {};
+ if('string' !== typeof name){
+ toss3("Invalid arguments: missing function name.");
+ }
+ let xStep = opt.xStep || 0;
+ let xFinal = opt.xFinal || 0;
+ const xValue = opt.xValue || 0;
+ const xInverse = opt.xInverse || 0;
+ let isWindow = undefined;
+ if(isFunc(xFunc)){
+ isWindow = false;
+ if(isFunc(xStep) || isFunc(xFinal)){
+ toss3("Ambiguous arguments: scalar or aggregate?");
+ }
+ xStep = xFinal = null;
+ }else if(isFunc(xStep)){
+ if(!isFunc(xFinal)){
+ toss3("Missing xFinal() callback for aggregate or window UDF.");
+ }
+ xFunc = null;
+ }else if(isFunc(xFinal)){
+ toss3("Missing xStep() callback for aggregate or window UDF.");
+ }else{
+ toss3("Missing function-type properties.");
+ }
+ if(false === isWindow){
+ if(isFunc(xValue) || isFunc(xInverse)){
+ toss3("xValue and xInverse are not permitted for non-window UDFs.");
+ }
+ }else if(isFunc(xValue)){
+ if(!isFunc(xInverse)){
+ toss3("xInverse must be provided if xValue is.");
+ }
+ isWindow = true;
+ }else if(isFunc(xInverse)){
+ toss3("xValue must be provided if xInverse is.");
+ }
+ const pApp = opt.pApp;
+ if(undefined!==pApp &&
+ null!==pApp &&
+ (('number'!==typeof pApp) || !util.isInt32(pApp))){
+ toss3("Invalid value for pApp property. Must be a legal WASM pointer value.");
+ }
+ const xDestroy = opt.xDestroy || 0;
+ if(xDestroy && !isFunc(xDestroy)){
+ toss3("xDestroy property must be a function.");
+ }
+ let fFlags = 0 /*flags for sqlite3_create_function_v2()*/;
+ if(getOwnOption(opt, 'deterministic')) fFlags |= capi.SQLITE_DETERMINISTIC;
+ if(getOwnOption(opt, 'directOnly')) fFlags |= capi.SQLITE_DIRECTONLY;
+ if(getOwnOption(opt, 'innocuous')) fFlags |= capi.SQLITE_INNOCUOUS;
+ name = name.toLowerCase();
+ const xArity = xFunc || xStep;
+ const arity = getOwnOption(opt, 'arity');
+ const arityArg = ('number'===typeof arity
+ ? arity
+ : (xArity.length ? xArity.length-1/*for pCtx arg*/ : 0));
+ let rc;
+ if( isWindow ){
+ rc = capi.sqlite3_create_window_function(
+ this.pointer, name, arityArg,
+ capi.SQLITE_UTF8 | fFlags, pApp || 0,
+ xStep, xFinal, xValue, xInverse, xDestroy);
+ }else{
+ rc = capi.sqlite3_create_function_v2(
+ this.pointer, name, arityArg,
+ capi.SQLITE_UTF8 | fFlags, pApp || 0,
+ xFunc, xStep, xFinal, xDestroy);
+ }
+ DB.checkRc(this, rc);
+ return this;
+ }/*createFunction()*/,
+ /**
+ Prepares the given SQL, step()s it one time, and returns
+ the value of the first result column. If it has no results,
+ undefined is returned.
+
+ If passed a second argument, it is treated like an argument
+ to Stmt.bind(), so may be any type supported by that
+ function. Passing the undefined value is the same as passing
+ no value, which is useful when...
+
+ If passed a 3rd argument, it is expected to be one of the
+ SQLITE_{typename} constants. Passing the undefined value is
+ the same as not passing a value.
+
+ Throws on error (e.g. malformed SQL).
+ */
+ selectValue: function(sql,bind,asType){
+ let stmt, rc;
+ try {
+ stmt = this.prepare(sql).bind(bind);
+ if(stmt.step()) rc = stmt.get(0,asType);
+ }finally{
+ if(stmt) stmt.finalize();
+ }
+ return rc;
+ },
+ /**
+ Prepares the given SQL, step()s it one time, and returns an
+ array containing the values of the first result row. If it has
+ no results, `undefined` is returned.
+
+ If passed a second argument other than `undefined`, it is
+ treated like an argument to Stmt.bind(), so may be any type
+ supported by that function.
+
+ Throws on error (e.g. malformed SQL).
+ */
+ selectArray: function(sql,bind){
+ return __selectFirstRow(this, sql, bind, []);
+ },
+
+ /**
+ Prepares the given SQL, step()s it one time, and returns an
+ object containing the key/value pairs of the first result
+ row. If it has no results, `undefined` is returned.
+
+ Note that the order of returned object's keys is not guaranteed
+ to be the same as the order of the fields in the query string.
+
+ If passed a second argument other than `undefined`, it is
+ treated like an argument to Stmt.bind(), so may be any type
+ supported by that function.
+
+ Throws on error (e.g. malformed SQL).
+ */
+ selectObject: function(sql,bind){
+ return __selectFirstRow(this, sql, bind, {});
+ },
+
+ /**
+ Returns the number of currently-opened Stmt handles for this db
+ handle, or 0 if this DB instance is closed.
+ */
+ openStatementCount: function(){
+ return this.pointer ? Object.keys(__stmtMap.get(this)).length : 0;
+ },
+
+ /**
+ Starts a transaction, calls the given callback, and then either
+ rolls back or commits the savepoint, depending on whether the
+ callback throws. The callback is passed this db object as its
+ only argument. On success, returns the result of the
+ callback. Throws on error.
+
+ Note that transactions may not be nested, so this will throw if
+ it is called recursively. For nested transactions, use the
+ savepoint() method or manually manage SAVEPOINTs using exec().
+ */
+ transaction: function(callback){
+ affirmDbOpen(this).exec("BEGIN");
+ try {
+ const rc = callback(this);
+ this.exec("COMMIT");
+ return rc;
+ }catch(e){
+ this.exec("ROLLBACK");
+ throw e;
+ }
+ },
+
+ /**
+ This works similarly to transaction() but uses sqlite3's SAVEPOINT
+ feature. This function starts a savepoint (with an unspecified name)
+ and calls the given callback function, passing it this db object.
+ If the callback returns, the savepoint is released (committed). If
+ the callback throws, the savepoint is rolled back. If it does not
+ throw, it returns the result of the callback.
+ */
+ savepoint: function(callback){
+ affirmDbOpen(this).exec("SAVEPOINT oo1");
+ try {
+ const rc = callback(this);
+ this.exec("RELEASE oo1");
+ return rc;
+ }catch(e){
+ this.exec("ROLLBACK to SAVEPOINT oo1; RELEASE SAVEPOINT oo1");
+ throw e;
+ }
+ }
+ }/*DB.prototype*/;
+
+
+ /** Throws if the given Stmt has been finalized, else stmt is
+ returned. */
+ const affirmStmtOpen = function(stmt){
+ if(!stmt.pointer) toss3("Stmt has been closed.");
+ return stmt;
+ };
+
+ /** Returns an opaque truthy value from the BindTypes
+ enum if v's type is a valid bindable type, else
+ returns a falsy value. As a special case, a value of
+ undefined is treated as a bind type of null. */
+ const isSupportedBindType = function(v){
+ let t = BindTypes[(null===v||undefined===v) ? 'null' : typeof v];
+ switch(t){
+ case BindTypes.boolean:
+ case BindTypes.null:
+ case BindTypes.number:
+ case BindTypes.string:
+ return t;
+ case BindTypes.bigint:
+ if(wasm.bigIntEnabled) return t;
+ /* else fall through */
+ default:
+ //console.log("isSupportedBindType",t,v);
+ return util.isBindableTypedArray(v) ? BindTypes.blob : undefined;
+ }
+ };
+
+ /**
+ If isSupportedBindType(v) returns a truthy value, this
+ function returns that value, else it throws.
+ */
+ const affirmSupportedBindType = function(v){
+ //console.log('affirmSupportedBindType',v);
+ return isSupportedBindType(v) || toss3("Unsupported bind() argument type:",typeof v);
+ };
+
+ /**
+ If key is a number and within range of stmt's bound parameter
+ count, key is returned.
+
+ If key is not a number then it is checked against named
+ parameters. If a match is found, its index is returned.
+
+ Else it throws.
+ */
+ const affirmParamIndex = function(stmt,key){
+ const n = ('number'===typeof key)
+ ? key : capi.sqlite3_bind_parameter_index(stmt.pointer, key);
+ if(0===n || !util.isInt32(n)){
+ toss3("Invalid bind() parameter name: "+key);
+ }
+ else if(n<1 || n>stmt.parameterCount) toss3("Bind index",key,"is out of range.");
+ return n;
+ };
+
+ /**
+ If stmt._isLocked is truthy, this throws an exception
+ complaining that the 2nd argument (an operation name,
+ e.g. "bind()") is not legal while the statement is "locked".
+ Locking happens before an exec()-like callback is passed a
+ statement, to ensure that the callback does not mutate or
+ finalize the statement. If it does not throw, it returns stmt.
+ */
+ const affirmUnlocked = function(stmt,currentOpName){
+ if(stmt._isLocked){
+ toss3("Operation is illegal when statement is locked:",currentOpName);
+ }
+ return stmt;
+ };
+
+ /**
+ Binds a single bound parameter value on the given stmt at the
+ given index (numeric or named) using the given bindType (see
+ the BindTypes enum) and value. Throws on error. Returns stmt on
+ success.
+ */
+ const bindOne = function f(stmt,ndx,bindType,val){
+ affirmUnlocked(stmt, 'bind()');
+ if(!f._){
+ f._tooBigInt = (v)=>toss3(
+ "BigInt value is too big to store without precision loss:", v
+ );
+ /* Reminder: when not in BigInt mode, it's impossible for
+ JS to represent a number out of the range we can bind,
+ so we have no range checking. */
+ f._ = {
+ string: function(stmt, ndx, val, asBlob){
+ if(1){
+ /* _Hypothetically_ more efficient than the impl in the 'else' block. */
+ const stack = wasm.scopedAllocPush();
+ try{
+ const n = wasm.jstrlen(val);
+ const pStr = wasm.scopedAlloc(n);
+ wasm.jstrcpy(val, wasm.heap8u(), pStr, n, false);
+ const f = asBlob ? capi.sqlite3_bind_blob : capi.sqlite3_bind_text;
+ return f(stmt.pointer, ndx, pStr, n, capi.SQLITE_TRANSIENT);
+ }finally{
+ wasm.scopedAllocPop(stack);
+ }
+ }else{
+ const bytes = wasm.jstrToUintArray(val,false);
+ const pStr = wasm.alloc(bytes.length || 1);
+ wasm.heap8u().set(bytes.length ? bytes : [0], pStr);
+ try{
+ const f = asBlob ? capi.sqlite3_bind_blob : capi.sqlite3_bind_text;
+ return f(stmt.pointer, ndx, pStr, bytes.length, capi.SQLITE_TRANSIENT);
+ }finally{
+ wasm.dealloc(pStr);
+ }
+ }
+ }
+ };
+ }/* static init */
+ affirmSupportedBindType(val);
+ ndx = affirmParamIndex(stmt,ndx);
+ let rc = 0;
+ switch((null===val || undefined===val) ? BindTypes.null : bindType){
+ case BindTypes.null:
+ rc = capi.sqlite3_bind_null(stmt.pointer, ndx);
+ break;
+ case BindTypes.string:
+ rc = f._.string(stmt, ndx, val, false);
+ break;
+ case BindTypes.number: {
+ let m;
+ if(util.isInt32(val)) m = capi.sqlite3_bind_int;
+ else if('bigint'===typeof val){
+ if(!util.bigIntFits64(val)){
+ f._tooBigInt(val);
+ }else if(wasm.bigIntEnabled){
+ m = capi.sqlite3_bind_int64;
+ }else if(util.bigIntFitsDouble(val)){
+ val = Number(val);
+ m = capi.sqlite3_bind_double;
+ }else{
+ f._tooBigInt(val);
+ }
+ }else{ // !int32, !bigint
+ val = Number(val);
+ if(wasm.bigIntEnabled && Number.isInteger(val)){
+ m = capi.sqlite3_bind_int64;
+ }else{
+ m = capi.sqlite3_bind_double;
+ }
+ }
+ rc = m(stmt.pointer, ndx, val);
+ break;
+ }
+ case BindTypes.boolean:
+ rc = capi.sqlite3_bind_int(stmt.pointer, ndx, val ? 1 : 0);
+ break;
+ case BindTypes.blob: {
+ if('string'===typeof val){
+ rc = f._.string(stmt, ndx, val, true);
+ }else if(!util.isBindableTypedArray(val)){
+ toss3("Binding a value as a blob requires",
+ "that it be a string, Uint8Array, or Int8Array.");
+ }else if(1){
+ /* _Hypothetically_ more efficient than the impl in the 'else' block. */
+ const stack = wasm.scopedAllocPush();
+ try{
+ const pBlob = wasm.scopedAlloc(val.byteLength || 1);
+ wasm.heap8().set(val.byteLength ? val : [0], pBlob)
+ rc = capi.sqlite3_bind_blob(stmt.pointer, ndx, pBlob, val.byteLength,
+ capi.SQLITE_TRANSIENT);
+ }finally{
+ wasm.scopedAllocPop(stack);
+ }
+ }else{
+ const pBlob = wasm.allocFromTypedArray(val);
+ try{
+ rc = capi.sqlite3_bind_blob(stmt.pointer, ndx, pBlob, val.byteLength,
+ capi.SQLITE_TRANSIENT);
+ }finally{
+ wasm.dealloc(pBlob);
+ }
+ }
+ break;
+ }
+ default:
+ console.warn("Unsupported bind() argument type:",val);
+ toss3("Unsupported bind() argument type: "+(typeof val));
+ }
+ if(rc) DB.checkRc(stmt.db.pointer, rc);
+ return stmt;
+ };
+
+ Stmt.prototype = {
+ /**
+ "Finalizes" this statement. This is a no-op if the
+ statement has already been finalizes. Returns
+ undefined. Most methods in this class will throw if called
+ after this is.
+ */
+ finalize: function(){
+ if(this.pointer){
+ affirmUnlocked(this,'finalize()');
+ delete __stmtMap.get(this.db)[this.pointer];
+ capi.sqlite3_finalize(this.pointer);
+ __ptrMap.delete(this);
+ delete this._mayGet;
+ delete this.columnCount;
+ delete this.parameterCount;
+ delete this.db;
+ delete this._isLocked;
+ }
+ },
+ /** Clears all bound values. Returns this object.
+ Throws if this statement has been finalized. */
+ clearBindings: function(){
+ affirmUnlocked(affirmStmtOpen(this), 'clearBindings()')
+ capi.sqlite3_clear_bindings(this.pointer);
+ this._mayGet = false;
+ return this;
+ },
+ /**
+ Resets this statement so that it may be step()ed again
+ from the beginning. Returns this object. Throws if this
+ statement has been finalized.
+
+ If passed a truthy argument then this.clearBindings() is
+ also called, otherwise any existing bindings, along with
+ any memory allocated for them, are retained.
+ */
+ reset: function(alsoClearBinds){
+ affirmUnlocked(this,'reset()');
+ if(alsoClearBinds) this.clearBindings();
+ capi.sqlite3_reset(affirmStmtOpen(this).pointer);
+ this._mayGet = false;
+ return this;
+ },
+ /**
+ Binds one or more values to its bindable parameters. It
+ accepts 1 or 2 arguments:
+
+ If passed a single argument, it must be either an array, an
+ object, or a value of a bindable type (see below).
+
+ If passed 2 arguments, the first one is the 1-based bind
+ index or bindable parameter name and the second one must be
+ a value of a bindable type.
+
+ Bindable value types:
+
+ - null is bound as NULL.
+
+ - undefined as a standalone value is a no-op intended to
+ simplify certain client-side use cases: passing undefined as
+ a value to this function will not actually bind anything and
+ this function will skip confirmation that binding is even
+ legal. (Those semantics simplify certain client-side uses.)
+ Conversely, a value of undefined as an array or object
+ property when binding an array/object (see below) is treated
+ the same as null.
+
+ - Numbers are bound as either doubles or integers: doubles if
+ they are larger than 32 bits, else double or int32, depending
+ on whether they have a fractional part. Booleans are bound as
+ integer 0 or 1. It is not expected the distinction of binding
+ doubles which have no fractional parts is integers is
+ significant for the majority of clients due to sqlite3's data
+ typing model. If [BigInt] support is enabled then this
+ routine will bind BigInt values as 64-bit integers if they'll
+ fit in 64 bits. If that support disabled, it will store the
+ BigInt as an int32 or a double if it can do so without loss
+ of precision. If the BigInt is _too BigInt_ then it will
+ throw.
+
+ - Strings are bound as strings (use bindAsBlob() to force
+ blob binding).
+
+ - Uint8Array and Int8Array instances are bound as blobs.
+ (TODO: binding the other TypedArray types.)
+
+ If passed an array, each element of the array is bound at
+ the parameter index equal to the array index plus 1
+ (because arrays are 0-based but binding is 1-based).
+
+ If passed an object, each object key is treated as a
+ bindable parameter name. The object keys _must_ match any
+ bindable parameter names, including any `$`, `@`, or `:`
+ prefix. Because `$` is a legal identifier chararacter in
+ JavaScript, that is the suggested prefix for bindable
+ parameters: `stmt.bind({$a: 1, $b: 2})`.
+
+ It returns this object on success and throws on
+ error. Errors include:
+
+ - Any bind index is out of range, a named bind parameter
+ does not match, or this statement has no bindable
+ parameters.
+
+ - Any value to bind is of an unsupported type.
+
+ - Passed no arguments or more than two.
+
+ - The statement has been finalized.
+ */
+ bind: function(/*[ndx,] arg*/){
+ affirmStmtOpen(this);
+ let ndx, arg;
+ switch(arguments.length){
+ case 1: ndx = 1; arg = arguments[0]; break;
+ case 2: ndx = arguments[0]; arg = arguments[1]; break;
+ default: toss3("Invalid bind() arguments.");
+ }
+ if(undefined===arg){
+ /* It might seem intuitive to bind undefined as NULL
+ but this approach simplifies certain client-side
+ uses when passing on arguments between 2+ levels of
+ functions. */
+ return this;
+ }else if(!this.parameterCount){
+ toss3("This statement has no bindable parameters.");
+ }
+ this._mayGet = false;
+ if(null===arg){
+ /* bind NULL */
+ return bindOne(this, ndx, BindTypes.null, arg);
+ }
+ else if(Array.isArray(arg)){
+ /* bind each entry by index */
+ if(1!==arguments.length){
+ toss3("When binding an array, an index argument is not permitted.");
+ }
+ arg.forEach((v,i)=>bindOne(this, i+1, affirmSupportedBindType(v), v));
+ return this;
+ }
+ else if('object'===typeof arg/*null was checked above*/
+ && !util.isBindableTypedArray(arg)){
+ /* Treat each property of arg as a named bound parameter. */
+ if(1!==arguments.length){
+ toss3("When binding an object, an index argument is not permitted.");
+ }
+ Object.keys(arg)
+ .forEach(k=>bindOne(this, k,
+ affirmSupportedBindType(arg[k]),
+ arg[k]));
+ return this;
+ }else{
+ return bindOne(this, ndx, affirmSupportedBindType(arg), arg);
+ }
+ toss3("Should not reach this point.");
+ },
+ /**
+ Special case of bind() which binds the given value using the
+ BLOB binding mechanism instead of the default selected one for
+ the value. The ndx may be a numbered or named bind index. The
+ value must be of type string, null/undefined (both get treated
+ as null), or a TypedArray of a type supported by the bind()
+ API.
+
+ If passed a single argument, a bind index of 1 is assumed and
+ the first argument is the value.
+ */
+ bindAsBlob: function(ndx,arg){
+ affirmStmtOpen(this);
+ if(1===arguments.length){
+ arg = ndx;
+ ndx = 1;
+ }
+ const t = affirmSupportedBindType(arg);
+ if(BindTypes.string !== t && BindTypes.blob !== t
+ && BindTypes.null !== t){
+ toss3("Invalid value type for bindAsBlob()");
+ }
+ bindOne(this, ndx, BindTypes.blob, arg);
+ this._mayGet = false;
+ return this;
+ },
+ /**
+ Steps the statement one time. If the result indicates that a
+ row of data is available, a truthy value is returned.
+ If no row of data is available, a falsy
+ value is returned. Throws on error.
+ */
+ step: function(){
+ affirmUnlocked(this, 'step()');
+ const rc = capi.sqlite3_step(affirmStmtOpen(this).pointer);
+ switch(rc){
+ case capi.SQLITE_DONE: return this._mayGet = false;
+ case capi.SQLITE_ROW: return this._mayGet = true;
+ default:
+ this._mayGet = false;
+ console.warn("sqlite3_step() rc=",rc,
+ capi.sqlite3_js_rc_str(rc),
+ "SQL =", capi.sqlite3_sql(this.pointer));
+ DB.checkRc(this.db.pointer, rc);
+ }
+ },
+ /**
+ Functions exactly like step() except that...
+
+ 1) On success, it calls this.reset() and returns this object.
+ 2) On error, it throws and does not call reset().
+
+ This is intended to simplify constructs like:
+
+ ```
+ for(...) {
+ stmt.bind(...).stepReset();
+ }
+ ```
+
+ Note that the reset() call makes it illegal to call this.get()
+ after the step.
+ */
+ stepReset: function(){
+ this.step();
+ return this.reset();
+ },
+ /**
+ Functions like step() except that it finalizes this statement
+ immediately after stepping unless the step cannot be performed
+ because the statement is locked. Throws on error, but any error
+ other than the statement-is-locked case will also trigger
+ finalization of this statement.
+
+ On success, it returns true if the step indicated that a row of
+ data was available, else it returns false.
+
+ This is intended to simplify use cases such as:
+
+ ```
+ aDb.prepare("insert into foo(a) values(?)").bind(123).stepFinalize();
+ ```
+ */
+ stepFinalize: function(){
+ const rc = this.step();
+ this.finalize();
+ return rc;
+ },
+ /**
+ Fetches the value from the given 0-based column index of
+ the current data row, throwing if index is out of range.
+
+ Requires that step() has just returned a truthy value, else
+ an exception is thrown.
+
+ By default it will determine the data type of the result
+ automatically. If passed a second arugment, it must be one
+ of the enumeration values for sqlite3 types, which are
+ defined as members of the sqlite3 module: SQLITE_INTEGER,
+ SQLITE_FLOAT, SQLITE_TEXT, SQLITE_BLOB. Any other value,
+ except for undefined, will trigger an exception. Passing
+ undefined is the same as not passing a value. It is legal
+ to, e.g., fetch an integer value as a string, in which case
+ sqlite3 will convert the value to a string.
+
+ If ndx is an array, this function behaves a differently: it
+ assigns the indexes of the array, from 0 to the number of
+ result columns, to the values of the corresponding column,
+ and returns that array.
+
+ If ndx is a plain object, this function behaves even
+ differentlier: it assigns the properties of the object to
+ the values of their corresponding result columns.
+
+ Blobs are returned as Uint8Array instances.
+
+ Potential TODO: add type ID SQLITE_JSON, which fetches the
+ result as a string and passes it (if it's not null) to
+ JSON.parse(), returning the result of that. Until then,
+ getJSON() can be used for that.
+ */
+ get: function(ndx,asType){
+ if(!affirmStmtOpen(this)._mayGet){
+ toss3("Stmt.step() has not (recently) returned true.");
+ }
+ if(Array.isArray(ndx)){
+ let i = 0;
+ while(i<this.columnCount){
+ ndx[i] = this.get(i++);
+ }
+ return ndx;
+ }else if(ndx && 'object'===typeof ndx){
+ let i = 0;
+ while(i<this.columnCount){
+ ndx[capi.sqlite3_column_name(this.pointer,i)] = this.get(i++);
+ }
+ return ndx;
+ }
+ affirmColIndex(this, ndx);
+ switch(undefined===asType
+ ? capi.sqlite3_column_type(this.pointer, ndx)
+ : asType){
+ case capi.SQLITE_NULL: return null;
+ case capi.SQLITE_INTEGER:{
+ if(wasm.bigIntEnabled){
+ const rc = capi.sqlite3_column_int64(this.pointer, ndx);
+ if(rc>=Number.MIN_SAFE_INTEGER && rc<=Number.MAX_SAFE_INTEGER){
+ /* Coerce "normal" number ranges to normal number values,
+ and only return BigInt-type values for numbers out of this
+ range. */
+ return Number(rc).valueOf();
+ }
+ return rc;
+ }else{
+ const rc = capi.sqlite3_column_double(this.pointer, ndx);
+ if(rc>Number.MAX_SAFE_INTEGER || rc<Number.MIN_SAFE_INTEGER){
+ /* Throwing here is arguable but, since we're explicitly
+ extracting an SQLITE_INTEGER-type value, it seems fair to throw
+ if the extracted number is out of range for that type.
+ This policy may be laxened to simply pass on the number and
+ hope for the best, as the C API would do. */
+ toss3("Integer is out of range for JS integer range: "+rc);
+ }
+ //console.log("get integer rc=",rc,isInt32(rc));
+ return util.isInt32(rc) ? (rc | 0) : rc;
+ }
+ }
+ case capi.SQLITE_FLOAT:
+ return capi.sqlite3_column_double(this.pointer, ndx);
+ case capi.SQLITE_TEXT:
+ return capi.sqlite3_column_text(this.pointer, ndx);
+ case capi.SQLITE_BLOB: {
+ const n = capi.sqlite3_column_bytes(this.pointer, ndx),
+ ptr = capi.sqlite3_column_blob(this.pointer, ndx),
+ rc = new Uint8Array(n);
+ //heap = n ? wasm.heap8() : false;
+ if(n) rc.set(wasm.heap8u().slice(ptr, ptr+n), 0);
+ //for(let i = 0; i < n; ++i) rc[i] = heap[ptr + i];
+ if(n && this.db._blobXfer instanceof Array){
+ /* This is an optimization soley for the
+ Worker-based API. These values will be
+ transfered to the main thread directly
+ instead of being copied. */
+ this.db._blobXfer.push(rc.buffer);
+ }
+ return rc;
+ }
+ default: toss3("Don't know how to translate",
+ "type of result column #"+ndx+".");
+ }
+ toss3("Not reached.");
+ },
+ /** Equivalent to get(ndx) but coerces the result to an
+ integer. */
+ getInt: function(ndx){return this.get(ndx,capi.SQLITE_INTEGER)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ float. */
+ getFloat: function(ndx){return this.get(ndx,capi.SQLITE_FLOAT)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ string. */
+ getString: function(ndx){return this.get(ndx,capi.SQLITE_TEXT)},
+ /** Equivalent to get(ndx) but coerces the result to a
+ Uint8Array. */
+ getBlob: function(ndx){return this.get(ndx,capi.SQLITE_BLOB)},
+ /**
+ A convenience wrapper around get() which fetches the value
+ as a string and then, if it is not null, passes it to
+ JSON.parse(), returning that result. Throws if parsing
+ fails. If the result is null, null is returned. An empty
+ string, on the other hand, will trigger an exception.
+ */
+ getJSON: function(ndx){
+ const s = this.get(ndx, capi.SQLITE_STRING);
+ return null===s ? s : JSON.parse(s);
+ },
+ // Design note: the only reason most of these getters have a 'get'
+ // prefix is for consistency with getVALUE_TYPE(). The latter
+ // arguably really need that prefix for API readability and the
+ // rest arguably don't, but consistency is a powerful thing.
+ /**
+ Returns the result column name of the given index, or
+ throws if index is out of bounds or this statement has been
+ finalized. This can be used without having run step()
+ first.
+ */
+ getColumnName: function(ndx){
+ return capi.sqlite3_column_name(
+ affirmColIndex(affirmStmtOpen(this),ndx).pointer, ndx
+ );
+ },
+ /**
+ If this statement potentially has result columns, this
+ function returns an array of all such names. If passed an
+ array, it is used as the target and all names are appended
+ to it. Returns the target array. Throws if this statement
+ cannot have result columns. This object's columnCount member
+ holds the number of columns.
+ */
+ getColumnNames: function(tgt=[]){
+ affirmColIndex(affirmStmtOpen(this),0);
+ for(let i = 0; i < this.columnCount; ++i){
+ tgt.push(capi.sqlite3_column_name(this.pointer, i));
+ }
+ return tgt;
+ },
+ /**
+ If this statement has named bindable parameters and the
+ given name matches one, its 1-based bind index is
+ returned. If no match is found, 0 is returned. If it has no
+ bindable parameters, the undefined value is returned.
+ */
+ getParamIndex: function(name){
+ return (affirmStmtOpen(this).parameterCount
+ ? capi.sqlite3_bind_parameter_index(this.pointer, name)
+ : undefined);
+ }
+ }/*Stmt.prototype*/;
+
+ {/* Add the `pointer` property to DB and Stmt. */
+ const prop = {
+ enumerable: true,
+ get: function(){return __ptrMap.get(this)},
+ set: ()=>toss3("The pointer property is read-only.")
+ }
+ Object.defineProperty(Stmt.prototype, 'pointer', prop);
+ Object.defineProperty(DB.prototype, 'pointer', prop);
+ }
+
+ /** The OO API's public namespace. */
+ sqlite3.oo1 = {
+ version: {
+ lib: capi.sqlite3_libversion(),
+ ooApi: "0.1"
+ },
+ DB,
+ Stmt
+ }/*oo1 object*/;
+
+ if(util.isUIThread()){
+ /**
+ Functionally equivalent to DB(storageName,'c','kvvfs') except
+ that it throws if the given storage name is not one of 'local'
+ or 'session'.
+ */
+ sqlite3.oo1.JsStorageDb = function(storageName='session'){
+ if('session'!==storageName && 'local'!==storageName){
+ toss3("JsStorageDb db name must be one of 'session' or 'local'.");
+ }
+ dbCtorHelper.call(this, {
+ filename: storageName,
+ flags: 'c',
+ vfs: "kvvfs"
+ });
+ };
+ const jdb = sqlite3.oo1.JsStorageDb;
+ jdb.prototype = Object.create(DB.prototype);
+ /** Equivalent to sqlite3_js_kvvfs_clear(). */
+ jdb.clearStorage = capi.sqlite3_js_kvvfs_clear;
+ /**
+ Clears this database instance's storage or throws if this
+ instance has been closed. Returns the number of
+ database blocks which were cleaned up.
+ */
+ jdb.prototype.clearStorage = function(){
+ return jdb.clearStorage(affirmDbOpen(this).filename);
+ };
+ /** Equivalent to sqlite3_js_kvvfs_size(). */
+ jdb.storageSize = capi.sqlite3_js_kvvfs_size;
+ /**
+ Returns the _approximate_ number of bytes this database takes
+ up in its storage or throws if this instance has been closed.
+ */
+ jdb.prototype.storageSize = function(){
+ return jdb.storageSize(affirmDbOpen(this).filename);
+ };
+ }/*main-window-only bits*/
+
+});
+
diff --git a/ext/wasm/api/sqlite3-api-opfs.js b/ext/wasm/api/sqlite3-api-opfs.js
new file mode 100644
index 0000000..da5496f
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-opfs.js
@@ -0,0 +1,1311 @@
+/*
+ 2022-09-18
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file holds the synchronous half of an sqlite3_vfs
+ implementation which proxies, in a synchronous fashion, the
+ asynchronous Origin-Private FileSystem (OPFS) APIs using a second
+ Worker, implemented in sqlite3-opfs-async-proxy.js. This file is
+ intended to be appended to the main sqlite3 JS deliverable somewhere
+ after sqlite3-api-oo1.js and before sqlite3-api-cleanup.js.
+*/
+'use strict';
+self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+/**
+ installOpfsVfs() returns a Promise which, on success, installs an
+ sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs
+ which accept a VFS. It is intended to be called via
+ sqlite3ApiBootstrap.initializersAsync or an equivalent mechanism.
+
+ The installed VFS uses the Origin-Private FileSystem API for
+ all file storage. On error it is rejected with an exception
+ explaining the problem. Reasons for rejection include, but are
+ not limited to:
+
+ - The counterpart Worker (see below) could not be loaded.
+
+ - The environment does not support OPFS. That includes when
+ this function is called from the main window thread.
+
+ Significant notes and limitations:
+
+ - As of this writing, OPFS is still very much in flux and only
+ available in bleeding-edge versions of Chrome (v102+, noting that
+ that number will increase as the OPFS API matures).
+
+ - The OPFS features used here are only available in dedicated Worker
+ threads. This file tries to detect that case, resulting in a
+ rejected Promise if those features do not seem to be available.
+
+ - It requires the SharedArrayBuffer and Atomics classes, and the
+ former is only available if the HTTP server emits the so-called
+ COOP and COEP response headers. These features are required for
+ proxying OPFS's synchronous API via the synchronous interface
+ required by the sqlite3_vfs API.
+
+ - This function may only be called a single time. When called, this
+ function removes itself from the sqlite3 object.
+
+ All arguments to this function are for internal/development purposes
+ only. They do not constitute a public API and may change at any
+ time.
+
+ The argument may optionally be a plain object with the following
+ configuration options:
+
+ - proxyUri: as described above
+
+ - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables
+ logging of errors. 2 enables logging of warnings and errors. 3
+ additionally enables debugging info.
+
+ - sanityChecks (=false): if true, some basic sanity tests are
+ run on the OPFS VFS API after it's initialized, before the
+ returned Promise resolves.
+
+ On success, the Promise resolves to the top-most sqlite3 namespace
+ object and that object gets a new object installed in its
+ `opfs` property, containing several OPFS-specific utilities.
+*/
+const installOpfsVfs = function callee(options){
+ if(!self.SharedArrayBuffer ||
+ !self.Atomics ||
+ !self.FileSystemHandle ||
+ !self.FileSystemDirectoryHandle ||
+ !self.FileSystemFileHandle ||
+ !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
+ !navigator.storage.getDirectory){
+ return Promise.reject(
+ new Error("This environment does not have OPFS support.")
+ );
+ }
+ if(!options || 'object'!==typeof options){
+ options = Object.create(null);
+ }
+ const urlParams = new URL(self.location.href).searchParams;
+ if(undefined===options.verbose){
+ options.verbose = urlParams.has('opfs-verbose') ? 3 : 2;
+ }
+ if(undefined===options.sanityChecks){
+ options.sanityChecks = urlParams.has('opfs-sanity-check');
+ }
+ if(undefined===options.proxyUri){
+ options.proxyUri = callee.defaultProxyUri;
+ }
+
+ if('function' === typeof options.proxyUri){
+ options.proxyUri = options.proxyUri();
+ }
+ const thePromise = new Promise(function(promiseResolve, promiseReject_){
+ const loggers = {
+ 0:console.error.bind(console),
+ 1:console.warn.bind(console),
+ 2:console.log.bind(console)
+ };
+ const logImpl = (level,...args)=>{
+ if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
+ };
+ const log = (...args)=>logImpl(2, ...args);
+ const warn = (...args)=>logImpl(1, ...args);
+ const error = (...args)=>logImpl(0, ...args);
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const capi = sqlite3.capi;
+ const wasm = sqlite3.wasm;
+ const sqlite3_vfs = capi.sqlite3_vfs;
+ const sqlite3_file = capi.sqlite3_file;
+ const sqlite3_io_methods = capi.sqlite3_io_methods;
+ /**
+ Generic utilities for working with OPFS. This will get filled out
+ by the Promise setup and, on success, installed as sqlite3.opfs.
+ */
+ const opfsUtil = Object.create(null);
+ /**
+ Not part of the public API. Solely for internal/development
+ use.
+ */
+ opfsUtil.metrics = {
+ dump: function(){
+ let k, n = 0, t = 0, w = 0;
+ for(k in state.opIds){
+ const m = metrics[k];
+ n += m.count;
+ t += m.time;
+ w += m.wait;
+ m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
+ m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
+ }
+ console.log(self.location.href,
+ "metrics for",self.location.href,":",metrics,
+ "\nTotal of",n,"op(s) for",t,
+ "ms (incl. "+w+" ms of waiting on the async side)");
+ console.log("Serialization metrics:",metrics.s11n);
+ W.postMessage({type:'opfs-async-metrics'});
+ },
+ reset: function(){
+ let k;
+ const r = (m)=>(m.count = m.time = m.wait = 0);
+ for(k in state.opIds){
+ r(metrics[k] = Object.create(null));
+ }
+ let s = metrics.s11n = Object.create(null);
+ s = s.serialize = Object.create(null);
+ s.count = s.time = 0;
+ s = metrics.s11n.deserialize = Object.create(null);
+ s.count = s.time = 0;
+ }
+ }/*metrics*/;
+ const promiseReject = function(err){
+ opfsVfs.dispose();
+ return promiseReject_(err);
+ };
+ const W = new Worker(options.proxyUri);
+ W._originalOnError = W.onerror /* will be restored later */;
+ W.onerror = function(err){
+ // The error object doesn't contain any useful info when the
+ // failure is, e.g., that the remote script is 404.
+ error("Error initializing OPFS asyncer:",err);
+ promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
+ };
+ const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
+ const dVfs = pDVfs
+ ? new sqlite3_vfs(pDVfs)
+ : null /* dVfs will be null when sqlite3 is built with
+ SQLITE_OS_OTHER. Though we cannot currently handle
+ that case, the hope is to eventually be able to. */;
+ const opfsVfs = new sqlite3_vfs();
+ const opfsIoMethods = new sqlite3_io_methods();
+ opfsVfs.$iVersion = 2/*yes, two*/;
+ opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
+ opfsVfs.$mxPathname = 1024/*sure, why not?*/;
+ opfsVfs.$zName = wasm.allocCString("opfs");
+ // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
+ opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
+ opfsVfs.ondispose = [
+ '$zName', opfsVfs.$zName,
+ 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
+ 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
+ ];
+ /**
+ Pedantic sidebar about opfsVfs.ondispose: the entries in that array
+ are items to clean up when opfsVfs.dispose() is called, but in this
+ environment it will never be called. The VFS instance simply
+ hangs around until the WASM module instance is cleaned up. We
+ "could" _hypothetically_ clean it up by "importing" an
+ sqlite3_os_end() impl into the wasm build, but the shutdown order
+ of the wasm engine and the JS one are undefined so there is no
+ guaranty that the opfsVfs instance would be available in one
+ environment or the other when sqlite3_os_end() is called (_if_ it
+ gets called at all in a wasm build, which is undefined).
+ */
+ /**
+ State which we send to the async-api Worker or share with it.
+ This object must initially contain only cloneable or sharable
+ objects. After the worker's "inited" message arrives, other types
+ of data may be added to it.
+
+ For purposes of Atomics.wait() and Atomics.notify(), we use a
+ SharedArrayBuffer with one slot reserved for each of the API
+ proxy's methods. The sync side of the API uses Atomics.wait()
+ on the corresponding slot and the async side uses
+ Atomics.notify() on that slot.
+
+ The approach of using a single SAB to serialize comms for all
+ instances might(?) lead to deadlock situations in multi-db
+ cases. We should probably have one SAB here with a single slot
+ for locking a per-file initialization step and then allocate a
+ separate SAB like the above one for each file. That will
+ require a bit of acrobatics but should be feasible. The most
+ problematic part is that xOpen() would have to use
+ postMessage() to communicate its SharedArrayBuffer, and mixing
+ that approach with Atomics.wait/notify() gets a bit messy.
+ */
+ const state = Object.create(null);
+ state.verbose = options.verbose;
+ state.littleEndian = (()=>{
+ const buffer = new ArrayBuffer(2);
+ new DataView(buffer).setInt16(0, 256, true /* ==>littleEndian */);
+ // Int16Array uses the platform's endianness.
+ return new Int16Array(buffer)[0] === 256;
+ })();
+ /**
+ Whether the async counterpart should log exceptions to
+ the serialization channel. That produces a great deal of
+ noise for seemingly innocuous things like xAccess() checks
+ for missing files, so this option may have one of 3 values:
+
+ 0 = no exception logging
+
+ 1 = only log exceptions for "significant" ops like xOpen(),
+ xRead(), and xWrite().
+
+ 2 = log all exceptions.
+ */
+ state.asyncS11nExceptions = 1;
+ /* Size of file I/O buffer block. 64k = max sqlite3 page size, and
+ xRead/xWrite() will never deal in blocks larger than that. */
+ state.fileBufferSize = 1024 * 64;
+ state.sabS11nOffset = state.fileBufferSize;
+ /**
+ The size of the block in our SAB for serializing arguments and
+ result values. Needs to be large enough to hold serialized
+ values of any of the proxied APIs. Filenames are the largest
+ part but are limited to opfsVfs.$mxPathname bytes.
+ */
+ state.sabS11nSize = opfsVfs.$mxPathname * 2;
+ /**
+ The SAB used for all data I/O between the synchronous and
+ async halves (file i/o and arg/result s11n).
+ */
+ state.sabIO = new SharedArrayBuffer(
+ state.fileBufferSize/* file i/o block */
+ + state.sabS11nSize/* argument/result serialization block */
+ );
+ state.opIds = Object.create(null);
+ const metrics = Object.create(null);
+ {
+ /* Indexes for use in our SharedArrayBuffer... */
+ let i = 0;
+ /* SAB slot used to communicate which operation is desired
+ between both workers. This worker writes to it and the other
+ listens for changes. */
+ state.opIds.whichOp = i++;
+ /* Slot for storing return values. This worker listens to that
+ slot and the other worker writes to it. */
+ state.opIds.rc = i++;
+ /* Each function gets an ID which this worker writes to
+ the whichOp slot. The async-api worker uses Atomic.wait()
+ on the whichOp slot to figure out which operation to run
+ next. */
+ state.opIds.xAccess = i++;
+ state.opIds.xClose = i++;
+ state.opIds.xDelete = i++;
+ state.opIds.xDeleteNoWait = i++;
+ state.opIds.xFileControl = i++;
+ state.opIds.xFileSize = i++;
+ state.opIds.xLock = i++;
+ state.opIds.xOpen = i++;
+ state.opIds.xRead = i++;
+ state.opIds.xSleep = i++;
+ state.opIds.xSync = i++;
+ state.opIds.xTruncate = i++;
+ state.opIds.xUnlock = i++;
+ state.opIds.xWrite = i++;
+ state.opIds.mkdir = i++;
+ state.opIds['opfs-async-metrics'] = i++;
+ state.opIds['opfs-async-shutdown'] = i++;
+ /* The retry slot is used by the async part for wait-and-retry
+ semantics. Though we could hypothetically use the xSleep slot
+ for that, doing so might lead to undesired side effects. */
+ state.opIds.retry = i++;
+ state.sabOP = new SharedArrayBuffer(
+ i * 4/* ==sizeof int32, noting that Atomics.wait() and friends
+ can only function on Int32Array views of an SAB. */);
+ opfsUtil.metrics.reset();
+ }
+ /**
+ SQLITE_xxx constants to export to the async worker
+ counterpart...
+ */
+ state.sq3Codes = Object.create(null);
+ [
+ 'SQLITE_ACCESS_EXISTS',
+ 'SQLITE_ACCESS_READWRITE',
+ 'SQLITE_ERROR',
+ 'SQLITE_IOERR',
+ 'SQLITE_IOERR_ACCESS',
+ 'SQLITE_IOERR_CLOSE',
+ 'SQLITE_IOERR_DELETE',
+ 'SQLITE_IOERR_FSYNC',
+ 'SQLITE_IOERR_LOCK',
+ 'SQLITE_IOERR_READ',
+ 'SQLITE_IOERR_SHORT_READ',
+ 'SQLITE_IOERR_TRUNCATE',
+ 'SQLITE_IOERR_UNLOCK',
+ 'SQLITE_IOERR_WRITE',
+ 'SQLITE_LOCK_EXCLUSIVE',
+ 'SQLITE_LOCK_NONE',
+ 'SQLITE_LOCK_PENDING',
+ 'SQLITE_LOCK_RESERVED',
+ 'SQLITE_LOCK_SHARED',
+ 'SQLITE_MISUSE',
+ 'SQLITE_NOTFOUND',
+ 'SQLITE_OPEN_CREATE',
+ 'SQLITE_OPEN_DELETEONCLOSE',
+ 'SQLITE_OPEN_READONLY'
+ ].forEach((k)=>{
+ if(undefined === (state.sq3Codes[k] = capi[k])){
+ toss("Maintenance required: not found:",k);
+ }
+ });
+
+ /**
+ Runs the given operation (by name) in the async worker
+ counterpart, waits for its response, and returns the result
+ which the async worker writes to SAB[state.opIds.rc]. The
+ 2nd and subsequent arguments must be the aruguments for the
+ async op.
+ */
+ const opRun = (op,...args)=>{
+ const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
+ state.s11n.serialize(...args);
+ Atomics.store(state.sabOPView, state.opIds.rc, -1);
+ Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx);
+ Atomics.notify(state.sabOPView, state.opIds.whichOp)
+ /* async thread will take over here */;
+ const t = performance.now();
+ Atomics.wait(state.sabOPView, state.opIds.rc, -1)
+ /* When this wait() call returns, the async half will have
+ completed the operation and reported its results. */;
+ const rc = Atomics.load(state.sabOPView, state.opIds.rc);
+ metrics[op].wait += performance.now() - t;
+ if(rc && state.asyncS11nExceptions){
+ const err = state.s11n.deserialize();
+ if(err) error(op+"() async error:",...err);
+ }
+ return rc;
+ };
+
+ /**
+ Not part of the public API. Only for test/development use.
+ */
+ opfsUtil.debug = {
+ asyncShutdown: ()=>{
+ warn("Shutting down OPFS async listener. The OPFS VFS will no longer work.");
+ opRun('opfs-async-shutdown');
+ },
+ asyncRestart: ()=>{
+ warn("Attempting to restart OPFS VFS async listener. Might work, might not.");
+ W.postMessage({type: 'opfs-async-restart'});
+ }
+ };
+
+ const initS11n = ()=>{
+ /**
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ ACHTUNG: this code is 100% duplicated in the other half of
+ this proxy! The documentation is maintained in the
+ "synchronous half".
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+ 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]. 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);
+ /* Only arguments and return values of these types may be
+ serialized. This covers the whole range of types needed by the
+ sqlite3_vfs API. */
+ 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 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){
+ ++metrics.s11n.deserialize.count;
+ const t = 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{/*String*/
+ 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;
+ //log("deserialize:",argc, rc);
+ metrics.s11n.deserialize.time += performance.now() - t;
+ 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){
+ const t = performance.now();
+ ++metrics.s11n.serialize.count;
+ if(args.length){
+ //log("serialize():",args);
+ const typeIds = [];
+ let i = 0, offset = 1;
+ viewU8[0] = args.length & 0xff /* header = # of args */;
+ for(; i < args.length; ++i, ++offset){
+ /* Write the TypeIds.id value into the next args.length
+ bytes. */
+ typeIds.push(getTypeId(args[i]));
+ viewU8[offset] = typeIds[i].id;
+ }
+ for(i = 0; i < args.length; ++i) {
+ /* Deserialize the following bytes based on their
+ corresponding TypeIds.id from the header. */
+ const t = typeIds[i];
+ if(t.setter){
+ viewDV[t.setter](offset, args[i], state.littleEndian);
+ offset += t.size;
+ }else{/*String*/
+ const s = textEncoder.encode(args[i]);
+ viewDV.setInt32(offset, s.byteLength, state.littleEndian);
+ offset += 4;
+ viewU8.set(s, offset);
+ offset += s.byteLength;
+ }
+ }
+ //log("serialize() result:",viewU8.slice(0,offset));
+ }else{
+ viewU8[0] = 0;
+ }
+ metrics.s11n.serialize.time += performance.now() - t;
+ };
+ return state.s11n;
+ }/*initS11n()*/;
+
+ /**
+ Generates a random ASCII string len characters long, intended for
+ use as a temporary file name.
+ */
+ const randomFilename = function f(len=16){
+ if(!f._chars){
+ f._chars = "abcdefghijklmnopqrstuvwxyz"+
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
+ "012346789";
+ f._n = f._chars.length;
+ }
+ const a = [];
+ let i = 0;
+ for( ; i < len; ++i){
+ const ndx = Math.random() * (f._n * 64) % f._n | 0;
+ a[i] = f._chars[ndx];
+ }
+ return a.join('');
+ };
+
+ /**
+ Map of sqlite3_file pointers to objects constructed by xOpen().
+ */
+ const __openFiles = Object.create(null);
+
+ /**
+ Installs a StructBinder-bound function pointer member of the
+ given name and function in the given StructType target object.
+ It creates a WASM proxy for the given function and arranges for
+ that proxy to be cleaned up when tgt.dispose() is called. Throws
+ on the slightest hint of error (e.g. tgt is-not-a StructType,
+ name does not map to a struct-bound member, etc.).
+
+ Returns a proxy for this function which is bound to tgt and takes
+ 2 args (name,func). That function returns the same thing,
+ permitting calls to be chained.
+
+ If called with only 1 arg, it has no side effects but returns a
+ func with the same signature as described above.
+ */
+ const installMethod = function callee(tgt, name, func){
+ if(!(tgt instanceof sqlite3.StructBinder.StructType)){
+ toss("Usage error: target object is-not-a StructType.");
+ }
+ if(1===arguments.length){
+ return (n,f)=>callee(tgt,n,f);
+ }
+ if(!callee.argcProxy){
+ callee.argcProxy = function(func,sig){
+ return function(...args){
+ if(func.length!==arguments.length){
+ toss("Argument mismatch. Native signature is:",sig);
+ }
+ return func.apply(this, args);
+ }
+ };
+ callee.removeFuncList = function(){
+ if(this.ondispose.__removeFuncList){
+ this.ondispose.__removeFuncList.forEach(
+ (v,ndx)=>{
+ if('number'===typeof v){
+ try{wasm.uninstallFunction(v)}
+ catch(e){/*ignore*/}
+ }
+ /* else it's a descriptive label for the next number in
+ the list. */
+ }
+ );
+ delete this.ondispose.__removeFuncList;
+ }
+ };
+ }/*static init*/
+ const sigN = tgt.memberSignature(name);
+ if(sigN.length<2){
+ toss("Member",name," is not a function pointer. Signature =",sigN);
+ }
+ const memKey = tgt.memberKey(name);
+ const fProxy = 0
+ /** This middle-man proxy is only for use during development, to
+ confirm that we always pass the proper number of
+ arguments. We know that the C-level code will always use the
+ correct argument count. */
+ ? callee.argcProxy(func, sigN)
+ : func;
+ const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
+ tgt[memKey] = pFunc;
+ if(!tgt.ondispose) tgt.ondispose = [];
+ if(!tgt.ondispose.__removeFuncList){
+ tgt.ondispose.push('ondispose.__removeFuncList handler',
+ callee.removeFuncList);
+ tgt.ondispose.__removeFuncList = [];
+ }
+ tgt.ondispose.__removeFuncList.push(memKey, pFunc);
+ return (n,f)=>callee(tgt, n, f);
+ }/*installMethod*/;
+
+ const opTimer = Object.create(null);
+ opTimer.op = undefined;
+ opTimer.start = undefined;
+ const mTimeStart = (op)=>{
+ opTimer.start = performance.now();
+ opTimer.op = op;
+ ++metrics[op].count;
+ };
+ const mTimeEnd = ()=>(
+ metrics[opTimer.op].time += performance.now() - opTimer.start
+ );
+
+ /**
+ Impls for the sqlite3_io_methods methods. Maintenance reminder:
+ members are in alphabetical order to simplify finding them.
+ */
+ const ioSyncWrappers = {
+ xCheckReservedLock: function(pFile,pOut){
+ /**
+ As of late 2022, only a single lock can be held on an OPFS
+ file. We have no way of checking whether any _other_ db
+ connection has a lock except by trying to obtain and (on
+ success) release a sync-handle for it, but doing so would
+ involve an inherent race condition. For the time being,
+ pending a better solution, we simply report whether the
+ given pFile instance has a lock.
+ */
+ const f = __openFiles[pFile];
+ wasm.setMemValue(pOut, f.lockMode ? 1 : 0, 'i32');
+ return 0;
+ },
+ xClose: function(pFile){
+ mTimeStart('xClose');
+ let rc = 0;
+ const f = __openFiles[pFile];
+ if(f){
+ delete __openFiles[pFile];
+ rc = opRun('xClose', pFile);
+ if(f.sq3File) f.sq3File.dispose();
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xDeviceCharacteristics: function(pFile){
+ //debug("xDeviceCharacteristics(",pFile,")");
+ return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
+ },
+ xFileControl: function(pFile, opId, pArg){
+ mTimeStart('xFileControl');
+ const rc = (capi.SQLITE_FCNTL_SYNC===opId)
+ ? opRun('xSync', pFile, 0)
+ : capi.SQLITE_NOTFOUND;
+ mTimeEnd();
+ return rc;
+ },
+ xFileSize: function(pFile,pSz64){
+ mTimeStart('xFileSize');
+ const rc = opRun('xFileSize', pFile);
+ if(0==rc){
+ const sz = state.s11n.deserialize()[0];
+ wasm.setMemValue(pSz64, sz, 'i64');
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xLock: function(pFile,lockType){
+ mTimeStart('xLock');
+ const f = __openFiles[pFile];
+ let rc = 0;
+ if( capi.SQLITE_LOCK_NONE === f.lockType ) {
+ rc = opRun('xLock', pFile, lockType);
+ if( 0===rc ) f.lockType = lockType;
+ }else{
+ f.lockType = lockType;
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xRead: function(pFile,pDest,n,offset64){
+ mTimeStart('xRead');
+ const f = __openFiles[pFile];
+ let rc;
+ try {
+ rc = opRun('xRead',pFile, n, Number(offset64));
+ if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
+ /**
+ Results get written to the SharedArrayBuffer f.sabView.
+ Because the heap is _not_ a SharedArrayBuffer, we have
+ to copy the results. TypedArray.set() seems to be the
+ fastest way to copy this. */
+ wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
+ }
+ }catch(e){
+ error("xRead(",arguments,") failed:",e,f);
+ rc = capi.SQLITE_IOERR_READ;
+ }
+ mTimeEnd();
+ return rc;
+ },
+ xSync: function(pFile,flags){
+ ++metrics.xSync.count;
+ return 0; // impl'd in xFileControl()
+ },
+ xTruncate: function(pFile,sz64){
+ mTimeStart('xTruncate');
+ const rc = opRun('xTruncate', pFile, Number(sz64));
+ mTimeEnd();
+ return rc;
+ },
+ xUnlock: function(pFile,lockType){
+ mTimeStart('xUnlock');
+ const f = __openFiles[pFile];
+ let rc = 0;
+ if( capi.SQLITE_LOCK_NONE === lockType
+ && f.lockType ){
+ rc = opRun('xUnlock', pFile, lockType);
+ }
+ if( 0===rc ) f.lockType = lockType;
+ mTimeEnd();
+ return rc;
+ },
+ xWrite: function(pFile,pSrc,n,offset64){
+ mTimeStart('xWrite');
+ const f = __openFiles[pFile];
+ let rc;
+ try {
+ f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
+ rc = opRun('xWrite', pFile, n, Number(offset64));
+ }catch(e){
+ error("xWrite(",arguments,") failed:",e,f);
+ rc = capi.SQLITE_IOERR_WRITE;
+ }
+ mTimeEnd();
+ return rc;
+ }
+ }/*ioSyncWrappers*/;
+
+ /**
+ Impls for the sqlite3_vfs methods. Maintenance reminder: members
+ are in alphabetical order to simplify finding them.
+ */
+ const vfsSyncWrappers = {
+ xAccess: function(pVfs,zName,flags,pOut){
+ mTimeStart('xAccess');
+ const rc = opRun('xAccess', wasm.cstringToJs(zName));
+ wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' );
+ mTimeEnd();
+ return 0;
+ },
+ xCurrentTime: function(pVfs,pOut){
+ /* If it turns out that we need to adjust for timezone, see:
+ https://stackoverflow.com/a/11760121/1458521 */
+ wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
+ 'double');
+ return 0;
+ },
+ xCurrentTimeInt64: function(pVfs,pOut){
+ // TODO: confirm that this calculation is correct
+ wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
+ 'i64');
+ return 0;
+ },
+ xDelete: function(pVfs, zName, doSyncDir){
+ mTimeStart('xDelete');
+ opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false);
+ /* We're ignoring errors because we cannot yet differentiate
+ between harmless and non-harmless failures. */
+ mTimeEnd();
+ return 0;
+ },
+ xFullPathname: function(pVfs,zName,nOut,pOut){
+ /* Until/unless we have some notion of "current dir"
+ in OPFS, simply copy zName to pOut... */
+ const i = wasm.cstrncpy(pOut, zName, nOut);
+ return i<nOut ? 0 : capi.SQLITE_CANTOPEN
+ /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
+ },
+ xGetLastError: function(pVfs,nOut,pOut){
+ /* TODO: store exception.message values from the async
+ partner in a dedicated SharedArrayBuffer, noting that we'd have
+ to encode them... TextEncoder can do that for us. */
+ warn("OPFS xGetLastError() has nothing sensible to return.");
+ return 0;
+ },
+ //xSleep is optionally defined below
+ xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
+ mTimeStart('xOpen');
+ if(0===zName){
+ zName = randomFilename();
+ }else if('number'===typeof zName){
+ zName = wasm.cstringToJs(zName);
+ }
+ const fh = Object.create(null);
+ fh.fid = pFile;
+ fh.filename = zName;
+ fh.sab = new SharedArrayBuffer(state.fileBufferSize);
+ fh.flags = flags;
+ const rc = opRun('xOpen', pFile, zName, flags);
+ if(!rc){
+ /* Recall that sqlite3_vfs::xClose() will be called, even on
+ error, unless pFile->pMethods is NULL. */
+ if(fh.readOnly){
+ wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
+ }
+ __openFiles[pFile] = fh;
+ fh.sabView = state.sabFileBufView;
+ fh.sq3File = new sqlite3_file(pFile);
+ fh.sq3File.$pMethods = opfsIoMethods.pointer;
+ fh.lockType = capi.SQLITE_LOCK_NONE;
+ }
+ mTimeEnd();
+ return rc;
+ }/*xOpen()*/
+ }/*vfsSyncWrappers*/;
+
+ if(dVfs){
+ opfsVfs.$xRandomness = dVfs.$xRandomness;
+ opfsVfs.$xSleep = dVfs.$xSleep;
+ }
+ if(!opfsVfs.$xRandomness){
+ /* If the default VFS has no xRandomness(), add a basic JS impl... */
+ vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
+ const heap = wasm.heap8u();
+ let i = 0;
+ for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
+ return i;
+ };
+ }
+ if(!opfsVfs.$xSleep){
+ /* If we can inherit an xSleep() impl from the default VFS then
+ assume it's sane and use it, otherwise install a JS-based
+ one. */
+ vfsSyncWrappers.xSleep = function(pVfs,ms){
+ Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
+ return 0;
+ };
+ }
+
+ /* Install the vfs/io_methods into their C-level shared instances... */
+ for(let k of Object.keys(ioSyncWrappers)){
+ installMethod(opfsIoMethods, k, ioSyncWrappers[k]);
+ }
+ for(let k of Object.keys(vfsSyncWrappers)){
+ installMethod(opfsVfs, k, vfsSyncWrappers[k]);
+ }
+
+ /**
+ Expects an OPFS file path. It gets resolved, such that ".."
+ components are properly expanded, and returned. If the 2nd arg
+ is true, the result is returned as an array of path elements,
+ else an absolute path string is returned.
+ */
+ opfsUtil.getResolvedPath = function(filename,splitIt){
+ const p = new URL(filename, "file://irrelevant").pathname;
+ return splitIt ? p.split('/').filter((v)=>!!v) : p;
+ };
+
+ /**
+ Takes the absolute path to a filesystem element. Returns an
+ array of [handleOfContainingDir, filename]. If the 2nd argument
+ is truthy then each directory element leading to the file is
+ created along the way. Throws if any creation or resolution
+ fails.
+ */
+ opfsUtil.getDirForFilename = async function f(absFilename, createDirs = false){
+ const path = opfsUtil.getResolvedPath(absFilename, true);
+ const filename = path.pop();
+ let dh = opfsUtil.rootDirectory;
+ for(const dirName of path){
+ if(dirName){
+ dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
+ }
+ }
+ return [dh, filename];
+ };
+
+ /**
+ Creates the given directory name, recursively, in
+ the OPFS filesystem. Returns true if it succeeds or the
+ directory already exists, else false.
+ */
+ opfsUtil.mkdir = async function(absDirName){
+ try {
+ await opfsUtil.getDirForFilename(absDirName+"/filepart", true);
+ return true;
+ }catch(e){
+ //console.warn("mkdir(",absDirName,") failed:",e);
+ return false;
+ }
+ };
+ /**
+ Checks whether the given OPFS filesystem entry exists,
+ returning true if it does, false if it doesn't.
+ */
+ opfsUtil.entryExists = async function(fsEntryName){
+ try {
+ const [dh, fn] = await opfsUtil.getDirForFilename(fsEntryName);
+ await dh.getFileHandle(fn);
+ return true;
+ }catch(e){
+ return false;
+ }
+ };
+
+ /**
+ Generates a random ASCII string, intended for use as a
+ temporary file name. Its argument is the length of the string,
+ defaulting to 16.
+ */
+ opfsUtil.randomFilename = randomFilename;
+
+ /**
+ Re-registers the OPFS VFS. This is intended only for odd use
+ cases which have to call sqlite3_shutdown() as part of their
+ initialization process, which will unregister the VFS
+ registered by installOpfsVfs(). If passed a truthy value, the
+ OPFS VFS is registered as the default VFS, else it is not made
+ the default. Returns the result of the the
+ sqlite3_vfs_register() call.
+
+ Design note: the problem of having to re-register things after
+ a shutdown/initialize pair is more general. How to best plug
+ that in to the library is unclear. In particular, we cannot
+ hook in to any C-side calls to sqlite3_initialize(), so we
+ cannot add an after-initialize callback mechanism.
+ */
+ opfsUtil.registerVfs = (asDefault=false)=>{
+ return wasm.exports.sqlite3_vfs_register(
+ opfsVfs.pointer, asDefault ? 1 : 0
+ );
+ };
+
+ /**
+ Returns a promise which resolves to an object which represents
+ all files and directories in the OPFS tree. The top-most object
+ has two properties: `dirs` is an array of directory entries
+ (described below) and `files` is a list of file names for all
+ files in that directory.
+
+ Traversal starts at sqlite3.opfs.rootDirectory.
+
+ Each `dirs` entry is an object in this form:
+
+ ```
+ { name: directoryName,
+ dirs: [...subdirs],
+ files: [...file names]
+ }
+ ```
+
+ The `files` and `subdirs` entries are always set but may be
+ empty arrays.
+
+ The returned object has the same structure but its `name` is
+ an empty string. All returned objects are created with
+ Object.create(null), so have no prototype.
+
+ Design note: the entries do not contain more information,
+ e.g. file sizes, because getting such info is not only
+ expensive but is subject to locking-related errors.
+ */
+ opfsUtil.treeList = async function(){
+ const doDir = async function callee(dirHandle,tgt){
+ tgt.name = dirHandle.name;
+ tgt.dirs = [];
+ tgt.files = [];
+ for await (const handle of dirHandle.values()){
+ if('directory' === handle.kind){
+ const subDir = Object.create(null);
+ tgt.dirs.push(subDir);
+ await callee(handle, subDir);
+ }else{
+ tgt.files.push(handle.name);
+ }
+ }
+ };
+ const root = Object.create(null);
+ await doDir(opfsUtil.rootDirectory, root);
+ return root;
+ };
+
+ /**
+ Irrevocably deletes _all_ files in the current origin's OPFS.
+ Obviously, this must be used with great caution. It may throw
+ an exception if removal of anything fails (e.g. a file is
+ locked), but the precise conditions under which it will throw
+ are not documented (so we cannot tell you what they are).
+ */
+ opfsUtil.rmfr = async function(){
+ const dir = opfsUtil.rootDirectory, opt = {recurse: true};
+ for await (const handle of dir.values()){
+ dir.removeEntry(handle.name, opt);
+ }
+ };
+
+ /**
+ Deletes the given OPFS filesystem entry. As this environment
+ has no notion of "current directory", the given name must be an
+ absolute path. If the 2nd argument is truthy, deletion is
+ recursive (use with caution!).
+
+ The returned Promise resolves to true if the deletion was
+ successful, else false (but...). The OPFS API reports the
+ reason for the failure only in human-readable form, not
+ exceptions which can be type-checked to determine the
+ failure. Because of that...
+
+ If the final argument is truthy then this function will
+ propagate any exception on error, rather than returning false.
+ */
+ opfsUtil.unlink = async function(fsEntryName, recursive = false,
+ throwOnError = false){
+ try {
+ const [hDir, filenamePart] =
+ await opfsUtil.getDirForFilename(fsEntryName, false);
+ await hDir.removeEntry(filenamePart, {recursive});
+ return true;
+ }catch(e){
+ if(throwOnError){
+ throw new Error("unlink(",arguments[0],") failed: "+e.message,{
+ cause: e
+ });
+ }
+ return false;
+ }
+ };
+
+ /**
+ Traverses the OPFS filesystem, calling a callback for each one.
+ The argument may be either a callback function or an options object
+ with any of the following properties:
+
+ - `callback`: function which gets called for each filesystem
+ entry. It gets passed 3 arguments: 1) the
+ FileSystemFileHandle or FileSystemDirectoryHandle of each
+ entry (noting that both are instanceof FileSystemHandle). 2)
+ the FileSystemDirectoryHandle of the parent directory. 3) the
+ current depth level, with 0 being at the top of the tree
+ relative to the starting directory. If the callback returns a
+ literal false, as opposed to any other falsy value, traversal
+ stops without an error. Any exceptions it throws are
+ propagated. Results are undefined if the callback manipulate
+ the filesystem (e.g. removing or adding entries) because the
+ how OPFS iterators behave in the face of such changes is
+ undocumented.
+
+ - `recursive` [bool=true]: specifies whether to recurse into
+ subdirectories or not. Whether recursion is depth-first or
+ breadth-first is unspecified!
+
+ - `directory` [FileSystemDirectoryEntry=sqlite3.opfs.rootDirectory]
+ specifies the starting directory.
+
+ If this function is passed a function, it is assumed to be the
+ callback.
+
+ Returns a promise because it has to (by virtue of being async)
+ but that promise has no specific meaning: the traversal it
+ performs is synchronous. The promise must be used to catch any
+ exceptions propagated by the callback, however.
+
+ TODO: add an option which specifies whether to traverse
+ depth-first or breadth-first. We currently do depth-first but
+ an incremental file browsing widget would benefit more from
+ breadth-first.
+ */
+ opfsUtil.traverse = async function(opt){
+ const defaultOpt = {
+ recursive: true,
+ directory: opfsUtil.rootDirectory
+ };
+ if('function'===typeof opt){
+ opt = {callback:opt};
+ }
+ opt = Object.assign(defaultOpt, opt||{});
+ const doDir = async function callee(dirHandle, depth){
+ for await (const handle of dirHandle.values()){
+ if(false === opt.callback(handle, dirHandle, depth)) return false;
+ else if(opt.recursive && 'directory' === handle.kind){
+ if(false === await callee(handle, depth + 1)) break;
+ }
+ }
+ };
+ doDir(opt.directory, 0);
+ };
+
+ //TODO to support fiddle and worker1 db upload:
+ //opfsUtil.createFile = function(absName, content=undefined){...}
+
+ if(sqlite3.oo1){
+ opfsUtil.OpfsDb = function(...args){
+ const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args);
+ opt.vfs = opfsVfs.$zName;
+ sqlite3.oo1.DB.dbCtorHelper.call(this, opt);
+ };
+ opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
+ sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql(
+ opfsVfs.pointer,
+ [
+ /* Truncate journal mode is faster than delete or wal for
+ this vfs, per speedtest1. */
+ "pragma journal_mode=truncate;"
+ /*
+ This vfs benefits hugely from cache on moderate/large
+ speedtest1 --size 50 and --size 100 workloads. We currently
+ rely on setting a non-default cache size when building
+ sqlite3.wasm. If that policy changes, the cache can
+ be set here.
+ */
+ //"pragma cache_size=-8388608;"
+ ].join('')
+ );
+ }
+
+ /**
+ Potential TODOs:
+
+ - Expose one or both of the Worker objects via opfsUtil and
+ publish an interface for proxying the higher-level OPFS
+ features like getting a directory listing.
+ */
+ const sanityCheck = function(){
+ const scope = wasm.scopedAllocPush();
+ const sq3File = new sqlite3_file();
+ try{
+ const fid = sq3File.pointer;
+ const openFlags = capi.SQLITE_OPEN_CREATE
+ | capi.SQLITE_OPEN_READWRITE
+ //| capi.SQLITE_OPEN_DELETEONCLOSE
+ | capi.SQLITE_OPEN_MAIN_DB;
+ const pOut = wasm.scopedAlloc(8);
+ const dbFile = "/sanity/check/file"+randomFilename(8);
+ const zDbFile = wasm.scopedAllocCString(dbFile);
+ let rc;
+ state.s11n.serialize("This is ä string.");
+ rc = state.s11n.deserialize();
+ log("deserialize() says:",rc);
+ if("This is ä string."!==rc[0]) toss("String d13n error.");
+ vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
+ rc = wasm.getMemValue(pOut,'i32');
+ log("xAccess(",dbFile,") exists ?=",rc);
+ rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
+ fid, openFlags, pOut);
+ log("open rc =",rc,"state.sabOPView[xOpen] =",
+ state.sabOPView[state.opIds.xOpen]);
+ if(0!==rc){
+ error("open failed with code",rc);
+ return;
+ }
+ vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
+ rc = wasm.getMemValue(pOut,'i32');
+ if(!rc) toss("xAccess() failed to detect file.");
+ rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
+ if(rc) toss('sync failed w/ rc',rc);
+ rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
+ if(rc) toss('truncate failed w/ rc',rc);
+ wasm.setMemValue(pOut,0,'i64');
+ rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
+ if(rc) toss('xFileSize failed w/ rc',rc);
+ log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
+ rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
+ if(rc) toss("xWrite() failed!");
+ const readBuf = wasm.scopedAlloc(16);
+ rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
+ wasm.setMemValue(readBuf+6,0);
+ let jRead = wasm.cstringToJs(readBuf);
+ log("xRead() got:",jRead);
+ if("sanity"!==jRead) toss("Unexpected xRead() value.");
+ if(vfsSyncWrappers.xSleep){
+ log("xSleep()ing before close()ing...");
+ vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
+ log("waking up from xSleep()");
+ }
+ rc = ioSyncWrappers.xClose(fid);
+ log("xClose rc =",rc,"sabOPView =",state.sabOPView);
+ log("Deleting file:",dbFile);
+ vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
+ vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
+ rc = wasm.getMemValue(pOut,'i32');
+ if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
+ warn("End of OPFS sanity checks.");
+ }finally{
+ sq3File.dispose();
+ wasm.scopedAllocPop(scope);
+ }
+ }/*sanityCheck()*/;
+
+ W.onmessage = function({data}){
+ //log("Worker.onmessage:",data);
+ switch(data.type){
+ case 'opfs-async-loaded':
+ /*Arrives as soon as the asyc proxy finishes loading.
+ Pass our config and shared state on to the async worker.*/
+ W.postMessage({type: 'opfs-async-init',args: state});
+ break;
+ case 'opfs-async-inited':{
+ /*Indicates that the async partner has received the 'init'
+ and has finished initializing, so the real work can
+ begin...*/
+ try {
+ const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
+ if(rc){
+ toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
+ }
+ if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
+ toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
+ }
+ capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
+ state.sabOPView = new Int32Array(state.sabOP);
+ state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
+ state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+ initS11n();
+ if(options.sanityChecks){
+ warn("Running sanity checks because of opfs-sanity-check URL arg...");
+ sanityCheck();
+ }
+ navigator.storage.getDirectory().then((d)=>{
+ W.onerror = W._originalOnError;
+ delete W._originalOnError;
+ sqlite3.opfs = opfsUtil;
+ opfsUtil.rootDirectory = d;
+ log("End of OPFS sqlite3_vfs setup.", opfsVfs);
+ promiseResolve(sqlite3);
+ });
+ }catch(e){
+ error(e);
+ promiseReject(e);
+ }
+ break;
+ }
+ default:
+ promiseReject(e);
+ error("Unexpected message from the async worker:",data);
+ break;
+ }/*switch(data.type)*/
+ }/*W.onmessage()*/;
+ })/*thePromise*/;
+ return thePromise;
+}/*installOpfsVfs()*/;
+installOpfsVfs.defaultProxyUri =
+ "sqlite3-opfs-async-proxy.js";
+self.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{
+ if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){
+ return;
+ }
+ try{
+ let proxyJs = installOpfsVfs.defaultProxyUri;
+ if(sqlite3.scriptInfo.sqlite3Dir){
+ installOpfsVfs.defaultProxyUri =
+ sqlite3.scriptInfo.sqlite3Dir + proxyJs;
+ //console.warn("installOpfsVfs.defaultProxyUri =",installOpfsVfs.defaultProxyUri);
+ }
+ return installOpfsVfs().catch((e)=>{
+ console.warn("Ignoring inability to install OPFS sqlite3_vfs:",e.message);
+ });
+ }catch(e){
+ console.error("installOpfsVfs() exception:",e);
+ throw e;
+ }
+});
+}/*sqlite3ApiBootstrap.initializers.push()*/);
diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js
new file mode 100644
index 0000000..fed1c56
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-prologue.js
@@ -0,0 +1,1602 @@
+/*
+ 2022-05-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file is intended to be combined at build-time with other
+ related code, most notably a header and footer which wraps this whole
+ file into an Emscripten Module.postRun() handler which has a parameter
+ named "Module" (the Emscripten Module object). The exact requirements,
+ conventions, and build process are very much under construction and
+ will be (re)documented once they've stopped fluctuating so much.
+
+ Project home page: https://sqlite.org
+
+ Documentation home page: https://sqlite.org/wasm
+
+ Specific goals of this subproject:
+
+ - Except where noted in the non-goals, provide a more-or-less
+ feature-complete wrapper to the sqlite3 C API, insofar as WASM
+ feature parity with C allows for. In fact, provide at least 4
+ APIs...
+
+ 1) 1-to-1 bindings as exported from WASM, with no automatic
+ type conversions between JS and C.
+
+ 2) A binding of (1) which provides certain JS/C type conversions
+ to greatly simplify its use.
+
+ 3) A higher-level API, more akin to sql.js and node.js-style
+ implementations. This one speaks directly to the low-level
+ API. This API must be used from the same thread as the
+ low-level API.
+
+ 4) A second higher-level API which speaks to the previous APIs via
+ worker messages. This one is intended for use in the main
+ thread, with the lower-level APIs installed in a Worker thread,
+ and talking to them via Worker messages. Because Workers are
+ asynchronouns and have only a single message channel, some
+ acrobatics are needed here to feed async work results back to
+ the client (as we cannot simply pass around callbacks between
+ the main and Worker threads).
+
+ - Insofar as possible, support client-side storage using JS
+ filesystem APIs. As of this writing, such things are still very
+ much under development.
+
+ Specific non-goals of this project:
+
+ - As WASM is a web-centric technology and UTF-8 is the King of
+ Encodings in that realm, there are no currently plans to support
+ the UTF16-related sqlite3 APIs. They would add a complication to
+ the bindings for no appreciable benefit. Though web-related
+ implementation details take priority, and the JavaScript
+ components of the API specifically focus on browser clients, the
+ lower-level WASM module "should" work in non-web WASM
+ environments.
+
+ - Supporting old or niche-market platforms. WASM is built for a
+ modern web and requires modern platforms.
+
+ - Though scalar User-Defined Functions (UDFs) may be created in
+ JavaScript, there are currently no plans to add support for
+ aggregate and window functions.
+
+ Attribution:
+
+ This project is endebted to the work of sql.js:
+
+ https://github.com/sql-js/sql.js
+
+ sql.js was an essential stepping stone in this code's development as
+ it demonstrated how to handle some of the WASM-related voodoo (like
+ handling pointers-to-pointers and adding JS implementations of
+ C-bound callback functions). These APIs have a considerably
+ different shape than sql.js's, however.
+*/
+
+/**
+ sqlite3ApiBootstrap() is the only global symbol persistently
+ exposed by this API. It is intended to be called one time at the
+ end of the API amalgamation process, passed configuration details
+ for the current environment, and then optionally be removed from
+ the global object using `delete self.sqlite3ApiBootstrap`.
+
+ This function expects a configuration object, intended to abstract
+ away details specific to any given WASM environment, primarily so
+ that it can be used without any _direct_ dependency on
+ Emscripten. (Note the default values for the config object!) The
+ config object is only honored the first time this is
+ called. Subsequent calls ignore the argument and return the same
+ (configured) object which gets initialized by the first call.
+ This function will throw if any of the required config options are
+ missing.
+
+ The config object properties include:
+
+ - `exports`[^1]: the "exports" object for the current WASM
+ environment. In an Emscripten-based build, this should be set to
+ `Module['asm']`.
+
+ - `memory`[^1]: optional WebAssembly.Memory object, defaulting to
+ `exports.memory`. In Emscripten environments this should be set
+ to `Module.wasmMemory` if the build uses `-sIMPORT_MEMORY`, or be
+ left undefined/falsy to default to `exports.memory` when using
+ WASM-exported memory.
+
+ - `bigIntEnabled`: true if BigInt support is enabled. Defaults to
+ true if `self.BigInt64Array` is available, else false. Some APIs
+ will throw exceptions if called without BigInt support, as BigInt
+ is required for marshalling C-side int64 into and out of JS.
+
+ - `allocExportName`: the name of the function, in `exports`, of the
+ `malloc(3)`-compatible routine for the WASM environment. Defaults
+ to `"malloc"`.
+
+ - `deallocExportName`: the name of the function, in `exports`, of
+ the `free(3)`-compatible routine for the WASM
+ environment. Defaults to `"free"`.
+
+ - `wasmfsOpfsDir`[^1]: if the environment supports persistent
+ storage, this directory names the "mount point" for that
+ directory. It must be prefixed by `/` and may contain only a
+ single directory-name part. Using the root directory name is not
+ supported by any current persistent backend. This setting is
+ only used in WASMFS-enabled builds.
+
+
+ [^1] = This property may optionally be a function, in which case this
+ function re-assigns it to the value returned from that function,
+ enabling delayed evaluation.
+
+*/
+'use strict';
+self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
+ apiConfig = (self.sqlite3ApiConfig || sqlite3ApiBootstrap.defaultConfig)
+){
+ if(sqlite3ApiBootstrap.sqlite3){ /* already initalized */
+ console.warn("sqlite3ApiBootstrap() called multiple times.",
+ "Config and external initializers are ignored on calls after the first.");
+ return sqlite3ApiBootstrap.sqlite3;
+ }
+ const config = Object.assign(Object.create(null),{
+ exports: undefined,
+ memory: undefined,
+ bigIntEnabled: (()=>{
+ if('undefined'!==typeof Module){
+ /* Emscripten module will contain HEAPU64 when built with
+ -sWASM_BIGINT=1, else it will not. */
+ return !!Module.HEAPU64;
+ }
+ return !!self.BigInt64Array;
+ })(),
+ allocExportName: 'malloc',
+ deallocExportName: 'free',
+ wasmfsOpfsDir: '/opfs'
+ }, apiConfig || {});
+
+ [
+ // If any of these config options are functions, replace them with
+ // the result of calling that function...
+ 'exports', 'memory', 'wasmfsOpfsDir'
+ ].forEach((k)=>{
+ if('function' === typeof config[k]){
+ config[k] = config[k]();
+ }
+ });
+
+ /**
+ The main sqlite3 binding API gets installed into this object,
+ mimicking the C API as closely as we can. The numerous members
+ names with prefixes 'sqlite3_' and 'SQLITE_' behave, insofar as
+ possible, identically to the C-native counterparts, as documented at:
+
+ https://www.sqlite.org/c3ref/intro.html
+
+ A very few exceptions require an additional level of proxy
+ function or may otherwise require special attention in the WASM
+ environment, and all such cases are documented somewhere below
+ in this file or in sqlite3-api-glue.js. capi members which are
+ not documented are installed as 1-to-1 proxies for their
+ C-side counterparts.
+ */
+ const capi = Object.create(null);
+ /**
+ Holds state which are specific to the WASM-related
+ infrastructure and glue code. It is not expected that client
+ code will normally need these, but they're exposed here in case
+ it does. These APIs are _not_ to be considered an
+ official/stable part of the sqlite3 WASM API. They may change
+ as the developers' experience suggests appropriate changes.
+
+ Note that a number of members of this object are injected
+ dynamically after the api object is fully constructed, so
+ not all are documented in this file.
+ */
+ const wasm = Object.create(null);
+
+ /** Internal helper for SQLite3Error ctor. */
+ const __rcStr = (rc)=>{
+ return (capi.sqlite3_js_rc_str && capi.sqlite3_js_rc_str(rc))
+ || ("Unknown result code #"+rc);
+ };
+
+ /** Internal helper for SQLite3Error ctor. */
+ const __isInt = (n)=>'number'===typeof n && n===(n | 0);
+
+ /**
+ An Error subclass specifically for reporting DB-level errors and
+ enabling clients to unambiguously identify such exceptions.
+ The C-level APIs never throw, but some of the higher-level
+ C-style APIs do and the object-oriented APIs use exceptions
+ exclusively to report errors.
+ */
+ class SQLite3Error extends Error {
+ /**
+ Constructs this object with a message depending on its arguments:
+
+ - If it's passed only a single integer argument, it is assumed
+ to be an sqlite3 C API result code. The message becomes the
+ result of sqlite3.capi.sqlite3_js_rc_str() or (if that returns
+ falsy) a synthesized string which contains that integer.
+
+ - If passed 2 arguments and the 2nd is a object, it bevaves
+ like the Error(string,object) constructor except that the first
+ argument is subject to the is-integer semantics from the
+ previous point.
+
+ - Else all arguments are concatenated with a space between each
+ one, using args.join(' '), to create the error message.
+ */
+ constructor(...args){
+ if(1===args.length && __isInt(args[0])){
+ super(__rcStr(args[0]));
+ }else if(2===args.length && 'object'===typeof args){
+ if(__isInt(args[0])) super(__rcStr(args[0]), args[1]);
+ else super(...args);
+ }else{
+ super(args.join(' '));
+ }
+ this.name = 'SQLite3Error';
+ }
+ };
+
+ /**
+ Functionally equivalent to the SQLite3Error constructor but may
+ be used as part of an expression, e.g.:
+
+ ```
+ return someFunction(x) || SQLite3Error.toss(...);
+ ```
+ */
+ SQLite3Error.toss = (...args)=>{
+ throw new SQLite3Error(...args);
+ };
+ const toss3 = SQLite3Error.toss;
+
+ if(config.wasmfsOpfsDir && !/^\/[^/]+$/.test(config.wasmfsOpfsDir)){
+ toss3("config.wasmfsOpfsDir must be falsy or in the form '/dir-name'.");
+ }
+
+ /**
+ Returns true if n is a 32-bit (signed) integer, else
+ false. This is used for determining when we need to switch to
+ double-type DB operations for integer values in order to keep
+ more precision.
+ */
+ const isInt32 = (n)=>{
+ return ('bigint'!==typeof n /*TypeError: can't convert BigInt to number*/)
+ && !!(n===(n|0) && n<=2147483647 && n>=-2147483648);
+ };
+ /**
+ Returns true if the given BigInt value is small enough to fit
+ into an int64 value, else false.
+ */
+ const bigIntFits64 = function f(b){
+ if(!f._max){
+ f._max = BigInt("0x7fffffffffffffff");
+ f._min = ~f._max;
+ }
+ return b >= f._min && b <= f._max;
+ };
+
+ /**
+ Returns true if the given BigInt value is small enough to fit
+ into an int32, else false.
+ */
+ const bigIntFits32 = (b)=>(b >= (-0x7fffffffn - 1n) && b <= 0x7fffffffn);
+
+ /**
+ Returns true if the given BigInt value is small enough to fit
+ into a double value without loss of precision, else false.
+ */
+ const bigIntFitsDouble = function f(b){
+ if(!f._min){
+ f._min = Number.MIN_SAFE_INTEGER;
+ f._max = Number.MAX_SAFE_INTEGER;
+ }
+ return b >= f._min && b <= f._max;
+ };
+
+ /** Returns v if v appears to be a TypedArray, else false. */
+ const isTypedArray = (v)=>{
+ return (v && v.constructor && isInt32(v.constructor.BYTES_PER_ELEMENT)) ? v : false;
+ };
+
+
+ /** Internal helper to use in operations which need to distinguish
+ between TypedArrays which are backed by a SharedArrayBuffer
+ from those which are not. */
+ const __SAB = ('undefined'===typeof SharedArrayBuffer)
+ ? function(){} : SharedArrayBuffer;
+ /** Returns true if the given TypedArray object is backed by a
+ SharedArrayBuffer, else false. */
+ const isSharedTypedArray = (aTypedArray)=>(aTypedArray.buffer instanceof __SAB);
+
+ /**
+ Returns either aTypedArray.slice(begin,end) (if
+ aTypedArray.buffer is a SharedArrayBuffer) or
+ aTypedArray.subarray(begin,end) (if it's not).
+
+ This distinction is important for APIs which don't like to
+ work on SABs, e.g. TextDecoder, and possibly for our
+ own APIs which work on memory ranges which "might" be
+ modified by other threads while they're working.
+ */
+ const typedArrayPart = (aTypedArray, begin, end)=>{
+ return isSharedTypedArray(aTypedArray)
+ ? aTypedArray.slice(begin, end)
+ : aTypedArray.subarray(begin, end);
+ };
+
+ /**
+ Returns true if v appears to be one of our bind()-able
+ TypedArray types: Uint8Array or Int8Array. Support for
+ TypedArrays with element sizes >1 is TODO.
+ */
+ const isBindableTypedArray = (v)=>{
+ return v && v.constructor && (1===v.constructor.BYTES_PER_ELEMENT);
+ };
+
+ /**
+ Returns true if v appears to be one of the TypedArray types
+ which is legal for holding SQL code (as opposed to binary blobs).
+
+ Currently this is the same as isBindableTypedArray() but it
+ seems likely that we'll eventually want to add Uint32Array
+ and friends to the isBindableTypedArray() list but not to the
+ isSQLableTypedArray() list.
+ */
+ const isSQLableTypedArray = (v)=>{
+ return v && v.constructor && (1===v.constructor.BYTES_PER_ELEMENT);
+ };
+
+ /** Returns true if isBindableTypedArray(v) does, else throws with a message
+ that v is not a supported TypedArray value. */
+ const affirmBindableTypedArray = (v)=>{
+ return isBindableTypedArray(v)
+ || toss3("Value is not of a supported TypedArray type.");
+ };
+
+ const utf8Decoder = new TextDecoder('utf-8');
+
+ /**
+ Uses TextDecoder to decode the given half-open range of the
+ given TypedArray to a string. This differs from a simple
+ call to TextDecoder in that it accounts for whether the
+ first argument is backed by a SharedArrayBuffer or not,
+ and can work more efficiently if it's not (TextDecoder
+ refuses to act upon an SAB).
+ */
+ const typedArrayToString = function(typedArray, begin, end){
+ return utf8Decoder.decode(typedArrayPart(typedArray, begin,end));
+ };
+
+ /**
+ If v is-a Array, its join("") result is returned. If
+ isSQLableTypedArray(v) is true then typedArrayToString(v) is
+ returned. If it looks like a WASM pointer, wasm.cstringToJs(v) is
+ returned. Else v is returned as-is.
+ */
+ const flexibleString = function(v){
+ if(isSQLableTypedArray(v)) return typedArrayToString(v);
+ else if(Array.isArray(v)) return v.join("");
+ else if(wasm.isPtr(v)) v = wasm.cstringToJs(v);
+ return v;
+ };
+
+ /**
+ An Error subclass specifically for reporting Wasm-level malloc()
+ failure and enabling clients to unambiguously identify such
+ exceptions.
+ */
+ class WasmAllocError extends Error {
+ /**
+ If called with 2 arguments and the 2nd one is an object, it
+ behaves like the Error constructor, else it concatenates all
+ arguments together with a single space between each to
+ construct an error message string. As a special case, if
+ called with no arguments then it uses a default error
+ message.
+ */
+ constructor(...args){
+ if(2===args.length && 'object'===typeof args){
+ super(...args);
+ }else if(args.length){
+ super(args.join(' '));
+ }else{
+ super("Allocation failed.");
+ }
+ this.name = 'WasmAllocError';
+ }
+ };
+ /**
+ Functionally equivalent to the WasmAllocError constructor but may
+ be used as part of an expression, e.g.:
+
+ ```
+ return someAllocatingFunction(x) || WasmAllocError.toss(...);
+ ```
+ */
+ WasmAllocError.toss = (...args)=>{
+ throw new WasmAllocError(...args);
+ };
+
+ Object.assign(capi, {
+ /**
+ sqlite3_create_function_v2() differs from its native
+ counterpart only in the following ways:
+
+ 1) The fourth argument (`eTextRep`) argument must not specify
+ any encoding other than sqlite3.SQLITE_UTF8. The JS API does not
+ currently support any other encoding and likely never
+ will. This function does not replace that argument on its own
+ because it may contain other flags.
+
+ 2) Any of the four final arguments may be either WASM pointers
+ (assumed to be function pointers) or JS Functions. In the
+ latter case, each gets bound to WASM using
+ sqlite3.capi.wasm.installFunction() and that wrapper is passed
+ on to the native implementation.
+
+ The semantics of JS functions are:
+
+ xFunc: is passed `(pCtx, ...values)`. Its return value becomes
+ the new SQL function's result.
+
+ xStep: is passed `(pCtx, ...values)`. Its return value is
+ ignored.
+
+ xFinal: is passed `(pCtx)`. Its return value becomes the new
+ aggregate SQL function's result.
+
+ xDestroy: is passed `(void*)`. Its return value is ignored. The
+ pointer passed to it is the one from the 5th argument to
+ sqlite3_create_function_v2().
+
+ Note that:
+
+ - `pCtx` in the above descriptions is a `sqlite3_context*`. At
+ least 99 times out of a hundred, that initial argument will
+ be irrelevant for JS UDF bindings, but it needs to be there
+ so that the cases where it _is_ relevant, in particular with
+ window and aggregate functions, have full access to the
+ lower-level sqlite3 APIs.
+
+ - When wrapping JS functions, the remaining arguments are passd
+ to them as positional arguments, not as an array of
+ arguments, because that allows callback definitions to be
+ more JS-idiomatic than C-like. For example `(pCtx,a,b)=>a+b`
+ is more intuitive and legible than
+ `(pCtx,args)=>args[0]+args[1]`. For cases where an array of
+ arguments would be more convenient, the callbacks simply need
+ to be declared like `(pCtx,...args)=>{...}`, in which case
+ `args` will be an array.
+
+ - If a JS wrapper throws, it gets translated to
+ sqlite3_result_error() or sqlite3_result_error_nomem(),
+ depending on whether the exception is an
+ sqlite3.WasmAllocError object or not.
+
+ - When passing on WASM function pointers, arguments are _not_
+ converted or reformulated. They are passed on as-is in raw
+ pointer form using their native C signatures. Only JS
+ functions passed in to this routine, and thus wrapped by this
+ routine, get automatic conversions of arguments and result
+ values. The routines which perform those conversions are
+ exposed for client-side use as
+ sqlite3_create_function_v2.convertUdfArgs() and
+ sqlite3_create_function_v2.setUdfResult(). sqlite3_create_function()
+ and sqlite3_create_window_function() have those same methods.
+
+ For xFunc(), xStep(), and xFinal():
+
+ - When called from SQL, arguments to the UDF, and its result,
+ will be converted between JS and SQL with as much fidelity as
+ is feasible, triggering an exception if a type conversion
+ cannot be determined. Some freedom is afforded to numeric
+ conversions due to friction between the JS and C worlds:
+ integers which are larger than 32 bits may be treated as
+ doubles or BigInts.
+
+ If any JS-side bound functions throw, those exceptions are
+ intercepted and converted to database-side errors with the
+ exception of xDestroy(): any exception from it is ignored,
+ possibly generating a console.error() message. Destructors
+ must not throw.
+
+ Once installed, there is currently no way to uninstall the
+ automatically-converted WASM-bound JS functions from WASM. They
+ can be uninstalled from the database as documented in the C
+ API, but this wrapper currently has no infrastructure in place
+ to also free the WASM-bound JS wrappers, effectively resulting
+ in a memory leak if the client uninstalls the UDF. Improving that
+ is a potential TODO, but removing client-installed UDFs is rare
+ in practice. If this factor is relevant for a given client,
+ they can create WASM-bound JS functions themselves, hold on to their
+ pointers, and pass the pointers in to here. Later on, they can
+ free those pointers (using `wasm.uninstallFunction()` or
+ equivalent).
+
+ C reference: https://www.sqlite.org/c3ref/create_function.html
+
+ Maintenance reminder: the ability to add new
+ WASM-accessible functions to the runtime requires that the
+ WASM build is compiled with emcc's `-sALLOW_TABLE_GROWTH`
+ flag.
+ */
+ sqlite3_create_function_v2: function(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, xStep, xFinal, xDestroy
+ ){/*installed later*/},
+ /**
+ Equivalent to passing the same arguments to
+ sqlite3_create_function_v2(), with 0 as the final argument.
+ */
+ sqlite3_create_function:function(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xFunc, xStep, xFinal
+ ){/*installed later*/},
+ /**
+ The sqlite3_create_window_function() JS wrapper differs from
+ its native implementation in the exact same way that
+ sqlite3_create_function_v2() does. The additional function,
+ xInverse(), is treated identically to xStep() by the wrapping
+ layer.
+ */
+ sqlite3_create_window_function: function(
+ pDb, funcName, nArg, eTextRep, pApp,
+ xStep, xFinal, xValue, xInverse, xDestroy
+ ){/*installed later*/},
+ /**
+ The sqlite3_prepare_v3() binding handles two different uses
+ with differing JS/WASM semantics:
+
+ 1) sqlite3_prepare_v3(pDb, sqlString, -1, prepFlags, ppStmt , null)
+
+ 2) sqlite3_prepare_v3(pDb, sqlPointer, sqlByteLen, prepFlags, ppStmt, sqlPointerToPointer)
+
+ Note that the SQL length argument (the 3rd argument) must, for
+ usage (1), always be negative because it must be a byte length
+ and that value is expensive to calculate from JS (where only
+ the character length of strings is readily available). It is
+ retained in this API's interface for code/documentation
+ compatibility reasons but is currently _always_ ignored. With
+ usage (2), the 3rd argument is used as-is but is is still
+ critical that the C-style input string (2nd argument) be
+ terminated with a 0 byte.
+
+ In usage (1), the 2nd argument must be of type string,
+ Uint8Array, or Int8Array (either of which is assumed to
+ hold SQL). If it is, this function assumes case (1) and
+ calls the underyling C function with the equivalent of:
+
+ (pDb, sqlAsString, -1, prepFlags, ppStmt, null)
+
+ The `pzTail` argument is ignored in this case because its
+ result is meaningless when a string-type value is passed
+ through: the string goes through another level of internal
+ conversion for WASM's sake and the result pointer would refer
+ to that transient conversion's memory, not the passed-in
+ string.
+
+ If the sql argument is not a string, it must be a _pointer_ to
+ a NUL-terminated string which was allocated in the WASM memory
+ (e.g. using capi.wasm.alloc() or equivalent). In that case,
+ the final argument may be 0/null/undefined or must be a pointer
+ to which the "tail" of the compiled SQL is written, as
+ documented for the C-side sqlite3_prepare_v3(). In case (2),
+ the underlying C function is called with the equivalent of:
+
+ (pDb, sqlAsPointer, sqlByteLen, prepFlags, ppStmt, pzTail)
+
+ It returns its result and compiled statement as documented in
+ the C API. Fetching the output pointers (5th and 6th
+ parameters) requires using `capi.wasm.getMemValue()` (or
+ equivalent) and the `pzTail` will point to an address relative to
+ the `sqlAsPointer` value.
+
+ If passed an invalid 2nd argument type, this function will
+ return SQLITE_MISUSE and sqlite3_errmsg() will contain a string
+ describing the problem.
+
+ Side-note: if given an empty string, or one which contains only
+ comments or an empty SQL expression, 0 is returned but the result
+ output pointer will be NULL.
+ */
+ sqlite3_prepare_v3: (dbPtr, sql, sqlByteLen, prepFlags,
+ stmtPtrPtr, strPtrPtr)=>{}/*installed later*/,
+
+ /**
+ Equivalent to calling sqlite3_prapare_v3() with 0 as its 4th argument.
+ */
+ sqlite3_prepare_v2: (dbPtr, sql, sqlByteLen,
+ stmtPtrPtr,strPtrPtr)=>{}/*installed later*/,
+
+ /**
+ This binding enables the callback argument to be a JavaScript.
+
+ If the callback is a function, then for the duration of the
+ sqlite3_exec() call, it installs a WASM-bound function which
+ acts as a proxy for the given callback. That proxy will also
+ perform a conversion of the callback's arguments from
+ `(char**)` to JS arrays of strings. However, for API
+ consistency's sake it will still honor the C-level callback
+ parameter order and will call it like:
+
+ `callback(pVoid, colCount, listOfValues, listOfColNames)`
+
+ If the callback is not a JS function then this binding performs
+ no translation of the callback, but the sql argument is still
+ converted to a WASM string for the call using the
+ "flexible-string" argument converter.
+ */
+ sqlite3_exec: (pDb, sql, callback, pVoid, pErrMsg)=>{}/*installed later*/,
+
+ /**
+ If passed a single argument which appears to be a byte-oriented
+ TypedArray (Int8Array or Uint8Array), this function treats that
+ TypedArray as an output target, fetches `theArray.byteLength`
+ bytes of randomness, and populates the whole array with it. As
+ a special case, if the array's length is 0, this function
+ behaves as if it were passed (0,0). When called this way, it
+ returns its argument, else it returns the `undefined` value.
+
+ If called with any other arguments, they are passed on as-is
+ to the C API. Results are undefined if passed any incompatible
+ values.
+ */
+ sqlite3_randomness: (n, outPtr)=>{/*installed later*/},
+ }/*capi*/);
+
+ /**
+ Various internal-use utilities are added here as needed. They
+ are bound to an object only so that we have access to them in
+ the differently-scoped steps of the API bootstrapping
+ process. At the end of the API setup process, this object gets
+ removed. These are NOT part of the public API.
+ */
+ const util = {
+ affirmBindableTypedArray, flexibleString,
+ bigIntFits32, bigIntFits64, bigIntFitsDouble,
+ isBindableTypedArray,
+ isInt32, isSQLableTypedArray, isTypedArray,
+ typedArrayToString,
+ isUIThread: ()=>'undefined'===typeof WorkerGlobalScope,
+ isSharedTypedArray,
+ typedArrayPart
+ };
+
+ Object.assign(wasm, {
+ /**
+ Emscripten APIs have a deep-seated assumption that all pointers
+ are 32 bits. We'll remain optimistic that that won't always be
+ the case and will use this constant in places where we might
+ otherwise use a hard-coded 4.
+ */
+ ptrSizeof: config.wasmPtrSizeof || 4,
+ /**
+ The WASM IR (Intermediate Representation) value for
+ pointer-type values. It MUST refer to a value type of the
+ size described by this.ptrSizeof _or_ it may be any value
+ which ends in '*', which Emscripten's glue code internally
+ translates to i32.
+ */
+ ptrIR: config.wasmPtrIR || "i32",
+ /**
+ True if BigInt support was enabled via (e.g.) the
+ Emscripten -sWASM_BIGINT flag, else false. When
+ enabled, certain 64-bit sqlite3 APIs are enabled which
+ are not otherwise enabled due to JS/WASM int64
+ impedence mismatches.
+ */
+ bigIntEnabled: !!config.bigIntEnabled,
+ /**
+ The symbols exported by the WASM environment.
+ */
+ exports: config.exports
+ || toss3("Missing API config.exports (WASM module exports)."),
+
+ /**
+ When Emscripten compiles with `-sIMPORT_MEMORY`, it
+ initalizes the heap and imports it into wasm, as opposed to
+ the other way around. In this case, the memory is not
+ available via this.exports.memory.
+ */
+ memory: config.memory || config.exports['memory']
+ || toss3("API config object requires a WebAssembly.Memory object",
+ "in either config.exports.memory (exported)",
+ "or config.memory (imported)."),
+
+ /**
+ The API's one single point of access to the WASM-side memory
+ allocator. Works like malloc(3) (and is likely bound to
+ malloc()) but throws an WasmAllocError if allocation fails. It is
+ important that any code which might pass through the sqlite3 C
+ API NOT throw and must instead return SQLITE_NOMEM (or
+ equivalent, depending on the context).
+
+ Very few cases in the sqlite3 JS APIs can result in
+ client-defined functions propagating exceptions via the C-style
+ API. Most notably, this applies to WASM-bound JS functions
+ which are created directly by clients and passed on _as WASM
+ function pointers_ to functions such as
+ sqlite3_create_function_v2(). Such bindings created
+ transparently by this API will automatically use wrappers which
+ catch exceptions and convert them to appropriate error codes.
+
+ For cases where non-throwing allocation is required, use
+ sqlite3.wasm.alloc.impl(), which is direct binding of the
+ underlying C-level allocator.
+
+ Design note: this function is not named "malloc" primarily
+ because Emscripten uses that name and we wanted to avoid any
+ confusion early on in this code's development, when it still
+ had close ties to Emscripten's glue code.
+ */
+ alloc: undefined/*installed later*/,
+
+ /**
+ The API's one single point of access to the WASM-side memory
+ deallocator. Works like free(3) (and is likely bound to
+ free()).
+
+ Design note: this function is not named "free" for the same
+ reason that this.alloc() is not called this.malloc().
+ */
+ dealloc: undefined/*installed later*/
+
+ /* Many more wasm-related APIs get installed later on. */
+ }/*wasm*/);
+
+ /**
+ wasm.alloc()'s srcTypedArray.byteLength bytes,
+ populates them with the values from the source
+ TypedArray, and returns the pointer to that memory. The
+ returned pointer must eventually be passed to
+ wasm.dealloc() to clean it up.
+
+ As a special case, to avoid further special cases where
+ this is used, if srcTypedArray.byteLength is 0, it
+ allocates a single byte and sets it to the value
+ 0. Even in such cases, calls must behave as if the
+ allocated memory has exactly srcTypedArray.byteLength
+ bytes.
+
+ ACHTUNG: this currently only works for Uint8Array and
+ Int8Array types and will throw if srcTypedArray is of
+ any other type.
+ */
+ wasm.allocFromTypedArray = function(srcTypedArray){
+ affirmBindableTypedArray(srcTypedArray);
+ const pRet = wasm.alloc(srcTypedArray.byteLength || 1);
+ wasm.heapForSize(srcTypedArray.constructor).set(
+ srcTypedArray.byteLength ? srcTypedArray : [0], pRet
+ );
+ return pRet;
+ };
+
+ const keyAlloc = config.allocExportName || 'malloc',
+ keyDealloc = config.deallocExportName || 'free';
+ for(const key of [keyAlloc, keyDealloc]){
+ const f = wasm.exports[key];
+ if(!(f instanceof Function)) toss3("Missing required exports[",key,"] function.");
+ }
+
+ wasm.alloc = function f(n){
+ const m = f.impl(n);
+ if(!m) throw new WasmAllocError("Failed to allocate",n," bytes.");
+ return m;
+ };
+ wasm.alloc.impl = wasm.exports[keyAlloc];
+ wasm.dealloc = wasm.exports[keyDealloc];
+
+ /**
+ Reports info about compile-time options using
+ sqlite_compileoption_get() and sqlite3_compileoption_used(). It
+ has several distinct uses:
+
+ If optName is an array then it is expected to be a list of
+ compilation options and this function returns an object
+ which maps each such option to true or false, indicating
+ whether or not the given option was included in this
+ build. That object is returned.
+
+ If optName is an object, its keys are expected to be compilation
+ options and this function sets each entry to true or false,
+ indicating whether the compilation option was used or not. That
+ object is returned.
+
+ If passed no arguments then it returns an object mapping
+ all known compilation options to their compile-time values,
+ or boolean true if they are defined with no value. This
+ result, which is relatively expensive to compute, is cached
+ and returned for future no-argument calls.
+
+ In all other cases it returns true if the given option was
+ active when when compiling the sqlite3 module, else false.
+
+ Compile-time option names may optionally include their
+ "SQLITE_" prefix. When it returns an object of all options,
+ the prefix is elided.
+ */
+ wasm.compileOptionUsed = function f(optName){
+ if(!arguments.length){
+ if(f._result) return f._result;
+ else if(!f._opt){
+ f._rx = /^([^=]+)=(.+)/;
+ f._rxInt = /^-?\d+$/;
+ f._opt = function(opt, rv){
+ const m = f._rx.exec(opt);
+ rv[0] = (m ? m[1] : opt);
+ rv[1] = m ? (f._rxInt.test(m[2]) ? +m[2] : m[2]) : true;
+ };
+ }
+ const rc = {}, ov = [0,0];
+ let i = 0, k;
+ while((k = capi.sqlite3_compileoption_get(i++))){
+ f._opt(k,ov);
+ rc[ov[0]] = ov[1];
+ }
+ return f._result = rc;
+ }else if(Array.isArray(optName)){
+ const rc = {};
+ optName.forEach((v)=>{
+ rc[v] = capi.sqlite3_compileoption_used(v);
+ });
+ return rc;
+ }else if('object' === typeof optName){
+ Object.keys(optName).forEach((k)=> {
+ optName[k] = capi.sqlite3_compileoption_used(k);
+ });
+ return optName;
+ }
+ return (
+ 'string'===typeof optName
+ ) ? !!capi.sqlite3_compileoption_used(optName) : false;
+ }/*compileOptionUsed()*/;
+
+ /**
+ Signatures for the WASM-exported C-side functions. Each entry
+ is an array with 2+ elements:
+
+ [ "c-side name",
+ "result type" (wasm.xWrap() syntax),
+ [arg types in xWrap() syntax]
+ // ^^^ this needn't strictly be an array: it can be subsequent
+ // elements instead: [x,y,z] is equivalent to x,y,z
+ ]
+
+ Note that support for the API-specific data types in the
+ result/argument type strings gets plugged in at a later phase in
+ the API initialization process.
+ */
+ wasm.bindingSignatures = [
+ // Please keep these sorted by function name!
+ ["sqlite3_aggregate_context","void*", "sqlite3_context*", "int"],
+ ["sqlite3_bind_blob","int", "sqlite3_stmt*", "int", "*", "int", "*"
+ /* TODO: we should arguably write a custom wrapper which knows
+ how to handle Blob, TypedArrays, and JS strings. */
+ ],
+ ["sqlite3_bind_double","int", "sqlite3_stmt*", "int", "f64"],
+ ["sqlite3_bind_int","int", "sqlite3_stmt*", "int", "int"],
+ ["sqlite3_bind_null",undefined, "sqlite3_stmt*", "int"],
+ ["sqlite3_bind_parameter_count", "int", "sqlite3_stmt*"],
+ ["sqlite3_bind_parameter_index","int", "sqlite3_stmt*", "string"],
+ ["sqlite3_bind_text","int", "sqlite3_stmt*", "int", "string", "int", "int"
+ /* We should arguably create a hand-written binding of
+ bind_text() which does more flexible text conversion, along
+ the lines of sqlite3_prepare_v3(). The slightly problematic
+ part is the final argument (text destructor). */
+ ],
+ ["sqlite3_close_v2", "int", "sqlite3*"],
+ ["sqlite3_changes", "int", "sqlite3*"],
+ ["sqlite3_clear_bindings","int", "sqlite3_stmt*"],
+ ["sqlite3_column_blob","*", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_bytes","int", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_count", "int", "sqlite3_stmt*"],
+ ["sqlite3_column_double","f64", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_int","int", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_name","string", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_text","string", "sqlite3_stmt*", "int"],
+ ["sqlite3_column_type","int", "sqlite3_stmt*", "int"],
+ ["sqlite3_compileoption_get", "string", "int"],
+ ["sqlite3_compileoption_used", "int", "string"],
+ /* sqlite3_create_function(), sqlite3_create_function_v2(), and
+ sqlite3_create_window_function() use hand-written bindings to
+ simplify handling of their function-type arguments. */
+ ["sqlite3_data_count", "int", "sqlite3_stmt*"],
+ ["sqlite3_db_filename", "string", "sqlite3*", "string"],
+ ["sqlite3_db_handle", "sqlite3*", "sqlite3_stmt*"],
+ ["sqlite3_db_name", "string", "sqlite3*", "int"],
+ ["sqlite3_deserialize", "int", "sqlite3*", "string", "*", "i64", "i64", "int"]
+ /* Careful! Short version: de/serialize() are problematic because they
+ might use a different allocator than the user for managing the
+ deserialized block. de/serialize() are ONLY safe to use with
+ sqlite3_malloc(), sqlite3_free(), and its 64-bit variants. */,
+ ["sqlite3_errmsg", "string", "sqlite3*"],
+ ["sqlite3_error_offset", "int", "sqlite3*"],
+ ["sqlite3_errstr", "string", "int"],
+ /*["sqlite3_exec", "int", "sqlite3*", "string", "*", "*", "**"
+ Handled seperately to perform translation of the callback
+ into a WASM-usable one. ],*/
+ ["sqlite3_expanded_sql", "string", "sqlite3_stmt*"],
+ ["sqlite3_extended_errcode", "int", "sqlite3*"],
+ ["sqlite3_extended_result_codes", "int", "sqlite3*", "int"],
+ ["sqlite3_file_control", "int", "sqlite3*", "string", "int", "*"],
+ ["sqlite3_finalize", "int", "sqlite3_stmt*"],
+ ["sqlite3_free", undefined,"*"],
+ ["sqlite3_initialize", undefined],
+ /*["sqlite3_interrupt", undefined, "sqlite3*"
+ ^^^ we cannot actually currently support this because JS is
+ single-threaded and we don't have a portable way to access a DB
+ from 2 SharedWorkers concurrently. ],*/
+ ["sqlite3_libversion", "string"],
+ ["sqlite3_libversion_number", "int"],
+ ["sqlite3_malloc", "*","int"],
+ ["sqlite3_open", "int", "string", "*"],
+ ["sqlite3_open_v2", "int", "string", "*", "int", "string"],
+ /* sqlite3_prepare_v2() and sqlite3_prepare_v3() are handled
+ separately due to us requiring two different sets of semantics
+ for those, depending on how their SQL argument is provided. */
+ /* sqlite3_randomness() uses a hand-written wrapper to extend
+ the range of supported argument types. */
+ ["sqlite3_realloc", "*","*","int"],
+ ["sqlite3_reset", "int", "sqlite3_stmt*"],
+ ["sqlite3_result_blob",undefined, "*", "*", "int", "*"],
+ ["sqlite3_result_double",undefined, "*", "f64"],
+ ["sqlite3_result_error",undefined, "*", "string", "int"],
+ ["sqlite3_result_error_code", undefined, "*", "int"],
+ ["sqlite3_result_error_nomem", undefined, "*"],
+ ["sqlite3_result_error_toobig", undefined, "*"],
+ ["sqlite3_result_int",undefined, "*", "int"],
+ ["sqlite3_result_null",undefined, "*"],
+ ["sqlite3_result_text",undefined, "*", "string", "int", "*"],
+ ["sqlite3_serialize","*", "sqlite3*", "string", "*", "int"],
+ ["sqlite3_shutdown", undefined],
+ ["sqlite3_sourceid", "string"],
+ ["sqlite3_sql", "string", "sqlite3_stmt*"],
+ ["sqlite3_step", "int", "sqlite3_stmt*"],
+ ["sqlite3_strglob", "int", "string","string"],
+ ["sqlite3_strlike", "int", "string","string","int"],
+ ["sqlite3_trace_v2", "int", "sqlite3*", "int", "*", "*"],
+ ["sqlite3_total_changes", "int", "sqlite3*"],
+ ["sqlite3_uri_boolean", "int", "string", "string", "int"],
+ ["sqlite3_uri_key", "string", "string", "int"],
+ ["sqlite3_uri_parameter", "string", "string", "string"],
+ ["sqlite3_user_data","void*", "sqlite3_context*"],
+ ["sqlite3_value_blob", "*", "sqlite3_value*"],
+ ["sqlite3_value_bytes","int", "sqlite3_value*"],
+ ["sqlite3_value_double","f64", "sqlite3_value*"],
+ ["sqlite3_value_int","int", "sqlite3_value*"],
+ ["sqlite3_value_text", "string", "sqlite3_value*"],
+ ["sqlite3_value_type", "int", "sqlite3_value*"],
+ ["sqlite3_vfs_find", "*", "string"],
+ ["sqlite3_vfs_register", "int", "sqlite3_vfs*", "int"],
+ ["sqlite3_vfs_unregister", "int", "sqlite3_vfs*"]
+ ]/*wasm.bindingSignatures*/;
+
+ if(false && wasm.compileOptionUsed('SQLITE_ENABLE_NORMALIZE')){
+ /* ^^^ "the problem" is that this is an option feature and the
+ build-time function-export list does not currently take
+ optional features into account. */
+ wasm.bindingSignatures.push(["sqlite3_normalized_sql", "string", "sqlite3_stmt*"]);
+ }
+
+ /**
+ Functions which require BigInt (int64) support are separated from
+ the others because we need to conditionally bind them or apply
+ dummy impls, depending on the capabilities of the environment.
+ */
+ wasm.bindingSignatures.int64 = [
+ ["sqlite3_bind_int64","int", ["sqlite3_stmt*", "int", "i64"]],
+ ["sqlite3_changes64","i64", ["sqlite3*"]],
+ ["sqlite3_column_int64","i64", ["sqlite3_stmt*", "int"]],
+ ["sqlite3_malloc64", "*","i64"],
+ ["sqlite3_msize", "i64", "*"],
+ ["sqlite3_realloc64", "*","*", "i64"],
+ ["sqlite3_result_int64",undefined, "*", "i64"],
+ ["sqlite3_total_changes64", "i64", ["sqlite3*"]],
+ ["sqlite3_uri_int64", "i64", ["string", "string", "i64"]],
+ ["sqlite3_value_int64","i64", "sqlite3_value*"],
+ ];
+
+ /**
+ Functions which are intended solely for API-internal use by the
+ WASM components, not client code. These get installed into
+ sqlite3.wasm.
+ */
+ wasm.bindingSignatures.wasm = [
+ ["sqlite3_wasm_db_reset", "int", "sqlite3*"],
+ ["sqlite3_wasm_db_vfs", "sqlite3_vfs*", "sqlite3*","string"],
+ ["sqlite3_wasm_vfs_create_file", "int",
+ "sqlite3_vfs*","string","*", "int"],
+ ["sqlite3_wasm_vfs_unlink", "int", "sqlite3_vfs*","string"]
+ ];
+
+
+ /**
+ sqlite3.wasm.pstack (pseudo-stack) holds a special-case
+ stack-style allocator intended only for use with _small_ data of
+ not more than (in total) a few kb in size, managed as if it were
+ stack-based.
+
+ It has only a single intended usage:
+
+ ```
+ const stackPos = pstack.pointer;
+ try{
+ const ptr = pstack.alloc(8);
+ // ==> pstack.pointer === ptr
+ const otherPtr = pstack.alloc(8);
+ // ==> pstack.pointer === otherPtr
+ ...
+ }finally{
+ pstack.restore(stackPos);
+ // ==> pstack.pointer === stackPos
+ }
+ ```
+
+ This allocator is much faster than a general-purpose one but is
+ limited to usage patterns like the one shown above.
+
+ It operates from a static range of memory which lives outside of
+ space managed by Emscripten's stack-management, so does not
+ collide with Emscripten-provided stack allocation APIs. The
+ memory lives in the WASM heap and can be used with routines such
+ as wasm.setMemValue() and any wasm.heap8u().slice().
+ */
+ wasm.pstack = Object.assign(Object.create(null),{
+ /**
+ Sets the current pstack position to the given pointer. Results
+ are undefined if the passed-in value did not come from
+ this.pointer.
+ */
+ restore: wasm.exports.sqlite3_wasm_pstack_restore,
+ /**
+ Attempts to allocate the given number of bytes from the
+ pstack. On success, it zeroes out a block of memory of the
+ given size, adjusts the pstack pointer, and returns a pointer
+ to the memory. On error, returns throws a WasmAllocError. The
+ memory must eventually be released using restore().
+
+ This method always adjusts the given value to be a multiple
+ of 8 bytes because failing to do so can lead to incorrect
+ results when reading and writing 64-bit values from/to the WASM
+ heap. Similarly, the returned address is always 8-byte aligned.
+ */
+ alloc: (n)=>{
+ return wasm.exports.sqlite3_wasm_pstack_alloc(n)
+ || WasmAllocError.toss("Could not allocate",n,
+ "bytes from the pstack.");
+ },
+ /**
+ alloc()'s n chunks, each sz bytes, as a single memory block and
+ returns the addresses as an array of n element, each holding
+ the address of one chunk.
+
+ Throws a WasmAllocError if allocation fails.
+
+ Example:
+
+ ```
+ const [p1, p2, p3] = wasm.pstack.allocChunks(3,4);
+ ```
+ */
+ allocChunks: (n,sz)=>{
+ const mem = wasm.pstack.alloc(n * sz);
+ const rc = [];
+ let i = 0, offset = 0;
+ for(; i < n; offset = (sz * ++i)){
+ rc.push(mem + offset);
+ }
+ return rc;
+ },
+ /**
+ A convenience wrapper for allocChunks() which sizes each chunk
+ as either 8 bytes (safePtrSize is truthy) or wasm.ptrSizeof (if
+ safePtrSize is falsy).
+
+ How it returns its result differs depending on its first
+ argument: if it's 1, it returns a single pointer value. If it's
+ more than 1, it returns the same as allocChunks().
+
+ When a returned pointers will refer to a 64-bit value, e.g. a
+ double or int64, and that value must be written or fetched,
+ e.g. using wasm.setMemValue() or wasm.getMemValue(), it is
+ important that the pointer in question be aligned to an 8-byte
+ boundary or else it will not be fetched or written properly and
+ will corrupt or read neighboring memory.
+
+ However, when all pointers involved point to "small" data, it
+ is safe to pass a falsy value to save a tiny bit of memory.
+ */
+ allocPtr: (n=1,safePtrSize=true)=>{
+ return 1===n
+ ? wasm.pstack.alloc(safePtrSize ? 8 : wasm.ptrSizeof)
+ : wasm.pstack.allocChunks(n, safePtrSize ? 8 : wasm.ptrSizeof);
+ }
+ })/*wasm.pstack*/;
+ Object.defineProperties(wasm.pstack, {
+ /**
+ sqlite3.wasm.pstack.pointer resolves to the current pstack
+ position pointer. This value is intended _only_ to be saved
+ for passing to restore(). Writing to this memory, without
+ first reserving it via wasm.pstack.alloc() and friends, leads
+ to undefined results.
+ */
+ pointer: {
+ configurable: false, iterable: true, writeable: false,
+ get: wasm.exports.sqlite3_wasm_pstack_ptr
+ //Whether or not a setter as an alternative to restore() is
+ //clearer or would just lead to confusion is unclear.
+ //set: wasm.exports.sqlite3_wasm_pstack_restore
+ },
+ /**
+ sqlite3.wasm.pstack.quota to the total number of bytes
+ available in the pstack, including any space which is currently
+ allocated. This value is a compile-time constant.
+ */
+ quota: {
+ configurable: false, iterable: true, writeable: false,
+ get: wasm.exports.sqlite3_wasm_pstack_quota
+ },
+ /**
+ sqlite3.wasm.pstack.remaining resolves to the amount of space
+ remaining in the pstack.
+ */
+ remaining: {
+ configurable: false, iterable: true, writeable: false,
+ get: wasm.exports.sqlite3_wasm_pstack_remaining
+ }
+ })/*wasm.pstack properties*/;
+
+ capi.sqlite3_randomness = (...args)=>{
+ if(1===args.length && util.isTypedArray(args[0])
+ && 1===args[0].BYTES_PER_ELEMENT){
+ const ta = args[0];
+ if(0===ta.byteLength){
+ wasm.exports.sqlite3_randomness(0,0);
+ return ta;
+ }
+ const stack = wasm.pstack.pointer;
+ try {
+ let n = ta.byteLength, offset = 0;
+ const r = wasm.exports.sqlite3_randomness;
+ const heap = wasm.heap8u();
+ const nAlloc = n < 512 ? n : 512;
+ const ptr = wasm.pstack.alloc(nAlloc);
+ do{
+ const j = (n>nAlloc ? nAlloc : n);
+ r(j, ptr);
+ ta.set(typedArrayPart(heap, ptr, ptr+j), offset);
+ n -= j;
+ offset += j;
+ } while(n > 0);
+ }catch(e){
+ console.error("Highly unexpected (and ignored!) "+
+ "exception in sqlite3_randomness():",e);
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ return ta;
+ }
+ wasm.exports.sqlite3_randomness(...args);
+ };
+
+ /** State for sqlite3_wasmfs_opfs_dir(). */
+ let __wasmfsOpfsDir = undefined;
+ /**
+ If the wasm environment has a WASMFS/OPFS-backed persistent
+ storage directory, its path is returned by this function. If it
+ does not then it returns "" (noting that "" is a falsy value).
+
+ The first time this is called, this function inspects the current
+ environment to determine whether persistence support is available
+ and, if it is, enables it (if needed).
+
+ This function currently only recognizes the WASMFS/OPFS storage
+ combination and its path refers to storage rooted in the
+ Emscripten-managed virtual filesystem.
+ */
+ capi.sqlite3_wasmfs_opfs_dir = function(){
+ if(undefined !== __wasmfsOpfsDir) return __wasmfsOpfsDir;
+ // If we have no OPFS, there is no persistent dir
+ const pdir = config.wasmfsOpfsDir;
+ if(!pdir
+ || !self.FileSystemHandle
+ || !self.FileSystemDirectoryHandle
+ || !self.FileSystemFileHandle){
+ return __wasmfsOpfsDir = "";
+ }
+ try{
+ if(pdir && 0===wasm.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], pdir
+ )){
+ return __wasmfsOpfsDir = pdir;
+ }else{
+ return __wasmfsOpfsDir = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return __wasmfsOpfsDir = "";
+ }
+ };
+
+ /**
+ Experimental and subject to change or removal.
+
+ Returns true if sqlite3.capi.sqlite3_wasmfs_opfs_dir() is a
+ non-empty string and the given name starts with (that string +
+ '/'), else returns false.
+ */
+ capi.sqlite3_wasmfs_filename_is_persistent = function(name){
+ const p = capi.sqlite3_wasmfs_opfs_dir();
+ return (p && name) ? name.startsWith(p+'/') : false;
+ };
+
+ // This bit is highly arguable and is incompatible with the fiddle shell.
+ if(false && 0===wasm.exports.sqlite3_vfs_find(0)){
+ /* Assume that sqlite3_initialize() has not yet been called.
+ This will be the case in an SQLITE_OS_KV build. */
+ wasm.exports.sqlite3_initialize();
+ }
+
+ /**
+ Given an `sqlite3*`, an sqlite3_vfs name, and an optional db name
+ (defaulting to "main"), returns a truthy value (see below) if
+ that db uses that VFS, else returns false. If pDb is falsy then
+ the 3rd argument is ignored and this function returns a truthy
+ value if the default VFS name matches that of the 2nd
+ argument. Results are undefined if pDb is truthy but refers to an
+ invalid pointer. The 3rd argument specifies the database name of
+ the given database connection to check, defaulting to the main
+ db.
+
+ The 2nd and 3rd arguments may either be a JS string or a WASM
+ C-string. If the 2nd argument is a NULL WASM pointer, the default
+ VFS is assumed. If the 3rd is a NULL WASM pointer, "main" is
+ assumed.
+
+ The truthy value it returns is a pointer to the `sqlite3_vfs`
+ object.
+
+ To permit safe use of this function from APIs which may be called
+ via the C stack (like SQL UDFs), this function does not throw: if
+ bad arguments cause a conversion error when passing into
+ wasm-space, false is returned.
+ */
+ capi.sqlite3_js_db_uses_vfs = function(pDb,vfsName,dbName=0){
+ try{
+ const pK = capi.sqlite3_vfs_find(vfsName);
+ if(!pK) return false;
+ else if(!pDb){
+ return pK===capi.sqlite3_vfs_find(0) ? pK : false;
+ }else{
+ return pK===capi.sqlite3_js_db_vfs(pDb,dbName) ? pK : false;
+ }
+ }catch(e){
+ /* Ignore - probably bad args to a wasm-bound function. */
+ return false;
+ }
+ };
+
+ /**
+ Returns an array of the names of all currently-registered sqlite3
+ VFSes.
+ */
+ capi.sqlite3_js_vfs_list = function(){
+ const rc = [];
+ let pVfs = capi.sqlite3_vfs_find(0);
+ while(pVfs){
+ const oVfs = new capi.sqlite3_vfs(pVfs);
+ rc.push(wasm.cstringToJs(oVfs.$zName));
+ pVfs = oVfs.$pNext;
+ oVfs.dispose();
+ }
+ return rc;
+ };
+
+ /**
+ Serializes the given `sqlite3*` pointer to a Uint8Array, as per
+ sqlite3_serialize(). On success it returns a Uint8Array. On
+ error it throws with a description of the problem.
+ */
+ capi.sqlite3_js_db_export = function(pDb){
+ if(!pDb) toss3('Invalid sqlite3* argument.');
+ if(!wasm.bigIntEnabled) toss3('BigInt64 support is not enabled.');
+ const stack = wasm.pstack.pointer;
+ let pOut;
+ try{
+ const pSize = wasm.pstack.alloc(8/*i64*/ + wasm.ptrSizeof);
+ const ppOut = pSize + 8;
+ /**
+ Maintenance reminder, since this cost a full hour of grief
+ and confusion: if the order of pSize/ppOut are reversed in
+ that memory block, fetching the value of pSize after the
+ export reads a garbage size because it's not on an 8-byte
+ memory boundary!
+ */
+ let rc = wasm.exports.sqlite3_wasm_db_serialize(
+ pDb, ppOut, pSize, 0
+ );
+ if(rc){
+ toss3("Database serialization failed with code",
+ sqlite3.capi.sqlite3_js_rc_str(rc));
+ }
+ pOut = wasm.getPtrValue(ppOut);
+ const nOut = wasm.getMemValue(pSize, 'i64');
+ rc = nOut
+ ? wasm.heap8u().slice(pOut, pOut + Number(nOut))
+ : new Uint8Array();
+ return rc;
+ }finally{
+ if(pOut) wasm.exports.sqlite3_free(pOut);
+ wasm.pstack.restore(stack);
+ }
+ };
+
+ /**
+ Given a `sqlite3*` and a database name (JS string or WASM
+ C-string pointer, which may be 0), returns a pointer to the
+ sqlite3_vfs responsible for it. If the given db name is null/0,
+ or not provided, then "main" is assumed.
+ */
+ capi.sqlite3_js_db_vfs =
+ (dbPointer, dbName=0)=>wasm.sqlite3_wasm_db_vfs(dbPointer, dbName);
+
+ /**
+ A thin wrapper around capi.sqlite3_aggregate_context() which
+ behaves the same except that it throws a WasmAllocError if that
+ function returns 0. As a special case, if n is falsy it does
+ _not_ throw if that function returns 0. That special case is
+ intended for use with xFinal() implementations.
+ */
+ capi.sqlite3_js_aggregate_context = (pCtx, n)=>{
+ return capi.sqlite3_aggregate_context(pCtx, n)
+ || (n ? WasmAllocError.toss("Cannot allocate",n,
+ "bytes for sqlite3_aggregate_context()")
+ : 0);
+ };
+
+ if( util.isUIThread() ){
+ /* Features specific to the main window thread... */
+
+ /**
+ Internal helper for sqlite3_js_kvvfs_clear() and friends.
+ Its argument should be one of ('local','session',"").
+ */
+ const __kvvfsInfo = function(which){
+ const rc = Object.create(null);
+ rc.prefix = 'kvvfs-'+which;
+ rc.stores = [];
+ if('session'===which || ""===which) rc.stores.push(self.sessionStorage);
+ if('local'===which || ""===which) rc.stores.push(self.localStorage);
+ return rc;
+ };
+
+ /**
+ Clears all storage used by the kvvfs DB backend, deleting any
+ DB(s) stored there. Its argument must be either 'session',
+ 'local', or "". In the first two cases, only sessionStorage
+ resp. localStorage is cleared. If it's an empty string (the
+ default) then both are cleared. Only storage keys which match
+ the pattern used by kvvfs are cleared: any other client-side
+ data are retained.
+
+ This function is only available in the main window thread.
+
+ Returns the number of entries cleared.
+ */
+ capi.sqlite3_js_kvvfs_clear = function(which=""){
+ let rc = 0;
+ const kvinfo = __kvvfsInfo(which);
+ kvinfo.stores.forEach((s)=>{
+ const toRm = [] /* keys to remove */;
+ let i;
+ for( i = 0; i < s.length; ++i ){
+ const k = s.key(i);
+ if(k.startsWith(kvinfo.prefix)) toRm.push(k);
+ }
+ toRm.forEach((kk)=>s.removeItem(kk));
+ rc += toRm.length;
+ });
+ return rc;
+ };
+
+ /**
+ This routine guesses the approximate amount of
+ window.localStorage and/or window.sessionStorage in use by the
+ kvvfs database backend. Its argument must be one of
+ ('session', 'local', ""). In the first two cases, only
+ sessionStorage resp. localStorage is counted. If it's an empty
+ string (the default) then both are counted. Only storage keys
+ which match the pattern used by kvvfs are counted. The returned
+ value is the "length" value of every matching key and value,
+ noting that JavaScript stores each character in 2 bytes.
+
+ Note that the returned size is not authoritative from the
+ perspective of how much data can fit into localStorage and
+ sessionStorage, as the precise algorithms for determining
+ those limits are unspecified and may include per-entry
+ overhead invisible to clients.
+ */
+ capi.sqlite3_js_kvvfs_size = function(which=""){
+ let sz = 0;
+ const kvinfo = __kvvfsInfo(which);
+ kvinfo.stores.forEach((s)=>{
+ let i;
+ for(i = 0; i < s.length; ++i){
+ const k = s.key(i);
+ if(k.startsWith(kvinfo.prefix)){
+ sz += k.length;
+ sz += s.getItem(k).length;
+ }
+ }
+ });
+ return sz * 2 /* because JS uses 2-byte char encoding */;
+ };
+
+ }/* main-window-only bits */
+
+
+ /* The remainder of the API will be set up in later steps. */
+ const sqlite3 = {
+ WasmAllocError: WasmAllocError,
+ SQLite3Error: SQLite3Error,
+ capi,
+ util,
+ wasm,
+ config,
+ /**
+ Holds the version info of the sqlite3 source tree from which
+ the generated sqlite3-api.js gets built. Note that its version
+ may well differ from that reported by sqlite3_libversion(), but
+ that should be considered a source file mismatch, as the JS and
+ WASM files are intended to be built and distributed together.
+
+ This object is initially a placeholder which gets replaced by a
+ build-generated object.
+ */
+ version: Object.create(null),
+ /**
+ Performs any optional asynchronous library-level initialization
+ which might be required. This function returns a Promise which
+ resolves to the sqlite3 namespace object. Any error in the
+ async init will be fatal to the init as a whole, but init
+ routines are themselves welcome to install dummy catch()
+ handlers which are not fatal if their failure should be
+ considered non-fatal. If called more than once, the second and
+ subsequent calls are no-ops which return a pre-resolved
+ Promise.
+
+ Ideally this function is called as part of the Promise chain
+ which handles the loading and bootstrapping of the API. If not
+ then it must be called by client-level code, which must not use
+ the library until the returned promise resolves.
+
+ Bug: if called while a prior call is still resolving, the 2nd
+ call will resolve prematurely, before the 1st call has finished
+ resolving. The current build setup precludes that possibility,
+ so it's only a hypothetical problem if/when this function
+ ever needs to be invoked by clients.
+
+ In Emscripten-based builds, this function is called
+ automatically and deleted from this object.
+ */
+ asyncPostInit: async function(){
+ let lip = sqlite3ApiBootstrap.initializersAsync;
+ delete sqlite3ApiBootstrap.initializersAsync;
+ if(!lip || !lip.length) return Promise.resolve(sqlite3);
+ // Is it okay to resolve these in parallel or do we need them
+ // to resolve in order? We currently only have 1, so it
+ // makes no difference.
+ lip = lip.map((f)=>{
+ const p = (f instanceof Promise) ? f : f(sqlite3);
+ return p.catch((e)=>{
+ console.error("an async sqlite3 initializer failed:",e);
+ throw e;
+ });
+ });
+ //let p = lip.shift();
+ //while(lip.length) p = p.then(lip.shift());
+ //return p.then(()=>sqlite3);
+ return Promise.all(lip).then(()=>sqlite3);
+ },
+ /**
+ scriptInfo ideally gets injected into this object by the
+ infrastructure which assembles the JS/WASM module. It contains
+ state which must be collected before sqlite3ApiBootstrap() can
+ be declared. It is not necessarily available to any
+ sqlite3ApiBootstrap.initializers but "should" be in place (if
+ it's added at all) by the time that
+ sqlite3ApiBootstrap.initializersAsync is processed.
+
+ This state is not part of the public API, only intended for use
+ with the sqlite3 API bootstrapping and wasm-loading process.
+ */
+ scriptInfo: undefined
+ };
+ try{
+ sqlite3ApiBootstrap.initializers.forEach((f)=>{
+ f(sqlite3);
+ });
+ }catch(e){
+ /* If we don't report this here, it can get completely swallowed
+ up and disappear into the abyss of Promises and Workers. */
+ console.error("sqlite3 bootstrap initializer threw:",e);
+ throw e;
+ }
+ delete sqlite3ApiBootstrap.initializers;
+ sqlite3ApiBootstrap.sqlite3 = sqlite3;
+ return sqlite3;
+}/*sqlite3ApiBootstrap()*/;
+/**
+ self.sqlite3ApiBootstrap.initializers is an internal detail used by
+ the various pieces of the sqlite3 API's amalgamation process. It
+ must not be modified by client code except when plugging such code
+ into the amalgamation process.
+
+ Each component of the amalgamation is expected to append a function
+ to this array. When sqlite3ApiBootstrap() is called for the first
+ time, each such function will be called (in their appended order)
+ and passed the sqlite3 namespace object, into which they can install
+ their features (noting that most will also require that certain
+ features alread have been installed). At the end of that process,
+ this array is deleted.
+
+ Note that the order of insertion into this array is significant for
+ some pieces. e.g. sqlite3.capi and sqlite3.wasm cannot be fully
+ utilized until the whwasmutil.js part is plugged in via
+ sqlite3-api-glue.js.
+*/
+self.sqlite3ApiBootstrap.initializers = [];
+/**
+ self.sqlite3ApiBootstrap.initializersAsync is an internal detail
+ used by the sqlite3 API's amalgamation process. It must not be
+ modified by client code except when plugging such code into the
+ amalgamation process.
+
+ The counterpart of self.sqlite3ApiBootstrap.initializers,
+ specifically for initializers which are asynchronous. All entries in
+ this list must be either async functions, non-async functions which
+ return a Promise, or a Promise. Each function in the list is called
+ with the sqlite3 ojbect as its only argument.
+
+ The resolved value of any Promise is ignored and rejection will kill
+ the asyncPostInit() process (at an indeterminate point because all
+ of them are run asynchronously in parallel).
+
+ This list is not processed until the client calls
+ sqlite3.asyncPostInit(). This means, for example, that intializers
+ added to self.sqlite3ApiBootstrap.initializers may push entries to
+ this list.
+*/
+self.sqlite3ApiBootstrap.initializersAsync = [];
+/**
+ Client code may assign sqlite3ApiBootstrap.defaultConfig an
+ object-type value before calling sqlite3ApiBootstrap() (without
+ arguments) in order to tell that call to use this object as its
+ default config value. The intention of this is to provide
+ downstream clients with a reasonably flexible approach for plugging in
+ an environment-suitable configuration without having to define a new
+ global-scope symbol.
+*/
+self.sqlite3ApiBootstrap.defaultConfig = Object.create(null);
+/**
+ Placeholder: gets installed by the first call to
+ self.sqlite3ApiBootstrap(). However, it is recommended that the
+ caller of sqlite3ApiBootstrap() capture its return value and delete
+ self.sqlite3ApiBootstrap after calling it. It returns the same
+ value which will be stored here.
+*/
+self.sqlite3ApiBootstrap.sqlite3 = undefined;
+
diff --git a/ext/wasm/api/sqlite3-api-worker1.js b/ext/wasm/api/sqlite3-api-worker1.js
new file mode 100644
index 0000000..62e2bb9
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-worker1.js
@@ -0,0 +1,654 @@
+/*
+ 2022-07-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file implements the initializer for the sqlite3 "Worker API
+ #1", a very basic DB access API intended to be scripted from a main
+ window thread via Worker-style messages. Because of limitations in
+ that type of communication, this API is minimalistic and only
+ capable of serving relatively basic DB requests (e.g. it cannot
+ process nested query loops concurrently).
+
+ This file requires that the core C-style sqlite3 API and OO API #1
+ have been loaded.
+*/
+
+/**
+ sqlite3.initWorker1API() implements a Worker-based wrapper around
+ SQLite3 OO API #1, colloquially known as "Worker API #1".
+
+ In order to permit this API to be loaded in worker threads without
+ automatically registering onmessage handlers, initializing the
+ worker API requires calling initWorker1API(). If this function is
+ called from a non-worker thread then it throws an exception. It
+ must only be called once per Worker.
+
+ When initialized, it installs message listeners to receive Worker
+ messages and then it posts a message in the form:
+
+ ```
+ {type:'sqlite3-api', result:'worker1-ready'}
+ ```
+
+ to let the client know that it has been initialized. Clients may
+ optionally depend on this function not returning until
+ initialization is complete, as the initialization is synchronous.
+ In some contexts, however, listening for the above message is
+ a better fit.
+
+ Note that the worker-based interface can be slightly quirky because
+ of its async nature. In particular, any number of messages may be posted
+ to the worker before it starts handling any of them. If, e.g., an
+ "open" operation fails, any subsequent messages will fail. The
+ Promise-based wrapper for this API (`sqlite3-worker1-promiser.js`)
+ is more comfortable to use in that regard.
+
+ The documentation for the input and output worker messages for
+ this API follows...
+
+ ====================================================================
+ Common message format...
+
+ Each message posted to the worker has an operation-independent
+ envelope and operation-dependent arguments:
+
+ ```
+ {
+ type: string, // one of: 'open', 'close', 'exec', 'config-get'
+
+ messageId: OPTIONAL arbitrary value. The worker will copy it as-is
+ into response messages to assist in client-side dispatching.
+
+ dbId: a db identifier string (returned by 'open') which tells the
+ operation which database instance to work on. If not provided, the
+ first-opened db is used. This is an "opaque" value, with no
+ inherently useful syntax or information. Its value is subject to
+ change with any given build of this API and cannot be used as a
+ basis for anything useful beyond its one intended purpose.
+
+ args: ...operation-dependent arguments...
+
+ // the framework may add other properties for testing or debugging
+ // purposes.
+
+ }
+ ```
+
+ Response messages, posted back to the main thread, look like:
+
+ ```
+ {
+ type: string. Same as above except for error responses, which have the type
+ 'error',
+
+ messageId: same value, if any, provided by the inbound message
+
+ dbId: the id of the db which was operated on, if any, as returned
+ by the corresponding 'open' operation.
+
+ result: ...operation-dependent result...
+
+ }
+ ```
+
+ ====================================================================
+ Error responses
+
+ Errors are reported messages in an operation-independent format:
+
+ ```
+ {
+ type: "error",
+
+ messageId: ...as above...,
+
+ dbId: ...as above...
+
+ result: {
+
+ operation: type of the triggering operation: 'open', 'close', ...
+
+ message: ...error message text...
+
+ errorClass: string. The ErrorClass.name property from the thrown exception.
+
+ input: the message object which triggered the error.
+
+ stack: _if available_, a stack trace array.
+
+ }
+
+ }
+ ```
+
+
+ ====================================================================
+ "config-get"
+
+ This operation fetches the serializable parts of the sqlite3 API
+ configuration.
+
+ Message format:
+
+ ```
+ {
+ type: "config-get",
+ messageId: ...as above...,
+ args: currently ignored and may be elided.
+ }
+ ```
+
+ Response:
+
+ ```
+ {
+ type: "config-get",
+ messageId: ...as above...,
+ result: {
+
+ version: sqlite3.version object
+
+ bigIntEnabled: bool. True if BigInt support is enabled.
+
+ wasmfsOpfsDir: path prefix, if any, _intended_ for use with
+ WASMFS OPFS persistent storage.
+
+ wasmfsOpfsEnabled: true if persistent storage is enabled in the
+ current environment. Only files stored under wasmfsOpfsDir
+ will persist using that mechanism, however. It is legal to use
+ the non-WASMFS OPFS VFS to open a database via a URI-style
+ db filename.
+
+ vfsList: result of sqlite3.capi.sqlite3_js_vfs_list()
+ }
+ }
+ ```
+
+
+ ====================================================================
+ "open" a database
+
+ Message format:
+
+ ```
+ {
+ type: "open",
+ messageId: ...as above...,
+ args:{
+
+ filename [=":memory:" or "" (unspecified)]: the db filename.
+ See the sqlite3.oo1.DB constructor for peculiarities and
+ transformations,
+
+ vfs: sqlite3_vfs name. Ignored if filename is ":memory:" or "".
+ This may change how the given filename is resolved.
+ }
+ }
+ ```
+
+ Response:
+
+ ```
+ {
+ type: "open",
+ messageId: ...as above...,
+ result: {
+ filename: db filename, possibly differing from the input.
+
+ dbId: an opaque ID value which must be passed in the message
+ envelope to other calls in this API to tell them which db to
+ use. If it is not provided to future calls, they will default to
+ operating on the least-recently-opened db. This property is, for
+ API consistency's sake, also part of the containing message
+ envelope. Only the `open` operation includes it in the `result`
+ property.
+
+ persistent: true if the given filename resides in the
+ known-persistent storage, else false.
+
+ vfs: name of the VFS the "main" db is using.
+ }
+ }
+ ```
+
+ ====================================================================
+ "close" a database
+
+ Message format:
+
+ ```
+ {
+ type: "close",
+ messageId: ...as above...
+ dbId: ...as above...
+ args: OPTIONAL {unlink: boolean}
+ }
+ ```
+
+ If the `dbId` does not refer to an opened ID, this is a no-op. If
+ the `args` object contains a truthy `unlink` value then the database
+ will be unlinked (deleted) after closing it. The inability to close a
+ db (because it's not opened) or delete its file does not trigger an
+ error.
+
+ Response:
+
+ ```
+ {
+ type: "close",
+ messageId: ...as above...,
+ result: {
+
+ filename: filename of closed db, or undefined if no db was closed
+
+ }
+ }
+ ```
+
+ ====================================================================
+ "exec" SQL
+
+ All SQL execution is processed through the exec operation. It offers
+ most of the features of the oo1.DB.exec() method, with a few limitations
+ imposed by the state having to cross thread boundaries.
+
+ Message format:
+
+ ```
+ {
+ type: "exec",
+ messageId: ...as above...
+ dbId: ...as above...
+ args: string (SQL) or {... see below ...}
+ }
+ ```
+
+ Response:
+
+ ```
+ {
+ type: "exec",
+ messageId: ...as above...,
+ dbId: ...as above...
+ result: {
+ input arguments, possibly modified. See below.
+ }
+ }
+ ```
+
+ The arguments are in the same form accepted by oo1.DB.exec(), with
+ the exceptions noted below.
+
+ A function-type args.callback property cannot cross
+ the window/Worker boundary, so is not useful here. If
+ args.callback is a string then it is assumed to be a
+ message type key, in which case a callback function will be
+ applied which posts each row result via:
+
+ postMessage({type: thatKeyType,
+ rowNumber: 1-based-#,
+ row: theRow,
+ columnNames: anArray
+ })
+
+ And, at the end of the result set (whether or not any result rows
+ were produced), it will post an identical message with
+ (row=undefined, rowNumber=null) to alert the caller than the result
+ set is completed. Note that a row value of `null` is a legal row
+ result for certain arg.rowMode values.
+
+ (Design note: we don't use (row=undefined, rowNumber=undefined) to
+ indicate end-of-results because fetching those would be
+ indistinguishable from fetching from an empty object unless the
+ client used hasOwnProperty() (or similar) to distinguish "missing
+ property" from "property with the undefined value". Similarly,
+ `null` is a legal value for `row` in some case , whereas the db
+ layer won't emit a result value of `undefined`.)
+
+ The callback proxy must not recurse into this interface. An exec()
+ call will tie up the Worker thread, causing any recursion attempt
+ to wait until the first exec() is completed.
+
+ The response is the input options object (or a synthesized one if
+ passed only a string), noting that options.resultRows and
+ options.columnNames may be populated by the call to db.exec().
+
+*/
+self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
+sqlite3.initWorker1API = function(){
+ 'use strict';
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ if('function' !== typeof importScripts){
+ toss("initWorker1API() must be run from a Worker thread.");
+ }
+ const self = this.self;
+ const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object.");
+ const DB = sqlite3.oo1.DB;
+
+ /**
+ Returns the app-wide unique ID for the given db, creating one if
+ needed.
+ */
+ const getDbId = function(db){
+ let id = wState.idMap.get(db);
+ if(id) return id;
+ id = 'db#'+(++wState.idSeq)+'@'+db.pointer;
+ /** ^^^ can't simply use db.pointer b/c closing/opening may re-use
+ the same address, which could map pending messages to a wrong
+ instance. */
+ wState.idMap.set(db, id);
+ return id;
+ };
+
+ /**
+ Internal helper for managing Worker-level state.
+ */
+ const wState = {
+ /**
+ Each opened DB is added to this.dbList, and the first entry in
+ that list is the default db. As each db is closed, its entry is
+ removed from the list.
+ */
+ dbList: [],
+ /** Sequence number of dbId generation. */
+ idSeq: 0,
+ /** Map of DB instances to dbId. */
+ idMap: new WeakMap,
+ /** Temp holder for "transferable" postMessage() state. */
+ xfer: [],
+ open: function(opt){
+ const db = new DB(opt);
+ this.dbs[getDbId(db)] = db;
+ if(this.dbList.indexOf(db)<0) this.dbList.push(db);
+ return db;
+ },
+ close: function(db,alsoUnlink){
+ if(db){
+ delete this.dbs[getDbId(db)];
+ const filename = db.filename;
+ const pVfs = sqlite3.wasm.sqlite3_wasm_db_vfs(db.pointer, 0);
+ db.close();
+ const ddNdx = this.dbList.indexOf(db);
+ if(ddNdx>=0) this.dbList.splice(ddNdx, 1);
+ if(alsoUnlink && filename && pVfs){
+ sqlite3.wasm.sqlite3_wasm_vfs_unlink(pVfs, filename);
+ }
+ }
+ },
+ /**
+ Posts the given worker message value. If xferList is provided,
+ it must be an array, in which case a copy of it passed as
+ postMessage()'s second argument and xferList.length is set to
+ 0.
+ */
+ post: function(msg,xferList){
+ if(xferList && xferList.length){
+ self.postMessage( msg, Array.from(xferList) );
+ xferList.length = 0;
+ }else{
+ self.postMessage(msg);
+ }
+ },
+ /** Map of DB IDs to DBs. */
+ dbs: Object.create(null),
+ /** Fetch the DB for the given id. Throw if require=true and the
+ id is not valid, else return the db or undefined. */
+ getDb: function(id,require=true){
+ return this.dbs[id]
+ || (require ? toss("Unknown (or closed) DB ID:",id) : undefined);
+ }
+ };
+
+ /** Throws if the given db is falsy or not opened, else returns its
+ argument. */
+ const affirmDbOpen = function(db = wState.dbList[0]){
+ return (db && db.pointer) ? db : toss("DB is not opened.");
+ };
+
+ /** Extract dbId from the given message payload. */
+ const getMsgDb = function(msgData,affirmExists=true){
+ const db = wState.getDb(msgData.dbId,false) || wState.dbList[0];
+ return affirmExists ? affirmDbOpen(db) : db;
+ };
+
+ const getDefaultDbId = function(){
+ return wState.dbList[0] && getDbId(wState.dbList[0]);
+ };
+
+ const guessVfs = function(filename){
+ const m = /^file:.+(vfs=(\w+))/.exec(filename);
+ return sqlite3.capi.sqlite3_vfs_find(m ? m[2] : 0);
+ };
+
+ const isSpecialDbFilename = (n)=>{
+ return ""===n || ':'===n[0];
+ };
+
+ /**
+ A level of "organizational abstraction" for the Worker1
+ API. Each method in this object must map directly to a Worker1
+ message type key. The onmessage() dispatcher attempts to
+ dispatch all inbound messages to a method of this object,
+ passing it the event.data part of the inbound event object. All
+ methods must return a plain Object containing any result
+ state, which the dispatcher may amend. All methods must throw
+ on error.
+ */
+ const wMsgHandler = {
+ open: function(ev){
+ const oargs = Object.create(null), args = (ev.args || Object.create(null));
+ if(args.simulateError){ // undocumented internal testing option
+ toss("Throwing because of simulateError flag.");
+ }
+ const rc = Object.create(null);
+ const pDir = sqlite3.capi.sqlite3_wasmfs_opfs_dir();
+ let byteArray, pVfs;
+ oargs.vfs = args.vfs;
+ if(isSpecialDbFilename(args.filename)){
+ oargs.filename = args.filename || "";
+ }else{
+ oargs.filename = args.filename;
+ byteArray = args.byteArray;
+ if(byteArray) pVfs = guessVfs(args.filename);
+ }
+ if(pVfs){
+ /* 2022-11-02: this feature is as-yet untested except that
+ sqlite3_wasm_vfs_create_file() has been tested from the
+ browser dev console. */
+ let pMem;
+ try{
+ pMem = sqlite3.wasm.allocFromTypedArray(byteArray);
+ const rc = sqlite3.wasm.sqlite3_wasm_vfs_create_file(
+ pVfs, oargs.filename, pMem, byteArray.byteLength
+ );
+ if(rc) sqlite3.SQLite3Error.toss(rc);
+ }catch(e){
+ throw new sqlite3.SQLite3Error(
+ e.name+' creating '+args.filename+": "+e.message, {
+ cause: e
+ }
+ );
+ }finally{
+ if(pMem) sqlite3.wasm.dealloc(pMem);
+ }
+ }
+ const db = wState.open(oargs);
+ rc.filename = db.filename;
+ rc.persistent = (!!pDir && db.filename.startsWith(pDir+'/'))
+ || !!sqlite3.capi.sqlite3_js_db_uses_vfs(db.pointer, "opfs");
+ rc.dbId = getDbId(db);
+ rc.vfs = db.dbVfsName();
+ return rc;
+ },
+
+ close: function(ev){
+ const db = getMsgDb(ev,false);
+ const response = {
+ filename: db && db.filename
+ };
+ if(db){
+ const doUnlink = ((ev.args && 'object'===typeof ev.args)
+ ? !!ev.args.unlink : false);
+ wState.close(db, doUnlink);
+ }
+ return response;
+ },
+
+ exec: function(ev){
+ const rc = (
+ 'string'===typeof ev.args
+ ) ? {sql: ev.args} : (ev.args || Object.create(null));
+ if('stmt'===rc.rowMode){
+ toss("Invalid rowMode for 'exec': stmt mode",
+ "does not work in the Worker API.");
+ }else if(!rc.sql){
+ toss("'exec' requires input SQL.");
+ }
+ const db = getMsgDb(ev);
+ if(rc.callback || Array.isArray(rc.resultRows)){
+ // Part of a copy-avoidance optimization for blobs
+ db._blobXfer = wState.xfer;
+ }
+ const theCallback = rc.callback;
+ let rowNumber = 0;
+ const hadColNames = !!rc.columnNames;
+ if('string' === typeof theCallback){
+ if(!hadColNames) rc.columnNames = [];
+ /* Treat this as a worker message type and post each
+ row as a message of that type. */
+ rc.callback = function(row,stmt){
+ wState.post({
+ type: theCallback,
+ columnNames: rc.columnNames,
+ rowNumber: ++rowNumber,
+ row: row
+ }, wState.xfer);
+ }
+ }
+ try {
+ db.exec(rc);
+ if(rc.callback instanceof Function){
+ rc.callback = theCallback;
+ /* Post a sentinel message to tell the client that the end
+ of the result set has been reached (possibly with zero
+ rows). */
+ wState.post({
+ type: theCallback,
+ columnNames: rc.columnNames,
+ rowNumber: null /*null to distinguish from "property not set"*/,
+ row: undefined /*undefined because null is a legal row value
+ for some rowType values, but undefined is not*/
+ });
+ }
+ }finally{
+ delete db._blobXfer;
+ if(rc.callback) rc.callback = theCallback;
+ }
+ return rc;
+ }/*exec()*/,
+
+ 'config-get': function(){
+ const rc = Object.create(null), src = sqlite3.config;
+ [
+ 'wasmfsOpfsDir', 'bigIntEnabled'
+ ].forEach(function(k){
+ if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k];
+ });
+ rc.wasmfsOpfsEnabled = !!sqlite3.capi.sqlite3_wasmfs_opfs_dir();
+ rc.version = sqlite3.version;
+ rc.vfsList = sqlite3.capi.sqlite3_js_vfs_list();
+ rc.opfsEnabled = !!sqlite3.opfs;
+ return rc;
+ },
+
+ /**
+ Exports the database to a byte array, as per
+ sqlite3_serialize(). Response is an object:
+
+ {
+ byteArray: Uint8Array (db file contents),
+ filename: the current db filename,
+ mimetype: 'application/x-sqlite3'
+ }
+ */
+ export: function(ev){
+ const db = getMsgDb(ev);
+ const response = {
+ byteArray: sqlite3.capi.sqlite3_js_db_export(db.pointer),
+ filename: db.filename,
+ mimetype: 'application/x-sqlite3'
+ };
+ wState.xfer.push(response.byteArray.buffer);
+ return response;
+ }/*export()*/,
+
+ toss: function(ev){
+ toss("Testing worker exception");
+ },
+
+ 'opfs-tree': async function(ev){
+ if(!sqlite3.opfs) toss("OPFS support is unavailable.");
+ const response = await sqlite3.opfs.treeList();
+ return response;
+ }
+ }/*wMsgHandler*/;
+
+ self.onmessage = async function(ev){
+ ev = ev.data;
+ let result, dbId = ev.dbId, evType = ev.type;
+ const arrivalTime = performance.now();
+ try {
+ if(wMsgHandler.hasOwnProperty(evType) &&
+ wMsgHandler[evType] instanceof Function){
+ result = await wMsgHandler[evType](ev);
+ }else{
+ toss("Unknown db worker message type:",ev.type);
+ }
+ }catch(err){
+ evType = 'error';
+ result = {
+ operation: ev.type,
+ message: err.message,
+ errorClass: err.name,
+ input: ev
+ };
+ if(err.stack){
+ result.stack = ('string'===typeof err.stack)
+ ? err.stack.split(/\n\s*/) : err.stack;
+ }
+ if(0) console.warn("Worker is propagating an exception to main thread.",
+ "Reporting it _here_ for the stack trace:",err,result);
+ }
+ if(!dbId){
+ dbId = result.dbId/*from 'open' cmd*/
+ || getDefaultDbId();
+ }
+ // Timing info is primarily for use in testing this API. It's not part of
+ // the public API. arrivalTime = when the worker got the message.
+ wState.post({
+ type: evType,
+ dbId: dbId,
+ messageId: ev.messageId,
+ workerReceivedTime: arrivalTime,
+ workerRespondTime: performance.now(),
+ departureTime: ev.departureTime,
+ // TODO: move the timing bits into...
+ //timing:{
+ // departure: ev.departureTime,
+ // workerReceived: arrivalTime,
+ // workerResponse: performance.now();
+ //},
+ result: result
+ }, wState.xfer);
+ };
+ self.postMessage({type:'sqlite3-api',result:'worker1-ready'});
+}.bind({self, sqlite3});
+});
diff --git a/ext/wasm/api/sqlite3-license-version-header.js b/ext/wasm/api/sqlite3-license-version-header.js
new file mode 100644
index 0000000..f8b3edd
--- /dev/null
+++ b/ext/wasm/api/sqlite3-license-version-header.js
@@ -0,0 +1,25 @@
+/*
+** LICENSE for the sqlite3 WebAssembly/JavaScript APIs.
+**
+** This bundle (typically released as sqlite3.js or sqlite3-wasmfs.js)
+** is an amalgamation of JavaScript source code from two projects:
+**
+** 1) https://emscripten.org: the Emscripten "glue code" is covered by
+** the terms of the MIT license and University of Illinois/NCSA
+** Open Source License, as described at:
+**
+** https://emscripten.org/docs/introducing_emscripten/emscripten_license.html
+**
+** 2) https://sqlite.org: all code and documentation labeled as being
+** from this source are released under the same terms as the sqlite3
+** C library:
+**
+** 2022-10-16
+**
+** The author disclaims copyright to this source code. In place of a
+** legal notice, here is a blessing:
+**
+** * May you do good and not evil.
+** * May you find forgiveness for yourself and forgive others.
+** * May you share freely, never taking more than you give.
+*/
diff --git a/ext/wasm/api/sqlite3-opfs-async-proxy.js b/ext/wasm/api/sqlite3-opfs-async-proxy.js
new file mode 100644
index 0000000..e465748
--- /dev/null
+++ b/ext/wasm/api/sqlite3-opfs-async-proxy.js
@@ -0,0 +1,830 @@
+/*
+ 2022-09-16
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A Worker which manages asynchronous OPFS handles on behalf of a
+ synchronous API which controls it via a combination of Worker
+ messages, SharedArrayBuffer, and Atomics. It is the asynchronous
+ counterpart of the API defined in sqlite3-api-opfs.js.
+
+ Highly indebted to:
+
+ https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js
+
+ for demonstrating how to use the OPFS APIs.
+
+ This file is to be loaded as a Worker. It does not have any direct
+ access to the sqlite3 JS/WASM bits, so any bits which it needs (most
+ notably SQLITE_xxx integer codes) have to be imported into it via an
+ initialization process.
+
+ This file represents an implementation detail of a larger piece of
+ code, and not a public interface. Its details may change at any time
+ and are not intended to be used by any client-level code.
+*/
+"use strict";
+const toss = function(...args){throw new Error(args.join(' '))};
+if(self.window === self){
+ toss("This code cannot run from the main thread.",
+ "Load it as a Worker from a separate Worker.");
+}else if(!navigator.storage.getDirectory){
+ toss("This API requires navigator.storage.getDirectory.");
+}
+
+/**
+ Will hold state copied to this object from the syncronous side of
+ this API.
+*/
+const state = Object.create(null);
+
+/**
+ verbose:
+
+ 0 = no logging output
+ 1 = only errors
+ 2 = warnings and errors
+ 3 = debug, warnings, and errors
+*/
+state.verbose = 2;
+
+const loggers = {
+ 0:console.error.bind(console),
+ 1:console.warn.bind(console),
+ 2:console.log.bind(console)
+};
+const logImpl = (level,...args)=>{
+ if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
+};
+const log = (...args)=>logImpl(2, ...args);
+const warn = (...args)=>logImpl(1, ...args);
+const error = (...args)=>logImpl(0, ...args);
+const metrics = Object.create(null);
+metrics.reset = ()=>{
+ let k;
+ const r = (m)=>(m.count = m.time = m.wait = 0);
+ for(k in state.opIds){
+ r(metrics[k] = Object.create(null));
+ }
+ let s = metrics.s11n = Object.create(null);
+ s = s.serialize = Object.create(null);
+ s.count = s.time = 0;
+ s = metrics.s11n.deserialize = Object.create(null);
+ s.count = s.time = 0;
+};
+metrics.dump = ()=>{
+ let k, n = 0, t = 0, w = 0;
+ for(k in state.opIds){
+ const m = metrics[k];
+ n += m.count;
+ t += m.time;
+ w += m.wait;
+ m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
+ }
+ console.log(self.location.href,
+ "metrics for",self.location.href,":\n",
+ metrics,
+ "\nTotal of",n,"op(s) for",t,"ms",
+ "approx",w,"ms spent waiting on OPFS APIs.");
+ console.log("Serialization metrics:",metrics.s11n);
+};
+
+/**
+ __openFiles is a map of sqlite3_file pointers (integers) to
+ metadata related to a given OPFS file handles. The pointers are, in
+ this side of the interface, opaque file handle IDs provided by the
+ synchronous part of this constellation. Each value is an object
+ with a structure demonstrated in the xOpen() impl.
+*/
+const __openFiles = Object.create(null);
+/**
+ __autoLocks 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.
+
+ Maintenance reminder: if we relinquish auto-locks at the end of the
+ operation which acquires them, we pay a massive performance
+ penalty: speedtest1 benchmarks take up to 4x as long. By delaying
+ the lock release until idle time, the hit is negligible.
+*/
+const __autoLocks = new Set();
+
+/**
+ Expects an OPFS file path. It gets resolved, such that ".."
+ components are properly expanded, and returned. If the 2nd arg is
+ true, the result is returned as an array of path elements, else an
+ absolute path string is returned.
+*/
+const getResolvedPath = function(filename,splitIt){
+ const p = new URL(
+ filename, 'file://irrelevant'
+ ).pathname;
+ return splitIt ? p.split('/').filter((v)=>!!v) : p;
+};
+
+/**
+ Takes the absolute path to a filesystem element. Returns an array
+ of [handleOfContainingDir, filename]. If the 2nd argument is truthy
+ then each directory element leading to the file is created along
+ the way. Throws if any creation or resolution fails.
+*/
+const getDirForFilename = async function f(absFilename, createDirs = false){
+ const path = getResolvedPath(absFilename, true);
+ const filename = path.pop();
+ let dh = state.rootDir;
+ for(const dirName of path){
+ if(dirName){
+ dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
+ }
+ }
+ return [dh, filename];
+};
+
+/**
+ An error class specifically for use with getSyncHandle(), the goal
+ of which is to eventually be able to distinguish unambiguously
+ between locking-related failures and other types, noting that we
+ cannot currently do so because createSyncAccessHandle() does not
+ define its exceptions in the required level of detail.
+*/
+class GetSyncHandleError extends Error {
+ constructor(errorObject, ...msg){
+ super();
+ this.error = errorObject;
+ this.message = [
+ ...msg, ': Original exception ['+errorObject.name+']:',
+ errorObject.message
+ ].join(' ');
+ this.name = 'GetSyncHandleError';
+ }
+};
+
+/**
+ Returns the sync access handle associated with the given file
+ handle object (which must be a valid handle object, as created by
+ xOpen()), lazily opening it if needed.
+
+ In order to help alleviate cross-tab contention for a dabase,
+ if an exception is thrown while acquiring the handle, this routine
+ will wait briefly and try again, up to 3 times. If acquisition
+ still fails at that point it will give up and propagate the
+ exception.
+*/
+const getSyncHandle = async (fh)=>{
+ if(!fh.syncHandle){
+ const t = performance.now();
+ log("Acquiring sync handle for",fh.filenameAbs);
+ const maxTries = 4, msBase = 300;
+ let i = 1, ms = msBase;
+ for(; true; ms = msBase * ++i){
+ try {
+ //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
+ //TODO? A config option which tells it to throw here
+ //randomly every now and then, for testing purposes.
+ fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
+ break;
+ }catch(e){
+ if(i === maxTries){
+ throw new GetSyncHandleError(
+ e, "Error getting sync handle.",maxTries,
+ "attempts failed.",fh.filenameAbs
+ );
+ }
+ warn("Error getting sync handle. Waiting",ms,
+ "ms and trying again.",fh.filenameAbs,e);
+ Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
+ }
+ }
+ log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms');
+ if(!fh.xLock){
+ __autoLocks.add(fh.fid);
+ log("Auto-locked",fh.fid,fh.filenameAbs);
+ }
+ }
+ return fh.syncHandle;
+};
+
+/**
+ If the given file-holding object has a sync handle attached to it,
+ that handle is remove and asynchronously closed. Though it may
+ sound sensible to continue work as soon as the close() returns
+ (noting that it's asynchronous), doing so can cause operations
+ performed soon afterwards, e.g. a call to getSyncHandle() to fail
+ because they may happen out of order from the close(). OPFS does
+ not guaranty that the actual order of operations is retained in
+ such cases. i.e. always "await" on the result of this function.
+*/
+const closeSyncHandle = async (fh)=>{
+ if(fh.syncHandle){
+ log("Closing sync handle for",fh.filenameAbs);
+ const h = fh.syncHandle;
+ delete fh.syncHandle;
+ delete fh.xLock;
+ __autoLocks.delete(fh.fid);
+ return h.close();
+ }
+};
+
+/**
+ A proxy for closeSyncHandle() which is guaranteed to not throw.
+
+ This function is part of a lock/unlock step in functions which
+ require a sync access handle but may be called without xLock()
+ having been called first. Such calls need to release that
+ handle to avoid locking the file for all of time. This is an
+ _attempt_ at reducing cross-tab contention but it may prove
+ to be more of a problem than a solution and may need to be
+ removed.
+*/
+const closeSyncHandleNoThrow = async (fh)=>{
+ try{await closeSyncHandle(fh)}
+ catch(e){
+ warn("closeSyncHandleNoThrow() ignoring:",e,fh);
+ }
+};
+
+/**
+ Stores the given value at state.sabOPView[state.opIds.rc] and then
+ Atomics.notify()'s it.
+*/
+const storeAndNotify = (opName, value)=>{
+ log(opName+"() => notify(",value,")");
+ Atomics.store(state.sabOPView, state.opIds.rc, value);
+ Atomics.notify(state.sabOPView, state.opIds.rc);
+};
+
+/**
+ Throws if fh is a file-holding object which is flagged as read-only.
+*/
+const affirmNotRO = function(opName,fh){
+ if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
+};
+const affirmLocked = function(opName,fh){
+ //if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs);
+ /**
+ Currently a no-op, as speedtest1 triggers xRead() without a
+ lock (that seems like a bug but it's currently uninvestigated).
+ This means, however, that some OPFS VFS routines may trigger
+ acquisition of a lock but never let it go until xUnlock() is
+ called (which it likely won't be if xLock() was not called).
+ */
+};
+
+/**
+ We track 2 different timers: the "metrics" timer records how much
+ time we spend performing work. The "wait" timer records how much
+ time we spend waiting on the underlying OPFS timer. See the calls
+ to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
+ throughout this file to see how they're used.
+*/
+const __mTimer = Object.create(null);
+__mTimer.op = undefined;
+__mTimer.start = undefined;
+const mTimeStart = (op)=>{
+ __mTimer.start = performance.now();
+ __mTimer.op = op;
+ //metrics[op] || toss("Maintenance required: missing metrics for",op);
+ ++metrics[op].count;
+};
+const mTimeEnd = ()=>(
+ metrics[__mTimer.op].time += performance.now() - __mTimer.start
+);
+const __wTimer = Object.create(null);
+__wTimer.op = undefined;
+__wTimer.start = undefined;
+const wTimeStart = (op)=>{
+ __wTimer.start = performance.now();
+ __wTimer.op = op;
+ //metrics[op] || toss("Maintenance required: missing metrics for",op);
+};
+const wTimeEnd = ()=>(
+ metrics[__wTimer.op].wait += performance.now() - __wTimer.start
+);
+
+/**
+ Gets set to true by the 'opfs-async-shutdown' command to quit the
+ wait loop. This is only intended for debugging purposes: we cannot
+ inspect this file's state while the tight waitLoop() is running and
+ need a way to stop that loop for introspection purposes.
+*/
+let flagAsyncShutdown = false;
+
+
+/**
+ Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
+ methods, as well as helpers like mkdir(). Maintenance reminder:
+ members are in alphabetical order to simplify finding them.
+*/
+const vfsAsyncImpls = {
+ 'opfs-async-metrics': async ()=>{
+ mTimeStart('opfs-async-metrics');
+ metrics.dump();
+ storeAndNotify('opfs-async-metrics', 0);
+ mTimeEnd();
+ },
+ 'opfs-async-shutdown': async ()=>{
+ flagAsyncShutdown = true;
+ storeAndNotify('opfs-async-shutdown', 0);
+ },
+ mkdir: async (dirname)=>{
+ mTimeStart('mkdir');
+ let rc = 0;
+ wTimeStart('mkdir');
+ try {
+ await getDirForFilename(dirname+"/filepart", true);
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR;
+ }finally{
+ wTimeEnd();
+ }
+ storeAndNotify('mkdir', rc);
+ mTimeEnd();
+ },
+ xAccess: async (filename)=>{
+ mTimeStart('xAccess');
+ /* OPFS cannot support the full range of xAccess() queries sqlite3
+ calls for. We can essentially just tell if the file is
+ accessible, but if it is it's automatically writable (unless
+ it's locked, which we cannot(?) know without trying to open
+ it). OPFS does not have the notion of read-only.
+
+ The return semantics of this function differ from sqlite3's
+ xAccess semantics because we are limited in what we can
+ communicate back to our synchronous communication partner: 0 =
+ accessible, non-0 means not accessible.
+ */
+ let rc = 0;
+ wTimeStart('xAccess');
+ try{
+ const [dh, fn] = await getDirForFilename(filename);
+ await dh.getFileHandle(fn);
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR;
+ }finally{
+ wTimeEnd();
+ }
+ storeAndNotify('xAccess', rc);
+ mTimeEnd();
+ },
+ xClose: async function(fid/*sqlite3_file pointer*/){
+ const opName = 'xClose';
+ mTimeStart(opName);
+ __autoLocks.delete(fid);
+ const fh = __openFiles[fid];
+ let rc = 0;
+ wTimeStart(opName);
+ if(fh){
+ delete __openFiles[fid];
+ await closeSyncHandle(fh);
+ if(fh.deleteOnClose){
+ try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
+ catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
+ }
+ }else{
+ state.s11n.serialize();
+ rc = state.sq3Codes.SQLITE_NOTFOUND;
+ }
+ wTimeEnd();
+ storeAndNotify(opName, rc);
+ mTimeEnd();
+ },
+ xDelete: async function(...args){
+ mTimeStart('xDelete');
+ const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
+ storeAndNotify('xDelete', rc);
+ mTimeEnd();
+ },
+ xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
+ /* The syncDir flag is, for purposes of the VFS API's semantics,
+ ignored here. However, if it has the value 0x1234 then: after
+ deleting the given file, recursively try to delete any empty
+ directories left behind in its wake (ignoring any errors and
+ stopping at the first failure).
+
+ That said: we don't know for sure that removeEntry() fails if
+ the dir is not empty because the API is not documented. It has,
+ however, a "recursive" flag which defaults to false, so
+ presumably it will fail if the dir is not empty and that flag
+ is false.
+ */
+ let rc = 0;
+ wTimeStart('xDelete');
+ try {
+ while(filename){
+ const [hDir, filenamePart] = await getDirForFilename(filename, false);
+ if(!filenamePart) break;
+ await hDir.removeEntry(filenamePart, {recursive});
+ if(0x1234 !== syncDir) break;
+ recursive = false;
+ filename = getResolvedPath(filename, true);
+ filename.pop();
+ filename = filename.join('/');
+ }
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR_DELETE;
+ }
+ wTimeEnd();
+ return rc;
+ },
+ xFileSize: async function(fid/*sqlite3_file pointer*/){
+ mTimeStart('xFileSize');
+ const fh = __openFiles[fid];
+ let rc;
+ wTimeStart('xFileSize');
+ try{
+ affirmLocked('xFileSize',fh);
+ rc = await (await getSyncHandle(fh)).getSize();
+ state.s11n.serialize(Number(rc));
+ rc = 0;
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR;
+ }
+ wTimeEnd();
+ storeAndNotify('xFileSize', rc);
+ mTimeEnd();
+ },
+ xLock: async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/){
+ mTimeStart('xLock');
+ const fh = __openFiles[fid];
+ let rc = 0;
+ const oldLockType = fh.xLock;
+ fh.xLock = lockType;
+ if( !fh.syncHandle ){
+ wTimeStart('xLock');
+ try {
+ await getSyncHandle(fh);
+ __autoLocks.delete(fid);
+ }catch(e){
+ state.s11n.storeException(1,e);
+ rc = state.sq3Codes.SQLITE_IOERR_LOCK;
+ fh.xLock = oldLockType;
+ }
+ wTimeEnd();
+ }
+ storeAndNotify('xLock',rc);
+ mTimeEnd();
+ },
+ xOpen: async function(fid/*sqlite3_file pointer*/, filename,
+ flags/*SQLITE_OPEN_...*/){
+ const opName = 'xOpen';
+ mTimeStart(opName);
+ const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
+ const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
+ wTimeStart('xOpen');
+ try{
+ let hDir, filenamePart;
+ try {
+ [hDir, filenamePart] = await getDirForFilename(filename, !!create);
+ }catch(e){
+ state.s11n.storeException(1,e);
+ storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
+ mTimeEnd();
+ wTimeEnd();
+ return;
+ }
+ const hFile = await hDir.getFileHandle(filenamePart, {create});
+ /**
+ wa-sqlite, at this point, grabs a SyncAccessHandle and
+ assigns it to the syncHandle prop of the file state
+ object, but only for certain cases and it's unclear why it
+ places that limitation on it.
+ */
+ wTimeEnd();
+ __openFiles[fid] = Object.assign(Object.create(null),{
+ fid: fid,
+ filenameAbs: filename,
+ filenamePart: filenamePart,
+ dirHandle: hDir,
+ fileHandle: hFile,
+ sabView: state.sabFileBufView,
+ readOnly: create
+ ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
+ deleteOnClose: deleteOnClose
+ });
+ storeAndNotify(opName, 0);
+ }catch(e){
+ wTimeEnd();
+ error(opName,e);
+ state.s11n.storeException(1,e);
+ storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
+ }
+ mTimeEnd();
+ },
+ xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
+ mTimeStart('xRead');
+ let rc = 0, nRead;
+ const fh = __openFiles[fid];
+ try{
+ affirmLocked('xRead',fh);
+ wTimeStart('xRead');
+ nRead = (await getSyncHandle(fh)).read(
+ fh.sabView.subarray(0, n),
+ {at: Number(offset64)}
+ );
+ wTimeEnd();
+ if(nRead < n){/* Zero-fill remaining bytes */
+ fh.sabView.fill(0, nRead, n);
+ rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
+ }
+ }catch(e){
+ if(undefined===nRead) wTimeEnd();
+ error("xRead() failed",e,fh);
+ state.s11n.storeException(1,e);
+ rc = state.sq3Codes.SQLITE_IOERR_READ;
+ }
+ storeAndNotify('xRead',rc);
+ mTimeEnd();
+ },
+ xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
+ mTimeStart('xSync');
+ const fh = __openFiles[fid];
+ let rc = 0;
+ if(!fh.readOnly && fh.syncHandle){
+ try {
+ wTimeStart('xSync');
+ await fh.syncHandle.flush();
+ }catch(e){
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
+ }
+ wTimeEnd();
+ }
+ storeAndNotify('xSync',rc);
+ mTimeEnd();
+ },
+ xTruncate: async function(fid/*sqlite3_file pointer*/,size){
+ mTimeStart('xTruncate');
+ let rc = 0;
+ const fh = __openFiles[fid];
+ wTimeStart('xTruncate');
+ try{
+ affirmLocked('xTruncate',fh);
+ affirmNotRO('xTruncate', fh);
+ await (await getSyncHandle(fh)).truncate(size);
+ }catch(e){
+ error("xTruncate():",e,fh);
+ state.s11n.storeException(2,e);
+ rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
+ }
+ wTimeEnd();
+ storeAndNotify('xTruncate',rc);
+ mTimeEnd();
+ },
+ xUnlock: async function(fid/*sqlite3_file pointer*/,
+ lockType/*SQLITE_LOCK_...*/){
+ mTimeStart('xUnlock');
+ let rc = 0;
+ const fh = __openFiles[fid];
+ if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
+ && fh.syncHandle ){
+ wTimeStart('xUnlock');
+ try { await closeSyncHandle(fh) }
+ catch(e){
+ state.s11n.storeException(1,e);
+ rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
+ }
+ wTimeEnd();
+ }
+ storeAndNotify('xUnlock',rc);
+ mTimeEnd();
+ },
+ xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
+ mTimeStart('xWrite');
+ let rc;
+ const fh = __openFiles[fid];
+ wTimeStart('xWrite');
+ try{
+ affirmLocked('xWrite',fh);
+ affirmNotRO('xWrite', fh);
+ rc = (
+ n === (await getSyncHandle(fh))
+ .write(fh.sabView.subarray(0, n),
+ {at: Number(offset64)})
+ ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
+ }catch(e){
+ error("xWrite():",e,fh);
+ state.s11n.storeException(1,e);
+ rc = state.sq3Codes.SQLITE_IOERR_WRITE;
+ }
+ wTimeEnd();
+ storeAndNotify('xWrite',rc);
+ mTimeEnd();
+ }
+}/*vfsAsyncImpls*/;
+
+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' };
+ 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);
+ }
+ };
+ state.s11n.deserialize = function(clear=false){
+ ++metrics.s11n.deserialize.count;
+ const t = 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{/*String*/
+ 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;
+ //log("deserialize:",argc, rc);
+ metrics.s11n.deserialize.time += performance.now() - t;
+ return rc;
+ };
+ state.s11n.serialize = function(...args){
+ const t = performance.now();
+ ++metrics.s11n.serialize.count;
+ if(args.length){
+ //log("serialize():",args);
+ const typeIds = [];
+ let i = 0, offset = 1;
+ viewU8[0] = args.length & 0xff /* header = # of args */;
+ for(; i < args.length; ++i, ++offset){
+ /* Write the TypeIds.id value into the next args.length
+ bytes. */
+ typeIds.push(getTypeId(args[i]));
+ viewU8[offset] = typeIds[i].id;
+ }
+ for(i = 0; i < args.length; ++i) {
+ /* Deserialize the following bytes based on their
+ corresponding TypeIds.id from the header. */
+ const t = typeIds[i];
+ if(t.setter){
+ viewDV[t.setter](offset, args[i], state.littleEndian);
+ offset += t.size;
+ }else{/*String*/
+ const s = textEncoder.encode(args[i]);
+ viewDV.setInt32(offset, s.byteLength, state.littleEndian);
+ offset += 4;
+ viewU8.set(s, offset);
+ offset += s.byteLength;
+ }
+ }
+ //log("serialize() result:",viewU8.slice(0,offset));
+ }else{
+ viewU8[0] = 0;
+ }
+ metrics.s11n.serialize.time += performance.now() - t;
+ };
+
+ state.s11n.storeException = state.asyncS11nExceptions
+ ? ((priority,e)=>{
+ if(priority<=state.asyncS11nExceptions){
+ state.s11n.serialize([e.name,': ',e.message].join(""));
+ }
+ })
+ : ()=>{};
+
+ return state.s11n;
+}/*initS11n()*/;
+
+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;
+ }
+ /**
+ waitTime is how long (ms) to wait for each Atomics.wait().
+ We need to wake up periodically to give the thread a chance
+ to do other things.
+ */
+ const waitTime = 500;
+ while(!flagAsyncShutdown){
+ try {
+ if('timed-out'===Atomics.wait(
+ state.sabOPView, state.opIds.whichOp, 0, waitTime
+ )){
+ if(__autoLocks.size){
+ /* Release all auto-locks. */
+ for(const fid of __autoLocks){
+ const fh = __openFiles[fid];
+ await closeSyncHandleNoThrow(fh);
+ log("Auto-unlocked",fid,fh.filenameAbs);
+ }
+ }
+ continue;
+ }
+ 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 /* clear s11n to keep the caller from confusing this with
+ an exception string written by the upcoming
+ operation */
+ ) || [];
+ //warn("waitLoop() whichOp =",opId, hnd, args);
+ if(hnd.f) await hnd.f(...args);
+ else error("Missing callback for opId",opId);
+ }catch(e){
+ error('in waitLoop():',e);
+ }
+ }
+};
+
+navigator.storage.getDirectory().then(function(d){
+ const wMsg = (type)=>postMessage({type});
+ state.rootDir = d;
+ self.onmessage = function({data}){
+ switch(data.type){
+ case 'opfs-async-init':{
+ /* Receive shared state from synchronous partner */
+ const opt = data.args;
+ state.littleEndian = opt.littleEndian;
+ state.asyncS11nExceptions = opt.asyncS11nExceptions;
+ state.verbose = opt.verbose ?? 2;
+ state.fileBufferSize = opt.fileBufferSize;
+ state.sabS11nOffset = opt.sabS11nOffset;
+ state.sabS11nSize = opt.sabS11nSize;
+ state.sabOP = opt.sabOP;
+ state.sabOPView = new Int32Array(state.sabOP);
+ state.sabIO = opt.sabIO;
+ state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
+ state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+ state.opIds = opt.opIds;
+ state.sq3Codes = opt.sq3Codes;
+ Object.keys(vfsAsyncImpls).forEach((k)=>{
+ if(!Number.isFinite(state.opIds[k])){
+ toss("Maintenance required: missing state.opIds[",k,"]");
+ }
+ });
+ initS11n();
+ metrics.reset();
+ log("init state",state);
+ wMsg('opfs-async-inited');
+ waitLoop();
+ break;
+ }
+ case 'opfs-async-restart':
+ if(flagAsyncShutdown){
+ warn("Restarting after opfs-async-shutdown. Might or might not work.");
+ flagAsyncShutdown = false;
+ waitLoop();
+ }
+ break;
+ case 'opfs-async-metrics':
+ metrics.dump();
+ break;
+ }
+ };
+ wMsg('opfs-async-loaded');
+}).catch((e)=>error("error initializing OPFS asyncer:",e));
diff --git a/ext/wasm/api/sqlite3-wasi.h b/ext/wasm/api/sqlite3-wasi.h
new file mode 100644
index 0000000..096f45d
--- /dev/null
+++ b/ext/wasm/api/sqlite3-wasi.h
@@ -0,0 +1,69 @@
+/**
+ Dummy function stubs to get sqlite3.c compiling with
+ wasi-sdk. This requires, in addition:
+
+ -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_GETPID
+
+ -lwasi-emulated-getpid
+*/
+typedef unsigned mode_t;
+int fchmod(int fd, mode_t mode);
+int fchmod(int fd, mode_t mode){
+ return (fd && mode) ? 0 : 0;
+}
+typedef unsigned uid_t;
+typedef uid_t gid_t;
+int fchown(int fd, uid_t owner, gid_t group);
+int fchown(int fd, uid_t owner, gid_t group){
+ return (fd && owner && group) ? 0 : 0;
+}
+uid_t geteuid(void);
+uid_t geteuid(void){return 0;}
+#if !defined(F_WRLCK)
+enum {
+F_WRLCK,
+F_RDLCK,
+F_GETLK,
+F_SETLK,
+F_UNLCK
+};
+#endif
+
+#undef HAVE_PREAD
+
+#include <wasi/api.h>
+#define WASM__KEEP __attribute__((used))
+
+#if 0
+/**
+ wasi-sdk cannot build sqlite3's default VFS without at least the following
+ functions. They are apparently syscalls which clients have to implement or
+ otherwise obtain.
+
+ https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md
+*/
+environ_get
+environ_sizes_get
+clock_time_get
+fd_close
+fd_fdstat_get
+fd_fdstat_set_flags
+fd_filestat_get
+fd_filestat_set_size
+fd_pread
+fd_prestat_get
+fd_prestat_dir_name
+fd_read
+fd_seek
+fd_sync
+fd_write
+path_create_directory
+path_filestat_get
+path_filestat_set_times
+path_open
+path_readlink
+path_remove_directory
+path_unlink_file
+poll_oneoff
+proc_exit
+#endif
diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c
new file mode 100644
index 0000000..af5ed6b
--- /dev/null
+++ b/ext/wasm/api/sqlite3-wasm.c
@@ -0,0 +1,1181 @@
+/*
+** This file requires access to sqlite3.c static state in order to
+** implement certain WASM-specific features, and thus directly
+** includes that file. Unlike the rest of sqlite3.c, this file
+** requires compiling with -std=c99 (or equivalent, or a later C
+** version) because it makes use of features not available in C89.
+**
+** At its simplest, to build sqlite3.wasm either place this file
+** in the same directory as sqlite3.c/h before compilation or use the
+** -I/path flag to tell the compiler where to find both of those
+** files, then compile this file. For example:
+**
+** emcc -o sqlite3.wasm ... -I/path/to/sqlite3-c-and-h sqlite3-wasm.c
+*/
+#define SQLITE_WASM
+#ifdef SQLITE_WASM_ENABLE_C_TESTS
+/*
+** Code blocked off by SQLITE_WASM_TESTS is intended solely for use in
+** unit/regression testing. They may be safely omitted from
+** client-side builds. The main unit test script, tester1.js, will
+** skip related tests if it doesn't find the corresponding functions
+** in the WASM exports.
+*/
+# define SQLITE_WASM_TESTS 1
+#else
+# define SQLITE_WASM_TESTS 0
+#endif
+
+/*
+** Threading and file locking: JS is single-threaded. Each Worker
+** thread is a separate instance of the JS engine so can never access
+** the same db handle as another thread, thus multi-threading support
+** is unnecessary in the library. Because the filesystems are virtual
+** and local to a given wasm runtime instance, two Workers can never
+** access the same db file at once, with the exception of OPFS. As of
+** this writing (2022-09-30), OPFS exclusively locks a file when
+** opening it, so two Workers can never open the same OPFS-backed file
+** at once. That situation will change if and when lower-level locking
+** features are added to OPFS (as is currently planned, per folks
+** involved with its development).
+**
+** Summary: except for the case of future OPFS, which supports
+** locking, and any similar future filesystems, threading and file
+** locking support are unnecessary in the wasm build.
+*/
+
+/*
+** Undefine any SQLITE_... config flags which we specifically do not
+** want undefined. Please keep these alphabetized.
+*/
+#undef SQLITE_OMIT_DESERIALIZE
+#undef SQLITE_OMIT_MEMORYDB
+
+/*
+** Define any SQLITE_... config defaults we want if they aren't
+** overridden by the builder. Please keep these alphabetized.
+*/
+
+/**********************************************************************/
+/* SQLITE_D... */
+#ifndef SQLITE_DEFAULT_CACHE_SIZE
+/*
+** The OPFS impls benefit tremendously from an increased cache size
+** when working on large workloads, e.g. speedtest1 --size 50 or
+** higher. On smaller workloads, e.g. speedtest1 --size 25, they
+** clearly benefit from having 4mb of cache, but not as much as a
+** larger cache benefits the larger workloads. Speed differences
+** between 2x and nearly 3x have been measured with ample page cache.
+*/
+# define SQLITE_DEFAULT_CACHE_SIZE -16384
+#endif
+#if 0 && !defined(SQLITE_DEFAULT_PAGE_SIZE)
+/* TODO: experiment with this. */
+# define SQLITE_DEFAULT_PAGE_SIZE 8192 /*4096*/
+#endif
+#ifndef SQLITE_DEFAULT_UNIX_VFS
+# define SQLITE_DEFAULT_UNIX_VFS "unix-none"
+#endif
+#undef SQLITE_DQS
+#define SQLITE_DQS 0
+
+/**********************************************************************/
+/* SQLITE_ENABLE_... */
+#ifndef SQLITE_ENABLE_BYTECODE_VTAB
+# define SQLITE_ENABLE_BYTECODE_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_DBPAGE_VTAB
+# define SQLITE_ENABLE_DBPAGE_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_DBSTAT_VTAB
+# define SQLITE_ENABLE_DBSTAT_VTAB 1
+#endif
+#ifndef SQLITE_ENABLE_EXPLAIN_COMMENTS
+# define SQLITE_ENABLE_EXPLAIN_COMMENTS 1
+#endif
+#ifndef SQLITE_ENABLE_FTS4
+# define SQLITE_ENABLE_FTS4 1
+#endif
+#ifndef SQLITE_ENABLE_OFFSET_SQL_FUNC
+# define SQLITE_ENABLE_OFFSET_SQL_FUNC 1
+#endif
+#ifndef SQLITE_ENABLE_RTREE
+# define SQLITE_ENABLE_RTREE 1
+#endif
+#ifndef SQLITE_ENABLE_STMTVTAB
+# define SQLITE_ENABLE_STMTVTAB 1
+#endif
+#ifndef SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION
+# define SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION
+#endif
+
+/**********************************************************************/
+/* SQLITE_O... */
+#ifndef SQLITE_OMIT_DEPRECATED
+# define SQLITE_OMIT_DEPRECATED 1
+#endif
+#ifndef SQLITE_OMIT_LOAD_EXTENSION
+# define SQLITE_OMIT_LOAD_EXTENSION 1
+#endif
+#ifndef SQLITE_OMIT_SHARED_CACHE
+# define SQLITE_OMIT_SHARED_CACHE 1
+#endif
+#ifndef SQLITE_OMIT_UTF16
+# define SQLITE_OMIT_UTF16 1
+#endif
+#ifndef SQLITE_OMIT_WAL
+# define SQLITE_OMIT_WAL 1
+#endif
+#ifndef SQLITE_OS_KV_OPTIONAL
+# define SQLITE_OS_KV_OPTIONAL 1
+#endif
+
+/**********************************************************************/
+/* SQLITE_T... */
+#ifndef SQLITE_TEMP_STORE
+# define SQLITE_TEMP_STORE 3
+#endif
+#ifndef SQLITE_THREADSAFE
+# define SQLITE_THREADSAFE 0
+#endif
+
+/**********************************************************************/
+/* SQLITE_USE_... */
+#ifndef SQLITE_USE_URI
+# define SQLITE_USE_URI 1
+#endif
+
+#include <assert.h>
+#include "sqlite3.c" /* yes, .c instead of .h. */
+
+#if defined(__EMSCRIPTEN__)
+# include <emscripten/console.h>
+#endif
+
+/*
+** SQLITE_WASM_KEEP is functionally identical to EMSCRIPTEN_KEEPALIVE
+** but is not Emscripten-specific. It explicitly marks functions for
+** export into the target wasm file without requiring explicit listing
+** of those functions in Emscripten's -sEXPORTED_FUNCTIONS=... list
+** (or equivalent in other build platforms). Any function with neither
+** this attribute nor which is listed as an explicit export will not
+** be exported from the wasm file (but may still be used internally
+** within the wasm file).
+**
+** The functions in this file (sqlite3-wasm.c) which require exporting
+** are marked with this flag. They may also be added to any explicit
+** build-time export list but need not be. All of these APIs are
+** intended for use only within the project's own JS/WASM code, and
+** not by client code, so an argument can be made for reducing their
+** visibility by not including them in any build-time export lists.
+**
+** 2022-09-11: it's not yet _proven_ that this approach works in
+** non-Emscripten builds. If not, such builds will need to export
+** those using the --export=... wasm-ld flag (or equivalent). As of
+** this writing we are tied to Emscripten for various reasons
+** and cannot test the library with other build environments.
+*/
+#define SQLITE_WASM_KEEP __attribute__((used,visibility("default")))
+// See also:
+//__attribute__((export_name("theExportedName"), used, visibility("default")))
+
+
+#if 0
+/*
+** An EXPERIMENT in implementing a stack-based allocator analog to
+** Emscripten's stackSave(), stackAlloc(), stackRestore().
+** Unfortunately, this cannot work together with Emscripten because
+** Emscripten defines its own native one and we'd stomp on each
+** other's memory. Other than that complication, basic tests show it
+** to work just fine.
+**
+** Another option is to malloc() a chunk of our own and call that our
+** "stack".
+*/
+SQLITE_WASM_KEEP void * sqlite3_wasm_stack_end(void){
+ extern void __heap_base
+ /* see https://stackoverflow.com/questions/10038964 */;
+ return &__heap_base;
+}
+SQLITE_WASM_KEEP void * sqlite3_wasm_stack_begin(void){
+ extern void __data_end;
+ return &__data_end;
+}
+static void * pWasmStackPtr = 0;
+SQLITE_WASM_KEEP void * sqlite3_wasm_stack_ptr(void){
+ if(!pWasmStackPtr) pWasmStackPtr = sqlite3_wasm_stack_end();
+ return pWasmStackPtr;
+}
+SQLITE_WASM_KEEP void sqlite3_wasm_stack_restore(void * p){
+ pWasmStackPtr = p;
+}
+SQLITE_WASM_KEEP void * sqlite3_wasm_stack_alloc(int n){
+ if(n<=0) return 0;
+ n = (n + 7) & ~7 /* align to 8-byte boundary */;
+ unsigned char * const p = (unsigned char *)sqlite3_wasm_stack_ptr();
+ unsigned const char * const b = (unsigned const char *)sqlite3_wasm_stack_begin();
+ if(b + n >= p || b + n < b/*overflow*/) return 0;
+ return pWasmStackPtr = p - n;
+}
+#endif /* stack allocator experiment */
+
+/*
+** State for the "pseudo-stack" allocator implemented in
+** sqlite3_wasm_pstack_xyz(). In order to avoid colliding with
+** Emscripten-controled stack space, it carves out a bit of stack
+** memory to use for that purpose. This memory ends up in the
+** WASM-managed memory, such that routines which manipulate the wasm
+** heap can also be used to manipulate this memory.
+**
+** This particular allocator is intended for small allocations such as
+** storage for output pointers. We cannot reasonably size it large
+** enough for general-purpose string conversions because some of our
+** tests use input files (strings) of 16MB+.
+*/
+static unsigned char PStack_mem[512 * 8] = {0};
+static struct {
+ unsigned const char * const pBegin;/* Start (inclusive) of memory */
+ unsigned const char * const pEnd; /* One-after-the-end of memory */
+ unsigned char * pPos; /* Current stack pointer */
+} PStack = {
+ &PStack_mem[0],
+ &PStack_mem[0] + sizeof(PStack_mem),
+ &PStack_mem[0] + sizeof(PStack_mem)
+};
+/*
+** Returns the current pstack position.
+*/
+SQLITE_WASM_KEEP void * sqlite3_wasm_pstack_ptr(void){
+ return PStack.pPos;
+}
+/*
+** Sets the pstack position poitner to p. Results are undefined if the
+** given value did not come from sqlite3_wasm_pstack_ptr().
+*/
+SQLITE_WASM_KEEP void sqlite3_wasm_pstack_restore(unsigned char * p){
+ assert(p>=PStack.pBegin && p<=PStack.pEnd && p>=PStack.pPos);
+ assert(0==(p & 0x7));
+ if(p>=PStack.pBegin && p<=PStack.pEnd /*&& p>=PStack.pPos*/){
+ PStack.pPos = p;
+ }
+}
+/*
+** Allocate and zero out n bytes from the pstack. Returns a pointer to
+** the memory on success, 0 on error (including a negative n value). n
+** is always adjusted to be a multiple of 8 and returned memory is
+** always zeroed out before returning (because this keeps the client
+** JS code from having to do so, and most uses of the pstack will
+** call for doing so).
+*/
+SQLITE_WASM_KEEP void * sqlite3_wasm_pstack_alloc(int n){
+ if( n<=0 ) return 0;
+ //if( n & 0x7 ) n += 8 - (n & 0x7) /* align to 8-byte boundary */;
+ n = (n + 7) & ~7 /* align to 8-byte boundary */;
+ if( PStack.pBegin + n > PStack.pPos /*not enough space left*/
+ || PStack.pBegin + n <= PStack.pBegin /*overflow*/ ) return 0;
+ memset((PStack.pPos = PStack.pPos - n), 0, (unsigned int)n);
+ return PStack.pPos;
+}
+/*
+** Return the number of bytes left which can be
+** sqlite3_wasm_pstack_alloc()'d.
+*/
+SQLITE_WASM_KEEP int sqlite3_wasm_pstack_remaining(void){
+ assert(PStack.pPos >= PStack.pBegin);
+ assert(PStack.pPos <= PStack.pEnd);
+ return (int)(PStack.pPos - PStack.pBegin);
+}
+
+/*
+** Return the total number of bytes available in the pstack, including
+** any space which is currently allocated. This value is a
+** compile-time constant.
+*/
+SQLITE_WASM_KEEP int sqlite3_wasm_pstack_quota(void){
+ return (int)(PStack.pEnd - PStack.pBegin);
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** For purposes of certain hand-crafted C/Wasm function bindings, we
+** need a way of reporting errors which is consistent with the rest of
+** the C API, as opposed to throwing JS exceptions. To that end, this
+** internal-use-only function is a thin proxy around
+** sqlite3ErrorWithMessage(). The intent is that it only be used from
+** Wasm bindings such as sqlite3_prepare_v2/v3(), and definitely not
+** from client code.
+**
+** Returns err_code.
+*/
+SQLITE_WASM_KEEP
+int sqlite3_wasm_db_error(sqlite3*db, int err_code, const char *zMsg){
+ if( 0!=zMsg ){
+ const int nMsg = sqlite3Strlen30(zMsg);
+ sqlite3ErrorWithMsg(db, err_code, "%.*s", nMsg, zMsg);
+ }else{
+ sqlite3ErrorWithMsg(db, err_code, NULL);
+ }
+ return err_code;
+}
+
+#if SQLITE_WASM_TESTS
+struct WasmTestStruct {
+ int v4;
+ void * ppV;
+ const char * cstr;
+ int64_t v8;
+ void (*xFunc)(void*);
+};
+typedef struct WasmTestStruct WasmTestStruct;
+SQLITE_WASM_KEEP
+void sqlite3_wasm_test_struct(WasmTestStruct * s){
+ if(s){
+ s->v4 *= 2;
+ s->v8 = s->v4 * 2;
+ s->ppV = s;
+ s->cstr = __FILE__;
+ if(s->xFunc) s->xFunc(s);
+ }
+ return;
+}
+#endif /* SQLITE_WASM_TESTS */
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings. Unlike the
+** rest of the sqlite3 API, this part requires C99 for snprintf() and
+** variadic macros.
+**
+** Returns a string containing a JSON-format "enum" of C-level
+** constants and struct-related metadata intended to be imported into
+** the JS environment. The JSON is initialized the first time this
+** function is called and that result is reused for all future calls.
+**
+** If this function returns NULL then it means that the internal
+** buffer is not large enough for the generated JSON and needs to be
+** increased. In debug builds that will trigger an assert().
+*/
+SQLITE_WASM_KEEP
+const char * sqlite3_wasm_enum_json(void){
+ static char aBuffer[1024 * 12] = {0} /* where the JSON goes */;
+ int n = 0, nChildren = 0, nStruct = 0
+ /* output counters for figuring out where commas go */;
+ char * zPos = &aBuffer[1] /* skip first byte for now to help protect
+ ** against a small race condition */;
+ char const * const zEnd = &aBuffer[0] + sizeof(aBuffer) /* one-past-the-end */;
+ if(aBuffer[0]) return aBuffer;
+ /* Leave aBuffer[0] at 0 until the end to help guard against a tiny
+ ** race condition. If this is called twice concurrently, they might
+ ** end up both writing to aBuffer, but they'll both write the same
+ ** thing, so that's okay. If we set byte 0 up front then the 2nd
+ ** instance might return and use the string before the 1st instance
+ ** is done filling it. */
+
+/* Core output macros... */
+#define lenCheck assert(zPos < zEnd - 128 \
+ && "sqlite3_wasm_enum_json() buffer is too small."); \
+ if( zPos >= zEnd - 128 ) return 0
+#define outf(format,...) \
+ zPos += snprintf(zPos, ((size_t)(zEnd - zPos)), format, __VA_ARGS__); \
+ lenCheck
+#define out(TXT) outf("%s",TXT)
+#define CloseBrace(LEVEL) \
+ assert(LEVEL<5); memset(zPos, '}', LEVEL); zPos+=LEVEL; lenCheck
+
+/* Macros for emitting maps of integer- and string-type macros to
+** their values. */
+#define DefGroup(KEY) n = 0; \
+ outf("%s\"" #KEY "\": {",(nChildren++ ? "," : ""));
+#define DefInt(KEY) \
+ outf("%s\"%s\": %d", (n++ ? ", " : ""), #KEY, (int)KEY)
+#define DefStr(KEY) \
+ outf("%s\"%s\": \"%s\"", (n++ ? ", " : ""), #KEY, KEY)
+#define _DefGroup CloseBrace(1)
+
+ /* The following groups are sorted alphabetic by group name. */
+ DefGroup(access){
+ DefInt(SQLITE_ACCESS_EXISTS);
+ DefInt(SQLITE_ACCESS_READWRITE);
+ DefInt(SQLITE_ACCESS_READ)/*docs say this is unused*/;
+ } _DefGroup;
+
+ DefGroup(blobFinalizers) {
+ /* SQLITE_STATIC/TRANSIENT need to be handled explicitly as
+ ** integers to avoid casting-related warnings. */
+ out("\"SQLITE_STATIC\":0, \"SQLITE_TRANSIENT\":-1");
+ } _DefGroup;
+
+ DefGroup(dataTypes) {
+ DefInt(SQLITE_INTEGER);
+ DefInt(SQLITE_FLOAT);
+ DefInt(SQLITE_TEXT);
+ DefInt(SQLITE_BLOB);
+ DefInt(SQLITE_NULL);
+ } _DefGroup;
+
+ DefGroup(encodings) {
+ /* Noting that the wasm binding only aims to support UTF-8. */
+ DefInt(SQLITE_UTF8);
+ DefInt(SQLITE_UTF16LE);
+ DefInt(SQLITE_UTF16BE);
+ DefInt(SQLITE_UTF16);
+ /*deprecated DefInt(SQLITE_ANY); */
+ DefInt(SQLITE_UTF16_ALIGNED);
+ } _DefGroup;
+
+ DefGroup(fcntl) {
+ DefInt(SQLITE_FCNTL_LOCKSTATE);
+ DefInt(SQLITE_FCNTL_GET_LOCKPROXYFILE);
+ DefInt(SQLITE_FCNTL_SET_LOCKPROXYFILE);
+ DefInt(SQLITE_FCNTL_LAST_ERRNO);
+ DefInt(SQLITE_FCNTL_SIZE_HINT);
+ DefInt(SQLITE_FCNTL_CHUNK_SIZE);
+ DefInt(SQLITE_FCNTL_FILE_POINTER);
+ DefInt(SQLITE_FCNTL_SYNC_OMITTED);
+ DefInt(SQLITE_FCNTL_WIN32_AV_RETRY);
+ DefInt(SQLITE_FCNTL_PERSIST_WAL);
+ DefInt(SQLITE_FCNTL_OVERWRITE);
+ DefInt(SQLITE_FCNTL_VFSNAME);
+ DefInt(SQLITE_FCNTL_POWERSAFE_OVERWRITE);
+ DefInt(SQLITE_FCNTL_PRAGMA);
+ DefInt(SQLITE_FCNTL_BUSYHANDLER);
+ DefInt(SQLITE_FCNTL_TEMPFILENAME);
+ DefInt(SQLITE_FCNTL_MMAP_SIZE);
+ DefInt(SQLITE_FCNTL_TRACE);
+ DefInt(SQLITE_FCNTL_HAS_MOVED);
+ DefInt(SQLITE_FCNTL_SYNC);
+ DefInt(SQLITE_FCNTL_COMMIT_PHASETWO);
+ DefInt(SQLITE_FCNTL_WIN32_SET_HANDLE);
+ DefInt(SQLITE_FCNTL_WAL_BLOCK);
+ DefInt(SQLITE_FCNTL_ZIPVFS);
+ DefInt(SQLITE_FCNTL_RBU);
+ DefInt(SQLITE_FCNTL_VFS_POINTER);
+ DefInt(SQLITE_FCNTL_JOURNAL_POINTER);
+ DefInt(SQLITE_FCNTL_WIN32_GET_HANDLE);
+ DefInt(SQLITE_FCNTL_PDB);
+ DefInt(SQLITE_FCNTL_BEGIN_ATOMIC_WRITE);
+ DefInt(SQLITE_FCNTL_COMMIT_ATOMIC_WRITE);
+ DefInt(SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE);
+ DefInt(SQLITE_FCNTL_LOCK_TIMEOUT);
+ DefInt(SQLITE_FCNTL_DATA_VERSION);
+ DefInt(SQLITE_FCNTL_SIZE_LIMIT);
+ DefInt(SQLITE_FCNTL_CKPT_DONE);
+ DefInt(SQLITE_FCNTL_RESERVE_BYTES);
+ DefInt(SQLITE_FCNTL_CKPT_START);
+ DefInt(SQLITE_FCNTL_EXTERNAL_READER);
+ DefInt(SQLITE_FCNTL_CKSM_FILE);
+ } _DefGroup;
+
+ DefGroup(flock) {
+ DefInt(SQLITE_LOCK_NONE);
+ DefInt(SQLITE_LOCK_SHARED);
+ DefInt(SQLITE_LOCK_RESERVED);
+ DefInt(SQLITE_LOCK_PENDING);
+ DefInt(SQLITE_LOCK_EXCLUSIVE);
+ } _DefGroup;
+
+ DefGroup(ioCap) {
+ DefInt(SQLITE_IOCAP_ATOMIC);
+ DefInt(SQLITE_IOCAP_ATOMIC512);
+ DefInt(SQLITE_IOCAP_ATOMIC1K);
+ DefInt(SQLITE_IOCAP_ATOMIC2K);
+ DefInt(SQLITE_IOCAP_ATOMIC4K);
+ DefInt(SQLITE_IOCAP_ATOMIC8K);
+ DefInt(SQLITE_IOCAP_ATOMIC16K);
+ DefInt(SQLITE_IOCAP_ATOMIC32K);
+ DefInt(SQLITE_IOCAP_ATOMIC64K);
+ DefInt(SQLITE_IOCAP_SAFE_APPEND);
+ DefInt(SQLITE_IOCAP_SEQUENTIAL);
+ DefInt(SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN);
+ DefInt(SQLITE_IOCAP_POWERSAFE_OVERWRITE);
+ DefInt(SQLITE_IOCAP_IMMUTABLE);
+ DefInt(SQLITE_IOCAP_BATCH_ATOMIC);
+ } _DefGroup;
+
+ DefGroup(openFlags) {
+ /* Noting that not all of these will have any effect in
+ ** WASM-space. */
+ DefInt(SQLITE_OPEN_READONLY);
+ DefInt(SQLITE_OPEN_READWRITE);
+ DefInt(SQLITE_OPEN_CREATE);
+ DefInt(SQLITE_OPEN_URI);
+ DefInt(SQLITE_OPEN_MEMORY);
+ DefInt(SQLITE_OPEN_NOMUTEX);
+ DefInt(SQLITE_OPEN_FULLMUTEX);
+ DefInt(SQLITE_OPEN_SHAREDCACHE);
+ DefInt(SQLITE_OPEN_PRIVATECACHE);
+ DefInt(SQLITE_OPEN_EXRESCODE);
+ DefInt(SQLITE_OPEN_NOFOLLOW);
+ /* OPEN flags for use with VFSes... */
+ DefInt(SQLITE_OPEN_MAIN_DB);
+ DefInt(SQLITE_OPEN_MAIN_JOURNAL);
+ DefInt(SQLITE_OPEN_TEMP_DB);
+ DefInt(SQLITE_OPEN_TEMP_JOURNAL);
+ DefInt(SQLITE_OPEN_TRANSIENT_DB);
+ DefInt(SQLITE_OPEN_SUBJOURNAL);
+ DefInt(SQLITE_OPEN_SUPER_JOURNAL);
+ DefInt(SQLITE_OPEN_WAL);
+ DefInt(SQLITE_OPEN_DELETEONCLOSE);
+ DefInt(SQLITE_OPEN_EXCLUSIVE);
+ } _DefGroup;
+
+ DefGroup(prepareFlags) {
+ DefInt(SQLITE_PREPARE_PERSISTENT);
+ DefInt(SQLITE_PREPARE_NORMALIZE);
+ DefInt(SQLITE_PREPARE_NO_VTAB);
+ } _DefGroup;
+
+ DefGroup(resultCodes) {
+ DefInt(SQLITE_OK);
+ DefInt(SQLITE_ERROR);
+ DefInt(SQLITE_INTERNAL);
+ DefInt(SQLITE_PERM);
+ DefInt(SQLITE_ABORT);
+ DefInt(SQLITE_BUSY);
+ DefInt(SQLITE_LOCKED);
+ DefInt(SQLITE_NOMEM);
+ DefInt(SQLITE_READONLY);
+ DefInt(SQLITE_INTERRUPT);
+ DefInt(SQLITE_IOERR);
+ DefInt(SQLITE_CORRUPT);
+ DefInt(SQLITE_NOTFOUND);
+ DefInt(SQLITE_FULL);
+ DefInt(SQLITE_CANTOPEN);
+ DefInt(SQLITE_PROTOCOL);
+ DefInt(SQLITE_EMPTY);
+ DefInt(SQLITE_SCHEMA);
+ DefInt(SQLITE_TOOBIG);
+ DefInt(SQLITE_CONSTRAINT);
+ DefInt(SQLITE_MISMATCH);
+ DefInt(SQLITE_MISUSE);
+ DefInt(SQLITE_NOLFS);
+ DefInt(SQLITE_AUTH);
+ DefInt(SQLITE_FORMAT);
+ DefInt(SQLITE_RANGE);
+ DefInt(SQLITE_NOTADB);
+ DefInt(SQLITE_NOTICE);
+ DefInt(SQLITE_WARNING);
+ DefInt(SQLITE_ROW);
+ DefInt(SQLITE_DONE);
+ // Extended Result Codes
+ DefInt(SQLITE_ERROR_MISSING_COLLSEQ);
+ DefInt(SQLITE_ERROR_RETRY);
+ DefInt(SQLITE_ERROR_SNAPSHOT);
+ DefInt(SQLITE_IOERR_READ);
+ DefInt(SQLITE_IOERR_SHORT_READ);
+ DefInt(SQLITE_IOERR_WRITE);
+ DefInt(SQLITE_IOERR_FSYNC);
+ DefInt(SQLITE_IOERR_DIR_FSYNC);
+ DefInt(SQLITE_IOERR_TRUNCATE);
+ DefInt(SQLITE_IOERR_FSTAT);
+ DefInt(SQLITE_IOERR_UNLOCK);
+ DefInt(SQLITE_IOERR_RDLOCK);
+ DefInt(SQLITE_IOERR_DELETE);
+ DefInt(SQLITE_IOERR_BLOCKED);
+ DefInt(SQLITE_IOERR_NOMEM);
+ DefInt(SQLITE_IOERR_ACCESS);
+ DefInt(SQLITE_IOERR_CHECKRESERVEDLOCK);
+ DefInt(SQLITE_IOERR_LOCK);
+ DefInt(SQLITE_IOERR_CLOSE);
+ DefInt(SQLITE_IOERR_DIR_CLOSE);
+ DefInt(SQLITE_IOERR_SHMOPEN);
+ DefInt(SQLITE_IOERR_SHMSIZE);
+ DefInt(SQLITE_IOERR_SHMLOCK);
+ DefInt(SQLITE_IOERR_SHMMAP);
+ DefInt(SQLITE_IOERR_SEEK);
+ DefInt(SQLITE_IOERR_DELETE_NOENT);
+ DefInt(SQLITE_IOERR_MMAP);
+ DefInt(SQLITE_IOERR_GETTEMPPATH);
+ DefInt(SQLITE_IOERR_CONVPATH);
+ DefInt(SQLITE_IOERR_VNODE);
+ DefInt(SQLITE_IOERR_AUTH);
+ DefInt(SQLITE_IOERR_BEGIN_ATOMIC);
+ DefInt(SQLITE_IOERR_COMMIT_ATOMIC);
+ DefInt(SQLITE_IOERR_ROLLBACK_ATOMIC);
+ DefInt(SQLITE_IOERR_DATA);
+ DefInt(SQLITE_IOERR_CORRUPTFS);
+ DefInt(SQLITE_LOCKED_SHAREDCACHE);
+ DefInt(SQLITE_LOCKED_VTAB);
+ DefInt(SQLITE_BUSY_RECOVERY);
+ DefInt(SQLITE_BUSY_SNAPSHOT);
+ DefInt(SQLITE_BUSY_TIMEOUT);
+ DefInt(SQLITE_CANTOPEN_NOTEMPDIR);
+ DefInt(SQLITE_CANTOPEN_ISDIR);
+ DefInt(SQLITE_CANTOPEN_FULLPATH);
+ DefInt(SQLITE_CANTOPEN_CONVPATH);
+ //DefInt(SQLITE_CANTOPEN_DIRTYWAL)/*docs say not used*/;
+ DefInt(SQLITE_CANTOPEN_SYMLINK);
+ DefInt(SQLITE_CORRUPT_VTAB);
+ DefInt(SQLITE_CORRUPT_SEQUENCE);
+ DefInt(SQLITE_CORRUPT_INDEX);
+ DefInt(SQLITE_READONLY_RECOVERY);
+ DefInt(SQLITE_READONLY_CANTLOCK);
+ DefInt(SQLITE_READONLY_ROLLBACK);
+ DefInt(SQLITE_READONLY_DBMOVED);
+ DefInt(SQLITE_READONLY_CANTINIT);
+ DefInt(SQLITE_READONLY_DIRECTORY);
+ DefInt(SQLITE_ABORT_ROLLBACK);
+ DefInt(SQLITE_CONSTRAINT_CHECK);
+ DefInt(SQLITE_CONSTRAINT_COMMITHOOK);
+ DefInt(SQLITE_CONSTRAINT_FOREIGNKEY);
+ DefInt(SQLITE_CONSTRAINT_FUNCTION);
+ DefInt(SQLITE_CONSTRAINT_NOTNULL);
+ DefInt(SQLITE_CONSTRAINT_PRIMARYKEY);
+ DefInt(SQLITE_CONSTRAINT_TRIGGER);
+ DefInt(SQLITE_CONSTRAINT_UNIQUE);
+ DefInt(SQLITE_CONSTRAINT_VTAB);
+ DefInt(SQLITE_CONSTRAINT_ROWID);
+ DefInt(SQLITE_CONSTRAINT_PINNED);
+ DefInt(SQLITE_CONSTRAINT_DATATYPE);
+ DefInt(SQLITE_NOTICE_RECOVER_WAL);
+ DefInt(SQLITE_NOTICE_RECOVER_ROLLBACK);
+ DefInt(SQLITE_WARNING_AUTOINDEX);
+ DefInt(SQLITE_AUTH_USER);
+ DefInt(SQLITE_OK_LOAD_PERMANENTLY);
+ //DefInt(SQLITE_OK_SYMLINK) /* internal use only */;
+ } _DefGroup;
+
+ DefGroup(serialize){
+ DefInt(SQLITE_SERIALIZE_NOCOPY);
+ DefInt(SQLITE_DESERIALIZE_FREEONCLOSE);
+ DefInt(SQLITE_DESERIALIZE_READONLY);
+ DefInt(SQLITE_DESERIALIZE_RESIZEABLE);
+ } _DefGroup;
+
+ DefGroup(syncFlags) {
+ DefInt(SQLITE_SYNC_NORMAL);
+ DefInt(SQLITE_SYNC_FULL);
+ DefInt(SQLITE_SYNC_DATAONLY);
+ } _DefGroup;
+
+ DefGroup(trace) {
+ DefInt(SQLITE_TRACE_STMT);
+ DefInt(SQLITE_TRACE_PROFILE);
+ DefInt(SQLITE_TRACE_ROW);
+ DefInt(SQLITE_TRACE_CLOSE);
+ } _DefGroup;
+
+ DefGroup(udfFlags) {
+ DefInt(SQLITE_DETERMINISTIC);
+ DefInt(SQLITE_DIRECTONLY);
+ DefInt(SQLITE_INNOCUOUS);
+ } _DefGroup;
+
+ DefGroup(version) {
+ DefInt(SQLITE_VERSION_NUMBER);
+ DefStr(SQLITE_VERSION);
+ DefStr(SQLITE_SOURCE_ID);
+ } _DefGroup;
+
+#undef DefGroup
+#undef DefStr
+#undef DefInt
+#undef _DefGroup
+
+ /*
+ ** Emit an array of "StructBinder" struct descripions, which look
+ ** like:
+ **
+ ** {
+ ** "name": "MyStruct",
+ ** "sizeof": 16,
+ ** "members": {
+ ** "member1": {"offset": 0,"sizeof": 4,"signature": "i"},
+ ** "member2": {"offset": 4,"sizeof": 4,"signature": "p"},
+ ** "member3": {"offset": 8,"sizeof": 8,"signature": "j"}
+ ** }
+ ** }
+ **
+ ** Detailed documentation for those bits are in the docs for the
+ ** Jaccwabyt JS-side component.
+ */
+
+ /** Macros for emitting StructBinder description. */
+#define StructBinder__(TYPE) \
+ n = 0; \
+ outf("%s{", (nStruct++ ? ", " : "")); \
+ out("\"name\": \"" # TYPE "\","); \
+ outf("\"sizeof\": %d", (int)sizeof(TYPE)); \
+ out(",\"members\": {");
+#define StructBinder_(T) StructBinder__(T)
+ /** ^^^ indirection needed to expand CurrentStruct */
+#define StructBinder StructBinder_(CurrentStruct)
+#define _StructBinder CloseBrace(2)
+#define M(MEMBER,SIG) \
+ outf("%s\"%s\": " \
+ "{\"offset\":%d,\"sizeof\": %d,\"signature\":\"%s\"}", \
+ (n++ ? ", " : ""), #MEMBER, \
+ (int)offsetof(CurrentStruct,MEMBER), \
+ (int)sizeof(((CurrentStruct*)0)->MEMBER), \
+ SIG)
+
+ nStruct = 0;
+ out(", \"structs\": ["); {
+
+#define CurrentStruct sqlite3_vfs
+ StructBinder {
+ M(iVersion,"i");
+ M(szOsFile,"i");
+ M(mxPathname,"i");
+ M(pNext,"p");
+ M(zName,"s");
+ M(pAppData,"p");
+ M(xOpen,"i(pppip)");
+ M(xDelete,"i(ppi)");
+ M(xAccess,"i(ppip)");
+ M(xFullPathname,"i(ppip)");
+ M(xDlOpen,"p(pp)");
+ M(xDlError,"p(pip)");
+ M(xDlSym,"p()");
+ M(xDlClose,"v(pp)");
+ M(xRandomness,"i(pip)");
+ M(xSleep,"i(pi)");
+ M(xCurrentTime,"i(pp)");
+ M(xGetLastError,"i(pip)");
+ M(xCurrentTimeInt64,"i(pp)");
+ M(xSetSystemCall,"i(ppp)");
+ M(xGetSystemCall,"p(pp)");
+ M(xNextSystemCall,"p(pp)");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_io_methods
+ StructBinder {
+ M(iVersion,"i");
+ M(xClose,"i(p)");
+ M(xRead,"i(ppij)");
+ M(xWrite,"i(ppij)");
+ M(xTruncate,"i(pj)");
+ M(xSync,"i(pi)");
+ M(xFileSize,"i(pp)");
+ M(xLock,"i(pi)");
+ M(xUnlock,"i(pi)");
+ M(xCheckReservedLock,"i(pp)");
+ M(xFileControl,"i(pip)");
+ M(xSectorSize,"i(p)");
+ M(xDeviceCharacteristics,"i(p)");
+ M(xShmMap,"i(piiip)");
+ M(xShmLock,"i(piii)");
+ M(xShmBarrier,"v(p)");
+ M(xShmUnmap,"i(pi)");
+ M(xFetch,"i(pjip)");
+ M(xUnfetch,"i(pjp)");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_file
+ StructBinder {
+ M(pMethods,"p");
+ } _StructBinder;
+#undef CurrentStruct
+
+#define CurrentStruct sqlite3_kvvfs_methods
+ StructBinder {
+ M(xRead,"i(sspi)");
+ M(xWrite,"i(sss)");
+ M(xDelete,"i(ss)");
+ M(nKeySize,"i");
+ } _StructBinder;
+#undef CurrentStruct
+
+#if SQLITE_WASM_TESTS
+#define CurrentStruct WasmTestStruct
+ StructBinder {
+ M(v4,"i");
+ M(cstr,"s");
+ M(ppV,"p");
+ M(v8,"j");
+ M(xFunc,"v(p)");
+ } _StructBinder;
+#undef CurrentStruct
+#endif
+
+ } out( "]"/*structs*/);
+
+ out("}"/*top-level object*/);
+ *zPos = 0;
+ aBuffer[0] = '{'/*end of the race-condition workaround*/;
+ return aBuffer;
+#undef StructBinder
+#undef StructBinder_
+#undef StructBinder__
+#undef M
+#undef _StructBinder
+#undef CloseBrace
+#undef out
+#undef outf
+#undef lenCheck
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** This function invokes the xDelete method of the given VFS (or the
+** default VFS if pVfs is NULL), passing on the given filename. If
+** zName is NULL, no default VFS is found, or it has no xDelete
+** method, SQLITE_MISUSE is returned, else the result of the xDelete()
+** call is returned.
+*/
+SQLITE_WASM_KEEP
+int sqlite3_wasm_vfs_unlink(sqlite3_vfs *pVfs, const char * zName){
+ int rc = SQLITE_MISUSE /* ??? */;
+ if( 0==pVfs && 0!=zName ) pVfs = sqlite3_vfs_find(0);
+ if( zName && pVfs && pVfs->xDelete ){
+ rc = pVfs->xDelete(pVfs, zName, 1);
+ }
+ return rc;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Returns a pointer to the given DB's VFS for the given DB name,
+** defaulting to "main" if zDbName is 0. Returns 0 if no db with the
+** given name is open.
+*/
+SQLITE_WASM_KEEP
+sqlite3_vfs * sqlite3_wasm_db_vfs(sqlite3 *pDb, const char *zDbName){
+ sqlite3_vfs * pVfs = 0;
+ sqlite3_file_control(pDb, zDbName ? zDbName : "main",
+ SQLITE_FCNTL_VFS_POINTER, &pVfs);
+ return pVfs;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** This function resets the given db pointer's database as described at
+**
+** https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase
+**
+** Returns 0 on success, an SQLITE_xxx code on error. Returns
+** SQLITE_MISUSE if pDb is NULL.
+*/
+SQLITE_WASM_KEEP
+int sqlite3_wasm_db_reset(sqlite3*pDb){
+ int rc = SQLITE_MISUSE;
+ if( pDb ){
+ rc = sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 1, 0);
+ if( 0==rc ) rc = sqlite3_exec(pDb, "VACUUM", 0, 0, 0);
+ sqlite3_db_config(pDb, SQLITE_DBCONFIG_RESET_DATABASE, 0, 0);
+ }
+ return rc;
+}
+
+/*
+** Uses the given database's VFS xRead to stream the db file's
+** contents out to the given callback. The callback gets a single
+** chunk of size n (its 2nd argument) on each call and must return 0
+** on success, non-0 on error. This function returns 0 on success,
+** SQLITE_NOTFOUND if no db is open, or propagates any other non-0
+** code from the callback. Note that this is not thread-friendly: it
+** expects that it will be the only thread reading the db file and
+** takes no measures to ensure that is the case.
+**
+** This implementation appears to work fine, but
+** sqlite3_wasm_db_serialize() is arguably the better way to achieve
+** this.
+*/
+SQLITE_WASM_KEEP
+int sqlite3_wasm_db_export_chunked( sqlite3* pDb,
+ int (*xCallback)(unsigned const char *zOut, int n) ){
+ sqlite3_int64 nSize = 0;
+ sqlite3_int64 nPos = 0;
+ sqlite3_file * pFile = 0;
+ unsigned char buf[1024 * 8];
+ int nBuf = (int)sizeof(buf);
+ int rc = pDb
+ ? sqlite3_file_control(pDb, "main",
+ SQLITE_FCNTL_FILE_POINTER, &pFile)
+ : SQLITE_NOTFOUND;
+ if( rc ) return rc;
+ rc = pFile->pMethods->xFileSize(pFile, &nSize);
+ if( rc ) return rc;
+ if(nSize % nBuf){
+ /* DB size is not an even multiple of the buffer size. Reduce
+ ** buffer size so that we do not unduly inflate the db size
+ ** with zero-padding when exporting. */
+ if(0 == nSize % 4096) nBuf = 4096;
+ else if(0 == nSize % 2048) nBuf = 2048;
+ else if(0 == nSize % 1024) nBuf = 1024;
+ else nBuf = 512;
+ }
+ for( ; 0==rc && nPos<nSize; nPos += nBuf ){
+ rc = pFile->pMethods->xRead(pFile, buf, nBuf, nPos);
+ if(SQLITE_IOERR_SHORT_READ == rc){
+ rc = (nPos + nBuf) < nSize ? rc : 0/*assume EOF*/;
+ }
+ if( 0==rc ) rc = xCallback(buf, nBuf);
+ }
+ return rc;
+}
+
+/*
+** A proxy for sqlite3_serialize() which serializes the "main" schema
+** of pDb, placing the serialized output in pOut and nOut. nOut may be
+** NULL. If pDb or pOut are NULL then SQLITE_MISUSE is returned. If
+** allocation of the serialized copy fails, SQLITE_NOMEM is returned.
+** On success, 0 is returned and `*pOut` will contain a pointer to the
+** memory unless mFlags includes SQLITE_SERIALIZE_NOCOPY and the
+** database has no contiguous memory representation, in which case
+** `*pOut` will be NULL but 0 will be returned.
+**
+** If `*pOut` is not NULL, the caller is responsible for passing it to
+** sqlite3_free() to free it.
+*/
+SQLITE_WASM_KEEP
+int sqlite3_wasm_db_serialize( sqlite3 *pDb, unsigned char **pOut,
+ sqlite3_int64 *nOut, unsigned int mFlags ){
+ unsigned char * z;
+ if( !pDb || !pOut ) return SQLITE_MISUSE;
+ if(nOut) *nOut = 0;
+ z = sqlite3_serialize(pDb, "main", nOut, mFlags);
+ if( z || (SQLITE_SERIALIZE_NOCOPY & mFlags) ){
+ *pOut = z;
+ return 0;
+ }else{
+ return SQLITE_NOMEM;
+ }
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Creates a new file using the I/O API of the given VFS, containing
+** the given number of bytes of the given data. If the file exists,
+** it is truncated to the given length and populated with the given
+** data.
+**
+** This function exists so that we can implement the equivalent of
+** Emscripten's FS.createDataFile() in a VFS-agnostic way. This
+** functionality is intended for use in uploading database files.
+**
+** If pVfs is NULL, sqlite3_vfs_find(0) is used.
+**
+** If zFile is NULL, pVfs is NULL (and sqlite3_vfs_find(0) returns
+** NULL), or nData is negative, SQLITE_MISUSE are returned.
+**
+** On success, it creates a new file with the given name, populated
+** with the fist nData bytes of pData. If pData is NULL, the file is
+** created and/or truncated to nData bytes.
+**
+** Whether or not directory components of zFilename are created
+** automatically or not is unspecified: that detail is left to the
+** VFS. The "opfs" VFS, for example, create them.
+**
+** Not all VFSes support this functionality, e.g. the "kvvfs" does
+** not.
+**
+** If an error happens while populating or truncating the file, the
+** target file will be deleted (if needed) if this function created
+** it. If this function did not create it, it is not deleted but may
+** be left in an undefined state.
+**
+** Returns 0 on success. On error, it returns a code described above
+** or propagates a code from one of the I/O methods.
+**
+** Design note: nData is an integer, instead of int64, for WASM
+** portability, so that the API can still work in builds where BigInt
+** support is disabled or unavailable.
+*/
+SQLITE_WASM_KEEP
+int sqlite3_wasm_vfs_create_file( sqlite3_vfs *pVfs,
+ const char *zFilename,
+ const unsigned char * pData,
+ int nData ){
+ int rc;
+ sqlite3_file *pFile = 0;
+ sqlite3_io_methods const *pIo;
+ const int openFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
+ int flagsOut = 0;
+ int fileExisted = 0;
+ int doUnlock = 0;
+ const unsigned char *pPos = pData;
+ const int blockSize = 512
+ /* Because we are using pFile->pMethods->xWrite() for writing, and
+ ** it may have a buffer limit related to sqlite3's pager size, we
+ ** conservatively write in 512-byte blocks (smallest page
+ ** size). */;
+
+ if( !pVfs ) pVfs = sqlite3_vfs_find(0);
+ if( !pVfs || !zFilename || nData<0 ) return SQLITE_MISUSE;
+ pVfs->xAccess(pVfs, zFilename, SQLITE_ACCESS_EXISTS, &fileExisted);
+ rc = sqlite3OsOpenMalloc(pVfs, zFilename, &pFile, openFlags, &flagsOut);
+ if(rc) return rc;
+ pIo = pFile->pMethods;
+ if( pIo->xLock ) {
+ /* We need xLock() in order to accommodate the OPFS VFS, as it
+ ** obtains a writeable handle via the lock operation and releases
+ ** it in xUnlock(). If we don't do those here, we have to add code
+ ** to the VFS to account check whether it was locked before
+ ** xFileSize(), xTruncate(), and the like, and release the lock
+ ** only if it was unlocked when the op was started. */
+ rc = pIo->xLock(pFile, SQLITE_LOCK_EXCLUSIVE);
+ doUnlock = 0==rc;
+ }
+ if( 0==rc) rc = pIo->xTruncate(pFile, nData);
+ if( 0==rc && 0!=pData && nData>0 ){
+ while( 0==rc && nData>0 ){
+ const int n = nData>=blockSize ? blockSize : nData;
+ rc = pIo->xWrite(pFile, pPos, n, (sqlite3_int64)(pPos - pData));
+ nData -= n;
+ pPos += n;
+ }
+ if( 0==rc && nData>0 ){
+ assert( nData<blockSize );
+ rc = pIo->xWrite(pFile, pPos, nData, (sqlite3_int64)(pPos - pData));
+ }
+ }
+ if( pIo->xUnlock && doUnlock!=0 ) pIo->xUnlock(pFile, SQLITE_LOCK_NONE);
+ pIo->xClose(pFile);
+ if( rc!=0 && 0==fileExisted ){
+ pVfs->xDelete(pVfs, zFilename, 1);
+ }
+ return rc;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Allocates sqlite3KvvfsMethods.nKeySize bytes from
+** sqlite3_wasm_pstack_alloc() and returns 0 if that allocation fails,
+** else it passes that string to kvstorageMakeKey() and returns a
+** NUL-terminated pointer to that string. It is up to the caller to
+** use sqlite3_wasm_pstack_restore() to free the returned pointer.
+*/
+SQLITE_WASM_KEEP
+char * sqlite3_wasm_kvvfsMakeKeyOnPstack(const char *zClass,
+ const char *zKeyIn){
+ assert(sqlite3KvvfsMethods.nKeySize>24);
+ char *zKeyOut =
+ (char *)sqlite3_wasm_pstack_alloc(sqlite3KvvfsMethods.nKeySize);
+ if(zKeyOut){
+ kvstorageMakeKey(zClass, zKeyIn, zKeyOut);
+ }
+ return zKeyOut;
+}
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings.
+**
+** Returns the pointer to the singleton object which holds the kvvfs
+** I/O methods and associated state.
+*/
+SQLITE_WASM_KEEP
+sqlite3_kvvfs_methods * sqlite3_wasm_kvvfs_methods(void){
+ return &sqlite3KvvfsMethods;
+}
+
+#if defined(__EMSCRIPTEN__) && defined(SQLITE_ENABLE_WASMFS)
+#include <emscripten/wasmfs.h>
+
+/*
+** This function is NOT part of the sqlite3 public API. It is strictly
+** for use by the sqlite project's own JS/WASM bindings, specifically
+** only when building with Emscripten's WASMFS support.
+**
+** This function should only be called if the JS side detects the
+** existence of the Origin-Private FileSystem (OPFS) APIs in the
+** client. The first time it is called, this function instantiates a
+** WASMFS backend impl for OPFS. On success, subsequent calls are
+** no-ops.
+**
+** This function may be passed a "mount point" name, which must have a
+** leading "/" and is currently restricted to a single path component,
+** e.g. "/foo" is legal but "/foo/" and "/foo/bar" are not. If it is
+** NULL or empty, it defaults to "/opfs".
+**
+** Returns 0 on success, SQLITE_NOMEM if instantiation of the backend
+** object fails, SQLITE_IOERR if mkdir() of the zMountPoint dir in
+** the virtual FS fails. In builds compiled without SQLITE_ENABLE_WASMFS
+** defined, SQLITE_NOTFOUND is returned without side effects.
+*/
+SQLITE_WASM_KEEP
+int sqlite3_wasm_init_wasmfs(const char *zMountPoint){
+ static backend_t pOpfs = 0;
+ if( !zMountPoint || !*zMountPoint ) zMountPoint = "/opfs";
+ if( !pOpfs ){
+ pOpfs = wasmfs_create_opfs_backend();
+ }
+ /** It's not enough to instantiate the backend. We have to create a
+ mountpoint in the VFS and attach the backend to it. */
+ if( pOpfs && 0!=access(zMountPoint, F_OK) ){
+ /* Note that this check and is not robust but it will
+ hypothetically suffice for the transient wasm-based virtual
+ filesystem we're currently running in. */
+ const int rc = wasmfs_create_directory(zMountPoint, 0777, pOpfs);
+ /*emscripten_console_logf("OPFS mkdir(%s) rc=%d", zMountPoint, rc);*/
+ if(rc) return SQLITE_IOERR;
+ }
+ return pOpfs ? 0 : SQLITE_NOMEM;
+}
+#else
+SQLITE_WASM_KEEP
+int sqlite3_wasm_init_wasmfs(const char *zUnused){
+ //emscripten_console_warn("WASMFS OPFS is not compiled in.");
+ if(zUnused){/*unused*/}
+ return SQLITE_NOTFOUND;
+}
+#endif /* __EMSCRIPTEN__ && SQLITE_ENABLE_WASMFS */
+
+#if SQLITE_WASM_TESTS
+
+SQLITE_WASM_KEEP
+int sqlite3_wasm_test_intptr(int * p){
+ return *p = *p * 2;
+}
+
+SQLITE_WASM_KEEP
+int64_t sqlite3_wasm_test_int64_max(void){
+ return (int64_t)0x7fffffffffffffff;
+}
+
+SQLITE_WASM_KEEP
+int64_t sqlite3_wasm_test_int64_min(void){
+ return ~sqlite3_wasm_test_int64_max();
+}
+
+SQLITE_WASM_KEEP
+int64_t sqlite3_wasm_test_int64_times2(int64_t x){
+ return x * 2;
+}
+
+SQLITE_WASM_KEEP
+void sqlite3_wasm_test_int64_minmax(int64_t * min, int64_t *max){
+ *max = sqlite3_wasm_test_int64_max();
+ *min = sqlite3_wasm_test_int64_min();
+ /*printf("minmax: min=%lld, max=%lld\n", *min, *max);*/
+}
+
+SQLITE_WASM_KEEP
+int64_t sqlite3_wasm_test_int64ptr(int64_t * p){
+ /*printf("sqlite3_wasm_test_int64ptr( @%lld = 0x%llx )\n", (int64_t)p, *p);*/
+ return *p = *p * 2;
+}
+
+SQLITE_WASM_KEEP
+void sqlite3_wasm_test_stack_overflow(int recurse){
+ if(recurse) sqlite3_wasm_test_stack_overflow(recurse);
+}
+
+/* For testing the 'string-free' whwasmutil.xWrap() conversion. */
+SQLITE_WASM_KEEP
+char * sqlite3_wasm_test_str_hello(int fail){
+ char * s = fail ? 0 : (char *)malloc(6);
+ if(s){
+ memcpy(s, "hello", 5);
+ s[5] = 0;
+ }
+ return s;
+}
+#endif /* SQLITE_WASM_TESTS */
+
+#undef SQLITE_WASM_KEEP
diff --git a/ext/wasm/api/sqlite3-worker1-promiser.js b/ext/wasm/api/sqlite3-worker1-promiser.js
new file mode 100644
index 0000000..7360512
--- /dev/null
+++ b/ext/wasm/api/sqlite3-worker1-promiser.js
@@ -0,0 +1,259 @@
+/*
+ 2022-08-24
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file implements a Promise-based proxy for the sqlite3 Worker
+ API #1. It is intended to be included either from the main thread or
+ a Worker, but only if (A) the environment supports nested Workers
+ and (B) it's _not_ a Worker which loads the sqlite3 WASM/JS
+ module. This file's features will load that module and provide a
+ slightly simpler client-side interface than the slightly-lower-level
+ Worker API does.
+
+ This script necessarily exposes one global symbol, but clients may
+ freely `delete` that symbol after calling it.
+*/
+'use strict';
+/**
+ Configures an sqlite3 Worker API #1 Worker such that it can be
+ manipulated via a Promise-based interface and returns a factory
+ function which returns Promises for communicating with the worker.
+ This proxy has an _almost_ identical interface to the normal
+ worker API, with any exceptions documented below.
+
+ It requires a configuration object with the following properties:
+
+ - `worker` (required): a Worker instance which loads
+ `sqlite3-worker1.js` or a functional equivalent. Note that the
+ promiser factory replaces the worker.onmessage property. This
+ config option may alternately be a function, in which case this
+ function re-assigns this property with the result of calling that
+ function, enabling delayed instantiation of a Worker.
+
+ - `onready` (optional, but...): this callback is called with no
+ arguments when the worker fires its initial
+ 'sqlite3-api'/'worker1-ready' message, which it does when
+ sqlite3.initWorker1API() completes its initialization. This is
+ the simplest way to tell the worker to kick off work at the
+ earliest opportunity.
+
+ - `onunhandled` (optional): a callback which gets passed the
+ message event object for any worker.onmessage() events which
+ are not handled by this proxy. Ideally that "should" never
+ happen, as this proxy aims to handle all known message types.
+
+ - `generateMessageId` (optional): a function which, when passed an
+ about-to-be-posted message object, generates a _unique_ message ID
+ for the message, which this API then assigns as the messageId
+ property of the message. It _must_ generate unique IDs on each call
+ so that dispatching can work. If not defined, a default generator
+ is used (which should be sufficient for most or all cases).
+
+ - `debug` (optional): a console.debug()-style function for logging
+ information about messages.
+
+ This function returns a stateful factory function with the
+ following interfaces:
+
+ - Promise function(messageType, messageArgs)
+ - Promise function({message object})
+
+ The first form expects the "type" and "args" values for a Worker
+ message. The second expects an object in the form {type:...,
+ args:...} plus any other properties the client cares to set. This
+ function will always set the `messageId` property on the object,
+ even if it's already set, and will set the `dbId` property to the
+ current database ID if it is _not_ set in the message object.
+
+ The function throws on error.
+
+ The function installs a temporary message listener, posts a
+ message to the configured Worker, and handles the message's
+ response via the temporary message listener. The then() callback
+ of the returned Promise is passed the `message.data` property from
+ the resulting message, i.e. the payload from the worker, stripped
+ of the lower-level event state which the onmessage() handler
+ receives.
+
+ Example usage:
+
+ ```
+ const config = {...};
+ const sq3Promiser = sqlite3Worker1Promiser(config);
+ sq3Promiser('open', {filename:"/foo.db"}).then(function(msg){
+ console.log("open response",msg); // => {type:'open', result: {filename:'/foo.db'}, ...}
+ });
+ sq3Promiser({type:'close'}).then((msg)=>{
+ console.log("close response",msg); // => {type:'close', result: {filename:'/foo.db'}, ...}
+ });
+ ```
+
+ Differences from Worker API #1:
+
+ - exec's {callback: STRING} option does not work via this
+ interface (it triggers an exception), but {callback: function}
+ does and works exactly like the STRING form does in the Worker:
+ the callback is called one time for each row of the result set,
+ passed the same worker message format as the worker API emits:
+
+ {type:typeString,
+ row:VALUE,
+ rowNumber:1-based-#,
+ columnNames: array}
+
+ Where `typeString` is an internally-synthesized message type string
+ used temporarily for worker message dispatching. It can be ignored
+ by all client code except that which tests this API. The `row`
+ property contains the row result in the form implied by the
+ `rowMode` option (defaulting to `'array'`). The `rowNumber` is a
+ 1-based integer value incremented by 1 on each call into th
+ callback.
+
+ At the end of the result set, the same event is fired with
+ (row=undefined, rowNumber=null) to indicate that
+ the end of the result set has been reached. Note that the rows
+ arrive via worker-posted messages, with all the implications
+ of that.
+*/
+self.sqlite3Worker1Promiser = function callee(config = callee.defaultConfig){
+ // Inspired by: https://stackoverflow.com/a/52439530
+ if(1===arguments.length && 'function'===typeof arguments[0]){
+ const f = config;
+ config = Object.assign(Object.create(null), callee.defaultConfig);
+ config.onready = f;
+ }else{
+ config = Object.assign(Object.create(null), callee.defaultConfig, config);
+ }
+ const handlerMap = Object.create(null);
+ const noop = function(){};
+ const err = config.onerror
+ || noop /* config.onerror is intentionally undocumented
+ pending finding a less ambiguous name */;
+ const debug = config.debug || noop;
+ const idTypeMap = config.generateMessageId ? undefined : Object.create(null);
+ const genMsgId = config.generateMessageId || function(msg){
+ return msg.type+'#'+(idTypeMap[msg.type] = (idTypeMap[msg.type]||0) + 1);
+ };
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ if(!config.worker) config.worker = callee.defaultConfig.worker;
+ if('function'===typeof config.worker) config.worker = config.worker();
+ let dbId;
+ config.worker.onmessage = function(ev){
+ ev = ev.data;
+ debug('worker1.onmessage',ev);
+ let msgHandler = handlerMap[ev.messageId];
+ if(!msgHandler){
+ if(ev && 'sqlite3-api'===ev.type && 'worker1-ready'===ev.result) {
+ /*fired one time when the Worker1 API initializes*/
+ if(config.onready) config.onready();
+ return;
+ }
+ msgHandler = handlerMap[ev.type] /* check for exec per-row callback */;
+ if(msgHandler && msgHandler.onrow){
+ msgHandler.onrow(ev);
+ return;
+ }
+ if(config.onunhandled) config.onunhandled(arguments[0]);
+ else err("sqlite3Worker1Promiser() unhandled worker message:",ev);
+ return;
+ }
+ delete handlerMap[ev.messageId];
+ switch(ev.type){
+ case 'error':
+ msgHandler.reject(ev);
+ return;
+ case 'open':
+ if(!dbId) dbId = ev.dbId;
+ break;
+ case 'close':
+ if(ev.dbId===dbId) dbId = undefined;
+ break;
+ default:
+ break;
+ }
+ try {msgHandler.resolve(ev)}
+ catch(e){msgHandler.reject(e)}
+ }/*worker.onmessage()*/;
+ return function(/*(msgType, msgArgs) || (msgEnvelope)*/){
+ let msg;
+ if(1===arguments.length){
+ msg = arguments[0];
+ }else if(2===arguments.length){
+ msg = {
+ type: arguments[0],
+ args: arguments[1]
+ };
+ }else{
+ toss("Invalid arugments for sqlite3Worker1Promiser()-created factory.");
+ }
+ if(!msg.dbId) msg.dbId = dbId;
+ msg.messageId = genMsgId(msg);
+ msg.departureTime = performance.now();
+ const proxy = Object.create(null);
+ proxy.message = msg;
+ let rowCallbackId /* message handler ID for exec on-row callback proxy */;
+ if('exec'===msg.type && msg.args){
+ if('function'===typeof msg.args.callback){
+ rowCallbackId = msg.messageId+':row';
+ proxy.onrow = msg.args.callback;
+ msg.args.callback = rowCallbackId;
+ handlerMap[rowCallbackId] = proxy;
+ }else if('string' === typeof msg.args.callback){
+ toss("exec callback may not be a string when using the Promise interface.");
+ /**
+ Design note: the reason for this limitation is that this
+ API takes over worker.onmessage() and the client has no way
+ of adding their own message-type handlers to it. Per-row
+ callbacks are implemented as short-lived message.type
+ mappings for worker.onmessage().
+
+ We "could" work around this by providing a new
+ config.fallbackMessageHandler (or some such) which contains
+ a map of event type names to callbacks. Seems like overkill
+ for now, seeing as the client can pass callback functions
+ to this interface (whereas the string-form "callback" is
+ needed for the over-the-Worker interface).
+ */
+ }
+ }
+ //debug("requestWork", msg);
+ let p = new Promise(function(resolve, reject){
+ proxy.resolve = resolve;
+ proxy.reject = reject;
+ handlerMap[msg.messageId] = proxy;
+ debug("Posting",msg.type,"message to Worker dbId="+(dbId||'default')+':',msg);
+ config.worker.postMessage(msg);
+ });
+ if(rowCallbackId) p = p.finally(()=>delete handlerMap[rowCallbackId]);
+ return p;
+ };
+}/*sqlite3Worker1Promiser()*/;
+self.sqlite3Worker1Promiser.defaultConfig = {
+ worker: function(){
+ let theJs = "sqlite3-worker1.js";
+ if(this.currentScript){
+ const src = this.currentScript.src.split('/');
+ src.pop();
+ theJs = src.join('/')+'/' + theJs;
+ //console.warn("promiser currentScript, theJs =",this.currentScript,theJs);
+ }else{
+ //console.warn("promiser self.location =",self.location);
+ const urlParams = new URL(self.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ theJs = urlParams.get('sqlite3.dir') + '/' + theJs;
+ }
+ }
+ return new Worker(theJs + self.location.search);
+ }.bind({
+ currentScript: self?.document?.currentScript
+ }),
+ onerror: (...args)=>console.error('worker1 promiser error',...args)
+};
diff --git a/ext/wasm/api/sqlite3-worker1.js b/ext/wasm/api/sqlite3-worker1.js
new file mode 100644
index 0000000..9424379
--- /dev/null
+++ b/ext/wasm/api/sqlite3-worker1.js
@@ -0,0 +1,49 @@
+/*
+ 2022-05-23
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This is a JS Worker file for the main sqlite3 api. It loads
+ sqlite3.js, initializes the module, and postMessage()'s a message
+ after the module is initialized:
+
+ {type: 'sqlite3-api', result: 'worker1-ready'}
+
+ This seemingly superfluous level of indirection is necessary when
+ loading sqlite3.js via a Worker. Instantiating a worker with new
+ Worker("sqlite.js") will not (cannot) call sqlite3InitModule() to
+ initialize the module due to a timing/order-of-operations conflict
+ (and that symbol is not exported in a way that a Worker loading it
+ that way can see it). Thus JS code wanting to load the sqlite3
+ Worker-specific API needs to pass _this_ file (or equivalent) to the
+ Worker constructor and then listen for an event in the form shown
+ above in order to know when the module has completed initialization.
+
+ This file accepts a URL arguments to adjust how it loads sqlite3.js:
+
+ - `sqlite3.dir`, if set, treats the given directory name as the
+ directory from which `sqlite3.js` will be loaded.
+*/
+"use strict";
+(()=>{
+ const urlParams = new URL(self.location.href).searchParams;
+ let theJs = 'sqlite3.js';
+ if(urlParams.has('sqlite3.dir')){
+ theJs = urlParams.get('sqlite3.dir') + '/' + theJs;
+ }
+ //console.warn("worker1 theJs =",theJs);
+ importScripts(theJs);
+ sqlite3InitModule().then((sqlite3)=>{
+ if(sqlite3.capi.sqlite3_wasmfs_opfs_dir){
+ sqlite3.capi.sqlite3_wasmfs_opfs_dir();
+ }
+ sqlite3.initWorker1API();
+ });
+})();
diff --git a/ext/wasm/batch-runner.html b/ext/wasm/batch-runner.html
new file mode 100644
index 0000000..5258f95
--- /dev/null
+++ b/ext/wasm/batch-runner.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3-api batch SQL runner</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3-api batch SQL runner</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <p>
+ This page is for batch-running extracts from the output
+ of <tt>speedtest1 --script</tt>, as well as other standalone SQL
+ scripts.
+ </p>
+ <p id='warn-list' class='warning'>ACHTUNG: this file requires a generated input list
+ file. Run "make batch" from this directory to generate it.
+ </p>
+ <p id='warn-opfs' class='warning hidden'>WARNING: if the WASMFS/OPFS layer crashes, this page may
+ become completely unresponsive and need to be closed and reloaded to recover.
+ </p>
+ <p id='warn-websql' class='warning hidden'>WARNING: WebSQL's limited API requires that
+ this app split up SQL batches into separate statements for execution. That will
+ only work so long as semicolon characters are <em>only</em> used to terminate
+ SQL statements, and not used within string literals or the like.
+ </p>
+ <hr>
+ <fieldset id='toolbar'>
+ <div>
+ <select class='disable-during-eval' id='sql-select'>
+ <option disabled selected>Populated via script code</option>
+ </select>
+ <button class='disable-during-eval' id='sql-run'>Run selected SQL</button>
+ <button class='disable-during-eval' id='sql-run-next'>Run next...</button>
+ <button class='disable-during-eval' id='sql-run-remaining'>Run all remaining...</button>
+ <button class='disable-during-eval' id='export-metrics' disabled>Export metrics (WIP)<br>(broken by refactoring)</button>
+ <button class='disable-during-eval' id='db-reset'>Reset db</button>
+ <button id='output-clear'>Clear output</button>
+ <span class='input-wrapper flex-col'>
+ <label for='select-impl'>Storage impl:</label>
+ <select id='select-impl'>
+ <option value='virtualfs'>Virtual filesystem</option>
+ <option value='memdb'>:memory:</option>
+ <option value='wasmfs-opfs'>WASMFS OPFS</option>
+ <option value='websql'>WebSQL</option>
+ </select>
+ </span>
+ </fieldset>
+ </div>
+ <hr>
+ <span class='input-wrapper'>
+ <input type='checkbox' class='disable-during-eval' id='cb-reverse-log-order' checked></input>
+ <label for='cb-reverse-log-order'>Reverse log order (newest first)</label>
+ </span>
+ <div id='test-output'></div>
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="batch-runner.js"></script>
+ <style>
+ .flex-col {
+ display: flex;
+ flex-direction: column;
+ }
+ #toolbar > div {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+ #toolbar > div > * {
+ margin: 0.25em;
+ }
+ </style>
+ </body>
+</html>
diff --git a/ext/wasm/batch-runner.js b/ext/wasm/batch-runner.js
new file mode 100644
index 0000000..11c4321
--- /dev/null
+++ b/ext/wasm/batch-runner.js
@@ -0,0 +1,588 @@
+/*
+ 2022-08-29
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A basic batch SQL runner for sqlite3-api.js. This file must be run in
+ main JS thread and sqlite3.js must have been loaded before it.
+*/
+'use strict';
+(function(){
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const warn = console.warn.bind(console);
+ let sqlite3;
+ const urlParams = new URL(self.location.href).searchParams;
+ const cacheSize = (()=>{
+ if(urlParams.has('cachesize')) return +urlParams.get('cachesize');
+ return 200;
+ })();
+
+ /** Throws if the given sqlite3 result code is not 0. */
+ const checkSqliteRc = (dbh,rc)=>{
+ if(rc) toss("Prepare failed:",sqlite3.capi.sqlite3_errmsg(dbh));
+ };
+
+ const sqlToDrop = [
+ "SELECT type,name FROM sqlite_schema ",
+ "WHERE name NOT LIKE 'sqlite\\_%' escape '\\' ",
+ "AND name NOT LIKE '\\_%' escape '\\'"
+ ].join('');
+
+ const clearDbWebSQL = function(db){
+ db.handle.transaction(function(tx){
+ const onErr = (e)=>console.error(e);
+ const callback = function(tx, result){
+ const rows = result.rows;
+ let i, n;
+ i = n = rows.length;
+ while(i--){
+ const row = rows.item(i);
+ const name = JSON.stringify(row.name);
+ const type = row.type;
+ switch(type){
+ case 'index': case 'table':
+ case 'trigger': case 'view': {
+ const sql2 = 'DROP '+type+' '+name;
+ tx.executeSql(sql2, [], ()=>{}, onErr);
+ break;
+ }
+ default:
+ warn("Unhandled db entry type:",type,'name =',name);
+ break;
+ }
+ }
+ };
+ tx.executeSql(sqlToDrop, [], callback, onErr);
+ db.handle.changeVersion(db.handle.version, "", ()=>{}, onErr, ()=>{});
+ });
+ };
+
+ const clearDbSqlite = function(db){
+ // This would be SO much easier with the oo1 API, but we specifically want to
+ // inject metrics we can't get via that API, and we cannot reliably (OPFS)
+ // open the same DB twice to clear it using that API, so...
+ const rc = sqlite3.wasm.exports.sqlite3_wasm_db_reset(db.handle);
+ App.logHtml("reset db rc =",rc,db.id, db.filename);
+ };
+
+
+ const E = (s)=>document.querySelector(s);
+ const App = {
+ e: {
+ output: E('#test-output'),
+ selSql: E('#sql-select'),
+ btnRun: E('#sql-run'),
+ btnRunNext: E('#sql-run-next'),
+ btnRunRemaining: E('#sql-run-remaining'),
+ btnExportMetrics: E('#export-metrics'),
+ btnClear: E('#output-clear'),
+ btnReset: E('#db-reset'),
+ cbReverseLog: E('#cb-reverse-log-order'),
+ selImpl: E('#select-impl'),
+ fsToolbar: E('#toolbar')
+ },
+ db: Object.create(null),
+ dbs: Object.create(null),
+ cache:{},
+ log: console.log.bind(console),
+ warn: console.warn.bind(console),
+ cls: function(){this.e.output.innerHTML = ''},
+ logHtml2: function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ this.e.output.append(ln);
+ //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
+ },
+ logHtml: function(...args){
+ console.log(...args);
+ if(1) this.logHtml2('', ...args);
+ },
+ logErr: function(...args){
+ console.error(...args);
+ if(1) this.logHtml2('error', ...args);
+ },
+
+ execSql: async function(name,sql){
+ const db = this.getSelectedDb();
+ const banner = "========================================";
+ this.logHtml(banner,
+ "Running",name,'('+sql.length,'bytes) using',db.id);
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ let pStmt = 0, pSqlBegin;
+ const stack = wasm.scopedAllocPush();
+ const metrics = db.metrics = Object.create(null);
+ metrics.prepTotal = metrics.stepTotal = 0;
+ metrics.stmtCount = 0;
+ metrics.malloc = 0;
+ metrics.strcpy = 0;
+ this.blockControls(true);
+ if(this.gotErr){
+ this.logErr("Cannot run SQL: error cleanup is pending.");
+ return;
+ }
+ // Run this async so that the UI can be updated for the above header...
+ const endRun = ()=>{
+ metrics.evalSqlEnd = performance.now();
+ metrics.evalTimeTotal = (metrics.evalSqlEnd - metrics.evalSqlStart);
+ this.logHtml(db.id,"metrics:",JSON.stringify(metrics, undefined, ' '));
+ this.logHtml("prepare() count:",metrics.stmtCount);
+ this.logHtml("Time in prepare_v2():",metrics.prepTotal,"ms",
+ "("+(metrics.prepTotal / metrics.stmtCount),"ms per prepare())");
+ this.logHtml("Time in step():",metrics.stepTotal,"ms",
+ "("+(metrics.stepTotal / metrics.stmtCount),"ms per step())");
+ this.logHtml("Total runtime:",metrics.evalTimeTotal,"ms");
+ this.logHtml("Overhead (time - prep - step):",
+ (metrics.evalTimeTotal - metrics.prepTotal - metrics.stepTotal)+"ms");
+ this.logHtml(banner,"End of",name);
+ };
+
+ let runner;
+ if('websql'===db.id){
+ const who = this;
+ runner = function(resolve, reject){
+ /* WebSQL cannot execute multiple statements, nor can it execute SQL without
+ an explicit transaction. Thus we have to do some fragile surgery on the
+ input SQL. Since we're only expecting carefully curated inputs, the hope is
+ that this will suffice. PS: it also can't run most SQL functions, e.g. even
+ instr() results in "not authorized". */
+ if('string'!==typeof sql){ // assume TypedArray
+ sql = new TextDecoder().decode(sql);
+ }
+ sql = sql.replace(/-- [^\n]+\n/g,''); // comment lines interfere with our split()
+ const sqls = sql.split(/;+\n/);
+ const rxBegin = /^BEGIN/i, rxCommit = /^COMMIT/i;
+ try {
+ const nextSql = ()=>{
+ let x = sqls.shift();
+ while(sqls.length && !x) x = sqls.shift();
+ return x && x.trim();
+ };
+ const who = this;
+ const transaction = function(tx){
+ try {
+ let s;
+ /* Try to approximate the spirit of the input scripts
+ by running batches bound by BEGIN/COMMIT statements. */
+ for(s = nextSql(); !!s; s = nextSql()){
+ if(rxBegin.test(s)) continue;
+ else if(rxCommit.test(s)) break;
+ //console.log("websql sql again",sqls.length, s);
+ ++metrics.stmtCount;
+ const t = performance.now();
+ tx.executeSql(s,[], ()=>{}, (t,e)=>{
+ console.error("WebSQL error",e,"SQL =",s);
+ who.logErr(e.message);
+ //throw e;
+ return false;
+ });
+ metrics.stepTotal += performance.now() - t;
+ }
+ }catch(e){
+ who.logErr("transaction():",e.message);
+ throw e;
+ }
+ };
+ const n = sqls.length;
+ const nextBatch = function(){
+ if(sqls.length){
+ console.log("websql sqls.length",sqls.length,'of',n);
+ db.handle.transaction(transaction, (e)=>{
+ who.logErr("Ignoring and contiuing:",e.message)
+ //reject(e);
+ return false;
+ }, nextBatch);
+ }else{
+ resolve(who);
+ }
+ };
+ metrics.evalSqlStart = performance.now();
+ nextBatch();
+ }catch(e){
+ //this.gotErr = e;
+ console.error("websql error:",e);
+ who.logErr(e.message);
+ //reject(e);
+ }
+ }.bind(this);
+ }else{/*sqlite3 db...*/
+ runner = function(resolve, reject){
+ metrics.evalSqlStart = performance.now();
+ try {
+ let t;
+ let sqlByteLen = sql.byteLength;
+ const [ppStmt, pzTail] = wasm.scopedAllocPtr(2);
+ t = performance.now();
+ pSqlBegin = wasm.scopedAlloc( sqlByteLen + 1/*SQL + NUL*/) || toss("alloc(",sqlByteLen,") failed");
+ metrics.malloc = performance.now() - t;
+ metrics.byteLength = sqlByteLen;
+ let pSql = pSqlBegin;
+ const pSqlEnd = pSqlBegin + sqlByteLen;
+ t = performance.now();
+ wasm.heap8().set(sql, pSql);
+ wasm.setMemValue(pSql + sqlByteLen, 0);
+ metrics.strcpy = performance.now() - t;
+ let breaker = 0;
+ while(pSql && wasm.getMemValue(pSql,'i8')){
+ wasm.setPtrValue(ppStmt, 0);
+ wasm.setPtrValue(pzTail, 0);
+ t = performance.now();
+ let rc = capi.sqlite3_prepare_v3(
+ db.handle, pSql, sqlByteLen, 0, ppStmt, pzTail
+ );
+ metrics.prepTotal += performance.now() - t;
+ checkSqliteRc(db.handle, rc);
+ pStmt = wasm.getPtrValue(ppStmt);
+ pSql = wasm.getPtrValue(pzTail);
+ sqlByteLen = pSqlEnd - pSql;
+ if(!pStmt) continue/*empty statement*/;
+ ++metrics.stmtCount;
+ t = performance.now();
+ rc = capi.sqlite3_step(pStmt);
+ capi.sqlite3_finalize(pStmt);
+ pStmt = 0;
+ metrics.stepTotal += performance.now() - t;
+ switch(rc){
+ case capi.SQLITE_ROW:
+ case capi.SQLITE_DONE: break;
+ default: checkSqliteRc(db.handle, rc); toss("Not reached.");
+ }
+ }
+ resolve(this);
+ }catch(e){
+ if(pStmt) capi.sqlite3_finalize(pStmt);
+ //this.gotErr = e;
+ reject(e);
+ }finally{
+ capi.sqlite3_exec(db.handle,"rollback;",0,0,0);
+ wasm.scopedAllocPop(stack);
+ }
+ }.bind(this);
+ }
+ let p;
+ if(1){
+ p = new Promise(function(res,rej){
+ setTimeout(()=>runner(res, rej), 50)/*give UI a chance to output the "running" banner*/;
+ });
+ }else{
+ p = new Promise(runner);
+ }
+ return p.catch(
+ (e)=>this.logErr("Error via execSql("+name+",...):",e.message)
+ ).finally(()=>{
+ endRun();
+ this.blockControls(false);
+ });
+ },
+
+ clearDb: function(){
+ const db = this.getSelectedDb();
+ if('websql'===db.id){
+ this.logErr("TODO: clear websql db.");
+ return;
+ }
+ if(!db.handle) return;
+ const capi = this.sqlite3, wasm = this.sqlite3.wasm;
+ //const scope = wasm.scopedAllocPush(
+ this.logErr("TODO: clear db");
+ },
+
+ /**
+ Loads batch-runner.list and populates the selection list from
+ it. Returns a promise which resolves to nothing in particular
+ when it completes. Only intended to be run once at the start
+ of the app.
+ */
+ loadSqlList: async function(){
+ const sel = this.e.selSql;
+ sel.innerHTML = '';
+ this.blockControls(true);
+ const infile = 'batch-runner.list';
+ this.logHtml("Loading list of SQL files:", infile);
+ let txt;
+ try{
+ const r = await fetch(infile);
+ if(404 === r.status){
+ toss("Missing file '"+infile+"'.");
+ }
+ if(!r.ok) toss("Loading",infile,"failed:",r.statusText);
+ txt = await r.text();
+ const warning = E('#warn-list');
+ if(warning) warning.remove();
+ }catch(e){
+ this.logErr(e.message);
+ throw e;
+ }finally{
+ this.blockControls(false);
+ }
+ const list = txt.split(/\n+/);
+ let opt;
+ if(0){
+ opt = document.createElement('option');
+ opt.innerText = "Select file to evaluate...";
+ opt.value = '';
+ opt.disabled = true;
+ opt.selected = true;
+ sel.appendChild(opt);
+ }
+ list.forEach(function(fn){
+ if(!fn) return;
+ opt = document.createElement('option');
+ opt.value = fn;
+ opt.innerText = fn.split('/').pop();
+ sel.appendChild(opt);
+ });
+ this.logHtml("Loaded",infile);
+ },
+
+ /** Fetch ./fn and return its contents as a Uint8Array. */
+ fetchFile: async function(fn, cacheIt=false){
+ if(cacheIt && this.cache[fn]) return this.cache[fn];
+ this.logHtml("Fetching",fn,"...");
+ let sql;
+ try {
+ const r = await fetch(fn);
+ if(!r.ok) toss("Fetch failed:",r.statusText);
+ sql = new Uint8Array(await r.arrayBuffer());
+ }catch(e){
+ this.logErr(e.message);
+ throw e;
+ }
+ this.logHtml("Fetched",sql.length,"bytes from",fn);
+ if(cacheIt) this.cache[fn] = sql;
+ return sql;
+ }/*fetchFile()*/,
+
+ /** Disable or enable certain UI controls. */
+ blockControls: function(disable){
+ //document.querySelectorAll('.disable-during-eval').forEach((e)=>e.disabled = disable);
+ this.e.fsToolbar.disabled = disable;
+ },
+
+ /**
+ Converts this.metrics() to a form which is suitable for easy conversion to
+ CSV. It returns an array of arrays. The first sub-array is the column names.
+ The 2nd and subsequent are the values, one per test file (only the most recent
+ metrics are kept for any given file).
+ */
+ metricsToArrays: function(){
+ const rc = [];
+ Object.keys(this.dbs).sort().forEach((k)=>{
+ const d = this.dbs[k];
+ const m = d.metrics;
+ delete m.evalSqlStart;
+ delete m.evalSqlEnd;
+ const mk = Object.keys(m).sort();
+ if(!rc.length){
+ rc.push(['db', ...mk]);
+ }
+ const row = [k.split('/').pop()/*remove dir prefix from filename*/];
+ rc.push(row);
+ row.push(...mk.map((kk)=>m[kk]));
+ });
+ return rc;
+ },
+
+ metricsToBlob: function(colSeparator='\t'){
+ const ar = [], ma = this.metricsToArrays();
+ if(!ma.length){
+ this.logErr("Metrics are empty. Run something.");
+ return;
+ }
+ ma.forEach(function(row){
+ ar.push(row.join(colSeparator),'\n');
+ });
+ return new Blob(ar);
+ },
+
+ downloadMetrics: function(){
+ const b = this.metricsToBlob();
+ if(!b) return;
+ const url = URL.createObjectURL(b);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'batch-runner-js-'+((new Date().getTime()/1000) | 0)+'.csv';
+ this.logHtml("Triggering download of",a.download);
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(()=>{
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 500);
+ },
+
+ /**
+ Fetch file fn and eval it as an SQL blob. This is an async
+ operation and returns a Promise which resolves to this
+ object on success.
+ */
+ evalFile: async function(fn){
+ const sql = await this.fetchFile(fn);
+ return this.execSql(fn,sql);
+ }/*evalFile()*/,
+
+ /**
+ Clears all DB tables in all _opened_ databases. Because of
+ disparities between backends, we cannot simply "unlink" the
+ databases to clean them up.
+ */
+ clearStorage: function(onlySelectedDb=false){
+ const list = onlySelectedDb
+ ? [('boolean'===typeof onlySelectedDb)
+ ? this.dbs[this.e.selImpl.value]
+ : onlySelectedDb]
+ : Object.values(this.dbs);
+ for(let db of list){
+ if(db && db.handle){
+ this.logHtml("Clearing db",db.id);
+ db.clear();
+ }
+ }
+ },
+
+ /**
+ Fetches the handle of the db associated with
+ this.e.selImpl.value, opening it if needed.
+ */
+ getSelectedDb: function(){
+ if(!this.dbs.memdb){
+ for(let opt of this.e.selImpl.options){
+ const d = this.dbs[opt.value] = Object.create(null);
+ d.id = opt.value;
+ switch(d.id){
+ case 'virtualfs':
+ d.filename = 'file:/virtualfs.sqlite3?vfs=unix-none';
+ break;
+ case 'memdb':
+ d.filename = ':memory:';
+ break;
+ case 'wasmfs-opfs':
+ d.filename = 'file:'+(
+ this.sqlite3.capi.sqlite3_wasmfs_opfs_dir()
+ )+'/wasmfs-opfs.sqlite3b';
+ break;
+ case 'websql':
+ d.filename = 'websql.db';
+ break;
+ default:
+ this.logErr("Unhandled db selection option (see details in the console).",opt);
+ toss("Unhandled db init option");
+ }
+ }
+ }/*first-time init*/
+ const dbId = this.e.selImpl.value;
+ const d = this.dbs[dbId];
+ if(d.handle) return d;
+ if('websql' === dbId){
+ d.handle = self.openDatabase('batch-runner', '0.1', 'foo', 1024 * 1024 * 50);
+ d.clear = ()=>clearDbWebSQL(d);
+ d.handle.transaction(function(tx){
+ tx.executeSql("PRAGMA cache_size="+cacheSize);
+ App.logHtml(dbId,"cache_size =",cacheSize);
+ });
+ }else{
+ const capi = this.sqlite3.capi, wasm = this.sqlite3.wasm;
+ const stack = wasm.scopedAllocPush();
+ let pDb = 0;
+ try{
+ const oFlags = capi.SQLITE_OPEN_CREATE | capi.SQLITE_OPEN_READWRITE;
+ const ppDb = wasm.scopedAllocPtr();
+ const rc = capi.sqlite3_open_v2(d.filename, ppDb, oFlags, null);
+ pDb = wasm.getPtrValue(ppDb)
+ if(rc) toss("sqlite3_open_v2() failed with code",rc);
+ capi.sqlite3_exec(pDb, "PRAGMA cache_size="+cacheSize, 0, 0, 0);
+ this.logHtml(dbId,"cache_size =",cacheSize);
+ }catch(e){
+ if(pDb) capi.sqlite3_close_v2(pDb);
+ }finally{
+ wasm.scopedAllocPop(stack);
+ }
+ d.handle = pDb;
+ d.clear = ()=>clearDbSqlite(d);
+ }
+ d.clear();
+ this.logHtml("Opened db:",dbId,d.filename);
+ console.log("db =",d);
+ return d;
+ },
+
+ run: function(sqlite3){
+ delete this.run;
+ this.sqlite3 = sqlite3;
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ this.logHtml("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ this.logHtml("WASM heap size =",wasm.heap8().length);
+ this.loadSqlList();
+ if(capi.sqlite3_wasmfs_opfs_dir()){
+ E('#warn-opfs').classList.remove('hidden');
+ }else{
+ E('#warn-opfs').remove();
+ E('option[value=wasmfs-opfs]').disabled = true;
+ }
+ if('function' === typeof self.openDatabase){
+ E('#warn-websql').classList.remove('hidden');
+ }else{
+ E('option[value=websql]').disabled = true;
+ E('#warn-websql').remove();
+ }
+ const who = this;
+ if(this.e.cbReverseLog.checked){
+ this.e.output.classList.add('reverse');
+ }
+ this.e.cbReverseLog.addEventListener('change', function(){
+ who.e.output.classList[this.checked ? 'add' : 'remove']('reverse');
+ }, false);
+ this.e.btnClear.addEventListener('click', ()=>this.cls(), false);
+ this.e.btnRun.addEventListener('click', function(){
+ if(!who.e.selSql.value) return;
+ who.evalFile(who.e.selSql.value);
+ }, false);
+ this.e.btnRunNext.addEventListener('click', function(){
+ ++who.e.selSql.selectedIndex;
+ if(!who.e.selSql.value) return;
+ who.evalFile(who.e.selSql.value);
+ }, false);
+ this.e.btnReset.addEventListener('click', function(){
+ who.clearStorage(true);
+ }, false);
+ this.e.btnExportMetrics.addEventListener('click', function(){
+ who.logHtml2('warning',"Triggering download of metrics CSV. Check your downloads folder.");
+ who.downloadMetrics();
+ //const m = who.metricsToArrays();
+ //console.log("Metrics:",who.metrics, m);
+ });
+ this.e.selImpl.addEventListener('change', function(){
+ who.getSelectedDb();
+ });
+ this.e.btnRunRemaining.addEventListener('click', async function(){
+ let v = who.e.selSql.value;
+ const timeStart = performance.now();
+ while(v){
+ await who.evalFile(v);
+ if(who.gotError){
+ who.logErr("Error handling script",v,":",who.gotError.message);
+ break;
+ }
+ ++who.e.selSql.selectedIndex;
+ v = who.e.selSql.value;
+ }
+ const timeTotal = performance.now() - timeStart;
+ who.logHtml("Run-remaining time:",timeTotal,"ms ("+(timeTotal/1000/60)+" minute(s))");
+ who.clearStorage();
+ }, false);
+ }/*run()*/
+ }/*App*/;
+
+ self.sqlite3TestModule.initSqlite3().then(function(sqlite3_){
+ sqlite3 = sqlite3_;
+ self.App = App /* only to facilitate dev console access */;
+ App.run(sqlite3);
+ });
+})();
diff --git a/ext/wasm/common/SqliteTestUtil.js b/ext/wasm/common/SqliteTestUtil.js
new file mode 100644
index 0000000..5ed4237
--- /dev/null
+++ b/ext/wasm/common/SqliteTestUtil.js
@@ -0,0 +1,236 @@
+/*
+ 2022-05-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This file contains bootstrapping code used by various test scripts
+ which live in this file's directory.
+*/
+'use strict';
+(function(self){
+ /* querySelectorAll() proxy */
+ const EAll = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelectorAll(arguments[arguments.length-1]);
+ };
+ /* querySelector() proxy */
+ const E = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelector(arguments[arguments.length-1]);
+ };
+
+ /**
+ Helpers for writing sqlite3-specific tests.
+ */
+ self.SqliteTestUtil = {
+ /** Running total of the number of tests run via
+ this API. */
+ counter: 0,
+ /**
+ If expr is a function, it is called and its result
+ is returned, coerced to a bool, else expr, coerced to
+ a bool, is returned.
+ */
+ toBool: function(expr){
+ return (expr instanceof Function) ? !!expr() : !!expr;
+ },
+ /** abort() if expr is false. If expr is a function, it
+ is called and its result is evaluated.
+ */
+ assert: function f(expr, msg){
+ if(!f._){
+ f._ = ('undefined'===typeof abort
+ ? (msg)=>{throw new Error(msg)}
+ : abort);
+ }
+ ++this.counter;
+ if(!this.toBool(expr)){
+ f._(msg || "Assertion failed.");
+ }
+ return this;
+ },
+ /** Identical to assert() but throws instead of calling
+ abort(). */
+ affirm: function(expr, msg){
+ ++this.counter;
+ if(!this.toBool(expr)) throw new Error(msg || "Affirmation failed.");
+ return this;
+ },
+ /** Calls f() and squelches any exception it throws. If it
+ does not throw, this function throws. */
+ mustThrow: function(f, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ return this;
+ },
+ /**
+ Works like mustThrow() but expects filter to be a regex,
+ function, or string to match/filter the resulting exception
+ against. If f() does not throw, this test fails and an Error is
+ thrown. If filter is a regex, the test passes if
+ filter.test(error.message) passes. If it's a function, the test
+ passes if filter(error) returns truthy. If it's a string, the
+ test passes if the filter matches the exception message
+ precisely. In all other cases the test fails, throwing an
+ Error.
+
+ If it throws, msg is used as the error report unless it's falsy,
+ in which case a default is used.
+ */
+ mustThrowMatching: function(f, filter, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ let pass = false;
+ if(filter instanceof RegExp) pass = filter.test(err.message);
+ else if(filter instanceof Function) pass = filter(err);
+ else if('string' === typeof filter) pass = (err.message === filter);
+ if(!pass){
+ throw new Error(msg || ("Filter rejected this exception: "+err.message));
+ }
+ return this;
+ },
+ /** Throws if expr is truthy or expr is a function and expr()
+ returns truthy. */
+ throwIf: function(expr, msg){
+ ++this.counter;
+ if(this.toBool(expr)) throw new Error(msg || "throwIf() failed");
+ return this;
+ },
+ /** Throws if expr is falsy or expr is a function and expr()
+ returns falsy. */
+ throwUnless: function(expr, msg){
+ ++this.counter;
+ if(!this.toBool(expr)) throw new Error(msg || "throwUnless() failed");
+ return this;
+ },
+
+ /**
+ Parses window.location.search-style string into an object
+ containing key/value pairs of URL arguments (already
+ urldecoded). The object is created using Object.create(null),
+ so contains only parsed-out properties and has no prototype
+ (and thus no inherited properties).
+
+ If the str argument is not passed (arguments.length==0) then
+ window.location.search.substring(1) is used by default. If
+ neither str is passed in nor window exists then false is returned.
+
+ On success it returns an Object containing the key/value pairs
+ parsed from the string. Keys which have no value are treated
+ has having the boolean true value.
+
+ Pedantic licensing note: this code has appeared in other source
+ trees, but was originally written by the same person who pasted
+ it into those trees.
+ */
+ processUrlArgs: function(str) {
+ if( 0 === arguments.length ) {
+ if( ('undefined' === typeof window) ||
+ !window.location ||
+ !window.location.search ) return false;
+ else str = (''+window.location.search).substring(1);
+ }
+ if( ! str ) return false;
+ str = (''+str).split(/#/,2)[0]; // remove #... to avoid it being added as part of the last value.
+ const args = Object.create(null);
+ const sp = str.split(/&+/);
+ const rx = /^([^=]+)(=(.+))?/;
+ var i, m;
+ for( i in sp ) {
+ m = rx.exec( sp[i] );
+ if( ! m ) continue;
+ args[decodeURIComponent(m[1])] = (m[3] ? decodeURIComponent(m[3]) : true);
+ }
+ return args;
+ }
+ };
+
+
+ /**
+ This is a module object for use with the emscripten-installed
+ sqlite3InitModule() factory function.
+ */
+ self.sqlite3TestModule = {
+ /**
+ Array of functions to call after Emscripten has initialized the
+ wasm module. Each gets passed the Emscripten module object
+ (which is _this_ object).
+ */
+ postRun: [
+ /* function(theModule){...} */
+ ],
+ //onRuntimeInitialized: function(){},
+ /* Proxy for C-side stdout output. */
+ print: (...args)=>{console.log(...args)},
+ /* Proxy for C-side stderr output. */
+ printErr: (...args)=>{console.error(...args)},
+ /**
+ Called by the Emscripten module init bits to report loading
+ progress. It gets passed an empty argument when loading is done
+ (after onRuntimeInitialized() and any this.postRun callbacks
+ have been run).
+ */
+ setStatus: function f(text){
+ if(!f.last){
+ f.last = { text: '', step: 0 };
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ if(text === f.last.text) return;
+ f.last.text = text;
+ if(f.ui.progress){
+ f.ui.progress.value = f.last.step;
+ f.ui.progress.max = f.last.step + 1;
+ }
+ ++f.last.step;
+ if(text) {
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ }
+ },
+ /**
+ Config options used by the Emscripten-dependent initialization
+ which happens via this.initSqlite3(). This object gets
+ (indirectly) passed to sqlite3ApiBootstrap() to configure the
+ sqlite3 API.
+ */
+ sqlite3ApiConfig: {
+ wasmfsOpfsDir: "/opfs"
+ },
+ /**
+ Intended to be called by apps which need to call the
+ Emscripten-installed sqlite3InitModule() routine. This function
+ temporarily installs this.sqlite3ApiConfig into the self
+ object, calls it sqlite3InitModule(), and removes
+ self.sqlite3ApiConfig after initialization is done. Returns the
+ promise from sqlite3InitModule(), and the next then() handler
+ will get the sqlite3 API object as its argument.
+ */
+ initSqlite3: function(){
+ self.sqlite3ApiConfig = this.sqlite3ApiConfig;
+ return self.sqlite3InitModule(this).finally(()=>delete self.sqlite3ApiConfig);
+ }
+ };
+})(self/*window or worker*/);
diff --git a/ext/wasm/common/emscripten.css b/ext/wasm/common/emscripten.css
new file mode 100644
index 0000000..7e3dc81
--- /dev/null
+++ b/ext/wasm/common/emscripten.css
@@ -0,0 +1,24 @@
+/* emcscript-related styling, used during the module load/intialization processes... */
+.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+div.emscripten { text-align: center; }
+div.emscripten_border { border: 1px solid black; }
+#module-spinner { overflow: visible; }
+#module-spinner > * {
+ margin-top: 1em;
+}
+.spinner {
+ height: 50px;
+ width: 50px;
+ margin: 0px auto;
+ animation: rotation 0.8s linear infinite;
+ border-left: 10px solid rgb(0,150,240);
+ border-right: 10px solid rgb(0,150,240);
+ border-bottom: 10px solid rgb(0,150,240);
+ border-top: 10px solid rgb(100,0,200);
+ border-radius: 100%;
+ background-color: rgb(200,100,250);
+}
+@keyframes rotation {
+ from {transform: rotate(0deg);}
+ to {transform: rotate(360deg);}
+}
diff --git a/ext/wasm/common/testing.css b/ext/wasm/common/testing.css
new file mode 100644
index 0000000..9438b33
--- /dev/null
+++ b/ext/wasm/common/testing.css
@@ -0,0 +1,63 @@
+body {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+}
+textarea {
+ font-family: monospace;
+}
+header {
+ font-size: 130%;
+ font-weight: bold;
+}
+.hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+}
+fieldset.options {
+ font-size: 75%;
+}
+fieldset > legend {
+ padding: 0 0.5em;
+}
+span.labeled-input {
+ padding: 0.25em;
+ margin: 0.25em 0.5em;
+ border-radius: 0.25em;
+ white-space: nowrap;
+ background: #0002;
+}
+.center { text-align: center; }
+.error {
+ color: red;
+ background-color: yellow;
+}
+.strong { font-weight: 700 }
+.warning { color: firebrick; }
+.green { color: darkgreen; }
+.tests-pass { background-color: green; color: white }
+.tests-fail { background-color: red; color: yellow }
+.faded { opacity: 0.5; }
+.group-start { color: blue; }
+.group-end { color: blue; }
+.input-wrapper {
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+}
+#test-output {
+ border: 1px inset;
+ border-radius: 0.25em;
+ padding: 0.25em;
+ /*max-height: 30em;*/
+ overflow: auto;
+ white-space: break-spaces;
+ display: flex; flex-direction: column;
+ font-family: monospace;
+}
+#test-output.reverse {
+ flex-direction: column-reverse;
+}
+label[for] { cursor: pointer }
diff --git a/ext/wasm/common/whwasmutil.js b/ext/wasm/common/whwasmutil.js
new file mode 100644
index 0000000..7e5e798
--- /dev/null
+++ b/ext/wasm/common/whwasmutil.js
@@ -0,0 +1,1706 @@
+/**
+ 2022-07-08
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ The whwasmutil is developed in conjunction with the Jaccwabyt
+ project:
+
+ https://fossil.wanderinghorse.net/r/jaccwabyt
+
+ and sqlite3:
+
+ https://sqlite.org
+
+ This file is kept in sync between both of those trees.
+
+ Maintenance reminder: If you're reading this in a tree other than
+ one of those listed above, note that this copy may be replaced with
+ upstream copies of that one from time to time. Thus the code
+ installed by this function "should not" be edited outside of those
+ projects, else it risks getting overwritten.
+*/
+/**
+ This function is intended to simplify porting around various bits
+ of WASM-related utility code from project to project.
+
+ The primary goal of this code is to replace, where possible,
+ Emscripten-generated glue code with equivalent utility code which
+ can be used in arbitrary WASM environments built with toolchains
+ other than Emscripten. As of this writing, this code is capable of
+ acting as a replacement for Emscripten's generated glue code
+ _except_ that the latter installs handlers for Emscripten-provided
+ APIs such as its "FS" (virtual filesystem) API. Loading of such
+ things still requires using Emscripten's glue, but the post-load
+ utility APIs provided by this code are still usable as replacements
+ for their sub-optimally-documented Emscripten counterparts.
+
+ Intended usage:
+
+ ```
+ self.WhWasmUtilInstaller(appObject);
+ delete self.WhWasmUtilInstaller;
+ ```
+
+ Its global-scope symbol is intended only to provide an easy way to
+ make it available to 3rd-party scripts and "should" be deleted
+ after calling it. That symbols is _not_ used within the library.
+
+ Forewarning: this API explicitly targets only browser
+ environments. If a given non-browser environment has the
+ capabilities needed for a given feature (e.g. TextEncoder), great,
+ but it does not go out of its way to account for them and does not
+ provide compatibility crutches for them.
+
+ It currently offers alternatives to the following
+ Emscripten-generated APIs:
+
+ - OPTIONALLY memory allocation, but how this gets imported is
+ environment-specific. Most of the following features only work
+ if allocation is available.
+
+ - WASM-exported "indirect function table" access and
+ manipulation. e.g. creating new WASM-side functions using JS
+ functions, analog to Emscripten's addFunction() and
+ uninstallFunction() but slightly different.
+
+ - Get/set specific heap memory values, analog to Emscripten's
+ getValue() and setValue().
+
+ - String length counting in UTF-8 bytes (C-style and JS strings).
+
+ - JS string to C-string conversion and vice versa, analog to
+ Emscripten's stringToUTF8Array() and friends, but with slighter
+ different interfaces.
+
+ - JS string to Uint8Array conversion, noting that browsers actually
+ already have this built in via TextEncoder.
+
+ - "Scoped" allocation, such that allocations made inside of a given
+ explicit scope will be automatically cleaned up when the scope is
+ closed. This is fundamentally similar to Emscripten's
+ stackAlloc() and friends but uses the heap instead of the stack
+ because access to the stack requires C code.
+
+ - Create JS wrappers for WASM functions, analog to Emscripten's
+ ccall() and cwrap() functions, except that the automatic
+ conversions for function arguments and return values can be
+ easily customized by the client by assigning custom function
+ signature type names to conversion functions. Essentially,
+ it's ccall() and cwrap() on steroids.
+
+ How to install...
+
+ Passing an object to this function will install the functionality
+ into that object. Afterwards, client code "should" delete the global
+ symbol.
+
+ This code requires that the target object have the following
+ properties, noting that they needn't be available until the first
+ time one of the installed APIs is used (as opposed to when this
+ function is called) except where explicitly noted:
+
+ - `exports` must be a property of the target object OR a property
+ of `target.instance` (a WebAssembly.Module instance) and it must
+ contain the symbols exported by the WASM module associated with
+ this code. In an Enscripten environment it must be set to
+ `Module['asm']`. The exports object must contain a minimum of the
+ following symbols:
+
+ - `memory`: a WebAssembly.Memory object representing the WASM
+ memory. _Alternately_, the `memory` property can be set as
+ `target.memory`, in particular if the WASM heap memory is
+ initialized in JS an _imported_ into WASM, as opposed to being
+ initialized in WASM and exported to JS.
+
+ - `__indirect_function_table`: the WebAssembly.Table object which
+ holds WASM-exported functions. This API does not strictly
+ require that the table be able to grow but it will throw if its
+ `installFunction()` is called and the table cannot grow.
+
+ In order to simplify downstream usage, if `target.exports` is not
+ set when this is called then a property access interceptor
+ (read-only, configurable, enumerable) gets installed as `exports`
+ which resolves to `target.instance.exports`, noting that the latter
+ property need not exist until the first time `target.exports` is
+ accessed.
+
+ Some APIs _optionally_ make use of the `bigIntEnabled` property of
+ the target object. It "should" be set to true if the WASM
+ environment is compiled with BigInt support, else it must be
+ false. If it is false, certain BigInt-related features will trigger
+ an exception if invoked. This property, if not set when this is
+ called, will get a default value of true only if the BigInt64Array
+ constructor is available, else it will default to false. Note that
+ having the BigInt type is not sufficient for full int64 integration
+ with WASM: the target WASM file must also have been built with
+ that support. In Emscripten that's done using the `-sWASM_BIGINT`
+ flag.
+
+ Some optional APIs require that the target have the following
+ methods:
+
+ - 'alloc()` must behave like C's `malloc()`, allocating N bytes of
+ memory and returning its pointer. In Emscripten this is
+ conventionally made available via `Module['_malloc']`. This API
+ requires that the alloc routine throw on allocation error, as
+ opposed to returning null or 0.
+
+ - 'dealloc()` must behave like C's `free()`, accepting either a
+ pointer returned from its allocation counterpart or the values
+ null/0 (for which it must be a no-op). allocating N bytes of
+ memory and returning its pointer. In Emscripten this is
+ conventionally made available via `Module['_free']`.
+
+ APIs which require allocation routines are explicitly documented as
+ such and/or have "alloc" in their names.
+
+ This code is developed and maintained in conjunction with the
+ Jaccwabyt project:
+
+ https://fossil.wanderinghorse.net/r/jaccwabbyt
+
+ More specifically:
+
+ https://fossil.wanderinghorse.net/r/jaccwabbyt/file/common/whwasmutil.js
+*/
+self.WhWasmUtilInstaller = function(target){
+ 'use strict';
+ if(undefined===target.bigIntEnabled){
+ target.bigIntEnabled = !!self['BigInt64Array'];
+ }
+
+ /** Throws a new Error, the message of which is the concatenation of
+ all args with a space between each. */
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+
+ if(!target.exports){
+ Object.defineProperty(target, 'exports', {
+ enumerable: true, configurable: true,
+ get: ()=>(target.instance && target.instance.exports)
+ });
+ }
+
+ /*********
+ alloc()/dealloc() auto-install...
+
+ This would be convenient but it can also cause us to pick up
+ malloc() even when the client code is using a different exported
+ allocator (who, me?), which is bad. malloc() may be exported even
+ if we're not explicitly using it and overriding the malloc()
+ function, linking ours first, is not always feasible when using a
+ malloc() proxy, as it can lead to recursion and stack overflow
+ (who, me?). So... we really need the downstream code to set up
+ target.alloc/dealloc() itself.
+ ******/
+ /******
+ if(target.exports){
+ //Maybe auto-install alloc()/dealloc()...
+ if(!target.alloc && target.exports.malloc){
+ target.alloc = function(n){
+ const m = this(n);
+ return m || toss("Allocation of",n,"byte(s) failed.");
+ }.bind(target.exports.malloc);
+ }
+
+ if(!target.dealloc && target.exports.free){
+ target.dealloc = function(ptr){
+ if(ptr) this(ptr);
+ }.bind(target.exports.free);
+ }
+ }*******/
+
+ /**
+ Pointers in WASM are currently assumed to be 32-bit, but someday
+ that will certainly change.
+ */
+ const ptrIR = target.pointerIR || 'i32';
+ const ptrSizeof = target.ptrSizeof =
+ ('i32'===ptrIR ? 4
+ : ('i64'===ptrIR
+ ? 8 : toss("Unhandled ptrSizeof:",ptrIR)));
+ /** Stores various cached state. */
+ const cache = Object.create(null);
+ /** Previously-recorded size of cache.memory.buffer, noted so that
+ we can recreate the view objects if the heap grows. */
+ cache.heapSize = 0;
+ /** WebAssembly.Memory object extracted from target.memory or
+ target.exports.memory the first time heapWrappers() is
+ called. */
+ cache.memory = null;
+ /** uninstallFunction() puts table indexes in here for reuse and
+ installFunction() extracts them. */
+ cache.freeFuncIndexes = [];
+ /**
+ Used by scopedAlloc() and friends.
+ */
+ cache.scopedAlloc = [];
+
+ cache.utf8Decoder = new TextDecoder();
+ cache.utf8Encoder = new TextEncoder('utf-8');
+
+ /**
+ If (cache.heapSize !== cache.memory.buffer.byteLength), i.e. if
+ the heap has grown since the last call, updates cache.HEAPxyz.
+ Returns the cache object.
+ */
+ const heapWrappers = function(){
+ if(!cache.memory){
+ cache.memory = (target.memory instanceof WebAssembly.Memory)
+ ? target.memory : target.exports.memory;
+ }else if(cache.heapSize === cache.memory.buffer.byteLength){
+ return cache;
+ }
+ // heap is newly-acquired or has been resized....
+ const b = cache.memory.buffer;
+ cache.HEAP8 = new Int8Array(b); cache.HEAP8U = new Uint8Array(b);
+ cache.HEAP16 = new Int16Array(b); cache.HEAP16U = new Uint16Array(b);
+ cache.HEAP32 = new Int32Array(b); cache.HEAP32U = new Uint32Array(b);
+ if(target.bigIntEnabled){
+ cache.HEAP64 = new BigInt64Array(b); cache.HEAP64U = new BigUint64Array(b);
+ }
+ cache.HEAP32F = new Float32Array(b); cache.HEAP64F = new Float64Array(b);
+ cache.heapSize = b.byteLength;
+ return cache;
+ };
+
+ /** Convenience equivalent of this.heapForSize(8,false). */
+ target.heap8 = ()=>heapWrappers().HEAP8;
+
+ /** Convenience equivalent of this.heapForSize(8,true). */
+ target.heap8u = ()=>heapWrappers().HEAP8U;
+
+ /** Convenience equivalent of this.heapForSize(16,false). */
+ target.heap16 = ()=>heapWrappers().HEAP16;
+
+ /** Convenience equivalent of this.heapForSize(16,true). */
+ target.heap16u = ()=>heapWrappers().HEAP16U;
+
+ /** Convenience equivalent of this.heapForSize(32,false). */
+ target.heap32 = ()=>heapWrappers().HEAP32;
+
+ /** Convenience equivalent of this.heapForSize(32,true). */
+ target.heap32u = ()=>heapWrappers().HEAP32U;
+
+ /**
+ Requires n to be one of:
+
+ - integer 8, 16, or 32.
+ - A integer-type TypedArray constructor: Int8Array, Int16Array,
+ Int32Array, or their Uint counterparts.
+
+ If this.bigIntEnabled is true, it also accepts the value 64 or a
+ BigInt64Array/BigUint64Array, else it throws if passed 64 or one
+ of those constructors.
+
+ Returns an integer-based TypedArray view of the WASM heap
+ memory buffer associated with the given block size. If passed
+ an integer as the first argument and unsigned is truthy then
+ the "U" (unsigned) variant of that view is returned, else the
+ signed variant is returned. If passed a TypedArray value, the
+ 2nd argument is ignored. Note that Float32Array and
+ Float64Array views are not supported by this function.
+
+ Note that growth of the heap will invalidate any references to
+ this heap, so do not hold a reference longer than needed and do
+ not use a reference after any operation which may
+ allocate. Instead, re-fetch the reference by calling this
+ function again.
+
+ Throws if passed an invalid n.
+
+ Pedantic side note: the name "heap" is a bit of a misnomer. In an
+ Emscripten environment, the memory managed via the stack
+ allocation API is in the same Memory object as the heap (which
+ makes sense because otherwise arbitrary pointer X would be
+ ambiguous: is it in the heap or the stack?).
+ */
+ target.heapForSize = function(n,unsigned = false){
+ let ctor;
+ const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength)
+ ? cache : heapWrappers();
+ switch(n){
+ case Int8Array: return c.HEAP8; case Uint8Array: return c.HEAP8U;
+ case Int16Array: return c.HEAP16; case Uint16Array: return c.HEAP16U;
+ case Int32Array: return c.HEAP32; case Uint32Array: return c.HEAP32U;
+ case 8: return unsigned ? c.HEAP8U : c.HEAP8;
+ case 16: return unsigned ? c.HEAP16U : c.HEAP16;
+ case 32: return unsigned ? c.HEAP32U : c.HEAP32;
+ case 64:
+ if(c.HEAP64) return unsigned ? c.HEAP64U : c.HEAP64;
+ break;
+ default:
+ if(target.bigIntEnabled){
+ if(n===self['BigUint64Array']) return c.HEAP64U;
+ else if(n===self['BigInt64Array']) return c.HEAP64;
+ break;
+ }
+ }
+ toss("Invalid heapForSize() size: expecting 8, 16, 32,",
+ "or (if BigInt is enabled) 64.");
+ };
+
+ /**
+ Returns the WASM-exported "indirect function table."
+ */
+ target.functionTable = function(){
+ return target.exports.__indirect_function_table;
+ /** -----------------^^^^^ "seems" to be a standardized export name.
+ From Emscripten release notes from 2020-09-10:
+ - Use `__indirect_function_table` as the import name for the
+ table, which is what LLVM does.
+ */
+ };
+
+ /**
+ Given a function pointer, returns the WASM function table entry
+ if found, else returns a falsy value.
+ */
+ target.functionEntry = function(fptr){
+ const ft = target.functionTable();
+ return fptr < ft.length ? ft.get(fptr) : undefined;
+ };
+
+ /**
+ Creates a WASM function which wraps the given JS function and
+ returns the JS binding of that WASM function. The signature
+ string must be the Jaccwabyt-format or Emscripten
+ addFunction()-format function signature string. In short: in may
+ have one of the following formats:
+
+ - Emscripten: `"x..."`, where the first x is a letter representing
+ the result type and subsequent letters represent the argument
+ types. Functions with no arguments have only a single
+ letter. See below.
+
+ - Jaccwabyt: `"x(...)"` where `x` is the letter representing the
+ result type and letters in the parens (if any) represent the
+ argument types. Functions with no arguments use `x()`. See
+ below.
+
+ Supported letters:
+
+ - `i` = int32
+ - `p` = int32 ("pointer")
+ - `j` = int64
+ - `f` = float32
+ - `d` = float64
+ - `v` = void, only legal for use as the result type
+
+ It throws if an invalid signature letter is used.
+
+ Jaccwabyt-format signatures support some additional letters which
+ have no special meaning here but (in this context) act as aliases
+ for other letters:
+
+ - `s`, `P`: same as `p`
+
+ Sidebar: this code is developed together with Jaccwabyt, thus the
+ support for its signature format.
+
+ The arguments may be supplied in either order: (func,sig) or
+ (sig,func).
+ */
+ target.jsFuncToWasm = function f(func, sig){
+ /** Attribution: adapted up from Emscripten-generated glue code,
+ refactored primarily for efficiency's sake, eliminating
+ call-local functions and superfluous temporary arrays. */
+ if(!f._){/*static init...*/
+ f._ = {
+ // Map of signature letters to type IR values
+ sigTypes: Object.assign(Object.create(null),{
+ i: 'i32', p: 'i32', P: 'i32', s: 'i32',
+ j: 'i64', f: 'f32', d: 'f64'
+ }),
+ // Map of type IR values to WASM type code values
+ typeCodes: Object.assign(Object.create(null),{
+ f64: 0x7c, f32: 0x7d, i64: 0x7e, i32: 0x7f
+ }),
+ /** Encodes n, which must be <2^14 (16384), into target array
+ tgt, as a little-endian value, using the given method
+ ('push' or 'unshift'). */
+ uleb128Encode: function(tgt, method, n){
+ if(n<128) tgt[method](n);
+ else tgt[method]( (n % 128) | 128, n>>7);
+ },
+ /** Intentionally-lax pattern for Jaccwabyt-format function
+ pointer signatures, the intent of which is simply to
+ distinguish them from Emscripten-format signatures. The
+ downstream checks are less lax. */
+ rxJSig: /^(\w)\((\w*)\)$/,
+ /** Returns the parameter-value part of the given signature
+ string. */
+ sigParams: function(sig){
+ const m = f._.rxJSig.exec(sig);
+ return m ? m[2] : sig.substr(1);
+ },
+ /** Returns the IR value for the given letter or throws
+ if the letter is invalid. */
+ letterType: (x)=>f._.sigTypes[x] || toss("Invalid signature letter:",x),
+ /** Returns an object describing the result type and parameter
+ type(s) of the given function signature, or throws if the
+ signature is invalid. */
+ /******** // only valid for use with the WebAssembly.Function ctor, which
+ // is not yet documented on MDN.
+ sigToWasm: function(sig){
+ const rc = {parameters:[], results: []};
+ if('v'!==sig[0]) rc.results.push(f.sigTypes(sig[0]));
+ for(const x of f._.sigParams(sig)){
+ rc.parameters.push(f._.typeCodes(x));
+ }
+ return rc;
+ },************/
+ /** Pushes the WASM data type code for the given signature
+ letter to the given target array. Throws if letter is
+ invalid. */
+ pushSigType: (dest, letter)=>dest.push(f._.typeCodes[f._.letterType(letter)])
+ };
+ }/*static init*/
+ if('string'===typeof func){
+ const x = sig;
+ sig = func;
+ func = x;
+ }
+ const sigParams = f._.sigParams(sig);
+ const wasmCode = [0x01/*count: 1*/, 0x60/*function*/];
+ f._.uleb128Encode(wasmCode, 'push', sigParams.length);
+ for(const x of sigParams) f._.pushSigType(wasmCode, x);
+ if('v'===sig[0]) wasmCode.push(0);
+ else{
+ wasmCode.push(1);
+ f._.pushSigType(wasmCode, sig[0]);
+ }
+ f._.uleb128Encode(wasmCode, 'unshift', wasmCode.length)/* type section length */;
+ wasmCode.unshift(
+ 0x00, 0x61, 0x73, 0x6d, /* magic: "\0asm" */
+ 0x01, 0x00, 0x00, 0x00, /* version: 1 */
+ 0x01 /* type section code */
+ );
+ wasmCode.push(
+ /* import section: */ 0x02, 0x07,
+ /* (import "e" "f" (func 0 (type 0))): */
+ 0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00,
+ /* export section: */ 0x07, 0x05,
+ /* (export "f" (func 0 (type 0))): */
+ 0x01, 0x01, 0x66, 0x00, 0x00
+ );
+ return (new WebAssembly.Instance(
+ new WebAssembly.Module(new Uint8Array(wasmCode)), {
+ e: { f: func }
+ })).exports['f'];
+ }/*jsFuncToWasm()*/;
+
+ /**
+ Expects a JS function and signature, exactly as for
+ this.jsFuncToWasm(). It uses that function to create a
+ WASM-exported function, installs that function to the next
+ available slot of this.functionTable(), and returns the
+ function's index in that table (which acts as a pointer to that
+ function). The returned pointer can be passed to
+ uninstallFunction() to uninstall it and free up the table slot for
+ reuse.
+
+ If passed (string,function) arguments then it treats the first
+ argument as the signature and second as the function.
+
+ As a special case, if the passed-in function is a WASM-exported
+ function then the signature argument is ignored and func is
+ installed as-is, without requiring re-compilation/re-wrapping.
+
+ This function will propagate an exception if
+ WebAssembly.Table.grow() throws or this.jsFuncToWasm() throws.
+ The former case can happen in an Emscripten-compiled
+ environment when building without Emscripten's
+ `-sALLOW_TABLE_GROWTH` flag.
+
+ Sidebar: this function differs from Emscripten's addFunction()
+ _primarily_ in that it does not share that function's
+ undocumented behavior of reusing a function if it's passed to
+ addFunction() more than once, which leads to uninstallFunction()
+ breaking clients which do not take care to avoid that case:
+
+ https://github.com/emscripten-core/emscripten/issues/17323
+ */
+ target.installFunction = function f(func, sig){
+ if(2!==arguments.length){
+ toss("installFunction() requires exactly 2 arguments");
+ }
+ if('string'===typeof func){
+ const x = sig;
+ sig = func;
+ func = x;
+ }
+ const ft = target.functionTable();
+ const oldLen = ft.length;
+ let ptr;
+ while(cache.freeFuncIndexes.length){
+ ptr = cache.freeFuncIndexes.pop();
+ if(ft.get(ptr)){ /* Table was modified via a different API */
+ ptr = null;
+ continue;
+ }else{
+ break;
+ }
+ }
+ if(!ptr){
+ ptr = oldLen;
+ ft.grow(1);
+ }
+ try{
+ /*this will only work if func is a WASM-exported function*/
+ ft.set(ptr, func);
+ return ptr;
+ }catch(e){
+ if(!(e instanceof TypeError)){
+ if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen);
+ throw e;
+ }
+ }
+ // It's not a WASM-exported function, so compile one...
+ try {
+ ft.set(ptr, target.jsFuncToWasm(func, sig));
+ }catch(e){
+ if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen);
+ throw e;
+ }
+ return ptr;
+ };
+
+ /**
+ Requires a pointer value previously returned from
+ this.installFunction(). Removes that function from the WASM
+ function table, marks its table slot as free for re-use, and
+ returns that function. It is illegal to call this before
+ installFunction() has been called and results are undefined if
+ ptr was not returned by that function. The returned function
+ may be passed back to installFunction() to reinstall it.
+ */
+ target.uninstallFunction = function(ptr){
+ const fi = cache.freeFuncIndexes;
+ const ft = target.functionTable();
+ fi.push(ptr);
+ const rc = ft.get(ptr);
+ ft.set(ptr, null);
+ return rc;
+ };
+
+ /**
+ Given a WASM heap memory address and a data type name in the form
+ (i8, i16, i32, i64, float (or f32), double (or f64)), this
+ fetches the numeric value from that address and returns it as a
+ number or, for the case of type='i64', a BigInt (noting that that
+ type triggers an exception if this.bigIntEnabled is
+ falsy). Throws if given an invalid type.
+
+ As a special case, if type ends with a `*`, it is considered to
+ be a pointer type and is treated as the WASM numeric type
+ appropriate for the pointer size (`i32`).
+
+ While likely not obvious, this routine and its setMemValue()
+ counterpart are how pointer-to-value _output_ parameters
+ in WASM-compiled C code can be interacted with:
+
+ ```
+ const ptr = alloc(4);
+ setMemValue(ptr, 0, 'i32'); // clear the ptr's value
+ aCFuncWithOutputPtrToInt32Arg( ptr ); // e.g. void foo(int *x);
+ const result = getMemValue(ptr, 'i32'); // fetch ptr's value
+ dealloc(ptr);
+ ```
+
+ scopedAlloc() and friends can be used to make handling of
+ `ptr` safe against leaks in the case of an exception:
+
+ ```
+ let result;
+ const scope = scopedAllocPush();
+ try{
+ const ptr = scopedAlloc(4);
+ setMemValue(ptr, 0, 'i32');
+ aCFuncWithOutputPtrArg( ptr );
+ result = getMemValue(ptr, 'i32');
+ }finally{
+ scopedAllocPop(scope);
+ }
+ ```
+
+ As a rule setMemValue() must be called to set (typically zero
+ out) the pointer's value, else it will contain an essentially
+ random value.
+
+ ACHTUNG: calling this often, e.g. in a loop, can have a noticably
+ painful impact on performance. Rather than doing so, use
+ heapForSize() to fetch the heap object and read directly from it.
+
+ See: setMemValue()
+ */
+ target.getMemValue = function(ptr, type='i8'){
+ if(type.endsWith('*')) type = ptrIR;
+ const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength)
+ ? cache : heapWrappers();
+ switch(type){
+ case 'i1':
+ case 'i8': return c.HEAP8[ptr>>0];
+ case 'i16': return c.HEAP16[ptr>>1];
+ case 'i32': return c.HEAP32[ptr>>2];
+ case 'i64':
+ if(target.bigIntEnabled) return BigInt(c.HEAP64[ptr>>3]);
+ break;
+ case 'float': case 'f32': return c.HEAP32F[ptr>>2];
+ case 'double': case 'f64': return Number(c.HEAP64F[ptr>>3]);
+ default: break;
+ }
+ toss('Invalid type for getMemValue():',type);
+ };
+
+ /**
+ The counterpart of getMemValue(), this sets a numeric value at
+ the given WASM heap address, using the type to define how many
+ bytes are written. Throws if given an invalid type. See
+ getMemValue() for details about the type argument. If the 3rd
+ argument ends with `*` then it is treated as a pointer type and
+ this function behaves as if the 3rd argument were `i32`.
+
+ This function returns itself.
+
+ ACHTUNG: calling this often, e.g. in a loop, can have a noticably
+ painful impact on performance. Rather than doing so, use
+ heapForSize() to fetch the heap object and assign directly to it.
+ */
+ target.setMemValue = function f(ptr, value, type='i8'){
+ if (type.endsWith('*')) type = ptrIR;
+ const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength)
+ ? cache : heapWrappers();
+ switch (type) {
+ case 'i1':
+ case 'i8': c.HEAP8[ptr>>0] = value; return f;
+ case 'i16': c.HEAP16[ptr>>1] = value; return f;
+ case 'i32': c.HEAP32[ptr>>2] = value; return f;
+ case 'i64':
+ if(c.HEAP64){
+ c.HEAP64[ptr>>3] = BigInt(value);
+ return f;
+ }
+ break;
+ case 'float': case 'f32': c.HEAP32F[ptr>>2] = value; return f;
+ case 'double': case 'f64': c.HEAP64F[ptr>>3] = value; return f;
+ }
+ toss('Invalid type for setMemValue(): ' + type);
+ };
+
+
+ /** Convenience form of getMemValue() intended for fetching
+ pointer-to-pointer values. */
+ target.getPtrValue = (ptr)=>target.getMemValue(ptr, ptrIR);
+
+ /** Convenience form of setMemValue() intended for setting
+ pointer-to-pointer values. */
+ target.setPtrValue = (ptr, value)=>target.setMemValue(ptr, value, ptrIR);
+
+ /**
+ Returns true if the given value appears to be legal for use as
+ a WASM pointer value. Its _range_ of values is not (cannot be)
+ validated except to ensure that it is a 32-bit integer with a
+ value of 0 or greater. Likewise, it cannot verify whether the
+ value actually refers to allocated memory in the WASM heap.
+ */
+ target.isPtr32 = (ptr)=>('number'===typeof ptr && (ptr===(ptr|0)) && ptr>=0);
+
+ /**
+ isPtr() is an alias for isPtr32(). If/when 64-bit WASM pointer
+ support becomes widespread, it will become an alias for either
+ isPtr32() or the as-yet-hypothetical isPtr64(), depending on a
+ configuration option.
+ */
+ target.isPtr = target.isPtr32;
+
+ /**
+ Expects ptr to be a pointer into the WASM heap memory which
+ refers to a NUL-terminated C-style string encoded as UTF-8.
+ Returns the length, in bytes, of the string, as for `strlen(3)`.
+ As a special case, if !ptr then it it returns `null`. Throws if
+ ptr is out of range for target.heap8u().
+ */
+ target.cstrlen = function(ptr){
+ if(!ptr) return null;
+ const h = heapWrappers().HEAP8U;
+ let pos = ptr;
+ for( ; h[pos] !== 0; ++pos ){}
+ return pos - ptr;
+ };
+
+ /** Internal helper to use in operations which need to distinguish
+ between SharedArrayBuffer heap memory and non-shared heap. */
+ const __SAB = ('undefined'===typeof SharedArrayBuffer)
+ ? function(){} : SharedArrayBuffer;
+ const __utf8Decode = function(arrayBuffer, begin, end){
+ return cache.utf8Decoder.decode(
+ (arrayBuffer.buffer instanceof __SAB)
+ ? arrayBuffer.slice(begin, end)
+ : arrayBuffer.subarray(begin, end)
+ );
+ };
+
+ /**
+ Expects ptr to be a pointer into the WASM heap memory which
+ refers to a NUL-terminated C-style string encoded as UTF-8. This
+ function counts its byte length using cstrlen() then returns a
+ JS-format string representing its contents. As a special case, if
+ ptr is falsy, `null` is returned.
+ */
+ target.cstringToJs = function(ptr){
+ const n = target.cstrlen(ptr);
+ return n ? __utf8Decode(heapWrappers().HEAP8U, ptr, ptr+n) : (null===n ? n : "");
+ };
+
+ /**
+ Given a JS string, this function returns its UTF-8 length in
+ bytes. Returns null if str is not a string.
+ */
+ target.jstrlen = function(str){
+ /** Attribution: derived from Emscripten's lengthBytesUTF8() */
+ if('string'!==typeof str) return null;
+ const n = str.length;
+ let len = 0;
+ for(let i = 0; i < n; ++i){
+ let u = str.charCodeAt(i);
+ if(u>=0xd800 && u<=0xdfff){
+ u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF);
+ }
+ if(u<=0x7f) ++len;
+ else if(u<=0x7ff) len += 2;
+ else if(u<=0xffff) len += 3;
+ else len += 4;
+ }
+ return len;
+ };
+
+ /**
+ Encodes the given JS string as UTF8 into the given TypedArray
+ tgt, starting at the given offset and writing, at most, maxBytes
+ bytes (including the NUL terminator if addNul is true, else no
+ NUL is added). If it writes any bytes at all and addNul is true,
+ it always NUL-terminates the output, even if doing so means that
+ the NUL byte is all that it writes.
+
+ If maxBytes is negative (the default) then it is treated as the
+ remaining length of tgt, starting at the given offset.
+
+ If writing the last character would surpass the maxBytes count
+ because the character is multi-byte, that character will not be
+ written (as opposed to writing a truncated multi-byte character).
+ This can lead to it writing as many as 3 fewer bytes than
+ maxBytes specifies.
+
+ Returns the number of bytes written to the target, _including_
+ the NUL terminator (if any). If it returns 0, it wrote nothing at
+ all, which can happen if:
+
+ - str is empty and addNul is false.
+ - offset < 0.
+ - maxBytes == 0.
+ - maxBytes is less than the byte length of a multi-byte str[0].
+
+ Throws if tgt is not an Int8Array or Uint8Array.
+
+ Design notes:
+
+ - In C's strcpy(), the destination pointer is the first
+ argument. That is not the case here primarily because the 3rd+
+ arguments are all referring to the destination, so it seems to
+ make sense to have them grouped with it.
+
+ - Emscripten's counterpart of this function (stringToUTF8Array())
+ returns the number of bytes written sans NUL terminator. That
+ is, however, ambiguous: str.length===0 or maxBytes===(0 or 1)
+ all cause 0 to be returned.
+ */
+ target.jstrcpy = function(jstr, tgt, offset = 0, maxBytes = -1, addNul = true){
+ /** Attribution: the encoding bits are taken from Emscripten's
+ stringToUTF8Array(). */
+ if(!tgt || (!(tgt instanceof Int8Array) && !(tgt instanceof Uint8Array))){
+ toss("jstrcpy() target must be an Int8Array or Uint8Array.");
+ }
+ if(maxBytes<0) maxBytes = tgt.length - offset;
+ if(!(maxBytes>0) || !(offset>=0)) return 0;
+ let i = 0, max = jstr.length;
+ const begin = offset, end = offset + maxBytes - (addNul ? 1 : 0);
+ for(; i < max && offset < end; ++i){
+ let u = jstr.charCodeAt(i);
+ if(u>=0xd800 && u<=0xdfff){
+ u = 0x10000 + ((u & 0x3FF) << 10) | (jstr.charCodeAt(++i) & 0x3FF);
+ }
+ if(u<=0x7f){
+ if(offset >= end) break;
+ tgt[offset++] = u;
+ }else if(u<=0x7ff){
+ if(offset + 1 >= end) break;
+ tgt[offset++] = 0xC0 | (u >> 6);
+ tgt[offset++] = 0x80 | (u & 0x3f);
+ }else if(u<=0xffff){
+ if(offset + 2 >= end) break;
+ tgt[offset++] = 0xe0 | (u >> 12);
+ tgt[offset++] = 0x80 | ((u >> 6) & 0x3f);
+ tgt[offset++] = 0x80 | (u & 0x3f);
+ }else{
+ if(offset + 3 >= end) break;
+ tgt[offset++] = 0xf0 | (u >> 18);
+ tgt[offset++] = 0x80 | ((u >> 12) & 0x3f);
+ tgt[offset++] = 0x80 | ((u >> 6) & 0x3f);
+ tgt[offset++] = 0x80 | (u & 0x3f);
+ }
+ }
+ if(addNul) tgt[offset++] = 0;
+ return offset - begin;
+ };
+
+ /**
+ Works similarly to C's strncpy(), copying, at most, n bytes (not
+ characters) from srcPtr to tgtPtr. It copies until n bytes have
+ been copied or a 0 byte is reached in src. _Unlike_ strncpy(), it
+ returns the number of bytes it assigns in tgtPtr, _including_ the
+ NUL byte (if any). If n is reached before a NUL byte in srcPtr,
+ tgtPtr will _not_ be NULL-terminated. If a NUL byte is reached
+ before n bytes are copied, tgtPtr will be NUL-terminated.
+
+ If n is negative, cstrlen(srcPtr)+1 is used to calculate it, the
+ +1 being for the NUL byte.
+
+ Throws if tgtPtr or srcPtr are falsy. Results are undefined if:
+
+ - either is not a pointer into the WASM heap or
+
+ - srcPtr is not NUL-terminated AND n is less than srcPtr's
+ logical length.
+
+ ACHTUNG: it is possible to copy partial multi-byte characters
+ this way, and converting such strings back to JS strings will
+ have undefined results.
+ */
+ target.cstrncpy = function(tgtPtr, srcPtr, n){
+ if(!tgtPtr || !srcPtr) toss("cstrncpy() does not accept NULL strings.");
+ if(n<0) n = target.cstrlen(strPtr)+1;
+ else if(!(n>0)) return 0;
+ const heap = target.heap8u();
+ let i = 0, ch;
+ for(; i < n && (ch = heap[srcPtr+i]); ++i){
+ heap[tgtPtr+i] = ch;
+ }
+ if(i<n) heap[tgtPtr + i++] = 0;
+ return i;
+ };
+
+ /**
+ For the given JS string, returns a Uint8Array of its contents
+ encoded as UTF-8. If addNul is true, the returned array will have
+ a trailing 0 entry, else it will not.
+ */
+ target.jstrToUintArray = (str, addNul=false)=>{
+ return cache.utf8Encoder.encode(addNul ? (str+"\0") : str);
+ // Or the hard way...
+ /** Attribution: derived from Emscripten's stringToUTF8Array() */
+ //const a = [], max = str.length;
+ //let i = 0, pos = 0;
+ //for(; i < max; ++i){
+ // let u = str.charCodeAt(i);
+ // if(u>=0xd800 && u<=0xdfff){
+ // u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF);
+ // }
+ // if(u<=0x7f) a[pos++] = u;
+ // else if(u<=0x7ff){
+ // a[pos++] = 0xC0 | (u >> 6);
+ // a[pos++] = 0x80 | (u & 63);
+ // }else if(u<=0xffff){
+ // a[pos++] = 0xe0 | (u >> 12);
+ // a[pos++] = 0x80 | ((u >> 6) & 63);
+ // a[pos++] = 0x80 | (u & 63);
+ // }else{
+ // a[pos++] = 0xf0 | (u >> 18);
+ // a[pos++] = 0x80 | ((u >> 12) & 63);
+ // a[pos++] = 0x80 | ((u >> 6) & 63);
+ // a[pos++] = 0x80 | (u & 63);
+ // }
+ // }
+ // return new Uint8Array(a);
+ };
+
+ const __affirmAlloc = (obj,funcName)=>{
+ if(!(obj.alloc instanceof Function) ||
+ !(obj.dealloc instanceof Function)){
+ toss("Object is missing alloc() and/or dealloc() function(s)",
+ "required by",funcName+"().");
+ }
+ };
+
+ const __allocCStr = function(jstr, returnWithLength, allocator, funcName){
+ __affirmAlloc(target, funcName);
+ if('string'!==typeof jstr) return null;
+ const n = target.jstrlen(jstr),
+ ptr = allocator(n+1);
+ target.jstrcpy(jstr, target.heap8u(), ptr, n+1, true);
+ return returnWithLength ? [ptr, n] : ptr;
+ };
+
+ /**
+ Uses target.alloc() to allocate enough memory for jstrlen(jstr)+1
+ bytes of memory, copies jstr to that memory using jstrcpy(),
+ NUL-terminates it, and returns the pointer to that C-string.
+ Ownership of the pointer is transfered to the caller, who must
+ eventually pass the pointer to dealloc() to free it.
+
+ If passed a truthy 2nd argument then its return semantics change:
+ it returns [ptr,n], where ptr is the C-string's pointer and n is
+ its cstrlen().
+
+ Throws if `target.alloc` or `target.dealloc` are not functions.
+ */
+ target.allocCString =
+ (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength,
+ target.alloc, 'allocCString()');
+
+ /**
+ Starts an "allocation scope." All allocations made using
+ scopedAlloc() are recorded in this scope and are freed when the
+ value returned from this function is passed to
+ scopedAllocPop().
+
+ This family of functions requires that the API's object have both
+ `alloc()` and `dealloc()` methods, else this function will throw.
+
+ Intended usage:
+
+ ```
+ const scope = scopedAllocPush();
+ try {
+ const ptr1 = scopedAlloc(100);
+ const ptr2 = scopedAlloc(200);
+ const ptr3 = scopedAlloc(300);
+ ...
+ // Note that only allocations made via scopedAlloc()
+ // are managed by this allocation scope.
+ }finally{
+ scopedAllocPop(scope);
+ }
+ ```
+
+ The value returned by this function must be treated as opaque by
+ the caller, suitable _only_ for passing to scopedAllocPop().
+ Its type and value are not part of this function's API and may
+ change in any given version of this code.
+
+ `scopedAlloc.level` can be used to determine how many scoped
+ alloc levels are currently active.
+ */
+ target.scopedAllocPush = function(){
+ __affirmAlloc(target, 'scopedAllocPush');
+ const a = [];
+ cache.scopedAlloc.push(a);
+ return a;
+ };
+
+ /**
+ Cleans up all allocations made using scopedAlloc() in the context
+ of the given opaque state object, which must be a value returned
+ by scopedAllocPush(). See that function for an example of how to
+ use this function.
+
+ Though scoped allocations are managed like a stack, this API
+ behaves properly if allocation scopes are popped in an order
+ other than the order they were pushed.
+
+ If called with no arguments, it pops the most recent
+ scopedAllocPush() result:
+
+ ```
+ scopedAllocPush();
+ try{ ... } finally { scopedAllocPop(); }
+ ```
+
+ It's generally recommended that it be passed an explicit argument
+ to help ensure that push/push are used in matching pairs, but in
+ trivial code that may be a non-issue.
+ */
+ target.scopedAllocPop = function(state){
+ __affirmAlloc(target, 'scopedAllocPop');
+ const n = arguments.length
+ ? cache.scopedAlloc.indexOf(state)
+ : cache.scopedAlloc.length-1;
+ if(n<0) toss("Invalid state object for scopedAllocPop().");
+ if(0===arguments.length) state = cache.scopedAlloc[n];
+ cache.scopedAlloc.splice(n,1);
+ for(let p; (p = state.pop()); ) target.dealloc(p);
+ };
+
+ /**
+ Allocates n bytes of memory using this.alloc() and records that
+ fact in the state for the most recent call of scopedAllocPush().
+ Ownership of the memory is given to scopedAllocPop(), which
+ will clean it up when it is called. The memory _must not_ be
+ passed to this.dealloc(). Throws if this API object is missing
+ the required `alloc()` or `dealloc()` functions or no scoped
+ alloc is active.
+
+ See scopedAllocPush() for an example of how to use this function.
+
+ The `level` property of this function can be queried to query how
+ many scoped allocation levels are currently active.
+
+ See also: scopedAllocPtr(), scopedAllocCString()
+ */
+ target.scopedAlloc = function(n){
+ if(!cache.scopedAlloc.length){
+ toss("No scopedAllocPush() scope is active.");
+ }
+ const p = target.alloc(n);
+ cache.scopedAlloc[cache.scopedAlloc.length-1].push(p);
+ return p;
+ };
+
+ Object.defineProperty(target.scopedAlloc, 'level', {
+ configurable: false, enumerable: false,
+ get: ()=>cache.scopedAlloc.length,
+ set: ()=>toss("The 'active' property is read-only.")
+ });
+
+ /**
+ Works identically to allocCString() except that it allocates the
+ memory using scopedAlloc().
+
+ Will throw if no scopedAllocPush() call is active.
+ */
+ target.scopedAllocCString =
+ (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength,
+ target.scopedAlloc, 'scopedAllocCString()');
+
+ // impl for allocMainArgv() and scopedAllocMainArgv().
+ const __allocMainArgv = function(isScoped, list){
+ if(!list.length) toss("Cannot allocate empty array.");
+ const pList = target[
+ isScoped ? 'scopedAlloc' : 'alloc'
+ ](list.length * target.ptrSizeof);
+ let i = 0;
+ list.forEach((e)=>{
+ target.setPtrValue(pList + (target.ptrSizeof * i++),
+ target[
+ isScoped ? 'scopedAllocCString' : 'allocCString'
+ ](""+e));
+ });
+ return pList;
+ };
+
+ /**
+ Creates an array, using scopedAlloc(), suitable for passing to a
+ C-level main() routine. The input is a collection with a length
+ property and a forEach() method. A block of memory list.length
+ entries long is allocated and each pointer-sized block of that
+ memory is populated with a scopedAllocCString() conversion of the
+ (""+value) of each element. Returns a pointer to the start of the
+ list, suitable for passing as the 2nd argument to a C-style
+ main() function.
+
+ Throws if list.length is falsy or scopedAllocPush() is not active.
+ */
+ target.scopedAllocMainArgv = (list)=>__allocMainArgv(true, list);
+
+ /**
+ Identical to scopedAllocMainArgv() but uses alloc() instead of
+ scopedAllocMainArgv
+ */
+ target.allocMainArgv = (list)=>__allocMainArgv(false, list);
+
+ /**
+ Wraps function call func() in a scopedAllocPush() and
+ scopedAllocPop() block, such that all calls to scopedAlloc() and
+ friends from within that call will have their memory freed
+ automatically when func() returns. If func throws or propagates
+ an exception, the scope is still popped, otherwise it returns the
+ result of calling func().
+ */
+ target.scopedAllocCall = function(func){
+ target.scopedAllocPush();
+ try{ return func() } finally{ target.scopedAllocPop() }
+ };
+
+ /** Internal impl for allocPtr() and scopedAllocPtr(). */
+ const __allocPtr = function(howMany, safePtrSize, method){
+ __affirmAlloc(target, method);
+ const pIr = safePtrSize ? 'i64' : ptrIR;
+ let m = target[method](howMany * (safePtrSize ? 8 : ptrSizeof));
+ target.setMemValue(m, 0, pIr)
+ if(1===howMany){
+ return m;
+ }
+ const a = [m];
+ for(let i = 1; i < howMany; ++i){
+ m += (safePtrSize ? 8 : ptrSizeof);
+ a[i] = m;
+ target.setMemValue(m, 0, pIr);
+ }
+ return a;
+ };
+
+ /**
+ Allocates one or more pointers as a single chunk of memory and
+ zeroes them out.
+
+ The first argument is the number of pointers to allocate. The
+ second specifies whether they should use a "safe" pointer size (8
+ bytes) or whether they may use the default pointer size
+ (typically 4 but also possibly 8).
+
+ How the result is returned depends on its first argument: if
+ passed 1, it returns the allocated memory address. If passed more
+ than one then an array of pointer addresses is returned, which
+ can optionally be used with "destructuring assignment" like this:
+
+ ```
+ const [p1, p2, p3] = allocPtr(3);
+ ```
+
+ ACHTUNG: when freeing the memory, pass only the _first_ result
+ value to dealloc(). The others are part of the same memory chunk
+ and must not be freed separately.
+
+ The reason for the 2nd argument is..
+
+ When one of the returned pointers will refer to a 64-bit value,
+ e.g. a double or int64, an that value must be written or fetched,
+ e.g. using setMemValue() or getMemValue(), it is important that
+ the pointer in question be aligned to an 8-byte boundary or else
+ it will not be fetched or written properly and will corrupt or
+ read neighboring memory. It is only safe to pass false when the
+ client code is certain that it will only get/fetch 4-byte values
+ (or smaller).
+ */
+ target.allocPtr =
+ (howMany=1, safePtrSize=true)=>__allocPtr(howMany, safePtrSize, 'alloc');
+
+ /**
+ Identical to allocPtr() except that it allocates using scopedAlloc()
+ instead of alloc().
+ */
+ target.scopedAllocPtr =
+ (howMany=1, safePtrSize=true)=>__allocPtr(howMany, safePtrSize, 'scopedAlloc');
+
+ /**
+ If target.exports[name] exists, it is returned, else an
+ exception is thrown.
+ */
+ target.xGet = function(name){
+ return target.exports[name] || toss("Cannot find exported symbol:",name);
+ };
+
+ const __argcMismatch =
+ (f,n)=>toss(f+"() requires",n,"argument(s).");
+
+ /**
+ Looks up a WASM-exported function named fname from
+ target.exports. If found, it is called, passed all remaining
+ arguments, and its return value is returned to xCall's caller. If
+ not found, an exception is thrown. This function does no
+ conversion of argument or return types, but see xWrap() and
+ xCallWrapped() for variants which do.
+
+ As a special case, if passed only 1 argument after the name and
+ that argument in an Array, that array's entries become the
+ function arguments. (This is not an ambiguous case because it's
+ not legal to pass an Array object to a WASM function.)
+ */
+ target.xCall = function(fname, ...args){
+ const f = target.xGet(fname);
+ if(!(f instanceof Function)) toss("Exported symbol",fname,"is not a function.");
+ if(f.length!==args.length) __argcMismatch(fname,f.length)
+ /* This is arguably over-pedantic but we want to help clients keep
+ from shooting themselves in the foot when calling C APIs. */;
+ return (2===arguments.length && Array.isArray(arguments[1]))
+ ? f.apply(null, arguments[1])
+ : f.apply(null, args);
+ };
+
+ /**
+ State for use with xWrap()
+ */
+ cache.xWrap = Object.create(null);
+ const xcv = cache.xWrap.convert = Object.create(null);
+ /** Map of type names to argument conversion functions. */
+ cache.xWrap.convert.arg = Object.create(null);
+ /** Map of type names to return result conversion functions. */
+ cache.xWrap.convert.result = Object.create(null);
+
+ if(target.bigIntEnabled){
+ xcv.arg.i64 = (i)=>BigInt(i);
+ }
+ xcv.arg.i32 = (i)=>(i | 0);
+ xcv.arg.i16 = (i)=>((i | 0) & 0xFFFF);
+ xcv.arg.i8 = (i)=>((i | 0) & 0xFF);
+ xcv.arg.f32 = xcv.arg.float = (i)=>Number(i).valueOf();
+ xcv.arg.f64 = xcv.arg.double = xcv.arg.f32;
+ xcv.arg.int = xcv.arg.i32;
+ xcv.result['*'] = xcv.result['pointer'] = xcv.arg['**'] = xcv.arg[ptrIR];
+ xcv.result['number'] = (v)=>Number(v);
+
+ { /* Copy certain xcv.arg[...] handlers to xcv.result[...] and
+ add pointer-style variants of them. */
+ const copyToResult = ['i8', 'i16', 'i32', 'int',
+ 'f32', 'float', 'f64', 'double'];
+ if(target.bigIntEnabled) copyToResult.push('i64');
+ for(const t of copyToResult){
+ xcv.arg[t+'*'] = xcv.result[t+'*'] = xcv.arg[ptrIR];
+ xcv.result[t] = xcv.arg[t] || toss("Missing arg converter:",t);
+ }
+ }
+
+ /**
+ In order for args of type string to work in various contexts in
+ the sqlite3 API, we need to pass them on as, variably, a C-string
+ or a pointer value. Thus for ARGs of type 'string' and
+ '*'/'pointer' we behave differently depending on whether the
+ argument is a string or not:
+
+ - If v is a string, scopeAlloc() a new C-string from it and return
+ that temp string's pointer.
+
+ - Else return the value from the arg adaptor defined for ptrIR.
+
+ TODO? Permit an Int8Array/Uint8Array and convert it to a string?
+ Would that be too much magic concentrated in one place, ready to
+ backfire?
+ */
+ xcv.arg.string = xcv.arg.utf8 = xcv.arg['pointer'] = xcv.arg['*']
+ = function(v){
+ if('string'===typeof v) return target.scopedAllocCString(v);
+ return v ? xcv.arg[ptrIR](v) : null;
+ };
+ xcv.result.string = xcv.result.utf8 = (i)=>target.cstringToJs(i);
+ xcv.result['string:free'] = xcv.result['utf8:free'] = (i)=>{
+ try { return i ? target.cstringToJs(i) : null }
+ finally{ target.dealloc(i) }
+ };
+ xcv.result.json = (i)=>JSON.parse(target.cstringToJs(i));
+ xcv.result['json:free'] = (i)=>{
+ try{ return i ? JSON.parse(target.cstringToJs(i)) : null }
+ finally{ target.dealloc(i) }
+ }
+ xcv.result['void'] = (v)=>undefined;
+ xcv.result['null'] = (v)=>v;
+
+ if(0){
+ /***
+ This idea can't currently work because we don't know the
+ signature for the func and don't have a way for the user to
+ convey it. To do this we likely need to be able to match
+ arg/result handlers by a regex, but that would incur an O(N)
+ cost as we check the regex one at a time. Another use case for
+ such a thing would be pseudotypes like "int:-1" to say that
+ the value will always be treated like -1 (which has a useful
+ case in the sqlite3 bindings).
+ */
+ xcv.arg['func-ptr'] = function(v){
+ if(!(v instanceof Function)) return xcv.arg[ptrIR];
+ const f = target.jsFuncToWasm(v, WHAT_SIGNATURE);
+ };
+ }
+
+ const __xArgAdapterCheck =
+ (t)=>xcv.arg[t] || toss("Argument adapter not found:",t);
+
+ const __xResultAdapterCheck =
+ (t)=>xcv.result[t] || toss("Result adapter not found:",t);
+
+ cache.xWrap.convertArg = (t,v)=>__xArgAdapterCheck(t)(v);
+ cache.xWrap.convertResult =
+ (t,v)=>(null===t ? v : (t ? __xResultAdapterCheck(t)(v) : undefined));
+
+ /**
+ Creates a wrapper for the WASM-exported function fname. Uses
+ xGet() to fetch the exported function (which throws on
+ error) and returns either that function or a wrapper for that
+ function which converts the JS-side argument types into WASM-side
+ types and converts the result type. If the function takes no
+ arguments and resultType is `null` then the function is returned
+ as-is, else a wrapper is created for it to adapt its arguments
+ and result value, as described below.
+
+ (If you're familiar with Emscripten's ccall() and cwrap(), this
+ function is essentially cwrap() on steroids.)
+
+ This function's arguments are:
+
+ - fname: the exported function's name. xGet() is used to fetch
+ this, so will throw if no exported function is found with that
+ name.
+
+ - resultType: the name of the result type. A literal `null` means
+ to return the original function's value as-is (mnemonic: there
+ is "null" conversion going on). Literal `undefined` or the
+ string `"void"` mean to ignore the function's result and return
+ `undefined`. Aside from those two special cases, it may be one
+ of the values described below or any mapping installed by the
+ client using xWrap.resultAdapter().
+
+ If passed 3 arguments and the final one is an array, that array
+ must contain a list of type names (see below) for adapting the
+ arguments from JS to WASM. If passed 2 arguments, more than 3,
+ or the 3rd is not an array, all arguments after the 2nd (if any)
+ are treated as type names. i.e.:
+
+ ```
+ xWrap('funcname', 'i32', 'string', 'f64');
+ // is equivalent to:
+ xWrap('funcname', 'i32', ['string', 'f64']);
+ ```
+
+ Type names are symbolic names which map the arguments to an
+ adapter function to convert, if needed, the value before passing
+ it on to WASM or to convert a return result from WASM. The list
+ of built-in names:
+
+ - `i8`, `i16`, `i32` (args and results): all integer conversions
+ which convert their argument to an integer and truncate it to
+ the given bit length.
+
+ - `N*` (args): a type name in the form `N*`, where N is a numeric
+ type name, is treated the same as WASM pointer.
+
+ - `*` and `pointer` (args): have multple semantics. They
+ behave exactly as described below for `string` args.
+
+ - `*` and `pointer` (results): are aliases for the current
+ WASM pointer numeric type.
+
+ - `**` (args): is simply a descriptive alias for the WASM pointer
+ type. It's primarily intended to mark output-pointer arguments.
+
+ - `i64` (args and results): passes the value to BigInt() to
+ convert it to an int64. Only available if bigIntEnabled is
+ true.
+
+ - `f32` (`float`), `f64` (`double`) (args and results): pass
+ their argument to Number(). i.e. the adaptor does not currently
+ distinguish between the two types of floating-point numbers.
+
+ - `number` (results): converts the result to a JS Number using
+ Number(theValue).valueOf(). Note that this is for result
+ conversions only, as it's not possible to generically know
+ which type of number to convert arguments to.
+
+ Non-numeric conversions include:
+
+ - `string` or `utf8` (args): has two different semantics in order
+ to accommodate various uses of certain C APIs
+ (e.g. output-style strings)...
+
+ - If the arg is a string, it creates a _temporary_
+ UTF-8-encoded C-string to pass to the exported function,
+ cleaning it up before the wrapper returns. If a long-lived
+ C-string pointer is required, that requires client-side code
+ to create the string, then pass its pointer to the function.
+
+ - Else the arg is assumed to be a pointer to a string the
+ client has already allocated and it's passed on as
+ a WASM pointer.
+
+ - `string` or `utf8` (results): treats the result value as a
+ const C-string, encoded as UTF-8, copies it to a JS string,
+ and returns that JS string.
+
+ - `string:free` or `utf8:free) (results): treats the result value
+ as a non-const UTF-8 C-string, ownership of which has just been
+ transfered to the caller. It copies the C-string to a JS
+ string, frees the C-string, and returns the JS string. If such
+ a result value is NULL, the JS result is `null`. Achtung: when
+ using an API which returns results from a specific allocator,
+ e.g. `my_malloc()`, this conversion _is not legal_. Instead, an
+ equivalent conversion which uses the appropriate deallocator is
+ required. For example:
+
+```js
+ target.xWrap.resultAdaptor('string:my_free',(i)=>{
+ try { return i ? target.cstringToJs(i) : null }
+ finally{ target.exports.my_free(i) }
+ };
+```
+
+ - `json` (results): treats the result as a const C-string and
+ returns the result of passing the converted-to-JS string to
+ JSON.parse(). Returns `null` if the C-string is a NULL pointer.
+
+ - `json:free` (results): works exactly like `string:free` but
+ returns the same thing as the `json` adapter. Note the
+ warning in `string:free` regarding maching allocators and
+ deallocators.
+
+ The type names for results and arguments are validated when
+ xWrap() is called and any unknown names will trigger an
+ exception.
+
+ Clients may map their own result and argument adapters using
+ xWrap.resultAdapter() and xWrap.argAdaptor(), noting that not all
+ type conversions are valid for both arguments _and_ result types
+ as they often have different memory ownership requirements.
+
+ TODOs:
+
+ - Figure out how/whether we can (semi-)transparently handle
+ pointer-type _output_ arguments. Those currently require
+ explicit handling by allocating pointers, assigning them before
+ the call using setMemValue(), and fetching them with
+ getMemValue() after the call. We may be able to automate some
+ or all of that.
+
+ - Figure out whether it makes sense to extend the arg adapter
+ interface such that each arg adapter gets an array containing
+ the results of the previous arguments in the current call. That
+ might allow some interesting type-conversion feature. Use case:
+ handling of the final argument to sqlite3_prepare_v2() depends
+ on the type (pointer vs JS string) of its 2nd
+ argument. Currently that distinction requires hand-writing a
+ wrapper for that function. That case is unusual enough that
+ abstracting it into this API (and taking on the associated
+ costs) may well not make good sense.
+ */
+ target.xWrap = function(fname, resultType, ...argTypes){
+ if(3===arguments.length && Array.isArray(arguments[2])){
+ argTypes = arguments[2];
+ }
+ const xf = target.xGet(fname);
+ if(argTypes.length!==xf.length) __argcMismatch(fname, xf.length);
+ if((null===resultType) && 0===xf.length){
+ /* Func taking no args with an as-is return. We don't need a wrapper. */
+ return xf;
+ }
+ /*Verify the arg type conversions are valid...*/;
+ if(undefined!==resultType && null!==resultType) __xResultAdapterCheck(resultType);
+ argTypes.forEach(__xArgAdapterCheck);
+ if(0===xf.length){
+ // No args to convert, so we can create a simpler wrapper...
+ return (...args)=>(args.length
+ ? __argcMismatch(fname, xf.length)
+ : cache.xWrap.convertResult(resultType, xf.call(null)));
+ }
+ return function(...args){
+ if(args.length!==xf.length) __argcMismatch(fname, xf.length);
+ const scope = target.scopedAllocPush();
+ try{
+ const rc = xf.apply(null,args.map((v,i)=>cache.xWrap.convertArg(argTypes[i], v)));
+ return cache.xWrap.convertResult(resultType, rc);
+ }finally{
+ target.scopedAllocPop(scope);
+ }
+ };
+ }/*xWrap()*/;
+
+ /** Internal impl for xWrap.resultAdapter() and argAdaptor(). */
+ const __xAdapter = function(func, argc, typeName, adapter, modeName, xcvPart){
+ if('string'===typeof typeName){
+ if(1===argc) return xcvPart[typeName];
+ else if(2===argc){
+ if(!adapter){
+ delete xcvPart[typeName];
+ return func;
+ }else if(!(adapter instanceof Function)){
+ toss(modeName,"requires a function argument.");
+ }
+ xcvPart[typeName] = adapter;
+ return func;
+ }
+ }
+ toss("Invalid arguments to",modeName);
+ };
+
+ /**
+ Gets, sets, or removes a result value adapter for use with
+ xWrap(). If passed only 1 argument, the adapter function for the
+ given type name is returned. If the second argument is explicit
+ falsy (as opposed to defaulted), the adapter named by the first
+ argument is removed. If the 2nd argument is not falsy, it must be
+ a function which takes one value and returns a value appropriate
+ for the given type name. The adapter may throw if its argument is
+ not of a type it can work with. This function throws for invalid
+ arguments.
+
+ Example:
+
+ ```
+ xWrap.resultAdapter('twice',(v)=>v+v);
+ ```
+
+ xWrap.resultAdapter() MUST NOT use the scopedAlloc() family of
+ APIs to allocate a result value. xWrap()-generated wrappers run
+ in the context of scopedAllocPush() so that argument adapters can
+ easily convert, e.g., to C-strings, and have them cleaned up
+ automatically before the wrapper returns to the caller. Likewise,
+ if a _result_ adapter uses scoped allocation, the result will be
+ freed before because they would be freed before the wrapper
+ returns, leading to chaos and undefined behavior.
+
+ Except when called as a getter, this function returns itself.
+ */
+ target.xWrap.resultAdapter = function f(typeName, adapter){
+ return __xAdapter(f, arguments.length, typeName, adapter,
+ 'resultAdaptor()', xcv.result);
+ };
+
+ /**
+ Functions identically to xWrap.resultAdapter() but applies to
+ call argument conversions instead of result value conversions.
+
+ xWrap()-generated wrappers perform argument conversion in the
+ context of a scopedAllocPush(), so any memory allocation
+ performed by argument adapters really, really, really should be
+ made using the scopedAlloc() family of functions unless
+ specifically necessary. For example:
+
+ ```
+ xWrap.argAdapter('my-string', function(v){
+ return ('string'===typeof v)
+ ? myWasmObj.scopedAllocCString(v) : null;
+ };
+ ```
+
+ Contrariwise, xWrap.resultAdapter() must _not_ use scopedAlloc()
+ to allocate its results because they would be freed before the
+ xWrap()-created wrapper returns.
+
+ Note that it is perfectly legitimate to use these adapters to
+ perform argument validation, as opposed (or in addition) to
+ conversion.
+ */
+ target.xWrap.argAdapter = function f(typeName, adapter){
+ return __xAdapter(f, arguments.length, typeName, adapter,
+ 'argAdaptor()', xcv.arg);
+ };
+
+ /**
+ Functions like xCall() but performs argument and result type
+ conversions as for xWrap(). The first argument is the name of the
+ exported function to call. The 2nd its the name of its result
+ type, as documented for xWrap(). The 3rd is an array of argument
+ type name, as documented for xWrap() (use a falsy value or an
+ empty array for nullary functions). The 4th+ arguments are
+ arguments for the call, with the special case that if the 4th
+ argument is an array, it is used as the arguments for the
+ call. Returns the converted result of the call.
+
+ This is just a thin wrapper around xWrap(). If the given function
+ is to be called more than once, it's more efficient to use
+ xWrap() to create a wrapper, then to call that wrapper as many
+ times as needed. For one-shot calls, however, this variant is
+ arguably more efficient because it will hypothetically free the
+ wrapper function quickly.
+ */
+ target.xCallWrapped = function(fname, resultType, argTypes, ...args){
+ if(Array.isArray(arguments[3])) args = arguments[3];
+ return target.xWrap(fname, resultType, argTypes||[]).apply(null, args||[]);
+ };
+
+ return target;
+};
+
+/**
+ yawl (Yet Another Wasm Loader) provides very basic wasm loader.
+ It requires a config object:
+
+ - `uri`: required URI of the WASM file to load.
+
+ - `onload(loadResult,config)`: optional callback. The first
+ argument is the result object from
+ WebAssembly.instantiate[Streaming](). The 2nd is the config
+ object passed to this function. Described in more detail below.
+
+ - `imports`: optional imports object for
+ WebAssembly.instantiate[Streaming](). The default is an empty set
+ of imports. If the module requires any imports, this object
+ must include them.
+
+ - `wasmUtilTarget`: optional object suitable for passing to
+ WhWasmUtilInstaller(). If set, it gets passed to that function
+ after the promise resolves. This function sets several properties
+ on it before passing it on to that function (which sets many
+ more):
+
+ - `module`, `instance`: the properties from the
+ instantiate[Streaming]() result.
+
+ - If `instance.exports.memory` is _not_ set then it requires that
+ `config.imports.env.memory` be set (else it throws), and
+ assigns that to `target.memory`.
+
+ - If `wasmUtilTarget.alloc` is not set and
+ `instance.exports.malloc` is, it installs
+ `wasmUtilTarget.alloc()` and `wasmUtilTarget.dealloc()`
+ wrappers for the exports `malloc` and `free` functions.
+
+ It returns a function which, when called, initiates loading of the
+ module and returns a Promise. When that Promise resolves, it calls
+ the `config.onload` callback (if set) and passes it
+ `(loadResult,config)`, where `loadResult` is the result of
+ WebAssembly.instantiate[Streaming](): an object in the form:
+
+ ```
+ {
+ module: a WebAssembly.Module,
+ instance: a WebAssembly.Instance
+ }
+ ```
+
+ (Note that the initial `then()` attached to the promise gets only
+ that object, and not the `config` one.)
+
+ Error handling is up to the caller, who may attach a `catch()` call
+ to the promise.
+*/
+self.WhWasmUtilInstaller.yawl = function(config){
+ const wfetch = ()=>fetch(config.uri, {credentials: 'same-origin'});
+ const wui = this;
+ const finalThen = function(arg){
+ //log("finalThen()",arg);
+ if(config.wasmUtilTarget){
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ const tgt = config.wasmUtilTarget;
+ tgt.module = arg.module;
+ tgt.instance = arg.instance;
+ //tgt.exports = tgt.instance.exports;
+ if(!tgt.instance.exports.memory){
+ /**
+ WhWasmUtilInstaller requires either tgt.exports.memory
+ (exported from WASM) or tgt.memory (JS-provided memory
+ imported into WASM).
+ */
+ tgt.memory = (config.imports && config.imports.env
+ && config.imports.env.memory)
+ || toss("Missing 'memory' object!");
+ }
+ if(!tgt.alloc && arg.instance.exports.malloc){
+ const exports = arg.instance.exports;
+ tgt.alloc = function(n){
+ return exports.malloc(n) || toss("Allocation of",n,"bytes failed.");
+ };
+ tgt.dealloc = function(m){exports.free(m)};
+ }
+ wui(tgt);
+ }
+ if(config.onload) config.onload(arg,config);
+ return arg /* for any then() handler attached to
+ yetAnotherWasmLoader()'s return value */;
+ };
+ const loadWasm = WebAssembly.instantiateStreaming
+ ? function loadWasmStreaming(){
+ return WebAssembly.instantiateStreaming(wfetch(), config.imports||{})
+ .then(finalThen);
+ }
+ : function loadWasmOldSchool(){ // Safari < v15
+ return wfetch()
+ .then(response => response.arrayBuffer())
+ .then(bytes => WebAssembly.instantiate(bytes, config.imports||{}))
+ .then(finalThen);
+ };
+ return loadWasm;
+}.bind(self.WhWasmUtilInstaller)/*yawl()*/;
diff --git a/ext/wasm/demo-123-worker.html b/ext/wasm/demo-123-worker.html
new file mode 100644
index 0000000..692203d
--- /dev/null
+++ b/ext/wasm/demo-123-worker.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>Hello, sqlite3</title>
+ <style>
+ .warning, .error {color: red}
+ .error {background-color: yellow}
+ body {
+ display: flex;
+ flex-direction: column;
+ font-family: monospace;
+ white-space: break-spaces;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>1-2-sqlite3 worker demo</h1>
+ <script>(function(){
+ const logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ document.body.append(ln);
+ };
+ const w = new Worker("demo-123.js?sqlite3.dir=jswasm"
+ /* Note the URL argument on that name. See
+ the notes in demo-123.js (search for
+ "importScripts") for why we need
+ that. */);
+ w.onmessage = function({data}){
+ switch(data.type){
+ case 'log':
+ logHtml(data.payload.cssClass, ...data.payload.args);
+ break;
+ default:
+ logHtml('error',"Unhandled message:",data.type);
+ };
+ };
+ })();</script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-123.html b/ext/wasm/demo-123.html
new file mode 100644
index 0000000..2046b07
--- /dev/null
+++ b/ext/wasm/demo-123.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>Hello, sqlite3</title>
+ <style>
+ .warning, .error {color: red}
+ .error {background-color: yellow}
+ body {
+ display: flex;
+ flex-direction: column;
+ font-family: monospace;
+ white-space: break-spaces;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>1-2-sqlite3 demo</h1>
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="demo-123.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-123.js b/ext/wasm/demo-123.js
new file mode 100644
index 0000000..311afcc
--- /dev/null
+++ b/ext/wasm/demo-123.js
@@ -0,0 +1,289 @@
+/*
+ 2022-09-19
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A basic demonstration of the SQLite3 "OO#1" API.
+*/
+'use strict';
+(function(){
+ /**
+ Set up our output channel differently depending
+ on whether we are running in a worker thread or
+ the main (UI) thread.
+ */
+ let logHtml;
+ if(self.window === self /* UI thread */){
+ console.log("Running demo from main UI thread.");
+ logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ document.body.append(ln);
+ };
+ }else{ /* Worker thread */
+ console.log("Running demo from Worker thread.");
+ logHtml = function(cssClass,...args){
+ postMessage({
+ type:'log',
+ payload:{cssClass, args}
+ });
+ };
+ }
+ const log = (...args)=>logHtml('',...args);
+ const warn = (...args)=>logHtml('warning',...args);
+ const error = (...args)=>logHtml('error',...args);
+
+ const demo1 = function(sqlite3){
+ const capi = sqlite3.capi/*C-style API*/,
+ oo = sqlite3.oo1/*high-level OO API*/;
+ log("sqlite3 version",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ const db = new oo.DB("/mydb.sqlite3",'ct');
+ log("transient db =",db.filename);
+ /**
+ Never(!) rely on garbage collection to clean up DBs and
+ (especially) prepared statements. Always wrap their lifetimes
+ in a try/finally construct, as demonstrated below. By and
+ large, client code can entirely avoid lifetime-related
+ complications of prepared statement objects by using the
+ DB.exec() method for SQL execution.
+ */
+ try {
+ log("Create a table...");
+ db.exec("CREATE TABLE IF NOT EXISTS t(a,b)");
+ //Equivalent:
+ db.exec({
+ sql:"CREATE TABLE IF NOT EXISTS t(a,b)"
+ // ... numerous other options ...
+ });
+ // SQL can be either a string or a byte array
+ // or an array of strings which get concatenated
+ // together as-is (so be sure to end each statement
+ // with a semicolon).
+
+ log("Insert some data using exec()...");
+ let i;
+ for( i = 20; i <= 25; ++i ){
+ db.exec({
+ sql: "insert into t(a,b) values (?,?)",
+ // bind by parameter index...
+ bind: [i, i*2]
+ });
+ db.exec({
+ sql: "insert into t(a,b) values ($a,$b)",
+ // bind by parameter name...
+ bind: {$a: i * 10, $b: i * 20}
+ });
+ }
+
+ log("Insert using a prepared statement...");
+ let q = db.prepare([
+ // SQL may be a string or array of strings
+ // (concatenated w/o separators).
+ "insert into t(a,b) ",
+ "values(?,?)"
+ ]);
+ try {
+ for( i = 100; i < 103; ++i ){
+ q.bind( [i, i*2] ).step();
+ q.reset();
+ }
+ // Equivalent...
+ for( i = 103; i <= 105; ++i ){
+ q.bind(1, i).bind(2, i*2).stepReset();
+ }
+ }finally{
+ q.finalize();
+ }
+
+ log("Query data with exec() using rowMode 'array'...");
+ db.exec({
+ sql: "select a from t order by a limit 3",
+ rowMode: 'array', // 'array' (default), 'object', or 'stmt'
+ callback: function(row){
+ log("row ",++this.counter,"=",row);
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode 'object'...");
+ db.exec({
+ sql: "select a as aa, b as bb from t order by aa limit 3",
+ rowMode: 'object',
+ callback: function(row){
+ log("row ",++this.counter,"=",JSON.stringify(row));
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode 'stmt'...");
+ db.exec({
+ sql: "select a from t order by a limit 3",
+ rowMode: 'stmt',
+ callback: function(row){
+ log("row ",++this.counter,"get(0) =",row.get(0));
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode INTEGER (result column index)...");
+ db.exec({
+ sql: "select a, b from t order by a limit 3",
+ rowMode: 1, // === result column 1
+ callback: function(row){
+ log("row ",++this.counter,"b =",row);
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() using rowMode $COLNAME (result column name)...");
+ db.exec({
+ sql: "select a a, b from t order by a limit 3",
+ rowMode: '$a',
+ callback: function(value){
+ log("row ",++this.counter,"a =",value);
+ }.bind({counter: 0})
+ });
+
+ log("Query data with exec() without a callback...");
+ let resultRows = [];
+ db.exec({
+ sql: "select a, b from t order by a limit 3",
+ rowMode: 'object',
+ resultRows: resultRows
+ });
+ log("Result rows:",JSON.stringify(resultRows,undefined,2));
+
+ log("Create a scalar UDF...");
+ db.createFunction({
+ name: 'twice',
+ xFunc: function(pCx, arg){ // note the call arg count
+ return arg + arg;
+ }
+ });
+ log("Run scalar UDF and collect result column names...");
+ let columnNames = [];
+ db.exec({
+ sql: "select a, twice(a), twice(''||a) from t order by a desc limit 3",
+ columnNames: columnNames,
+ rowMode: 'stmt',
+ callback: function(row){
+ log("a =",row.get(0), "twice(a) =", row.get(1),
+ "twice(''||a) =",row.get(2));
+ }
+ });
+ log("Result column names:",columnNames);
+
+ try{
+ log("The following use of the twice() UDF will",
+ "fail because of incorrect arg count...");
+ db.exec("select twice(1,2,3)");
+ }catch(e){
+ warn("Got expected exception:",e.message);
+ }
+
+ try {
+ db.transaction( function(D) {
+ D.exec("delete from t");
+ log("In transaction: count(*) from t =",db.selectValue("select count(*) from t"));
+ throw new sqlite3.SQLite3Error("Demonstrating transaction() rollback");
+ });
+ }catch(e){
+ if(e instanceof sqlite3.SQLite3Error){
+ log("Got expected exception from db.transaction():",e.message);
+ log("count(*) from t =",db.selectValue("select count(*) from t"));
+ }else{
+ throw e;
+ }
+ }
+
+ try {
+ db.savepoint( function(D) {
+ D.exec("delete from t");
+ log("In savepoint: count(*) from t =",db.selectValue("select count(*) from t"));
+ D.savepoint(function(DD){
+ const rows = [];
+ DD.exec({
+ sql: ["insert into t(a,b) values(99,100);",
+ "select count(*) from t"],
+ rowMode: 0,
+ resultRows: rows
+ });
+ log("In nested savepoint. Row count =",rows[0]);
+ throw new sqlite3.SQLite3Error("Demonstrating nested savepoint() rollback");
+ })
+ });
+ }catch(e){
+ if(e instanceof sqlite3.SQLite3Error){
+ log("Got expected exception from nested db.savepoint():",e.message);
+ log("count(*) from t =",db.selectValue("select count(*) from t"));
+ }else{
+ throw e;
+ }
+ }
+ }finally{
+ db.close();
+ }
+
+ log("That's all, folks!");
+
+ /**
+ Some of the features of the OO API not demonstrated above...
+
+ - get change count (total or statement-local, 32- or 64-bit)
+ - get a DB's file name
+
+ Misc. Stmt features:
+
+ - Various forms of bind()
+ - clearBindings()
+ - reset()
+ - Various forms of step()
+ - Variants of get() for explicit type treatment/conversion,
+ e.g. getInt(), getFloat(), getBlob(), getJSON()
+ - getColumnName(ndx), getColumnNames()
+ - getParamIndex(name)
+ */
+ }/*demo1()*/;
+
+ log("Loading and initializing sqlite3 module...");
+ if(self.window!==self) /*worker thread*/{
+ /*
+ If sqlite3.js is in a directory other than this script, in order
+ to get sqlite3.js to resolve sqlite3.wasm properly, we have to
+ explicitly tell it where sqlite3.js is being loaded from. We do
+ that by passing the `sqlite3.dir=theDirName` URL argument to
+ _this_ script. That URL argument will be seen by the JS/WASM
+ loader and it will adjust the sqlite3.wasm path accordingly. If
+ sqlite3.js/.wasm are in the same directory as this script then
+ that's not needed.
+
+ URL arguments passed as part of the filename via importScripts()
+ are simply lost, and such scripts see the self.location of
+ _this_ script.
+ */
+ let sqlite3Js = 'sqlite3.js';
+ const urlParams = new URL(self.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
+ }
+ importScripts(sqlite3Js);
+ }
+ self.sqlite3InitModule({
+ // We can redirect any stdout/stderr from the module
+ // like so...
+ print: log,
+ printErr: error
+ }).then(function(sqlite3){
+ //console.log('sqlite3 =',sqlite3);
+ log("Done initializing. Running demo...");
+ try {
+ demo1(sqlite3);
+ }catch(e){
+ error("Exception:",e.message);
+ }
+ });
+})();
diff --git a/ext/wasm/demo-jsstorage.html b/ext/wasm/demo-jsstorage.html
new file mode 100644
index 0000000..79f4a3b
--- /dev/null
+++ b/ext/wasm/demo-jsstorage.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3-kvvfs.js tests</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3-kvvfs.js tests</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <fieldset>
+ <legend>Options</legend>
+ <div class='toolbar'>
+ <button id='btn-clear-log'>Clear log</button>
+ <button id='btn-clear-storage'>Clear storage</button>
+ <button id='btn-init-db'>(Re)init db</button>
+ <button id='btn-select1'>Select db rows</button>
+ <button id='btn-storage-size'>Approx. storage size</button>
+ </div>
+ </fieldset>
+ <div id='test-output'></div>
+ <style>
+ .toolbar {
+ display: flex;
+ }
+ .toolbar > * { margin: 0.25em; }
+ fieldset { border-radius: 0.5em; }
+ </style>
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="demo-jsstorage.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-jsstorage.js b/ext/wasm/demo-jsstorage.js
new file mode 100644
index 0000000..cf820e4
--- /dev/null
+++ b/ext/wasm/demo-jsstorage.js
@@ -0,0 +1,114 @@
+/*
+ 2022-09-12
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A basic test script for sqlite3.wasm with kvvfs support. This file
+ must be run in main JS thread and sqlite3.js must have been loaded
+ before it.
+*/
+'use strict';
+(function(){
+ const T = self.SqliteTestUtil;
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const debug = console.debug.bind(console);
+ const eOutput = document.querySelector('#test-output');
+ const logC = console.log.bind(console)
+ const logE = function(domElement){
+ eOutput.append(domElement);
+ };
+ const logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ logE(ln);
+ }
+ const log = function(...args){
+ logC(...args);
+ logHtml('',...args);
+ };
+ const warn = function(...args){
+ logHtml('warning',...args);
+ };
+ const error = function(...args){
+ logHtml('error',...args);
+ };
+
+ const runTests = function(sqlite3){
+ const capi = sqlite3.capi,
+ oo = sqlite3.oo1,
+ wasm = sqlite3.wasm;
+ log("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ T.assert( 0 !== capi.sqlite3_vfs_find(null) );
+ if(!capi.sqlite3_vfs_find('kvvfs')){
+ error("This build is not kvvfs-capable.");
+ return;
+ }
+
+ const dbStorage = 0 ? 'session' : 'local';
+ const theStore = 's'===dbStorage[0] ? sessionStorage : localStorage;
+ const db = new oo.JsStorageDb( dbStorage );
+ // Or: oo.DB(dbStorage, 'c', 'kvvfs')
+ log("db.storageSize():",db.storageSize());
+ document.querySelector('#btn-clear-storage').addEventListener('click',function(){
+ const sz = db.clearStorage();
+ log("kvvfs",db.filename+"Storage cleared:",sz,"entries.");
+ });
+ document.querySelector('#btn-clear-log').addEventListener('click',function(){
+ eOutput.innerText = '';
+ });
+ document.querySelector('#btn-init-db').addEventListener('click',function(){
+ try{
+ const saveSql = [];
+ db.exec({
+ sql: ["drop table if exists t;",
+ "create table if not exists t(a);",
+ "insert into t(a) values(?),(?),(?)"],
+ bind: [performance.now() >> 0,
+ (performance.now() * 2) >> 0,
+ (performance.now() / 2) >> 0],
+ saveSql
+ });
+ console.log("saveSql =",saveSql,theStore);
+ log("DB (re)initialized.");
+ }catch(e){
+ error(e.message);
+ }
+ });
+ const btnSelect = document.querySelector('#btn-select1');
+ btnSelect.addEventListener('click',function(){
+ log("DB rows:");
+ try{
+ db.exec({
+ sql: "select * from t order by a",
+ rowMode: 0,
+ callback: (v)=>log(v)
+ });
+ }catch(e){
+ error(e.message);
+ }
+ });
+ document.querySelector('#btn-storage-size').addEventListener('click',function(){
+ log("size.storageSize(",dbStorage,") says", db.storageSize(),
+ "bytes");
+ });
+ log("Storage backend:",db.filename);
+ if(0===db.selectValue('select count(*) from sqlite_master')){
+ log("DB is empty. Use the init button to populate it.");
+ }else{
+ log("DB contains data from a previous session. Use the Clear Ctorage button to delete it.");
+ btnSelect.click();
+ }
+ };
+
+ sqlite3InitModule(self.sqlite3TestModule).then((sqlite3)=>{
+ runTests(sqlite3);
+ });
+})();
diff --git a/ext/wasm/demo-worker1-promiser.html b/ext/wasm/demo-worker1-promiser.html
new file mode 100644
index 0000000..e99131e
--- /dev/null
+++ b/ext/wasm/demo-worker1-promiser.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>worker-promise tests</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>worker-promise tests</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <div>Most stuff on this page happens in the dev console.</div>
+ <hr>
+ <div id='test-output'></div>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="jswasm/sqlite3-worker1-promiser.js"></script>
+ <script src="demo-worker1-promiser.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-worker1-promiser.js b/ext/wasm/demo-worker1-promiser.js
new file mode 100644
index 0000000..a65cc31
--- /dev/null
+++ b/ext/wasm/demo-worker1-promiser.js
@@ -0,0 +1,270 @@
+/*
+ 2022-08-23
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ Demonstration of the sqlite3 Worker API #1 Promiser: a Promise-based
+ proxy for for the sqlite3 Worker #1 API.
+*/
+'use strict';
+(function(){
+ const T = self.SqliteTestUtil;
+ const eOutput = document.querySelector('#test-output');
+ const warn = console.warn.bind(console);
+ const error = console.error.bind(console);
+ const log = console.log.bind(console);
+ const logHtml = async function(cssClass,...args){
+ log.apply(this, args);
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOutput.append(ln);
+ };
+
+ let startTime;
+ const testCount = async ()=>{
+ logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms");
+ };
+
+ //why is this triggered even when we catch() a Promise?
+ //window.addEventListener('unhandledrejection', function(event) {
+ // warn('unhandledrejection',event);
+ //});
+
+ const promiserConfig = {
+ worker: ()=>{
+ const w = new Worker("jswasm/sqlite3-worker1.js");
+ w.onerror = (event)=>error("worker.onerror",event);
+ return w;
+ },
+ debug: 1 ? undefined : (...args)=>console.debug('worker debug',...args),
+ onunhandled: function(ev){
+ error("Unhandled worker message:",ev.data);
+ },
+ onready: function(){
+ self.sqlite3TestModule.setStatus(null)/*hide the HTML-side is-loading spinner*/;
+ runTests();
+ },
+ onerror: function(ev){
+ error("worker1 error:",ev);
+ }
+ };
+ const workerPromise = self.sqlite3Worker1Promiser(promiserConfig);
+ delete self.sqlite3Worker1Promiser;
+
+ const wtest = async function(msgType, msgArgs, callback){
+ if(2===arguments.length && 'function'===typeof msgArgs){
+ callback = msgArgs;
+ msgArgs = undefined;
+ }
+ const p = workerPromise({type: msgType, args:msgArgs});
+ return callback ? p.then(callback).finally(testCount) : p;
+ };
+
+ const runTests = async function(){
+ const dbFilename = '/testing2.sqlite3';
+ startTime = performance.now();
+
+ let sqConfig;
+ await wtest('config-get', (ev)=>{
+ const r = ev.result;
+ log('sqlite3.config subset:', r);
+ T.assert('boolean' === typeof r.bigIntEnabled)
+ .assert('string'===typeof r.wasmfsOpfsDir)
+ .assert('boolean' === typeof r.wasmfsOpfsEnabled);
+ sqConfig = r;
+ });
+ logHtml('',
+ "Sending 'open' message and waiting for its response before continuing...");
+
+ await wtest('open', {
+ filename: dbFilename,
+ simulateError: 0 /* if true, fail the 'open' */,
+ }, function(ev){
+ const r = ev.result;
+ log("then open result",r);
+ T.assert(ev.dbId === r.dbId)
+ .assert(ev.messageId)
+ .assert('string' === typeof r.vfs);
+ promiserConfig.dbId = ev.dbId;
+ }).then(runTests2);
+ };
+
+ const runTests2 = async function(){
+ const mustNotReach = ()=>toss("This is not supposed to be reached.");
+
+ await wtest('exec',{
+ sql: ["create table t(a,b)",
+ "insert into t(a,b) values(1,2),(3,4),(5,6)"
+ ].join(';'),
+ multi: true,
+ resultRows: [], columnNames: []
+ }, function(ev){
+ ev = ev.result;
+ T.assert(0===ev.resultRows.length)
+ .assert(0===ev.columnNames.length);
+ });
+
+ await wtest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [],
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0][0])
+ .assert(6===ev.resultRows[2][1])
+ .assert(2===ev.columnNames.length)
+ .assert('b'===ev.columnNames[1]);
+ });
+
+ await wtest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [],
+ rowMode: 'object'
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0].a)
+ .assert(6===ev.resultRows[2].b)
+ });
+
+ await wtest(
+ 'exec',
+ {sql:'intentional_error'},
+ mustNotReach
+ ).catch((e)=>{
+ warn("Intentional error:",e);
+ });
+
+ await wtest('exec',{
+ sql:'select 1 union all select 3',
+ resultRows: [],
+ }, function(ev){
+ ev = ev.result;
+ T.assert(2 === ev.resultRows.length)
+ .assert(1 === ev.resultRows[0][0])
+ .assert(3 === ev.resultRows[1][0]);
+ });
+
+ const resultRowTest1 = function f(ev){
+ if(undefined === f.counter) f.counter = 0;
+ if(null === ev.rowNumber){
+ /* End of result set. */
+ T.assert(undefined === ev.row)
+ .assert(2===ev.columnNames.length)
+ .assert('a'===ev.columnNames[0])
+ .assert('B'===ev.columnNames[1]);
+ }else{
+ T.assert(ev.rowNumber > 0);
+ ++f.counter;
+ }
+ log("exec() result row:",ev);
+ T.assert(null === ev.rowNumber || 'number' === typeof ev.row.B);
+ };
+ await wtest('exec',{
+ sql: 'select a a, b B from t order by a limit 3',
+ callback: resultRowTest1,
+ rowMode: 'object'
+ }, function(ev){
+ T.assert(3===resultRowTest1.counter);
+ resultRowTest1.counter = 0;
+ });
+
+ const resultRowTest2 = function f(ev){
+ if(null === ev.rowNumber){
+ /* End of result set. */
+ T.assert(undefined === ev.row)
+ .assert(1===ev.columnNames.length)
+ .assert('a'===ev.columnNames[0])
+ }else{
+ T.assert(ev.rowNumber > 0);
+ f.counter = ev.rowNumber;
+ }
+ log("exec() result row:",ev);
+ T.assert(null === ev.rowNumber || 'number' === typeof ev.row);
+ };
+ await wtest('exec',{
+ sql: 'select a a from t limit 3',
+ callback: resultRowTest2,
+ rowMode: 0
+ }, function(ev){
+ T.assert(3===resultRowTest2.counter);
+ });
+
+ const resultRowTest3 = function f(ev){
+ if(null === ev.rowNumber){
+ T.assert(3===ev.columnNames.length)
+ .assert('foo'===ev.columnNames[0])
+ .assert('bar'===ev.columnNames[1])
+ .assert('baz'===ev.columnNames[2]);
+ }else{
+ f.counter = ev.rowNumber;
+ T.assert('number' === typeof ev.row);
+ }
+ };
+ await wtest('exec',{
+ sql: "select 'foo' foo, a bar, 'baz' baz from t limit 2",
+ callback: resultRowTest3,
+ columnNames: [],
+ rowMode: ':bar'
+ }, function(ev){
+ log("exec() result row:",ev);
+ T.assert(2===resultRowTest3.counter);
+ });
+
+ await wtest('exec',{
+ multi: true,
+ sql:[
+ 'pragma foreign_keys=0;',
+ // ^^^ arbitrary query with no result columns
+ 'select a, b from t order by a desc; select a from t;'
+ // multi-exec only honors results from the first
+ // statement with result columns (regardless of whether)
+ // it has any rows).
+ ],
+ rowMode: 1,
+ resultRows: []
+ },function(ev){
+ const rows = ev.result.resultRows;
+ T.assert(3===rows.length).
+ assert(6===rows[0]);
+ });
+
+ await wtest('exec',{sql: 'delete from t where a>3'});
+
+ await wtest('exec',{
+ sql: 'select count(a) from t',
+ resultRows: []
+ },function(ev){
+ ev = ev.result;
+ T.assert(1===ev.resultRows.length)
+ .assert(2===ev.resultRows[0][0]);
+ });
+
+ await wtest('export', function(ev){
+ ev = ev.result;
+ T.assert('string' === typeof ev.filename)
+ .assert(ev.byteArray instanceof Uint8Array)
+ .assert(ev.byteArray.length > 1024)
+ .assert('application/x-sqlite3' === ev.mimetype);
+ });
+
+ /***** close() tests must come last. *****/
+ await wtest('close',{},function(ev){
+ T.assert('string' === typeof ev.result.filename);
+ });
+
+ await wtest('close', (ev)=>{
+ T.assert(undefined === ev.result.filename);
+ }).finally(()=>logHtml('',"That's all, folks!"));
+ }/*runTests2()*/;
+
+ log("Init complete, but async init bits may still be running.");
+})();
diff --git a/ext/wasm/demo-worker1.html b/ext/wasm/demo-worker1.html
new file mode 100644
index 0000000..c766ffd
--- /dev/null
+++ b/ext/wasm/demo-worker1.html
@@ -0,0 +1,34 @@
+
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3-worker1.js tests</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3-worker1.js tests</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <div>Most stuff on this page happens in the dev console.</div>
+ <hr>
+ <div id='test-output'></div>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="demo-worker1.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/demo-worker1.js b/ext/wasm/demo-worker1.js
new file mode 100644
index 0000000..cc63f3a
--- /dev/null
+++ b/ext/wasm/demo-worker1.js
@@ -0,0 +1,345 @@
+/*
+ 2022-05-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A basic test script for sqlite3-worker1.js.
+
+ Note that the wrapper interface demonstrated in
+ demo-worker1-promiser.js is much easier to use from client code, as it
+ lacks the message-passing acrobatics demonstrated in this file.
+*/
+'use strict';
+(function(){
+ const T = self.SqliteTestUtil;
+ const SW = new Worker("jswasm/sqlite3-worker1.js");
+ const DbState = {
+ id: undefined
+ };
+ const eOutput = document.querySelector('#test-output');
+ const log = console.log.bind(console);
+ const logHtml = function(cssClass,...args){
+ log.apply(this, args);
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOutput.append(ln);
+ };
+ const warn = console.warn.bind(console);
+ const error = console.error.bind(console);
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+
+ SW.onerror = function(event){
+ error("onerror",event);
+ };
+
+ let startTime;
+
+ /**
+ A queue for callbacks which are to be run in response to async
+ DB commands. See the notes in runTests() for why we need
+ this. The event-handling plumbing of this file requires that
+ any DB command which includes a `messageId` property also have
+ a queued callback entry, as the existence of that property in
+ response payloads is how it knows whether or not to shift an
+ entry off of the queue.
+ */
+ const MsgHandlerQueue = {
+ queue: [],
+ id: 0,
+ push: function(type,callback){
+ this.queue.push(callback);
+ return type + '-' + (++this.id);
+ },
+ shift: function(){
+ return this.queue.shift();
+ }
+ };
+
+ const testCount = ()=>{
+ logHtml("","Total test count:",T.counter+". Total time =",(performance.now() - startTime),"ms");
+ };
+
+ const logEventResult = function(ev){
+ const evd = ev.result;
+ logHtml(evd.errorClass ? 'error' : '',
+ "runOneTest",ev.messageId,"Worker time =",
+ (ev.workerRespondTime - ev.workerReceivedTime),"ms.",
+ "Round-trip event time =",
+ (performance.now() - ev.departureTime),"ms.",
+ (evd.errorClass ? ev.message : "")//, JSON.stringify(evd)
+ );
+ };
+
+ const runOneTest = function(eventType, eventArgs, callback){
+ T.assert(eventArgs && 'object'===typeof eventArgs);
+ /* ^^^ that is for the testing and messageId-related code, not
+ a hard requirement of all of the Worker-exposed APIs. */
+ const messageId = MsgHandlerQueue.push(eventType,function(ev){
+ logEventResult(ev);
+ if(callback instanceof Function){
+ callback(ev);
+ testCount();
+ }
+ });
+ const msg = {
+ type: eventType,
+ args: eventArgs,
+ dbId: DbState.id,
+ messageId: messageId,
+ departureTime: performance.now()
+ };
+ log("Posting",eventType,"message to worker dbId="+(DbState.id||'default')+':',msg);
+ SW.postMessage(msg);
+ };
+
+ /** Methods which map directly to onmessage() event.type keys.
+ They get passed the inbound event.data. */
+ const dbMsgHandler = {
+ open: function(ev){
+ DbState.id = ev.dbId;
+ log("open result",ev);
+ },
+ exec: function(ev){
+ log("exec result",ev);
+ },
+ export: function(ev){
+ log("export result",ev);
+ },
+ error: function(ev){
+ error("ERROR from the worker:",ev);
+ logEventResult(ev);
+ },
+ resultRowTest1: function f(ev){
+ if(undefined === f.counter) f.counter = 0;
+ if(null === ev.rowNumber){
+ /* End of result set. */
+ T.assert(undefined === ev.row)
+ .assert(Array.isArray(ev.columnNames))
+ .assert(ev.columnNames.length);
+ }else{
+ T.assert(ev.rowNumber > 0);
+ ++f.counter;
+ }
+ //log("exec() result row:",ev);
+ T.assert(null === ev.rowNumber || 'number' === typeof ev.row.b);
+ }
+ };
+
+ /**
+ "The problem" now is that the test results are async. We
+ know, however, that the messages posted to the worker will
+ be processed in the order they are passed to it, so we can
+ create a queue of callbacks to handle them. The problem
+ with that approach is that it's not error-handling
+ friendly, in that an error can cause us to bypass a result
+ handler queue entry. We have to perform some extra
+ acrobatics to account for that.
+
+ Problem #2 is that we cannot simply start posting events: we
+ first have to post an 'open' event, wait for it to respond, and
+ collect its db ID before continuing. If we don't wait, we may
+ well fire off 10+ messages before the open actually responds.
+ */
+ const runTests2 = function(){
+ const mustNotReach = ()=>{
+ throw new Error("This is not supposed to be reached.");
+ };
+ runOneTest('exec',{
+ sql: ["create table t(a,b);",
+ "insert into t(a,b) values(1,2),(3,4),(5,6)"
+ ],
+ resultRows: [], columnNames: []
+ }, function(ev){
+ ev = ev.result;
+ T.assert(0===ev.resultRows.length)
+ .assert(0===ev.columnNames.length);
+ });
+ runOneTest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [], saveSql:[]
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0][0])
+ .assert(6===ev.resultRows[2][1])
+ .assert(2===ev.columnNames.length)
+ .assert('b'===ev.columnNames[1]);
+ });
+ //if(1){ error("Returning prematurely for testing."); return; }
+ runOneTest('exec',{
+ sql: 'select a a, b b from t order by a',
+ resultRows: [], columnNames: [],
+ rowMode: 'object'
+ }, function(ev){
+ ev = ev.result;
+ T.assert(3===ev.resultRows.length)
+ .assert(1===ev.resultRows[0].a)
+ .assert(6===ev.resultRows[2].b)
+ });
+ runOneTest('exec',{sql:'intentional_error'}, mustNotReach);
+ // Ensure that the message-handler queue survives ^^^ that error...
+ runOneTest('exec',{
+ sql:'select 1',
+ resultRows: [],
+ //rowMode: 'array', // array is the default in the Worker interface
+ }, function(ev){
+ ev = ev.result;
+ T.assert(1 === ev.resultRows.length)
+ .assert(1 === ev.resultRows[0][0]);
+ });
+ runOneTest('exec',{
+ sql: 'select a a, b b from t order by a',
+ callback: 'resultRowTest1',
+ rowMode: 'object'
+ }, function(ev){
+ T.assert(3===dbMsgHandler.resultRowTest1.counter);
+ dbMsgHandler.resultRowTest1.counter = 0;
+ });
+ runOneTest('exec',{
+ sql:[
+ "pragma foreign_keys=0;",
+ // ^^^ arbitrary query with no result columns
+ "select a, b from t order by a desc;",
+ "select a from t;"
+ // multi-statement exec only honors results from the first
+ // statement with result columns (regardless of whether)
+ // it has any rows).
+ ],
+ rowMode: 1,
+ resultRows: []
+ },function(ev){
+ const rows = ev.result.resultRows;
+ T.assert(3===rows.length).
+ assert(6===rows[0]);
+ });
+ runOneTest('exec',{sql: 'delete from t where a>3'});
+ runOneTest('exec',{
+ sql: 'select count(a) from t',
+ resultRows: []
+ },function(ev){
+ ev = ev.result;
+ T.assert(1===ev.resultRows.length)
+ .assert(2===ev.resultRows[0][0]);
+ });
+ runOneTest('export',{}, function(ev){
+ ev = ev.result;
+ log("export result:",ev);
+ T.assert('string' === typeof ev.filename)
+ .assert(ev.byteArray instanceof Uint8Array)
+ .assert(ev.byteArray.length > 1024)
+ .assert('application/x-sqlite3' === ev.mimetype);
+ });
+ /***** close() tests must come last. *****/
+ runOneTest('close',{unlink:true},function(ev){
+ ev = ev.result;
+ T.assert('string' === typeof ev.filename);
+ });
+ runOneTest('close',{unlink:true},function(ev){
+ ev = ev.result;
+ T.assert(undefined === ev.filename);
+ logHtml('warning',"This is the final test.");
+ });
+ logHtml('warning',"Finished posting tests. Waiting on async results.");
+ };
+
+ const runTests = function(){
+ /**
+ Design decision time: all remaining tests depend on the 'open'
+ command having succeeded. In order to support multiple DBs, the
+ upcoming commands ostensibly have to know the ID of the DB they
+ want to talk to. We have two choices:
+
+ 1) We run 'open' and wait for its response, which contains the
+ db id.
+
+ 2) We have the Worker automatically use the current "default
+ db" (the one which was most recently opened) if no db id is
+ provided in the message. When we do this, the main thread may
+ well fire off _all_ of the test messages before the 'open'
+ actually responds, but because the messages are handled on a
+ FIFO basis, those after the initial 'open' will pick up the
+ "default" db. However, if the open fails, then all pending
+ messages (until next next 'open', at least) except for 'close'
+ will fail and we have no way of cancelling them once they've
+ been posted to the worker.
+
+ Which approach we use below depends on the boolean value of
+ waitForOpen.
+ */
+ const waitForOpen = 1,
+ simulateOpenError = 0 /* if true, the remaining tests will
+ all barf if waitForOpen is
+ false. */;
+ logHtml('',
+ "Sending 'open' message and",(waitForOpen ? "" : "NOT ")+
+ "waiting for its response before continuing.");
+ startTime = performance.now();
+ runOneTest('open', {
+ filename:'testing2.sqlite3',
+ simulateError: simulateOpenError
+ }, function(ev){
+ log("open result",ev);
+ T.assert('testing2.sqlite3'===ev.result.filename)
+ .assert(ev.dbId)
+ .assert(ev.messageId)
+ .assert('string' === typeof ev.result.vfs);
+ DbState.id = ev.dbId;
+ if(waitForOpen) setTimeout(runTests2, 0);
+ });
+ if(!waitForOpen) runTests2();
+ };
+
+ SW.onmessage = function(ev){
+ if(!ev.data || 'object'!==typeof ev.data){
+ warn("Unknown sqlite3-worker message type:",ev);
+ return;
+ }
+ ev = ev.data/*expecting a nested object*/;
+ //log("main window onmessage:",ev);
+ if(ev.result && ev.messageId){
+ /* We're expecting a queued-up callback handler. */
+ const f = MsgHandlerQueue.shift();
+ if('error'===ev.type){
+ dbMsgHandler.error(ev);
+ return;
+ }
+ T.assert(f instanceof Function);
+ f(ev);
+ return;
+ }
+ switch(ev.type){
+ case 'sqlite3-api':
+ switch(ev.result){
+ case 'worker1-ready':
+ log("Message:",ev);
+ self.sqlite3TestModule.setStatus(null);
+ runTests();
+ return;
+ default:
+ warn("Unknown sqlite3-api message type:",ev);
+ return;
+ }
+ default:
+ if(dbMsgHandler.hasOwnProperty(ev.type)){
+ try{dbMsgHandler[ev.type](ev);}
+ catch(err){
+ error("Exception while handling db result message",
+ ev,":",err);
+ }
+ return;
+ }
+ warn("Unknown sqlite3-api message type:",ev);
+ }
+ };
+ log("Init complete, but async init bits may still be running.");
+ log("Installing Worker into global scope SW for dev purposes.");
+ self.SW = SW;
+})();
diff --git a/ext/wasm/dist.make b/ext/wasm/dist.make
new file mode 100644
index 0000000..5aee8af
--- /dev/null
+++ b/ext/wasm/dist.make
@@ -0,0 +1,101 @@
+#!/do/not/make
+#^^^ help emacs select edit mode
+#
+# Intended to include'd by ./GNUmakefile.
+#
+# 'make dist' rules for creating a distribution archive of the WASM/JS
+# pieces, noting that we only build a dist of the built files, not the
+# numerous pieces required to build them.
+#######################################################################
+MAKEFILE.dist := $(lastword $(MAKEFILE_LIST))
+
+########################################################################
+# Chicken/egg situation: we need $(bin.version-info) to get the version
+# info for the archive name, but that binary may not yet be built, and
+# won't be built until we expand the dependencies. We have to use a
+# temporary name for the archive.
+dist-name = sqlite-wasm-TEMP
+#ifeq (0,1)
+# $(info WARNING *******************************************************************)
+# $(info ** Be sure to create the desired build configuration before creating the)
+# $(info ** distribution archive. Use one of the following targets to do so:)
+# $(info **)
+# $(info ** o2: builds with -O2, resulting in the fastest builds)
+# $(info ** oz: builds with -Oz, resulting in the smallest builds)
+# $(info /WARNING *******************************************************************)
+#endif
+
+########################################################################
+# dist.build must be the name of a target which triggers the
+# build of the files to be packed into the dist archive. The
+# intention is that it be one of (o0, o1, o2, o3, os, oz), each of
+# which uses like-named -Ox optimization level flags. The o2 target
+# provides the best overall runtime speeds. The oz target provides
+# slightly slower speeds (roughly 10%) with significantly smaller WASM
+# file sizes. Note that -O2 (the o2 target) results in faster binaries
+# than both -O3 and -Os (the o3 and os targets) in all tests run to
+# date.
+dist.build ?= oz
+
+dist-dir.top := $(dist-name)
+dist-dir.jswasm := $(dist-dir.top)/$(notdir $(dir.dout))
+dist-dir.common := $(dist-dir.top)/common
+dist.top.extras := \
+ demo-123.html demo-123-worker.html demo-123.js \
+ tester1.html tester1-worker.html tester1.js \
+ demo-jsstorage.html demo-jsstorage.js \
+ demo-worker1.html demo-worker1.js \
+ demo-worker1-promiser.html demo-worker1-promiser.js
+dist.jswasm.extras := $(sqlite3-api.ext.jses) $(sqlite3.wasm)
+dist.common.extras := \
+ $(wildcard $(dir.common)/*.css) \
+ $(dir.common)/SqliteTestUtil.js
+
+.PHONY: dist
+########################################################################
+# dist: create the end-user deliverable archive.
+#
+# Maintenance reminder: because dist depends on $(dist.build), and
+# $(dist.build) will depend on clean, having any deps on
+# $(dist-archive) which themselves may be cleaned up by the clean
+# target will lead to grief in parallel builds (-j #). Thus
+# $(dist-target)'s deps must be trimmed to non-generated files or
+# files which are _not_ cleaned up by the clean target.
+#
+# Note that we require $(bin.version-info) in order to figure out the
+# dist file's name, so cannot (without a recursive make) have the
+# target name equal to the archive name.
+dist: \
+ $(bin.stripccomments) $(bin.version-info) \
+ $(dist.build) \
+ $(MAKEFILE) $(MAKEFILE.dist)
+ @echo "Making end-user deliverables..."
+ @rm -fr $(dist-dir.top)
+ @mkdir -p $(dist-dir.jswasm) $(dist-dir.common)
+ @cp -p $(dist.top.extras) $(dist-dir.top)
+ @cp -p README-dist.txt $(dist-dir.top)/README.txt
+ @cp -p index-dist.html $(dist-dir.top)/index.html
+ @cp -p $(dist.jswasm.extras) $(dist-dir.jswasm)
+ @$(bin.stripccomments) -k -k < $(sqlite3.js) \
+ > $(dist-dir.jswasm)/$(notdir $(sqlite3.js))
+ @cp -p $(dist.common.extras) $(dist-dir.common)
+ @set -e; \
+ vnum=$$($(bin.version-info) --download-version); \
+ vdir=sqlite-wasm-$$vnum; \
+ arczip=$$vdir.zip; \
+ echo "Making $$arczip ..."; \
+ rm -fr $$arczip $$vdir; \
+ mv $(dist-dir.top) $$vdir; \
+ zip -qr $$arczip $$vdir; \
+ rm -fr $$vdir; \
+ ls -la $$arczip; \
+ set +e; \
+ unzip -lv $$arczip || echo "Missing unzip app? Not fatal."
+
+# We need a separate `clean` rule to account for weirdness in
+# a sub-make, where we get a copy of the $(dist-name) dir
+# copied into the new $(dist-name) dir.
+.PHONY: dist-clean
+clean: dist-clean
+dist-clean:
+ rm -fr $(dist-name) $(wildcard sqlite-wasm-*.zip)
diff --git a/ext/wasm/fiddle.make b/ext/wasm/fiddle.make
new file mode 100644
index 0000000..43e6941
--- /dev/null
+++ b/ext/wasm/fiddle.make
@@ -0,0 +1,194 @@
+#!/do/not/make
+#^^^ help emacs select edit mode
+#
+# Intended to include'd by ./GNUmakefile.
+#######################################################################
+MAKEFILE.fiddle := $(lastword $(MAKEFILE_LIST))
+
+########################################################################
+# shell.c and its build flags...
+make-np-0 := make -C $(dir.top) -n -p
+make-np-1 := sed -e 's/(TOP)/(dir.top)/g'
+$(eval $(shell $(make-np-0) | grep -e '^SHELL_OPT ' | $(make-np-1)))
+$(eval $(shell $(make-np-0) | grep -e '^SHELL_SRC ' | $(make-np-1)))
+# ^^^ can't do that in 1 invocation b/c newlines get stripped
+ifeq (,$(SHELL_OPT))
+$(error Could not parse SHELL_OPT from $(dir.top)/Makefile.)
+endif
+ifeq (,$(SHELL_SRC))
+$(error Could not parse SHELL_SRC from $(dir.top)/Makefile.)
+endif
+$(dir.top)/shell.c: $(SHELL_SRC) $(dir.top)/tool/mkshellc.tcl
+ $(MAKE) -C $(dir.top) shell.c
+# /shell.c
+########################################################################
+
+EXPORTED_FUNCTIONS.fiddle := $(dir.tmp)/EXPORTED_FUNCTIONS.fiddle
+fiddle.emcc-flags = \
+ $(emcc.cflags) $(emcc_opt_full) \
+ --minify 0 \
+ -sALLOW_TABLE_GROWTH \
+ -sABORTING_MALLOC \
+ -sSTRICT_JS \
+ -sENVIRONMENT=web,worker \
+ -sMODULARIZE \
+ -sDYNAMIC_EXECUTION=0 \
+ -sWASM_BIGINT=$(emcc.WASM_BIGINT) \
+ -sEXPORT_NAME=$(sqlite3.js.init-func) \
+ -Wno-limited-postlink-optimizations \
+ $(sqlite3.js.flags.--post-js) \
+ $(emcc.exportedRuntimeMethods) \
+ -sEXPORTED_FUNCTIONS=@$(abspath $(EXPORTED_FUNCTIONS.fiddle)) \
+ $(SQLITE_OPT) $(SHELL_OPT) \
+ -DSQLITE_SHELL_FIDDLE
+# -D_POSIX_C_SOURCE is needed for strdup() with emcc
+
+fiddle.EXPORTED_FUNCTIONS.in := \
+ EXPORTED_FUNCTIONS.fiddle.in \
+ $(EXPORTED_FUNCTIONS.api)
+
+$(EXPORTED_FUNCTIONS.fiddle): $(fiddle.EXPORTED_FUNCTIONS.in) $(MAKEFILE.fiddle)
+ sort -u $(fiddle.EXPORTED_FUNCTIONS.in) > $@
+
+fiddle-module.js := $(dir.fiddle)/fiddle-module.js
+fiddle-module.wasm := $(subst .js,.wasm,$(fiddle-module.js))
+fiddle.cses := $(dir.top)/shell.c $(sqlite3-wasm.c)
+
+fiddle.SOAP.js := $(dir.fiddle)/$(notdir $(SOAP.js))
+$(fiddle.SOAP.js): $(SOAP.js)
+ cp $< $@
+
+$(eval $(call call-make-pre-js,fiddle-module))
+$(fiddle-module.js): $(MAKEFILE) $(MAKEFILE.fiddle) \
+ $(EXPORTED_FUNCTIONS.fiddle) \
+ $(fiddle.cses) $(pre-post-fiddle-module.deps) $(fiddle.SOAP.js)
+ $(emcc.bin) -o $@ $(fiddle.emcc-flags) \
+ $(pre-post-common.flags) $(pre-post-fiddle-module.flags) \
+ $(fiddle.cses)
+ $(maybe-wasm-strip) $(fiddle-module.wasm)
+ gzip < $@ > $@.gz
+ gzip < $(fiddle-module.wasm) > $(fiddle-module.wasm).gz
+
+$(dir.fiddle)/fiddle.js.gz: $(dir.fiddle)/fiddle.js
+ gzip < $< > $@
+
+clean: clean-fiddle
+clean-fiddle:
+ rm -f $(fiddle-module.js) $(fiddle-module.js).gz \
+ $(fiddle-module.wasm) $(fiddle-module.wasm).gz \
+ $(dir.fiddle)/$(SOAP.js) \
+ $(dir.fiddle)/fiddle-module.worker.js \
+ EXPORTED_FUNCTIONS.fiddle
+.PHONY: fiddle
+fiddle: $(fiddle-module.js) $(dir.fiddle)/fiddle.js.gz
+all: fiddle
+
+########################################################################
+# fiddle_remote is the remote destination for the fiddle app. It
+# must be a [user@]HOST:/path for rsync.
+# Note that the target "should probably" contain a symlink of
+# index.html -> fiddle.html.
+fiddle_remote ?=
+ifeq (,$(fiddle_remote))
+ifneq (,$(wildcard /home/stephan))
+ fiddle_remote = wh:www/wh/sqlite3/.
+else ifneq (,$(wildcard /home/drh))
+ #fiddle_remote = if appropriate, add that user@host:/path here
+endif
+endif
+push-fiddle: fiddle
+ @if [ x = "x$(fiddle_remote)" ]; then \
+ echo "fiddle_remote must be a [user@]HOST:/path for rsync"; \
+ exit 1; \
+ fi
+ rsync -va fiddle/ $(fiddle_remote)
+# end fiddle remote push
+########################################################################
+
+
+########################################################################
+# Explanation of the emcc build flags follows. Full docs for these can
+# be found at:
+#
+# https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
+#
+# -sENVIRONMENT=web: elides bootstrap code related to non-web JS
+# environments like node.js. Removing this makes the output a tiny
+# tick larger but hypothetically makes it more portable to
+# non-browser JS environments.
+#
+# -sMODULARIZE: changes how the generated code is structured to avoid
+# declaring a global Module object and instead installing a function
+# which loads and initializes the module. The function is named...
+#
+# -sEXPORT_NAME=jsFunctionName (see -sMODULARIZE)
+#
+# -sEXPORTED_RUNTIME_METHODS=@/absolute/path/to/file: a file
+# containing a list of emscripten-supplied APIs, one per line, which
+# must be exported into the generated JS. Must be an absolute path!
+#
+# -sEXPORTED_FUNCTIONS=@/absolute/path/to/file: a file containing a
+# list of C functions, one per line, which must be exported via wasm
+# so they're visible to JS. C symbols names in that file must all
+# start with an underscore for reasons known only to the emcc
+# developers. e.g., _sqlite3_open_v2 and _sqlite3_finalize. Must be
+# an absolute path!
+#
+# -sSTRICT_JS ensures that the emitted JS code includes the 'use
+# strict' option. Note that -sSTRICT is more broadly-scoped and
+# results in build errors.
+#
+# -sALLOW_TABLE_GROWTH is required for (at a minimum) the UDF-binding
+# feature. Without it, JS functions cannot be made to proxy C-side
+# callbacks.
+#
+# -sABORTING_MALLOC causes the JS-bound _malloc() to abort rather than
+# return 0 on OOM. If set to 0 then all code which uses _malloc()
+# must, just like in C, check the result before using it, else
+# they're likely to corrupt the JS/WASM heap by writing to its
+# address of 0. It is, as of this writing, enabled in Emscripten by
+# default but we enable it explicitly in case that default changes.
+#
+# -sDYNAMIC_EXECUTION=0 disables eval() and the Function constructor.
+# If the build runs without these, it's preferable to use this flag
+# because certain execution environments disallow those constructs.
+# This flag is not strictly necessary, however.
+#
+# -sWASM_BIGINT is UNTESTED but "should" allow the int64-using C APIs
+# to work with JS/wasm, insofar as the JS environment supports the
+# BigInt type. That support requires an extremely recent browser:
+# Safari didn't get that support until late 2020.
+#
+# --no-entry: for compiling library code with no main(). If this is
+# not supplied and the code has a main(), it is called as part of the
+# module init process. Note that main() is #if'd out of shell.c
+# (renamed) when building in wasm mode.
+#
+# --pre-js/--post-js=FILE relative or absolute paths to JS files to
+# prepend/append to the emcc-generated bootstrapping JS. It's
+# easier/faster to develop with separate JS files (reduces rebuilding
+# requirements) but certain configurations, namely -sMODULARIZE, may
+# require using at least a --pre-js file. They can be used
+# individually and need not be paired.
+#
+# -O0..-O3 and -Oz: optimization levels affect not only C-style
+# optimization but whether or not the resulting generated JS code
+# gets minified. -O0 compiles _much_ more quickly than -O3 or -Oz,
+# and doesn't minimize any JS code, so is recommended for
+# development. -O3 or -Oz are recommended for deployment, but
+# primarily because -Oz will shrink the wasm file notably. JS-side
+# minification makes little difference in terms of overall
+# distributable size.
+#
+# --minify 0: disables minification of the generated JS code,
+# regardless of optimization level. Minification of the JS has
+# minimal overall effect in the larger scheme of things and results
+# in JS files which can neither be edited nor viewed as text files in
+# Fossil (which flags them as binary because of their extreme line
+# lengths). Interestingly, whether or not the comments in the
+# generated JS file get stripped is unaffected by this setting and
+# depends entirely on the optimization level. Higher optimization
+# levels reduce the size of the JS considerably even without
+# minification.
+#
+########################################################################
diff --git a/ext/wasm/fiddle/emscripten.css b/ext/wasm/fiddle/emscripten.css
new file mode 100644
index 0000000..7e3dc81
--- /dev/null
+++ b/ext/wasm/fiddle/emscripten.css
@@ -0,0 +1,24 @@
+/* emcscript-related styling, used during the module load/intialization processes... */
+.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+div.emscripten { text-align: center; }
+div.emscripten_border { border: 1px solid black; }
+#module-spinner { overflow: visible; }
+#module-spinner > * {
+ margin-top: 1em;
+}
+.spinner {
+ height: 50px;
+ width: 50px;
+ margin: 0px auto;
+ animation: rotation 0.8s linear infinite;
+ border-left: 10px solid rgb(0,150,240);
+ border-right: 10px solid rgb(0,150,240);
+ border-bottom: 10px solid rgb(0,150,240);
+ border-top: 10px solid rgb(100,0,200);
+ border-radius: 100%;
+ background-color: rgb(200,100,250);
+}
+@keyframes rotation {
+ from {transform: rotate(0deg);}
+ to {transform: rotate(360deg);}
+}
diff --git a/ext/wasm/fiddle/fiddle-worker.js b/ext/wasm/fiddle/fiddle-worker.js
new file mode 100644
index 0000000..a60b79a
--- /dev/null
+++ b/ext/wasm/fiddle/fiddle-worker.js
@@ -0,0 +1,379 @@
+/*
+ 2022-05-20
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This is the JS Worker file for the sqlite3 fiddle app. It loads the
+ sqlite3 wasm module and offers access to the db via the Worker
+ message-passing interface.
+
+ Forewarning: this API is still very much Under Construction and
+ subject to any number of changes as experience reveals what those
+ need to be.
+
+ Because we can have only a single message handler, as opposed to an
+ arbitrary number of discrete event listeners like with DOM elements,
+ we have to define a lower-level message API. Messages abstractly
+ look like:
+
+ { type: string, data: type-specific value }
+
+ Where 'type' is used for dispatching and 'data' is a
+ 'type'-dependent value.
+
+ The 'type' values expected by each side of the main/worker
+ connection vary. The types are described below but subject to
+ change at any time as this experiment evolves.
+
+ Workers-to-Main types
+
+ - stdout, stderr: indicate stdout/stderr output from the wasm
+ layer. The data property is the string of the output, noting
+ that the emscripten binding emits these one line at a time. Thus,
+ if a C-side puts() emits multiple lines in a single call, the JS
+ side will see that as multiple calls. Example:
+
+ {type:'stdout', data: 'Hi, world.'}
+
+ - module: Status text. This is intended to alert the main thread
+ about module loading status so that, e.g., the main thread can
+ update a progress widget and DTRT when the module is finished
+ loading and available for work. Status messages come in the form
+
+ {type:'module', data:{
+ type:'status',
+ data: {text:string|null, step:1-based-integer}
+ }
+
+ with an incrementing step value for each subsequent message. When
+ the module loading is complete, a message with a text value of
+ null is posted.
+
+ - working: data='start'|'end'. Indicates that work is about to be
+ sent to the module or has just completed. This can be used, e.g.,
+ to disable UI elements which should not be activated while work
+ is pending. Example:
+
+ {type:'working', data:'start'}
+
+ Main-to-Worker types:
+
+ - shellExec: data=text to execute as if it had been entered in the
+ sqlite3 CLI shell app (as opposed to sqlite3_exec()). This event
+ causes the worker to emit a 'working' event (data='start') before
+ it starts and a 'working' event (data='end') when it finished. If
+ called while work is currently being executed it emits stderr
+ message instead of doing actual work, as the underlying db cannot
+ handle concurrent tasks. Example:
+
+ {type:'shellExec', data: 'select * from sqlite_master'}
+
+ - More TBD as the higher-level db layer develops.
+*/
+
+/*
+ Apparent browser(s) bug: console messages emitted may be duplicated
+ in the console, even though they're provably only run once. See:
+
+ https://stackoverflow.com/questions/49659464
+
+ Noting that it happens in Firefox as well as Chrome. Harmless but
+ annoying.
+*/
+"use strict";
+(function(){
+ /**
+ Posts a message in the form {type,data}. If passed more than 2
+ args, the 3rd must be an array of "transferable" values to pass
+ as the 2nd argument to postMessage(). */
+ const wMsg =
+ (type,data,transferables)=>{
+ postMessage({type, data}, transferables || []);
+ };
+ const stdout = (...args)=>wMsg('stdout', args);
+ const stderr = (...args)=>wMsg('stderr', args);
+ const toss = (...args)=>{
+ throw new Error(args.join(' '));
+ };
+ const fixmeOPFS = "(FIXME: won't work with OPFS-over-sqlite3_vfs.)";
+ let sqlite3 /* gets assigned when the wasm module is loaded */;
+
+ self.onerror = function(/*message, source, lineno, colno, error*/) {
+ const err = arguments[4];
+ if(err && 'ExitStatus'==err.name){
+ /* This is relevant for the sqlite3 shell binding but not the
+ lower-level binding. */
+ fiddleModule.isDead = true;
+ stderr("FATAL ERROR:", err.message);
+ stderr("Restarting the app requires reloading the page.");
+ wMsg('error', err);
+ }
+ console.error(err);
+ fiddleModule.setStatus('Exception thrown, see JavaScript console: '+err);
+ };
+
+ const Sqlite3Shell = {
+ /** Returns the name of the currently-opened db. */
+ dbFilename: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_db_filename', "string", ['string']);
+ return f._(0);
+ },
+ dbHandle: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap("fiddle_db_handle", "sqlite3*");
+ return f._();
+ },
+ dbIsOpfs: function f(){
+ return sqlite3.opfs && sqlite3.capi.sqlite3_js_db_uses_vfs(
+ this.dbHandle(), "opfs"
+ );
+ },
+ runMain: function f(){
+ if(f.argv) return 0===f.argv.rc;
+ const dbName = "/fiddle.sqlite3";
+ f.argv = [
+ 'sqlite3-fiddle.wasm',
+ '-bail', '-safe',
+ dbName
+ /* Reminder: because of how we run fiddle, we have to ensure
+ that any argv strings passed to its main() are valid until
+ the wasm environment shuts down. */
+ ];
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ /* We need to call sqlite3_shutdown() in order to avoid numerous
+ legitimate warnings from the shell about it being initialized
+ after sqlite3_initialize() has been called. This means,
+ however, that any initialization done by the JS code may need
+ to be re-done (e.g. re-registration of dynamically-loaded
+ VFSes). We need a more generic approach to running such
+ init-level code. */
+ capi.sqlite3_shutdown();
+ f.argv.pArgv = wasm.allocMainArgv(f.argv);
+ f.argv.rc = wasm.exports.fiddle_main(
+ f.argv.length, f.argv.pArgv
+ );
+ if(f.argv.rc){
+ stderr("Fatal error initializing sqlite3 shell.");
+ fiddleModule.isDead = true;
+ return false;
+ }
+ stdout("SQLite version", capi.sqlite3_libversion(),
+ capi.sqlite3_sourceid().substr(0,19));
+ stdout('Welcome to the "fiddle" shell.');
+ if(sqlite3.opfs){
+ stdout("\nOPFS is available. To open a persistent db, use:\n\n",
+ " .open file:name?vfs=opfs\n\nbut note that some",
+ "features (e.g. upload) do not yet work with OPFS.");
+ sqlite3.opfs.registerVfs();
+ }
+ stdout('\nEnter ".help" for usage hints.');
+ this.exec([ // initialization commands...
+ '.nullvalue NULL',
+ '.headers on'
+ ].join('\n'));
+ return true;
+ },
+ /**
+ Runs the given text through the shell as if it had been typed
+ in by a user. Fires a working/start event before it starts and
+ working/end event when it finishes.
+ */
+ exec: function f(sql){
+ if(!f._){
+ if(!this.runMain()) return;
+ f._ = sqlite3.wasm.xWrap('fiddle_exec', null, ['string']);
+ }
+ if(fiddleModule.isDead){
+ stderr("shell module has exit()ed. Cannot run SQL.");
+ return;
+ }
+ wMsg('working','start');
+ try {
+ if(f._running){
+ stderr('Cannot run multiple commands concurrently.');
+ }else if(sql){
+ if(Array.isArray(sql)) sql = sql.join('');
+ f._running = true;
+ f._(sql);
+ }
+ }finally{
+ delete f._running;
+ wMsg('working','end');
+ }
+ },
+ resetDb: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_reset_db', null);
+ stdout("Resetting database.");
+ f._();
+ stdout("Reset",this.dbFilename());
+ },
+ /* Interrupt can't work: this Worker is tied up working, so won't get the
+ interrupt event which would be needed to perform the interrupt. */
+ interrupt: function f(){
+ if(!f._) f._ = sqlite3.wasm.xWrap('fiddle_interrupt', null);
+ stdout("Requesting interrupt.");
+ f._();
+ }
+ };
+
+ self.onmessage = function f(ev){
+ ev = ev.data;
+ if(!f.cache){
+ f.cache = {
+ prevFilename: null
+ };
+ }
+ //console.debug("worker: onmessage.data",ev);
+ switch(ev.type){
+ case 'shellExec': Sqlite3Shell.exec(ev.data); return;
+ case 'db-reset': Sqlite3Shell.resetDb(); return;
+ case 'interrupt': Sqlite3Shell.interrupt(); return;
+ /** Triggers the export of the current db. Fires an
+ event in the form:
+
+ {type:'db-export',
+ data:{
+ filename: name of db,
+ buffer: contents of the db file (Uint8Array),
+ error: on error, a message string and no buffer property.
+ }
+ }
+ */
+ case 'db-export': {
+ const fn = Sqlite3Shell.dbFilename();
+ stdout("Exporting",fn+".");
+ const fn2 = fn ? fn.split(/[/\\]/).pop() : null;
+ try{
+ if(!fn2) toss("DB appears to be closed.");
+ const buffer = sqlite3.capi.sqlite3_js_db_export(
+ Sqlite3Shell.dbHandle()
+ );
+ wMsg('db-export',{filename: fn2, buffer: buffer.buffer}, [buffer.buffer]);
+ }catch(e){
+ console.error("Export failed:",e);
+ /* Post a failure message so that UI elements disabled
+ during the export can be re-enabled. */
+ wMsg('db-export',{
+ filename: fn,
+ error: e.message
+ });
+ }
+ return;
+ }
+ case 'open': {
+ /* Expects: {
+ buffer: ArrayBuffer | Uint8Array,
+ filename: the filename for the db. Any dir part is
+ stripped.
+ }
+ */
+ const opt = ev.data;
+ let buffer = opt.buffer;
+ stderr('open():',fixmeOPFS);
+ if(buffer instanceof ArrayBuffer){
+ buffer = new Uint8Array(buffer);
+ }else if(!(buffer instanceof Uint8Array)){
+ stderr("'open' expects {buffer:Uint8Array} containing an uploaded db.");
+ return;
+ }
+ const fn = (
+ opt.filename
+ ? opt.filename.split(/[/\\]/).pop().replace('"','_')
+ : ("db-"+((Math.random() * 10000000) | 0)+
+ "-"+((Math.random() * 10000000) | 0)+".sqlite3")
+ );
+ try {
+ /* We cannot delete the existing db file until the new one
+ is installed, which means that we risk overflowing our
+ quota (if any) by having both the previous and current
+ db briefly installed in the virtual filesystem. */
+ const fnAbs = '/'+fn;
+ const oldName = Sqlite3Shell.dbFilename();
+ if(oldName && oldName===fnAbs){
+ /* We cannot create the replacement file while the current file
+ is opened, nor does the shell have a .close command, so we
+ must temporarily switch to another db... */
+ Sqlite3Shell.exec('.open :memory:');
+ fiddleModule.FS.unlink(fnAbs);
+ }
+ fiddleModule.FS.createDataFile("/", fn, buffer, true, true);
+ Sqlite3Shell.exec('.open "'+fnAbs+'"');
+ if(oldName && oldName!==fnAbs){
+ try{fiddleModule.fsUnlink(oldName)}
+ catch(e){/*ignored*/}
+ }
+ stdout("Replaced DB with",fn+".");
+ }catch(e){
+ stderr("Error installing db",fn+":",e.message);
+ }
+ return;
+ }
+ };
+ console.warn("Unknown fiddle-worker message type:",ev);
+ };
+
+ /**
+ emscripten module for use with build mode -sMODULARIZE.
+ */
+ const fiddleModule = {
+ print: stdout,
+ printErr: stderr,
+ /**
+ Intercepts status updates from the emscripting module init
+ and fires worker events with a type of 'status' and a
+ payload of:
+
+ {
+ text: string | null, // null at end of load process
+ step: integer // starts at 1, increments 1 per call
+ }
+
+ We have no way of knowing in advance how many steps will
+ be processed/posted, so creating a "percentage done" view is
+ not really practical. One can be approximated by giving it a
+ current value of message.step and max value of message.step+1,
+ though.
+
+ When work is finished, a message with a text value of null is
+ submitted.
+
+ After a message with text==null is posted, the module may later
+ post messages about fatal problems, e.g. an exit() being
+ triggered, so it is recommended that UI elements for posting
+ status messages not be outright removed from the DOM when
+ text==null, and that they instead be hidden until/unless
+ text!=null.
+ */
+ setStatus: function f(text){
+ if(!f.last) f.last = { step: 0, text: '' };
+ else if(text === f.last.text) return;
+ f.last.text = text;
+ wMsg('module',{
+ type:'status',
+ data:{step: ++f.last.step, text: text||null}
+ });
+ }
+ };
+
+ importScripts('fiddle-module.js'+self.location.search);
+ /**
+ initFiddleModule() is installed via fiddle-module.js due to
+ building with:
+
+ emcc ... -sMODULARIZE=1 -sEXPORT_NAME=initFiddleModule
+ */
+ sqlite3InitModule(fiddleModule).then((_sqlite3)=>{
+ sqlite3 = _sqlite3;
+ const dbVfs = sqlite3.wasm.xWrap('fiddle_db_vfs', "*", ['string']);
+ fiddleModule.fsUnlink = (fn)=>{
+ return sqlite3.wasm.sqlite3_wasm_vfs_unlink(dbVfs(0), fn);
+ };
+ wMsg('fiddle-ready');
+ })/*then()*/;
+})();
diff --git a/ext/wasm/fiddle/fiddle.js b/ext/wasm/fiddle/fiddle.js
new file mode 100644
index 0000000..2a3d174
--- /dev/null
+++ b/ext/wasm/fiddle/fiddle.js
@@ -0,0 +1,815 @@
+/*
+ 2022-05-20
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ This is the main entry point for the sqlite3 fiddle app. It sets up the
+ various UI bits, loads a Worker for the db connection, and manages the
+ communication between the UI and worker.
+*/
+(function(){
+ 'use strict';
+ /* Recall that the 'self' symbol, except where locally
+ overwritten, refers to the global window or worker object. */
+
+ const storage = (function(NS/*namespace object in which to store this module*/){
+ /* Pedantic licensing note: this code originated in the Fossil SCM
+ source tree, where it has a different license, but the person who
+ ported it into sqlite is the same one who wrote it for fossil. */
+ 'use strict';
+ NS = NS||{};
+
+ /**
+ This module provides a basic wrapper around localStorage
+ or sessionStorage or a dummy proxy object if neither
+ of those are available.
+ */
+ const tryStorage = function f(obj){
+ if(!f.key) f.key = 'storage.access.check';
+ try{
+ obj.setItem(f.key, 'f');
+ const x = obj.getItem(f.key);
+ obj.removeItem(f.key);
+ if(x!=='f') throw new Error(f.key+" failed")
+ return obj;
+ }catch(e){
+ return undefined;
+ }
+ };
+
+ /** Internal storage impl for this module. */
+ const $storage =
+ tryStorage(window.localStorage)
+ || tryStorage(window.sessionStorage)
+ || tryStorage({
+ // A basic dummy xyzStorage stand-in
+ $$$:{},
+ setItem: function(k,v){this.$$$[k]=v},
+ getItem: function(k){
+ return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
+ },
+ removeItem: function(k){delete this.$$$[k]},
+ clear: function(){this.$$$={}}
+ });
+
+ /**
+ For the dummy storage we need to differentiate between
+ $storage and its real property storage for hasOwnProperty()
+ to work properly...
+ */
+ const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
+
+ /**
+ A prefix which gets internally applied to all storage module
+ property keys so that localStorage and sessionStorage across the
+ same browser profile instance do not "leak" across multiple apps
+ being hosted by the same origin server. Such cross-polination is
+ still there but, with this key prefix applied, it won't be
+ immediately visible via the storage API.
+
+ With this in place we can justify using localStorage instead of
+ sessionStorage.
+
+ One implication of using localStorage and sessionStorage is that
+ their scope (the same "origin" and client application/profile)
+ allows multiple apps on the same origin to use the same
+ storage. Thus /appA/foo could then see changes made via
+ /appB/foo. The data do not cross user- or browser boundaries,
+ though, so it "might" arguably be called a
+ feature. storageKeyPrefix was added so that we can sandbox that
+ state for each separate app which shares an origin.
+
+ See: https://fossil-scm.org/forum/forumpost/4afc4d34de
+
+ Sidebar: it might seem odd to provide a key prefix and stick all
+ properties in the topmost level of the storage object. We do that
+ because adding a layer of object to sandbox each app would mean
+ (de)serializing that whole tree on every storage property change.
+ e.g. instead of storageObject.projectName.foo we have
+ storageObject[storageKeyPrefix+'foo']. That's soley for
+ efficiency's sake (in terms of battery life and
+ environment-internal storage-level effort).
+ */
+ const storageKeyPrefix = (
+ $storageHolder===$storage/*localStorage or sessionStorage*/
+ ? (
+ (NS.config ?
+ (NS.config.projectCode || NS.config.projectName
+ || NS.config.shortProjectName)
+ : false)
+ || window.location.pathname
+ )+'::' : (
+ '' /* transient storage */
+ )
+ );
+
+ /**
+ A proxy for localStorage or sessionStorage or a
+ page-instance-local proxy, if neither one is availble.
+
+ Which exact storage implementation is uses is unspecified, and
+ apps must not rely on it.
+ */
+ NS.storage = {
+ storageKeyPrefix: storageKeyPrefix,
+ /** Sets the storage key k to value v, implicitly converting
+ it to a string. */
+ set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
+ /** Sets storage key k to JSON.stringify(v). */
+ setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
+ /** Returns the value for the given storage key, or
+ dflt if the key is not found in the storage. */
+ get: (k,dflt)=>$storageHolder.hasOwnProperty(
+ storageKeyPrefix+k
+ ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
+ /** Returns true if the given key has a value of "true". If the
+ key is not found, it returns true if the boolean value of dflt
+ is "true". (Remember that JS persistent storage values are all
+ strings.) */
+ getBool: function(k,dflt){
+ return 'true'===this.get(k,''+(!!dflt));
+ },
+ /** Returns the JSON.parse()'d value of the given
+ storage key's value, or dflt is the key is not
+ found or JSON.parse() fails. */
+ getJSON: function f(k,dflt){
+ try {
+ const x = this.get(k,f);
+ return x===f ? dflt : JSON.parse(x);
+ }
+ catch(e){return dflt}
+ },
+ /** Returns true if the storage contains the given key,
+ else false. */
+ contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
+ /** Removes the given key from the storage. Returns this. */
+ remove: function(k){
+ $storage.removeItem(storageKeyPrefix+k);
+ return this;
+ },
+ /** Clears ALL keys from the storage. Returns this. */
+ clear: function(){
+ this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
+ return this;
+ },
+ /** Returns an array of all keys currently in the storage. */
+ keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
+ /** Returns true if this storage is transient (only available
+ until the page is reloaded), indicating that fileStorage
+ and sessionStorage are unavailable. */
+ isTransient: ()=>$storageHolder!==$storage,
+ /** Returns a symbolic name for the current storage mechanism. */
+ storageImplName: function(){
+ if($storage===window.localStorage) return 'localStorage';
+ else if($storage===window.sessionStorage) return 'sessionStorage';
+ else return 'transient';
+ },
+
+ /**
+ Returns a brief help text string for the currently-selected
+ storage type.
+ */
+ storageHelpDescription: function(){
+ return {
+ localStorage: "Browser-local persistent storage with an "+
+ "unspecified long-term lifetime (survives closing the browser, "+
+ "but maybe not a browser upgrade).",
+ sessionStorage: "Storage local to this browser tab, "+
+ "lost if this tab is closed.",
+ "transient": "Transient storage local to this invocation of this page."
+ }[this.storageImplName()];
+ }
+ };
+ return NS.storage;
+ })({})/*storage API setup*/;
+
+
+ /** Name of the stored copy of SqliteFiddle.config. */
+ const configStorageKey = 'sqlite3-fiddle-config';
+
+ /**
+ The SqliteFiddle object is intended to be the primary
+ app-level object for the main-thread side of the sqlite
+ fiddle application. It uses a worker thread to load the
+ sqlite WASM module and communicate with it.
+ */
+ const SF/*local convenience alias*/
+ = window.SqliteFiddle/*canonical name*/ = {
+ /* Config options. */
+ config: {
+ /* If true, SqliteFiddle.echo() will auto-scroll the
+ output widget to the bottom when it receives output,
+ else it won't. */
+ autoScrollOutput: true,
+ /* If true, the output area will be cleared before each
+ command is run, else it will not. */
+ autoClearOutput: false,
+ /* If true, SqliteFiddle.echo() will echo its output to
+ the console, in addition to its normal output widget.
+ That slows it down but is useful for testing. */
+ echoToConsole: false,
+ /* If true, display input/output areas side-by-side. */
+ sideBySide: true,
+ /* If true, swap positions of the input/output areas. */
+ swapInOut: false
+ },
+ /**
+ Emits the given text, followed by a line break, to the
+ output widget. If given more than one argument, they are
+ join()'d together with a space between each. As a special
+ case, if passed a single array, that array is used in place
+ of the arguments array (this is to facilitate receiving
+ lists of arguments via worker events).
+ */
+ echo: function f(text) {
+ /* Maintenance reminder: we currently require/expect a textarea
+ output element. It might be nice to extend this to behave
+ differently if the output element is a non-textarea element,
+ in which case it would need to append the given text as a TEXT
+ node and add a line break. */
+ if(!f._){
+ f._ = document.getElementById('output');
+ f._.value = ''; // clear browser cache
+ }
+ if(arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+ else if(1===arguments.length && Array.isArray(text)) text = text.join(' ');
+ // These replacements are necessary if you render to raw HTML
+ //text = text.replace(/&/g, "&amp;");
+ //text = text.replace(/</g, "&lt;");
+ //text = text.replace(/>/g, "&gt;");
+ //text = text.replace('\n', '<br>', 'g');
+ if(null===text){/*special case: clear output*/
+ f._.value = '';
+ return;
+ }else if(this.echo._clearPending){
+ delete this.echo._clearPending;
+ f._.value = '';
+ }
+ if(this.config.echoToConsole) console.log(text);
+ if(this.jqTerm) this.jqTerm.echo(text);
+ f._.value += text + "\n";
+ if(this.config.autoScrollOutput){
+ f._.scrollTop = f._.scrollHeight;
+ }
+ },
+ _msgMap: {},
+ /** Adds a worker message handler for messages of the given
+ type. */
+ addMsgHandler: function f(type,callback){
+ if(Array.isArray(type)){
+ type.forEach((t)=>this.addMsgHandler(t, callback));
+ return this;
+ }
+ (this._msgMap.hasOwnProperty(type)
+ ? this._msgMap[type]
+ : (this._msgMap[type] = [])).push(callback);
+ return this;
+ },
+ /** Given a worker message, runs all handlers for msg.type. */
+ runMsgHandlers: function(msg){
+ const list = (this._msgMap.hasOwnProperty(msg.type)
+ ? this._msgMap[msg.type] : false);
+ if(!list){
+ console.warn("No handlers found for message type:",msg);
+ return false;
+ }
+ //console.debug("runMsgHandlers",msg);
+ list.forEach((f)=>f(msg));
+ return true;
+ },
+ /** Removes all message handlers for the given message type. */
+ clearMsgHandlers: function(type){
+ delete this._msgMap[type];
+ return this;
+ },
+ /* Posts a message in the form {type, data} to the db worker. Returns this. */
+ wMsg: function(type,data,transferables){
+ this.worker.postMessage({type, data}, transferables || []);
+ return this;
+ },
+ /**
+ Prompts for confirmation and, if accepted, deletes
+ all content and tables in the (transient) database.
+ */
+ resetDb: function(){
+ if(window.confirm("Really destroy all content and tables "
+ +"in the (transient) db?")){
+ this.wMsg('db-reset');
+ }
+ return this;
+ },
+ /** Stores this object's config in the browser's storage. */
+ storeConfig: function(){
+ storage.setJSON(configStorageKey,this.config);
+ }
+ };
+
+ if(1){ /* Restore SF.config */
+ const storedConfig = storage.getJSON(configStorageKey);
+ if(storedConfig){
+ /* Copy all properties to SF.config which are currently in
+ storedConfig. We don't bother copying any other
+ properties: those have been removed from the app in the
+ meantime. */
+ Object.keys(SF.config).forEach(function(k){
+ if(storedConfig.hasOwnProperty(k)){
+ SF.config[k] = storedConfig[k];
+ }
+ });
+ }
+ }
+
+ SF.worker = new Worker('fiddle-worker.js'+self.location.search);
+ SF.worker.onmessage = (ev)=>SF.runMsgHandlers(ev.data);
+ SF.addMsgHandler(['stdout', 'stderr'], (ev)=>SF.echo(ev.data));
+
+ /* querySelectorAll() proxy */
+ const EAll = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelectorAll(arguments[arguments.length-1]);
+ };
+ /* querySelector() proxy */
+ const E = function(/*[element=document,] cssSelector*/){
+ return (arguments.length>1 ? arguments[0] : document)
+ .querySelector(arguments[arguments.length-1]);
+ };
+
+ /** Handles status updates from the Emscripten Module object. */
+ SF.addMsgHandler('module', function f(ev){
+ ev = ev.data;
+ if('status'!==ev.type){
+ console.warn("Unexpected module-type message:",ev);
+ return;
+ }
+ if(!f.ui){
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ const msg = ev.data;
+ if(f.ui.progres){
+ progress.value = msg.step;
+ progress.max = msg.step + 1/*we don't know how many steps to expect*/;
+ }
+ if(1==msg.step){
+ f.ui.progress.classList.remove('hidden');
+ f.ui.spinner.classList.remove('hidden');
+ }
+ if(msg.text){
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = msg.text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ /* The module can post messages about fatal problems,
+ e.g. an exit() being triggered or assertion failure,
+ after the last "load" message has arrived, so
+ leave f.ui.status and message listener intact. */
+ }
+ });
+
+ /**
+ The 'fiddle-ready' event is fired (with no payload) when the
+ wasm module has finished loading. Interestingly, that happens
+ _before_ the final module:status event */
+ SF.addMsgHandler('fiddle-ready', function(){
+ SF.clearMsgHandlers('fiddle-ready');
+ self.onSFLoaded();
+ });
+
+ /**
+ Performs all app initialization which must wait until after the
+ worker module is loaded. This function removes itself when it's
+ called.
+ */
+ self.onSFLoaded = function(){
+ delete this.onSFLoaded;
+ // Unhide all elements which start out hidden
+ EAll('.initially-hidden').forEach((e)=>e.classList.remove('initially-hidden'));
+ E('#btn-reset').addEventListener('click',()=>SF.resetDb());
+ const taInput = E('#input');
+ const btnClearIn = E('#btn-clear');
+ btnClearIn.addEventListener('click',function(){
+ taInput.value = '';
+ },false);
+ // Ctrl-enter and shift-enter both run the current SQL.
+ taInput.addEventListener('keydown',function(ev){
+ if((ev.ctrlKey || ev.shiftKey) && 13 === ev.keyCode){
+ ev.preventDefault();
+ ev.stopPropagation();
+ btnShellExec.click();
+ }
+ }, false);
+ const taOutput = E('#output');
+ const btnClearOut = E('#btn-clear-output');
+ btnClearOut.addEventListener('click',function(){
+ taOutput.value = '';
+ if(SF.jqTerm) SF.jqTerm.clear();
+ },false);
+ const btnShellExec = E('#btn-shell-exec');
+ btnShellExec.addEventListener('click',function(ev){
+ let sql;
+ ev.preventDefault();
+ if(taInput.selectionStart<taInput.selectionEnd){
+ sql = taInput.value.substring(taInput.selectionStart,taInput.selectionEnd).trim();
+ }else{
+ sql = taInput.value.trim();
+ }
+ if(sql) SF.dbExec(sql);
+ },false);
+
+ const btnInterrupt = E("#btn-interrupt");
+ //btnInterrupt.classList.add('hidden');
+ /** To be called immediately before work is sent to the
+ worker. Updates some UI elements. The 'working'/'end'
+ event will apply the inverse, undoing the bits this
+ function does. This impl is not in the 'working'/'start'
+ event handler because that event is given to us
+ asynchronously _after_ we need to have performed this
+ work.
+ */
+ const preStartWork = function f(){
+ if(!f._){
+ const title = E('title');
+ f._ = {
+ btnLabel: btnShellExec.innerText,
+ pageTitle: title,
+ pageTitleOrig: title.innerText
+ };
+ }
+ f._.pageTitle.innerText = "[working...] "+f._.pageTitleOrig;
+ btnShellExec.setAttribute('disabled','disabled');
+ btnInterrupt.removeAttribute('disabled','disabled');
+ };
+
+ /* Sends the given text to the db module to evaluate as if it
+ had been entered in the sqlite3 CLI shell. If it's null or
+ empty, this is a no-op. */
+ SF.dbExec = function f(sql){
+ if(null!==sql && this.config.autoClearOutput){
+ this.echo._clearPending = true;
+ }
+ preStartWork();
+ this.wMsg('shellExec',sql);
+ };
+
+ SF.addMsgHandler('working',function f(ev){
+ switch(ev.data){
+ case 'start': /* See notes in preStartWork(). */; return;
+ case 'end':
+ preStartWork._.pageTitle.innerText = preStartWork._.pageTitleOrig;
+ btnShellExec.innerText = preStartWork._.btnLabel;
+ btnShellExec.removeAttribute('disabled');
+ btnInterrupt.setAttribute('disabled','disabled');
+ return;
+ }
+ console.warn("Unhandled 'working' event:",ev.data);
+ });
+
+ /* For each checkbox with data-csstgt, set up a handler which
+ toggles the given CSS class on the element matching
+ E(data-csstgt). */
+ EAll('input[type=checkbox][data-csstgt]')
+ .forEach(function(e){
+ const tgt = E(e.dataset.csstgt);
+ const cssClass = e.dataset.cssclass || 'error';
+ e.checked = tgt.classList.contains(cssClass);
+ e.addEventListener('change', function(){
+ tgt.classList[
+ this.checked ? 'add' : 'remove'
+ ](cssClass)
+ }, false);
+ });
+ /* For each checkbox with data-config=X, set up a binding to
+ SF.config[X]. These must be set up AFTER data-csstgt
+ checkboxes so that those two states can be synced properly. */
+ EAll('input[type=checkbox][data-config]')
+ .forEach(function(e){
+ const confVal = !!SF.config[e.dataset.config];
+ if(e.checked !== confVal){
+ /* Ensure that data-csstgt mappings (if any) get
+ synced properly. */
+ e.checked = confVal;
+ e.dispatchEvent(new Event('change'));
+ }
+ e.addEventListener('change', function(){
+ SF.config[this.dataset.config] = this.checked;
+ SF.storeConfig();
+ }, false);
+ });
+ /* For each button with data-cmd=X, map a click handler which
+ calls SF.dbExec(X). */
+ const cmdClick = function(){SF.dbExec(this.dataset.cmd);};
+ EAll('button[data-cmd]').forEach(
+ e => e.addEventListener('click', cmdClick, false)
+ );
+
+ btnInterrupt.addEventListener('click',function(){
+ SF.wMsg('interrupt');
+ });
+
+ /** Initiate a download of the db. */
+ const btnExport = E('#btn-export');
+ const eLoadDb = E('#load-db');
+ const btnLoadDb = E('#btn-load-db');
+ btnLoadDb.addEventListener('click', ()=>eLoadDb.click());
+ /**
+ Enables (if passed true) or disables all UI elements which
+ "might," if timed "just right," interfere with an
+ in-progress db import/export/exec operation.
+ */
+ const enableMutatingElements = function f(enable){
+ if(!f._elems){
+ f._elems = [
+ /* UI elements to disable while import/export are
+ running. Normally the export is fast enough
+ that this won't matter, but we really don't
+ want to be reading (from outside of sqlite) the
+ db when the user taps btnShellExec. */
+ btnShellExec, btnExport, eLoadDb
+ ];
+ }
+ f._elems.forEach( enable
+ ? (e)=>e.removeAttribute('disabled')
+ : (e)=>e.setAttribute('disabled','disabled') );
+ };
+ btnExport.addEventListener('click',function(){
+ enableMutatingElements(false);
+ SF.wMsg('db-export');
+ });
+ SF.addMsgHandler('db-export', function(ev){
+ enableMutatingElements(true);
+ ev = ev.data;
+ if(ev.error){
+ SF.echo("Export failed:",ev.error);
+ return;
+ }
+ const blob = new Blob([ev.buffer],
+ {type:"application/x-sqlite3"});
+ const a = document.createElement('a');
+ document.body.appendChild(a);
+ a.href = window.URL.createObjectURL(blob);
+ a.download = ev.filename;
+ a.addEventListener('click',function(){
+ setTimeout(function(){
+ SF.echo("Exported (possibly auto-downloaded):",ev.filename);
+ window.URL.revokeObjectURL(a.href);
+ a.remove();
+ },500);
+ });
+ a.click();
+ });
+ /**
+ Handle load/import of an external db file.
+ */
+ eLoadDb.addEventListener('change',function(){
+ const f = this.files[0];
+ const r = new FileReader();
+ const status = {loaded: 0, total: 0};
+ enableMutatingElements(false);
+ r.addEventListener('loadstart', function(){
+ SF.echo("Loading",f.name,"...");
+ });
+ r.addEventListener('progress', function(ev){
+ SF.echo("Loading progress:",ev.loaded,"of",ev.total,"bytes.");
+ });
+ const that = this;
+ r.addEventListener('load', function(){
+ enableMutatingElements(true);
+ SF.echo("Loaded",f.name+". Opening db...");
+ SF.wMsg('open',{
+ filename: f.name,
+ buffer: this.result
+ }, [this.result]);
+ });
+ r.addEventListener('error',function(){
+ enableMutatingElements(true);
+ SF.echo("Loading",f.name,"failed for unknown reasons.");
+ });
+ r.addEventListener('abort',function(){
+ enableMutatingElements(true);
+ SF.echo("Cancelled loading of",f.name+".");
+ });
+ r.readAsArrayBuffer(f);
+ });
+
+ EAll('fieldset.collapsible').forEach(function(fs){
+ const btnToggle = E(fs,'legend > .fieldset-toggle'),
+ content = EAll(fs,':scope > div');
+ btnToggle.addEventListener('click', function(){
+ fs.classList.toggle('collapsed');
+ content.forEach((d)=>d.classList.toggle('hidden'));
+ }, false);
+ });
+
+ /**
+ Given a DOM element, this routine measures its "effective
+ height", which is the bounding top/bottom range of this element
+ and all of its children, recursively. For some DOM structure
+ cases, a parent may have a reported height of 0 even though
+ children have non-0 sizes.
+
+ Returns 0 if !e or if the element really has no height.
+ */
+ const effectiveHeight = function f(e){
+ if(!e) return 0;
+ if(!f.measure){
+ f.measure = function callee(e, depth){
+ if(!e) return;
+ const m = e.getBoundingClientRect();
+ if(0===depth){
+ callee.top = m.top;
+ callee.bottom = m.bottom;
+ }else{
+ callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
+ callee.bottom = Math.max(callee.bottom, m.bottom);
+ }
+ Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
+ if(0===depth){
+ //console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
+ f.extra += callee.bottom - callee.top;
+ }
+ return f.extra;
+ };
+ }
+ f.extra = 0;
+ f.measure(e,0);
+ return f.extra;
+ };
+
+ /**
+ Returns a function, that, as long as it continues to be invoked,
+ will not be triggered. The function will be called after it stops
+ being called for N milliseconds. If `immediate` is passed, call
+ the callback immediately and hinder future invocations until at
+ least the given time has passed.
+
+ If passed only 1 argument, or passed a falsy 2nd argument,
+ the default wait time set in this function's $defaultDelay
+ property is used.
+
+ Source: underscore.js, by way of https://davidwalsh.name/javascript-debounce-function
+ */
+ const debounce = function f(func, wait, immediate) {
+ var timeout;
+ if(!wait) wait = f.$defaultDelay;
+ return function() {
+ const context = this, args = Array.prototype.slice.call(arguments);
+ const later = function() {
+ timeout = undefined;
+ if(!immediate) func.apply(context, args);
+ };
+ const callNow = immediate && !timeout;
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ if(callNow) func.apply(context, args);
+ };
+ };
+ debounce.$defaultDelay = 500 /*arbitrary*/;
+
+ const ForceResizeKludge = (function(){
+ /* Workaround for Safari mayhem regarding use of vh CSS
+ units.... We cannot use vh units to set the main view
+ size because Safari chokes on that, so we calculate
+ that height here. Larger than ~95% is too big for
+ Firefox on Android, causing the input area to move
+ off-screen. */
+ const appViews = EAll('.app-view');
+ const elemsToCount = [
+ /* Elements which we need to always count in the
+ visible body size. */
+ E('body > header'),
+ E('body > footer')
+ ];
+ const resized = function f(){
+ if(f.$disabled) return;
+ const wh = window.innerHeight;
+ var ht;
+ var extra = 0;
+ elemsToCount.forEach((e)=>e ? extra += effectiveHeight(e) : false);
+ ht = wh - extra;
+ appViews.forEach(function(e){
+ e.style.height =
+ e.style.maxHeight = [
+ "calc(", (ht>=100 ? ht : 100), "px",
+ " - 2em"/*fudge value*/,")"
+ /* ^^^^ hypothetically not needed, but both
+ Chrome/FF on Linux will force scrollbars on the
+ body if this value is too small. */
+ ].join('');
+ });
+ };
+ resized.$disabled = true/*gets deleted when setup is finished*/;
+ window.addEventListener('resize', debounce(resized, 250), false);
+ return resized;
+ })();
+
+ /** Set up a selection list of examples */
+ (function(){
+ const xElem = E('#select-examples');
+ const examples = [
+ {name: "Help", sql: [
+ "-- ================================================\n",
+ "-- Use ctrl-enter or shift-enter to execute sqlite3\n",
+ "-- shell commands and SQL.\n",
+ "-- If a subset of the text is currently selected,\n",
+ "-- only that part is executed.\n",
+ "-- ================================================\n",
+ ".help\n"
+ ]},
+ //{name: "Timer on", sql: ".timer on"},
+ // ^^^ re-enable if emscripten re-enables getrusage()
+ {name: "Setup table T", sql:[
+ ".nullvalue NULL\n",
+ "CREATE TABLE t(a,b);\n",
+ "INSERT INTO t(a,b) VALUES('abc',123),('def',456),(NULL,789),('ghi',012);\n",
+ "SELECT * FROM t;\n"
+ ]},
+ {name: "Table list", sql: ".tables"},
+ {name: "Box Mode", sql: ".mode box"},
+ {name: "JSON Mode", sql: ".mode json"},
+ {name: "Mandlebrot", sql:[
+ "WITH RECURSIVE",
+ " xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),\n",
+ " yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),\n",
+ " m(iter, cx, cy, x, y) AS (\n",
+ " SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis\n",
+ " UNION ALL\n",
+ " SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m \n",
+ " WHERE (x*x + y*y) < 4.0 AND iter<28\n",
+ " ),\n",
+ " m2(iter, cx, cy) AS (\n",
+ " SELECT max(iter), cx, cy FROM m GROUP BY cx, cy\n",
+ " ),\n",
+ " a(t) AS (\n",
+ " SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') \n",
+ " FROM m2 GROUP BY cy\n",
+ " )\n",
+ "SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;\n",
+ ]}
+ ];
+ const newOpt = function(lbl,val){
+ const o = document.createElement('option');
+ if(Array.isArray(val)) val = val.join('');
+ o.value = val;
+ if(!val) o.setAttribute('disabled',true);
+ o.appendChild(document.createTextNode(lbl));
+ xElem.appendChild(o);
+ };
+ newOpt("Examples (replaces input!)");
+ examples.forEach((o)=>newOpt(o.name, o.sql));
+ //xElem.setAttribute('disabled',true);
+ xElem.selectedIndex = 0;
+ xElem.addEventListener('change', function(){
+ taInput.value = '-- ' +
+ this.selectedOptions[0].innerText +
+ '\n' + this.value;
+ SF.dbExec(this.value);
+ });
+ })()/* example queries */;
+
+ //SF.echo(null/*clear any output generated by the init process*/);
+ if(window.jQuery && window.jQuery.terminal){
+ /* Set up the terminal-style view... */
+ const eTerm = window.jQuery('#view-terminal').empty();
+ SF.jqTerm = eTerm.terminal(SF.dbExec.bind(SF),{
+ prompt: 'sqlite> ',
+ greetings: false /* note that the docs incorrectly call this 'greeting' */
+ });
+ /* Set up a button to toggle the views... */
+ const head = E('header#titlebar');
+ const btnToggleView = document.createElement('button');
+ btnToggleView.appendChild(document.createTextNode("Toggle View"));
+ head.appendChild(btnToggleView);
+ btnToggleView.addEventListener('click',function f(){
+ EAll('.app-view').forEach(e=>e.classList.toggle('hidden'));
+ if(document.body.classList.toggle('terminal-mode')){
+ ForceResizeKludge();
+ }
+ }, false);
+ btnToggleView.click()/*default to terminal view*/;
+ }
+ SF.echo('This experimental app is provided in the hope that it',
+ 'may prove interesting or useful but is not an officially',
+ 'supported deliverable of the sqlite project. It is subject to',
+ 'any number of changes or outright removal at any time.\n');
+ const urlParams = new URL(self.location.href).searchParams;
+ SF.dbExec(urlParams.get('sql') || null);
+ delete ForceResizeKludge.$disabled;
+ ForceResizeKludge();
+ }/*onSFLoaded()*/;
+})();
diff --git a/ext/wasm/fiddle/index.html b/ext/wasm/fiddle/index.html
new file mode 100644
index 0000000..272f1ac
--- /dev/null
+++ b/ext/wasm/fiddle/index.html
@@ -0,0 +1,278 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>SQLite3 Fiddle</title>
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <!-- to add a togglable terminal-style view, uncomment the following
+ two lines and ensure that these files are on the web server. -->
+ <!--script src="jqterm/jqterm-bundle.min.js"></script>
+ <link rel="stylesheet" href="jqterm/jquery.terminal.min.css"/-->
+ <link rel="stylesheet" href="emscripten.css"/>
+ <style>
+ /* The following styles are for app-level use. */
+ :root {
+ --sqlite-blue: #044a64;
+ --textarea-color1: #044a64;
+ --textarea-color2: white;
+ }
+ textarea {
+ font-family: monospace;
+ flex: 1 1 auto;
+ background-color: var(--textarea-color1);
+ color: var(--textarea-color2);
+ }
+ textarea#input {
+ color: var(--textarea-color1);
+ background-color: var(--textarea-color2);
+ }
+ header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: var(--sqlite-blue);
+ color: white;
+ font-size: 120%;
+ font-weight: bold;
+ border-radius: 0.25em;
+ padding: 0.2em 0.5em;
+ }
+ header > .powered-by {
+ font-size: 80%;
+ }
+ header a, header a:visited, header a:hover {
+ color: inherit;
+ }
+ #main-wrapper {
+ display: flex;
+ flex-direction: column-reverse;
+ flex: 1 1 auto;
+ margin: 0.5em 0;
+ overflow: hidden;
+ }
+ #main-wrapper.side-by-side {
+ flex-direction: row;
+ }
+ #main-wrapper.side-by-side > fieldset {
+ margin-left: 0.25em;
+ margin-right: 0.25em;
+ }
+ #main-wrapper:not(.side-by-side) > fieldset {
+ margin-bottom: 0.25em;
+ }
+ #main-wrapper.swapio {
+ flex-direction: column;
+ }
+ #main-wrapper.side-by-side.swapio {
+ flex-direction: row-reverse;
+ }
+ .zone-wrapper{
+ display: flex;
+ margin: 0;
+ flex: 1 1 0%;
+ border-radius: 0.5em;
+ min-width: inherit/*important: resolves inability to scroll fieldset child element!*/;
+ padding: 0.35em 0 0 0;
+ }
+ .zone-wrapper textarea {
+ border-radius: 0.5em;
+ flex: 1 1 auto;
+ /*min/max width resolve an inexplicable margin on the RHS. The -1em
+ is for the padding, else we overlap the parent boundaries.*/
+ /*min-width: calc(100% - 1em);
+ max-width: calc(100% - 1em);
+ padding: 0 0.5em;*/
+ }
+
+ .zone-wrapper.input { flex: 10 1 auto; }
+ .zone-wrapper.output { flex: 20 1 auto; }
+ .zone-wrapper > div {
+ display:flex;
+ flex: 1 1 0%;
+ }
+ .zone-wrapper.output {}
+ .button-bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ align-content: space-between;
+ justify-content: flex-start;
+ }
+ .button-bar > * {
+ margin: 0.05em 0.5em 0.05em 0;
+ flex: 0 1 auto;
+ align-self: auto;
+ }
+ label[for] {
+ cursor: pointer;
+ }
+ .error {
+ color: red;
+ background-color: yellow;
+ }
+ .hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+ }
+ fieldset {
+ border-radius: 0.5em;
+ border: 1px inset;
+ padding: 0.25em;
+ }
+ fieldset.options {
+ font-size: 80%;
+ margin-top: 0.5em;
+ }
+ fieldset:not(.options) > legend {
+ font-size: 80%;
+ }
+ fieldset.options > div {
+ display: flex;
+ flex-wrap: wrap;
+ }
+ fieldset button {
+ font-size: inherit;
+ }
+ fieldset.collapsible > legend > .fieldset-toggle::after {
+ content: " [hide]";
+ position: relative;
+ }
+ fieldset.collapsible.collapsed > legend > .fieldset-toggle::after {
+ content: " [show]";
+ position: relative;
+ }
+ span.labeled-input {
+ padding: 0.25em;
+ margin: 0.05em 0.25em;
+ border-radius: 0.25em;
+ white-space: nowrap;
+ background: #0002;
+ display: flex;
+ align-items: center;
+ }
+ span.labeled-input > *:nth-child(2) {
+ margin-left: 0.3em;
+ }
+ .center { text-align: center; }
+ body.terminal-mode {
+ max-height: calc(100% - 2em);
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ }
+ #view-terminal {}
+ .app-view {
+ flex: 20 1 auto;
+ }
+ #view-split {
+ display: flex;
+ flex-direction: column-reverse;
+ }
+ </style>
+ </head>
+ <body>
+ <header id='titlebar'>
+ <span>SQLite3 Fiddle</span>
+ <span class='powered-by'>Powered by
+ <a href='https://sqlite.org'>SQLite3</a></span>
+ </header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+
+ <div id='view-terminal' class='app-view hidden initially-hidden'>
+ This is a placeholder for a terminal-like view which is not in
+ the default build.
+ </div>
+
+ <div id='view-split' class='app-view initially-hidden'>
+ <fieldset class='options collapsible'>
+ <legend><button class='fieldset-toggle'>Options</button></legend>
+ <div class=''>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-sbs'
+ data-csstgt='#main-wrapper'
+ data-cssclass='side-by-side'
+ data-config='sideBySide'>
+ <label for='opt-cb-sbs'>Side-by-side</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-swapio'
+ data-csstgt='#main-wrapper'
+ data-cssclass='swapio'
+ data-config='swapInOut'>
+ <label for='opt-cb-swapio'>Swap in/out</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-autoscroll'
+ data-config='autoScrollOutput'>
+ <label for='opt-cb-autoscroll'>Auto-scroll output</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='checkbox' id='opt-cb-autoclear'
+ data-config='autoClearOutput'>
+ <label for='opt-cb-autoclear'>Auto-clear output</label>
+ </span>
+ <span class='labeled-input'>
+ <input type='file' id='load-db' class='hidden'/>
+ <button id='btn-load-db'>Load DB...</button>
+ </span>
+ <span class='labeled-input'>
+ <button id='btn-export'>Download DB</button>
+ </span>
+ <span class='labeled-input'>
+ <button id='btn-reset'>Reset DB</button>
+ </span>
+ </div>
+ </fieldset><!-- .options -->
+ <div id='main-wrapper' class=''>
+ <fieldset class='zone-wrapper input'>
+ <legend><div class='button-bar'>
+ <button id='btn-shell-exec'>Run</button>
+ <button id='btn-clear'>Clear Input</button>
+ <!--button data-cmd='.help'>Help</button-->
+ <select id='select-examples'></select>
+ </div></legend>
+ <div><textarea id="input"
+ placeholder="Shell input. Ctrl-enter/shift-enter runs it.">
+-- ==================================================
+-- Use ctrl-enter or shift-enter to execute sqlite3
+-- shell commands and SQL.
+-- If a subset of the text is currently selected,
+-- only that part is executed.
+-- ==================================================
+.nullvalue NULL
+.headers on
+</textarea></div>
+ </fieldset>
+ <fieldset class='zone-wrapper output'>
+ <legend><div class='button-bar'>
+ <button id='btn-clear-output'>Clear Output</button>
+ <button id='btn-interrupt' class='hidden' disabled>Interrupt</button>
+ <!-- interruption cannot work in the current configuration
+ because we cannot send an interrupt message when work
+ is currently underway. At that point the Worker is
+ tied up and will not receive the message. -->
+ </div></legend>
+ <div><textarea id="output" readonly
+ placeholder="Shell output."></textarea></div>
+ </fieldset>
+ </div>
+ </div> <!-- #view-split -->
+ <script src="fiddle.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/index-dist.html b/ext/wasm/index-dist.html
new file mode 100644
index 0000000..6b038c8
--- /dev/null
+++ b/ext/wasm/index-dist.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>sqlite3 WASM Demo Page Index</title>
+ </head>
+ <body>
+ <style>
+ body {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ }
+ textarea {
+ font-family: monospace;
+ }
+ header {
+ font-size: 130%;
+ font-weight: bold;
+ }
+ .hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+ }
+ .warning { color: firebrick; }
+ </style>
+ <header id='titlebar'><span>sqlite3 WASM demo pages</span></header>
+ <hr>
+ <div>Below is the list of demo pages for the sqlite3 WASM
+ builds. The intent is that <em>this</em> page be run
+ using the functional equivalent of:</div>
+ <blockquote><pre><a href='https://sqlite.org/althttpd'>althttpd</a> -enable-sab -page index.html</pre></blockquote>
+ <div>and the individual pages be started in their own tab.
+ Warnings and Caveats:
+ <ul class='warning'>
+ <li>Some of these pages require that the web server emit the
+ so-called
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy'>COOP</a>
+ and
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy'>COEP</a>
+ headers. <a href='https://sqlite.org/althttpd'>althttpd</a> requires the
+ <code>-enable-sab</code> flag for that.
+ </li>
+ </ul>
+ </div>
+ <div>The tests and demos...
+ <ul id='test-list'>
+ <li>Core-most tests
+ <ul>
+ <li><a href='tester1.html'>tester1</a>: Core unit and
+ regression tests for the various APIs and surrounding
+ utility code.</li>
+ <li><a href='tester1-worker.html'>tester1-worker</a>: same thing
+ but running in a Worker.</li>
+ </ul>
+ </li>
+ <li>Higher-level apps and demos...
+ <ul>
+ <li><a href='demo-123.html'>demo-123</a> provides a
+ no-nonsense example of adding sqlite3 support to a web
+ page in the UI thread.</li>
+ <li><a href='demo-123-worker.html'>demo-123-worker</a> is
+ the same as <code>demo-123</code> but loads and runs
+ sqlite3 from a Worker thread.</li>
+ <li><a href='demo-jsstorage.html'>demo-jsstorage</a>: very basic
+ demo of using the key-value VFS for storing a persistent db
+ in JS <code>localStorage</code> or <code>sessionStorage</code>.</li>
+ <li><a href='demo-worker1.html'>demo-worker1</a>:
+ Worker-based wrapper of the OO API #1. Its Promise-based
+ wrapper is significantly easier to use, however.</li>
+ <li><a href='demo-worker1-promiser.html'>demo-worker1-promiser</a>:
+ a demo of the Promise-based wrapper of the Worker1 API.</li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <style>
+ #test-list { font-size: 120%; }
+ </style>
+ <script>//Assign a distinct target tab name for each test page...
+ document.querySelectorAll('a').forEach(function(e){
+ e.target = e.href;
+ });
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/index.html b/ext/wasm/index.html
new file mode 100644
index 0000000..044cd13
--- /dev/null
+++ b/ext/wasm/index.html
@@ -0,0 +1,115 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3 WASM Testing Page Index</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3 WASM test pages</span></header>
+ <hr>
+ <div>Below is the list of test pages for the sqlite3 WASM
+ builds. All of them require that this directory have been
+ "make"d first. The intent is that <em>this</em> page be run
+ using:</div>
+ <blockquote><pre>althttpd -enable-sab -page index.html</pre></blockquote>
+ <div>and the individual tests be started in their own tab.
+ Warnings and Caveats:
+ <ul class='warning'>
+ <li>Some of these pages require that
+ the web server emit the so-called
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy'>COOP</a>
+ and
+ <a href='https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy'>COEP</a>
+ headers. <a href='https://sqlite.org/althttpd'>althttpd</a> requires the
+ <code>-enable-sab</code> flag for that.
+ </li>
+ <li>Any OPFS-related pages require very recent version of
+ Chrome or Chromium (v102 at least, possibly newer). OPFS
+ support in the other major browsers is pending. Development
+ and testing is currently done against a dev-channel release
+ of Chrome (v107 as of 2022-09-26).
+ </li>
+ <li>Whether or not WASMFS/OPFS support is enabled on any given
+ page may depend on build-time options which are <em>off by
+ default</em>.
+ </li>
+ </ul>
+ </div>
+ <div>The tests and demos...
+ <ul id='test-list'>
+ <li>Core-most tests
+ <ul>
+ <li><a href='tester1.html'>tester1</a>: Core unit and
+ regression tests for the various APIs and surrounding
+ utility code.</li>
+ <li><a href='tester1-worker.html'>tester1-worker</a>: same thing
+ but running in a Worker.</li>
+ </ul>
+ </li>
+ <li>High-level apps and demos...
+ <ul>
+ <li><a href='fiddle/index.html'>fiddle</a> is an HTML front-end
+ to a wasm build of the sqlite3 shell.</li>
+ <li><a href='demo-123.html'>demo-123</a> provides a
+ no-nonsense example of adding sqlite3 support to a web
+ page in the UI thread.</li>
+ <li><a href='demo-123-worker.html'>demo-123-worker</a> is
+ the same as <code>demo-123</code> but loads and runs
+ sqlite3 from a Worker thread.</li>
+ <li><a href='demo-jsstorage.html'>demo-jsstorage</a>: very basic
+ demo of using the key-value VFS for storing a persistent db
+ in JS <code>localStorage</code> or <code>sessionStorage</code>.</li>
+ <li><a href='demo-worker1.html'>demo-worker1</a>:
+ Worker-based wrapper of the OO API #1. Its Promise-based
+ wrapper is significantly easier to use, however.</li>
+ <li><a href='demo-worker1-promiser.html'>demo-worker1-promiser</a>:
+ a demo of the Promise-based wrapper of the Worker1 API.</li>
+ </ul>
+ </li>
+ <li>speedtest1 ports (sqlite3's primary benchmarking tool)...
+ <ul>
+ <li><a href='speedtest1.html'>speedtest1</a>: a main-thread WASM build of speedtest1.</li>
+ <!--li><a href='speedtest1-wasmfs.html?flags=--size,25'>speedtest1-wasmfs</a>: a variant of speedtest1 built solely for the wasmfs/opfs feature.
+ </li-->
+ <li><a href='speedtest1.html?vfs=kvvfs'>speedtest1-kvvfs</a>: speedtest1 with the kvvfs.</li>
+ <li><a href='speedtest1-worker.html?size=25'>speedtest1-worker</a>: an interactive Worker-thread variant of speedtest1.</li>
+ <li><a href='speedtest1-worker.html?vfs=opfs&size=25'>speedtest1-worker-opfs</a>: speedtest1-worker with the
+ OPFS VFS preselected and configured for a moderate workload.</li>
+ </ul>
+ </li>
+ <li>The obligatory "misc." category...
+ <ul>
+ <li><a href='module-symbols.html'>module-symbols</a> gives
+ a high-level overview of the symbols exposed by the JS
+ module.</li>
+ <li><a href='batch-runner.html'>batch-runner</a>: runs batches of SQL exported from speedtest1.</li>
+ <!--li><a href='scratchpad-wasmfs-main.html'>scratchpad-wasmfs-main</a>:
+ experimenting with WASMFS/OPFS-based persistence. Maintenance
+ reminder: we cannot currently (2022-09-15) load WASMFS in a
+ worker due to an Emscripten limitation.</li-->
+ <li><a href='test-opfs-vfs.html'>test-opfs-vfs</a>
+ (<a href='test-opfs-vfs.html?opfs-sanity-check&opfs-verbose'>same
+ with verbose output and sanity-checking tests</a>) is an
+ sqlite3_vfs OPFS proxy using SharedArrayBuffer and the
+ Atomics APIs to regulate communication between the
+ synchronous sqlite3_vfs interface and the async OPFS
+ impl.
+ </li>
+ </ul>
+ </li>
+ <!--li><a href='x.html'></a></li-->
+ </ul>
+ </div>
+ <style>
+ #test-list { font-size: 120%; }
+ </style>
+ <script>//Assign a distinct target tab name for each test page...
+ document.querySelectorAll('a').forEach(function(e){
+ e.target = e.href;
+ });
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/jaccwabyt/jaccwabyt.js b/ext/wasm/jaccwabyt/jaccwabyt.js
new file mode 100644
index 0000000..dee7258
--- /dev/null
+++ b/ext/wasm/jaccwabyt/jaccwabyt.js
@@ -0,0 +1,746 @@
+/**
+ 2022-06-30
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ The Jaccwabyt API is documented in detail in an external file.
+
+ Project home: https://fossil.wanderinghorse.net/r/jaccwabyt
+
+*/
+'use strict';
+self.Jaccwabyt = function StructBinderFactory(config){
+/* ^^^^ it is recommended that clients move that object into wherever
+ they'd like to have it and delete the self-held copy ("self" being
+ the global window or worker object). This API does not require the
+ global reference - it is simply installed as a convenience for
+ connecting these bits to other co-developed code before it gets
+ removed from the global namespace.
+*/
+
+ /** Throws a new Error, the message of which is the concatenation
+ all args with a space between each. */
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+
+ /**
+ Implementing function bindings revealed significant
+ shortcomings in Emscripten's addFunction()/removeFunction()
+ interfaces:
+
+ https://github.com/emscripten-core/emscripten/issues/17323
+
+ Until those are resolved, or a suitable replacement can be
+ implemented, our function-binding API will be more limited
+ and/or clumsier to use than initially hoped.
+ */
+ if(!(config.heap instanceof WebAssembly.Memory)
+ && !(config.heap instanceof Function)){
+ toss("config.heap must be WebAssembly.Memory instance or a function.");
+ }
+ ['alloc','dealloc'].forEach(function(k){
+ (config[k] instanceof Function) ||
+ toss("Config option '"+k+"' must be a function.");
+ });
+ const SBF = StructBinderFactory;
+ const heap = (config.heap instanceof Function)
+ ? config.heap : (()=>new Uint8Array(config.heap.buffer)),
+ alloc = config.alloc,
+ dealloc = config.dealloc,
+ log = config.log || console.log.bind(console),
+ memberPrefix = (config.memberPrefix || ""),
+ memberSuffix = (config.memberSuffix || ""),
+ bigIntEnabled = (undefined===config.bigIntEnabled
+ ? !!self['BigInt64Array'] : !!config.bigIntEnabled),
+ BigInt = self['BigInt'],
+ BigInt64Array = self['BigInt64Array'],
+ /* Undocumented (on purpose) config options: */
+ functionTable = config.functionTable/*EXPERIMENTAL, undocumented*/,
+ ptrSizeof = config.ptrSizeof || 4,
+ ptrIR = config.ptrIR || 'i32'
+ ;
+
+ if(!SBF.debugFlags){
+ SBF.__makeDebugFlags = function(deriveFrom=null){
+ /* This is disgustingly overengineered. :/ */
+ if(deriveFrom && deriveFrom.__flags) deriveFrom = deriveFrom.__flags;
+ const f = function f(flags){
+ if(0===arguments.length){
+ return f.__flags;
+ }
+ if(flags<0){
+ delete f.__flags.getter; delete f.__flags.setter;
+ delete f.__flags.alloc; delete f.__flags.dealloc;
+ }else{
+ f.__flags.getter = 0!==(0x01 & flags);
+ f.__flags.setter = 0!==(0x02 & flags);
+ f.__flags.alloc = 0!==(0x04 & flags);
+ f.__flags.dealloc = 0!==(0x08 & flags);
+ }
+ return f._flags;
+ };
+ Object.defineProperty(f,'__flags', {
+ iterable: false, writable: false,
+ value: Object.create(deriveFrom)
+ });
+ if(!deriveFrom) f(0);
+ return f;
+ };
+ SBF.debugFlags = SBF.__makeDebugFlags();
+ }/*static init*/
+
+ const isLittleEndian = (function() {
+ const buffer = new ArrayBuffer(2);
+ new DataView(buffer).setInt16(0, 256, true /* littleEndian */);
+ // Int16Array uses the platform's endianness.
+ return new Int16Array(buffer)[0] === 256;
+ })();
+ /**
+ Some terms used in the internal docs:
+
+ StructType: a struct-wrapping class generated by this
+ framework.
+ DEF: struct description object.
+ SIG: struct member signature string.
+ */
+
+ /** True if SIG s looks like a function signature, else
+ false. */
+ const isFuncSig = (s)=>'('===s[1];
+ /** True if SIG s is-a pointer signature. */
+ const isPtrSig = (s)=>'p'===s || 'P'===s;
+ const isAutoPtrSig = (s)=>'P'===s /*EXPERIMENTAL*/;
+ const sigLetter = (s)=>isFuncSig(s) ? 'p' : s[0];
+ /** Returns the WASM IR form of the Emscripten-conventional letter
+ at SIG s[0]. Throws for an unknown SIG. */
+ const sigIR = function(s){
+ switch(sigLetter(s)){
+ case 'i': return 'i32';
+ case 'p': case 'P': case 's': return ptrIR;
+ case 'j': return 'i64';
+ case 'f': return 'float';
+ case 'd': return 'double';
+ }
+ toss("Unhandled signature IR:",s);
+ };
+ /** Returns the sizeof value for the given SIG. Throws for an
+ unknown SIG. */
+ const sigSizeof = function(s){
+ switch(sigLetter(s)){
+ case 'i': return 4;
+ case 'p': case 'P': case 's': return ptrSizeof;
+ case 'j': return 8;
+ case 'f': return 4 /* C-side floats, not JS-side */;
+ case 'd': return 8;
+ }
+ toss("Unhandled signature sizeof:",s);
+ };
+ const affirmBigIntArray = BigInt64Array
+ ? ()=>true : ()=>toss('BigInt64Array is not available.');
+ /** Returns the (signed) TypedArray associated with the type
+ described by the given SIG. Throws for an unknown SIG. */
+ /**********
+ const sigTypedArray = function(s){
+ switch(sigIR(s)) {
+ case 'i32': return Int32Array;
+ case 'i64': return affirmBigIntArray() && BigInt64Array;
+ case 'float': return Float32Array;
+ case 'double': return Float64Array;
+ }
+ toss("Unhandled signature TypedArray:",s);
+ };
+ **************/
+ /** Returns the name of a DataView getter method corresponding
+ to the given SIG. */
+ const sigDVGetter = function(s){
+ switch(sigLetter(s)) {
+ case 'p': case 'P': case 's': {
+ switch(ptrSizeof){
+ case 4: return 'getInt32';
+ case 8: return affirmBigIntArray() && 'getBigInt64';
+ }
+ break;
+ }
+ case 'i': return 'getInt32';
+ case 'j': return affirmBigIntArray() && 'getBigInt64';
+ case 'f': return 'getFloat32';
+ case 'd': return 'getFloat64';
+ }
+ toss("Unhandled DataView getter for signature:",s);
+ };
+ /** Returns the name of a DataView setter method corresponding
+ to the given SIG. */
+ const sigDVSetter = function(s){
+ switch(sigLetter(s)){
+ case 'p': case 'P': case 's': {
+ switch(ptrSizeof){
+ case 4: return 'setInt32';
+ case 8: return affirmBigIntArray() && 'setBigInt64';
+ }
+ break;
+ }
+ case 'i': return 'setInt32';
+ case 'j': return affirmBigIntArray() && 'setBigInt64';
+ case 'f': return 'setFloat32';
+ case 'd': return 'setFloat64';
+ }
+ toss("Unhandled DataView setter for signature:",s);
+ };
+ /**
+ Returns either Number of BigInt, depending on the given
+ SIG. This constructor is used in property setters to coerce
+ the being-set value to the correct size.
+ */
+ const sigDVSetWrapper = function(s){
+ switch(sigLetter(s)) {
+ case 'i': case 'f': case 'd': return Number;
+ case 'j': return affirmBigIntArray() && BigInt;
+ case 'p': case 'P': case 's':
+ switch(ptrSizeof){
+ case 4: return Number;
+ case 8: return affirmBigIntArray() && BigInt;
+ }
+ break;
+ }
+ toss("Unhandled DataView set wrapper for signature:",s);
+ };
+
+ const sPropName = (s,k)=>s+'::'+k;
+
+ const __propThrowOnSet = function(structName,propName){
+ return ()=>toss(sPropName(structName,propName),"is read-only.");
+ };
+
+ /**
+ When C code passes a pointer of a bound struct to back into
+ a JS function via a function pointer struct member, it
+ arrives in JS as a number (pointer).
+ StructType.instanceForPointer(ptr) can be used to get the
+ instance associated with that pointer, and __ptrBacklinks
+ holds that mapping. WeakMap keys must be objects, so we
+ cannot use a weak map to map pointers to instances. We use
+ the StructType constructor as the WeakMap key, mapped to a
+ plain, prototype-less Object which maps the pointers to
+ struct instances. That arrangement gives us a
+ per-StructType type-safe way to resolve pointers.
+ */
+ const __ptrBacklinks = new WeakMap();
+ /**
+ Similar to __ptrBacklinks but is scoped at the StructBinder
+ level and holds pointer-to-object mappings for all struct
+ instances created by any struct from any StructFactory
+ which this specific StructBinder has created. The intention
+ of this is to help implement more transparent handling of
+ pointer-type property resolution.
+ */
+ const __ptrBacklinksGlobal = Object.create(null);
+
+ /**
+ In order to completely hide StructBinder-bound struct
+ pointers from JS code, we store them in a scope-local
+ WeakMap which maps the struct-bound objects to their WASM
+ pointers. The pointers are accessible via
+ boundObject.pointer, which is gated behind an accessor
+ function, but are not exposed anywhere else in the
+ object. The main intention of that is to make it impossible
+ for stale copies to be made.
+ */
+ const __instancePointerMap = new WeakMap();
+
+ /** Property name for the pointer-is-external marker. */
+ const xPtrPropName = '(pointer-is-external)';
+
+ /** Frees the obj.pointer memory and clears the pointer
+ property. */
+ const __freeStruct = function(ctor, obj, m){
+ if(!m) m = __instancePointerMap.get(obj);
+ if(m) {
+ if(obj.ondispose instanceof Function){
+ try{obj.ondispose()}
+ catch(e){
+ /*do not rethrow: destructors must not throw*/
+ console.warn("ondispose() for",ctor.structName,'@',
+ m,'threw. NOT propagating it.',e);
+ }
+ }else if(Array.isArray(obj.ondispose)){
+ obj.ondispose.forEach(function(x){
+ try{
+ if(x instanceof Function) x.call(obj);
+ else if('number' === typeof x) dealloc(x);
+ // else ignore. Strings are permitted to annotate entries
+ // to assist in debugging.
+ }catch(e){
+ console.warn("ondispose() for",ctor.structName,'@',
+ m,'threw. NOT propagating it.',e);
+ }
+ });
+ }
+ delete obj.ondispose;
+ delete __ptrBacklinks.get(ctor)[m];
+ delete __ptrBacklinksGlobal[m];
+ __instancePointerMap.delete(obj);
+ if(ctor.debugFlags.__flags.dealloc){
+ log("debug.dealloc:",(obj[xPtrPropName]?"EXTERNAL":""),
+ ctor.structName,"instance:",
+ ctor.structInfo.sizeof,"bytes @"+m);
+ }
+ if(!obj[xPtrPropName]) dealloc(m);
+ }
+ };
+
+ /** Returns a skeleton for a read-only property accessor wrapping
+ value v. */
+ const rop = (v)=>{return {configurable: false, writable: false,
+ iterable: false, value: v}};
+
+ /** Allocates obj's memory buffer based on the size defined in
+ DEF.sizeof. */
+ const __allocStruct = function(ctor, obj, m){
+ let fill = !m;
+ if(m) Object.defineProperty(obj, xPtrPropName, rop(m));
+ else{
+ m = alloc(ctor.structInfo.sizeof);
+ if(!m) toss("Allocation of",ctor.structName,"structure failed.");
+ }
+ try {
+ if(ctor.debugFlags.__flags.alloc){
+ log("debug.alloc:",(fill?"":"EXTERNAL"),
+ ctor.structName,"instance:",
+ ctor.structInfo.sizeof,"bytes @"+m);
+ }
+ if(fill) heap().fill(0, m, m + ctor.structInfo.sizeof);
+ __instancePointerMap.set(obj, m);
+ __ptrBacklinks.get(ctor)[m] = obj;
+ __ptrBacklinksGlobal[m] = obj;
+ }catch(e){
+ __freeStruct(ctor, obj, m);
+ throw e;
+ }
+ };
+ /** Gets installed as the memoryDump() method of all structs. */
+ const __memoryDump = function(){
+ const p = this.pointer;
+ return p
+ ? new Uint8Array(heap().slice(p, p+this.structInfo.sizeof))
+ : null;
+ };
+
+ const __memberKey = (k)=>memberPrefix + k + memberSuffix;
+ const __memberKeyProp = rop(__memberKey);
+
+ /**
+ Looks up a struct member in structInfo.members. Throws if found
+ if tossIfNotFound is true, else returns undefined if not
+ found. The given name may be either the name of the
+ structInfo.members key (faster) or the key as modified by the
+ memberPrefix/memberSuffix settings.
+ */
+ const __lookupMember = function(structInfo, memberName, tossIfNotFound=true){
+ let m = structInfo.members[memberName];
+ if(!m && (memberPrefix || memberSuffix)){
+ // Check for a match on members[X].key
+ for(const v of Object.values(structInfo.members)){
+ if(v.key===memberName){ m = v; break; }
+ }
+ if(!m && tossIfNotFound){
+ toss(sPropName(structInfo.name,memberName),'is not a mapped struct member.');
+ }
+ }
+ return m;
+ };
+
+ /**
+ Uses __lookupMember(obj.structInfo,memberName) to find a member,
+ throwing if not found. Returns its signature, either in this
+ framework's native format or in Emscripten format.
+ */
+ const __memberSignature = function f(obj,memberName,emscriptenFormat=false){
+ if(!f._) f._ = (x)=>x.replace(/[^vipPsjrd]/g,"").replace(/[pPs]/g,'i');
+ const m = __lookupMember(obj.structInfo, memberName, true);
+ return emscriptenFormat ? f._(m.signature) : m.signature;
+ };
+
+ /**
+ Returns the instanceForPointer() impl for the given
+ StructType constructor.
+ */
+ const __instanceBacklinkFactory = function(ctor){
+ const b = Object.create(null);
+ __ptrBacklinks.set(ctor, b);
+ return (ptr)=>b[ptr];
+ };
+
+ const __ptrPropDescriptor = {
+ configurable: false, enumerable: false,
+ get: function(){return __instancePointerMap.get(this)},
+ set: ()=>toss("Cannot assign the 'pointer' property of a struct.")
+ // Reminder: leaving `set` undefined makes assignments
+ // to the property _silently_ do nothing. Current unit tests
+ // rely on it throwing, though.
+ };
+
+ /** Impl of X.memberKeys() for StructType and struct ctors. */
+ const __structMemberKeys = rop(function(){
+ const a = [];
+ Object.keys(this.structInfo.members).forEach((k)=>a.push(this.memberKey(k)));
+ return a;
+ });
+
+ const __utf8Decoder = new TextDecoder('utf-8');
+ const __utf8Encoder = new TextEncoder();
+ /** Internal helper to use in operations which need to distinguish
+ between SharedArrayBuffer heap memory and non-shared heap. */
+ const __SAB = ('undefined'===typeof SharedArrayBuffer)
+ ? function(){} : SharedArrayBuffer;
+ const __utf8Decode = function(arrayBuffer, begin, end){
+ return __utf8Decoder.decode(
+ (arrayBuffer.buffer instanceof __SAB)
+ ? arrayBuffer.slice(begin, end)
+ : arrayBuffer.subarray(begin, end)
+ );
+ };
+ /**
+ Uses __lookupMember() to find the given obj.structInfo key.
+ Returns that member if it is a string, else returns false. If the
+ member is not found, throws if tossIfNotFound is true, else
+ returns false.
+ */
+ const __memberIsString = function(obj,memberName, tossIfNotFound=false){
+ const m = __lookupMember(obj.structInfo, memberName, tossIfNotFound);
+ return (m && 1===m.signature.length && 's'===m.signature[0]) ? m : false;
+ };
+
+ /**
+ Given a member description object, throws if member.signature is
+ not valid for assigning to or interpretation as a C-style string.
+ It optimistically assumes that any signature of (i,p,s) is
+ C-string compatible.
+ */
+ const __affirmCStringSignature = function(member){
+ if('s'===member.signature) return;
+ toss("Invalid member type signature for C-string value:",
+ JSON.stringify(member));
+ };
+
+ /**
+ Looks up the given member in obj.structInfo. If it has a
+ signature of 's' then it is assumed to be a C-style UTF-8 string
+ and a decoded copy of the string at its address is returned. If
+ the signature is of any other type, it throws. If an s-type
+ member's address is 0, `null` is returned.
+ */
+ const __memberToJsString = function f(obj,memberName){
+ const m = __lookupMember(obj.structInfo, memberName, true);
+ __affirmCStringSignature(m);
+ const addr = obj[m.key];
+ //log("addr =",addr,memberName,"m =",m);
+ if(!addr) return null;
+ let pos = addr;
+ const mem = heap();
+ for( ; mem[pos]!==0; ++pos ) {
+ //log("mem[",pos,"]",mem[pos]);
+ };
+ //log("addr =",addr,"pos =",pos);
+ return (addr===pos) ? "" : __utf8Decode(mem, addr, pos);
+ };
+
+ /**
+ Adds value v to obj.ondispose, creating ondispose,
+ or converting it to an array, if needed.
+ */
+ const __addOnDispose = function(obj, v){
+ if(obj.ondispose){
+ if(obj.ondispose instanceof Function){
+ obj.ondispose = [obj.ondispose];
+ }/*else assume it's an array*/
+ }else{
+ obj.ondispose = [];
+ }
+ obj.ondispose.push(v);
+ };
+
+ /**
+ Allocates a new UTF-8-encoded, NUL-terminated copy of the given
+ JS string and returns its address relative to heap(). If
+ allocation returns 0 this function throws. Ownership of the
+ memory is transfered to the caller, who must eventually pass it
+ to the configured dealloc() function.
+ */
+ const __allocCString = function(str){
+ const u = __utf8Encoder.encode(str);
+ const mem = alloc(u.length+1);
+ if(!mem) toss("Allocation error while duplicating string:",str);
+ const h = heap();
+ let i = 0;
+ for( ; i < u.length; ++i ) h[mem + i] = u[i];
+ h[mem + u.length] = 0;
+ //log("allocCString @",mem," =",u);
+ return mem;
+ };
+
+ /**
+ Sets the given struct member of obj to a dynamically-allocated,
+ UTF-8-encoded, NUL-terminated copy of str. It is up to the caller
+ to free any prior memory, if appropriate. The newly-allocated
+ string is added to obj.ondispose so will be freed when the object
+ is disposed.
+ */
+ const __setMemberCString = function(obj, memberName, str){
+ const m = __lookupMember(obj.structInfo, memberName, true);
+ __affirmCStringSignature(m);
+ /* Potential TODO: if obj.ondispose contains obj[m.key] then
+ dealloc that value and clear that ondispose entry */
+ const mem = __allocCString(str);
+ obj[m.key] = mem;
+ __addOnDispose(obj, mem);
+ return obj;
+ };
+
+ /**
+ Prototype for all StructFactory instances (the constructors
+ returned from StructBinder).
+ */
+ const StructType = function ctor(structName, structInfo){
+ if(arguments[2]!==rop){
+ toss("Do not call the StructType constructor",
+ "from client-level code.");
+ }
+ Object.defineProperties(this,{
+ //isA: rop((v)=>v instanceof ctor),
+ structName: rop(structName),
+ structInfo: rop(structInfo)
+ });
+ };
+
+ /**
+ Properties inherited by struct-type-specific StructType instances
+ and (indirectly) concrete struct-type instances.
+ */
+ StructType.prototype = Object.create(null, {
+ dispose: rop(function(){__freeStruct(this.constructor, this)}),
+ lookupMember: rop(function(memberName, tossIfNotFound=true){
+ return __lookupMember(this.structInfo, memberName, tossIfNotFound);
+ }),
+ memberToJsString: rop(function(memberName){
+ return __memberToJsString(this, memberName);
+ }),
+ memberIsString: rop(function(memberName, tossIfNotFound=true){
+ return __memberIsString(this, memberName, tossIfNotFound);
+ }),
+ memberKey: __memberKeyProp,
+ memberKeys: __structMemberKeys,
+ memberSignature: rop(function(memberName, emscriptenFormat=false){
+ return __memberSignature(this, memberName, emscriptenFormat);
+ }),
+ memoryDump: rop(__memoryDump),
+ pointer: __ptrPropDescriptor,
+ setMemberCString: rop(function(memberName, str){
+ return __setMemberCString(this, memberName, str);
+ })
+ });
+
+ /**
+ "Static" properties for StructType.
+ */
+ Object.defineProperties(StructType, {
+ allocCString: rop(__allocCString),
+ instanceForPointer: rop((ptr)=>__ptrBacklinksGlobal[ptr]),
+ isA: rop((v)=>v instanceof StructType),
+ hasExternalPointer: rop((v)=>(v instanceof StructType) && !!v[xPtrPropName]),
+ memberKey: __memberKeyProp
+ });
+
+ const isNumericValue = (v)=>Number.isFinite(v) || (v instanceof (BigInt || Number));
+
+ /**
+ Pass this a StructBinder-generated prototype, and the struct
+ member description object. It will define property accessors for
+ proto[memberKey] which read from/write to memory in
+ this.pointer. It modifies descr to make certain downstream
+ operations much simpler.
+ */
+ const makeMemberWrapper = function f(ctor,name, descr){
+ if(!f._){
+ /*cache all available getters/setters/set-wrappers for
+ direct reuse in each accessor function. */
+ f._ = {getters: {}, setters: {}, sw:{}};
+ const a = ['i','p','P','s','f','d','v()'];
+ if(bigIntEnabled) a.push('j');
+ a.forEach(function(v){
+ //const ir = sigIR(v);
+ f._.getters[v] = sigDVGetter(v) /* DataView[MethodName] values for GETTERS */;
+ f._.setters[v] = sigDVSetter(v) /* DataView[MethodName] values for SETTERS */;
+ f._.sw[v] = sigDVSetWrapper(v) /* BigInt or Number ctor to wrap around values
+ for conversion */;
+ });
+ const rxSig1 = /^[ipPsjfd]$/,
+ rxSig2 = /^[vipPsjfd]\([ipPsjfd]*\)$/;
+ f.sigCheck = function(obj, name, key,sig){
+ if(Object.prototype.hasOwnProperty.call(obj, key)){
+ toss(obj.structName,'already has a property named',key+'.');
+ }
+ rxSig1.test(sig) || rxSig2.test(sig)
+ || toss("Malformed signature for",
+ sPropName(obj.structName,name)+":",sig);
+ };
+ }
+ const key = ctor.memberKey(name);
+ f.sigCheck(ctor.prototype, name, key, descr.signature);
+ descr.key = key;
+ descr.name = name;
+ const sizeOf = sigSizeof(descr.signature);
+ const sigGlyph = sigLetter(descr.signature);
+ const xPropName = sPropName(ctor.prototype.structName,key);
+ const dbg = ctor.prototype.debugFlags.__flags;
+ /*
+ TODO?: set prototype of descr to an object which can set/fetch
+ its prefered representation, e.g. conversion to string or mapped
+ function. Advantage: we can avoid doing that via if/else if/else
+ in the get/set methods.
+ */
+ const prop = Object.create(null);
+ prop.configurable = false;
+ prop.enumerable = false;
+ prop.get = function(){
+ if(dbg.getter){
+ log("debug.getter:",f._.getters[sigGlyph],"for", sigIR(sigGlyph),
+ xPropName,'@', this.pointer,'+',descr.offset,'sz',sizeOf);
+ }
+ let rc = (
+ new DataView(heap().buffer, this.pointer + descr.offset, sizeOf)
+ )[f._.getters[sigGlyph]](0, isLittleEndian);
+ if(dbg.getter) log("debug.getter:",xPropName,"result =",rc);
+ if(rc && isAutoPtrSig(descr.signature)){
+ rc = StructType.instanceForPointer(rc) || rc;
+ if(dbg.getter) log("debug.getter:",xPropName,"resolved =",rc);
+ }
+ return rc;
+ };
+ if(descr.readOnly){
+ prop.set = __propThrowOnSet(ctor.prototype.structName,key);
+ }else{
+ prop.set = function(v){
+ if(dbg.setter){
+ log("debug.setter:",f._.setters[sigGlyph],"for", sigIR(sigGlyph),
+ xPropName,'@', this.pointer,'+',descr.offset,'sz',sizeOf, v);
+ }
+ if(!this.pointer){
+ toss("Cannot set struct property on disposed instance.");
+ }
+ if(null===v) v = 0;
+ else while(!isNumericValue(v)){
+ if(isAutoPtrSig(descr.signature) && (v instanceof StructType)){
+ // It's a struct instance: let's store its pointer value!
+ v = v.pointer || 0;
+ if(dbg.setter) log("debug.setter:",xPropName,"resolved to",v);
+ break;
+ }
+ toss("Invalid value for pointer-type",xPropName+'.');
+ }
+ (
+ new DataView(heap().buffer, this.pointer + descr.offset, sizeOf)
+ )[f._.setters[sigGlyph]](0, f._.sw[sigGlyph](v), isLittleEndian);
+ };
+ }
+ Object.defineProperty(ctor.prototype, key, prop);
+ }/*makeMemberWrapper*/;
+
+ /**
+ The main factory function which will be returned to the
+ caller.
+ */
+ const StructBinder = function StructBinder(structName, structInfo){
+ if(1===arguments.length){
+ structInfo = structName;
+ structName = structInfo.name;
+ }else if(!structInfo.name){
+ structInfo.name = structName;
+ }
+ if(!structName) toss("Struct name is required.");
+ let lastMember = false;
+ Object.keys(structInfo.members).forEach((k)=>{
+ const m = structInfo.members[k];
+ if(!m.sizeof) toss(structName,"member",k,"is missing sizeof.");
+ else if(0!==(m.sizeof%4)){
+ toss(structName,"member",k,"sizeof is not aligned.");
+ }
+ else if(0!==(m.offset%4)){
+ toss(structName,"member",k,"offset is not aligned.");
+ }
+ if(!lastMember || lastMember.offset < m.offset) lastMember = m;
+ });
+ if(!lastMember) toss("No member property descriptions found.");
+ else if(structInfo.sizeof < lastMember.offset+lastMember.sizeof){
+ toss("Invalid struct config:",structName,
+ "max member offset ("+lastMember.offset+") ",
+ "extends past end of struct (sizeof="+structInfo.sizeof+").");
+ }
+ const debugFlags = rop(SBF.__makeDebugFlags(StructBinder.debugFlags));
+ /** Constructor for the StructCtor. */
+ const StructCtor = function StructCtor(externalMemory){
+ if(!(this instanceof StructCtor)){
+ toss("The",structName,"constructor may only be called via 'new'.");
+ }else if(arguments.length){
+ if(externalMemory!==(externalMemory|0) || externalMemory<=0){
+ toss("Invalid pointer value for",structName,"constructor.");
+ }
+ __allocStruct(StructCtor, this, externalMemory);
+ }else{
+ __allocStruct(StructCtor, this);
+ }
+ };
+ Object.defineProperties(StructCtor,{
+ debugFlags: debugFlags,
+ disposeAll: rop(function(){
+ const map = __ptrBacklinks.get(StructCtor);
+ Object.keys(map).forEach(function(ptr){
+ const b = map[ptr];
+ if(b) __freeStruct(StructCtor, b, ptr);
+ });
+ __ptrBacklinks.set(StructCtor, Object.create(null));
+ return StructCtor;
+ }),
+ instanceForPointer: rop(__instanceBacklinkFactory(StructCtor)),
+ isA: rop((v)=>v instanceof StructCtor),
+ memberKey: __memberKeyProp,
+ memberKeys: __structMemberKeys,
+ resolveToInstance: rop(function(v, throwIfNot=false){
+ if(!(v instanceof StructCtor)){
+ v = Number.isSafeInteger(v)
+ ? StructCtor.instanceForPointer(v) : undefined;
+ }
+ if(!v && throwIfNot) toss("Value is-not-a",StructCtor.structName);
+ return v;
+ }),
+ methodInfoForKey: rop(function(mKey){
+ }),
+ structInfo: rop(structInfo),
+ structName: rop(structName)
+ });
+ StructCtor.prototype = new StructType(structName, structInfo, rop);
+ Object.defineProperties(StructCtor.prototype,{
+ debugFlags: debugFlags,
+ constructor: rop(StructCtor)
+ /*if we assign StructCtor.prototype and don't do
+ this then StructCtor!==instance.constructor!*/
+ });
+ Object.keys(structInfo.members).forEach(
+ (name)=>makeMemberWrapper(StructCtor, name, structInfo.members[name])
+ );
+ return StructCtor;
+ };
+ StructBinder.instanceForPointer = StructType.instanceForPointer;
+ StructBinder.StructType = StructType;
+ StructBinder.config = config;
+ StructBinder.allocCString = __allocCString;
+ if(!StructBinder.debugFlags){
+ StructBinder.debugFlags = SBF.__makeDebugFlags(SBF.debugFlags);
+ }
+ return StructBinder;
+}/*StructBinderFactory*/;
diff --git a/ext/wasm/jaccwabyt/jaccwabyt.md b/ext/wasm/jaccwabyt/jaccwabyt.md
new file mode 100644
index 0000000..edcba26
--- /dev/null
+++ b/ext/wasm/jaccwabyt/jaccwabyt.md
@@ -0,0 +1,1076 @@
+Jaccwabyt 🐇
+============================================================
+
+**Jaccwabyt**: _JavaScript ⇄ C Struct Communication via WASM Byte
+Arrays_
+
+
+Welcome to Jaccwabyt, a JavaScript API which creates bindings for
+WASM-compiled C structs, defining them in such a way that changes to
+their state in JS are visible in C/WASM, and vice versa, permitting
+two-way interchange of struct state with very little user-side
+friction.
+
+(If that means nothing to you, neither will the rest of this page!)
+
+**Browser compatibility**: this library requires a _recent_ browser
+and makes no attempt whatsoever to accommodate "older" or
+lesser-capable ones, where "recent," _very roughly_, means released in
+mid-2018 or later, with late 2021 releases required for some optional
+features in some browsers (e.g. [BigInt64Array][] in Safari). It also
+relies on a couple non-standard, but widespread, features, namely
+[TextEncoder][] and [TextDecoder][]. It is developed primarily on
+Firefox and Chrome on Linux and all claims of Safari compatibility
+are based solely on feature compatibility tables provided at
+[MDN][].
+
+**Formalities:**
+
+- Author: [Stephan Beal][sgb]
+- License: Public Domain
+- Project Home: <https://fossil.wanderinghorse.net/r/jaccwabyt>
+
+<a name='overview'></a>
+Table of Contents
+============================================================
+
+- [Overview](#overview)
+ - [Architecture](#architecture)
+- [Creating and Binding Structs](#creating-binding)
+ - [Step 1: Configure Jaccwabyt](#step-1)
+ - [Step 2: Struct Description](#step-2)
+ - [`P` vs `p`](#step-2-pvsp)
+ - [Step 3: Binding a Struct](#step-3)
+ - [Step 4: Creating, Using, and Destroying Instances](#step-4)
+- APIs
+ - [Struct Binder Factory](#api-binderfactory)
+ - [Struct Binder](#api-structbinder)
+ - [Struct Type](#api-structtype)
+ - [Struct Constructors](#api-structctor)
+ - [Struct Protypes](#api-structprototype)
+ - [Struct Instances](#api-structinstance)
+- Appendices
+ - [Appendix A: Limitations, TODOs, etc.](#appendix-a)
+ - [Appendix D: Debug Info](#appendix-d)
+ - [Appendix G: Generating Struct Descriptions](#appendix-g)
+
+<a name='overview'></a>
+Overview
+============================================================
+
+Management summary: this JavaScript-only framework provides limited
+two-way bindings between C structs and JavaScript objects, such that
+changes to the struct in one environment are visible in the other.
+
+Details...
+
+It works by creating JavaScript proxies for C structs. Reads and
+writes of the JS-side members are marshaled through a flat byte array
+allocated from the WASM heap. As that heap is shared with the C-side
+code, and the memory block is written using the same approach C does,
+that byte array can be used to access and manipulate a given struct
+instance from both JS and C.
+
+Motivating use case: this API was initially developed as an
+experiment to determine whether it would be feasible to implement,
+completely in JS, custom "VFS" and "virtual table" objects for the
+WASM build of [sqlite3][]. Doing so was going to require some form of
+two-way binding of several structs. Once the proof of concept was
+demonstrated, a rabbit hole appeared and _down we went_... It has
+since grown beyond its humble proof-of-concept origins and is believed
+to be a useful (or at least interesting) tool for mixed JS/C
+applications.
+
+Portability notes:
+
+- These docs sometimes use [Emscripten][] as a point of reference
+ because it is the most widespread WASM toolchain, but this code is
+ specifically designed to be usable in arbitrary WASM environments.
+ It abstracts away a few Emscripten-specific features into
+ configurable options. Similarly, the build tree requires Emscripten
+ but Jaccwabyt does not have any hard Emscripten dependencies.
+- This code is encapsulated into a single JavaScript function. It
+ should be trivial to copy/paste into arbitrary WASM/JS-using
+ projects.
+- The source tree includes C code, but only for testing and
+ demonstration purposes. It is not part of the core distributable.
+
+<a name='architecture'></a>
+Architecture
+------------------------------------------------------------
+
+<!--
+bug(?) (fossil): using "center" shrinks pikchr too much.
+-->
+
+```pikchr
+BSBF: box rad 0.3*boxht "StructBinderFactory" fit fill lightblue
+BSB: box same "StructBinder" fit at 0.75 e of 0.7 s of BSBF.c
+BST: box same "StructType<T>" fit at 1.5 e of BSBF
+BSC: box same "Struct<T>" "Ctor" fit at 1.5 s of BST
+BSI: box same "Struct<T>" "Instances" fit at 1 right of BSB.e
+BC: box same at 0.25 right of 1.6 e of BST "C Structs" fit fill lightgrey
+
+arrow -> from BSBF.s to BSB.w "Generates" aligned above
+arrow -> from BSB.n to BST.sw "Contains" aligned above
+arrow -> from BSB.s to BSC.nw "Generates" aligned below
+arrow -> from BSC.ne to BSI.s "Constructs" aligned below
+arrow <- from BST.se to BSI.n "Inherits" aligned above
+arrow <-> from BSI.e to BC.s dotted "Shared" aligned above "Memory" aligned below
+arrow -> from BST.e to BC.w dotted "Mirrors Struct" aligned above "Model From" aligned below
+arrow -> from BST.s to BSC.n "Prototype of" aligned above
+```
+
+Its major classes and functions are:
+
+- **[StructBinderFactory][StructBinderFactory]** is a factory function which
+ accepts a configuration object to customize it for a given WASM
+ environment. A client will typically call this only one time, with
+ an appropriate configuration, to generate a single...
+- **[StructBinder][]** is a factory function which converts an
+ arbitrary number struct descriptions into...
+- **[StructTypes][StructCtors]** are constructors, one per struct
+ description, which inherit from
+ **[`StructBinder.StructType`][StructType]** and are used to instantiate...
+- **[Struct instances][StructInstance]** are objects representing
+ individual instances of generated struct types.
+
+An app may have any number of StructBinders, but will typically
+need only one. Each StructBinder is effectively a separate
+namespace for struct creation.
+
+
+<a name='creating-binding'></a>
+Creating and Binding Structs
+============================================================
+
+From the amount of documentation provided, it may seem that
+creating and using struct bindings is a daunting task, but it
+essentially boils down to:
+
+1. [Confire Jaccwabyt for your WASM environment](#step-1). This is a
+ one-time task per project and results is a factory function which
+ can create new struct bindings.
+2. [Create a JSON-format description of your C structs](#step-2). This is
+ required once for each struct and required updating if the C
+ structs change.
+3. [Feed (2) to the function generated by (1)](#step-3) to create JS
+ constuctor functions for each struct. This is done at runtime, as
+ opposed to during a build-process step, and can be set up in such a
+ way that it does not require any maintenace after its initial
+ setup.
+4. [Create and use instances of those structs](#step-4).
+
+Detailed instructions for each of those steps follows...
+
+<a name='step-1'></a>
+Step 1: Configure Jaccwabyt for the Environment
+------------------------------------------------------------
+
+Jaccwabyt's highest-level API is a single function. It creates a
+factory for processing struct descriptions, but does not process any
+descriptions itself. This level of abstraction exist primarily so that
+the struct-specific factories can be configured for a given WASM
+environment. Its usage looks like:
+
+>
+```javascript
+const MyBinder = StructBinderFactory({
+ // These config options are all required:
+ heap: WebAssembly.Memory instance or a function which returns
+ a Uint8Array or Int8Array view of the WASM memory,
+ alloc: function(howMuchMemory){...},
+ dealloc: function(pointerToFree){...}
+});
+```
+
+It also offers a number of other settings, but all are optional except
+for the ones shown above. Those three config options abstract away
+details which are specific to a given WASM environment. They provide
+the WASM "heap" memory (a byte array), the memory allocator, and the
+deallocator. In a conventional Emscripten setup, that config might
+simply look like:
+
+>
+```javascript
+{
+ heap: Module['asm']['memory'],
+ //Or:
+ // heap: ()=>Module['HEAP8'],
+ alloc: (n)=>Module['_malloc'](n),
+ dealloc: (m)=>Module['_free'](m)
+}
+```
+
+The StructBinder factory function returns a function which can then be
+used to create bindings for our structs.
+
+
+<a name='step-2'></a>
+Step 2: Create a Struct Description
+------------------------------------------------------------
+
+The primary input for this framework is a JSON-compatible construct
+which describes a struct we want to bind. For example, given this C
+struct:
+
+>
+```c
+// C-side:
+struct Foo {
+ int member1;
+ void * member2;
+ int64_t member3;
+};
+```
+
+Its JSON description looks like:
+
+>
+```json
+{
+ "name": "Foo",
+ "sizeof": 16,
+ "members": {
+ "member1": {"offset": 0,"sizeof": 4,"signature": "i"},
+ "member2": {"offset": 4,"sizeof": 4,"signature": "p"},
+ "member3": {"offset": 8,"sizeof": 8,"signature": "j"}
+ }
+}
+```
+
+These data _must_ match up with the C-side definition of the struct
+(if any). See [Appendix G][appendix-g] for one way to easily generate
+these from C code.
+
+Each entry in the `members` object maps the member's name to
+its low-level layout:
+
+- `offset`: the byte offset from the start of the struct, as reported
+ by C's `offsetof()` feature.
+- `sizeof`: as reported by C's `sizeof()`.
+- `signature`: described below.
+- `readOnly`: optional. If set to true, the binding layer will
+ throw if JS code tries to set that property.
+
+The order of the `members` entries is not important: their memory
+layout is determined by their `offset` and `sizeof` members. The
+`name` property is technically optional, but one of the steps in the
+binding process requires that either it be passed an explicit name or
+there be one in the struct description. The names of the `members`
+entries need not match their C counterparts. Project conventions may
+call for giving them different names in the JS side and the
+[StructBinderFactory][] can be configured to automatically add a
+prefix and/or suffix to their names.
+
+Nested structs are as-yet unsupported by this tool.
+
+Struct member "signatures" describe the data types of the members and
+are an extended variant of the format used by Emscripten's
+`addFunction()`. A signature for a non-function-pointer member, or
+function pointer member which is to be modelled as an opaque pointer,
+is a single letter. A signature for a function pointer may also be
+modelled as a series of letters describing the call signature. The
+supported letters are:
+
+- **`v`** = `void` (only used as return type for function pointer members)
+- **`i`** = `int32` (4 bytes)
+- **`j`** = `int64` (8 bytes) is only really usable if this code is built
+ with BigInt support (e.g. using the Emscripten `-sWASM_BIGINT` build
+ flag). Without that, this API may throw when encountering the `j`
+ signature entry.
+- **`f`** = `float` (4 bytes)
+- **`d`** = `double` (8 bytes)
+- **`p`** = `int32` (but see below!)
+- **`P`** = Like `p` but with extra handling. Described below.
+- **`s`** = like `int32` but is a _hint_ that it's a pointer to a string
+ so that _some_ (very limited) contexts may treat it as such, noting
+ such algorithms must, for lack of information to the contrary,
+ assume both that the encoding is UTF-8 and that the pointer's member
+ is NUL-terminated. If that is _not_ the case for a given string
+ member, do not use `s`: use `i` or `p` instead and do any string
+ handling yourself.
+
+Noting that:
+
+- All of these types are numeric. Attempting to set any struct-bound
+ property to a non-numeric value will trigger an exception except in
+ cases explicitly noted otherwise.
+
+> Sidebar: Emscripten's public docs do not mention `p`, but their
+generated code includes `p` as an alias for `i`, presumably to mean
+"pointer". Though `i` is legal for pointer types in the signature, `p`
+is more descriptive, so this framework encourages the use of `p` for
+pointer-type members. Using `p` for pointers also helps future-proof
+the signatures against the eventuality that WASM eventually supports
+64-bit pointers. Note that sometimes `p` really means
+pointer-to-pointer, but the Emscripten JS/WASM glue does not offer
+that level of expressiveness in these signatures. We simply have to be
+aware of when we need to deal with pointers and pointers-to-pointers
+in JS code.
+
+> Trivia: this API treates `p` as distinctly different from `i` in
+some contexts, so its use is encouraged for pointer types.
+
+Signatures in the form `x(...)` denote function-pointer members and
+`x` denotes non-function members. Functions with no arguments use the
+form `x()`. For function-type signatures, the strings are formulated
+such that they can be passed to Emscripten's `addFunction()` after
+stripping out the `(` and `)` characters. For good measure, to match
+the public Emscripten docs, `p` should also be replaced with `i`. In
+JavaScript that might look like:
+
+>
+```
+signature.replace(/[^vipPsjfd]/g,'').replace(/[pPs]/g,'i');
+```
+
+<a name='step-2-pvsp'></a>
+### `P` vs `p` in Method Signatures
+
+*This support is experimental and subject to change.*
+
+The method signature letter `p` means "pointer," which, in WASM, means
+"integer." `p` is treated as an integer for most contexts, while still
+also being a separate type (analog to how pointers in C are just a
+special use of unsigned numbers). A capital `P` changes the semantics
+of plain member pointers (but not, as of this writing, function
+pointer members) as follows:
+
+- When a `P`-type member is **fetched** via `myStruct.x` and its value is
+ a non-0 integer, [`StructBinder.instanceForPointer()`][StructBinder]
+ is used to try to map that pointer to a struct instance. If a match
+ is found, the "get" operation returns that instance instead of the
+ integer. If no match is found, it behaves exactly as for `p`, returning
+ the integer value.
+- When a `P`-type member is **set** via `myStruct.x=y`, if
+ [`(y instanceof StructType)`][StructType] then the value of `y.pointer` is
+ stored in `myStruct.x`. If `y` is neither a number nor
+ a [StructType][], an exception is triggered (regardless of whether
+ `p` or `P` is used).
+
+
+<a name='step-3'></a>
+Step 3: Binding the Struct
+------------------------------------------------------------
+
+We can now use the results of steps 1 and 2:
+
+>
+```javascript
+const MyStruct = MyBinder(myStructDescription);
+```
+
+That creates a new constructor function, `MyStruct`, which can be used
+to instantiate new instances. The binder will throw if it encounters
+any problems.
+
+That's all there is to it.
+
+> Sidebar: that function may modify the struct description object
+and/or its sub-objects, or may even replace sub-objects, in order to
+simplify certain later operations. If that is not desired, then feed
+it a copy of the original, e.g. by passing it
+`JSON.parse(JSON.stringify(structDefinition))`.
+
+<a name='step-4'></a>
+Step 4: Creating, Using, and Destroying Struct Instances
+------------------------------------------------------------
+
+Now that we have our constructor...
+
+>
+```javascript
+const my = new MyStruct();
+```
+
+It is important to understand that creating a new instance allocates
+memory on the WASM heap. We must not simply rely on garbage collection
+to clean up the instances because doing so will not free up the WASM
+heap memory. The correct way to free up that memory is to use the
+object's `dispose()` method. Alternately, there is a "nuclear option":
+`MyBinder.disposeAll()` will free the memory allocated for _all_
+instances which have not been manually disposed.
+
+The following usage pattern offers one way to easily ensure proper
+cleanup of struct instances:
+
+
+>
+```javascript
+const my = new MyStruct();
+try {
+ console.log(my.member1, my.member2, my.member3);
+ my.member1 = 12;
+ assert(12 === my.member1);
+ /* ^^^ it may seem silly to test that, but recall that assigning that
+ property encodes the value into a byte array in heap memory, not
+ a normal JS property. Similarly, fetching the property decodes it
+ from the byte array. */
+ // Pass the struct to C code which takes a MyStruct pointer:
+ aCFunction( my.pointer );
+ // Type-safely check if a pointer returned from C is a MyStruct:
+ const x = MyStruct.instanceForPointer( anotherCFunction() );
+ // If it is a MyStruct, x now refers to that object. Note, however,
+ // that this only works for instances created in JS, as the
+ // pointer mapping only exists in JS space.
+} finally {
+ my.dispose();
+}
+```
+
+> Sidebar: the `finally` block will be run no matter how the `try`
+exits, whether it runs to completion, propagates an exception, or uses
+flow-control keywords like `return` or `break`. It is perfectly legal
+to use `try`/`finally` without a `catch`, and doing so is an ideal
+match for the memory management requirements of Jaccwaby-bound struct
+instances.
+
+Now that we have struct instances, there are a number of things we
+can do with them, as covered in the rest of this document.
+
+
+<a name='api'></a>
+API Reference
+============================================================
+
+<a name='api-binderfactory'></a>
+API: Binder Factory
+------------------------------------------------------------
+
+This is the top-most function of the API, from which all other
+functions and types are generated. The binder factory's signature is:
+
+>
+```
+Function StructBinderFactory(object configOptions);
+```
+
+It returns a function which these docs refer to as a [StructBinder][]
+(covered in the next section). It throws on error.
+
+The binder factory supports the following options in its
+configuration object argument:
+
+
+- `heap`
+ Must be either a `WebAssembly.Memory` instance representing the WASM
+ heap memory OR a function which returns an Int8Array or Uint8Array
+ view of the WASM heap. In the latter case the function should, if
+ appropriate for the environment, account for the heap being able to
+ grow. Jaccwabyt uses this property in such a way that it "should" be
+ okay for the WASM heap to grow at runtime (that case is, however,
+ untested).
+
+- `alloc`
+ Must be a function semantically compatible with Emscripten's
+ `Module._malloc()`. That is, it is passed the number of bytes to
+ allocate and it returns a pointer. On allocation failure it may
+ either return 0 or throw an exception. This API will throw an
+ exception if allocation fails or will propagate whatever exception
+ the allocator throws. The allocator _must_ use the same heap as the
+ `heap` config option.
+
+- `dealloc`
+ Must be a function semantically compatible with Emscripten's
+ `Module._free()`. That is, it takes a pointer returned from
+ `alloc()` and releases that memory. It must never throw and must
+ accept a value of 0/null to mean "do nothing" (noting that 0 is
+ _technically_ a legal memory address in WASM, but that seems like a
+ design flaw).
+
+- `bigIntEnabled` (bool=true if BigInt64Array is available, else false)
+ If true, the WASM bits this code is used with must have been
+ compiled with int64 support (e.g. using Emscripten's `-sWASM_BIGINT`
+ flag). If that's not the case, this flag should be set to false. If
+ it's enabled, BigInt support is assumed to work and certain extra
+ features are enabled. Trying to use features which requires BigInt
+ when it is disabled (e.g. using 64-bit integer types) will trigger
+ an exception.
+
+- `memberPrefix` and `memberSuffix` (string="")
+ If set, struct-defined properties get bound to JS with this string
+ as a prefix resp. suffix. This can be used to avoid symbol name
+ collisions between the struct-side members and the JS-side ones
+ and/or to make more explicit which object-level properties belong to
+ the struct mapping and which to the JS side. This does not modify
+ the values in the struct description objects, just the property
+ names through which they are accessed via property access operations
+ and the various a [StructInstance][] APIs (noting that the latter
+ tend to permit both the original names and the names as modified by
+ these settings).
+
+- `log`
+ Optional function used for debugging output. By default
+ `console.log` is used but by default no debug output is generated.
+ This API assumes that the function will space-separate each argument
+ (like `console.log` does). See [Appendix D](#appendix-d) for info
+ about enabling debugging output.
+
+
+<a name='api-structbinder'></a>
+API: Struct Binder
+------------------------------------------------------------
+
+Struct Binders are factories which are created by the
+[StructBinderFactory][]. A given Struct Binder can process any number
+of distinct structs. In a typical setup, an app will have ony one
+shared Binder Factory and one Struct Binder. Struct Binders which are
+created via different [StructBinderFactory][] calls are unrelated to each
+other, sharing no state except, perhaps, indirectly via
+[StructBinderFactory][] configuration (e.g. the memory heap).
+
+These factories have two call signatures:
+
+>
+```javascript
+Function StructBinder([string structName,] object structDescription)
+```
+
+If the struct description argument has a `name` property then the name
+argument is optional, otherwise it is required.
+
+The returned object is a constructor for instances of the struct
+described by its argument(s), each of which derives from
+a separate [StructType][] instance.
+
+The Struct Binder has the following members:
+
+- `allocCString(str)`
+ Allocates a new UTF-8-encoded, NUL-terminated copy of the given JS
+ string and returns its address relative to `config.heap()`. If
+ allocation returns 0 this function throws. Ownership of the memory
+ is transfered to the caller, who must eventually pass it to the
+ configured `config.dealloc()` function.
+
+- `config`
+ The configuration object passed to the [StructBinderFactory][],
+ primarily for accessing the memory (de)allocator and memory. Modifying
+ any of its "significant" configuration values may have undefined
+ results.
+
+- `instanceForPointer(pointer)`
+ Given a pointer value relative to `config.memory`, if that pointer
+ resolves to a struct of _any type_ generated via the same Struct
+ Binder, this returns the struct instance associated with it, or
+ `undefined` if no struct object is mapped to that pointer. This
+ differs from the struct-type-specific member of the same name in
+ that this one is not "type-safe": it does not know the type of the
+ returned object (if any) and may return a struct of any
+ [StructType][] for which this Struct Binder has created a
+ constructor. It cannot return instances created via a different
+ [StructBinderFactory][] because each factory can hypothetically have
+ a different memory heap.
+
+
+<a name='api-structtype'></a>
+API: Struct Type
+------------------------------------------------------------
+
+The StructType class is a property of the [StructBinder][] function.
+
+Each constructor created by a [StructBinder][] inherits from _its own
+instance_ of the StructType class, which contains state specific to
+that struct type (e.g. the struct name and description metadata).
+StructTypes which are created via different [StructBinder][] instances
+are unrelated to each other, sharing no state except [StructBinderFactory][]
+config options.
+
+The StructType constructor cannot be called from client code. It is
+only called by the [StructBinder][]-generated
+[constructors][StructCtors]. The `StructBinder.StructType` object
+has the following "static" properties (^Which are accessible from
+individual instances via `theInstance.constructor`.):
+
+- `allocCString(str)`
+ Identical to the [StructBinder][] method of the same name.
+
+- `hasExternalPointer(object)`
+ Returns true if the given object's `pointer` member refers to an
+ "external" object. That is the case when a pointer is passed to a
+ [struct's constructor][StructCtors]. If true, the memory is owned by
+ someone other than the object and must outlive the object.
+
+- `instanceForPointer(pointer)`
+ Works identically to the [StructBinder][] method of the same name.
+
+- `isA(value)`
+ Returns true if its argument is a StructType instance _from the same
+ [StructBinder][]_ as this StructType.
+
+- `memberKey(string)`
+ Returns the given string wrapped in the configured `memberPrefix`
+ and `memberSuffix` values. e.g. if passed `"x"` and `memberPrefix`
+ is `"$"` then it returns `"$x"`. This does not verify that the
+ property is actually a struct a member, it simply transforms the
+ given string. TODO(?): add a 2nd parameter indicating whether it
+ should validate that it's a known member name.
+
+The base StructType prototype has the following members, all of which
+are inherited by [struct instances](#api-structinstance) and may only
+legally be called on concrete struct instances unless noted otherwise:
+
+- `dispose()`
+ Frees, if appropriate, the WASM-allocated memory which is allocated
+ by the constructor. If this is not called before the JS engine
+ cleans up the object, a leak in the WASM heap memory pool will result.
+ When `dispose()` is called, if the object has a property named `ondispose`
+ then it is treated as follows:
+ - If it is a function, it is called with the struct object as its `this`.
+ That method must not throw - if it does, the exception will be
+ ignored.
+ - If it is an array, it may contain functions, pointers, and/or JS
+ strings. If an entry is a function, it is called as described
+ above. If it's a number, it's assumed to be a pointer and is
+ passed to the `dealloc()` function configured for the parent
+ [StructBinder][]. If it's a JS string, it's assumed to be a
+ helpful description of the next entry in the list and is simply
+ ignored. Strings are supported primarily for use as debugging
+ information.
+ - Some struct APIs will manipulate the `ondispose` member, creating
+ it as an array or converting it from a function to array as
+ needed.
+
+- `lookupMember(memberName,throwIfNotFound=true)`
+ Given the name of a mapped struct member, it returns the member
+ description object. If not found, it either throws (if the 2nd
+ argument is true) or returns `undefined` (if the second argument is
+ false). The first argument may be either the member name as it is
+ mapped in the struct description or that same name with the
+ configured `memberPrefix` and `memberSuffix` applied, noting that
+ the lookup in the former case is faster.\
+ This method may be called directly on the prototype, without a
+ struct instance.
+
+- `memberToJsString(memberName)`
+ Uses `this.lookupMember(memberName,true)` to look up the given
+ member. If its signature is `s` then it is assumed to refer to a
+ NUL-terminated, UTF-8-encoded string and its memory is decoded as
+ such. If its signature is not one of those then an exception is
+ thrown. If its address is 0, `null` is returned. See also:
+ `setMemberCString()`.
+
+- `memberIsString(memberName [,throwIfNotFound=true])`
+ Uses `this.lookupMember(memberName,throwIfNotFound)` to look up the
+ given member. Returns the member description object if the member
+ has a signature of `s`, else returns false. If the given member is
+ not found, it throws if the 2nd argument is true, else it returns
+ false.
+
+- `memberKey(string)`
+ Works identically to `StructBinder.StructType.memberKey()`.
+
+- `memberKeys()`
+ Returns an array of the names of the properties of this object
+ which refer to C-side struct counterparts.
+
+- `memberSignature(memberName [,emscriptenFormat=false])`
+ Returns the signature for a given a member property, either in this
+ framework's format or, if passed a truthy 2nd argument, in a format
+ suitable for the 2nd argument to Emscripten's `addFunction()`.
+ Throws if the first argument does not resolve to a struct-bound
+ member name. The member name is resolved using `this.lookupMember()`
+ and throws if the member is found mapped.
+
+- `memoryDump()`
+ Returns a Uint8Array which contains the current state of this
+ object's raw memory buffer. Potentially useful for debugging, but
+ not much else. Note that the memory is necessarily, for
+ compatibility with C, written in the host platform's endianness and
+ is thus not useful as a persistent/portable serialization format.
+
+- `setMemberCString(memberName,str)`
+ Uses `StructType.allocCString()` to allocate a new C-style string,
+ assign it to the given member, and add the new string to this
+ object's `ondispose` list for cleanup when `this.dispose()` is
+ called. This function throws if `lookupMember()` fails for the given
+ member name, if allocation of the string fails, or if the member has
+ a signature value of anything other than `s`. Returns `this`.
+ *Achtung*: calling this repeatedly will not immediately free the
+ previous values because this code cannot know whether they are in
+ use in other places, namely C. Instead, each time this is called,
+ the prior value is retained in the `ondispose` list for cleanup when
+ the struct is disposed of. Because of the complexities and general
+ uncertainties of memory ownership and lifetime in such
+ constellations, it is recommended that the use of C-string members
+ from JS be kept to a minimum or that the relationship be one-way:
+ let C manage the strings and only fetch them from JS using, e.g.,
+ `memberToJsString()`.
+
+
+<a name='api-structctor'></a>
+API: Struct Constructors
+------------------------------------------------------------
+
+Struct constructors (the functions returned from [StructBinder][])
+are used for, intuitively enough, creating new instances of a given
+struct type:
+
+>
+```
+const x = new MyStruct;
+```
+
+Normally they should be passed no arguments, but they optionally
+accept a single argument: a WASM heap pointer address of memory
+which the object will use for storage. It does _not_ take over
+ownership of that memory and that memory must be valid at
+for least as long as this struct instance. This is used, for example,
+to proxy static/shared C-side instances:
+
+>
+```
+const x = new MyStruct( someCFuncWhichReturnsAMyStructPointer() );
+...
+x.dispose(); // does NOT free the memory
+```
+
+The JS-side construct does not own the memory in that case and has no
+way of knowing when the C-side struct is destroyed. Results are
+specifically undefined if the JS-side struct is used after the C-side
+struct's member is freed.
+
+> Potential TODO: add a way of passing ownership of the C-side struct
+to the JS-side object. e.g. maybe simply pass `true` as the second
+argument to tell the constructor to take over ownership. Currently the
+pointer can be taken over using something like
+`myStruct.ondispose=[myStruct.pointer]` immediately after creation.
+
+These constructors have the following "static" members:
+
+- `disposeAll()`
+ For each instance of this struct, the equivalent of its `dispose()`
+ method is called. This frees all WASM-allocated memory associated
+ with _all_ instances and clears the `instanceForPointer()`
+ mappings. Returns `this`.
+
+- `instanceForPointer(pointer)`
+ Given a pointer value (accessible via the `pointer` property of all
+ struct instances) which ostensibly refers to an instance of this
+ class, this returns the instance associated with it, or `undefined`
+ if no object _of this specific struct type_ is mapped to that
+ pointer. When C-side code calls back into JS code and passes a
+ pointer to an object, this function can be used to type-safely
+ "cast" that pointer back to its original object.
+
+- `isA(value)`
+ Returns true if its argument was created by this constructor.
+
+- `memberKey(string)`
+ Works exactly as documented for [StructType][].
+
+- `memberKeys(string)`
+ Works exactly as documented for [StructType][].
+
+- `resolveToInstance(value [,throwIfNot=false])`
+ Works like `instanceForPointer()` but accepts either an instance
+ of this struct type or a pointer which resolves to one.
+ It returns an instance of this struct type on success.
+ By default it returns a falsy value if its argument is not,
+ or does not resolve to, an instance of this struct type,
+ but if passed a truthy second argument then it will throw
+ instead.
+
+- `structInfo`
+ The structure description passed to [StructBinder][] when this
+ constructor was generated.
+
+- `structName`
+ The structure name passed to [StructBinder][] when this constructor
+ was generated.
+
+
+<a name='api-structprototype'></a>
+API: Struct Prototypes
+------------------------------------------------------------
+
+The prototypes of structs created via [the constructors described in
+the previous section][StructCtors] are each a struct-type-specific
+instance of [StructType][] and add the following struct-type-specific
+properties to the mix:
+
+- `structInfo`
+ The struct description metadata, as it was given to the
+ [StructBinder][] which created this class.
+
+- `structName`
+ The name of the struct, as it was given to the [StructBinder][] which
+ created this class.
+
+<a name='api-structinstance'></a>
+API: Struct Instances
+------------------------------------------------------------------------
+
+Instances of structs created via [the constructors described
+above][StructCtors] each have the following instance-specific state in
+common:
+
+- `pointer`
+ A read-only numeric property which is the "pointer" returned by the
+ configured allocator when this object is constructed. After
+ `dispose()` (inherited from [StructType][]) is called, this property
+ has the `undefined` value. When calling C-side code which takes a
+ pointer to a struct of this type, simply pass it `myStruct.pointer`.
+
+<a name='appendices'></a>
+Appendices
+============================================================
+
+<a name='appendix-a'></a>
+Appendix A: Limitations, TODOs, and Non-TODOs
+------------------------------------------------------------
+
+- This library only supports the basic set of member types supported
+ by WASM: numbers (which includes pointers). Nested structs are not
+ handled except that a member may be a _pointer_ to such a
+ struct. Whether or not it ever will depends entirely on whether its
+ developer ever needs that support. Conversion of strings between
+ JS and C requires infrastructure specific to each WASM environment
+ and is not directly supported by this library.
+
+- Binding functions to struct instances, such that C can see and call
+ JS-defined functions, is not as transparent as it really could be,
+ due to [shortcomings in the Emscripten
+ `addFunction()`/`removeFunction()`
+ interfaces](https://github.com/emscripten-core/emscripten/issues/17323). Until
+ a replacement for that API can be written, this support will be
+ quite limited. It _is_ possible to bind a JS-defined function to a
+ C-side function pointer and call that function from C. What's
+ missing is easier-to-use/more transparent support for doing so.
+ - In the meantime, a [standalone
+ subproject](/file/common/whwasmutil.js) of Jaccwabyt provides such a
+ binding mechanism, but integrating it directly with Jaccwabyt would
+ not only more than double its size but somehow feels inappropriate, so
+ experimentation is in order for how to offer that capability via
+ completely optional [StructBinderFactory][] config options.
+
+- It "might be interesting" to move access of the C-bound members into
+ a sub-object. e.g., from JS they might be accessed via
+ `myStructInstance.s.structMember`. The main advantage is that it would
+ eliminate any potential confusion about which members are part of
+ the C struct and which exist purely in JS. "The problem" with that
+ is that it requires internally mapping the `s` member back to the
+ object which contains it, which makes the whole thing more costly
+ and adds one more moving part which can break. Even so, it's
+ something to try out one rainy day. Maybe even make it optional and
+ make the `s` name configurable via the [StructBinderFactory][]
+ options. (Over-engineering is an arguably bad habit of mine.)
+
+- It "might be interesting" to offer (de)serialization support. It
+ would be very limited, e.g. we can't serialize arbitrary pointers in
+ any meaningful way, but "might" be useful for structs which contain
+ only numeric or C-string state. As it is, it's easy enough for
+ client code to write wrappers for that and handle the members in
+ ways appropriate to their apps. Any impl provided in this library
+ would have the shortcoming that it may inadvertently serialize
+ pointers (since they're just integers), resulting in potential chaos
+ after deserialization. Perhaps the struct description can be
+ extended to tag specific members as serializable and how to
+ serialize them.
+
+<a name='appendix-d'></a>
+Appendix D: Debug Info
+------------------------------------------------------------
+
+The [StructBinderFactory][], [StructBinder][], and [StructType][] classes
+all have the following "unsupported" method intended primarily
+to assist in their own development, as opposed to being for use in
+client code:
+
+- `debugFlags(flags)` (integer)
+ An "unsupported" debugging option which may change or be removed at
+ any time. Its argument is a set of flags to enable/disable certain
+ debug/tracing output for property accessors: 0x01 for getters, 0x02
+ for setters, 0x04 for allocations, 0x08 for deallocations. Pass 0 to
+ disable all flags and pass a negative value to _completely_ clear
+ all flags. The latter has the side effect of telling the flags to be
+ inherited from the next-higher-up class in the hierarchy, with
+ [StructBinderFactory][] being top-most, followed by [StructBinder][], then
+ [StructType][].
+
+
+<a name='appendix-g'></a>
+Appendix G: Generating Struct Descriptions From C
+------------------------------------------------------------
+
+Struct definitions are _ideally_ generated from WASM-compiled C, as
+opposed to simply guessing the sizeofs and offsets, so that the sizeof
+and offset information can be collected using C's `sizeof()` and
+`offsetof()` features (noting that struct padding may impact offsets
+in ways which might not be immediately obvious, so writing them by
+hand is _most certainly not recommended_).
+
+How exactly the desciption is generated is necessarily
+project-dependent. It's tempting say, "oh, that's easy! We'll just
+write it by hand!" but that would be folly. The struct sizes and byte
+offsets into the struct _must_ be precisely how C-side code sees the
+struct or the runtime results are completely undefined.
+
+The approach used in developing and testing _this_ software is...
+
+Below is a complete copy/pastable example of how we can use a small
+set of macros to generate struct descriptions from C99 or later into
+static string memory. Simply add such a file to your WASM build,
+arrange for its function to be exported[^export-func], and call it
+from JS (noting that it requires environment-specific JS glue to
+convert the returned pointer to a JS-side string). Use `JSON.parse()`
+to process it, then feed the included struct descriptions into the
+binder factory at your leisure.
+
+------------------------------------------------------------
+
+```c
+#include <string.h> /* memset() */
+#include <stddef.h> /* offsetof() */
+#include <stdio.h> /* snprintf() */
+#include <stdint.h> /* int64_t */
+#include <assert.h>
+
+struct ExampleStruct {
+ int v4;
+ void * ppV;
+ int64_t v8;
+ void (*xFunc)(void*);
+};
+typedef struct ExampleStruct ExampleStruct;
+
+const char * wasm__ctype_json(void){
+ static char strBuf[512 * 8] = {0}
+ /* Static buffer which must be sized large enough for
+ our JSON. The string-generation macros try very
+ hard to assert() if this buffer is too small. */;
+ int n = 0, structCount = 0 /* counters for the macros */;
+ char * pos = &strBuf[1]
+ /* Write-position cursor. Skip the first byte for now to help
+ protect against a small race condition */;
+ char const * const zEnd = pos + sizeof(strBuf)
+ /* one-past-the-end cursor (virtual EOF) */;
+ if(strBuf[0]) return strBuf; // Was set up in a previous call.
+
+ ////////////////////////////////////////////////////////////////////
+ // First we need to build up our macro framework...
+
+ ////////////////////////////////////////////////////////////////////
+ // Core output-generating macros...
+#define lenCheck assert(pos < zEnd - 100)
+#define outf(format,...) \
+ pos += snprintf(pos, ((size_t)(zEnd - pos)), format, __VA_ARGS__); \
+ lenCheck
+#define out(TXT) outf("%s",TXT)
+#define CloseBrace(LEVEL) \
+ assert(LEVEL<5); memset(pos, '}', LEVEL); pos+=LEVEL; lenCheck
+
+ ////////////////////////////////////////////////////////////////////
+ // Macros for emiting StructBinders...
+#define StructBinder__(TYPE) \
+ n = 0; \
+ outf("%s{", (structCount++ ? ", " : "")); \
+ out("\"name\": \"" # TYPE "\","); \
+ outf("\"sizeof\": %d", (int)sizeof(TYPE)); \
+ out(",\"members\": {");
+#define StructBinder_(T) StructBinder__(T)
+// ^^^ extra indirection needed to expand CurrentStruct
+#define StructBinder StructBinder_(CurrentStruct)
+#define _StructBinder CloseBrace(2)
+#define M(MEMBER,SIG) \
+ outf("%s\"%s\": " \
+ "{\"offset\":%d,\"sizeof\": %d,\"signature\":\"%s\"}", \
+ (n++ ? ", " : ""), #MEMBER, \
+ (int)offsetof(CurrentStruct,MEMBER), \
+ (int)sizeof(((CurrentStruct*)0)->MEMBER), \
+ SIG)
+ // End of macros.
+ ////////////////////////////////////////////////////////////////////
+
+ ////////////////////////////////////////////////////////////////////
+ // With that out of the way, we can do what we came here to do.
+ out("\"structs\": ["); {
+
+// For each struct description, do...
+#define CurrentStruct ExampleStruct
+ StructBinder {
+ M(v4,"i");
+ M(ppV,"p");
+ M(v8,"j");
+ M(xFunc,"v(p)");
+ } _StructBinder;
+#undef CurrentStruct
+
+ } out( "]"/*structs*/);
+ ////////////////////////////////////////////////////////////////////
+ // Done! Finalize the output...
+ out("}"/*top-level wrapper*/);
+ *pos = 0;
+ strBuf[0] = '{'/*end of the race-condition workaround*/;
+ return strBuf;
+
+// If this file will ever be concatenated or #included with others,
+// it's good practice to clean up our macros:
+#undef StructBinder
+#undef StructBinder_
+#undef StructBinder__
+#undef M
+#undef _StructBinder
+#undef CloseBrace
+#undef out
+#undef outf
+#undef lenCheck
+}
+```
+
+------------------------------------------------------------
+
+<style>
+div.content {
+ counter-reset: h1 -1;
+}
+div.content h1, div.content h2, div.content h3 {
+ border-radius: 0.25em;
+ border-bottom: 1px solid #70707070;
+}
+div.content h1 {
+ counter-reset: h2;
+}
+div.content h1::before, div.content h2::before, div.content h3::before {
+ background-color: #a5a5a570;
+ margin-right: 0.5em;
+ border-radius: 0.25em;
+}
+div.content h1::before {
+ counter-increment: h1;
+ content: counter(h1) ;
+ padding: 0 0.5em;
+ border-radius: 0.25em;
+}
+div.content h2::before {
+ counter-increment: h2;
+ content: counter(h1) "." counter(h2);
+ padding: 0 0.5em 0 1.75em;
+ border-radius: 0.25em;
+}
+div.content h2 {
+ counter-reset: h3;
+}
+div.content h3::before {
+ counter-increment: h3;
+ content: counter(h1) "." counter(h2) "." counter(h3);
+ padding: 0 0.5em 0 2.5em;
+}
+div.content h3 {border-left-width: 2.5em}
+</style>
+
+[sqlite3]: https://sqlite.org
+[emscripten]: https://emscripten.org
+[sgb]: https://wanderinghorse.net/home/stephan/
+[appendix-g]: #appendix-g
+[StructBinderFactory]: #api-binderfactory
+[StructCtors]: #api-structctor
+[StructType]: #api-structtype
+[StructBinder]: #api-structbinder
+[StructInstance]: #api-structinstance
+[^export-func]: In Emscripten, add its name, prefixed with `_`, to the
+ project's `EXPORT_FUNCTIONS` list.
+[BigInt64Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array
+[TextDecoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
+[TextEncoder]: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
+[MDN]: https://developer.mozilla.org/docs/Web/API
diff --git a/ext/wasm/module-symbols.html b/ext/wasm/module-symbols.html
new file mode 100644
index 0000000..427d2dc
--- /dev/null
+++ b/ext/wasm/module-symbols.html
@@ -0,0 +1,333 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <title>sqlite3 Module Symbols</title>
+ <style>
+ body {
+ font-size: 12.5pt;
+ padding-bottom: 1em;
+ }
+ </style>
+ </head>
+ <body>
+<div class="fossil-doc" data-title="sqlite3 Module Symbols"><!-- EXTRACT_BEGIN -->
+<!--
+ The part of this doc wrapped in div.fossil-doc gets snipped out
+ from the canonical copy in the main tree (ext/wasm/module-symbols.html)
+ and added to the wasm docs repository, where it's served from
+ fossil.
+-->
+ <style>
+ .pseudolist {
+ column-count: auto;
+ column-width: 12rem;
+ column-gap: 1.5em;
+ width: 90%;
+ margin: auto;
+ }
+ .pseudolist.wide {
+ column-width: 21rem;
+ }
+ .pseudolist.wide2 {
+ column-width: 25rem;
+ }
+ .pseudolist > span {
+ font-family: monospace;
+ margin: 0.25em 0;
+ display: block;
+ }
+ .pseudolist.wrap-anywhere {
+ overflow-wrap: anywhere;
+ }
+ .warning { color: firebrick }
+ .error { color: firebrick; background-color: yellow}
+ .hidden, .initially-hidden {
+ position: absolute !important;
+ opacity: 0 !important;
+ pointer-events: none !important;
+ display: none !important;
+ }
+ h1::before, h2::before, h3::before, h4::before {
+ /* Remove automatic numbering */
+ content: "" !important;
+ background-color: transparent !important;
+ margin: 0 !important;
+ border: 0 !important;
+ padding: 0 !important;
+ }
+ .func-wasm {
+
+ }
+ .func-wasm::after {
+ content: "WASM";
+ color: saddlebrown;
+ /* ^^^^ the color must be legible in both "bright" and "dark"
+ s ite themes. */
+ font-size: 0.65em;
+ /* baseline-shift: super; */
+ vertical-align: super;
+ }
+ </style>
+ <p id='module-load-status'><strong>Loading WASM module...</strong>
+ If this takes "a long time" it may have failed and the browser's
+ dev console may contain hints as to why.
+ </p>
+
+ <p>
+ This page lists the SQLite3 APIs exported
+ by <code>sqlite3.wasm</code> and exposed to clients
+ by <code>sqlite3.js</code>. These lists are generated dynamically
+ by loading the JS/WASM module and introspecting it, with the following
+ caveats:
+ </p>
+
+ <ul>
+ <li>Some APIs are explicitly filtered out of these lists because
+ they are strictly for internal use within the JS/WASM APIs and
+ its own test code.
+ </li>
+ <li>This page runs in the main UI thread so cannot see features
+ which are only available in a Worker thread. If this page were
+ to function via a Worker, it would not be able to see
+ functionality only available in the main thread. Starting a
+ Worker here to fetch those symbols requires loading a second
+ copy of the sqlite3 WASM module and JS code.
+ </li>
+ </ul>
+
+ <div class='initially-hidden'>
+
+ <p>This page exposes a global symbol named <code>sqlite3</code>
+ which can be inspected using the browser's dev tools.
+ </p>
+
+ <p>Jump to...</p>
+ <ul>
+ <li><a href='#sqlite3-namespace'><code>sqlite3</code> namespace</a></li>
+ <li><a href='#sqlite3-version'><code>sqlite3.version</code> object</a></li>
+ <li><a href='#sqlite3-functions'><code>sqlite3_...()</code> functions</a></li>
+ <li><a href='#sqlite3-constants'><code>SQLITE_...</code> constants</a></li>
+ <li><a href='#sqlite3.oo1'><code>sqlite3.oo1</code> namespace</a>
+ <!--ul>
+ <li><a href='#sqlite3.oo1.DB'><code>sqlite3.oo1.DB</code></a></li>
+ <li><a href='#sqlite3.oo1.Stmt'><code>sqlite3.oo1.Stmt</code></a></li>
+ </ul-->
+ </li>
+ <li><a href='#sqlite3.wasm'><code>sqlite3.wasm</code> namespace</a></li>
+ <li><a href='#sqlite3.wasm.pstack'><code>sqlite3.wasm.pstack</code> namespace</a></li>
+ <li><a href='#compile-options'>Compilation options used in this module build</a></li>
+ </ul>
+
+ <a id="sqlite3-namespace"></a>
+ <h1><code>sqlite3</code> Namespace</h1>
+ <p>
+ The <code>sqlite3</code> namespace object exposes the following...
+ </p>
+ <div id='list-namespace' class='pseudolist'></div>
+
+ <a id="sqlite3-version"></a>
+ <h1><code>sqlite3.version</code> Object</h1>
+ <p>
+ The <code>sqlite3.version</code> object exposes the following...
+ </p>
+ <div id='list-version' class='pseudolist wide wrap-anywhere'></div>
+
+ <a id="sqlite3-functions"></a>
+ <h1><code>sqlite3_...()</code> Function List</h1>
+
+ <p>The <code>sqlite3.capi</code> namespace exposes the following
+ <a href='https://sqlite.org/c3ref/funclist.html'><code>sqlite3_...()</code>
+ functions</a>...
+ </p>
+ <div id='list-functions' class='pseudolist wide'></div>
+ <p>
+ <code class='func-wasm'></code> = function is specific to the JS/WASM
+ bindings, not part of the C API.
+ </p>
+
+ <a id="sqlite3-constants"></a>
+ <h1><code>SQLITE_...</code> Constants</h1>
+
+ <p>The <code>sqlite3.capi</code> namespace exposes the following
+ <a href='https://sqlite.org/c3ref/constlist.html'><code>SQLITE_...</code>
+ constants</a>...
+ </p>
+ <div id='list-constants' class='pseudolist wide'></div>
+
+ <a id="sqlite3.oo1"></a>
+ <h1><code>sqlite3.oo1</code> Namespace</h1>
+ <p>
+ The <code>sqlite3.oo1</code> namespace exposes the following...
+ </p>
+ <div id='list-oo1' class='pseudolist'></div>
+
+ <a id="sqlite3.wasm"></a>
+ <h1><code>sqlite3.wasm</code> Namespace</h1>
+ <p>
+ The <code>sqlite3.wasm</code> namespace exposes the
+ following...
+ </p>
+ <div id='list-wasm' class='pseudolist'></div>
+
+ <a id="sqlite3.wasm.pstack"></a>
+ <h1><code>sqlite3.wasm.pstack</code> Namespace</h1>
+ <p>
+ The <code>sqlite3.wasm.pstack</code> namespace exposes the
+ following...
+ </p>
+ <div id='list-wasm-pstack' class='pseudolist'></div>
+
+ <a id="compile-options"></a>
+ <h1>Compilation Options</h1>
+ <p>
+ <code>SQLITE_...</code> compilation options used in this build
+ of <code>sqlite3.wasm</code>...
+ </p>
+ <div id='list-compile-options' class='pseudolist wide2'></div>
+
+ </div><!-- .initially-hidden -->
+ <script src="jswasm/sqlite3.js">/* This tag MUST be inside the
+ fossil-doc block so that this part can work without modification in
+ the wasm docs repo. */</script>
+ <script>(async function(){
+ const eNew = (tag,parent)=>{
+ const e = document.createElement(tag);
+ if(parent) parent.appendChild(e);
+ return e;
+ };
+ const eLi = (label,parent)=>{
+ const e = eNew('span',parent);
+ e.innerText = label;
+ return e;
+ };
+ const E = (sel)=>document.querySelector(sel);
+ const EAll = (sel)=>document.querySelectorAll(sel);
+ const eFuncs = E('#list-functions'),
+ eConst = E('#list-constants');
+ const renderConst = function(name){
+ eLi(name, eConst);
+ };
+ const renderFunc = function(name){
+ let lbl = name+'()';
+ const e = eLi(lbl, eFuncs);;
+ if(name.startsWith('sqlite3_js')
+ || name.startsWith('sqlite3_wasm')){
+ e.classList.add('func-wasm');
+ }
+ };
+ const renderGeneric = function(name,value,eParent){
+ let lbl;
+ if(value instanceof Function) lbl = name+'()';
+ else{
+ switch(typeof value){
+ case 'number': case 'boolean': case 'string':
+ lbl = name+' = '+JSON.stringify(value);
+ break;
+ default:
+ lbl = name + ' ['+(typeof value)+']';
+ }
+ }
+ const e = eLi(lbl, eParent);
+ if(name.startsWith('sqlite3_wasm')){
+ e.classList.add('func-wasm');
+ }
+ };
+ const renderIt = async function(sqlite3){
+ self.sqlite3 = sqlite3;
+ console.warn("sqlite3 installed as global symbol self.sqlite3.");
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ const cmpIcase = (a,b)=>a.toLowerCase().localeCompare(b.toLowerCase());
+ const renderX = function(tgtElem, container, keys){
+ for(const k of keys.sort(cmpIcase)){
+ renderGeneric(k, container[k], tgtElem);
+ }
+ };
+
+ const excludeNamespace = ['scriptInfo','StructBinder'];
+ renderX(
+ E('#list-namespace'), sqlite3,
+ Object.keys(sqlite3)
+ .filter((v)=>excludeNamespace.indexOf(v)<0)
+ );
+ renderX(
+ E('#list-version'), sqlite3.version,
+ Object.keys(sqlite3.version)
+ );
+
+ /* sqlite3_...() and SQLITE_... */
+ const lists = {c: [], f: []};
+ for(const [k,v] of Object.entries(capi)){
+ if(k.startsWith('SQLITE_')) lists.c.push(k);
+ else if(k.startsWith('sqlite3_')) lists.f.push(k);
+ }
+ const excludeCapi = [
+ 'sqlite3_wasmfs_filename_is_persistent',
+ 'sqlite3_wasmfs_opfs_dir'
+ ];
+ lists.c.sort().forEach(renderConst);
+ lists.f
+ .filter((v)=>excludeCapi.indexOf(v)<0)
+ .sort()
+ .forEach(renderFunc);
+ lists.c = lists.f = null;
+
+ renderX(E('#list-oo1'), sqlite3.oo1,
+ Object.keys(sqlite3.oo1) );
+
+ const excludeWasm = ['ctype'];
+ renderX(E('#list-wasm'),
+ wasm, Object.keys(wasm).filter((v)=>{
+ return !v.startsWith('sqlite3_wasm_')
+ && excludeWasm.indexOf(v)<0;
+ }));
+ const psKeys = Object.keys(wasm.pstack);
+ psKeys.push('pointer','quota','remaining');
+ renderX(E('#list-wasm-pstack'), wasm.pstack, psKeys);
+
+ const cou = wasm.compileOptionUsed();
+ //const cou2 = Object.create(null);
+ //Object.entries(cou).forEach((e)=>cou2['SQLITE_'+e[0]] = e[1]);
+ renderX(E('#list-compile-options'), cou, Object.keys(cou));
+ };
+
+ /**
+ This is a module object for use with the emscripten-installed
+ sqlite3InitModule() factory function.
+ */
+ const myModule = {
+ print: (...args)=>{console.log(...args)},
+ printErr: (...args)=>{console.error(...args)},
+ /**
+ Called by the Emscripten module init bits to report loading
+ progress. It gets passed an empty argument when loading is done
+ (after onRuntimeInitialized() and any this.postRun callbacks
+ have been run).
+ */
+ setStatus: function f(text){
+ if(!f.last){
+ f.last = { text: '', step: 0 };
+ f.ui = {
+ status: E('#module-load-status')
+ };
+ }
+ if(text === f.last.text) return;
+ f.last.text = text;
+ ++f.last.step;
+ if(text) {
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = text;
+ }else{
+ f.ui.status.classList.add('hidden');
+ EAll('.initially-hidden').forEach((e)=>{
+ e.classList.remove('initially-hidden');
+ });
+ }
+ }
+ }/*myModule*/;
+ self.sqlite3InitModule(myModule).then(renderIt);
+})();</script>
+</div><!-- .fossil-doc EXTRACT_END -->
+</body></html>
diff --git a/ext/wasm/scratchpad-wasmfs-main.html b/ext/wasm/scratchpad-wasmfs-main.html
new file mode 100644
index 0000000..91f6152
--- /dev/null
+++ b/ext/wasm/scratchpad-wasmfs-main.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3 WASMFS/OPFS Main-thread Scratchpad</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>sqlite3 WASMFS/OPFS Main-thread Scratchpad</span></header>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <p>Scratchpad/test app for the WASMF/OPFS integration in the
+ main window thread. This page requires that the sqlite3 API have
+ been built with WASMFS support. If OPFS support is available then
+ it "should" persist a database across reloads (watch the dev console
+ output), otherwise it will not.
+ </p>
+ <p>All stuff on this page happens in the dev console.</p>
+ <hr>
+ <div id='test-output'></div>
+ <script src="sqlite3-wasmfs.js"></script>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="scratchpad-wasmfs-main.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/scratchpad-wasmfs-main.js b/ext/wasm/scratchpad-wasmfs-main.js
new file mode 100644
index 0000000..56f9325
--- /dev/null
+++ b/ext/wasm/scratchpad-wasmfs-main.js
@@ -0,0 +1,70 @@
+/*
+ 2022-05-22
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A basic test script for sqlite3-api.js. This file must be run in
+ main JS thread and sqlite3.js must have been loaded before it.
+*/
+'use strict';
+(function(){
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const log = console.log.bind(console),
+ warn = console.warn.bind(console),
+ error = console.error.bind(console);
+
+ const stdout = log;
+ const stderr = error;
+
+ const test1 = function(db){
+ db.exec("create table if not exists t(a);")
+ .transaction(function(db){
+ db.prepare("insert into t(a) values(?)")
+ .bind(new Date().getTime())
+ .stepFinalize();
+ stdout("Number of values in table t:",
+ db.selectValue("select count(*) from t"));
+ });
+ };
+
+ const runTests = function(sqlite3){
+ const capi = sqlite3.capi,
+ oo = sqlite3.oo1,
+ wasm = sqlite3.wasm;
+ stdout("Loaded sqlite3:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
+ const persistentDir = capi.sqlite3_wasmfs_opfs_dir();
+ if(persistentDir){
+ stdout("Persistent storage dir:",persistentDir);
+ }else{
+ stderr("No persistent storage available.");
+ }
+ const startTime = performance.now();
+ let db;
+ try {
+ db = new oo.DB(persistentDir+'/foo.db');
+ stdout("DB filename:",db.filename);
+ const banner1 = '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>',
+ banner2 = '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<';
+ [
+ test1
+ ].forEach((f)=>{
+ const n = performance.now();
+ stdout(banner1,"Running",f.name+"()...");
+ f(db, sqlite3);
+ stdout(banner2,f.name+"() took ",(performance.now() - n),"ms");
+ });
+ }finally{
+ if(db) db.close();
+ }
+ stdout("Total test time:",(performance.now() - startTime),"ms");
+ };
+
+ sqlite3InitModule(self.sqlite3TestModule).then(runTests);
+})();
diff --git a/ext/wasm/speedtest1-wasmfs.html b/ext/wasm/speedtest1-wasmfs.html
new file mode 100644
index 0000000..e355467
--- /dev/null
+++ b/ext/wasm/speedtest1-wasmfs.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>speedtest1-wasmfs.wasm</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>speedtest1-wasmfs.wasm</span></header>
+ <div>See also: <a href='speedtest1-worker.html'>A Worker-thread variant of this page.</a></div>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <div class='warning'>This page starts running the main exe when it loads, which will
+ block the UI until it finishes! Adding UI controls to manually configure and start it
+ are TODO.</div>
+ </div>
+ <div class='warning'>Achtung: running it with the dev tools open may
+ <em>drastically</em> slow it down. For faster results, keep the dev
+ tools closed when running it!
+ </div>
+ <div>Output is delayed/buffered because we cannot update the UI while the
+ speedtest is running. Output will appear below when ready...
+ <div id='test-output'></div>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="speedtest1-wasmfs.js"></script>
+ <script>(function(){
+ /**
+ If this environment contains OPFS, this function initializes it and
+ returns the name of the dir on which OPFS is mounted, else it returns
+ an empty string.
+ */
+ const wasmfsDir = function f(wasmUtil,dirName="/opfs"){
+ if(undefined !== f._) return f._;
+ if( !self.FileSystemHandle
+ || !self.FileSystemDirectoryHandle
+ || !self.FileSystemFileHandle){
+ return f._ = "";
+ }
+ try{
+ if(0===wasmUtil.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], dirName
+ )){
+ return f._ = dirName;
+ }else{
+ return f._ = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return f._ = "";
+ }
+ };
+ wasmfsDir._ = undefined;
+
+ const eOut = document.querySelector('#test-output');
+ const log2 = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOut.append(ln);
+ //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
+ };
+ const logList = [];
+ const dumpLogList = function(){
+ logList.forEach((v)=>log2('',v));
+ logList.length = 0;
+ };
+ /* can't update DOM while speedtest is running unless we run
+ speedtest in a worker thread. */;
+ const log = (...args)=>{
+ console.log(...args);
+ logList.push(args.join(' '));
+ };
+ const logErr = function(...args){
+ console.error(...args);
+ logList.push('ERROR: '+args.join(' '));
+ };
+
+ const runTests = function(sqlite3){
+ console.log("Module inited.");
+ const wasm = sqlite3.capi.wasm;
+ const unlink = wasm.xWrap("sqlite3_wasm_vfs_unlink", "int", ["string"]);
+ const pDir = wasmfsDir(wasm);
+ if(pDir) log2('',"Persistent storage:",pDir);
+ else{
+ log2('error',"Expecting persistent storage in this build.");
+ return;
+ }
+ const scope = wasm.scopedAllocPush();
+ const dbFile = pDir+"/speedtest1.db";
+ const urlParams = new URL(self.location.href).searchParams;
+ const argv = ["speedtest1"];
+ if(urlParams.has('flags')){
+ argv.push(...(urlParams.get('flags').split(',')));
+ let i = argv.indexOf('--vfs');
+ if(i>=0) argv.splice(i,2);
+ }else{
+ argv.push(
+ "--singlethread",
+ "--nomutex",
+ "--nosync",
+ "--nomemstat"
+ );
+ //"--memdb", // note that memdb trumps the filename arg
+ }
+
+ if(argv.indexOf('--memdb')>=0){
+ log2('error',"WARNING: --memdb flag trumps db filename.");
+ }
+ argv.push("--big-transactions"/*important for tests 410 and 510!*/,
+ dbFile);
+ console.log("argv =",argv);
+ // These log messages are not emitted to the UI until after main() returns. Fixing that
+ // requires moving the main() call and related cleanup into a timeout handler.
+ if(pDir) unlink(dbFile);
+ log2('',"Starting native app:\n ",argv.join(' '));
+ log2('',"This will take a while and the browser might warn about the runaway JS.",
+ "Give it time...");
+ logList.length = 0;
+ setTimeout(function(){
+ wasm.xCall('wasm_main', argv.length,
+ wasm.scopedAllocMainArgv(argv));
+ wasm.scopedAllocPop(scope);
+ if(pDir) unlink(dbFile);
+ logList.unshift("Done running native main(). Output:");
+ dumpLogList();
+ }, 25);
+ }/*runTests()*/;
+
+ self.sqlite3TestModule.print = log;
+ self.sqlite3TestModule.printErr = logErr;
+ sqlite3InitModule(self.sqlite3TestModule).then(runTests);
+})();</script>
+ </body>
+</html>
diff --git a/ext/wasm/speedtest1-worker.html b/ext/wasm/speedtest1-worker.html
new file mode 100644
index 0000000..5638ce3
--- /dev/null
+++ b/ext/wasm/speedtest1-worker.html
@@ -0,0 +1,372 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>speedtest1.wasm Worker</title>
+ </head>
+ <body>
+ <header id='titlebar'>speedtest1.wasm Worker</header>
+ <div>See also: <a href='speedtest1.html'>A main-thread variant of this page.</a></div>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <fieldset id='ui-controls' class='hidden'>
+ <legend>Options</legend>
+ <div id='toolbar'>
+ <div id='toolbar-select'>
+ <select id='select-flags' size='10' multiple></select>
+ <div>The following flags can be passed as URL parameters:
+ vfs=NAME, size=N, journal=MODE, cachesize=SIZE
+ </div>
+ </div>
+ <div class='toolbar-inner-vertical'>
+ <div id='toolbar-selected-flags'></div>
+ <div class='toolbar-inner-vertical'>
+ <span>&rarr; <a id='link-main-thread' href='#' target='speedtest-main'
+ title='Start speedtest1.html with the selected flags'>speedtest1</a>
+ </span>
+ <span>&rarr; <a id='link-wasmfs' href='#' target='speedtest-wasmfs'
+ title='Start speedtest1-wasmfs.html with the selected flags'>speedtest1-wasmfs</a>
+ </span>
+ <span>&rarr; <a id='link-kvvfs' href='#' target='speedtest-kvvfs'
+ title='Start kvvfs speedtest1 with the selected flags'>speedtest1-kvvfs</a>
+ </span>
+ </div>
+ </div>
+ <div class='toolbar-inner-vertical' id='toolbar-runner-controls'>
+ <button id='btn-reset-flags'>Reset Flags</button>
+ <button id='btn-output-clear'>Clear output</button>
+ <button id='btn-run'>Run</button>
+ </div>
+ </div>
+ </fieldset>
+ <div>
+ <span class='input-wrapper'>
+ <input type='checkbox' class='disable-during-eval' id='cb-reverse-log-order' checked></input>
+ <label for='cb-reverse-log-order' id='lbl-reverse-log-order'>Reverse log order</label>
+ </span>
+ </div>
+ <div id='test-output'>
+ </div>
+ <div id='tips'>
+ <strong>Tips:</strong>
+ <ul>
+ <li>Control-click the flags to (de)select multiple flags.</li>
+ <li>The <tt>--big-transactions</tt> flag is important for two
+ of the bigger tests. Without it, those tests create a
+ combined total of 140k implicit transactions, reducing their
+ speed to an absolute crawl, especially when WASMFS is
+ activated.
+ </li>
+ <li>The easiest way to try different optimization levels is,
+ from this directory:
+ <pre>$ rm -f speedtest1.js; make -e emcc_opt='-O2' speedtest1.js</pre>
+ Then reload this page. -O2 seems to consistently produce the fastest results.
+ </li>
+ </ul>
+ </div>
+ <style>
+ #test-output {
+ white-space: break-spaces;
+ overflow: auto;
+ }
+ div#tips { margin-top: 1em; }
+ #toolbar {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+ #toolbar > * {
+ margin: 0 0.5em;
+ }
+ .toolbar-inner-vertical {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+ #toolbar-select {
+ display: flex;
+ flex-direction: column;
+ }
+ .toolbar-inner-vertical > *, #toolbar-select > * {
+ margin: 0.2em 0;
+ }
+ #select-flags > option {
+ white-space: pre;
+ font-family: monospace;
+ }
+ fieldset {
+ border-radius: 0.5em;
+ }
+ #toolbar-runner-controls { flex-grow: 1 }
+ #toolbar-runner-controls > * { flex: 1 0 auto }
+ #toolbar-selected-flags::before {
+ font-family: initial;
+ content:"Selected flags: ";
+ }
+ #toolbar-selected-flags {
+ display: flex;
+ flex-direction: column;
+ font-family: monospace;
+ justify-content: flex-start;
+ }
+ </style>
+ <script>(function(){
+ 'use strict';
+ const E = (sel)=>document.querySelector(sel);
+ const eOut = E('#test-output');
+ const log2 = function(cssClass,...args){
+ let ln;
+ if(1 || cssClass){
+ ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ }else{
+ // This doesn't work with the "reverse order" option!
+ ln = document.createTextNode(args.join(' ')+'\n');
+ }
+ eOut.append(ln);
+ };
+ const log = (...args)=>{
+ //console.log(...args);
+ log2('', ...args);
+ };
+ const logErr = function(...args){
+ console.error(...args);
+ log2('error', ...args);
+ };
+ const logWarn = function(...args){
+ console.warn(...args);
+ log2('warning', ...args);
+ };
+
+ const spacePad = function(str,len=21){
+ if(str.length===len) return str;
+ else if(str.length>len) return str.substr(0,len);
+ const a = []; a.length = len - str.length;
+ return str+a.join(' ');
+ };
+ // OPTION elements seem to ignore white-space:pre, so do this the hard way...
+ const nbspPad = function(str,len=21){
+ if(str.length===len) return str;
+ else if(str.length>len) return str.substr(0,len);
+ const a = []; a.length = len - str.length;
+ return str+a.join('&nbsp;');
+ };
+
+ const urlParams = new URL(self.location.href).searchParams;
+ const W = new Worker(
+ "speedtest1-worker.js?sqlite3.dir=jswasm"+
+ (urlParams.has('opfs-verbose') ? '&opfs-verbose' : '')
+ );
+ const mPost = function(msgType,payload){
+ W.postMessage({type: msgType, data: payload});
+ };
+
+ const eFlags = E('#select-flags');
+ const eSelectedFlags = E('#toolbar-selected-flags');
+ const eLinkMainThread = E('#link-main-thread');
+ const eLinkWasmfs = E('#link-wasmfs');
+ const eLinkKvvfs = E('#link-kvvfs');
+ const getSelectedFlags = ()=>{
+ const f = Array.prototype.map.call(eFlags.selectedOptions, (v)=>v.value);
+ [
+ 'size', 'vfs', 'journal', 'cachesize'
+ ].forEach(function(k){
+ if(urlParams.has(k)) f.push('--'+k, urlParams.get(k));
+ });
+ return f;
+ };
+ const updateSelectedFlags = function(){
+ eSelectedFlags.innerText = '';
+ const flags = getSelectedFlags();
+ flags.forEach(function(f){
+ const e = document.createElement('span');
+ e.innerText = f;
+ eSelectedFlags.appendChild(e);
+ });
+ const rxStripDash = /^(-+)?/;
+ const comma = flags.join(',');
+ eLinkMainThread.setAttribute('target', 'speedtest1-main-'+comma);
+ eLinkMainThread.href = 'speedtest1.html?flags='+comma;
+ eLinkWasmfs.setAttribute('target', 'speedtest1-wasmfs-'+comma);
+ eLinkWasmfs.href = 'speedtest1-wasmfs.html?flags='+comma;
+ eLinkKvvfs.setAttribute('target', 'speedtest1-kvvfs-'+comma);
+ eLinkKvvfs.href = 'speedtest1.html?vfs=kvvfs&flags='+comma;
+ };
+ eFlags.addEventListener('change', updateSelectedFlags );
+ {
+ const flags = Object.create(null);
+ /* TODO? Flags which require values need custom UI
+ controls and some of them make little sense here
+ (e.g. --script FILE). */
+ flags["--autovacuum"] = "Enable AUTOVACUUM mode";
+ flags["--big-transactions"] = "Important for tests 410 and 510!";
+ //flags["--cachesize"] = "N Set the cache size to N pages";
+ flags["--checkpoint"] = "Run PRAGMA wal_checkpoint after each test case";
+ flags["--exclusive"] = "Enable locking_mode=EXCLUSIVE";
+ flags["--explain"] = "Like --sqlonly but with added EXPLAIN keywords";
+ //flags["--heap"] = "SZ MIN Memory allocator uses SZ bytes & min allocation MIN";
+ flags["--incrvacuum"] = "Enable incremenatal vacuum mode";
+ //flags["--journal"] = "M Set the journal_mode to M";
+ //flags["--key"] = "KEY Set the encryption key to KEY";
+ //flags["--lookaside"] = "N SZ Configure lookaside for N slots of SZ bytes each";
+ flags["--memdb"] = "Use an in-memory database";
+ //flags["--mmap"] = "SZ MMAP the first SZ bytes of the database file";
+ flags["--multithread"] = "Set multithreaded mode";
+ flags["--nomemstat"] = "Disable memory statistics";
+ flags["--nomutex"] = "Open db with SQLITE_OPEN_NOMUTEX";
+ flags["--nosync"] = "Set PRAGMA synchronous=OFF";
+ flags["--notnull"] = "Add NOT NULL constraints to table columns";
+ //flags["--output"] = "FILE Store SQL output in FILE";
+ //flags["--pagesize"] = "N Set the page size to N";
+ //flags["--pcache"] = "N SZ Configure N pages of pagecache each of size SZ bytes";
+ //flags["--primarykey"] = "Use PRIMARY KEY instead of UNIQUE where appropriate";
+ //flags["--repeat"] = "N Repeat each SELECT N times (default: 1)";
+ flags["--reprepare"] = "Reprepare each statement upon every invocation";
+ //flags["--reserve"] = "N Reserve N bytes on each database page";
+ //flags["--script"] = "FILE Write an SQL script for the test into FILE";
+ flags["--serialized"] = "Set serialized threading mode";
+ flags["--singlethread"] = "Set single-threaded mode - disables all mutexing";
+ flags["--sqlonly"] = "No-op. Only show the SQL that would have been run.";
+ flags["--shrink-memory"] = "Invoke sqlite3_db_release_memory() frequently.";
+ //flags["--size"] = "N Relative test size. Default=100";
+ flags["--strict"] = "Use STRICT table where appropriate";
+ flags["--stats"] = "Show statistics at the end";
+ //flags["--temp"] = "N N from 0 to 9. 0: no temp table. 9: all temp tables";
+ //flags["--testset"] = "T Run test-set T (main, cte, rtree, orm, fp, debug)";
+ flags["--trace"] = "Turn on SQL tracing";
+ //flags["--threads"] = "N Use up to N threads for sorting";
+ /*
+ The core API's WASM build does not support UTF16, but in
+ this app it's not an issue because the data are not crossing
+ JS/WASM boundaries.
+ */
+ flags["--utf16be"] = "Set text encoding to UTF-16BE";
+ flags["--utf16le"] = "Set text encoding to UTF-16LE";
+ flags["--verify"] = "Run additional verification steps.";
+ flags["--without"] = "rowid Use WITHOUT ROWID where appropriate";
+ const preselectedFlags = [
+ '--big-transactions',
+ '--singlethread'
+ ];
+ if(urlParams.has('flags')){
+ preselectedFlags.push(...urlParams.get('flags').split(','));
+ }
+ if('opfs'!==urlParams.get('vfs')){
+ preselectedFlags.push('--memdb');
+ }
+ Object.keys(flags).sort().forEach(function(f){
+ const opt = document.createElement('option');
+ eFlags.appendChild(opt);
+ const lbl = nbspPad(f)+flags[f];
+ //opt.innerText = lbl;
+ opt.innerHTML = lbl;
+ opt.value = f;
+ if(preselectedFlags.indexOf(f) >= 0) opt.selected = true;
+ });
+ const cbReverseLog = E('#cb-reverse-log-order');
+ const lblReverseLog = E('#lbl-reverse-log-order');
+ if(cbReverseLog.checked){
+ lblReverseLog.classList.add('warning');
+ eOut.classList.add('reverse');
+ }
+ cbReverseLog.addEventListener('change', function(){
+ if(this.checked){
+ eOut.classList.add('reverse');
+ lblReverseLog.classList.add('warning');
+ }else{
+ eOut.classList.remove('reverse');
+ lblReverseLog.classList.remove('warning');
+ }
+ }, false);
+ updateSelectedFlags();
+ }
+ E('#btn-output-clear').addEventListener('click', ()=>{
+ eOut.innerText = '';
+ });
+ E('#btn-reset-flags').addEventListener('click',()=>{
+ eFlags.value = '';
+ updateSelectedFlags();
+ });
+ E('#btn-run').addEventListener('click',function(){
+ log("Running speedtest1. UI controls will be disabled until it completes.");
+ mPost('run', getSelectedFlags());
+ });
+
+ const eControls = E('#ui-controls');
+ /** Update Emscripten-related UI elements while loading the module. */
+ const updateLoadStatus = function f(text){
+ if(!f.last){
+ f.last = { text: '', step: 0 };
+ const E = (cssSelector)=>document.querySelector(cssSelector);
+ f.ui = {
+ status: E('#module-status'),
+ progress: E('#module-progress'),
+ spinner: E('#module-spinner')
+ };
+ }
+ if(text === f.last.text) return;
+ f.last.text = text;
+ if(f.ui.progress){
+ f.ui.progress.value = f.last.step;
+ f.ui.progress.max = f.last.step + 1;
+ }
+ ++f.last.step;
+ if(text) {
+ f.ui.status.classList.remove('hidden');
+ f.ui.status.innerText = text;
+ }else{
+ if(f.ui.progress){
+ f.ui.progress.remove();
+ f.ui.spinner.remove();
+ delete f.ui.progress;
+ delete f.ui.spinner;
+ }
+ f.ui.status.classList.add('hidden');
+ }
+ };
+
+ W.onmessage = function(msg){
+ msg = msg.data;
+ switch(msg.type){
+ case 'ready':
+ log("Worker is ready.");
+ eControls.classList.remove('hidden');
+ break;
+ case 'stdout': log(msg.data); break;
+ case 'stdout': logErr(msg.data); break;
+ case 'run-start':
+ eControls.disabled = true;
+ log("Running speedtest1 with argv =",msg.data.join(' '));
+ break;
+ case 'run-end':
+ log("speedtest1 finished.");
+ eControls.disabled = false;
+ // app output is in msg.data
+ break;
+ case 'error': logErr(msg.data); break;
+ case 'load-status': updateLoadStatus(msg.data); break;
+ default:
+ logErr("Unhandled worker message type:",msg);
+ break;
+ }
+ };
+})();</script>
+ </body>
+</html>
diff --git a/ext/wasm/speedtest1-worker.js b/ext/wasm/speedtest1-worker.js
new file mode 100644
index 0000000..c61cab9
--- /dev/null
+++ b/ext/wasm/speedtest1-worker.js
@@ -0,0 +1,99 @@
+'use strict';
+(function(){
+ let speedtestJs = 'speedtest1.js';
+ const urlParams = new URL(self.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ speedtestJs = urlParams.get('sqlite3.dir') + '/' + speedtestJs;
+ }
+ importScripts('common/whwasmutil.js', speedtestJs);
+ /**
+ If this environment contains OPFS, this function initializes it and
+ returns the name of the dir on which OPFS is mounted, else it returns
+ an empty string.
+ */
+ const wasmfsDir = function f(wasmUtil){
+ if(undefined !== f._) return f._;
+ const pdir = '/opfs';
+ if( !self.FileSystemHandle
+ || !self.FileSystemDirectoryHandle
+ || !self.FileSystemFileHandle){
+ return f._ = "";
+ }
+ try{
+ if(0===wasmUtil.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], pdir
+ )){
+ return f._ = pdir;
+ }else{
+ return f._ = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return f._ = "";
+ }
+ };
+ wasmfsDir._ = undefined;
+
+ const mPost = function(msgType,payload){
+ postMessage({type: msgType, data: payload});
+ };
+
+ const App = Object.create(null);
+ App.logBuffer = [];
+ const logMsg = (type,msgArgs)=>{
+ const msg = msgArgs.join(' ');
+ App.logBuffer.push(msg);
+ mPost(type,msg);
+ };
+ const log = (...args)=>logMsg('stdout',args);
+ const logErr = (...args)=>logMsg('stderr',args);
+
+ const runSpeedtest = function(cliFlagsArray){
+ const scope = App.wasm.scopedAllocPush();
+ const dbFile = App.pDir+"/speedtest1.sqlite3";
+ try{
+ const argv = [
+ "speedtest1.wasm", ...cliFlagsArray, dbFile
+ ];
+ App.logBuffer.length = 0;
+ mPost('run-start', [...argv]);
+ App.wasm.xCall('wasm_main', argv.length,
+ App.wasm.scopedAllocMainArgv(argv));
+ }catch(e){
+ mPost('error',e.message);
+ }finally{
+ App.wasm.scopedAllocPop(scope);
+ mPost('run-end', App.logBuffer.join('\n'));
+ App.logBuffer.length = 0;
+ }
+ };
+
+ self.onmessage = function(msg){
+ msg = msg.data;
+ switch(msg.type){
+ case 'run': runSpeedtest(msg.data || []); break;
+ default:
+ logErr("Unhandled worker message type:",msg.type);
+ break;
+ }
+ };
+
+ const EmscriptenModule = {
+ print: log,
+ printErr: logErr,
+ setStatus: (text)=>mPost('load-status',text)
+ };
+ self.sqlite3InitModule(EmscriptenModule).then((sqlite3)=>{
+ const S = sqlite3;
+ App.vfsUnlink = function(pDb, fname){
+ const pVfs = S.wasm.sqlite3_wasm_db_vfs(pDb, 0);
+ if(pVfs) S.wasm.sqlite3_wasm_vfs_unlink(pVfs, fname||0);
+ };
+ App.pDir = wasmfsDir(S.wasm);
+ App.wasm = S.wasm;
+ //if(App.pDir) log("Persistent storage:",pDir);
+ //else log("Using transient storage.");
+ mPost('ready',true);
+ log("Registered VFSes:", ...S.capi.sqlite3_js_vfs_list());
+ });
+})();
diff --git a/ext/wasm/speedtest1.html b/ext/wasm/speedtest1.html
new file mode 100644
index 0000000..5286b9e
--- /dev/null
+++ b/ext/wasm/speedtest1.html
@@ -0,0 +1,174 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>speedtest1.wasm</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>speedtest1.wasm</span></header>
+ <div>See also: <a href='speedtest1-worker.html'>A Worker-thread variant of this page.</a></div>
+ <!-- emscripten bits -->
+ <figure id="module-spinner">
+ <div class="spinner"></div>
+ <div class='center'><strong>Initializing app...</strong></div>
+ <div class='center'>
+ On a slow internet connection this may take a moment. If this
+ message displays for "a long time", intialization may have
+ failed and the JavaScript console may contain clues as to why.
+ </div>
+ </figure>
+ <div class="emscripten" id="module-status">Downloading...</div>
+ <div class="emscripten">
+ <progress value="0" max="100" id="module-progress" hidden='1'></progress>
+ </div><!-- /emscripten bits -->
+ <div class='warning'>This page starts running the main exe when it loads, which will
+ block the UI until it finishes! Adding UI controls to manually configure and start it
+ are TODO.</div>
+ </div>
+ <div class='warning'>Achtung: running it with the dev tools open may
+ <em>drastically</em> slow it down. For faster results, keep the dev
+ tools closed when running it!
+ </div>
+ <div>Output is delayed/buffered because we cannot update the UI while the
+ speedtest is running. Output will appear below when ready...
+ <div id='test-output'></div>
+ <script src="common/SqliteTestUtil.js"></script>
+ <script src="jswasm/speedtest1.js"></script>
+ <script>(function(){
+ /**
+ If this environment contains OPFS, this function initializes it and
+ returns the name of the dir on which OPFS is mounted, else it returns
+ an empty string.
+ */
+ const wasmfsDir = function f(wasmUtil){
+ if(undefined !== f._) return f._;
+ const pdir = '/persistent';
+ if( !self.FileSystemHandle
+ || !self.FileSystemDirectoryHandle
+ || !self.FileSystemFileHandle){
+ return f._ = "";
+ }
+ try{
+ if(0===wasmUtil.xCallWrapped(
+ 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], pdir
+ )){
+ return f._ = pdir;
+ }else{
+ return f._ = "";
+ }
+ }catch(e){
+ // sqlite3_wasm_init_wasmfs() is not available
+ return f._ = "";
+ }
+ };
+ wasmfsDir._ = undefined;
+
+ const eOut = document.querySelector('#test-output');
+ const log2 = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass) ln.classList.add(cssClass);
+ ln.append(document.createTextNode(args.join(' ')));
+ eOut.append(ln);
+ //this.e.output.lastElementChild.scrollIntoViewIfNeeded();
+ };
+ const logList = [];
+ const dumpLogList = function(){
+ logList.forEach((v)=>log2('',v));
+ logList.length = 0;
+ };
+ /* can't update DOM while speedtest is running unless we run
+ speedtest in a worker thread. */;
+ const log = (...args)=>{
+ console.log(...args);
+ logList.push(args.join(' '));
+ };
+ const logErr = function(...args){
+ console.error(...args);
+ logList.push('ERROR: '+args.join(' '));
+ };
+
+ const runTests = function(sqlite3){
+ const capi = sqlite3.capi, wasm = sqlite3.wasm;
+ //console.debug('sqlite3 =',sqlite3);
+ const pDir = wasmfsDir(wasm);
+ if(pDir){
+ console.warn("Persistent storage:",pDir);
+ }
+ const scope = wasm.scopedAllocPush();
+ let dbFile = pDir+"/speedtest1.db";
+ const urlParams = new URL(self.location.href).searchParams;
+ const argv = ["speedtest1"];
+ if(urlParams.has('flags')){
+ argv.push(...(urlParams.get('flags').split(',')));
+ }
+
+ let forceSize = 0;
+ let vfs, pVfs = 0;
+ if(urlParams.has('vfs')){
+ vfs = urlParams.get('vfs');
+ pVfs = capi.sqlite3_vfs_find(vfs);
+ if(!pVfs){
+ log2('error',"Unknown VFS:",vfs);
+ return;
+ }
+ argv.push("--vfs", vfs);
+ log2('',"Using VFS:",vfs);
+ if('kvvfs' === vfs){
+ forceSize = 4 /* 5 uses approx. 4.96mb */;
+ dbFile = 'session';
+ log2('warning',"kvvfs VFS: forcing --size",forceSize,
+ "and filename '"+dbFile+"'.");
+ capi.sqlite3_js_kvvfs_clear(dbFile);
+ }
+ }
+ if(forceSize){
+ argv.push('--size',forceSize);
+ }else{
+ [
+ 'size'
+ ].forEach(function(k){
+ const v = urlParams.get(k);
+ if(v) argv.push('--'+k, urlParams[k]);
+ });
+ }
+ argv.push(
+ "--singlethread",
+ //"--nomutex",
+ //"--nosync",
+ //"--memdb", // note that memdb trumps the filename arg
+ "--nomemstat"
+ );
+ argv.push("--big-transactions"/*important for tests 410 and 510!*/,
+ dbFile);
+ console.log("argv =",argv);
+ // These log messages are not emitted to the UI until after main() returns. Fixing that
+ // requires moving the main() call and related cleanup into a timeout handler.
+ if(pDir) wasm.sqlite3_wasm_vfs_unlink(pVfs,dbFile);
+ log2('',"Starting native app:\n ",argv.join(' '));
+ log2('',"This will take a while and the browser might warn about the runaway JS.",
+ "Give it time...");
+ logList.length = 0;
+ setTimeout(function(){
+ wasm.xCall('wasm_main', argv.length,
+ wasm.scopedAllocMainArgv(argv));
+ wasm.scopedAllocPop(scope);
+ if('kvvfs'===vfs){
+ logList.unshift("KVVFS "+dbFile+" size = "+
+ capi.sqlite3_js_kvvfs_size(dbFile));
+ }
+ if(pDir) wasm.sqlite3_wasm_vfs_unlink(pVfs,dbFile);
+ logList.unshift("Done running native main(). Output:");
+ dumpLogList();
+ }, 50);
+ }/*runTests()*/;
+
+ self.sqlite3TestModule.print = log;
+ self.sqlite3TestModule.printErr = logErr;
+ sqlite3InitModule(self.sqlite3TestModule).then(runTests);
+})();</script>
+</body>
+</html>
diff --git a/ext/wasm/split-speedtest1-script.sh b/ext/wasm/split-speedtest1-script.sh
new file mode 100755
index 0000000..e072d08
--- /dev/null
+++ b/ext/wasm/split-speedtest1-script.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Expects $1 to be a (speedtest1 --script) output file. Output is a
+# series of SQL files extracted from that file.
+infile=${1:?arg = speedtest1 --script output file}
+testnums=$(grep -e '^-- begin test' "$infile" | cut -d' ' -f4)
+if [ x = "x${testnums}" ]; then
+ echo "Could not parse any begin/end blocks out of $infile" 1>&2
+ exit 1
+fi
+odir=${infile%%/*}
+if [ "$odir" = "$infile" ]; then odir="."; fi
+#echo testnums=$testnums
+for n in $testnums; do
+ ofile=$odir/$(printf "speedtest1-%03d.sql" $n)
+ sed -n -e "/^-- begin test $n /,/^-- end test $n\$/p" $infile > $ofile
+ echo -e "$n\t$ofile"
+done
diff --git a/ext/wasm/sql/000-mandelbrot.sql b/ext/wasm/sql/000-mandelbrot.sql
new file mode 100644
index 0000000..3aa5f57
--- /dev/null
+++ b/ext/wasm/sql/000-mandelbrot.sql
@@ -0,0 +1,17 @@
+WITH RECURSIVE
+ xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),
+ yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0),
+ m(iter, cx, cy, x, y) AS (
+ SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis
+ UNION ALL
+ SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m
+ WHERE (x*x + y*y) < 4.0 AND iter<28
+ ),
+ m2(iter, cx, cy) AS (
+ SELECT max(iter), cx, cy FROM m GROUP BY cx, cy
+ ),
+ a(t) AS (
+ SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '')
+ FROM m2 GROUP BY cy
+ )
+SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;
diff --git a/ext/wasm/sql/001-sudoku.sql b/ext/wasm/sql/001-sudoku.sql
new file mode 100644
index 0000000..53661b1
--- /dev/null
+++ b/ext/wasm/sql/001-sudoku.sql
@@ -0,0 +1,28 @@
+WITH RECURSIVE
+ input(sud) AS (
+ VALUES('53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79')
+ ),
+ digits(z, lp) AS (
+ VALUES('1', 1)
+ UNION ALL SELECT
+ CAST(lp+1 AS TEXT), lp+1 FROM digits WHERE lp<9
+ ),
+ x(s, ind) AS (
+ SELECT sud, instr(sud, '.') FROM input
+ UNION ALL
+ SELECT
+ substr(s, 1, ind-1) || z || substr(s, ind+1),
+ instr( substr(s, 1, ind-1) || z || substr(s, ind+1), '.' )
+ FROM x, digits AS z
+ WHERE ind>0
+ AND NOT EXISTS (
+ SELECT 1
+ FROM digits AS lp
+ WHERE z.z = substr(s, ((ind-1)/9)*9 + lp, 1)
+ OR z.z = substr(s, ((ind-1)%9) + (lp-1)*9 + 1, 1)
+ OR z.z = substr(s, (((ind-1)/3) % 3) * 3
+ + ((ind-1)/27) * 27 + lp
+ + ((lp-1) / 3) * 6, 1)
+ )
+ )
+SELECT s FROM x WHERE ind=0;
diff --git a/ext/wasm/test-opfs-vfs.html b/ext/wasm/test-opfs-vfs.html
new file mode 100644
index 0000000..235ef51
--- /dev/null
+++ b/ext/wasm/test-opfs-vfs.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>Async-behind-Sync experiment</title>
+ </head>
+ <body>
+ <header id='titlebar'><span>Async-behind-Sync sqlite3_vfs</span></header>
+ <div>This performs a sanity test of the "opfs" sqlite3_vfs.
+ <strong>See the dev console for all output.</strong>
+ </div>
+ <div>
+ <a href='?delete'>Use this link</a> to delete the persistent OPFS-side db (if any).
+ </div>
+ <div id='test-output'></div>
+ <script>
+ new Worker(
+ "test-opfs-vfs.js?sqlite3.dir=jswasm&"+self.location.search.substr(1)
+ );
+ </script>
+ </body>
+</html>
diff --git a/ext/wasm/test-opfs-vfs.js b/ext/wasm/test-opfs-vfs.js
new file mode 100644
index 0000000..bba31bb
--- /dev/null
+++ b/ext/wasm/test-opfs-vfs.js
@@ -0,0 +1,85 @@
+/*
+ 2022-09-17
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ A testing ground for the OPFS VFS.
+*/
+'use strict';
+const tryOpfsVfs = async function(sqlite3){
+ const toss = function(...args){throw new Error(args.join(' '))};
+ const logPrefix = "OPFS tester:";
+ const log = (...args)=>console.log(logPrefix,...args);
+ const warn = (...args)=>console.warn(logPrefix,...args);
+ const error = (...args)=>console.error(logPrefix,...args);
+ const opfs = sqlite3.opfs;
+ log("tryOpfsVfs()");
+ if(!sqlite3.opfs){
+ const e = toss("OPFS is not available.");
+ error(e);
+ throw e;
+ }
+ const capi = sqlite3.capi;
+ const pVfs = capi.sqlite3_vfs_find("opfs") || toss("Missing 'opfs' VFS.");
+ const oVfs = capi.sqlite3_vfs.instanceForPointer(pVfs) || toss("Unexpected instanceForPointer() result.");;
+ log("OPFS VFS:",pVfs, oVfs);
+
+ const wait = async (ms)=>{
+ return new Promise((resolve)=>setTimeout(resolve, ms));
+ };
+
+ const urlArgs = new URL(self.location.href).searchParams;
+ const dbFile = "my-persistent.db";
+ if(urlArgs.has('delete')) sqlite3.opfs.unlink(dbFile);
+
+ const db = new opfs.OpfsDb(dbFile,'ct');
+ log("db file:",db.filename);
+ try{
+ if(opfs.entryExists(dbFile)){
+ let n = db.selectValue("select count(*) from sqlite_schema");
+ log("Persistent data found. sqlite_schema entry count =",n);
+ }
+ db.transaction((db)=>{
+ db.exec({
+ sql:[
+ "create table if not exists t(a);",
+ "insert into t(a) values(?),(?),(?);",
+ ],
+ bind: [performance.now() | 0,
+ (performance.now() |0) / 2,
+ (performance.now() |0) / 4]
+ });
+ });
+ log("count(*) from t =",db.selectValue("select count(*) from t"));
+
+ // Some sanity checks of the opfs utility functions...
+ const testDir = '/sqlite3-opfs-'+opfs.randomFilename(12);
+ const aDir = testDir+'/test/dir';
+ await opfs.mkdir(aDir) || toss("mkdir failed");
+ await opfs.mkdir(aDir) || toss("mkdir must pass if the dir exists");
+ await opfs.unlink(testDir+'/test') && toss("delete 1 should have failed (dir not empty)");
+ //await opfs.entryExists(testDir)
+ await opfs.unlink(testDir+'/test/dir') || toss("delete 2 failed");
+ await opfs.unlink(testDir+'/test/dir') && toss("delete 2b should have failed (dir already deleted)");
+ await opfs.unlink(testDir, true) || toss("delete 3 failed");
+ await opfs.entryExists(testDir) && toss("entryExists(",testDir,") should have failed");
+ }finally{
+ db.close();
+ }
+
+ log("Done!");
+}/*tryOpfsVfs()*/;
+
+importScripts('jswasm/sqlite3.js');
+self.sqlite3InitModule()
+ .then((sqlite3)=>tryOpfsVfs(sqlite3))
+ .catch((e)=>{
+ console.error("Error initializing module:",e);
+ });
diff --git a/ext/wasm/tester1-worker.html b/ext/wasm/tester1-worker.html
new file mode 100644
index 0000000..4d2df0c
--- /dev/null
+++ b/ext/wasm/tester1-worker.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="../common/emscripten.css"/>
+ <link rel="stylesheet" href="../common/testing.css"/>
+ <title>sqlite3 tester #1 (Worker thread)</title>
+ <style>
+ body {
+ font-family: monospace;
+ }
+ </style>
+ </head>
+ <body>
+ <h1 id='color-target'>sqlite3 WASM/JS tester #1 (Worker thread)</h1>
+ <div>See <a href='tester1.html' target='tester1.html'>tester1.html</a>
+ for the UI-thread variant.</div>
+ <div class='input-wrapper'>
+ <input type='checkbox' id='cb-log-reverse'>
+ <label for='cb-log-reverse'>Reverse log order?</label>
+ </div>
+ <div id='test-output'></div>
+ <script>(function(){
+ const logTarget = document.querySelector('#test-output');
+ const logHtml = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass){
+ for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){
+ ln.classList.add(c);
+ }
+ }
+ ln.append(document.createTextNode(args.join(' ')));
+ logTarget.append(ln);
+ };
+ const cbReverse = document.querySelector('#cb-log-reverse');
+ const cbReverseIt = ()=>{
+ logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
+ };
+ cbReverse.addEventListener('change',cbReverseIt,true);
+ cbReverseIt();
+ const w = new Worker("tester1.js?sqlite3.dir=jswasm");
+ w.onmessage = function({data}){
+ switch(data.type){
+ case 'log':
+ logHtml(data.payload.cssClass, ...data.payload.args);
+ break;
+ case 'error':
+ logHtml('error', ...data.payload.args);
+ break;
+ case 'test-result':
+ document.querySelector('#color-target').classList.add(
+ data.payload.pass ? 'tests-pass' : 'tests-fail'
+ );
+ break;
+ default:
+ logHtml('error',"Unhandled message:",data.type);
+ };
+ };
+ })();</script>
+ </body>
+</html>
diff --git a/ext/wasm/tester1.html b/ext/wasm/tester1.html
new file mode 100644
index 0000000..f7a2fba
--- /dev/null
+++ b/ext/wasm/tester1.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
+ <link rel="stylesheet" href="common/emscripten.css"/>
+ <link rel="stylesheet" href="common/testing.css"/>
+ <title>sqlite3 tester #1 (UI thread)</title>
+ <style>
+ body {
+ font-family: monospace;
+ }
+ </style>
+ </head>
+ <body>
+ <h1 id='color-target'>sqlite3 WASM/JS tester #1 (UI thread)</h1>
+ <div>See <a href='tester1-worker.html' target='tester1-worker.html'>tester1-worker.html</a>
+ for the Worker-thread variant.</div>
+ <div class='input-wrapper'>
+ <input type='checkbox' id='cb-log-reverse'>
+ <label for='cb-log-reverse'>Reverse log order?</label>
+ </div>
+ <div id='test-output'></div>
+ <script src="jswasm/sqlite3.js"></script>
+ <script src="tester1.js"></script>
+ </body>
+</html>
diff --git a/ext/wasm/tester1.js b/ext/wasm/tester1.js
new file mode 100644
index 0000000..99fb5b3
--- /dev/null
+++ b/ext/wasm/tester1.js
@@ -0,0 +1,1864 @@
+/*
+ 2022-10-12
+
+ The author disclaims copyright to this source code. In place of a
+ legal notice, here is a blessing:
+
+ * May you do good and not evil.
+ * May you find forgiveness for yourself and forgive others.
+ * May you share freely, never taking more than you give.
+
+ ***********************************************************************
+
+ Main functional and regression tests for the sqlite3 WASM API.
+
+ This mini-framework works like so:
+
+ This script adds a series of test groups, each of which contains an
+ arbitrary number of tests, into a queue. After loading of the
+ sqlite3 WASM/JS module is complete, that queue is processed. If any
+ given test fails, the whole thing fails. This script is built such
+ that it can run from the main UI thread or worker thread. Test
+ groups and individual tests can be assigned a predicate function
+ which determines whether to run them or not, and this is
+ specifically intended to be used to toggle certain tests on or off
+ for the main/worker threads.
+
+ Each test group defines a state object which gets applied as each
+ test function's `this`. Test functions can use that to, e.g., set up
+ a db in an early test and close it in a later test. Each test gets
+ passed the sqlite3 namespace object as its only argument.
+*/
+'use strict';
+(function(){
+ /**
+ Set up our output channel differently depending
+ on whether we are running in a worker thread or
+ the main (UI) thread.
+ */
+ let logClass;
+ /* Predicate for tests/groups. */
+ const isUIThread = ()=>(self.window===self && self.document);
+ /* Predicate for tests/groups. */
+ const isWorker = ()=>!isUIThread();
+ /* Predicate for tests/groups. */
+ const testIsTodo = ()=>false;
+ const haveWasmCTests = ()=>{
+ return !!wasm.exports.sqlite3_wasm_test_intptr;
+ };
+ {
+ const mapToString = (v)=>{
+ switch(typeof v){
+ case 'number': case 'string': case 'boolean':
+ case 'undefined': case 'bigint':
+ return ''+v;
+ default: break;
+ }
+ if(null===v) return 'null';
+ if(v instanceof Error){
+ v = {
+ message: v.message,
+ stack: v.stack,
+ errorClass: v.name
+ };
+ }
+ return JSON.stringify(v,undefined,2);
+ };
+ const normalizeArgs = (args)=>args.map(mapToString);
+ if( isUIThread() ){
+ console.log("Running in the UI thread.");
+ const logTarget = document.querySelector('#test-output');
+ logClass = function(cssClass,...args){
+ const ln = document.createElement('div');
+ if(cssClass){
+ for(const c of (Array.isArray(cssClass) ? cssClass : [cssClass])){
+ ln.classList.add(c);
+ }
+ }
+ ln.append(document.createTextNode(normalizeArgs(args).join(' ')));
+ logTarget.append(ln);
+ };
+ const cbReverse = document.querySelector('#cb-log-reverse');
+ const cbReverseKey = 'tester1:cb-log-reverse';
+ const cbReverseIt = ()=>{
+ logTarget.classList[cbReverse.checked ? 'add' : 'remove']('reverse');
+ //localStorage.setItem(cbReverseKey, cbReverse.checked ? 1 : 0);
+ };
+ cbReverse.addEventListener('change', cbReverseIt, true);
+ /*if(localStorage.getItem(cbReverseKey)){
+ cbReverse.checked = !!(+localStorage.getItem(cbReverseKey));
+ }*/
+ cbReverseIt();
+ }else{ /* Worker thread */
+ console.log("Running in a Worker thread.");
+ logClass = function(cssClass,...args){
+ postMessage({
+ type:'log',
+ payload:{cssClass, args: normalizeArgs(args)}
+ });
+ };
+ }
+ }
+ const reportFinalTestStatus = function(pass){
+ if(isUIThread()){
+ const e = document.querySelector('#color-target');
+ e.classList.add(pass ? 'tests-pass' : 'tests-fail');
+ }else{
+ postMessage({type:'test-result', payload:{pass}});
+ }
+ };
+ const log = (...args)=>{
+ //console.log(...args);
+ logClass('',...args);
+ }
+ const warn = (...args)=>{
+ console.warn(...args);
+ logClass('warning',...args);
+ }
+ const error = (...args)=>{
+ console.error(...args);
+ logClass('error',...args);
+ };
+
+ const toss = (...args)=>{
+ error(...args);
+ throw new Error(args.join(' '));
+ };
+ const tossQuietly = (...args)=>{
+ throw new Error(args.join(' '));
+ };
+
+ const roundMs = (ms)=>Math.round(ms*100)/100;
+
+ /**
+ Helpers for writing sqlite3-specific tests.
+ */
+ const TestUtil = {
+ /** Running total of the number of tests run via
+ this API. */
+ counter: 0,
+ /* Separator line for log messages. */
+ separator: '------------------------------------------------------------',
+ /**
+ If expr is a function, it is called and its result
+ is returned, coerced to a bool, else expr, coerced to
+ a bool, is returned.
+ */
+ toBool: function(expr){
+ return (expr instanceof Function) ? !!expr() : !!expr;
+ },
+ /** Throws if expr is false. If expr is a function, it is called
+ and its result is evaluated. If passed multiple arguments,
+ those after the first are a message string which get applied
+ as an exception message if the assertion fails. The message
+ arguments are concatenated together with a space between each.
+ */
+ assert: function f(expr, ...msg){
+ ++this.counter;
+ if(!this.toBool(expr)){
+ throw new Error(msg.length ? msg.join(' ') : "Assertion failed.");
+ }
+ return this;
+ },
+ /** Calls f() and squelches any exception it throws. If it
+ does not throw, this function throws. */
+ mustThrow: function(f, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ return this;
+ },
+ /**
+ Works like mustThrow() but expects filter to be a regex,
+ function, or string to match/filter the resulting exception
+ against. If f() does not throw, this test fails and an Error is
+ thrown. If filter is a regex, the test passes if
+ filter.test(error.message) passes. If it's a function, the test
+ passes if filter(error) returns truthy. If it's a string, the
+ test passes if the filter matches the exception message
+ precisely. In all other cases the test fails, throwing an
+ Error.
+
+ If it throws, msg is used as the error report unless it's falsy,
+ in which case a default is used.
+ */
+ mustThrowMatching: function(f, filter, msg){
+ ++this.counter;
+ let err;
+ try{ f(); } catch(e){err=e;}
+ if(!err) throw new Error(msg || "Expected exception.");
+ let pass = false;
+ if(filter instanceof RegExp) pass = filter.test(err.message);
+ else if(filter instanceof Function) pass = filter(err);
+ else if('string' === typeof filter) pass = (err.message === filter);
+ if(!pass){
+ throw new Error(msg || ("Filter rejected this exception: "+err.message));
+ }
+ return this;
+ },
+ /** Throws if expr is truthy or expr is a function and expr()
+ returns truthy. */
+ throwIf: function(expr, msg){
+ ++this.counter;
+ if(this.toBool(expr)) throw new Error(msg || "throwIf() failed");
+ return this;
+ },
+ /** Throws if expr is falsy or expr is a function and expr()
+ returns falsy. */
+ throwUnless: function(expr, msg){
+ ++this.counter;
+ if(!this.toBool(expr)) throw new Error(msg || "throwUnless() failed");
+ return this;
+ },
+ eqApprox: (v1,v2,factor=0.05)=>(v1>=(v2-factor) && v1<=(v2+factor)),
+ TestGroup: (function(){
+ let groupCounter = 0;
+ const TestGroup = function(name, predicate){
+ this.number = ++groupCounter;
+ this.name = name;
+ this.predicate = predicate;
+ this.tests = [];
+ };
+ TestGroup.prototype = {
+ addTest: function(testObj){
+ this.tests.push(testObj);
+ return this;
+ },
+ run: async function(sqlite3){
+ log(TestUtil.separator);
+ logClass('group-start',"Group #"+this.number+':',this.name);
+ const indent = ' ';
+ if(this.predicate && !this.predicate(sqlite3)){
+ logClass('warning',indent,
+ "SKIPPING group because predicate says to.");
+ return;
+ }
+ const assertCount = TestUtil.counter;
+ const groupState = Object.create(null);
+ const skipped = [];
+ let runtime = 0, i = 0;
+ for(const t of this.tests){
+ ++i;
+ const n = this.number+"."+i;
+ log(indent, n+":", t.name);
+ if(t.predicate && !t.predicate(sqlite3)){
+ logClass('warning', indent, indent,
+ 'SKIPPING because predicate says to');
+ skipped.push( n+': '+t.name );
+ }else{
+ const tc = TestUtil.counter, now = performance.now();
+ await t.test.call(groupState, sqlite3);
+ const then = performance.now();
+ runtime += then - now;
+ logClass('faded',indent, indent,
+ TestUtil.counter - tc, 'assertion(s) in',
+ roundMs(then-now),'ms');
+ }
+ }
+ logClass('green',
+ "Group #"+this.number+":",(TestUtil.counter - assertCount),
+ "assertion(s) in",roundMs(runtime),"ms");
+ if(skipped.length){
+ logClass('warning',"SKIPPED test(s) in group",this.number+":",skipped);
+ }
+ }
+ };
+ return TestGroup;
+ })()/*TestGroup*/,
+ testGroups: [],
+ currentTestGroup: undefined,
+ addGroup: function(name, predicate){
+ this.testGroups.push( this.currentTestGroup =
+ new this.TestGroup(name, predicate) );
+ return this;
+ },
+ addTest: function(name, callback){
+ let predicate;
+ if(1===arguments.length){
+ const opt = arguments[0];
+ predicate = opt.predicate;
+ name = opt.name;
+ callback = opt.test;
+ }
+ this.currentTestGroup.addTest({
+ name, predicate, test: callback
+ });
+ return this;
+ },
+ runTests: async function(sqlite3){
+ return new Promise(async function(pok,pnok){
+ try {
+ let runtime = 0;
+ for(let g of this.testGroups){
+ const now = performance.now();
+ await g.run(sqlite3);
+ runtime += performance.now() - now;
+ }
+ log(TestUtil.separator);
+ logClass(['strong','green'],
+ "Done running tests.",TestUtil.counter,"assertions in",
+ roundMs(runtime),'ms');
+ pok();
+ reportFinalTestStatus(true);
+ }catch(e){
+ error(e);
+ pnok(e);
+ reportFinalTestStatus(false);
+ }
+ }.bind(this));
+ }
+ }/*TestUtil*/;
+ const T = TestUtil;
+ T.g = T.addGroup;
+ T.t = T.addTest;
+ let capi, wasm/*assigned after module init*/;
+ ////////////////////////////////////////////////////////////////////////
+ // End of infrastructure setup. Now define the tests...
+ ////////////////////////////////////////////////////////////////////////
+
+ ////////////////////////////////////////////////////////////////////
+ T.g('Basic sanity checks')
+ .t('Namespace object checks', function(sqlite3){
+ const wasmCtypes = wasm.ctype;
+ T.assert(wasmCtypes.structs[0].name==='sqlite3_vfs').
+ assert(wasmCtypes.structs[0].members.szOsFile.sizeof>=4).
+ assert(wasmCtypes.structs[1/*sqlite3_io_methods*/
+ ].members.xFileSize.offset>0);
+ [ /* Spot-check a handful of constants to make sure they got installed... */
+ 'SQLITE_SCHEMA','SQLITE_NULL','SQLITE_UTF8',
+ 'SQLITE_STATIC', 'SQLITE_DIRECTONLY',
+ 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE'
+ ].forEach((k)=>T.assert('number' === typeof capi[k]));
+ [/* Spot-check a few of the WASM API methods. */
+ 'alloc', 'dealloc', 'installFunction'
+ ].forEach((k)=>T.assert(wasm[k] instanceof Function));
+
+ T.assert(capi.sqlite3_errstr(capi.SQLITE_IOERR_ACCESS).indexOf("I/O")>=0).
+ assert(capi.sqlite3_errstr(capi.SQLITE_CORRUPT).indexOf('malformed')>0).
+ assert(capi.sqlite3_errstr(capi.SQLITE_OK) === 'not an error');
+
+ try {
+ throw new sqlite3.WasmAllocError;
+ }catch(e){
+ T.assert(e instanceof Error)
+ .assert(e instanceof sqlite3.WasmAllocError)
+ .assert("Allocation failed." === e.message);
+ }
+ try {
+ throw new sqlite3.WasmAllocError("test",{
+ cause: 3
+ });
+ }catch(e){
+ T.assert(3 === e.cause)
+ .assert("test" === e.message);
+ }
+ try {throw new sqlite3.WasmAllocError("test","ing",".")}
+ catch(e){T.assert("test ing ." === e.message)}
+
+ try{ throw new sqlite3.SQLite3Error(capi.SQLITE_SCHEMA) }
+ catch(e){ T.assert('SQLITE_SCHEMA' === e.message) }
+ try{ sqlite3.SQLite3Error.toss(capi.SQLITE_CORRUPT,{cause: true}) }
+ catch(e){
+ T.assert('SQLITE_CORRUPT'===e.message)
+ .assert(true===e.cause);
+ }
+ })
+ ////////////////////////////////////////////////////////////////////
+ .t('strglob/strlike', function(sqlite3){
+ T.assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")).
+ assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")).
+ assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)).
+ assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0));
+ })
+ ////////////////////////////////////////////////////////////////////
+ ;/*end of basic sanity checks*/
+
+ ////////////////////////////////////////////////////////////////////
+ T.g('C/WASM Utilities')
+ .t('sqlite3.wasm namespace', function(sqlite3){
+ const w = wasm;
+ const chr = (x)=>x.charCodeAt(0);
+ //log("heap getters...");
+ {
+ const li = [8, 16, 32];
+ if(w.bigIntEnabled) li.push(64);
+ for(const n of li){
+ const bpe = n/8;
+ const s = w.heapForSize(n,false);
+ T.assert(bpe===s.BYTES_PER_ELEMENT).
+ assert(w.heapForSize(s.constructor) === s);
+ const u = w.heapForSize(n,true);
+ T.assert(bpe===u.BYTES_PER_ELEMENT).
+ assert(s!==u).
+ assert(w.heapForSize(u.constructor) === u);
+ }
+ }
+
+ // isPtr32()
+ {
+ const ip = w.isPtr32;
+ T.assert(ip(0))
+ .assert(!ip(-1))
+ .assert(!ip(1.1))
+ .assert(!ip(0xffffffff))
+ .assert(ip(0x7fffffff))
+ .assert(!ip())
+ .assert(!ip(null)/*might change: under consideration*/)
+ ;
+ }
+
+ //log("jstrlen()...");
+ {
+ T.assert(3 === w.jstrlen("abc")).assert(4 === w.jstrlen("äbc"));
+ }
+
+ //log("jstrcpy()...");
+ {
+ const fillChar = 10;
+ let ua = new Uint8Array(8), rc,
+ refill = ()=>ua.fill(fillChar);
+ refill();
+ rc = w.jstrcpy("hello", ua);
+ T.assert(6===rc).assert(0===ua[5]).assert(chr('o')===ua[4]);
+ refill();
+ ua[5] = chr('!');
+ rc = w.jstrcpy("HELLO", ua, 0, -1, false);
+ T.assert(5===rc).assert(chr('!')===ua[5]).assert(chr('O')===ua[4]);
+ refill();
+ rc = w.jstrcpy("the end", ua, 4);
+ //log("rc,ua",rc,ua);
+ T.assert(4===rc).assert(0===ua[7]).
+ assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
+ refill();
+ rc = w.jstrcpy("the end", ua, 4, -1, false);
+ T.assert(4===rc).assert(chr(' ')===ua[7]).
+ assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
+ refill();
+ rc = w.jstrcpy("", ua, 0, 1, true);
+ //log("rc,ua",rc,ua);
+ T.assert(1===rc).assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy("x", ua, 0, 1, true);
+ //log("rc,ua",rc,ua);
+ T.assert(1===rc).assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy('äbä', ua, 0, 1, true);
+ T.assert(1===rc, 'Must not write partial multi-byte char.')
+ .assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy('äbä', ua, 0, 2, true);
+ T.assert(1===rc, 'Must not write partial multi-byte char.')
+ .assert(0===ua[0]);
+ refill();
+ rc = w.jstrcpy('äbä', ua, 0, 2, false);
+ T.assert(2===rc).assert(fillChar!==ua[1]).assert(fillChar===ua[2]);
+ }/*jstrcpy()*/
+
+ //log("cstrncpy()...");
+ {
+ const scope = w.scopedAllocPush();
+ try {
+ let cStr = w.scopedAllocCString("hello");
+ const n = w.cstrlen(cStr);
+ let cpy = w.scopedAlloc(n+10);
+ let rc = w.cstrncpy(cpy, cStr, n+10);
+ T.assert(n+1 === rc).
+ assert("hello" === w.cstringToJs(cpy)).
+ assert(chr('o') === w.getMemValue(cpy+n-1)).
+ assert(0 === w.getMemValue(cpy+n));
+ let cStr2 = w.scopedAllocCString("HI!!!");
+ rc = w.cstrncpy(cpy, cStr2, 3);
+ T.assert(3===rc).
+ assert("HI!lo" === w.cstringToJs(cpy)).
+ assert(chr('!') === w.getMemValue(cpy+2)).
+ assert(chr('l') === w.getMemValue(cpy+3));
+ }finally{
+ w.scopedAllocPop(scope);
+ }
+ }
+
+ //log("jstrToUintArray()...");
+ {
+ let a = w.jstrToUintArray("hello", false);
+ T.assert(5===a.byteLength).assert(chr('o')===a[4]);
+ a = w.jstrToUintArray("hello", true);
+ T.assert(6===a.byteLength).assert(chr('o')===a[4]).assert(0===a[5]);
+ a = w.jstrToUintArray("äbä", false);
+ T.assert(5===a.byteLength).assert(chr('b')===a[2]);
+ a = w.jstrToUintArray("äbä", true);
+ T.assert(6===a.byteLength).assert(chr('b')===a[2]).assert(0===a[5]);
+ }
+
+ //log("allocCString()...");
+ {
+ const cstr = w.allocCString("hällo, world");
+ const n = w.cstrlen(cstr);
+ T.assert(13 === n)
+ .assert(0===w.getMemValue(cstr+n))
+ .assert(chr('d')===w.getMemValue(cstr+n-1));
+ }
+
+ //log("scopedAlloc() and friends...");
+ {
+ const alloc = w.alloc, dealloc = w.dealloc;
+ w.alloc = w.dealloc = null;
+ T.assert(!w.scopedAlloc.level)
+ .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
+ .mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
+ w.alloc = alloc;
+ T.mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
+ w.dealloc = dealloc;
+ T.mustThrowMatching(()=>w.scopedAllocPop(), /^Invalid state/)
+ .mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
+ .mustThrowMatching(()=>w.scopedAlloc.level=0, /read-only/);
+ const asc = w.scopedAllocPush();
+ let asc2;
+ try {
+ const p1 = w.scopedAlloc(16),
+ p2 = w.scopedAlloc(16);
+ T.assert(1===w.scopedAlloc.level)
+ .assert(Number.isFinite(p1))
+ .assert(Number.isFinite(p2))
+ .assert(asc[0] === p1)
+ .assert(asc[1]===p2);
+ asc2 = w.scopedAllocPush();
+ const p3 = w.scopedAlloc(16);
+ T.assert(2===w.scopedAlloc.level)
+ .assert(Number.isFinite(p3))
+ .assert(2===asc.length)
+ .assert(p3===asc2[0]);
+
+ const [z1, z2, z3] = w.scopedAllocPtr(3);
+ T.assert('number'===typeof z1).assert(z2>z1).assert(z3>z2)
+ .assert(0===w.getMemValue(z1,'i32'), 'allocPtr() must zero the targets')
+ .assert(0===w.getMemValue(z3,'i32'));
+ }finally{
+ // Pop them in "incorrect" order to make sure they behave:
+ w.scopedAllocPop(asc);
+ T.assert(0===asc.length);
+ T.mustThrowMatching(()=>w.scopedAllocPop(asc),
+ /^Invalid state object/);
+ if(asc2){
+ T.assert(2===asc2.length,'Should be p3 and z1');
+ w.scopedAllocPop(asc2);
+ T.assert(0===asc2.length);
+ T.mustThrowMatching(()=>w.scopedAllocPop(asc2),
+ /^Invalid state object/);
+ }
+ }
+ T.assert(0===w.scopedAlloc.level);
+ w.scopedAllocCall(function(){
+ T.assert(1===w.scopedAlloc.level);
+ const [cstr, n] = w.scopedAllocCString("hello, world", true);
+ T.assert(12 === n)
+ .assert(0===w.getMemValue(cstr+n))
+ .assert(chr('d')===w.getMemValue(cstr+n-1));
+ });
+ }/*scopedAlloc()*/
+
+ //log("xCall()...");
+ {
+ const pJson = w.xCall('sqlite3_wasm_enum_json');
+ T.assert(Number.isFinite(pJson)).assert(w.cstrlen(pJson)>300);
+ }
+
+ //log("xWrap()...");
+ {
+ T.mustThrowMatching(()=>w.xWrap('sqlite3_libversion',null,'i32'),
+ /requires 0 arg/).
+ assert(w.xWrap.resultAdapter('i32') instanceof Function).
+ assert(w.xWrap.argAdapter('i32') instanceof Function);
+ let fw = w.xWrap('sqlite3_libversion','utf8');
+ T.mustThrowMatching(()=>fw(1), /requires 0 arg/);
+ let rc = fw();
+ T.assert('string'===typeof rc).assert(rc.length>5);
+ rc = w.xCallWrapped('sqlite3_wasm_enum_json','*');
+ T.assert(rc>0 && Number.isFinite(rc));
+ rc = w.xCallWrapped('sqlite3_wasm_enum_json','utf8');
+ T.assert('string'===typeof rc).assert(rc.length>300);
+ if(haveWasmCTests()){
+ fw = w.xWrap('sqlite3_wasm_test_str_hello', 'utf8:free',['i32']);
+ rc = fw(0);
+ T.assert('hello'===rc);
+ rc = fw(1);
+ T.assert(null===rc);
+
+ if(w.bigIntEnabled){
+ w.xWrap.resultAdapter('thrice', (v)=>3n*BigInt(v));
+ w.xWrap.argAdapter('twice', (v)=>2n*BigInt(v));
+ fw = w.xWrap('sqlite3_wasm_test_int64_times2','thrice','twice');
+ rc = fw(1);
+ T.assert(12n===rc);
+
+ w.scopedAllocCall(function(){
+ let pI1 = w.scopedAlloc(8), pI2 = pI1+4;
+ w.setMemValue(pI1, 0,'*')(pI2, 0, '*');
+ let f = w.xWrap('sqlite3_wasm_test_int64_minmax',undefined,['i64*','i64*']);
+ let r1 = w.getMemValue(pI1, 'i64'), r2 = w.getMemValue(pI2, 'i64');
+ T.assert(!Number.isSafeInteger(r1)).assert(!Number.isSafeInteger(r2));
+ });
+ }
+ }
+ }
+ }/*WhWasmUtil*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('sqlite3.StructBinder (jaccwabyt)', function(sqlite3){
+ const S = sqlite3, W = S.wasm;
+ const MyStructDef = {
+ sizeof: 16,
+ members: {
+ p4: {offset: 0, sizeof: 4, signature: "i"},
+ pP: {offset: 4, sizeof: 4, signature: "P"},
+ ro: {offset: 8, sizeof: 4, signature: "i", readOnly: true},
+ cstr: {offset: 12, sizeof: 4, signature: "s"}
+ }
+ };
+ if(W.bigIntEnabled){
+ const m = MyStructDef;
+ m.members.p8 = {offset: m.sizeof, sizeof: 8, signature: "j"};
+ m.sizeof += m.members.p8.sizeof;
+ }
+ const StructType = S.StructBinder.StructType;
+ const K = S.StructBinder('my_struct',MyStructDef);
+ T.mustThrowMatching(()=>K(), /via 'new'/).
+ mustThrowMatching(()=>new K('hi'), /^Invalid pointer/);
+ const k1 = new K(), k2 = new K();
+ try {
+ T.assert(k1.constructor === K).
+ assert(K.isA(k1)).
+ assert(k1 instanceof K).
+ assert(K.prototype.lookupMember('p4').key === '$p4').
+ assert(K.prototype.lookupMember('$p4').name === 'p4').
+ mustThrowMatching(()=>K.prototype.lookupMember('nope'), /not a mapped/).
+ assert(undefined === K.prototype.lookupMember('nope',false)).
+ assert(k1 instanceof StructType).
+ assert(StructType.isA(k1)).
+ assert(K.resolveToInstance(k1.pointer)===k1).
+ mustThrowMatching(()=>K.resolveToInstance(null,true), /is-not-a my_struct/).
+ assert(k1 === StructType.instanceForPointer(k1.pointer)).
+ mustThrowMatching(()=>k1.$ro = 1, /read-only/);
+ Object.keys(MyStructDef.members).forEach(function(key){
+ key = K.memberKey(key);
+ T.assert(0 == k1[key],
+ "Expecting allocation to zero the memory "+
+ "for "+key+" but got: "+k1[key]+
+ " from "+k1.memoryDump());
+ });
+ T.assert('number' === typeof k1.pointer).
+ mustThrowMatching(()=>k1.pointer = 1, /pointer/).
+ assert(K.instanceForPointer(k1.pointer) === k1);
+ k1.$p4 = 1; k1.$pP = 2;
+ T.assert(1 === k1.$p4).assert(2 === k1.$pP);
+ if(MyStructDef.members.$p8){
+ k1.$p8 = 1/*must not throw despite not being a BigInt*/;
+ k1.$p8 = BigInt(Number.MAX_SAFE_INTEGER * 2);
+ T.assert(BigInt(2 * Number.MAX_SAFE_INTEGER) === k1.$p8);
+ }
+ T.assert(!k1.ondispose);
+ k1.setMemberCString('cstr', "A C-string.");
+ T.assert(Array.isArray(k1.ondispose)).
+ assert(k1.ondispose[0] === k1.$cstr).
+ assert('number' === typeof k1.$cstr).
+ assert('A C-string.' === k1.memberToJsString('cstr'));
+ k1.$pP = k2;
+ T.assert(k1.$pP === k2);
+ k1.$pP = null/*null is special-cased to 0.*/;
+ T.assert(0===k1.$pP);
+ let ptr = k1.pointer;
+ k1.dispose();
+ T.assert(undefined === k1.pointer).
+ assert(undefined === K.instanceForPointer(ptr)).
+ mustThrowMatching(()=>{k1.$pP=1}, /disposed instance/);
+ const k3 = new K();
+ ptr = k3.pointer;
+ T.assert(k3 === K.instanceForPointer(ptr));
+ K.disposeAll();
+ T.assert(ptr).
+ assert(undefined === k2.pointer).
+ assert(undefined === k3.pointer).
+ assert(undefined === K.instanceForPointer(ptr));
+ }finally{
+ k1.dispose();
+ k2.dispose();
+ }
+
+ if(!W.bigIntEnabled){
+ log("Skipping WasmTestStruct tests: BigInt not enabled.");
+ return;
+ }
+
+ const WTStructDesc =
+ W.ctype.structs.filter((e)=>'WasmTestStruct'===e.name)[0];
+ const autoResolvePtr = true /* EXPERIMENTAL */;
+ if(autoResolvePtr){
+ WTStructDesc.members.ppV.signature = 'P';
+ }
+ const WTStruct = S.StructBinder(WTStructDesc);
+ //log(WTStruct.structName, WTStruct.structInfo);
+ const wts = new WTStruct();
+ //log("WTStruct.prototype keys:",Object.keys(WTStruct.prototype));
+ try{
+ T.assert(wts.constructor === WTStruct).
+ assert(WTStruct.memberKeys().indexOf('$ppV')>=0).
+ assert(wts.memberKeys().indexOf('$v8')>=0).
+ assert(!K.isA(wts)).
+ assert(WTStruct.isA(wts)).
+ assert(wts instanceof WTStruct).
+ assert(wts instanceof StructType).
+ assert(StructType.isA(wts)).
+ assert(wts === StructType.instanceForPointer(wts.pointer));
+ T.assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8).
+ assert(0===wts.$ppV).assert(0===wts.$xFunc).
+ assert(WTStruct.instanceForPointer(wts.pointer) === wts);
+ const testFunc =
+ W.xGet('sqlite3_wasm_test_struct'/*name gets mangled in -O3 builds!*/);
+ let counter = 0;
+ //log("wts.pointer =",wts.pointer);
+ const wtsFunc = function(arg){
+ /*log("This from a JS function called from C, "+
+ "which itself was called from JS. arg =",arg);*/
+ ++counter;
+ T.assert(WTStruct.instanceForPointer(arg) === wts);
+ if(3===counter){
+ tossQuietly("Testing exception propagation.");
+ }
+ }
+ wts.$v4 = 10; wts.$v8 = 20;
+ wts.$xFunc = W.installFunction(wtsFunc, wts.memberSignature('xFunc'))
+ T.assert(0===counter).assert(10 === wts.$v4).assert(20n === wts.$v8)
+ .assert(0 === wts.$ppV).assert('number' === typeof wts.$xFunc)
+ .assert(0 === wts.$cstr)
+ .assert(wts.memberIsString('$cstr'))
+ .assert(!wts.memberIsString('$v4'))
+ .assert(null === wts.memberToJsString('$cstr'))
+ .assert(W.functionEntry(wts.$xFunc) instanceof Function);
+ /* It might seem silly to assert that the values match
+ what we just set, but recall that all of those property
+ reads and writes are, via property interceptors,
+ actually marshaling their data to/from a raw memory
+ buffer, so merely reading them back is actually part of
+ testing the struct-wrapping API. */
+
+ testFunc(wts.pointer);
+ //log("wts.pointer, wts.$ppV",wts.pointer, wts.$ppV);
+ T.assert(1===counter).assert(20 === wts.$v4).assert(40n === wts.$v8)
+ .assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer))
+ .assert('string' === typeof wts.memberToJsString('cstr'))
+ .assert(wts.memberToJsString('cstr') === wts.memberToJsString('$cstr'))
+ .mustThrowMatching(()=>wts.memberToJsString('xFunc'),
+ /Invalid member type signature for C-string/)
+ ;
+ testFunc(wts.pointer);
+ T.assert(2===counter).assert(40 === wts.$v4).assert(80n === wts.$v8)
+ .assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer));
+ /** The 3rd call to wtsFunc throw from JS, which is called
+ from C, which is called from JS. Let's ensure that
+ that exception propagates back here... */
+ T.mustThrowMatching(()=>testFunc(wts.pointer),/^Testing/);
+ W.uninstallFunction(wts.$xFunc);
+ wts.$xFunc = 0;
+ if(autoResolvePtr){
+ wts.$ppV = 0;
+ T.assert(!wts.$ppV);
+ //WTStruct.debugFlags(0x03);
+ wts.$ppV = wts;
+ T.assert(wts === wts.$ppV)
+ //WTStruct.debugFlags(0);
+ }
+ wts.setMemberCString('cstr', "A C-string.");
+ T.assert(Array.isArray(wts.ondispose)).
+ assert(wts.ondispose[0] === wts.$cstr).
+ assert('A C-string.' === wts.memberToJsString('cstr'));
+ const ptr = wts.pointer;
+ wts.dispose();
+ T.assert(ptr).assert(undefined === wts.pointer).
+ assert(undefined === WTStruct.instanceForPointer(ptr))
+ }finally{
+ wts.dispose();
+ }
+ }/*StructBinder*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('sqlite3.StructBinder part 2', function(sqlite3){
+ // https://www.sqlite.org/c3ref/vfs.html
+ // https://www.sqlite.org/c3ref/io_methods.html
+ const sqlite3_io_methods = capi.sqlite3_io_methods,
+ sqlite3_vfs = capi.sqlite3_vfs,
+ sqlite3_file = capi.sqlite3_file;
+ //log("struct sqlite3_file", sqlite3_file.memberKeys());
+ //log("struct sqlite3_vfs", sqlite3_vfs.memberKeys());
+ //log("struct sqlite3_io_methods", sqlite3_io_methods.memberKeys());
+ const installMethod = function callee(tgt, name, func){
+ if(1===arguments.length){
+ return (n,f)=>callee(tgt,n,f);
+ }
+ if(!callee.argcProxy){
+ callee.argcProxy = function(func,sig){
+ return function(...args){
+ if(func.length!==arguments.length){
+ toss("Argument mismatch. Native signature is:",sig);
+ }
+ return func.apply(this, args);
+ }
+ };
+ callee.ondisposeRemoveFunc = function(){
+ if(this.__ondispose){
+ const who = this;
+ this.__ondispose.forEach(
+ (v)=>{
+ if('number'===typeof v){
+ try{wasm.uninstallFunction(v)}
+ catch(e){/*ignore*/}
+ }else{/*wasm function wrapper property*/
+ delete who[v];
+ }
+ }
+ );
+ delete this.__ondispose;
+ }
+ };
+ }/*static init*/
+ const sigN = tgt.memberSignature(name),
+ memKey = tgt.memberKey(name);
+ //log("installMethod",tgt, name, sigN);
+ if(!tgt.__ondispose){
+ T.assert(undefined === tgt.ondispose);
+ tgt.ondispose = [callee.ondisposeRemoveFunc];
+ tgt.__ondispose = [];
+ }
+ const fProxy = callee.argcProxy(func, sigN);
+ const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
+ tgt[memKey] = pFunc;
+ /**
+ ACHTUNG: function pointer IDs are from a different pool than
+ allocation IDs, starting at 1 and incrementing in steps of 1,
+ so if we set tgt[memKey] to those values, we'd very likely
+ later misinterpret them as plain old pointer addresses unless
+ unless we use some silly heuristic like "all values <5k are
+ presumably function pointers," or actually perform a function
+ lookup on every pointer to first see if it's a function. That
+ would likely work just fine, but would be kludgy.
+
+ It turns out that "all values less than X are functions" is
+ essentially how it works in wasm: a function pointer is
+ reported to the client as its index into the
+ __indirect_function_table.
+
+ So... once jaccwabyt can be told how to access the
+ function table, it could consider all pointer values less
+ than that table's size to be functions. As "real" pointer
+ values start much, much higher than the function table size,
+ that would likely work reasonably well. e.g. the object
+ pointer address for sqlite3's default VFS is (in this local
+ setup) 65104, whereas the function table has fewer than 600
+ entries.
+ */
+ const wrapperKey = '$'+memKey;
+ tgt[wrapperKey] = fProxy;
+ tgt.__ondispose.push(pFunc, wrapperKey);
+ //log("tgt.__ondispose =",tgt.__ondispose);
+ return (n,f)=>callee(tgt, n, f);
+ }/*installMethod*/;
+
+ const installIOMethods = function instm(iom){
+ (iom instanceof capi.sqlite3_io_methods) || toss("Invalid argument type.");
+ if(!instm._requireFileArg){
+ instm._requireFileArg = function(arg,methodName){
+ arg = capi.sqlite3_file.resolveToInstance(arg);
+ if(!arg){
+ err("sqlite3_io_methods::xClose() was passed a non-sqlite3_file.");
+ }
+ return arg;
+ };
+ instm._methods = {
+ // https://sqlite.org/c3ref/io_methods.html
+ xClose: /*i(P)*/function(f){
+ /* int (*xClose)(sqlite3_file*) */
+ log("xClose(",f,")");
+ if(!(f = instm._requireFileArg(f,'xClose'))) return capi.SQLITE_MISUSE;
+ f.dispose(/*noting that f has externally-owned memory*/);
+ return 0;
+ },
+ xRead: /*i(Ppij)*/function(f,dest,n,offset){
+ /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
+ log("xRead(",arguments,")");
+ if(!(f = instm._requireFileArg(f))) return capi.SQLITE_MISUSE;
+ wasm.heap8().fill(0, dest + offset, n);
+ return 0;
+ },
+ xWrite: /*i(Ppij)*/function(f,dest,n,offset){
+ /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
+ log("xWrite(",arguments,")");
+ if(!(f=instm._requireFileArg(f,'xWrite'))) return capi.SQLITE_MISUSE;
+ return 0;
+ },
+ xTruncate: /*i(Pj)*/function(f){
+ /* int (*xTruncate)(sqlite3_file*, sqlite3_int64 size) */
+ log("xTruncate(",arguments,")");
+ if(!(f=instm._requireFileArg(f,'xTruncate'))) return capi.SQLITE_MISUSE;
+ return 0;
+ },
+ xSync: /*i(Pi)*/function(f){
+ /* int (*xSync)(sqlite3_file*, int flags) */
+ log("xSync(",arguments,")");
+ if(!(f=instm._requireFileArg(f,'xSync'))) return capi.SQLITE_MISUSE;
+ return 0;
+ },
+ xFileSize: /*i(Pp)*/function(f,pSz){
+ /* int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize) */
+ log("xFileSize(",arguments,")");
+ if(!(f=instm._requireFileArg(f,'xFileSize'))) return capi.SQLITE_MISUSE;
+ wasm.setMemValue(pSz, 0/*file size*/);
+ return 0;
+ },
+ xLock: /*i(Pi)*/function(f){
+ /* int (*xLock)(sqlite3_file*, int) */
+ log("xLock(",arguments,")");
+ if(!(f=instm._requireFileArg(f,'xLock'))) return capi.SQLITE_MISUSE;
+ return 0;
+ },
+ xUnlock: /*i(Pi)*/function(f){
+ /* int (*xUnlock)(sqlite3_file*, int) */
+ log("xUnlock(",arguments,")");
+ if(!(f=instm._requireFileArg(f,'xUnlock'))) return capi.SQLITE_MISUSE;
+ return 0;
+ },
+ xCheckReservedLock: /*i(Pp)*/function(){
+ /* int (*xCheckReservedLock)(sqlite3_file*, int *pResOut) */
+ log("xCheckReservedLock(",arguments,")");
+ return 0;
+ },
+ xFileControl: /*i(Pip)*/function(){
+ /* int (*xFileControl)(sqlite3_file*, int op, void *pArg) */
+ log("xFileControl(",arguments,")");
+ return capi.SQLITE_NOTFOUND;
+ },
+ xSectorSize: /*i(P)*/function(){
+ /* int (*xSectorSize)(sqlite3_file*) */
+ log("xSectorSize(",arguments,")");
+ return 0/*???*/;
+ },
+ xDeviceCharacteristics:/*i(P)*/function(){
+ /* int (*xDeviceCharacteristics)(sqlite3_file*) */
+ log("xDeviceCharacteristics(",arguments,")");
+ return 0;
+ }
+ };
+ }/*static init*/
+ iom.$iVersion = 1;
+ Object.keys(instm._methods).forEach(
+ (k)=>installMethod(iom, k, instm._methods[k])
+ );
+ }/*installIOMethods()*/;
+
+ const iom = new sqlite3_io_methods, sfile = new sqlite3_file;
+ const err = console.error.bind(console);
+ try {
+ const IOM = sqlite3_io_methods, S3F = sqlite3_file;
+ //log("iom proto",iom,iom.constructor.prototype);
+ //log("sfile",sfile,sfile.constructor.prototype);
+ T.assert(0===sfile.$pMethods).assert(iom.pointer > 0);
+ //log("iom",iom);
+ sfile.$pMethods = iom.pointer;
+ T.assert(iom.pointer === sfile.$pMethods)
+ .assert(IOM.resolveToInstance(iom))
+ .assert(undefined ===IOM.resolveToInstance(sfile))
+ .mustThrow(()=>IOM.resolveToInstance(0,true))
+ .assert(S3F.resolveToInstance(sfile.pointer))
+ .assert(undefined===S3F.resolveToInstance(iom))
+ .assert(iom===IOM.resolveToInstance(sfile.$pMethods));
+ T.assert(0===iom.$iVersion);
+ installIOMethods(iom);
+ T.assert(1===iom.$iVersion);
+ //log("iom.__ondispose",iom.__ondispose);
+ T.assert(Array.isArray(iom.__ondispose)).assert(iom.__ondispose.length>10);
+ }finally{
+ iom.dispose();
+ T.assert(undefined === iom.__ondispose);
+ }
+
+ const dVfs = new sqlite3_vfs(capi.sqlite3_vfs_find(null));
+ try {
+ const SB = sqlite3.StructBinder;
+ T.assert(dVfs instanceof SB.StructType)
+ .assert(dVfs.pointer)
+ .assert('sqlite3_vfs' === dVfs.structName)
+ .assert(!!dVfs.structInfo)
+ .assert(SB.StructType.hasExternalPointer(dVfs))
+ .assert(dVfs.$iVersion>0)
+ .assert('number'===typeof dVfs.$zName)
+ .assert('number'===typeof dVfs.$xSleep)
+ .assert(wasm.functionEntry(dVfs.$xOpen))
+ .assert(dVfs.memberIsString('zName'))
+ .assert(dVfs.memberIsString('$zName'))
+ .assert(!dVfs.memberIsString('pAppData'))
+ .mustThrowMatching(()=>dVfs.memberToJsString('xSleep'),
+ /Invalid member type signature for C-string/)
+ .mustThrowMatching(()=>dVfs.memberSignature('nope'), /nope is not a mapped/)
+ .assert('string' === typeof dVfs.memberToJsString('zName'))
+ .assert(dVfs.memberToJsString('zName')===dVfs.memberToJsString('$zName'))
+ ;
+ //log("Default VFS: @",dVfs.pointer);
+ Object.keys(sqlite3_vfs.structInfo.members).forEach(function(mname){
+ const mk = sqlite3_vfs.memberKey(mname), mbr = sqlite3_vfs.structInfo.members[mname],
+ addr = dVfs[mk], prefix = 'defaultVfs.'+mname;
+ if(1===mbr.signature.length){
+ let sep = '?', val = undefined;
+ switch(mbr.signature[0]){
+ // TODO: move this into an accessor, e.g. getPreferredValue(member)
+ case 'i': case 'j': case 'f': case 'd': sep = '='; val = dVfs[mk]; break
+ case 'p': case 'P': sep = '@'; val = dVfs[mk]; break;
+ case 's': sep = '=';
+ val = dVfs.memberToJsString(mname);
+ break;
+ }
+ //log(prefix, sep, val);
+ }else{
+ //log(prefix," = funcptr @",addr, wasm.functionEntry(addr));
+ }
+ });
+ }finally{
+ dVfs.dispose();
+ T.assert(undefined===dVfs.pointer);
+ }
+ }/*StructBinder part 2*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('sqlite3.wasm.pstack', function(sqlite3){
+ const P = wasm.pstack;
+ const isAllocErr = (e)=>e instanceof sqlite3.WasmAllocError;
+ const stack = P.pointer;
+ T.assert(0===stack % 8 /* must be 8-byte aligned */);
+ try{
+ const remaining = P.remaining;
+ T.assert(P.quota >= 4096)
+ .assert(remaining === P.quota)
+ .mustThrowMatching(()=>P.alloc(0), isAllocErr)
+ .mustThrowMatching(()=>P.alloc(-1), isAllocErr);
+ let p1 = P.alloc(12);
+ T.assert(p1 === stack - 16/*8-byte aligned*/)
+ .assert(P.pointer === p1);
+ let p2 = P.alloc(7);
+ T.assert(p2 === p1-8/*8-byte aligned, stack grows downwards*/)
+ .mustThrowMatching(()=>P.alloc(remaining), isAllocErr)
+ .assert(24 === stack - p2)
+ .assert(P.pointer === p2);
+ let n = remaining - (stack - p2);
+ let p3 = P.alloc(n);
+ T.assert(p3 === stack-remaining)
+ .mustThrowMatching(()=>P.alloc(1), isAllocErr);
+ }finally{
+ P.restore(stack);
+ }
+
+ T.assert(P.pointer === stack);
+ try {
+ const [p1, p2, p3] = P.allocChunks(3,4);
+ T.assert(P.pointer === stack-16/*always rounded to multiple of 8*/)
+ .assert(p2 === p1 + 4)
+ .assert(p3 === p2 + 4);
+ T.mustThrowMatching(()=>P.allocChunks(1024, 1024 * 16),
+ (e)=>e instanceof sqlite3.WasmAllocError)
+ }finally{
+ P.restore(stack);
+ }
+
+ T.assert(P.pointer === stack);
+ try {
+ let [p1, p2, p3] = P.allocPtr(3,false);
+ let sPos = stack-16/*always rounded to multiple of 8*/;
+ T.assert(P.pointer === sPos)
+ .assert(p2 === p1 + 4)
+ .assert(p3 === p2 + 4);
+ [p1, p2, p3] = P.allocPtr(3);
+ T.assert(P.pointer === sPos-24/*3 x 8 bytes*/)
+ .assert(p2 === p1 + 8)
+ .assert(p3 === p2 + 8);
+ p1 = P.allocPtr();
+ T.assert('number'===typeof p1);
+ }finally{
+ P.restore(stack);
+ }
+ }/*pstack tests*/)
+
+ ////////////////////////////////////////////////////////////////////
+ ;/*end of C/WASM utils checks*/
+
+ T.g('sqlite3_randomness()')
+ .t('To memory buffer', function(sqlite3){
+ const stack = wasm.pstack.pointer;
+ try{
+ const n = 520;
+ const p = wasm.pstack.alloc(n);
+ T.assert(0===wasm.getMemValue(p))
+ .assert(0===wasm.getMemValue(p+n-1));
+ T.assert(undefined === capi.sqlite3_randomness(n - 10, p));
+ let j, check = 0;
+ const heap = wasm.heap8u();
+ for(j = 0; j < 10 && 0===check; ++j){
+ check += heap[p + j];
+ }
+ T.assert(check > 0);
+ check = 0;
+ // Ensure that the trailing bytes were not modified...
+ for(j = n - 10; j < n && 0===check; ++j){
+ check += heap[p + j];
+ }
+ T.assert(0===check);
+ }finally{
+ wasm.pstack.restore(stack);
+ }
+ })
+ .t('To byte array', function(sqlite3){
+ const ta = new Uint8Array(117);
+ let i, n = 0;
+ for(i=0; i<ta.byteLength && 0===n; ++i){
+ n += ta[i];
+ }
+ T.assert(0===n)
+ .assert(ta === capi.sqlite3_randomness(ta));
+ for(i=ta.byteLength-10; i<ta.byteLength && 0===n; ++i){
+ n += ta[i];
+ }
+ T.assert(n>0);
+ const t0 = new Uint8Array(0);
+ T.assert(t0 === capi.sqlite3_randomness(t0),
+ "0-length array is a special case");
+ })
+ ;/*end sqlite3_randomness() checks*/
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('sqlite3.oo1')
+ .t('Create db', function(sqlite3){
+ const dbFile = '/tester1.db';
+ wasm.sqlite3_wasm_vfs_unlink(0, dbFile);
+ const db = this.db = new sqlite3.oo1.DB(dbFile);
+ T.assert(Number.isInteger(db.pointer))
+ .mustThrowMatching(()=>db.pointer=1, /read-only/)
+ .assert(0===sqlite3.capi.sqlite3_extended_result_codes(db.pointer,1))
+ .assert('main'===db.dbName(0))
+ .assert('string' === typeof db.dbVfsName());
+ // Custom db error message handling via sqlite3_prepare_v2/v3()
+ let rc = capi.sqlite3_prepare_v3(db.pointer, {/*invalid*/}, -1, 0, null, null);
+ T.assert(capi.SQLITE_MISUSE === rc)
+ .assert(0 === capi.sqlite3_errmsg(db.pointer).indexOf("Invalid SQL"))
+ .assert(dbFile === db.dbFilename())
+ .assert(!db.dbFilename('nope'));
+ })
+
+ ////////////////////////////////////////////////////////////////////
+ .t('DB.Stmt', function(S){
+ let st = this.db.prepare(
+ new TextEncoder('utf-8').encode("select 3 as a")
+ );
+ //debug("statement =",st);
+ try {
+ T.assert(Number.isInteger(st.pointer))
+ .mustThrowMatching(()=>st.pointer=1, /read-only/)
+ .assert(1===this.db.openStatementCount())
+ .assert(!st._mayGet)
+ .assert('a' === st.getColumnName(0))
+ .assert(1===st.columnCount)
+ .assert(0===st.parameterCount)
+ .mustThrow(()=>st.bind(1,null))
+ .assert(true===st.step())
+ .assert(3 === st.get(0))
+ .mustThrow(()=>st.get(1))
+ .mustThrow(()=>st.get(0,~capi.SQLITE_INTEGER))
+ .assert(3 === st.get(0,capi.SQLITE_INTEGER))
+ .assert(3 === st.getInt(0))
+ .assert('3' === st.get(0,capi.SQLITE_TEXT))
+ .assert('3' === st.getString(0))
+ .assert(3.0 === st.get(0,capi.SQLITE_FLOAT))
+ .assert(3.0 === st.getFloat(0))
+ .assert(3 === st.get({}).a)
+ .assert(3 === st.get([])[0])
+ .assert(3 === st.getJSON(0))
+ .assert(st.get(0,capi.SQLITE_BLOB) instanceof Uint8Array)
+ .assert(1===st.get(0,capi.SQLITE_BLOB).length)
+ .assert(st.getBlob(0) instanceof Uint8Array)
+ .assert('3'.charCodeAt(0) === st.getBlob(0)[0])
+ .assert(st._mayGet)
+ .assert(false===st.step())
+ .assert(!st._mayGet)
+ ;
+ T.assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")).
+ assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")).
+ assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)).
+ assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0));
+ }finally{
+ st.finalize();
+ }
+ T.assert(!st.pointer)
+ .assert(0===this.db.openStatementCount());
+ })
+
+ ////////////////////////////////////////////////////////////////////////
+ .t('sqlite3_js_...()', function(){
+ const db = this.db;
+ if(1){
+ const vfsList = capi.sqlite3_js_vfs_list();
+ T.assert(vfsList.length>1);
+ T.assert('string'===typeof vfsList[0]);
+ //log("vfsList =",vfsList);
+ for(const v of vfsList){
+ T.assert('string' === typeof v)
+ .assert(capi.sqlite3_vfs_find(v) > 0);
+ }
+ }
+ /**
+ Trivia: the magic db name ":memory:" does not actually use the
+ "memdb" VFS unless "memdb" is _explicitly_ provided as the VFS
+ name. Instead, it uses the default VFS with an in-memory btree.
+ Thus this.db's VFS may not be memdb even though it's an in-memory
+ db.
+ */
+ const pVfsMem = capi.sqlite3_vfs_find('memdb'),
+ pVfsDflt = capi.sqlite3_vfs_find(0),
+ pVfsDb = capi.sqlite3_js_db_vfs(db.pointer);
+ T.assert(pVfsMem > 0)
+ .assert(pVfsDflt > 0)
+ .assert(pVfsDb > 0)
+ .assert(pVfsMem !== pVfsDflt
+ /* memdb lives on top of the default vfs */)
+ .assert(pVfsDb === pVfsDflt || pVfsdb === pVfsMem)
+ ;
+ /*const vMem = new capi.sqlite3_vfs(pVfsMem),
+ vDflt = new capi.sqlite3_vfs(pVfsDflt),
+ vDb = new capi.sqlite3_vfs(pVfsDb);*/
+ const duv = capi.sqlite3_js_db_uses_vfs;
+ T.assert(pVfsDflt === duv(db.pointer, 0)
+ || pVfsMem === duv(db.pointer,0))
+ .assert(!duv(db.pointer, "foo"))
+ ;
+ }/*sqlite3_js_...()*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('Table t', function(sqlite3){
+ const db = this.db;
+ let list = [];
+ let rc = db.exec({
+ sql:['CREATE TABLE t(a,b);',
+ // ^^^ using TEMP TABLE breaks the db export test
+ "INSERT INTO t(a,b) VALUES(1,2),(3,4),",
+ "(?,?),('blob',X'6869')"/*intentionally missing semicolon to test for
+ off-by-one bug in string-to-WASM conversion*/],
+ saveSql: list,
+ bind: [5,6]
+ });
+ //debug("Exec'd SQL:", list);
+ T.assert(rc === db)
+ .assert(2 === list.length)
+ .assert('string'===typeof list[1])
+ .assert(4===db.changes());
+ if(wasm.bigIntEnabled){
+ T.assert(4n===db.changes(false,true));
+ }
+ let blob = db.selectValue("select b from t where a='blob'");
+ T.assert(blob instanceof Uint8Array).
+ assert(0x68===blob[0] && 0x69===blob[1]);
+ blob = null;
+ let counter = 0, colNames = [];
+ list.length = 0;
+ db.exec(new TextEncoder('utf-8').encode("SELECT a a, b b FROM t"),{
+ rowMode: 'object',
+ resultRows: list,
+ columnNames: colNames,
+ callback: function(row,stmt){
+ ++counter;
+ T.assert((row.a%2 && row.a<6) || 'blob'===row.a);
+ }
+ });
+ T.assert(2 === colNames.length)
+ .assert('a' === colNames[0])
+ .assert(4 === counter)
+ .assert(4 === list.length);
+ list.length = 0;
+ db.exec("SELECT a a, b b FROM t",{
+ rowMode: 'array',
+ callback: function(row,stmt){
+ ++counter;
+ T.assert(Array.isArray(row))
+ .assert((0===row[1]%2 && row[1]<7)
+ || (row[1] instanceof Uint8Array));
+ }
+ });
+ T.assert(8 === counter);
+ T.assert(Number.MIN_SAFE_INTEGER ===
+ db.selectValue("SELECT "+Number.MIN_SAFE_INTEGER)).
+ assert(Number.MAX_SAFE_INTEGER ===
+ db.selectValue("SELECT "+Number.MAX_SAFE_INTEGER));
+ if(wasm.bigIntEnabled && haveWasmCTests()){
+ const mI = wasm.xCall('sqlite3_wasm_test_int64_max');
+ const b = BigInt(Number.MAX_SAFE_INTEGER * 2);
+ T.assert(b === db.selectValue("SELECT "+b)).
+ assert(b === db.selectValue("SELECT ?", b)).
+ assert(mI == db.selectValue("SELECT $x", {$x:mI}));
+ }else{
+ /* Curiously, the JS spec seems to be off by one with the definitions
+ of MIN/MAX_SAFE_INTEGER:
+
+ https://github.com/emscripten-core/emscripten/issues/17391 */
+ T.mustThrow(()=>db.selectValue("SELECT "+(Number.MAX_SAFE_INTEGER+1))).
+ mustThrow(()=>db.selectValue("SELECT "+(Number.MIN_SAFE_INTEGER-1)));
+ }
+
+ let st = db.prepare("update t set b=:b where a='blob'");
+ try {
+ const ndx = st.getParamIndex(':b');
+ T.assert(1===ndx);
+ st.bindAsBlob(ndx, "ima blob").reset(true);
+ } finally {
+ st.finalize();
+ }
+
+ try {
+ db.prepare("/*empty SQL*/");
+ toss("Must not be reached.");
+ }catch(e){
+ T.assert(e instanceof sqlite3.SQLite3Error)
+ .assert(0==e.message.indexOf('Cannot prepare empty'));
+ }
+ })
+
+ ////////////////////////////////////////////////////////////////////////
+ .t('selectArray/Object()', function(sqlite3){
+ const db = this.db;
+ let rc = db.selectArray('select a, b from t where a=?', 5);
+ T.assert(Array.isArray(rc))
+ .assert(2===rc.length)
+ .assert(5===rc[0] && 6===rc[1]);
+ rc = db.selectArray('select a, b from t where b=-1');
+ T.assert(undefined === rc);
+ rc = db.selectObject('select a A, b b from t where b=?', 6);
+ T.assert(rc && 'object'===typeof rc)
+ .assert(5===rc.A)
+ .assert(6===rc.b);
+ rc = db.selectArray('select a, b from t where b=-1');
+ T.assert(undefined === rc);
+ })
+
+ ////////////////////////////////////////////////////////////////////////
+ .t('sqlite3_js_db_export()', function(){
+ const db = this.db;
+ const xp = capi.sqlite3_js_db_export(db.pointer);
+ T.assert(xp instanceof Uint8Array)
+ .assert(xp.byteLength>0)
+ .assert(0 === xp.byteLength % 512);
+ }/*sqlite3_js_db_export()*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t('Scalar UDFs', function(sqlite3){
+ const db = this.db;
+ db.createFunction("foo",(pCx,a,b)=>a+b);
+ T.assert(7===db.selectValue("select foo(3,4)")).
+ assert(5===db.selectValue("select foo(3,?)",2)).
+ assert(5===db.selectValue("select foo(?,?2)",[1,4])).
+ assert(5===db.selectValue("select foo($a,$b)",{$a:0,$b:5}));
+ db.createFunction("bar", {
+ arity: -1,
+ xFunc: (pCx,...args)=>{
+ let rc = 0;
+ for(const v of args) rc += v;
+ return rc;
+ }
+ }).createFunction({
+ name: "asis",
+ xFunc: (pCx,arg)=>arg
+ });
+ T.assert(0===db.selectValue("select bar()")).
+ assert(1===db.selectValue("select bar(1)")).
+ assert(3===db.selectValue("select bar(1,2)")).
+ assert(-1===db.selectValue("select bar(1,2,-4)")).
+ assert('hi' === db.selectValue("select asis('hi')")).
+ assert('hi' === db.selectValue("select ?",'hi')).
+ assert(null === db.selectValue("select null")).
+ assert(null === db.selectValue("select asis(null)")).
+ assert(1 === db.selectValue("select ?",1)).
+ assert(2 === db.selectValue("select ?",[2])).
+ assert(3 === db.selectValue("select $a",{$a:3})).
+ assert(T.eqApprox(3.1,db.selectValue("select 3.0 + 0.1"))).
+ assert(T.eqApprox(1.3,db.selectValue("select asis(1 + 0.3)")));
+
+ let blobArg = new Uint8Array(2);
+ blobArg.set([0x68, 0x69], 0);
+ let blobRc = db.selectValue("select asis(?1)", blobArg);
+ T.assert(blobRc instanceof Uint8Array).
+ assert(2 === blobRc.length).
+ assert(0x68==blobRc[0] && 0x69==blobRc[1]);
+ blobRc = db.selectValue("select asis(X'6869')");
+ T.assert(blobRc instanceof Uint8Array).
+ assert(2 === blobRc.length).
+ assert(0x68==blobRc[0] && 0x69==blobRc[1]);
+
+ blobArg = new Int8Array(2);
+ blobArg.set([0x68, 0x69]);
+ //debug("blobArg=",blobArg);
+ blobRc = db.selectValue("select asis(?1)", blobArg);
+ T.assert(blobRc instanceof Uint8Array).
+ assert(2 === blobRc.length);
+ //debug("blobRc=",blobRc);
+ T.assert(0x68==blobRc[0] && 0x69==blobRc[1]);
+ })
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'Aggregate UDFs',
+ test: function(sqlite3){
+ const db = this.db;
+ const sjac = capi.sqlite3_js_aggregate_context;
+ db.createFunction({
+ name: 'summer',
+ xStep: (pCtx, n)=>{
+ const ac = sjac(pCtx, 4);
+ wasm.setMemValue(ac, wasm.getMemValue(ac,'i32') + Number(n), 'i32');
+ },
+ xFinal: (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ return ac ? wasm.getMemValue(ac,'i32') : 0;
+ }
+ });
+ let v = db.selectValue([
+ "with cte(v) as (",
+ "select 3 union all select 5 union all select 7",
+ ") select summer(v), summer(v+1) from cte"
+ /* ------------------^^^^^^^^^^^ ensures that we're handling
+ sqlite3_aggregate_context() properly. */
+ ]);
+ T.assert(15===v);
+ T.mustThrowMatching(()=>db.selectValue("select summer(1,2)"),
+ /wrong number of arguments/);
+
+ db.createFunction({
+ name: 'summerN',
+ arity: -1,
+ xStep: (pCtx, ...args)=>{
+ const ac = sjac(pCtx, 4);
+ let sum = wasm.getMemValue(ac, 'i32');
+ for(const v of args) sum += Number(v);
+ wasm.setMemValue(ac, sum, 'i32');
+ },
+ xFinal: (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ capi.sqlite3_result_int( pCtx, ac ? wasm.getMemValue(ac,'i32') : 0 );
+ // xFinal() may either return its value directly or call
+ // sqlite3_result_xyz() and return undefined. Both are
+ // functionally equivalent.
+ }
+ });
+ T.assert(18===db.selectValue('select summerN(1,8,9), summerN(2,3,4)'));
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{
+ xFunc: ()=>{}, xStep: ()=>{}
+ });
+ }, /scalar or aggregate\?/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xStep: ()=>{}});
+ }, /Missing xFinal/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xFinal: ()=>{}});
+ }, /Missing xStep/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{});
+ }, /Missing function-type properties/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xFunc:()=>{}, xDestroy:'nope'});
+ }, /xDestroy property must be a function/);
+ T.mustThrowMatching(()=>{
+ db.createFunction('nope',{xFunc:()=>{}, pApp:'nope'});
+ }, /Invalid value for pApp/);
+ }
+ }/*aggregate UDFs*/)
+
+ ////////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'Aggregate UDFs (64-bit)',
+ predicate: ()=>wasm.bigIntEnabled,
+ test: function(sqlite3){
+ const db = this.db;
+ const sjac = capi.sqlite3_js_aggregate_context;
+ db.createFunction({
+ name: 'summer64',
+ xStep: (pCtx, n)=>{
+ const ac = sjac(pCtx, 8);
+ wasm.setMemValue(ac, wasm.getMemValue(ac,'i64') + BigInt(n), 'i64');
+ },
+ xFinal: (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ return ac ? wasm.getMemValue(ac,'i64') : 0n;
+ }
+ });
+ let v = db.selectValue([
+ "with cte(v) as (",
+ "select 9007199254740991 union all select 1 union all select 2",
+ ") select summer64(v), summer64(v+1) from cte"
+ ]);
+ T.assert(9007199254740994n===v);
+ }
+ }/*aggregate UDFs*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'Window UDFs',
+ test: function(){
+ /* Example window function, table, and results taken from:
+ https://sqlite.org/windowfunctions.html#udfwinfunc */
+ const db = this.db;
+ const sjac = (cx,n=4)=>capi.sqlite3_js_aggregate_context(cx,n);
+ const xValueFinal = (pCtx)=>{
+ const ac = sjac(pCtx, 0);
+ return ac ? wasm.getMemValue(ac,'i32') : 0;
+ };
+ const xStepInverse = (pCtx, n)=>{
+ const ac = sjac(pCtx);
+ wasm.setMemValue(ac, wasm.getMemValue(ac,'i32') + Number(n), 'i32');
+ };
+ db.createFunction({
+ name: 'winsumint',
+ xStep: (pCtx, n)=>xStepInverse(pCtx, n),
+ xInverse: (pCtx, n)=>xStepInverse(pCtx, -n),
+ xFinal: xValueFinal,
+ xValue: xValueFinal
+ });
+ db.exec([
+ "CREATE TEMP TABLE twin(x, y); INSERT INTO twin VALUES",
+ "('a', 4),('b', 5),('c', 3),('d', 8),('e', 1)"
+ ]);
+ let rc = db.exec({
+ returnValue: 'resultRows',
+ sql:[
+ "SELECT x, winsumint(y) OVER (",
+ "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING",
+ ") AS sum_y ",
+ "FROM twin ORDER BY x;"
+ ]
+ });
+ T.assert(Array.isArray(rc))
+ .assert(5 === rc.length);
+ let count = 0;
+ for(const row of rc){
+ switch(++count){
+ case 1: T.assert('a'===row[0] && 9===row[1]); break;
+ case 2: T.assert('b'===row[0] && 12===row[1]); break;
+ case 3: T.assert('c'===row[0] && 16===row[1]); break;
+ case 4: T.assert('d'===row[0] && 12===row[1]); break;
+ case 5: T.assert('e'===row[0] && 9===row[1]); break;
+ default: toss("Too many rows to window function.");
+ }
+ }
+ const resultRows = [];
+ rc = db.exec({
+ resultRows,
+ returnValue: 'resultRows',
+ sql:[
+ "SELECT x, winsumint(y) OVER (",
+ "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING",
+ ") AS sum_y ",
+ "FROM twin ORDER BY x;"
+ ]
+ });
+ T.assert(rc === resultRows)
+ .assert(5 === rc.length);
+
+ rc = db.exec({
+ returnValue: 'saveSql',
+ sql: "select 1; select 2; -- empty\n; select 3"
+ });
+ T.assert(Array.isArray(rc))
+ .assert(3===rc.length)
+ .assert('select 1;' === rc[0])
+ .assert('select 2;' === rc[1])
+ .assert('-- empty\n; select 3' === rc[2]
+ /* Strange but true. */);
+
+ T.mustThrowMatching(()=>{
+ db.exec({sql:'', returnValue: 'nope'});
+ }, /^Invalid returnValue/);
+
+ db.exec("DROP TABLE twin");
+ }
+ }/*window UDFs*/)
+
+ ////////////////////////////////////////////////////////////////////
+ .t("ATTACH", function(){
+ const db = this.db;
+ const resultRows = [];
+ db.exec({
+ sql:new TextEncoder('utf-8').encode([
+ // ^^^ testing string-vs-typedarray handling in exec()
+ "attach 'session' as foo;",
+ "create table foo.bar(a);",
+ "insert into foo.bar(a) values(1),(2),(3);",
+ "select a from foo.bar order by a;"
+ ].join('')),
+ rowMode: 0,
+ resultRows
+ });
+ T.assert(3===resultRows.length)
+ .assert(2===resultRows[1]);
+ T.assert(2===db.selectValue('select a from foo.bar where a>1 order by a'));
+ let colCount = 0, rowCount = 0;
+ const execCallback = function(pVoid, nCols, aVals, aNames){
+ colCount = nCols;
+ ++rowCount;
+ T.assert(2===aVals.length)
+ .assert(2===aNames.length)
+ .assert(+(aVals[1]) === 2 * +(aVals[0]));
+ };
+ let rc = capi.sqlite3_exec(
+ db.pointer, "select a, a*2 from foo.bar", execCallback,
+ 0, 0
+ );
+ T.assert(0===rc).assert(3===rowCount).assert(2===colCount);
+ rc = capi.sqlite3_exec(
+ db.pointer, "select a from foo.bar", ()=>{
+ tossQuietly("Testing throwing from exec() callback.");
+ }, 0, 0
+ );
+ T.assert(capi.SQLITE_ABORT === rc);
+ db.exec("detach foo");
+ T.mustThrow(()=>db.exec("select * from foo.bar"));
+ })
+
+ ////////////////////////////////////////////////////////////////////
+ .t({
+ name: 'C-side WASM tests (if compiled in)',
+ predicate: haveWasmCTests,
+ test: function(){
+ const w = wasm, db = this.db;
+ const stack = w.scopedAllocPush();
+ let ptrInt;
+ const origValue = 512;
+ const ptrValType = 'i32';
+ try{
+ ptrInt = w.scopedAlloc(4);
+ w.setMemValue(ptrInt,origValue, ptrValType);
+ const cf = w.xGet('sqlite3_wasm_test_intptr');
+ const oldPtrInt = ptrInt;
+ //log('ptrInt',ptrInt);
+ //log('getMemValue(ptrInt)',w.getMemValue(ptrInt));
+ T.assert(origValue === w.getMemValue(ptrInt, ptrValType));
+ const rc = cf(ptrInt);
+ //log('cf(ptrInt)',rc);
+ //log('ptrInt',ptrInt);
+ //log('getMemValue(ptrInt)',w.getMemValue(ptrInt,ptrValType));
+ T.assert(2*origValue === rc).
+ assert(rc === w.getMemValue(ptrInt,ptrValType)).
+ assert(oldPtrInt === ptrInt);
+ const pi64 = w.scopedAlloc(8)/*ptr to 64-bit integer*/;
+ const o64 = 0x010203040506/*>32-bit integer*/;
+ const ptrType64 = 'i64';
+ if(w.bigIntEnabled){
+ w.setMemValue(pi64, o64, ptrType64);
+ //log("pi64 =",pi64, "o64 = 0x",o64.toString(16), o64);
+ const v64 = ()=>w.getMemValue(pi64,ptrType64)
+ //log("getMemValue(pi64)",v64());
+ T.assert(v64() == o64);
+ //T.assert(o64 === w.getMemValue(pi64, ptrType64));
+ const cf64w = w.xGet('sqlite3_wasm_test_int64ptr');
+ cf64w(pi64);
+ //log("getMemValue(pi64)",v64());
+ T.assert(v64() == BigInt(2 * o64));
+ cf64w(pi64);
+ T.assert(v64() == BigInt(4 * o64));
+
+ const biTimes2 = w.xGet('sqlite3_wasm_test_int64_times2');
+ T.assert(BigInt(2 * o64) ===
+ biTimes2(BigInt(o64)/*explicit conv. required to avoid TypeError
+ in the call :/ */));
+
+ const pMin = w.scopedAlloc(16);
+ const pMax = pMin + 8;
+ const g64 = (p)=>w.getMemValue(p,ptrType64);
+ w.setMemValue(pMin, 0, ptrType64);
+ w.setMemValue(pMax, 0, ptrType64);
+ const minMaxI64 = [
+ w.xCall('sqlite3_wasm_test_int64_min'),
+ w.xCall('sqlite3_wasm_test_int64_max')
+ ];
+ T.assert(minMaxI64[0] < BigInt(Number.MIN_SAFE_INTEGER)).
+ assert(minMaxI64[1] > BigInt(Number.MAX_SAFE_INTEGER));
+ //log("int64_min/max() =",minMaxI64, typeof minMaxI64[0]);
+ w.xCall('sqlite3_wasm_test_int64_minmax', pMin, pMax);
+ T.assert(g64(pMin) === minMaxI64[0], "int64 mismatch").
+ assert(g64(pMax) === minMaxI64[1], "int64 mismatch");
+ //log("pMin",g64(pMin), "pMax",g64(pMax));
+ w.setMemValue(pMin, minMaxI64[0], ptrType64);
+ T.assert(g64(pMin) === minMaxI64[0]).
+ assert(minMaxI64[0] === db.selectValue("select ?",g64(pMin))).
+ assert(minMaxI64[1] === db.selectValue("select ?",g64(pMax)));
+ const rxRange = /too big/;
+ T.mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[0] - BigInt(1))},
+ rxRange).
+ mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[1] + BigInt(1))},
+ (e)=>rxRange.test(e.message));
+ }else{
+ log("No BigInt support. Skipping related tests.");
+ log("\"The problem\" here is that we can manipulate, at the byte level,",
+ "heap memory to set 64-bit values, but we can't get those values",
+ "back into JS because of the lack of 64-bit integer support.");
+ }
+ }finally{
+ const x = w.scopedAlloc(1), y = w.scopedAlloc(1), z = w.scopedAlloc(1);
+ //log("x=",x,"y=",y,"z=",z); // just looking at the alignment
+ w.scopedAllocPop(stack);
+ }
+ }
+ }/* jaccwabyt-specific tests */)
+
+ .t('Close db', function(){
+ T.assert(this.db).assert(Number.isInteger(this.db.pointer));
+ wasm.exports.sqlite3_wasm_db_reset(this.db.pointer);
+ this.db.close();
+ T.assert(!this.db.pointer);
+ })
+ ;/* end of oo1 checks */
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('kvvfs')
+ .t('kvvfs sanity checks', function(sqlite3){
+ if(isWorker()){
+ T.assert(
+ !capi.sqlite3_vfs_find('kvvfs'),
+ "Expecting kvvfs to be unregistered."
+ );
+ log("kvvfs is (correctly) unavailable in a Worker.");
+ return;
+ }
+ const filename = 'session';
+ const pVfs = capi.sqlite3_vfs_find('kvvfs');
+ T.assert(pVfs);
+ const JDb = sqlite3.oo1.JsStorageDb;
+ const unlink = ()=>JDb.clearStorage(filename);
+ unlink();
+ let db = new JDb(filename);
+ try {
+ db.exec([
+ 'create table kvvfs(a);',
+ 'insert into kvvfs(a) values(1),(2),(3)'
+ ]);
+ T.assert(3 === db.selectValue('select count(*) from kvvfs'));
+ db.close();
+ db = new JDb(filename);
+ db.exec('insert into kvvfs(a) values(4),(5),(6)');
+ T.assert(6 === db.selectValue('select count(*) from kvvfs'));
+ }finally{
+ db.close();
+ unlink();
+ }
+ }/*kvvfs sanity checks*/)
+ ;/* end kvvfs tests */
+
+ ////////////////////////////////////////////////////////////////////////
+ T.g('OPFS (Worker thread only and only in supported browsers)',
+ (sqlite3)=>{return !!sqlite3.opfs})
+ .t({
+ name: 'OPFS sanity checks',
+ test: async function(sqlite3){
+ const opfs = sqlite3.opfs;
+ const filename = 'sqlite3-tester1.db';
+ const pVfs = capi.sqlite3_vfs_find('opfs');
+ T.assert(pVfs);
+ const unlink = (fn=filename)=>wasm.sqlite3_wasm_vfs_unlink(pVfs,fn);
+ unlink();
+ let db = new opfs.OpfsDb(filename);
+ try {
+ db.exec([
+ 'create table p(a);',
+ 'insert into p(a) values(1),(2),(3)'
+ ]);
+ T.assert(3 === db.selectValue('select count(*) from p'));
+ db.close();
+ db = new opfs.OpfsDb(filename);
+ db.exec('insert into p(a) values(4),(5),(6)');
+ T.assert(6 === db.selectValue('select count(*) from p'));
+ }finally{
+ db.close();
+ unlink();
+ }
+
+ if(1){
+ // Sanity-test sqlite3_wasm_vfs_create_file()...
+ const fSize = 1379;
+ let sh;
+ try{
+ T.assert(!(await opfs.entryExists(filename)));
+ let rc = wasm.sqlite3_wasm_vfs_create_file(
+ pVfs, filename, null, fSize
+ );
+ T.assert(0===rc)
+ .assert(await opfs.entryExists(filename));
+ const fh = await opfs.rootDirectory.getFileHandle(filename);
+ sh = await fh.createSyncAccessHandle();
+ T.assert(fSize === await sh.getSize());
+ }finally{
+ if(sh) await sh.close();
+ unlink();
+ }
+ }
+
+ // Some sanity checks of the opfs utility functions...
+ const testDir = '/sqlite3-opfs-'+opfs.randomFilename(12);
+ const aDir = testDir+'/test/dir';
+ T.assert(await opfs.mkdir(aDir), "mkdir failed")
+ .assert(await opfs.mkdir(aDir), "mkdir must pass if the dir exists")
+ .assert(!(await opfs.unlink(testDir+'/test')), "delete 1 should have failed (dir not empty)")
+ .assert((await opfs.unlink(testDir+'/test/dir')), "delete 2 failed")
+ .assert(!(await opfs.unlink(testDir+'/test/dir')),
+ "delete 2b should have failed (dir already deleted)")
+ .assert((await opfs.unlink(testDir, true)), "delete 3 failed")
+ .assert(!(await opfs.entryExists(testDir)),
+ "entryExists(",testDir,") should have failed");
+ }
+ }/*OPFS sanity checks*/)
+ ;/* end OPFS tests */
+
+ ////////////////////////////////////////////////////////////////////////
+ log("Loading and initializing sqlite3 WASM module...");
+ if(!isUIThread()){
+ /*
+ If sqlite3.js is in a directory other than this script, in order
+ to get sqlite3.js to resolve sqlite3.wasm properly, we have to
+ explicitly tell it where sqlite3.js is being loaded from. We do
+ that by passing the `sqlite3.dir=theDirName` URL argument to
+ _this_ script. That URL argument will be seen by the JS/WASM
+ loader and it will adjust the sqlite3.wasm path accordingly. If
+ sqlite3.js/.wasm are in the same directory as this script then
+ that's not needed.
+
+ URL arguments passed as part of the filename via importScripts()
+ are simply lost, and such scripts see the self.location of
+ _this_ script.
+ */
+ let sqlite3Js = 'sqlite3.js';
+ const urlParams = new URL(self.location.href).searchParams;
+ if(urlParams.has('sqlite3.dir')){
+ sqlite3Js = urlParams.get('sqlite3.dir') + '/' + sqlite3Js;
+ }
+ importScripts(sqlite3Js);
+ }
+ self.sqlite3InitModule({
+ print: log,
+ printErr: error
+ }).then(function(sqlite3){
+ //console.log('sqlite3 =',sqlite3);
+ log("Done initializing WASM/JS bits. Running tests...");
+ capi = sqlite3.capi;
+ wasm = sqlite3.wasm;
+ log("sqlite3 version:",capi.sqlite3_libversion(),
+ capi.sqlite3_sourceid());
+ if(wasm.bigIntEnabled){
+ log("BigInt/int64 support is enabled.");
+ }else{
+ logClass('warning',"BigInt/int64 support is disabled.");
+ }
+ if(haveWasmCTests()){
+ log("sqlite3_wasm_test_...() APIs are available.");
+ }else{
+ logClass('warning',"sqlite3_wasm_test_...() APIs unavailable.");
+ }
+ TestUtil.runTests(sqlite3);
+ });
+})();
diff --git a/ext/wasm/version-info.c b/ext/wasm/version-info.c
new file mode 100644
index 0000000..62fcd63
--- /dev/null
+++ b/ext/wasm/version-info.c
@@ -0,0 +1,106 @@
+/*
+** 2022-10-16
+**
+** The author disclaims copyright to this source code. In place of a
+** legal notice, here is a blessing:
+**
+** * May you do good and not evil.
+** * May you find forgiveness for yourself and forgive others.
+** * May you share freely, never taking more than you give.
+**
+*************************************************************************
+** This file simply outputs sqlite3 version information in JSON form,
+** intended for embedding in the sqlite3 JS API build.
+*/
+#ifdef TEST_VERSION
+/*3029003 3039012*/
+#define SQLITE_VERSION "X.Y.Z"
+#define SQLITE_VERSION_NUMBER TEST_VERSION
+#define SQLITE_SOURCE_ID "dummy"
+#else
+#include "sqlite3.h"
+#endif
+#include <stdio.h>
+#include <string.h>
+static void usage(const char *zAppName){
+ puts("Emits version info about the sqlite3 it is built against.");
+ printf("Usage: %s [--quote] --INFO-FLAG:\n\n", zAppName);
+ puts(" --version Emit SQLITE_VERSION (3.X.Y)");
+ puts(" --version-number Emit SQLITE_VERSION_NUMBER (30XXYYZZ)");
+ puts(" --download-version Emit /download.html version number (3XXYYZZ)");
+ puts(" --source-id Emit SQLITE_SOURCE_ID");
+ puts(" --json Emit all info in JSON form");
+ puts("\nThe non-JSON formats may be modified by:\n");
+ puts(" --quote Add double quotes around output.");
+}
+
+int main(int argc, char const * const * argv){
+ int fJson = 0;
+ int fVersion = 0;
+ int fVersionNumber = 0;
+ int fDlVersion = 0;
+ int dlVersion = 0;
+ int fSourceInfo = 0;
+ int fQuote = 0;
+ int nFlags = 0;
+ int i;
+
+ for( i = 1; i < argc; ++i ){
+ const char * zArg = argv[i];
+ while('-'==*zArg) ++zArg;
+ if( 0==strcmp("version", zArg) ){
+ fVersion = 1;
+ }else if( 0==strcmp("version-number", zArg) ){
+ fVersionNumber = 1;
+ }else if( 0==strcmp("download-version", zArg) ){
+ fDlVersion = 1;
+ }else if( 0==strcmp("source-id", zArg) ){
+ fSourceInfo = 1;
+ }else if( 0==strcmp("json", zArg) ){
+ fJson = 1;
+ }else if( 0==strcmp("quote", zArg) ){
+ fQuote = 1;
+ --nFlags;
+ }else{
+ printf("Unhandled flag: %s\n", argv[i]);
+ usage(argv[0]);
+ return 1;
+ }
+ ++nFlags;
+ }
+
+ if( 0==nFlags ) fJson = 1;
+
+ {
+ const int v = SQLITE_VERSION_NUMBER;
+ int ver[4] = {0,0,0,0};
+ ver[0] = (v / 1000000) * 1000000;
+ ver[1] = v % 1000000 / 100 * 1000;
+ ver[2] = v % 100 * 100;
+ dlVersion = ver[0] + ver[1] + ver[2] + ver[3];
+ }
+ if( fJson ){
+ printf("{\"libVersion\": \"%s\", "
+ "\"libVersionNumber\": %d, "
+ "\"sourceId\": \"%s\","
+ "\"downloadVersion\": %d}"/*missing newline is intentional*/,
+ SQLITE_VERSION,
+ SQLITE_VERSION_NUMBER,
+ SQLITE_SOURCE_ID,
+ dlVersion);
+ }else{
+ if(fQuote) printf("%c", '"');
+ if( fVersion ){
+ printf("%s", SQLITE_VERSION);
+ }else if( fVersionNumber ){
+ printf("%d", SQLITE_VERSION_NUMBER);
+ }else if( fSourceInfo ){
+ printf("%s", SQLITE_SOURCE_ID);
+ }else if( fDlVersion ){
+ printf("%d", dlVersion);
+ }
+ if(fQuote) printf("%c", '"');
+ puts("");
+ }
+ return 0;
+}
diff --git a/ext/wasm/wasmfs.make b/ext/wasm/wasmfs.make
new file mode 100644
index 0000000..81b4187
--- /dev/null
+++ b/ext/wasm/wasmfs.make
@@ -0,0 +1,113 @@
+#!/usr/bin/make
+#^^^^ help emacs select makefile mode
+#
+# This is a sub-make for building a standalone wasmfs-based
+# sqlite3.wasm. It is intended to be "include"d from the main
+# GNUMakefile.
+########################################################################
+MAKEFILE.wasmfs := $(lastword $(MAKEFILE_LIST))
+
+# Maintenance reminder: these particular files cannot be built into a
+# subdirectory because loading of the auxiliary
+# sqlite3-wasmfs.worker.js file it creates fails if sqlite3-wasmfs.js
+# is loaded from any directory other than the one in which the
+# containing HTML lives. Similarly, they cannot be loaded from a
+# Worker to an Emscripten quirk regarding loading nested Workers.
+dir.wasmfs := $(dir.wasm)
+sqlite3-wasmfs.js := $(dir.wasmfs)/sqlite3-wasmfs.js
+sqlite3-wasmfs.wasm := $(dir.wasmfs)/sqlite3-wasmfs.wasm
+
+CLEAN_FILES += $(sqlite3-wasmfs.js) $(sqlite3-wasmfs.wasm) \
+ $(subst .js,.worker.js,$(sqlite3-wasmfs.js))
+
+########################################################################
+# emcc flags for .c/.o.
+sqlite3-wasmfs.cflags :=
+sqlite3-wasmfs.cflags += -std=c99 -fPIC
+sqlite3-wasmfs.cflags += -pthread
+sqlite3-wasmfs.cflags += $(cflags.common)
+sqlite3-wasmfs.cflags += $(SQLITE_OPT) -DSQLITE_ENABLE_WASMFS
+
+########################################################################
+# emcc flags specific to building the final .js/.wasm file...
+sqlite3-wasmfs.jsflags := -fPIC
+sqlite3-wasmfs.jsflags += --no-entry
+sqlite3-wasmfs.jsflags += --minify 0
+sqlite3-wasmfs.jsflags += -sMODULARIZE
+sqlite3-wasmfs.jsflags += -sSTRICT_JS
+sqlite3-wasmfs.jsflags += -sDYNAMIC_EXECUTION=0
+sqlite3-wasmfs.jsflags += -sNO_POLYFILL
+sqlite3-wasmfs.jsflags += -sEXPORTED_FUNCTIONS=@$(abspath $(dir.api)/EXPORTED_FUNCTIONS.sqlite3-api)
+sqlite3-wasmfs.jsflags += -sEXPORTED_RUNTIME_METHODS=FS,wasmMemory,allocateUTF8OnStack
+ # wasmMemory ==> for -sIMPORTED_MEMORY
+ # allocateUTF8OnStack ==> wasmfs internals
+sqlite3-wasmfs.jsflags += -sUSE_CLOSURE_COMPILER=0
+sqlite3-wasmfs.jsflags += -sIMPORTED_MEMORY
+#sqlite3-wasmfs.jsflags += -sINITIAL_MEMORY=13107200
+#sqlite3-wasmfs.jsflags += -sTOTAL_STACK=4194304
+sqlite3-wasmfs.jsflags += -sEXPORT_NAME=$(sqlite3.js.init-func)
+sqlite3-wasmfs.jsflags += -sGLOBAL_BASE=4096 # HYPOTHETICALLY keep func table indexes from overlapping w/ heap addr.
+#sqlite3-wasmfs.jsflags += -sFILESYSTEM=0 # only for experimentation. sqlite3 needs the FS API
+# Perhaps the wasmfs build doesn't?
+#sqlite3-wasmfs.jsflags += -sABORTING_MALLOC
+sqlite3-wasmfs.jsflags += -sALLOW_TABLE_GROWTH
+sqlite3-wasmfs.jsflags += -Wno-limited-postlink-optimizations
+# ^^^^^ it likes to warn when we have "limited optimizations" via the -g3 flag.
+sqlite3-wasmfs.jsflags += -sERROR_ON_UNDEFINED_SYMBOLS=0
+sqlite3-wasmfs.jsflags += -sLLD_REPORT_UNDEFINED
+#sqlite3-wasmfs.jsflags += --import-undefined
+sqlite3-wasmfs.jsflags += -sMEMORY64=0
+sqlite3-wasmfs.jsflags += -sINITIAL_MEMORY=128450560
+# ^^^^ 64MB is not enough for WASMFS/OPFS test runs using batch-runner.js
+sqlite3-wasmfs.fsflags := -pthread -sWASMFS -sPTHREAD_POOL_SIZE=2 -sENVIRONMENT=web,worker
+# -sPTHREAD_POOL_SIZE values of 2 or higher trigger that bug.
+sqlite3-wasmfs.jsflags += $(sqlite3-wasmfs.fsflags)
+#sqlite3-wasmfs.jsflags += -sALLOW_MEMORY_GROWTH
+#^^^ using ALLOW_MEMORY_GROWTH produces a warning from emcc:
+# USE_PTHREADS + ALLOW_MEMORY_GROWTH may run non-wasm code slowly,
+# see https://github.com/WebAssembly/design/issues/1271 [-Wpthreads-mem-growth]
+sqlite3-wasmfs.jsflags += -sWASM_BIGINT=$(emcc.WASM_BIGINT)
+$(eval $(call call-make-pre-js,sqlite3-wasmfs))
+sqlite3-wasmfs.jsflags += $(pre-post-common.flags) $(pre-post-sqlite3-wasmfs.flags)
+$(sqlite3-wasmfs.js): $(sqlite3-wasm.c) \
+ $(EXPORTED_FUNCTIONS.api) $(MAKEFILE) $(MAKEFILE.wasmfs) \
+ $(pre-post-sqlite3-wasmfs.deps)
+ @echo "Building $@ ..."
+ $(emcc.bin) -o $@ $(emcc_opt_full) $(emcc.flags) \
+ $(sqlite3-wasmfs.cflags) $(sqlite3-wasmfs.jsflags) \
+ $(sqlite3-wasm.c)
+ chmod -x $(sqlite3-wasmfs.wasm)
+ $(maybe-wasm-strip) $(sqlite3-wasmfs.wasm)
+ @ls -la $@ $(sqlite3-wasmfs.wasm)
+$(sqlite3-wasmfs.wasm): $(sqlite3-wasmfs.js)
+wasmfs: $(sqlite3-wasmfs.js)
+all: wasmfs
+
+########################################################################
+# speedtest1 for wasmfs.
+speedtest1-wasmfs.js := $(dir.wasmfs)/speedtest1-wasmfs.js
+speedtest1-wasmfs.wasm := $(subst .js,.wasm,$(speedtest1-wasmfs.js))
+speedtest1-wasmfs.eflags := $(sqlite3-wasmfs.fsflags)
+speedtest1-wasmfs.eflags += $(SQLITE_OPT) -DSQLITE_ENABLE_WASMFS
+speedtest1-wasmfs.eflags += -sALLOW_MEMORY_GROWTH=0
+speedtest1-wasmfs.eflags += -sINITIAL_MEMORY=$(emcc.INITIAL_MEMORY.128)
+$(eval $(call call-make-pre-js,speedtest1-wasmfs))
+$(speedtest1-wasmfs.js): $(speedtest1.cses) $(sqlite3-wasmfs.js) \
+ $(MAKEFILE) $(MAKEFILE.wasmfs) \
+ $(pre-post-speedtest1-wasmfs.deps) \
+ $(EXPORTED_FUNCTIONS.speedtest1)
+ @echo "Building $@ ..."
+ $(emcc.bin) \
+ $(speedtest1-wasmfs.eflags) $(speedtest1-common.eflags) \
+ $(pre-post-speedtest1-wasmfs.flags) \
+ $(speedtest1.cflags) \
+ $(sqlite3-wasmfs.cflags) \
+ -o $@ $(speedtest1.cses) -lm
+ $(maybe-wasm-strip) $(speedtest1-wasmfs.wasm)
+ ls -la $@ $(speedtest1-wasmfs.wasm)
+
+speedtest1: $(speedtest1-wasmfs.js)
+CLEAN_FILES += $(speedtest1-wasmfs.js) $(speedtest1-wasmfs.wasm) \
+ $(subst .js,.worker.js,$(speedtest1-wasmfs.js))
+# end speedtest1.js
+########################################################################