@mcp-b/do-runtime 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-wasm.js","names":["#host","#prefix","#provider","#ownedFiles","#filename","#database","#setLengthLimit","#pragma","#closed","#statement"],"sources":["../../backends/sqlite-wasm.ts"],"sourcesContent":["/**\n * ← workerd `NO upstream correspondence (storage-backend adaptation)`\n *\n * `SqlDatabaseProvider` over the browser's OPFS SAH pool.\n *\n * The pool is a parameter, not something this module goes and gets. Installing\n * the VFS is the host's job — `installOpfsSAHPoolVfs` decides the OPFS\n * directory, the pool capacity and whether to clear on init, all of which are\n * layout questions this package deliberately knows nothing about. What arrives\n * here is the already-installed pool, and with it the two things a backend\n * needs that a bare `sqlite3` module cannot give: a database constructor bound\n * to that VFS, plus the pool's file export/import/unlink operations used by\n * snapshots and `reset()`.\n *\n * The pool is structurally typed rather than imported from\n * `@sqlite.org/sqlite-wasm`, so this package takes no dependency on the driver\n * and the caller is free to pass a pool from any build of it. The shape below\n * is the subset of `SAHPoolUtil` and `oo1.DB` that is used, copied from the\n * driver's own `.d.mts`.\n *\n * NOT exercised by the unit lane: it needs OPFS, which means a browser. It is\n * exercised twice in the browser lane — by `sqlite-wasm.smoke.spec.ts`, which\n * drives this file directly, and by the conformance suite, which runs the whole\n * package over it.\n */\n\nimport {\n requireSqliteLength,\n requireSafeDatabaseName,\n requireValidSqlDatabaseSnapshot,\n SQLITE_LENGTH_LIMIT,\n SQL_WRONG_BINDINGS_MESSAGE,\n type SqlDatabase,\n type SqlDatabaseProvider,\n type SqlDatabaseSnapshot,\n type SqlDatabaseSnapshotProvider,\n type SqlDatabaseStatement,\n type SqlResult,\n type SqlValue,\n} from \"../src/util/sqlite\";\nimport { requireImportableRuntimeStorage } from \"../src/util/sqlite-migrations\";\n\n/** ← `PreparedStatement`, the members used here. */\nexport interface SqliteWasmStatement {\n readonly columnCount: number;\n readonly parameterCount: number;\n bind(bindings: readonly (string | number | bigint | null | Uint8Array)[]): unknown;\n step(): boolean;\n get(index: number): unknown;\n getColumnNames(target?: string[]): string[];\n finalize(): number | undefined;\n}\n\n/** ← `oo1.DB` / `OpfsSAHPoolDatabase`, the members used here. */\nexport interface SqliteWasmDatabaseHandle {\n /** ← `oo1.DB.pointer`, which is absent once the handle is closed. */\n readonly pointer?: number | undefined;\n prepare(sql: string): SqliteWasmStatement;\n changes(total?: boolean, sixtyFour?: false): number;\n close(): void;\n}\n\n/** ← `SAHPoolUtil`, the members used here. */\nexport interface OpfsSahPool {\n /** Constructs a database inside this pool's VFS. Names are absolute, so they start with \"/\". */\n readonly OpfsSAHPoolDb: new (filename: string) => SqliteWasmDatabaseHandle;\n exportFile(filename: string): Uint8Array | Promise<Uint8Array>;\n importDb(filename: string, image: Uint8Array): number | Promise<number>;\n getFileNames(): string[];\n /** Disassociates a virtual file from the pool. Results are undefined if it is in active use. */\n unlink(filename: string): boolean;\n}\n\n/**\n * ← `Sqlite3Static[\"capi\"]`, restricted to the one C function `oo1.DB` does not\n * wrap.\n *\n * Takes the pointer rather than the handle, even though upstream's `DbPtr`\n * accepts either: a structural subset of `oo1.DB` is not assignable to the\n * `Database` class, so asking for the handle would make the real `capi` fail to\n * satisfy this interface.\n */\nexport interface SqliteWasmCapi {\n readonly SQLITE_LIMIT_LENGTH: number;\n sqlite3_complete(sql: string): 0 | 1;\n sqlite3_get_autocommit(db: number): number;\n sqlite3_limit(db: number, id: number, newValue: number): number;\n}\n\n/**\n * What the host hands over: the pool it installed, and the C-API namespace it\n * already holds. Both come off the same `sqlite3` object the caller used to\n * call `installOpfsSAHPoolVfs`, so this asks for nothing it does not have.\n */\nexport interface SqliteWasmHost {\n readonly pool: OpfsSahPool;\n readonly capi: SqliteWasmCapi;\n}\n\nexport type SqliteWasmProviderOptions = {\n /** Absolute path prefix inside the pool, e.g. `/actor-<id>`. Must start with \"/\". */\n prefix: string;\n};\n\n/**\n * One actor's named databases and their file lifecycle inside an OPFS SAH pool.\n *\n * A root or facet container only needs the `SqlDatabaseProvider` surface. Its\n * host also has to close every connection when that placement dies, remove the\n * prefix on delete, and copy every database on clone. Those file operations\n * belong here because SAH-pool files are virtual and can only be reached through\n * the pool that owns them.\n */\nexport class SqliteWasmActorStorage implements SqlDatabaseProvider {\n readonly #host: SqliteWasmHost;\n readonly #prefix: string;\n readonly #provider: SqlDatabaseSnapshotProvider;\n\n constructor(host: SqliteWasmHost, prefix: string) {\n this.#host = host;\n this.#prefix = prefix;\n this.#provider = createSqliteWasmProvider(host, { prefix });\n }\n\n open(name: string): Promise<SqlDatabase> {\n return this.#provider.open(name);\n }\n\n /**\n * Drop every handle. Leaving one behind per respawn or facet abort would\n * accumulate concurrent writers inside a VFS that expects to own its files.\n */\n close(): void {\n this.#provider.close();\n }\n\n /** Close every handle, then physically remove every database under this prefix. */\n deleteAll(): void {\n this.close();\n for (const file of this.#ownedFiles()) {\n if (!this.#host.pool.unlink(file)) throw new Error(`SAH pool did not unlink ${file}`);\n }\n }\n\n /**\n * Replace this prefix with every database under `source`, including files\n * from an earlier placement that this session never opened.\n *\n * The source may still be running, so this uses the pool's file operations\n * rather than the snapshot API, which correctly refuses open handles. A\n * recovery sidecar means the bytes are not a stable database image and is\n * refused before the destination is touched. Every source image is also\n * exported before replacement starts, so a failed read preserves the target.\n */\n async copyFrom(source: SqliteWasmActorStorage): Promise<void> {\n const files = source.#ownedFiles();\n const sidecar = files.find((file) => !file.endsWith(\".sqlite\"));\n if (sidecar !== undefined) {\n throw new Error(`Cannot clone actor storage with a SQLite recovery sidecar: ${sidecar}`);\n }\n const images: Array<{ name: string; bytes: Uint8Array }> = [];\n for (const file of files) {\n const name = file.slice(source.#prefix.length + 1, -\".sqlite\".length);\n requireSafeDatabaseName(name);\n images.push({\n name,\n bytes: new Uint8Array(await source.#host.pool.exportFile(file)),\n });\n }\n this.deleteAll();\n for (const { name, bytes } of images) {\n await this.#host.pool.importDb(\n `${this.#prefix}.${name}.sqlite`,\n bytes,\n );\n }\n }\n\n #ownedFiles(): string[] {\n return this.#host.pool\n .getFileNames()\n .filter((name) => name.startsWith(`${this.#prefix}.`));\n }\n}\n\nexport function createSqliteWasmProvider(\n host: SqliteWasmHost,\n options: SqliteWasmProviderOptions,\n): SqlDatabaseSnapshotProvider {\n const { prefix } = options;\n if (!prefix.startsWith(\"/\")) {\n throw new Error(`SAH pool names are absolute; prefix must start with \"/\": ${prefix}`);\n }\n const openDatabases = new Set<SqliteWasmDatabase>();\n const ownedFiles = (): string[] =>\n host.pool.getFileNames().filter((name) => name.startsWith(`${prefix}.`));\n return {\n async open(name: string): Promise<SqlDatabase> {\n // Names come from inside the package, so this is defence in depth — but\n // it is the one place a name becomes a pool file name.\n requireSafeDatabaseName(name);\n let database: SqliteWasmDatabase;\n database = new SqliteWasmDatabase(host, `${prefix}.${name}.sqlite`, () =>\n openDatabases.delete(database),\n );\n openDatabases.add(database);\n return database;\n },\n close(): void {\n for (const database of [...openDatabases]) database.close();\n },\n async exportSnapshot(): Promise<SqlDatabaseSnapshot> {\n requireClosed(openDatabases);\n const files = ownedFiles();\n requireNoRecoverySidecars(files);\n const databases = await Promise.all(\n files\n .filter((file) => file.endsWith(\".sqlite\"))\n .sort()\n .map(async (file) => {\n const name = file.slice(prefix.length + 1, -\".sqlite\".length);\n requireSafeDatabaseName(name);\n return { name, image: new Uint8Array(await host.pool.exportFile(file)) };\n }),\n );\n const snapshot: SqlDatabaseSnapshot = { version: 1, databases };\n requireValidSqlDatabaseSnapshot(snapshot);\n return snapshot;\n },\n async importSnapshot(snapshot: SqlDatabaseSnapshot): Promise<void> {\n requireClosed(openDatabases);\n requireValidSqlDatabaseSnapshot(snapshot);\n requireImportableRuntimeStorage(snapshot);\n for (const file of ownedFiles()) host.pool.unlink(file);\n for (const { name, image } of snapshot.databases) {\n await host.pool.importDb(`${prefix}.${name}.sqlite`, new Uint8Array(image));\n }\n },\n };\n}\n\nexport class SqliteWasmDatabase implements SqlDatabase {\n readonly #host: SqliteWasmHost;\n readonly #filename: string;\n #database: SqliteWasmDatabaseHandle;\n #closed = false;\n\n constructor(\n host: SqliteWasmHost,\n filename: string,\n private readonly onClose: () => void = () => {},\n ) {\n this.#host = host;\n this.#filename = filename;\n this.#database = new host.pool.OpfsSAHPoolDb(filename);\n this.#setLengthLimit();\n }\n\n prepare(sql: string): SqlDatabaseStatement {\n const source = firstCompleteStatement(this.#host.capi, sql);\n return new WasmSqlStatement(this.#database, this.#database.prepare(source), source);\n }\n\n exec(sql: string, params: readonly SqlValue[]): SqlResult {\n const statement = this.prepare(sql);\n try {\n return statement.execute(params);\n } finally {\n statement.close();\n }\n }\n\n get databaseSize(): number {\n const pageCount = this.#pragma(\"page_count\");\n const pageSize = this.#pragma(\"page_size\");\n return pageCount * pageSize;\n }\n\n /**\n * `oo1.DB` wraps no equivalent, so this is the one place the backend reaches\n * past it into the C API. `DbPtr` accepts the database object itself.\n */\n get inTransaction(): boolean {\n const pointer = this.#database.pointer;\n if (pointer === undefined) throw new Error(\"The database handle is closed.\");\n return this.#host.capi.sqlite3_get_autocommit(pointer) === 0;\n }\n\n reset(): void {\n // The pool's files are not visible in OPFS under these names, so deleting\n // one goes through the pool rather than through the filesystem. The handle\n // has to be closed first: `unlink`'s results are undefined for a file in\n // active use.\n this.#database.close();\n if (!this.#host.pool.unlink(this.#filename)) {\n throw new Error(`SAH pool did not unlink ${this.#filename}`);\n }\n this.#database = new this.#host.pool.OpfsSAHPoolDb(this.#filename);\n this.#setLengthLimit();\n }\n\n close(): void {\n if (this.#closed) return;\n this.#database.close();\n this.#closed = true;\n this.onClose();\n }\n\n #setLengthLimit(): void {\n const pointer = this.#database.pointer;\n if (pointer === undefined) throw new Error(\"The database handle is closed.\");\n this.#host.capi.sqlite3_limit(\n pointer,\n this.#host.capi.SQLITE_LIMIT_LENGTH,\n SQLITE_LENGTH_LIMIT,\n );\n }\n\n #pragma(name: string): number {\n const row = this.exec(`PRAGMA ${name}`, []).rawRows[0];\n const value = row?.[0];\n if (typeof value !== \"number\") {\n throw new Error(`PRAGMA ${name} did not return a number.`);\n }\n return value;\n }\n}\n\nfunction requireClosed(openDatabases: ReadonlySet<SqliteWasmDatabase>): void {\n if (openDatabases.size > 0) {\n throw new Error(\"Cannot snapshot or restore while database handles are open.\");\n }\n}\n\nfunction requireNoRecoverySidecars(files: readonly string[]): void {\n const sidecar = files.find((file) => !file.endsWith(\".sqlite\"));\n if (sidecar !== undefined) {\n throw new Error(`Cannot export a snapshot with a SQLite recovery sidecar: ${sidecar}`);\n }\n}\n\nclass WasmSqlStatement implements SqlDatabaseStatement {\n readonly #database: SqliteWasmDatabaseHandle;\n readonly #statement: SqliteWasmStatement;\n #closed = false;\n\n constructor(\n database: SqliteWasmDatabaseHandle,\n statement: SqliteWasmStatement,\n readonly sql: string,\n ) {\n this.#database = database;\n this.#statement = statement;\n }\n\n get parameterCount(): number {\n return this.#statement.parameterCount;\n }\n\n execute(params: readonly SqlValue[]): SqlResult {\n if (params.length !== this.parameterCount) throw new Error(SQL_WRONG_BINDINGS_MESSAGE);\n params.forEach(requireSqliteLength);\n if (params.length > 0) this.#statement.bind(params);\n\n const columnCount = this.#statement.columnCount;\n if (columnCount === 0) {\n this.#statement.step();\n return { columnNames: [], rawRows: [], rowsWritten: this.#database.changes(false) };\n }\n\n const changesBefore = this.#database.changes(true);\n const columnNames = this.#statement.getColumnNames();\n const rawRows: unknown[][] = [];\n while (this.#statement.step()) {\n const row: unknown[] = [];\n for (let column = 0; column < columnCount; column += 1) {\n row.push(this.#statement.get(column));\n }\n rawRows.push(row);\n }\n // A SELECT leaves total_changes() untouched; DML RETURNING advances it.\n // The delta avoids a SQL classifier and matches the public cursor contract.\n return {\n columnNames,\n rawRows,\n rowsWritten: this.#database.changes(true) - changesBefore,\n };\n }\n\n close(): void {\n if (this.#closed) return;\n this.#closed = true;\n this.#statement.finalize();\n }\n}\n\n/** Finds the first complete statement without reimplementing SQLite's trigger grammar. */\nfunction firstCompleteStatement(capi: SqliteWasmCapi, sql: string): string {\n let semicolon = sql.indexOf(\";\");\n while (semicolon !== -1) {\n const candidate = sql.slice(0, semicolon + 1);\n if (capi.sqlite3_complete(candidate) === 1) return candidate;\n semicolon = sql.indexOf(\";\", semicolon + 1);\n }\n return sql;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqlite-wasm.js","names":["#host","#prefix","#provider","#ownedFiles","#filename","#database","#setLengthLimit","#pragma","#closed","#statement"],"sources":["../../backends/sqlite-wasm.ts"],"sourcesContent":["/**\n * ← workerd `NO upstream correspondence (storage-backend adaptation)`\n *\n * `SqlDatabaseProvider` over the browser's OPFS SAH pool.\n *\n * The pool is a parameter, not something this module goes and gets. Installing\n * the VFS is the host's job — `installOpfsSAHPoolVfs` decides the OPFS\n * directory, the pool capacity and whether to clear on init, all of which are\n * layout questions this package deliberately knows nothing about. What arrives\n * here is the already-installed pool, and with it the two things a backend\n * needs that a bare `sqlite3` module cannot give: a database constructor bound\n * to that VFS, plus the pool's file export/import/unlink operations used by\n * snapshots and `reset()`.\n *\n * The pool is structurally typed rather than imported from\n * `@sqlite.org/sqlite-wasm`, so this package takes no dependency on the driver\n * and the caller is free to pass a pool from any build of it. The shape below\n * is the subset of `SAHPoolUtil` and `oo1.DB` that is used, copied from the\n * driver's own `.d.mts`.\n *\n * NOT exercised by the unit lane: it needs OPFS, which means a browser. It is\n * exercised twice in the browser lane — by `sqlite-wasm.smoke.spec.ts`, which\n * drives this file directly, and by the conformance suite, which runs the whole\n * package over it.\n */\n\nimport {\n requireSqliteLength,\n requireSafeDatabaseName,\n requireValidSqlDatabaseSnapshot,\n SQLITE_LENGTH_LIMIT,\n SQL_WRONG_BINDINGS_MESSAGE,\n type SqlDatabase,\n type SqlDatabaseProvider,\n type SqlDatabaseSnapshot,\n type SqlDatabaseSnapshotProvider,\n type SqlDatabaseStatement,\n type SqlResult,\n type SqlValue,\n} from \"../src/util/sqlite\";\nimport { requireImportableRuntimeStorage } from \"../src/util/sqlite-migrations\";\n\n/** ← `PreparedStatement`, the members used here. */\nexport interface SqliteWasmStatement {\n readonly columnCount: number;\n readonly parameterCount: number;\n bind(bindings: readonly (string | number | bigint | null | Uint8Array)[]): unknown;\n step(): boolean;\n get(index: number): unknown;\n getColumnNames(target?: string[]): string[];\n finalize(): number | undefined;\n}\n\n/** ← `oo1.DB` / `OpfsSAHPoolDatabase`, the members used here. */\nexport interface SqliteWasmDatabaseHandle {\n /** ← `oo1.DB.pointer`, which is absent once the handle is closed. */\n readonly pointer?: number | undefined;\n prepare(sql: string): SqliteWasmStatement;\n changes(total?: boolean, sixtyFour?: false): number;\n close(): void;\n}\n\n/** ← `SAHPoolUtil`, the members used here. */\nexport interface OpfsSahPool {\n /** Constructs a database inside this pool's VFS. Names are absolute, so they start with \"/\". */\n readonly OpfsSAHPoolDb: new (filename: string) => SqliteWasmDatabaseHandle;\n exportFile(filename: string): Uint8Array | Promise<Uint8Array>;\n importDb(filename: string, image: Uint8Array): number | Promise<number>;\n getFileNames(): string[];\n /** Disassociates a virtual file from the pool. Results are undefined if it is in active use. */\n unlink(filename: string): boolean;\n}\n\n/**\n * ← `Sqlite3Static[\"capi\"]`, restricted to the one C function `oo1.DB` does not\n * wrap.\n *\n * Takes the pointer rather than the handle, even though upstream's `DbPtr`\n * accepts either: a structural subset of `oo1.DB` is not assignable to the\n * `Database` class, so asking for the handle would make the real `capi` fail to\n * satisfy this interface.\n */\nexport interface SqliteWasmCapi {\n readonly SQLITE_LIMIT_LENGTH: number;\n sqlite3_complete(sql: string): 0 | 1;\n sqlite3_get_autocommit(db: number): number;\n sqlite3_limit(db: number, id: number, newValue: number): number;\n}\n\n/**\n * What the host hands over: the pool it installed, and the C-API namespace it\n * already holds. Both come off the same `sqlite3` object the caller used to\n * call `installOpfsSAHPoolVfs`, so this asks for nothing it does not have.\n */\nexport interface SqliteWasmHost {\n readonly pool: OpfsSahPool;\n readonly capi: SqliteWasmCapi;\n}\n\nexport type SqliteWasmProviderOptions = {\n /** Absolute path prefix inside the pool, e.g. `/actor-<id>`. Must start with \"/\". */\n prefix: string;\n};\n\n/**\n * One actor's named databases and their file lifecycle inside an OPFS SAH pool.\n *\n * A root or facet container only needs the `SqlDatabaseProvider` surface. Its\n * host also has to close every connection when that placement dies, remove the\n * prefix on delete, and copy every database on clone. Those file operations\n * belong here because SAH-pool files are virtual and can only be reached through\n * the pool that owns them.\n */\nexport class SqliteWasmActorStorage implements SqlDatabaseProvider {\n readonly #host: SqliteWasmHost;\n readonly #prefix: string;\n readonly #provider: SqlDatabaseSnapshotProvider;\n\n constructor(host: SqliteWasmHost, prefix: string) {\n this.#host = host;\n this.#prefix = prefix;\n this.#provider = createSqliteWasmProvider(host, { prefix });\n }\n\n open(name: string): Promise<SqlDatabase> {\n return this.#provider.open(name);\n }\n\n /**\n * Drop every handle. Leaving one behind per respawn or facet abort would\n * accumulate concurrent writers inside a VFS that expects to own its files.\n */\n close(): void {\n this.#provider.close();\n }\n\n /** Close every handle, then physically remove every database under this prefix. */\n deleteAll(): void {\n this.close();\n for (const file of this.#ownedFiles()) {\n if (!this.#host.pool.unlink(file)) throw new Error(`SAH pool did not unlink ${file}`);\n }\n }\n\n /**\n * Replace this prefix with every database under `source`, including files\n * from an earlier placement that this session never opened.\n *\n * The source may still be running, so this uses the pool's file operations\n * rather than the snapshot API, which correctly refuses open handles. A\n * recovery sidecar means the bytes are not a stable database image and is\n * refused before the destination is touched. Every source image is also\n * exported before replacement starts, so a failed read preserves the target.\n */\n async copyFrom(source: SqliteWasmActorStorage): Promise<void> {\n const files = source.#ownedFiles();\n const sidecar = files.find((file) => !file.endsWith(\".sqlite\"));\n if (sidecar !== undefined) {\n throw new Error(`Cannot clone actor storage with a SQLite recovery sidecar: ${sidecar}`);\n }\n const images: Array<{ name: string; bytes: Uint8Array }> = [];\n for (const file of files) {\n const name = file.slice(source.#prefix.length + 1, -\".sqlite\".length);\n requireSafeDatabaseName(name);\n images.push({\n name,\n bytes: new Uint8Array(await source.#host.pool.exportFile(file)),\n });\n }\n this.close();\n await replaceDatabases(\n this.#host.pool,\n this.#prefix,\n images.map(({ name, bytes }) => ({ name, image: bytes })),\n );\n }\n\n #ownedFiles(): string[] {\n return this.#host.pool\n .getFileNames()\n .filter((name) => name.startsWith(`${this.#prefix}.`));\n }\n}\n\nexport function createSqliteWasmProvider(\n host: SqliteWasmHost,\n options: SqliteWasmProviderOptions,\n): SqlDatabaseSnapshotProvider {\n const { prefix } = options;\n if (!prefix.startsWith(\"/\")) {\n throw new Error(`SAH pool names are absolute; prefix must start with \"/\": ${prefix}`);\n }\n const openDatabases = new Set<SqliteWasmDatabase>();\n const ownedFiles = (): string[] =>\n host.pool.getFileNames().filter((name) => name.startsWith(`${prefix}.`));\n return {\n async open(name: string): Promise<SqlDatabase> {\n // Names come from inside the package, so this is defence in depth — but\n // it is the one place a name becomes a pool file name.\n requireSafeDatabaseName(name);\n let database: SqliteWasmDatabase;\n database = new SqliteWasmDatabase(host, `${prefix}.${name}.sqlite`, () =>\n openDatabases.delete(database),\n );\n openDatabases.add(database);\n return database;\n },\n close(): void {\n for (const database of [...openDatabases]) database.close();\n },\n async exportSnapshot(): Promise<SqlDatabaseSnapshot> {\n requireClosed(openDatabases);\n const files = ownedFiles();\n requireNoRecoverySidecars(files);\n const databases = await Promise.all(\n files\n .filter((file) => file.endsWith(\".sqlite\"))\n .sort()\n .map(async (file) => {\n const name = file.slice(prefix.length + 1, -\".sqlite\".length);\n requireSafeDatabaseName(name);\n return { name, image: new Uint8Array(await host.pool.exportFile(file)) };\n }),\n );\n const snapshot: SqlDatabaseSnapshot = { version: 1, databases };\n requireValidSqlDatabaseSnapshot(snapshot);\n return snapshot;\n },\n async importSnapshot(snapshot: SqlDatabaseSnapshot): Promise<void> {\n requireClosed(openDatabases);\n requireValidSqlDatabaseSnapshot(snapshot);\n requireImportableRuntimeStorage(snapshot);\n await replaceDatabases(host.pool, prefix, snapshot.databases);\n },\n };\n}\n\n/** A failed rollback retains the original images so the host can recover to another prefix. */\nexport class SqliteWasmRestoreError extends AggregateError {\n constructor(errors: unknown[], readonly recoverySnapshot: SqlDatabaseSnapshot) {\n super(\n errors,\n \"SQLite replacement and rollback failed; restore recoverySnapshot to an idle provider.\",\n );\n this.name = \"SqliteWasmRestoreError\";\n }\n}\n\nasync function replaceDatabases(\n pool: OpfsSahPool,\n prefix: string,\n databases: SqlDatabaseSnapshot[\"databases\"],\n): Promise<void> {\n const replacement = databases.map(({ name, image }) => ({ name, image: new Uint8Array(image) }));\n const ownedFiles = () => pool.getFileNames().filter((file) => file.startsWith(`${prefix}.`));\n const files = ownedFiles();\n requireNoRecoverySidecars(files);\n const original: Array<{ name: string; image: Uint8Array }> = [];\n for (const file of files) {\n const name = file.slice(prefix.length + 1, -\".sqlite\".length);\n requireSafeDatabaseName(name);\n original.push({ name, image: new Uint8Array(await pool.exportFile(file)) });\n }\n const touched = new Set<string>();\n try {\n for (const file of files) {\n // unlink can remove the pool's mapping before a backing-file write fails.\n touched.add(file);\n if (!pool.unlink(file)) throw new Error(`SAH pool did not unlink ${file}`);\n }\n for (const { name, image } of replacement) {\n const file = `${prefix}.${name}.sqlite`;\n touched.add(file);\n await pool.importDb(file, image);\n }\n } catch (error) {\n // ponytail: rollback lives in memory; use a fresh prefix and a host-owned switch\n // when replacement must survive the worker/process dying during import.\n const errors = [error];\n for (const file of ownedFiles()) {\n if (!touched.has(file)) continue;\n try {\n if (!pool.unlink(file)) throw new Error(`SAH pool did not unlink ${file}`);\n } catch (rollbackError) {\n errors.push(rollbackError);\n }\n }\n for (const { name, image } of original) {\n const file = `${prefix}.${name}.sqlite`;\n if (!touched.has(file)) continue;\n try {\n await pool.importDb(file, new Uint8Array(image));\n } catch (rollbackError) {\n errors.push(rollbackError);\n }\n }\n if (errors.length > 1) {\n throw new SqliteWasmRestoreError(errors, { version: 1, databases: original });\n }\n throw error;\n }\n}\n\nexport class SqliteWasmDatabase implements SqlDatabase {\n readonly #host: SqliteWasmHost;\n readonly #filename: string;\n #database: SqliteWasmDatabaseHandle;\n #closed = false;\n\n constructor(\n host: SqliteWasmHost,\n filename: string,\n private readonly onClose: () => void = () => {},\n ) {\n this.#host = host;\n this.#filename = filename;\n this.#database = new host.pool.OpfsSAHPoolDb(filename);\n this.#setLengthLimit();\n }\n\n prepare(sql: string): SqlDatabaseStatement {\n const source = firstCompleteStatement(this.#host.capi, sql);\n return new WasmSqlStatement(this.#database, this.#database.prepare(source), source);\n }\n\n exec(sql: string, params: readonly SqlValue[]): SqlResult {\n const statement = this.prepare(sql);\n try {\n return statement.execute(params);\n } finally {\n statement.close();\n }\n }\n\n get databaseSize(): number {\n const pageCount = this.#pragma(\"page_count\");\n const pageSize = this.#pragma(\"page_size\");\n return pageCount * pageSize;\n }\n\n /**\n * `oo1.DB` wraps no equivalent, so this is the one place the backend reaches\n * past it into the C API. `DbPtr` accepts the database object itself.\n */\n get inTransaction(): boolean {\n const pointer = this.#database.pointer;\n if (pointer === undefined) throw new Error(\"The database handle is closed.\");\n return this.#host.capi.sqlite3_get_autocommit(pointer) === 0;\n }\n\n reset(): void {\n // The pool's files are not visible in OPFS under these names, so deleting\n // one goes through the pool rather than through the filesystem. The handle\n // has to be closed first: `unlink`'s results are undefined for a file in\n // active use.\n this.#database.close();\n if (!this.#host.pool.unlink(this.#filename)) {\n throw new Error(`SAH pool did not unlink ${this.#filename}`);\n }\n this.#database = new this.#host.pool.OpfsSAHPoolDb(this.#filename);\n this.#setLengthLimit();\n }\n\n close(): void {\n if (this.#closed) return;\n this.#database.close();\n this.#closed = true;\n this.onClose();\n }\n\n #setLengthLimit(): void {\n const pointer = this.#database.pointer;\n if (pointer === undefined) throw new Error(\"The database handle is closed.\");\n this.#host.capi.sqlite3_limit(\n pointer,\n this.#host.capi.SQLITE_LIMIT_LENGTH,\n SQLITE_LENGTH_LIMIT,\n );\n }\n\n #pragma(name: string): number {\n const row = this.exec(`PRAGMA ${name}`, []).rawRows[0];\n const value = row?.[0];\n if (typeof value !== \"number\") {\n throw new Error(`PRAGMA ${name} did not return a number.`);\n }\n return value;\n }\n}\n\nfunction requireClosed(openDatabases: ReadonlySet<SqliteWasmDatabase>): void {\n if (openDatabases.size > 0) {\n throw new Error(\"Cannot snapshot or restore while database handles are open.\");\n }\n}\n\nfunction requireNoRecoverySidecars(files: readonly string[]): void {\n const sidecar = files.find((file) => !file.endsWith(\".sqlite\"));\n if (sidecar !== undefined) {\n throw new Error(`Cannot export a snapshot with a SQLite recovery sidecar: ${sidecar}`);\n }\n}\n\nclass WasmSqlStatement implements SqlDatabaseStatement {\n readonly #database: SqliteWasmDatabaseHandle;\n readonly #statement: SqliteWasmStatement;\n #closed = false;\n\n constructor(\n database: SqliteWasmDatabaseHandle,\n statement: SqliteWasmStatement,\n readonly sql: string,\n ) {\n this.#database = database;\n this.#statement = statement;\n }\n\n get parameterCount(): number {\n return this.#statement.parameterCount;\n }\n\n execute(params: readonly SqlValue[]): SqlResult {\n if (params.length !== this.parameterCount) throw new Error(SQL_WRONG_BINDINGS_MESSAGE);\n params.forEach(requireSqliteLength);\n if (params.length > 0) this.#statement.bind(params);\n\n const columnCount = this.#statement.columnCount;\n if (columnCount === 0) {\n this.#statement.step();\n return { columnNames: [], rawRows: [], rowsWritten: this.#database.changes(false) };\n }\n\n const changesBefore = this.#database.changes(true);\n const columnNames = this.#statement.getColumnNames();\n const rawRows: unknown[][] = [];\n while (this.#statement.step()) {\n const row: unknown[] = [];\n for (let column = 0; column < columnCount; column += 1) {\n row.push(this.#statement.get(column));\n }\n rawRows.push(row);\n }\n // A SELECT leaves total_changes() untouched; DML RETURNING advances it.\n // The delta avoids a SQL classifier and matches the public cursor contract.\n return {\n columnNames,\n rawRows,\n rowsWritten: this.#database.changes(true) - changesBefore,\n };\n }\n\n close(): void {\n if (this.#closed) return;\n this.#closed = true;\n this.#statement.finalize();\n }\n}\n\n/** Finds the first complete statement without reimplementing SQLite's trigger grammar. */\nfunction firstCompleteStatement(capi: SqliteWasmCapi, sql: string): string {\n let semicolon = sql.indexOf(\";\");\n while (semicolon !== -1) {\n const candidate = sql.slice(0, semicolon + 1);\n if (capi.sqlite3_complete(candidate) === 1) return candidate;\n semicolon = sql.indexOf(\";\", semicolon + 1);\n }\n return sql;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiHA,IAAa,yBAAb,MAAmE;CACjE;CACA;CACA;CAEA,YAAY,MAAsB,QAAgB;EAChD,KAAKA,QAAQ;EACb,KAAKC,UAAU;EACf,KAAKC,YAAY,yBAAyB,MAAM,EAAE,OAAO,CAAC;CAC5D;CAEA,KAAK,MAAoC;EACvC,OAAO,KAAKA,UAAU,KAAK,IAAI;CACjC;;;;;CAMA,QAAc;EACZ,KAAKA,UAAU,MAAM;CACvB;;CAGA,YAAkB;EAChB,KAAK,MAAM;EACX,KAAK,MAAM,QAAQ,KAAKC,YAAY,GAClC,IAAI,CAAC,KAAKH,MAAM,KAAK,OAAO,IAAI,GAAG,MAAM,IAAI,MAAM,2BAA2B,MAAM;CAExF;;;;;;;;;;;CAYA,MAAM,SAAS,QAA+C;EAC5D,MAAM,QAAQ,OAAOG,YAAY;EACjC,MAAM,UAAU,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,SAAS,CAAC;EAC9D,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MAAM,8DAA8D,SAAS;EAEzF,MAAM,SAAqD,CAAC;EAC5D,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,OAAO,KAAK,MAAM,OAAOF,QAAQ,SAAS,GAAG,EAAiB;GACpE,wBAAwB,IAAI;GAC5B,OAAO,KAAK;IACV;IACA,OAAO,IAAI,WAAW,MAAM,OAAOD,MAAM,KAAK,WAAW,IAAI,CAAC;GAChE,CAAC;EACH;EACA,KAAK,MAAM;EACX,MAAM,iBACJ,KAAKA,MAAM,MACX,KAAKC,SACL,OAAO,KAAK,EAAE,MAAM,aAAa;GAAE;GAAM,OAAO;EAAM,EAAE,CAC1D;CACF;CAEA,cAAwB;EACtB,OAAO,KAAKD,MAAM,KACf,aAAa,CAAC,CACd,QAAQ,SAAS,KAAK,WAAW,GAAG,KAAKC,QAAQ,EAAE,CAAC;CACzD;AACF;AAEA,SAAgB,yBACd,MACA,SAC6B;CAC7B,MAAM,EAAE,WAAW;CACnB,IAAI,CAAC,OAAO,WAAW,GAAG,GACxB,MAAM,IAAI,MAAM,4DAA4D,QAAQ;CAEtF,MAAM,gCAAgB,IAAI,IAAwB;CAClD,MAAM,mBACJ,KAAK,KAAK,aAAa,CAAC,CAAC,QAAQ,SAAS,KAAK,WAAW,GAAG,OAAO,EAAE,CAAC;CACzE,OAAO;EACL,MAAM,KAAK,MAAoC;GAG7C,wBAAwB,IAAI;GAC5B,IAAI;GACJ,WAAW,IAAI,mBAAmB,MAAM,GAAG,OAAO,GAAG,KAAK,gBACxD,cAAc,OAAO,QAAQ,CAC/B;GACA,cAAc,IAAI,QAAQ;GAC1B,OAAO;EACT;EACA,QAAc;GACZ,KAAK,MAAM,YAAY,CAAC,GAAG,aAAa,GAAG,SAAS,MAAM;EAC5D;EACA,MAAM,iBAA+C;GACnD,cAAc,aAAa;GAC3B,MAAM,QAAQ,WAAW;GACzB,0BAA0B,KAAK;GAW/B,MAAM,WAAgC;IAAE,SAAS;IAAG,WAAA,MAV5B,QAAQ,IAC9B,MACG,QAAQ,SAAS,KAAK,SAAS,SAAS,CAAC,CAAC,CAC1C,KAAK,CAAC,CACN,IAAI,OAAO,SAAS;KACnB,MAAM,OAAO,KAAK,MAAM,OAAO,SAAS,GAAG,EAAiB;KAC5D,wBAAwB,IAAI;KAC5B,OAAO;MAAE;MAAM,OAAO,IAAI,WAAW,MAAM,KAAK,KAAK,WAAW,IAAI,CAAC;KAAE;IACzE,CAAC,CACL;GAC8D;GAC9D,gCAAgC,QAAQ;GACxC,OAAO;EACT;EACA,MAAM,eAAe,UAA8C;GACjE,cAAc,aAAa;GAC3B,gCAAgC,QAAQ;GACxC,gCAAgC,QAAQ;GACxC,MAAM,iBAAiB,KAAK,MAAM,QAAQ,SAAS,SAAS;EAC9D;CACF;AACF;;AAGA,IAAa,yBAAb,cAA4C,eAAe;CACjB;CAAxC,YAAY,QAAmB,kBAAgD;EAC7E,MACE,QACA,uFACF;EAJsC,KAAA,mBAAA;EAKtC,KAAK,OAAO;CACd;AACF;AAEA,eAAe,iBACb,MACA,QACA,WACe;CACf,MAAM,cAAc,UAAU,KAAK,EAAE,MAAM,aAAa;EAAE;EAAM,OAAO,IAAI,WAAW,KAAK;CAAE,EAAE;CAC/F,MAAM,mBAAmB,KAAK,aAAa,CAAC,CAAC,QAAQ,SAAS,KAAK,WAAW,GAAG,OAAO,EAAE,CAAC;CAC3F,MAAM,QAAQ,WAAW;CACzB,0BAA0B,KAAK;CAC/B,MAAM,WAAuD,CAAC;CAC9D,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,KAAK,MAAM,OAAO,SAAS,GAAG,EAAiB;EAC5D,wBAAwB,IAAI;EAC5B,SAAS,KAAK;GAAE;GAAM,OAAO,IAAI,WAAW,MAAM,KAAK,WAAW,IAAI,CAAC;EAAE,CAAC;CAC5E;CACA,MAAM,0BAAU,IAAI,IAAY;CAChC,IAAI;EACF,KAAK,MAAM,QAAQ,OAAO;GAExB,QAAQ,IAAI,IAAI;GAChB,IAAI,CAAC,KAAK,OAAO,IAAI,GAAG,MAAM,IAAI,MAAM,2BAA2B,MAAM;EAC3E;EACA,KAAK,MAAM,EAAE,MAAM,WAAW,aAAa;GACzC,MAAM,OAAO,GAAG,OAAO,GAAG,KAAK;GAC/B,QAAQ,IAAI,IAAI;GAChB,MAAM,KAAK,SAAS,MAAM,KAAK;EACjC;CACF,SAAS,OAAO;EAGd,MAAM,SAAS,CAAC,KAAK;EACrB,KAAK,MAAM,QAAQ,WAAW,GAAG;GAC/B,IAAI,CAAC,QAAQ,IAAI,IAAI,GAAG;GACxB,IAAI;IACF,IAAI,CAAC,KAAK,OAAO,IAAI,GAAG,MAAM,IAAI,MAAM,2BAA2B,MAAM;GAC3E,SAAS,eAAe;IACtB,OAAO,KAAK,aAAa;GAC3B;EACF;EACA,KAAK,MAAM,EAAE,MAAM,WAAW,UAAU;GACtC,MAAM,OAAO,GAAG,OAAO,GAAG,KAAK;GAC/B,IAAI,CAAC,QAAQ,IAAI,IAAI,GAAG;GACxB,IAAI;IACF,MAAM,KAAK,SAAS,MAAM,IAAI,WAAW,KAAK,CAAC;GACjD,SAAS,eAAe;IACtB,OAAO,KAAK,aAAa;GAC3B;EACF;EACA,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,uBAAuB,QAAQ;GAAE,SAAS;GAAG,WAAW;EAAS,CAAC;EAE9E,MAAM;CACR;AACF;AAEA,IAAa,qBAAb,MAAuD;CASlC;CARnB;CACA;CACA;CACA,UAAU;CAEV,YACE,MACA,UACA,gBAA6C,CAAC,GAC9C;EADiB,KAAA,UAAA;EAEjB,KAAKD,QAAQ;EACb,KAAKI,YAAY;EACjB,KAAKC,YAAY,IAAI,KAAK,KAAK,cAAc,QAAQ;EACrD,KAAKC,gBAAgB;CACvB;CAEA,QAAQ,KAAmC;EACzC,MAAM,SAAS,uBAAuB,KAAKN,MAAM,MAAM,GAAG;EAC1D,OAAO,IAAI,iBAAiB,KAAKK,WAAW,KAAKA,UAAU,QAAQ,MAAM,GAAG,MAAM;CACpF;CAEA,KAAK,KAAa,QAAwC;EACxD,MAAM,YAAY,KAAK,QAAQ,GAAG;EAClC,IAAI;GACF,OAAO,UAAU,QAAQ,MAAM;EACjC,UAAU;GACR,UAAU,MAAM;EAClB;CACF;CAEA,IAAI,eAAuB;EAGzB,OAFkB,KAAKE,QAAQ,YAExB,IADU,KAAKA,QAAQ,WACX;CACrB;;;;;CAMA,IAAI,gBAAyB;EAC3B,MAAM,UAAU,KAAKF,UAAU;EAC/B,IAAI,YAAY,KAAA,GAAW,MAAM,IAAI,MAAM,gCAAgC;EAC3E,OAAO,KAAKL,MAAM,KAAK,uBAAuB,OAAO,MAAM;CAC7D;CAEA,QAAc;EAKZ,KAAKK,UAAU,MAAM;EACrB,IAAI,CAAC,KAAKL,MAAM,KAAK,OAAO,KAAKI,SAAS,GACxC,MAAM,IAAI,MAAM,2BAA2B,KAAKA,WAAW;EAE7D,KAAKC,YAAY,IAAI,KAAKL,MAAM,KAAK,cAAc,KAAKI,SAAS;EACjE,KAAKE,gBAAgB;CACvB;CAEA,QAAc;EACZ,IAAI,KAAKE,SAAS;EAClB,KAAKH,UAAU,MAAM;EACrB,KAAKG,UAAU;EACf,KAAK,QAAQ;CACf;CAEA,kBAAwB;EACtB,MAAM,UAAU,KAAKH,UAAU;EAC/B,IAAI,YAAY,KAAA,GAAW,MAAM,IAAI,MAAM,gCAAgC;EAC3E,KAAKL,MAAM,KAAK,cACd,SACA,KAAKA,MAAM,KAAK,qBAChB,mBACF;CACF;CAEA,QAAQ,MAAsB;EAE5B,MAAM,QADM,KAAK,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,EACtC,GAAM;EACpB,IAAI,OAAO,UAAU,UACnB,MAAM,IAAI,MAAM,UAAU,KAAK,0BAA0B;EAE3D,OAAO;CACT;AACF;AAEA,SAAS,cAAc,eAAsD;CAC3E,IAAI,cAAc,OAAO,GACvB,MAAM,IAAI,MAAM,6DAA6D;AAEjF;AAEA,SAAS,0BAA0B,OAAgC;CACjE,MAAM,UAAU,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,SAAS,CAAC;CAC9D,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MAAM,4DAA4D,SAAS;AAEzF;AAEA,IAAM,mBAAN,MAAuD;CAQ1C;CAPX;CACA;CACA,UAAU;CAEV,YACE,UACA,WACA,KACA;EADS,KAAA,MAAA;EAET,KAAKK,YAAY;EACjB,KAAKI,aAAa;CACpB;CAEA,IAAI,iBAAyB;EAC3B,OAAO,KAAKA,WAAW;CACzB;CAEA,QAAQ,QAAwC;EAC9C,IAAI,OAAO,WAAW,KAAK,gBAAgB,MAAM,IAAI,MAAM,0BAA0B;EACrF,OAAO,QAAQ,mBAAmB;EAClC,IAAI,OAAO,SAAS,GAAG,KAAKA,WAAW,KAAK,MAAM;EAElD,MAAM,cAAc,KAAKA,WAAW;EACpC,IAAI,gBAAgB,GAAG;GACrB,KAAKA,WAAW,KAAK;GACrB,OAAO;IAAE,aAAa,CAAC;IAAG,SAAS,CAAC;IAAG,aAAa,KAAKJ,UAAU,QAAQ,KAAK;GAAE;EACpF;EAEA,MAAM,gBAAgB,KAAKA,UAAU,QAAQ,IAAI;EACjD,MAAM,cAAc,KAAKI,WAAW,eAAe;EACnD,MAAM,UAAuB,CAAC;EAC9B,OAAO,KAAKA,WAAW,KAAK,GAAG;GAC7B,MAAM,MAAiB,CAAC;GACxB,KAAK,IAAI,SAAS,GAAG,SAAS,aAAa,UAAU,GACnD,IAAI,KAAK,KAAKA,WAAW,IAAI,MAAM,CAAC;GAEtC,QAAQ,KAAK,GAAG;EAClB;EAGA,OAAO;GACL;GACA;GACA,aAAa,KAAKJ,UAAU,QAAQ,IAAI,IAAI;EAC9C;CACF;CAEA,QAAc;EACZ,IAAI,KAAKG,SAAS;EAClB,KAAKA,UAAU;EACf,KAAKC,WAAW,SAAS;CAC3B;AACF;;AAGA,SAAS,uBAAuB,MAAsB,KAAqB;CACzE,IAAI,YAAY,IAAI,QAAQ,GAAG;CAC/B,OAAO,cAAc,IAAI;EACvB,MAAM,YAAY,IAAI,MAAM,GAAG,YAAY,CAAC;EAC5C,IAAI,KAAK,iBAAiB,SAAS,MAAM,GAAG,OAAO;EACnD,YAAY,IAAI,QAAQ,KAAK,YAAY,CAAC;CAC5C;CACA,OAAO;AACT"}
@@ -0,0 +1,159 @@
1
+ import { alarmRetryDelayMs } from "../server/alarm-scheduler.js";
2
+ //#region src/browser/alarm-coordinator.ts
3
+ /**
4
+ * Projects a logical namespace alarm onto one crash-prone browser alarm.
5
+ *
6
+ * `AlarmScheduler` remains authoritative for actor delivery and retries. This
7
+ * coordinator journals the physical hop so a browser background worker can be
8
+ * stopped between any two awaited operations without losing the next wake.
9
+ */
10
+ var BrowserAlarmCoordinator = class {
11
+ options;
12
+ #activeDeliveries = /* @__PURE__ */ new Set();
13
+ #now;
14
+ #tail = Promise.resolve();
15
+ constructor(options) {
16
+ this.options = options;
17
+ this.#now = options.now ?? Date.now;
18
+ }
19
+ project(projection) {
20
+ const parsed = requireProjection(projection);
21
+ return this.#mutate(async () => {
22
+ const current = await this.options.store.load();
23
+ if (current && parsed.generation < current.projection.generation) return;
24
+ if (current?.delivery) {
25
+ if (parsed.generation !== current.projection.generation || parsed.when !== current.projection.when) await this.options.store.save({
26
+ delivery: current.delivery,
27
+ projection: parsed
28
+ });
29
+ return;
30
+ }
31
+ const next = {
32
+ delivery: null,
33
+ projection: parsed
34
+ };
35
+ await this.options.store.save(next);
36
+ await this.#apply(parsed.when);
37
+ });
38
+ }
39
+ /** Repairs an acknowledged physical operation interrupted by browser suspension. */
40
+ reconcile() {
41
+ return this.#mutate(async () => {
42
+ const current = await this.options.store.load();
43
+ if (!current) return;
44
+ if (current.delivery) {
45
+ await this.options.physical.create(Math.max(current.delivery.wake, this.#now()));
46
+ return;
47
+ }
48
+ await this.#apply(current.projection.when);
49
+ });
50
+ }
51
+ async fire(scheduledTime) {
52
+ const attempt = await this.#mutate(async () => {
53
+ const current = await this.options.store.load() ?? {
54
+ delivery: null,
55
+ projection: {
56
+ generation: 0,
57
+ when: scheduledTime
58
+ }
59
+ };
60
+ const expectedWake = current.delivery?.wake ?? current.projection.when;
61
+ if (expectedWake === null) return null;
62
+ if (scheduledTime < expectedWake) {
63
+ await this.options.physical.create(expectedWake);
64
+ return null;
65
+ }
66
+ const retryCount = current.delivery?.generation === current.projection.generation ? current.delivery.retryCount : 0;
67
+ const delay = alarmRetryDelayMs(Math.min(retryCount, 5));
68
+ const delivery = {
69
+ generation: current.projection.generation,
70
+ retryCount: Math.min(retryCount + 1, 6),
71
+ wake: this.#now() + delay
72
+ };
73
+ await this.options.physical.create(delivery.wake);
74
+ await this.options.store.save({
75
+ ...current,
76
+ delivery
77
+ });
78
+ if (this.#activeDeliveries.has(delivery.generation)) return null;
79
+ this.#activeDeliveries.add(delivery.generation);
80
+ return delivery;
81
+ });
82
+ if (!attempt) return null;
83
+ try {
84
+ const projection = requireProjection(await this.options.deliver(scheduledTime));
85
+ await this.#mutate(async () => {
86
+ const current = await this.options.store.load();
87
+ if (current && projection.generation < current.projection.generation) return;
88
+ await this.options.store.save({
89
+ delivery: null,
90
+ projection
91
+ });
92
+ await this.#apply(projection.when);
93
+ });
94
+ return projection;
95
+ } finally {
96
+ this.#activeDeliveries.delete(attempt.generation);
97
+ }
98
+ }
99
+ #apply(when) {
100
+ return when === null ? this.options.physical.clear() : this.options.physical.create(when);
101
+ }
102
+ #mutate(operation) {
103
+ const result = this.#tail.then(operation);
104
+ this.#tail = result.then(() => void 0, () => void 0);
105
+ return result;
106
+ }
107
+ };
108
+ function parseBrowserAlarmProjection(value) {
109
+ if (!isRecord(value)) return null;
110
+ if (!isNonnegativeInteger(value.generation)) return null;
111
+ if (value.when !== null && !isFiniteNumber(value.when)) return null;
112
+ return {
113
+ ...value,
114
+ generation: value.generation,
115
+ when: value.when
116
+ };
117
+ }
118
+ function parseBrowserAlarmTransportJournal(value) {
119
+ if (!isRecord(value)) return null;
120
+ const delivery = value.delivery === null ? null : parseBrowserAlarmDelivery(value.delivery);
121
+ if (delivery === null && value.delivery !== null) return null;
122
+ const projection = parseBrowserAlarmProjection(value.projection);
123
+ if (projection === null) return null;
124
+ return {
125
+ ...value,
126
+ delivery,
127
+ projection
128
+ };
129
+ }
130
+ function requireProjection(value) {
131
+ const parsed = parseBrowserAlarmProjection(value);
132
+ if (parsed === null) throw new TypeError("invalid browser alarm projection");
133
+ return parsed;
134
+ }
135
+ function parseBrowserAlarmDelivery(value) {
136
+ if (!isRecord(value)) return null;
137
+ if (!isNonnegativeInteger(value.generation)) return null;
138
+ if (!isNonnegativeInteger(value.retryCount)) return null;
139
+ if (!isFiniteNumber(value.wake)) return null;
140
+ return {
141
+ ...value,
142
+ generation: value.generation,
143
+ retryCount: value.retryCount,
144
+ wake: value.wake
145
+ };
146
+ }
147
+ function isRecord(value) {
148
+ return typeof value === "object" && value !== null && !Array.isArray(value);
149
+ }
150
+ function isNonnegativeInteger(value) {
151
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
152
+ }
153
+ function isFiniteNumber(value) {
154
+ return typeof value === "number" && Number.isFinite(value);
155
+ }
156
+ //#endregion
157
+ export { BrowserAlarmCoordinator, parseBrowserAlarmTransportJournal };
158
+
159
+ //# sourceMappingURL=alarm-coordinator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-coordinator.js","names":["#activeDeliveries","#now","#mutate","#apply","#tail"],"sources":["../../src/browser/alarm-coordinator.ts"],"sourcesContent":["import {\n ALARM_RETRY_MAX_TRIES,\n alarmRetryDelayMs,\n} from \"../server/alarm-scheduler\";\n\ntype LooseRecord = Record<string, unknown>;\n\nexport type BrowserAlarmProjection = {\n readonly generation: number;\n readonly when: number | null;\n} & LooseRecord;\n\nexport type BrowserAlarmDelivery = {\n readonly generation: number;\n readonly retryCount: number;\n readonly wake: number;\n} & LooseRecord;\n\nexport type BrowserAlarmTransportJournal = {\n readonly delivery: BrowserAlarmDelivery | null;\n readonly projection: BrowserAlarmProjection;\n} & LooseRecord;\n\nexport interface BrowserAlarmTransportStore {\n load(): Promise<BrowserAlarmTransportJournal | null>;\n save(journal: BrowserAlarmTransportJournal): Promise<void>;\n}\n\nexport interface BrowserPhysicalAlarm {\n clear(): Promise<void>;\n create(when: number): Promise<void>;\n}\n\nexport type BrowserAlarmCoordinatorOptions = {\n deliver(scheduledTime: number): Promise<BrowserAlarmProjection>;\n now?: () => number;\n physical: BrowserPhysicalAlarm;\n store: BrowserAlarmTransportStore;\n};\n\n/**\n * Projects a logical namespace alarm onto one crash-prone browser alarm.\n *\n * `AlarmScheduler` remains authoritative for actor delivery and retries. This\n * coordinator journals the physical hop so a browser background worker can be\n * stopped between any two awaited operations without losing the next wake.\n */\nexport class BrowserAlarmCoordinator {\n readonly #activeDeliveries = new Set<number>();\n readonly #now: () => number;\n #tail: Promise<void> = Promise.resolve();\n\n constructor(private readonly options: BrowserAlarmCoordinatorOptions) {\n this.#now = options.now ?? Date.now;\n }\n\n project(projection: BrowserAlarmProjection): Promise<void> {\n const parsed = requireProjection(projection);\n return this.#mutate(async () => {\n const current = await this.options.store.load();\n if (current && parsed.generation < current.projection.generation) return;\n if (current?.delivery) {\n // A projection describes logical state, not completion of the transport\n // call carrying this delivery. Keep its recovery wake until fire()\n // receives the scheduler's final acknowledgement.\n if (\n parsed.generation !== current.projection.generation ||\n parsed.when !== current.projection.when\n ) {\n await this.options.store.save({ delivery: current.delivery, projection: parsed });\n }\n return;\n }\n const next = {\n delivery: null,\n projection: parsed,\n } satisfies BrowserAlarmTransportJournal;\n // Journal first: startup reconciliation can finish the physical operation\n // if the background worker is stopped between these two writes.\n await this.options.store.save(next);\n await this.#apply(parsed.when);\n });\n }\n\n /** Repairs an acknowledged physical operation interrupted by browser suspension. */\n reconcile(): Promise<void> {\n return this.#mutate(async () => {\n const current = await this.options.store.load();\n if (!current) return;\n if (current.delivery) {\n await this.options.physical.create(Math.max(current.delivery.wake, this.#now()));\n return;\n }\n await this.#apply(current.projection.when);\n });\n }\n\n async fire(scheduledTime: number): Promise<BrowserAlarmProjection | null> {\n const attempt = await this.#mutate(async () => {\n const stored = await this.options.store.load();\n const current =\n stored ??\n ({\n delivery: null,\n projection: { generation: 0, when: scheduledTime },\n } satisfies BrowserAlarmTransportJournal);\n const expectedWake = current.delivery?.wake ?? current.projection.when;\n if (expectedWake === null) return null;\n if (scheduledTime < expectedWake) {\n // A consumed, slightly early watchdog must not erase the journaled wake.\n await this.options.physical.create(expectedWake);\n return null;\n }\n const retryCount =\n current.delivery?.generation === current.projection.generation\n ? current.delivery.retryCount\n : 0;\n const delay = alarmRetryDelayMs(Math.min(retryCount, ALARM_RETRY_MAX_TRIES - 1));\n const delivery = {\n generation: current.projection.generation,\n retryCount: Math.min(retryCount + 1, ALARM_RETRY_MAX_TRIES),\n wake: this.#now() + delay,\n };\n // Arm first so a stop between these operations leaves a wake capable of\n // repairing the stale journal rather than losing a consumed one-shot alarm.\n await this.options.physical.create(delivery.wake);\n await this.options.store.save({ ...current, delivery });\n if (this.#activeDeliveries.has(delivery.generation)) return null;\n this.#activeDeliveries.add(delivery.generation);\n return delivery;\n });\n if (!attempt) return null;\n\n try {\n const projection = requireProjection(await this.options.deliver(scheduledTime));\n await this.#mutate(async () => {\n const current = await this.options.store.load();\n if (current && projection.generation < current.projection.generation) return;\n await this.options.store.save({ delivery: null, projection });\n await this.#apply(projection.when);\n });\n return projection;\n } finally {\n this.#activeDeliveries.delete(attempt.generation);\n }\n }\n\n #apply(when: number | null): Promise<void> {\n return when === null ? this.options.physical.clear() : this.options.physical.create(when);\n }\n\n #mutate<T>(operation: () => T | PromiseLike<T>): Promise<T> {\n const result = this.#tail.then(operation);\n this.#tail = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n}\n\nfunction parseBrowserAlarmProjection(value: unknown): BrowserAlarmProjection | null {\n if (!isRecord(value)) return null;\n if (!isNonnegativeInteger(value.generation)) return null;\n if (value.when !== null && !isFiniteNumber(value.when)) return null;\n return { ...value, generation: value.generation, when: value.when };\n}\n\nexport function parseBrowserAlarmTransportJournal(\n value: unknown,\n): BrowserAlarmTransportJournal | null {\n if (!isRecord(value)) return null;\n const delivery = value.delivery === null ? null : parseBrowserAlarmDelivery(value.delivery);\n if (delivery === null && value.delivery !== null) return null;\n const projection = parseBrowserAlarmProjection(value.projection);\n if (projection === null) return null;\n return { ...value, delivery, projection };\n}\n\nfunction requireProjection(value: unknown): BrowserAlarmProjection {\n const parsed = parseBrowserAlarmProjection(value);\n if (parsed === null) throw new TypeError(\"invalid browser alarm projection\");\n return parsed;\n}\n\nfunction parseBrowserAlarmDelivery(value: unknown): BrowserAlarmDelivery | null {\n if (!isRecord(value)) return null;\n if (!isNonnegativeInteger(value.generation)) return null;\n if (!isNonnegativeInteger(value.retryCount)) return null;\n if (!isFiniteNumber(value.wake)) return null;\n return {\n ...value,\n generation: value.generation,\n retryCount: value.retryCount,\n wake: value.wake,\n };\n}\n\nfunction isRecord(value: unknown): value is LooseRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isNonnegativeInteger(value: unknown): value is number {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n"],"mappings":"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"}
@@ -0,0 +1,243 @@
1
+ //#region src/browser/message-port-websocket.ts
2
+ /** One WebSocket-shaped endpoint backed by one dedicated MessagePort. */
3
+ var MessagePortWebSocket = class MessagePortWebSocket extends EventTarget {
4
+ url;
5
+ port;
6
+ static CONNECTING = 0;
7
+ static OPEN = 1;
8
+ static CLOSING = 2;
9
+ static CLOSED = 3;
10
+ CONNECTING = MessagePortWebSocket.CONNECTING;
11
+ OPEN = MessagePortWebSocket.OPEN;
12
+ CLOSING = MessagePortWebSocket.CLOSING;
13
+ CLOSED = MessagePortWebSocket.CLOSED;
14
+ binaryType = "arraybuffer";
15
+ bufferedAmount = 0;
16
+ extensions = "";
17
+ protocol = "";
18
+ readyState = MessagePortWebSocket.CONNECTING;
19
+ onopen = null;
20
+ onmessage = null;
21
+ onclose = null;
22
+ onerror = null;
23
+ #queuedWireMessages = [];
24
+ #queuedWireFlushPending = false;
25
+ constructor(url, port, autoOpen = true) {
26
+ super();
27
+ this.url = url;
28
+ this.port = port;
29
+ this.port.addEventListener("message", (event) => {
30
+ this.#handleWireMessage(event.data);
31
+ });
32
+ this.port.start();
33
+ if (autoOpen) queueMicrotask(() => this.open());
34
+ }
35
+ send(data) {
36
+ if (this.readyState !== MessagePortWebSocket.OPEN) throw new TypeError("WebSocket send() after close");
37
+ this.port.postMessage({
38
+ type: "message",
39
+ data
40
+ });
41
+ }
42
+ close(code = 1e3, reason = "") {
43
+ if (!this.#beginClose()) return;
44
+ this.port.postMessage({
45
+ type: "close",
46
+ code,
47
+ reason
48
+ });
49
+ this.#finishClose(code, reason, true);
50
+ }
51
+ accept(_options) {
52
+ this.open();
53
+ }
54
+ open() {
55
+ if (!this.#markOpen()) return;
56
+ this.#queuedWireFlushPending = true;
57
+ queueMicrotask(() => {
58
+ for (const message of this.#queuedWireMessages.splice(0)) {
59
+ if (this.readyState !== MessagePortWebSocket.OPEN) break;
60
+ this.#dispatchWireMessage(message);
61
+ }
62
+ this.#queuedWireFlushPending = false;
63
+ });
64
+ }
65
+ /** End a socket whose physical host disappeared without notifying that host. */
66
+ disconnect(code = 1006, reason = "") {
67
+ this.#finishClose(code, reason, false);
68
+ }
69
+ #handleWireMessage(value) {
70
+ const message = parseWireMessage(value);
71
+ if (message === null) {
72
+ this.close(1002, "Invalid MessagePort WebSocket message");
73
+ return;
74
+ }
75
+ if (message.type === "open") {
76
+ this.open();
77
+ return;
78
+ }
79
+ if (message.type === "close") {
80
+ this.#dispatchWireMessage(message);
81
+ return;
82
+ }
83
+ if (this.readyState === MessagePortWebSocket.CONNECTING || this.#queuedWireFlushPending) {
84
+ this.#queuedWireMessages.push(message);
85
+ return;
86
+ }
87
+ this.#dispatchWireMessage(message);
88
+ }
89
+ #dispatchWireMessage(message) {
90
+ if (message.type === "close") {
91
+ this.#finishClose(message.code, message.reason, true);
92
+ return;
93
+ }
94
+ if (this.readyState !== MessagePortWebSocket.OPEN) return;
95
+ this.#emit(new MessageEvent("message", { data: message.data }), this.onmessage);
96
+ }
97
+ #markOpen() {
98
+ if (this.readyState !== MessagePortWebSocket.CONNECTING) return false;
99
+ this.readyState = MessagePortWebSocket.OPEN;
100
+ this.#emit(new Event("open"), this.onopen);
101
+ return true;
102
+ }
103
+ #beginClose() {
104
+ if (this.readyState === MessagePortWebSocket.CLOSING || this.readyState === MessagePortWebSocket.CLOSED) return false;
105
+ this.readyState = MessagePortWebSocket.CLOSING;
106
+ return true;
107
+ }
108
+ #finishClose(code, reason, wasClean) {
109
+ if (this.readyState === MessagePortWebSocket.CLOSED) return;
110
+ this.readyState = MessagePortWebSocket.CLOSED;
111
+ this.#queuedWireMessages.length = 0;
112
+ this.#queuedWireFlushPending = false;
113
+ this.port.close();
114
+ this.#emit(new CloseEvent("close", {
115
+ code,
116
+ reason,
117
+ wasClean
118
+ }), this.onclose);
119
+ }
120
+ #emit(event, handler) {
121
+ handler?.(event);
122
+ this.dispatchEvent(event);
123
+ }
124
+ };
125
+ /** Connect two accepted socket endpoints and open the MessagePort side. */
126
+ function bridgeWebSocket(socket, bridge) {
127
+ if (bridge.readyState >= MessagePortWebSocket.CLOSING) {
128
+ socket.close(1001, "MessagePort transport closed");
129
+ return;
130
+ }
131
+ socket.addEventListener("message", (event) => {
132
+ if (bridge.readyState >= MessagePortWebSocket.CLOSING) return;
133
+ const data = parseMessageData(event);
134
+ if (data === null) {
135
+ bridge.close(1002, "Invalid WebSocket message");
136
+ return;
137
+ }
138
+ bridge.send(data);
139
+ });
140
+ socket.addEventListener("close", (event) => {
141
+ const close = parseCloseEvent(event);
142
+ bridge.close(close?.code ?? 1006, close?.reason ?? "Invalid WebSocket close event");
143
+ });
144
+ socket.addEventListener("error", () => {
145
+ bridge.close(1011, "WebSocket failed");
146
+ });
147
+ bridge.addEventListener("message", (event) => {
148
+ socket.send(event.data);
149
+ });
150
+ bridge.addEventListener("close", (event) => {
151
+ socket.close(event.code, event.reason);
152
+ });
153
+ socket.accept();
154
+ bridge.open();
155
+ }
156
+ /** A browser WebSocket constructor whose sockets cross one broker MessagePort. */
157
+ function createMessagePortWebSocketConstructor(port) {
158
+ return class BrokeredMessagePortWebSocket extends MessagePortWebSocket {
159
+ constructor(url, _protocols) {
160
+ const channel = new MessageChannel();
161
+ super(String(url), channel.port1, false);
162
+ port.postMessage({
163
+ type: "connect",
164
+ url: String(url),
165
+ port: channel.port2
166
+ }, [channel.port2]);
167
+ }
168
+ };
169
+ }
170
+ /** Serve brokered MessagePort sockets from real in-worker socket endpoints. */
171
+ function serveMessagePortWebSockets(port, connect) {
172
+ const bridges = /* @__PURE__ */ new Set();
173
+ const listener = (event) => {
174
+ const request = parseSocketRequest(event.data);
175
+ if (request === null) return;
176
+ const bridge = new MessagePortWebSocket(request.url, request.port, false);
177
+ bridges.add(bridge);
178
+ bridge.addEventListener("close", () => bridges.delete(bridge), { once: true });
179
+ connect(request.url).then((socket) => {
180
+ if (!bridges.has(bridge)) {
181
+ socket.close(1001, "host stopped");
182
+ return;
183
+ }
184
+ bridgeWebSocket(socket, bridge);
185
+ request.port.postMessage({ type: "open" });
186
+ }, () => bridge.close(1011, "WebSocket connection failed"));
187
+ };
188
+ port.addEventListener("message", listener);
189
+ port.start();
190
+ return () => {
191
+ port.removeEventListener("message", listener);
192
+ for (const bridge of bridges) bridge.close(1001, "host stopped");
193
+ bridges.clear();
194
+ port.close();
195
+ };
196
+ }
197
+ function parseWireMessage(value) {
198
+ if (!isRecord(value)) return null;
199
+ if (value.type === "open") return { type: "open" };
200
+ if (value.type === "message" && isWebSocketData(value.data)) return {
201
+ type: "message",
202
+ data: value.data
203
+ };
204
+ if (value.type === "close" && typeof value.code === "number" && Number.isInteger(value.code) && typeof value.reason === "string") return {
205
+ type: "close",
206
+ code: value.code,
207
+ reason: value.reason
208
+ };
209
+ return null;
210
+ }
211
+ function parseSocketRequest(value) {
212
+ if (!isRecord(value)) return null;
213
+ if (value.type !== "connect" || typeof value.url !== "string") return null;
214
+ if (!(value.port instanceof MessagePort)) return null;
215
+ return {
216
+ type: "connect",
217
+ url: value.url,
218
+ port: value.port
219
+ };
220
+ }
221
+ function parseMessageData(event) {
222
+ if (!("data" in event) || !isWebSocketData(event.data)) return null;
223
+ return event.data;
224
+ }
225
+ function parseCloseEvent(event) {
226
+ if (!("code" in event) || !("reason" in event)) return null;
227
+ if (typeof event.code !== "number" || !Number.isInteger(event.code)) return null;
228
+ if (typeof event.reason !== "string") return null;
229
+ return {
230
+ code: event.code,
231
+ reason: event.reason
232
+ };
233
+ }
234
+ function isWebSocketData(value) {
235
+ return typeof value === "string" || value instanceof ArrayBuffer || ArrayBuffer.isView(value);
236
+ }
237
+ function isRecord(value) {
238
+ return typeof value === "object" && value !== null && !Array.isArray(value);
239
+ }
240
+ //#endregion
241
+ export { MessagePortWebSocket, bridgeWebSocket, createMessagePortWebSocketConstructor, serveMessagePortWebSockets };
242
+
243
+ //# sourceMappingURL=message-port-websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-port-websocket.js","names":["#queuedWireMessages","#handleWireMessage","#beginClose","#finishClose","#markOpen","#queuedWireFlushPending","#dispatchWireMessage","#emit"],"sources":["../../src/browser/message-port-websocket.ts"],"sourcesContent":["import type { RawWebSocket } from \"../api/web-socket\";\nimport type { UpgradeWebSocket } from \"../browser\";\n\nexport type MessagePortWebSocketData = string | ArrayBuffer | ArrayBufferView;\n\nexport type MessagePortWebSocketWireMessage =\n | { readonly type: \"message\"; readonly data: MessagePortWebSocketData }\n | { readonly type: \"close\"; readonly code: number; readonly reason: string };\n\ntype MessagePortWebSocketReadyMessage = { readonly type: \"open\" };\n\n/** One WebSocket-shaped endpoint backed by one dedicated MessagePort. */\nexport class MessagePortWebSocket extends EventTarget implements RawWebSocket {\n static readonly CONNECTING = 0;\n static readonly OPEN = 1;\n static readonly CLOSING = 2;\n static readonly CLOSED = 3;\n\n readonly CONNECTING = MessagePortWebSocket.CONNECTING;\n readonly OPEN = MessagePortWebSocket.OPEN;\n readonly CLOSING = MessagePortWebSocket.CLOSING;\n readonly CLOSED = MessagePortWebSocket.CLOSED;\n\n binaryType: BinaryType = \"arraybuffer\";\n bufferedAmount = 0;\n extensions = \"\";\n protocol = \"\";\n readyState: number = MessagePortWebSocket.CONNECTING;\n onopen: ((event: Event) => void) | null = null;\n onmessage: ((event: MessageEvent<MessagePortWebSocketData>) => void) | null = null;\n onclose: ((event: CloseEvent) => void) | null = null;\n onerror: ((event: Event) => void) | null = null;\n\n readonly #queuedWireMessages: MessagePortWebSocketWireMessage[] = [];\n #queuedWireFlushPending = false;\n\n constructor(\n readonly url: string,\n private readonly port: MessagePort,\n autoOpen = true,\n ) {\n super();\n this.port.addEventListener(\"message\", (event: MessageEvent<unknown>) => {\n this.#handleWireMessage(event.data);\n });\n this.port.start();\n if (autoOpen) queueMicrotask(() => this.open());\n }\n\n send(data: MessagePortWebSocketData): void {\n if (this.readyState !== MessagePortWebSocket.OPEN) {\n throw new TypeError(\"WebSocket send() after close\");\n }\n this.port.postMessage({ type: \"message\", data } satisfies MessagePortWebSocketWireMessage);\n }\n\n close(code = 1000, reason = \"\"): void {\n if (!this.#beginClose()) return;\n this.port.postMessage({\n type: \"close\",\n code,\n reason,\n } satisfies MessagePortWebSocketWireMessage);\n this.#finishClose(code, reason, true);\n }\n\n accept(_options?: { allowHalfOpen?: boolean }): void {\n this.open();\n }\n\n open(): void {\n if (!this.#markOpen()) return;\n // Native sockets do not deliver a frame synchronously from accept(). Give\n // the accept path the rest of this turn to attach its message listener.\n this.#queuedWireFlushPending = true;\n queueMicrotask(() => {\n for (const message of this.#queuedWireMessages.splice(0)) {\n if (this.readyState !== MessagePortWebSocket.OPEN) break;\n this.#dispatchWireMessage(message);\n }\n this.#queuedWireFlushPending = false;\n });\n }\n\n /** End a socket whose physical host disappeared without notifying that host. */\n protected disconnect(code = 1006, reason = \"\"): void {\n this.#finishClose(code, reason, false);\n }\n\n #handleWireMessage(value: unknown): void {\n const message = parseWireMessage(value);\n if (message === null) {\n this.close(1002, \"Invalid MessagePort WebSocket message\");\n return;\n }\n if (message.type === \"open\") {\n this.open();\n return;\n }\n if (message.type === \"close\") {\n this.#dispatchWireMessage(message);\n return;\n }\n if (this.readyState === MessagePortWebSocket.CONNECTING || this.#queuedWireFlushPending) {\n this.#queuedWireMessages.push(message);\n return;\n }\n this.#dispatchWireMessage(message);\n }\n\n #dispatchWireMessage(message: MessagePortWebSocketWireMessage): void {\n if (message.type === \"close\") {\n this.#finishClose(message.code, message.reason, true);\n return;\n }\n if (this.readyState !== MessagePortWebSocket.OPEN) return;\n this.#emit(\n new MessageEvent<MessagePortWebSocketData>(\"message\", { data: message.data }),\n this.onmessage,\n );\n }\n\n #markOpen(): boolean {\n if (this.readyState !== MessagePortWebSocket.CONNECTING) return false;\n this.readyState = MessagePortWebSocket.OPEN;\n this.#emit(new Event(\"open\"), this.onopen);\n return true;\n }\n\n #beginClose(): boolean {\n if (\n this.readyState === MessagePortWebSocket.CLOSING ||\n this.readyState === MessagePortWebSocket.CLOSED\n ) {\n return false;\n }\n this.readyState = MessagePortWebSocket.CLOSING;\n return true;\n }\n\n #finishClose(code: number, reason: string, wasClean: boolean): void {\n if (this.readyState === MessagePortWebSocket.CLOSED) return;\n this.readyState = MessagePortWebSocket.CLOSED;\n this.#queuedWireMessages.length = 0;\n this.#queuedWireFlushPending = false;\n this.port.close();\n this.#emit(new CloseEvent(\"close\", { code, reason, wasClean }), this.onclose);\n }\n\n #emit<E extends Event>(event: E, handler: ((event: E) => void) | null): void {\n handler?.(event);\n this.dispatchEvent(event);\n }\n}\n\nexport interface MessagePortWebSocket {\n addEventListener(\n type: \"message\",\n listener: (event: MessageEvent<MessagePortWebSocketData>) => void,\n options?: boolean | AddEventListenerOptions,\n ): void;\n addEventListener(\n type: \"close\",\n listener: (event: CloseEvent) => void,\n options?: boolean | AddEventListenerOptions,\n ): void;\n addEventListener(\n type: \"error\" | \"open\",\n listener: (event: Event) => void,\n options?: boolean | AddEventListenerOptions,\n ): void;\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject | null,\n options?: boolean | AddEventListenerOptions,\n ): void;\n}\n\n/** Connect two accepted socket endpoints and open the MessagePort side. */\nexport function bridgeWebSocket(socket: UpgradeWebSocket, bridge: MessagePortWebSocket): void {\n if (bridge.readyState >= MessagePortWebSocket.CLOSING) {\n socket.close(1001, \"MessagePort transport closed\");\n return;\n }\n socket.addEventListener(\"message\", (event) => {\n if (bridge.readyState >= MessagePortWebSocket.CLOSING) return;\n const data = parseMessageData(event);\n if (data === null) {\n bridge.close(1002, \"Invalid WebSocket message\");\n return;\n }\n bridge.send(data);\n });\n socket.addEventListener(\"close\", (event) => {\n const close = parseCloseEvent(event);\n bridge.close(close?.code ?? 1006, close?.reason ?? \"Invalid WebSocket close event\");\n });\n socket.addEventListener(\"error\", () => {\n bridge.close(1011, \"WebSocket failed\");\n });\n bridge.addEventListener(\"message\", (event) => {\n socket.send(event.data);\n });\n bridge.addEventListener(\"close\", (event) => {\n socket.close(event.code, event.reason);\n });\n socket.accept();\n bridge.open();\n}\n\ntype SocketRequest = {\n readonly type: \"connect\";\n readonly url: string;\n readonly port: MessagePort;\n};\n\nexport type MessagePortWebSocketConstructor = {\n new (url: string | URL, protocols?: string | string[]): MessagePortWebSocket;\n readonly CONNECTING: 0;\n readonly OPEN: 1;\n readonly CLOSING: 2;\n readonly CLOSED: 3;\n};\n\n/** A browser WebSocket constructor whose sockets cross one broker MessagePort. */\nexport function createMessagePortWebSocketConstructor(\n port: MessagePort,\n): MessagePortWebSocketConstructor {\n return class BrokeredMessagePortWebSocket extends MessagePortWebSocket {\n constructor(url: string | URL, _protocols?: string | string[]) {\n const channel = new MessageChannel();\n super(String(url), channel.port1, false);\n port.postMessage(\n { type: \"connect\", url: String(url), port: channel.port2 } satisfies SocketRequest,\n [channel.port2],\n );\n }\n };\n}\n\n/** Serve brokered MessagePort sockets from real in-worker socket endpoints. */\nexport function serveMessagePortWebSockets(\n port: MessagePort,\n connect: (url: string) => Promise<UpgradeWebSocket>,\n): () => void {\n const bridges = new Set<MessagePortWebSocket>();\n const listener = (event: MessageEvent<unknown>): void => {\n const request = parseSocketRequest(event.data);\n if (request === null) return;\n const bridge = new MessagePortWebSocket(request.url, request.port, false);\n bridges.add(bridge);\n bridge.addEventListener(\"close\", () => bridges.delete(bridge), { once: true });\n void connect(request.url).then(\n (socket) => {\n if (!bridges.has(bridge)) {\n socket.close(1001, \"host stopped\");\n return;\n }\n bridgeWebSocket(socket, bridge);\n request.port.postMessage({ type: \"open\" } satisfies MessagePortWebSocketReadyMessage);\n },\n () => bridge.close(1011, \"WebSocket connection failed\"),\n );\n };\n port.addEventListener(\"message\", listener);\n port.start();\n\n return () => {\n port.removeEventListener(\"message\", listener);\n for (const bridge of bridges) bridge.close(1001, \"host stopped\");\n bridges.clear();\n port.close();\n };\n}\n\nfunction parseWireMessage(\n value: unknown,\n): MessagePortWebSocketWireMessage | MessagePortWebSocketReadyMessage | null {\n if (!isRecord(value)) return null;\n if (value.type === \"open\") return { type: \"open\" };\n if (value.type === \"message\" && isWebSocketData(value.data)) {\n return { type: \"message\", data: value.data };\n }\n if (\n value.type === \"close\" &&\n typeof value.code === \"number\" &&\n Number.isInteger(value.code) &&\n typeof value.reason === \"string\"\n ) {\n return { type: \"close\", code: value.code, reason: value.reason };\n }\n return null;\n}\n\nfunction parseSocketRequest(value: unknown): SocketRequest | null {\n if (!isRecord(value)) return null;\n if (value.type !== \"connect\" || typeof value.url !== \"string\") return null;\n if (!(value.port instanceof MessagePort)) return null;\n return { type: \"connect\", url: value.url, port: value.port };\n}\n\nfunction parseMessageData(event: Event): MessagePortWebSocketData | null {\n if (!(\"data\" in event) || !isWebSocketData(event.data)) return null;\n return event.data;\n}\n\nfunction parseCloseEvent(event: Event): { readonly code: number; readonly reason: string } | null {\n if (!(\"code\" in event) || !(\"reason\" in event)) return null;\n if (typeof event.code !== \"number\" || !Number.isInteger(event.code)) return null;\n if (typeof event.reason !== \"string\") return null;\n return { code: event.code, reason: event.reason };\n}\n\nfunction isWebSocketData(value: unknown): value is MessagePortWebSocketData {\n return (\n typeof value === \"string\" ||\n value instanceof ArrayBuffer ||\n ArrayBuffer.isView(value)\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"],"mappings":";;AAYA,IAAa,uBAAb,MAAa,6BAA6B,YAAoC;CAyBjE;CACQ;CAzBnB,OAAgB,aAAa;CAC7B,OAAgB,OAAO;CACvB,OAAgB,UAAU;CAC1B,OAAgB,SAAS;CAEzB,aAAsB,qBAAqB;CAC3C,OAAgB,qBAAqB;CACrC,UAAmB,qBAAqB;CACxC,SAAkB,qBAAqB;CAEvC,aAAyB;CACzB,iBAAiB;CACjB,aAAa;CACb,WAAW;CACX,aAAqB,qBAAqB;CAC1C,SAA0C;CAC1C,YAA8E;CAC9E,UAAgD;CAChD,UAA2C;CAE3C,sBAAkE,CAAC;CACnE,0BAA0B;CAE1B,YACE,KACA,MACA,WAAW,MACX;EACA,MAAM;EAJG,KAAA,MAAA;EACQ,KAAA,OAAA;EAIjB,KAAK,KAAK,iBAAiB,YAAY,UAAiC;GACtE,KAAKC,mBAAmB,MAAM,IAAI;EACpC,CAAC;EACD,KAAK,KAAK,MAAM;EAChB,IAAI,UAAU,qBAAqB,KAAK,KAAK,CAAC;CAChD;CAEA,KAAK,MAAsC;EACzC,IAAI,KAAK,eAAe,qBAAqB,MAC3C,MAAM,IAAI,UAAU,8BAA8B;EAEpD,KAAK,KAAK,YAAY;GAAE,MAAM;GAAW;EAAK,CAA2C;CAC3F;CAEA,MAAM,OAAO,KAAM,SAAS,IAAU;EACpC,IAAI,CAAC,KAAKC,YAAY,GAAG;EACzB,KAAK,KAAK,YAAY;GACpB,MAAM;GACN;GACA;EACF,CAA2C;EAC3C,KAAKC,aAAa,MAAM,QAAQ,IAAI;CACtC;CAEA,OAAO,UAA8C;EACnD,KAAK,KAAK;CACZ;CAEA,OAAa;EACX,IAAI,CAAC,KAAKC,UAAU,GAAG;EAGvB,KAAKC,0BAA0B;EAC/B,qBAAqB;GACnB,KAAK,MAAM,WAAW,KAAKL,oBAAoB,OAAO,CAAC,GAAG;IACxD,IAAI,KAAK,eAAe,qBAAqB,MAAM;IACnD,KAAKM,qBAAqB,OAAO;GACnC;GACA,KAAKD,0BAA0B;EACjC,CAAC;CACH;;CAGA,WAAqB,OAAO,MAAM,SAAS,IAAU;EACnD,KAAKF,aAAa,MAAM,QAAQ,KAAK;CACvC;CAEA,mBAAmB,OAAsB;EACvC,MAAM,UAAU,iBAAiB,KAAK;EACtC,IAAI,YAAY,MAAM;GACpB,KAAK,MAAM,MAAM,uCAAuC;GACxD;EACF;EACA,IAAI,QAAQ,SAAS,QAAQ;GAC3B,KAAK,KAAK;GACV;EACF;EACA,IAAI,QAAQ,SAAS,SAAS;GAC5B,KAAKG,qBAAqB,OAAO;GACjC;EACF;EACA,IAAI,KAAK,eAAe,qBAAqB,cAAc,KAAKD,yBAAyB;GACvF,KAAKL,oBAAoB,KAAK,OAAO;GACrC;EACF;EACA,KAAKM,qBAAqB,OAAO;CACnC;CAEA,qBAAqB,SAAgD;EACnE,IAAI,QAAQ,SAAS,SAAS;GAC5B,KAAKH,aAAa,QAAQ,MAAM,QAAQ,QAAQ,IAAI;GACpD;EACF;EACA,IAAI,KAAK,eAAe,qBAAqB,MAAM;EACnD,KAAKI,MACH,IAAI,aAAuC,WAAW,EAAE,MAAM,QAAQ,KAAK,CAAC,GAC5E,KAAK,SACP;CACF;CAEA,YAAqB;EACnB,IAAI,KAAK,eAAe,qBAAqB,YAAY,OAAO;EAChE,KAAK,aAAa,qBAAqB;EACvC,KAAKA,MAAM,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM;EACzC,OAAO;CACT;CAEA,cAAuB;EACrB,IACE,KAAK,eAAe,qBAAqB,WACzC,KAAK,eAAe,qBAAqB,QAEzC,OAAO;EAET,KAAK,aAAa,qBAAqB;EACvC,OAAO;CACT;CAEA,aAAa,MAAc,QAAgB,UAAyB;EAClE,IAAI,KAAK,eAAe,qBAAqB,QAAQ;EACrD,KAAK,aAAa,qBAAqB;EACvC,KAAKP,oBAAoB,SAAS;EAClC,KAAKK,0BAA0B;EAC/B,KAAK,KAAK,MAAM;EAChB,KAAKE,MAAM,IAAI,WAAW,SAAS;GAAE;GAAM;GAAQ;EAAS,CAAC,GAAG,KAAK,OAAO;CAC9E;CAEA,MAAuB,OAAU,SAA4C;EAC3E,UAAU,KAAK;EACf,KAAK,cAAc,KAAK;CAC1B;AACF;;AA0BA,SAAgB,gBAAgB,QAA0B,QAAoC;CAC5F,IAAI,OAAO,cAAc,qBAAqB,SAAS;EACrD,OAAO,MAAM,MAAM,8BAA8B;EACjD;CACF;CACA,OAAO,iBAAiB,YAAY,UAAU;EAC5C,IAAI,OAAO,cAAc,qBAAqB,SAAS;EACvD,MAAM,OAAO,iBAAiB,KAAK;EACnC,IAAI,SAAS,MAAM;GACjB,OAAO,MAAM,MAAM,2BAA2B;GAC9C;EACF;EACA,OAAO,KAAK,IAAI;CAClB,CAAC;CACD,OAAO,iBAAiB,UAAU,UAAU;EAC1C,MAAM,QAAQ,gBAAgB,KAAK;EACnC,OAAO,MAAM,OAAO,QAAQ,MAAM,OAAO,UAAU,+BAA+B;CACpF,CAAC;CACD,OAAO,iBAAiB,eAAe;EACrC,OAAO,MAAM,MAAM,kBAAkB;CACvC,CAAC;CACD,OAAO,iBAAiB,YAAY,UAAU;EAC5C,OAAO,KAAK,MAAM,IAAI;CACxB,CAAC;CACD,OAAO,iBAAiB,UAAU,UAAU;EAC1C,OAAO,MAAM,MAAM,MAAM,MAAM,MAAM;CACvC,CAAC;CACD,OAAO,OAAO;CACd,OAAO,KAAK;AACd;;AAiBA,SAAgB,sCACd,MACiC;CACjC,OAAO,MAAM,qCAAqC,qBAAqB;EACrE,YAAY,KAAmB,YAAgC;GAC7D,MAAM,UAAU,IAAI,eAAe;GACnC,MAAM,OAAO,GAAG,GAAG,QAAQ,OAAO,KAAK;GACvC,KAAK,YACH;IAAE,MAAM;IAAW,KAAK,OAAO,GAAG;IAAG,MAAM,QAAQ;GAAM,GACzD,CAAC,QAAQ,KAAK,CAChB;EACF;CACF;AACF;;AAGA,SAAgB,2BACd,MACA,SACY;CACZ,MAAM,0BAAU,IAAI,IAA0B;CAC9C,MAAM,YAAY,UAAuC;EACvD,MAAM,UAAU,mBAAmB,MAAM,IAAI;EAC7C,IAAI,YAAY,MAAM;EACtB,MAAM,SAAS,IAAI,qBAAqB,QAAQ,KAAK,QAAQ,MAAM,KAAK;EACxE,QAAQ,IAAI,MAAM;EAClB,OAAO,iBAAiB,eAAe,QAAQ,OAAO,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;EAC7E,QAAa,QAAQ,GAAG,CAAC,CAAC,MACvB,WAAW;GACV,IAAI,CAAC,QAAQ,IAAI,MAAM,GAAG;IACxB,OAAO,MAAM,MAAM,cAAc;IACjC;GACF;GACA,gBAAgB,QAAQ,MAAM;GAC9B,QAAQ,KAAK,YAAY,EAAE,MAAM,OAAO,CAA4C;EACtF,SACM,OAAO,MAAM,MAAM,6BAA6B,CACxD;CACF;CACA,KAAK,iBAAiB,WAAW,QAAQ;CACzC,KAAK,MAAM;CAEX,aAAa;EACX,KAAK,oBAAoB,WAAW,QAAQ;EAC5C,KAAK,MAAM,UAAU,SAAS,OAAO,MAAM,MAAM,cAAc;EAC/D,QAAQ,MAAM;EACd,KAAK,MAAM;CACb;AACF;AAEA,SAAS,iBACP,OAC2E;CAC3E,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAC7B,IAAI,MAAM,SAAS,QAAQ,OAAO,EAAE,MAAM,OAAO;CACjD,IAAI,MAAM,SAAS,aAAa,gBAAgB,MAAM,IAAI,GACxD,OAAO;EAAE,MAAM;EAAW,MAAM,MAAM;CAAK;CAE7C,IACE,MAAM,SAAS,WACf,OAAO,MAAM,SAAS,YACtB,OAAO,UAAU,MAAM,IAAI,KAC3B,OAAO,MAAM,WAAW,UAExB,OAAO;EAAE,MAAM;EAAS,MAAM,MAAM;EAAM,QAAQ,MAAM;CAAO;CAEjE,OAAO;AACT;AAEA,SAAS,mBAAmB,OAAsC;CAChE,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAC7B,IAAI,MAAM,SAAS,aAAa,OAAO,MAAM,QAAQ,UAAU,OAAO;CACtE,IAAI,EAAE,MAAM,gBAAgB,cAAc,OAAO;CACjD,OAAO;EAAE,MAAM;EAAW,KAAK,MAAM;EAAK,MAAM,MAAM;CAAK;AAC7D;AAEA,SAAS,iBAAiB,OAA+C;CACvE,IAAI,EAAE,UAAU,UAAU,CAAC,gBAAgB,MAAM,IAAI,GAAG,OAAO;CAC/D,OAAO,MAAM;AACf;AAEA,SAAS,gBAAgB,OAAyE;CAChG,IAAI,EAAE,UAAU,UAAU,EAAE,YAAY,QAAQ,OAAO;CACvD,IAAI,OAAO,MAAM,SAAS,YAAY,CAAC,OAAO,UAAU,MAAM,IAAI,GAAG,OAAO;CAC5E,IAAI,OAAO,MAAM,WAAW,UAAU,OAAO;CAC7C,OAAO;EAAE,MAAM,MAAM;EAAM,QAAQ,MAAM;CAAO;AAClD;AAEA,SAAS,gBAAgB,OAAmD;CAC1E,OACE,OAAO,UAAU,YACjB,iBAAiB,eACjB,YAAY,OAAO,KAAK;AAE5B;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E"}
@@ -0,0 +1,33 @@
1
+ //#region src/browser/offscreen-document.ts
2
+ /**
3
+ * Keeps one browser offscreen document alive across concurrent callers and a
4
+ * stale, unlisted document slot. Readiness and application policy stay with
5
+ * the embedding host.
6
+ */
7
+ var OffscreenDocumentCoordinator = class {
8
+ adapter;
9
+ #ensuring;
10
+ constructor(adapter) {
11
+ this.adapter = adapter;
12
+ }
13
+ ensure() {
14
+ this.#ensuring ??= this.#ensureOnce().finally(() => {
15
+ this.#ensuring = void 0;
16
+ });
17
+ return this.#ensuring;
18
+ }
19
+ async #ensureOnce() {
20
+ if (await this.adapter.exists()) return;
21
+ try {
22
+ await this.adapter.create();
23
+ } catch (error) {
24
+ if (!this.adapter.isOccupiedError(error)) throw error;
25
+ await this.adapter.close();
26
+ await this.adapter.create();
27
+ }
28
+ }
29
+ };
30
+ //#endregion
31
+ export { OffscreenDocumentCoordinator };
32
+
33
+ //# sourceMappingURL=offscreen-document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offscreen-document.js","names":["#ensuring","#ensureOnce"],"sources":["../../src/browser/offscreen-document.ts"],"sourcesContent":["export type OffscreenDocumentAdapter = {\n exists(): Promise<boolean>;\n create(): Promise<void>;\n close(): Promise<void>;\n isOccupiedError(error: unknown): boolean;\n};\n\n/**\n * Keeps one browser offscreen document alive across concurrent callers and a\n * stale, unlisted document slot. Readiness and application policy stay with\n * the embedding host.\n */\nexport class OffscreenDocumentCoordinator {\n #ensuring: Promise<void> | undefined;\n\n constructor(private readonly adapter: OffscreenDocumentAdapter) {}\n\n ensure(): Promise<void> {\n this.#ensuring ??= this.#ensureOnce().finally(() => {\n this.#ensuring = undefined;\n });\n return this.#ensuring;\n }\n\n async #ensureOnce(): Promise<void> {\n if (await this.adapter.exists()) return;\n try {\n await this.adapter.create();\n } catch (error) {\n if (!this.adapter.isOccupiedError(error)) throw error;\n await this.adapter.close();\n await this.adapter.create();\n }\n }\n}\n"],"mappings":";;;;;;AAYA,IAAa,+BAAb,MAA0C;CAGX;CAF7B;CAEA,YAAY,SAAoD;EAAnC,KAAA,UAAA;CAAoC;CAEjE,SAAwB;EACtB,KAAKA,cAAc,KAAKC,YAAY,CAAC,CAAC,cAAc;GAClD,KAAKD,YAAY,KAAA;EACnB,CAAC;EACD,OAAO,KAAKA;CACd;CAEA,MAAMC,cAA6B;EACjC,IAAI,MAAM,KAAK,QAAQ,OAAO,GAAG;EACjC,IAAI;GACF,MAAM,KAAK,QAAQ,OAAO;EAC5B,SAAS,OAAO;GACd,IAAI,CAAC,KAAK,QAAQ,gBAAgB,KAAK,GAAG,MAAM;GAChD,MAAM,KAAK,QAAQ,MAAM;GACzB,MAAM,KAAK,QAAQ,OAAO;EAC5B;CACF;AACF"}