diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c80beaff8..404cf9ab5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -62,6 +62,10 @@ jobs: env: ELECTRIC_URL: http://localhost:3000 + - name: Run Cloudflare Durable Object persisted collection E2E tests + run: | + cd packages/db-cloudflare-do-sqlite-persisted-collection + pnpm test:e2e - name: Run React Native/Expo persisted collection E2E tests run: | diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/README.md b/packages/db-cloudflare-do-sqlite-persisted-collection/README.md new file mode 100644 index 000000000..2393faea0 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/README.md @@ -0,0 +1,48 @@ +# @tanstack/db-cloudflare-do-sqlite-persisted-collection + +Thin SQLite persistence for Cloudflare Durable Objects. + +## Public API + +- `createCloudflareDOSQLitePersistence(...)` +- `persistedCollectionOptions(...)` (re-exported from core) + +## Quick start + +```ts +import { createCollection } from '@tanstack/db' +import { + createCloudflareDOSQLitePersistence, + persistedCollectionOptions, +} from '@tanstack/db-cloudflare-do-sqlite-persisted-collection' + +type Todo = { + id: string + title: string + completed: boolean +} + +export class TodosObject extends DurableObject { + persistence = createCloudflareDOSQLitePersistence({ + // Pass full storage to use native DO transaction support. + storage: this.ctx.storage, + }) + + todos = createCollection( + persistedCollectionOptions({ + id: `todos`, + getKey: (todo) => todo.id, + persistence: this.persistence, + schemaVersion: 1, // Per-collection schema version + }), + ) +} +``` + +## Notes + +- One shared persistence instance can serve multiple collections. +- Mode defaults are inferred from collection usage: + - sync config present => `sync-present-reset` + - no sync config => `sync-absent-error` +- You can still override with `schemaMismatchPolicy` if needed. diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/cloudflare-do-runtime-bridge.e2e.test.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/cloudflare-do-runtime-bridge.e2e.test.ts new file mode 100644 index 000000000..0cb1c1435 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/cloudflare-do-runtime-bridge.e2e.test.ts @@ -0,0 +1,422 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { setTimeout as delay } from 'node:timers/promises' +import { fileURLToPath } from 'node:url' +import { spawn } from 'node:child_process' +import { describe, expect, it } from 'vitest' +import { runRuntimeBridgeE2EContractSuite } from '../../db-sqlite-persisted-collection-core/tests/contracts/runtime-bridge-e2e-contract' +import type { + RuntimeBridgeE2EContractError, + RuntimeBridgeE2EContractHarness, + RuntimeBridgeE2EContractHarnessFactory, + RuntimeBridgeE2EContractTodo, +} from '../../db-sqlite-persisted-collection-core/tests/contracts/runtime-bridge-e2e-contract' + +type RuntimeProcessHarness = { + baseUrl: string + restart: () => Promise + stop: () => Promise +} + +type WranglerRuntimeResponse = + | { + ok: true + rows?: TPayload + } + | { + ok: false + error: RuntimeBridgeE2EContractError + } + +const packageDirectory = dirname(fileURLToPath(import.meta.url)) +const wranglerConfigPath = join(packageDirectory, `fixtures`, `wrangler.toml`) + +async function getAvailablePort(): Promise { + const netModule = await import('node:net') + return new Promise((resolve, reject) => { + const server = netModule.createServer() + server.listen(0, `127.0.0.1`, () => { + const address = server.address() + if (!address || typeof address === `string`) { + server.close() + reject(new Error(`Unable to allocate an available local port`)) + return + } + const selectedPort = address.port + server.close((error) => { + if (error) { + reject(error) + return + } + resolve(selectedPort) + }) + }) + server.on(`error`, reject) + }) +} + +function createRuntimeError( + message: string, + stderr: string, + stdout: string, +): Error { + return new Error([message, `stderr=${stderr}`, `stdout=${stdout}`].join(`\n`)) +} + +async function stopWranglerProcess( + child: ReturnType | undefined, +): Promise { + if (!child || child.exitCode !== null) { + return + } + + child.kill(`SIGTERM`) + const closed = await Promise.race([ + new Promise((resolve) => { + child.once(`close`, () => resolve(true)) + }), + delay(5_000).then(() => false), + ]) + + if (closed) { + return + } + + child.kill(`SIGKILL`) + await new Promise((resolve) => { + child.once(`close`, () => resolve()) + }) +} + +async function startWranglerRuntime(options: { + persistPath: string + syncEnabled?: boolean + schemaVersion?: number + collectionId?: string +}): Promise { + let child: ReturnType | undefined + let stdoutBuffer = `` + let stderrBuffer = `` + const port = await getAvailablePort() + + const spawnProcess = async (): Promise => { + const runtimeVarEntries = [ + [ + `PERSISTENCE_WITH_SYNC`, + options.syncEnabled !== undefined + ? String(options.syncEnabled) + : undefined, + ], + [ + `PERSISTENCE_SCHEMA_VERSION`, + options.schemaVersion !== undefined + ? String(options.schemaVersion) + : undefined, + ], + [`PERSISTENCE_COLLECTION_ID`, options.collectionId], + ].filter((entry): entry is [string, string] => entry[1] !== undefined) + + const wranglerArgs = [ + `exec`, + `wrangler`, + `dev`, + `--local`, + `--ip`, + `127.0.0.1`, + `--port`, + String(port), + `--persist-to`, + options.persistPath, + `--config`, + wranglerConfigPath, + ...runtimeVarEntries.flatMap(([key, value]) => [ + `--var`, + `${key}:${value}`, + ]), + ] + + child = spawn(`pnpm`, wranglerArgs, { + cwd: packageDirectory, + env: { + ...process.env, + CI: `1`, + WRANGLER_SEND_METRICS: `false`, + }, + stdio: [`ignore`, `pipe`, `pipe`], + }) + + if (!child.stdout || !child.stderr) { + throw new Error(`Unable to capture wrangler dev process output streams`) + } + + child.stdout.on(`data`, (chunk: Buffer) => { + stdoutBuffer += chunk.toString() + }) + child.stderr.on(`data`, (chunk: Buffer) => { + stderrBuffer += chunk.toString() + }) + + const baseUrl = `http://127.0.0.1:${String(port)}` + const startAt = Date.now() + while (Date.now() - startAt < 45_000) { + if (child.exitCode !== null) { + throw createRuntimeError( + `Wrangler dev exited before becoming healthy`, + stderrBuffer, + stdoutBuffer, + ) + } + + try { + const healthResponse = await fetch(`${baseUrl}/health`) + if (healthResponse.ok) { + return + } + } catch { + // Runtime may still be starting. + } + + await delay(250) + } + + throw createRuntimeError( + `Timed out waiting for wrangler dev runtime`, + stderrBuffer, + stdoutBuffer, + ) + } + + await spawnProcess() + + return { + baseUrl: `http://127.0.0.1:${String(port)}`, + restart: async () => { + await stopWranglerProcess(child) + stdoutBuffer = `` + stderrBuffer = `` + await spawnProcess() + }, + stop: async () => { + await stopWranglerProcess(child) + }, + } +} + +async function postJson( + baseUrl: string, + path: string, + body: unknown, +): Promise> { + const response = await fetch(`${baseUrl}${path}`, { + method: `POST`, + headers: { + 'content-type': `application/json`, + }, + body: JSON.stringify(body), + }) + + const parsed = (await response.json()) as WranglerRuntimeResponse + return parsed +} + +function assertRuntimeError( + response: WranglerRuntimeResponse, +): RuntimeBridgeE2EContractError { + if (!response.ok) { + return response.error + } + + throw new Error(`Expected runtime call to fail, but it succeeded`) +} + +function assertRuntimeSuccess( + response: WranglerRuntimeResponse, +): TPayload | undefined { + if (response.ok) { + return response.rows + } + + throw new Error(`${response.error.name}: ${response.error.message}`) +} + +const createHarness: RuntimeBridgeE2EContractHarnessFactory = () => { + const tempDirectory = mkdtempSync(join(tmpdir(), `db-cloudflare-do-e2e-`)) + const persistPath = join(tempDirectory, `wrangler-state`) + const collectionId = `todos` + let nextSequence = 1 + const runtimePromise = startWranglerRuntime({ + persistPath, + }) + + const harness: RuntimeBridgeE2EContractHarness = { + writeTodoFromClient: async (todo: RuntimeBridgeE2EContractTodo) => { + const runtime = await runtimePromise + const result = await postJson(runtime.baseUrl, `/write-todo`, { + collectionId, + todo, + txId: `tx-${nextSequence}`, + seq: nextSequence, + rowVersion: nextSequence, + }) + nextSequence++ + + if (!result.ok) { + throw new Error(`${result.error.name}: ${result.error.message}`) + } + }, + loadTodosFromClient: async (targetCollectionId?: string) => { + const runtime = await runtimePromise + const result = await postJson< + Array<{ key: string; value: RuntimeBridgeE2EContractTodo }> + >(runtime.baseUrl, `/load-todos`, { + collectionId: targetCollectionId ?? collectionId, + }) + if (!result.ok) { + throw new Error(`${result.error.name}: ${result.error.message}`) + } + return result.rows ?? [] + }, + loadUnknownCollectionErrorFromClient: + async (): Promise => { + const runtime = await runtimePromise + const result = await postJson( + runtime.baseUrl, + `/load-unknown-collection-error`, + { + collectionId: `missing`, + }, + ) + if (result.ok) { + throw new Error( + `Expected unknown collection request to fail, but it succeeded`, + ) + } + return result.error + }, + restartHost: async () => { + const runtime = await runtimePromise + await runtime.restart() + }, + cleanup: async () => { + try { + const runtime = await runtimePromise + await runtime.stop() + } finally { + rmSync(tempDirectory, { recursive: true, force: true }) + } + }, + } + + return harness +} + +runRuntimeBridgeE2EContractSuite( + `cloudflare durable object runtime bridge e2e (wrangler local)`, + createHarness, + { + testTimeoutMs: 90_000, + }, +) + +describe(`cloudflare durable object schema mismatch behavior (wrangler local)`, () => { + it(`throws on schema mismatch in sync-absent mode`, async () => { + const tempDirectory = mkdtempSync( + join(tmpdir(), `db-cloudflare-do-local-mismatch-e2e-`), + ) + const persistPath = join(tempDirectory, `wrangler-state`) + const collectionId = `todos` + let runtime = await startWranglerRuntime({ + persistPath, + syncEnabled: false, + schemaVersion: 1, + collectionId, + }) + + try { + const writeResult = await postJson(runtime.baseUrl, `/write-todo`, { + collectionId, + txId: `tx-1`, + seq: 1, + rowVersion: 1, + todo: { + id: `local-1`, + title: `Local mode row`, + score: 10, + }, + }) + assertRuntimeSuccess(writeResult) + } finally { + await runtime.stop() + } + + runtime = await startWranglerRuntime({ + persistPath, + syncEnabled: false, + schemaVersion: 2, + collectionId, + }) + try { + const loadResult = await postJson< + Array<{ key: string; value: RuntimeBridgeE2EContractTodo }> + >(runtime.baseUrl, `/load-todos`, { + collectionId, + }) + const runtimeError = assertRuntimeError(loadResult) + expect(runtimeError.message).toContain(`Schema version mismatch`) + } finally { + await runtime.stop() + rmSync(tempDirectory, { recursive: true, force: true }) + } + }, 90_000) + + it(`resets collection on schema mismatch in sync-present mode`, async () => { + const tempDirectory = mkdtempSync( + join(tmpdir(), `db-cloudflare-do-sync-mismatch-e2e-`), + ) + const persistPath = join(tempDirectory, `wrangler-state`) + const collectionId = `todos` + let runtime = await startWranglerRuntime({ + persistPath, + syncEnabled: true, + schemaVersion: 1, + collectionId, + }) + + try { + const writeResult = await postJson(runtime.baseUrl, `/write-todo`, { + collectionId, + txId: `tx-1`, + seq: 1, + rowVersion: 1, + todo: { + id: `sync-1`, + title: `Sync mode row`, + score: 20, + }, + }) + assertRuntimeSuccess(writeResult) + } finally { + await runtime.stop() + } + + runtime = await startWranglerRuntime({ + persistPath, + syncEnabled: true, + schemaVersion: 2, + collectionId, + }) + try { + const loadResult = await postJson< + Array<{ key: string; value: RuntimeBridgeE2EContractTodo }> + >(runtime.baseUrl, `/load-todos`, { + collectionId, + }) + const rows = assertRuntimeSuccess(loadResult) ?? [] + expect(rows).toEqual([]) + } finally { + await runtime.stop() + rmSync(tempDirectory, { recursive: true, force: true }) + } + }, 90_000) +}) diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/fixtures/worker.mjs b/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/fixtures/worker.mjs new file mode 100644 index 000000000..60b863e40 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/fixtures/worker.mjs @@ -0,0 +1,286 @@ +// @ts-nocheck +import { DurableObject } from 'cloudflare:workers' +import { createCollection } from '../../../db/dist/esm/index.js' +import { + createCloudflareDOSQLitePersistence, + persistedCollectionOptions, +} from '../../dist/esm/index.js' + +const DEFAULT_COLLECTION_ID = `todos` +const DEFAULT_SCHEMA_VERSION = 1 + +function resolveCollectionPersistence({ + persistence, + collectionId, + syncEnabled, + schemaVersion, +}) { + const mode = syncEnabled ? `sync-present` : `sync-absent` + return ( + persistence.resolvePersistenceForCollection?.({ + collectionId, + mode, + schemaVersion, + }) ?? + persistence.resolvePersistenceForMode?.(mode) ?? + persistence + ) +} + +function parseSyncEnabled(rawValue) { + if (rawValue == null) { + return false + } + + const normalized = String(rawValue).toLowerCase() + if (normalized === `1` || normalized === `true`) { + return true + } + if (normalized === `0` || normalized === `false`) { + return false + } + + throw new Error(`Invalid PERSISTENCE_WITH_SYNC "${String(rawValue)}"`) +} + +function parseSchemaVersion(rawSchemaVersion) { + if (rawSchemaVersion == null) { + return DEFAULT_SCHEMA_VERSION + } + const parsed = Number(rawSchemaVersion) + if (Number.isInteger(parsed) && parsed >= 0) { + return parsed + } + throw new Error( + `Invalid PERSISTENCE_SCHEMA_VERSION "${String(rawSchemaVersion)}"`, + ) +} + +function jsonResponse(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { + 'content-type': 'application/json', + }, + }) +} + +function serializeError(error) { + if (error && typeof error === `object`) { + const maybeCode = error.code + return { + name: typeof error.name === `string` ? error.name : `Error`, + message: + typeof error.message === `string` + ? error.message + : `Unknown Cloudflare DO runtime error`, + code: typeof maybeCode === `string` ? maybeCode : undefined, + } + } + + return { + name: `Error`, + message: `Unknown Cloudflare DO runtime error`, + code: undefined, + } +} + +function createUnknownCollectionError(collectionId) { + const error = new Error( + `Unknown cloudflare durable object persistence collection "${collectionId}"`, + ) + error.name = `UnknownCloudflareDOPersistenceCollectionError` + error.code = `UNKNOWN_COLLECTION` + return error +} + +export class PersistenceObject extends DurableObject { + constructor(ctx, env) { + super(ctx, env) + this.collectionId = env.PERSISTENCE_COLLECTION_ID ?? DEFAULT_COLLECTION_ID + this.syncEnabled = parseSyncEnabled(env.PERSISTENCE_WITH_SYNC) + this.schemaVersion = parseSchemaVersion(env.PERSISTENCE_SCHEMA_VERSION) + this.persistence = createCloudflareDOSQLitePersistence({ + storage: this.ctx.storage, + }) + this.collectionPersistence = resolveCollectionPersistence({ + persistence: this.persistence, + collectionId: this.collectionId, + syncEnabled: this.syncEnabled, + schemaVersion: this.schemaVersion, + }) + this.ready = this.collectionPersistence.adapter.loadSubset( + this.collectionId, + { + limit: 0, + }, + ) + + const baseCollectionOptions = { + id: this.collectionId, + schemaVersion: this.schemaVersion, + getKey: (todo) => todo.id, + persistence: this.persistence, + } + this.collection = createCollection( + this.syncEnabled + ? persistedCollectionOptions({ + ...baseCollectionOptions, + sync: { + sync: ({ markReady }) => { + markReady() + }, + }, + }) + : persistedCollectionOptions(baseCollectionOptions), + ) + this.collectionReady = this.collection.stateWhenReady() + } + + async fetch(request) { + const url = new URL(request.url) + + try { + if (request.method === `GET` && url.pathname === `/health`) { + return jsonResponse(200, { + ok: true, + }) + } + + await this.ready + + if (request.method === `GET` && url.pathname === `/runtime-config`) { + return jsonResponse(200, { + ok: true, + collectionId: this.collectionId, + mode: this.syncEnabled ? `sync` : `local`, + syncEnabled: this.syncEnabled, + schemaVersion: this.schemaVersion, + }) + } + + const requestBody = await request.json() + const collectionId = requestBody.collectionId ?? this.collectionId + + if (request.method === `POST` && url.pathname === `/write-todo`) { + if (collectionId !== this.collectionId) { + throw createUnknownCollectionError(collectionId) + } + if (this.syncEnabled) { + const txId = + typeof requestBody.txId === `string` + ? requestBody.txId + : crypto.randomUUID() + const seq = + typeof requestBody.seq === `number` ? requestBody.seq : Date.now() + const rowVersion = + typeof requestBody.rowVersion === `number` + ? requestBody.rowVersion + : seq + await this.collectionPersistence.adapter.applyCommittedTx( + collectionId, + { + txId, + term: 1, + seq, + rowVersion, + mutations: [ + { + type: `insert`, + key: requestBody.todo.id, + value: requestBody.todo, + }, + ], + }, + ) + + return jsonResponse(200, { + ok: true, + }) + } + await this.collectionReady + const tx = this.collection.insert(requestBody.todo) + await tx.isPersisted.promise + + return jsonResponse(200, { + ok: true, + }) + } + + if (request.method === `POST` && url.pathname === `/load-todos`) { + if (collectionId !== this.collectionId) { + throw createUnknownCollectionError(collectionId) + } + if (this.syncEnabled) { + const rows = await this.collectionPersistence.adapter.loadSubset( + collectionId, + {}, + ) + return jsonResponse(200, { + ok: true, + rows: rows.map((row) => ({ + key: row.key, + value: row.value, + })), + }) + } + await this.collectionReady + const rows = this.collection.toArray.map((todo) => ({ + key: todo.id, + value: todo, + })) + return jsonResponse(200, { + ok: true, + rows, + }) + } + + if ( + request.method === `POST` && + url.pathname === `/load-unknown-collection-error` + ) { + const unknownCollectionId = requestBody.collectionId ?? `missing` + if (unknownCollectionId !== this.collectionId) { + throw createUnknownCollectionError(unknownCollectionId) + } + const rows = await this.persistence.adapter.loadSubset( + unknownCollectionId, + {}, + ) + return jsonResponse(200, { + ok: true, + rows, + }) + } + + return jsonResponse(404, { + ok: false, + error: { + name: `NotFound`, + message: `Unknown durable object endpoint "${url.pathname}"`, + code: `NOT_FOUND`, + }, + }) + } catch (error) { + return jsonResponse(500, { + ok: false, + error: serializeError(error), + }) + } + } +} + +export default { + async fetch(request, env) { + const url = new URL(request.url) + if (url.pathname === `/health`) { + return jsonResponse(200, { + ok: true, + }) + } + + const id = env.PERSISTENCE.idFromName(`default`) + const stub = env.PERSISTENCE.get(id) + return stub.fetch(request) + }, +} diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/fixtures/wrangler.toml b/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/fixtures/wrangler.toml new file mode 100644 index 000000000..ad9fabad1 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/e2e/fixtures/wrangler.toml @@ -0,0 +1,11 @@ +name = "tanstack-db-cloudflare-do-e2e" +main = "./worker.mjs" +compatibility_date = "2026-02-11" + +[[durable_objects.bindings]] +name = "PERSISTENCE" +class_name = "PersistenceObject" + +[[migrations]] +tag = "v1" +new_sqlite_classes = ["PersistenceObject"] diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/package.json b/packages/db-cloudflare-do-sqlite-persisted-collection/package.json new file mode 100644 index 000000000..44ed62eda --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/package.json @@ -0,0 +1,61 @@ +{ + "name": "@tanstack/db-cloudflare-do-sqlite-persisted-collection", + "version": "0.1.0", + "description": "Cloudflare Durable Object SQLite persisted collection adapter for TanStack DB", + "author": "TanStack Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/db.git", + "directory": "packages/db-cloudflare-do-sqlite-persisted-collection" + }, + "homepage": "https://tanstack.com/db", + "keywords": [ + "sqlite", + "cloudflare", + "durable-objects", + "persistence", + "typescript" + ], + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "lint": "eslint . --fix", + "test": "vitest --run", + "test:e2e": "pnpm --filter @tanstack/db-ivm build && pnpm --filter @tanstack/db build && pnpm --filter @tanstack/db-sqlite-persisted-collection-core build && pnpm --filter @tanstack/db-cloudflare-do-sqlite-persisted-collection build && vitest --config vitest.e2e.config.ts --run" + }, + "type": "module", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "dependencies": { + "@tanstack/db-sqlite-persisted-collection-core": "workspace:*" + }, + "peerDependencies": { + "typescript": ">=4.7" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@vitest/coverage-istanbul": "^3.2.4", + "better-sqlite3": "^12.6.2", + "wrangler": "^4.64.0" + } +} diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/src/do-driver.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/src/do-driver.ts new file mode 100644 index 000000000..4cd98bdf9 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/src/do-driver.ts @@ -0,0 +1,263 @@ +import { InvalidPersistedCollectionConfigError } from '@tanstack/db-sqlite-persisted-collection-core' +import type { SQLiteDriver } from '@tanstack/db-sqlite-persisted-collection-core' + +type DurableObjectSqlRow = Record + +type DurableObjectSqlCursorLike = Iterable & { + toArray?: () => Array +} + +export type DurableObjectSqlStorageLike = { + exec: ( + sql: string, + ...params: ReadonlyArray + ) => DurableObjectSqlCursorLike | ReadonlyArray | null +} + +export type DurableObjectTransactionExecutor = ( + fn: () => Promise, +) => Promise + +export type DurableObjectStorageLike = { + sql: DurableObjectSqlStorageLike + transaction?: DurableObjectTransactionExecutor +} + +type CloudflareDOProvidedSqlOptions = { + sql: DurableObjectSqlStorageLike + transaction?: DurableObjectTransactionExecutor +} + +type CloudflareDOProvidedStorageOptions = { + storage: DurableObjectStorageLike +} + +export type CloudflareDOSQLiteDriverOptions = + | CloudflareDOProvidedSqlOptions + | CloudflareDOProvidedStorageOptions + +function assertTransactionCallbackHasDriverArg( + fn: (transactionDriver: SQLiteDriver) => Promise, +): void { + if (fn.length > 0) { + return + } + + throw new InvalidPersistedCollectionConfigError( + `SQLiteDriver.transaction callback must accept the transaction driver argument`, + ) +} + +function isIterableRecord( + value: unknown, +): value is Iterable { + if (!value || typeof value !== `object`) { + return false + } + + const iterator = (value as { [Symbol.iterator]?: unknown })[Symbol.iterator] + return typeof iterator === `function` +} + +function toRowArray( + result: ReturnType, + sql: string, +): ReadonlyArray { + if (result == null) { + return [] + } + + if (Array.isArray(result)) { + return result as ReadonlyArray + } + + const cursorResult = result as DurableObjectSqlCursorLike + if (typeof cursorResult.toArray === `function`) { + return cursorResult.toArray() as ReadonlyArray + } + + if (isIterableRecord(cursorResult)) { + return Array.from(cursorResult as Iterable) + } + + throw new InvalidPersistedCollectionConfigError( + `Unsupported Durable Object SQL result shape for query "${sql}"`, + ) +} + +export class CloudflareDOSQLiteDriver implements SQLiteDriver { + private readonly sqlStorage: DurableObjectSqlStorageLike + private readonly storage: DurableObjectStorageLike + private readonly transactionExecutor: DurableObjectTransactionExecutor | null + private queue: Promise = Promise.resolve() + private nextSavepointId = 1 + + constructor(options: CloudflareDOSQLiteDriverOptions) { + const resolvedStorage: DurableObjectStorageLike = + `storage` in options + ? options.storage + : { + sql: options.sql, + ...(typeof options.transaction === `function` + ? { transaction: options.transaction } + : {}), + } + const resolvedSqlStorage = resolvedStorage.sql + if (typeof resolvedSqlStorage.exec !== `function`) { + throw new InvalidPersistedCollectionConfigError( + `Cloudflare DO SQL driver requires a sql.exec function`, + ) + } + this.storage = resolvedStorage + this.sqlStorage = resolvedSqlStorage + if (typeof resolvedStorage.transaction === `function`) { + const transactionMethod = resolvedStorage.transaction + this.transactionExecutor = (fn: () => Promise) => + Promise.resolve( + transactionMethod.call(resolvedStorage, fn) as Promise | T, + ) + } else { + this.transactionExecutor = null + } + } + + async exec(sql: string): Promise { + await this.enqueue(() => { + this.execute(sql) + }) + } + + async query( + sql: string, + params: ReadonlyArray = [], + ): Promise> { + return this.enqueue(() => this.executeQuery(sql, params)) + } + + async run(sql: string, params: ReadonlyArray = []): Promise { + await this.enqueue(() => { + this.execute(sql, params) + }) + } + + async transaction( + fn: (transactionDriver: SQLiteDriver) => Promise, + ): Promise { + assertTransactionCallbackHasDriverArg(fn) + + return this.enqueue(async () => { + const transactionDriver = this.createTransactionDriver() + if (this.transactionExecutor) { + return this.transactionExecutor(() => fn(transactionDriver)) + } + + this.execute(`BEGIN IMMEDIATE`) + try { + const result = await fn(transactionDriver) + this.execute(`COMMIT`) + return result + } catch (error) { + try { + this.execute(`ROLLBACK`) + } catch { + // Keep the original transaction error as the primary failure. + } + throw error + } + }) + } + + async transactionWithDriver( + fn: (transactionDriver: SQLiteDriver) => Promise, + ): Promise { + return this.transaction(fn) + } + + getStorage(): DurableObjectStorageLike { + return this.storage + } + + private execute(sql: string, params: ReadonlyArray = []): unknown { + return this.sqlStorage.exec(sql, ...params) + } + + private executeQuery( + sql: string, + params: ReadonlyArray, + ): ReadonlyArray { + const result = this.execute(sql, params) + return toRowArray( + result as ReturnType, + sql, + ) + } + + private enqueue(operation: () => Promise | T): Promise { + const queuedOperation = this.queue.then(operation, operation) + this.queue = queuedOperation.then( + () => undefined, + () => undefined, + ) + return queuedOperation + } + + private createTransactionDriver(): SQLiteDriver { + const transactionDriver: SQLiteDriver = { + exec: (sql) => { + this.execute(sql) + return Promise.resolve() + }, + query: ( + sql: string, + params: ReadonlyArray = [], + ): Promise> => + Promise.resolve(this.executeQuery(sql, params)), + run: (sql, params = []) => { + this.execute(sql, params) + return Promise.resolve() + }, + transaction: ( + fn: (nestedDriver: SQLiteDriver) => Promise, + ): Promise => { + assertTransactionCallbackHasDriverArg(fn) + return this.runNestedTransaction(transactionDriver, fn) + }, + transactionWithDriver: ( + fn: (nestedDriver: SQLiteDriver) => Promise, + ): Promise => this.runNestedTransaction(transactionDriver, fn), + } + + return transactionDriver + } + + private async runNestedTransaction( + transactionDriver: SQLiteDriver, + fn: (nestedDriver: SQLiteDriver) => Promise, + ): Promise { + if (this.transactionExecutor) { + throw new InvalidPersistedCollectionConfigError( + `Nested SQL savepoints are not supported when using Durable Object transaction API`, + ) + } + + const savepointName = `tsdb_sp_${this.nextSavepointId}` + this.nextSavepointId++ + this.execute(`SAVEPOINT ${savepointName}`) + + try { + const result = await fn(transactionDriver) + this.execute(`RELEASE SAVEPOINT ${savepointName}`) + return result + } catch (error) { + this.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`) + this.execute(`RELEASE SAVEPOINT ${savepointName}`) + throw error + } + } +} + +export function createCloudflareDOSQLiteDriver( + options: CloudflareDOSQLiteDriverOptions, +): CloudflareDOSQLiteDriver { + return new CloudflareDOSQLiteDriver(options) +} diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/src/do-persistence.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/src/do-persistence.ts new file mode 100644 index 000000000..d33302847 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/src/do-persistence.ts @@ -0,0 +1,166 @@ +import { + SingleProcessCoordinator, + createSQLiteCorePersistenceAdapter, +} from '@tanstack/db-sqlite-persisted-collection-core' +import { CloudflareDOSQLiteDriver } from './do-driver' +import type { + PersistedCollectionCoordinator, + PersistedCollectionMode, + PersistedCollectionPersistence, + SQLiteCoreAdapterOptions, + SQLiteDriver, +} from '@tanstack/db-sqlite-persisted-collection-core' +import type { DurableObjectStorageLike } from './do-driver' + +export type { DurableObjectStorageLike } from './do-driver' + +type CloudflareDOCoreSchemaMismatchPolicy = + | `sync-present-reset` + | `sync-absent-error` + | `reset` + +export type CloudflareDOSchemaMismatchPolicy = + | CloudflareDOCoreSchemaMismatchPolicy + | `throw` + +type CloudflareDOSQLitePersistenceBaseOptions = Omit< + SQLiteCoreAdapterOptions, + `driver` | `schemaVersion` | `schemaMismatchPolicy` +> & { + storage: DurableObjectStorageLike + coordinator?: PersistedCollectionCoordinator + schemaMismatchPolicy?: CloudflareDOSchemaMismatchPolicy +} + +export type CloudflareDOSQLitePersistenceOptions = + CloudflareDOSQLitePersistenceBaseOptions + +function normalizeSchemaMismatchPolicy( + policy: CloudflareDOSchemaMismatchPolicy, +): CloudflareDOCoreSchemaMismatchPolicy { + if (policy === `throw`) { + return `sync-absent-error` + } + return policy +} + +function resolveSchemaMismatchPolicy( + explicitPolicy: CloudflareDOSchemaMismatchPolicy | undefined, + mode: PersistedCollectionMode, +): CloudflareDOCoreSchemaMismatchPolicy { + if (explicitPolicy) { + return normalizeSchemaMismatchPolicy(explicitPolicy) + } + + return mode === `sync-present` ? `sync-present-reset` : `sync-absent-error` +} + +function createAdapterCacheKey( + schemaMismatchPolicy: CloudflareDOCoreSchemaMismatchPolicy, + schemaVersion: number | undefined, +): string { + const schemaVersionKey = + schemaVersion === undefined ? `schema:default` : `schema:${schemaVersion}` + return `${schemaMismatchPolicy}|${schemaVersionKey}` +} + +function resolveSQLiteDriver( + options: CloudflareDOSQLitePersistenceOptions, +): SQLiteDriver { + return new CloudflareDOSQLiteDriver({ + storage: options.storage, + }) +} + +function resolveAdapterBaseOptions( + options: CloudflareDOSQLitePersistenceOptions, +): Omit< + SQLiteCoreAdapterOptions, + `driver` | `schemaVersion` | `schemaMismatchPolicy` +> { + return { + appliedTxPruneMaxRows: options.appliedTxPruneMaxRows, + appliedTxPruneMaxAgeSeconds: options.appliedTxPruneMaxAgeSeconds, + pullSinceReloadThreshold: options.pullSinceReloadThreshold, + } +} + +/** + * Creates a shared Durable Object SQLite persistence instance that can be reused + * by many collections in a single Durable Object storage. + */ +export function createCloudflareDOSQLitePersistence< + T extends object, + TKey extends string | number = string | number, +>( + options: CloudflareDOSQLitePersistenceOptions, +): PersistedCollectionPersistence { + const { coordinator, schemaMismatchPolicy } = options + const driver = resolveSQLiteDriver(options) + const adapterBaseOptions = resolveAdapterBaseOptions(options) + const resolvedCoordinator = coordinator ?? new SingleProcessCoordinator() + const adapterCache = new Map< + string, + ReturnType< + typeof createSQLiteCorePersistenceAdapter< + Record, + string | number + > + > + >() + + const getAdapterForCollection = ( + mode: PersistedCollectionMode, + schemaVersion: number | undefined, + ) => { + const resolvedSchemaMismatchPolicy = resolveSchemaMismatchPolicy( + schemaMismatchPolicy, + mode, + ) + const cacheKey = createAdapterCacheKey( + resolvedSchemaMismatchPolicy, + schemaVersion, + ) + const cachedAdapter = adapterCache.get(cacheKey) + if (cachedAdapter) { + return cachedAdapter + } + + const adapter = createSQLiteCorePersistenceAdapter< + Record, + string | number + >({ + ...adapterBaseOptions, + driver, + schemaMismatchPolicy: resolvedSchemaMismatchPolicy, + ...(schemaVersion === undefined ? {} : { schemaVersion }), + }) + adapterCache.set(cacheKey, adapter) + return adapter + } + + const createCollectionPersistence = ( + mode: PersistedCollectionMode, + schemaVersion: number | undefined, + ): PersistedCollectionPersistence => ({ + adapter: getAdapterForCollection( + mode, + schemaVersion, + ) as unknown as PersistedCollectionPersistence[`adapter`], + coordinator: resolvedCoordinator, + }) + + const defaultPersistence = createCollectionPersistence( + `sync-absent`, + undefined, + ) + + return { + ...defaultPersistence, + resolvePersistenceForCollection: ({ mode, schemaVersion }) => + createCollectionPersistence(mode, schemaVersion), + // Backward compatible fallback for older callers. + resolvePersistenceForMode: (mode) => + createCollectionPersistence(mode, undefined), + } +} diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/src/index.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/src/index.ts new file mode 100644 index 000000000..b7c212b0c --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/src/index.ts @@ -0,0 +1,11 @@ +export { createCloudflareDOSQLitePersistence } from './do-persistence' +export type { + CloudflareDOSchemaMismatchPolicy, + CloudflareDOSQLitePersistenceOptions, + DurableObjectStorageLike, +} from './do-persistence' +export { persistedCollectionOptions } from '@tanstack/db-sqlite-persisted-collection-core' +export type { + PersistedCollectionCoordinator, + PersistedCollectionPersistence, +} from '@tanstack/db-sqlite-persisted-collection-core' diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-driver.test.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-driver.test.ts new file mode 100644 index 000000000..e5c47f4f0 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-driver.test.ts @@ -0,0 +1,100 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { runSQLiteDriverContractSuite } from '../../db-sqlite-persisted-collection-core/tests/contracts/sqlite-driver-contract' +import { CloudflareDOSQLiteDriver } from '../src/do-driver' +import { InvalidPersistedCollectionConfigError } from '../../db-sqlite-persisted-collection-core/src' +import { createBetterSqliteDoStorageHarness } from './helpers/better-sqlite-do-storage' +import type { SQLiteDriverContractHarness } from '../../db-sqlite-persisted-collection-core/tests/contracts/sqlite-driver-contract' + +function createDriverHarness(): SQLiteDriverContractHarness { + const tempDirectory = mkdtempSync(join(tmpdir(), `db-cf-do-driver-`)) + const dbPath = join(tempDirectory, `state.sqlite`) + const storageHarness = createBetterSqliteDoStorageHarness({ + filename: dbPath, + }) + const driver = new CloudflareDOSQLiteDriver({ + storage: storageHarness.storage, + }) + + return { + driver, + cleanup: () => { + try { + storageHarness.close() + } finally { + rmSync(tempDirectory, { recursive: true, force: true }) + } + }, + } +} + +runSQLiteDriverContractSuite( + `cloudflare durable object sqlite driver`, + createDriverHarness, +) + +describe(`cloudflare durable object sqlite driver (native transaction mode)`, () => { + it(`uses storage.transaction when available`, async () => { + const executedSql = new Array() + let transactionCalls = 0 + const driver = new CloudflareDOSQLiteDriver({ + storage: { + sql: { + exec: (sql) => { + executedSql.push(sql) + if (sql.startsWith(`SELECT`)) { + return [{ value: 1 }] + } + return [] + }, + }, + transaction: async (fn) => { + transactionCalls++ + return fn() + }, + }, + }) + + await driver.transaction(async (transactionDriver) => { + await transactionDriver.run(`INSERT INTO todos (id) VALUES (?)`, [`1`]) + const rows = await transactionDriver.query<{ value: number }>( + `SELECT 1 AS value`, + ) + expect(rows).toEqual([{ value: 1 }]) + }) + + expect(transactionCalls).toBe(1) + expect(executedSql).toContain(`INSERT INTO todos (id) VALUES (?)`) + expect(executedSql).not.toContain(`BEGIN IMMEDIATE`) + expect(executedSql).not.toContain(`COMMIT`) + }) + + it(`throws a clear error for nested transactions in native transaction mode`, async () => { + const driver = new CloudflareDOSQLiteDriver({ + storage: { + sql: { + exec: () => [], + }, + transaction: async (fn) => fn(), + }, + }) + + await expect( + driver.transaction(async (transactionDriver) => + transactionDriver.transaction((_nestedDriver) => + Promise.resolve(undefined), + ), + ), + ).rejects.toBeInstanceOf(InvalidPersistedCollectionConfigError) + + await expect( + driver.transaction(async (transactionDriver) => + transactionDriver.transaction((_nestedDriver) => + Promise.resolve(undefined), + ), + ), + ).rejects.toThrow(`Nested SQL savepoints are not supported`) + }) +}) diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-persistence.test.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-persistence.test.ts new file mode 100644 index 000000000..2f2f58712 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-persistence.test.ts @@ -0,0 +1,182 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { + createCloudflareDOSQLitePersistence, + persistedCollectionOptions, +} from '../src' +import { CloudflareDOSQLiteDriver } from '../src/do-driver' +import { SingleProcessCoordinator } from '../../db-sqlite-persisted-collection-core/src' +import { runRuntimePersistenceContractSuite } from '../../db-sqlite-persisted-collection-core/tests/contracts/runtime-persistence-contract' +import { createBetterSqliteDoStorageHarness } from './helpers/better-sqlite-do-storage' +import type { + RuntimePersistenceContractTodo, + RuntimePersistenceDatabaseHarness, +} from '../../db-sqlite-persisted-collection-core/tests/contracts/runtime-persistence-contract' + +function createRuntimeDatabaseHarness(): RuntimePersistenceDatabaseHarness { + const tempDirectory = mkdtempSync(join(tmpdir(), `db-cf-do-persistence-`)) + const dbPath = join(tempDirectory, `state.sqlite`) + const activeStorageHarnesses = new Set< + ReturnType + >() + + return { + createDriver: () => { + const storageHarness = createBetterSqliteDoStorageHarness({ + filename: dbPath, + }) + activeStorageHarnesses.add(storageHarness) + return new CloudflareDOSQLiteDriver({ + storage: storageHarness.storage, + }) + }, + cleanup: () => { + for (const storageHarness of activeStorageHarnesses) { + try { + storageHarness.close() + } catch { + // ignore cleanup errors from already-closed handles + } + } + activeStorageHarnesses.clear() + rmSync(tempDirectory, { recursive: true, force: true }) + }, + } +} + +runRuntimePersistenceContractSuite( + `cloudflare durable object runtime helpers`, + { + createDatabaseHarness: createRuntimeDatabaseHarness, + createAdapter: (driver) => + createCloudflareDOSQLitePersistence< + RuntimePersistenceContractTodo, + string + >({ + storage: (driver as CloudflareDOSQLiteDriver).getStorage(), + }).adapter, + createPersistence: (driver, coordinator) => + createCloudflareDOSQLitePersistence< + RuntimePersistenceContractTodo, + string + >({ + storage: (driver as CloudflareDOSQLiteDriver).getStorage(), + coordinator, + }), + createCoordinator: () => new SingleProcessCoordinator(), + }, +) + +describe(`cloudflare durable object persistence helpers`, () => { + it(`defaults coordinator to SingleProcessCoordinator`, () => { + const runtimeHarness = createRuntimeDatabaseHarness() + const driver = runtimeHarness.createDriver() + + try { + const persistence = createCloudflareDOSQLitePersistence({ + storage: (driver as CloudflareDOSQLiteDriver).getStorage(), + }) + expect(persistence.coordinator).toBeInstanceOf(SingleProcessCoordinator) + } finally { + runtimeHarness.cleanup() + } + }) + + it(`infers mode from sync presence and keeps schema per collection`, async () => { + const tempDirectory = mkdtempSync(join(tmpdir(), `db-cf-do-schema-infer-`)) + const dbPath = join(tempDirectory, `state.sqlite`) + const collectionId = `todos` + const firstStorageHarness = createBetterSqliteDoStorageHarness({ + filename: dbPath, + }) + const firstPersistence = createCloudflareDOSQLitePersistence< + RuntimePersistenceContractTodo, + string + >({ + storage: firstStorageHarness.storage, + }) + + try { + const firstCollectionOptions = persistedCollectionOptions< + RuntimePersistenceContractTodo, + string + >({ + id: collectionId, + schemaVersion: 1, + getKey: (todo) => todo.id, + persistence: firstPersistence, + }) + await firstCollectionOptions.persistence.adapter.applyCommittedTx( + collectionId, + { + txId: `tx-1`, + term: 1, + seq: 1, + rowVersion: 1, + mutations: [ + { + type: `insert`, + key: `1`, + value: { + id: `1`, + title: `before mismatch`, + score: 1, + }, + }, + ], + }, + ) + } finally { + firstStorageHarness.close() + } + + const secondStorageHarness = createBetterSqliteDoStorageHarness({ + filename: dbPath, + }) + const secondPersistence = createCloudflareDOSQLitePersistence< + RuntimePersistenceContractTodo, + string + >({ + storage: secondStorageHarness.storage, + }) + try { + const syncAbsentOptions = persistedCollectionOptions< + RuntimePersistenceContractTodo, + string + >({ + id: collectionId, + schemaVersion: 2, + getKey: (todo) => todo.id, + persistence: secondPersistence, + }) + await expect( + syncAbsentOptions.persistence.adapter.loadSubset(collectionId, {}), + ).rejects.toThrow(`Schema version mismatch`) + + const syncPresentOptions = persistedCollectionOptions< + RuntimePersistenceContractTodo, + string + >({ + id: collectionId, + schemaVersion: 2, + getKey: (todo) => todo.id, + sync: { + sync: ({ markReady }) => { + markReady() + }, + }, + persistence: secondPersistence, + }) + const rows = await syncPresentOptions.persistence.adapter.loadSubset( + collectionId, + {}, + ) + expect(rows).toEqual([]) + } finally { + secondStorageHarness.close() + rmSync(tempDirectory, { recursive: true, force: true }) + } + }) +}) diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-sqlite-core-adapter-contract.test.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-sqlite-core-adapter-contract.test.ts new file mode 100644 index 000000000..2e0c866e8 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/do-sqlite-core-adapter-contract.test.ts @@ -0,0 +1,46 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { runSQLiteCoreAdapterContractSuite } from '../../db-sqlite-persisted-collection-core/tests/contracts/sqlite-core-adapter-contract' +import { CloudflareDOSQLiteDriver } from '../src/do-driver' +import { SQLiteCorePersistenceAdapter } from '../../db-sqlite-persisted-collection-core/src' +import { createBetterSqliteDoStorageHarness } from './helpers/better-sqlite-do-storage' +import type { + SQLiteCoreAdapterContractTodo, + SQLiteCoreAdapterHarnessFactory, +} from '../../db-sqlite-persisted-collection-core/tests/contracts/sqlite-core-adapter-contract' + +const createHarness: SQLiteCoreAdapterHarnessFactory = (options) => { + const tempDirectory = mkdtempSync(join(tmpdir(), `db-cf-do-sql-core-`)) + const dbPath = join(tempDirectory, `state.sqlite`) + const storageHarness = createBetterSqliteDoStorageHarness({ + filename: dbPath, + }) + const driver = new CloudflareDOSQLiteDriver({ + storage: storageHarness.storage, + }) + const adapter = new SQLiteCorePersistenceAdapter< + SQLiteCoreAdapterContractTodo, + string + >({ + driver, + ...options, + }) + + return { + adapter, + driver, + cleanup: () => { + try { + storageHarness.close() + } finally { + rmSync(tempDirectory, { recursive: true, force: true }) + } + }, + } +} + +runSQLiteCoreAdapterContractSuite( + `SQLiteCorePersistenceAdapter (cloudflare do sqlite driver harness)`, + createHarness, +) diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/tests/helpers/better-sqlite-do-storage.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/helpers/better-sqlite-do-storage.ts new file mode 100644 index 000000000..c0d5e354c --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/tests/helpers/better-sqlite-do-storage.ts @@ -0,0 +1,63 @@ +import BetterSqlite3 from 'better-sqlite3' +import type { + DurableObjectSqlStorageLike, + DurableObjectStorageLike, +} from '../../src/do-driver' + +type BetterSqliteDoStorageHarness = { + sql: DurableObjectSqlStorageLike + storage: DurableObjectStorageLike + close: () => void +} + +type BetterSqliteStatement = ReturnType + +function readRows( + statement: BetterSqliteStatement, + params: ReadonlyArray, +) { + const statementWithVariadicIterate = statement as BetterSqliteStatement & { + iterate: (...params: ReadonlyArray) => Iterable + } + return statementWithVariadicIterate.iterate(...params) as Iterable< + Record + > +} + +function runStatement( + statement: BetterSqliteStatement, + params: ReadonlyArray, +): void { + const statementWithVariadicRun = statement as BetterSqliteStatement & { + run: (...params: ReadonlyArray) => unknown + } + statementWithVariadicRun.run(...params) +} + +export function createBetterSqliteDoStorageHarness(options: { + filename: string +}): BetterSqliteDoStorageHarness { + const database = new BetterSqlite3(options.filename) + + const sql: DurableObjectSqlStorageLike = { + exec: (sqlText, ...params) => { + const statement = database.prepare(sqlText) + if (statement.reader) { + return readRows(statement, params) + } + runStatement(statement, params) + return [] + }, + } + const storage: DurableObjectStorageLike = { + sql, + } + + return { + sql, + storage, + close: () => { + database.close() + }, + } +} diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/tsconfig.docs.json b/packages/db-cloudflare-do-sqlite-persisted-collection/tsconfig.docs.json new file mode 100644 index 000000000..5fddb4598 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/tsconfig.docs.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@tanstack/db": ["../db/src"], + "@tanstack/db-sqlite-persisted-collection-core": [ + "../db-sqlite-persisted-collection-core/src" + ] + } + }, + "include": ["src"] +} diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/tsconfig.json b/packages/db-cloudflare-do-sqlite-persisted-collection/tsconfig.json new file mode 100644 index 000000000..97ec70305 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "paths": { + "@tanstack/db": ["../db/src"], + "@tanstack/db-ivm": ["../db-ivm/src"], + "@tanstack/db-sqlite-persisted-collection-core": [ + "../db-sqlite-persisted-collection-core/src" + ] + } + }, + "include": [ + "src", + "tests", + "e2e/**/*.e2e.test.ts", + "vite.config.ts", + "vitest.e2e.config.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/vite.config.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/vite.config.ts new file mode 100644 index 000000000..ea27c667a --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/vite-config' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + name: packageJson.name, + include: [`tests/**/*.test.ts`], + environment: `node`, + coverage: { enabled: true, provider: `istanbul`, include: [`src/**/*`] }, + typecheck: { + enabled: true, + include: [`tests/**/*.test.ts`, `tests/**/*.test-d.ts`], + }, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: `./src/index.ts`, + srcDir: `./src`, + }), +) diff --git a/packages/db-cloudflare-do-sqlite-persisted-collection/vitest.e2e.config.ts b/packages/db-cloudflare-do-sqlite-persisted-collection/vitest.e2e.config.ts new file mode 100644 index 000000000..b17779a39 --- /dev/null +++ b/packages/db-cloudflare-do-sqlite-persisted-collection/vitest.e2e.config.ts @@ -0,0 +1,31 @@ +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +const packageDirectory = dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + resolve: { + alias: { + '@tanstack/db': resolve(packageDirectory, `../db/src`), + '@tanstack/db-ivm': resolve(packageDirectory, `../db-ivm/src`), + '@tanstack/db-sqlite-persisted-collection-core': resolve( + packageDirectory, + `../db-sqlite-persisted-collection-core/src`, + ), + }, + }, + test: { + include: [`e2e/**/*.e2e.test.ts`], + fileParallelism: false, + testTimeout: 90_000, + hookTimeout: 120_000, + environment: `node`, + typecheck: { + enabled: false, + }, + coverage: { + enabled: false, + }, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index beb589e68..f2533d1e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -829,6 +829,28 @@ importers: specifier: ^12.6.2 version: 12.6.2 + packages/db-cloudflare-do-sqlite-persisted-collection: + dependencies: + '@tanstack/db-sqlite-persisted-collection-core': + specifier: workspace:* + version: link:../db-sqlite-persisted-collection-core + typescript: + specifier: '>=4.7' + version: 5.9.3 + devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@vitest/coverage-istanbul': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4) + better-sqlite3: + specifier: ^12.6.2 + version: 12.6.2 + wrangler: + specifier: ^4.64.0 + version: 4.72.0 + packages/db-collection-e2e: dependencies: '@tanstack/db': @@ -2015,6 +2037,49 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.15.0': + resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260310.1': + resolution: {integrity: sha512-hF2VpoWaMb1fiGCQJqCY6M8I+2QQqjkyY4LiDYdTL5D/w6C1l5v1zhc0/jrjdD1DXfpJtpcSMSmEPjHse4p9Ig==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260310.1': + resolution: {integrity: sha512-h/Vl3XrYYPI6yFDE27XO1QPq/1G1lKIM8tzZGIWYpntK3IN5XtH3Ee/sLaegpJ49aIJoqhF2mVAZ6Yw+Vk2gJw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260310.1': + resolution: {integrity: sha512-XzQ0GZ8G5P4d74bQYOIP2Su4CLdNPpYidrInaSOuSxMw+HamsHaFrjVsrV2mPy/yk2hi6SY2yMbgKFK9YjA7vw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260310.1': + resolution: {integrity: sha512-sxv4CxnN4ZR0uQGTFVGa0V4KTqwdej/czpIc5tYS86G8FQQoGIBiAIs2VvU7b8EROPcandxYHDBPTb+D9HIMPw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260310.1': + resolution: {integrity: sha512-+1ZTViWKJypLfgH/luAHCqkent0DEBjAjvO40iAhOMHRLYP/SPphLvr4Jpi6lb+sIocS8Q1QZL4uM5Etg1Wskg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -2027,6 +2092,10 @@ packages: resolution: {integrity: sha512-KTy0OqRDLR5y/zZMnizyx09z/rPlPC/zKhYgH8o/q6PuAjoQAKlRfY4zzv0M64yybQ//6//4H1n14pxaLZfUnA==} engines: {node: '>=v18'} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -2100,6 +2169,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -2124,6 +2199,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -2148,6 +2229,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -2172,6 +2259,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -2196,6 +2289,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -2220,6 +2319,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -2244,6 +2349,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -2268,6 +2379,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -2292,6 +2409,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -2316,6 +2439,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -2340,6 +2469,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -2364,6 +2499,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -2388,6 +2529,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -2412,6 +2559,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -2436,6 +2589,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -2460,6 +2619,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -2484,6 +2649,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.11': resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} engines: {node: '>=18'} @@ -2502,6 +2673,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -2526,6 +2703,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.11': resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} engines: {node: '>=18'} @@ -2544,6 +2727,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -2568,6 +2757,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.11': resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} engines: {node: '>=18'} @@ -2586,6 +2781,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -2610,6 +2811,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -2634,6 +2841,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -2658,6 +2871,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -2682,6 +2901,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3073,6 +3298,143 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/checkbox@4.2.2': resolution: {integrity: sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==} engines: {node: '>=18'} @@ -3278,6 +3640,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -3785,6 +4150,15 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@powersync/common@1.47.0': resolution: {integrity: sha512-fL7NcjCMONaFmag5pPAHyIjY5uR56E2wo2RzxckrayevQS/emqKryQusQMZKkw0e8oCGHtUZSOH76zXe9zyw8w==} @@ -4173,6 +4547,10 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -4277,6 +4655,9 @@ packages: '@solidjs/router': optional: true + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -5522,6 +5903,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5862,6 +6246,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -6062,6 +6450,10 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devalue@5.6.3: resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} @@ -6323,6 +6715,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} @@ -6386,6 +6781,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -8188,6 +8588,11 @@ packages: mingo@6.5.6: resolution: {integrity: sha512-XV89xbTakngi/oIEpuq7+FXXYvdA/Ht6aAsNTuIl8zLW1jfv369Va1PPWod1UTa/cqL0pC6LD2P6ggBcSSeH+A==} + miniflare@4.20260310.0: + resolution: {integrity: sha512-uC5vNPenFpDSj5aUU3wGSABG6UUqMr+Xs1m4AkCrTHo37F4Z6xcQw5BXqViTfPDVT/zcYH1UgTVoXhr1l6ZMXw==} + engines: {node: '>=18.0.0'} + hasBin: true + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -8694,6 +9099,9 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -9349,6 +9757,10 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -9738,6 +10150,10 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -10071,10 +10487,17 @@ packages: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + undici@7.21.0: resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -10492,6 +10915,21 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerd@1.20260310.1: + resolution: {integrity: sha512-yawXhypXXHtArikJj15HOMknNGikpBbSg2ZDe6lddUbqZnJXuCVSkgc/0ArUeVMG1jbbGvpst+REFtKwILvRTQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.72.0: + resolution: {integrity: sha512-bKkb8150JGzJZJWiNB2nu/33smVfawmfYiecA6rW4XH7xS23/jqMbgpdelM34W/7a1IhR66qeQGVqTRXROtAZg==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260310.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -10550,6 +10988,18 @@ packages: utf-8-validate: optional: true + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -10656,6 +11106,12 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + z-schema@6.0.2: resolution: {integrity: sha512-9fQb2ZhpMD0ZQXYw0ll5ya6uLQm3Xtt4DXY2RV3QO1QVI4ihSzSWirlgkDsMgGg4qK0EV4tLOJgRSH2bn0cbIw==} engines: {node: '>=16.0.0'} @@ -11789,6 +12245,29 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260310.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260310.1 + + '@cloudflare/workerd-darwin-64@1.20260310.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260310.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260310.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260310.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260310.1': + optional: true + '@colors/colors@1.5.0': {} '@commitlint/parse@20.2.0': @@ -11802,6 +12281,10 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -11867,6 +12350,9 @@ snapshots: '@esbuild/aix-ppc64@0.27.2': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true @@ -11879,6 +12365,9 @@ snapshots: '@esbuild/android-arm64@0.27.2': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.18.20': optional: true @@ -11891,6 +12380,9 @@ snapshots: '@esbuild/android-arm@0.27.2': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.18.20': optional: true @@ -11903,6 +12395,9 @@ snapshots: '@esbuild/android-x64@0.27.2': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true @@ -11915,6 +12410,9 @@ snapshots: '@esbuild/darwin-arm64@0.27.2': optional: true + '@esbuild/darwin-arm64@0.27.3': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true @@ -11927,6 +12425,9 @@ snapshots: '@esbuild/darwin-x64@0.27.2': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true @@ -11939,6 +12440,9 @@ snapshots: '@esbuild/freebsd-arm64@0.27.2': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true @@ -11951,6 +12455,9 @@ snapshots: '@esbuild/freebsd-x64@0.27.2': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true @@ -11963,6 +12470,9 @@ snapshots: '@esbuild/linux-arm64@0.27.2': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true @@ -11975,6 +12485,9 @@ snapshots: '@esbuild/linux-arm@0.27.2': optional: true + '@esbuild/linux-arm@0.27.3': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true @@ -11987,6 +12500,9 @@ snapshots: '@esbuild/linux-ia32@0.27.2': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true @@ -11999,6 +12515,9 @@ snapshots: '@esbuild/linux-loong64@0.27.2': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true @@ -12011,6 +12530,9 @@ snapshots: '@esbuild/linux-mips64el@0.27.2': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true @@ -12023,6 +12545,9 @@ snapshots: '@esbuild/linux-ppc64@0.27.2': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true @@ -12035,6 +12560,9 @@ snapshots: '@esbuild/linux-riscv64@0.27.2': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true @@ -12047,6 +12575,9 @@ snapshots: '@esbuild/linux-s390x@0.27.2': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true @@ -12059,6 +12590,9 @@ snapshots: '@esbuild/linux-x64@0.27.2': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + '@esbuild/netbsd-arm64@0.25.11': optional: true @@ -12068,6 +12602,9 @@ snapshots: '@esbuild/netbsd-arm64@0.27.2': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true @@ -12080,6 +12617,9 @@ snapshots: '@esbuild/netbsd-x64@0.27.2': optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + '@esbuild/openbsd-arm64@0.25.11': optional: true @@ -12089,6 +12629,9 @@ snapshots: '@esbuild/openbsd-arm64@0.27.2': optional: true + '@esbuild/openbsd-arm64@0.27.3': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true @@ -12101,6 +12644,9 @@ snapshots: '@esbuild/openbsd-x64@0.27.2': optional: true + '@esbuild/openbsd-x64@0.27.3': + optional: true + '@esbuild/openharmony-arm64@0.25.11': optional: true @@ -12110,6 +12656,9 @@ snapshots: '@esbuild/openharmony-arm64@0.27.2': optional: true + '@esbuild/openharmony-arm64@0.27.3': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true @@ -12122,6 +12671,9 @@ snapshots: '@esbuild/sunos-x64@0.27.2': optional: true + '@esbuild/sunos-x64@0.27.3': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true @@ -12134,6 +12686,9 @@ snapshots: '@esbuild/win32-arm64@0.27.2': optional: true + '@esbuild/win32-arm64@0.27.3': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true @@ -12146,6 +12701,9 @@ snapshots: '@esbuild/win32-ia32@0.27.2': optional: true + '@esbuild/win32-ia32@0.27.3': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true @@ -12158,6 +12716,9 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true + '@esbuild/win32-x64@0.27.3': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))': dependencies: eslint: 9.39.3(jiti@2.6.1) @@ -12835,6 +13396,102 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@inquirer/checkbox@4.2.2(@types/node@25.2.2)': dependencies: '@inquirer/core': 10.2.0(@types/node@25.2.2) @@ -13069,6 +13726,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3 @@ -13524,6 +14186,18 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@powersync/common@1.47.0': dependencies: async-mutex: 0.5.0 @@ -13954,6 +14628,8 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@sindresorhus/is@7.2.0': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -14082,6 +14758,8 @@ snapshots: '@testing-library/dom': 10.4.1 solid-js: 1.9.11 + '@speed-highlight/core@1.2.14': {} + '@standard-schema/spec@1.1.0': {} '@stylistic/eslint-plugin@5.9.0(eslint@9.39.3(jiti@2.6.1))': @@ -15651,6 +16329,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -16053,6 +16733,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -16227,6 +16909,8 @@ snapshots: detect-libc@2.0.4: {} + detect-libc@2.1.2: {} + devalue@5.6.3: {} dexie@4.0.10: {} @@ -16406,6 +17090,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 @@ -16632,6 +17318,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -18693,6 +19408,18 @@ snapshots: mingo@6.5.6: {} + miniflare@4.20260310.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260310.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 @@ -19241,6 +19968,8 @@ snapshots: lru-cache: 11.2.5 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + path-to-regexp@8.3.0: {} path-type@4.0.0: {} @@ -20036,6 +20765,37 @@ snapshots: shallowequal@1.1.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -20475,6 +21235,8 @@ snapshots: dependencies: copy-anything: 4.0.5 + supports-color@10.2.2: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -20821,8 +21583,14 @@ snapshots: undici@6.23.0: {} + undici@7.18.2: {} + undici@7.21.0: {} + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -21258,6 +22026,30 @@ snapshots: word-wrap@1.2.5: {} + workerd@1.20260310.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260310.1 + '@cloudflare/workerd-darwin-arm64': 1.20260310.1 + '@cloudflare/workerd-linux-64': 1.20260310.1 + '@cloudflare/workerd-linux-arm64': 1.20260310.1 + '@cloudflare/workerd-windows-64': 1.20260310.1 + + wrangler@4.72.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260310.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260310.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260310.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -21297,6 +22089,8 @@ snapshots: ws@8.17.1: {} + ws@8.18.0: {} + ws@8.18.3: {} ws@8.19.0: {} @@ -21377,6 +22171,19 @@ snapshots: yoctocolors-cjs@2.1.3: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 + z-schema@6.0.2: dependencies: lodash.get: 4.4.2