diff --git a/.dockerignore b/.dockerignore index e61ff58d2..060be0c1f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,11 +12,16 @@ scripts/ !scripts/lite/ !scripts/build-plugin-sandbox.mjs !scripts/build-pst-worker.mjs +!scripts/build-store-worker.mjs +!scripts/fetch-sqlite3mc.mjs !scripts/healthcheck.mjs !scripts/sourcemaps.mjs !scripts/standalone-guard.mjs # Built in the image by scripts/build-plugin-sandbox.mjs public/plugin-sandbox-assets/ +# Built in the image by scripts/build-store-worker.mjs +public/store-worker.js +public/sqlite3mc/ TODO.md *.md !README.md diff --git a/.gitignore b/.gitignore index 0914d4091..67e25a46d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ /public/plugin-sandbox-assets/ # scripts/build-pst-worker.mjs /public/pst-worker.js +# scripts/build-store-worker.mjs +/public/store-worker.js +/public/sqlite3mc/ # production /build diff --git a/Dockerfile b/Dockerfile index ed2713b1f..d8d5081c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,7 @@ RUN rm -rf "app/api/dev-jmap" && \ \( -name __tests__ -o -name "*.test.ts" -o -name "*.test.tsx" \) \ -prune -exec rm -rf {} \; && \ node scripts/build-pst-worker.mjs && \ + node scripts/build-store-worker.mjs && \ node scripts/build-plugin-sandbox.mjs && \ npx next build --webpack && \ node scripts/standalone-guard.mjs diff --git a/lib/store/__tests__/helpers/fake-jmap.ts b/lib/store/__tests__/helpers/fake-jmap.ts new file mode 100644 index 000000000..9aa917be5 --- /dev/null +++ b/lib/store/__tests__/helpers/fake-jmap.ts @@ -0,0 +1,285 @@ +/** + * A small JMAP server for engine tests, modelled on how Stalwart really + * behaves (local-data/desktop-offline-facts-parts/stalwart-changes-prf.md): + * a 5000 cap on /changes pages, fixed log ranges, the three kinds of state + * refusal, 500 ids per Email/get, 16 calls per request, `ids: null` giving the + * first 500. + */ + +import { TransportError, type JmapResponse, type JmapTransport, type MethodCall, type MethodResponse, type StoreSession } from '../../engine/transport'; + +export type FakeEmail = Record & { id: string }; + +export const JMAP_ACCOUNT = 'acc1'; + +export const FAKE_SESSION: StoreSession = { + apiUrl: 'https://fake.test/jmap/', + downloadUrl: 'https://fake.test/d/{accountId}/{blobId}/{name}?type={type}', + uploadUrl: 'https://fake.test/u/{accountId}', + primaryAccounts: { 'urn:ietf:params:jmap:mail': JMAP_ACCOUNT }, + accounts: { [JMAP_ACCOUNT]: { name: 'a@example.org', isPersonal: true, accountCapabilities: { 'urn:ietf:params:jmap:mail': {} } } }, + capabilities: { 'urn:ietf:params:jmap:mail': {} }, +}; + +type ChangeKind = 'created' | 'updated' | 'destroyed'; + +interface Log { kind: ChangeKind; id: string } + +class Collection { + log: Log[] = []; + /** Below this log position, states are refused as truncated. */ + truncatedBelow = 0; + + state(): string { + return `s${this.log.length}`; + } + + push(kind: ChangeKind, id: string): void { + this.log.push({ kind, id }); + } + + changes(since: string, maxChanges: number): { created: string[]; updated: string[]; destroyed: string[]; newState: string; hasMoreChanges: boolean } { + const from = Number(since.slice(1)); + const slice = this.log.slice(from, from + maxChanges); + const created = new Set(); + const updated = new Set(); + const destroyed = new Set(); + for (const { kind, id } of slice) { + if (kind === 'created') created.add(id); + else if (kind === 'updated') { + if (!created.has(id)) updated.add(id); + } else if (created.delete(id)) { + // Created and destroyed in the window: nowhere. + } else { + updated.delete(id); + destroyed.add(id); + } + } + const end = from + slice.length; + return { + created: [...created], + updated: [...updated], + destroyed: [...destroyed], + newState: `s${end}`, + hasMoreChanges: end < this.log.length, + }; + } +} + +export interface FakeOptions { + /** Ten mailboxes by default, a single Inbox here unless given. */ + mailboxes?: Array & { id: string }>; +} + +export class FakeJmap implements JmapTransport { + readonly emails = new Map(); + readonly mailboxes = new Map & { id: string }>(); + readonly threads = new Map(); + readonly coll = { Email: new Collection(), Mailbox: new Collection(), Thread: new Collection() }; + readonly calls: MethodCall[] = []; + requests = 0; + /** Set to make every request fail like a network drop. */ + offline = false; + /** Fails the next N requests as busy. */ + busyNext = 0; + /** Called before each request; can change data while a sync is running. */ + beforeRequest: ((n: number) => void) | null = null; + nextId = 1; + + constructor(options: FakeOptions = {}) { + for (const box of options.mailboxes ?? [{ id: 'inbox', name: 'Inbox', role: 'inbox', totalEmails: 0, unreadEmails: 0 }]) { + this.mailboxes.set(box.id, box); + this.coll.Mailbox.push('created', box.id); + } + } + + addEmail(partial: Partial = {}): FakeEmail { + const n = this.nextId++; + const id = partial.id ?? `e${n}`; + const threadId = (partial.threadId as string) ?? `t${n}`; + const email: FakeEmail = { + id, + threadId, + mailboxIds: { inbox: true }, + keywords: {}, + size: 100, + receivedAt: new Date(Date.UTC(2026, 9, 1) + n * 1000).toISOString(), + sentAt: new Date(Date.UTC(2026, 9, 1) + n * 1000).toISOString(), + from: [{ name: `Sender ${n}`, email: `s${n}@example.org` }], + to: [{ name: 'A', email: 'a@example.org' }], + cc: [], + subject: `Subject ${n}`, + preview: `Preview ${n}`, + hasAttachment: false, + blobId: `b${n}`, + textBody: [{ partId: '1', type: 'text/plain' }], + htmlBody: [{ partId: '1', type: 'text/plain' }], + bodyValues: { 1: { value: `Body of message ${n}`, isTruncated: false, isEncodingProblem: false } }, + attachments: [], + messageId: [`m${n}@example.org`], + headers: [], + bodyStructure: { partId: '1', type: 'text/plain' }, + ...partial, + }; + this.emails.set(id, email); + this.coll.Email.push('created', id); + const members = this.threads.get(threadId); + if (members) { + members.push(id); + this.coll.Thread.push('updated', threadId); + } else { + this.threads.set(threadId, [id]); + this.coll.Thread.push('created', threadId); + } + return email; + } + + updateEmail(id: string, patch: Partial): void { + Object.assign(this.emails.get(id) as FakeEmail, patch); + this.coll.Email.push('updated', id); + } + + destroyEmail(id: string): void { + const email = this.emails.get(id); + if (!email) return; + this.emails.delete(id); + this.coll.Email.push('destroyed', id); + const threadId = email.threadId as string; + const members = (this.threads.get(threadId) ?? []).filter((m) => m !== id); + if (members.length > 0) { + this.threads.set(threadId, members); + this.coll.Thread.push('updated', threadId); + } else { + this.threads.delete(threadId); + this.coll.Thread.push('destroyed', threadId); + } + } + + /** The next /changes from before this point gets cannotCalculateChanges. */ + truncateLog(type: 'Email' | 'Mailbox' | 'Thread'): void { + this.coll[type].truncatedBelow = this.coll[type].log.length; + } + + private queryIds(args: Record): string[] { + const filter = args.filter as { after?: string; inMailbox?: string } | undefined; + let list = [...this.emails.values()]; + if (filter?.after) list = list.filter((e) => Date.parse(String(e.receivedAt)) >= Date.parse(filter.after as string)); + if (filter?.inMailbox) list = list.filter((e) => (e.mailboxIds as Record)[filter.inMailbox as string]); + list.sort((a, b) => Date.parse(String(b.receivedAt)) - Date.parse(String(a.receivedAt))); + return list.map((e) => e.id); + } + + private handle(name: string, args: Record): Record { + switch (name) { + case 'Mailbox/get': { + const ids = args.ids as string[] | null; + const all = [...this.mailboxes.values()]; + const list = ids ? all.filter((b) => ids.includes(b.id)) : all; + return { accountId: args.accountId, state: this.coll.Mailbox.state(), list, notFound: [] }; + } + case 'Email/query': { + const ids = this.queryIds(args); + const position = Number(args.position ?? 0); + const limit = Number(args.limit ?? 5000) || 5000; + return { + accountId: args.accountId, + queryState: this.coll.Email.state(), + canCalculateChanges: true, + position, + ids: ids.slice(position, position + Math.min(limit, 5000)), + ...(args.calculateTotal ? { total: ids.length } : {}), + }; + } + case 'Email/get': { + const ids = args.ids as string[] | null; + if (ids && ids.length > 500) throw Object.assign(new Error('requestTooLarge'), { jmapError: 'requestTooLarge' }); + const chosen = ids ?? [...this.emails.keys()].slice(0, 500); + const properties = args.properties as string[] | undefined; + const list: Record[] = []; + const notFound: string[] = []; + for (const id of chosen) { + const email = this.emails.get(id); + if (!email) { + notFound.push(id); + continue; + } + if (!properties) { + list.push({ ...email }); + continue; + } + const out: Record = { id }; + for (const p of properties) if (p in email) out[p] = email[p]; + list.push(out); + } + return { accountId: args.accountId, state: this.coll.Email.state(), list, notFound }; + } + case 'Thread/get': { + const ids = (args.ids ?? []) as string[]; + return { + accountId: args.accountId, + state: this.coll.Thread.state(), + list: ids.filter((id) => this.threads.has(id)).map((id) => ({ id, emailIds: this.threads.get(id) })), + notFound: ids.filter((id) => !this.threads.has(id)), + }; + } + case 'Email/changes': + case 'Mailbox/changes': + case 'Thread/changes': { + const type = name.split('/')[0] as 'Email' | 'Mailbox' | 'Thread'; + const coll = this.coll[type]; + const since = String(args.sinceState); + if (!/^s\d+$/.test(since)) throw Object.assign(new Error('bad state'), { jmapError: 'invalidArguments' }); + const from = Number(since.slice(1)); + if (from < coll.truncatedBelow || from > coll.log.length) { + throw Object.assign(new Error('log truncated'), { jmapError: 'cannotCalculateChanges' }); + } + const maxChanges = Math.min(Number(args.maxChanges ?? 5000) || 5000, 5000); + return { accountId: args.accountId, oldState: since, ...coll.changes(since, maxChanges) }; + } + default: + throw Object.assign(new Error(name), { jmapError: 'unknownMethod' }); + } + } + + async request(calls: MethodCall[]): Promise { + this.requests += 1; + this.beforeRequest?.(this.requests); + if (this.offline) throw new TransportError('network', 'Failed to fetch'); + if (this.busyNext > 0) { + this.busyNext -= 1; + throw new TransportError('rate', 'Server busy', 429, undefined, 1); + } + if (calls.length > 16) throw new TransportError('http', 'too many calls', 400, 'urn:ietf:params:jmap:error:limit'); + const done: MethodResponse[] = []; + for (const call of calls) { + this.calls.push(call); + const [name, raw, id] = call; + const args: Record = {}; + for (const [key, value] of Object.entries(raw)) { + if (key.startsWith('#')) { + const ref = value as { resultOf: string; path: string }; + const source = done.find(([, , callId]) => callId === ref.resultOf); + args[key.slice(1)] = source?.[1][ref.path.slice(1)]; + } else { + args[key] = value; + } + } + try { + done.push([name, this.handle(name, args), id]); + } catch (error) { + const type = (error as { jmapError?: string }).jmapError; + if (!type) throw error; + done.push(['error', { type }, id]); + } + } + return { methodResponses: done }; + } + + async download(): Promise { + return new Uint8Array(); + } + + async upload(): Promise<{ blobId: string; type: string; size: number }> { + return { blobId: 'uploaded', type: 'application/octet-stream', size: 0 }; + } +} diff --git a/lib/store/__tests__/helpers/node-adapter.ts b/lib/store/__tests__/helpers/node-adapter.ts new file mode 100644 index 000000000..421324cfa --- /dev/null +++ b/lib/store/__tests__/helpers/node-adapter.ts @@ -0,0 +1,36 @@ +/** The storage adapter for tests: real SQLite with FTS5 through node:sqlite. */ + +import { DatabaseSync, type SQLInputValue } from 'node:sqlite'; +import { runTransaction, type SqlRow, type SqlValue, type StorageAdapter } from '../../storage'; + +export function openMemoryStore(): StorageAdapter { + return adapterFor(new DatabaseSync(':memory:')); +} + +export function openFileStore(file: string): StorageAdapter { + return adapterFor(new DatabaseSync(file)); +} + +function adapterFor(db: DatabaseSync): StorageAdapter { + const tx = { depth: 0 }; + const prepared = new Map>(); + const statement = (sql: string) => { + let stmt = prepared.get(sql); + if (!stmt) { + stmt = db.prepare(sql); + prepared.set(sql, stmt); + } + return stmt; + }; + return { + exec: (sql) => db.exec(sql), + run: (sql, params = []) => { + const result = statement(sql).run(...(params as SQLInputValue[])); + return { changes: Number(result.changes), lastInsertRowid: Number(result.lastInsertRowid) }; + }, + all: (sql: string, params: readonly SqlValue[] = []) => statement(sql).all(...(params as SQLInputValue[])) as T[], + get: (sql: string, params: readonly SqlValue[] = []) => statement(sql).get(...(params as SQLInputValue[])) as T | undefined, + transaction: (fn) => runTransaction(tx, (sql) => db.exec(sql), fn), + close: () => db.close(), + }; +} diff --git a/lib/store/__tests__/mail-sync.test.ts b/lib/store/__tests__/mail-sync.test.ts new file mode 100644 index 000000000..dd8b70728 --- /dev/null +++ b/lib/store/__tests__/mail-sync.test.ts @@ -0,0 +1,390 @@ +// @vitest-environment node +import { describe, expect, it } from 'vitest'; +import { mkdtempSync, rmSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { DEFAULT_SCOPE, type StoreEvent } from '../area'; +import { AccountEngine } from '../engine/account-engine'; +import { runChangesLoop } from '../engine/changes'; +import { TransportError, type MethodCall } from '../engine/transport'; +import { mailArea } from '../mail'; +import { FAKE_SESSION, FakeJmap, JMAP_ACCOUNT } from './helpers/fake-jmap'; +import { openFileStore, openMemoryStore } from './helpers/node-adapter'; + +const EVERYTHING = { ...DEFAULT_SCOPE, days: null }; +const INBOX = { inMailbox: 'inbox' }; + +function engineFor(server: FakeJmap, db = openMemoryStore(), emit?: (e: StoreEvent) => void) { + return new AccountEngine({ + account: 'a@fake.test', + db, + session: FAKE_SESSION, + transport: server, + areas: [mailArea], + scope: EVERYTHING, + emit, + now: () => Date.UTC(2026, 10, 3), + sleep: async () => {}, + }); +} + +function count(db: ReturnType, table: string): number { + return Number(db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM ${table}`)?.n); +} + +function callsNamed(server: FakeJmap, name: string): MethodCall[] { + return server.calls.filter(([n]) => n === name); +} + +describe('full sync', () => { + it('stores every message, thread and body across pages', async () => { + const server = new FakeJmap(); + for (let i = 0; i < 1234; i += 1) server.addEmail(); + const db = openMemoryStore(); + await engineFor(server, db).requestSync(); + expect(count(db, 'email')).toBe(1234); + expect(count(db, 'email_body')).toBe(1234); + expect(count(db, 'thread')).toBe(1234); + expect(count(db, 'email_mailbox')).toBe(1234); + }); + + it('captures the state before paging, so a message landing mid-sync is not lost', async () => { + const server = new FakeJmap(); + for (let i = 0; i < 600; i += 1) server.addEmail(); + let landed = false; + server.beforeRequest = (n) => { + if (n === 4 && !landed) { + landed = true; + server.addEmail({ id: 'late' }); + } + }; + const db = openMemoryStore(); + await engineFor(server, db).requestSync(); + expect(landed).toBe(true); + expect(db.get('SELECT id FROM email WHERE id = ?', ['late'])).toBeTruthy(); + }); + + it('keeps only the chosen window', async () => { + const server = new FakeJmap(); + server.addEmail({ id: 'old', receivedAt: '2020-01-01T00:00:00Z' }); + server.addEmail({ id: 'new', receivedAt: '2026-10-30T00:00:00Z' }); + const db = openMemoryStore(); + const engine = new AccountEngine({ + account: 'a@fake.test', db, session: FAKE_SESSION, transport: server, areas: [mailArea], + scope: { ...DEFAULT_SCOPE, days: 30 }, now: () => Date.UTC(2026, 10, 3), sleep: async () => {}, + }); + await engine.requestSync(); + expect(db.all('SELECT id FROM email').map((r) => r.id)).toEqual(['new']); + }); +}); + +describe('persisted state', () => { + it('survives a reload and continues from the saved state with a delta', async () => { + const dir = mkdtempSync(path.join(os.tmpdir(), 'store-')); + const file = path.join(dir, 's.db'); + try { + const server = new FakeJmap(); + for (let i = 0; i < 50; i += 1) server.addEmail(); + const first = openFileStore(file); + await engineFor(server, first).requestSync(); + first.close(); + + server.addEmail({ id: 'after-reload' }); + server.calls.length = 0; + const second = openFileStore(file); + await engineFor(server, second).requestSync(); + expect(second.get('SELECT id FROM email WHERE id = ?', ['after-reload'])).toBeTruthy(); + expect(count(second, 'email')).toBe(51); + // A delta, not another full listing. + expect(callsNamed(server, 'Email/query')).toHaveLength(0); + expect(callsNamed(server, 'Email/changes').length).toBeGreaterThan(0); + second.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('resumes an interrupted full sync from its cursor', async () => { + const dir = mkdtempSync(path.join(os.tmpdir(), 'store-')); + const file = path.join(dir, 's.db'); + try { + const server = new FakeJmap(); + for (let i = 0; i < 4500; i += 1) server.addEmail(); + const first = openFileStore(file); + // Goes down after the first page of the list. + server.beforeRequest = (n) => { + if (n === 4) server.offline = true; + }; + await engineFor(server, first).requestSync(); + const listed = count(first, 'email'); + expect(listed).toBeGreaterThan(0); + expect(listed).toBeLessThan(4500); + first.close(); + + server.offline = false; + server.beforeRequest = null; + server.calls.length = 0; + const second = openFileStore(file); + await engineFor(server, second).requestSync(); + expect(count(second, 'email')).toBe(4500); + const firstQuery = callsNamed(server, 'Email/query')[0]; + expect(firstQuery[1].position).toBe(listed); + second.close(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); + +describe('deltas', () => { + it('applies created, updated and destroyed, and leaves bodies alone on an update', async () => { + const server = new FakeJmap(); + const a = server.addEmail(); + const b = server.addEmail(); + const db = openMemoryStore(); + const engine = engineFor(server, db); + await engine.requestSync(); + + server.updateEmail(a.id, { keywords: { $seen: true } }); + server.destroyEmail(b.id); + server.addEmail({ id: 'c' }); + await engine.requestSync(); + + expect(db.all('SELECT id FROM email ORDER BY id').map((r) => r.id)).toEqual(['c', a.id]); + expect(db.get('SELECT 1 AS x FROM email_keyword WHERE keyword = ?', ['$seen'])).toBeTruthy(); + const kept = db.get<{ body_state: number }>('SELECT body_state FROM email WHERE id = ?', [a.id]); + expect(kept?.body_state).toBe(1); + // The destroyed message left nothing behind in the search index. + expect(db.all("SELECT rowid FROM email_fts_word WHERE email_fts_word MATCH ?", [`"Subject" `]).length).toBe(2); + }); + + it('pages through more changes than one call gives', async () => { + const server = new FakeJmap(); + server.addEmail(); + const db = openMemoryStore(); + const engine = engineFor(server, db); + await engine.requestSync(); + server.calls.length = 0; + for (let i = 0; i < 1500; i += 1) server.addEmail(); + await engine.requestSync(); + expect(count(db, 'email')).toBe(1501); + expect(callsNamed(server, 'Email/changes').length).toBeGreaterThanOrEqual(3); + const page = callsNamed(server, 'Email/changes')[0][1]; + expect(page.maxChanges).toBe(500); + }); + + it('sends sinceState last, because a malformed one otherwise fails the whole request on Stalwart', async () => { + const server = new FakeJmap(); + server.addEmail(); + const engine = engineFor(server); + await engine.requestSync(); + server.addEmail(); + server.calls.length = 0; + await engine.requestSync(); + const keys = Object.keys(callsNamed(server, 'Email/changes')[0][1]); + expect(keys[keys.length - 1]).toBe('sinceState'); + }); + + it('keeps going while behind the pushed state, since hasMoreChanges false does not mean caught up', async () => { + const server = new FakeJmap(); + server.addEmail(); + const applied: string[] = []; + let wrote = false; + const outcome = await runChangesLoop({ + transport: server, + type: 'Email', + jmap: JMAP_ACCOUNT, + using: [], + since: 's0', + target: 's2', + apply: async (page) => { + applied.push(page.newState); + if (!wrote) { + wrote = true; + server.addEmail(); + } + }, + }); + expect(outcome).toMatchObject({ outcome: 'caughtUp', state: 's2' }); + expect(applied).toEqual(['s1', 's2']); + }); + + it('stops when the state stands still even though the push named a newer one', async () => { + const server = new FakeJmap(); + server.addEmail(); + const outcome = await runChangesLoop({ + transport: server, type: 'Email', jmap: JMAP_ACCOUNT, using: [], since: 's1', target: 's9', + apply: async () => {}, + }); + expect(outcome.outcome).toBe('caughtUp'); + expect(server.requests).toBe(1); + }); +}); + +describe('resync', () => { + it('starts Email over on cannotCalculateChanges, keeps bodies, and drops what is gone', async () => { + const server = new FakeJmap(); + const keep = server.addEmail({ id: 'keep' }); + server.addEmail({ id: 'gone' }); + const db = openMemoryStore(); + const engine = engineFor(server, db); + await engine.requestSync(); + + server.destroyEmail('gone'); + server.updateEmail(keep.id, { keywords: { $flagged: true } }); + server.truncateLog('Email'); + server.calls.length = 0; + await engine.requestSync(); + + expect(callsNamed(server, 'Email/query').length).toBeGreaterThan(0); + expect(db.all('SELECT id FROM email').map((r) => r.id)).toEqual(['keep']); + expect(db.get('SELECT 1 AS x FROM email_keyword WHERE keyword = ?', ['$flagged'])).toBeTruthy(); + // The body wasn't fetched a second time. + const bodyFetches = callsNamed(server, 'Email/get').filter(([, a]) => a.fetchTextBodyValues); + expect(bodyFetches).toHaveLength(0); + expect(count(db, 'email_body')).toBe(1); + }); + + it.each([ + ['invalidArguments', { jmapError: 'invalidArguments' }], + ['cannotCalculateChanges', { jmapError: 'cannotCalculateChanges' }], + ])('treats %s as a reason to start over', async (_name, thrown) => { + const server = new FakeJmap(); + const original = server.request.bind(server); + let refused = false; + server.request = async (calls) => { + if (!refused && calls[0][0] === 'Email/changes') { + refused = true; + return { methodResponses: [['error', { type: thrown.jmapError }, 'c']] }; + } + return original(calls); + }; + const outcome = await runChangesLoop({ transport: server, type: 'Email', jmap: JMAP_ACCOUNT, using: [], since: 's0', apply: async () => {} }); + expect(outcome.outcome).toBe('resync'); + }); + + it('treats an HTTP 400 notRequest as a reason to start over, not as an outage', async () => { + const server = new FakeJmap(); + server.request = async () => { + throw new TransportError('http', 'bad', 400, 'urn:ietf:params:jmap:error:notRequest'); + }; + const outcome = await runChangesLoop({ transport: server, type: 'Email', jmap: JMAP_ACCOUNT, using: [], since: 's0', apply: async () => {} }); + expect(outcome.outcome).toBe('resync'); + }); + + it('never sends a stored state a server cannot have sent', async () => { + const server = new FakeJmap(); + const outcome = await runChangesLoop({ transport: server, type: 'Email', jmap: JMAP_ACCOUNT, using: [], since: 'bad state\n', apply: async () => {} }); + expect(outcome.outcome).toBe('resync'); + expect(server.requests).toBe(0); + }); +}); + +describe('when the server is busy or gone', () => { + it('backs off and carries on after a 429', async () => { + const server = new FakeJmap(); + server.addEmail(); + server.busyNext = 2; + const db = openMemoryStore(); + await engineFor(server, db).requestSync(); + expect(count(db, 'email')).toBe(1); + }); + + it('ends the run quietly when the network is gone, and picks up when back', async () => { + const server = new FakeJmap(); + server.addEmail(); + server.offline = true; + const events: StoreEvent[] = []; + const db = openMemoryStore(); + const engine = engineFor(server, db, (e) => events.push(e)); + await engine.requestSync(); + expect(count(db, 'email')).toBe(0); + expect(events.some((e) => e.type === 'error')).toBe(false); + server.offline = false; + await engine.requestSync(); + expect(count(db, 'email')).toBe(1); + }); + + it('tells the page about new mail only for messages it had not seen', async () => { + const server = new FakeJmap(); + server.addEmail(); + const events: StoreEvent[] = []; + const engine = engineFor(server, openMemoryStore(), (e) => events.push(e)); + await engine.requestSync(); + expect(events.filter((e) => e.type === 'created')).toHaveLength(0); + server.addEmail({ id: 'fresh' }); + await engine.requestSync(); + expect(events.filter((e) => e.type === 'created')).toEqual([ + { type: 'created', account: 'a@fake.test', jmap: JMAP_ACCOUNT, collection: 'Email', ids: ['fresh'] }, + ]); + }); +}); + +describe('answers from the store', () => { + async function seeded() { + const server = new FakeJmap(); + server.addEmail({ id: 'one', subject: 'Zahlungserinnerung Rechnungsbetrag', keywords: { $seen: true } }); + server.addEmail({ id: 'two', subject: 'Hello', mailboxIds: { inbox: true }, bodyValues: { 1: { value: 'See you Tuesday' } } }); + const db = openMemoryStore(); + const engine = engineFor(server, db); + await engine.requestSync(); + return { engine, db, server }; + } + + it('gives every call its own answer, in order, with a per-call error for what it cannot answer', async () => { + const { engine } = await seeded(); + const answer = await engine.answer([ + ['Email/query', { filter: INBOX, sort: [{ property: 'receivedAt', isAscending: false }], calculateTotal: true }, 'a'], + ['SieveScript/get', {}, 'b'], + ['Email/get', { '#ids': { resultOf: 'a', name: 'Email/query', path: '/ids' }, properties: ['id', 'subject'] }, 'c'], + ]); + expect(answer?.methodResponses.map(([name, , id]) => [name, id])).toEqual([ + ['Email/query', 'a'], ['error', 'b'], ['Email/get', 'c'], + ]); + expect(answer?.methodResponses[0][1]).toMatchObject({ ids: ['two', 'one'], total: 2, position: 0 }); + expect(answer?.methodResponses[1][1].type).toBe('serverUnavailable'); + expect(answer?.methodResponses[2][1].list).toEqual([ + { id: 'two', subject: 'Hello' }, + { id: 'one', subject: 'Zahlungserinnerung Rechnungsbetrag' }, + ]); + }); + + it('answers null when nothing in the request could be answered', async () => { + const { engine } = await seeded(); + expect(await engine.answer([['SieveScript/get', {}, 'a']])).toBeNull(); + }); + + it('finds a German compound from inside the word, and a word start', async () => { + const { engine } = await seeded(); + const search = async (text: string) => (await engine.answer([['Email/query', { filter: { text } }, 'q']]))?.methodResponses[0][1].ids; + expect(await search('rechnung')).toEqual(['one']); + expect(await search('erinnerung')).toEqual(['one']); + expect(await search('zahl*')).toEqual(['one']); + expect(await search('tuesday')).toEqual(['two']); + expect(await search('nichtda')).toEqual([]); + }); + + it('reads keyword sort the way the client sends it for that server', async () => { + const { engine } = await seeded(); + const sorted = async () => (await engine.answer([['Email/query', { + filter: INBOX, + sort: [{ property: 'hasKeyword', keyword: '$seen', isAscending: false }], + }, 'q']]))?.methodResponses[0][1].ids; + expect(await sorted()).toEqual(['one', 'two']); + engine.setInvertedKeywordSort(true); + expect(await sorted()).toEqual(['two', 'one']); + }); + + it('does not make up a message with empty text when the body is not kept', async () => { + const { engine, db } = await seeded(); + db.run('DELETE FROM email_body'); + const answer = await engine.answer([['Email/get', { ids: ['one'], fetchTextBodyValues: true }, 'g']]); + expect(answer?.methodResponses[0][1]).toMatchObject({ list: [], notFound: ['one'] }); + }); + + it('answers with the real server state, never a made-up one', async () => { + const { engine, server } = await seeded(); + const answer = await engine.answer([['Email/get', { ids: ['one'], properties: ['id'] }, 'g']]); + expect(answer?.methodResponses[0][1].state).toBe(server.coll.Email.state()); + }); +}); diff --git a/lib/store/adapters/wasm.ts b/lib/store/adapters/wasm.ts new file mode 100644 index 000000000..e85bf14f4 --- /dev/null +++ b/lib/store/adapters/wasm.ts @@ -0,0 +1,197 @@ +/** + * SQLite3MultipleCiphers on OPFS, for the owner tab's worker. + * + * Rules that come from bugs reproduced in 2.6.1, see + * local-data/desktop-offline-facts-parts/sqlite-wasm.md section 5: + * - PRAGMA key is the very first thing on a connection. Reading first deletes + * a hot journal (#292). + * - The page size must be 8192. On 4096, rollback doesn't work (#293). + * - No PRAGMA rekey (#291, #294). The key is random and only ever re-wrapped. + * - Always the multipleciphers VFS. On plain opfs-sahpool, PRAGMA key throws + * and the connection still writes in the clear if that's ignored. + */ + +import { runTransaction, type SqlRow, type SqlValue, type StorageAdapter } from '../storage'; + +const VFS = 'multipleciphers-opfs-sahpool'; +const INIT_TIMEOUT_MS = 30_000; +const REQUIRED_PAGE_SIZE = 8192; +const POOL_DIRECTORY = '.bulwark-store'; +const INITIAL_CAPACITY = 16; +/** A database and its journal, plus room for one more. */ +const SLOTS_PER_DATABASE = 3; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface Pool { + getFileNames(): string[]; + getCapacity(): number; + getFileCount(): number; + addCapacity(n: number): Promise; + unlink(name: string): boolean; + wipeFiles(): Promise; + pauseVfs(): void; + unpauseVfs(): Promise; + isPaused(): boolean; +} + +export class StoreKeyError extends Error { + constructor() { + super('The local store could not be opened with this key'); + this.name = 'StoreKeyError'; + } +} + +function withTimeout(promise: Promise, ms: number, what: string): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`${what} timed out`)), ms); + promise.then((value) => { + clearTimeout(timer); + resolve(value); + }, (error) => { + clearTimeout(timer); + reject(error); + }); + }); +} + +class WasmAdapter implements StorageAdapter { + private readonly tx = { depth: 0 }; + private readonly statements = new Map(); + + constructor(private readonly sqlite3: any, private readonly db: any) {} + + exec(sql: string): void { + this.db.exec(sql); + } + + private statement(sql: string): any { + let stmt = this.statements.get(sql); + if (!stmt) { + stmt = this.db.prepare(sql); + this.statements.set(sql, stmt); + } + return stmt; + } + + private bindAndRun(sql: string, params: readonly SqlValue[] | undefined, read: (stmt: any) => T): T { + const stmt = this.statement(sql); + try { + if (params && params.length > 0) stmt.bind(params as SqlValue[]); + return read(stmt); + } finally { + stmt.reset(true); + } + } + + run(sql: string, params?: readonly SqlValue[]): { changes: number; lastInsertRowid: number } { + this.bindAndRun(sql, params, (stmt) => { + while (stmt.step()) { /* no rows wanted */ } + }); + return { + changes: Number(this.db.changes()), + lastInsertRowid: Number(this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer)), + }; + } + + all(sql: string, params?: readonly SqlValue[]): T[] { + return this.bindAndRun(sql, params, (stmt) => { + const rows: T[] = []; + while (stmt.step()) rows.push(stmt.get({}) as T); + return rows; + }); + } + + get(sql: string, params?: readonly SqlValue[]): T | undefined { + return this.bindAndRun(sql, params, (stmt) => (stmt.step() ? stmt.get({}) as T : undefined)); + } + + transaction(fn: () => T): T { + return runTransaction(this.tx, (sql) => this.db.exec(sql), fn); + } + + close(): void { + for (const stmt of this.statements.values()) stmt.finalize(); + this.statements.clear(); + this.db.close(); + } +} + +/** The module and the pool, once. Only the owner gets here. */ +export class WasmStorage { + private constructor(private readonly sqlite3: any, private readonly pool: Pool) {} + + /** + * Call only while holding the owner lock. The holder before may still be + * letting go, so a refusal is retried for a while. + */ + static async install(moduleUrl: string): Promise { + const { default: init } = await import(/* webpackIgnore: true */ moduleUrl) as { default: (o: unknown) => Promise }; + // A failed wasm load never settles the promise from the module's init. + const sqlite3 = await withTimeout(init({ print() {}, printErr() {} }), INIT_TIMEOUT_MS, 'Loading the store engine'); + let lastError: unknown; + for (let attempt = 0; attempt < 20; attempt += 1) { + try { + const pool = await sqlite3.installOpfsSAHPoolVfs({ + name: 'opfs-sahpool', + directory: POOL_DIRECTORY, + initialCapacity: INITIAL_CAPACITY, + preserveOnInitFailure: true, + forceReinitIfPreviouslyFailed: true, + }) as Pool; + return new WasmStorage(sqlite3, pool); + } catch (error) { + lastError = error; + await new Promise((resolve) => setTimeout(resolve, 100 + attempt * 50)); + } + } + throw lastError; + } + + /** Opens one account's database. `keyHex` is 32 random bytes in hex. */ + async open(file: string, keyHex: string): Promise { + if (!/^\/a[0-9a-f]+\.db$/.test(file)) throw new Error('Store file names carry no account data'); + if (!/^[0-9a-f]{64}$/.test(keyHex)) throw new Error('A store key is 32 bytes of hex'); + const needed = this.pool.getFileCount() + SLOTS_PER_DATABASE; + if (this.pool.getCapacity() < needed) await this.pool.addCapacity(needed - this.pool.getCapacity()); + const db = new this.sqlite3.oo1.DB({ filename: file, vfs: VFS, flags: 'c' }); + try { + db.exec(`PRAGMA key = "raw:${keyHex}"`); + try { + db.selectValue('SELECT count(*) FROM sqlite_master'); + } catch { + throw new StoreKeyError(); + } + const pageSize = Number(db.selectValue('PRAGMA page_size')); + if (pageSize !== REQUIRED_PAGE_SIZE) throw new Error(`Refusing an unsafe page size: ${pageSize}`); + db.exec('PRAGMA temp_store = MEMORY'); + db.exec('PRAGMA foreign_keys = OFF'); + db.exec('PRAGMA synchronous = NORMAL'); + } catch (error) { + db.close(); + throw error; + } + return new WasmAdapter(this.sqlite3, db); + } + + remove(file: string): boolean { + let removed = false; + for (const name of this.pool.getFileNames()) { + if (name === file || name === `${file}-journal`) removed = this.pool.unlink(name) || removed; + } + return removed; + } + + /** Lets go of the OPFS handles. Every database must be closed first. */ + pause(): void { + this.pool.pauseVfs(); + } + + async unpause(): Promise { + await this.pool.unpauseVfs(); + } + + files(): string[] { + return this.pool.getFileNames(); + } +} diff --git a/lib/store/area.ts b/lib/store/area.ts new file mode 100644 index 000000000..fa757e37d --- /dev/null +++ b/lib/store/area.ts @@ -0,0 +1,101 @@ +/** + * What one part of the local store (mail, calendar, contacts, files, ...) + * plugs into the engine: its tables, the JMAP methods it answers while the + * server can't be reached, and how it syncs. + * + * Migration id ranges: core 1-99, mail 100-199, outbox 200-299, search + * 300-399, drafts 400-499, calendar 500-599, contacts 600-699, files 700-799, + * settings and reminders 800-899, wipe and lock 900-999. + */ + +import type { JmapTransport, MethodCall, StoreSession } from './engine/transport'; +import type { Migration, StorageAdapter } from './storage'; + +export interface StoreScope { + /** How far back mail is kept, in days. Null keeps everything. */ + days: number | null; + /** Whether attachments are kept. */ + attachments: boolean; + /** The largest attachment kept, and the total for all of them. */ + maxAttachmentBytes: number; + maxTotalBytes: number; +} + +export const DEFAULT_SCOPE: StoreScope = { + days: 90, + attachments: true, + maxAttachmentBytes: 25 * 1024 * 1024, + maxTotalBytes: 2 * 1024 * 1024 * 1024, +}; + +export interface SyncStateRow { + state: string | null; + /** The newest state the server told us about, to know when a run is caught up. */ + target: string | null; + /** The area's own phase name; null when in normal delta mode. */ + phase: string | null; + /** The area's own resume point. */ + cursor: Record | null; +} + +export type StoreEvent = + | { type: 'progress'; account: string; area: string; phase: string; done: number; total: number | null } + | { type: 'synced'; account: string; at: number } + | { type: 'changed'; account: string; collections: string[] } + | { type: 'created'; account: string; jmap: string; collection: string; ids: string[] } + | { type: 'error'; account: string; area: string; message: string }; + +export interface StoreContext { + account: string; + db: StorageAdapter; + session: StoreSession; + scope: StoreScope; + now(): number; + emit(event: StoreEvent): void; +} + +export interface SyncContext extends StoreContext { + transport: JmapTransport; + /** True once the engine is closing or lost the network: stop at the next batch. */ + stopped(): boolean; + syncState(jmap: string, type: string): SyncStateRow; + /** Call inside the transaction that writes the data, so the two can't disagree. */ + setSyncState(jmap: string, type: string, row: Partial): void; +} + +export interface Syncer { + /** A short name for progress and errors. */ + name: string; + /** The JMAP capability an account must advertise for this to run for it. */ + capability: string; + /** Runs one pass for one JMAP account. Must be safe to stop between batches and start again. */ + run(ctx: SyncContext, jmap: string): Promise; +} + +export type MethodResult = Record; + +/** Thrown by a method handler: the client gets a per-call JMAP error. */ +export class MethodError extends Error { + constructor(readonly type: string, readonly description?: string) { + super(description ?? type); + this.name = 'MethodError'; + } +} + +export interface MethodContext extends StoreContext { + /** The account's own default JMAP account for calls that don't say. */ + defaultJmap: string; + /** Whether keyword sort comparators arrive inverted from this client. */ + invertedKeywordSort: boolean; + /** Earlier responses in this request, for creation-id references. */ + createdIds: Map; +} + +export type MethodHandler = (ctx: MethodContext, args: Record, call: MethodCall) => MethodResult | Promise; + +export interface StoreArea { + name: string; + migrations: readonly Migration[]; + methods: Readonly>; + syncers: readonly Syncer[]; +} diff --git a/lib/store/areas.ts b/lib/store/areas.ts new file mode 100644 index 000000000..6bff665ca --- /dev/null +++ b/lib/store/areas.ts @@ -0,0 +1,6 @@ +/** Every part of the local store. New areas go here. */ + +import type { StoreArea } from './area'; +import { mailArea } from './mail'; + +export const STORE_AREAS: readonly StoreArea[] = [mailArea]; diff --git a/lib/store/client/keys.ts b/lib/store/client/keys.ts new file mode 100644 index 000000000..ecd4fe4c2 --- /dev/null +++ b/lib/store/client/keys.ts @@ -0,0 +1,173 @@ +/** + * The per-account database keys, as the page sees them. + * + * Each account's database has a random 32-byte key. It is only ever stored + * wrapped: AES-GCM with a non-extractable key that lives in IndexedDB, then + * any extra layers on top (the desktop app's OS keychain, app lock). Forgetting + * the record makes the file unreadable even if deleting it failed. + */ + +const DB_NAME = 'bulwark-store'; +const DB_VERSION = 1; +const KEYS = 'keys'; +const ACCOUNTS = 'accounts'; +const WRAP_ID = 'wrap'; + +export interface WrapLayer { + /** A name for the layer: 'safeStorage', 'pin', 'prf'. */ + name: string; + wrap(data: Uint8Array): Promise; + unwrap(data: Uint8Array): Promise; +} + +export interface AccountRecord { + account: string; + /** `/a.db`. */ + file: string; + iv: Uint8Array; + wrapped: Uint8Array; + /** Layers applied after the WebCrypto wrap, innermost first. */ + layers: string[]; + createdAt: number; +} + +function promisify(request: IDBRequest): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +function openDb(): Promise { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(KEYS)) db.createObjectStore(KEYS, { keyPath: 'id' }); + if (!db.objectStoreNames.contains(ACCOUNTS)) db.createObjectStore(ACCOUNTS, { keyPath: 'account' }); + }; + return promisify(request); +} + +async function withDb(fn: (db: IDBDatabase) => Promise): Promise { + const db = await openDb(); + try { + return await fn(db); + } finally { + db.close(); + } +} + +async function wrapKey(db: IDBDatabase): Promise { + const read = db.transaction(KEYS, 'readonly').objectStore(KEYS).get(WRAP_ID); + const existing = await promisify(read) as { key: CryptoKey } | undefined; + if (existing) return existing.key; + const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']); + const tx = db.transaction(KEYS, 'readwrite'); + const store = tx.objectStore(KEYS); + // Another tab may have got there first: keep theirs. + const raced = await promisify(store.get(WRAP_ID)) as { key: CryptoKey } | undefined; + if (raced) return raced.key; + await promisify(store.put({ id: WRAP_ID, key })); + return key; +} + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); +} + +function asBuffer(bytes: Uint8Array): ArrayBuffer { + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; +} + +export function newStoreFileName(): string { + return `/a${toHex(crypto.getRandomValues(new Uint8Array(8)))}.db`; +} + +export async function getAccountRecord(account: string): Promise { + return withDb(async (db) => { + const found = await promisify(db.transaction(ACCOUNTS, 'readonly').objectStore(ACCOUNTS).get(account)) as AccountRecord | undefined; + return found ?? null; + }); +} + +export async function listAccountRecords(): Promise { + return withDb((db) => promisify(db.transaction(ACCOUNTS, 'readonly').objectStore(ACCOUNTS).getAll()) as Promise); +} + +/** The account's key in hex, created on first use. Returns the record too, for the file name. */ +export async function openAccountKey(account: string, layers: readonly WrapLayer[] = []): Promise<{ keyHex: string; record: AccountRecord }> { + return withDb(async (db) => { + const wrapping = await wrapKey(db); + const found = await promisify(db.transaction(ACCOUNTS, 'readonly').objectStore(ACCOUNTS).get(account)) as AccountRecord | undefined; + if (found) { + let data = found.wrapped; + for (const name of [...found.layers].reverse()) { + const layer = layers.find((candidate) => candidate.name === name); + if (!layer) throw new StoreKeyLayerMissingError(name); + data = await layer.unwrap(data); + } + const raw = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: asBuffer(found.iv) }, wrapping, asBuffer(data))); + const keyHex = toHex(raw); + raw.fill(0); + return { keyHex, record: found }; + } + const raw = crypto.getRandomValues(new Uint8Array(32)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + let wrapped = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, wrapping, raw)); + for (const layer of layers) wrapped = new Uint8Array(await layer.wrap(wrapped)); + const record: AccountRecord = { + account, + file: newStoreFileName(), + iv, + wrapped, + layers: layers.map((layer) => layer.name), + createdAt: Date.now(), + }; + await promisify(db.transaction(ACCOUNTS, 'readwrite').objectStore(ACCOUNTS).put(record)); + const keyHex = toHex(raw); + raw.fill(0); + return { keyHex, record }; + }); +} + +/** Adds or removes wrap layers without touching the database key. */ +export async function rewrapAccountKey( + account: string, + from: readonly WrapLayer[], + to: readonly WrapLayer[], +): Promise { + await withDb(async (db) => { + const found = await promisify(db.transaction(ACCOUNTS, 'readonly').objectStore(ACCOUNTS).get(account)) as AccountRecord | undefined; + if (!found) return; + let data = found.wrapped; + for (const name of [...found.layers].reverse()) { + const layer = from.find((candidate) => candidate.name === name); + if (!layer) throw new StoreKeyLayerMissingError(name); + data = await layer.unwrap(data); + } + for (const layer of to) data = new Uint8Array(await layer.wrap(data)); + const record: AccountRecord = { ...found, wrapped: data, layers: to.map((layer) => layer.name) }; + await promisify(db.transaction(ACCOUNTS, 'readwrite').objectStore(ACCOUNTS).put(record)); + }); +} + +/** Forgets the account's key. The database file can't be read after this. */ +export async function forgetAccountKey(account: string): Promise { + await withDb((db) => promisify(db.transaction(ACCOUNTS, 'readwrite').objectStore(ACCOUNTS).delete(account)).then(() => undefined)); +} + +export class StoreKeyLayerMissingError extends Error { + constructor(readonly layer: string) { + super(`The key needs the ${layer} layer`); + this.name = 'StoreKeyLayerMissingError'; + } +} + +/** Deletes every key at sign-out. Works with no worker running, on any page. */ +export function deleteKeyDatabase(): void { + try { + indexedDB.deleteDatabase(DB_NAME); + } catch { + // Private mode or similar: nothing was stored either. + } +} diff --git a/lib/store/client/store-client.ts b/lib/store/client/store-client.ts new file mode 100644 index 000000000..2b47961a6 --- /dev/null +++ b/lib/store/client/store-client.ts @@ -0,0 +1,261 @@ +/** + * The page's side of the local store. Each tab starts its own worker, which + * waits for the owner lock. Calls go over a BroadcastChannel to whichever + * worker owns the store, including this tab's own, so a tab doesn't need to + * know whether it's the owner. A key never goes over the channel. + */ + +import type { StoreEvent, StoreScope } from '../area'; +import type { JmapResponse, MethodCall, StoreSession } from '../engine/transport'; +import { SQLITE3MC_PUBLIC_DIR } from '../sqlite-version'; +import { + RPC_CHANNEL, + type ChannelMessage, + type OpenAccountParams, + type PageToWorker, + type RpcReply, + type StoreRpc, + type StoreStatus, + type WorkerToPage, +} from '../worker/protocol'; + +const CALL_TIMEOUT_MS = 60_000; +const OWNER_WAIT_MS = 15_000; + +export class StoreOwnerGoneError extends Error { + constructor() { + super('The local store is not available in this browser window'); + this.name = 'StoreOwnerGoneError'; + } +} + +interface Pending { + resolve(value: unknown): void; + reject(error: Error): void; + timer: ReturnType; +} + +export interface StoreClientOptions { + /** NEXT_PUBLIC_BASE_PATH plus the build id, passed in so this works in tests. */ + basePath?: string; + buildId?: string; + onEvent?(event: StoreEvent): void; + /** The tab became the owner: open its accounts. */ + onOwner?(): void; + /** The worker asks for a fresh Authorization header. */ + onNeedAuth?(account: string): void; +} + +export class StoreClient { + readonly clientId = crypto.randomUUID(); + private worker: Worker | null = null; + private channel: BroadcastChannel | null = null; + private owner: string | null = null; + private isOwner = false; + private readonly pending = new Map(); + private readonly ownerWaiters: Array<() => void> = []; + + constructor(private readonly options: StoreClientOptions = {}) {} + + static supported(): boolean { + return typeof Worker === 'function' + && typeof BroadcastChannel === 'function' + && typeof navigator !== 'undefined' + && Boolean(navigator.locks) + && typeof navigator.storage?.getDirectory === 'function' + && typeof FileSystemFileHandle !== 'undefined' + && 'createSyncAccessHandle' in FileSystemFileHandle.prototype; + } + + start(): void { + if (this.worker) return; + const base = this.options.basePath ?? ''; + const query = this.options.buildId ? `?v=${encodeURIComponent(this.options.buildId)}` : ''; + this.channel = new BroadcastChannel(RPC_CHANNEL); + this.channel.onmessage = (event: MessageEvent) => this.onChannel(event.data); + this.worker = new Worker(`${base}/store-worker.js${query}`, { type: 'module' }); + this.worker.onmessage = (event: MessageEvent) => this.onWorker(event.data); + this.post({ + kind: 'init', + clientId: this.clientId, + moduleUrl: new URL(`${base}/${SQLITE3MC_PUBLIC_DIR}/sqlite3.mjs`, location.origin).href, + }); + this.bindLifecycle(); + } + + private bindLifecycle(): void { + // A clean handover when the tab goes. If this doesn't get to run, the lock + // and the handles die with the worker and the next owner retries. + addEventListener('pagehide', (event) => { + if (!event.persisted) void this.release().catch(() => {}); + }); + } + + private post(message: PageToWorker): void { + this.worker?.postMessage(message); + } + + private onWorker(message: WorkerToPage): void { + switch (message.kind) { + case 'owner': + this.isOwner = true; + this.owner = this.clientId; + this.flushOwnerWaiters(); + this.options.onOwner?.(); + break; + case 'result': { + const pending = this.pending.get(message.id); + if (!pending) return; + this.pending.delete(message.id); + clearTimeout(pending.timer); + if (message.error) { + const error = new Error(message.error); + if (message.errorName) error.name = message.errorName; + pending.reject(error); + } else { + pending.resolve(message.result); + } + break; + } + case 'need-auth': + this.options.onNeedAuth?.(message.account); + break; + case 'event': + // The owner's events also reach every other tab over the channel. + this.options.onEvent?.(message.event); + break; + } + } + + private onChannel(message: ChannelMessage): void { + switch (message.kind) { + case 'reply': + if ((message as RpcReply).to === this.clientId) this.settle(message); + break; + case 'event': + if (!this.isOwner) this.options.onEvent?.(message.event); + break; + case 'owner-changed': + this.owner = message.owner; + if (message.owner) this.flushOwnerWaiters(); + break; + default: + break; + } + } + + private settle(message: RpcReply): void { + const pending = this.pending.get(message.id); + if (!pending) return; + this.pending.delete(message.id); + clearTimeout(pending.timer); + if (message.error) pending.reject(new Error(message.error)); + else pending.resolve(message.result); + } + + private flushOwnerWaiters(): void { + for (const waiter of this.ownerWaiters.splice(0)) waiter(); + } + + private track(id: string, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new StoreOwnerGoneError()); + }, timeoutMs); + this.pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timer }); + }); + } + + private async waitForOwner(): Promise { + if (this.owner) return; + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new StoreOwnerGoneError()), OWNER_WAIT_MS); + this.ownerWaiters.push(() => { + clearTimeout(timer); + resolve(); + }); + }); + } + + private async rpc(method: K, params: Parameters[0]): Promise> { + await this.waitForOwner(); + const id = crypto.randomUUID(); + const result = this.track>(id, CALL_TIMEOUT_MS); + this.channel?.postMessage({ kind: 'rpc', id, from: this.clientId, method, params } as ChannelMessage); + return result; + } + + private direct(message: PageToWorker & { id: string }): Promise { + const result = this.track(message.id, CALL_TIMEOUT_MS); + this.post(message); + return result; + } + + // ---- Owner only: these carry keys or manage files ---- + + get ownsStore(): boolean { + return this.isOwner; + } + + openAccount(params: OpenAccountParams): Promise { + return this.direct({ kind: 'open', id: crypto.randomUUID(), params }); + } + + closeAccount(account: string): Promise { + return this.direct({ kind: 'close-account', id: crypto.randomUUID(), account }); + } + + removeAccount(account: string, file: string): Promise { + return this.direct({ kind: 'remove-account', id: crypto.randomUUID(), account, file }); + } + + setAuth(account: string, auth: string | null): void { + this.post({ kind: 'set-auth', account, auth }); + } + + setSession(account: string, session: StoreSession): void { + this.post({ kind: 'set-session', account, session }); + } + + setScope(account: string, scope: StoreScope): void { + this.post({ kind: 'set-scope', account, scope }); + } + + /** Hands the store over: the worker closes everything, then lets go of the lock. */ + async release(): Promise { + if (!this.worker) return; + const worker = this.worker; + if (this.isOwner) { + await this.direct({ kind: 'release', id: crypto.randomUUID() }); + this.isOwner = false; + this.owner = null; + } + worker.terminate(); + this.worker = null; + this.channel?.close(); + this.channel = null; + } + + // ---- Any tab ---- + + answer(account: string, calls: MethodCall[]): Promise { + return this.rpc('answer', { account, calls }); + } + + sync(account: string): Promise { + return this.rpc('sync', { account }); + } + + noteStateChange(account: string, changed: Record>): Promise { + return this.rpc('noteStateChange', { account, changed }); + } + + setOnline(online: boolean): Promise { + return this.rpc('setOnline', { online }); + } + + status(account: string): Promise { + return this.rpc('status', { account }); + } +} diff --git a/lib/store/engine/account-engine.ts b/lib/store/engine/account-engine.ts new file mode 100644 index 000000000..3b460e3a0 --- /dev/null +++ b/lib/store/engine/account-engine.ts @@ -0,0 +1,223 @@ +/** + * One local account in the store: its database, its sync runs, its answers. + * It acts as a JMAP client toward the real server and as a JMAP server toward + * the UI. Nothing here touches the DOM, so tests run it on node:sqlite. + */ + +import { MethodError, type MethodHandler, type StoreArea, type StoreEvent, type StoreScope, type SyncContext, type SyncStateRow, type Syncer } from '../area'; +import { migrate, type StorageAdapter } from '../storage'; +import { answerRequest, buildMethodTable } from './responder'; +import { TransportError, type JmapResponse, type JmapTransport, type MethodCall, type StoreSession } from './transport'; +import { CORE_MIGRATIONS, metaGet, metaSet } from './core-schema'; + +export interface AccountEngineOptions { + account: string; + db: StorageAdapter; + session: StoreSession; + transport: JmapTransport; + areas: readonly StoreArea[]; + scope: StoreScope; + invertedKeywordSort?: boolean; + emit?: (event: StoreEvent) => void; + now?: () => number; + /** Waits before a retry after the server said it's busy. Tests pass a stub. */ + sleep?: (ms: number) => Promise; +} + +const RATE_RETRIES = 5; +const RATE_BACKOFF_MS = 2_000; + +export class AccountEngine { + readonly account: string; + private readonly db: StorageAdapter; + private session: StoreSession; + private scope: StoreScope; + private readonly transport: JmapTransport; + private readonly syncers: readonly Syncer[]; + private readonly methods: ReadonlyMap; + private readonly emitter: (event: StoreEvent) => void; + private readonly now: () => number; + private readonly sleep: (ms: number) => Promise; + private invertedKeywordSort: boolean; + private closed = false; + private online = true; + private running: Promise | null = null; + private again = false; + + constructor(options: AccountEngineOptions) { + this.account = options.account; + this.db = options.db; + this.session = options.session; + this.scope = options.scope; + this.transport = options.transport; + this.syncers = options.areas.flatMap((area) => area.syncers); + this.methods = buildMethodTable(options.areas); + this.emitter = options.emit ?? (() => {}); + this.now = options.now ?? Date.now; + this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))); + this.invertedKeywordSort = options.invertedKeywordSort ?? false; + migrate(this.db, [...CORE_MIGRATIONS, ...options.areas.flatMap((area) => area.migrations)]); + metaSet(this.db, 'session', JSON.stringify(options.session)); + } + + /** The session kept from the last time, for a start without a network. */ + static storedSession(db: StorageAdapter): StoreSession | null { + migrate(db, CORE_MIGRATIONS); + const raw = metaGet(db, 'session'); + return raw ? JSON.parse(raw) as StoreSession : null; + } + + setSession(session: StoreSession): void { + this.session = session; + metaSet(this.db, 'session', JSON.stringify(session)); + } + + setScope(scope: StoreScope): void { + this.scope = scope; + metaSet(this.db, 'scope', JSON.stringify(scope)); + } + + setInvertedKeywordSort(inverted: boolean): void { + this.invertedKeywordSort = inverted; + } + + setOnline(online: boolean): void { + this.online = online; + if (online) this.requestSync(); + } + + private syncState(jmap: string, type: string): SyncStateRow { + const row = this.db.get<{ state: string | null; target: string | null; phase: string | null; cursor: string | null }>( + 'SELECT state, target, phase, cursor FROM sync_state WHERE jmap = ? AND type = ?', + [jmap, type], + ); + return { + state: row?.state ?? null, + target: row?.target ?? null, + phase: row?.phase ?? null, + cursor: row?.cursor ? JSON.parse(row.cursor) as Record : null, + }; + } + + private setSyncState(jmap: string, type: string, patch: Partial): void { + const row = { ...this.syncState(jmap, type), ...patch }; + this.db.run( + `INSERT INTO sync_state (jmap, type, state, target, phase, cursor, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (jmap, type) DO UPDATE SET state = excluded.state, target = excluded.target, phase = excluded.phase, + cursor = excluded.cursor, updated_at = excluded.updated_at`, + [jmap, type, row.state, row.target, row.phase, row.cursor ? JSON.stringify(row.cursor) : null, this.now()], + ); + } + + /** What the server pushed: jmap account id to { type: state }. */ + noteStateChange(changed: Record>): void { + this.db.transaction(() => { + for (const [jmap, types] of Object.entries(changed)) { + for (const [type, state] of Object.entries(types)) this.setSyncState(jmap, type, { target: state }); + } + }); + this.requestSync(); + } + + /** Starts a run, or asks the running one to go again when it's done. */ + requestSync(): Promise { + if (this.closed) return Promise.resolve(); + if (this.running) { + this.again = true; + return this.running; + } + this.running = this.runLoop().finally(() => { + this.running = null; + }); + return this.running; + } + + private async runLoop(): Promise { + do { + this.again = false; + await this.runOnce(); + } while (this.again && !this.closed && this.online); + } + + private async runOnce(): Promise { + if (!this.online) return; + const ctx = this.syncContext(); + const jmapAccounts = Object.keys(this.session.accounts); + for (const syncer of this.syncers) { + for (const jmap of jmapAccounts) { + const capabilities = this.session.accounts[jmap].accountCapabilities ?? this.session.capabilities; + if (!(syncer.capability in capabilities)) continue; + if (this.closed || !this.online) return; + if (!(await this.runSyncer(ctx, syncer, jmap))) return; + } + } + if (!this.closed && this.online) this.emitter({ type: 'synced', account: this.account, at: this.now() }); + } + + /** False when the run should end: no network, no credentials. */ + private async runSyncer(ctx: SyncContext, syncer: Syncer, jmap: string): Promise { + for (let attempt = 0; ; attempt += 1) { + try { + await syncer.run(ctx, jmap); + return true; + } catch (error) { + if (error instanceof TransportError) { + if (error.kind === 'network' || error.kind === 'auth') return false; + if (error.kind === 'rate' && attempt < RATE_RETRIES) { + await this.sleep(error.retryAfterMs ?? RATE_BACKOFF_MS * 2 ** attempt); + continue; + } + } + // One part failing doesn't hold up the others. It runs again next time. + this.emitter({ + type: 'error', + account: this.account, + area: syncer.name, + message: error instanceof Error ? error.message : String(error), + }); + return true; + } + } + } + + private syncContext(): SyncContext { + return { + account: this.account, + db: this.db, + session: this.session, + scope: this.scope, + transport: this.transport, + now: this.now, + emit: this.emitter, + stopped: () => this.closed || !this.online, + syncState: (jmap, type) => this.syncState(jmap, type), + setSyncState: (jmap, type, row) => this.setSyncState(jmap, type, row), + }; + } + + /** An answer from the device. Null when nothing in the request could be answered. */ + async answer(calls: MethodCall[]): Promise { + if (this.closed) throw new MethodError('serverUnavailable', 'The local store is closed'); + const primary = this.session.primaryAccounts['urn:ietf:params:jmap:mail'] ?? Object.keys(this.session.accounts)[0]; + return answerRequest( + { + account: this.account, + db: this.db, + session: this.session, + scope: this.scope, + now: this.now, + emit: this.emitter, + defaultJmap: primary, + invertedKeywordSort: this.invertedKeywordSort, + }, + this.methods, + calls, + ); + } + + /** Stops syncing and waits for the current batch. The database stays open for the caller to close. */ + async close(): Promise { + this.closed = true; + await this.running?.catch(() => {}); + } +} diff --git a/lib/store/engine/changes.ts b/lib/store/engine/changes.ts new file mode 100644 index 000000000..a14b7ca17 --- /dev/null +++ b/lib/store/engine/changes.ts @@ -0,0 +1,101 @@ +/** + * The /changes loop, written for how servers really answer (see + * local-data/desktop-offline-facts-parts/stalwart-changes-prf.md): + * + * - sinceState goes last: on Stalwart a malformed state followed by more + * arguments is an HTTP 400 that fails the whole request. + * - hasMoreChanges false doesn't mean caught up. A page walks a fixed range of + * the log, so the loop also runs while newState differs from the state the + * server last pushed. + * - An unusable state comes as cannotCalculateChanges, invalidArguments or that + * HTTP 400. All three mean: start this type over. + */ + +import { JmapMethodError, responseFor, TransportError, type JmapTransport } from './transport'; + +export interface ChangesPage { + created: string[]; + updated: string[]; + destroyed: string[]; + newState: string; + /** Set when only these properties changed (Mailbox counts). */ + updatedProperties: string[] | null; +} + +export interface ChangesLoopOptions { + transport: JmapTransport; + type: string; + jmap: string; + using: string[]; + since: string; + /** The newest state the server pushed, if any. */ + target?: string | null; + maxChanges?: number; + stopped?: () => boolean; + /** Stores one page and its newState. Runs to completion before the next call. */ + apply(page: ChangesPage): Promise; +} + +export type ChangesOutcome = + | { outcome: 'caughtUp'; state: string; pages: number } + | { outcome: 'stopped'; state: string; pages: number } + | { outcome: 'resync'; state: string; pages: number }; + +const MAX_PAGES_PAST_TARGET = 3; + +/** A stored state a server can't have sent: don't send it, it can fail the whole request. */ +export function isUsableState(state: unknown): state is string { + return typeof state === 'string' && state.length > 0 && state.length < 1024 && /^[\w.~+=:/-]+$/.test(state); +} + +function isStateRefusal(error: unknown): boolean { + if (error instanceof JmapMethodError) { + return error.type === 'cannotCalculateChanges' || error.type === 'invalidArguments'; + } + if (error instanceof TransportError) { + return error.kind === 'http' && error.status === 400 && /notRequest/.test(error.problem ?? ''); + } + return false; +} + +export async function runChangesLoop(options: ChangesLoopOptions): Promise { + const { transport, type, jmap, using, target } = options; + const maxChanges = options.maxChanges ?? 500; + let state = options.since; + let pages = 0; + let pastTarget = 0; + if (!isUsableState(state)) return { outcome: 'resync', state, pages }; + for (;;) { + if (options.stopped?.()) return { outcome: 'stopped', state, pages }; + let page: ChangesPage & { hasMoreChanges: boolean }; + try { + const response = await transport.request( + [[`${type}/changes`, { accountId: jmap, maxChanges, sinceState: state }, 'c']], + using, + ); + const [, result] = responseFor(response, 'c'); + page = { + created: result.created ?? [], + updated: result.updated ?? [], + destroyed: result.destroyed ?? [], + newState: String(result.newState), + updatedProperties: Array.isArray(result.updatedProperties) ? result.updatedProperties : null, + hasMoreChanges: Boolean(result.hasMoreChanges), + }; + } catch (error) { + if (isStateRefusal(error)) return { outcome: 'resync', state, pages }; + throw error; + } + const before = state; + await options.apply(page); + state = page.newState; + pages += 1; + if (page.hasMoreChanges) continue; + if (!target || state === target) return { outcome: 'caughtUp', state, pages }; + // Behind the pushed state: the log moved on while we paged. A state that + // stands still means the server has nothing more for this type. + if (state === before) return { outcome: 'caughtUp', state, pages }; + pastTarget += 1; + if (pastTarget >= MAX_PAGES_PAST_TARGET) return { outcome: 'caughtUp', state, pages }; + } +} diff --git a/lib/store/engine/core-schema.ts b/lib/store/engine/core-schema.ts new file mode 100644 index 000000000..938f4baa2 --- /dev/null +++ b/lib/store/engine/core-schema.ts @@ -0,0 +1,57 @@ +import type { Migration, StorageAdapter } from '../storage'; + +export const CORE_MIGRATIONS: readonly Migration[] = [ + { + id: 1, + name: 'core', + sql: ` + CREATE TABLE meta (k TEXT PRIMARY KEY, v TEXT) WITHOUT ROWID; + CREATE TABLE sync_state ( + jmap TEXT NOT NULL, + type TEXT NOT NULL, + state TEXT, + target TEXT, + phase TEXT, + cursor TEXT, + updated_at INTEGER NOT NULL, + PRIMARY KEY (jmap, type) + ) WITHOUT ROWID; + CREATE TABLE object ( + jmap TEXT NOT NULL, + type TEXT NOT NULL, + id TEXT NOT NULL, + json TEXT NOT NULL, + PRIMARY KEY (jmap, type, id) + ) WITHOUT ROWID; + `, + }, +]; + +export function metaGet(db: StorageAdapter, key: string): string | null { + return db.get<{ v: string | null }>('SELECT v FROM meta WHERE k = ?', [key])?.v ?? null; +} + +export function metaSet(db: StorageAdapter, key: string, value: string | null): void { + db.run('INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT (k) DO UPDATE SET v = excluded.v', [key, value]); +} + +export function putObject(db: StorageAdapter, jmap: string, type: string, id: string, value: unknown): void { + db.run( + 'INSERT INTO object (jmap, type, id, json) VALUES (?, ?, ?, ?) ON CONFLICT (jmap, type, id) DO UPDATE SET json = excluded.json', + [jmap, type, id, JSON.stringify(value)], + ); +} + +export function deleteObject(db: StorageAdapter, jmap: string, type: string, id: string): void { + db.run('DELETE FROM object WHERE jmap = ? AND type = ? AND id = ?', [jmap, type, id]); +} + +export function getObject>(db: StorageAdapter, jmap: string, type: string, id: string): T | null { + const row = db.get<{ json: string }>('SELECT json FROM object WHERE jmap = ? AND type = ? AND id = ?', [jmap, type, id]); + return row ? JSON.parse(row.json) as T : null; +} + +export function listObjects>(db: StorageAdapter, jmap: string, type: string): T[] { + return db.all<{ json: string }>('SELECT json FROM object WHERE jmap = ? AND type = ?', [jmap, type]) + .map((row) => JSON.parse(row.json) as T); +} diff --git a/lib/store/engine/responder.ts b/lib/store/engine/responder.ts new file mode 100644 index 000000000..be03dc5db --- /dev/null +++ b/lib/store/engine/responder.ts @@ -0,0 +1,89 @@ +/** + * Answers a JMAP request from the local store: the store acting as a server + * toward the UI. Every call gets its own answer, in order, as a server would: + * one the store can't answer gets a per-call error and the rest still work. + */ + +import { MethodError, type MethodContext, type MethodHandler, type MethodResult, type StoreArea } from '../area'; +import type { JmapResponse, MethodCall, MethodResponse } from './transport'; + +class BadReference extends Error {} + +/** RFC 8620 §3.7 result references, with the `*` array step. */ +export function resolvePointer(value: unknown, path: string): unknown { + const steps = path.split('/').slice(1).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~')); + const walk = (node: unknown, i: number): unknown => { + if (i === steps.length) return node; + const step = steps[i]; + if (step === '*') { + if (!Array.isArray(node)) throw new BadReference(); + return node.flatMap((item) => { + const out = walk(item, i + 1); + return Array.isArray(out) ? out : [out]; + }); + } + if (node === null || typeof node !== 'object') throw new BadReference(); + return walk((node as Record)[step], i + 1); + }; + return walk(value, 0); +} + +function resolveArgs(args: Record, done: readonly MethodResponse[]): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(args)) { + if (!key.startsWith('#')) { + out[key] = value; + continue; + } + const ref = value as { resultOf?: string; name?: string; path?: string }; + const source = done.find(([name, , id]) => id === ref.resultOf && name === ref.name); + if (!source || typeof ref.path !== 'string') throw new BadReference(); + out[key.slice(1)] = resolvePointer(source[1], ref.path); + } + return out; +} + +export function buildMethodTable(areas: readonly StoreArea[]): Map { + const table = new Map(); + for (const area of areas) { + for (const [name, handler] of Object.entries(area.methods)) { + if (table.has(name)) throw new Error(`Two store areas answer ${name}`); + table.set(name, handler); + } + } + return table; +} + +/** Answers are copied by structured clone on the way to the page, so callers can't mutate stored objects. */ +export async function answerRequest( + ctx: Omit, + methods: ReadonlyMap, + calls: readonly MethodCall[], +): Promise { + const done: MethodResponse[] = []; + const full: MethodContext = { ...ctx, createdIds: new Map() }; + let answered = 0; + for (const call of calls) { + const [name, rawArgs, callId] = call; + const handler = methods.get(name); + if (!handler) { + done.push(['error', { type: 'serverUnavailable', description: `${name} needs the server` }, callId]); + continue; + } + try { + const args = resolveArgs(rawArgs, done); + const result: MethodResult = await handler(full, args, call); + done.push([name, result, callId]); + answered += 1; + } catch (error) { + if (error instanceof BadReference) { + done.push(['error', { type: 'invalidResultReference' }, callId]); + } else if (error instanceof MethodError) { + done.push(['error', { type: error.type, description: error.description }, callId]); + } else { + throw error; + } + } + } + return answered === 0 ? null : { methodResponses: done }; +} diff --git a/lib/store/engine/transport.ts b/lib/store/engine/transport.ts new file mode 100644 index 000000000..cc55ff300 --- /dev/null +++ b/lib/store/engine/transport.ts @@ -0,0 +1,142 @@ +/** + * The engine's way to the real JMAP server. Bulk sync runs in the worker with + * its own fetch, so large JSON isn't parsed on the main thread. Auth stays + * with the page: it hands over the header and refreshes it on request. + */ + +export type MethodCall = [string, Record, string]; +// Shaped like the server's answer (any per method, as the callers read it). +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type MethodResponse = [string, any, string]; + +export interface JmapResponse { + methodResponses: MethodResponse[]; + sessionState?: string; +} + +/** The parts of a JMAP session the engine uses. */ +export interface StoreSession { + apiUrl: string; + downloadUrl: string; + uploadUrl: string; + primaryAccounts: Record; + accounts: Record }>; + capabilities: Record; +} + +export type TransportFailure = 'network' | 'auth' | 'rate' | 'http'; + +export class TransportError extends Error { + constructor( + readonly kind: TransportFailure, + message: string, + readonly status?: number, + /** The `type` of an RFC 7807 problem body, when there was one. */ + readonly problem?: string, + readonly retryAfterMs?: number, + ) { + super(message); + this.name = 'TransportError'; + } +} + +export interface JmapTransport { + request(calls: MethodCall[], using: string[]): Promise; + download(jmap: string, blobId: string, type: string, name: string): Promise; + upload(jmap: string, data: Uint8Array, type: string): Promise<{ blobId: string; type: string; size: number }>; +} + +export interface FetchTransportOptions { + session(): StoreSession; + /** The current Authorization header value, or null when the page has none yet. */ + auth(): string | null; + /** Asks the page for a fresh header; resolves null when it can't get one. */ + refreshAuth(): Promise; + fetch?: typeof fetch; +} + +const REQUEST_TIMEOUT_MS = 120_000; + +function fillTemplate(template: string, values: Record): string { + return template.replace(/\{(\w+)\}/g, (whole, key: string) => (key in values ? encodeURIComponent(values[key]) : whole)); +} + +export class FetchTransport implements JmapTransport { + constructor(private readonly options: FetchTransportOptions) {} + + private async fetchWithAuth(url: string, init: RequestInit): Promise { + const doFetch = this.options.fetch ?? fetch; + const send = async (header: string | null): Promise => { + if (!header) throw new TransportError('auth', 'No credentials yet'); + const signal = AbortSignal.timeout(REQUEST_TIMEOUT_MS); + try { + return await doFetch(url, { ...init, signal, headers: { ...init.headers, Authorization: header } }); + } catch (error) { + throw new TransportError('network', error instanceof Error ? error.message : 'Network failure'); + } + }; + let response = await send(this.options.auth()); + if (response.status === 401) { + response = await send(await this.options.refreshAuth()); + if (response.status === 401) throw new TransportError('auth', 'The server refused the credentials', 401); + } + if (response.status === 429 || response.status === 503) { + const seconds = Number(response.headers.get('Retry-After')); + throw new TransportError('rate', `Server busy (${response.status})`, response.status, undefined, Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : undefined); + } + return response; + } + + async request(calls: MethodCall[], using: string[]): Promise { + const response = await this.fetchWithAuth(this.options.session().apiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ using, methodCalls: calls }), + }); + if (!response.ok) { + let problem: string | undefined; + try { + problem = (await response.json() as { type?: string }).type; + } catch { + // Not a problem document. + } + throw new TransportError('http', `JMAP request failed: ${response.status}`, response.status, problem); + } + return await response.json() as JmapResponse; + } + + async download(jmap: string, blobId: string, type: string, name: string): Promise { + const url = fillTemplate(this.options.session().downloadUrl, { accountId: jmap, blobId, type, name }); + const response = await this.fetchWithAuth(url, { method: 'GET' }); + if (!response.ok) throw new TransportError('http', `Download failed: ${response.status}`, response.status); + return new Uint8Array(await response.arrayBuffer()); + } + + async upload(jmap: string, data: Uint8Array, type: string): Promise<{ blobId: string; type: string; size: number }> { + const url = fillTemplate(this.options.session().uploadUrl, { accountId: jmap }); + const response = await this.fetchWithAuth(url, { + method: 'POST', + headers: { 'Content-Type': type || 'application/octet-stream' }, + body: data as BodyInit, + }); + if (!response.ok) throw new TransportError('http', `Upload failed: ${response.status}`, response.status); + return await response.json() as { blobId: string; type: string; size: number }; + } +} + +/** Finds the response to one call. Throws if the server answered it with an error. */ +export function responseFor(response: JmapResponse, callId: string): MethodResponse { + const found = response.methodResponses.find(([name, , id]) => id === callId && name !== 'error'); + if (found) return found; + const failed = response.methodResponses.find(([, , id]) => id === callId); + if (failed) throw new JmapMethodError(String(failed[1]?.type ?? 'serverFail'), failed[1]?.description); + throw new JmapMethodError('serverFail', `No response for call ${callId}`); +} + +/** The server answered a call with a method-level error. */ +export class JmapMethodError extends Error { + constructor(readonly type: string, description?: string) { + super(description ? `${type}: ${description}` : type); + this.name = 'JmapMethodError'; + } +} diff --git a/lib/store/mail/index.ts b/lib/store/mail/index.ts new file mode 100644 index 000000000..e76d61d28 --- /dev/null +++ b/lib/store/mail/index.ts @@ -0,0 +1,11 @@ +import type { StoreArea } from '../area'; +import { MAIL_METHODS } from './methods'; +import { MAIL_MIGRATIONS } from './schema'; +import { mailSyncer } from './sync'; + +export const mailArea: StoreArea = { + name: 'mail', + migrations: MAIL_MIGRATIONS, + methods: MAIL_METHODS, + syncers: [mailSyncer], +}; diff --git a/lib/store/mail/methods.ts b/lib/store/mail/methods.ts new file mode 100644 index 000000000..e91bdd45c --- /dev/null +++ b/lib/store/mail/methods.ts @@ -0,0 +1,273 @@ +/** Mail reads answered from the store. */ + +import { MethodError, type MethodContext, type MethodHandler, type MethodResult } from '../area'; +import { listObjects } from '../engine/core-schema'; +import type { SqlValue } from '../storage'; +import { textCondition, type TextField } from './search'; + +type Filter = Record; + +interface Sql { sql: string; params: SqlValue[] } + +const TEXT_FIELDS: readonly TextField[] = ['text', 'subject', 'from', 'to', 'body']; + +function jmapOf(ctx: MethodContext, args: Record): string { + return (args.accountId as string | undefined) ?? ctx.defaultJmap; +} + +function filterSql(filter: Filter, jmap: string): Sql { + if (typeof filter.operator === 'string') { + const conditions = Array.isArray(filter.conditions) ? filter.conditions as Filter[] : []; + const parts = conditions.map((c) => filterSql(c, jmap)); + const params = parts.flatMap((p) => p.params); + const joined = (joiner: string) => parts.map((p) => `(${p.sql})`).join(joiner); + if (filter.operator === 'AND') return { sql: parts.length ? joined(' AND ') : '1', params }; + if (filter.operator === 'OR') return { sql: parts.length ? joined(' OR ') : '0', params }; + if (filter.operator === 'NOT') return { sql: parts.length ? `NOT (${joined(' OR ')})` : '1', params }; + throw new MethodError('unsupportedFilter', `Unknown operator ${String(filter.operator)}`); + } + const conditions: string[] = []; + const params: SqlValue[] = []; + for (const [key, value] of Object.entries(filter)) { + switch (key) { + case 'inMailbox': + conditions.push('EXISTS (SELECT 1 FROM email_mailbox m WHERE m.email_rowid = e.rowid AND m.mailbox_id = ?)'); + params.push(String(value)); + break; + case 'inMailboxOtherThan': { + const ids = (value as string[]).map(String); + conditions.push(`EXISTS (SELECT 1 FROM email_mailbox m WHERE m.email_rowid = e.rowid AND m.mailbox_id NOT IN (${ids.map(() => '?').join(',') || "''"}))`); + params.push(...ids); + break; + } + case 'hasKeyword': + conditions.push('EXISTS (SELECT 1 FROM email_keyword k WHERE k.email_rowid = e.rowid AND k.keyword = ?)'); + params.push(String(value).toLowerCase()); + break; + case 'notKeyword': + conditions.push('NOT EXISTS (SELECT 1 FROM email_keyword k WHERE k.email_rowid = e.rowid AND k.keyword = ?)'); + params.push(String(value).toLowerCase()); + break; + case 'after': + conditions.push('e.received_at >= ?'); + params.push(Date.parse(String(value)) || 0); + break; + case 'before': + conditions.push('e.received_at < ?'); + params.push(Date.parse(String(value)) || 0); + break; + case 'minSize': + conditions.push('e.size >= ?'); + params.push(Number(value)); + break; + case 'maxSize': + conditions.push('e.size < ?'); + params.push(Number(value)); + break; + case 'hasAttachment': + conditions.push(value ? 'e.has_attachment = 1' : 'e.has_attachment = 0'); + break; + case 'id': + conditions.push(`e.id IN (${(value as string[]).map(() => '?').join(',') || "''"})`); + params.push(...(value as string[]).map(String)); + break; + default: + if ((TEXT_FIELDS as readonly string[]).includes(key)) { + const text = textCondition(key as TextField, String(value)); + conditions.push(text.sql); + params.push(...text.params); + break; + } + // cc, bcc, header and the thread keyword filters: the server's job. + throw new MethodError('unsupportedFilter', `${key} isn't available offline`); + } + } + return { sql: conditions.length ? conditions.join(' AND ') : '1', params }; +} + +interface Comparator { property?: string; isAscending?: boolean; keyword?: string } + +function orderSql(ctx: MethodContext, sort: Comparator[]): Sql { + const levels = sort.length > 0 ? sort : [{ property: 'receivedAt', isAscending: false }]; + const parts: string[] = []; + const params: SqlValue[] = []; + for (const level of levels) { + // RFC 8621 says isAscending defaults to true. + let ascending = level.isAscending !== false; + switch (level.property) { + case 'receivedAt': parts.push(`e.received_at ${ascending ? 'ASC' : 'DESC'}`); break; + case 'sentAt': parts.push(`COALESCE(e.sent_at, e.received_at) ${ascending ? 'ASC' : 'DESC'}`); break; + case 'size': parts.push(`e.size ${ascending ? 'ASC' : 'DESC'}`); break; + case 'subject': parts.push(`e.subject_sort COLLATE NOCASE ${ascending ? 'ASC' : 'DESC'}`); break; + case 'from': parts.push(`e.from_sort COLLATE NOCASE ${ascending ? 'ASC' : 'DESC'}`); break; + case 'to': parts.push(`e.to_sort COLLATE NOCASE ${ascending ? 'ASC' : 'DESC'}`); break; + case 'hasKeyword': { + // The client flips this on servers that read it the other way, so a + // stored answer has to read it the way that server would. + if (ctx.invertedKeywordSort) ascending = !ascending; + parts.push(`EXISTS (SELECT 1 FROM email_keyword s WHERE s.email_rowid = e.rowid AND s.keyword = ?) ${ascending ? 'ASC' : 'DESC'}`); + params.push(String(level.keyword ?? '').toLowerCase()); + break; + } + default: + throw new MethodError('unsupportedSort', `${String(level.property)} isn't available offline`); + } + } + parts.push('e.rowid DESC'); + return { sql: parts.join(', '), params }; +} + +/** The one folder a filter limits itself to, so the query can start from that folder's index. */ +function drivingMailbox(filter: Filter | undefined): string | null { + if (!filter) return null; + if (typeof filter.inMailbox === 'string') return filter.inMailbox; + if (filter.operator === 'AND' && Array.isArray(filter.conditions)) { + for (const condition of filter.conditions as Filter[]) { + const found = drivingMailbox(condition); + if (found) return found; + } + } + return null; +} + +const emailQuery: MethodHandler = (ctx, args) => { + const jmap = jmapOf(ctx, args); + const filter = (args.filter ?? undefined) as Filter | undefined; + if (args.anchor !== undefined) throw new MethodError('unsupportedFilter', 'anchor needs the server'); + const where = filter ? filterSql(filter, jmap) : { sql: '1', params: [] }; + const order = orderSql(ctx, (args.sort as Comparator[]) ?? []); + const box = drivingMailbox(filter); + // Starting from the folder's own index keeps small folders fast in a large account. + const from = box + ? 'email_mailbox d INDEXED BY email_mailbox_list CROSS JOIN email e ON e.rowid = d.email_rowid' + : 'email e'; + const base = box + ? { sql: 'd.jmap = ? AND d.mailbox_id = ?', params: [jmap, box] as SqlValue[] } + : { sql: 'e.jmap = ?', params: [jmap] as SqlValue[] }; + const position = Math.max(0, Number(args.position ?? 0)); + const limit = typeof args.limit === 'number' && args.limit > 0 ? args.limit : -1; + const collapse = Boolean(args.collapseThreads); + const group = collapse ? 'GROUP BY COALESCE(e.thread_id, e.id)' : ''; + const ids = ctx.db.all<{ id: string }>( + `SELECT e.id FROM ${from} WHERE ${base.sql} AND (${where.sql}) ${group} ORDER BY ${order.sql} LIMIT ? OFFSET ?`, + [...base.params, ...where.params, ...order.params, limit, position], + ).map((row) => String(row.id)); + const result: MethodResult = { + accountId: jmap, + canCalculateChanges: false, + // Local answers aren't a position in the server's log, so this is a token only. + queryState: 'local', + position, + ids, + }; + if (args.calculateTotal) { + const count = collapse ? 'COUNT(DISTINCT COALESCE(e.thread_id, e.id))' : 'COUNT(*)'; + result.total = Number(ctx.db.get<{ n: number }>( + `SELECT ${count} AS n FROM ${from} WHERE ${base.sql} AND (${where.sql})`, + [...base.params, ...where.params], + )?.n ?? 0); + } + return result; +}; + +const BODY_PROPERTIES = new Set(['bodyValues', 'textBody', 'htmlBody', 'bodyStructure', 'headers', 'attachments', 'bcc', 'replyTo', 'messageId', 'inReplyTo', 'references']); + +function pick(email: Record, properties: string[] | undefined): Record { + if (!properties) return email; + const out: Record = { id: email.id }; + for (const property of properties) if (property in email) out[property] = email[property]; + return out; +} + +const emailGet: MethodHandler = (ctx, args) => { + const jmap = jmapOf(ctx, args); + if (!Array.isArray(args.ids)) throw new MethodError('requestTooLarge', 'ids: null needs the server'); + const properties = args.properties as string[] | undefined; + const wantsValues = Boolean(args.fetchTextBodyValues || args.fetchHTMLBodyValues || args.fetchAllBodyValues) || Boolean(properties?.includes('bodyValues')); + const wantsBody = wantsValues || !properties || properties.some((p) => BODY_PROPERTIES.has(p)); + const list: Record[] = []; + const notFound: string[] = []; + for (const id of args.ids as string[]) { + const row = ctx.db.get<{ rowid: number; json: string; body_state: number }>( + 'SELECT rowid, json, body_state FROM email WHERE jmap = ? AND id = ?', + [jmap, id], + ); + if (!row) { + notFound.push(id); + continue; + } + const email = JSON.parse(row.json) as Record; + if (!wantsBody) { + list.push(pick(email, properties)); + continue; + } + const body = ctx.db.get<{ json: string }>('SELECT json FROM email_body WHERE email_rowid = ?', [row.rowid]); + // Asked for the text and it isn't here: as if the message weren't there, not an empty message. + if (!body && wantsValues) { + notFound.push(id); + continue; + } + list.push(pick({ ...(body ? JSON.parse(body.json) as object : {}), ...email }, properties)); + } + const state = ctx.db.get<{ state: string | null }>("SELECT state FROM sync_state WHERE jmap = ? AND type = 'Email'", [jmap])?.state; + return { accountId: jmap, ...(state ? { state } : {}), list, notFound }; +}; + +const threadGet: MethodHandler = (ctx, args) => { + const jmap = jmapOf(ctx, args); + const ids = Array.isArray(args.ids) ? [...new Set(args.ids as string[])] : []; + const list: Array<{ id: string; emailIds: string[] }> = []; + const notFound: string[] = []; + for (const id of ids) { + const stored = ctx.db.get<{ email_ids: string }>('SELECT email_ids FROM thread WHERE jmap = ? AND id = ?', [jmap, id]); + if (stored) { + list.push({ id, emailIds: JSON.parse(stored.email_ids) as string[] }); + continue; + } + const rows = ctx.db.all<{ id: string }>('SELECT id FROM email WHERE jmap = ? AND thread_id = ? ORDER BY received_at ASC', [jmap, id]); + if (rows.length > 0) list.push({ id, emailIds: rows.map((row) => String(row.id)) }); + else notFound.push(id); + } + return { accountId: jmap, list, notFound }; +}; + +const mailboxGet: MethodHandler = (ctx, args) => { + const jmap = jmapOf(ctx, args); + const boxes = listObjects>(ctx.db, jmap, 'Mailbox'); + if (boxes.length === 0) throw new MethodError('serverUnavailable', 'Folders not kept yet'); + const ids = Array.isArray(args.ids) ? new Set(args.ids as string[]) : null; + const chosen = ids ? boxes.filter((box) => ids.has(box.id as string)) : boxes; + const state = ctx.db.get<{ state: string | null }>("SELECT state FROM sync_state WHERE jmap = ? AND type = 'Mailbox'", [jmap])?.state; + return { + accountId: jmap, + ...(state ? { state } : {}), + list: chosen.map((box) => pick(box, args.properties as string[] | undefined)), + notFound: ids ? [...ids].filter((id) => !boxes.some((box) => box.id === id)) : [], + }; +}; + +const mailboxQuery: MethodHandler = (ctx, args) => { + const jmap = jmapOf(ctx, args); + const boxes = listObjects>(ctx.db, jmap, 'Mailbox'); + if (boxes.length === 0) throw new MethodError('serverUnavailable', 'Folders not kept yet'); + const filter = (args.filter ?? {}) as { role?: string }; + const chosen = filter.role ? boxes.filter((box) => box.role === filter.role) : boxes; + const position = typeof args.position === 'number' ? args.position : 0; + const limit = typeof args.limit === 'number' ? args.limit : chosen.length; + return { + accountId: jmap, + queryState: 'local', + canCalculateChanges: false, + position, + ids: chosen.slice(position, position + limit).map((box) => box.id), + ...(args.calculateTotal ? { total: chosen.length } : {}), + }; +}; + +export const MAIL_METHODS: Readonly> = { + 'Email/query': emailQuery, + 'Email/get': emailGet, + 'Thread/get': threadGet, + 'Mailbox/get': mailboxGet, + 'Mailbox/query': mailboxQuery, +}; diff --git a/lib/store/mail/records.ts b/lib/store/mail/records.ts new file mode 100644 index 000000000..b0e292718 --- /dev/null +++ b/lib/store/mail/records.ts @@ -0,0 +1,147 @@ +/** How an Email object becomes rows, and back. */ + +import type { StorageAdapter } from '../storage'; + +export const MAIL_USING = ['urn:ietf:params:jmap:core', 'urn:ietf:params:jmap:mail']; + +export const EMAIL_LIST_PROPERTIES = [ + 'id', 'threadId', 'mailboxIds', 'keywords', 'size', 'receivedAt', 'sentAt', 'from', 'to', 'cc', + 'subject', 'preview', 'hasAttachment', 'blobId', 'header:Authentication-Results:all', +]; + +/** What opening a message asks for, past the list properties. */ +export const EMAIL_BODY_PROPERTIES = [ + 'bcc', 'replyTo', 'textBody', 'htmlBody', 'bodyValues', 'attachments', 'messageId', 'inReplyTo', + 'references', 'headers', 'bodyStructure', +]; + +export type EmailObject = Record & { id: string }; + +interface Address { name?: string | null; email?: string | null } + +function firstAddress(value: unknown): string | null { + const first = Array.isArray(value) ? value[0] as Address | undefined : undefined; + const text = first?.name || first?.email; + return text ? text.toLowerCase() : null; +} + +function addressText(value: unknown): string { + if (!Array.isArray(value)) return ''; + return (value as Address[]).map((a) => [a.name, a.email].filter(Boolean).join(' ')).join(' '); +} + +function time(value: unknown): number | null { + if (typeof value !== 'string') return null; + const ms = Date.parse(value); + return Number.isFinite(ms) ? ms : null; +} + +/** The subject without Re:, Fwd: and the like, as the client sorts it. */ +export function baseSubject(subject: string): string { + return subject.replace(/^(\s*(re|aw|fw|fwd|wg)\s*(\[\d+\])?\s*:\s*)+/i, '').trim().toLowerCase(); +} + +/** Inserts or updates one message from its list properties. Keeps a body already stored. */ +export function upsertEmail(db: StorageAdapter, jmap: string, email: EmailObject, gen: number): number { + const receivedAt = time(email.receivedAt) ?? 0; + const subject = typeof email.subject === 'string' ? email.subject : ''; + const existing = db.get<{ rowid: number }>('SELECT rowid FROM email WHERE jmap = ? AND id = ?', [jmap, email.id]); + const values = [ + typeof email.threadId === 'string' ? email.threadId : null, + receivedAt, + time(email.sentAt), + typeof email.size === 'number' ? email.size : 0, + email.hasAttachment ? 1 : 0, + baseSubject(subject), + firstAddress(email.from), + firstAddress(email.to), + JSON.stringify(email), + gen, + ]; + let rowid: number; + if (existing) { + rowid = Number(existing.rowid); + db.run( + `UPDATE email SET thread_id = ?, received_at = ?, sent_at = ?, size = ?, has_attachment = ?, subject_sort = ?, + from_sort = ?, to_sort = ?, json = ?, gen = ? WHERE rowid = ?`, + [...values, rowid], + ); + db.run('DELETE FROM email_mailbox WHERE email_rowid = ?', [rowid]); + db.run('DELETE FROM email_keyword WHERE email_rowid = ?', [rowid]); + } else { + rowid = db.run( + `INSERT INTO email (thread_id, received_at, sent_at, size, has_attachment, subject_sort, from_sort, to_sort, json, gen, jmap, id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [...values, jmap, email.id], + ).lastInsertRowid; + indexEmail(db, rowid, email, ''); + } + for (const [box, on] of Object.entries((email.mailboxIds ?? {}) as Record)) { + if (on) db.run('INSERT INTO email_mailbox (email_rowid, jmap, mailbox_id, received_at) VALUES (?, ?, ?, ?)', [rowid, jmap, box, receivedAt]); + } + for (const [keyword, on] of Object.entries((email.keywords ?? {}) as Record)) { + if (on) db.run('INSERT INTO email_keyword (email_rowid, keyword) VALUES (?, ?)', [rowid, keyword.toLowerCase()]); + } + return rowid; +} + +export function deleteEmail(db: StorageAdapter, jmap: string, id: string): void { + const row = db.get<{ rowid: number }>('SELECT rowid FROM email WHERE jmap = ? AND id = ?', [jmap, id]); + if (!row) return; + deleteEmailRow(db, Number(row.rowid)); +} + +export function deleteEmailRow(db: StorageAdapter, rowid: number): void { + db.run('DELETE FROM email_fts_word WHERE rowid = ?', [rowid]); + db.run('DELETE FROM email_fts_tri WHERE rowid = ?', [rowid]); + // Explicit rather than ON DELETE CASCADE: foreign keys stay off for bulk speed. + db.run('DELETE FROM email_mailbox WHERE email_rowid = ?', [rowid]); + db.run('DELETE FROM email_keyword WHERE email_rowid = ?', [rowid]); + db.run('DELETE FROM email_body WHERE email_rowid = ?', [rowid]); + db.run('DELETE FROM email WHERE rowid = ?', [rowid]); +} + +/** Plain text of a message for the search index: the text part, or the HTML part without its tags. */ +export function bodyText(email: Record): string { + const values = (email.bodyValues ?? {}) as Record; + const parts = (key: string) => ((email[key] ?? []) as Array<{ partId?: string; type?: string }>); + const text = parts('textBody') + .filter((part) => part.type !== 'text/html') + .map((part) => values[part.partId ?? '']?.value ?? '') + .join('\n'); + if (text.trim()) return text; + return parts('htmlBody') + .map((part) => values[part.partId ?? '']?.value ?? '') + .join('\n') + .replace(/<(style|script)[\s\S]*?<\/\1>/gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/\s+/g, ' '); +} + +/** A long message past this is rare and only bloats the index. */ +const INDEX_BODY_CHARS = 200_000; + +export function indexEmail(db: StorageAdapter, rowid: number, email: Record, body: string): void { + const subject = typeof email.subject === 'string' ? email.subject : ''; + const from = addressText(email.from); + const to = [addressText(email.to), addressText(email.cc), addressText(email.bcc)].filter(Boolean).join(' '); + const text = body.slice(0, INDEX_BODY_CHARS); + for (const table of ['email_fts_word', 'email_fts_tri']) { + db.run(`INSERT INTO ${table} (rowid, subject, from_addr, to_addr, body) VALUES (?, ?, ?, ?, ?)`, [rowid, subject, from, to, text]); + } +} + +export function storeBody(db: StorageAdapter, rowid: number, listJson: string, full: Record): void { + const list = JSON.parse(listJson) as Record; + const body: Record = {}; + for (const [key, value] of Object.entries(full)) if (!(key in list)) body[key] = value; + db.run('INSERT INTO email_body (email_rowid, json) VALUES (?, ?) ON CONFLICT (email_rowid) DO UPDATE SET json = excluded.json', [rowid, JSON.stringify(body)]); + db.run('UPDATE email SET body_state = 1 WHERE rowid = ?', [rowid]); + db.run('DELETE FROM email_fts_word WHERE rowid = ?', [rowid]); + db.run('DELETE FROM email_fts_tri WHERE rowid = ?', [rowid]); + indexEmail(db, rowid, { ...list, ...body }, bodyText(full)); +} diff --git a/lib/store/mail/schema.ts b/lib/store/mail/schema.ts new file mode 100644 index 000000000..52ef16dde --- /dev/null +++ b/lib/store/mail/schema.ts @@ -0,0 +1,65 @@ +import type { Migration } from '../storage'; + +export const MAIL_MIGRATIONS: readonly Migration[] = [ + { + id: 100, + name: 'mail', + sql: ` + CREATE TABLE email ( + rowid INTEGER PRIMARY KEY, + jmap TEXT NOT NULL, + id TEXT NOT NULL, + thread_id TEXT, + received_at INTEGER NOT NULL, + sent_at INTEGER, + size INTEGER NOT NULL DEFAULT 0, + has_attachment INTEGER NOT NULL DEFAULT 0, + subject_sort TEXT, + from_sort TEXT, + to_sort TEXT, + json TEXT NOT NULL, + -- 0 no body yet, 1 kept, 2 the server had none to give + body_state INTEGER NOT NULL DEFAULT 0, + gen INTEGER NOT NULL DEFAULT 0, + UNIQUE (jmap, id) + ); + CREATE INDEX email_received ON email (jmap, received_at DESC); + CREATE INDEX email_thread ON email (jmap, thread_id); + CREATE INDEX email_backfill ON email (jmap, body_state, received_at DESC); + CREATE TABLE email_mailbox ( + email_rowid INTEGER NOT NULL REFERENCES email (rowid) ON DELETE CASCADE, + jmap TEXT NOT NULL, + mailbox_id TEXT NOT NULL, + received_at INTEGER NOT NULL, + PRIMARY KEY (email_rowid, mailbox_id) + ) WITHOUT ROWID; + CREATE INDEX email_mailbox_list ON email_mailbox (jmap, mailbox_id, received_at DESC); + CREATE TABLE email_keyword ( + email_rowid INTEGER NOT NULL REFERENCES email (rowid) ON DELETE CASCADE, + keyword TEXT NOT NULL, + PRIMARY KEY (email_rowid, keyword) + ) WITHOUT ROWID; + CREATE INDEX email_keyword_lookup ON email_keyword (keyword, email_rowid); + CREATE TABLE email_body ( + email_rowid INTEGER PRIMARY KEY REFERENCES email (rowid) ON DELETE CASCADE, + json TEXT NOT NULL + ); + CREATE TABLE thread ( + jmap TEXT NOT NULL, + id TEXT NOT NULL, + email_ids TEXT NOT NULL, + PRIMARY KEY (jmap, id) + ) WITHOUT ROWID; + CREATE VIRTUAL TABLE email_fts_word USING fts5( + subject, from_addr, to_addr, body, + content = '', contentless_delete = 1, + tokenize = 'unicode61 remove_diacritics 2' + ); + CREATE VIRTUAL TABLE email_fts_tri USING fts5( + subject, from_addr, to_addr, body, + content = '', contentless_delete = 1, + tokenize = 'trigram remove_diacritics 1' + ); + `, + }, +]; diff --git a/lib/store/mail/search.ts b/lib/store/mail/search.ts new file mode 100644 index 000000000..57b94f849 --- /dev/null +++ b/lib/store/mail/search.ts @@ -0,0 +1,59 @@ +/** Full-text matching over the two FTS indexes. */ + +import type { SqlValue } from '../storage'; + +export type TextField = 'text' | 'subject' | 'from' | 'to' | 'body'; + +const COLUMNS: Record = { + text: null, + subject: 'subject', + from: 'from_addr', + to: 'to_addr', + body: 'body', +}; + +function quote(term: string): string { + return `"${term.replace(/"/g, '""')}"`; +} + +/** The terms of a search box value: quoted phrases stay whole, `word*` keeps its star. */ +export function splitTerms(input: string): string[] { + const out: string[] = []; + for (const match of input.matchAll(/"([^"]+)"|(\S+)/g)) out.push(match[1] ?? match[2]); + return out; +} + +/** + * One term as SQL over rowids. The word index finds words and word starts, + * the trigram index finds a term inside a longer word, which is how German + * compounds are found ("Rechnung" in "Zahlungserinnerungsrechnung"). + */ +function termRowids(field: TextField, term: string): { sql: string; params: SqlValue[] } { + const column = COLUMNS[field]; + const star = term.endsWith('*'); + const clean = star ? term.slice(0, -1) : term; + const scope = column ? `${column} : ` : ''; + const word = `${scope}${quote(clean)}${' *'.slice(1)}`; + const parts = ['SELECT rowid FROM email_fts_word WHERE email_fts_word MATCH ?']; + const params: SqlValue[] = [word]; + // Trigrams need three characters. Shorter terms use the word index alone. + if ([...clean].length >= 3) { + parts.push('SELECT rowid FROM email_fts_tri WHERE email_fts_tri MATCH ?'); + params.push(`${scope}${quote(clean)}`); + } + return { sql: parts.join(' UNION '), params }; +} + +/** SQL for a text condition, as a condition on `e.rowid`. Every term has to match. */ +export function textCondition(field: TextField, input: string): { sql: string; params: SqlValue[] } { + const terms = splitTerms(input); + if (terms.length === 0) return { sql: '1', params: [] }; + const conditions: string[] = []; + const params: SqlValue[] = []; + for (const term of terms) { + const part = termRowids(field, term); + conditions.push(`e.rowid IN (${part.sql})`); + params.push(...part.params); + } + return { sql: conditions.join(' AND '), params }; +} diff --git a/lib/store/mail/sync.ts b/lib/store/mail/sync.ts new file mode 100644 index 000000000..21ad9bb0d --- /dev/null +++ b/lib/store/mail/sync.ts @@ -0,0 +1,323 @@ +/** + * Mail sync for one JMAP account. + * + * A full sync captures the state first, then pages through the window, so a + * change that lands during it is picked up by the delta afterwards. Its cursor + * lives in sync_state, written in the same transaction as the page, so a + * reload continues where it stopped. Bodies come after the list, newest first. + */ + +import type { SyncContext, Syncer } from '../area'; +import { deleteObject, putObject } from '../engine/core-schema'; +import { runChangesLoop } from '../engine/changes'; +import { responseFor, type MethodCall } from '../engine/transport'; +import { + deleteEmail, + deleteEmailRow, + EMAIL_BODY_PROPERTIES, + EMAIL_LIST_PROPERTIES, + MAIL_USING, + storeBody, + upsertEmail, + type EmailObject, +} from './records'; + +const PAGE = 500; +const PAIRS_PER_REQUEST = 4; +const BODY_BATCH_IDS = 100; +const BODY_BATCH_BYTES = 8 * 1024 * 1024; +const MAX_BODY_VALUE_BYTES = 1_000_000; +const DELTA_BATCH = 500; + +function windowFilter(ctx: SyncContext): Record | undefined { + if (ctx.scope.days === null) return undefined; + return { after: new Date(ctx.now() - ctx.scope.days * 86_400_000).toISOString() }; +} + +function inWindow(ctx: SyncContext, email: EmailObject): boolean { + if (ctx.scope.days === null) return true; + return (Date.parse(String(email.receivedAt)) || 0) >= ctx.now() - ctx.scope.days * 86_400_000; +} + +// ---- Mailboxes ---- + +async function fullMailboxes(ctx: SyncContext, jmap: string): Promise { + const response = await ctx.transport.request([['Mailbox/get', { accountId: jmap, ids: null }, 'm']], MAIL_USING); + const [, result] = responseFor(response, 'm'); + ctx.db.transaction(() => { + ctx.db.run("DELETE FROM object WHERE jmap = ? AND type = 'Mailbox'", [jmap]); + for (const box of result.list as Array<{ id: string }>) putObject(ctx.db, jmap, 'Mailbox', box.id, box); + ctx.setSyncState(jmap, 'Mailbox', { state: String(result.state), phase: null, cursor: null }); + }); +} + +async function syncMailboxes(ctx: SyncContext, jmap: string): Promise { + const { state, target } = ctx.syncState(jmap, 'Mailbox'); + if (!state) return fullMailboxes(ctx, jmap); + const outcome = await runChangesLoop({ + transport: ctx.transport, + type: 'Mailbox', + jmap, + using: MAIL_USING, + since: state, + target, + stopped: ctx.stopped, + apply: async (page) => { + const ids = [...page.created, ...page.updated]; + const list: Array<{ id: string }> = []; + for (let i = 0; i < ids.length; i += PAGE) { + const response = await ctx.transport.request([['Mailbox/get', { accountId: jmap, ids: ids.slice(i, i + PAGE) }, 'm']], MAIL_USING); + list.push(...responseFor(response, 'm')[1].list); + } + ctx.db.transaction(() => { + for (const box of list) putObject(ctx.db, jmap, 'Mailbox', box.id, box); + for (const id of page.destroyed) deleteObject(ctx.db, jmap, 'Mailbox', id); + ctx.setSyncState(jmap, 'Mailbox', { state: page.newState }); + }); + }, + }); + if (outcome.outcome === 'resync') await fullMailboxes(ctx, jmap); +} + +// ---- Email list ---- + +interface ListCursor { position: number; gen: number; captured: string } + +async function startFullList(ctx: SyncContext, jmap: string): Promise { + const response = await ctx.transport.request([['Email/get', { accountId: jmap, ids: [], properties: ['id'] }, 's']], MAIL_USING); + const captured = String(responseFor(response, 's')[1].state); + const gen = (ctx.db.get<{ gen: number | null }>('SELECT MAX(gen) AS gen FROM email WHERE jmap = ?', [jmap])?.gen ?? 0) + 1; + const cursor: ListCursor = { position: 0, gen: Number(gen), captured }; + ctx.db.transaction(() => ctx.setSyncState(jmap, 'Email', { state: null, phase: 'list', cursor: { ...cursor } })); + return cursor; +} + +async function runFullList(ctx: SyncContext, jmap: string, start: ListCursor): Promise { + const cursor = { ...start }; + const filter = windowFilter(ctx); + let total: number | null = null; + for (;;) { + if (ctx.stopped()) return false; + const calls: MethodCall[] = []; + for (let i = 0; i < PAIRS_PER_REQUEST; i += 1) { + calls.push(['Email/query', { + accountId: jmap, + ...(filter ? { filter } : {}), + sort: [{ property: 'receivedAt', isAscending: false }], + position: cursor.position + i * PAGE, + limit: PAGE, + calculateTotal: i === 0, + }, `q${i}`]); + calls.push(['Email/get', { + accountId: jmap, + '#ids': { resultOf: `q${i}`, name: 'Email/query', path: '/ids' }, + properties: EMAIL_LIST_PROPERTIES, + }, `g${i}`]); + } + const response = await ctx.transport.request(calls, MAIL_USING); + let done = false; + let got = 0; + const lists: EmailObject[][] = []; + for (let i = 0; i < PAIRS_PER_REQUEST; i += 1) { + const [, query] = responseFor(response, `q${i}`); + if (i === 0 && typeof query.total === 'number') total = query.total; + lists.push(responseFor(response, `g${i}`)[1].list as EmailObject[]); + got += (query.ids as string[]).length; + if ((query.ids as string[]).length < PAGE) { + done = true; + break; + } + } + cursor.position += got; + ctx.db.transaction(() => { + for (const list of lists) for (const email of list) upsertEmail(ctx.db, jmap, email, cursor.gen); + if (done) { + // Whatever this pass didn't see is gone from the server or the window. + const stale = ctx.db.all<{ rowid: number }>('SELECT rowid FROM email WHERE jmap = ? AND gen < ?', [jmap, cursor.gen]); + for (const row of stale) deleteEmailRow(ctx.db, Number(row.rowid)); + ctx.setSyncState(jmap, 'Email', { state: cursor.captured, phase: null, cursor: null }); + } else { + ctx.setSyncState(jmap, 'Email', { cursor: { ...cursor } }); + } + }); + ctx.emit({ type: 'progress', account: ctx.account, area: 'mail', phase: 'list', done: cursor.position, total }); + if (done) { + ctx.emit({ type: 'changed', account: ctx.account, collections: ['Email'] }); + return true; + } + } +} + +async function syncEmailDelta(ctx: SyncContext, jmap: string, since: string, target: string | null): Promise<'done' | 'resync' | 'stopped'> { + const outcome = await runChangesLoop({ + transport: ctx.transport, + type: 'Email', + jmap, + using: MAIL_USING, + since, + target, + stopped: ctx.stopped, + apply: async (page) => { + const ids = [...page.created, ...page.updated]; + const fetched: EmailObject[] = []; + for (let i = 0; i < ids.length; i += DELTA_BATCH) { + const response = await ctx.transport.request([['Email/get', { + accountId: jmap, + ids: ids.slice(i, i + DELTA_BATCH), + properties: EMAIL_LIST_PROPERTIES, + }, 'g']], MAIL_USING); + fetched.push(...responseFor(response, 'g')[1].list as EmailObject[]); + } + const created = new Set(page.created); + const newIds: string[] = []; + ctx.db.transaction(() => { + for (const email of fetched) { + const known = ctx.db.get('SELECT 1 AS x FROM email WHERE jmap = ? AND id = ?', [jmap, email.id]); + // An old message changing doesn't pull it into the window. + if (!known && !inWindow(ctx, email)) continue; + const gen = Number(ctx.db.get<{ gen: number | null }>('SELECT MAX(gen) AS gen FROM email WHERE jmap = ?', [jmap])?.gen ?? 0); + upsertEmail(ctx.db, jmap, email, gen); + if (!known && created.has(email.id)) newIds.push(email.id); + } + for (const id of page.destroyed) deleteEmail(ctx.db, jmap, id); + ctx.setSyncState(jmap, 'Email', { state: page.newState }); + }); + if (newIds.length > 0) ctx.emit({ type: 'created', account: ctx.account, jmap, collection: 'Email', ids: newIds }); + if (ids.length + page.destroyed.length > 0) ctx.emit({ type: 'changed', account: ctx.account, collections: ['Email'] }); + }, + }); + if (outcome.outcome === 'resync') return 'resync'; + return outcome.outcome === 'stopped' ? 'stopped' : 'done'; +} + +// ---- Threads ---- + +async function fetchThreads(ctx: SyncContext, jmap: string, ids: string[]): Promise> { + const out: Array<{ id: string; emailIds: string[] }> = []; + for (let i = 0; i < ids.length; i += PAGE) { + const response = await ctx.transport.request([['Thread/get', { accountId: jmap, ids: ids.slice(i, i + PAGE) }, 't']], MAIL_USING); + out.push(...responseFor(response, 't')[1].list); + } + return out; +} + +async function syncThreads(ctx: SyncContext, jmap: string): Promise { + const { state, target } = ctx.syncState(jmap, 'Thread'); + const fullThreads = async () => { + const response = await ctx.transport.request([['Thread/get', { accountId: jmap, ids: [] }, 's']], MAIL_USING); + const captured = String(responseFor(response, 's')[1].state); + const known = ctx.db.all<{ id: string }>( + `SELECT DISTINCT e.thread_id AS id FROM email e WHERE e.jmap = ? AND e.thread_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM thread t WHERE t.jmap = e.jmap AND t.id = e.thread_id)`, + [jmap], + ).map((row) => String(row.id)); + for (let i = 0; i < known.length; i += PAGE) { + if (ctx.stopped()) return; + const list = await fetchThreads(ctx, jmap, known.slice(i, i + PAGE)); + ctx.db.transaction(() => { + for (const thread of list) ctx.db.run('INSERT OR REPLACE INTO thread (jmap, id, email_ids) VALUES (?, ?, ?)', [jmap, thread.id, JSON.stringify(thread.emailIds)]); + }); + ctx.emit({ type: 'progress', account: ctx.account, area: 'mail', phase: 'threads', done: i + list.length, total: known.length }); + } + ctx.db.transaction(() => ctx.setSyncState(jmap, 'Thread', { state: captured })); + }; + if (!state) return fullThreads(); + const outcome = await runChangesLoop({ + transport: ctx.transport, + type: 'Thread', + jmap, + using: MAIL_USING, + since: state, + target, + stopped: ctx.stopped, + apply: async (page) => { + const list = await fetchThreads(ctx, jmap, [...page.created, ...page.updated]); + ctx.db.transaction(() => { + for (const thread of list) ctx.db.run('INSERT OR REPLACE INTO thread (jmap, id, email_ids) VALUES (?, ?, ?)', [jmap, thread.id, JSON.stringify(thread.emailIds)]); + for (const id of page.destroyed) ctx.db.run('DELETE FROM thread WHERE jmap = ? AND id = ?', [jmap, id]); + ctx.setSyncState(jmap, 'Thread', { state: page.newState }); + }); + }, + }); + if (outcome.outcome === 'resync') { + ctx.db.transaction(() => { + ctx.db.run('DELETE FROM thread WHERE jmap = ?', [jmap]); + ctx.setSyncState(jmap, 'Thread', { state: null }); + }); + await fullThreads(); + } +} + +// ---- Bodies ---- + +async function backfillBodies(ctx: SyncContext, jmap: string): Promise { + let done = 0; + const totalRow = ctx.db.get<{ n: number }>('SELECT COUNT(*) AS n FROM email WHERE jmap = ? AND body_state = 0', [jmap]); + const total = Number(totalRow?.n ?? 0); + for (;;) { + if (ctx.stopped()) return; + const candidates = ctx.db.all<{ rowid: number; id: string; size: number; json: string }>( + 'SELECT rowid, id, size, json FROM email WHERE jmap = ? AND body_state = 0 ORDER BY received_at DESC LIMIT ?', + [jmap, BODY_BATCH_IDS], + ); + if (candidates.length === 0) return; + let bytes = 0; + const batch: typeof candidates = []; + for (const row of candidates) { + if (batch.length > 0 && bytes + Number(row.size) > BODY_BATCH_BYTES) break; + bytes += Number(row.size); + batch.push(row); + } + const response = await ctx.transport.request([['Email/get', { + accountId: jmap, + ids: batch.map((row) => String(row.id)), + properties: ['id', ...EMAIL_BODY_PROPERTIES], + fetchTextBodyValues: true, + fetchHTMLBodyValues: true, + fetchAllBodyValues: true, + maxBodyValueBytes: MAX_BODY_VALUE_BYTES, + }, 'b']], MAIL_USING); + const list = responseFor(response, 'b')[1].list as EmailObject[]; + const byId = new Map(list.map((email) => [email.id, email])); + ctx.db.transaction(() => { + for (const row of batch) { + const full = byId.get(String(row.id)); + if (full) storeBody(ctx.db, Number(row.rowid), String(row.json), full); + else ctx.db.run('UPDATE email SET body_state = 2 WHERE rowid = ?', [row.rowid]); + } + }); + done += batch.length; + ctx.emit({ type: 'progress', account: ctx.account, area: 'mail', phase: 'bodies', done, total }); + } +} + +// ---- The whole pass ---- + +export const mailSyncer: Syncer = { + name: 'mail', + capability: 'urn:ietf:params:jmap:mail', + async run(ctx, jmap) { + await syncMailboxes(ctx, jmap); + if (ctx.stopped()) return; + + const email = ctx.syncState(jmap, 'Email'); + let cursor = email.phase === 'list' ? email.cursor as unknown as ListCursor : null; + if (!cursor && !email.state) cursor = await startFullList(ctx, jmap); + if (!cursor && email.state) { + const outcome = await syncEmailDelta(ctx, jmap, email.state, email.target); + if (outcome === 'stopped') return; + if (outcome === 'resync') cursor = await startFullList(ctx, jmap); + } + if (cursor) { + const finished = await runFullList(ctx, jmap, cursor); + if (!finished) return; + // Catch what changed while paging. + const after = ctx.syncState(jmap, 'Email'); + if (after.state && (await syncEmailDelta(ctx, jmap, after.state, after.target)) === 'stopped') return; + } + if (ctx.stopped()) return; + await syncThreads(ctx, jmap); + if (ctx.stopped()) return; + await backfillBodies(ctx, jmap); + }, +}; diff --git a/lib/store/sqlite-version.ts b/lib/store/sqlite-version.ts new file mode 100644 index 000000000..1b6ee653c --- /dev/null +++ b/lib/store/sqlite-version.ts @@ -0,0 +1,3 @@ +/** The vendored SQLite3MultipleCiphers build. Change it with scripts/fetch-sqlite3mc.mjs. */ +export const SQLITE3MC_VERSION = '2.6.1-3.54.0'; +export const SQLITE3MC_PUBLIC_DIR = `sqlite3mc/${SQLITE3MC_VERSION}`; diff --git a/lib/store/storage.ts b/lib/store/storage.ts new file mode 100644 index 000000000..e8cde1216 --- /dev/null +++ b/lib/store/storage.ts @@ -0,0 +1,73 @@ +/** + * The small storage interface the local store is written against. + * + * Synchronous on purpose: SQLite3MC in a worker and node:sqlite in the tests + * both are, and a transaction then can't hold an await by accident. A native + * or mobile adapter runs the engine next to its database, not across a + * message boundary. + */ + +export type SqlValue = string | number | bigint | null | Uint8Array; +export type SqlRow = Record; + +export interface StorageAdapter { + /** Runs one or more statements without parameters. */ + exec(sql: string): void; + /** Runs one statement. */ + run(sql: string, params?: readonly SqlValue[]): { changes: number; lastInsertRowid: number }; + all(sql: string, params?: readonly SqlValue[]): T[]; + get(sql: string, params?: readonly SqlValue[]): T | undefined; + /** Commits when fn returns, rolls back when it throws. Nested calls join the outer one. */ + transaction(fn: () => T): T; + close(): void; +} + +/** Shared by the adapters: BEGIN/COMMIT around fn, and a guard against async bodies. */ +export function runTransaction( + state: { depth: number }, + exec: (sql: string) => void, + fn: () => T, +): T { + if (state.depth > 0) return fn(); + exec('BEGIN IMMEDIATE'); + state.depth += 1; + try { + const result = fn(); + if (result && typeof (result as { then?: unknown }).then === 'function') { + throw new Error('A store transaction must be synchronous'); + } + exec('COMMIT'); + return result; + } catch (error) { + exec('ROLLBACK'); + throw error; + } finally { + state.depth -= 1; + } +} + +export interface Migration { + /** Unique, ascending. Ranges per area are listed in lib/store/area.ts. */ + id: number; + name: string; + sql: string; +} + +/** Applies what's missing, in id order, each in its own transaction. */ +export function migrate(db: StorageAdapter, migrations: readonly Migration[]): number[] { + db.exec('CREATE TABLE IF NOT EXISTS schema_migration (id INTEGER PRIMARY KEY, name TEXT NOT NULL, at INTEGER NOT NULL)'); + const done = new Set(db.all<{ id: number }>('SELECT id FROM schema_migration').map((row) => Number(row.id))); + const ids = new Set(); + const applied: number[] = []; + for (const migration of [...migrations].sort((a, b) => a.id - b.id)) { + if (ids.has(migration.id)) throw new Error(`Duplicate store migration id ${migration.id}`); + ids.add(migration.id); + if (done.has(migration.id)) continue; + db.transaction(() => { + db.exec(migration.sql); + db.run('INSERT INTO schema_migration (id, name, at) VALUES (?, ?, ?)', [migration.id, migration.name, Date.now()]); + }); + applied.push(migration.id); + } + return applied; +} diff --git a/lib/store/worker/host.ts b/lib/store/worker/host.ts new file mode 100644 index 000000000..25e63e270 --- /dev/null +++ b/lib/store/worker/host.ts @@ -0,0 +1,168 @@ +/** + * What runs in the owner tab's worker: one database and engine per account. + * The list of areas is the only thing that decides what the store answers. + */ + +import type { StoreArea, StoreEvent } from '../area'; +import { AccountEngine } from '../engine/account-engine'; +import { FetchTransport, type StoreSession } from '../engine/transport'; +import type { StorageAdapter } from '../storage'; +import type { OpenAccountParams, StoreRpc, StoreStatus } from './protocol'; + +export interface HostStorage { + open(file: string, keyHex: string): Promise; + remove(file: string): boolean; +} + +interface OpenAccount { + engine: AccountEngine; + db: StorageAdapter; + file: string; + auth: string | null; + pendingAuth: Array<(header: string | null) => void>; + session: StoreSession; +} + +export interface StoreHostOptions { + areas: readonly StoreArea[]; + storage: HostStorage; + emit(event: StoreEvent): void; + needAuth(account: string): void; +} + +export class StoreHost { + private readonly accounts = new Map(); + private online = true; + private readonly synced = new Map(); + + constructor(private readonly options: StoreHostOptions) {} + + async openAccount(params: OpenAccountParams): Promise { + const existing = this.accounts.get(params.account); + if (existing) { + if (params.session) this.setSession(params.account, params.session); + this.setAuth(params.account, params.auth); + existing.engine.setScope(params.scope); + existing.engine.setInvertedKeywordSort(params.invertedKeywordSort); + void existing.engine.requestSync(); + return; + } + const db = await this.options.storage.open(params.file, params.keyHex); + const session = params.session ?? AccountEngine.storedSession(db); + if (!session) { + db.close(); + throw new Error('No session for this account yet'); + } + const account: OpenAccount = { db, file: params.file, auth: params.auth, pendingAuth: [], session, engine: undefined as unknown as AccountEngine }; + const transport = new FetchTransport({ + session: () => account.session, + auth: () => account.auth, + refreshAuth: () => new Promise((resolve) => { + account.pendingAuth.push(resolve); + this.options.needAuth(params.account); + }), + }); + account.engine = new AccountEngine({ + account: params.account, + db, + session, + transport, + areas: this.options.areas, + scope: params.scope, + invertedKeywordSort: params.invertedKeywordSort, + emit: (event) => { + if (event.type === 'synced') this.synced.set(event.account, event.at); + this.options.emit(event); + }, + }); + account.engine.setOnline(this.online); + this.accounts.set(params.account, account); + void account.engine.requestSync(); + } + + setAuth(account: string, auth: string | null): void { + const open = this.accounts.get(account); + if (!open) return; + open.auth = auth; + for (const resolve of open.pendingAuth.splice(0)) resolve(auth); + } + + setSession(account: string, session: StoreSession): void { + const open = this.accounts.get(account); + if (!open) return; + open.session = session; + open.engine.setSession(session); + } + + setScope(account: string, scope: Parameters[0]): void { + const open = this.accounts.get(account); + if (!open) return; + open.engine.setScope(scope); + void open.engine.requestSync(); + } + + async closeAccount(account: string): Promise { + const open = this.accounts.get(account); + if (!open) return; + this.accounts.delete(account); + await open.engine.close(); + for (const resolve of open.pendingAuth.splice(0)) resolve(null); + open.db.close(); + } + + async removeAccount(account: string, file: string): Promise { + await this.closeAccount(account); + this.synced.delete(account); + return this.options.storage.remove(file); + } + + async closeAll(): Promise { + await Promise.all([...this.accounts.keys()].map((account) => this.closeAccount(account))); + } + + /** The calls other tabs can make. */ + async call(method: keyof StoreRpc, params: unknown): Promise { + switch (method) { + case 'answer': { + const { account, calls } = params as Parameters[0]; + const open = this.accounts.get(account); + if (!open) throw new Error('The local store is not open for this account'); + return open.engine.answer(calls); + } + case 'sync': { + const { account } = params as Parameters[0]; + await this.accounts.get(account)?.engine.requestSync(); + return undefined; + } + case 'noteStateChange': { + const { account, changed } = params as Parameters[0]; + this.accounts.get(account)?.engine.noteStateChange(changed); + return undefined; + } + case 'setOnline': { + const { online } = params as Parameters[0]; + this.online = online; + for (const open of this.accounts.values()) open.engine.setOnline(online); + return undefined; + } + case 'status': { + const { account } = params as Parameters[0]; + return this.status(account); + } + default: + throw new Error(`Unknown store call ${String(method)}`); + } + } + + private status(account: string): StoreStatus | null { + const open = this.accounts.get(account); + if (!open) return null; + return { + account, + open: true, + emails: Number(open.db.get<{ n: number }>('SELECT COUNT(*) AS n FROM email')?.n ?? 0), + bodies: Number(open.db.get<{ n: number }>('SELECT COUNT(*) AS n FROM email_body')?.n ?? 0), + lastSyncedAt: this.synced.get(account) ?? null, + }; + } +} diff --git a/lib/store/worker/protocol.ts b/lib/store/worker/protocol.ts new file mode 100644 index 000000000..8b3257648 --- /dev/null +++ b/lib/store/worker/protocol.ts @@ -0,0 +1,76 @@ +/** Messages between the page and the store worker. */ + +import type { StoreEvent, StoreScope } from '../area'; +import type { JmapResponse, MethodCall, StoreSession } from '../engine/transport'; + +export const RPC_CHANNEL = 'bulwark-store-rpc'; +export const OWNER_LOCK = 'bulwark-store-owner'; + +export interface OpenAccountParams { + account: string; + /** `/a.db`, no account data: the name is plaintext in the OPFS slot. */ + file: string; + /** 32 bytes in hex. Only ever sent to this tab's own worker, never over the channel. */ + keyHex: string; + session: StoreSession | null; + scope: StoreScope; + invertedKeywordSort: boolean; + /** The page's current Authorization header. */ + auth: string | null; +} + +/** What other tabs can ask for, over the channel. */ +export interface StoreRpc { + answer(params: { account: string; calls: MethodCall[] }): JmapResponse | null; + sync(params: { account: string }): void; + noteStateChange(params: { account: string; changed: Record> }): void; + setOnline(params: { online: boolean }): void; + status(params: { account: string }): StoreStatus | null; +} + +export interface StoreStatus { + account: string; + open: boolean; + emails: number; + bodies: number; + lastSyncedAt: number | null; +} + +export type RpcRequest = { + kind: 'rpc'; + id: string; + from: string; + method: K; + params: Parameters[0]; +}; + +export type RpcReply = { + kind: 'reply'; + id: string; + to: string; + result?: unknown; + error?: string; +}; + +export type ChannelMessage = + | RpcRequest + | RpcReply + | { kind: 'event'; event: StoreEvent } + | { kind: 'owner-changed'; owner: string | null }; + +/** From this tab's page to its own worker, never on the channel. */ +export type PageToWorker = + | { kind: 'init'; clientId: string; moduleUrl: string } + | { kind: 'open'; id: string; params: OpenAccountParams } + | { kind: 'close-account'; id: string; account: string } + | { kind: 'remove-account'; id: string; account: string; file: string } + | { kind: 'set-auth'; account: string; auth: string | null } + | { kind: 'set-session'; account: string; session: StoreSession } + | { kind: 'set-scope'; account: string; scope: StoreScope } + | { kind: 'release'; id: string }; + +export type WorkerToPage = + | { kind: 'owner' } + | { kind: 'result'; id: string; result?: unknown; error?: string; errorName?: string } + | { kind: 'need-auth'; account: string } + | { kind: 'event'; event: StoreEvent }; diff --git a/lib/store/worker/worker.ts b/lib/store/worker/worker.ts new file mode 100644 index 000000000..003340c82 --- /dev/null +++ b/lib/store/worker/worker.ts @@ -0,0 +1,147 @@ +/** + * The store worker. Every tab has one, parked on the owner lock, and only the + * holder loads the wasm and touches OPFS. The lock is held for the worker's + * whole life, so handing over means: close, pause the VFS, then let go. + * + * Built to public/store-worker.js by scripts/build-store-worker.mjs. + */ + +import { WasmStorage } from '../adapters/wasm'; +import { STORE_AREAS } from '../areas'; +import { RPC_CHANNEL, OWNER_LOCK, type ChannelMessage, type PageToWorker, type WorkerToPage } from './protocol'; +import { StoreHost } from './host'; + +const scope = self as unknown as { + postMessage(message: WorkerToPage): void; + addEventListener(type: 'message', listener: (event: MessageEvent) => void): void; + close(): void; +}; + +let clientId = ''; +let moduleUrl = ''; +let storage: WasmStorage | null = null; +let host: StoreHost | null = null; +let channel: BroadcastChannel | null = null; +let releaseLock: (() => void) | null = null; + +function reply(id: string, result?: unknown): void { + scope.postMessage({ kind: 'result', id, result }); +} + +function replyError(id: string, error: unknown): void { + scope.postMessage({ + kind: 'result', + id, + error: error instanceof Error ? error.message : String(error), + errorName: error instanceof Error ? error.name : undefined, + }); +} + +function requireHost(): StoreHost { + if (!host) throw new Error('This tab does not own the local store'); + return host; +} + +async function becomeOwner(): Promise { + storage = await WasmStorage.install(moduleUrl); + host = new StoreHost({ + areas: STORE_AREAS, + storage, + emit: (event) => { + scope.postMessage({ kind: 'event', event }); + channel?.postMessage({ kind: 'event', event } satisfies ChannelMessage); + }, + needAuth: (account) => scope.postMessage({ kind: 'need-auth', account }), + }); + channel = new BroadcastChannel(RPC_CHANNEL); + channel.onmessage = async (event: MessageEvent) => { + const message = event.data; + if (message.kind !== 'rpc') return; + try { + const result = await requireHost().call(message.method, message.params); + channel?.postMessage({ kind: 'reply', id: message.id, to: message.from, result } satisfies ChannelMessage); + } catch (error) { + channel?.postMessage({ + kind: 'reply', + id: message.id, + to: message.from, + error: error instanceof Error ? error.message : String(error), + } satisfies ChannelMessage); + } + }; + channel.postMessage({ kind: 'owner-changed', owner: clientId } satisfies ChannelMessage); + scope.postMessage({ kind: 'owner' }); +} + +async function release(): Promise { + // The order matters: the OPFS handles have to be free before the lock is. + channel?.postMessage({ kind: 'owner-changed', owner: null } satisfies ChannelMessage); + await host?.closeAll(); + host = null; + storage?.pause(); + storage = null; + channel?.close(); + channel = null; + releaseLock?.(); + releaseLock = null; +} + +scope.addEventListener('message', async (event) => { + const message = event.data; + switch (message.kind) { + case 'init': + clientId = message.clientId; + moduleUrl = message.moduleUrl; + void navigator.locks.request(OWNER_LOCK, () => new Promise((resolve) => { + releaseLock = resolve; + becomeOwner().catch((error) => { + scope.postMessage({ + kind: 'event', + event: { type: 'error', account: '', area: 'store', message: error instanceof Error ? error.message : String(error) }, + }); + resolve(); + }); + })); + break; + case 'open': + try { + await requireHost().openAccount(message.params); + reply(message.id); + } catch (error) { + replyError(message.id, error); + } + break; + case 'close-account': + try { + await requireHost().closeAccount(message.account); + reply(message.id); + } catch (error) { + replyError(message.id, error); + } + break; + case 'remove-account': + try { + reply(message.id, await requireHost().removeAccount(message.account, message.file)); + } catch (error) { + replyError(message.id, error); + } + break; + case 'set-auth': + host?.setAuth(message.account, message.auth); + break; + case 'set-session': + host?.setSession(message.account, message.session); + break; + case 'set-scope': + host?.setScope(message.account, message.scope); + break; + case 'release': + try { + await release(); + reply(message.id); + } catch (error) { + replyError(message.id, error); + } + break; + } +}); diff --git a/next.config.ts b/next.config.ts index e57a5f3af..1ce8a7714 100644 --- a/next.config.ts +++ b/next.config.ts @@ -140,6 +140,11 @@ const nextConfig: NextConfig = { { key: "Cache-Control", value: "public, max-age=31536000, immutable" }, ], }, + { + // The local store's database engine, in a folder named after its version. + source: "/sqlite3mc/:path*", + headers: [{ key: "Cache-Control", value: "public, max-age=31536000, immutable" }], + }, // The upload routes proxy.ts's matcher leaves out, with the // headers it sets on every other API response. ...["/api/webdav", "/api/plugin-proxy", "/api/wopi/files/:fileId/contents"].map((source) => ({ diff --git a/package.json b/package.json index 9c1abb8ac..52adefb78 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "typescript" ], "scripts": { - "dev": "node scripts/build-pst-worker.mjs && node scripts/build-plugin-sandbox.mjs --watch -- next dev --turbopack", - "build": "node scripts/build-pst-worker.mjs && node scripts/build-plugin-sandbox.mjs && next build --turbopack && node scripts/standalone-guard.mjs", + "dev": "node scripts/build-pst-worker.mjs && node scripts/build-store-worker.mjs && node scripts/build-plugin-sandbox.mjs --watch -- next dev --turbopack", + "build": "node scripts/build-pst-worker.mjs && node scripts/build-store-worker.mjs && node scripts/build-plugin-sandbox.mjs && next build --turbopack && node scripts/standalone-guard.mjs", "build:lite": "node scripts/lite/build.mjs", "lite:serve": "node scripts/lite/serve.mjs out 4173", "test:lite-smoke": "playwright test -c scripts/lite/smoke/playwright.config.ts", diff --git a/proxy.ts b/proxy.ts index eb767eb03..4e47be5e8 100644 --- a/proxy.ts +++ b/proxy.ts @@ -86,7 +86,7 @@ export const config = { // /en/mail/folder/inbox/statement.pdf a real signed-in page, and it rendered // without a CSP or any other security header (GHSA-xvjh-v9c6-qcvc). Only what // is genuinely static may skip locale routing and the headers. -const STATIC_ASSET_PATTERN = /^\/[^/]+\.[^/]+$|^\/(?:branding|notification|demo|plugin-sandbox-assets)\//; +const STATIC_ASSET_PATTERN = /^\/[^/]+\.[^/]+$|^\/(?:branding|notification|demo|plugin-sandbox-assets|sqlite3mc)\//; export function isStaticAssetPath(pathname: string): boolean { return STATIC_ASSET_PATTERN.test(pathname); diff --git a/scripts/build-store-worker.mjs b/scripts/build-store-worker.mjs new file mode 100644 index 000000000..835bc10e6 --- /dev/null +++ b/scripts/build-store-worker.mjs @@ -0,0 +1,46 @@ +// Builds the local store's worker (lib/store/worker/worker.ts) into +// public/store-worker.js and copies the vendored SQLite3MultipleCiphers build +// next to it, in a folder named after its version: emscripten looks for the +// literal name sqlite3.wasm, so the files can't have hashed names. +// Both go through proxy.ts's static asset rule, which keeps the page CSP off +// them: wasm compiles under the worker's own response headers. +// +// node scripts/build-store-worker.mjs (npm run build, npm run dev) + +import { build } from 'esbuild'; +import { copyFileSync, mkdirSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { SQLITE3MC, SQLITE3MC_VENDOR_DIR, verifyVendored } from './fetch-sqlite3mc.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +export const STORE_WORKER_OUTFILE = path.join(root, 'public', 'store-worker.js'); +export const SQLITE3MC_PUBLIC_DIR = path.join(root, 'public', 'sqlite3mc', SQLITE3MC.version); + +export async function buildStoreWorker({ outfile = STORE_WORKER_OUTFILE, publicDir = SQLITE3MC_PUBLIC_DIR } = {}) { + verifyVendored(); + mkdirSync(publicDir, { recursive: true }); + for (const name of [...Object.keys(SQLITE3MC.files), 'LICENSE']) { + copyFileSync(path.join(SQLITE3MC_VENDOR_DIR, name), path.join(publicDir, name)); + } + const result = await build({ + entryPoints: [path.join(root, 'lib/store/worker/worker.ts')], + bundle: true, + format: 'esm', + platform: 'browser', + target: 'es2022', + outfile, + minify: true, + legalComments: 'none', + alias: { '@': root }, + define: { 'process.env.NODE_ENV': '"production"' }, + metafile: true, + logLevel: 'warning', + }); + return { bytes: Object.values(result.metafile.outputs).reduce((n, o) => n + o.bytes, 0) }; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const { bytes } = await buildStoreWorker(); + console.log(`[build-store-worker] ${path.relative(root, STORE_WORKER_OUTFILE)} ${bytes}`); +} diff --git a/scripts/fetch-sqlite3mc.mjs b/scripts/fetch-sqlite3mc.mjs new file mode 100644 index 000000000..f119f3df7 --- /dev/null +++ b/scripts/fetch-sqlite3mc.mjs @@ -0,0 +1,79 @@ +// Fetches the pinned SQLite3MultipleCiphers WASM build into vendor/sqlite3mc/. +// Run it by hand when moving to a new version, and commit the result, so a +// Docker build needs no access to GitHub. Every download is checked against the +// pinned sha256: a changed artifact fails here. +// +// node scripts/fetch-sqlite3mc.mjs downloads and verifies +// node scripts/fetch-sqlite3mc.mjs --check verifies what is committed +// +// Fixes for bugs #291-#294 are on trunk, not in a release yet. Check the rules +// in lib/store/adapters/wasm.ts when moving to a newer build, and never go below 2.6.0. + +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { unzipSync } from 'fflate'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +export const SQLITE3MC = { + version: '2.6.1-3.54.0', + tag: 'v2.6.1', + zip: 'sqlite3mc-2.6.1-sqlite-3.54.0-wasm.zip', + zipSha256: 'aba121490fd6123bcbef8404ee26df9b26c0301063781e2aa31f05aed6f48384', + innerDir: 'sqlite3mc-wasm-3540000/jswasm', + files: { + 'sqlite3.mjs': '5308c7cc857408697c657a8d314e647a6f31c4aee67974ab8efd80c1a07161e0', + 'sqlite3.wasm': '0b5a4bfff35f1ec87bf6f68ea293f425b86cbd661b77628a955cb827c9fbf524', + }, +}; + +export const SQLITE3MC_VENDOR_DIR = path.join(root, 'vendor', 'sqlite3mc', SQLITE3MC.version); + +function sha256(data) { + return createHash('sha256').update(data).digest('hex'); +} + +/** Throws unless the committed files match their pins. */ +export function verifyVendored(dir = SQLITE3MC_VENDOR_DIR) { + for (const [name, expected] of Object.entries(SQLITE3MC.files)) { + const actual = sha256(readFileSync(path.join(dir, name))); + if (actual !== expected) throw new Error(`[sqlite3mc] ${name}: expected ${expected}, got ${actual}`); + } + readFileSync(path.join(dir, 'LICENSE')); +} + +async function download(url) { + const response = await fetch(url); + if (!response.ok) throw new Error(`[sqlite3mc] ${url}: ${response.status}`); + return Buffer.from(await response.arrayBuffer()); +} + +async function fetchAndVendor() { + const base = `https://github.com/utelle/SQLite3MultipleCiphers/releases/download/${SQLITE3MC.tag}`; + const zip = await download(`${base}/${SQLITE3MC.zip}`); + const actual = sha256(zip); + if (actual !== SQLITE3MC.zipSha256) throw new Error(`[sqlite3mc] zip: expected ${SQLITE3MC.zipSha256}, got ${actual}`); + const entries = unzipSync(zip); + mkdirSync(SQLITE3MC_VENDOR_DIR, { recursive: true }); + for (const name of Object.keys(SQLITE3MC.files)) { + const entry = entries[`${SQLITE3MC.innerDir}/${name}`]; + if (!entry) throw new Error(`[sqlite3mc] ${name} is not in the zip`); + writeFileSync(path.join(SQLITE3MC_VENDOR_DIR, name), entry); + } + // The zip has no licence file. + const license = await download(`https://raw.githubusercontent.com/utelle/SQLite3MultipleCiphers/${SQLITE3MC.tag}/LICENSE`); + writeFileSync(path.join(SQLITE3MC_VENDOR_DIR, 'LICENSE'), license); + verifyVendored(); + console.log(`[fetch-sqlite3mc] vendored ${SQLITE3MC.version}`); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + if (process.argv.includes('--check')) { + verifyVendored(); + console.log('[fetch-sqlite3mc] ok'); + } else { + await fetchAndVendor(); + } +} diff --git a/scripts/lite/lib.mjs b/scripts/lite/lib.mjs index 559c09eca..18bdc6cdf 100644 --- a/scripts/lite/lib.mjs +++ b/scripts/lite/lib.mjs @@ -43,6 +43,9 @@ export const LITE_REMOVED_PATHS = [ "app/(main)/protocol", "e2e", "integration", + // The local store: Lite has no offline mode. A dev checkout may hold these. + "public/store-worker.js", + "public/sqlite3mc", ]; /** Test trees are pruned too: they import the routes removed above. */ diff --git a/vendor/sqlite3mc/2.6.1-3.54.0/LICENSE b/vendor/sqlite3mc/2.6.1-3.54.0/LICENSE new file mode 100644 index 000000000..2fff29de4 --- /dev/null +++ b/vendor/sqlite3mc/2.6.1-3.54.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2026 Ulrich Telle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/sqlite3mc/2.6.1-3.54.0/sqlite3.mjs b/vendor/sqlite3mc/2.6.1-3.54.0/sqlite3.mjs new file mode 100644 index 000000000..4ea31fb03 --- /dev/null +++ b/vendor/sqlite3mc/2.6.1-3.54.0/sqlite3.mjs @@ -0,0 +1,14727 @@ +/* @preserve +** +** LICENSE for the sqlite3 WebAssembly/JavaScript APIs. +** +** This bundle (typically released as sqlite3.js or sqlite3.mjs) +** 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. +*/ +/* @preserve +** This code was built from sqlite3 version... +** +** SQLITE_VERSION "3.54.0" +** SQLITE_VERSION_NUMBER 3054000 +** SQLITE_SOURCE_ID "2026-10-09 15:46:58 be8d059e9a49089ab2dce5ed26dd87aaf598fdc5fbe0b107c0dd758464bcd5e3" +** +** Emscripten SDK: 6.0.3 +*/ + + + + + +async function sqlite3InitModule(moduleArg = {}) { + var Module = moduleArg; + + + + + + + + + + + + + + + + + + + + + +var ENVIRONMENT_IS_WEB = !!globalThis.window; +var ENVIRONMENT_IS_WORKER = !!globalThis.WorkerGlobalScope; + + +var ENVIRONMENT_IS_NODE = globalThis.process?.versions?.node && globalThis.process?.type != 'renderer'; +var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; + + + + + + +(function(Module){ + const sIMS = + globalThis.sqlite3InitModuleState + || Object.assign(Object.create(null),{ + + debugModule: function(){ + console.warn("globalThis.sqlite3InitModuleState is missing",arguments); + } + }); + delete globalThis.sqlite3InitModuleState; + sIMS.debugModule('pre-js.js sqlite3InitModuleState =',sIMS); + + + Module['locateFile'] = function(path, prefix) { + if( this.emscriptenLocateFile instanceof Function ){ + + return this.emscriptenLocateFile(path, prefix); + } + return new URL(path, import.meta.url).href; + }.bind(sIMS); + + + Module['instantiateWasm'] = function callee(imports, onSuccess){ + if( this.emscriptenInstantiateWasm instanceof Function ){ + + return this.emscriptenInstantiateWasm(imports, onSuccess); + } + const sims = this; + const finalThen = (arg)=>{ + arg.imports = imports; + sims.instantiateWasm = arg; + onSuccess(arg.instance, arg.module); + }; + const uri = Module.locateFile( + sims.wasmFilename, ( + ('undefined'===typeof scriptDirectory) + ? "" : scriptDirectory) + ); + sims.debugModule("instantiateWasm() uri =", uri, "sIMS =",this); + const wfetch = ()=>fetch(uri, {credentials: 'same-origin'}); + const loadWasm = WebAssembly.instantiateStreaming + ? async ()=> + WebAssembly + .instantiateStreaming(wfetch(), imports) + .then(finalThen) + : async ()=> + wfetch() + .then(response => response.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, imports)) + .then(finalThen) + return loadWasm(); + }.bind(sIMS); +})(Module); + + + + +var programArgs = []; +var thisProgram = './this.program'; +var quit_ = (status, toThrow) => { + throw toThrow; +}; + +var _scriptName = import.meta.url; + + +var scriptDirectory = ''; +function locateFile(path) { + if (Module['locateFile']) { + return Module['locateFile'](path, scriptDirectory); + } + return scriptDirectory + path; +} + + +var readAsync, readBinary; + + + + +if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { + try { + scriptDirectory = new URL('.', _scriptName).href; + } catch { + + + } + + { + +if (ENVIRONMENT_IS_WORKER) { + readBinary = (url) => { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + xhr.responseType = 'arraybuffer'; + xhr.send(null); + return new Uint8Array((xhr.response)); + }; + } + + readAsync = async (url) => { + var response = await fetch(url, { credentials: 'same-origin' }); + if (response.ok) { + return response.arrayBuffer(); + } + throw new Error(response.status + ' : ' + response.url); + }; + + } +} else +{ +} + +var out = console.log.bind(console); +var err = console.error.bind(console); + + + + + + + + + + + + + + +var wasmBinary; + + + + + + + + + +var ABORT = false; + + + + +var EXITSTATUS; + + + + + + +function assert(condition, text) { + if (!condition) { + + + + abort(text); + } +} + + +var isFileURI = (filename) => filename.startsWith('file://'); + + + + +class EmscriptenEH {} + +class EmscriptenSjLj extends EmscriptenEH {} + + + + + + +var runtimeInitialized = false; + + + + + +function getMemoryBuffer() { + return wasmMemory.buffer; +} + +function updateMemoryViews() { + + + if (HEAP8?.buffer?.resizable) return; + var b = getMemoryBuffer(); + HEAP8 = new Int8Array(b); + HEAP16 = new Int16Array(b); + HEAPU8 = new Uint8Array(b); + HEAPU16 = new Uint16Array(b); + HEAP32 = new Int32Array(b); + HEAPU32 = new Uint32Array(b); + HEAPF32 = new Float32Array(b); + HEAPF64 = new Float64Array(b); + HEAP64 = new BigInt64Array(b); + HEAPU64 = new BigUint64Array(b); +} + + + + + + + +function initMemory() { + + + + if (Module['wasmMemory']) { + wasmMemory = Module['wasmMemory']; + } else + { + var INITIAL_MEMORY = Module['INITIAL_MEMORY'] || 8388608; + + + wasmMemory = new WebAssembly.Memory({ + 'initial': INITIAL_MEMORY / 65536, + + + + + + 'maximum': 32768, + }); + } + + updateMemoryViews(); +} + + + + + + +function preRun() { + var preRun = Module['preRun']; + if (preRun) { + if (typeof preRun == 'function') preRun = [preRun]; + onPreRuns.push(...preRun); + } + + callRuntimeCallbacks(onPreRuns); + +} + +function initRuntime() { + runtimeInitialized = true; + + + if (!Module['noFSInit'] && !FS.initialized) FS.init(); +TTY.init(); + + + wasmExports['__wasm_call_ctors'](); + + + FS.ignorePermissions = false; + + +} + +function postRun() { + + var postRun = Module['postRun']; + if (postRun) { + if (typeof postRun == 'function') postRun = [postRun]; + onPostRuns.push(...postRun); + } + + + callRuntimeCallbacks(onPostRuns); + +} + + +function abort(what) { + Module['onAbort']?.(what); + + what = `Aborted(${what})`; + + + err(what); + + ABORT = true; + + what += '. Build with -sASSERTIONS for more info.'; + + + + + + + + + + + + + + + + var e = new WebAssembly.RuntimeError(what); + + + + + throw e; +} + +var wasmBinaryFile; + +function findWasmBinary() { + + if (Module['locateFile']) { + return locateFile('sqlite3.wasm'); + } + + + return new URL('sqlite3.wasm', import.meta.url).href; + +} + +function getBinarySync(file) { + if (readBinary) { + return readBinary(file); + } + + + throw 'both async and sync fetching of the wasm failed'; +} + +async function getWasmBinary(binaryFile) { + + if (!wasmBinary) { + + try { + var response = await readAsync(binaryFile); + return new Uint8Array(response); + } catch { + + } + } + + + return getBinarySync(binaryFile); +} + +async function instantiateArrayBuffer(binaryFile, imports) { + try { + var binary = await getWasmBinary(binaryFile); + var instance = await WebAssembly.instantiate(binary, imports); + return instance; + } catch (reason) { + err(`failed to asynchronously prepare wasm: ${reason}`); + + abort(reason); + } +} + +async function instantiateAsync(binary, binaryFile, imports) { + if (!binary + ) { + try { + var response = fetch(binaryFile, { credentials: 'same-origin' }); + var instantiationResult = await WebAssembly.instantiateStreaming(response, imports); + return instantiationResult; + } catch (reason) { + + + err(`wasm streaming compile failed: ${reason}`); + err('falling back to ArrayBuffer instantiation'); + + }; + } + return instantiateArrayBuffer(binaryFile, imports); +} + +function getWasmImports() { + + var imports = { + 'env': wasmImports, + 'wasi_snapshot_preview1': wasmImports, + }; + return imports; +} + + + +async function createWasm() { + + + + function receiveInstance(instance) { + wasmExports = instance.exports; + + assignWasmExports(wasmExports); + + return wasmExports; + } + + + function receiveInstantiationResult(result) { + + + + + return receiveInstance(result['instance']); + } + + var info = getWasmImports(); + + + + + + + + var instantiateWasm = Module['instantiateWasm']; + if (instantiateWasm) { + return new Promise((resolve) => { + instantiateWasm(info, (inst) => resolve(receiveInstance(inst))); + }); + } + + wasmBinaryFile ??= findWasmBinary(); + var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info); + var exports = receiveInstantiationResult(result); + return exports; +} + + + + + + + class ExitStatus { + name = 'ExitStatus'; + constructor(status) { + this.message = `Program terminated with exit(${status})`; + this.status = status; + } + } + + + var HEAP16; + + + var HEAP32; + + + var HEAP64; + + + var HEAP8; + + + var HEAPF32; + + + var HEAPF64; + + + var HEAPU16; + + + var HEAPU32; + + + var HEAPU64; + + + var HEAPU8; + + var callRuntimeCallbacks = (callbacks) => { + while (callbacks.length > 0) { + + callbacks.shift()(Module); + } + }; + var onPostRuns = []; + var addOnPostRun = (cb) => onPostRuns.push(cb); + + var onPreRuns = []; + var addOnPreRun = (cb) => onPreRuns.push(cb); + + + + + function getValue(ptr, type = 'i8') { + if (type.endsWith('*')) type = '*'; + switch (type) { + case 'i1': return HEAP8[ptr]; + case 'i8': return HEAP8[ptr]; + case 'i16': return HEAP16[((ptr)>>1)]; + case 'i32': return HEAP32[((ptr)>>2)]; + case 'i64': return HEAP64[((ptr)>>3)]; + case 'float': return HEAPF32[((ptr)>>2)]; + case 'double': return HEAPF64[((ptr)>>3)]; + case '*': return HEAPU32[((ptr)>>2)]; + default: abort(`invalid type for getValue: ${type}`); + } + } + + var noExitRuntime = true; + + + + function setValue(ptr, value, type = 'i8') { + if (type.endsWith('*')) type = '*'; + switch (type) { + case 'i1': HEAP8[ptr] = value; break; + case 'i8': HEAP8[ptr] = value; break; + case 'i16': HEAP16[((ptr)>>1)] = value; break; + case 'i32': HEAP32[((ptr)>>2)] = value; break; + case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; + case 'float': HEAPF32[((ptr)>>2)] = value; break; + case 'double': HEAPF64[((ptr)>>3)] = value; break; + case '*': HEAPU32[((ptr)>>2)] = value; break; + default: abort(`invalid type for setValue: ${type}`); + } + } + + var stackRestore = (val) => __emscripten_stack_restore(val); + + var stackSave = () => _emscripten_stack_get_current(); + + var wasmMemory; + + var PATH = { + isAbs:(path) => path.charAt(0) === '/', + splitPath:(filename) => { + var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + return splitPathRe.exec(filename).slice(1); + }, + normalizeArray:(parts, allowAboveRoot) => { + + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + if (allowAboveRoot) { + for (; up; up--) { + parts.unshift('..'); + } + } + return parts; + }, + normalize:(path) => { + var isAbsolute = PATH.isAbs(path), + trailingSlash = path.slice(-1) === '/'; + + path = PATH.normalizeArray(path.split('/').filter((p) => !!p), !isAbsolute).join('/'); + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + return (isAbsolute ? '/' : '') + path; + }, + dirname:(path) => { + var result = PATH.splitPath(path), + root = result[0], + dir = result[1]; + if (!root && !dir) { + + return '.'; + } + if (dir) { + + dir = dir.slice(0, -1); + } + return root + dir; + }, + basename:(path) => path && path.match(/([^\/]+|\/)\/*$/)[1], +join:(...paths) => PATH.normalize(paths.join('/')), +join2:(l, r) => PATH.normalize(l + '/' + r), +}; + +var initRandomFill = () => { + + return (view) => (crypto.getRandomValues(view), 0); + }; +var randomFill = (view) => (randomFill = initRandomFill())(view); + + + +var PATH_FS = { +resolve:(...args) => { + var resolvedPath = '', + resolvedAbsolute = false; + for (var i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? args[i] : FS.cwd(); + + if (typeof path != 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + return ''; + } + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = PATH.isAbs(path); + } + + + resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter((p) => !!p), !resolvedAbsolute).join('/'); + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; + }, +relative:(from, to) => { + from = PATH_FS.resolve(from).slice(1); + to = PATH_FS.resolve(to).slice(1); + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + return outputParts.join('/'); + }, +}; + + +var UTF8Decoder = new TextDecoder(); + + + + var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { + var maxIdx = idx + maxBytesToRead; + if (ignoreNul) return maxIdx; + + + + + while (heapOrArray[idx] && !(idx >= maxIdx)) ++idx; + return idx; + }; + + + var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead, ignoreNul) => { + + var endPtr = findStringEnd(heapOrArray, idx, maxBytesToRead, ignoreNul); + + return UTF8Decoder.decode(heapOrArray.buffer ? heapOrArray.subarray(idx, endPtr) : new Uint8Array(heapOrArray.slice(idx, endPtr))); + }; + + var FS_stdin_getChar_buffer = []; + + var lengthBytesUTF8 = (str) => { + var len = 0; + for (var i = 0; i < str.length; ++i) { + + + + + var c = str.charCodeAt(i); + if (c <= 0x7F) { + len++; + } else if (c <= 0x7FF) { + len += 2; + } else if (c >= 0xD800 && c <= 0xDFFF) { + len += 4; ++i; + } else { + len += 3; + } + } + return len; + }; + + var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + + + if (!(maxBytesToWrite > 0)) + return 0; + + var startIdx = outIdx; + var endIdx = outIdx + maxBytesToWrite - 1; + for (var i = 0; i < str.length; ++i) { + + + + var u = str.codePointAt(i); + if (u <= 0x7F) { + if (outIdx >= endIdx) break; + heap[outIdx++] = u; + } else if (u <= 0x7FF) { + if (outIdx + 1 >= endIdx) break; + heap[outIdx++] = 0xC0 | (u >> 6); + heap[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + if (outIdx + 2 >= endIdx) break; + heap[outIdx++] = 0xE0 | (u >> 12); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + } else { + if (outIdx + 3 >= endIdx) break; + heap[outIdx++] = 0xF0 | (u >> 18); + heap[outIdx++] = 0x80 | ((u >> 12) & 63); + heap[outIdx++] = 0x80 | ((u >> 6) & 63); + heap[outIdx++] = 0x80 | (u & 63); + + + i++; + } + } + + heap[outIdx] = 0; + return outIdx - startIdx; + }; + + var intArrayFromString = (stringy, dontAddNull, length) => { + var len = length > 0 ? length : lengthBytesUTF8(stringy)+1; + var u8array = new Array(len); + var numBytesWritten = stringToUTF8Array(stringy, u8array, 0, u8array.length); + if (dontAddNull) u8array.length = numBytesWritten; + return u8array; + }; + var FS_stdin_getChar = () => { + if (!FS_stdin_getChar_buffer.length) { + var result = null; + if (globalThis.window?.prompt) { + + result = window.prompt('Input: '); + if (result !== null) { + result += '\n'; + } + } else + {} + if (!result) { + return null; + } + FS_stdin_getChar_buffer = intArrayFromString(result, true); + } + return FS_stdin_getChar_buffer.shift(); + }; + var TTY = { + ttys:[], + init() { + + + + + + + + + }, + shutdown() { + + + + + + + + + + }, + register(dev, ops) { + TTY.ttys[dev] = { input: [], output: [], ops: ops }; + FS.registerDevice(dev, TTY.stream_ops); + }, + stream_ops:{ + open(stream) { + var tty = TTY.ttys[stream.node.rdev]; + if (!tty) { + throw new FS.ErrnoError(43); + } + stream.tty = tty; + stream.seekable = false; + }, + close(stream) { + + stream.tty.ops.fsync(stream.tty); + }, + fsync(stream) { + stream.tty.ops.fsync(stream.tty); + }, + read(stream, buffer, offset, length, pos ) { + if (!stream.tty || !stream.tty.ops.get_char) { + throw new FS.ErrnoError(60); + } + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = stream.tty.ops.get_char(stream.tty); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset+i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + if (!stream.tty || !stream.tty.ops.put_char) { + throw new FS.ErrnoError(60); + } + try { + for (var i = 0; i < length; i++) { + stream.tty.ops.put_char(stream.tty, buffer[offset+i]); + } + } catch (e) { + throw new FS.ErrnoError(29); + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + }, + }, + default_tty_ops:{ + get_char(tty) { + return FS_stdin_getChar(); + }, + put_char(tty, val) { + if (val === null || val === 10) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + out(UTF8ArrayToString(tty.output)); + tty.output = []; + } + }, + ioctl_tcgets(tty) { + + return { + c_iflag: 25856, + c_oflag: 5, + c_cflag: 191, + c_lflag: 35387, + c_cc: [ + 0x03, 0x1c, 0x7f, 0x15, 0x04, 0x00, 0x01, 0x00, 0x11, 0x13, 0x1a, 0x00, + 0x12, 0x0f, 0x17, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + }; + }, + ioctl_tcsets(tty, optional_actions, data) { + + return 0; + }, + ioctl_tiocgwinsz(tty) { + return [24, 80]; + }, + }, + default_tty1_ops:{ + put_char(tty, val) { + if (val === null || val === 10) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } else { + if (val != 0) tty.output.push(val); + } + }, + fsync(tty) { + if (tty.output?.length > 0) { + err(UTF8ArrayToString(tty.output)); + tty.output = []; + } + }, + }, + }; + + + var zeroMemory = (ptr, size) => HEAPU8.fill(0, ptr, ptr + size); + + var alignMemory = (size, alignment) => { + return Math.ceil(size / alignment) * alignment; + }; + var mmapAlloc = (size) => { + size = alignMemory(size, 65536); + var ptr = _emscripten_builtin_memalign(65536, size); + if (ptr) zeroMemory(ptr, size); + return ptr; + }; + var MEMFS = { + ops_table:null, + mount(mount) { + return MEMFS.createNode(null, '/', 16895, 0); + }, + createNode(parent, name, mode, dev) { + if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { + + throw new FS.ErrnoError(63); + } + MEMFS.ops_table ||= { + dir: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + lookup: MEMFS.node_ops.lookup, + mknod: MEMFS.node_ops.mknod, + rename: MEMFS.node_ops.rename, + unlink: MEMFS.node_ops.unlink, + rmdir: MEMFS.node_ops.rmdir, + readdir: MEMFS.node_ops.readdir, + symlink: MEMFS.node_ops.symlink + }, + stream: { + llseek: MEMFS.stream_ops.llseek + } + }, + file: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: { + llseek: MEMFS.stream_ops.llseek, + read: MEMFS.stream_ops.read, + write: MEMFS.stream_ops.write, + mmap: MEMFS.stream_ops.mmap, + msync: MEMFS.stream_ops.msync + } + }, + link: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr, + readlink: MEMFS.node_ops.readlink + }, + stream: {} + }, + chrdev: { + node: { + getattr: MEMFS.node_ops.getattr, + setattr: MEMFS.node_ops.setattr + }, + stream: FS.chrdev_stream_ops + } + }; + var node = FS.createNode(parent, name, mode, dev); + if (FS.isDir(node.mode)) { + node.node_ops = MEMFS.ops_table.dir.node; + node.stream_ops = MEMFS.ops_table.dir.stream; + node.contents = {}; + } else if (FS.isFile(node.mode)) { + node.node_ops = MEMFS.ops_table.file.node; + node.stream_ops = MEMFS.ops_table.file.stream; + + + node.usedBytes = 0; + + + + + node.contents = MEMFS.emptyFileContents ??= new Uint8Array(0); + } else if (FS.isLink(node.mode)) { + node.node_ops = MEMFS.ops_table.link.node; + node.stream_ops = MEMFS.ops_table.link.stream; + } else if (FS.isChrdev(node.mode)) { + node.node_ops = MEMFS.ops_table.chrdev.node; + node.stream_ops = MEMFS.ops_table.chrdev.stream; + } + node.atime = node.mtime = node.ctime = Date.now(); + + if (parent) { + parent.contents[name] = node; + parent.atime = parent.mtime = parent.ctime = node.atime; + } + return node; + }, + getFileDataAsTypedArray(node) { + return node.contents.subarray(0, node.usedBytes); + }, + expandFileStorage(node, newCapacity) { + var prevCapacity = node.contents.length; + if (prevCapacity >= newCapacity) return; + + + + + + var CAPACITY_DOUBLING_MAX = 1024 * 1024; + newCapacity = Math.max(newCapacity, (prevCapacity * (prevCapacity < CAPACITY_DOUBLING_MAX ? 2.0 : 1.125)) >>> 0); + if (prevCapacity) newCapacity = Math.max(newCapacity, 256); + var oldContents = MEMFS.getFileDataAsTypedArray(node); + node.contents = new Uint8Array(newCapacity); + node.contents.set(oldContents); + }, + resizeFileStorage(node, newSize) { + if (node.usedBytes == newSize) return; + var oldContents = node.contents; + node.contents = new Uint8Array(newSize); + node.contents.set(oldContents.subarray(0, Math.min(newSize, node.usedBytes))); + node.usedBytes = newSize; + }, + node_ops:{ + getattr(node) { + var attr = {}; + + attr.dev = FS.isChrdev(node.mode) ? node.id : 1; + attr.ino = node.id; + attr.mode = node.mode; + attr.nlink = 1; + attr.uid = 0; + attr.gid = 0; + attr.rdev = node.rdev; + if (FS.isDir(node.mode)) { + attr.size = 4096; + } else if (FS.isFile(node.mode)) { + attr.size = node.usedBytes; + } else if (FS.isLink(node.mode)) { + attr.size = node.link.length; + } else { + attr.size = 0; + } + attr.atime = new Date(node.atime); + attr.mtime = new Date(node.mtime); + attr.ctime = new Date(node.ctime); + + + attr.blksize = 4096; + attr.blocks = Math.ceil(attr.size / attr.blksize); + return attr; + }, + setattr(node, attr) { + for (const key of ["mode", "atime", "mtime", "ctime"]) { + if (attr[key] != null) { + node[key] = attr[key]; + } + } + if (attr.size !== undefined) { + MEMFS.resizeFileStorage(node, attr.size); + } + }, + lookup(parent, name) { + + + if (!MEMFS.doesNotExistError) { + MEMFS.doesNotExistError = new FS.ErrnoError(44); + + MEMFS.doesNotExistError.stack = ''; + } + throw MEMFS.doesNotExistError; + }, + mknod(parent, name, mode, dev) { + return MEMFS.createNode(parent, name, mode, dev); + }, + rename(old_node, new_dir, new_name) { + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) {} + if (new_node) { + if (FS.isDir(old_node.mode)) { + + for (var i in new_node.contents) { + throw new FS.ErrnoError(55); + } + } + FS.hashRemoveNode(new_node); + } + + delete old_node.parent.contents[old_node.name]; + new_dir.contents[new_name] = old_node; + old_node.name = new_name; + new_dir.ctime = new_dir.mtime = old_node.parent.ctime = old_node.parent.mtime = Date.now(); + }, + unlink(parent, name) { + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + rmdir(parent, name) { + var node = FS.lookupNode(parent, name); + for (var i in node.contents) { + throw new FS.ErrnoError(55); + } + delete parent.contents[name]; + parent.ctime = parent.mtime = Date.now(); + }, + readdir(node) { + return ['.', '..', ...Object.keys(node.contents)]; + }, + symlink(parent, newname, oldpath) { + var node = MEMFS.createNode(parent, newname, 0o777 | 40960, 0); + node.link = oldpath; + return node; + }, + readlink(node) { + if (!FS.isLink(node.mode)) { + throw new FS.ErrnoError(28); + } + return node.link; + }, + }, + stream_ops:{ + read(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= stream.node.usedBytes) return 0; + var size = Math.min(stream.node.usedBytes - position, length); + buffer.set(contents.subarray(position, position + size), offset); + return size; + }, + write(stream, buffer, offset, length, position, canOwn) { + + + + + if (buffer.buffer === HEAP8.buffer) { + canOwn = false; + } + + if (!length) return 0; + var node = stream.node; + node.mtime = node.ctime = Date.now(); + + if (canOwn) { + node.contents = buffer.subarray(offset, offset + length); + node.usedBytes = length; + } else if (node.usedBytes === 0 && position === 0) { + node.contents = buffer.slice(offset, offset + length); + node.usedBytes = length; + } else { + MEMFS.expandFileStorage(node, position+length); + + node.contents.set(buffer.subarray(offset, offset + length), position); + node.usedBytes = Math.max(node.usedBytes, position + length); + } + return length; + }, + llseek(stream, offset, whence) { + var position = offset; + if (whence === 1) { + position += stream.position; + } else if (whence === 2) { + if (FS.isFile(stream.node.mode)) { + position += stream.node.usedBytes; + } + } + if (position < 0) { + throw new FS.ErrnoError(28); + } + return position; + }, + mmap(stream, length, position, prot, flags) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + var ptr; + var allocated; + var contents = stream.node.contents; + + if (!(flags & 2) && contents.buffer === HEAP8.buffer) { + + + allocated = false; + ptr = contents.byteOffset; + } else { + allocated = true; + ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + if (contents) { + + if (position > 0 || position + length < contents.length) { + if (contents.subarray) { + contents = contents.subarray(position, position + length); + } else { + contents = Array.prototype.slice.call(contents, position, position + length); + } + } + HEAP8.set(contents, ptr); + } + } + return { ptr, allocated }; + }, + msync(stream, buffer, offset, length, mmapFlags) { + MEMFS.stream_ops.write(stream, buffer, 0, length, offset, false); + + return 0; + }, + }, + }; + + var FS_modeStringToFlags = (str) => { + if (typeof str != 'string') return str; + var flagModes = { + 'r': 0, + 'r+': 2, + 'w': 512 | 64 | 1, + 'w+': 512 | 64 | 2, + 'a': 1024 | 64 | 1, + 'a+': 1024 | 64 | 2, + }; + var flags = flagModes[str]; + if (typeof flags == 'undefined') { + throw new Error(`Unknown file open mode: ${str}`); + } + return flags; + }; + + var FS_fileDataToTypedArray = (data) => { + if (typeof data == 'string') { + data = intArrayFromString(data, true); + } + if (!data.subarray) { + data = new Uint8Array(data); + } + return data; + }; + + var FS_getMode = (canRead, canWrite) => { + var mode = 0; + if (canRead) mode |= 292 | 73; + if (canWrite) mode |= 146; + return mode; + }; + + + var asyncLoad = async (url) => { + var arrayBuffer = await readAsync(url); + return new Uint8Array(arrayBuffer); + }; + + + var FS_createDataFile = (...args) => FS.createDataFile(...args); + + var getUniqueRunDependency = (id) => { + return id; + }; + + var dependenciesPromise = null; + var resolveRunDependencies = async () => dependenciesPromise; + var runDependencies = 0; + + + var dependenciesPromiseResolve = null; + var removeRunDependency = (id) => { + runDependencies--; + + Module['monitorRunDependencies']?.(runDependencies); + + if (!runDependencies) { + dependenciesPromiseResolve(); + } + }; + + + var addRunDependency = (id) => { + if (!runDependencies) { + dependenciesPromise = new Promise((resolve) => dependenciesPromiseResolve = resolve); + } + runDependencies++; + + Module['monitorRunDependencies']?.(runDependencies); + + }; + + + var preloadPlugins = []; + var FS_handledByPreloadPlugin = async (byteArray, fullname) => { + + if (typeof Browser != 'undefined') Browser.init(); + + for (var plugin of preloadPlugins) { + if (plugin['canHandle'](fullname)) { + return plugin['handle'](byteArray, fullname); + } + } + + + return byteArray; + }; + var FS_preloadFile = async (parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish) => { + + + var fullname = name ? PATH_FS.resolve(PATH.join2(parent, name)) : parent; + var dep = getUniqueRunDependency(`cp ${fullname}`); + addRunDependency(dep); + + try { + var byteArray = url; + if (typeof url == 'string') { + byteArray = await asyncLoad(url); + } + + byteArray = await FS_handledByPreloadPlugin(byteArray, fullname); + preFinish?.(); + if (!dontCreateFile) { + FS_createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); + } + } finally { + removeRunDependency(dep); + } + }; + var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { + FS_preloadFile(parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish).then(onload).catch(onerror); + }; + var FS = { + root:null, + mounts:[], + devices:{ + }, + streams:[], + nextInode:1, + nameTable:null, + currentPath:"/", + initialized:false, + ignorePermissions:true, + filesystems:null, + syncFSRequests:0, + ErrnoError:class { + name = 'ErrnoError'; + + + + + + + constructor(errno) { + this.errno = errno; + } + }, + FSStream:class { + shared = {}; + get object() { + return this.node; + } + set object(val) { + this.node = val; + } + get isRead() { + return (this.flags & 2097155) !== 1; + } + get isWrite() { + return (this.flags & 2097155) !== 0; + } + get isAppend() { + return (this.flags & 1024); + } + get flags() { + return this.shared.flags; + } + set flags(val) { + this.shared.flags = val; + } + get position() { + return this.shared.position; + } + set position(val) { + this.shared.position = val; + } + }, + FSNode:class { + node_ops = {}; + stream_ops = {}; + readMode = 292 | 73; + writeMode = 146; + mounted = null; + constructor(parent, name, mode, rdev) { + if (!parent) { + parent = this; + } + this.parent = parent; + this.mount = parent.mount; + this.id = FS.nextInode++; + this.name = name; + this.mode = mode; + this.rdev = rdev; + this.atime = this.mtime = this.ctime = Date.now(); + } + get read() { + return (this.mode & this.readMode) === this.readMode; + } + set read(val) { + val ? this.mode |= this.readMode : this.mode &= ~this.readMode; + } + get write() { + return (this.mode & this.writeMode) === this.writeMode; + } + set write(val) { + val ? this.mode |= this.writeMode : this.mode &= ~this.writeMode; + } + get isFolder() { + return FS.isDir(this.mode); + } + get isDevice() { + return FS.isChrdev(this.mode); + } + + + + + + + addListener(cb, exclusive = false) { + var entry = {cb, exclusive}; + var listeners = (this.listeners ??= new Set()); + listeners.add(entry); + return {listeners, entry}; + } + notifyListeners(flags) { + + + + + + + + + + + + + + if (!this.listeners) return; + + + + + var excl; + for (var entry of this.listeners) { + if (entry.exclusive) (excl ||= []).push(entry); + else entry.cb(flags); + } + if (excl) { + var i = (this.exclTurn || 0) % excl.length; + this.exclTurn = i + 1; + excl[i].cb(flags); + } + } + }, + lookupPath(path, opts = {}) { + if (!path) { + throw new FS.ErrnoError(44); + } + opts.follow_mount ??= true + + if (!PATH.isAbs(path)) { + path = FS.cwd() + '/' + path; + } + + + linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { + + var parts = path.split('/').filter((p) => !!p); + + + var current = FS.root; + var current_path = '/'; + + for (var i = 0; i < parts.length; i++) { + var islast = (i === parts.length-1); + if (islast && opts.parent) { + + break; + } + + if (parts[i] === '.') { + continue; + } + + if (parts[i] === '..') { + current_path = PATH.dirname(current_path); + if (FS.isRoot(current)) { + path = current_path + '/' + parts.slice(i + 1).join('/'); + + + nlinks--; + continue linkloop; + } else { + current = current.parent; + } + continue; + } + + current_path = PATH.join2(current_path, parts[i]); + try { + current = FS.lookupNode(current, parts[i]); + } catch (e) { + + + + if ((e?.errno === 44) && islast && opts.noent_okay) { + return { path: current_path }; + } + throw e; + } + + + if (FS.isMountpoint(current) && (!islast || opts.follow_mount)) { + current = current.mounted.root; + } + + + + if (FS.isLink(current.mode) && (!islast || opts.follow)) { + if (!current.node_ops.readlink) { + throw new FS.ErrnoError(52); + } + var link = current.node_ops.readlink(current); + if (!PATH.isAbs(link)) { + link = PATH.dirname(current_path) + '/' + link; + } + path = link + '/' + parts.slice(i + 1).join('/'); + continue linkloop; + } + } + return { path: current_path, node: current }; + } + throw new FS.ErrnoError(32); + }, + getPath(node) { + var path; + while (true) { + if (FS.isRoot(node)) { + var mount = node.mount.mountpoint; + if (!path) return mount; + return mount[mount.length-1] !== '/' ? `${mount}/${path}` : mount + path; + } + path = path ? `${node.name}/${path}` : node.name; + node = node.parent; + } + }, + hashName(parentid, name) { + var hash = 0; + + for (var i = 0; i < name.length; i++) { + hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; + } + return ((parentid + hash) >>> 0) % FS.nameTable.length; + }, + hashAddNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + node.name_next = FS.nameTable[hash]; + FS.nameTable[hash] = node; + }, + hashRemoveNode(node) { + var hash = FS.hashName(node.parent.id, node.name); + if (FS.nameTable[hash] === node) { + FS.nameTable[hash] = node.name_next; + } else { + var current = FS.nameTable[hash]; + while (current) { + if (current.name_next === node) { + current.name_next = node.name_next; + break; + } + current = current.name_next; + } + } + }, + lookupNode(parent, name) { + var errCode = FS.mayLookup(parent); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + var hash = FS.hashName(parent.id, name); + for (var node = FS.nameTable[hash]; node; node = node.name_next) { + var nodeName = node.name; + if (node.parent.id === parent.id && nodeName === name) { + return node; + } + } + + return FS.lookup(parent, name); + }, + createNode(parent, name, mode, rdev) { + var node = new FS.FSNode(parent, name, mode, rdev); + + FS.hashAddNode(node); + + return node; + }, + destroyNode(node) { + FS.hashRemoveNode(node); + }, + isRoot(node) { + return node === node.parent; + }, + isMountpoint(node) { + return !!node.mounted; + }, + isFile(mode) { + return (mode & 61440) === 32768; + }, + isDir(mode) { + return (mode & 61440) === 16384; + }, + isLink(mode) { + return (mode & 61440) === 40960; + }, + isChrdev(mode) { + return (mode & 61440) === 8192; + }, + isBlkdev(mode) { + return (mode & 61440) === 24576; + }, + isFIFO(mode) { + return (mode & 61440) === 4096; + }, + isSocket(mode) { + return (mode & 49152) === 49152; + }, + flagsToPermissionString(flag) { + var perms = ['r', 'w', 'rw'][flag & 3]; + if ((flag & 512)) { + perms += 'w'; + } + return perms; + }, + nodePermissions(node, perms) { + if (FS.ignorePermissions) { + return 0; + } + + if (perms.includes('r') && !(node.mode & 292)) { + return 2; + } + if (perms.includes('w') && !(node.mode & 146)) { + return 2; + } + if (perms.includes('x') && !(node.mode & 73)) { + return 2; + } + return 0; + }, + mayLookup(dir) { + if (!FS.isDir(dir.mode)) return 54; + var errCode = FS.nodePermissions(dir, 'x'); + if (errCode) return errCode; + if (!dir.node_ops.lookup) return 2; + return 0; + }, + mayCreate(dir, name) { + if (!FS.isDir(dir.mode)) { + return 54; + } + try { + var node = FS.lookupNode(dir, name); + return 20; + } catch (e) { + } + return FS.nodePermissions(dir, 'wx'); + }, + mayDelete(dir, name, isdir) { + var node; + try { + node = FS.lookupNode(dir, name); + } catch (e) { + return e.errno; + } + var errCode = FS.nodePermissions(dir, 'wx'); + if (errCode) { + return errCode; + } + if (isdir) { + if (!FS.isDir(node.mode)) { + return 54; + } + if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { + return 10; + } + } else if (FS.isDir(node.mode)) { + return 31; + } + return 0; + }, + mayOpen(node, flags) { + if (!node) { + return 44; + } + if (FS.isLink(node.mode)) { + return 32; + } + var mode = FS.flagsToPermissionString(flags); + if (FS.isDir(node.mode)) { + + + if (mode !== 'r' || (flags & (512 | 64))) { + return 31; + } + } + return FS.nodePermissions(node, mode); + }, + checkOpExists(op, err) { + if (!op) { + throw new FS.ErrnoError(err); + } + return op; + }, + MAX_OPEN_FDS:4096, + nextfd() { + for (var fd = 0; fd <= FS.MAX_OPEN_FDS; fd++) { + if (!FS.streams[fd]) { + return fd; + } + } + throw new FS.ErrnoError(33); + }, + getStreamChecked(fd) { + var stream = FS.getStream(fd); + if (!stream) { + throw new FS.ErrnoError(8); + } + return stream; + }, + getStream:(fd) => FS.streams[fd], + createStream(stream, fd = -1) { + + + stream = Object.assign(new FS.FSStream(), stream); + if (fd == -1) { + fd = FS.nextfd(); + } + stream.fd = fd; + FS.streams[fd] = stream; + return stream; + }, + closeStream(fd) { + FS.streams[fd] = null; + }, + dupStream(origStream, fd = -1) { + var stream = FS.createStream(origStream, fd); + stream.stream_ops?.dup?.(stream); + return stream; + }, + doSetAttr(stream, node, attr) { + var setattr = stream?.stream_ops.setattr; + var arg = setattr ? stream : node; + setattr ??= node.node_ops.setattr; + FS.checkOpExists(setattr, 63) + try { + setattr(arg, attr); + } catch (e) { + if (e instanceof RangeError) { + throw new FS.ErrnoError(22); + } + throw e; + } + }, + chrdev_stream_ops:{ + open(stream) { + var device = FS.getDevice(stream.node.rdev); + + stream.stream_ops = device.stream_ops; + + stream.stream_ops.open?.(stream); + }, + llseek() { + throw new FS.ErrnoError(70); + }, + }, + major:(dev) => ((dev) >> 8), + minor:(dev) => ((dev) & 0xff), + makedev:(ma, mi) => ((ma) << 8 | (mi)), + registerDevice(dev, ops) { + FS.devices[dev] = { stream_ops: ops }; + }, + getDevice:(dev) => FS.devices[dev], + getMounts(mount) { + var mounts = []; + var check = [mount]; + + while (check.length) { + var m = check.pop(); + + mounts.push(m); + + check.push(...m.mounts); + } + + return mounts; + }, + syncfs(populate, callback) { + if (typeof populate == 'function') { + callback = populate; + populate = false; + } + + FS.syncFSRequests++; + + if (FS.syncFSRequests > 1) { + err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`); + } + + var mounts = FS.getMounts(FS.root.mount); + var completed = 0; + + function doCallback(errCode) { + FS.syncFSRequests--; + return callback(errCode); + } + + function done(errCode) { + if (errCode) { + if (!done.errored) { + done.errored = true; + return doCallback(errCode); + } + return; + } + if (++completed >= mounts.length) { + doCallback(null); + } + }; + + + for (var mount of mounts) { + if (mount.type.syncfs) { + mount.type.syncfs(mount, populate, done); + } else { + done(null); + } + } + }, + mount(type, opts, mountpoint) { + var root = mountpoint === '/'; + var pseudo = !mountpoint; + var node; + + if (root && FS.root) { + throw new FS.ErrnoError(10); + } else if (!root && !pseudo) { + var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); + + mountpoint = lookup.path; + node = lookup.node; + + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + + if (!FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + } + + var mount = { + type, + opts, + mountpoint, + mounts: [] + }; + + + var mountRoot = type.mount(mount); + mountRoot.mount = mount; + mount.root = mountRoot; + + if (root) { + FS.root = mountRoot; + } else if (node) { + + node.mounted = mount; + + + if (node.mount) { + node.mount.mounts.push(mount); + } + } + + return mountRoot; + }, + unmount(mountpoint) { + var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); + + if (!FS.isMountpoint(lookup.node)) { + throw new FS.ErrnoError(28); + } + + + var node = lookup.node; + var mount = node.mounted; + var mounts = FS.getMounts(mount); + + for (var [hash, current] of Object.entries(FS.nameTable)) { + while (current) { + var next = current.name_next; + + if (mounts.includes(current.mount)) { + FS.destroyNode(current); + } + + current = next; + } + } + + + node.mounted = null; + + + var idx = node.mount.mounts.indexOf(mount); + node.mount.mounts.splice(idx, 1); + }, + lookup(parent, name) { + return parent.node_ops.lookup(parent, name); + }, + mknod(path, mode, dev) { + var lookup = FS.lookupPath(path, { parent: true }); + var parent = lookup.node; + var name = PATH.basename(path); + if (!name) { + throw new FS.ErrnoError(28); + } + if (name === '.' || name === '..') { + throw new FS.ErrnoError(20); + } + var errCode = FS.mayCreate(parent, name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.mknod) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.mknod(parent, name, mode, dev); + }, + statfs(path) { + return FS.statfsNode(FS.lookupPath(path, {follow: true}).node); + }, + statfsStream(stream) { + + + + return FS.statfsNode(stream.node); + }, + statfsNode(node) { + + + + var rtn = { + bsize: 4096, + frsize: 4096, + blocks: 1e6, + bfree: 5e5, + bavail: 5e5, + files: FS.nextInode, + ffree: FS.nextInode - 1, + fsid: 42, + flags: 2, + namelen: 255, + }; + + if (node.node_ops.statfs) { + Object.assign(rtn, node.node_ops.statfs(node.mount.opts.root)); + } + return rtn; + }, + create(path, mode = 0o666) { + mode &= 4095; + mode |= 32768; + return FS.mknod(path, mode, 0); + }, + mkdir(path, mode = 0o777) { + mode &= 511 | 512; + mode |= 16384; + return FS.mknod(path, mode, 0); + }, + mkdirTree(path, mode) { + var dirs = path.split('/'); + var d = ''; + for (var dir of dirs) { + if (!dir) continue; + if (d || PATH.isAbs(path)) d += '/'; + d += dir; + try { + FS.mkdir(d, mode); + } catch(e) { + if (e.errno != 20) throw e; + } + } + }, + mkdev(path, mode, dev) { + if (typeof dev == 'undefined') { + dev = mode; + mode = 0o666; + } + mode |= 8192; + return FS.mknod(path, mode, dev); + }, + symlink(oldpath, newpath) { + if (!PATH_FS.resolve(oldpath)) { + throw new FS.ErrnoError(44); + } + var lookup = FS.lookupPath(newpath, { parent: true }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.symlink) { + throw new FS.ErrnoError(63); + } + return parent.node_ops.symlink(parent, newname, oldpath); + }, + link(oldpath, newpath, flags) { + var lookup = FS.lookupPath(newpath, { parent: true }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + + + + if (!parent.node_ops.link) { + throw new FS.ErrnoError(34); + } + return parent.node_ops.link(parent, newname, oldpath, flags); + }, + rename(old_path, new_path) { + var old_dirname = PATH.dirname(old_path); + var new_dirname = PATH.dirname(new_path); + var old_name = PATH.basename(old_path); + var new_name = PATH.basename(new_path); + + var lookup, old_dir, new_dir; + + + lookup = FS.lookupPath(old_path, { parent: true }); + old_dir = lookup.node; + lookup = FS.lookupPath(new_path, { parent: true }); + new_dir = lookup.node; + + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); + + if (old_dir.mount !== new_dir.mount) { + throw new FS.ErrnoError(75); + } + + var old_node = FS.lookupNode(old_dir, old_name); + + var relative = PATH_FS.relative(old_path, new_dirname); + if (relative.charAt(0) !== '.') { + throw new FS.ErrnoError(28); + } + + relative = PATH_FS.relative(new_path, old_dirname); + if (relative.charAt(0) !== '.') { + throw new FS.ErrnoError(55); + } + + var new_node; + try { + new_node = FS.lookupNode(new_dir, new_name); + } catch (e) { + + } + + if (old_node === new_node) { + return; + } + + var isdir = FS.isDir(old_node.mode); + var errCode = FS.mayDelete(old_dir, old_name, isdir); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + + + errCode = new_node ? + FS.mayDelete(new_dir, new_name, isdir) : + FS.mayCreate(new_dir, new_name); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!old_dir.node_ops.rename) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(old_node) || (new_node && FS.isMountpoint(new_node))) { + throw new FS.ErrnoError(10); + } + + if (new_dir !== old_dir) { + errCode = FS.nodePermissions(old_dir, 'w'); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + + FS.hashRemoveNode(old_node); + + try { + old_dir.node_ops.rename(old_node, new_dir, new_name); + + + old_node.parent = new_dir; + } catch (e) { + throw e; + } finally { + + + FS.hashAddNode(old_node); + } + }, + rmdir(path) { + var lookup = FS.lookupPath(path, { parent: true }); + var parent = lookup.node; + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, true); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.rmdir) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.rmdir(parent, name); + FS.destroyNode(node); + }, + readdir(path) { + var lookup = FS.lookupPath(path, { follow: true }); + var node = lookup.node; + var readdir = FS.checkOpExists(node.node_ops.readdir, 54); + return readdir(node); + }, + unlink(path) { + var lookup = FS.lookupPath(path, { parent: true }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var name = PATH.basename(path); + var node = FS.lookupNode(parent, name); + var errCode = FS.mayDelete(parent, name, false); + if (errCode) { + + + + throw new FS.ErrnoError(errCode); + } + if (!parent.node_ops.unlink) { + throw new FS.ErrnoError(63); + } + if (FS.isMountpoint(node)) { + throw new FS.ErrnoError(10); + } + parent.node_ops.unlink(parent, name); + FS.destroyNode(node); + }, + readlink(path) { + var lookup = FS.lookupPath(path); + var link = lookup.node; + if (!link) { + throw new FS.ErrnoError(44); + } + if (!link.node_ops.readlink) { + throw new FS.ErrnoError(28); + } + return link.node_ops.readlink(link); + }, + stat(path, dontFollow) { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + var node = lookup.node; + var getattr = FS.checkOpExists(node.node_ops.getattr, 63); + return getattr(node); + }, + fstat(fd) { + var stream = FS.getStreamChecked(fd); + var node = stream.node; + var getattr = stream.stream_ops.getattr; + var arg = getattr ? stream : node; + getattr ??= node.node_ops.getattr; + FS.checkOpExists(getattr, 63) + return getattr(arg); + }, + lstat(path) { + return FS.stat(path, true); + }, + doChmod(stream, node, mode, dontFollow) { + FS.doSetAttr(stream, node, { + mode: (mode & 4095) | (node.mode & ~4095), + ctime: Date.now(), + dontFollow + }); + }, + chmod(path, mode, dontFollow) { + var node; + if (typeof path == 'string') { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + node = lookup.node; + } else { + node = path; + } + FS.doChmod(null, node, mode, dontFollow); + }, + lchmod(path, mode) { + FS.chmod(path, mode, true); + }, + fchmod(fd, mode) { + var stream = FS.getStreamChecked(fd); + FS.doChmod(stream, stream.node, mode, false); + }, + doChown(stream, node, dontFollow) { + FS.doSetAttr(stream, node, { + timestamp: Date.now(), + dontFollow + + }); + }, + chown(path, uid, gid, dontFollow) { + var node; + if (typeof path == 'string') { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + node = lookup.node; + } else { + node = path; + } + FS.doChown(null, node, dontFollow); + }, + lchown(path, uid, gid) { + FS.chown(path, uid, gid, true); + }, + fchown(fd, uid, gid) { + var stream = FS.getStreamChecked(fd); + FS.doChown(stream, stream.node, false); + }, + doTruncate(stream, node, len) { + if (FS.isDir(node.mode)) { + throw new FS.ErrnoError(31); + } + if (!FS.isFile(node.mode)) { + throw new FS.ErrnoError(28); + } + var errCode = FS.nodePermissions(node, 'w'); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.doSetAttr(stream, node, { + size: len, + timestamp: Date.now() + }); + }, + truncate(path, len) { + if (len < 0) { + throw new FS.ErrnoError(28); + } + var node; + if (typeof path == 'string') { + var lookup = FS.lookupPath(path, { follow: true }); + node = lookup.node; + } else { + node = path; + } + FS.doTruncate(null, node, len); + }, + ftruncate(fd, len) { + var stream = FS.getStreamChecked(fd); + if (len < 0 || (stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(28); + } + FS.doTruncate(stream, stream.node, len); + }, + utime(path, atime, mtime, dontFollow) { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + FS.doSetAttr(null, lookup.node, { + atime: atime, + mtime: mtime, + dontFollow + }); + }, + open(path, flags, mode = 0o666) { + if (path === "") { + throw new FS.ErrnoError(44); + } + flags = FS_modeStringToFlags(flags); + if ((flags & 64)) { + mode = (mode & 4095) | 32768; + } else { + mode = 0; + } + var node; + var isDirPath; + if (typeof path == 'object') { + node = path; + } else { + isDirPath = path.endsWith("/"); + + + + var lookup = FS.lookupPath(path, { + follow: !(flags & 131072), + noent_okay: true + }); + node = lookup.node; + path = lookup.path; + } + + var created = false; + if ((flags & 64)) { + if (node) { + + if ((flags & 128)) { + throw new FS.ErrnoError(20); + } + } else if (isDirPath) { + throw new FS.ErrnoError(31); + } else { + + + + + node = FS.mknod(path, mode | 0o777, 0); + created = true; + } + } + if (!node) { + throw new FS.ErrnoError(44); + } + + if (FS.isChrdev(node.mode)) { + flags &= ~512; + } + + if ((flags & 65536) && !FS.isDir(node.mode)) { + throw new FS.ErrnoError(54); + } + + + + if (!created) { + var errCode = FS.mayOpen(node, flags); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + } + + if ((flags & 512) && !created) { + FS.truncate(node, 0); + } + + flags &= ~(128 | 512 | 131072); + + + var stream = FS.createStream({ + node, + path: FS.getPath(node), + flags, + seekable: true, + position: 0, + stream_ops: node.stream_ops, + + ungotten: [], + error: false + }); + + if (stream.stream_ops.open) { + stream.stream_ops.open(stream); + } + if (created) { + FS.chmod(node, mode & 0o777); + } + return stream; + }, + close(stream) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (stream.getdents) stream.getdents = null; + + + + + stream.node?.notifyListeners(32); + try { + if (stream.stream_ops.close) { + stream.stream_ops.close(stream); + } + } catch (e) { + throw e; + } finally { + FS.closeStream(stream.fd); + } + stream.fd = null; + }, + isClosed(stream) { + return stream.fd === null; + }, + llseek(stream, offset, whence) { + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if (!stream.seekable || !stream.stream_ops.llseek) { + throw new FS.ErrnoError(70); + } + if (whence != 0 && whence != 1 && whence != 2) { + throw new FS.ErrnoError(28); + } + stream.position = stream.stream_ops.llseek(stream, offset, whence); + stream.ungotten = []; + return stream.position; + }, + read(stream, buffer, offset, length, position) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.read) { + throw new FS.ErrnoError(28); + } + var seeking = typeof position != 'undefined'; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); + if (!seeking) stream.position += bytesRead; + return bytesRead; + }, + write(stream, buffer, offset, length, position, canOwn) { + if (length < 0 || position < 0) { + throw new FS.ErrnoError(28); + } + if (FS.isClosed(stream)) { + throw new FS.ErrnoError(8); + } + if ((stream.flags & 2097155) === 0) { + throw new FS.ErrnoError(8); + } + if (FS.isDir(stream.node.mode)) { + throw new FS.ErrnoError(31); + } + if (!stream.stream_ops.write) { + throw new FS.ErrnoError(28); + } + if (stream.seekable && stream.flags & 1024) { + + FS.llseek(stream, 0, 2); + } + var seeking = typeof position != 'undefined'; + if (!seeking) { + position = stream.position; + } else if (!stream.seekable) { + throw new FS.ErrnoError(70); + } + var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); + if (!seeking) stream.position += bytesWritten; + return bytesWritten; + }, + mmap(stream, length, position, prot, flags) { + + + + + + + if ((prot & 2) !== 0 + && (flags & 2) === 0 + && (stream.flags & 2097155) !== 2) { + throw new FS.ErrnoError(2); + } + if ((stream.flags & 2097155) === 1) { + throw new FS.ErrnoError(2); + } + if (!stream.stream_ops.mmap) { + throw new FS.ErrnoError(43); + } + if (!length) { + throw new FS.ErrnoError(28); + } + return stream.stream_ops.mmap(stream, length, position, prot, flags); + }, + msync(stream, buffer, offset, length, mmapFlags) { + if (!stream.stream_ops.msync) { + return 0; + } + return stream.stream_ops.msync(stream, buffer, offset, length, mmapFlags); + }, + ioctl(stream, cmd, arg) { + if (!stream.stream_ops.ioctl) { + throw new FS.ErrnoError(59); + } + return stream.stream_ops.ioctl(stream, cmd, arg); + }, + readFile(path, opts = {}) { + opts.flags = opts.flags ?? 0; + opts.encoding = opts.encoding ?? 'binary'; + if (opts.encoding !== 'utf8' && opts.encoding !== 'binary') { + abort(`Invalid encoding type "${opts.encoding}"`); + } + var stream = FS.open(path, opts.flags); + var stat = FS.stat(path); + var length = stat.size; + var buf = new Uint8Array(length); + FS.read(stream, buf, 0, length, 0); + if (opts.encoding === 'utf8') { + buf = UTF8ArrayToString(buf); + } + FS.close(stream); + return buf; + }, + writeFile(path, data, opts = {}) { + opts.flags = opts.flags ?? 577; + var stream = FS.open(path, opts.flags, opts.mode); + data = FS_fileDataToTypedArray(data); + FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); + FS.close(stream); + }, + cwd:() => FS.currentPath, + chdir(path) { + var lookup = FS.lookupPath(path, { follow: true }); + if (lookup.node === null) { + throw new FS.ErrnoError(44); + } + if (!FS.isDir(lookup.node.mode)) { + throw new FS.ErrnoError(54); + } + var errCode = FS.nodePermissions(lookup.node, 'x'); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + FS.currentPath = lookup.path; + }, + createDefaultDirectories() { + FS.mkdir('/tmp'); + FS.mkdir('/home'); + FS.mkdir('/home/web_user'); + }, + createDefaultDevices() { + + FS.mkdir('/dev'); + + FS.registerDevice(FS.makedev(1, 3), { + read: () => 0, + write: (stream, buffer, offset, length, pos) => length, + llseek: () => 0, + }); + FS.mkdev('/dev/null', FS.makedev(1, 3)); + + + + TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); + TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); + FS.mkdev('/dev/tty', FS.makedev(5, 0)); + FS.mkdev('/dev/tty1', FS.makedev(6, 0)); + + + var randomBuffer = new Uint8Array(1024), randomLeft = 0; + var randomByte = () => { + if (randomLeft === 0) { + randomFill(randomBuffer); + randomLeft = randomBuffer.byteLength; + } + return randomBuffer[--randomLeft]; + }; + FS.createDevice('/dev', 'random', randomByte); + FS.createDevice('/dev', 'urandom', randomByte); + + + FS.mkdir('/dev/shm'); + FS.mkdir('/dev/shm/tmp'); + }, + createSpecialDirectories() { + + + FS.mkdir('/proc'); + var proc_self = FS.mkdir('/proc/self'); + FS.mkdir('/proc/self/fd'); + FS.mount({ + mount() { + var node = FS.createNode(proc_self, 'fd', 16895, 73); + node.stream_ops = { + llseek: MEMFS.stream_ops.llseek, + }; + node.node_ops = { + lookup(parent, name) { + var fd = +name; + var stream = FS.getStreamChecked(fd); + var ret = { + parent: null, + mount: { mountpoint: 'fake' }, + node_ops: { readlink: () => stream.path }, + id: fd + 1, + }; + ret.parent = ret; + return ret; + }, + readdir() { + return Array.from(FS.streams.entries()) + .filter(([k, v]) => v) + .map(([k, v]) => k.toString()); + } + }; + return node; + } + }, {}, '/proc/self/fd'); + }, + createStandardStreams(input, output, error) { + + + + + + + + + if (input) { + FS.createDevice('/dev', 'stdin', input); + } else { + FS.symlink('/dev/tty', '/dev/stdin'); + } + if (output) { + FS.createDevice('/dev', 'stdout', null, output); + } else { + FS.symlink('/dev/tty', '/dev/stdout'); + } + if (error) { + FS.createDevice('/dev', 'stderr', null, error); + } else { + FS.symlink('/dev/tty1', '/dev/stderr'); + } + + + var stdin = FS.open('/dev/stdin', 0); + var stdout = FS.open('/dev/stdout', 1); + var stderr = FS.open('/dev/stderr', 1); + }, + staticInit() { + FS.nameTable = new Array(4096); + + FS.mount(MEMFS, {}, '/'); + + FS.createDefaultDirectories(); + FS.createDefaultDevices(); + FS.createSpecialDirectories(); + + FS.filesystems = { + 'MEMFS': MEMFS, + }; + }, + init(input, output, error) { + FS.initialized = true; + + + input ??= Module['stdin']; + output ??= Module['stdout']; + error ??= Module['stderr']; + + FS.createStandardStreams(input, output, error); + }, + quit() { + FS.initialized = false; + + + for (var stream of FS.streams) { + if (stream) { + FS.close(stream); + } + } + }, + findObject(path, dontResolveLastLink) { + var ret = FS.analyzePath(path, dontResolveLastLink); + if (!ret.exists) { + return null; + } + return ret.object; + }, + analyzePath(path, dontResolveLastLink) { + + try { + var lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); + path = lookup.path; + } catch (e) { + } + var ret = { + isRoot: false, exists: false, error: 0, name: null, path: null, object: null, + parentExists: false, parentPath: null, parentObject: null + }; + try { + var lookup = FS.lookupPath(path, { parent: true }); + ret.parentExists = true; + ret.parentPath = lookup.path; + ret.parentObject = lookup.node; + ret.name = PATH.basename(path); + lookup = FS.lookupPath(path, { follow: !dontResolveLastLink }); + ret.exists = true; + ret.path = lookup.path; + ret.object = lookup.node; + ret.name = lookup.node.name; + ret.isRoot = lookup.path === '/'; + } catch (e) { + ret.error = e.errno; + }; + return ret; + }, + createPath(parent, path, canRead, canWrite) { + parent = typeof parent == 'string' ? parent : FS.getPath(parent); + var parts = path.split('/').reverse(); + while (parts.length) { + var part = parts.pop(); + if (!part) continue; + var current = PATH.join2(parent, part); + try { + FS.mkdir(current); + } catch (e) { + if (e.errno != 20) throw e; + } + parent = current; + } + return current; + }, + createFile(parent, name, properties, canRead, canWrite) { + var path = PATH.join2(typeof parent == 'string' ? parent : FS.getPath(parent), name); + var mode = FS_getMode(canRead, canWrite); + return FS.create(path, mode); + }, + createDataFile(parent, name, data, canRead, canWrite, canOwn) { + var path = name; + if (parent) { + parent = typeof parent == 'string' ? parent : FS.getPath(parent); + path = name ? PATH.join2(parent, name) : parent; + } + var mode = FS_getMode(canRead, canWrite); + var node = FS.create(path, mode); + if (data) { + data = FS_fileDataToTypedArray(data); + + FS.chmod(node, mode | 146); + var stream = FS.open(node, 577); + FS.write(stream, data, 0, data.length, 0, canOwn); + FS.close(stream); + FS.chmod(node, mode); + } + }, + createDevice(parent, name, input, output) { + var path = PATH.join2(typeof parent == 'string' ? parent : FS.getPath(parent), name); + var mode = FS_getMode(!!input, !!output); + FS.createDevice.major ??= 64; + var dev = FS.makedev(FS.createDevice.major++, 0); + + + FS.registerDevice(dev, { + open(stream) { + stream.seekable = false; + }, + close(stream) { + + if (output?.buffer?.length) { + output(10); + } + }, + read(stream, buffer, offset, length, pos ) { + var bytesRead = 0; + for (var i = 0; i < length; i++) { + var result; + try { + result = input(); + } catch (e) { + throw new FS.ErrnoError(29); + } + if (result === undefined && bytesRead === 0) { + throw new FS.ErrnoError(6); + } + if (result === null || result === undefined) break; + bytesRead++; + buffer[offset+i] = result; + } + if (bytesRead) { + stream.node.atime = Date.now(); + } + return bytesRead; + }, + write(stream, buffer, offset, length, pos) { + for (var i = 0; i < length; i++) { + try { + output(buffer[offset+i]); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + if (length) { + stream.node.mtime = stream.node.ctime = Date.now(); + } + return i; + } + }); + return FS.mkdev(path, mode, dev); + }, + forceLoadFile(obj) { + if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; + if (globalThis.XMLHttpRequest) { + abort("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + } else { + try { + obj.contents = readBinary(obj.url); + } catch (e) { + throw new FS.ErrnoError(29); + } + } + }, + createLazyFile(parent, name, url, canRead, canWrite) { + + + class LazyUint8Array { + lengthKnown = false; + chunks = []; + get(idx) { + if (idx > this.length-1 || idx < 0) { + return undefined; + } + var chunkOffset = idx % this.chunkSize; + var chunkNum = (idx / this.chunkSize)|0; + return this.getter(chunkNum)[chunkOffset]; + } + setDataGetter(getter) { + this.getter = getter; + } + cacheLength() { + + var xhr = new XMLHttpRequest(); + xhr.open('HEAD', url, false); + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + var datalength = Number(xhr.getResponseHeader("Content-length")); + var header; + var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; + var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + + var chunkSize = 1024*1024; + + if (!hasByteServing) chunkSize = datalength; + + + var doXHR = (from, to) => { + if (from > to) abort(`invalid range (${from}, ${to}) or no bytes requested!`); + if (to > datalength-1) abort(`only ${datalength} bytes available! programmer error!`); + + + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, false); + if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + + + xhr.responseType = 'arraybuffer'; + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.send(null); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + if (xhr.response !== undefined) { + return new Uint8Array((xhr.response || [])); + } + return intArrayFromString(xhr.responseText ?? '', true); + }; + var lazyArray = this; + lazyArray.setDataGetter((chunkNum) => { + var start = chunkNum * chunkSize; + var end = (chunkNum+1) * chunkSize - 1; + end = Math.min(end, datalength-1); + if (typeof lazyArray.chunks[chunkNum] == 'undefined') { + lazyArray.chunks[chunkNum] = doXHR(start, end); + } + if (typeof lazyArray.chunks[chunkNum] == 'undefined') abort('doXHR failed!'); + return lazyArray.chunks[chunkNum]; + }); + + if (usesGzip || !datalength) { + + chunkSize = datalength = 1; + datalength = this.getter(0).length; + chunkSize = datalength; + out("LazyFiles on gzip forces download of the whole file when length is accessed"); + } + + this._length = datalength; + this._chunkSize = chunkSize; + this.lengthKnown = true; + } + get length() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._length; + } + get chunkSize() { + if (!this.lengthKnown) { + this.cacheLength(); + } + return this._chunkSize; + } + } + + if (globalThis.XMLHttpRequest) { + if (!ENVIRONMENT_IS_WORKER) abort('Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'); + var lazyArray = new LazyUint8Array(); + var properties = { isDevice: false, contents: lazyArray }; + } else { + var properties = { isDevice: false, url: url }; + } + + var node = FS.createFile(parent, name, properties, canRead, canWrite); + + + + if (properties.contents) { + node.contents = properties.contents; + } else if (properties.url) { + node.contents = null; + node.url = properties.url; + } + + Object.defineProperties(node, { + usedBytes: { + get: function() { return this.contents.length; } + } + }); + + var stream_ops = {}; + for (const [key, fn] of Object.entries(node.stream_ops)) { + stream_ops[key] = (...args) => { + FS.forceLoadFile(node); + return fn(...args); + }; + } + function writeChunks(stream, buffer, offset, length, position) { + var contents = stream.node.contents; + if (position >= contents.length) + return 0; + var size = Math.min(contents.length - position, length); + if (contents.slice) { + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents[position + i]; + } + } else { + for (var i = 0; i < size; i++) { + buffer[offset + i] = contents.get(position + i); + } + } + return size; + } + + stream_ops.read = (stream, buffer, offset, length, position) => { + FS.forceLoadFile(node); + return writeChunks(stream, buffer, offset, length, position) + }; + + stream_ops.mmap = (stream, length, position, prot, flags) => { + FS.forceLoadFile(node); + var ptr = mmapAlloc(length); + if (!ptr) { + throw new FS.ErrnoError(48); + } + writeChunks(stream, HEAP8, ptr, length, position); + return { ptr, allocated: true }; + }; + node.stream_ops = stream_ops; + return node; + }, + }; + + + + + var UTF8ToString = (ptr, maxBytesToRead, ignoreNul) => { + if (!ptr) return ''; + var end = findStringEnd(HEAPU8, ptr, maxBytesToRead, ignoreNul); + return UTF8Decoder.decode(HEAPU8.subarray(ptr, end)); + }; + var SYSCALLS = { + currentUmask:18, + calculateAt(dirfd, path, allowEmpty) { + if (PATH.isAbs(path)) { + return path; + } + + var dir; + if (dirfd === -100) { + dir = FS.cwd(); + } else { + var dirstream = SYSCALLS.getStreamFromFD(dirfd); + dir = dirstream.path; + } + if (path.length == 0) { + if (!allowEmpty) { + throw new FS.ErrnoError(44);; + } + return dir; + } + return dir + '/' + path; + }, + writeStat(buf, stat) { + HEAPU32[((buf)>>2)] = stat.dev; + HEAPU32[(((buf)+(4))>>2)] = stat.mode; + HEAPU32[(((buf)+(8))>>2)] = stat.nlink; + HEAPU32[(((buf)+(12))>>2)] = stat.uid; + HEAPU32[(((buf)+(16))>>2)] = stat.gid; + HEAPU32[(((buf)+(20))>>2)] = stat.rdev; + HEAP64[(((buf)+(24))>>3)] = BigInt(stat.size); + HEAP32[(((buf)+(32))>>2)] = 4096; + HEAP32[(((buf)+(36))>>2)] = stat.blocks; + var atime = stat.atime.getTime(); + var mtime = stat.mtime.getTime(); + var ctime = stat.ctime.getTime(); + HEAP64[(((buf)+(40))>>3)] = BigInt(Math.floor(atime / 1000)); + HEAPU32[(((buf)+(48))>>2)] = (atime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(56))>>3)] = BigInt(Math.floor(mtime / 1000)); + HEAPU32[(((buf)+(64))>>2)] = (mtime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(72))>>3)] = BigInt(Math.floor(ctime / 1000)); + HEAPU32[(((buf)+(80))>>2)] = (ctime % 1000) * 1000 * 1000; + HEAP64[(((buf)+(88))>>3)] = BigInt(stat.ino); + return 0; + }, + writeStatFs(buf, stats) { + HEAPU32[(((buf)+(4))>>2)] = stats.bsize; + HEAPU32[(((buf)+(60))>>2)] = stats.bsize; + HEAP64[(((buf)+(8))>>3)] = BigInt(stats.blocks); + HEAP64[(((buf)+(16))>>3)] = BigInt(stats.bfree); + HEAP64[(((buf)+(24))>>3)] = BigInt(stats.bavail); + HEAP64[(((buf)+(32))>>3)] = BigInt(stats.files); + HEAP64[(((buf)+(40))>>3)] = BigInt(stats.ffree); + HEAPU32[(((buf)+(48))>>2)] = stats.fsid; + HEAPU32[(((buf)+(64))>>2)] = stats.flags; + HEAPU32[(((buf)+(56))>>2)] = stats.namelen; + }, + doMsync(addr, stream, len, flags, offset) { + if (!FS.isFile(stream.node.mode)) { + throw new FS.ErrnoError(43); + } + if (flags & 2) { + + return 0; + } + var buffer = HEAPU8.subarray(addr, addr + len); + FS.msync(stream, buffer, offset, len, flags); + }, + getStreamFromFD(fd) { + var stream = FS.getStreamChecked(fd); + return stream; + }, + varargs:undefined, + getStr(ptr) { + var ret = UTF8ToString(ptr); + return ret; + }, + }; + function ___syscall_chmod(path, mode) { + try { + + path = SYSCALLS.getStr(path); + FS.chmod(path, mode); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_faccessat(dirfd, path, amode, flags) { + try { + + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + if (amode & ~7) { + + return -28; + } + var lookup = FS.lookupPath(path, { follow: true }); + var node = lookup.node; + if (!node) { + return -44; + } + var perms = ''; + if (amode & 4) perms += 'r'; + if (amode & 2) perms += 'w'; + if (amode & 1) perms += 'x'; + if (perms && FS.nodePermissions(node, perms)) { + return -2; + } + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_fchmod(fd, mode) { + try { + + FS.fchmod(fd, mode); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_fchown32(fd, owner, group) { + try { + + FS.fchown(fd, owner, group); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + var syscallGetVarargI = () => { + + var ret = HEAP32[((+SYSCALLS.varargs)>>2)]; + SYSCALLS.varargs += 4; + return ret; + }; + var syscallGetVarargP = syscallGetVarargI; + + + function ___syscall_fcntl64(fd, cmd, varargs) { + SYSCALLS.varargs = varargs; + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + switch (cmd) { + case 0: { + var arg = syscallGetVarargI(); + if (arg < 0) { + return -28; + } + while (FS.streams[arg]) { + arg++; + } + var newStream; + newStream = FS.dupStream(stream, arg); + return newStream.fd; + } + case 1: + case 2: + return 0; + case 3: + return stream.flags; + case 4: { + var arg = syscallGetVarargI(); + var mask = 289792; + stream.flags = (stream.flags & ~mask) | (arg & mask); + return 0; + } + case 12: { + var arg = syscallGetVarargP(); + var offset = 0; + + HEAP16[(((arg)+(offset))>>1)] = 2; + return 0; + } + case 13: + case 14: + + + + + return 0; + } + return -28; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_fstat64(fd, buf) { + try { + + return SYSCALLS.writeStat(buf, FS.fstat(fd)); + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + var INT53_MAX = 9007199254740992; + + var INT53_MIN = -9007199254740992; + var bigintToI53Checked = (num) => (num < INT53_MIN || num > INT53_MAX) ? NaN : Number(num); + function ___syscall_ftruncate64(fd, length) { + length = bigintToI53Checked(length); + + + try { + + if (isNaN(length)) return -22; + FS.ftruncate(fd, length); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + ; + } + + + var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { + return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); + }; + function ___syscall_getcwd(buf, size) { + try { + + if (size === 0) return -28; + var cwd = FS.cwd(); + var cwdLengthInBytes = lengthBytesUTF8(cwd) + 1; + if (size < cwdLengthInBytes) return -68; + stringToUTF8(cwd, buf, size); + return cwdLengthInBytes; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + + function ___syscall_ioctl(fd, op, varargs) { + SYSCALLS.varargs = varargs; + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + switch (op) { + case 21509: { + if (!stream.tty) return -59; + return 0; + } + case 21505: { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcgets) { + var termios = stream.tty.ops.ioctl_tcgets(stream); + var argp = syscallGetVarargP(); + HEAP32[((argp)>>2)] = termios.c_iflag || 0; + HEAP32[(((argp)+(4))>>2)] = termios.c_oflag || 0; + HEAP32[(((argp)+(8))>>2)] = termios.c_cflag || 0; + HEAP32[(((argp)+(12))>>2)] = termios.c_lflag || 0; + for (var i = 0; i < 32; i++) { + HEAP8[(argp + i)+(17)] = termios.c_cc[i] || 0; + } + return 0; + } + return 0; + } + case 21510: + case 21511: + case 21512: { + if (!stream.tty) return -59; + return 0; + } + case 21506: + case 21507: + case 21508: { + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tcsets) { + var argp = syscallGetVarargP(); + var c_iflag = HEAP32[((argp)>>2)]; + var c_oflag = HEAP32[(((argp)+(4))>>2)]; + var c_cflag = HEAP32[(((argp)+(8))>>2)]; + var c_lflag = HEAP32[(((argp)+(12))>>2)]; + var c_cc = [] + for (var i = 0; i < 32; i++) { + c_cc.push(HEAP8[(argp + i)+(17)]); + } + return stream.tty.ops.ioctl_tcsets(stream.tty, op, { c_iflag, c_oflag, c_cflag, c_lflag, c_cc }); + } + return 0; + } + case 21519: { + if (!stream.tty) return -59; + var argp = syscallGetVarargP(); + HEAP32[((argp)>>2)] = 0; + return 0; + } + case 21520: { + if (!stream.tty) return -59; + return -28; + } + case 21537: + case 21531: { + var argp = syscallGetVarargP(); + return FS.ioctl(stream, op, argp); + } + case 21523: { + + + if (!stream.tty) return -59; + if (stream.tty.ops.ioctl_tiocgwinsz) { + var winsize = stream.tty.ops.ioctl_tiocgwinsz(stream.tty); + var argp = syscallGetVarargP(); + HEAP16[((argp)>>1)] = winsize[0]; + HEAP16[(((argp)+(2))>>1)] = winsize[1]; + } + return 0; + } + case 21524: { + + + + if (!stream.tty) return -59; + return 0; + } + case 21515: { + if (!stream.tty) return -59; + return 0; + } + default: return -28; + } + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_lstat64(path, buf) { + try { + + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.lstat(path)); + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_mkdirat(dirfd, path, mode) { + try { + + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + mode &= ~SYSCALLS.currentUmask; + FS.mkdir(path, mode, 0); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_newfstatat(dirfd, path, buf, flags) { + try { + + path = SYSCALLS.getStr(path); + var nofollow = flags & 256; + var allowEmpty = flags & 4096; + flags = flags & (~6400); + path = SYSCALLS.calculateAt(dirfd, path, allowEmpty); + return SYSCALLS.writeStat(buf, nofollow ? FS.lstat(path) : FS.stat(path)); + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + + function ___syscall_openat(dirfd, path, flags, varargs) { + SYSCALLS.varargs = varargs; + try { + + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + var mode = varargs ? syscallGetVarargI() : 0; + if (flags & 64) { + mode &= ~SYSCALLS.currentUmask; + } + return FS.open(path, flags, mode).fd; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + + + function ___syscall_readlinkat(dirfd, path, buf, bufsize) { + try { + + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + if (bufsize <= 0) return -28; + var ret = FS.readlink(path); + + var len = Math.min(bufsize, lengthBytesUTF8(ret)); + var endChar = HEAP8[buf+len]; + stringToUTF8(ret, buf, bufsize+1); + + + HEAP8[buf+len] = endChar; + return len; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_rmdir(path) { + try { + + path = SYSCALLS.getStr(path); + FS.rmdir(path); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_stat64(path, buf) { + try { + + path = SYSCALLS.getStr(path); + return SYSCALLS.writeStat(buf, FS.stat(path)); + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + function ___syscall_unlinkat(dirfd, path, flags) { + try { + + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path); + if (!flags) { + FS.unlink(path); + } else if (flags === 512) { + FS.rmdir(path); + } else { + return -28; + } + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + var readI53FromI64 = (ptr) => { + return HEAPU32[((ptr)>>2)] + HEAP32[(((ptr)+(4))>>2)] * 4294967296; + }; + + function ___syscall_utimensat(dirfd, path, times, flags) { + try { + + var nofollow = flags & 256; + path = SYSCALLS.getStr(path); + path = SYSCALLS.calculateAt(dirfd, path, true); + var now = Date.now(), atime, mtime; + if (!times) { + atime = now; + mtime = now; + } else { + var seconds = readI53FromI64(times); + var nanoseconds = HEAP32[(((times)+(8))>>2)]; + if (nanoseconds == 1073741823) { + atime = now; + } else if (nanoseconds == 1073741822) { + atime = null; + } else { + atime = (seconds*1000) + (nanoseconds/(1000*1000)); + } + times += 16; + seconds = readI53FromI64(times); + nanoseconds = HEAP32[(((times)+(8))>>2)]; + if (nanoseconds == 1073741823) { + mtime = now; + } else if (nanoseconds == 1073741822) { + mtime = null; + } else { + mtime = (seconds*1000) + (nanoseconds/(1000*1000)); + } + } + + + if ((mtime ?? atime) !== null) { + FS.utime(path, atime, mtime, nofollow); + } + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + } + + + var __abort_js = () => + abort(''); + + var isLeapYear = (year) => year%4 === 0 && (year%100 !== 0 || year%400 === 0); + + var MONTH_DAYS_LEAP_CUMULATIVE = [0,31,60,91,121,152,182,213,244,274,305,335]; + + var MONTH_DAYS_REGULAR_CUMULATIVE = [0,31,59,90,120,151,181,212,243,273,304,334]; + var ydayFromDate = (date) => { + var leap = isLeapYear(date.getFullYear()); + var monthDaysCumulative = (leap ? MONTH_DAYS_LEAP_CUMULATIVE : MONTH_DAYS_REGULAR_CUMULATIVE); + var yday = monthDaysCumulative[date.getMonth()] + date.getDate() - 1; + + return yday; + }; + + function __localtime_js(time, tmPtr) { + time = bigintToI53Checked(time); + + + var date = new Date(time*1000); + if (isNaN(date.getTime())) { + return 1; + } + HEAP32[((tmPtr)>>2)] = date.getSeconds(); + HEAP32[(((tmPtr)+(4))>>2)] = date.getMinutes(); + HEAP32[(((tmPtr)+(8))>>2)] = date.getHours(); + HEAP32[(((tmPtr)+(12))>>2)] = date.getDate(); + HEAP32[(((tmPtr)+(16))>>2)] = date.getMonth(); + HEAP32[(((tmPtr)+(20))>>2)] = date.getFullYear()-1900; + HEAP32[(((tmPtr)+(24))>>2)] = date.getDay(); + + var yday = ydayFromDate(date)|0; + HEAP32[(((tmPtr)+(28))>>2)] = yday; + HEAP32[(((tmPtr)+(36))>>2)] = -(date.getTimezoneOffset() * 60); + + + var start = new Date(date.getFullYear(), 0, 1); + var summerOffset = new Date(date.getFullYear(), 6, 1).getTimezoneOffset(); + var winterOffset = start.getTimezoneOffset(); + var dst = (summerOffset != winterOffset && date.getTimezoneOffset() == Math.min(winterOffset, summerOffset))|0; + HEAP32[(((tmPtr)+(32))>>2)] = dst; + return 0; + ; + } + + + + + + + function __mmap_js(len, prot, flags, fd, offset, allocated, addr) { + offset = bigintToI53Checked(offset); + + + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + var res = FS.mmap(stream, len, offset, prot, flags); + var ptr = res.ptr; + HEAP32[((allocated)>>2)] = res.allocated; + HEAPU32[((addr)>>2)] = ptr; + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + ; + } + + + function __munmap_js(addr, len, prot, flags, fd, offset) { + offset = bigintToI53Checked(offset); + + + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + if (prot & 2) { + SYSCALLS.doMsync(addr, stream, len, flags, offset); + } + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return -e.errno; + } + ; + } + + var __tzset_js = (timezone, daylight, std_name, dst_name) => { + + var currentYear = new Date().getFullYear(); + var winter = new Date(currentYear, 0, 1); + var summer = new Date(currentYear, 6, 1); + var winterOffset = winter.getTimezoneOffset(); + var summerOffset = summer.getTimezoneOffset(); + + + + + + + + var stdTimezoneOffset = Math.max(winterOffset, summerOffset); + + + + + + + HEAPU32[((timezone)>>2)] = stdTimezoneOffset * 60; + + HEAP32[((daylight)>>2)] = Number(winterOffset != summerOffset); + + var extractZone = (timezoneOffset) => { + + + var sign = timezoneOffset >= 0 ? "-" : "+"; + + var absOffset = Math.abs(timezoneOffset) + var hours = String(Math.floor(absOffset / 60)).padStart(2, "0"); + var minutes = String(absOffset % 60).padStart(2, "0"); + + return `UTC${sign}${hours}${minutes}`; + } + + var winterName = extractZone(winterOffset); + var summerName = extractZone(summerOffset); + if (summerOffset < winterOffset) { + + stringToUTF8(winterName, std_name, 17); + stringToUTF8(summerName, dst_name, 17); + } else { + stringToUTF8(winterName, dst_name, 17); + stringToUTF8(summerName, std_name, 17); + } + }; + + var _emscripten_get_now = () => performance.now(); + + var _emscripten_date_now = () => Date.now(); + + var nowIsMonotonic = 1; + + var checkWasiClock = (clock_id) => clock_id >= 0 && clock_id <= 3; + + function _clock_time_get(clk_id, ignored_precision, ptime) { + ignored_precision = bigintToI53Checked(ignored_precision); + + + if (!checkWasiClock(clk_id)) { + return 28; + } + var now; + + if (clk_id === 0) { + now = _emscripten_date_now(); + } else if (nowIsMonotonic) { + now = _emscripten_get_now(); + } else { + return 52; + } + + var nsec = Math.round(now * 1000 * 1000); + HEAP64[((ptime)>>3)] = BigInt(nsec); + return 0; + ; + } + + + var getHeapMax = () => + + + + + 2147483648; + var _emscripten_get_heap_max = () => getHeapMax(); + + + + + var growMemory = (size) => { + var oldHeapSize = wasmMemory.buffer.byteLength; + var pages = ((size - oldHeapSize + 65535) / 65536) | 0; + try { + + wasmMemory.grow(pages); + updateMemoryViews(); + return 1 ; + } catch(e) { + } + + + }; + var _emscripten_resize_heap = (requestedSize) => { + var oldSize = HEAPU8.length; + + requestedSize >>>= 0; + + + + + + + + + + + + + + + + + + + + + + + var maxHeapSize = getHeapMax(); + if (requestedSize > maxHeapSize) { + return false; + } + + + + + for (var cutDown = 1; cutDown <= 4; cutDown *= 2) { + var overGrownHeapSize = oldSize * (1 + 0.2 / cutDown); + + overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296 ); + + var newSize = Math.min(maxHeapSize, alignMemory(Math.max(requestedSize, overGrownHeapSize), 65536)); + + var replacement = growMemory(newSize); + if (replacement) { + + return true; + } + } + return false; + }; + + var ENV = { + }; + + var getExecutableName = () => thisProgram; + var getEnvStrings = () => { + if (!getEnvStrings.strings) { + + var lang = (globalThis.navigator?.language ?? 'C').replace('-', '_') + '.UTF-8'; + var env = { + 'USER': 'web_user', + 'LOGNAME': 'web_user', + 'PATH': '/', + 'PWD': '/', + 'HOME': '/home/web_user', + 'LANG': lang, + '_': getExecutableName() + }; + + for (var x in ENV) { + + + + if (ENV[x] === undefined) delete env[x]; + else env[x] = ENV[x]; + } + var strings = []; + for (var x in env) { + strings.push(`${x}=${env[x]}`); + } + getEnvStrings.strings = strings; + } + return getEnvStrings.strings; + }; + + var _environ_get = (__environ, environ_buf) => { + var bufSize = 0; + var envp = 0; + for (var string of getEnvStrings()) { + var ptr = environ_buf + bufSize; + HEAPU32[(((__environ)+(envp))>>2)] = ptr; + bufSize += stringToUTF8(string, ptr, Infinity) + 1; + envp += 4; + } + return 0; + }; + + + var _environ_sizes_get = (penviron_count, penviron_buf_size) => { + var strings = getEnvStrings(); + HEAPU32[((penviron_count)>>2)] = strings.length; + var bufSize = 0; + for (var string of strings) { + bufSize += lengthBytesUTF8(string) + 1; + } + HEAPU32[((penviron_buf_size)>>2)] = bufSize; + return 0; + }; + + + var runtimeKeepaliveCounter = 0; + var keepRuntimeAlive = () => noExitRuntime || runtimeKeepaliveCounter > 0; + var _proc_exit = (code) => { + EXITSTATUS = code; + if (!keepRuntimeAlive()) { + Module['onExit']?.(code); + ABORT = true; + } + quit_(code, new ExitStatus(code)); + }; + + var exitJS = (status, implicit) => { + EXITSTATUS = status; + + _proc_exit(status); + }; + var _exit = exitJS; + + function _fd_close(fd) { + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + FS.close(stream); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + } + + + function _fd_fdstat_get(fd, pbuf) { + try { + + var rightsBase = 0; + var rightsInheriting = 0; + var flags = 0; + { + var stream = SYSCALLS.getStreamFromFD(fd); + + + var type = stream.tty ? 2 : + FS.isDir(stream.mode) ? 3 : + FS.isLink(stream.mode) ? 7 : + 4; + } + HEAP8[pbuf] = type; + HEAP16[(((pbuf)+(2))>>1)] = flags; + HEAP64[(((pbuf)+(8))>>3)] = BigInt(rightsBase); + HEAP64[(((pbuf)+(16))>>3)] = BigInt(rightsInheriting); + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + } + + + + var doReadv = (stream, iov, iovcnt, offset) => { + var ret = 0; + for (var i = 0; i < iovcnt; i++) { + var ptr = HEAPU32[((iov)>>2)]; + var len = HEAPU32[(((iov)+(4))>>2)]; + iov += 8; + try { + var curr = FS.read(stream, HEAP8, ptr, len, offset); + } catch (e) { + + + + if (ret > 0 && e instanceof FS.ErrnoError && + (e.errno == 6 || e.errno == 6)) { + break; + } + throw e; + } + if (curr < 0) return -1; + ret += curr; + if (curr < len) break; + if (typeof offset != 'undefined') { + offset += curr; + } + } + return ret; + }; + + function _fd_read(fd, iov, iovcnt, pnum) { + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doReadv(stream, iov, iovcnt); + HEAPU32[((pnum)>>2)] = num; + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + } + + + + function _fd_seek(fd, offset, whence, newOffset) { + offset = bigintToI53Checked(offset); + + + try { + + if (isNaN(offset)) return 22; + var stream = SYSCALLS.getStreamFromFD(fd); + FS.llseek(stream, offset, whence); + HEAP64[((newOffset)>>3)] = BigInt(stream.position); + if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + ; + } + + function _fd_sync(fd) { + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + var rtn = stream.stream_ops?.fsync?.(stream); + return rtn; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + } + + + + var doWritev = (stream, iov, iovcnt, offset) => { + + + + + if (iovcnt == 1) { + + return FS.write(stream, HEAP8, HEAPU32[((iov)>>2)], HEAPU32[(((iov)+(4))>>2)], offset); + } + var total = 0; + for (var i = 0, p = iov; i < iovcnt; i++, p += 8) { + total += HEAPU32[(((p)+(4))>>2)]; + } + var view = new Uint8Array(total); + var voff = 0; + for (var i = 0; i < iovcnt; i++, iov += 8) { + var ptr = HEAPU32[((iov)>>2)]; + var len = HEAPU32[(((iov)+(4))>>2)]; + view.set(HEAPU8.subarray(ptr, ptr + len), voff); + voff += len; + } + return FS.write(stream, view, 0, total, offset); + }; + + function _fd_write(fd, iov, iovcnt, pnum) { + try { + + var stream = SYSCALLS.getStreamFromFD(fd); + var num = doWritev(stream, iov, iovcnt); + HEAPU32[((pnum)>>2)] = num; + return 0; + } catch (e) { + if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; + return e.errno; + } + } + + + + FS.createPreloadedFile = FS_createPreloadedFile; + FS.preloadFile = FS_preloadFile; + FS.staticInit();; + + + + + + +{ + + + initMemory(); + + + if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; + +if (Module['print']) out = Module['print']; +if (Module['printErr']) err = Module['printErr']; + + + if (Module['arguments']) programArgs = Module['arguments']; + if (Module['thisProgram']) thisProgram = Module['thisProgram']; + + var preInit = Module['preInit']; + if (preInit) { + if (typeof preInit == 'function') Module['preInit'] = preInit = [preInit]; + + + while (preInit.length > 0) { + preInit.shift()(); + } + } +} + + + Module['wasmMemory'] = wasmMemory; + + + + + + +function wasm_crypto_getrandom(buf,n) { if (typeof crypto === 'undefined' || !crypto.getRandomValues) return -1; try { var view = new Uint8Array(HEAPU8.buffer, buf, n); var chunk = 65536; for (var i = 0; i < n; i += chunk) { crypto.getRandomValues(view.subarray(i, Math.min(i + chunk, n))); } return 0; } catch (e) { return -1; } } + + +var _sqlite3_status64, + _sqlite3_status, + _sqlite3_db_status64, + _sqlite3_msize, + _sqlite3_db_status, + _sqlite3_vfs_find, + _sqlite3_initialize, + _sqlite3_vfs_register, + _sqlite3_vfs_unregister, + _sqlite3_malloc, + _sqlite3_malloc64, + _sqlite3_free, + _sqlite3_realloc, + _sqlite3_realloc64, + _sqlite3_value_text, + _sqlite3_randomness, + _sqlite3_stricmp, + _sqlite3_strnicmp, + _sqlite3_uri_parameter, + _sqlite3_uri_boolean, + _sqlite3_serialize, + _sqlite3_prepare_v2, + _sqlite3_step, + _sqlite3_column_int64, + _sqlite3_reset, + _sqlite3_exec, + _sqlite3_column_int, + _sqlite3_finalize, + _sqlite3_file_control, + _sqlite3_column_name, + _sqlite3_column_text, + _sqlite3_column_type, + _sqlite3_errmsg, + _sqlite3_deserialize, + _sqlite3_backup_init, + _sqlite3_backup_step, + _sqlite3_backup_finish, + _sqlite3_backup_remaining, + _sqlite3_backup_pagecount, + _sqlite3_clear_bindings, + _sqlite3_value_blob, + _sqlite3_value_bytes, + _sqlite3_value_double, + _sqlite3_value_int, + _sqlite3_value_int64, + _sqlite3_value_subtype, + _sqlite3_value_pointer, + _sqlite3_value_type, + _sqlite3_value_nochange, + _sqlite3_value_frombind, + _sqlite3_value_dup, + _sqlite3_value_free, + _sqlite3_result_blob, + _sqlite3_result_error_toobig, + _sqlite3_result_error_nomem, + _sqlite3_result_double, + _sqlite3_result_error, + _sqlite3_result_int, + _sqlite3_result_int64, + _sqlite3_result_null, + _sqlite3_result_pointer, + _sqlite3_result_subtype, + _sqlite3_result_text, + _sqlite3_result_zeroblob, + _sqlite3_result_zeroblob64, + _sqlite3_result_error_code, + _sqlite3_user_data, + _sqlite3_context_db_handle, + _sqlite3_vtab_nochange, + _sqlite3_vtab_in_first, + _sqlite3_vtab_in_next, + _sqlite3_aggregate_context, + _sqlite3_get_auxdata, + _sqlite3_set_auxdata, + _sqlite3_column_count, + _sqlite3_data_count, + _sqlite3_column_blob, + _sqlite3_column_bytes, + _sqlite3_column_double, + _sqlite3_column_value, + _sqlite3_column_decltype, + _sqlite3_column_database_name, + _sqlite3_column_table_name, + _sqlite3_column_origin_name, + _sqlite3_bind_blob, + _sqlite3_bind_double, + _sqlite3_bind_int, + _sqlite3_bind_int64, + _sqlite3_bind_null, + _sqlite3_bind_pointer, + _sqlite3_bind_text, + _sqlite3_bind_zeroblob, + _sqlite3_bind_parameter_count, + _sqlite3_bind_parameter_name, + _sqlite3_bind_parameter_index, + _sqlite3_db_handle, + _sqlite3_stmt_readonly, + _sqlite3_stmt_isexplain, + _sqlite3_stmt_explain, + _sqlite3_stmt_busy, + _sqlite3_next_stmt, + _sqlite3_stmt_status, + _sqlite3_sql, + _sqlite3_expanded_sql, + _sqlite3_preupdate_old, + _sqlite3_preupdate_count, + _sqlite3_preupdate_depth, + _sqlite3_preupdate_blobwrite, + _sqlite3_preupdate_new, + _sqlite3_value_numeric_type, + _sqlite3_set_authorizer, + _sqlite3_strglob, + _sqlite3_strlike, + _sqlite3_auto_extension, + _sqlite3_cancel_auto_extension, + _sqlite3_reset_auto_extension, + _sqlite3_prepare_v3, + _sqlite3_create_module, + _sqlite3_create_module_v2, + _sqlite3_drop_modules, + _sqlite3_declare_vtab, + _sqlite3_vtab_on_conflict, + _sqlite3_vtab_collation, + _sqlite3_vtab_in, + _sqlite3_vtab_rhs_value, + _sqlite3_vtab_distinct, + _sqlite3_keyword_name, + _sqlite3_keyword_count, + _sqlite3_keyword_check, + _sqlite3_complete, + _sqlite3_libversion, + _sqlite3_libversion_number, + _sqlite3mc_vfs_create, + _sqlite3_shutdown, + _sqlite3mc_vfs_shutdown, + _sqlite3_last_insert_rowid, + _sqlite3_set_last_insert_rowid, + _sqlite3_changes64, + _sqlite3_changes, + _sqlite3_total_changes64, + _sqlite3_total_changes, + _sqlite3_txn_state, + _sqlite3_close_v2, + _sqlite3_busy_handler, + _sqlite3_progress_handler, + _sqlite3_busy_timeout, + _sqlite3_interrupt, + _sqlite3_is_interrupted, + _sqlite3_create_function, + _sqlite3_create_function_v2, + _sqlite3_create_window_function, + _sqlite3_overload_function, + _sqlite3_trace_v2, + _sqlite3_commit_hook, + _sqlite3_update_hook, + _sqlite3_rollback_hook, + _sqlite3_preupdate_hook, + _sqlite3_set_errmsg, + _sqlite3_error_offset, + _sqlite3_errcode, + _sqlite3_extended_errcode, + _sqlite3_errstr, + _sqlite3_limit, + _sqlite3_open, + _sqlite3_open_v2, + _sqlite3_create_collation, + _sqlite3_create_collation_v2, + _sqlite3_collation_needed, + _sqlite3_get_autocommit, + _sqlite3_table_column_metadata, + _sqlite3_extended_result_codes, + _sqlite3_uri_key, + _sqlite3_uri_int64, + _sqlite3_db_name, + _sqlite3_db_filename, + _sqlite3_db_readonly, + _sqlite3_compileoption_used, + _sqlite3_compileoption_get, + _sqlite3session_diff, + _sqlite3session_attach, + _sqlite3session_create, + _sqlite3session_delete, + _sqlite3session_table_filter, + _sqlite3session_changeset, + _sqlite3session_changeset_strm, + _sqlite3session_patchset_strm, + _sqlite3session_patchset, + _sqlite3session_enable, + _sqlite3session_indirect, + _sqlite3session_isempty, + _sqlite3session_memory_used, + _sqlite3session_object_config, + _sqlite3session_changeset_size, + _sqlite3changeset_start, + _sqlite3changeset_start_v2, + _sqlite3changeset_start_strm, + _sqlite3changeset_start_v2_strm, + _sqlite3changeset_next, + _sqlite3changeset_op, + _sqlite3changeset_pk, + _sqlite3changeset_old, + _sqlite3changeset_new, + _sqlite3changeset_conflict, + _sqlite3changeset_fk_conflicts, + _sqlite3changeset_finalize, + _sqlite3changeset_invert, + _sqlite3changeset_invert_strm, + _sqlite3changeset_apply_v2, + _sqlite3changeset_apply_v3, + _sqlite3changeset_apply, + _sqlite3changeset_apply_v3_strm, + _sqlite3changeset_apply_v2_strm, + _sqlite3changeset_apply_strm, + _sqlite3changegroup_new, + _sqlite3changegroup_add, + _sqlite3changegroup_output, + _sqlite3changegroup_add_strm, + _sqlite3changegroup_output_strm, + _sqlite3changegroup_delete, + _sqlite3changeset_concat, + _sqlite3changeset_concat_strm, + _sqlite3session_config, + _sqlite3_sourceid, + _sqlite3mc_version, + _sqlite3mc_config, + _sqlite3mc_cipher_count, + _sqlite3mc_cipher_index, + _sqlite3mc_cipher_name, + _sqlite3mc_cipher_name_copy, + _sqlite3mc_config_cipher, + _sqlite3mc_codec_data, + _sqlite3_activate_see, + _sqlite3_key, + _sqlite3_key_v2, + _sqlite3_rekey_v2, + _sqlite3_rekey, + _sqlite3mc_vfs_destroy, + _sqlite3__wasm_pstack_ptr, + _sqlite3__wasm_pstack_restore, + _sqlite3__wasm_pstack_alloc, + _sqlite3__wasm_pstack_remaining, + _sqlite3__wasm_pstack_quota, + _sqlite3__wasm_test_struct, + _sqlite3__wasm_enum_json, + _sqlite3__wasm_vfs_unlink, + _sqlite3__wasm_db_vfs, + _sqlite3__wasm_db_reset, + _sqlite3__wasm_db_export_chunked, + _sqlite3__wasm_db_serialize, + _sqlite3__wasm_vfs_create_file, + _sqlite3__wasm_posix_create_file, + _sqlite3__wasm_kvvfsMakeKey, + _sqlite3__wasm_kvvfs_methods, + _sqlite3__wasm_vtab_config, + _sqlite3__wasm_db_config_ip, + _sqlite3__wasm_db_config_pii, + _sqlite3__wasm_db_config_s, + _sqlite3__wasm_config_i, + _sqlite3__wasm_config_ii, + _sqlite3__wasm_config_j, + _sqlite3__wasm_qfmt_token, + _sqlite3__wasm_kvvfs_decode, + _sqlite3__wasm_kvvfs_encode, + _sqlite3__wasm_init_wasmfs, + _sqlite3__wasm_test_intptr, + _sqlite3__wasm_test_voidptr, + _sqlite3__wasm_test_int64_max, + _sqlite3__wasm_test_int64_min, + _sqlite3__wasm_test_int64_times2, + _sqlite3__wasm_test_int64_minmax, + _sqlite3__wasm_test_int64ptr, + _sqlite3__wasm_test_stack_overflow, + _sqlite3__wasm_test_str_hello, + _sqlite3__wasm_SQLTester_strglob, + _malloc, + _free, + _realloc, + _emscripten_builtin_memalign, + __emscripten_stack_restore, + __emscripten_stack_alloc, + _emscripten_stack_get_current, + __indirect_function_table; + + +function assignWasmExports(wasmExports) { + _sqlite3_status64 = Module['_sqlite3_status64'] = wasmExports['sqlite3_status64']; + _sqlite3_status = Module['_sqlite3_status'] = wasmExports['sqlite3_status']; + _sqlite3_db_status64 = Module['_sqlite3_db_status64'] = wasmExports['sqlite3_db_status64']; + _sqlite3_msize = Module['_sqlite3_msize'] = wasmExports['sqlite3_msize']; + _sqlite3_db_status = Module['_sqlite3_db_status'] = wasmExports['sqlite3_db_status']; + _sqlite3_vfs_find = Module['_sqlite3_vfs_find'] = wasmExports['sqlite3_vfs_find']; + _sqlite3_initialize = Module['_sqlite3_initialize'] = wasmExports['sqlite3_initialize']; + _sqlite3_vfs_register = Module['_sqlite3_vfs_register'] = wasmExports['sqlite3_vfs_register']; + _sqlite3_vfs_unregister = Module['_sqlite3_vfs_unregister'] = wasmExports['sqlite3_vfs_unregister']; + _sqlite3_malloc = Module['_sqlite3_malloc'] = wasmExports['sqlite3_malloc']; + _sqlite3_malloc64 = Module['_sqlite3_malloc64'] = wasmExports['sqlite3_malloc64']; + _sqlite3_free = Module['_sqlite3_free'] = wasmExports['sqlite3_free']; + _sqlite3_realloc = Module['_sqlite3_realloc'] = wasmExports['sqlite3_realloc']; + _sqlite3_realloc64 = Module['_sqlite3_realloc64'] = wasmExports['sqlite3_realloc64']; + _sqlite3_value_text = Module['_sqlite3_value_text'] = wasmExports['sqlite3_value_text']; + _sqlite3_randomness = Module['_sqlite3_randomness'] = wasmExports['sqlite3_randomness']; + _sqlite3_stricmp = Module['_sqlite3_stricmp'] = wasmExports['sqlite3_stricmp']; + _sqlite3_strnicmp = Module['_sqlite3_strnicmp'] = wasmExports['sqlite3_strnicmp']; + _sqlite3_uri_parameter = Module['_sqlite3_uri_parameter'] = wasmExports['sqlite3_uri_parameter']; + _sqlite3_uri_boolean = Module['_sqlite3_uri_boolean'] = wasmExports['sqlite3_uri_boolean']; + _sqlite3_serialize = Module['_sqlite3_serialize'] = wasmExports['sqlite3_serialize']; + _sqlite3_prepare_v2 = Module['_sqlite3_prepare_v2'] = wasmExports['sqlite3_prepare_v2']; + _sqlite3_step = Module['_sqlite3_step'] = wasmExports['sqlite3_step']; + _sqlite3_column_int64 = Module['_sqlite3_column_int64'] = wasmExports['sqlite3_column_int64']; + _sqlite3_reset = Module['_sqlite3_reset'] = wasmExports['sqlite3_reset']; + _sqlite3_exec = Module['_sqlite3_exec'] = wasmExports['sqlite3_exec']; + _sqlite3_column_int = Module['_sqlite3_column_int'] = wasmExports['sqlite3_column_int']; + _sqlite3_finalize = Module['_sqlite3_finalize'] = wasmExports['sqlite3_finalize']; + _sqlite3_file_control = Module['_sqlite3_file_control'] = wasmExports['sqlite3_file_control']; + _sqlite3_column_name = Module['_sqlite3_column_name'] = wasmExports['sqlite3_column_name']; + _sqlite3_column_text = Module['_sqlite3_column_text'] = wasmExports['sqlite3_column_text']; + _sqlite3_column_type = Module['_sqlite3_column_type'] = wasmExports['sqlite3_column_type']; + _sqlite3_errmsg = Module['_sqlite3_errmsg'] = wasmExports['sqlite3_errmsg']; + _sqlite3_deserialize = Module['_sqlite3_deserialize'] = wasmExports['sqlite3_deserialize']; + _sqlite3_backup_init = Module['_sqlite3_backup_init'] = wasmExports['sqlite3_backup_init']; + _sqlite3_backup_step = Module['_sqlite3_backup_step'] = wasmExports['sqlite3_backup_step']; + _sqlite3_backup_finish = Module['_sqlite3_backup_finish'] = wasmExports['sqlite3_backup_finish']; + _sqlite3_backup_remaining = Module['_sqlite3_backup_remaining'] = wasmExports['sqlite3_backup_remaining']; + _sqlite3_backup_pagecount = Module['_sqlite3_backup_pagecount'] = wasmExports['sqlite3_backup_pagecount']; + _sqlite3_clear_bindings = Module['_sqlite3_clear_bindings'] = wasmExports['sqlite3_clear_bindings']; + _sqlite3_value_blob = Module['_sqlite3_value_blob'] = wasmExports['sqlite3_value_blob']; + _sqlite3_value_bytes = Module['_sqlite3_value_bytes'] = wasmExports['sqlite3_value_bytes']; + _sqlite3_value_double = Module['_sqlite3_value_double'] = wasmExports['sqlite3_value_double']; + _sqlite3_value_int = Module['_sqlite3_value_int'] = wasmExports['sqlite3_value_int']; + _sqlite3_value_int64 = Module['_sqlite3_value_int64'] = wasmExports['sqlite3_value_int64']; + _sqlite3_value_subtype = Module['_sqlite3_value_subtype'] = wasmExports['sqlite3_value_subtype']; + _sqlite3_value_pointer = Module['_sqlite3_value_pointer'] = wasmExports['sqlite3_value_pointer']; + _sqlite3_value_type = Module['_sqlite3_value_type'] = wasmExports['sqlite3_value_type']; + _sqlite3_value_nochange = Module['_sqlite3_value_nochange'] = wasmExports['sqlite3_value_nochange']; + _sqlite3_value_frombind = Module['_sqlite3_value_frombind'] = wasmExports['sqlite3_value_frombind']; + _sqlite3_value_dup = Module['_sqlite3_value_dup'] = wasmExports['sqlite3_value_dup']; + _sqlite3_value_free = Module['_sqlite3_value_free'] = wasmExports['sqlite3_value_free']; + _sqlite3_result_blob = Module['_sqlite3_result_blob'] = wasmExports['sqlite3_result_blob']; + _sqlite3_result_error_toobig = Module['_sqlite3_result_error_toobig'] = wasmExports['sqlite3_result_error_toobig']; + _sqlite3_result_error_nomem = Module['_sqlite3_result_error_nomem'] = wasmExports['sqlite3_result_error_nomem']; + _sqlite3_result_double = Module['_sqlite3_result_double'] = wasmExports['sqlite3_result_double']; + _sqlite3_result_error = Module['_sqlite3_result_error'] = wasmExports['sqlite3_result_error']; + _sqlite3_result_int = Module['_sqlite3_result_int'] = wasmExports['sqlite3_result_int']; + _sqlite3_result_int64 = Module['_sqlite3_result_int64'] = wasmExports['sqlite3_result_int64']; + _sqlite3_result_null = Module['_sqlite3_result_null'] = wasmExports['sqlite3_result_null']; + _sqlite3_result_pointer = Module['_sqlite3_result_pointer'] = wasmExports['sqlite3_result_pointer']; + _sqlite3_result_subtype = Module['_sqlite3_result_subtype'] = wasmExports['sqlite3_result_subtype']; + _sqlite3_result_text = Module['_sqlite3_result_text'] = wasmExports['sqlite3_result_text']; + _sqlite3_result_zeroblob = Module['_sqlite3_result_zeroblob'] = wasmExports['sqlite3_result_zeroblob']; + _sqlite3_result_zeroblob64 = Module['_sqlite3_result_zeroblob64'] = wasmExports['sqlite3_result_zeroblob64']; + _sqlite3_result_error_code = Module['_sqlite3_result_error_code'] = wasmExports['sqlite3_result_error_code']; + _sqlite3_user_data = Module['_sqlite3_user_data'] = wasmExports['sqlite3_user_data']; + _sqlite3_context_db_handle = Module['_sqlite3_context_db_handle'] = wasmExports['sqlite3_context_db_handle']; + _sqlite3_vtab_nochange = Module['_sqlite3_vtab_nochange'] = wasmExports['sqlite3_vtab_nochange']; + _sqlite3_vtab_in_first = Module['_sqlite3_vtab_in_first'] = wasmExports['sqlite3_vtab_in_first']; + _sqlite3_vtab_in_next = Module['_sqlite3_vtab_in_next'] = wasmExports['sqlite3_vtab_in_next']; + _sqlite3_aggregate_context = Module['_sqlite3_aggregate_context'] = wasmExports['sqlite3_aggregate_context']; + _sqlite3_get_auxdata = Module['_sqlite3_get_auxdata'] = wasmExports['sqlite3_get_auxdata']; + _sqlite3_set_auxdata = Module['_sqlite3_set_auxdata'] = wasmExports['sqlite3_set_auxdata']; + _sqlite3_column_count = Module['_sqlite3_column_count'] = wasmExports['sqlite3_column_count']; + _sqlite3_data_count = Module['_sqlite3_data_count'] = wasmExports['sqlite3_data_count']; + _sqlite3_column_blob = Module['_sqlite3_column_blob'] = wasmExports['sqlite3_column_blob']; + _sqlite3_column_bytes = Module['_sqlite3_column_bytes'] = wasmExports['sqlite3_column_bytes']; + _sqlite3_column_double = Module['_sqlite3_column_double'] = wasmExports['sqlite3_column_double']; + _sqlite3_column_value = Module['_sqlite3_column_value'] = wasmExports['sqlite3_column_value']; + _sqlite3_column_decltype = Module['_sqlite3_column_decltype'] = wasmExports['sqlite3_column_decltype']; + _sqlite3_column_database_name = Module['_sqlite3_column_database_name'] = wasmExports['sqlite3_column_database_name']; + _sqlite3_column_table_name = Module['_sqlite3_column_table_name'] = wasmExports['sqlite3_column_table_name']; + _sqlite3_column_origin_name = Module['_sqlite3_column_origin_name'] = wasmExports['sqlite3_column_origin_name']; + _sqlite3_bind_blob = Module['_sqlite3_bind_blob'] = wasmExports['sqlite3_bind_blob']; + _sqlite3_bind_double = Module['_sqlite3_bind_double'] = wasmExports['sqlite3_bind_double']; + _sqlite3_bind_int = Module['_sqlite3_bind_int'] = wasmExports['sqlite3_bind_int']; + _sqlite3_bind_int64 = Module['_sqlite3_bind_int64'] = wasmExports['sqlite3_bind_int64']; + _sqlite3_bind_null = Module['_sqlite3_bind_null'] = wasmExports['sqlite3_bind_null']; + _sqlite3_bind_pointer = Module['_sqlite3_bind_pointer'] = wasmExports['sqlite3_bind_pointer']; + _sqlite3_bind_text = Module['_sqlite3_bind_text'] = wasmExports['sqlite3_bind_text']; + _sqlite3_bind_zeroblob = Module['_sqlite3_bind_zeroblob'] = wasmExports['sqlite3_bind_zeroblob']; + _sqlite3_bind_parameter_count = Module['_sqlite3_bind_parameter_count'] = wasmExports['sqlite3_bind_parameter_count']; + _sqlite3_bind_parameter_name = Module['_sqlite3_bind_parameter_name'] = wasmExports['sqlite3_bind_parameter_name']; + _sqlite3_bind_parameter_index = Module['_sqlite3_bind_parameter_index'] = wasmExports['sqlite3_bind_parameter_index']; + _sqlite3_db_handle = Module['_sqlite3_db_handle'] = wasmExports['sqlite3_db_handle']; + _sqlite3_stmt_readonly = Module['_sqlite3_stmt_readonly'] = wasmExports['sqlite3_stmt_readonly']; + _sqlite3_stmt_isexplain = Module['_sqlite3_stmt_isexplain'] = wasmExports['sqlite3_stmt_isexplain']; + _sqlite3_stmt_explain = Module['_sqlite3_stmt_explain'] = wasmExports['sqlite3_stmt_explain']; + _sqlite3_stmt_busy = Module['_sqlite3_stmt_busy'] = wasmExports['sqlite3_stmt_busy']; + _sqlite3_next_stmt = Module['_sqlite3_next_stmt'] = wasmExports['sqlite3_next_stmt']; + _sqlite3_stmt_status = Module['_sqlite3_stmt_status'] = wasmExports['sqlite3_stmt_status']; + _sqlite3_sql = Module['_sqlite3_sql'] = wasmExports['sqlite3_sql']; + _sqlite3_expanded_sql = Module['_sqlite3_expanded_sql'] = wasmExports['sqlite3_expanded_sql']; + _sqlite3_preupdate_old = Module['_sqlite3_preupdate_old'] = wasmExports['sqlite3_preupdate_old']; + _sqlite3_preupdate_count = Module['_sqlite3_preupdate_count'] = wasmExports['sqlite3_preupdate_count']; + _sqlite3_preupdate_depth = Module['_sqlite3_preupdate_depth'] = wasmExports['sqlite3_preupdate_depth']; + _sqlite3_preupdate_blobwrite = Module['_sqlite3_preupdate_blobwrite'] = wasmExports['sqlite3_preupdate_blobwrite']; + _sqlite3_preupdate_new = Module['_sqlite3_preupdate_new'] = wasmExports['sqlite3_preupdate_new']; + _sqlite3_value_numeric_type = Module['_sqlite3_value_numeric_type'] = wasmExports['sqlite3_value_numeric_type']; + _sqlite3_set_authorizer = Module['_sqlite3_set_authorizer'] = wasmExports['sqlite3_set_authorizer']; + _sqlite3_strglob = Module['_sqlite3_strglob'] = wasmExports['sqlite3_strglob']; + _sqlite3_strlike = Module['_sqlite3_strlike'] = wasmExports['sqlite3_strlike']; + _sqlite3_auto_extension = Module['_sqlite3_auto_extension'] = wasmExports['sqlite3_auto_extension']; + _sqlite3_cancel_auto_extension = Module['_sqlite3_cancel_auto_extension'] = wasmExports['sqlite3_cancel_auto_extension']; + _sqlite3_reset_auto_extension = Module['_sqlite3_reset_auto_extension'] = wasmExports['sqlite3_reset_auto_extension']; + _sqlite3_prepare_v3 = Module['_sqlite3_prepare_v3'] = wasmExports['sqlite3_prepare_v3']; + _sqlite3_create_module = Module['_sqlite3_create_module'] = wasmExports['sqlite3_create_module']; + _sqlite3_create_module_v2 = Module['_sqlite3_create_module_v2'] = wasmExports['sqlite3_create_module_v2']; + _sqlite3_drop_modules = Module['_sqlite3_drop_modules'] = wasmExports['sqlite3_drop_modules']; + _sqlite3_declare_vtab = Module['_sqlite3_declare_vtab'] = wasmExports['sqlite3_declare_vtab']; + _sqlite3_vtab_on_conflict = Module['_sqlite3_vtab_on_conflict'] = wasmExports['sqlite3_vtab_on_conflict']; + _sqlite3_vtab_collation = Module['_sqlite3_vtab_collation'] = wasmExports['sqlite3_vtab_collation']; + _sqlite3_vtab_in = Module['_sqlite3_vtab_in'] = wasmExports['sqlite3_vtab_in']; + _sqlite3_vtab_rhs_value = Module['_sqlite3_vtab_rhs_value'] = wasmExports['sqlite3_vtab_rhs_value']; + _sqlite3_vtab_distinct = Module['_sqlite3_vtab_distinct'] = wasmExports['sqlite3_vtab_distinct']; + _sqlite3_keyword_name = Module['_sqlite3_keyword_name'] = wasmExports['sqlite3_keyword_name']; + _sqlite3_keyword_count = Module['_sqlite3_keyword_count'] = wasmExports['sqlite3_keyword_count']; + _sqlite3_keyword_check = Module['_sqlite3_keyword_check'] = wasmExports['sqlite3_keyword_check']; + _sqlite3_complete = Module['_sqlite3_complete'] = wasmExports['sqlite3_complete']; + _sqlite3_libversion = Module['_sqlite3_libversion'] = wasmExports['sqlite3_libversion']; + _sqlite3_libversion_number = Module['_sqlite3_libversion_number'] = wasmExports['sqlite3_libversion_number']; + _sqlite3mc_vfs_create = Module['_sqlite3mc_vfs_create'] = wasmExports['sqlite3mc_vfs_create']; + _sqlite3_shutdown = Module['_sqlite3_shutdown'] = wasmExports['sqlite3_shutdown']; + _sqlite3mc_vfs_shutdown = Module['_sqlite3mc_vfs_shutdown'] = wasmExports['sqlite3mc_vfs_shutdown']; + _sqlite3_last_insert_rowid = Module['_sqlite3_last_insert_rowid'] = wasmExports['sqlite3_last_insert_rowid']; + _sqlite3_set_last_insert_rowid = Module['_sqlite3_set_last_insert_rowid'] = wasmExports['sqlite3_set_last_insert_rowid']; + _sqlite3_changes64 = Module['_sqlite3_changes64'] = wasmExports['sqlite3_changes64']; + _sqlite3_changes = Module['_sqlite3_changes'] = wasmExports['sqlite3_changes']; + _sqlite3_total_changes64 = Module['_sqlite3_total_changes64'] = wasmExports['sqlite3_total_changes64']; + _sqlite3_total_changes = Module['_sqlite3_total_changes'] = wasmExports['sqlite3_total_changes']; + _sqlite3_txn_state = Module['_sqlite3_txn_state'] = wasmExports['sqlite3_txn_state']; + _sqlite3_close_v2 = Module['_sqlite3_close_v2'] = wasmExports['sqlite3_close_v2']; + _sqlite3_busy_handler = Module['_sqlite3_busy_handler'] = wasmExports['sqlite3_busy_handler']; + _sqlite3_progress_handler = Module['_sqlite3_progress_handler'] = wasmExports['sqlite3_progress_handler']; + _sqlite3_busy_timeout = Module['_sqlite3_busy_timeout'] = wasmExports['sqlite3_busy_timeout']; + _sqlite3_interrupt = Module['_sqlite3_interrupt'] = wasmExports['sqlite3_interrupt']; + _sqlite3_is_interrupted = Module['_sqlite3_is_interrupted'] = wasmExports['sqlite3_is_interrupted']; + _sqlite3_create_function = Module['_sqlite3_create_function'] = wasmExports['sqlite3_create_function']; + _sqlite3_create_function_v2 = Module['_sqlite3_create_function_v2'] = wasmExports['sqlite3_create_function_v2']; + _sqlite3_create_window_function = Module['_sqlite3_create_window_function'] = wasmExports['sqlite3_create_window_function']; + _sqlite3_overload_function = Module['_sqlite3_overload_function'] = wasmExports['sqlite3_overload_function']; + _sqlite3_trace_v2 = Module['_sqlite3_trace_v2'] = wasmExports['sqlite3_trace_v2']; + _sqlite3_commit_hook = Module['_sqlite3_commit_hook'] = wasmExports['sqlite3_commit_hook']; + _sqlite3_update_hook = Module['_sqlite3_update_hook'] = wasmExports['sqlite3_update_hook']; + _sqlite3_rollback_hook = Module['_sqlite3_rollback_hook'] = wasmExports['sqlite3_rollback_hook']; + _sqlite3_preupdate_hook = Module['_sqlite3_preupdate_hook'] = wasmExports['sqlite3_preupdate_hook']; + _sqlite3_set_errmsg = Module['_sqlite3_set_errmsg'] = wasmExports['sqlite3_set_errmsg']; + _sqlite3_error_offset = Module['_sqlite3_error_offset'] = wasmExports['sqlite3_error_offset']; + _sqlite3_errcode = Module['_sqlite3_errcode'] = wasmExports['sqlite3_errcode']; + _sqlite3_extended_errcode = Module['_sqlite3_extended_errcode'] = wasmExports['sqlite3_extended_errcode']; + _sqlite3_errstr = Module['_sqlite3_errstr'] = wasmExports['sqlite3_errstr']; + _sqlite3_limit = Module['_sqlite3_limit'] = wasmExports['sqlite3_limit']; + _sqlite3_open = Module['_sqlite3_open'] = wasmExports['sqlite3_open']; + _sqlite3_open_v2 = Module['_sqlite3_open_v2'] = wasmExports['sqlite3_open_v2']; + _sqlite3_create_collation = Module['_sqlite3_create_collation'] = wasmExports['sqlite3_create_collation']; + _sqlite3_create_collation_v2 = Module['_sqlite3_create_collation_v2'] = wasmExports['sqlite3_create_collation_v2']; + _sqlite3_collation_needed = Module['_sqlite3_collation_needed'] = wasmExports['sqlite3_collation_needed']; + _sqlite3_get_autocommit = Module['_sqlite3_get_autocommit'] = wasmExports['sqlite3_get_autocommit']; + _sqlite3_table_column_metadata = Module['_sqlite3_table_column_metadata'] = wasmExports['sqlite3_table_column_metadata']; + _sqlite3_extended_result_codes = Module['_sqlite3_extended_result_codes'] = wasmExports['sqlite3_extended_result_codes']; + _sqlite3_uri_key = Module['_sqlite3_uri_key'] = wasmExports['sqlite3_uri_key']; + _sqlite3_uri_int64 = Module['_sqlite3_uri_int64'] = wasmExports['sqlite3_uri_int64']; + _sqlite3_db_name = Module['_sqlite3_db_name'] = wasmExports['sqlite3_db_name']; + _sqlite3_db_filename = Module['_sqlite3_db_filename'] = wasmExports['sqlite3_db_filename']; + _sqlite3_db_readonly = Module['_sqlite3_db_readonly'] = wasmExports['sqlite3_db_readonly']; + _sqlite3_compileoption_used = Module['_sqlite3_compileoption_used'] = wasmExports['sqlite3_compileoption_used']; + _sqlite3_compileoption_get = Module['_sqlite3_compileoption_get'] = wasmExports['sqlite3_compileoption_get']; + _sqlite3session_diff = Module['_sqlite3session_diff'] = wasmExports['sqlite3session_diff']; + _sqlite3session_attach = Module['_sqlite3session_attach'] = wasmExports['sqlite3session_attach']; + _sqlite3session_create = Module['_sqlite3session_create'] = wasmExports['sqlite3session_create']; + _sqlite3session_delete = Module['_sqlite3session_delete'] = wasmExports['sqlite3session_delete']; + _sqlite3session_table_filter = Module['_sqlite3session_table_filter'] = wasmExports['sqlite3session_table_filter']; + _sqlite3session_changeset = Module['_sqlite3session_changeset'] = wasmExports['sqlite3session_changeset']; + _sqlite3session_changeset_strm = Module['_sqlite3session_changeset_strm'] = wasmExports['sqlite3session_changeset_strm']; + _sqlite3session_patchset_strm = Module['_sqlite3session_patchset_strm'] = wasmExports['sqlite3session_patchset_strm']; + _sqlite3session_patchset = Module['_sqlite3session_patchset'] = wasmExports['sqlite3session_patchset']; + _sqlite3session_enable = Module['_sqlite3session_enable'] = wasmExports['sqlite3session_enable']; + _sqlite3session_indirect = Module['_sqlite3session_indirect'] = wasmExports['sqlite3session_indirect']; + _sqlite3session_isempty = Module['_sqlite3session_isempty'] = wasmExports['sqlite3session_isempty']; + _sqlite3session_memory_used = Module['_sqlite3session_memory_used'] = wasmExports['sqlite3session_memory_used']; + _sqlite3session_object_config = Module['_sqlite3session_object_config'] = wasmExports['sqlite3session_object_config']; + _sqlite3session_changeset_size = Module['_sqlite3session_changeset_size'] = wasmExports['sqlite3session_changeset_size']; + _sqlite3changeset_start = Module['_sqlite3changeset_start'] = wasmExports['sqlite3changeset_start']; + _sqlite3changeset_start_v2 = Module['_sqlite3changeset_start_v2'] = wasmExports['sqlite3changeset_start_v2']; + _sqlite3changeset_start_strm = Module['_sqlite3changeset_start_strm'] = wasmExports['sqlite3changeset_start_strm']; + _sqlite3changeset_start_v2_strm = Module['_sqlite3changeset_start_v2_strm'] = wasmExports['sqlite3changeset_start_v2_strm']; + _sqlite3changeset_next = Module['_sqlite3changeset_next'] = wasmExports['sqlite3changeset_next']; + _sqlite3changeset_op = Module['_sqlite3changeset_op'] = wasmExports['sqlite3changeset_op']; + _sqlite3changeset_pk = Module['_sqlite3changeset_pk'] = wasmExports['sqlite3changeset_pk']; + _sqlite3changeset_old = Module['_sqlite3changeset_old'] = wasmExports['sqlite3changeset_old']; + _sqlite3changeset_new = Module['_sqlite3changeset_new'] = wasmExports['sqlite3changeset_new']; + _sqlite3changeset_conflict = Module['_sqlite3changeset_conflict'] = wasmExports['sqlite3changeset_conflict']; + _sqlite3changeset_fk_conflicts = Module['_sqlite3changeset_fk_conflicts'] = wasmExports['sqlite3changeset_fk_conflicts']; + _sqlite3changeset_finalize = Module['_sqlite3changeset_finalize'] = wasmExports['sqlite3changeset_finalize']; + _sqlite3changeset_invert = Module['_sqlite3changeset_invert'] = wasmExports['sqlite3changeset_invert']; + _sqlite3changeset_invert_strm = Module['_sqlite3changeset_invert_strm'] = wasmExports['sqlite3changeset_invert_strm']; + _sqlite3changeset_apply_v2 = Module['_sqlite3changeset_apply_v2'] = wasmExports['sqlite3changeset_apply_v2']; + _sqlite3changeset_apply_v3 = Module['_sqlite3changeset_apply_v3'] = wasmExports['sqlite3changeset_apply_v3']; + _sqlite3changeset_apply = Module['_sqlite3changeset_apply'] = wasmExports['sqlite3changeset_apply']; + _sqlite3changeset_apply_v3_strm = Module['_sqlite3changeset_apply_v3_strm'] = wasmExports['sqlite3changeset_apply_v3_strm']; + _sqlite3changeset_apply_v2_strm = Module['_sqlite3changeset_apply_v2_strm'] = wasmExports['sqlite3changeset_apply_v2_strm']; + _sqlite3changeset_apply_strm = Module['_sqlite3changeset_apply_strm'] = wasmExports['sqlite3changeset_apply_strm']; + _sqlite3changegroup_new = Module['_sqlite3changegroup_new'] = wasmExports['sqlite3changegroup_new']; + _sqlite3changegroup_add = Module['_sqlite3changegroup_add'] = wasmExports['sqlite3changegroup_add']; + _sqlite3changegroup_output = Module['_sqlite3changegroup_output'] = wasmExports['sqlite3changegroup_output']; + _sqlite3changegroup_add_strm = Module['_sqlite3changegroup_add_strm'] = wasmExports['sqlite3changegroup_add_strm']; + _sqlite3changegroup_output_strm = Module['_sqlite3changegroup_output_strm'] = wasmExports['sqlite3changegroup_output_strm']; + _sqlite3changegroup_delete = Module['_sqlite3changegroup_delete'] = wasmExports['sqlite3changegroup_delete']; + _sqlite3changeset_concat = Module['_sqlite3changeset_concat'] = wasmExports['sqlite3changeset_concat']; + _sqlite3changeset_concat_strm = Module['_sqlite3changeset_concat_strm'] = wasmExports['sqlite3changeset_concat_strm']; + _sqlite3session_config = Module['_sqlite3session_config'] = wasmExports['sqlite3session_config']; + _sqlite3_sourceid = Module['_sqlite3_sourceid'] = wasmExports['sqlite3_sourceid']; + _sqlite3mc_version = Module['_sqlite3mc_version'] = wasmExports['sqlite3mc_version']; + _sqlite3mc_config = Module['_sqlite3mc_config'] = wasmExports['sqlite3mc_config']; + _sqlite3mc_cipher_count = Module['_sqlite3mc_cipher_count'] = wasmExports['sqlite3mc_cipher_count']; + _sqlite3mc_cipher_index = Module['_sqlite3mc_cipher_index'] = wasmExports['sqlite3mc_cipher_index']; + _sqlite3mc_cipher_name = Module['_sqlite3mc_cipher_name'] = wasmExports['sqlite3mc_cipher_name']; + _sqlite3mc_cipher_name_copy = Module['_sqlite3mc_cipher_name_copy'] = wasmExports['sqlite3mc_cipher_name_copy']; + _sqlite3mc_config_cipher = Module['_sqlite3mc_config_cipher'] = wasmExports['sqlite3mc_config_cipher']; + _sqlite3mc_codec_data = Module['_sqlite3mc_codec_data'] = wasmExports['sqlite3mc_codec_data']; + _sqlite3_activate_see = Module['_sqlite3_activate_see'] = wasmExports['sqlite3_activate_see']; + _sqlite3_key = Module['_sqlite3_key'] = wasmExports['sqlite3_key']; + _sqlite3_key_v2 = Module['_sqlite3_key_v2'] = wasmExports['sqlite3_key_v2']; + _sqlite3_rekey_v2 = Module['_sqlite3_rekey_v2'] = wasmExports['sqlite3_rekey_v2']; + _sqlite3_rekey = Module['_sqlite3_rekey'] = wasmExports['sqlite3_rekey']; + _sqlite3mc_vfs_destroy = Module['_sqlite3mc_vfs_destroy'] = wasmExports['sqlite3mc_vfs_destroy']; + _sqlite3__wasm_pstack_ptr = Module['_sqlite3__wasm_pstack_ptr'] = wasmExports['sqlite3__wasm_pstack_ptr']; + _sqlite3__wasm_pstack_restore = Module['_sqlite3__wasm_pstack_restore'] = wasmExports['sqlite3__wasm_pstack_restore']; + _sqlite3__wasm_pstack_alloc = Module['_sqlite3__wasm_pstack_alloc'] = wasmExports['sqlite3__wasm_pstack_alloc']; + _sqlite3__wasm_pstack_remaining = Module['_sqlite3__wasm_pstack_remaining'] = wasmExports['sqlite3__wasm_pstack_remaining']; + _sqlite3__wasm_pstack_quota = Module['_sqlite3__wasm_pstack_quota'] = wasmExports['sqlite3__wasm_pstack_quota']; + _sqlite3__wasm_test_struct = Module['_sqlite3__wasm_test_struct'] = wasmExports['sqlite3__wasm_test_struct']; + _sqlite3__wasm_enum_json = Module['_sqlite3__wasm_enum_json'] = wasmExports['sqlite3__wasm_enum_json']; + _sqlite3__wasm_vfs_unlink = Module['_sqlite3__wasm_vfs_unlink'] = wasmExports['sqlite3__wasm_vfs_unlink']; + _sqlite3__wasm_db_vfs = Module['_sqlite3__wasm_db_vfs'] = wasmExports['sqlite3__wasm_db_vfs']; + _sqlite3__wasm_db_reset = Module['_sqlite3__wasm_db_reset'] = wasmExports['sqlite3__wasm_db_reset']; + _sqlite3__wasm_db_export_chunked = Module['_sqlite3__wasm_db_export_chunked'] = wasmExports['sqlite3__wasm_db_export_chunked']; + _sqlite3__wasm_db_serialize = Module['_sqlite3__wasm_db_serialize'] = wasmExports['sqlite3__wasm_db_serialize']; + _sqlite3__wasm_vfs_create_file = Module['_sqlite3__wasm_vfs_create_file'] = wasmExports['sqlite3__wasm_vfs_create_file']; + _sqlite3__wasm_posix_create_file = Module['_sqlite3__wasm_posix_create_file'] = wasmExports['sqlite3__wasm_posix_create_file']; + _sqlite3__wasm_kvvfsMakeKey = Module['_sqlite3__wasm_kvvfsMakeKey'] = wasmExports['sqlite3__wasm_kvvfsMakeKey']; + _sqlite3__wasm_kvvfs_methods = Module['_sqlite3__wasm_kvvfs_methods'] = wasmExports['sqlite3__wasm_kvvfs_methods']; + _sqlite3__wasm_vtab_config = Module['_sqlite3__wasm_vtab_config'] = wasmExports['sqlite3__wasm_vtab_config']; + _sqlite3__wasm_db_config_ip = Module['_sqlite3__wasm_db_config_ip'] = wasmExports['sqlite3__wasm_db_config_ip']; + _sqlite3__wasm_db_config_pii = Module['_sqlite3__wasm_db_config_pii'] = wasmExports['sqlite3__wasm_db_config_pii']; + _sqlite3__wasm_db_config_s = Module['_sqlite3__wasm_db_config_s'] = wasmExports['sqlite3__wasm_db_config_s']; + _sqlite3__wasm_config_i = Module['_sqlite3__wasm_config_i'] = wasmExports['sqlite3__wasm_config_i']; + _sqlite3__wasm_config_ii = Module['_sqlite3__wasm_config_ii'] = wasmExports['sqlite3__wasm_config_ii']; + _sqlite3__wasm_config_j = Module['_sqlite3__wasm_config_j'] = wasmExports['sqlite3__wasm_config_j']; + _sqlite3__wasm_qfmt_token = Module['_sqlite3__wasm_qfmt_token'] = wasmExports['sqlite3__wasm_qfmt_token']; + _sqlite3__wasm_kvvfs_decode = Module['_sqlite3__wasm_kvvfs_decode'] = wasmExports['sqlite3__wasm_kvvfs_decode']; + _sqlite3__wasm_kvvfs_encode = Module['_sqlite3__wasm_kvvfs_encode'] = wasmExports['sqlite3__wasm_kvvfs_encode']; + _sqlite3__wasm_init_wasmfs = Module['_sqlite3__wasm_init_wasmfs'] = wasmExports['sqlite3__wasm_init_wasmfs']; + _sqlite3__wasm_test_intptr = Module['_sqlite3__wasm_test_intptr'] = wasmExports['sqlite3__wasm_test_intptr']; + _sqlite3__wasm_test_voidptr = Module['_sqlite3__wasm_test_voidptr'] = wasmExports['sqlite3__wasm_test_voidptr']; + _sqlite3__wasm_test_int64_max = Module['_sqlite3__wasm_test_int64_max'] = wasmExports['sqlite3__wasm_test_int64_max']; + _sqlite3__wasm_test_int64_min = Module['_sqlite3__wasm_test_int64_min'] = wasmExports['sqlite3__wasm_test_int64_min']; + _sqlite3__wasm_test_int64_times2 = Module['_sqlite3__wasm_test_int64_times2'] = wasmExports['sqlite3__wasm_test_int64_times2']; + _sqlite3__wasm_test_int64_minmax = Module['_sqlite3__wasm_test_int64_minmax'] = wasmExports['sqlite3__wasm_test_int64_minmax']; + _sqlite3__wasm_test_int64ptr = Module['_sqlite3__wasm_test_int64ptr'] = wasmExports['sqlite3__wasm_test_int64ptr']; + _sqlite3__wasm_test_stack_overflow = Module['_sqlite3__wasm_test_stack_overflow'] = wasmExports['sqlite3__wasm_test_stack_overflow']; + _sqlite3__wasm_test_str_hello = Module['_sqlite3__wasm_test_str_hello'] = wasmExports['sqlite3__wasm_test_str_hello']; + _sqlite3__wasm_SQLTester_strglob = Module['_sqlite3__wasm_SQLTester_strglob'] = wasmExports['sqlite3__wasm_SQLTester_strglob']; + _malloc = Module['_malloc'] = wasmExports['malloc']; + _free = Module['_free'] = wasmExports['free']; + _realloc = Module['_realloc'] = wasmExports['realloc']; + _emscripten_builtin_memalign = wasmExports['emscripten_builtin_memalign']; + __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; + __emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; + _emscripten_stack_get_current = wasmExports['emscripten_stack_get_current']; + __indirect_function_table = wasmExports['__indirect_function_table']; +} + +var wasmImports = { + + __syscall_chmod: ___syscall_chmod, + + __syscall_faccessat: ___syscall_faccessat, + + __syscall_fchmod: ___syscall_fchmod, + + __syscall_fchown32: ___syscall_fchown32, + + __syscall_fcntl64: ___syscall_fcntl64, + + __syscall_fstat64: ___syscall_fstat64, + + __syscall_ftruncate64: ___syscall_ftruncate64, + + __syscall_getcwd: ___syscall_getcwd, + + __syscall_ioctl: ___syscall_ioctl, + + __syscall_lstat64: ___syscall_lstat64, + + __syscall_mkdirat: ___syscall_mkdirat, + + __syscall_newfstatat: ___syscall_newfstatat, + + __syscall_openat: ___syscall_openat, + + __syscall_readlinkat: ___syscall_readlinkat, + + __syscall_rmdir: ___syscall_rmdir, + + __syscall_stat64: ___syscall_stat64, + + __syscall_unlinkat: ___syscall_unlinkat, + + __syscall_utimensat: ___syscall_utimensat, + + _abort_js: __abort_js, + + _localtime_js: __localtime_js, + + _mmap_js: __mmap_js, + + _munmap_js: __munmap_js, + + _tzset_js: __tzset_js, + + clock_time_get: _clock_time_get, + + emscripten_date_now: _emscripten_date_now, + + emscripten_get_heap_max: _emscripten_get_heap_max, + + emscripten_get_now: _emscripten_get_now, + + emscripten_resize_heap: _emscripten_resize_heap, + + environ_get: _environ_get, + + environ_sizes_get: _environ_sizes_get, + + exit: _exit, + + fd_close: _fd_close, + + fd_fdstat_get: _fd_fdstat_get, + + fd_read: _fd_read, + + fd_seek: _fd_seek, + + fd_sync: _fd_sync, + + fd_write: _fd_write, + + memory: wasmMemory, + + wasm_crypto_getrandom +}; + + + + + +async function run() { + + preRun(); + + if (runDependencies) { + await resolveRunDependencies(); + } + + var setStatus = Module['setStatus']; + if (setStatus) { + setStatus('Running...'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + setTimeout(setStatus, 1, ''); + } + + if (ABORT) return; + + initRuntime(); + + Module['onRuntimeInitialized']?.(); + + postRun(); +} + +var wasmExports; + + + +wasmExports = await createWasm(); +await run(); + + + + + +Module.runSQLite3PostLoadInit = async function( + sqlite3InitScriptInfo, + EmscriptenModule, + sqlite3IsUnderTest +){ + + 'use strict'; + delete EmscriptenModule.runSQLite3PostLoadInit; + + + + + + + +'use strict'; +globalThis.sqlite3ApiBootstrap = async function sqlite3ApiBootstrap( + apiConfig = (globalThis.sqlite3ApiConfig || sqlite3ApiBootstrap.defaultConfig) +){ + if(sqlite3ApiBootstrap.sqlite3){ + (sqlite3ApiBootstrap.sqlite3.config || console).warn( + "sqlite3ApiBootstrap() called multiple times.", + "Config and external initializers are ignored on calls after the first." + ); + return sqlite3ApiBootstrap.sqlite3; + } + const nu = (...obj)=>Object.assign(Object.create(null),...obj); + const config = nu({ + exports: undefined, + memory: undefined, + bigIntEnabled: !!globalThis.BigInt64Array, + debug: console.debug.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), + log: console.log.bind(console), + wasmfsOpfsDir: '/opfs', + + useStdAlloc: false + }, apiConfig); + + Object.assign(config, { + allocExportName: config.useStdAlloc ? 'malloc' : 'sqlite3_malloc', + deallocExportName: config.useStdAlloc ? 'free' : 'sqlite3_free', + reallocExportName: config.useStdAlloc ? 'realloc' : 'sqlite3_realloc' + }); + + [ + + + 'exports', 'memory', 'functionTable', 'wasmfsOpfsDir' + ].forEach((k)=>{ + if('function' === typeof config[k]){ + config[k] = config[k](); + } + }); + + + const capi = nu(); + + const wasm = nu(); + + + const __rcStr = (rc)=>{ + return (capi.sqlite3_js_rc_str && capi.sqlite3_js_rc_str(rc)) + || ("Unknown result code #"+rc); + }; + + + const isInt32 = (n)=> + 'number'===typeof n + && n===(n | 0) + && n<=2147483647 && n>=-2147483648; + + + class SQLite3Error extends Error { + + constructor(...args){ + let rc; + if(args.length){ + if(isInt32(args[0])){ + rc = args[0]; + if(1===args.length){ + super(__rcStr(args[0])); + }else{ + const rcStr = __rcStr(rc); + if('object'===typeof args[1]){ + super(rcStr,args[1]); + }else{ + args[0] = rcStr+':'; + super(args.join(' ')); + } + } + }else{ + if(2===args.length && 'object'===typeof args[1]){ + super(...args); + }else{ + super(args.join(' ')); + } + } + } + this.resultCode = rc || capi.SQLITE_ERROR; + this.name = 'SQLite3Error'; + } + }; + + + 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'."); + } + + + const bigIntFits64 = function f(b){ + if(!f._max){ + f._max = BigInt("0x7fffffffffffffff"); + f._min = ~f._max; + } + return b >= f._min && b <= f._max; + }; + + + const bigIntFits32 = (b)=>(b >= (-0x7fffffffn - 1n) && b <= 0x7fffffffn); + + + 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; + }; + + + const isTypedArray = (v)=>{ + return (v && v.constructor && isInt32(v.constructor.BYTES_PER_ELEMENT)) ? v : false; + }; + + + const isBindableTypedArray = (v)=> + v && (v instanceof Uint8Array + || v instanceof Int8Array + || v instanceof ArrayBuffer); + + + const isSQLableTypedArray = (v)=> + v && (v instanceof Uint8Array + || v instanceof Int8Array + || v instanceof ArrayBuffer); + + + const affirmBindableTypedArray = (v)=> + isBindableTypedArray(v) + || toss3("Value is not of a supported TypedArray type."); + + + const flexibleString = function(v){ + if(isSQLableTypedArray(v)){ + return wasm.typedArrayToString( + (v instanceof ArrayBuffer) ? new Uint8Array(v) : v, + 0, v.length + ); + } + else if(Array.isArray(v)) return v.join(""); + else if(wasm.isPtr(v)) v = wasm.cstrToJs(v); + return v; + }; + + + class WasmAllocError extends Error { + + constructor(...args){ + if(2===args.length && 'object'===typeof args[1]){ + super(...args); + }else if(args.length){ + super(args.join(' ')); + }else{ + super("Allocation failed."); + } + this.resultCode = capi.SQLITE_NOMEM; + this.name = 'WasmAllocError'; + } + }; + + WasmAllocError.toss = (...args)=>{ + throw new WasmAllocError(...args); + }; + + Object.assign(capi, { + + sqlite3_bind_blob: undefined, + + + sqlite3_bind_text: undefined, + + + sqlite3_create_function_v2: ( + pDb, funcName, nArg, eTextRep, pApp, + xFunc, xStep, xFinal, xDestroy + )=>{}, + + sqlite3_create_function: ( + pDb, funcName, nArg, eTextRep, pApp, + xFunc, xStep, xFinal + )=>{}, + + sqlite3_create_window_function: ( + pDb, funcName, nArg, eTextRep, pApp, + xStep, xFinal, xValue, xInverse, xDestroy + )=>{}, + + sqlite3_prepare_v3: (dbPtr, sql, sqlByteLen, prepFlags, + stmtPtrPtr, strPtrPtr)=>{}, + + + sqlite3_prepare_v2: (dbPtr, sql, sqlByteLen, + stmtPtrPtr,strPtrPtr)=>{}, + + + sqlite3_exec: (pDb, sql, callback, pVoid, pErrMsg)=>{}, + + + sqlite3_randomness: (n, outPtr)=>{}, + }); + + + const util = { + affirmBindableTypedArray, flexibleString, + bigIntFits32, bigIntFits64, bigIntFitsDouble, + isBindableTypedArray, + isInt32, isSQLableTypedArray, isTypedArray, + isUIThread: ()=>(globalThis.window===globalThis && !!globalThis.document), + + toss: function(...args){throw new Error(args.join(' '))}, + toss3, + typedArrayPart: wasm.typedArrayPart, + nu, + assert: function(arg,msg){ + if( !arg ){ + util.toss("Assertion failed:",msg); + } + }, + + affirmDbHeader: function(bytes){ + if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); + const header = "SQLite format 3"; + if( header.length > bytes.byteLength ){ + toss3("Input does not contain an SQLite3 database header."); + } + for(let i = 0; i < header.length; ++i){ + if( header.charCodeAt(i) !== bytes[i] ){ + break ; + } + } + }, + + affirmIsDb: function(bytes){ + if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); + const n = bytes.byteLength; + if(n<512 || n%512!==0) { + toss3("Byte array size",n,"is invalid for an SQLite3 db."); + } + util.affirmDbHeader(bytes); + } + }; + + + Object.assign(wasm, { + + + exports: config.exports + || toss3("Missing API config.exports (WASM module exports)."), + + + 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)."), + + + pointerSize: ('number'===typeof config.exports.sqlite3_libversion()) ? 4 : 8, + + + bigIntEnabled: !!config.bigIntEnabled, + + + functionTable: config.functionTable, + + + alloc: undefined, + + + realloc: undefined, + + + dealloc: undefined + + + }); + + + wasm.allocFromTypedArray = function(srcTypedArray){ + if(srcTypedArray instanceof ArrayBuffer){ + srcTypedArray = new Uint8Array(srcTypedArray); + } + affirmBindableTypedArray(srcTypedArray); + const pRet = wasm.alloc(srcTypedArray.byteLength || 1); + wasm.heapForSize(srcTypedArray.constructor) + .set(srcTypedArray.byteLength ? srcTypedArray : [0], Number(pRet)) + ; + return pRet; + }; + + { + + const keyAlloc = config.allocExportName, + keyDealloc = config.deallocExportName, + keyRealloc = config.reallocExportName; + for(const key of [keyAlloc, keyDealloc, keyRealloc]){ + const f = wasm.exports[key]; + if(!(f instanceof Function)) toss3("Missing required exports[",key,"] function."); + } + + wasm.alloc = function f(n){ + return f.impl(n) || WasmAllocError.toss("Failed to allocate",n," bytes."); + }; + wasm.alloc.impl = wasm.exports[keyAlloc]; + wasm.realloc = function f(m,n){ + const m2 = f.impl(wasm.ptr.coerce(m),n); + return n ? (m2 || WasmAllocError.toss("Failed to reallocate",n," bytes.")) : wasm.ptr.null; + }; + wasm.realloc.impl = wasm.exports[keyRealloc]; + wasm.dealloc = function f(m){ + f.impl(wasm.ptr.coerce(m)) + ; + }; + wasm.dealloc.impl = wasm.exports[keyDealloc]; + } + + + 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 = nu(), 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 = nu(); + 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; + }; + + + wasm.pstack = nu({ + + restore: wasm.exports.sqlite3__wasm_pstack_restore, + + + alloc: function(n){ + if('string'===typeof n && !(n = wasm.sizeofIR(n))){ + WasmAllocError.toss("Invalid value for pstack.alloc(",arguments[0],")"); + } + return wasm.exports.sqlite3__wasm_pstack_alloc(n) + || WasmAllocError.toss("Could not allocate",n, + "bytes from the pstack."); + }, + + + allocChunks: function(n,sz){ + if('string'===typeof sz && !(sz = wasm.sizeofIR(sz))){ + WasmAllocError.toss("Invalid size value for allocChunks(",arguments[1],")"); + } + const mem = wasm.pstack.alloc(n * sz); + const rc = [mem]; + let i = 1, offset = sz; + for(; i < n; ++i, offset += sz) rc.push(wasm.ptr.add(mem, offset)); + return rc; + }, + + + allocPtr: (n=1,safePtrSize=true)=>{ + return 1===n + ? wasm.pstack.alloc(safePtrSize ? 8 : wasm.ptr.size) + : wasm.pstack.allocChunks(n, safePtrSize ? 8 : wasm.ptr.size); + }, + + + call: function(f){ + const stackPos = wasm.pstack.pointer; + try{ return f(sqlite3) } + finally{ wasm.pstack.restore(stackPos); } + } + + }); + + Object.defineProperties(wasm.pstack, { + + pointer: { + configurable: false, iterable: true, writeable: false, + get: wasm.exports.sqlite3__wasm_pstack_ptr + + + + }, + + + quota: { + configurable: false, iterable: true, writeable: false, + get: wasm.exports.sqlite3__wasm_pstack_quota + }, + + + remaining: { + configurable: false, iterable: true, writeable: false, + get: wasm.exports.sqlite3__wasm_pstack_remaining + } + }); + + + 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,wasm.ptr.null); + 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(wasm.typedArrayPart(heap, ptr, wasm.ptr.add(ptr,j)), offset); + n -= j; + offset += j; + } while(n > 0); + }catch(e){ + config.error("Highly unexpected (and ignored!) "+ + "exception in sqlite3_randomness():",e); + }finally{ + wasm.pstack.restore(stack); + } + return ta; + } + wasm.exports.sqlite3_randomness(...args); + }; + + + capi.sqlite3_wasmfs_opfs_dir = function(){ + if(undefined !== this.dir) return this.dir; + + const pdir = config.wasmfsOpfsDir; + if(!pdir + || !globalThis.FileSystemHandle + || !globalThis.FileSystemDirectoryHandle + || !globalThis.FileSystemFileHandle + || !wasm.exports.sqlite3__wasm_init_wasmfs){ + return this.dir = ""; + } + try{ + if(pdir && 0===wasm.xCallWrapped( + 'sqlite3__wasm_init_wasmfs', 'i32', ['string'], pdir + )){ + return this.dir = pdir; + }else{ + return this.dir = ""; + } + }catch(e){ + + return this.dir = ""; + } + }.bind(nu()); + + + capi.sqlite3_wasmfs_filename_is_persistent = function(name){ + const p = capi.sqlite3_wasmfs_opfs_dir(); + return (p && name) ? name.startsWith(p+'/') : false; + }; + + + 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){ + + return false; + } + }; + + + capi.sqlite3_js_vfs_list = function(){ + const rc = []; + let pVfs = capi.sqlite3_vfs_find(wasm.ptr.null); + while(pVfs){ + const oVfs = new capi.sqlite3_vfs(pVfs); + rc.push(wasm.cstrToJs(oVfs.$zName)); + pVfs = oVfs.$pNext; + oVfs.dispose(); + } + return rc; + }; + + + capi.sqlite3_js_db_export = function(pDb, schema=0){ + pDb = wasm.xWrap.testConvertArg('sqlite3*', pDb); + if(!pDb) toss3('Invalid sqlite3* argument.'); + if(!wasm.bigIntEnabled) toss3('BigInt support is not enabled.'); + const scope = wasm.scopedAllocPush(); + let pOut; + try{ + const pSize = wasm.scopedAlloc(8 + wasm.ptr.size); + const ppOut = wasm.ptr.add(pSize, 8); + + const zSchema = schema + ? (wasm.isPtr(schema) ? schema : wasm.scopedAllocCString(''+schema)) + : wasm.ptr.null; + let rc = wasm.exports.sqlite3__wasm_db_serialize( + pDb, zSchema, ppOut, pSize, 0 + ); + if(rc){ + toss3("Database serialization failed with code", + sqlite3.capi.sqlite3_js_rc_str(rc)); + } + pOut = wasm.peekPtr(ppOut); + const nOut = wasm.peek(pSize, 'i64'); + rc = nOut + ? wasm.heap8u().slice(Number(pOut), Number(pOut) + Number(nOut)) + : new Uint8Array(); + return rc; + }finally{ + if(pOut) wasm.exports.sqlite3_free(pOut); + wasm.scopedAllocPop(scope); + } + }; + + + capi.sqlite3_js_db_vfs = + (dbPointer, dbName=wasm.ptr.null)=>util.sqlite3__wasm_db_vfs(dbPointer, dbName); + + + 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); + }; + + + capi.sqlite3_js_posix_create_file = function(filename, data, dataLen){ + let pData; + if(data && wasm.isPtr(data)){ + pData = data; + }else if(data instanceof ArrayBuffer || data instanceof Uint8Array){ + pData = wasm.allocFromTypedArray(data); + if(arguments.length<3 || !util.isInt32(dataLen) || dataLen<0){ + dataLen = data.byteLength; + } + }else{ + SQLite3Error.toss("Invalid 2nd argument for sqlite3_js_posix_create_file()."); + } + try{ + if(!util.isInt32(dataLen) || dataLen<0){ + SQLite3Error.toss("Invalid 3rd argument for sqlite3_js_posix_create_file()."); + } + const rc = util.sqlite3__wasm_posix_create_file(filename, pData, dataLen); + if(rc) SQLite3Error.toss("Creation of file failed with sqlite3 result code", + capi.sqlite3_js_rc_str(rc)); + }finally{ + if( pData && pData!==data ) wasm.dealloc(pData); + } + }; + + + capi.sqlite3_js_vfs_create_file = function(vfs, filename, data, dataLen){ + config.warn("sqlite3_js_vfs_create_file() is deprecated and", + "should be avoided because it can lead to C-level crashes.", + "See its documentation for alternatives."); + let pData; + if(data){ + if( wasm.isPtr(data) ){ + pData = data; + }else{ + if( data instanceof ArrayBuffer ){ + data = new Uint8Array(data); + } + if( data instanceof Uint8Array ){ + pData = wasm.allocFromTypedArray(data); + if(arguments.length<4 || !util.isInt32(dataLen) || dataLen<0){ + dataLen = data.byteLength; + } + }else{ + SQLite3Error.toss("Invalid 3rd argument type for sqlite3_js_vfs_create_file()."); + } + } + }else{ + pData = 0; + } + if(!util.isInt32(dataLen) || dataLen<0){ + if( pData && pData!==data ) wasm.dealloc(pData); + SQLite3Error.toss("Invalid 4th argument for sqlite3_js_vfs_create_file()."); + } + try{ + const rc = util.sqlite3__wasm_vfs_create_file(vfs, filename, pData, dataLen); + if(rc) SQLite3Error.toss("Creation of file failed with sqlite3 result code", + capi.sqlite3_js_rc_str(rc)); + }finally{ + if( pData && pData!==data ) wasm.dealloc(pData); + } + }; + + + capi.sqlite3_js_sql_to_string = (sql)=>{ + if('string' === typeof sql){ + return sql; + } + const x = flexibleString(v); + return x===v ? undefined : x; + } + + + capi.sqlite3_db_config = function(pDb, op, ...args){ + switch(op){ + case capi.SQLITE_DBCONFIG_ENABLE_FKEY: + case capi.SQLITE_DBCONFIG_ENABLE_TRIGGER: + case capi.SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER: + case capi.SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION: + case capi.SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE: + case capi.SQLITE_DBCONFIG_ENABLE_QPSG: + case capi.SQLITE_DBCONFIG_TRIGGER_EQP: + case capi.SQLITE_DBCONFIG_RESET_DATABASE: + case capi.SQLITE_DBCONFIG_DEFENSIVE: + case capi.SQLITE_DBCONFIG_WRITABLE_SCHEMA: + case capi.SQLITE_DBCONFIG_LEGACY_ALTER_TABLE: + case capi.SQLITE_DBCONFIG_DQS_DML: + case capi.SQLITE_DBCONFIG_DQS_DDL: + case capi.SQLITE_DBCONFIG_ENABLE_VIEW: + case capi.SQLITE_DBCONFIG_LEGACY_FILE_FORMAT: + case capi.SQLITE_DBCONFIG_TRUSTED_SCHEMA: + case capi.SQLITE_DBCONFIG_STMT_SCANSTATUS: + case capi.SQLITE_DBCONFIG_REVERSE_SCANORDER: + case capi.SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE: + case capi.SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE: + case capi.SQLITE_DBCONFIG_ENABLE_COMMENTS: + case capi.SQLITE_DBCONFIG_FP_DIGITS: + if( !this.ip ){ + this.ip = wasm.xWrap('sqlite3__wasm_db_config_ip','int', + ['sqlite3*', 'int', 'int', '*']); + } + return this.ip(pDb, op, args[0], args[1] || 0); + case capi.SQLITE_DBCONFIG_LOOKASIDE: + if( !this.pii ){ + this.pii = wasm.xWrap('sqlite3__wasm_db_config_pii', 'int', + ['sqlite3*', 'int', '*', 'int', 'int']); + } + return this.pii(pDb, op, args[0], args[1], args[2]); + case capi.SQLITE_DBCONFIG_MAINDBNAME: + if(!this.s){ + this.s = wasm.xWrap('sqlite3__wasm_db_config_s','int', + ['sqlite3*', 'int', 'string:static'] + ); + } + return this.s(pDb, op, args[0]); + default: + return capi.SQLITE_MISUSE; + } + }.bind(nu()); + + + capi.sqlite3_value_to_js = function(pVal,throwIfCannotConvert=true){ + let arg; + const 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); + 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(Number(pBlob), Number(pBlob) + Number(n)) + : null; + break; + } + case capi.SQLITE_NULL: + arg = null; break; + default: + if(throwIfCannotConvert){ + toss3(capi.SQLITE_MISMATCH, + "Unhandled sqlite3_value_type():",valType); + } + arg = undefined; + } + return arg; + }; + + + capi.sqlite3_values_to_js = function(argc,pArgv,throwIfCannotConvert=true){ + let i; + const tgt = []; + for(i = 0; i < argc; ++i){ + + tgt.push(capi.sqlite3_value_to_js( + wasm.peekPtr(wasm.ptr.add(pArgv, wasm.ptr.size * i)), + throwIfCannotConvert + )); + } + return tgt; + }; + + + capi.sqlite3_result_error_js = function(pCtx,e){ + if(e instanceof WasmAllocError){ + capi.sqlite3_result_error_nomem(pCtx); + }else{ + ; + capi.sqlite3_result_error(pCtx, ''+e, -1); + } + }; + + + capi.sqlite3_result_js = function(pCtx,val){ + if(val instanceof Error){ + capi.sqlite3_result_error_js(pCtx, val); + return; + } + try{ + switch(typeof val) { + case 'undefined': + + break; + case 'boolean': + capi.sqlite3_result_int(pCtx, val ? 1 : 0); + break; + case 'bigint': + if(util.bigIntFits32(val)){ + capi.sqlite3_result_int(pCtx, Number(val)); + }else if(util.bigIntFitsDouble(val)){ + capi.sqlite3_result_double(pCtx, Number(val)); + }else 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{ + toss3("BigInt value",val.toString(),"is too BigInt."); + } + break; + case 'number': { + let f; + if(util.isInt32(val)){ + f = capi.sqlite3_result_int; + }else if(wasm.bigIntEnabled + && Number.isInteger(val) + && util.bigIntFits64(BigInt(val))){ + f = capi.sqlite3_result_int64; + }else{ + f = capi.sqlite3_result_double; + } + f(pCtx, val); + break; + } + case 'string': { + const [p, n] = wasm.allocCString(val,true); + capi.sqlite3_result_text(pCtx, p, n, capi.SQLITE_WASM_DEALLOC); + break; + } + case 'object': + if(null===val) { + capi.sqlite3_result_null(pCtx); + break; + }else if(util.isBindableTypedArray(val)){ + const pBlob = wasm.allocFromTypedArray(val); + capi.sqlite3_result_blob( + pCtx, pBlob, val.byteLength, + capi.SQLITE_WASM_DEALLOC + ); + break; + } + + default: + toss3("Don't not how to handle this UDF result value:",(typeof val), val); + } + }catch(e){ + capi.sqlite3_result_error_js(pCtx, e); + } + }; + + + capi.sqlite3_column_js = function(pStmt, iCol, throwIfCannotConvert=true){ + const v = capi.sqlite3_column_value(pStmt, iCol); + return (0===v) ? undefined : capi.sqlite3_value_to_js(v, throwIfCannotConvert); + }; + + if( true ){ + + const __newOldValue = function(pObj, iCol, impl){ + impl = capi[impl]; + if(!this.ptr) this.ptr = wasm.allocPtr(); + else wasm.pokePtr(this.ptr, 0); + const rc = impl(pObj, iCol, this.ptr); + if(rc) return SQLite3Error.toss(rc,arguments[2]+"() failed with code "+rc); + const pv = wasm.peekPtr(this.ptr); + return pv ? capi.sqlite3_value_to_js( pv, true ) : undefined; + }.bind(nu()); + + + capi.sqlite3_preupdate_new_js = + (pDb, iCol)=>__newOldValue(pDb, iCol, 'sqlite3_preupdate_new'); + + + capi.sqlite3_preupdate_old_js = + (pDb, iCol)=>__newOldValue(pDb, iCol, 'sqlite3_preupdate_old'); + + + capi.sqlite3changeset_new_js = + (pChangesetIter, iCol) => __newOldValue(pChangesetIter, iCol, + 'sqlite3changeset_new'); + + + capi.sqlite3changeset_old_js = + (pChangesetIter, iCol)=>__newOldValue(pChangesetIter, iCol, + 'sqlite3changeset_old'); + } + + + capi.sqlite3_js_retry_busy = function(maxTimes, callback, beforeRetry){ + for(let n = 1; n <= maxTimes; ++n){ + try{ + if( beforeRetry && n>1 ) beforeRetry(n); + const rc = callback(); + if( capi.SQLITE_BUSY===rc ){ + if( n===maxTimes ){ + throw new SQLite3Error(rc, [ + "sqlite3_js_retry_busy() max retry attempts (", + maxTimes, + ") reached." + ].join('')); + } + continue; + } + return rc; + }catch(e){ + if( n{ + if(!sqlite3.__isUnderTest){ + + delete sqlite3.util; + delete sqlite3.StructBinder; + delete sqlite3.opfs; + } + return sqlite3; + }; + const catcher = (e)=>{ + config.error("an async sqlite3 initializer failed:",e); + throw e; + }; + if(!lia || !lia.length){ + return ff.isReady = postInit().catch(catcher); + } + lia = lia.map((f)=>{ + return (f instanceof Function) ? async x=>f(sqlite3) : f; + }); + lia.push(postInit); + let p = Promise.resolve(sqlite3); + while(lia.length) p = p.then(lia.shift()); + return ff.isReady = p.catch(catcher); + }.bind(sqlite3ApiBootstrap), + + scriptInfo: undefined + }; + if( 'undefined'!==typeof sqlite3IsUnderTest ){ + sqlite3.__isUnderTest = !!sqlite3IsUnderTest; + } + try{ + sqlite3ApiBootstrap.initializers.forEach((f)=>{ + f(sqlite3); + }); + }catch(e){ + + console.error("sqlite3 bootstrap initializer threw:",e); + throw e; + } + delete sqlite3ApiBootstrap.initializers; + sqlite3ApiBootstrap.sqlite3 = sqlite3; + if( 'undefined'!==typeof sqlite3InitScriptInfo ){ + sqlite3InitScriptInfo.debugModule( + "sqlite3ApiBootstrap() complete", sqlite3 + ); + sqlite3.scriptInfo + = sqlite3InitScriptInfo; + } + if( sqlite3.__isUnderTest ){ + if( 'undefined'!==typeof EmscriptenModule ){ + sqlite3.config.emscripten = EmscriptenModule; + } + + const iw = sqlite3.scriptInfo?.instantiateWasm; + if( iw ){ + + sqlite3.wasm.module = iw.module; + sqlite3.wasm.instance = iw.instance; + sqlite3.wasm.imports = iw.imports; + } + } + + + delete globalThis.sqlite3ApiConfig; + delete globalThis.sqlite3ApiBootstrap; + delete sqlite3ApiBootstrap.defaultConfig; + return sqlite3.asyncPostInit().then((s)=>{ + if( 'undefined'!==typeof sqlite3InitScriptInfo ){ + sqlite3InitScriptInfo.debugModule( + "sqlite3.asyncPostInit() complete", s + ); + } + delete s.asyncPostInit; + delete s.scriptInfo; + delete s.emscripten; + return s; + }); +}; + + +globalThis.sqlite3ApiBootstrap.initializers = []; + + +globalThis.sqlite3ApiBootstrap.initializersAsync = []; + + +globalThis.sqlite3ApiBootstrap.defaultConfig = Object.create(null); + + +globalThis.sqlite3ApiBootstrap.sqlite3 = undefined; +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + sqlite3.version = {"libVersion": "3.54.0", "libVersionNumber": 3054000, "sourceId": "2026-10-09 15:46:58 be8d059e9a49089ab2dce5ed26dd87aaf598fdc5fbe0b107c0dd758464bcd5e3","downloadVersion": 3540000,"scm":{ "sha3-256": "be8d059e9a49089ab2dce5ed26dd87aaf598fdc5fbe0b107c0dd758464bcd5e3","branch": "trunk","tags": "release major-release version-3.54.0","datetime": "2026-10-09T15:46:58.543Z"}}; +}); + + +'use strict'; +globalThis.WhWasmUtilInstaller = +function WhWasmUtilInstaller(target){ + 'use strict'; + if(undefined===target.bigIntEnabled){ + target.bigIntEnabled = !!globalThis['BigInt64Array']; + } + + + const toss = (...args)=>{throw new Error(args.join(' '))}; + + if( !target.pointerSize && !target.pointerIR + && target.alloc && target.dealloc ){ + + const ptr = target.alloc(1); + target.pointerSize = ('bigint'===typeof ptr ? 8 : 4); + target.dealloc(ptr); + } + + + if( target.pointerSize && !target.pointerIR ){ + target.pointerIR = (4===target.pointerSize ? 'i32' : 'i64'); + } + const __ptrIR = (target.pointerIR ??= 'i32'); + const __ptrSize = (target.pointerSize ??= + ('i32'===__ptrIR ? 4 : ('i64'===__ptrIR ? 8 : 0))); + delete target.pointerSize; + delete target.pointerIR; + + if( 'i32'!==__ptrIR && 'i64'!==__ptrIR ){ + toss("Invalid pointerIR:",__ptrIR); + }else if( 8!==__ptrSize && 4!==__ptrSize ){ + toss("Invalid pointerSize:",__ptrSize); + } + + + const __BigInt = target.bigIntEnabled + ? (v)=>BigInt(v || 0) + : (v)=>toss("BigInt support is disabled in this build."); + + const __Number = (v)=>Number(v||0); + + + const __asPtrType = (4===__ptrSize) ? __Number : __BigInt; + + + const __NullPtr = __asPtrType(0); + + + const __ptrAdd = function(...args){ + let rc = __asPtrType(0); + for(const v of args) rc += __asPtrType(v); + return rc; + }; + + + { + const __ptr = Object.create(null); + Object.defineProperty(target, 'ptr', { + enumerable: true, + get: ()=>__ptr, + set: ()=>toss("The ptr property is read-only.") + }); + (function f(name, val){ + Object.defineProperty(__ptr, name, { + enumerable: true, + get: ()=>val, + set: ()=>toss("ptr["+name+"] is read-only.") + }); + return f; + })( + 'null', __NullPtr + )( + 'size', __ptrSize + )( + 'ir', __ptrIR + )( + 'coerce', __asPtrType + )( + 'add', __ptrAdd + )( + 'addn', (4===__ptrIR) ? __ptrAdd : (...args)=>Number(__ptrAdd(...args)) + ); + } + + if(!target.exports){ + Object.defineProperty(target, 'exports', { + enumerable: true, configurable: true, + get: ()=>(target.instance?.exports) + }); + } + + + const cache = Object.create(null); + + cache.heapSize = 0; + + cache.memory = null; + + cache.freeFuncIndexes = []; + + cache.scopedAlloc = []; + + + cache.scopedAlloc.pushPtr = (ptr)=>{ + cache.scopedAlloc[cache.scopedAlloc.length-1].push(ptr); + return ptr; + }; + + cache.utf8Decoder = new TextDecoder(); + cache.utf8Encoder = new TextEncoder('utf-8'); + + + target.sizeofIR = (n)=>{ + switch(n){ + case 'i8': return 1; + case 'i16': return 2; + case 'i32': case 'f32': case 'float': return 4; + case 'i64': case 'f64': case 'double': return 8; + case '*': return __ptrSize; + default: + return (''+n).endsWith('*') ? __ptrSize : undefined; + } + }; + + + 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; + } + + 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); + cache.HEAP32F = new Float32Array(b); cache.HEAP64F = new Float64Array(b); + if(target.bigIntEnabled){ + if( 'undefined'!==typeof BigInt64Array ){ + cache.HEAP64 = new BigInt64Array(b); cache.HEAP64U = new BigUint64Array(b); + }else{ + toss("BigInt support is enabled, but the BigInt64Array type is missing."); + } + } + cache.heapSize = b.byteLength; + return cache; + }; + + + target.heap8 = ()=>heapWrappers().HEAP8; + + + target.heap8u = ()=>heapWrappers().HEAP8U; + + + target.heap16 = ()=>heapWrappers().HEAP16; + + + target.heap16u = ()=>heapWrappers().HEAP16U; + + + target.heap32 = ()=>heapWrappers().HEAP32; + + + target.heap32u = ()=>heapWrappers().HEAP32U; + + + target.heapForSize = function(n,unsigned = true){ + 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===globalThis['BigUint64Array']) return c.HEAP64U; + else if(n===globalThis['BigInt64Array']) return c.HEAP64; + break; + } + } + toss("Invalid heapForSize() size: expecting 8, 16, 32,", + "or (if BigInt is enabled) 64."); + }; + + const __funcTable = target.functionTable; + delete target.functionTable; + + + target.functionTable = __funcTable + ? ()=>__funcTable + : ()=>target.exports.__indirect_function_table + ; + + + target.functionEntry = function(fptr){ + const ft = target.functionTable(); + + + + return fptr < ft.length ? ft.get(__asPtrType(fptr)) : undefined; + }; + + + target.jsFuncToWasm = function f(func, sig){ + + if(!f._){ + f._ = { + + sigTypes: Object.assign(Object.create(null),{ + i: 'i32', p: __ptrIR, P: __ptrIR, s: __ptrIR, + j: 'i64', f: 'f32', d: 'f64' + }), + + + typeCodes: Object.assign(Object.create(null),{ + f64: 0x7c, f32: 0x7d, i64: 0x7e, i32: 0x7f + }), + + + uleb128Encode: (tgt, method, n)=>{ + if(n<128) tgt[method](n); + else tgt[method]( (n % 128) | 128, n>>7); + }, + + + rxJSig: /^(\w)\((\w*)\)$/, + + + sigParams: (sig)=>{ + const m = f._.rxJSig.exec(sig); + return m ? m[2] : sig.substr(1); + }, + + + letterType: (x)=>f._.sigTypes[x] || toss("Invalid signature letter:",x), + + + pushSigType: (dest, letter)=>dest.push(f._.typeCodes[f._.letterType(letter)]) + + + + }; + } + if('string'===typeof func){ + const x = sig; + sig = func; + func = x; + } + const _ = f._; + const sigParams = _.sigParams(sig); + const wasmCode = [0x01, 0x60]; + _.uleb128Encode(wasmCode, 'push', sigParams.length); + for(const x of sigParams) _.pushSigType(wasmCode, x); + if('v'===sig[0]) wasmCode.push(0); + else{ + wasmCode.push(1); + _.pushSigType(wasmCode, sig[0]); + } + _.uleb128Encode(wasmCode, 'unshift', wasmCode.length); + wasmCode.unshift( + 0x00, 0x61, 0x73, 0x6d, + 0x01, 0x00, 0x00, 0x00, + 0x01 + ); + wasmCode.push( + 0x02, 0x07, + + 0x01, 0x01, 0x65, 0x01, 0x66, 0x00, 0x00, + 0x07, 0x05, + + 0x01, 0x01, 0x66, 0x00, 0x00 + ); + return (new WebAssembly.Instance( + new WebAssembly.Module(new Uint8Array(wasmCode)), { + e: { f: func } + })).exports['f']; + }; + + + const __installFunction = function f(func, sig, scoped){ + if(scoped && !cache.scopedAlloc.length){ + toss("No scopedAllocPush() scope is active."); + } + if('string'===typeof func){ + const x = sig; + sig = func; + func = x; + } + if('string'!==typeof sig || !(func instanceof Function)){ + toss("Invalid arguments: expecting (function,signature) "+ + "or (signature,function)."); + } + const ft = target.functionTable(); + const oldLen = __asPtrType(ft.length); + let ptr; + while( (ptr = cache.freeFuncIndexes.pop()) ){ + if(ft.get(ptr)){ + + ptr = null; + continue; + }else{ + + break; + } + } + if(!ptr){ + ptr = __asPtrType(oldLen); + ft.grow(__asPtrType(1)); + } + try{ + + ft.set(ptr, func); + if(scoped){ + cache.scopedAlloc.pushPtr(ptr); + } + return ptr; + }catch(e){ + if(!(e instanceof TypeError)){ + if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); + throw e; + } + } + + try { + const fptr = target.jsFuncToWasm(func, sig); + ft.set(ptr, fptr); + if(scoped){ + cache.scopedAlloc.pushPtr(ptr); + } + }catch(e){ + if(ptr===oldLen) cache.freeFuncIndexes.push(oldLen); + throw e; + } + return ptr; + }; + + + target.installFunction = (func, sig)=>__installFunction(func, sig, false); + + + target.scopedInstallFunction = (func, sig)=>__installFunction(func, sig, true); + + + target.uninstallFunction = function(ptr){ + if(!ptr && __NullPtr!==ptr) return undefined; + + const ft = target.functionTable(); + cache.freeFuncIndexes.push(ptr); + const rc = ft.get(ptr); + ft.set(ptr, null); + return rc; + }; + + + target.peek = function f(ptr, type='i8'){ + if(type.endsWith('*')) type = __ptrIR; + const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) + ? cache : heapWrappers(); + const list = Array.isArray(ptr) ? [] : undefined; + let rc; + do{ + if(list) ptr = arguments[0].shift(); + switch(type){ + case 'i1': + case 'i8': rc = c.HEAP8[Number(ptr)>>0]; break; + case 'i16': rc = c.HEAP16[Number(ptr)>>1]; break; + case 'i32': rc = c.HEAP32[Number(ptr)>>2]; break; + case 'float': case 'f32': rc = c.HEAP32F[Number(ptr)>>2]; break; + case 'double': case 'f64': rc = Number(c.HEAP64F[Number(ptr)>>3]); break; + case 'i64': + if(c.HEAP64){ + rc = __BigInt(c.HEAP64[Number(ptr)>>3]); + break; + } + + default: + toss('Invalid type for peek():',type); + } + if(list) list.push(rc); + }while(list && arguments[0].length); + return list || rc; + }; + + + target.poke = function(ptr, value, type='i8'){ + if (type.endsWith('*')) type = __ptrIR; + const c = (cache.memory && cache.heapSize === cache.memory.buffer.byteLength) + ? cache : heapWrappers(); + for(const p of (Array.isArray(ptr) ? ptr : [ptr])){ + switch (type) { + case 'i1': + case 'i8': c.HEAP8[Number(p)>>0] = value; continue; + case 'i16': c.HEAP16[Number(p)>>1] = value; continue; + case 'i32': c.HEAP32[Number(p)>>2] = value; continue; + case 'float': case 'f32': c.HEAP32F[Number(p)>>2] = value; continue; + case 'double': case 'f64': c.HEAP64F[Number(p)>>3] = value; continue; + case 'i64': + if(c.HEAP64){ + c.HEAP64[Number(p)>>3] = __BigInt(value); + continue; + } + + default: + toss('Invalid type for poke(): ' + type); + } + } + return this; + }; + + + target.peekPtr = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), __ptrIR ); + + + target.pokePtr = (ptr, value=0)=>target.poke(ptr, value, __ptrIR); + + + target.peek8 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i8' ); + + target.poke8 = (ptr, value)=>target.poke(ptr, value, 'i8'); + + target.peek16 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i16' ); + + target.poke16 = (ptr, value)=>target.poke(ptr, value, 'i16'); + + target.peek32 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i32' ); + + target.poke32 = (ptr, value)=>target.poke(ptr, value, 'i32'); + + target.peek64 = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'i64' ); + + target.poke64 = (ptr, value)=>target.poke(ptr, value, 'i64'); + + target.peek32f = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'f32' ); + + target.poke32f = (ptr, value)=>target.poke(ptr, value, 'f32'); + + target.peek64f = (...ptr)=>target.peek( (1===ptr.length ? ptr[0] : ptr), 'f64' ); + + target.poke64f = (ptr, value)=>target.poke(ptr, value, 'f64'); + + + target.getMemValue = target.peek; + + target.getPtrValue = target.peekPtr; + + target.setMemValue = target.poke; + + target.setPtrValue = target.pokePtr; + + + target.isPtr32 = (ptr)=>( + 'number'===typeof ptr && ptr>=0 && ptr===(ptr|0) + ); + + + target.isPtr64 = (ptr)=>( + ('bigint'===typeof ptr) ? ptr >= 0 : target.isPtr32(ptr) + ); + + + target.isPtr = (4===__ptrSize) ? target.isPtr32 : target.isPtr64; + + + target.cstrlen = function(ptr){ + if(!ptr || !target.isPtr(ptr)) return null; + ptr = Number(ptr) ; + const h = heapWrappers().HEAP8U; + let pos = ptr; + for( ; h[pos] !== 0; ++pos ){} + return pos - ptr; + }; + + + const __SAB = ('undefined'===typeof SharedArrayBuffer) + ? function(){} : SharedArrayBuffer; + + const isSharedTypedArray = (aTypedArray)=>(aTypedArray.buffer instanceof __SAB); + + target.isSharedTypedArray = isSharedTypedArray; + + + const typedArrayPart = (aTypedArray, begin, end)=>{ + if( 8===__ptrSize ){ + + if( 'bigint'===typeof begin ) begin = Number(begin); + if( 'bigint'===typeof end ) end = Number(end); + } + return isSharedTypedArray(aTypedArray) + ? aTypedArray.slice(begin, end) + : aTypedArray.subarray(begin, end); + }; + + target.typedArrayPart = typedArrayPart; + + + const typedArrayToString = (typedArray, begin, end)=> + cache.utf8Decoder.decode( + typedArrayPart(typedArray, begin, end) + ); + + target.typedArrayToString = typedArrayToString; + + + target.cstrToJs = function(ptr){ + const n = target.cstrlen(ptr); + return n + ? typedArrayToString(heapWrappers().HEAP8U, Number(ptr), Number(ptr)+n) + : (null===n ? n : ""); + }; + + + target.jstrlen = function(str){ + + 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; + }; + + + target.jstrcpy = function(jstr, tgt, offset = 0, maxBytes = -1, addNul = true){ + + if(!tgt || (!(tgt instanceof Int8Array) && !(tgt instanceof Uint8Array))){ + toss("jstrcpy() target must be an Int8Array or Uint8Array."); + } + maxBytes = Number(maxBytes); + offset = Number(offset); + 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; + }; + + + 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; + const tgtNumber = Number(tgtPtr), srcNumber = Number(srcPtr); + for(; i < n && (ch = heap[srcNumber+i]); ++i){ + heap[tgtNumber+i] = ch; + } + if(i{ + return cache.utf8Encoder.encode(addNul ? (str+"\0") : str); + }; + + 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 u = cache.utf8Encoder.encode(jstr), + ptr = allocator(u.length+1); + let toFree = ptr; + try{ + const heap = heapWrappers().HEAP8U; + heap.set(u, Number(ptr)); + heap[__ptrAdd(ptr, u.length)] = 0; + toFree = __NullPtr; + return returnWithLength ? [ptr, u.length] : ptr; + }finally{ + if( toFree ) target.dealloc(toFree); + } + }; + + + target.allocCString = + (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength, + target.alloc, 'allocCString()'); + + + target.scopedAllocPush = function(){ + __affirmAlloc(target, 'scopedAllocPush'); + const a = []; + cache.scopedAlloc.push(a); + return a; + }; + + + 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()); ){ + if(target.functionEntry(p)){ + + target.uninstallFunction(p); + }else{ + target.dealloc(p); + } + } + }; + + + target.scopedAlloc = function(n){ + if(!cache.scopedAlloc.length){ + toss("No scopedAllocPush() scope is active."); + } + const p = __asPtrType(target.alloc(n)); + return cache.scopedAlloc.pushPtr(p); + }; + + Object.defineProperty(target.scopedAlloc, 'level', { + configurable: false, enumerable: false, + get: ()=>cache.scopedAlloc.length, + set: ()=>toss("The 'active' property is read-only.") + }); + + + target.scopedAllocCString = + (jstr, returnWithLength=false)=>__allocCStr(jstr, returnWithLength, + target.scopedAlloc, + 'scopedAllocCString()'); + + + const __allocMainArgv = function(isScoped, list){ + const pList = target[ + isScoped ? 'scopedAlloc' : 'alloc' + ]((list.length + 1) * target.ptr.size); + let i = 0; + list.forEach((e)=>{ + target.pokePtr(__ptrAdd(pList, target.ptr.size * i++), + target[ + isScoped ? 'scopedAllocCString' : 'allocCString' + ](""+e)); + }); + target.pokePtr(__ptrAdd(pList, target.ptr.size * i), 0); + return pList; + }; + + + target.scopedAllocMainArgv = (list)=>__allocMainArgv(true, list); + + + target.allocMainArgv = (list)=>__allocMainArgv(false, list); + + + target.cArgvToJs = (argc, pArgv)=>{ + const list = []; + for(let i = 0; i < argc; ++i){ + const arg = target.peekPtr(__ptrAdd(pArgv, target.ptr.size * i)); + list.push( arg ? target.cstrToJs(arg) : null ); + } + return list; + }; + + + target.scopedAllocCall = function(func){ + target.scopedAllocPush(); + try{ return func() } finally{ target.scopedAllocPop() } + }; + + + const __allocPtr = function(howMany, safePtrSize, method){ + __affirmAlloc(target, method); + const pIr = safePtrSize ? 'i64' : __ptrIR; + let m = target[method](howMany * (safePtrSize ? 8 : __ptrSize)); + target.poke(m, 0, pIr) + if(1===howMany){ + return m; + } + const a = [m]; + for(let i = 1; i < howMany; ++i){ + m = __ptrAdd(m, (safePtrSize ? 8 : __ptrSize)); + a[i] = m; + target.poke(m, 0, pIr); + } + return a; + }; + + + target.allocPtr = + (howMany=1, safePtrSize=true)=>__allocPtr(howMany, safePtrSize, 'alloc'); + + + target.scopedAllocPtr = + (howMany=1, safePtrSize=true)=>__allocPtr(howMany, safePtrSize, 'scopedAlloc'); + + + target.xGet = function(name){ + return target.exports[name] || toss("Cannot find exported symbol:",name); + }; + + const __argcMismatch = + (f,n)=>toss(f+"() requires",n,"argument(s)."); + + + target.xCall = function(fname, ...args){ + const f = (fname instanceof Function) ? fname : target.xGet(fname); + if(!(f instanceof Function)) toss("Exported symbol",fname,"is not a function."); + if(f.length!==args.length){ + __argcMismatch(((f===fname) ? f.name : fname),f.length) + ; + } + return (2===arguments.length && Array.isArray(arguments[1])) + ? f.apply(null, arguments[1]) + : f.apply(null, args); + }; + + + cache.xWrap = Object.create(null); + cache.xWrap.convert = Object.create(null); + + cache.xWrap.convert.arg = new Map; + + cache.xWrap.convert.result = new Map; + + const xArg = cache.xWrap.convert.arg, xResult = cache.xWrap.convert.result; + + const __xArgPtr = __asPtrType; + xArg + .set('i64', __BigInt) + .set('i32', (i)=>i|0) + .set('i16', (i)=>((i | 0) & 0xFFFF)) + .set('i8', (i)=>((i | 0) & 0xFF)) + .set('f32', (i)=>Number(i).valueOf()) + .set('float', xArg.get('f32')) + .set('f64', xArg.get('f32')) + .set('double', xArg.get('f64')) + .set('int', xArg.get('i32')) + .set('null', (i)=>i) + .set(null, xArg.get('null')) + .set('**', __xArgPtr) + .set('*', __xArgPtr) + ; + xResult.set('*', __xArgPtr) + .set('pointer', __xArgPtr) + .set('number', (v)=>Number(v)) + .set('void', (v)=>undefined) + .set(undefined, xResult.get('void')) + .set('null', (v)=>v) + .set(null, xResult.get('null')) + ; + + + for(const t of [ + 'i8', 'i16', 'i32', 'i64', 'int', + 'f32', 'float', 'f64', 'double' + ]){ + xArg.set(t+'*', __xArgPtr); + xResult.set(t+'*', __xArgPtr); + xResult.set( + t, xArg.get(t) + || toss("Maintenance required: missing arg converter for",t) + ); + } + + + const __xArgString = (v)=>{ + return ('string'===typeof v) + ? target.scopedAllocCString(v) + : __asPtrType(v); + }; + + xArg.set('string', __xArgString) + .set('utf8', __xArgString) + + ; + + xResult + .set('string', (i)=>target.cstrToJs(i)) + .set('utf8', xResult.get('string')) + .set('string:dealloc', (i)=>{ + try { return i ? target.cstrToJs(i) : null } + finally{ target.dealloc(i) } + }) + .set('utf8:dealloc', xResult.get('string:dealloc')) + .set('json', (i)=>JSON.parse(target.cstrToJs(i))) + .set('json:dealloc', (i)=>{ + try{ return i ? JSON.parse(target.cstrToJs(i)) : null } + finally{ target.dealloc(i) } + }); + + + const AbstractArgAdapter = class { + constructor(opt){ + this.name = opt.name || 'unnamed adapter'; + } + + convertArg(v,argv,argIndex){ + toss("AbstractArgAdapter must be subclassed."); + } + }; + + + xArg.FuncPtrAdapter = class FuncPtrAdapter extends AbstractArgAdapter { + constructor(opt) { + super(opt); + if(xArg.FuncPtrAdapter.warnOnUse){ + console.warn('xArg.FuncPtrAdapter is an internal-only API', + 'and is not intended to be invoked from', + 'client-level code. Invoked with:',opt); + } + this.name = opt.name || "unnamed"; + this.signature = opt.signature; + if(opt.contextKey instanceof Function){ + this.contextKey = opt.contextKey; + if(!opt.bindScope) opt.bindScope = 'context'; + } + this.bindScope = opt.bindScope + || toss("FuncPtrAdapter options requires a bindScope (explicit or implied)."); + if(FuncPtrAdapter.bindScopes.indexOf(opt.bindScope)<0){ + toss("Invalid options.bindScope ("+opt.bindMod+") for FuncPtrAdapter. "+ + "Expecting one of: ("+FuncPtrAdapter.bindScopes.join(', ')+')'); + } + this.isTransient = 'transient'===this.bindScope; + this.isContext = 'context'===this.bindScope; + this.isPermanent = 'permanent'===this.bindScope; + this.singleton = ('singleton'===this.bindScope) ? [] : undefined; + + this.callProxy = (opt.callProxy instanceof Function) + ? opt.callProxy : undefined; + } + + + + + contextKey(argv,argIndex){ + return this; + } + + + contextMap(key){ + const cm = (this.__cmap || (this.__cmap = new Map)); + let rc = cm.get(key); + if(undefined===rc) cm.set(key, (rc = [])); + return rc; + } + + + convertArg(v,argv,argIndex){ + let pair = this.singleton; + if(!pair && this.isContext){ + pair = this.contextMap(this.contextKey(argv,argIndex)); + + } + if( 0 ){ + FuncPtrAdapter.debugOut("FuncPtrAdapter.convertArg()",this.name, + 'signature =',this.signature, + 'transient ?=',this.transient, + 'pair =',pair, + 'v =',v); + } + if(pair && 2===pair.length && pair[0]===v){ + + return pair[1]; + } + if(v instanceof Function){ + + + if(this.callProxy){ + v = this.callProxy(v); + } + const fp = __installFunction(v, this.signature, this.isTransient); + if(FuncPtrAdapter.debugFuncInstall){ + FuncPtrAdapter.debugOut("FuncPtrAdapter installed", this, + this.contextKey(argv,argIndex), '@'+fp, v); + } + if(pair){ + + if(pair[1]){ + if(FuncPtrAdapter.debugFuncInstall){ + FuncPtrAdapter.debugOut("FuncPtrAdapter uninstalling", this, + this.contextKey(argv,argIndex), '@'+pair[1], v); + } + try{ + + cache.scopedAlloc.pushPtr(pair[1]); + } + catch(e){} + } + pair[0] = arguments[0] || __NullPtr; + pair[1] = fp; + } + return fp; + }else if(target.isPtr(v) || null===v || undefined===v){ + if(pair && pair[1] && pair[1]!==v){ + + if(FuncPtrAdapter.debugFuncInstall){ + FuncPtrAdapter.debugOut("FuncPtrAdapter uninstalling", this, + this.contextKey(argv,argIndex), '@'+pair[1], v); + } + try{cache.scopedAlloc.pushPtr(pair[1]);} + catch(e){} + pair[0] = pair[1] = (v || __NullPtr); + } + return v || __NullPtr; + }else{ + throw new TypeError("Invalid FuncPtrAdapter argument type. "+ + "Expecting a function pointer or a "+ + (this.name ? this.name+' ' : '')+ + "function matching signature "+ + this.signature+"."); + } + } + }; + + + xArg.FuncPtrAdapter.warnOnUse = false; + + + xArg.FuncPtrAdapter.debugFuncInstall = false; + + + xArg.FuncPtrAdapter.debugOut = console.debug.bind(console); + + + xArg.FuncPtrAdapter.bindScopes = [ + 'transient', 'context', 'singleton', 'permanent' + ]; + + + const __xArgAdapterCheck = + (t)=>xArg.get(t) || toss("Argument adapter not found:",t); + + + const __xResultAdapterCheck = + (t)=>xResult.get(t) || toss("Result adapter not found:",t); + + + cache.xWrap.convertArg = (t,...args)=>__xArgAdapterCheck(t)(...args); + + cache.xWrap.convertArgNoCheck = (t,...args)=>xArg.get(t)(...args); + + + cache.xWrap.convertResult = + (t,v)=>(null===t ? v : (t ? __xResultAdapterCheck(t)(v) : undefined)); + + cache.xWrap.convertResultNoCheck = + (t,v)=>(null===t ? v : (t ? xResult.get(t)(v) : undefined)); + + + target.xWrap = function callee(fArg, resultType, ...argTypes){ + if(3===arguments.length && Array.isArray(arguments[2])){ + argTypes = arguments[2]; + } + if(target.isPtr(fArg)){ + fArg = target.functionEntry(fArg) + || toss("Function pointer not found in WASM function table."); + } + const fIsFunc = (fArg instanceof Function); + const xf = fIsFunc ? fArg : target.xGet(fArg); + if(fIsFunc) fArg = xf.name || 'unnamed function'; + if(argTypes.length!==xf.length) __argcMismatch(fArg, xf.length); + if( 0===xf.length + && (null===resultType || 'null'===resultType) ){ + + return xf; + } + ; + __xResultAdapterCheck(resultType); + for(const t of argTypes){ + if(t instanceof AbstractArgAdapter) xArg.set(t, (...args)=>t.convertArg(...args)); + else __xArgAdapterCheck(t); + } + const cxw = cache.xWrap; + if(0===xf.length){ + + return (...args)=>(args.length + ? __argcMismatch(fArg, xf.length) + : cxw.convertResult(resultType, xf.call(null))); + } + return function(...args){ + if(args.length!==xf.length) __argcMismatch(fArg, xf.length); + const scope = target.scopedAllocPush(); + try{ + + let i = 0; + if( callee.debug ){ + console.debug("xWrap() preparing: resultType ",resultType, 'xf',xf,"argTypes",argTypes,"args",args); + } + for(; i < args.length; ++i) args[i] = cxw.convertArgNoCheck( + argTypes[i], args[i], args, i + ); + if( callee.debug ){ + console.debug("xWrap() calling: resultType ",resultType, 'xf',xf,"argTypes",argTypes,"args",args); + } + return cxw.convertResultNoCheck(resultType, xf.apply(null,args)); + }finally{ + target.scopedAllocPop(scope); + } + }; + }; + + + const __xAdapter = function(func, argc, typeName, adapter, modeName, xcvPart){ + if('string'===typeof typeName){ + if(1===argc) return xcvPart.get(typeName); + else if(2===argc){ + if(!adapter){ + xcvPart.delete(typeName); + return func; + }else if(!(adapter instanceof Function)){ + toss(modeName,"requires a function argument."); + } + xcvPart.set(typeName, adapter); + return func; + } + } + toss("Invalid arguments to",modeName); + }; + + + target.xWrap.resultAdapter = function f(typeName, adapter){ + return __xAdapter(f, arguments.length, typeName, adapter, + 'resultAdapter()', xResult); + }; + + + target.xWrap.argAdapter = function f(typeName, adapter){ + return __xAdapter(f, arguments.length, typeName, adapter, + 'argAdapter()', xArg); + }; + + target.xWrap.FuncPtrAdapter = xArg.FuncPtrAdapter; + + + target.xCallWrapped = function(fArg, resultType, argTypes, ...args){ + if(Array.isArray(arguments[3])) args = arguments[3]; + return target.xWrap(fArg, resultType, argTypes||[]).apply(null, args||[]); + }; + + + target.xWrap.testConvertArg = cache.xWrap.convertArg; + + + target.xWrap.testConvertResult = cache.xWrap.convertResult; + + return target; +}; + + +globalThis.WhWasmUtilInstaller +.yawl = function yawl(config){ + 'use strict'; + const wfetch = ()=>fetch(config.uri, {credentials: 'same-origin'}); + const wui = this; + const finalThen = function(arg){ + + if(config.wasmUtilTarget){ + const toss = (...args)=>{throw new Error(args.join(' '))}; + const tgt = config.wasmUtilTarget; + tgt.module = arg.module; + tgt.instance = arg.instance; + + if(!tgt.instance.exports.memory){ + + tgt.memory = 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){m && exports.free(m)}; + } + wui(tgt); + } + arg.config = config; + if(config.onload) config.onload(arg); + return arg ; + }; + const loadWasm = WebAssembly.instantiateStreaming + ? ()=>WebAssembly + .instantiateStreaming(wfetch(), config.imports||{}) + .then(finalThen) + : ()=> wfetch() + .then(response => response.arrayBuffer()) + .then(bytes => WebAssembly.instantiate(bytes, config.imports||{})) + .then(finalThen) + ; + return loadWasm; +}.bind( +globalThis.WhWasmUtilInstaller +); + +'use strict'; +globalThis.Jaccwabyt = +function StructBinderFactory(config){ + 'use strict'; + + + + const toss = (...args)=>{throw new Error(args.join(' '))}; + + { + let h = config.heap; + if( h instanceof WebAssembly.Memory ){ + h = function(){return new Uint8Array(this.buffer)}.bind(h); + }else if( !(h instanceof Function) ){ + + toss("config.heap must be WebAssembly.Memory instance or", + "a function which returns one."); + } + config.heap = h; + } + ['alloc','dealloc'].forEach(function(k){ + (config[k] instanceof Function) || + toss("Config option '"+k+"' must be a function."); + }); + const SBF = StructBinderFactory; + const heap = config.heap, + alloc = config.alloc, + dealloc = config.dealloc, + realloc = (config.realloc || function(){ + toss("This StructBinderFactory was configured without realloc()"); + + }), + log = config.log || console.debug.bind(console), + memberPrefix = (config.memberPrefix || ""), + memberSuffix = (config.memberSuffix || ""), + BigInt = globalThis['BigInt'], + BigInt64Array = globalThis['BigInt64Array'], + bigIntEnabled = config.bigIntEnabled ?? !!BigInt64Array; + + + let ptr; + const ptrSize = config.pointerSize + || config.ptrSize + || ('bigint'===typeof (ptr = alloc(1)) ? 8 : 4); + const ptrIR = config.pointerIR + || config.ptrIR + || (4===ptrSize ? 'i32' : 'i64'); + if( ptr ){ + dealloc(ptr); + ptr = undefined; + } + + + if(ptrSize!==4 && ptrSize!==8) toss("Invalid pointer size:",ptrSize); + if(ptrIR!=='i32' && ptrIR!=='i64') toss("Invalid pointer representation:",ptrIR); + + + const __BigInt = (bigIntEnabled && BigInt) + ? (v)=>BigInt(v || 0) + : (v)=>toss("BigInt support is disabled in this build."); + const __asPtrType = ('i32'==ptrIR) ? Number : __BigInt; + const __NullPtr = __asPtrType(0); + + + const __ptrAdd = function(...args){ + let rc = __NullPtr; + for( let i = 0; i < args.length; ++i ){ + rc += __asPtrType(args[i]); + } + return rc; + }; + + const __ptrAddSelf = function(...args){ + return __ptrAdd(this.pointer,...args); + }; + + if(!SBF.debugFlags){ + SBF.__makeDebugFlags = function(deriveFrom=null){ + + 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(); + } + + const isLittleEndian = true || (function() { + const buffer = new ArrayBuffer(2); + new DataView(buffer).setInt16(0, 256, true ); + + return new Int16Array(buffer)[0] === 256; + })() ; + + + + + const isFuncSig = (s)=>'('===s[1]; + + const isPtrSig = (s)=>'p'===s || 'P'===s || 's'===s; + const isAutoPtrSig = (s)=>'P'===s ; + + const sigLetter = (s)=>s ? (isFuncSig(s) ? 'p' : s[0]) : undefined; + + + const sigIR = function(s){ + switch(sigLetter(s)){ + case 'c': case 'C': return 'i8'; + 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); + }; + + + const sigSize = function(s){ + switch(sigLetter(s)){ + case 'c': case 'C': return 1; + case 'i': return 4; + case 'p': case 'P': case 's': return ptrSize; + case 'j': return 8; + case 'f': return 4; + case 'd': return 8; + } + toss("Unhandled signature sizeof:",s); + }; + + const affirmBigIntArray = BigInt64Array + ? ()=>true : ()=>toss('BigInt64Array is not available.'); + + + const sigDVGetter = function(s){ + switch(sigLetter(s)) { + case 'p': case 'P': case 's': { + switch(ptrSize){ + case 4: return 'getInt32'; + case 8: return affirmBigIntArray() && 'getBigInt64'; + } + break; + } + case 'i': return 'getInt32'; + case 'c': return 'getInt8'; + case 'C': return 'getUint8'; + case 'j': return affirmBigIntArray() && 'getBigInt64'; + case 'f': return 'getFloat32'; + case 'd': return 'getFloat64'; + } + toss("Unhandled DataView getter for signature:",s); + }; + + + const sigDVSetter = function(s){ + switch(sigLetter(s)){ + case 'p': case 'P': case 's': { + switch(ptrSize){ + case 4: return 'setInt32'; + case 8: return affirmBigIntArray() && 'setBigInt64'; + } + break; + } + case 'i': return 'setInt32'; + case 'c': return 'setInt8'; + case 'C': return 'setUint8'; + case 'j': return affirmBigIntArray() && 'setBigInt64'; + case 'f': return 'setFloat32'; + case 'd': return 'setFloat64'; + } + toss("Unhandled DataView setter for signature:",s); + }; + + + const sigDVSetWrapper = function(s){ + switch(sigLetter(s)) { + case 'i': case 'f': case 'c': case 'C': case 'd': return Number; + case 'j': return __BigInt; + case 'p': case 'P': case 's': + switch(ptrSize){ + case 4: return Number; + case 8: return __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."); + }; + + + const getInstanceHandle = function f(obj, create=true){ + let ii = f.map.get(obj); + if( !ii && create ){ + f.map.set(obj, (ii=f.create(obj))); + } + return ii; + }; + getInstanceHandle.map = new WeakMap; + getInstanceHandle.create = (forObj)=>{ + return Object.assign(Object.create(null),{ + o: forObj, + p: undefined, + ownsPointer: false, + zod: false, + xb: 0 + }); + }; + + + const rmInstanceHandle = (obj)=>getInstanceHandle.map.delete(obj) + ; + + const __isPtr32 = (ptr)=>('number'===typeof ptr && (ptr===(ptr|0)) && ptr>=0); + const __isPtr64 = (ptr)=>( + ('bigint'===typeof ptr && ptr >= 0) || __isPtr32(ptr) + ); + + + const __isPtr = (4===ptrSize) ? __isPtr32 : __isPtr64; + + const __isNonNullPtr = (v)=>__isPtr(v) && (v>0); + + + const __freeStruct = function(ctor, obj, m){ + const ii = getInstanceHandle(obj, false); + if( !ii || ii.isDisposing ) return; + ii.isDisposing = true; + if( !m && !(m = ii.p) ){ + console.warn("Cannot(?) happen: __freeStruct() found no instanceInfo"); + return; + } + if(Array.isArray(obj.ondispose)){ + let x; + while((x = obj.ondispose.pop())){ + try{ + if(x instanceof Function) x.call(obj); + else if(x instanceof StructType) x.dispose(); + else if(__isPtr(x)) dealloc(x); + + + }catch(e){ + console.warn("ondispose() for",ctor.structName,'@', + m,'threw. NOT propagating it.',e); + } + } + }else if(obj.ondispose instanceof Function){ + try{obj.ondispose()} + catch(e){ + + console.warn("ondispose() for",ctor.structName,'@', + m,'threw. NOT propagating it.',e); + } + } + rmInstanceHandle(obj); + delete obj.ondispose; + if(ctor.debugFlags.__flags.dealloc){ + log("debug.dealloc:",(ii.ownsPointer?"":"EXTERNAL"), + ctor.structName,"instance:", + ctor.structInfo.sizeof,"bytes @"+m); + } + if(ii.ownsPointer){ + if( ii.zod || ctor.structInfo.zeroOnDispose ){ + heap().fill(0, Number(m), + Number(m) + ctor.structInfo.sizeof + ii.xb); + } + dealloc(m); + } + }; + + + const rop0 = ()=>{return {configurable: false, writable: false, + iterable: false}}; + + + const rop = (v)=>{return {...rop0(), value: v}}; + + + const __allocStruct = function f(ctor, obj, xm){ + let opt; + const checkPtr = (ptr)=>{ + __isNonNullPtr(ptr) || + toss("Invalid pointer value",arguments[0],"for",ctor.structName,"constructor."); + }; + if( arguments.length>=3 ){ + if( xm && ('object'===typeof xm) ){ + opt = xm; + xm = opt?.wrap; + }else{ + checkPtr(xm); + opt = {wrap: xm}; + } + }else{ + opt = {} + } + + const fill = !xm ; + let nAlloc = 0; + let ownsPointer = false; + if(xm){ + + checkPtr(xm); + ownsPointer = !!opt?.takeOwnership; + }else{ + const nX = opt?.extraBytes ?? 0; + if( nX<0 || (nX!==(nX|0)) ){ + toss("Invalid extraBytes value:",opt?.extraBytes); + } + nAlloc = ctor.structInfo.sizeof + nX; + xm = alloc(nAlloc) + || toss("Allocation of",ctor.structName,"structure failed."); + ownsPointer = true; + } + try { + if( opt?.debugFlags ){ + + obj.debugFlags(opt.debugFlags); + } + if(ctor.debugFlags.__flags.alloc){ + log("debug.alloc:",(fill?"":"EXTERNAL"), + ctor.structName,"instance:", + ctor.structInfo.sizeof,"bytes @"+xm); + } + if(fill){ + heap().fill(0, Number(xm), Number(xm) + nAlloc); + } + const ii = getInstanceHandle(obj); + ii.p = xm; + ii.ownsPointer = ownsPointer; + ii.xb = nAlloc ? (nAlloc-ctor.structInfo.sizeof) : 0; + ii.zod = !!opt?.zeroOnDispose; + if( opt?.ondispose && opt.ondispose!==xm ){ + obj.addOnDispose( opt.ondispose ); + } + }catch(e){ + __freeStruct(ctor, obj, xm); + throw e; + } + }; + + + const looksLikeASig = function f(sig){ + f.rxSig1 ??= /^[ipPsjfdcC]$/; + f.rxSig2 ??= /^[vipPsjfdcC]\([ipPsjfdcC]*\)$/; + return f.rxSig1.test(sig) || f.rxSig2.test(sig); + }; + + + const __adaptorsFor = function(who){ + let x = this.get(who); + if( !x ){ + x = [ Object.create(null), Object.create(null), Object.create(null) ]; + this.set(who, x); + } + return x; + }.bind(new WeakMap); + + + const __adaptor = function(who, which, key, proxy){ + const a = __adaptorsFor(who)[which]; + if(3===arguments.length) return a[key]; + if( proxy ) return a[key] = proxy; + return delete a[key]; + }; + + const noopAdapter = (x)=>x; + + + const __adaptGet = function(key, ...args){ + return __adaptor(this, 0, key, ...args); + }; + + const __affirmNotASig = function(ctx,key){ + looksLikeASig(key) && + toss(ctx,"(",key,") collides with a data type signature."); + }; + + + const __adaptSet = function(key, ...args){ + __affirmNotASig('Setter adaptor',key); + return __adaptor(this, 1, key, ...args); + }; + + + const __adaptStruct = function(key, ...args){ + __affirmNotASig('Struct adaptor',key); + return __adaptor(this, 2, key, ...args); + }; + + + const __adaptStruct2 = function(who,key){ + const si = ('string'===typeof key) + ? __adaptor(who, 2, key) : key; + if( 'object'!==typeof si ){ + toss("Invalid struct mapping object. Arg =",key,JSON.stringify(si)); + } + return si; + }; + + const __memberKey = (k)=>memberPrefix + k + memberSuffix; + const __memberKeyProp = rop(__memberKey); + + + + const __lookupMember = function(structInfo, memberName, tossIfNotFound=true){ + let m = structInfo.members[memberName]; + if(!m && (memberPrefix || memberSuffix)){ + + for(const v of Object.values(structInfo.members)){ + if(v.key===memberName){ m = v; break; } + } + if(!m && tossIfNotFound){ + toss(sPropName(structInfo.name || structInfo.structName, memberName), + 'is not a mapped struct member.'); + } + } + return m; + }; + + + const __memberSignature = function f(obj,memberName,emscriptenFormat=false){ + if(!f._) f._ = (x)=>x.replace(/[^vipPsjrdcC]/g,"").replace(/[pPscC]/g,'i'); + const m = __lookupMember(obj.structInfo, memberName, true); + return emscriptenFormat ? f._(m.signature) : m.signature; + }; + + + const __structMemberKeys = rop(function(){ + const a = []; + for(const k of Object.keys(this.structInfo.members)){ + a.push(this.memberKey(k)); + } + return a; + }); + + const __utf8Decoder = new TextDecoder('utf-8'); + const __utf8Encoder = new TextEncoder(); + + const __SAB = ('undefined'===typeof SharedArrayBuffer) + ? function(){} : SharedArrayBuffer; + const __utf8Decode = function(arrayBuffer, begin, end){ + if( 8===ptrSize ){ + begin = Number(begin); + end = Number(end); + } + return __utf8Decoder.decode( + (arrayBuffer.buffer instanceof __SAB) + ? arrayBuffer.slice(begin, end) + : arrayBuffer.subarray(begin, end) + ); + }; + + + 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; + }; + + + const __affirmCStringSignature = function(member){ + if('s'===member.signature) return; + toss("Invalid member type signature for C-string value:", + JSON.stringify(member)); + }; + + + const __memberToJsString = function f(obj,memberName){ + const m = __lookupMember(obj.structInfo, memberName, true); + __affirmCStringSignature(m); + const addr = obj[m.key]; + + if(!addr) return null; + let pos = addr; + const mem = heap(); + for( ; mem[pos]!==0; ++pos ) { + + }; + + return (addr===pos) ? "" : __utf8Decode(mem, addr, pos); + }; + + + const __addOnDispose = function(obj, ...v){ + if(obj.ondispose){ + if(!Array.isArray(obj.ondispose)){ + obj.ondispose = [obj.ondispose]; + } + }else{ + obj.ondispose = []; + } + obj.ondispose.push(...v); + }; + + + 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(); + + + h.set(u, Number(mem)); + h[__ptrAdd(mem, u.length)] = 0; + + return mem; + }; + + + const __setMemberCString = function(obj, memberName, str){ + const m = __lookupMember(obj.structInfo, memberName, true); + __affirmCStringSignature(m); + + const mem = __allocCString(str); + obj[m.key] = mem; + __addOnDispose(obj, mem); + return obj; + }; + + + const StructType = function StructType(structName, structInfo){ + if(arguments[2]!==rop){ + toss("Do not call the StructType constructor", + "from client-level code."); + } + Object.defineProperties(this,{ + + structName: rop(structName), + structInfo: rop(structInfo) + }); + }; + + + 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(function(){ + const p = this.pointer; + return p + ? new Uint8Array(heap().slice(Number(p), Number(p) + this.structInfo.sizeof)) + : null; + }), + extraBytes: { + configurable: false, enumerable: false, + get: function(){return getInstanceHandle(this, false)?.xb ?? 0;} + }, + zeroOnDispose: { + configurable: false, enumerable: false, + get: function(){ + return getInstanceHandle(this, false)?.zod + ?? !!this.structInfo.zeroOnDispose; + } + }, + pointer: { + configurable: false, enumerable: false, + get: function(){return getInstanceHandle(this, false)?.p}, + set: ()=>toss("Cannot assign the 'pointer' property of a struct.") + + + + }, + setMemberCString: rop(function(memberName, str){ + return __setMemberCString(this, memberName, str); + }) + }); + + Object.assign(StructType.prototype,{ + addOnDispose: function(...v){ + __addOnDispose(this,...v); + return this; + } + }); + + + Object.defineProperties(StructType, { + allocCString: rop(__allocCString), + isA: rop((v)=>v instanceof StructType), + hasExternalPointer: rop((v)=>{ + const ii = getInstanceHandle(v, false); + return !!(ii?.p && !ii?.ownsPointer); + }), + memberKey: __memberKeyProp + + }); + + + const memberGetterProxy = function(si){ + return si.get || (si.adaptGet + ? StructBinder.adaptGet(si.adaptGet) + : undefined); + }; + + + const memberSetterProxy = function(si){ + return si.set || (si.adaptSet + ? StructBinder.adaptSet(si.adaptSet) + : undefined); + }; + + + const makeMemberStructWrapper = function callee(ctor, name, si){ + + const __innerStructs = (callee.innerStructs ??= new Map()); + const key = ctor.memberKey(name); + if( undefined!==si.signature ){ + toss("'signature' cannot be used on an embedded struct (", + ctor.structName,".",key,")."); + } + if( memberSetterProxy(si) ){ + toss("'set' and 'adaptSet' are not permitted for nested struct members."); + } + + si.structName ??= ctor.structName+'::'+name; + si.key = key; + si.name = name; + si.constructor = this.call(this, si.structName, si); + + + const getterProxy = memberGetterProxy(si); + const prop = Object.assign(Object.create(null),{ + configurable: false, + enumerable: false, + set: __propThrowOnSet(ctor.structName, key), + get: function(){ + const dbg = this.debugFlags.__flags; + const p = this.pointer; + const k = p+'.'+key; + let s = __innerStructs.get(k); + if(dbg.getter){ log("debug.getter: k =",k); } + if( !s ){ + s = new si.constructor(__ptrAdd(p, si.offset)); + __innerStructs.set(k, s); + this.addOnDispose(()=>s.dispose()); + s.addOnDispose(()=>__innerStructs.delete(k)); + + } + if(getterProxy) s = getterProxy.apply(this,[s,key]); + if(dbg.getter) log("debug.getter: result =",s); + return s; + } + }); + Object.defineProperty(ctor.prototype, key, prop); + }; + + + const makeMemberWrapper = function f(ctor, name, si){ + si = __adaptStruct2(this, si); + if( si.members ){ + return makeMemberStructWrapper.call(this, ctor, name, si); + } + + if(!f.cache){ + + f.cache = {getters: {}, setters: {}, sw:{}}; + const a = ['i','c','C','p','P','s','f','d','v()']; + if(bigIntEnabled) a.push('j'); + a.forEach(function(v){ + f.cache.getters[v] = sigDVGetter(v) ; + f.cache.setters[v] = sigDVSetter(v) ; + f.cache.sw[v] = sigDVSetWrapper(v) ; + }); + f.sigCheck = function(obj, name, key,sig){ + if(Object.prototype.hasOwnProperty.call(obj, key)){ + toss(obj.structName,'already has a property named',key+'.'); + } + looksLikeASig(sig) + || toss("Malformed signature for", + sPropName(obj.structName,name)+":",sig); + }; + } + const key = ctor.memberKey(name); + f.sigCheck(ctor.prototype, name, key, si.signature); + si.key = key; + si.name = name; + const sigGlyph = sigLetter(si.signature); + const xPropName = sPropName(ctor.structName,key); + const dbg = ctor.debugFlags.__flags; + + const getterProxy = memberGetterProxy(si); + const prop = Object.create(null); + prop.configurable = false; + prop.enumerable = false; + prop.get = function(){ + + if(dbg.getter){ + log("debug.getter:",f.cache.getters[sigGlyph],"for", sigIR(sigGlyph), + xPropName,'@', this.pointer,'+',si.offset,'sz',si.sizeof); + } + let rc = ( + new DataView(heap().buffer, Number(this.pointer) + si.offset, si.sizeof) + )[f.cache.getters[sigGlyph]](0, isLittleEndian); + + if(getterProxy) rc = getterProxy.apply(this,[key,rc]); + if(dbg.getter) log("debug.getter:",xPropName,"result =",rc); + return rc; + }; + if(si.readOnly){ + prop.set = __propThrowOnSet(ctor.prototype.structName,key); + }else{ + const setterProxy = memberSetterProxy(si); + prop.set = function(v){ + + if(dbg.setter){ + log("debug.setter:",f.cache.setters[sigGlyph],"for", sigIR(sigGlyph), + xPropName,'@', this.pointer,'+',si.offset,'sz',si.sizeof, v); + } + if(!this.pointer){ + toss("Cannot set native property on a disposed", + this.structName,"instance."); + } + if( setterProxy ) v = setterProxy.apply(this,[key,v]); + if( null===v || undefined===v ) v = __NullPtr; + else if( isPtrSig(si.signature) && !__isPtr(v) ){ + if(isAutoPtrSig(si.signature) && (v instanceof StructType)){ + + v = v.pointer || __NullPtr; + if(dbg.setter) log("debug.setter:",xPropName,"resolved to",v); + }else{ + toss("Invalid value for pointer-type",xPropName+'.'); + } + } + ( + new DataView(heap().buffer, Number(this.pointer) + si.offset, + si.sizeof) + )[f.cache.setters[sigGlyph]](0, f.cache.sw[sigGlyph](v), isLittleEndian); + }; + } + Object.defineProperty(ctor.prototype, key, prop); + }; + + + const StructBinderImpl = function StructBinderImpl( + structName, si, opt = Object.create(null) + ){ + + const StructCtor = function StructCtor(arg){ + + if(!(this instanceof StructCtor)){ + toss("The",structName,"constructor may only be called via 'new'."); + } + __allocStruct(StructCtor, this, ...arguments); + }; + const self = this; + + const ads = (x)=>{ + + return (('string'===typeof x) && looksLikeASig(x)) + ? {signature: x} : __adaptStruct2(self,x); + }; + if(1===arguments.length){ + si = ads(structName); + structName = si.structName || si.name; + }else if(2===arguments.length){ + si = ads(si); + si.name ??= structName; + }else{ + si = ads(si); + } + structName ??= si.structName; + + structName ??= opt.structName; + if( !structName ) toss("One of 'name' or 'structName' are required."); + if( si.adapt ){ + + Object.keys(si.adapt.struct||{}).forEach((k)=>{ + __adaptStruct.call(StructBinderImpl, k, si.adapt.struct[k]); + }); + Object.keys(si.adapt.set||{}).forEach((k)=>{ + __adaptSet.call(StructBinderImpl, k, si.adapt.set[k]); + }); + Object.keys(si.adapt.get||{}).forEach((k)=>{ + __adaptGet.call(StructBinderImpl, k, si.adapt.get[k]); + }); + } + if(!si.members && !si.sizeof){ + si.sizeof = sigSize(si.signature); + } + + const debugFlags = rop(SBF.__makeDebugFlags(StructBinder.debugFlags)); + Object.defineProperties(StructCtor,{ + debugFlags: debugFlags, + isA: rop((v)=>v instanceof StructCtor), + memberKey: __memberKeyProp, + memberKeys: __structMemberKeys, + + structInfo: rop(si), + structName: rop(structName), + ptrAdd: rop(__ptrAdd) + }); + StructCtor.prototype = new StructType(structName, si, rop); + Object.defineProperties(StructCtor.prototype,{ + debugFlags: debugFlags, + constructor: rop(StructCtor) + , + ptrAdd: rop(__ptrAddSelf) + }); + let lastMember = false; + let offset = 0; + const autoCalc = !!si.autoCalcSizeOffset; + + if( !autoCalc ){ + if( !si.sizeof ){ + toss(structName,"description is missing its sizeof property."); + } + + si.offset ??= 0; + }else{ + si.offset ??= 0; + } + Object.keys(si.members || {}).forEach((k)=>{ + + let m = ads(si.members[k]); + if(!m.members && !m.sizeof){ + + m.sizeof = sigSize(m.signature); + if(!m.sizeof){ + toss(sPropName(structName,k), "is missing a sizeof property.",m); + } + } + if( undefined===m.offset ){ + if( autoCalc ) m.offset = offset; + else{ + toss(sPropName(structName,k),"is missing its offset.", + JSON.stringify(m)); + } + + } + si.members[k] = m ; + if(!lastMember || lastMember.offset < m.offset) lastMember = m; + const oldAutoCalc = !!m.autoCalc; + if( autoCalc ) m.autoCalcSizeOffset = true; + makeMemberWrapper.call(self, StructCtor, k, m); + if( oldAutoCalc ) m.autoCalcSizeOffset = true; + else delete m.autoCalcSizeOffset; + offset += m.sizeof; + + }); + + if( !lastMember ) toss("No member property descriptions found."); + if( !si.sizeof ) si.sizeof = offset; + if(si.sizeof===1){ + (si.signature === 'c' || si.signature === 'C') || + toss("Unexpected sizeof==1 member", + sPropName(structName,k), + "with signature",si.signature); + }else{ + + + if(0!==(si.sizeof%4)){ + console.warn("Invalid struct member description",si); + toss(structName,"sizeof is not aligned. sizeof="+si.sizeof); + } + if(0!==(si.offset%4)){ + console.warn("Invalid struct member description",si); + toss(structName,"offset is not aligned. offset="+si.offset); + } + } + if( si.sizeof < offset ){ + console.warn("Suspect struct description:",si,"offset =",offset); + toss("Mismatch in the calculated vs. the provided sizeof/offset info.", + "Expected sizeof",offset,"but got",si.sizeof,"for",si); + + } + delete si.autoCalcSizeOffset; + return StructCtor; + }; + + const StructBinder = function StructBinder(structName, structInfo){ + return (1==arguments.length) + ? StructBinderImpl.call(StructBinder, structName) + : StructBinderImpl.call(StructBinder, structName, structInfo); + }; + StructBinder.StructType = StructType; + StructBinder.config = config; + StructBinder.allocCString = __allocCString; + StructBinder.adaptGet = __adaptGet; + StructBinder.adaptSet = __adaptSet; + StructBinder.adaptStruct = __adaptStruct; + StructBinder.ptrAdd = __ptrAdd; + if(!StructBinder.debugFlags){ + StructBinder.debugFlags = SBF.__makeDebugFlags(SBF.debugFlags); + } + return StructBinder; +}; + +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + 'use strict'; + const toss = (...args)=>{throw new Error(args.join(' '))}; + const capi = sqlite3.capi, wasm = sqlite3.wasm, util = sqlite3.util; + globalThis.WhWasmUtilInstaller(wasm); + delete globalThis.WhWasmUtilInstaller; + + if(0){ + + + const dealloc = wasm.exports[sqlite3.config.deallocExportName]; + const nFunc = wasm.functionTable().length; + let i; + for(i = 0; i < nFunc; ++i){ + const e = wasm.functionEntry(i); + if(dealloc === e){ + capi.SQLITE_WASM_DEALLOC = i; + break; + } + } + if(dealloc !== wasm.functionEntry(capi.SQLITE_WASM_DEALLOC)){ + toss("Internal error: cannot find function pointer for SQLITE_WASM_DEALLOC."); + } + } + + + const bindingSignatures = { + core: [ + + ["sqlite3_aggregate_context","void*", "sqlite3_context*", "int"], + + ["sqlite3_backup_init", "sqlite3_backup*", "sqlite3*", "string", "sqlite3*", "string"], + ["sqlite3_backup_step", "int", "sqlite3_backup*", "int"], + ["sqlite3_backup_finish", "int", "sqlite3_backup*"], + ["sqlite3_backup_remaining", "int", "sqlite3_backup*"], + ["sqlite3_backup_pagecount", "int", "sqlite3_backup*"], + + ["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_parameter_name", "string", "sqlite3_stmt*", "int"], + ["sqlite3_bind_pointer", "int", + "sqlite3_stmt*", "int", "*", "string:static", "*"], + + ["sqlite3_bind_zeroblob", "int", "sqlite3_stmt*", "int", "int"], + ["sqlite3_busy_handler","int", [ + "sqlite3*", + new wasm.xWrap.FuncPtrAdapter({ + signature: 'i(pi)', + contextKey: (argv,argIndex)=>argv[0] + }), + "*" + ]], + ["sqlite3_busy_timeout","int", "sqlite3*", "int"], + + + ["sqlite3_changes", "int", "sqlite3*"], + ["sqlite3_clear_bindings","int", "sqlite3_stmt*"], + ["sqlite3_collation_needed", "int", "sqlite3*", "*", "*"], + ["sqlite3_column_blob","*", "sqlite3_stmt*", "int"], + ["sqlite3_column_bytes","int", "sqlite3_stmt*", "int"], + ["sqlite3_column_count", "int", "sqlite3_stmt*"], + ["sqlite3_column_decltype", "string", "sqlite3_stmt*", "int"], + ["sqlite3_column_double","f64", "sqlite3_stmt*", "int"], + ["sqlite3_column_int","int", "sqlite3_stmt*", "int"], + ["sqlite3_column_name","string", "sqlite3_stmt*", "int"], + ["sqlite3_column_type","int", "sqlite3_stmt*", "int"], + ["sqlite3_column_value","sqlite3_value*", "sqlite3_stmt*", "int"], + ["sqlite3_commit_hook", "void*", [ + "sqlite3*", + new wasm.xWrap.FuncPtrAdapter({ + name: 'sqlite3_commit_hook', + signature: 'i(p)', + contextKey: (argv)=>argv[0] + }), + '*' + ]], + ["sqlite3_compileoption_get", "string", "int"], + ["sqlite3_compileoption_used", "int", "string"], + ["sqlite3_complete", "int", "string:flexible"], + ["sqlite3_context_db_handle", "sqlite3*", "sqlite3_context*"], + + + ["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_db_readonly", "int", "sqlite3*", "string"], + ["sqlite3_db_status", "int", "sqlite3*", "int", "*", "*", "int"], + ["sqlite3_errcode", "int", "sqlite3*"], + ["sqlite3_errmsg", "string", "sqlite3*"], + ["sqlite3_error_offset", "int", "sqlite3*"], + ["sqlite3_errstr", "string", "int"], + ["sqlite3_exec", "int", [ + "sqlite3*", "string:flexible", + new wasm.xWrap.FuncPtrAdapter({ + signature: 'i(pipp)', + bindScope: 'transient', + callProxy: (callback)=>{ + let aNames; + return (pVoid, nCols, pColVals, pColNames)=>{ + try { + const aVals = wasm.cArgvToJs(nCols, pColVals); + if(!aNames) aNames = wasm.cArgvToJs(nCols, pColNames); + return callback(aVals, aNames) | 0; + }catch(e){ + + return e.resultCode || capi.SQLITE_ERROR; + } + } + } + }), + "*", "**" + ]], + ["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_get_autocommit", "int", "sqlite3*"], + ["sqlite3_get_auxdata", "*", "sqlite3_context*", "int"], + ["sqlite3_initialize", undefined], + ["sqlite3_interrupt", undefined, "sqlite3*"], + ["sqlite3_is_interrupted", "int", "sqlite3*"], + ["sqlite3_keyword_count", "int"], + ["sqlite3_keyword_name", "int", ["int", "**", "*"]], + ["sqlite3_keyword_check", "int", ["string", "int"]], + ["sqlite3_libversion", "string"], + ["sqlite3_libversion_number", "int"], + ["sqlite3_limit", "int", ["sqlite3*", "int", "int"]], + ["sqlite3_malloc", "*","int"], + ["sqlite3_next_stmt", "sqlite3_stmt*", ["sqlite3*","sqlite3_stmt*"]], + ["sqlite3_open", "int", "string", "*"], + ["sqlite3_open_v2", "int", "string", "*", "int", "string"], + + + ["sqlite3_realloc", "*","*","int"], + ["sqlite3_reset", "int", "sqlite3_stmt*"], + + ["sqlite3_result_blob", undefined, "sqlite3_context*", "*", "int", "*"], + ["sqlite3_result_double", undefined, "sqlite3_context*", "f64"], + ["sqlite3_result_error", undefined, "sqlite3_context*", "string", "int"], + ["sqlite3_result_error_code", undefined, "sqlite3_context*", "int"], + ["sqlite3_result_error_nomem", undefined, "sqlite3_context*"], + ["sqlite3_result_error_toobig", undefined, "sqlite3_context*"], + ["sqlite3_result_int", undefined, "sqlite3_context*", "int"], + ["sqlite3_result_null", undefined, "sqlite3_context*"], + ["sqlite3_result_pointer", undefined, + "sqlite3_context*", "*", "string:static", "*"], + ["sqlite3_result_subtype", undefined, "sqlite3_value*", "int"], + ["sqlite3_result_text", undefined, "sqlite3_context*", "string", "int", "*"], + ["sqlite3_result_zeroblob", undefined, "sqlite3_context*", "int"], + ["sqlite3_rollback_hook", "void*", [ + "sqlite3*", + new wasm.xWrap.FuncPtrAdapter({ + name: 'sqlite3_rollback_hook', + signature: 'v(p)', + contextKey: (argv)=>argv[0] + }), + '*' + ]], + + ["sqlite3_set_auxdata", undefined, [ + "sqlite3_context*", "int", "*", + true + ? "*" + : new wasm.xWrap.FuncPtrAdapter({ + + name: 'xDestroyAuxData', + signature: 'v(p)', + contextKey: (argv, argIndex)=>argv[0] + }) + ]], + ['sqlite3_set_errmsg', 'int', 'sqlite3*', 'int', 'string'], + ["sqlite3_shutdown", undefined], + ["sqlite3_sourceid", "string"], + ["sqlite3_sql", "string", "sqlite3_stmt*"], + ["sqlite3_status", "int", "int", "*", "*", "int"], + ["sqlite3_step", "int", "sqlite3_stmt*"], + ["sqlite3_stmt_busy", "int", "sqlite3_stmt*"], + ["sqlite3_stmt_readonly", "int", "sqlite3_stmt*"], + ["sqlite3_stmt_status", "int", "sqlite3_stmt*", "int", "int"], + ["sqlite3_strglob", "int", "string","string"], + ["sqlite3_stricmp", "int", "string", "string"], + ["sqlite3_strlike", "int", "string", "string","int"], + ["sqlite3_strnicmp", "int", "string", "string", "int"], + ["sqlite3_table_column_metadata", "int", + "sqlite3*", "string", "string", "string", + "**", "**", "*", "*", "*"], + ["sqlite3_total_changes", "int", "sqlite3*"], + ["sqlite3_trace_v2", "int", [ + "sqlite3*", "int", + new wasm.xWrap.FuncPtrAdapter({ + name: 'sqlite3_trace_v2::callback', + signature: 'i(ippp)', + contextKey: (argv,argIndex)=>argv[0] + }), + "*" + ]], + ["sqlite3_txn_state", "int", ["sqlite3*","string"]], + + ["sqlite3_uri_boolean", "int", "sqlite3_filename", "string", "int"], + ["sqlite3_uri_key", "string", "sqlite3_filename", "int"], + ["sqlite3_uri_parameter", "string", "sqlite3_filename", "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_dup", "sqlite3_value*", "sqlite3_value*"], + ["sqlite3_value_free", undefined, "sqlite3_value*"], + ["sqlite3_value_frombind", "int", "sqlite3_value*"], + ["sqlite3_value_int","int", "sqlite3_value*"], + ["sqlite3_value_nochange", "int", "sqlite3_value*"], + ["sqlite3_value_numeric_type", "int", "sqlite3_value*"], + ["sqlite3_value_pointer", "*", "sqlite3_value*", "string:static"], + ["sqlite3_value_subtype", "int", "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*"] + + + ], + + int64: [ + ["sqlite3_bind_int64","int", ["sqlite3_stmt*", "int", "i64"]], + ["sqlite3_changes64","i64", ["sqlite3*"]], + ["sqlite3_column_int64","i64", ["sqlite3_stmt*", "int"]], + ["sqlite3_deserialize", "int", "sqlite3*", "string", "*", "i64", "i64", "int"] + , + ["sqlite3_last_insert_rowid", "i64", ["sqlite3*"]], + ["sqlite3_malloc64", "*","i64"], + ["sqlite3_msize", "i64", "*"], + ["sqlite3_overload_function", "int", ["sqlite3*","string","int"]], + ["sqlite3_realloc64", "*","*", "i64"], + ["sqlite3_result_int64", undefined, "*", "i64"], + ["sqlite3_result_zeroblob64", "int", "*", "i64"], + ["sqlite3_serialize","*", "sqlite3*", "string", "*", "int"], + ["sqlite3_set_last_insert_rowid", undefined, ["sqlite3*", "i64"]], + ["sqlite3_status64", "int", "int", "*", "*", "int"], + ["sqlite3_db_status64", "int", "sqlite3*", "int", "*", "*", "int"], + ["sqlite3_total_changes64", "i64", ["sqlite3*"]], + ["sqlite3_update_hook", "*", [ + "sqlite3*", + new wasm.xWrap.FuncPtrAdapter({ + name: 'sqlite3_update_hook::callback', + signature: "v(pippj)", + contextKey: (argv)=>argv[0], + callProxy: (callback)=>{ + return (p,op,z0,z1,rowid)=>{ + callback(p, op, wasm.cstrToJs(z0), wasm.cstrToJs(z1), rowid); + }; + } + }), + "*" + ]], + ["sqlite3_uri_int64", "i64", ["sqlite3_filename", "string", "i64"]], + ["sqlite3_value_int64","i64", "sqlite3_value*"] + + ], + + wasmInternal: [ + ["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_posix_create_file", "int", "string","*", "int"], + ["sqlite3__wasm_vfs_unlink", "int", "sqlite3_vfs*","string"], + ["sqlite3__wasm_qfmt_token","string:dealloc", "string","int"] + ] + } ; + + if( !!wasm.exports.sqlite3_progress_handler ){ + bindingSignatures.core.push( + ["sqlite3_progress_handler", undefined, [ + "sqlite3*", "int", new wasm.xWrap.FuncPtrAdapter({ + name: 'xProgressHandler', + signature: 'i(p)', + bindScope: 'context', + contextKey: (argv,argIndex)=>argv[0] + }), "*" + ]] + ); + } + + if( !!wasm.exports.sqlite3_stmt_explain ){ + bindingSignatures.core.push( + ["sqlite3_stmt_explain", "int", "sqlite3_stmt*", "int"], + ["sqlite3_stmt_isexplain", "int", "sqlite3_stmt*"] + ); + } + + if( !!wasm.exports.sqlite3_set_authorizer ){ + bindingSignatures.core.push( + ["sqlite3_set_authorizer", "int", [ + "sqlite3*", + new wasm.xWrap.FuncPtrAdapter({ + name: "sqlite3_set_authorizer::xAuth", + signature: "i(pi"+"ssss)", + contextKey: (argv, argIndex)=>argv[0], + + callProxy: (callback)=>{ + return (pV, iCode, s0, s1, s2, s3)=>{ + try{ + s0 = s0 && wasm.cstrToJs(s0); s1 = s1 && wasm.cstrToJs(s1); + s2 = s2 && wasm.cstrToJs(s2); s3 = s3 && wasm.cstrToJs(s3); + return callback(pV, iCode, s0, s1, s2, s3) | 0; + }catch(e){ + return e.resultCode || capi.SQLITE_ERROR; + } + } + } + }), + "*" + ]] + ); + } + + if( !!wasm.exports.sqlite3_column_origin_name ){ + bindingSignatures.core.push( + ["sqlite3_column_database_name","string", "sqlite3_stmt*", "int"], + ["sqlite3_column_origin_name","string", "sqlite3_stmt*", "int"], + ["sqlite3_column_table_name","string", "sqlite3_stmt*", "int"] + ); + } + + if(false && wasm.compileOptionUsed('SQLITE_ENABLE_NORMALIZE')){ + + bindingSignatures.core.push(["sqlite3_normalized_sql", "string", "sqlite3_stmt*"]); + } + + if( !!wasm.exports.sqlite3_key_v2 ){ + + bindingSignatures.core.push( + ["sqlite3_key", "int", "sqlite3*", "string", "int"], + ["sqlite3_key_v2","int","sqlite3*","string","*","int"], + ["sqlite3_rekey", "int", "sqlite3*", "string", "int"], + ["sqlite3_rekey_v2", "int", "sqlite3*", "string", "*", "int"], + ["sqlite3_activate_see", undefined, "string"], + ["sqlite3mc_cipher_count", "int"], + ["sqlite3mc_cipher_index", "int", "string"], + ["sqlite3mc_cipher_name", "string", "int"], + ["sqlite3mc_cipher_name_copy", "int", "int", "string", "int"], + ["sqlite3mc_config", "int", "sqlite3*", "string", "int"], + ["sqlite3mc_config_cipher", "int", "sqlite3*", "string", "string", "int"], + ["sqlite3mc_codec_data", "string", "sqlite3*", "string", "string"], + ["sqlite3mc_version", "string"], + ["sqlite3mc_vfs_create", "int", "string", "int"], + ["sqlite3mc_vfs_destroy", undefined, "string"], + ["sqlite3mc_vfs_shutdown", undefined] + ); + } + + if( wasm.bigIntEnabled && !!wasm.exports.sqlite3_declare_vtab ){ + bindingSignatures.int64.push( + ["sqlite3_create_module", "int", + ["sqlite3*","string","sqlite3_module*","*"]], + ["sqlite3_create_module_v2", "int", + ["sqlite3*","string","sqlite3_module*","*","*"]], + ["sqlite3_declare_vtab", "int", ["sqlite3*", "string:flexible"]], + ["sqlite3_drop_modules", "int", ["sqlite3*", "**"]], + ["sqlite3_vtab_collation","string","sqlite3_index_info*","int"], + + ["sqlite3_vtab_distinct","int", "sqlite3_index_info*"], + ["sqlite3_vtab_in","int", "sqlite3_index_info*", "int", "int"], + ["sqlite3_vtab_in_first", "int", "sqlite3_value*", "**"], + ["sqlite3_vtab_in_next", "int", "sqlite3_value*", "**"], + ["sqlite3_vtab_nochange","int", "sqlite3_context*"], + ["sqlite3_vtab_on_conflict","int", "sqlite3*"], + ["sqlite3_vtab_rhs_value","int", "sqlite3_index_info*", "int", "**"] + ); + } + + if(wasm.bigIntEnabled && !!wasm.exports.sqlite3_preupdate_hook){ + bindingSignatures.int64.push( + ["sqlite3_preupdate_blobwrite", "int", "sqlite3*"], + ["sqlite3_preupdate_count", "int", "sqlite3*"], + ["sqlite3_preupdate_depth", "int", "sqlite3*"], + ["sqlite3_preupdate_hook", "*", [ + "sqlite3*", + new wasm.xWrap.FuncPtrAdapter({ + name: 'sqlite3_preupdate_hook', + signature: "v(ppippjj)", + contextKey: (argv)=>argv[0], + callProxy: (callback)=>{ + return (p,db,op,zDb,zTbl,iKey1,iKey2)=>{ + callback(p, db, op, wasm.cstrToJs(zDb), wasm.cstrToJs(zTbl), + iKey1, iKey2); + }; + } + }), + "*" + ]], + ["sqlite3_preupdate_new", "int", ["sqlite3*", "int", "**"]], + ["sqlite3_preupdate_old", "int", ["sqlite3*", "int", "**"]] + ); + } + + + if(wasm.bigIntEnabled + && !!wasm.exports.sqlite3changegroup_add + && !!wasm.exports.sqlite3session_create + && !!wasm.exports.sqlite3_preupdate_hook ){ + + const __ipsProxy = { + signature: 'i(ps)', + callProxy:(callback)=>{ + return (p,s)=>{ + try{return callback(p, wasm.cstrToJs(s)) | 0} + catch(e){return e.resultCode || capi.SQLITE_ERROR} + } + } + }; + + bindingSignatures.int64.push( + ['sqlite3changegroup_add', 'int', ['sqlite3_changegroup*', 'int', 'void*']], + ['sqlite3changegroup_add_strm', 'int', [ + 'sqlite3_changegroup*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3changegroup_delete', undefined, ['sqlite3_changegroup*']], + ['sqlite3changegroup_new', 'int', ['**']], + ['sqlite3changegroup_output', 'int', ['sqlite3_changegroup*', 'int*', '**']], + ['sqlite3changegroup_output_strm', 'int', [ + 'sqlite3_changegroup*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xOutput', signature: 'i(ppi)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3changeset_apply', 'int', [ + 'sqlite3*', 'int', 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xFilter', bindScope: 'transient', ...__ipsProxy + }), + new wasm.xWrap.FuncPtrAdapter({ + name: 'xConflict', signature: 'i(pip)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3changeset_apply_strm', 'int', [ + 'sqlite3*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xFilter', bindScope: 'transient', ...__ipsProxy + }), + new wasm.xWrap.FuncPtrAdapter({ + name: 'xConflict', signature: 'i(pip)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3changeset_apply_v2', 'int', [ + 'sqlite3*', 'int', 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xFilter', bindScope: 'transient', ...__ipsProxy + }), + new wasm.xWrap.FuncPtrAdapter({ + name: 'xConflict', signature: 'i(pip)', bindScope: 'transient' + }), + 'void*', '**', 'int*', 'int' + + ]], + ['sqlite3changeset_apply_v2_strm', 'int', [ + 'sqlite3*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xFilter', bindScope: 'transient', ...__ipsProxy + }), + new wasm.xWrap.FuncPtrAdapter({ + name: 'xConflict', signature: 'i(pip)', bindScope: 'transient' + }), + 'void*', '**', 'int*', 'int' + ]], + ['sqlite3changeset_apply_v3', 'int', [ + 'sqlite3*', 'int', 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xFilter', signature: 'i(pp)', bindScope: 'transient' + }), + new wasm.xWrap.FuncPtrAdapter({ + name: 'xConflict', signature: 'i(pip)', bindScope: 'transient' + }), + 'void*', '**', 'int*', 'int' + + ]], + ['sqlite3changeset_apply_v3_strm', 'int', [ + 'sqlite3*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xFilter', signature: 'i(pp)', bindScope: 'transient' + }), + new wasm.xWrap.FuncPtrAdapter({ + name: 'xConflict', signature: 'i(pip)', bindScope: 'transient' + }), + 'void*', '**', 'int*', 'int' + ]], + ['sqlite3changeset_concat', 'int', ['int','void*', 'int', 'void*', 'int*', '**']], + ['sqlite3changeset_concat_strm', 'int', [ + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInputA', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInputB', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xOutput', signature: 'i(ppi)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3changeset_conflict', 'int', ['sqlite3_changeset_iter*', 'int', '**']], + ['sqlite3changeset_finalize', 'int', ['sqlite3_changeset_iter*']], + ['sqlite3changeset_fk_conflicts', 'int', ['sqlite3_changeset_iter*', 'int*']], + ['sqlite3changeset_invert', 'int', ['int', 'void*', 'int*', '**']], + ['sqlite3changeset_invert_strm', 'int', [ + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xOutput', signature: 'i(ppi)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3changeset_new', 'int', ['sqlite3_changeset_iter*', 'int', '**']], + ['sqlite3changeset_next', 'int', ['sqlite3_changeset_iter*']], + ['sqlite3changeset_old', 'int', ['sqlite3_changeset_iter*', 'int', '**']], + ['sqlite3changeset_op', 'int', [ + 'sqlite3_changeset_iter*', '**', 'int*', 'int*','int*' + ]], + ['sqlite3changeset_pk', 'int', ['sqlite3_changeset_iter*', '**', 'int*']], + ['sqlite3changeset_start', 'int', ['**', 'int', '*']], + ['sqlite3changeset_start_strm', 'int', [ + '**', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3changeset_start_v2', 'int', ['**', 'int', '*', 'int']], + ['sqlite3changeset_start_v2_strm', 'int', [ + '**', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xInput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*', 'int' + ]], + ['sqlite3session_attach', 'int', ['sqlite3_session*', 'string']], + ['sqlite3session_changeset', 'int', ['sqlite3_session*', 'int*', '**']], + ['sqlite3session_changeset_size', 'i64', ['sqlite3_session*']], + ['sqlite3session_changeset_strm', 'int', [ + 'sqlite3_session*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xOutput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3session_config', 'int', ['int', 'void*']], + ['sqlite3session_create', 'int', ['sqlite3*', 'string', '**']], + + ['sqlite3session_diff', 'int', ['sqlite3_session*', 'string', 'string', '**']], + ['sqlite3session_enable', 'int', ['sqlite3_session*', 'int']], + ['sqlite3session_indirect', 'int', ['sqlite3_session*', 'int']], + ['sqlite3session_isempty', 'int', ['sqlite3_session*']], + ['sqlite3session_memory_used', 'i64', ['sqlite3_session*']], + ['sqlite3session_object_config', 'int', ['sqlite3_session*', 'int', 'void*']], + ['sqlite3session_patchset', 'int', ['sqlite3_session*', '*', '**']], + ['sqlite3session_patchset_strm', 'int', [ + 'sqlite3_session*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xOutput', signature: 'i(ppp)', bindScope: 'transient' + }), + 'void*' + ]], + ['sqlite3session_table_filter', undefined, [ + 'sqlite3_session*', + new wasm.xWrap.FuncPtrAdapter({ + name: 'xFilter', ...__ipsProxy, + contextKey: (argv,argIndex)=>argv[0] + }), + '*' + ]] + ); + } + + + sqlite3.StructBinder = globalThis.Jaccwabyt({ + heap: wasm.heap8u, + alloc: wasm.alloc, + dealloc: wasm.dealloc, + bigIntEnabled: wasm.bigIntEnabled, + pointerIR: wasm.ptr.ir, + memberPrefix: '$' + }); + delete globalThis.Jaccwabyt; + + { + + + const __xString = wasm.xWrap.argAdapter('string'); + wasm.xWrap.argAdapter( + 'string:flexible', (v)=>__xString(util.flexibleString(v)) + ); + + + wasm.xWrap.argAdapter( + 'string:static', + function(v){ + if(wasm.isPtr(v)) return v; + v = ''+v; + let rc = this[v]; + return rc || (this[v] = wasm.allocCString(v)); + }.bind(Object.create(null)) + ); + + + const __xArgPtr = wasm.xWrap.argAdapter('*'); + const nilType = function(){ + + }; + wasm.xWrap.argAdapter('sqlite3_filename', __xArgPtr) + ('sqlite3_backup*', __xArgPtr) + ('sqlite3_context*', __xArgPtr) + ('sqlite3_value*', __xArgPtr) + ('void*', __xArgPtr) + ('sqlite3_changegroup*', __xArgPtr) + ('sqlite3_changeset_iter*', __xArgPtr) + ('sqlite3_session*', __xArgPtr) + ('sqlite3_stmt*', (v)=> + __xArgPtr((v instanceof (sqlite3?.oo1?.Stmt || nilType)) + ? v.pointer : v)) + ('sqlite3*', (v)=> + __xArgPtr((v instanceof (sqlite3?.oo1?.DB || nilType)) + ? v.pointer : v)) + + ('sqlite3_vfs*', (v)=>{ + if('string'===typeof v){ + + return capi.sqlite3_vfs_find(v) + || sqlite3.SQLite3Error.toss( + capi.SQLITE_NOTFOUND, + "Unknown sqlite3_vfs name:", v + ); + } + return __xArgPtr((v instanceof (capi.sqlite3_vfs || nilType)) + ? v.pointer : v); + }); + if( wasm.exports.sqlite3_declare_vtab ){ + wasm.xWrap.argAdapter('sqlite3_index_info*', (v)=> + __xArgPtr((v instanceof (capi.sqlite3_index_info || nilType)) + ? v.pointer : v)) + ('sqlite3_module*', (v)=> + __xArgPtr((v instanceof (capi.sqlite3_module || nilType)) + ? v.pointer : v) + ); + } + + + const __xRcPtr = wasm.xWrap.resultAdapter('*'); + wasm.xWrap.resultAdapter('sqlite3*', __xRcPtr) + ('sqlite3_backup*', __xRcPtr) + ('sqlite3_context*', __xRcPtr) + ('sqlite3_stmt*', __xRcPtr) + ('sqlite3_value*', __xRcPtr) + ('sqlite3_vfs*', __xRcPtr) + ('void*', __xRcPtr); + + + for(const e of bindingSignatures.core){ + capi[e[0]] = wasm.xWrap.apply(null, e); + } + for(const e of bindingSignatures.wasmInternal){ + util[e[0]] = wasm.xWrap.apply(null, e); + } + + + for(const e of bindingSignatures.int64){ + capi[e[0]] = wasm.bigIntEnabled + ? wasm.xWrap.apply(null, e) + : ()=>toss(e[0]+"() is unavailable due to lack", + "of BigInt support in this build."); + } + + + delete bindingSignatures.core; + delete bindingSignatures.int64; + delete bindingSignatures.wasmInternal; + + + util.sqlite3__wasm_db_error = function(pDb, resultCode, message){ + if( !pDb ) return capi.SQLITE_MISUSE; + if(resultCode instanceof sqlite3.WasmAllocError){ + resultCode = capi.SQLITE_NOMEM; + message = 0 ; + }else if(resultCode instanceof Error){ + message = message || ''+resultCode; + resultCode = (resultCode.resultCode || capi.SQLITE_ERROR); + } + return capi.sqlite3_set_errmsg(pDb, resultCode, message) || resultCode; + }; + } + + { + 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.cstrToJs(cJson)); + + const defineGroups = ['access', 'authorizer', + 'blobFinalizers', 'changeset', + 'config', 'dataTypes', + 'dbConfig', 'dbStatus', + 'encodings', 'fcntl', 'flock', 'ioCap', + 'limits', 'openFlags', + 'prepareFlags', 'resultCodes', + 'sqlite3Status', + 'stmtStatus', 'syncFlags', + 'trace', 'txnState', 'udfFlags', + 'version']; + if(wasm.bigIntEnabled){ + defineGroups.push('serialize', 'session', 'vtab'); + } + for(const t of defineGroups){ + for(const e of Object.entries(wasm.ctype[t])){ + + + capi[e[0]] = e[1]; + } + } + if(!wasm.functionEntry(capi.SQLITE_WASM_DEALLOC)){ + toss("Internal error: cannot resolve exported function", + "entry SQLITE_WASM_DEALLOC (=="+capi.SQLITE_WASM_DEALLOC+")."); + } + const __rcMap = Object.create(null); + for(const e of Object.entries(wasm.ctype['resultCodes'])){ + __rcMap[e[1]] = e[0]; + } + + capi.sqlite3_js_rc_str = (rc)=>__rcMap[rc]; + + + const notThese = Object.assign(Object.create(null),{ + + WasmTestStruct: true, + + sqlite3_index_info: !wasm.bigIntEnabled, + sqlite3_index_constraint: !wasm.bigIntEnabled, + sqlite3_index_orderby: !wasm.bigIntEnabled, + sqlite3_index_constraint_usage: !wasm.bigIntEnabled + }); + for(const s of wasm.ctype.structs){ + if(!notThese[s.name]){ + capi[s.name] = sqlite3.StructBinder(s); + } + } + if(capi.sqlite3_index_info){ + + for(const k of ['sqlite3_index_constraint', + 'sqlite3_index_orderby', + 'sqlite3_index_constraint_usage']){ + capi.sqlite3_index_info[k] = capi[k]; + delete capi[k]; + } + capi.sqlite3_vtab_config = wasm.xWrap( + 'sqlite3__wasm_vtab_config','int',[ + 'sqlite3*', 'int', 'int'] + ); + } + } + + + const __dbArgcMismatch = (pDb,f,n)=>{ + return util.sqlite3__wasm_db_error(pDb, capi.SQLITE_MISUSE, + f+"() requires "+n+" argument"+ + (1===n?"":'s')+"."); + }; + + + const __errEncoding = (pDb)=>{ + return util.sqlite3__wasm_db_error( + pDb, capi.SQLITE_FORMAT, "SQLITE_UTF8 is the only supported encoding." + ); + }; + + + const __argPDb = (pDb)=>wasm.xWrap.argAdapter('sqlite3*')(pDb); + const __argStr = (str)=>wasm.isPtr(str) ? wasm.cstrToJs(str) : str; + const __dbCleanupMap = function( + pDb, mode + ){ + pDb = __argPDb(pDb); + let m = this.dbMap.get(pDb); + if(!mode){ + this.dbMap.delete(pDb); + return m; + }else if(!m && mode>0){ + this.dbMap.set(pDb, (m = Object.create(null))); + } + return m; + }.bind(Object.assign(Object.create(null),{ + dbMap: new Map + })); + + __dbCleanupMap.addCollation = function(pDb, name){ + const m = __dbCleanupMap(pDb, 1); + if(!m.collation) m.collation = new Set; + m.collation.add(__argStr(name).toLowerCase()); + }; + + __dbCleanupMap._addUDF = function(pDb, name, arity, map){ + + name = __argStr(name).toLowerCase(); + let u = map.get(name); + if(!u) map.set(name, (u = new Set)); + u.add((arity<0) ? -1 : arity); + }; + + __dbCleanupMap.addFunction = function(pDb, name, arity){ + const m = __dbCleanupMap(pDb, 1); + if(!m.udf) m.udf = new Map; + this._addUDF(pDb, name, arity, m.udf); + }; + + if( wasm.exports.sqlite3_create_window_function ){ + __dbCleanupMap.addWindowFunc = function(pDb, name, arity){ + const m = __dbCleanupMap(pDb, 1); + if(!m.wudf) m.wudf = new Map; + this._addUDF(pDb, name, arity, m.wudf); + }; + } + + + __dbCleanupMap.cleanup = function(pDb){ + pDb = __argPDb(pDb); + + + for(const obj of [ + + ['sqlite3_busy_handler',3], + ['sqlite3_commit_hook',3], + ['sqlite3_preupdate_hook',3], + ['sqlite3_progress_handler',4], + ['sqlite3_rollback_hook',3], + ['sqlite3_set_authorizer',3], + ['sqlite3_trace_v2', 4], + ['sqlite3_update_hook',3] + + ]){ + const [name, arity] = obj; + const x = wasm.exports[name]; + if( !x ){ + + continue; + } + const closeArgs = [pDb]; + closeArgs.length = arity + ; + + try{ capi[name](...closeArgs) } + catch(e){ + + sqlite3.config.warn("close-time call of",name+"(",closeArgs,") threw:",e); + } + + } + const m = __dbCleanupMap(pDb, 0); + if(!m) return; + if(m.collation){ + for(const name of m.collation){ + try{ + capi.sqlite3_create_collation_v2( + pDb, name, capi.SQLITE_UTF8, 0, 0, 0 + ); + }catch(e){ + + } + } + delete m.collation; + } + let i; + for(i = 0; i < 2; ++i){ + const fmap = i ? m.wudf : m.udf; + if(!fmap) continue; + const func = i + ? capi.sqlite3_create_window_function + : capi.sqlite3_create_function_v2; + for(const e of fmap){ + const name = e[0], arities = e[1]; + const fargs = [pDb, name, 0, capi.SQLITE_UTF8, 0, 0, 0, 0, 0]; + if(i) fargs.push(0); + for(const arity of arities){ + try{ fargs[2] = arity; func.apply(null, fargs); } + catch(e){} + } + arities.clear(); + } + fmap.clear(); + } + delete m.udf; + delete m.wudf; + }; + + { + const __sqlite3CloseV2 = wasm.xWrap("sqlite3_close_v2", "int", "sqlite3*"); + capi.sqlite3_close_v2 = function(pDb){ + if(1!==arguments.length) return __dbArgcMismatch(pDb, 'sqlite3_close_v2', 1); + if(pDb){ + try{__dbCleanupMap.cleanup(pDb)} catch(e){} + } + return __sqlite3CloseV2(pDb); + }; + } + + if(capi.sqlite3session_create){ + const __sqlite3SessionDelete = wasm.xWrap( + 'sqlite3session_delete', undefined, ['sqlite3_session*'] + ); + capi.sqlite3session_delete = function(pSession){ + if(1!==arguments.length){ + return __dbArgcMismatch(pDb, 'sqlite3session_delete', 1); + + } + else if(pSession){ + + capi.sqlite3session_table_filter(pSession, 0, 0); + } + __sqlite3SessionDelete(pSession); + }; + } + + { + + const contextKey = (argv,argIndex)=>{ + return 'argv['+argIndex+']:'+argv[0]+ + ':'+wasm.cstrToJs(argv[1]).toLowerCase() + }; + const __sqlite3CreateCollationV2 = wasm.xWrap( + 'sqlite3_create_collation_v2', 'int', [ + 'sqlite3*', 'string', 'int', '*', + new wasm.xWrap.FuncPtrAdapter({ + + name: 'xCompare', signature: 'i(pipip)', contextKey + }), + new wasm.xWrap.FuncPtrAdapter({ + + name: 'xDestroy', signature: 'v(p)', contextKey + }) + ] + ); + + + capi.sqlite3_create_collation_v2 = function(pDb,zName,eTextRep,pArg,xCompare,xDestroy){ + if(6!==arguments.length) return __dbArgcMismatch(pDb, 'sqlite3_create_collation_v2', 6); + else if( 0 === (eTextRep & 0xf) ){ + eTextRep |= capi.SQLITE_UTF8; + }else if( capi.SQLITE_UTF8 !== (eTextRep & 0xf) ){ + return __errEncoding(pDb); + } + try{ + const rc = __sqlite3CreateCollationV2(pDb, zName, eTextRep, pArg, xCompare, xDestroy); + if(0===rc && xCompare instanceof Function){ + __dbCleanupMap.addCollation(pDb, zName); + } + return rc; + }catch(e){ + return util.sqlite3__wasm_db_error(pDb, e); + } + }; + + capi.sqlite3_create_collation = (pDb,zName,eTextRep,pArg,xCompare)=>{ + return (5===arguments.length) + ? capi.sqlite3_create_collation_v2(pDb,zName,eTextRep,pArg,xCompare,0) + : __dbArgcMismatch(pDb, 'sqlite3_create_collation', 5); + }; + + } + + { + + const contextKey = function(argv,argIndex){ + return ( + argv[0] + +':'+(argv[2] < 0 ? -1 : argv[2]) + +':'+argIndex + +':'+wasm.cstrToJs(argv[1]).toLowerCase() + ) + }; + + + const __cfProxy = Object.assign(Object.create(null), { + xInverseAndStep: { + signature:'v(pip)', contextKey, + callProxy: (callback)=>{ + return (pCtx, argc, pArgv)=>{ + try{ callback(pCtx, ...capi.sqlite3_values_to_js(argc, pArgv)) } + catch(e){ capi.sqlite3_result_error_js(pCtx, e) } + }; + } + }, + xFinalAndValue: { + signature:'v(p)', contextKey, + callProxy: (callback)=>{ + return (pCtx)=>{ + try{ capi.sqlite3_result_js(pCtx, callback(pCtx)) } + catch(e){ capi.sqlite3_result_error_js(pCtx, e) } + }; + } + }, + xFunc: { + signature:'v(pip)', contextKey, + callProxy: (callback)=>{ + return (pCtx, argc, pArgv)=>{ + try{ + capi.sqlite3_result_js( + pCtx, + callback(pCtx, ...capi.sqlite3_values_to_js(argc, pArgv)) + ); + }catch(e){ + + capi.sqlite3_result_error_js(pCtx, e); + } + }; + } + }, + xDestroy: { + signature:'v(p)', contextKey, + + callProxy: (callback)=>{ + return (pVoid)=>{ + try{ callback(pVoid) } + catch(e){ console.error("UDF xDestroy method threw:",e) } + }; + } + } + }); + + const __sqlite3CreateFunction = wasm.xWrap( + "sqlite3_create_function_v2", "int", [ + "sqlite3*", "string", "int", + "int", "*", + new wasm.xWrap.FuncPtrAdapter({name: 'xFunc', ...__cfProxy.xFunc}), + new wasm.xWrap.FuncPtrAdapter({name: 'xStep', ...__cfProxy.xInverseAndStep}), + new wasm.xWrap.FuncPtrAdapter({name: 'xFinal', ...__cfProxy.xFinalAndValue}), + new wasm.xWrap.FuncPtrAdapter({name: 'xDestroy', ...__cfProxy.xDestroy}) + ] + ); + + const __sqlite3CreateWindowFunction = + wasm.exports.sqlite3_create_window_function + ? wasm.xWrap( + "sqlite3_create_window_function", "int", [ + "sqlite3*", "string", "int", + "int", "*", + new wasm.xWrap.FuncPtrAdapter({name: 'xStep', ...__cfProxy.xInverseAndStep}), + new wasm.xWrap.FuncPtrAdapter({name: 'xFinal', ...__cfProxy.xFinalAndValue}), + new wasm.xWrap.FuncPtrAdapter({name: 'xValue', ...__cfProxy.xFinalAndValue}), + new wasm.xWrap.FuncPtrAdapter({name: 'xInverse', ...__cfProxy.xInverseAndStep}), + new wasm.xWrap.FuncPtrAdapter({name: 'xDestroy', ...__cfProxy.xDestroy}) + ] + ) + : undefined; + + + capi.sqlite3_create_function_v2 = function f( + pDb, funcName, nArg, eTextRep, pApp, + xFunc, + xStep, + xFinal, + xDestroy + ){ + if( f.length!==arguments.length ){ + return __dbArgcMismatch(pDb,"sqlite3_create_function_v2",f.length); + }else if( 0 === (eTextRep & 0xf) ){ + eTextRep |= capi.SQLITE_UTF8; + }else if( capi.SQLITE_UTF8 !== (eTextRep & 0xf) ){ + return __errEncoding(pDb); + } + try{ + const rc = __sqlite3CreateFunction(pDb, funcName, nArg, eTextRep, + pApp, xFunc, xStep, xFinal, xDestroy); + if(0===rc && (xFunc instanceof Function + || xStep instanceof Function + || xFinal instanceof Function + || xDestroy instanceof Function)){ + __dbCleanupMap.addFunction(pDb, funcName, nArg); + } + return rc; + }catch(e){ + console.error("sqlite3_create_function_v2() setup threw:",e); + return util.sqlite3__wasm_db_error(pDb, e, "Creation of UDF threw: "+e); + } + }; + + + 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); + }; + + + if( __sqlite3CreateWindowFunction ){ + capi.sqlite3_create_window_function = function f( + pDb, funcName, nArg, eTextRep, pApp, + xStep, + xFinal, + xValue, + xInverse, + xDestroy + ){ + if( f.length!==arguments.length ){ + return __dbArgcMismatch(pDb,"sqlite3_create_window_function",f.length); + }else if( 0 === (eTextRep & 0xf) ){ + eTextRep |= capi.SQLITE_UTF8; + }else if( capi.SQLITE_UTF8 !== (eTextRep & 0xf) ){ + return __errEncoding(pDb); + } + try{ + const rc = __sqlite3CreateWindowFunction(pDb, funcName, nArg, eTextRep, + pApp, xStep, xFinal, xValue, + xInverse, xDestroy); + if(0===rc && (xStep instanceof Function + || xFinal instanceof Function + || xValue instanceof Function + || xInverse instanceof Function + || xDestroy instanceof Function)){ + __dbCleanupMap.addWindowFunc(pDb, funcName, nArg); + } + return rc; + }catch(e){ + console.error("sqlite3_create_window_function() setup threw:",e); + return util.sqlite3__wasm_db_error(pDb, e, "Creation of UDF threw: "+e); + } + }; + }else{ + delete capi.sqlite3_create_window_function; + } + + capi.sqlite3_create_function_v2.udfSetResult = + capi.sqlite3_create_function.udfSetResult = capi.sqlite3_result_js; + if(capi.sqlite3_create_window_function){ + capi.sqlite3_create_window_function.udfSetResult = capi.sqlite3_result_js; + } + + + capi.sqlite3_create_function_v2.udfConvertArgs = + capi.sqlite3_create_function.udfConvertArgs = capi.sqlite3_values_to_js; + if(capi.sqlite3_create_window_function){ + capi.sqlite3_create_window_function.udfConvertArgs = capi.sqlite3_values_to_js; + } + + + capi.sqlite3_create_function_v2.udfSetError = + capi.sqlite3_create_function.udfSetError = capi.sqlite3_result_error_js; + if(capi.sqlite3_create_window_function){ + capi.sqlite3_create_window_function.udfSetError = capi.sqlite3_result_error_js; + } + + }; + + { + + + const __flexiString = (v,n)=>{ + if('string'===typeof v){ + n = -1; + }else if(util.isSQLableTypedArray(v)){ + n = v.byteLength; + v = wasm.typedArrayToString( + (v instanceof ArrayBuffer) ? new Uint8Array(v) : v + ); + }else if(Array.isArray(v)){ + v = v.join(""); + n = -1; + } + return [v, n]; + }; + + + const __prepare = { + + basic: wasm.xWrap('sqlite3_prepare_v3', + "int", ["sqlite3*", "string", + "int", + "int", "**", + "**"]), + + full: wasm.xWrap('sqlite3_prepare_v3', + "int", ["sqlite3*", "*", "int", "int", + "**", "**"]) + }; + + + 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, Number(sqlLen)); + switch(typeof xSql){ + case 'string': return __prepare.basic(pDb, xSql, xSqlLen, prepFlags, ppStmt, null); + case (typeof wasm.ptr.null): + return __prepare.full(pDb, wasm.ptr.coerce(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(). typeof="+(typeof xSql) + ); + } + }; + + + 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); + }; + + } + + { + const __bindText = wasm.xWrap("sqlite3_bind_text", "int", [ + "sqlite3_stmt*", "int", "string", "int", "*" + ]); + const __bindBlob = wasm.xWrap("sqlite3_bind_blob", "int", [ + "sqlite3_stmt*", "int", "*", "int", "*" + ]); + + + capi.sqlite3_bind_text = function f(pStmt, iCol, text, nText, xDestroy){ + if(f.length!==arguments.length){ + return __dbArgcMismatch(capi.sqlite3_db_handle(pStmt), + "sqlite3_bind_text", f.length); + }else if(wasm.isPtr(text) || null===text){ + return __bindText(pStmt, iCol, text, nText, xDestroy); + }else if(text instanceof ArrayBuffer){ + text = new Uint8Array(text); + }else if(Array.isArray(pMem)){ + text = pMem.join(''); + } + let p, n; + try{ + if(util.isSQLableTypedArray(text)){ + p = wasm.allocFromTypedArray(text); + n = text.byteLength; + }else if('string'===typeof text){ + [p, n] = wasm.allocCString(text); + }else{ + return util.sqlite3__wasm_db_error( + capi.sqlite3_db_handle(pStmt), capi.SQLITE_MISUSE, + "Invalid 3rd argument type for sqlite3_bind_text()." + ); + } + return __bindText(pStmt, iCol, p, n, capi.SQLITE_WASM_DEALLOC); + }catch(e){ + wasm.dealloc(p); + return util.sqlite3__wasm_db_error( + capi.sqlite3_db_handle(pStmt), e + ); + } + }; + + + capi.sqlite3_bind_blob = function f(pStmt, iCol, pMem, nMem, xDestroy){ + if(f.length!==arguments.length){ + return __dbArgcMismatch(capi.sqlite3_db_handle(pStmt), + "sqlite3_bind_blob", f.length); + }else if(wasm.isPtr(pMem) || null===pMem){ + return __bindBlob(pStmt, iCol, pMem, nMem, xDestroy); + }else if(pMem instanceof ArrayBuffer){ + pMem = new Uint8Array(pMem); + }else if(Array.isArray(pMem)){ + pMem = pMem.join(''); + } + let p, n; + try{ + if(util.isBindableTypedArray(pMem)){ + p = wasm.allocFromTypedArray(pMem); + n = nMem>=0 ? nMem : pMem.byteLength; + }else if('string'===typeof pMem){ + [p, n] = wasm.allocCString(pMem); + }else{ + return util.sqlite3__wasm_db_error( + capi.sqlite3_db_handle(pStmt), capi.SQLITE_MISUSE, + "Invalid 3rd argument type for sqlite3_bind_blob()." + ); + } + return __bindBlob(pStmt, iCol, p, n, capi.SQLITE_WASM_DEALLOC); + }catch(e){ + wasm.dealloc(p); + return util.sqlite3__wasm_db_error( + capi.sqlite3_db_handle(pStmt), e + ); + } + }; + + } + + if(!capi.sqlite3_column_text){ + + const argStmt = wasm.xWrap.argAdapter('sqlite3_stmt*'), + argInt = wasm.xWrap.argAdapter('int'), + argValue = wasm.xWrap.argAdapter('sqlite3_value*'), + newStr = + (cstr,n)=>wasm.typedArrayToString(wasm.heap8u(), + Number(cstr), Number(cstr)+n) + capi.sqlite3_column_text = function(stmt, colIndex){ + const a0 = argStmt(stmt), a1 = argInt(colIndex); + const cstr = wasm.exports.sqlite3_column_text(a0, a1); + return cstr + ? newStr(cstr,wasm.exports.sqlite3_column_bytes(a0, a1)) + : null; + }; + capi.sqlite3_value_text = function(val){ + const a0 = argValue(val); + const cstr = wasm.exports.sqlite3_value_text(a0); + return cstr + ? newStr(cstr,wasm.exports.sqlite3_value_bytes(a0)) + : null; + }; + } + + { + + capi.sqlite3_config = function(op, ...args){ + if(arguments.length<2) return capi.SQLITE_MISUSE; + switch(op){ + case capi.SQLITE_CONFIG_COVERING_INDEX_SCAN: + case capi.SQLITE_CONFIG_MEMSTATUS: + case capi.SQLITE_CONFIG_SMALL_MALLOC: + case capi.SQLITE_CONFIG_SORTERREF_SIZE: + case capi.SQLITE_CONFIG_STMTJRNL_SPILL: + case capi.SQLITE_CONFIG_URI: + return wasm.exports.sqlite3__wasm_config_i(op, args[0]); + case capi.SQLITE_CONFIG_LOOKASIDE: + return wasm.exports.sqlite3__wasm_config_ii(op, args[0], args[1]); + case capi.SQLITE_CONFIG_MEMDB_MAXSIZE: + return wasm.exports.sqlite3__wasm_config_j(op, args[0]); + case capi.SQLITE_CONFIG_GETMALLOC: + case capi.SQLITE_CONFIG_GETMUTEX: + case capi.SQLITE_CONFIG_GETPCACHE2: + case capi.SQLITE_CONFIG_GETPCACHE: + case capi.SQLITE_CONFIG_HEAP: + case capi.SQLITE_CONFIG_LOG: + case capi.SQLITE_CONFIG_MALLOC: + case capi.SQLITE_CONFIG_MMAP_SIZE: + case capi.SQLITE_CONFIG_MULTITHREAD: + case capi.SQLITE_CONFIG_MUTEX: + case capi.SQLITE_CONFIG_PAGECACHE: + case capi.SQLITE_CONFIG_PCACHE2: + case capi.SQLITE_CONFIG_PCACHE: + case capi.SQLITE_CONFIG_PCACHE_HDRSZ: + case capi.SQLITE_CONFIG_PMASZ: + case capi.SQLITE_CONFIG_SERIALIZED: + case capi.SQLITE_CONFIG_SINGLETHREAD: + case capi.SQLITE_CONFIG_SQLLOG: + case capi.SQLITE_CONFIG_WIN32_HEAPSIZE: + default: + + return capi.SQLITE_NOTFOUND; + } + }; + } + + { + const __autoExtFptr = new Set; + + capi.sqlite3_auto_extension = function(fPtr){ + if( fPtr instanceof Function ){ + fPtr = wasm.installFunction('i(ppp)', fPtr); + }else if( 1!==arguments.length || !wasm.isPtr(fPtr) ){ + return capi.SQLITE_MISUSE; + } + const rc = wasm.exports.sqlite3_auto_extension(fPtr); + if( fPtr!==arguments[0] ){ + if(0===rc) __autoExtFptr.add(fPtr); + else wasm.uninstallFunction(fPtr); + } + return rc; + }; + + capi.sqlite3_cancel_auto_extension = function(fPtr){ + ; + if(!fPtr || 1!==arguments.length || !wasm.isPtr(fPtr)) return 0; + return wasm.exports.sqlite3_cancel_auto_extension(fPtr); + + }; + + capi.sqlite3_reset_auto_extension = function(){ + wasm.exports.sqlite3_reset_auto_extension(); + for(const fp of __autoExtFptr) wasm.uninstallFunction(fp); + __autoExtFptr.clear(); + }; + } + + + wasm.xWrap.FuncPtrAdapter.warnOnUse = true; + + const StructBinder = sqlite3.StructBinder + ; + + const installMethod = function callee( + tgt, name, func, applyArgcCheck = callee.installMethodArgcCheck + ){ + if(!(tgt instanceof StructBinder.StructType)){ + toss("Usage error: target object is-not-a StructType."); + }else if(!(func instanceof Function) && !wasm.isPtr(func)){ + toss("Usage error: expecting a Function or WASM pointer to one."); + } + if(1===arguments.length){ + return (n,f)=>callee(tgt, n, f, applyArgcCheck); + } + if(!callee.argcProxy){ + callee.argcProxy = function(tgt, funcName, func,sig){ + return function(...args){ + if(func.length!==arguments.length){ + toss("Argument mismatch for", + tgt.structInfo.name+"::"+funcName + +": Native signature is:",sig); + } + return func.apply(this, args); + } + }; + + callee.removeFuncList = function(){ + if(this.ondispose.__removeFuncList){ + this.ondispose.__removeFuncList.forEach( + (v,ndx)=>{ + if(wasm.isPtr(v)){ + try{wasm.uninstallFunction(v)} + catch(e){} + } + + } + ); + delete this.ondispose.__removeFuncList; + } + }; + } + const sigN = tgt.memberSignature(name); + if(sigN.length<2){ + toss("Member",name,"does not have a function pointer signature:",sigN); + } + const memKey = tgt.memberKey(name); + const fProxy = (applyArgcCheck && !wasm.isPtr(func)) + + ? callee.argcProxy(tgt, memKey, func, sigN) + : func; + if(wasm.isPtr(fProxy)){ + if(fProxy && !wasm.functionEntry(fProxy)){ + toss("Pointer",fProxy,"is not a WASM function table entry."); + } + tgt[memKey] = fProxy; + }else{ + const pFunc = wasm.installFunction(fProxy, sigN); + tgt[memKey] = pFunc; + if(!tgt.ondispose || !tgt.ondispose.__removeFuncList){ + tgt.addOnDispose('ondispose.__removeFuncList handler', + callee.removeFuncList); + tgt.ondispose.__removeFuncList = []; + } + tgt.ondispose.__removeFuncList.push(memKey, pFunc); + } + return (n,f)=>callee(tgt, n, f, applyArgcCheck); + }; + installMethod.installMethodArgcCheck = false; + + + const installMethods = function( + structInstance, methods, applyArgcCheck = installMethod.installMethodArgcCheck + ){ + const seen = new Map ; + for(const k of Object.keys(methods)){ + const m = methods[k]; + const prior = seen.get(m); + if(prior){ + const mkey = structInstance.memberKey(k); + structInstance[mkey] = structInstance[structInstance.memberKey(prior)]; + }else{ + installMethod(structInstance, k, m, applyArgcCheck); + seen.set(m, k); + } + } + return structInstance; + }; + + + StructBinder.StructType.prototype.installMethod = function callee( + name, func, applyArgcCheck = installMethod.installMethodArgcCheck + ){ + return (arguments.length < 3 && name && 'object'===typeof name) + ? installMethods(this, ...arguments) + : installMethod(this, ...arguments); + }; + + + StructBinder.StructType.prototype.installMethods = function( + methods, applyArgcCheck = installMethod.installMethodArgcCheck + ){ + return installMethods(this, methods, applyArgcCheck); + }; + +}); + +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + const toss3 = (...args)=>{throw new sqlite3.SQLite3Error(...args)}; + + const capi = sqlite3.capi, wasm = sqlite3.wasm, util = sqlite3.util; + + + const outWrapper = function(f){ + return (...args)=>f("sqlite3.oo1:",...args); + }; + + const debug = sqlite3.__isUnderTest + ? outWrapper(console.debug.bind(console)) + : outWrapper(sqlite3.config.debug); + const warn = sqlite3.__isUnderTest + ? outWrapper(console.warn.bind(console)) + : outWrapper(sqlite3.config.warn); + const error = sqlite3.__isUnderTest + ? outWrapper(console.error.bind(console)) + : outWrapper(sqlite3.config.error); + + + const __ptrMap = new WeakMap(); + + const __doesNotOwnHandle = new Set(); + + const __stmtMap = new WeakMap(); + + + const getOwnOption = (opts, p, dflt)=>{ + const d = Object.getOwnPropertyDescriptor(opts,p); + return d ? d.value : dflt; + }; + + + const checkSqlite3Rc = function(dbPtr, sqliteResultCode){ + if(sqliteResultCode){ + if(dbPtr instanceof DB) dbPtr = dbPtr.pointer; + toss3( + sqliteResultCode, + "sqlite3 result code",sqliteResultCode+":", + (dbPtr + ? capi.sqlite3_errmsg(dbPtr) + : capi.sqlite3_errstr(sqliteResultCode)) + ); + } + return arguments[0]; + }; + + + const __dbTraceToConsole = + wasm.installFunction('i(ippp)', function(t,c,p,x){ + if(capi.SQLITE_TRACE_STMT===t){ + + console.log("SQL TRACE #"+(++this.counter), + 'via sqlite3@'+c+'['+capi.sqlite3_db_filename(c,null)+']', + wasm.cstrToJs(x)); + } + }.bind({counter: 0})); + + + const __vfsPostOpenCallback = Object.create(null); + + + const byteArrayToHex = function(ba){ + if( ba instanceof ArrayBuffer ){ + ba = new Uint8Array(ba); + } + const li = []; + const digits = "0123456789abcdef"; + for( const d of ba ){ + li.push( digits[(d & 0xf0) >> 4], digits[d & 0x0f] ); + } + return li.join(''); + }; + + + const dbCtorApplySEEKey = function(db,opt){ + if( !capi.sqlite3_key_v2 ) return; + let keytype; + let key; + const check = (opt.key ? 1 : 0) + (opt.hexkey ? 1 : 0) + (opt.textkey ? 1 : 0); + if( !check ) return; + else if( check>1 ){ + toss3(capi.SQLITE_MISUSE, + "Only ONE of (key, hexkey, textkey) may be provided."); + } + if( opt.key ){ + + keytype = 'key'; + key = opt.key; + if('string'===typeof key){ + key = new TextEncoder('utf-8').encode(key); + } + if((key instanceof ArrayBuffer) || (key instanceof Uint8Array)){ + key = byteArrayToHex(key); + keytype = 'hexkey'; + }else{ + toss3(capi.SQLITE_MISUSE, + "Invalid value for the 'key' option. Expecting a string,", + "ArrayBuffer, or Uint8Array."); + return; + } + }else if( opt.textkey ){ + + keytype = 'textkey'; + key = opt.textkey; + if(key instanceof ArrayBuffer){ + key = new Uint8Array(key); + } + if(key instanceof Uint8Array){ + key = new TextDecoder('utf-8').decode(key); + }else if('string'!==typeof key){ + toss3(capi.SQLITE_MISUSE, + "Invalid value for the 'textkey' option. Expecting a string,", + "ArrayBuffer, or Uint8Array."); + } + }else if( opt.hexkey ){ + keytype = 'hexkey'; + key = opt.hexkey; + if((key instanceof ArrayBuffer) || (key instanceof Uint8Array)){ + key = byteArrayToHex(key); + }else if('string'!==typeof key){ + toss3(capi.SQLITE_MISUSE, + "Invalid value for the 'hexkey' option. Expecting a string,", + "ArrayBuffer, or Uint8Array."); + } + + }else{ + return; + } + let stmt; + try{ + stmt = db.prepare("PRAGMA "+keytype+"="+util.sqlite3__wasm_qfmt_token(key, 1)); + stmt.step(); + return true; + }finally{ + if(stmt) stmt.finalize(); + } + }; + + + const dbCtorHelper = function ctor(...args){ + const opt = ctor.normalizeArgs(...args); + + let pDb; + if( (pDb = opt['sqlite3*']) ){ + + + if( !opt['sqlite3*:takeOwnership'] ){ + + __doesNotOwnHandle.add(this); + } + this.filename = capi.sqlite3_db_filename(pDb,'main'); + }else{ + let fn = opt.filename, vfsName = opt.vfs, flagsStr = opt.flags; + if( ('string'!==typeof fn && !wasm.isPtr(fn)) + || 'string'!==typeof flagsStr + || (vfsName && ('string'!==typeof vfsName && !wasm.isPtr(vfsName))) ){ + sqlite3.config.error("Invalid DB ctor args",opt,arguments); + toss3("Invalid arguments for DB constructor:", arguments, "opts:", opt); + } + let 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() ; + let rc = capi.sqlite3_open_v2(fn, pPtr, oflags, vfsName || wasm.ptr.null); + pDb = wasm.peekPtr(pPtr); + checkSqlite3Rc(pDb, rc); + capi.sqlite3_extended_result_codes(pDb, 1); + if(flagsStr.indexOf('t')>=0){ + capi.sqlite3_trace_v2(pDb, capi.SQLITE_TRACE_STMT, + __dbTraceToConsole, pDb); + } + }catch( e ){ + if( pDb ) capi.sqlite3_close_v2(pDb); + throw e; + }finally{ + wasm.pstack.restore(stack); + } + this.filename = + wasm.isPtr(fn) ? wasm.cstrToJs(fn) : fn; + + } + __ptrMap.set(this, pDb); + __stmtMap.set(this, Object.create(null)); + if( !opt['sqlite3*'] ){ + try{ + dbCtorApplySEEKey(this,opt); + + const pVfs = capi.sqlite3_js_db_vfs(pDb) + || toss3("Internal error: cannot get VFS for new db handle."); + const postInitSql = __vfsPostOpenCallback[pVfs]; + if(postInitSql){ + + if(postInitSql instanceof Function){ + postInitSql(this, sqlite3); + }else{ + checkSqlite3Rc( + pDb, capi.sqlite3_exec(pDb, postInitSql, 0, 0, 0) + ); + } + } + }catch(e){ + this.close(); + throw e; + } + } + }; + + + dbCtorHelper.setVfsPostOpenCallback = function(pVfs, callback){ + if( !(callback instanceof Function)){ + toss3("dbCtorHelper.setVfsPostOpenCallback() should not be used with "+ + "a non-function argument.",arguments); + } + __vfsPostOpenCallback[pVfs] = callback; + }; + + + dbCtorHelper.normalizeArgs = function(filename=':memory:',flags = 'c',vfs = null){ + const arg = {}; + if(1===arguments.length && arguments[0] && 'object'===typeof arguments[0]){ + Object.assign(arg, arguments[0]); + 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; + }; + + const DB = function(...args){ + dbCtorHelper.apply(this, args); + }; + DB.dbCtorHelper = dbCtorHelper; + + + const BindTypes = { + null: 1, + number: 2, + string: 3, + boolean: 4, + blob: 5 + }; + if(wasm.bigIntEnabled){ + BindTypes.bigint = BindTypes.number; + } + + + const Stmt = function(){ + if(BindTypes!==arguments[2]){ + toss3(capi.SQLITE_MISUSE, "Do not call the Stmt constructor directly. Use DB.prepare()."); + } + this.db = arguments[0]; + __ptrMap.set(this, arguments[1]); + if( arguments.length>3 && !arguments[3] ){ + __doesNotOwnHandle.add(this); + } + }; + + + const affirmDbOpen = function(db){ + if(!db.pointer) toss3("DB has been closed."); + return db; + }; + + + const affirmColIndex = function(stmt,ndx){ + if((ndx !== (ndx|0)) || ndx<0 || ndx>=stmt.columnCount){ + toss3("Column index",ndx,"is out of range."); + } + return stmt; + }; + + + 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.returnValue && undefined!==opt.rowMode){ + if(!opt.resultRows) opt.resultRows = []; + out.returnVal = ()=>opt.resultRows; + } + if(opt.callback || opt.resultRows){ + switch((undefined===opt.rowMode) ? 'array' : opt.rowMode) { + case 'object': + out.cbArg = (stmt,cache)=>{ + if( !cache.columnNames ) cache.columnNames = stmt.getColumnNames([]); + + const row = stmt.get([]); + const rv = Object.create(null); + for( const i in cache.columnNames ) rv[cache.columnNames[i]] = row[i]; + return rv; + }; + 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 + && '$'===opt.rowMode[0]){ + + const $colName = opt.rowMode.substr(1); + out.cbArg = (stmt)=>{ + const rc = stmt.get(Object.create(null))[$colName]; + return (undefined===rc) + ? toss3(capi.SQLITE_NOTFOUND, + "exec(): unknown result column:",$colName) + : rc; + }; + break; + } + toss3("Invalid rowMode:",opt.rowMode); + } + } + return out; + }; + + + const __selectFirstRow = (db, sql, bind, ...getArgs)=>{ + const stmt = db.prepare(sql); + try { + const rc = stmt.bind(bind).step() ? stmt.get(...getArgs) : undefined; + stmt.reset(); + return rc; + }finally{ + stmt.finalize(); + } + }; + + + const __selectAll = + (db, sql, bind, rowMode)=>db.exec({ + sql, bind, rowMode, returnValue: 'resultRows' + }); + + + const __stmtGetColumn = (stmt, ndx, asType)=>{ + switch(undefined===asType + ? capi.sqlite3_column_type(stmt.pointer, ndx) + : asType){ + case capi.SQLITE_NULL: return null; + case capi.SQLITE_INTEGER:{ + if(wasm.bigIntEnabled){ + const rc = capi.sqlite3_column_int64(stmt.pointer, ndx); + if(rc>=Number.MIN_SAFE_INTEGER && rc<=Number.MAX_SAFE_INTEGER){ + return Number(rc).valueOf(); + } + return rc; + }else{ + const rc = capi.sqlite3_column_double(stmt.pointer, ndx); + if(rc>Number.MAX_SAFE_INTEGER || rccheckSqlite3Rc(db,resultCode); + + DB.prototype = { + + isOpen: function(){ + return !!this.pointer; + }, + + affirmOpen: function(){ + return affirmDbOpen(this); + }, + + close: function(){ + const pDb = this.pointer; + if(pDb){ + if(this.onclose?.before instanceof Function){ + try{this.onclose.before(this)} + catch(e){} + } + for(const s of Object.values(__stmtMap.get(this))){ + if(s?.pointer){ + try{s.finalize()} + catch(ex){} + } + } + __ptrMap.delete(this); + __stmtMap.delete(this); + if( !__doesNotOwnHandle.delete(this) ){ + capi.sqlite3_close_v2(pDb); + } + if(this.onclose?.after instanceof Function){ + try{this.onclose.after(this)} + catch(e){} + } + delete this.filename; + } + }, + + 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); + } + }, + + dbFilename: function(dbName='main'){ + return capi.sqlite3_db_filename(affirmDbOpen(this).pointer, dbName); + }, + + dbName: function(dbNumber=0){ + return capi.sqlite3_db_name(affirmDbOpen(this).pointer, dbNumber); + }, + + 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.cstrToJs(v.$zName) } + finally { v.dispose() } + } + return rc; + }, + + prepare: function(sql){ + affirmDbOpen(this); + const stack = wasm.pstack.pointer; + let ppStmt, pStmt; + try{ + ppStmt = wasm.pstack.alloc(8); + DB.checkRc(this, capi.sqlite3_prepare_v2(this.pointer, sql, -1, ppStmt, null)); + pStmt = wasm.peekPtr(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; + }, + + exec: function(){ + 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 || resultRows + ) ; + const stack = wasm.scopedAllocPush(); + const saveSql = Array.isArray(opt.saveSql) ? opt.saveSql : undefined; + try{ + const isTA = util.isSQLableTypedArray(arg.sql) + ; + + let sqlByteLen = isTA ? arg.sql.byteLength : wasm.jstrlen(arg.sql); + const ppStmt = wasm.scopedAlloc( + + (2 * wasm.ptr.size) + (sqlByteLen + 1) + ); + const pzTail = wasm.ptr.add(ppStmt, wasm.ptr.size) ; + let pSql = wasm.ptr.add(pzTail, wasm.ptr.size); + const pSqlEnd = wasm.ptr.add(pSql, sqlByteLen); + if(isTA) wasm.heap8().set(arg.sql, pSql); + else wasm.jstrcpy(arg.sql, wasm.heap8(), pSql, sqlByteLen, false); + wasm.poke8(wasm.ptr.add(pSql, sqlByteLen), 0); + while(pSql && wasm.peek8(pSql) + ){ + wasm.pokePtr([ppStmt, pzTail], 0); + DB.checkRc(this, capi.sqlite3_prepare_v3( + this.pointer, pSql, sqlByteLen, 0, ppStmt, pzTail + )); + const pStmt = wasm.peekPtr(ppStmt); + pSql = wasm.peekPtr(pzTail); + sqlByteLen = Number(wasm.ptr.add(pSqlEnd,-pSql)); + if(!pStmt) continue; + + if(saveSql) 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){ + + let gotColNames = Array.isArray( + opt.columnNames + ) ? 0 : 1; + evalFirstResult = false; + if(arg.cbArg || resultRows){ + const cbArgCache = Object.create(null) + ; + for( ; stmt.step(); __execLock.delete(stmt) ){ + if(0===gotColNames++){ + stmt.getColumnNames(cbArgCache.columnNames = (opt.columnNames || [])); + } + __execLock.add(stmt); + const row = arg.cbArg(stmt,cbArgCache); + if(resultRows) resultRows.push(row); + if(callback && false === callback.call(opt, row, stmt)){ + break; + } + } + __execLock.delete(stmt); + } + if(0===gotColNames){ + + stmt.getColumnNames(opt.columnNames); + } + }else{ + stmt.step(); + } + stmt.reset( + ).finalize(); + stmt = null; + } + }finally{ + if(stmt){ + __execLock.delete(stmt); + stmt.finalize(); + } + wasm.scopedAllocPop(stack); + } + return arg.returnVal(); + }, + + + + createFunction: function f(name, xFunc, opt){ + const isFunc = (f)=>(f instanceof Function); + switch(arguments.length){ + case 1: + opt = name; + name = opt.name; + xFunc = opt.xFunc || 0; + break; + case 2: + if(!isFunc(xFunc)){ + opt = xFunc; + xFunc = opt.xFunc || 0; + } + break; + case 3: + 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 + && !wasm.isPtr(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 ; + 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 : 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; + }, + + selectValue: function(sql,bind,asType){ + return __selectFirstRow(this, sql, bind, 0, asType); + }, + + + selectValues: function(sql,bind,asType){ + const stmt = this.prepare(sql), rc = []; + try { + affirmColIndex(stmt, 0); + stmt.bind(bind); + while(stmt.step()) rc.push( + __stmtGetColumn(stmt, 0, asType) + ); + stmt.reset(); + }finally{ + stmt.finalize(); + } + return rc; + }, + + + selectArray: function(sql,bind){ + return __selectFirstRow(this, sql, bind, []); + }, + + + selectObject: function(sql,bind){ + return __selectFirstRow(this, sql, bind, {}); + }, + + + selectArrays: function(sql,bind){ + return __selectAll(this, sql, bind, 'array'); + }, + + + selectObjects: function(sql,bind){ + return __selectAll(this, sql, bind, 'object'); + }, + + + openStatementCount: function(){ + return this.pointer ? Object.keys(__stmtMap.get(this)).length : 0; + }, + + + transaction: function(callback){ + let opener = 'BEGIN'; + if(arguments.length>1){ + if(/[^a-zA-Z]/.test(arguments[0])){ + toss3(capi.SQLITE_MISUSE, "Invalid argument for BEGIN qualifier."); + } + opener += ' '+arguments[0]; + callback = arguments[1]; + } + affirmDbOpen(this).exec(opener); + try { + const rc = callback(this); + this.exec("COMMIT"); + return rc; + }catch(e){ + this.exec("ROLLBACK"); + throw e; + } + }, + + + 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; + } + }, + + + checkRc: function(resultCode){ + return checkSqlite3Rc(this, resultCode); + }, + }; + + + DB.wrapHandle = function(pDb, takeOwnership=false){ + if( !pDb || !wasm.isPtr(pDb) ){ + throw new sqlite3.SQLite3Error(capi.SQLITE_MISUSE, + "Argument must be a WASM sqlite3 pointer"); + } + return new DB({ + + "sqlite3*": pDb, + "sqlite3*:takeOwnership": !!takeOwnership + }); + }; + + + const affirmStmtOpen = function(stmt){ + if(!stmt.pointer) toss3("Stmt has been closed."); + return stmt; + }; + + + 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: + return wasm.bigIntEnabled ? t : undefined; + default: + return util.isBindableTypedArray(v) ? BindTypes.blob : undefined; + } + }; + + + const affirmSupportedBindType = function(v){ + + return isSupportedBindType(v) || toss3("Unsupported bind() argument type:",typeof v); + }; + + + 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; + }; + + + const __execLock = new Set(); + + const __stmtMayGet = new Set(); + + + const affirmNotLockedByExec = function(stmt,currentOpName){ + if(__execLock.has(stmt)){ + toss3("Operation is illegal when statement is locked:",currentOpName); + } + return stmt; + }; + + + const bindOne = function f(stmt,ndx,bindType,val){ + affirmNotLockedByExec(affirmStmtOpen(stmt), 'bind()'); + if(!f._){ + f._tooBigInt = (v)=>toss3( + "BigInt value is too big to store without precision loss:", v + ); + f._ = { + string: function(stmt, ndx, val, asBlob){ + const [pStr, n] = wasm.allocCString(val, true); + const f = asBlob ? capi.sqlite3_bind_blob : capi.sqlite3_bind_text; + return f(stmt.pointer, ndx, pStr, n, capi.SQLITE_WASM_DEALLOC); + } + }; + } + 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{ + 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); + break; + }else if(val instanceof ArrayBuffer){ + val = new Uint8Array(val); + }else if(!util.isBindableTypedArray(val)){ + toss3("Binding a value as a blob requires", + "that it be a string, Uint8Array, Int8Array, or ArrayBuffer."); + } + const pBlob = wasm.alloc(val.byteLength || 1); + wasm.heap8().set(val.byteLength ? val : [0], Number(pBlob)) + rc = capi.sqlite3_bind_blob(stmt.pointer, ndx, pBlob, val.byteLength, + capi.SQLITE_WASM_DEALLOC); + break; + } + default: + sqlite3.config.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 = { + + finalize: function(){ + const ptr = this.pointer; + if(ptr){ + affirmNotLockedByExec(this,'finalize()'); + const rc = (__doesNotOwnHandle.delete(this) + ? 0 + : capi.sqlite3_finalize(ptr)); + delete __stmtMap.get(this.db)[ptr]; + __ptrMap.delete(this); + __execLock.delete(this); + __stmtMayGet.delete(this); + delete this.parameterCount; + delete this.db; + return rc; + } + }, + + clearBindings: function(){ + affirmNotLockedByExec(affirmStmtOpen(this), 'clearBindings()') + capi.sqlite3_clear_bindings(this.pointer); + __stmtMayGet.delete(this); + return this; + }, + + reset: function(alsoClearBinds){ + affirmNotLockedByExec(this,'reset()'); + if(alsoClearBinds) this.clearBindings(); + const rc = capi.sqlite3_reset(affirmStmtOpen(this).pointer); + __stmtMayGet.delete(this); + checkSqlite3Rc(this.db, rc); + return this; + }, + + + resetNoThrow: function(alsoClearBinds){ + affirmNotLockedByExec(this,'reset()'); + if(alsoClearBinds) this.clearBindings(); + const rc = capi.sqlite3_reset(affirmStmtOpen(this).pointer); + __stmtMayGet.delete(this); + return rc; + }, + + + bind: function(){ + 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){ + + return this; + }else if(!this.parameterCount){ + toss3("This statement has no bindable parameters."); + } + __stmtMayGet.delete(this); + if(null===arg){ + + return bindOne(this, ndx, BindTypes.null, arg); + } + else if(Array.isArray(arg)){ + + 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(arg instanceof ArrayBuffer){ + arg = new Uint8Array(arg); + } + if('object'===typeof arg + && !util.isBindableTypedArray(arg)){ + + 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."); + }, + + 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()"); + } + return bindOne(this, ndx, BindTypes.blob, arg); + }, + + step: function(){ + affirmNotLockedByExec(this, 'step()'); + const rc = capi.sqlite3_step(affirmStmtOpen(this).pointer); + switch(rc){ + case capi.SQLITE_DONE: + __stmtMayGet.delete(this); + return false; + case capi.SQLITE_ROW: + __stmtMayGet.add(this); + return true; + default: + __stmtMayGet.delete(this); + sqlite3.config.warn("sqlite3_step() rc=",rc, + capi.sqlite3_js_rc_str(rc), + "SQL =", capi.sqlite3_sql(this.pointer)); + DB.checkRc(this.db.pointer, rc); + } + }, + + stepReset: function(){ + this.step(); + return this.reset(); + }, + + stepFinalize: function(){ + try{ + const rc = this.step(); + this.reset(); + return rc; + }finally{ + try{this.finalize()} + catch(e){} + } + }, + + + get: function(ndx,asType){ + if(!__stmtMayGet.has(affirmStmtOpen(this))){ + toss3("Stmt.step() has not (recently) returned true."); + } + if(Array.isArray(ndx)){ + let i = 0; + const n = this.columnCount; + while(itoss3("The pointer property is read-only.") + } + Object.defineProperty(Stmt.prototype, 'pointer', prop); + Object.defineProperty(DB.prototype, 'pointer', prop); + } + + Object.defineProperty(Stmt.prototype, 'columnCount', { + enumerable: false, + get: function(){return capi.sqlite3_column_count(this.pointer)}, + set: ()=>toss3("The columnCount property is read-only.") + }); + + Object.defineProperty(Stmt.prototype, 'parameterCount', { + enumerable: false, + get: function(){return capi.sqlite3_bind_parameter_count(this.pointer)}, + set: ()=>toss3("The parameterCount property is read-only.") + }); + + + Stmt.wrapHandle = function(oo1db, pStmt, takeOwnership=false){ + let ctor = Stmt; + if( !(oo1db instanceof DB) || !oo1db.pointer ){ + throw new sqlite3.SQLite3Error(sqlite3.SQLITE_MISUSE, + "First argument must be an opened "+ + "sqlite3.oo1.DB instance"); + } + if( !pStmt || !wasm.isPtr(pStmt) ){ + throw new sqlite3.SQLite3Error(sqlite3.SQLITE_MISUSE, + "Second argument must be a WASM "+ + "sqlite3_stmt pointer"); + } + return new Stmt(oo1db, pStmt, BindTypes, !!takeOwnership); + } + + + sqlite3.oo1 = { + DB, + Stmt + }; + +}); + + + +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ +const util = sqlite3.util; +sqlite3.initWorker1API = function(){ + 'use strict'; + const toss = (...args)=>{throw new Error(args.join(' '))}; + if(!(globalThis.WorkerGlobalScope instanceof Function)){ + toss("initWorker1API() must be run from a Worker thread."); + } + const sqlite3 = this.sqlite3 || toss("Missing this.sqlite3 object."); + const DB = sqlite3.oo1.DB; + + + const getDbId = function(db){ + let id = wState.idMap.get(db); + if(id) return id; + id = 'db#'+(++wState.idSeq)+':'+ + Math.floor(Math.random() * 100000000)+':'+ + Math.floor(Math.random() * 100000000); + + wState.idMap.set(db, id); + return id; + }; + + + const wState = { + + dbList: [], + + idSeq: 0, + + idMap: new WeakMap, + + 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 = util.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){ + util.sqlite3__wasm_vfs_unlink(pVfs, filename); + } + } + }, + + post: function(msg,xferList){ + if(xferList && xferList.length){ + globalThis.postMessage( msg, Array.from(xferList) ); + xferList.length = 0; + }else{ + globalThis.postMessage(msg); + } + }, + + dbs: Object.create(null), + + getDb: function(id,require=true){ + return this.dbs[id] + || (require ? toss("Unknown (or closed) DB ID:",id) : undefined); + } + }; + + + const affirmDbOpen = function(db = wState.dbList[0]){ + return (db && db.pointer) ? db : toss("DB is not opened."); + }; + + + 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 isSpecialDbFilename = (n)=>{ + return ""===n || ':'===n[0]; + }; + + + const wMsgHandler = { + open: function(ev){ + const oargs = Object.create(null), args = (ev.args || Object.create(null)); + if(args.simulateError){ + toss("Throwing because of simulateError flag."); + } + const rc = Object.create(null); + oargs.vfs = args.vfs; + oargs.filename = args.filename || ""; + const db = wState.open(oargs); + rc.filename = db.filename; + rc.persistent = !!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)){ + + db._blobXfer = wState.xfer; + } + const theCallback = rc.callback; + let rowNumber = 0; + const hadColNames = !!rc.columnNames; + if('string' === typeof theCallback){ + if(!hadColNames) rc.columnNames = []; + + rc.callback = function(row,stmt){ + wState.post({ + type: theCallback, + columnNames: rc.columnNames, + rowNumber: ++rowNumber, + row: row + }, wState.xfer); + } + } + try { + const changeCount = !!rc.countChanges + ? db.changes(true,(64===rc.countChanges)) + : undefined; + db.exec(rc); + if(undefined !== changeCount){ + rc.changeCount = db.changes(true,64===rc.countChanges) - changeCount; + } + const lastInsertRowId = !!rc.lastInsertRowId + ? sqlite3.capi.sqlite3_last_insert_rowid(db) + : undefined; + if( undefined!==lastInsertRowId ){ + rc.lastInsertRowId = lastInsertRowId; + } + if(rc.callback instanceof Function){ + rc.callback = theCallback; + + wState.post({ + type: theCallback, + columnNames: rc.columnNames, + rowNumber: null , + row: undefined + }); + } + }finally{ + delete db._blobXfer; + if(rc.callback) rc.callback = theCallback; + } + return rc; + }, + + 'config-get': function(){ + const rc = Object.create(null), src = sqlite3.config; + [ + 'bigIntEnabled' + ].forEach(function(k){ + if(Object.getOwnPropertyDescriptor(src, k)) rc[k] = src[k]; + }); + rc.version = sqlite3.version; + rc.vfsList = sqlite3.capi.sqlite3_js_vfs_list(); + return rc; + }, + + + 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; + }, + + toss: function(ev){ + toss("Testing worker exception"); + } + }; + + globalThis.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) sqlite3.config.warn("Worker is propagating an exception to main thread.", + "Reporting it _here_ for the stack trace:",err,result); + } + if(!dbId){ + dbId = result.dbId + || getDefaultDbId(); + } + + + wState.post({ + type: evType, + dbId: dbId, + messageId: ev.messageId, + workerReceivedTime: arrivalTime, + workerRespondTime: performance.now(), + departureTime: ev.departureTime, + + + + + + + result: result + }, wState.xfer); + }; + globalThis.postMessage({type:'sqlite3-api',result:'worker1-ready'}); +}.bind({sqlite3}); +}); + + + +'use strict'; +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss3; + const vfs = Object.create(null); + sqlite3.vfs = vfs; + + + capi.sqlite3_vfs.prototype.registerVfs = function(asDefault=false){ + if(!(this instanceof sqlite3.capi.sqlite3_vfs)){ + toss("Expecting a sqlite3_vfs-type argument."); + } + const rc = capi.sqlite3_vfs_register(this, asDefault ? 1 : 0); + if(rc){ + toss("sqlite3_vfs_register(",this,") failed with rc",rc); + } + if(this.pointer !== capi.sqlite3_vfs_find(this.$zName)){ + toss("BUG: sqlite3_vfs_find(vfs.$zName) failed for just-installed VFS", + this); + } + return this; + }; + + + vfs.installVfs = function(opt){ + let count = 0; + const propList = ['io','vfs']; + for(const key of propList){ + const o = opt[key]; + if(o){ + ++count; + o.struct.installMethods(o.methods, !!o.applyArgcCheck); + if('vfs'===key){ + if(!o.struct.$zName && 'string'===typeof o.name){ + o.struct.addOnDispose( + o.struct.$zName = wasm.allocCString(o.name) + ); + } + o.struct.registerVfs(!!o.asDefault); + } + } + } + if(!count) toss("Misuse: installVfs() options object requires at least", + "one of:", propList); + return this; + }; +}); + + + +'use strict'; +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + if( !sqlite3.wasm.exports.sqlite3_declare_vtab ){ + return; + } + const wasm = sqlite3.wasm, capi = sqlite3.capi, toss = sqlite3.util.toss3; + const vtab = Object.create(null); + sqlite3.vtab = vtab; + + const sii = capi.sqlite3_index_info; + + sii.prototype.nthConstraint = function(n, asPtr=false){ + if(n<0 || n>=this.$nConstraint) return false; + const ptr = wasm.ptr.add( + this.$aConstraint, + sii.sqlite3_index_constraint.structInfo.sizeof * n + ); + return asPtr ? ptr : new sii.sqlite3_index_constraint(ptr); + }; + + + sii.prototype.nthConstraintUsage = function(n, asPtr=false){ + if(n<0 || n>=this.$nConstraint) return false; + const ptr = wasm.ptr.add( + this.$aConstraintUsage, + sii.sqlite3_index_constraint_usage.structInfo.sizeof * n + ); + return asPtr ? ptr : new sii.sqlite3_index_constraint_usage(ptr); + }; + + + sii.prototype.nthOrderBy = function(n, asPtr=false){ + if(n<0 || n>=this.$nOrderBy) return false; + const ptr = wasm.ptr.add( + this.$aOrderBy, + sii.sqlite3_index_orderby.structInfo.sizeof * n + ); + return asPtr ? ptr : new sii.sqlite3_index_orderby(ptr); + }; + + + const __xWrapFactory = function(methodName,StructType){ + + return function(ptr,removeMapping=false){ + if(0===arguments.length) ptr = new StructType; + if(ptr instanceof StructType){ + + this.set(ptr.pointer, ptr); + return ptr; + }else if(!wasm.isPtr(ptr)){ + sqlite3.SQLite3Error.toss("Invalid argument to",methodName+"()"); + } + let rc = this.get(ptr); + if(removeMapping) this.delete(ptr); + return rc; + }.bind(new Map); + }; + + + const StructPtrMapper = function(name, StructType){ + const __xWrap = __xWrapFactory(name,StructType); + + return Object.assign(Object.create(null),{ + + StructType, + + create: (ppOut)=>{ + const rc = __xWrap(); + wasm.pokePtr(ppOut, rc.pointer); + return rc; + }, + + get: (pCObj)=>__xWrap(pCObj), + + unget: (pCObj)=>__xWrap(pCObj,true), + + dispose: (pCObj)=>__xWrap(pCObj,true)?.dispose?.() + }); + }; + + + vtab.xVtab = StructPtrMapper('xVtab', capi.sqlite3_vtab); + + + vtab.xCursor = StructPtrMapper('xCursor', capi.sqlite3_vtab_cursor); + + + vtab.xIndexInfo = (pIdxInfo)=>new capi.sqlite3_index_info(pIdxInfo); + + + vtab.xError = function f(methodName, err, defaultRc){ + if(f.errorReporter instanceof Function){ + try{f.errorReporter("sqlite3_module::"+methodName+"(): "+err.message);} + catch(e){} + } + let rc; + if(err instanceof sqlite3.WasmAllocError) rc = capi.SQLITE_NOMEM; + else if(arguments.length>2) rc = defaultRc; + else if(err instanceof sqlite3.SQLite3Error) rc = err.resultCode; + return rc || capi.SQLITE_ERROR; + }; + + vtab.xError.errorReporter = 1 ? sqlite3.config.error.bind(sqlite3.config) : false; + + + vtab.xRowid = (ppRowid64, value)=>wasm.poke(ppRowid64, value, 'i64'); + + + vtab.setupModule = function(opt){ + let createdMod = false; + const mod = (this instanceof capi.sqlite3_module) + ? this : (opt.struct || (createdMod = new capi.sqlite3_module())); + try{ + const methods = opt.methods || toss("Missing 'methods' object."); + for(const e of Object.entries({ + + + xConnect: 'xCreate', xDisconnect: 'xDestroy' + })){ + + const k = e[0], v = e[1]; + if(true === methods[k]) methods[k] = methods[v]; + else if(true === methods[v]) methods[v] = methods[k]; + } + if(opt.catchExceptions){ + const fwrap = function(methodName, func){ + if(['xConnect','xCreate'].indexOf(methodName) >= 0){ + return function(pDb, pAux, argc, argv, ppVtab, pzErr){ + try{return func(...arguments) || 0} + catch(e){ + if(!(e instanceof sqlite3.WasmAllocError)){ + wasm.dealloc(wasm.peekPtr(pzErr)); + wasm.pokePtr(pzErr, wasm.allocCString(e.message)); + } + return vtab.xError(methodName, e); + } + }; + }else{ + return function(...args){ + try{return func(...args) || 0} + catch(e){ + return vtab.xError(methodName, e); + } + }; + } + }; + const mnames = [ + 'xCreate', 'xConnect', 'xBestIndex', 'xDisconnect', + 'xDestroy', 'xOpen', 'xClose', 'xFilter', 'xNext', + 'xEof', 'xColumn', 'xRowid', 'xUpdate', + 'xBegin', 'xSync', 'xCommit', 'xRollback', + 'xFindFunction', 'xRename', 'xSavepoint', 'xRelease', + 'xRollbackTo', 'xShadowName' + ]; + const remethods = Object.create(null); + for(const k of mnames){ + const m = methods[k]; + if(!(m instanceof Function)) continue; + else if('xConnect'===k && methods.xCreate===m){ + remethods[k] = methods.xCreate; + }else if('xCreate'===k && methods.xConnect===m){ + remethods[k] = methods.xConnect; + }else{ + remethods[k] = fwrap(k, m); + } + } + mod.installMethods(remethods, false); + }else{ + + + mod.installMethods( + methods, !!opt.applyArgcCheck + ); + } + if(0===mod.$iVersion){ + let v; + if('number'===typeof opt.iVersion) v = opt.iVersion; + else if(mod.$xIntegrity) v = 4; + else if(mod.$xShadowName) v = 3; + else if(mod.$xSavePoint || mod.$xRelease || mod.$xRollbackTo) v = 2; + else v = 1; + mod.$iVersion = v; + } + }catch(e){ + if(createdMod) createdMod.dispose(); + throw e; + } + return mod; + }; + + + capi.sqlite3_module.prototype.setupModule = function(opt){ + return vtab.setupModule.call(this, opt); + }; +}); + + + +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + if( sqlite3.config.disable?.vfs?.kvvfs ){ + return; + } + 'use strict'; + const capi = sqlite3.capi, + sqlite3_kvvfs_methods = capi.sqlite3_kvvfs_methods, + KVVfsFile = capi.KVVfsFile, + pKvvfs = sqlite3.capi.sqlite3_vfs_find("kvvfs") + + + delete capi.sqlite3_kvvfs_methods; + delete capi.KVVfsFile; + + if( !pKvvfs ) return ; + if( 0 ){ + + capi.sqlite3_vfs_register(pKvvfs, 1); + } + + const util = sqlite3.util, + wasm = sqlite3.wasm, + toss3 = util.toss3, + hop = (o,k)=>Object.prototype.hasOwnProperty.call(o,k); + + const kvvfsMethods = new sqlite3_kvvfs_methods( + + wasm.exports.sqlite3__wasm_kvvfs_methods() + ); + util.assert( 32<=kvvfsMethods.$nKeySize, "unexpected kvvfsMethods.$nKeySize: "+kvvfsMethods.$nKeySize); + + + const cache = Object.assign(Object.create(null),{ + + rxJournalSuffix: /-journal$/, + + zKeyJrnl: wasm.allocCString("jrnl"), + + zKeySz: wasm.allocCString("sz"), + + keySize: kvvfsMethods.$nKeySize, + + buffer: Object.assign(Object.create(null),{ + + n: kvvfsMethods.$nBufferSize, + + pool: Object.create(null) + }) + }); + + + cache.memBuffer = (id=0)=>cache.buffer.pool[id] ??= wasm.alloc(cache.buffer.n); + + + cache.memBufferFree = (id)=>{ + const b = cache.buffer.pool[id]; + if( b ){ + wasm.dealloc(b); + delete cache.buffer.pool[id]; + } + }; + + const noop = ()=>{}; + const debug = sqlite3.__isUnderTest + ? (...args)=>sqlite3.config.debug?.("kvvfs:", ...args) + : noop; + const warn = (...args)=>sqlite3.config.warn?.("kvvfs:", ...args); + const error = (...args)=>sqlite3.config.error?.("kvvfs:", ...args); + + + class KVVfsStorage { + #map = Object.create(null); + #keys = null; + #size = 0; + + constructor(){ + this.clear(); + } + + #getKeys(){ + return this.#keys ??= Object.keys(this.#map); + } + + key(n){ + if(n < 0 || n >= this.#size) return null; + return this.#getKeys()[n]; + } + + getItem(k){ + return this.#map[k] ?? null; + } + + setItem(k,v){ + if( !(k in this.#map) ){ + ++this.#size; + this.#keys = null; + } + this.#map[k] = ''+v; + } + + removeItem(k){ + if( k in this.#map ){ + delete this.#map[k]; + --this.#size; + this.#keys = null; + } + } + + clear(){ + this.#map = Object.create(null); + this.#keys = null; + this.#size = 0; + } + + get length() { + return this.#size; + } + }; + + + const kvvfsIsPersistentName = (v)=>'local'===v || 'session'===v; + + + const kvvfsKeyPrefix = (v)=>kvvfsIsPersistentName(v) ? 'kvvfs-'+v+'-' : ''; + + + const validateStorageName = function(n,mayBeJournal=false){ + if( kvvfsIsPersistentName(n) ) return; + const len = (new Blob([n])).size; + if( !len ) toss3(capi.SQLITE_MISUSE, "Empty name is not permitted."); + let maxLen = cache.keySize - 1; + if( cache.rxJournalSuffix.test(n) ){ + if( !mayBeJournal ){ + toss3(capi.SQLITE_MISUSE, + "Storage names may not have a '-journal' suffix."); + } + }else if( ['-wal','-shm'].filter(v=>n.endsWith(v)).length ){ + toss3(capi.SQLITE_MISUSE, + "Storage names may not have a -wal or -shm suffix."); + }else{ + maxLen -= 8 ; + } + if( len > maxLen ){ + toss3(capi.SQLITE_RANGE, "Storage name is too long. Limit =", maxLen); + } + let i; + for( i = 0; i < len; ++i ){ + const ch = n.codePointAt(i); + if( ch<32 ){ + toss3(capi.SQLITE_RANGE, + "Illegal character ("+ch+"d) in storage name:",n); + } + } + }; + + + const newStorageObj = (name,storage=undefined)=>Object.assign(Object.create(null),{ + + jzClass: name, + + refc: 1, + + deleteAtRefc0: false, + + storage: storage || new KVVfsStorage, + + keyPrefix: kvvfsKeyPrefix(name), + + files: [], + + listeners: undefined + }); + + + const kvvfs = sqlite3.kvvfs = Object.create(null); + if( sqlite3.__isUnderTest ){ + + kvvfs.log = Object.assign(Object.create(null),{ + xOpen: false, + xClose: false, + xWrite: false, + xRead: false, + xSync: false, + xAccess: false, + xFileControl: false, + xRcrdRead: false, + xRcrdWrite: false, + xRcrdDelete: false, + }); + } + + + const deleteStorage = function(store){ + const other = cache.rxJournalSuffix.test(store.jzClass) + ? store.jzClass.replace(cache.rxJournalSuffix,'') + : store.jzClass+'-journal'; + kvvfs?.log?.xClose + && debug("cleaning up storage handles [", store.jzClass, other,"]",store); + delete cache.storagePool[store.jzClass]; + delete cache.storagePool[other]; + if( !sqlite3.__isUnderTest ){ + + delete store.storage; + delete store.refc; + } + }; + + + const installStorageAndJournal = (store)=> + cache.storagePool[store.jzClass] = + cache.storagePool[store.jzClass+'-journal'] = store; + + + const nameOfThisThreadStorage = '.'; + + + cache.storagePool = Object.assign(Object.create(null),{ + + [nameOfThisThreadStorage]: newStorageObj(nameOfThisThreadStorage) + }); + + if( globalThis.Storage ){ + + if( globalThis.localStorage instanceof globalThis.Storage ){ + cache.storagePool.local = newStorageObj('local', globalThis.localStorage); + } + if( globalThis.sessionStorage instanceof globalThis.Storage ){ + cache.storagePool.session = newStorageObj('session', globalThis.sessionStorage); + } + } + + cache.builtinStorageNames = Object.keys(cache.storagePool); + + const isBuiltinName = (n)=>cache.builtinStorageNames.indexOf(n)>-1; + + + for(const k of Object.keys(cache.storagePool)){ + + const orig = cache.storagePool[k]; + cache.storagePool[k+'-journal'] = orig; + } + + cache.setError = (e=undefined, dfltErrCode=capi.SQLITE_ERROR)=>{ + if( e ){ + cache.lastError = e; + return (e.resultCode | 0) || dfltErrCode; + } + delete cache.lastError; + return 0; + }; + + cache.popError = ()=>{ + const e = cache.lastError; + delete cache.lastError; + return e; + }; + + + const catchForNotify = (e)=>{ + warn("kvvfs.listener handler threw:",e); + }; + + const kvvfsDecode = wasm.exports.sqlite3__wasm_kvvfs_decode; + const kvvfsEncode = wasm.exports.sqlite3__wasm_kvvfs_encode; + + + const notifyListeners = async function(eventName,store,...args){ + try{ + + if( store.keyPrefix && args[0] ){ + args[0] = args[0].replace(store.keyPrefix,''); + } + let u8enc, z0, z1, wcache; + for(const ear of store.listeners){ + const ev = Object.create(null); + ev.storageName = store.jzClass; + ev.type = eventName; + const decodePages = ear.decodePages; + const f = ear.events[eventName]; + if( f ){ + if( !ear.includeJournal && args[0]==='jrnl' ){ + continue; + } + if( 'write'===eventName && ear.decodePages && +args[0]>0 ){ + + ev.data = [args[0]]; + if( wcache?.[args[0]] ){ + ev.data[1] = wcache[args[0]]; + continue; + } + u8enc ??= new TextEncoder('utf-8'); + z0 ??= cache.memBuffer(10); + z1 ??= cache.memBuffer(11); + const u = u8enc.encode(args[1]); + const heap = wasm.heap8u(); + heap.set(u, Number(z0)); + heap[wasm.ptr.addn(z0, u.length)] = 0; + const rc = kvvfsDecode(z0, z1, cache.buffer.n); + if( rc>0 ){ + wcache ??= Object.create(null); + wcache[args[0]] + = ev.data[1] + = heap.slice(Number(z1), wasm.ptr.addn(z1,rc)); + }else{ + continue; + } + }else{ + ev.data = args.length + ? ((args.length===1) ? args[0] : args) + : undefined; + } + try{f(ev)?.catch?.(catchForNotify)} + catch(e){ + warn("notifyListeners [",store.jzClass,"]",eventName,e); + } + } + } + }catch(e){ + catchForNotify(e); + } + }; + + + const storageForZClass = (zClass)=> + 'string'===typeof zClass + ? cache.storagePool[zClass] + : cache.storagePool[wasm.cstrToJs(zClass)]; + + + const kvvfsMakeKey = wasm.exports.sqlite3__wasm_kvvfsMakeKey; + + const zKeyForStorage = (store, zClass, zKey)=>{ + + return (zClass && store.keyPrefix) ? kvvfsMakeKey(zClass, zKey) : zKey; + }; + + const jsKeyForStorage = (store,zClass,zKey)=> + wasm.cstrToJs(zKeyForStorage(store, zClass, zKey)); + + const storageGetDbSize = (store)=>+store.storage.getItem(store.keyPrefix + "sz"); + + + const pFileHandles = new Map(); + + + const mapPathToLocks = new Map(); + + + const unlockFile = function(fh, lockType){ + if(fh.lockType<=lockType) return; + const lk = mapPathToLocks.get(fh.jzClass); + if(fh.lockType>capi.SQLITE_LOCK_SHARED){ + lk.lockType = capi.SQLITE_LOCK_SHARED; + } + if(lockType===capi.SQLITE_LOCK_NONE && 0===--lk.nShared + ){ + mapPathToLocks.delete(fh.jzClass); + } + fh.lockType = lockType; + }; + + + const originalMethods = { + vfs: Object.create(null), + ioDb: Object.create(null), + ioJrnl: Object.create(null) + }; + + + const originalIoMethods = (kvvfsFile)=> + originalMethods[kvvfsFile.$isJournal ? 'ioJrnl' : 'ioDb']; + + const pVfs = new capi.sqlite3_vfs(kvvfsMethods.$pVfs); + const pIoDb = new capi.sqlite3_io_methods(kvvfsMethods.$pIoDb); + const pIoJrnl = new capi.sqlite3_io_methods(kvvfsMethods.$pIoJrnl); + const recordHandler = + Object.create(null); + const kvvfsInternal = Object.assign(Object.create(null),{ + pFileHandles, + cache, + storageForZClass, + KVVfsStorage, + + disablePageSizeChange: true + }); + if( kvvfs.log ){ + + kvvfs.internal = kvvfsInternal; + } + + + const methodOverrides = { + + + recordHandler: { + xRcrdRead: (zClass, zKey, zBuf, nBuf)=>{ + try{ + const jzClass = wasm.cstrToJs(zClass); + const store = storageForZClass(jzClass); + if( !store ) return -1; + const jXKey = jsKeyForStorage(store, zClass, zKey); + kvvfs?.log?.xRcrdRead && warn("xRcrdRead", jzClass, jXKey, nBuf, store ); + const jV = store.storage.getItem(jXKey); + if(null===jV) return -1; + const nV = jV.length ; + if( 0 ){ + debug("xRcrdRead", jXKey, store, jV); + } + if(nBuf<=0) return nV; + else if(1===nBuf){ + wasm.poke(zBuf, 0); + return nV; + }else if( nBuf nV + 1 ){ + nBuf = nV + 1; + } + if( 0 ){ + debug("xRcrdRead", nBuf, zClass, wasm.cstrToJs(zClass), + wasm.cstrToJs(zKey), nV, jV, store); + } + const nCopy = nV{ + try { + const store = storageForZClass(zClass); + const jxKey = jsKeyForStorage(store, zClass, zKey); + const jData = wasm.cstrToJs(zData); + kvvfs?.log?.xRcrdWrite && warn("xRcrdWrite",jxKey, store); + store.storage.setItem(jxKey, jData); + store.listeners && notifyListeners('write', store, jxKey, jData); + return 0; + }catch(e){ + error("kvrecordWrite()",e); + return cache.setError(e, capi.SQLITE_IOERR); + } + }, + + xRcrdDelete: (zClass, zKey)=>{ + try { + const store = storageForZClass(zClass); + const jxKey = jsKeyForStorage(store, zClass, zKey); + kvvfs?.log?.xRcrdDelete && warn("xRcrdDelete",jxKey, store); + store.storage.removeItem(jxKey); + store.listeners && notifyListeners('delete', store, jxKey); + return 0; + }catch(e){ + error("kvrecordDelete()",e); + return cache.setError(e, capi.SQLITE_IOERR); + } + } + }, + + + vfs:{ + + xOpen: function(pProtoVfs,zName,pProtoFile,flags,pOutFlags){ + cache.popError(); + let zToFree ; + try{ + if( !zName ){ + zToFree = wasm.allocCString(""+pProtoFile+"." + +(Math.random() * 100000 | 0)); + zName = zToFree; + } + const jzClass = wasm.cstrToJs(zName); + kvvfs?.log?.xOpen && debug("xOpen",jzClass,"flags =",flags); + validateStorageName(jzClass, true); + if( (flags & (capi.SQLITE_OPEN_MAIN_DB + | capi.SQLITE_OPEN_TEMP_DB + | capi.SQLITE_OPEN_TRANSIENT_DB)) + && cache.rxJournalSuffix.test(jzClass) ){ + toss3(capi.SQLITE_ERROR, + "DB files may not have a '-journal' suffix."); + } + let s = storageForZClass(jzClass); + if( !s && !(flags & capi.SQLITE_OPEN_CREATE) ){ + toss3(capi.SQLITE_ERROR, "Storage not found:", jzClass); + } + const rc = originalMethods.vfs.xOpen(pProtoVfs, zName, pProtoFile, + flags, pOutFlags); + if( rc ) return rc; + let deleteAt0 = !!(capi.SQLITE_OPEN_DELETEONCLOSE & flags); + if(wasm.isPtr(arguments[1])){ + if(capi.sqlite3_uri_boolean(zName, "delete-on-close", 0)){ + deleteAt0 = true; + } + } + const f = new KVVfsFile(pProtoFile); + util.assert(f.$zClass, "Missing f.$zClass"); + f.addOnDispose(zToFree); + zToFree = undefined; + + if( s ){ + ++s.refc; + + s.files.push(f); + wasm.poke32(pOutFlags, flags); + }else{ + wasm.poke32(pOutFlags, flags | capi.SQLITE_OPEN_CREATE); + util.assert( !f.$isJournal, "Opening a journal before its db? "+jzClass ); + + const nm = jzClass.replace(cache.rxJournalSuffix,''); + s = newStorageObj(nm); + installStorageAndJournal(s); + s.files.push(f); + s.deleteAtRefc0 = deleteAt0; + kvvfs?.log?.xOpen + && debug("xOpen installed storage handle [",nm, nm+"-journal","]", s); + } + pFileHandles.set(pProtoFile, Object.assign(Object.create(null),{ + store: s, + file: f, + jzClass, + lockType: 0 + })); + s.listeners && notifyListeners('open', s, s.files.length); + return 0; + }catch(e){ + warn("xOpen:",e); + return cache.setError(e); + }finally{ + zToFree && wasm.dealloc(zToFree); + } + }, + + xDelete: function(pVfs, zName, iSyncFlag){ + cache.popError(); + try{ + const jzName = wasm.cstrToJs(zName); + if( cache.rxJournalSuffix.test(jzName) ){ + recordHandler.xRcrdDelete(zName, cache.zKeyJrnl); + } + return 0; + }catch(e){ + warn("xDelete",e); + return cache.setError(e); + } + }, + + xAccess: function(pProtoVfs, zPath, flags, pResOut){ + cache.popError(); + try{ + const s = storageForZClass(zPath); + const jzPath = s?.jzClass || wasm.cstrToJs(zPath); + if( kvvfs?.log?.xAccess ){ + debug("xAccess",jzPath,"flags =", + flags,"*pResOut =",wasm.peek32(pResOut), + "store =",s); + } + if( !s ){ + + + + try{validateStorageName(jzPath)} + catch(e){ + + wasm.poke32(pResOut, 0); + return 0; + } + } + if( s ){ + const key = s.keyPrefix+ + (cache.rxJournalSuffix.test(jzPath) ? "jrnl" : "1"); + const res = s.storage.getItem(key) ? 0 : 1; + + + wasm.poke32(pResOut, res); + }else{ + wasm.poke32(pResOut, 0); + } + return 0; + }catch(e){ + error('xAccess',e); + return cache.setError(e); + } + }, + + xRandomness: function(pVfs, nOut, pOut){ + const heap = wasm.heap8u(); + let i = 0; + const npOut = Number(pOut); + for(; i < nOut; ++i) heap[npOut + i] = (Math.random()*255000) & 0xFF; + return nOut; + }, + + xGetLastError: function(pVfs,nOut,pOut){ + const e = cache.popError(); + debug('xGetLastError',e); + if(e){ + const scope = wasm.scopedAllocPush(); + try{ + const [cMsg, n] = wasm.scopedAllocCString(e.message, true); + wasm.cstrncpy(pOut, cMsg, nOut); + if(n > nOut) wasm.poke8(wasm.ptr.add(pOut,nOut,-1), 0); + debug("set xGetLastError",e.message); + return (e.resultCode | 0) || capi.SQLITE_IOERR; + }catch(e){ + return capi.SQLITE_NOMEM; + }finally{ + wasm.scopedAllocPop(scope); + } + } + return 0; + }, + + + xSleep: (pVfs,ms)=>0, + + }, + + + ioDb:{ + + xClose: function(pFile){ + cache.popError(); + try{ + const h = pFileHandles.get(pFile); + kvvfs?.log?.xClose && debug("xClose", pFile, h); + if( h ){ + pFileHandles.delete(pFile); + unlockFile(h, capi.SQLITE_LOCK_NONE); + const s = h.store; + s.files = s.files.filter((v)=>v!==h.file); + if( --s.refc<=0 && s.deleteAtRefc0 ){ + deleteStorage(s); + } + originalMethods.ioDb.xClose(pFile); + h.file.dispose(); + s.listeners && notifyListeners('close', s, s.files.length); + }else{ + + } + return 0; + }catch(e){ + error("xClose",e); + return cache.setError(e); + } + }, + + xFileControl: function(pFile, opId, pArg){ + cache.popError(); + try{ + const h = pFileHandles.get(pFile); + util.assert(h, "Missing KVVfsFile handle"); + kvvfs?.log?.xFileControl && debug("xFileControl",h,'op =',opId); + if( opId===capi.SQLITE_FCNTL_PRAGMA + && kvvfsInternal.disablePageSizeChange ){ + + + const zName = wasm.peekPtr(wasm.ptr.add(pArg, wasm.ptr.size)); + if( "page_size"===wasm.cstrToJs(zName) ){ + kvvfs?.log?.xFileControl + && debug("xFileControl pragma",wasm.cstrToJs(zName)); + const zVal = wasm.peekPtr(wasm.ptr.add(pArg, 2*wasm.ptr.size)); + if( zVal ){ + + kvvfs?.log?.xFileControl + && warn("xFileControl pragma", h, + "NOT setting page size to", wasm.cstrToJs(zVal)); + h.file.$szPage = -1; + return 0; + }else if( h.file.$szPage>0 ){ + kvvfs?.log?.xFileControl && + warn("xFileControl", h, "getting page size",h.file.$szPage); + wasm.pokePtr(pArg, wasm.allocCString(""+h.file.$szPage) + ); + return 0; + } + } + } + const rc = originalMethods.ioDb.xFileControl(pFile, opId, pArg); + if( 0==rc && capi.SQLITE_FCNTL_SYNC===opId ){ + h.store.listeners && notifyListeners('sync', h.store, false); + } + return rc; + }catch(e){ + error("xFileControl",e); + return cache.setError(e); + } + }, + + xSync: function(pFile,flags){ + cache.popError(); + try{ + const h = pFileHandles.get(pFile); + kvvfs?.log?.xSync && debug("xSync", h); + util.assert(h, "Missing KVVfsFile handle"); + const rc = originalMethods.ioDb.xSync(pFile, flags); + if( 0==rc && h.store.listeners ) notifyListeners('sync', h.store, true); + return rc; + }catch(e){ + error("xSync",e); + return cache.setError(e); + } + }, + + xLock: function(pFile, iLock){ + cache.popError(); + try{ + const h = pFileHandles.get(pFile); + if( capi.SQLITE_LOCK_NONE!==iLock ){ + + originalMethods.ioDb.xLock(pFile, iLock); + } + + if(h.lockType>=iLock) return 0; + let lk = mapPathToLocks.get(h.jzClass); + if(!lk){ + lk = Object.assign(Object.create(null),{ + nShared: 0, lockType: capi.SQLITE_LOCK_NONE + }); + mapPathToLocks.set(h.jzClass, lk); + } + if(h.lockType!==lk.lockType + && (lk.lockType>=capi.SQLITE_LOCK_PENDING + || iLock>capi.SQLITE_LOCK_SHARED)){ + return capi.SQLITE_BUSY; + } + if(iLock===capi.SQLITE_LOCK_SHARED){ + ++lk.nShared; + if(lk.lockType===capi.SQLITE_LOCK_NONE) lk.lockType = iLock; + }else if(iLock===capi.SQLITE_LOCK_EXCLUSIVE && lk.nShared>1){ + if(h.lockType===capi.SQLITE_LOCK_RESERVED){ + h.lockType = lk.lockType = capi.SQLITE_LOCK_PENDING; + } + return capi.SQLITE_BUSY; + }else{ + lk.lockType = iLock; + } + h.lockType = iLock; + return 0; + }catch(e){ + error("xLock", e); + return cache.setError(e); + } + }, + + xUnlock: function(pFile, iLock){ + cache.popError(); + try{ + if( capi.SQLITE_LOCK_NONE===iLock ){ + + originalMethods.ioDb.xUnlock(pFile, iLock); + } + const h = pFileHandles.get(pFile); + unlockFile(h, iLock); + return 0; + }catch(e){ + error("xUnlock", e); + return cache.setError(e); + } + }, + + xCheckReservedLock: function(pFile, pOut){ + cache.popError(); + try{ + const h = pFileHandles.get(pFile); + const lk = mapPathToLocks.get(h.jzClass); + wasm.poke32(pOut, + (!!lk && lk.lockType>capi.SQLITE_LOCK_SHARED) + ? 1 : 0) + return 0; + }catch(e){ + error("xCheckReservedLock", e); + return cache.setError(e); + } + }, + + + }, + + ioJrnl:{ + + xClose: true, + xLock: true, + xUnlock: true, + xCheckReservedLock: true, + } + }; + + + try { + util.assert( cache.buffer.n>1024*129, "Heap buffer is not large enough" + ); + for(const e of Object.entries(methodOverrides.recordHandler)){ + + const k = e[0], f = e[1]; + recordHandler[k] = f; + if( 0 ){ + + kvvfsMethods.installMethod(k, f); + }else{ + kvvfsMethods[kvvfsMethods.memberKey(k)] = + wasm.installFunction(kvvfsMethods.memberSignature(k), f); + } + } + for(const e of Object.entries(methodOverrides.vfs)){ + + const k = e[0], f = e[1], km = pVfs.memberKey(k), + member = pVfs.structInfo.members[k] + || util.toss("Missing pVfs.structInfo[",k,"]"); + originalMethods.vfs[k] = wasm.functionEntry(pVfs[km]); + pVfs[km] = wasm.installFunction(member.signature, f); + } + for(const e of Object.entries(methodOverrides.ioDb)){ + + const k = e[0], f = e[1], km = pIoDb.memberKey(k); + originalMethods.ioDb[k] = wasm.functionEntry(pIoDb[km]) + || util.toss("Missing native pIoDb[",km,"]"); + pIoDb[km] = wasm.installFunction(pIoDb.memberSignature(k), f); + } + for(const e of Object.entries(methodOverrides.ioJrnl)){ + + const k = e[0], f = e[1], km = pIoJrnl.memberKey(k); + originalMethods.ioJrnl[k] = wasm.functionEntry(pIoJrnl[km]) + || util.toss("Missing native pIoJrnl[",km,"]"); + if( true===f ){ + + pIoJrnl[km] = pIoDb[km] || util.toss("Missing copied pIoDb[",km,"]"); + }else{ + pIoJrnl[km] = wasm.installFunction(pIoJrnl.memberSignature(k), f); + } + } + }finally{ + kvvfsMethods.dispose(); + pVfs.dispose(); + pIoDb.dispose(); + pIoJrnl.dispose(); + } + + + + + const sqlite3_js_kvvfs_clear = function callee(which){ + if( ''===which ){ + return callee('local') + callee('session'); + } + const store = storageForZClass(which); + if( !store ) return 0; + if( store.files.length ){ + if( globalThis.localStorage===store.storage + || globalThis.sessionStorage===store.storage ){ + + }else{ + + toss3(capi.SQLITE_ACCESS, + "Cannot clear in-use database storage."); + } + } + const s = store.storage; + const toRm = [] ; + let i, n = s.length; + + for( i = 0; i < n; ++i ){ + const k = s.key(i); + + if(!store.keyPrefix || k.startsWith(store.keyPrefix)) toRm.push(k); + } + toRm.forEach((kk)=>s.removeItem(kk)); + + return toRm.length; + }; + + + const sqlite3_js_kvvfs_size = function callee(which){ + if( ''===which ){ + return callee('local') + callee('session'); + } + const store = storageForZClass(which); + if( !store ) return 0; + const s = store.storage; + let i, sz = 0; + for(i = 0; i < s.length; ++i){ + const k = s.key(i); + if(!store.keyPrefix || k.startsWith(store.keyPrefix)){ + sz += k.length; + sz += s.getItem(k).length; + } + } + return sz * 2 ; + }; + + + const sqlite3_js_kvvfs_export = function callee(...args){ + let opt; + if( 1===args.length && 'object'===typeof args[0] ){ + opt = args[0]; + }else if(args.length){ + opt = Object.assign(Object.create(null),{ + name: args[0], + + }); + } + const store = opt ? storageForZClass(opt.name) : null; + if( !store ){ + toss3(capi.SQLITE_NOTFOUND, + "There is no kvvfs storage named",opt?.name); + } + + const s = store.storage; + const rc = Object.assign(Object.create(null),{ + name: store.jzClass, + timestamp: Date.now(), + pages: [] + }); + const pages = Object.create(null); + let xpages; + const keyPrefix = store.keyPrefix; + const rxTail = keyPrefix + ? /^kvvfs-[^-]+-(\w+)/ + : undefined; + let i = 0, n = s.length; + for( ; i < n; ++i ){ + const k = s.key(i); + if( !keyPrefix || k.startsWith(keyPrefix) ){ + let kk = (keyPrefix ? rxTail.exec(k) : undefined)?.[1] ?? k; + switch( kk ){ + case 'jrnl': + if( opt.includeJournal ) rc.journal = s.getItem(k); + break; + case 'sz': + rc.size = +s.getItem(k); + break; + default: + kk = +kk ; + if( !util.isInt32(kk) || kk<=0 ){ + toss3(capi.SQLITE_RANGE, "Malformed kvvfs key: "+k); + } + if( opt.decodePages ){ + const spg = s.getItem(k), + n = spg.length, + z = cache.memBuffer(0), + zDec = cache.memBuffer(1), + heap = wasm.heap8u(); + let i = 0; + for( ; i < n; ++i ){ + heap[wasm.ptr.add(z, i)] = spg.codePointAt(i) & 0xff; + } + heap[wasm.ptr.add(z, i)] = 0; + + const nDec = kvvfsDecode( + z, zDec, cache.buffer.n + ); + + pages[kk] = heap.slice(Number(zDec), wasm.ptr.addn(zDec, nDec)); + }else{ + pages[kk] = s.getItem(k); + } + break; + } + } + } + if( opt.decodePages ) cache.memBufferFree(1); + + Object.keys(pages).map((v)=>+v).sort().forEach( + (v)=>rc.pages.push(pages[v]) + ); + return rc; + }; + + + const sqlite3_js_kvvfs_import = function(exp, overwrite=false){ + if( !exp?.timestamp + || !exp.name + || undefined===exp.size + || !Array.isArray(exp.pages) ){ + toss3(capi.SQLITE_MISUSE, "Malformed export object."); + }else if( !exp.size + || (exp.size !== (exp.size | 0)) + + || exp.size>=0x7fffffff ){ + toss3(capi.SQLITE_RANGE, "Invalid db size: "+exp.size); + } + + validateStorageName(exp.name); + let store = storageForZClass(exp.name); + const isNew = !store; + if( store ){ + if( !overwrite ){ + + toss3(capi.SQLITE_ACCESS, + "Storage '"+exp.name+"' already exists and", + "overwrite was not specified."); + }else if( !store.files || !store.jzClass ){ + toss3(capi.SQLITE_ERROR, + "Internal storage object", exp.name,"seems to be malformed."); + }else if( store.files.length ){ + toss3(capi.SQLITE_IOERR_ACCESS, + "Cannot import db storage while it is in use."); + } + sqlite3_js_kvvfs_clear(exp.name); + }else{ + store = newStorageObj(exp.name); + + } + + + const keyPrefix = kvvfsKeyPrefix(exp.name); + let zEnc; + try{ + ; + const s = store.storage; + s.setItem(keyPrefix+'sz', exp.size); + if( exp.journal ) s.setItem(keyPrefix+'jrnl', exp.journal); + if( exp.pages[0] instanceof Uint8Array ){ + + + exp.pages.forEach((u,ndx)=>{ + const n = u.length; + if( 0 && cache.fixedPageSize !== n ){ + util.toss3(capi.SQLITE_RANGE,"Unexpected page size:", n); + } + zEnc ??= cache.memBuffer(1); + const zBin = cache.memBuffer(0), + heap = wasm.heap8u(); + + heap.set(u, Number(zBin)); + heap[wasm.ptr.addn(zBin,n)] = 0; + const rc = kvvfsEncode(zBin, n, zEnc); + util.assert( rc < cache.buffer.n, + "Impossibly long output - possibly smashed the heap" ); + util.assert( 0===wasm.peek8(wasm.ptr.add(zEnc,rc)), + "Expecting NUL-terminated encoded output" ); + const jenc = wasm.cstrToJs(zEnc); + + s.setItem(keyPrefix+(ndx+1), jenc); + }); + }else if( exp.pages[0] ){ + + exp.pages.forEach((v,ndx)=>s.setItem(keyPrefix+(ndx+1), v)); + } + if( isNew ) installStorageAndJournal(store); + }catch{ + if( !isNew ){ + try{sqlite3_js_kvvfs_clear(exp.name);}catch(ee){} + } + }finally{ + if( zEnc ) cache.memBufferFree(1); + } + return this; + }; + + + const sqlite3_js_kvvfs_reserve = function(name){ + let store = storageForZClass(name); + if( store ){ + ++store.refc; + return; + } + validateStorageName(name); + installStorageAndJournal(newStorageObj(name)); + }; + + + const sqlite3_js_kvvfs_unlink = function(name){ + const store = storageForZClass(name); + if( !store + || kvvfsIsPersistentName(store.jzClass) + || isBuiltinName(store.jzClass) + || cache.rxJournalSuffix.test(name) ) return false; + if( store.refc > store.files.length || 0===store.files.length ){ + if( --store.refc<=0 ){ + + deleteStorage(store); + } + return true; + } + return false; + }; + + + const sqlite3_js_kvvfs_listen = function(opt){ + if( !opt || 'object'!==typeof opt ){ + toss3(capi.SQLITE_MISUSE, "Expecting a listener object."); + } + let store = storageForZClass(opt.storage); + if( !store ){ + if( opt.storage && opt.reserve ){ + sqlite3_js_kvvfs_reserve(opt.storage); + store = storageForZClass(opt.storage); + util.assert(store, + "Unexpectedly cannot fetch reserved storage " + +opt.storage); + }else{ + toss3(capi.SQLITE_NOTFOUND,"No such storage:",opt.storage); + } + } + if( opt.events ){ + (store.listeners ??= []).push(opt); + } + }; + + + const sqlite3_js_kvvfs_unlisten = function(opt){ + const store = storageForZClass(opt?.storage); + if( store?.listeners && opt.events ){ + const n = store.listeners.length; + store.listeners = store.listeners.filter((v)=>v!==opt); + const rc = n>store.listeners.length; + if( !store.listeners.length ){ + + store.listeners = undefined; + } + return rc; + } + return false; + }; + + sqlite3.kvvfs.reserve = sqlite3_js_kvvfs_reserve; + sqlite3.kvvfs.import = sqlite3_js_kvvfs_import; + sqlite3.kvvfs.export = sqlite3_js_kvvfs_export; + sqlite3.kvvfs.unlink = sqlite3_js_kvvfs_unlink; + sqlite3.kvvfs.listen = sqlite3_js_kvvfs_listen; + sqlite3.kvvfs.unlisten = sqlite3_js_kvvfs_unlisten; + sqlite3.kvvfs.exists = (name)=>!!storageForZClass(name); + sqlite3.kvvfs.estimateSize = sqlite3_js_kvvfs_size; + sqlite3.kvvfs.clear = sqlite3_js_kvvfs_clear; + + + if( globalThis.Storage ){ + + capi.sqlite3_js_kvvfs_size = (which="")=>sqlite3_js_kvvfs_size(which); + capi.sqlite3_js_kvvfs_clear = (which="")=>sqlite3_js_kvvfs_clear(which); + } + + if(sqlite3.oo1?.DB){ + + const DB = sqlite3.oo1.DB; + sqlite3.oo1.JsStorageDb = function( + storageName = sqlite3.oo1.JsStorageDb.defaultStorageName + ){ + const opt = DB.dbCtorHelper.normalizeArgs(...arguments); + opt.vfs = 'kvvfs'; + if( 0 ){ + + if( opt.flags ) opt.flags = 'cw'+opt.flags; + else opt.flags = 'cw'; + } + switch( opt.filename ){ + + case ":sessionStorage:": opt.filename = 'session'; break; + case ":localStorage:": opt.filename = 'local'; break; + } + const m = /(file:(\/\/)?)([^?]+)/.exec(opt.filename); + validateStorageName( m ? m[3] : opt.filename); + DB.dbCtorHelper.call(this, opt); + }; + sqlite3.oo1.JsStorageDb.defaultStorageName + = cache.storagePool.session ? 'session' : nameOfThisThreadStorage; + const jdb = sqlite3.oo1.JsStorageDb; + jdb.prototype = Object.create(DB.prototype); + jdb.clearStorage = sqlite3_js_kvvfs_clear; + + jdb.prototype.clearStorage = function(){ + return jdb.clearStorage(this.affirmOpen().dbFilename(), true); + }; + + jdb.storageSize = sqlite3_js_kvvfs_size; + + jdb.prototype.storageSize = function(){ + return jdb.storageSize(this.affirmOpen().dbFilename(), true); + }; + } + + if( sqlite3.__isUnderTest && sqlite3.vtab ){ + + const cols = Object.assign(Object.create(null),{ + rowid: {type: 'INTEGER'}, + name: {type: 'TEXT'}, + nRef: {type: 'INTEGER'}, + nOpen: {type: 'INTEGER'}, + isTransient: {type: 'INTEGER'}, + dbSize: {type: 'INTEGER'} + }); + Object.keys(cols).forEach((v,i)=>cols[v].colId = i); + + const VT = sqlite3.vtab; + const ProtoCursor = Object.assign(Object.create(null),{ + row: function(){ + return cache.storagePool[this.names[this.rowid]]; + } + }); + Object.assign(Object.create(ProtoCursor),{ + rowid: 0, + names: Object.keys(cache.storagePool) + .filter(v=>!cache.rxJournalSuffix.test(v)) + }); + const cursorState = function(cursor, reset){ + const o = (cursor instanceof capi.sqlite3_vtab_cursor) + ? cursor + : VT.xCursor.get(cursor); + if( reset || !o.vTabState ){ + o.vTabState = Object.assign(Object.create(ProtoCursor),{ + rowid: 0, + names: Object.keys(cache.storagePool) + .filter(v=>!cache.rxJournalSuffix.test(v)) + }); + } + return o.vTabState; + }; + + const dbg = 1 ? ()=>{} : (...args)=>debug("vtab",...args); + + const theModule = function f(){ + return f.mod ??= new sqlite3.capi.sqlite3_module().setupModule({ + catchExceptions: true, + methods: { + xConnect: function(pDb, pAux, argc, argv, ppVtab, pzErr){ + dbg("xConnect"); + try{ + const xcol = []; + Object.keys(cols).forEach((k)=>{ + xcol.push(k+" "+cols[k].type); + }); + const rc = capi.sqlite3_declare_vtab( + pDb, "CREATE TABLE ignored("+xcol.join(',')+")" + ); + if(0===rc){ + const t = VT.xVtab.create(ppVtab); + util.assert( + (t === VT.xVtab.get(wasm.peekPtr(ppVtab))), + "output pointer check failed" + ); + } + return rc; + }catch(e){ + return VT.xError('xConnect', e, capi.SQLITE_ERROR); + } + }, + xCreate: wasm.ptr.null, + + xDisconnect: function(pVtab){ + dbg("xDisconnect",...arguments); + VT.xVtab.dispose(pVtab); + return 0; + }, + xOpen: function(pVtab, ppCursor){ + dbg("xOpen",...arguments); + VT.xCursor.create(ppCursor); + return 0; + }, + xClose: function(pCursor){ + dbg("xClose",...arguments); + const c = VT.xCursor.unget(pCursor); + delete c.vTabState; + c.dispose(); + return 0; + }, + xNext: function(pCursor){ + dbg("xNext",...arguments); + const c = VT.xCursor.get(pCursor); + ++cursorState(c).rowid; + return 0; + }, + xColumn: function(pCursor, pCtx, iCol){ + dbg("xColumn",...arguments); + + const st = cursorState(pCursor); + const store = st.row(); + util.assert(store, "Unexpected xColumn call"); + switch(iCol){ + case cols.rowid.colId: + capi.sqlite3_result_int(pCtx, st.rowid); + break; + case cols.name.colId: + capi.sqlite3_result_text(pCtx, store.jzClass, -1, capi.SQLITE_TRANSIENT); + break; + case cols.nRef.colId: + capi.sqlite3_result_int(pCtx, store.refc); + break; + case cols.nOpen.colId: + capi.sqlite3_result_int(pCtx, store.files.length); + break; + case cols.isTransient.colId: + capi.sqlite3_result_int(pCtx, !!store.deleteAtRefc0); + break; + case cols.dbSize.colId: + capi.sqlite3_result_int(pCtx, storageGetDbSize(store)); + break; + default: + capi.sqlite3_result_error(pCtx, "Invalid column id: "+iCol); + return capi.SQLITE_RANGE; + } + return 0; + }, + xRowid: function(pCursor, ppRowid64){ + dbg("xRowid",...arguments); + const st = cursorState(pCursor); + VT.xRowid(ppRowid64, st.rowid); + return 0; + }, + xEof: function(pCursor){ + const st = cursorState(pCursor); + dbg("xEof?="+(!st.row()),...arguments); + return !st.row(); + }, + xFilter: function(pCursor, idxNum, idxCStr, + argc, argv){ + dbg("xFilter",...arguments); + const st = cursorState(pCursor, true); + return 0; + }, + xBestIndex: function(pVtab, pIdxInfo){ + dbg("xBestIndex",...arguments); + + const pii = new capi.sqlite3_index_info(pIdxInfo); + pii.$estimatedRows = cache.storagePool.size; + pii.$estimatedCost = 1.0; + pii.dispose(); + return 0; + } + } + }); + }; + + sqlite3.kvvfs.create_module = function(pDb, name="sqlite_kvvfs"){ + return capi.sqlite3_create_module(pDb, name, theModule(), + wasm.ptr.null); + }; + + } + + +}); + +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + 'use strict'; + if( sqlite3.config.disable?.vfs?.opfs && + sqlite3.config.disable.vfs['opfs-vfs'] ){ + return; + } + const toss = sqlite3.util.toss, + capi = sqlite3.capi, + util = sqlite3.util, + wasm = sqlite3.wasm; + + + const opfsUtil = sqlite3.opfs = Object.create(null); + + + opfsUtil.thisThreadHasOPFS = ()=>{ + return globalThis.FileSystemHandle && + globalThis.FileSystemDirectoryHandle && + globalThis.FileSystemFileHandle && + globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle && + navigator?.storage?.getDirectory; + }; + + + opfsUtil.getRootDir = async function f(){ + return f.promise ??= navigator.storage.getDirectory().then(d=>{ + opfsUtil.rootDirectory = d; + return d; + }).catch(e=>{ + delete f.promise; + throw e; + }); + }; + + + opfsUtil.getResolvedPath = function(filename,splitIt){ + const p = new URL(filename, "file://irrelevant").pathname; + return splitIt ? p.split('/').filter((v)=>!!v) : p; + }; + + + opfsUtil.getDirForFilename = async function f(absFilename, createDirs = false){ + const path = opfsUtil.getResolvedPath(absFilename, true); + const filename = path.pop(); + let dh = await opfsUtil.getRootDir(); + for(const dirName of path){ + if(dirName){ + dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); + } + } + return [dh, filename]; + }; + + + opfsUtil.mkdir = async function(absDirName){ + try { + await opfsUtil.getDirForFilename(absDirName+"/filepart", true); + return true; + }catch(e){ + + return false; + } + }; + + + opfsUtil.entryExists = async function(fsEntryName){ + try { + const [dh, fn] = await opfsUtil.getDirForFilename(fsEntryName); + await dh.getFileHandle(fn); + return true; + }catch(e){ + return false; + } + }; + + + opfsUtil.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(""); + + }; + + + 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); + const dir = await opfsUtil.getRootDir(); + await doDir(dir, root); + return root; + }; + + + opfsUtil.rmfr = async function(){ + const rd = await opfsUtil.getRootDir(); + const dir = rd, opt = {recurse: true}; + for await (const handle of dir.values()){ + dir.removeEntry(handle.name, opt); + } + }; + + + 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; + } + }; + + + opfsUtil.traverse = async function(opt){ + const defaultOpt = { + recursive: true, + directory: await opfsUtil.getRootDir() + }; + 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); + }; + + + const importDbChunked = async function(filename, callback){ + const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true); + const hFile = await hDir.getFileHandle(fnamePart, {create:true}); + let sah = await hFile.createSyncAccessHandle(); + let nWrote = 0, chunk, checkedHeader = false, err = false; + try{ + sah.truncate(0); + while( undefined !== (chunk = await callback()) ){ + if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk); + if( !checkedHeader && 0===nWrote && chunk.byteLength>=15 ){ + util.affirmDbHeader(chunk); + checkedHeader = true; + } + sah.write(chunk, {at: nWrote}); + nWrote += chunk.byteLength; + } + if( nWrote < 512 || 0!==nWrote % 512 ){ + toss("Input size",nWrote,"is not correct for an SQLite database."); + } + if( !checkedHeader ){ + const header = new Uint8Array(20); + sah.read( header, {at: 0} ); + util.affirmDbHeader( header ); + } + sah.write(new Uint8Array([1,1]), {at: 18}); + return nWrote; + }catch(e){ + await sah.close(); + sah = undefined; + await hDir.removeEntry( fnamePart ).catch(()=>{}); + throw e; + }finally { + if( sah ) await sah.close(); + } + }; + + + opfsUtil.importDb = async function(filename, bytes){ + if( bytes instanceof Function ){ + return importDbChunked(filename, bytes); + } + if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); + util.affirmIsDb(bytes); + const n = bytes.byteLength; + const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true); + let sah, err, nWrote = 0; + try { + const hFile = await hDir.getFileHandle(fnamePart, {create:true}); + sah = await hFile.createSyncAccessHandle(); + sah.truncate(0); + nWrote = sah.write(bytes, {at: 0}); + if(nWrote != n){ + toss("Expected to write "+n+" bytes but wrote "+nWrote+"."); + } + sah.write(new Uint8Array([1,1]), {at: 18}) ; + return nWrote; + }catch(e){ + if( sah ){ await sah.close(); sah = undefined; } + await hDir.removeEntry( fnamePart ).catch(()=>{}); + throw e; + }finally{ + if( sah ) await sah.close(); + } + }; + + + opfsUtil.vfsInstallationFeatureCheck = function(vfsName){ + if( !globalThis.SharedArrayBuffer || !globalThis.Atomics ){ + toss("Cannot install OPFS: Missing SharedArrayBuffer and/or Atomics.", + "The server must emit the COOP/COEP response headers to enable those.", + "See https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep"); + }else if( 'undefined'===typeof WorkerGlobalScope ){ + toss("The OPFS sqlite3_vfs cannot run in the main thread", + "because it requires Atomics.wait()."); + }else if( !globalThis.FileSystemHandle || + !globalThis.FileSystemDirectoryHandle || + !globalThis.FileSystemFileHandle?.prototype?.createSyncAccessHandle || + !navigator?.storage?.getDirectory ){ + toss("Missing required OPFS APIs."); + }else if( 'opfs-wl'===vfsName && !globalThis.Atomics.waitAsync ){ + toss('The',vfsName,'VFS requires Atomics.waitAsync(), which is not available.'); + } + }; + + + opfsUtil.initOptions = function callee(vfsName, options){ + const urlParams = new URL(globalThis.location.href).searchParams; + if( urlParams.has(vfsName+'-disable') ){ + + return; + } + try{ + opfsUtil.vfsInstallationFeatureCheck(vfsName); + }catch(e){ + return; + } + options = util.nu(options); + options.vfsName = vfsName; + options.verbose ??= urlParams.has('opfs-verbose') + ? +urlParams.get('opfs-verbose') : 1; + options.sanityChecks ??= urlParams.has('opfs-sanity-check'); + + if( !opfsUtil.proxyUri ){ + opfsUtil.proxyUri = "sqlite3-opfs-async-proxy.js"; + if( sqlite3.scriptInfo?.sqlite3Dir ){ + + opfsUtil.proxyUri = ( + sqlite3.scriptInfo.sqlite3Dir + opfsUtil.proxyUri + ); + } + } + options.proxyUri ??= opfsUtil.proxyUri; + if('function' === typeof options.proxyUri){ + options.proxyUri = options.proxyUri(); + } + + return opfsUtil.options = options; + }; + + + opfsUtil.createVfsState = function(){ + const state = util.nu(); + const options = opfsUtil.options; + state.verbose = options.verbose; + + const loggers = [ + sqlite3.config.error, + sqlite3.config.warn, + sqlite3.config.log + ]; + const vfsName = options.vfsName + || toss("Maintenance required: missing VFS name"); + const logImpl = (level,...args)=>{ + if(state.verbose>level) loggers[level](vfsName+":",...args); + }; + const log = (...args)=>logImpl(2, ...args), + warn = (...args)=>logImpl(1, ...args), + error = (...args)=>logImpl(0, ...args), + capi = sqlite3.capi, + wasm = sqlite3.wasm; + + const opfsVfs = state.vfs = new capi.sqlite3_vfs(); + const opfsIoMethods = opfsVfs.ioMethods = new capi.sqlite3_io_methods(); + + opfsIoMethods.$iVersion = 1; + opfsVfs.$iVersion = 2; + opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof; + opfsVfs.$mxPathname = 1024; + opfsVfs.$zName = wasm.allocCString(vfsName); + opfsVfs.addOnDispose( + '$zName', opfsVfs.$zName, opfsIoMethods + + ); + + opfsVfs.metrics = util.nu({ + counters: util.nu(), + 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; + } + sqlite3.config.log(globalThis.location.href, + "metrics for",globalThis.location.href,":",metrics, + "\nTotal of",n,"op(s) for",t, + "ms (incl. "+w+" ms of waiting on the async side)"); + sqlite3.config.log("Serialization metrics:",opfsVfs.metrics.counters.s11n); + opfsVfs.worker?.postMessage?.({type:'opfs-async-metrics'}); + }, + reset: function(){ + let k; + const r = (m)=>(m.count = m.time = m.wait = 0); + const m = opfsVfs.metrics.counters; + for(k in state.opIds){ + r(m[k] = Object.create(null)); + } + let s = m.s11n = Object.create(null); + s = s.serialize = Object.create(null); + s.count = s.time = 0; + s = m.s11n.deserialize = Object.create(null); + s.count = s.time = 0; + } + }); + + + state.asyncIdleWaitTime = 150; + + + state.asyncS11nExceptions = 1; + + state.fileBufferSize = 1024 * 64; + state.sabS11nOffset = state.fileBufferSize; + + state.sabS11nSize = opfsVfs.$mxPathname * 2; + + state.sabIO = new SharedArrayBuffer( + state.fileBufferSize + + state.sabS11nSize + ); + + + state.opIds = Object.create(null); + { + + let i = 0; + + state.opIds.whichOp = i++; + + state.opIds.rc = i++; + + state.opIds.xAccess = i++; + state.opIds.xClose = i++; + state.opIds.xDelete = i++; + state.opIds.xDeleteNoWait = 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++; + + state.opIds.retry = i++; + state.sabOP = new SharedArrayBuffer( + i * 4); + } + + state.sq3Codes = Object.create(null); + for(const k of [ + 'SQLITE_ACCESS_EXISTS', + 'SQLITE_ACCESS_READWRITE', + 'SQLITE_BUSY', + 'SQLITE_CANTOPEN', + '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_LOCKED', + 'SQLITE_MISUSE', + 'SQLITE_NOTFOUND', + 'SQLITE_OPEN_CREATE', + 'SQLITE_OPEN_DELETEONCLOSE', + 'SQLITE_OPEN_MAIN_DB', + 'SQLITE_OPEN_READONLY', + 'SQLITE_LOCK_NONE', + 'SQLITE_LOCK_SHARED', + 'SQLITE_LOCK_RESERVED', + 'SQLITE_LOCK_PENDING', + 'SQLITE_LOCK_EXCLUSIVE' + ]){ + state.sq3Codes[k] = + capi[k] ?? toss("Maintenance required: not found:",k); + } + + state.opfsFlags = Object.assign(Object.create(null),{ + + OPFS_UNLOCK_ASAP: 0x01, + + OPFS_UNLINK_BEFORE_OPEN: 0x02, + + defaultUnlockAsap: false + }); + + opfsVfs.metrics.reset(); + const metrics = opfsVfs.metrics.counters; + + + const opRun = opfsVfs.opRun = (op,...args)=>{ + const opNdx = state.opIds[op] || toss(opfsVfs.vfsName+": 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) + ; + const t = performance.now(); + while('not-equal'!==Atomics.wait(state.sabOPView, state.opIds.rc, -1)){ + + } + + 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; + }; + + const opTimer = Object.create(null); + opTimer.op = undefined; + opTimer.start = undefined; + const mTimeStart = opfsVfs.mTimeStart = (op)=>{ + opTimer.start = performance.now(); + opTimer.op = op; + ++metrics[op].count; + }; + const mTimeEnd = opfsVfs.mTimeEnd = ()=>( + metrics[opTimer.op].time += performance.now() - opTimer.start + ); + + + const __openFiles = opfsVfs.__openFiles = Object.create(null); + + + const ioSyncWrappers = opfsVfs.ioSyncWrappers = util.nu({ + xCheckReservedLock: function(pFile,pOut){ + + wasm.poke(pOut, 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){ + return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; + }, + xFileControl: function(pFile, opId, pArg){ + + return capi.SQLITE_NOTFOUND; + }, + xFileSize: function(pFile,pSz64){ + mTimeStart('xFileSize'); + let rc = opRun('xFileSize', pFile); + if(0==rc){ + try { + const sz = state.s11n.deserialize()[0]; + wasm.poke(pSz64, sz, 'i64'); + }catch(e){ + error("Unexpected error reading xFileSize() result:",e); + rc = state.sq3Codes.SQLITE_IOERR; + } + } + 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){ + + wasm.heap8u().set(f.sabView.subarray(0, n), Number(pDest)); + } + }catch(e){ + error("xRead(",arguments,") failed:",e,f); + rc = capi.SQLITE_IOERR_READ; + } + mTimeEnd(); + return rc; + }, + xSync: function(pFile,flags){ + mTimeStart('xSync'); + const rc = opRun('xSync', pFile, flags); + mTimeEnd(); + return rc; + }, + xTruncate: function(pFile,sz64){ + mTimeStart('xTruncate'); + const rc = opRun('xTruncate', pFile, Number(sz64)); + mTimeEnd(); + return rc; + }, + xWrite: function(pFile,pSrc,n,offset64){ + mTimeStart('xWrite'); + const f = __openFiles[pFile]; + let rc; + try { + f.sabView.set(wasm.heap8u().subarray( + Number(pSrc), Number(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; + } + }); + + + const vfsSyncWrappers = opfsVfs.vfsSyncWrappers = { + xAccess: function(pVfs,zName,flags,pOut){ + mTimeStart('xAccess'); + const rc = opRun('xAccess', wasm.cstrToJs(zName)); + wasm.poke( pOut, (rc ? 0 : 1), 'i32' ); + mTimeEnd(); + return 0; + }, + xCurrentTime: function(pVfs,pOut){ + wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000), + 'double'); + return 0; + }, + xCurrentTimeInt64: function(pVfs,pOut){ + wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(), + 'i64'); + return 0; + }, + xDelete: function(pVfs, zName, doSyncDir){ + mTimeStart('xDelete'); + const rc = opRun('xDelete', wasm.cstrToJs(zName), doSyncDir, false); + mTimeEnd(); + return rc; + }, + xFullPathname: function(pVfs,zName,nOut,pOut){ + + const i = wasm.cstrncpy(pOut, zName, nOut); + return i( + 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){ + 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{ + n = viewDV.getInt32(offset, state.littleEndian); + offset += 4; + v = textDecoder.decode(viewU8.slice(offset, offset+n)); + offset += n; + } + rc.push(v); + } + } + if(clear) viewU8[0] = 0; + + return rc; + }; + + + state.s11n.serialize = function(...args){ + const t = performance.now(); + if(args.length){ + + const typeIds = []; + let i = 0, offset = 1; + viewU8[0] = args.length & 0xff ; + for(; i < args.length; ++i, ++offset){ + + typeIds.push(getTypeId(args[i])); + viewU8[offset] = typeIds[i].id; + } + for(i = 0; i < args.length; ++i) { + + const t = typeIds[i]; + if(t.setter){ + viewDV[t.setter](offset, args[i], state.littleEndian); + offset += t.size; + }else{ + const s = textEncoder.encode(args[i]); + viewDV.setInt32(offset, s.byteLength, state.littleEndian); + offset += 4; + viewU8.set(s, offset); + offset += s.byteLength; + } + } + + }else{ + viewU8[0] = 0; + } + }; + + + return state.s11n; +}; + opfsVfs.initS11n = initS11n; + + + opfsVfs.bindVfs = function(ioMethods, callback){ + Object.assign(opfsVfs.ioSyncWrappers, ioMethods); + const thePromise = new Promise(function(promiseResolve_, promiseReject_){ + let promiseWasRejected = undefined; + const promiseReject = (err)=>{ + promiseWasRejected = true; + opfsVfs.dispose(); + return promiseReject_(err); + }; + const promiseResolve = ()=>{ + try{ + callback(sqlite3, opfsVfs); + }catch(e){ + return promiseReject(e); + } + promiseWasRejected = false; + return promiseResolve_(sqlite3); + }; + const options = opfsUtil.options; + const proxyUri = options.proxyUri +( + (options.proxyUri.indexOf('?')<0) ? '?' : '&' + )+'vfs='+vfsName; + + const W = opfsVfs.worker = + new Worker(new URL(proxyUri, import.meta.url)); + let zombieTimer = setTimeout(()=>{ + + if(undefined===promiseWasRejected){ + promiseReject( + new Error("Timeout while waiting for OPFS async proxy worker.") + ); + } + }, 4000); + W._originalOnError = W.onerror ; + W.onerror = function(err){ + + + error("Error initializing OPFS asyncer:",err); + promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons.")); + }; + + const opRun = opfsVfs.opRun; + + const sanityCheck = function(){ + const scope = wasm.scopedAllocPush(); + const sq3File = new capi.sqlite3_file(); + try{ + const fid = sq3File.pointer; + const openFlags = capi.SQLITE_OPEN_CREATE + | capi.SQLITE_OPEN_READWRITE + + | capi.SQLITE_OPEN_MAIN_DB; + const pOut = wasm.scopedAlloc(8); + const dbFile = "/sanity/check/file"+opfsUtil.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."); + opfsVfs.vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); + rc = wasm.peek(pOut,'i32'); + log("xAccess(",dbFile,") exists ?=",rc); + rc = opfsVfs.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; + } + opfsVfs.vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); + rc = wasm.peek(pOut,'i32'); + if(!rc) toss("xAccess() failed to detect file."); + rc = opfsVfs.ioSyncWrappers.xSync(sq3File.pointer, 0); + if(rc) toss('sync failed w/ rc',rc); + rc = opfsVfs.ioSyncWrappers.xTruncate(sq3File.pointer, 1024); + if(rc) toss('truncate failed w/ rc',rc); + wasm.poke(pOut,0,'i64'); + rc = opfsVfs.ioSyncWrappers.xFileSize(sq3File.pointer, pOut); + if(rc) toss('xFileSize failed w/ rc',rc); + log("xFileSize says:",wasm.peek(pOut, 'i64')); + rc = opfsVfs.ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1); + if(rc) toss("xWrite() failed!"); + const readBuf = wasm.scopedAlloc(16); + rc = opfsVfs.ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2); + wasm.poke(readBuf+6,0); + let jRead = wasm.cstrToJs(readBuf); + log("xRead() got:",jRead); + if("sanity"!==jRead) toss("Unexpected xRead() value."); + if(opfsVfs.vfsSyncWrappers.xSleep){ + log("xSleep()ing before close()ing..."); + opfsVfs.vfsSyncWrappers.xSleep(opfsVfs.pointer,2000); + log("waking up from xSleep()"); + } + rc = opfsVfs.ioSyncWrappers.xClose(fid); + log("xClose rc =",rc,"sabOPView =",state.sabOPView); + log("Deleting file:",dbFile); + opfsVfs.vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234); + opfsVfs.vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); + rc = wasm.peek(pOut,'i32'); + if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete()."); + warn("End of OPFS sanity checks."); + }finally{ + sq3File.dispose(); + wasm.scopedAllocPop(scope); + } + }; + + W.onmessage = function({data}){ + + switch(data.type){ + case 'opfs-unavailable': + + promiseReject(new Error(data.payload.join(' '))); + break; + case 'opfs-async-loaded': + + delete state.vfs; + W.postMessage({type: 'opfs-async-init', args: util.nu(state)}); + break; + case 'opfs-async-inited': { + + if(true===promiseWasRejected){ + break ; + } + clearTimeout(zombieTimer); + zombieTimer = null; + try { + sqlite3.vfs.installVfs({ + io: {struct: opfsVfs.ioMethods, methods: opfsVfs.ioSyncWrappers}, + vfs: {struct: opfsVfs, methods: opfsVfs.vfsSyncWrappers} + }); + 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); + opfsVfs.initS11n(); + opfsVfs.initS11n = null; + if(options.sanityChecks){ + warn("Running sanity checks because of opfs-sanity-check URL arg..."); + sanityCheck(); + } + if(opfsUtil.thisThreadHasOPFS()){ + opfsUtil.getRootDir().then((d)=>{ + W.onerror = W._originalOnError; + delete W._originalOnError; + log("End of OPFS sqlite3_vfs setup.", opfsVfs); + promiseResolve(); + }).catch(promiseReject); + }else{ + promiseResolve(); + } + }catch(e){ + error(e); + promiseReject(e); + } + break; + } + case 'debug': + warn("debug message from worker:",data); + break; + default: { + const errMsg = ( + "Unexpected message from the OPFS async worker: " + + JSON.stringify(data) + ); + error(errMsg); + promiseReject(new Error(errMsg)); + break; + } + } + }; + }); + return thePromise; + }; + + return state; + }; + +}); + +'use strict'; +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + if( !sqlite3.opfs || sqlite3.config.disable?.vfs?.opfs ){ + return; + } + const util = sqlite3.util, + opfsUtil = sqlite3.opfs || sqlite3.util.toss("Missing sqlite3.opfs"); + +const installOpfsVfs = async function(options){ + options = opfsUtil.initOptions('opfs',options); + if( !options ) return sqlite3; + const capi = sqlite3.capi, + state = opfsUtil.createVfsState(), + opfsVfs = state.vfs, + metrics = opfsVfs.metrics.counters, + mTimeStart = opfsVfs.mTimeStart, + mTimeEnd = opfsVfs.mTimeEnd, + opRun = opfsVfs.opRun, + debug = (...args)=>sqlite3.config.debug("opfs:",...args), + warn = (...args)=>sqlite3.config.warn("opfs:",...args), + __openFiles = opfsVfs.__openFiles; + + + + return opfsVfs.bindVfs(util.nu({ + xLock: function(pFile,lockType){ + mTimeStart('xLock'); + ++metrics.xLock.count; + const f = __openFiles[pFile]; + let rc = 0; + + if( f.lockType ) { + f.lockType = lockType; + }else{ + rc = opRun('xLock', pFile, lockType); + if( 0===rc ) f.lockType = lockType; + } + mTimeEnd(); + return rc; + }, + xUnlock: function(pFile,lockType){ + mTimeStart('xUnlock'); + ++metrics.xUnlock.count; + 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; + } + }), function(sqlite3, vfs){ + + if(sqlite3.oo1){ + const OpfsDb = function(...args){ + const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args); + opt.vfs = vfs.$zName; + sqlite3.oo1.DB.dbCtorHelper.call(this, opt); + }; + OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype); + sqlite3.oo1.OpfsDb = OpfsDb; + OpfsDb.importDb = opfsUtil.importDb; + if( true ){ + + sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenCallback( + opfsVfs.pointer, + function(oo1Db, sqlite3){ + + sqlite3.capi.sqlite3_busy_timeout(oo1Db, 10000); + } + ); + } + } + }); +}; +globalThis.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{ + return installOpfsVfs().catch((e)=>{ + sqlite3.config.warn("Ignoring inability to install 'opfs' sqlite3_vfs:",e); + }) +}); +}); + +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + 'use strict'; + if( sqlite3.config.disable?.vfs?.['opfs-sahpool'] ){ + return; + } + + const toss = sqlite3.util.toss; + const toss3 = sqlite3.util.toss3; + const initPromises = Object.create(null) ; + const capi = sqlite3.capi; + const util = sqlite3.util; + const wasm = sqlite3.wasm; + + const SECTOR_SIZE = 4096; + const HEADER_MAX_PATH_SIZE = 512; + const HEADER_FLAGS_SIZE = 4; + const HEADER_DIGEST_SIZE = 8; + const HEADER_CORPUS_SIZE = HEADER_MAX_PATH_SIZE + HEADER_FLAGS_SIZE; + const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE; + const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE; + const HEADER_OFFSET_DATA = SECTOR_SIZE; + + const PERSISTENT_FILE_TYPES = + capi.SQLITE_OPEN_MAIN_DB | + capi.SQLITE_OPEN_MAIN_JOURNAL | + capi.SQLITE_OPEN_SUPER_JOURNAL | + capi.SQLITE_OPEN_WAL; + const FLAG_COMPUTE_DIGEST_V2 = capi.SQLITE_OPEN_MEMORY + ; + + + const OPAQUE_DIR_NAME = ".opaque"; + + + const getRandomName = ()=>Math.random().toString(36).slice(2); + + const textDecoder = new TextDecoder(); + const textEncoder = new TextEncoder(); + + const optionDefaults = Object.assign(Object.create(null),{ + name: 'opfs-sahpool', + directory: undefined , + initialCapacity: 6, + clearOnInit: false, + + verbosity: 2, + forceReinitIfPreviouslyFailed: false, + preserveOnInitFailure: false + }); + + + const loggers = [ + sqlite3.config.error, + sqlite3.config.warn, + sqlite3.config.log + ]; + const log = sqlite3.config.log; + const warn = sqlite3.config.warn; + const error = sqlite3.config.error; + + + const __mapVfsToPool = new Map(); + const getPoolForVfs = (pVfs)=>__mapVfsToPool.get(pVfs); + const setPoolForVfs = (pVfs,pool)=>{ + if(pool) __mapVfsToPool.set(pVfs, pool); + else __mapVfsToPool.delete(pVfs); + }; + + const __mapSqlite3File = new Map(); + const getPoolForPFile = (pFile)=>__mapSqlite3File.get(pFile); + const setPoolForPFile = (pFile,pool)=>{ + if(pool) __mapSqlite3File.set(pFile, pool); + else __mapSqlite3File.delete(pFile); + }; + + + const ioMethods = { + xCheckReservedLock: function(pFile,pOut){ + const pool = getPoolForPFile(pFile); + pool.log('xCheckReservedLock'); + pool.storeErr(); + const file = pool.getOFileForS3File(pFile); + wasm.poke32( + pOut, + pool.hasReservedLock(file.path) ? 1 : 0 + + ); + return 0; + }, + xClose: function(pFile){ + const pool = getPoolForPFile(pFile); + pool.storeErr(); + const file = pool.getOFileForS3File(pFile); + if(file) { + try{ + pool.log(`xClose ${file.path}`); + pool.unlockFile(file, capi.SQLITE_LOCK_NONE); + pool.mapS3FileToOFile(pFile, false); + file.sah.flush(); + if(file.flags & capi.SQLITE_OPEN_DELETEONCLOSE){ + pool.deletePath(file.path); + } + }catch(e){ + return pool.storeErr(e, capi.SQLITE_IOERR); + } + } + return 0; + }, + xDeviceCharacteristics: function(pFile){ + return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; + }, + xFileControl: function(pFile, opId, pArg){ + return capi.SQLITE_NOTFOUND; + }, + xFileSize: function(pFile,pSz64){ + const pool = getPoolForPFile(pFile); + pool.log(`xFileSize`); + const file = pool.getOFileForS3File(pFile); + const size = file.sah.getSize() - HEADER_OFFSET_DATA; + + wasm.poke64(pSz64, BigInt(size)); + return 0; + }, + xLock: function(pFile,lockType){ + const pool = getPoolForPFile(pFile); + pool.log(`xLock ${lockType}`); + pool.storeErr(); + const file = pool.getOFileForS3File(pFile); + return pool.lockFile(file, lockType); + }, + xRead: function(pFile,pDest,n,offset64){ + const pool = getPoolForPFile(pFile); + pool.storeErr(); + const file = pool.getOFileForS3File(pFile); + pool.log(`xRead ${file.path} ${n} @ ${offset64}`); + try { + const nRead = file.sah.read( + wasm.heap8u().subarray(Number(pDest), Number(pDest)+n), + {at: HEADER_OFFSET_DATA + Number(offset64)} + ); + if(nRead < n){ + wasm.heap8u().fill(0, Number(pDest) + nRead, Number(pDest) + n); + return capi.SQLITE_IOERR_SHORT_READ; + } + return 0; + }catch(e){ + return pool.storeErr(e, capi.SQLITE_IOERR); + } + }, + xSectorSize: function(pFile){ + return SECTOR_SIZE; + }, + xSync: function(pFile,flags){ + const pool = getPoolForPFile(pFile); + pool.log(`xSync ${flags}`); + pool.storeErr(); + const file = pool.getOFileForS3File(pFile); + + try{ + file.sah.flush(); + return 0; + }catch(e){ + return pool.storeErr(e, capi.SQLITE_IOERR); + } + }, + xTruncate: function(pFile,sz64){ + const pool = getPoolForPFile(pFile); + pool.log(`xTruncate ${sz64}`); + pool.storeErr(); + const file = pool.getOFileForS3File(pFile); + + try{ + file.sah.truncate(HEADER_OFFSET_DATA + Number(sz64)); + return 0; + }catch(e){ + return pool.storeErr(e, capi.SQLITE_IOERR); + } + }, + xUnlock: function(pFile,lockType){ + const pool = getPoolForPFile(pFile); + pool.log('xUnlock'); + const file = pool.getOFileForS3File(pFile); + pool.unlockFile(file, lockType); + return 0; + }, + xWrite: function(pFile,pSrc,n,offset64){ + const pool = getPoolForPFile(pFile); + pool.storeErr(); + const file = pool.getOFileForS3File(pFile); + pool.log(`xWrite ${file.path} ${n} ${offset64}`); + try{ + const nBytes = file.sah.write( + wasm.heap8u().subarray(Number(pSrc), Number(pSrc)+n), + { at: HEADER_OFFSET_DATA + Number(offset64) } + ); + return n===nBytes ? 0 : toss("Unknown write() failure."); + }catch(e){ + return pool.storeErr(e, capi.SQLITE_IOERR); + } + } + }; + + const opfsIoMethods = new capi.sqlite3_io_methods(); + opfsIoMethods.$iVersion = 1; + sqlite3.vfs.installVfs({ + io: {struct: opfsIoMethods, methods: ioMethods} + }); + + + const vfsMethods = { + xAccess: function(pVfs,zName,flags,pOut){ + + const pool = getPoolForVfs(pVfs); + pool.storeErr(); + try{ + const name = pool.getPath(zName); + wasm.poke32(pOut, pool.hasFilename(name) ? 1 : 0); + }catch(e){ + + wasm.poke32(pOut, 0); + } + return 0; + }, + xCurrentTime: function(pVfs,pOut){ + wasm.poke(pOut, 2440587.5 + (new Date().getTime()/86400000), + 'double'); + return 0; + }, + xCurrentTimeInt64: function(pVfs,pOut){ + wasm.poke(pOut, (2440587.5 * 86400000) + new Date().getTime(), + 'i64'); + return 0; + }, + xDelete: function(pVfs, zName, doSyncDir){ + const pool = getPoolForVfs(pVfs); + pool.log(`xDelete ${wasm.cstrToJs(zName)}`); + pool.storeErr(); + try{ + pool.deletePath(pool.getPath(zName)); + return 0; + }catch(e){ + pool.storeErr(e); + return capi.SQLITE_IOERR_DELETE; + } + }, + xFullPathname: function(pVfs,zName,nOut,pOut){ + + + const i = wasm.cstrncpy(pOut, zName, nOut); + return i nOut) wasm.poke8(wasm.ptr.add(pOut,nOut,-1), 0); + }catch(e){ + return capi.SQLITE_NOMEM; + }finally{ + wasm.scopedAllocPop(scope); + } + } + return e ? (e.sqlite3Rc || capi.SQLITE_IOERR) : 0; + }, + + xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){ + const pool = getPoolForVfs(pVfs); + try{ + flags &= ~FLAG_COMPUTE_DIGEST_V2; + pool.log(`xOpen ${wasm.cstrToJs(zName)} ${flags}`); + + const path = (zName && wasm.peek8(zName)) + ? pool.getPath(zName) + : getRandomName(); + let sah = pool.getSAHForPath(path); + if(sah && (flags & capi.SQLITE_OPEN_CREATE) + && (flags & capi.SQLITE_OPEN_EXCLUSIVE)){ + toss('file already exists:', path); + } + if(!sah && (flags & capi.SQLITE_OPEN_CREATE)) { + + if(pool.getFileCount() < pool.getCapacity()) { + + sah = pool.nextAvailableSAH(); + pool.setAssociatedPath(sah, path, flags); + }else{ + + toss('SAH pool is full. Cannot create file',path); + } + } + if(!sah){ + toss('file not found:',path); + } + + + const file = {path, flags, sah}; + pool.mapS3FileToOFile(pFile, file); + file.lockType = capi.SQLITE_LOCK_NONE; + const sq3File = new capi.sqlite3_file(pFile); + sq3File.$pMethods = opfsIoMethods.pointer; + sq3File.dispose(); + wasm.poke32(pOutFlags, flags); + return 0; + }catch(e){ + pool.storeErr(e); + return capi.SQLITE_CANTOPEN; + } + } + }; + + + const createOpfsVfs = function(vfsName){ + if( sqlite3.capi.sqlite3_vfs_find(vfsName)){ + toss3("VFS name is already registered:", vfsName); + } + const opfsVfs = new capi.sqlite3_vfs(); + + const pDVfs = capi.sqlite3_vfs_find(null); + const dVfs = pDVfs + ? new capi.sqlite3_vfs(pDVfs) + : null ; + opfsVfs.$iVersion = 2; + opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof; + opfsVfs.$mxPathname = HEADER_MAX_PATH_SIZE; + opfsVfs.addOnDispose( + opfsVfs.$zName = wasm.allocCString(vfsName), + ()=>setPoolForVfs(opfsVfs.pointer, 0) + ); + + if(dVfs){ + + opfsVfs.$xRandomness = dVfs.$xRandomness; + dVfs.dispose(); + } + if(!opfsVfs.$xRandomness && !vfsMethods.xRandomness){ + + vfsMethods.xRandomness = function(pVfs, nOut, pOut){ + const heap = wasm.heap8u(); + let i = 0; + const npOut = Number(pOut); + for(; i < nOut; ++i) heap[npOut + i] = (Math.random()*255000) & 0xFF; + return i; + }; + } + + vfsMethods.xSleep = (pVfs,ms)=>0; + sqlite3.vfs.installVfs({ + vfs: {struct: opfsVfs, methods: vfsMethods} + }); + return opfsVfs; + }; + + + class OpfsSAHPool { + + vfsDir; + + #dhVfsRoot; + + #dhOpaque; + + #dhVfsParent; + + #mapSAHToName = new Map(); + + #mapFilenameToSAH = new Map(); + + #availableSAH = new Set(); + + #mapS3FileToOFile = new Map(); + + #mapPathToLock = new Map(); + + + + + #apBody = new Uint8Array(HEADER_CORPUS_SIZE); + + #dvBody; + + + #cVfs; + + + #verbosity; + + + constructor(options = Object.create(null)){ + this.#verbosity = options.verbosity ?? optionDefaults.verbosity; + this.vfsName = options.name || optionDefaults.name; + this.#cVfs = createOpfsVfs(this.vfsName); + setPoolForVfs(this.#cVfs.pointer, this); + this.vfsDir = options.directory || ("."+this.vfsName); + this.#dvBody = + new DataView(this.#apBody.buffer, this.#apBody.byteOffset); + this.isReady = this + .reset(!!(options.clearOnInit ?? optionDefaults.clearOnInit)) + .then(()=>{ + if(this.$error) throw this.$error; + return this.getCapacity() + ? Promise.resolve(undefined) + : this.addCapacity(options.initialCapacity + || optionDefaults.initialCapacity); + }); + } + + #logImpl(level,...args){ + if(this.#verbosity>level) loggers[level](this.vfsName+":",...args); + }; + log(...args){this.#logImpl(2, ...args)}; + warn(...args){this.#logImpl(1, ...args)}; + error(...args){this.#logImpl(0, ...args)}; + + + getVfs(){return this.#cVfs} + + + getCapacity(){return this.#mapSAHToName.size} + + + getFileCount(){return this.#mapFilenameToSAH.size} + + + getFileNames(){ + const rc = []; + for(const n of this.#mapFilenameToSAH.keys()) rc.push(n); + return rc; + } + + + async addCapacity(n){ + for(let i = 0; i < n; ++i){ + const name = getRandomName(); + const h = await this.#dhOpaque.getFileHandle(name, {create:true}); + const ah = await h.createSyncAccessHandle(); + this.#mapSAHToName.set(ah,name); + this.setAssociatedPath(ah, '', 0); + + } + return this.getCapacity(); + } + + + async reduceCapacity(n){ + if(!Number.isSafeInteger(n) || n < 0){ + toss('Invalid capacity reduction:', n); + } + let nRm = 0; + for(const ah of Array.from(this.#availableSAH)){ + if(nRm === n || this.getFileCount() === this.getCapacity()){ + break; + } + const name = this.#mapSAHToName.get(ah); + ah.close(); + this.#mapSAHToName.delete(ah); + this.#availableSAH.delete(ah); + await this.#dhOpaque.removeEntry(name); + ++nRm; + } + return nRm; + } + + + releaseAccessHandles(){ + for(const ah of this.#mapSAHToName.keys()) ah.close(); + this.#mapSAHToName.clear(); + this.#mapFilenameToSAH.clear(); + this.#availableSAH.clear(); + this.#mapS3FileToOFile.clear(); + this.#mapPathToLock.clear(); + } + + + async acquireAccessHandles(clearFiles=false){ + const files = []; + for await (const [name,h] of this.#dhOpaque){ + if('file'===h.kind){ + files.push([name,h]); + } + } + const results = await Promise.allSettled(files.map(async([name,h])=>{ + const ah = await h.createSyncAccessHandle() + this.#mapSAHToName.set(ah, name); + if(clearFiles){ + ah.truncate(HEADER_OFFSET_DATA); + this.setAssociatedPath(ah, '', 0); + }else{ + const path = this.getAssociatedPath(ah); + if(path){ + this.#mapFilenameToSAH.set(path, ah); + }else{ + this.#availableSAH.add(ah); + } + } + })); + const failure = results.find((r)=>'rejected'===r.status) + ; + if( failure ){ + this.storeErr(failure.reason); + this.releaseAccessHandles(); + throw failure.reason; + } + } + + + getAssociatedPath(sah){ + sah.read(this.#apBody, {at: 0}); + + + const flags = this.#dvBody.getUint32(HEADER_OFFSET_FLAGS); + if(this.#apBody[0] && + ((flags & capi.SQLITE_OPEN_DELETEONCLOSE) || + (flags & PERSISTENT_FILE_TYPES)===0)){ + warn(`Removing file with unexpected flags ${flags.toString(16)}`, + this.#apBody); + this.setAssociatedPath(sah, '', 0); + return ''; + } + + const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4); + sah.read(fileDigest, {at: HEADER_OFFSET_DIGEST}); + const compDigest = this.computeDigest(this.#apBody, flags); + + if(fileDigest.every((v,i) => v===compDigest[i])){ + + const pathBytes = this.#apBody.findIndex((v)=>0===v); + if(0===pathBytes){ + + + sah.truncate(HEADER_OFFSET_DATA); + } + + return pathBytes + ? textDecoder.decode(this.#apBody.subarray(0,pathBytes)) + : ''; + }else{ + + warn('Disassociating file with bad digest.'); + this.setAssociatedPath(sah, '', 0); + return ''; + } + } + + + setAssociatedPath(sah, path, flags){ + const enc = textEncoder.encodeInto(path, this.#apBody); + if(HEADER_MAX_PATH_SIZE <= enc.written + 1){ + toss("Path too long:",path); + } + if(path && flags){ + + flags |= FLAG_COMPUTE_DIGEST_V2; + } + this.#apBody.fill(0, enc.written, HEADER_MAX_PATH_SIZE); + this.#dvBody.setUint32(HEADER_OFFSET_FLAGS, flags); + const digest = this.computeDigest(this.#apBody, flags); + + sah.write(this.#apBody, {at: 0}); + sah.write(digest, {at: HEADER_OFFSET_DIGEST}); + sah.flush(); + + if(path){ + this.#mapFilenameToSAH.set(path, sah); + this.#availableSAH.delete(sah); + }else{ + + sah.truncate(HEADER_OFFSET_DATA); + this.#availableSAH.add(sah); + } + } + + + computeDigest(byteArray, fileFlags){ + if( fileFlags & FLAG_COMPUTE_DIGEST_V2 ){ + let h1 = 0xdeadbeef; + let h2 = 0x41c6ce57; + for(const v of byteArray){ + h1 = Math.imul(h1 ^ v, 2654435761); + h2 = Math.imul(h2 ^ v, 104729); + } + return new Uint32Array([h1>>>0, h2>>>0]); + }else{ + + return new Uint32Array([0,0]); + } + } + + + async reset(clearFiles){ + await this.isReady; + let h = await navigator.storage.getDirectory(); + let prev, prevName; + for(const d of this.vfsDir.split('/')){ + if(d){ + prev = h; + h = await h.getDirectoryHandle(d,{create:true}); + } + } + this.#dhVfsRoot = h; + this.#dhVfsParent = prev; + this.#dhOpaque = await this.#dhVfsRoot.getDirectoryHandle( + OPAQUE_DIR_NAME,{create:true} + ); + this.releaseAccessHandles(); + return this.acquireAccessHandles(clearFiles); + } + + + getPath(arg) { + if(wasm.isPtr(arg)) arg = wasm.cstrToJs(arg); + return ((arg instanceof URL) + ? arg + : new URL(arg, 'file://localhost/')).pathname; + } + + + deletePath(path) { + const sah = this.#mapFilenameToSAH.get(path); + if(sah) { + + this.#mapFilenameToSAH.delete(path); + this.setAssociatedPath(sah, '', 0); + } + return !!sah; + } + + + storeErr(e,code){ + if(e){ + e.sqlite3Rc = code || capi.SQLITE_IOERR; + this.error(e); + } + this.$error = e; + return code; + } + + popErr(){ + const rc = this.$error; + this.$error = undefined; + return rc; + } + + + nextAvailableSAH(){ + const [rc] = this.#availableSAH.keys(); + return rc; + } + + + getOFileForS3File(pFile){ + return this.#mapS3FileToOFile.get(pFile); + } + + mapS3FileToOFile(pFile,file){ + if(file){ + this.#mapS3FileToOFile.set(pFile, file); + setPoolForPFile(pFile, this); + }else{ + this.#mapS3FileToOFile.delete(pFile); + setPoolForPFile(pFile, false); + } + } + + + hasFilename(name){ + return this.#mapFilenameToSAH.has(name) + } + + + getSAHForPath(path){ + return this.#mapFilenameToSAH.get(path); + } + + + async removeVfs(){ + if(!this.#cVfs.pointer || !this.#dhOpaque) return false; + capi.sqlite3_vfs_unregister(this.#cVfs.pointer); + this.#cVfs.dispose(); + delete initPromises[this.vfsName]; + try{ + this.releaseAccessHandles(); + await this.#dhVfsRoot.removeEntry(OPAQUE_DIR_NAME, {recursive: true}); + this.#dhOpaque = undefined; + await this.#dhVfsParent.removeEntry( + this.#dhVfsRoot.name, {recursive: true} + ); + this.#dhVfsRoot = this.#dhVfsParent = undefined; + }catch(e){ + sqlite3.config.error(this.vfsName,"removeVfs() failed with no recovery strategy:",e); + + } + return true; + } + + + pauseVfs(){ + if(this.#mapS3FileToOFile.size>0){ + sqlite3.SQLite3Error.toss( + capi.SQLITE_MISUSE, "Cannot pause VFS", + this.vfsName,"because it has opened files." + ); + } + if(this.#mapSAHToName.size>0){ + capi.sqlite3_vfs_unregister(this.vfsName); + this.releaseAccessHandles(); + } + return this; + } + + + isPaused(){ + return 0===this.#mapSAHToName.size; + } + + + async unpauseVfs(){ + if(0===this.#mapSAHToName.size){ + return this.acquireAccessHandles(false). + then(()=>capi.sqlite3_vfs_register(this.#cVfs, 0),this); + } + return this; + } + + + exportFile(name){ + const sah = this.#mapFilenameToSAH.get(name) || toss("File not found:",name); + const n = sah.getSize() - HEADER_OFFSET_DATA; + const b = new Uint8Array(n>0 ? n : 0); + if(n>0){ + const nRead = sah.read(b, {at: HEADER_OFFSET_DATA}); + if(nRead != n){ + toss("Expected to read "+n+" bytes but read "+nRead+"."); + } + } + return b; + } + + + async importDbChunked(name, callback){ + const sah = this.#mapFilenameToSAH.get(name) + || this.nextAvailableSAH() + || toss("No available handles to import to."); + sah.truncate(0); + let nWrote = 0, chunk, checkedHeader = false, err = false; + try{ + while( undefined !== (chunk = await callback()) ){ + if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk); + if( !checkedHeader && 0===nWrote && chunk.byteLength>=15 ){ + util.affirmDbHeader(chunk); + checkedHeader = true; + } + const n = sah.write(chunk, {at: HEADER_OFFSET_DATA + nWrote}); + if( n!==chunk.byteLength ){ + toss("Expected to write "+chunk.byteLength+ + "bytes but wrote"+n+"."); + } + nWrote += chunk.byteLength; + } + if( nWrote < 512 || 0!==nWrote % 512 ){ + toss("Input size",nWrote,"is not correct for an SQLite database."); + } + if( !checkedHeader ){ + const header = new Uint8Array(20); + sah.read( header, {at: HEADER_OFFSET_DATA} ); + util.affirmDbHeader( header ); + } + sah.write(new Uint8Array([1,1]), { + at: HEADER_OFFSET_DATA + 18 + }); + }catch(e){ + this.setAssociatedPath(sah, '', 0); + throw e; + } + this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB); + return nWrote; + } + + + importDb(name, bytes){ + if( bytes instanceof ArrayBuffer ) bytes = new Uint8Array(bytes); + else if( bytes instanceof Function ) return this.importDbChunked(name, bytes); + util.affirmIsDb(bytes); + const sah = this.#mapFilenameToSAH.get(name) + || this.nextAvailableSAH() + || toss("No available handles to import to."); + const n = bytes.byteLength; + const nWrote = sah.write(bytes, {at: HEADER_OFFSET_DATA}); + if(nWrote != n){ + this.setAssociatedPath(sah, '', 0); + toss("Expected to write "+n+" bytes but wrote "+nWrote+"."); + }else{ + sah.truncate(HEADER_OFFSET_DATA + n + ); + sah.write(new Uint8Array([1,1]), {at: HEADER_OFFSET_DATA+18} + ); + this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB); + } + return nWrote; + } + + + hasReservedLock(filename){ + const lk = this.#mapPathToLock.get(filename); + return !!lk && lk.lockType>capi.SQLITE_LOCK_SHARED; + } + + + lockFile(file, lockType){ + if(file.lockType>=lockType) return 0; + let lk = this.#mapPathToLock.get(file.path); + if(!lk){ + lk = Object.assign(Object.create(null),{ + nShared: 0, lockType: capi.SQLITE_LOCK_NONE + }); + this.#mapPathToLock.set(file.path, lk); + } + if(file.lockType!==lk.lockType + && (lk.lockType>=capi.SQLITE_LOCK_PENDING + || lockType>capi.SQLITE_LOCK_SHARED)){ + return capi.SQLITE_BUSY; + } + if(lockType===capi.SQLITE_LOCK_SHARED){ + ++lk.nShared; + if(lk.lockType===capi.SQLITE_LOCK_NONE) lk.lockType = lockType; + }else if(lockType===capi.SQLITE_LOCK_EXCLUSIVE && lk.nShared>1){ + if(file.lockType===capi.SQLITE_LOCK_RESERVED){ + file.lockType = lk.lockType = capi.SQLITE_LOCK_PENDING; + } + return capi.SQLITE_BUSY; + }else{ + lk.lockType = lockType; + } + file.lockType = lockType; + return 0; + } + + + unlockFile(file, lockType){ + if(file.lockType<=lockType) return; + const lk = this.#mapPathToLock.get(file.path); + if(file.lockType>capi.SQLITE_LOCK_SHARED){ + lk.lockType = capi.SQLITE_LOCK_SHARED; + } + if(lockType===capi.SQLITE_LOCK_NONE && 0===--lk.nShared + ){ + this.#mapPathToLock.delete(file.path); + } + file.lockType = lockType; + } + + }; + + + class OpfsSAHPoolUtil { + + #p; + + constructor(sahPool){ + this.#p = sahPool; + this.vfsName = sahPool.vfsName; + } + + async addCapacity(n){ return this.#p.addCapacity(n) } + + async reduceCapacity(n){ return this.#p.reduceCapacity(n) } + + getCapacity(){ return this.#p.getCapacity(this.#p) } + + getFileCount(){ return this.#p.getFileCount() } + getFileNames(){ return this.#p.getFileNames() } + + async reserveMinimumCapacity(min){ + const c = this.#p.getCapacity(); + return (c < min) ? this.#p.addCapacity(min - c) : c; + } + + exportFile(name){ return this.#p.exportFile(name) } + + importDb(name, bytes){ return this.#p.importDb(name,bytes) } + + async wipeFiles(){ return this.#p.reset(true) } + + unlink(filename){ return this.#p.deletePath(filename) } + + async removeVfs(){ return this.#p.removeVfs() } + + pauseVfs(){ this.#p.pauseVfs(); return this; } + async unpauseVfs(){ return this.#p.unpauseVfs().then(()=>this); } + isPaused(){ return this.#p.isPaused() } + + }; + + + const apiVersionCheck = async ()=>{ + const dh = await navigator.storage.getDirectory(); + const fn = '.opfs-sahpool-sync-check-'+getRandomName(); + const fh = await dh.getFileHandle(fn, { create: true }); + const ah = await fh.createSyncAccessHandle(); + const close = ah.close(); + await close; + await dh.removeEntry(fn); + if(close?.then){ + toss("The local OPFS API is too old for opfs-sahpool:", + "it has an async FileSystemSyncAccessHandle.close() method."); + } + return true; + }; + + + sqlite3.installOpfsSAHPoolVfs = async function(options=Object.create(null)){ + options = Object.assign(Object.create(null), optionDefaults, (options||{})); + const vfsName = options.name; + if(options.$testThrowPhase1){ + throw options.$testThrowPhase1; + } + if(initPromises[vfsName]){ + try { + const p = await initPromises[vfsName]; + + return p; + }catch(e){ + + if( options.forceReinitIfPreviouslyFailed ){ + delete initPromises[vfsName]; + + }else{ + throw e; + } + } + } + if(!globalThis.FileSystemHandle || + !globalThis.FileSystemDirectoryHandle || + !globalThis.FileSystemFileHandle || + !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle || + !navigator?.storage?.getDirectory){ + return (initPromises[vfsName] = Promise.reject(new Error("Missing required OPFS APIs."))); + } + + + const unregisterVfs = (thePool)=>{ + const cVfs = thePool.getVfs(); + if( cVfs?.pointer ){ + capi.sqlite3_vfs_unregister(cVfs.pointer); + cVfs.dispose(); + thePool.releaseAccessHandles(); + } + }; + + + return initPromises[vfsName] = apiVersionCheck().then(async function(){ + if(options.$testThrowPhase2){ + throw options.$testThrowPhase2; + } + const thePool = new OpfsSAHPool(options); + return thePool.isReady.then(async()=>{ + + const poolUtil = new OpfsSAHPoolUtil(thePool); + if(sqlite3.oo1){ + const oo1 = sqlite3.oo1; + const theVfs = thePool.getVfs(); + const OpfsSAHPoolDb = function(...args){ + const opt = oo1.DB.dbCtorHelper.normalizeArgs(...args); + opt.vfs = theVfs.$zName; + oo1.DB.dbCtorHelper.call(this, opt); + }; + OpfsSAHPoolDb.prototype = Object.create(oo1.DB.prototype); + poolUtil.OpfsSAHPoolDb = OpfsSAHPoolDb; + } + thePool.log("VFS initialized."); + return poolUtil; + }).catch(async (e)=>{ + if( options.preserveOnInitFailure ){ + unregisterVfs(thePool); + }else{ + await thePool.removeVfs().catch(()=>{}); + } + throw e; + }); + }).catch((err)=>{ + + return initPromises[vfsName] = Promise.reject(err); + }); + }; +}); + +'use strict'; +globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ + if( !sqlite3.opfs || sqlite3.config.disable?.vfs?.['opfs-wl'] ){ + return; + } + const util = sqlite3.util, + toss = sqlite3.util.toss; + const opfsUtil = sqlite3.opfs; + const vfsName = 'opfs-wl'; + +const installOpfsWlVfs = async function(options){ + options = opfsUtil.initOptions(vfsName,options); + if( !options ) return sqlite3; + const capi = sqlite3.capi, + state = opfsUtil.createVfsState(), + opfsVfs = state.vfs, + metrics = opfsVfs.metrics.counters, + mTimeStart = opfsVfs.mTimeStart, + mTimeEnd = opfsVfs.mTimeEnd, + opRun = opfsVfs.opRun, + debug = (...args)=>sqlite3.config.debug(vfsName+":",...args), + warn = (...args)=>sqlite3.config.warn(vfsName+":",...args), + __openFiles = opfsVfs.__openFiles; + + + + return opfsVfs.bindVfs(util.nu({ + xLock: function(pFile,lockType){ + mTimeStart('xLock'); + + const f = __openFiles[pFile]; + const rc = opRun('xLock', pFile, lockType); + if( !rc ) f.lockType = lockType; + mTimeEnd(); + return rc; + }, + xUnlock: function(pFile,lockType){ + mTimeStart('xUnlock'); + const f = __openFiles[pFile]; + const rc = opRun('xUnlock', pFile, lockType); + if( !rc ) f.lockType = lockType; + mTimeEnd(); + return rc; + } + }), function(sqlite3, vfs){ + + if(sqlite3.oo1){ + const OpfsWlDb = function(...args){ + const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args); + opt.vfs = vfs.$zName; + sqlite3.oo1.DB.dbCtorHelper.call(this, opt); + }; + OpfsWlDb.prototype = Object.create(sqlite3.oo1.DB.prototype); + sqlite3.oo1.OpfsWlDb = OpfsWlDb; + OpfsWlDb.importDb = opfsUtil.importDb; + + } + }); +}; +globalThis.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{ + return installOpfsWlVfs().catch((e)=>{ + sqlite3.config.warn("Ignoring inability to install the",vfsName,"sqlite3_vfs:",e); + }); +}); +}); + +try{ + + + + const bootstrapConfig = Object.assign( + Object.create(null), + + { + memory: ('undefined'!==typeof wasmMemory) + ? wasmMemory + : EmscriptenModule['wasmMemory'], + exports: ('undefined'!==typeof wasmExports) + ? wasmExports + : (Object.prototype.hasOwnProperty.call(EmscriptenModule,'wasmExports') + ? EmscriptenModule['wasmExports'] + : EmscriptenModule['asm']) + }, + globalThis.sqlite3ApiBootstrap.defaultConfig, + globalThis.sqlite3ApiConfig || {} + ); + + sqlite3InitScriptInfo.debugModule("Bootstrapping lib config", bootstrapConfig); + + + const p = globalThis.sqlite3ApiBootstrap(bootstrapConfig); + delete globalThis.sqlite3ApiBootstrap; + return p ; +}catch(e){ + console.error("sqlite3ApiBootstrap() error:",e); + throw e; +} + + +}; + + + + + + + + + + + + return Module; +} + + + + + +const toExportForESM = +(function(){ + + + const originalInit = sqlite3InitModule; + if(!originalInit){ + throw new Error("Expecting sqlite3InitModule to be defined by the Emscripten build."); + } + + const sIMS = globalThis.sqlite3InitModuleState = Object.assign(Object.create(null),{ + moduleScript: globalThis?.document?.currentScript, + isWorker: ('undefined' !== typeof WorkerGlobalScope), + location: globalThis.location, + urlParams: globalThis?.location?.href + ? new URL(globalThis.location.href).searchParams + : new URLSearchParams(), + + wasmFilename: 'sqlite3.wasm' + }); + sIMS.debugModule = + sIMS.urlParams.has('sqlite3.debugModule') + ? (...args)=>console.warn('sqlite3.debugModule:',...args) + : ()=>{}; + + if(sIMS.urlParams.has('sqlite3.dir')){ + sIMS.sqlite3Dir = sIMS.urlParams.get('sqlite3.dir') +'/'; + }else if(sIMS.moduleScript){ + const li = sIMS.moduleScript.src.split('/'); + li.pop(); + sIMS.sqlite3Dir = li.join('/') + '/'; + } + + const sIM = globalThis.sqlite3InitModule = function ff(...args){ + + sIMS.emscriptenLocateFile = args[0]?.locateFile ; + sIMS.emscriptenInstantiateWasm = args[0]?.instantiateWasm ; + return originalInit(...args).then((EmscriptenModule)=>{ + sIMS.debugModule("sqlite3InitModule() sIMS =",sIMS); + sIMS.debugModule("sqlite3InitModule() EmscriptenModule =",EmscriptenModule); + const s = EmscriptenModule.runSQLite3PostLoadInit( + sIMS, + EmscriptenModule , + !!ff.__isUnderTest + ); + sIMS.debugModule("sqlite3InitModule() sqlite3 =",s); + + + return s; + }).catch((e)=>{ + console.error("Exception loading sqlite3 module:",e); + throw e; + }); + }; + sIM.ready = originalInit.ready; + + if(sIMS.moduleScript){ + let src = sIMS.moduleScript.src.split('/'); + src.pop(); + sIMS.scriptDir = src.join('/') + '/'; + } + sIMS.debugModule('extern-post-js.c-pp.js sqlite3InitModuleState =',sIMS); + return sIM; +})(); +sqlite3InitModule = toExportForESM; +export default sqlite3InitModule; diff --git a/vendor/sqlite3mc/2.6.1-3.54.0/sqlite3.wasm b/vendor/sqlite3mc/2.6.1-3.54.0/sqlite3.wasm new file mode 100644 index 000000000..970116279 Binary files /dev/null and b/vendor/sqlite3mc/2.6.1-3.54.0/sqlite3.wasm differ