From e61059a6790d587d805320fc91d9890202fbcd14 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 26 Nov 2025 21:47:33 +0200 Subject: [PATCH 01/44] No longer awaiting when aborting connection on tab closure. Fixes some edge cases where multiple tabs with OPFS/Safari can cause deadlocks. --- .changeset/rare-windows-argue.md | 5 +++++ packages/web/src/worker/sync/SharedSyncImplementation.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/rare-windows-argue.md diff --git a/.changeset/rare-windows-argue.md b/.changeset/rare-windows-argue.md new file mode 100644 index 000000000..c7b9b3bf3 --- /dev/null +++ b/.changeset/rare-windows-argue.md @@ -0,0 +1,5 @@ +--- +'@powersync/web': patch +--- + +No longer awaiting when aborting connection on tab closure. Fixes some edge cases where multiple tabs with OPFS/Safari can cause deadlocks. diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 5c4f5b683..a65d0ae11 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -487,7 +487,7 @@ export class SharedSyncImplementation extends BaseObserver { this.logger.info('Aborting open connection because associated tab closed.'); - await wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex)); + wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex)); wrapped.markRemoteClosed(); }); From a12145f2fe65e20d2fe6cc92a0902195a4d17c3c Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 27 Nov 2025 18:18:04 +0200 Subject: [PATCH 02/44] fix opfs deadlocks --- .../SharedWebStreamingSyncImplementation.ts | 34 +++++++++-------- .../worker/sync/SharedSyncImplementation.ts | 38 ++++++++++++------- packages/web/src/worker/sync/WorkerClient.ts | 9 +++-- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 66d3b51ed..c8ca829a3 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -6,16 +6,16 @@ import { SyncStatusOptions } from '@powersync/common'; import * as Comlink from 'comlink'; +import { getNavigatorLocks } from '../../shared/navigator'; import { AbstractSharedSyncClientProvider } from '../../worker/sync/AbstractSharedSyncClientProvider'; import { ManualSharedSyncPayload, SharedSyncClientEvent } from '../../worker/sync/SharedSyncImplementation'; -import { DEFAULT_CACHE_SIZE_KB, resolveWebSQLFlags, TemporaryStorageOption } from '../adapters/web-sql-flags'; +import { WorkerClient } from '../../worker/sync/WorkerClient'; import { WebDBAdapter } from '../adapters/WebDBAdapter'; +import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption, resolveWebSQLFlags } from '../adapters/web-sql-flags'; import { WebStreamingSyncImplementation, WebStreamingSyncImplementationOptions } from './WebStreamingSyncImplementation'; -import { WorkerClient } from '../../worker/sync/WorkerClient'; -import { getNavigatorLocks } from '../../shared/navigator'; /** * The shared worker will trigger methods on this side of the message port @@ -160,6 +160,21 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options; const flags = { ...this.webOptions.flags, workers: undefined }; + // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which + // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker + // to free resources associated with this tab. + // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs. + getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => { + if (!this.abortOnClose.signal.aborted) { + // Awaiting here ensures the worker is waiting for the lock + await this.syncManager.addLockBasedCloseSignal(lock!.name); + + await new Promise((r) => { + this.abortOnClose.signal.onabort = () => r(); + }); + } + }); + this.isInitialized = this.syncManager.setParams( { dbParams: this.dbAdapter.getConfiguration(), @@ -190,19 +205,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem * This performs bi-directional method calling. */ Comlink.expose(this.clientProvider, this.messagePort); - - // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which - // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker - // to free resources associated with this tab. - getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => { - if (!this.abortOnClose.signal.aborted) { - this.syncManager.addLockBasedCloseSignal(lock!.name); - - await new Promise((r) => { - this.abortOnClose.signal.onabort = () => r(); - }); - } - }); } /** diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 5c4f5b683..863dbbc21 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -322,6 +322,22 @@ export class SharedSyncImplementation extends BaseObserver 0; + + /** + * If the current database adapter is the one that is being closed, we need to disconnect from the backend. + * We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other + * connect operations coming from other tabs. + */ + if (this.dbAdapter && this.dbAdapter == trackedPort.db) { + this.logger.debug('disconnecting due to closed database', shouldReconnect); + this.dbAdapter = null; + // Unconditionally close the connection because the database it's writing to has just been closed. + // The connection has been closed previously, this might throw. We should be able to ignore it. + await this.connectionManager + .disconnect() + .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex)); + } + return { shouldReconnect, trackedPort @@ -337,19 +353,8 @@ export class SharedSyncImplementation extends BaseObserver this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex)); - - // Clearing the adapter will result in a new one being opened in connect - this.dbAdapter = null; - - if (shouldReconnect) { - await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {}); - } + if (shouldReconnect) { + await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {}); } // Re-index subscriptions, the subscriptions of the removed port would no longer be considered. @@ -487,7 +492,12 @@ export class SharedSyncImplementation extends BaseObserver { this.logger.info('Aborting open connection because associated tab closed.'); - await wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex)); + /** + * Don't await this close operation. It might never resolve if the tab is closed. + * We run the close operation first, before marking the remote as closed. This gives the database some chance + * to close the connection. + */ + wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex)); wrapped.markRemoteClosed(); }); diff --git a/packages/web/src/worker/sync/WorkerClient.ts b/packages/web/src/worker/sync/WorkerClient.ts index 1dfffe9b2..4a1e44de7 100644 --- a/packages/web/src/worker/sync/WorkerClient.ts +++ b/packages/web/src/worker/sync/WorkerClient.ts @@ -1,4 +1,6 @@ +import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common'; import * as Comlink from 'comlink'; +import { getNavigatorLocks } from '../../shared/navigator'; import { ManualSharedSyncPayload, SharedSyncClientEvent, @@ -6,8 +8,6 @@ import { SharedSyncInitOptions, WrappedSyncPort } from './SharedSyncImplementation'; -import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common'; -import { getNavigatorLocks } from '../../shared/navigator'; /** * A client to the shared sync worker. @@ -21,7 +21,9 @@ export class WorkerClient { constructor( private readonly sync: SharedSyncImplementation, private readonly port: MessagePort - ) {} + ) { + Comlink.expose(this, this.port); + } async initialize() { /** @@ -36,7 +38,6 @@ export class WorkerClient { }); this.resolvedPort = await this.sync.addPort(this.port); - Comlink.expose(this, this.port); } private async removePort() { From b4f5c1b1c683d217327097b68efe5dfd8acb37cd Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 27 Nov 2025 18:26:02 +0200 Subject: [PATCH 03/44] Add multiple tabs test --- packages/web/tests/mocks/MockWebRemote.ts | 56 +++ .../web/tests/multiple_tabs_iframe.test.ts | 463 ++++++++++++++++++ packages/web/tests/utils/iframeInitializer.ts | 227 +++++++++ packages/web/vitest.config.ts | 8 +- 4 files changed, 752 insertions(+), 2 deletions(-) create mode 100644 packages/web/tests/mocks/MockWebRemote.ts create mode 100644 packages/web/tests/multiple_tabs_iframe.test.ts create mode 100644 packages/web/tests/utils/iframeInitializer.ts diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts new file mode 100644 index 000000000..0c3c655e2 --- /dev/null +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -0,0 +1,56 @@ +import { + AbstractRemote, + AbstractRemoteOptions, + BSONImplementation, + DEFAULT_REMOTE_LOGGER, + FetchImplementation, + FetchImplementationProvider, + ILogger, + RemoteConnector +} from '@powersync/common'; + +/** + * Mock WebRemote that throws 401 Unauthorized errors for all HTTP requests. + * Used for testing error handling in the shared sync worker. + * Other tests may override this for managed streams. + */ +class MockFetchProvider extends FetchImplementationProvider { + getFetch(): FetchImplementation { + // Return a mock fetch that always returns 401 + return async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const response = new Response(null, { + status: 401, + statusText: 'Unauthorized' + }); + return response; + }; + } +} + +export class WebRemote extends AbstractRemote { + private _bson: BSONImplementation | undefined; + + constructor( + protected connector: RemoteConnector, + protected logger: ILogger = DEFAULT_REMOTE_LOGGER, + options?: Partial + ) { + super(connector, logger, { + ...(options ?? {}), + fetchImplementation: options?.fetchImplementation ?? new MockFetchProvider() + }); + } + + getUserAgent(): string { + return 'powersync-web-mock'; + } + + async getBSON(): Promise { + if (this._bson) { + return this._bson; + } + const { BSON } = await import('bson'); + this._bson = BSON; + return this._bson; + } +} diff --git a/packages/web/tests/multiple_tabs_iframe.test.ts b/packages/web/tests/multiple_tabs_iframe.test.ts new file mode 100644 index 000000000..51749d0ed --- /dev/null +++ b/packages/web/tests/multiple_tabs_iframe.test.ts @@ -0,0 +1,463 @@ +import { WASQLiteVFS } from '@powersync/web'; +import { v4 as uuid } from 'uuid'; +import { describe, expect, it, onTestFinished, vi } from 'vitest'; + +/** + * Creates an iframe with a PowerSync client that connects using the same database. + * The iframe uses dynamic import to load PowerSync modules. + * + * Note: This approach works in Vitest browser mode where modules are available + * via the Vite dev server. The iframe needs to access modules from the same origin. + */ +interface IframeClient { + iframe: HTMLIFrameElement; + cleanup: () => Promise; + executeQuery: (query: string, parameters?: unknown[]) => Promise; + getSyncStatus: () => Promise<{ + connected: boolean; + connecting: boolean; + downloading: boolean; + uploading: boolean; + lastSyncedAt?: string; + hasSynced?: boolean; + }>; + getCredentialsFetchCount: () => Promise; +} + +// Run tests for both IndexedDB and OPFS +createMultipleTabsTest(); // IndexedDB (default) +createMultipleTabsTest(WASQLiteVFS.OPFSCoopSyncVFS); + +async function createIframeWithPowerSyncClient( + dbFilename: string, + identifier: string, + vfs?: WASQLiteVFS +): Promise { + const iframe = document.createElement('iframe'); + // Make iframe visible for debugging + iframe.style.display = 'block'; + iframe.style.width = '300px'; + iframe.style.height = '150px'; + iframe.style.border = '2px solid #007bff'; + iframe.style.margin = '10px'; + iframe.style.borderRadius = '4px'; + iframe.title = `PowerSync Client: ${identifier}`; + document.body.appendChild(iframe); + + // Get the base URL for module imports + // In Vitest browser mode, we need to construct a path relative to where the test file is served + // Use import.meta.url to get the current test file's location + const testFileUrl = new URL(import.meta.url); + const testFileDir = testFileUrl.pathname.substring(0, testFileUrl.pathname.lastIndexOf('/')); + // Construct the absolute path to the initializer module relative to the test file + const modulePath = `${testFileUrl.origin}${testFileDir}/utils/iframeInitializer.ts`; + + // Create HTML content with module script that imports and executes the setup function + // Vite will serve the module file, allowing proper module resolution + const htmlContent = ` + + + + PowerSync Client ${identifier} + + + +
Initializing...
+
+
ID:${identifier}
+
DB:${dbFilename}
+
VFS:${vfs || 'IndexedDB (default)'}
+
+ + +`; + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + iframe.src = url; + + return new Promise((resolve, reject) => { + let requestIdCounter = 0; + const pendingRequests = new Map< + string, + { + resolve: (value: any) => void; + reject: (error: Error) => void; + } + >(); + + const messageHandler = async (event: MessageEvent) => { + const data = event.data; + + if (data?.type === 'powersync-ready' && data.identifier === identifier) { + // Don't remove the message handler - we need it to receive query results! + resolve({ + iframe, + cleanup: async () => { + // Simulate abrupt tab closure - just remove the iframe without calling + // disconnect/close on the PowerSync client. This tests dead tab detection. + URL.revokeObjectURL(url); + if (iframe.parentNode) { + iframe.remove(); + } + }, + executeQuery: (query: string, parameters?: unknown[]): Promise => { + return new Promise((resolveQuery, rejectQuery) => { + const requestId = `query-${identifier}-${++requestIdCounter}`; + pendingRequests.set(requestId, { + resolve: resolveQuery, + reject: rejectQuery + }); + + const iframeWindow = iframe.contentWindow; + if (!iframeWindow) { + rejectQuery(new Error('Iframe window not available')); + return; + } + + iframeWindow.postMessage( + { + type: 'execute-query', + requestId, + query, + parameters + }, + '*' + ); + + // Cleanup after timeout to prevent memory leaks + setTimeout(() => { + if (pendingRequests.has(requestId)) { + pendingRequests.delete(requestId); + rejectQuery(new Error('Query timeout')); + } + }, 30000); + }); + }, + getSyncStatus: (): Promise<{ + connected: boolean; + connecting: boolean; + downloading: boolean; + uploading: boolean; + lastSyncedAt?: string; + hasSynced?: boolean; + }> => { + return new Promise((resolveStatus, rejectStatus) => { + const requestId = `status-${identifier}-${++requestIdCounter}`; + pendingRequests.set(requestId, { + resolve: resolveStatus, + reject: rejectStatus + }); + + const iframeWindow = iframe.contentWindow; + if (!iframeWindow) { + rejectStatus(new Error('Iframe window not available')); + return; + } + + iframeWindow.postMessage( + { + type: 'get-sync-status', + requestId + }, + '*' + ); + + // Cleanup after timeout to prevent memory leaks + setTimeout(() => { + if (pendingRequests.has(requestId)) { + pendingRequests.delete(requestId); + rejectStatus(new Error('Status request timeout')); + } + }, 10000); + }); + }, + getCredentialsFetchCount: (): Promise => { + return new Promise((resolveCount, rejectCount) => { + const requestId = `credentials-count-${identifier}-${++requestIdCounter}`; + pendingRequests.set(requestId, { + resolve: resolveCount, + reject: rejectCount + }); + + const iframeWindow = iframe.contentWindow; + if (!iframeWindow) { + rejectCount(new Error('Iframe window not available')); + return; + } + + iframeWindow.postMessage( + { + type: 'get-credentials-count', + requestId + }, + '*' + ); + + // Cleanup after timeout to prevent memory leaks + setTimeout(() => { + if (pendingRequests.has(requestId)) { + pendingRequests.delete(requestId); + rejectCount(new Error('Credentials count request timeout')); + } + }, 10000); + }); + } + }); + } else if (data?.type === 'powersync-error' && data.identifier === identifier) { + window.removeEventListener('message', messageHandler); + URL.revokeObjectURL(url); + if (iframe.parentNode) { + iframe.remove(); + } + reject(new Error(`PowerSync error in iframe: ${data.error}`)); + } else if (data?.type === 'query-result' && data.identifier === identifier) { + const pending = pendingRequests.get(data.requestId); + if (pending) { + pendingRequests.delete(data.requestId); + if (data.success) { + pending.resolve(data.result); + } else { + pending.reject(new Error(data.error || 'Query failed')); + } + } + } else if (data?.type === 'sync-status-result' && data.identifier === identifier) { + const pending = pendingRequests.get(data.requestId); + if (pending) { + pendingRequests.delete(data.requestId); + if (data.success) { + pending.resolve(data.status); + } else { + pending.reject(new Error(data.error || 'Status request failed')); + } + } + } else if (data?.type === 'credentials-count-result' && data.identifier === identifier) { + const pending = pendingRequests.get(data.requestId); + if (pending) { + pendingRequests.delete(data.requestId); + if (data.success) { + pending.resolve(data.count); + } else { + pending.reject(new Error(data.error || 'Credentials count request failed')); + } + } + } + }; + window.addEventListener('message', messageHandler); + }); +} + +/** + * Test suite for simulating multiple browser tabs with PowerSync clients. + * + * Purpose: + * These tests simulate the behavior of closing and reopening multiple browser tabs + * that share a PowerSync database connection via a SharedWorker. This is critical + * for testing PowerSync's dead tab detection and resource cleanup mechanisms. + * + * Iframe vs Real Tab Behavior: + * Closing an iframe by removing it from the DOM is similar to closing a real browser tab + * for PowerSync's purposes because: + * 1. Navigator Locks API: PowerSync uses Navigator Locks to detect tab closure. When an + * iframe is removed, its execution context is destroyed and any held locks are automatically + * released, just like when a real tab closes. This is the primary mechanism PowerSync uses + * for dead tab detection (see SharedWebStreamingSyncImplementation.ts). + * 2. MessagePort Closure: When an iframe is removed, any MessagePorts used for communication + * with the SharedWorker are closed, triggering cleanup in the worker. + * 3. Window Unload: The iframe's window context is destroyed, which would trigger unload + * event listeners if registered (PowerSyncDatabase registers an 'unload' listener when + * enableMultiTabs is true). + * + * Test Scenarios: + * - Opening a long-lived reference tab that remains open throughout the test + * - Opening multiple additional tabs simultaneously + * - Simultaneously closing half of the tabs (simulating abrupt tab closures) + * - Simultaneously reopening the closed tabs + * - Verifying that all tabs remain functional and the shared database connection + * is properly maintained across tab lifecycle events + * + * This test suite runs for both IndexedDB and OPFS VFS backends to ensure dead tab + * detection works correctly across different storage mechanisms. + */ +function createMultipleTabsTest(vfs?: WASQLiteVFS) { + const vfsName = vfs || 'IndexedDB'; + describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 120000 }, () => { + const dbFilename = `test-multi-tab-${uuid()}.db`; + + // Configurable number of tabs to create (excluding the long-lived tab) + const NUM_TABS = 10; + + it('should handle simultaneous close and reopen of tabs', async () => { + // Step 1: Open a long-lived reference tab that stays open throughout the test + const longLivedTab = await createIframeWithPowerSyncClient(dbFilename, 'long-lived-tab', vfs); + onTestFinished(async () => { + try { + await longLivedTab.cleanup(); + } catch (e) { + // Ignore cleanup errors + } + }); + + // Test query execution right after creating the long-lived tab + const initialQueryResult = await longLivedTab.executeQuery('SELECT 1 as value'); + expect(initialQueryResult).toBeDefined(); + expect(Array.isArray(initialQueryResult)).toBe(true); + expect(initialQueryResult.length).toBe(1); + expect((initialQueryResult[0] as { value: number }).value).toBe(1); + + // Step 2: Open a configurable number of other tabs + const tabs: IframeClient[] = []; + const tabIdentifiers: string[] = []; + + for (let i = 0; i < NUM_TABS; i++) { + const identifier = `tab-${i}`; + tabIdentifiers.push(identifier); + const result = await createIframeWithPowerSyncClient(dbFilename, identifier, vfs); + tabs.push(result); + + // Register cleanup for each tab + onTestFinished(async () => { + try { + await result.cleanup(); + } catch (e) { + // Ignore cleanup errors - tab might already be closed + } + }); + } + + expect(tabs.length).toBe(NUM_TABS); + + // Verify all tabs are connected + for (const tab of tabs) { + expect(tab.iframe.isConnected).toBe(true); + } + expect(longLivedTab.iframe.isConnected).toBe(true); + + // Step 3: Simultaneously close half of the tabs (simulating abrupt closure) + const halfCount = Math.floor(NUM_TABS / 2); + // Close the latest opened tabs since we usually use the last connected tabs for operations. + const tabsToClose = tabs.slice(halfCount); + const tabsToKeep = tabs.slice(0, halfCount); + + // Close half the tabs simultaneously (without proper cleanup) + const closePromises = tabsToClose.map((tab) => tab.cleanup()); + await Promise.all(closePromises); + + // Verify closed tabs are removed + for (const tab of tabsToClose) { + expect(tab.iframe.isConnected).toBe(false); + expect(document.body.contains(tab.iframe)).toBe(false); + } + + // Verify remaining tabs and long-lived tab are still connected + for (const tab of tabsToKeep) { + expect(tab.iframe.isConnected).toBe(true); + } + expect(longLivedTab.iframe.isConnected).toBe(true); + + // Step 4: Reopen the closed tabs + const reopenedTabs: IframeClient[] = []; + const reopenPromises = tabsToClose.map(async (_, index) => { + const identifier = tabIdentifiers[index]; + const result = await createIframeWithPowerSyncClient(dbFilename, identifier, vfs); + reopenedTabs.push(result); + + // Register cleanup for reopened tabs + onTestFinished(async () => { + try { + await result.cleanup(); + } catch (e) { + // Ignore cleanup errors + } + }); + return result; + }); + + // Reopen all closed tabs simultaneously + await Promise.all(reopenPromises); + + // Verify all reopened tabs are connected + for (const tab of reopenedTabs) { + expect(tab.iframe.isConnected).toBe(true); + } + + // Verify tabs that were kept open are still connected + for (const tab of tabsToKeep) { + expect(tab.iframe.isConnected).toBe(true); + } + + // Final verification: all tabs should be mounted + const allTabs = [...tabsToKeep, ...reopenedTabs]; + expect(allTabs.length).toBe(NUM_TABS); + expect(longLivedTab.iframe.isConnected).toBe(true); + + // Step 5: Execute a test query in the long-lived tab to verify its DB is still functional + const queryResult = await longLivedTab.executeQuery('SELECT 1 as value'); + + // Verify the query result + expect(queryResult).toBeDefined(); + expect(Array.isArray(queryResult)).toBe(true); + expect(queryResult.length).toBe(1); + expect((queryResult[0] as { value: number }).value).toBe(1); + + // Step 6: Create a new tab which should trigger a connect. The shared sync worker should reconnect. + // This ensures the shared sync worker is not stuck and is properly handling new connections + const newTabIdentifier = `new-tab-${Date.now()}`; + const newTab = await createIframeWithPowerSyncClient(dbFilename, newTabIdentifier, vfs); + onTestFinished(async () => { + try { + await newTab.cleanup(); + } catch (e) { + // Ignore cleanup errors + } + }); + + // Wait for the new tab's credentials to be fetched (indicating the shared sync worker is active) + // The mocked remote always returns 401, so the shared sync worker should try and fetch credentials again. + await vi.waitFor( + async () => { + const credentialsFetchCount = await newTab.getCredentialsFetchCount(); + console.log('credentialsFetchCount', credentialsFetchCount); + expect( + credentialsFetchCount, + 'The new client should have been asked for credentials by the shared sync worker. ' + + 'This indicates the shared sync worker may be stuck or not processing new connections.' + ).toBeGreaterThanOrEqual(1); + }, + { timeout: 10000 } + ); + }); + }); +} diff --git a/packages/web/tests/utils/iframeInitializer.ts b/packages/web/tests/utils/iframeInitializer.ts new file mode 100644 index 000000000..c43c707a7 --- /dev/null +++ b/packages/web/tests/utils/iframeInitializer.ts @@ -0,0 +1,227 @@ +import { + LogLevel, + Schema, + SyncStatus, + SyncStreamConnectionMethod, + TableV2, + column, + createBaseLogger +} from '@powersync/common'; +import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; + +/** + * Initializes a PowerSync client in the current iframe context and notifies the parent. + * This function is designed to be called from within an iframe's script tag. + * + * @param vfs - VFS option as a string (e.g., 'OPFSCoopSyncVFS' or 'IDBBatchAtomicVFS') + */ +export async function setupPowerSyncInIframe(dbFilename: string, identifier: string, vfs?: string): Promise { + try { + // Track the number of times fetchCredentials has been called + let credentialsFetchCount = 0; + + const connector = { + async fetchCredentials() { + credentialsFetchCount++; + return { endpoint: 'http://localhost/test', token: 'test-token' }; + }, + async uploadData() {} + }; + + // Create the same schema as used in tests + const schema = new Schema({ + assets: new TableV2( + { + created_at: column.text, + make: column.text, + model: column.text, + serial_number: column.text, + quantity: column.integer, + user_id: column.text, + customer_id: column.text, + description: column.text + }, + { indexes: { makemodel: ['make', 'model'] } } + ), + customers: new TableV2({ + name: column.text, + email: column.text + }) + }); + + // Configure database with optional VFS + // The vfs string value is the enum value itself (string enums) + const databaseOptions = vfs + ? new WASQLiteOpenFactory({ + dbFilename, + vfs: vfs as WASQLiteVFS + }) + : { dbFilename }; + + // Configure verbose logging + const logger = createBaseLogger(); + logger.setLevel(LogLevel.DEBUG); + logger.useDefaults(); + + const db = new PowerSyncDatabase({ + database: databaseOptions, + schema: schema, + flags: { enableMultiTabs: true, useWebWorker: true }, + logger + }); + + // Register a listener for sync status updates + const updateStatusDisplay = (status: SyncStatus) => { + const statusEl = document.getElementById('status'); + if (statusEl) { + const connected = status.connected ? 'Connected' : 'Disconnected'; + const syncing = status.dataFlowStatus.downloading ? ' (Syncing...)' : ''; + statusEl.textContent = `${connected}${syncing}`; + + // Update color based on connection status + if (status.connected) { + statusEl.style.color = '#28a745'; + statusEl.style.borderColor = '#28a745'; + } else { + statusEl.style.color = '#dc3545'; + statusEl.style.borderColor = '#dc3545'; + } + } + }; + + // Register listener for status changes + db.registerListener({ + statusChanged: (status) => { + updateStatusDisplay(status); + } + }); + + // Connect to PowerSync + await db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP }); + + // Store reference for cleanup + (window as any).powersyncClient = db; + + // Update initial status + updateStatusDisplay(db.currentStatus); + + // Set up message handlers for test operations + window.addEventListener('message', async (event: MessageEvent) => { + // Only handle messages from parent window + // Note: event.source might not match exactly with blob URLs, so we'll be less strict + if (event.source && event.source !== window.parent && event.source !== window) { + return; + } + + const { type, requestId, query, parameters } = event.data || {}; + + if (type === 'execute-query' && requestId) { + try { + const result = await db.getAll(query, parameters || []); + window.parent.postMessage( + { + type: 'query-result', + requestId, + identifier, + success: true, + result + }, + '*' + ); + } catch (error) { + window.parent.postMessage( + { + type: 'query-result', + requestId, + identifier, + success: false, + error: (error as Error).message + }, + '*' + ); + } + } else if (type === 'get-sync-status' && requestId) { + try { + const status = db.currentStatus; + window.parent.postMessage( + { + type: 'sync-status-result', + requestId, + identifier, + success: true, + status: { + connected: status.connected, + connecting: status.connecting, + downloading: status.dataFlowStatus.downloading, + uploading: status.dataFlowStatus.uploading, + lastSyncedAt: status.lastSyncedAt?.toISOString(), + hasSynced: status.hasSynced + } + }, + '*' + ); + } catch (error) { + window.parent.postMessage( + { + type: 'sync-status-result', + requestId, + identifier, + success: false, + error: (error as Error).message + }, + '*' + ); + } + } else if (type === 'get-credentials-count' && requestId) { + try { + window.parent.postMessage( + { + type: 'credentials-count-result', + requestId, + identifier, + success: true, + count: credentialsFetchCount + }, + '*' + ); + } catch (error) { + window.parent.postMessage( + { + type: 'credentials-count-result', + requestId, + identifier, + success: false, + error: (error as Error).message + }, + '*' + ); + } + } + }); + + // Notify parent that client is ready + window.parent.postMessage( + { + type: 'powersync-ready', + identifier: identifier + }, + '*' + ); + } catch (error) { + const statusEl = document.getElementById('status'); + if (statusEl) { + statusEl.textContent = 'Error: ' + (error as Error).message; + statusEl.style.color = '#dc3545'; + statusEl.style.borderColor = '#dc3545'; + } + console.error('PowerSync initialization error:', error); + window.parent.postMessage( + { + type: 'powersync-error', + identifier: identifier, + error: (error as Error).message + }, + '*' + ); + } +} diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index 0e586da8d..0de9a4717 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -20,7 +20,11 @@ const config: UserConfigExport = { */ '@powersync/web': path.resolve(__dirname, './lib/src'), // https://jira.mongodb.org/browse/NODE-5773 - bson: require.resolve('bson') + bson: require.resolve('bson'), + // Mock WebRemote to throw 401 errors for all HTTP requests in tests + '../../db/sync/WebRemote': path.resolve(__dirname, './tests/mocks/MockWebRemote.ts'), + // Also handle the case where it's imported from the worker context + '@powersync/web/src/db/sync/WebRemote': path.resolve(__dirname, './tests/mocks/MockWebRemote.ts') } }, worker: { @@ -50,7 +54,7 @@ const config: UserConfigExport = { */ isolate: true, provider: 'playwright', - headless: true, + headless: false, instances: [ { browser: 'chromium' From 355e396bc1c1f291df6a2bcd50a2b759c425df9b Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 27 Nov 2025 18:33:14 +0200 Subject: [PATCH 04/44] enable headless --- packages/web/vitest.config.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index 0de9a4717..737bd4c69 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -54,11 +54,11 @@ const config: UserConfigExport = { */ isolate: true, provider: 'playwright', - headless: false, + headless: true, instances: [ - { - browser: 'chromium' - } + // { + // browser: 'chromium' + // }, // { // browser: 'firefox' // } @@ -66,7 +66,17 @@ const config: UserConfigExport = { // { // browser: 'webkit' // } - ] + ], + // Disable private browsing mode for WebKit + // This allows persistent storage (IndexedDB, localStorage, etc.) to work properly + providerOptions: { + webkit: { + launch: { + // WebKit-specific launch options + }, + context: {} + } + } } } }; From c6ba9f62fd82c15425f1b235afba00a3d732abf0 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 27 Nov 2025 18:40:41 +0200 Subject: [PATCH 05/44] reenable browsers --- packages/web/vitest.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index 737bd4c69..6cfbbe37f 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -56,9 +56,9 @@ const config: UserConfigExport = { provider: 'playwright', headless: true, instances: [ - // { - // browser: 'chromium' - // }, + { + browser: 'chromium' + } // { // browser: 'firefox' // } From 9730337a2b09b06ae5f630c1aa18ed3438ddc73e Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Thu, 27 Nov 2025 18:58:44 +0200 Subject: [PATCH 06/44] cleanup tests --- packages/web/tests/multiple_tabs_iframe.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/tests/multiple_tabs_iframe.test.ts b/packages/web/tests/multiple_tabs_iframe.test.ts index 51749d0ed..cad468bfb 100644 --- a/packages/web/tests/multiple_tabs_iframe.test.ts +++ b/packages/web/tests/multiple_tabs_iframe.test.ts @@ -372,7 +372,8 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { const tabsToKeep = tabs.slice(0, halfCount); // Close half the tabs simultaneously (without proper cleanup) - const closePromises = tabsToClose.map((tab) => tab.cleanup()); + // Do this in reverse order to ensure the last connected tab is closed first. + const closePromises = tabsToClose.reverse().map((tab) => tab.cleanup()); await Promise.all(closePromises); // Verify closed tabs are removed @@ -449,7 +450,6 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { await vi.waitFor( async () => { const credentialsFetchCount = await newTab.getCredentialsFetchCount(); - console.log('credentialsFetchCount', credentialsFetchCount); expect( credentialsFetchCount, 'The new client should have been asked for credentials by the shared sync worker. ' + From 8721538715164770ba5b4ce4a5cf09354c6546c7 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 10:41:48 +0200 Subject: [PATCH 07/44] Add default lock timeout for shared sync workers. Add flagging for unprotected clients. Perform reconnect operations inside shared port mutex. --- .../components/providers/SystemProvider.tsx | 20 ++++- .../db/adapters/LockedAsyncDatabaseAdapter.ts | 5 +- .../SharedWebStreamingSyncImplementation.ts | 79 +++++++++++------- .../worker/sync/SharedSyncImplementation.ts | 82 ++++++++++++------- packages/web/src/worker/sync/WorkerClient.ts | 24 +++++- 5 files changed, 146 insertions(+), 64 deletions(-) diff --git a/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx b/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx index 8a3f3c209..5aae4f4e2 100644 --- a/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx +++ b/demos/react-supabase-todolist/src/components/providers/SystemProvider.tsx @@ -3,7 +3,14 @@ import { AppSchema, ListRecord, LISTS_TABLE, TODOS_TABLE } from '@/library/power import { SupabaseConnector } from '@/library/powersync/SupabaseConnector'; import { CircularProgress } from '@mui/material'; import { PowerSyncContext } from '@powersync/react'; -import { createBaseLogger, DifferentialWatchedQuery, LogLevel, PowerSyncDatabase } from '@powersync/web'; +import { + createBaseLogger, + DifferentialWatchedQuery, + LogLevel, + PowerSyncDatabase, + WASQLiteOpenFactory, + WASQLiteVFS +} from '@powersync/web'; import React, { Suspense } from 'react'; import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext'; @@ -12,8 +19,15 @@ export const useSupabase = () => React.useContext(SupabaseContext); export const db = new PowerSyncDatabase({ schema: AppSchema, - database: { - dbFilename: 'example.db' + database: new WASQLiteOpenFactory({ + dbFilename: 'example.db', + vfs: WASQLiteVFS.OPFSCoopSyncVFS, + flags: { + enableMultiTabs: typeof SharedWorker !== 'undefined' + } + }), + flags: { + enableMultiTabs: typeof SharedWorker !== 'undefined' } }); diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 2b1f2221a..67e2910ae 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -26,6 +26,7 @@ export interface LockedAsyncDatabaseAdapterOptions { openConnection: () => Promise; debugMode?: boolean; logger?: ILogger; + defaultLockTimeoutMs?: number; } export type LockedAsyncDatabaseAdapterListener = DBAdapterListener & { @@ -196,7 +197,7 @@ export class LockedAsyncDatabaseAdapter return this.acquireLock( async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), { - timeoutMs: options?.timeoutMs + timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs } ); } @@ -206,7 +207,7 @@ export class LockedAsyncDatabaseAdapter return this.acquireLock( async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })), { - timeoutMs: options?.timeoutMs + timeoutMs: options?.timeoutMs ?? this.options.defaultLockTimeoutMs } ); } diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index c8ca829a3..96f1b2595 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -157,36 +157,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem * DB worker, but a port to the DB worker can be transferred to the * sync worker. */ - const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options; - const flags = { ...this.webOptions.flags, workers: undefined }; - - // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which - // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker - // to free resources associated with this tab. - // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs. - getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => { - if (!this.abortOnClose.signal.aborted) { - // Awaiting here ensures the worker is waiting for the lock - await this.syncManager.addLockBasedCloseSignal(lock!.name); - - await new Promise((r) => { - this.abortOnClose.signal.onabort = () => r(); - }); - } - }); - this.isInitialized = this.syncManager.setParams( - { - dbParams: this.dbAdapter.getConfiguration(), - streamOptions: { - crudUploadThrottleMs, - identifier, - retryDelayMs, - flags: flags - } - }, - options.subscriptions - ); + this.isInitialized = this._init(); /** * Pass along any sync status updates to this listener @@ -198,6 +170,42 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem }, options.db ); + } + + protected async _init() { + /** + * The general flow of initialization is: + * - The client requests a unique navigator lock. + * - Once the lock is acquired, we register the lock with the shared worker. + * - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock. + * - Once the shared worker knows the client's lock, we can guarentee that the shared worker will detect if the client has been closed. + * - This makes the client safe for the shared worker to use. + * - The client side lock is held until the client is disposed. + * - We resolve the top-level promise after the lock has been registered with the shared worker. + * - The client sends the params to the shared worker after locks have been registered. + */ + await new Promise((resolve) => { + // Request a random lock until this client is disposed. The name of the lock is sent to the shared worker, which + // will also attempt to acquire it. Since the lock is returned when the tab is closed, this allows the share worker + // to free resources associated with this tab. + // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs. + getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => { + if (!this.abortOnClose.signal.aborted) { + // Awaiting here ensures the worker is waiting for the lock + await this.syncManager.addLockBasedCloseSignal(lock!.name); + + // The lock has been registered, we can continue with the initialization + resolve(); + + await new Promise((r) => { + this.abortOnClose.signal.onabort = () => r(); + }); + } + }); + }); + + const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options; + const flags = { ...this.webOptions.flags, workers: undefined }; /** * The sync worker will call this client provider when it needs @@ -205,6 +213,19 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem * This performs bi-directional method calling. */ Comlink.expose(this.clientProvider, this.messagePort); + + await this.syncManager.setParams( + { + dbParams: this.dbAdapter.getConfiguration(), + streamOptions: { + crudUploadThrottleMs, + identifier, + retryDelayMs, + flags: flags + } + }, + this.options.subscriptions + ); } /** diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index c5b12aa33..ce8677a12 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -76,6 +76,10 @@ export type WrappedSyncPort = { db?: DBAdapter; currentSubscriptions: SubscribedStream[]; closeListeners: (() => void | Promise)[]; + /** + * If we can use Navigator locks to detect if the client has closed. + */ + isProtectedFromClose: boolean; }; /** @@ -142,7 +146,9 @@ export class SharedSyncImplementation extends BaseObserver { return this.portMutex.runExclusive(async () => { await this.waitForReady(); + this.logger.debug('Creating sync implementation'); if (!this.dbAdapter) { + this.logger.debug('Opening internal DB'); await this.openInternalDB(); } @@ -171,6 +177,19 @@ export class SharedSyncImplementation extends BaseObserver= 0; i--) { + if (this.ports[i].isProtectedFromClose) { + return this.ports[i]; + } + } + return; + } + async waitForStatus(status: SyncStatusOptions): Promise { return this.withSyncImplementation(async (sync) => { return sync.waitForStatus(status); @@ -276,7 +295,8 @@ export class SharedSyncImplementation extends BaseObserver(port), currentSubscriptions: [], - closeListeners: [] + closeListeners: [], + isProtectedFromClose: false } satisfies WrappedSyncPort; this.ports.push(portProvider); @@ -298,11 +318,11 @@ export class SharedSyncImplementation extends BaseObserver { + return await this.portMutex.runExclusive(async () => { const index = this.ports.findIndex((p) => p == port); if (index < 0) { this.logger.warn(`Could not remove port ${port} since it is not present in active ports.`); - return {}; + return () => {}; } const trackedPort = this.ports[index]; @@ -322,7 +342,6 @@ export class SharedSyncImplementation extends BaseObserver 0; - /** * If the current database adapter is the one that is being closed, we need to disconnect from the backend. * We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other @@ -338,30 +357,25 @@ export class SharedSyncImplementation extends BaseObserver this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex)); } - return { - shouldReconnect, - trackedPort - }; - }); - - if (!trackedPort) { - // We could not find the port to remove - return () => {}; - } - - for (const closeListener of trackedPort.closeListeners) { - await closeListener(); - } + for (const closeListener of trackedPort.closeListeners) { + await closeListener(); + } - if (shouldReconnect) { - await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {}); - } + try { + await trackedPort.db?.close(); + } catch (ex) { + this.logger.warn('error closing database', ex); + } - // Re-index subscriptions, the subscriptions of the removed port would no longer be considered. - this.collectActiveSubscriptions(); + // Re-index subscriptions, the subscriptions of the removed port would no longer be considered. + this.collectActiveSubscriptions(); + if (shouldReconnect) { + // The internals of this needs a port mutex lock. It should be safe to start this operation here, but we cannot and don't need to await it. + this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {}); + } - // Release proxy - return () => trackedPort.clientProvider[Comlink.releaseProxy](); + return () => trackedPort.clientProvider[Comlink.releaseProxy](); + }); } triggerCrudUpload() { @@ -410,7 +424,10 @@ export class SharedSyncImplementation extends BaseObserver { - const lastPort = this.ports[this.ports.length - 1]; + const lastPort = this.lastWrappedPort; + if (!lastPort) { + throw new Error('No client port found to invalidate credentials'); + } try { this.logger.log('calling the last port client provider to invalidate credentials'); lastPort.clientProvider.invalidateCredentials(); @@ -419,7 +436,10 @@ export class SharedSyncImplementation extends BaseObserver { - const lastPort = this.ports[this.ports.length - 1]; + const lastPort = this.lastWrappedPort; + if (!lastPort) { + throw new Error('No client port found to fetch credentials'); + } return new Promise(async (resolve, reject) => { const abortController = new AbortController(); this.fetchCredentialsController = { @@ -442,7 +462,10 @@ export class SharedSyncImplementation extends BaseObserver { - const lastPort = this.ports[this.ports.length - 1]; + const lastPort = this.lastWrappedPort; + if (!lastPort) { + throw new Error('No client port found to upload crud'); + } return new Promise(async (resolve, reject) => { const abortController = new AbortController(); @@ -470,7 +493,7 @@ export class SharedSyncImplementation extends BaseObserver { const wrapped = new WorkerWrappedAsyncDatabaseConnection({ remote, diff --git a/packages/web/src/worker/sync/WorkerClient.ts b/packages/web/src/worker/sync/WorkerClient.ts index 4a1e44de7..b88b190ad 100644 --- a/packages/web/src/worker/sync/WorkerClient.ts +++ b/packages/web/src/worker/sync/WorkerClient.ts @@ -17,6 +17,7 @@ import { */ export class WorkerClient { private resolvedPort: WrappedSyncPort | null = null; + protected resolvedPortPromise: Promise | null = null; constructor( private readonly sync: SharedSyncImplementation, @@ -37,7 +38,15 @@ export class WorkerClient { } }); - this.resolvedPort = await this.sync.addPort(this.port); + /** + * Keep a reference to the resolved port promise. + * The init timing is difficult to predict due to the async message passing. + * We only want to use a port if we are know it's been protected from being closed. + * The lock based close signal will be added asynchronously. We need to use the + * added port once the lock is configured. + */ + this.resolvedPortPromise = this.sync.addPort(this.port); + this.resolvedPort = await this.resolvedPortPromise; } private async removePort() { @@ -61,6 +70,19 @@ export class WorkerClient { * it can consider the connection to be closed. */ addLockBasedCloseSignal(name: string) { + if (!this.resolvedPortPromise) { + // The init logic above is actually synchronous, so this should not happen. + this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered'); + } else { + this.resolvedPortPromise.then((wrappedPort) => { + /** + * The client registered a navigator lock. We now can guarantee detecting if the client has closed. + * E.g. before this point: It's possible some ports might have been created and closed before the + * lock based close signal is added. We should not trust those ports. + */ + wrappedPort.isProtectedFromClose = true; + }); + } getNavigatorLocks().request(name, async () => { await this.removePort(); }); From feca86326ade8d7b7cd2b0c32b1396abd1259916 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 11:46:35 +0200 Subject: [PATCH 08/44] fix broken test --- .../AbstractStreamingSyncImplementation.ts | 2 +- .../SharedWebStreamingSyncImplementation.ts | 55 ++++++++++--------- packages/web/tests/multiple_instances.test.ts | 2 + 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index ab3e37c7c..0de88f9a6 100644 --- a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -16,7 +16,7 @@ import { import { CrudEntry } from '../bucket/CrudEntry.js'; import { SyncDataBucket } from '../bucket/SyncDataBucket.js'; import { AbstractRemote, FetchStrategy, SyncStreamOptions } from './AbstractRemote.js'; -import { coreStatusToJs, EstablishSyncStream, Instruction, SyncPriorityStatus } from './core-instruction.js'; +import { EstablishSyncStream, Instruction, coreStatusToJs } from './core-instruction.js'; import { BucketRequest, CrudUploadNotification, diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 96f1b2595..9e4d1e315 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -146,7 +146,25 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem ).port; } + /** + * Pass along any sync status updates to this listener + */ + this.clientProvider = new SharedSyncClientProvider( + this.webOptions, + (status) => { + this.updateSyncStatus(status); + }, + options.db + ); + this.syncManager = Comlink.wrap(this.messagePort); + /** + * The sync worker will call this client provider when it needs + * to fetch credentials or upload data. + * This performs bi-directional method calling. + */ + Comlink.expose(this.clientProvider, this.messagePort); + this.syncManager.setLogLevel(this.logger.getLevel()); this.triggerCrudUpload = this.syncManager.triggerCrudUpload; @@ -159,17 +177,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem */ this.isInitialized = this._init(); - - /** - * Pass along any sync status updates to this listener - */ - this.clientProvider = new SharedSyncClientProvider( - this.webOptions, - (status) => { - this.iterateListeners((l) => this.updateSyncStatus(status)); - }, - options.db - ); } protected async _init() { @@ -190,30 +197,24 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem // to free resources associated with this tab. // We take hold of this lock as soon-as-possible in order to cater for potentially closed tabs. getNavigatorLocks().request(`tab-close-signal-${crypto.randomUUID()}`, async (lock) => { - if (!this.abortOnClose.signal.aborted) { - // Awaiting here ensures the worker is waiting for the lock - await this.syncManager.addLockBasedCloseSignal(lock!.name); + if (this.abortOnClose.signal.aborted) { + return; + } + // Awaiting here ensures the worker is waiting for the lock + await this.syncManager.addLockBasedCloseSignal(lock!.name); - // The lock has been registered, we can continue with the initialization - resolve(); + // The lock has been registered, we can continue with the initialization + resolve(); - await new Promise((r) => { - this.abortOnClose.signal.onabort = () => r(); - }); - } + await new Promise((r) => { + this.abortOnClose.signal.onabort = () => r(); + }); }); }); const { crudUploadThrottleMs, identifier, retryDelayMs } = this.options; const flags = { ...this.webOptions.flags, workers: undefined }; - /** - * The sync worker will call this client provider when it needs - * to fetch credentials or upload data. - * This performs bi-directional method calling. - */ - Comlink.expose(this.clientProvider, this.messagePort); - await this.syncManager.setParams( { dbParams: this.dbAdapter.getConfiguration(), diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 76fadaf26..15e80918c 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -380,6 +380,8 @@ describe('Multiple Instances', { sequential: true }, () => { // Close the second client, leaving only the first one await stream2.dispose(); + // The dispose above will disconnect, but we need to wait for the sync stream to be created before we can update the status + await vi.waitFor(() => expect(stream1.syncStatus.connecting).true); // Hack, set the status to connected in order to trigger the upload await (stream1 as any)['_testUpdateStatus'](new SyncStatus({ connected: true })); stream1.triggerCrudUpload(); From b0dd596e1e16eb4653820a1adb69837c5dd2636d Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 12:04:37 +0200 Subject: [PATCH 09/44] cleanup tests --- .../web/tests/multiple_tabs_iframe.test.ts | 85 ++--------------- packages/web/tests/utils/iframeInitializer.ts | 92 +------------------ packages/web/vitest.config.ts | 18 +--- 3 files changed, 13 insertions(+), 182 deletions(-) diff --git a/packages/web/tests/multiple_tabs_iframe.test.ts b/packages/web/tests/multiple_tabs_iframe.test.ts index cad468bfb..0135b069e 100644 --- a/packages/web/tests/multiple_tabs_iframe.test.ts +++ b/packages/web/tests/multiple_tabs_iframe.test.ts @@ -13,14 +13,6 @@ interface IframeClient { iframe: HTMLIFrameElement; cleanup: () => Promise; executeQuery: (query: string, parameters?: unknown[]) => Promise; - getSyncStatus: () => Promise<{ - connected: boolean; - connecting: boolean; - downloading: boolean; - uploading: boolean; - lastSyncedAt?: string; - hasSynced?: boolean; - }>; getCredentialsFetchCount: () => Promise; } @@ -67,15 +59,6 @@ async function createIframeWithPowerSyncClient( font-size: 12px; background: #f5f5f5; } - #status { - padding: 8px; - background: #fff; - border: 1px solid #ddd; - border-radius: 4px; - margin-bottom: 8px; - font-weight: 500; - transition: color 0.3s, border-color 0.3s; - } #info { padding: 8px; background: #e7f3ff; @@ -91,7 +74,6 @@ async function createIframeWithPowerSyncClient( -
Initializing...
ID:${identifier}
DB:${dbFilename}
@@ -166,44 +148,6 @@ async function createIframeWithPowerSyncClient( }, 30000); }); }, - getSyncStatus: (): Promise<{ - connected: boolean; - connecting: boolean; - downloading: boolean; - uploading: boolean; - lastSyncedAt?: string; - hasSynced?: boolean; - }> => { - return new Promise((resolveStatus, rejectStatus) => { - const requestId = `status-${identifier}-${++requestIdCounter}`; - pendingRequests.set(requestId, { - resolve: resolveStatus, - reject: rejectStatus - }); - - const iframeWindow = iframe.contentWindow; - if (!iframeWindow) { - rejectStatus(new Error('Iframe window not available')); - return; - } - - iframeWindow.postMessage( - { - type: 'get-sync-status', - requestId - }, - '*' - ); - - // Cleanup after timeout to prevent memory leaks - setTimeout(() => { - if (pendingRequests.has(requestId)) { - pendingRequests.delete(requestId); - rejectStatus(new Error('Status request timeout')); - } - }, 10000); - }); - }, getCredentialsFetchCount: (): Promise => { return new Promise((resolveCount, rejectCount) => { const requestId = `credentials-count-${identifier}-${++requestIdCounter}`; @@ -253,16 +197,6 @@ async function createIframeWithPowerSyncClient( pending.reject(new Error(data.error || 'Query failed')); } } - } else if (data?.type === 'sync-status-result' && data.identifier === identifier) { - const pending = pendingRequests.get(data.requestId); - if (pending) { - pendingRequests.delete(data.requestId); - if (data.success) { - pending.resolve(data.status); - } else { - pending.reject(new Error(data.error || 'Status request failed')); - } - } } else if (data?.type === 'credentials-count-result' && data.identifier === identifier) { const pending = pendingRequests.get(data.requestId); if (pending) { @@ -447,17 +381,14 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { // Wait for the new tab's credentials to be fetched (indicating the shared sync worker is active) // The mocked remote always returns 401, so the shared sync worker should try and fetch credentials again. - await vi.waitFor( - async () => { - const credentialsFetchCount = await newTab.getCredentialsFetchCount(); - expect( - credentialsFetchCount, - 'The new client should have been asked for credentials by the shared sync worker. ' + - 'This indicates the shared sync worker may be stuck or not processing new connections.' - ).toBeGreaterThanOrEqual(1); - }, - { timeout: 10000 } - ); + await vi.waitFor(async () => { + const credentialsFetchCount = await newTab.getCredentialsFetchCount(); + expect( + credentialsFetchCount, + 'The new client should have been asked for credentials by the shared sync worker. ' + + 'This indicates the shared sync worker may be stuck or not processing new connections.' + ).toBeGreaterThanOrEqual(1); + }); }); }); } diff --git a/packages/web/tests/utils/iframeInitializer.ts b/packages/web/tests/utils/iframeInitializer.ts index c43c707a7..b31d6726d 100644 --- a/packages/web/tests/utils/iframeInitializer.ts +++ b/packages/web/tests/utils/iframeInitializer.ts @@ -1,12 +1,4 @@ -import { - LogLevel, - Schema, - SyncStatus, - SyncStreamConnectionMethod, - TableV2, - column, - createBaseLogger -} from '@powersync/common'; +import { LogLevel, Schema, SyncStreamConnectionMethod, TableV2, column, createBaseLogger } from '@powersync/common'; import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; /** @@ -28,21 +20,8 @@ export async function setupPowerSyncInIframe(dbFilename: string, identifier: str async uploadData() {} }; - // Create the same schema as used in tests + // Create a simple schema for testing const schema = new Schema({ - assets: new TableV2( - { - created_at: column.text, - make: column.text, - model: column.text, - serial_number: column.text, - quantity: column.integer, - user_id: column.text, - customer_id: column.text, - description: column.text - }, - { indexes: { makemodel: ['make', 'model'] } } - ), customers: new TableV2({ name: column.text, email: column.text @@ -70,41 +49,12 @@ export async function setupPowerSyncInIframe(dbFilename: string, identifier: str logger }); - // Register a listener for sync status updates - const updateStatusDisplay = (status: SyncStatus) => { - const statusEl = document.getElementById('status'); - if (statusEl) { - const connected = status.connected ? 'Connected' : 'Disconnected'; - const syncing = status.dataFlowStatus.downloading ? ' (Syncing...)' : ''; - statusEl.textContent = `${connected}${syncing}`; - - // Update color based on connection status - if (status.connected) { - statusEl.style.color = '#28a745'; - statusEl.style.borderColor = '#28a745'; - } else { - statusEl.style.color = '#dc3545'; - statusEl.style.borderColor = '#dc3545'; - } - } - }; - - // Register listener for status changes - db.registerListener({ - statusChanged: (status) => { - updateStatusDisplay(status); - } - }); - // Connect to PowerSync await db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP }); // Store reference for cleanup (window as any).powersyncClient = db; - // Update initial status - updateStatusDisplay(db.currentStatus); - // Set up message handlers for test operations window.addEventListener('message', async (event: MessageEvent) => { // Only handle messages from parent window @@ -140,38 +90,6 @@ export async function setupPowerSyncInIframe(dbFilename: string, identifier: str '*' ); } - } else if (type === 'get-sync-status' && requestId) { - try { - const status = db.currentStatus; - window.parent.postMessage( - { - type: 'sync-status-result', - requestId, - identifier, - success: true, - status: { - connected: status.connected, - connecting: status.connecting, - downloading: status.dataFlowStatus.downloading, - uploading: status.dataFlowStatus.uploading, - lastSyncedAt: status.lastSyncedAt?.toISOString(), - hasSynced: status.hasSynced - } - }, - '*' - ); - } catch (error) { - window.parent.postMessage( - { - type: 'sync-status-result', - requestId, - identifier, - success: false, - error: (error as Error).message - }, - '*' - ); - } } else if (type === 'get-credentials-count' && requestId) { try { window.parent.postMessage( @@ -208,12 +126,6 @@ export async function setupPowerSyncInIframe(dbFilename: string, identifier: str '*' ); } catch (error) { - const statusEl = document.getElementById('status'); - if (statusEl) { - statusEl.textContent = 'Error: ' + (error as Error).message; - statusEl.style.color = '#dc3545'; - statusEl.style.borderColor = '#dc3545'; - } console.error('PowerSync initialization error:', error); window.parent.postMessage( { diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts index 6cfbbe37f..31b3acc55 100644 --- a/packages/web/vitest.config.ts +++ b/packages/web/vitest.config.ts @@ -22,9 +22,7 @@ const config: UserConfigExport = { // https://jira.mongodb.org/browse/NODE-5773 bson: require.resolve('bson'), // Mock WebRemote to throw 401 errors for all HTTP requests in tests - '../../db/sync/WebRemote': path.resolve(__dirname, './tests/mocks/MockWebRemote.ts'), - // Also handle the case where it's imported from the worker context - '@powersync/web/src/db/sync/WebRemote': path.resolve(__dirname, './tests/mocks/MockWebRemote.ts') + '../../db/sync/WebRemote': path.resolve(__dirname, './tests/mocks/MockWebRemote.ts') } }, worker: { @@ -35,7 +33,7 @@ const config: UserConfigExport = { // Don't optimise these packages as they contain web workers and WASM files. // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673 exclude: ['@journeyapps/wa-sqlite', '@powersync/web'], - include: ['bson', 'comlink', 'async-mutex'] + include: [] }, plugins: [wasm(), topLevelAwait()], test: { @@ -66,17 +64,7 @@ const config: UserConfigExport = { // { // browser: 'webkit' // } - ], - // Disable private browsing mode for WebKit - // This allows persistent storage (IndexedDB, localStorage, etc.) to work properly - providerOptions: { - webkit: { - launch: { - // WebKit-specific launch options - }, - context: {} - } - } + ] } } }; From de7804f14a11132d68f18bf6bebd152f1e5d3f9d Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 12:13:00 +0200 Subject: [PATCH 10/44] restore collectActiveSubscriptions --- .../src/worker/sync/SharedSyncImplementation.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index ce8677a12..ed14e492c 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -368,10 +368,18 @@ export class SharedSyncImplementation extends BaseObserver this.collectActiveSubscriptions()); + } else { + // The port removed was not the database in use. We didn't need to reconnect explicitly, but we need to update the subscriptions. + this.collectActiveSubscriptions(); } return () => trackedPort.clientProvider[Comlink.releaseProxy](); From c08e664601b33971af73e9b7747b87f6a5b66ed8 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 12:30:59 +0200 Subject: [PATCH 11/44] cleanup code flow --- .../worker/sync/SharedSyncImplementation.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index ed14e492c..08c13dc09 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -341,34 +341,38 @@ export class SharedSyncImplementation extends BaseObserver 0; - /** - * If the current database adapter is the one that is being closed, we need to disconnect from the backend. - * We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other - * connect operations coming from other tabs. - */ - if (this.dbAdapter && this.dbAdapter == trackedPort.db) { - this.logger.debug(`Disconnecting due to closed database: should reconnect: ${shouldReconnect}`); - this.dbAdapter = null; - // Unconditionally close the connection because the database it's writing to has just been closed. - // The connection has been closed previously, this might throw. We should be able to ignore it. - await this.connectionManager - .disconnect() - .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex)); - } + const shouldReconnect = + !!this.connectionManager.syncStreamImplementation && + this.ports.length > 0 && + this.dbAdapter && + this.dbAdapter == trackedPort.db; + // Close the worker wrapped database connection, we can't accurately rely on this connection for (const closeListener of trackedPort.closeListeners) { await closeListener(); } + // Close the LockedAsyncDatabaseAdapter for cleanup try { await trackedPort.db?.close(); } catch (ex) { this.logger.warn('error closing database', ex); } - // Re-index subscriptions, the subscriptions of the removed port would no longer be considered. + /** + * If the current database adapter is the one that is being closed, we need to disconnect from the backend. + * We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other + * connect operations coming from other tabs. + * Re-index subscriptions, the subscriptions of the removed port would no longer be considered. + */ if (shouldReconnect) { + this.logger.debug(`Disconnecting due to closed database: should reconnect: ${shouldReconnect}`); + this.dbAdapter = null; + // Unconditionally close the connection because the database it's writing to has just been closed. + // The connection has been closed previously, this might throw. We should be able to ignore it. + await this.connectionManager + .disconnect() + .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex)); /** * The internals of this needs a port mutex lock. * It should be safe to start this operation here, but we cannot and don't need to await it. From aedc8556984147e6588bc21979f7503dc3c1a4d6 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 14:31:19 +0200 Subject: [PATCH 12/44] Add withTimeout for opening db connections --- .../worker/sync/SharedSyncImplementation.ts | 27 +++++++++++++- .../web/tests/multiple_tabs_iframe.test.ts | 37 ++++++++++++++----- packages/web/tests/utils/iframeInitializer.ts | 4 +- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 08c13dc09..167d8025a 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -510,10 +510,18 @@ export class SharedSyncImplementation extends BaseObserver lastClient.clientProvider.getDBWorkerPort(), 5_000); const remote = Comlink.wrap(workerPort); const identifier = this.syncParams!.dbParams.dbFilename; - const db = await remote(this.syncParams!.dbParams); + + /** + * The open could fail if the tab is closed while we're busy opening the database. + * This operation is typically executed inside an exclusive portMutex lock. + * We typically execute the closeListeners using the portMutex in a different context. + * We can't rely on the closeListeners to abort the operation if the tab is closed. + */ + const db = await withTimeout(() => remote(this.syncParams!.dbParams), 5_000); + const locked = new LockedAsyncDatabaseAdapter({ name: identifier, defaultLockTimeoutMs: 20_000, // Max wait time for a lock request (we will retry failed attempts) @@ -567,3 +575,18 @@ export class SharedSyncImplementation extends BaseObserver(action: () => Promise, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Timeout waiting for action')); + }, timeoutMs); + action() + .then(resolve) + .catch(reject) + .finally(() => clearTimeout(timeout)); + }); +} diff --git a/packages/web/tests/multiple_tabs_iframe.test.ts b/packages/web/tests/multiple_tabs_iframe.test.ts index 0135b069e..4cdc35244 100644 --- a/packages/web/tests/multiple_tabs_iframe.test.ts +++ b/packages/web/tests/multiple_tabs_iframe.test.ts @@ -247,11 +247,11 @@ async function createIframeWithPowerSyncClient( */ function createMultipleTabsTest(vfs?: WASQLiteVFS) { const vfsName = vfs || 'IndexedDB'; - describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 120000 }, () => { + describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 20_000 }, () => { const dbFilename = `test-multi-tab-${uuid()}.db`; // Configurable number of tabs to create (excluding the long-lived tab) - const NUM_TABS = 10; + const NUM_TABS = 50; it('should handle simultaneous close and reopen of tabs', async () => { // Step 1: Open a long-lived reference tab that stays open throughout the test @@ -299,13 +299,20 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { } expect(longLivedTab.iframe.isConnected).toBe(true); - // Step 3: Simultaneously close half of the tabs (simulating abrupt closure) - const halfCount = Math.floor(NUM_TABS / 2); - // Close the latest opened tabs since we usually use the last connected tabs for operations. - const tabsToClose = tabs.slice(halfCount); - const tabsToKeep = tabs.slice(0, halfCount); + // Step 3: Simultaneously close the first and last quarters of the tabs (simulating abrupt closure) + const quarterCount = Math.floor(NUM_TABS / 4); + const firstQuarterEnd = quarterCount; + const lastQuarterStart = NUM_TABS - quarterCount; - // Close half the tabs simultaneously (without proper cleanup) + // Close the first quarter and last quarter of tabs + const firstQuarter = tabs.slice(0, firstQuarterEnd); + const lastQuarter = tabs.slice(lastQuarterStart); + const tabsToClose = [...firstQuarter, ...lastQuarter]; + + // Keep the middle two quarters + const tabsToKeep = tabs.slice(firstQuarterEnd, lastQuarterStart); + + // Close the first and last quarters of tabs simultaneously (without proper cleanup) // Do this in reverse order to ensure the last connected tab is closed first. const closePromises = tabsToClose.reverse().map((tab) => tab.cleanup()); await Promise.all(closePromises); @@ -324,8 +331,13 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { // Step 4: Reopen the closed tabs const reopenedTabs: IframeClient[] = []; - const reopenPromises = tabsToClose.map(async (_, index) => { - const identifier = tabIdentifiers[index]; + // Get the identifiers for the closed tabs by finding their indices in the original tabs array + const closedTabIdentifiers = tabsToClose.map((closedTab) => { + const index = tabs.indexOf(closedTab); + return tabIdentifiers[index]; + }); + + const reopenPromises = closedTabIdentifiers.map(async (identifier) => { const result = await createIframeWithPowerSyncClient(dbFilename, identifier, vfs); reopenedTabs.push(result); @@ -367,6 +379,11 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { expect(queryResult.length).toBe(1); expect((queryResult[0] as { value: number }).value).toBe(1); + /** + * Wait a little for the state to settle. + */ + await new Promise((resolve) => setTimeout(resolve, 1000)); + // Step 6: Create a new tab which should trigger a connect. The shared sync worker should reconnect. // This ensures the shared sync worker is not stuck and is properly handling new connections const newTabIdentifier = `new-tab-${Date.now()}`; diff --git a/packages/web/tests/utils/iframeInitializer.ts b/packages/web/tests/utils/iframeInitializer.ts index b31d6726d..5d3d7df7c 100644 --- a/packages/web/tests/utils/iframeInitializer.ts +++ b/packages/web/tests/utils/iframeInitializer.ts @@ -49,8 +49,8 @@ export async function setupPowerSyncInIframe(dbFilename: string, identifier: str logger }); - // Connect to PowerSync - await db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP }); + // Connect to PowerSync (don't await this since we want to create multiple tabs) + db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP }); // Store reference for cleanup (window as any).powersyncClient = db; From 62b04de08a6f0d8ed6d5dafc91c0f246c7407b61 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 14:46:32 +0200 Subject: [PATCH 13/44] reduce number of iframes for CI --- packages/web/tests/multiple_tabs_iframe.test.ts | 14 +++++--------- packages/web/tests/utils/iframeInitializer.ts | 13 +++++++++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/web/tests/multiple_tabs_iframe.test.ts b/packages/web/tests/multiple_tabs_iframe.test.ts index 4cdc35244..9817de990 100644 --- a/packages/web/tests/multiple_tabs_iframe.test.ts +++ b/packages/web/tests/multiple_tabs_iframe.test.ts @@ -23,7 +23,8 @@ createMultipleTabsTest(WASQLiteVFS.OPFSCoopSyncVFS); async function createIframeWithPowerSyncClient( dbFilename: string, identifier: string, - vfs?: WASQLiteVFS + vfs?: WASQLiteVFS, + waitForConnection?: boolean ): Promise { const iframe = document.createElement('iframe'); // Make iframe visible for debugging @@ -81,7 +82,7 @@ async function createIframeWithPowerSyncClient(
`; @@ -251,7 +252,7 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { const dbFilename = `test-multi-tab-${uuid()}.db`; // Configurable number of tabs to create (excluding the long-lived tab) - const NUM_TABS = 50; + const NUM_TABS = 20; it('should handle simultaneous close and reopen of tabs', async () => { // Step 1: Open a long-lived reference tab that stays open throughout the test @@ -379,15 +380,10 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { expect(queryResult.length).toBe(1); expect((queryResult[0] as { value: number }).value).toBe(1); - /** - * Wait a little for the state to settle. - */ - await new Promise((resolve) => setTimeout(resolve, 1000)); - // Step 6: Create a new tab which should trigger a connect. The shared sync worker should reconnect. // This ensures the shared sync worker is not stuck and is properly handling new connections const newTabIdentifier = `new-tab-${Date.now()}`; - const newTab = await createIframeWithPowerSyncClient(dbFilename, newTabIdentifier, vfs); + const newTab = await createIframeWithPowerSyncClient(dbFilename, newTabIdentifier, vfs, true); onTestFinished(async () => { try { await newTab.cleanup(); diff --git a/packages/web/tests/utils/iframeInitializer.ts b/packages/web/tests/utils/iframeInitializer.ts index 5d3d7df7c..3f9e7b91e 100644 --- a/packages/web/tests/utils/iframeInitializer.ts +++ b/packages/web/tests/utils/iframeInitializer.ts @@ -7,7 +7,12 @@ import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/ * * @param vfs - VFS option as a string (e.g., 'OPFSCoopSyncVFS' or 'IDBBatchAtomicVFS') */ -export async function setupPowerSyncInIframe(dbFilename: string, identifier: string, vfs?: string): Promise { +export async function setupPowerSyncInIframe( + dbFilename: string, + identifier: string, + vfs?: string, + waitForConnection?: boolean +): Promise { try { // Track the number of times fetchCredentials has been called let credentialsFetchCount = 0; @@ -50,7 +55,11 @@ export async function setupPowerSyncInIframe(dbFilename: string, identifier: str }); // Connect to PowerSync (don't await this since we want to create multiple tabs) - db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP }); + const connectionPromise = db.connect(connector, { connectionMethod: SyncStreamConnectionMethod.HTTP }); + + if (waitForConnection) { + await connectionPromise; + } // Store reference for cleanup (window as any).powersyncClient = db; From 0fc0d734c4a541c3cb230d78e68026dab863a322 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 17:06:24 +0200 Subject: [PATCH 14/44] Use a distributed database adapter instead of reconnecting. Retry opening internal worker connections. --- .../common/src/client/ConnectionManager.ts | 2 +- .../db/adapters/LockedAsyncDatabaseAdapter.ts | 34 +++- .../WorkerWrappedAsyncDatabaseConnection.ts | 11 +- .../worker/sync/SharedSyncImplementation.ts | 166 +++++++----------- 4 files changed, 104 insertions(+), 109 deletions(-) diff --git a/packages/common/src/client/ConnectionManager.ts b/packages/common/src/client/ConnectionManager.ts index 859add550..0d877d7e2 100644 --- a/packages/common/src/client/ConnectionManager.ts +++ b/packages/common/src/client/ConnectionManager.ts @@ -1,4 +1,5 @@ import { ILogger } from 'js-logger'; +import { SyncStatus } from '../db/crud/SyncStatus.js'; import { BaseListener, BaseObserver } from '../utils/BaseObserver.js'; import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js'; import { @@ -13,7 +14,6 @@ import { SyncStreamSubscribeOptions, SyncStreamSubscription } from './sync/sync-streams.js'; -import { SyncStatus } from '../db/crud/SyncStatus.js'; /** * @internal diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 67e2910ae..41c56c93b 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -13,7 +13,10 @@ import { import { getNavigatorLocks } from '../..//shared/navigator'; import { AsyncDatabaseConnection } from './AsyncDatabaseConnection'; import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter'; -import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection'; +import { + WorkerConnectionClosedError, + WorkerWrappedAsyncDatabaseConnection +} from './WorkerWrappedAsyncDatabaseConnection'; import { WASQLiteVFS } from './wa-sqlite/WASQLiteConnection'; import { ResolvedWASQLiteOpenFactoryOptions } from './wa-sqlite/WASQLiteOpenFactory'; import { ResolvedWebSQLOpenOptions } from './web-sql-flags'; @@ -52,6 +55,7 @@ export class LockedAsyncDatabaseAdapter private _config: ResolvedWebSQLOpenOptions | null = null; protected pendingAbortControllers: Set; protected requiresHolds: boolean | null; + protected requiresReOpen: boolean; closing: boolean; closed: boolean; @@ -64,6 +68,7 @@ export class LockedAsyncDatabaseAdapter this.closed = false; this.closing = false; this.requiresHolds = null; + this.requiresReOpen = false; // Set the name if provided. We can query for the name if not available yet this.debugMode = options.debugMode ?? false; if (this.debugMode) { @@ -106,18 +111,26 @@ export class LockedAsyncDatabaseAdapter return this.initPromise; } - protected async _init() { + protected async openInternalDB() { + // Dispose any previous table change listener. + this._disposeTableChangeListener?.(); + this._disposeTableChangeListener = null; + this._db = await this.options.openConnection(); await this._db.init(); this._config = await this._db.getConfig(); await this.registerOnChangeListener(this._db); - this.iterateListeners((cb) => cb.initialized?.()); /** * This is only required for the long-lived shared IndexedDB connections. */ this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS; } + protected async _init() { + await this.openInternalDB(); + this.iterateListeners((cb) => cb.initialized?.()); + } + getConfiguration(): ResolvedWebSQLOpenOptions { if (!this._config) { throw new Error(`Cannot get config before initialization is completed`); @@ -223,7 +236,7 @@ export class LockedAsyncDatabaseAdapter this.pendingAbortControllers.add(abortController); const { timeoutMs } = options ?? {}; - const timoutId = timeoutMs + const timeoutId = timeoutMs ? setTimeout(() => { abortController.abort(`Timeout after ${timeoutMs}ms`); this.pendingAbortControllers.delete(abortController); @@ -235,12 +248,21 @@ export class LockedAsyncDatabaseAdapter { signal: abortController.signal }, async () => { this.pendingAbortControllers.delete(abortController); - if (timoutId) { - clearTimeout(timoutId); + if (timeoutId) { + clearTimeout(timeoutId); } const holdId = this.requiresHolds ? await this.baseDB.markHold() : null; try { + if (this.requiresReOpen) { + await this.openInternalDB(); + this.requiresReOpen = false; + } return await callback(); + } catch (ex) { + if (ex instanceof WorkerConnectionClosedError) { + this.requiresReOpen = true; + } + throw ex; } finally { if (holdId) { await this.baseDB.releaseHold(holdId); diff --git a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts index e03bf8aa8..8eda58c41 100644 --- a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts @@ -23,6 +23,13 @@ export type WrappedWorkerConnectionOptions void; }; +export class WorkerConnectionClosedError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorkerConnectionClosedError'; + } +} + /** * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy * functions for worker listeners. @@ -77,13 +84,13 @@ export class WorkerWrappedAsyncDatabaseConnection { if (controller.signal.aborted) { - reject(new Error('Called operation on closed remote')); + reject(new WorkerConnectionClosedError('Called operation on closed remote')); // Don't run the operation if we're going to reject return; } function handleAbort() { - reject(new Error('Remote peer closed with request in flight')); + reject(new WorkerConnectionClosedError('Remote peer closed with request in flight')); } function completePromise(action: () => void) { diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 167d8025a..68557d556 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -2,14 +2,14 @@ import { AbortOperation, BaseObserver, ConnectionManager, - createLogger, DBAdapter, PowerSyncBackendConnector, SqliteBucketStorage, SubscribedStream, SyncStatus, - type ILogger, + createLogger, type ILogLevel, + type ILogger, type PowerSyncConnectionOptions, type StreamingSyncImplementation, type StreamingSyncImplementationListener, @@ -25,8 +25,8 @@ import { import { OpenAsyncDatabaseConnection } from '../../db/adapters/AsyncDatabaseConnection'; import { LockedAsyncDatabaseAdapter } from '../../db/adapters/LockedAsyncDatabaseAdapter'; -import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags'; import { WorkerWrappedAsyncDatabaseConnection } from '../../db/adapters/WorkerWrappedAsyncDatabaseConnection'; +import { ResolvedWebSQLOpenOptions } from '../../db/adapters/web-sql-flags'; import { AbstractSharedSyncClientProvider } from './AbstractSharedSyncClientProvider'; import { BroadcastLogger } from './BroadcastLogger'; @@ -80,6 +80,7 @@ export type WrappedSyncPort = { * If we can use Navigator locks to detect if the client has closed. */ isProtectedFromClose: boolean; + isClosing: boolean; }; /** @@ -110,7 +111,6 @@ export class SharedSyncImplementation extends BaseObserver { - return this.portMutex.runExclusive(async () => { - await this.waitForReady(); - this.logger.debug('Creating sync implementation'); - if (!this.dbAdapter) { - this.logger.debug('Opening internal DB'); - await this.openInternalDB(); - } - - const sync = this.generateStreamingImplementation(); - const onDispose = sync.registerListener({ - statusChanged: (status) => { - this.updateAllStatuses(status.toJSON()); - } - }); + await this.waitForReady(); - return { - sync, - onDispose - }; + const sync = this.generateStreamingImplementation(); + const onDispose = sync.registerListener({ + statusChanged: (status) => { + this.updateAllStatuses(status.toJSON()); + } }); + + return { + sync, + onDispose + }; }, logger: this.logger }); @@ -183,7 +179,7 @@ export class SharedSyncImplementation extends BaseObserver= 0; i--) { - if (this.ports[i].isProtectedFromClose) { + if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) { return this.ports[i]; } } @@ -238,11 +234,6 @@ export class SharedSyncImplementation extends BaseObserver { + // Gets a connection from the clients when a new connection is requested. + return await this.openInternalDB(); + }, + logger: this.logger + }); + this.distributedDB = lockedAdapter; + await lockedAdapter.init(); + self.onerror = (event) => { // Share any uncaught events on the broadcast logger this.logger.error('Uncaught exception in PowerSync shared sync worker', event); }; - if (!this.dbAdapter) { - await this.openInternalDB(); - } - this.iterateListeners((l) => l.initialized?.()); }); } @@ -296,7 +294,8 @@ export class SharedSyncImplementation extends BaseObserver(port), currentSubscriptions: [], closeListeners: [], - isProtectedFromClose: false + isProtectedFromClose: false, + isClosing: false } satisfies WrappedSyncPort; this.ports.push(portProvider); @@ -315,6 +314,9 @@ export class SharedSyncImplementation extends BaseObserver 0 && - this.dbAdapter && - this.dbAdapter == trackedPort.db; - // Close the worker wrapped database connection, we can't accurately rely on this connection for (const closeListener of trackedPort.closeListeners) { await closeListener(); } - // Close the LockedAsyncDatabaseAdapter for cleanup - try { - await trackedPort.db?.close(); - } catch (ex) { - this.logger.warn('error closing database', ex); - } - - /** - * If the current database adapter is the one that is being closed, we need to disconnect from the backend. - * We can disconnect in the portMutex lock. This ensures the disconnect is not affected by potential other - * connect operations coming from other tabs. - * Re-index subscriptions, the subscriptions of the removed port would no longer be considered. - */ - if (shouldReconnect) { - this.logger.debug(`Disconnecting due to closed database: should reconnect: ${shouldReconnect}`); - this.dbAdapter = null; - // Unconditionally close the connection because the database it's writing to has just been closed. - // The connection has been closed previously, this might throw. We should be able to ignore it. - await this.connectionManager - .disconnect() - .catch((ex) => this.logger.warn('Error while disconnecting. Will attempt to reconnect.', ex)); - /** - * The internals of this needs a port mutex lock. - * It should be safe to start this operation here, but we cannot and don't need to await it. - * Since we disconnected, we need to collectActiveSubscriptions after reconnecting. - */ - this.connectionManager - .connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {}) - .then(() => this.collectActiveSubscriptions()); - } else { - // The port removed was not the database in use. We didn't need to reconnect explicitly, but we need to update the subscriptions. - this.collectActiveSubscriptions(); - } + this.collectActiveSubscriptions(); return () => trackedPort.clientProvider[Comlink.releaseProxy](); }); @@ -432,7 +396,7 @@ export class SharedSyncImplementation extends BaseObserver { @@ -504,28 +468,30 @@ export class SharedSyncImplementation extends BaseObserver lastClient.clientProvider.getDBWorkerPort(), 5_000); - const remote = Comlink.wrap(workerPort); - const identifier = this.syncParams!.dbParams.dbFilename; - - /** - * The open could fail if the tab is closed while we're busy opening the database. - * This operation is typically executed inside an exclusive portMutex lock. - * We typically execute the closeListeners using the portMutex in a different context. - * We can't rely on the closeListeners to abort the operation if the tab is closed. - */ - const db = await withTimeout(() => remote(this.syncParams!.dbParams), 5_000); - - const locked = new LockedAsyncDatabaseAdapter({ - name: identifier, - defaultLockTimeoutMs: 20_000, // Max wait time for a lock request (we will retry failed attempts) - openConnection: async () => { + while (true) { + try { + const lastClient = this.lastWrappedPort; + if (!lastClient) { + // Should not really happen in practice + throw new Error(`Could not open DB connection since no client is connected.`); + } + + const workerPort = await withTimeout(() => lastClient.clientProvider.getDBWorkerPort(), 5_000); + const remote = Comlink.wrap(workerPort); + const identifier = this.syncParams!.dbParams.dbFilename; + + /** + * The open could fail if the tab is closed while we're busy opening the database. + * This operation is typically executed inside an exclusive portMutex lock. + * We typically execute the closeListeners using the portMutex in a different context. + * We can't rely on the closeListeners to abort the operation if the tab is closed. + */ + const db = await withTimeout(() => remote(this.syncParams!.dbParams), 5_000); + const wrapped = new WorkerWrappedAsyncDatabaseConnection({ remote, baseConnection: db, @@ -546,11 +512,11 @@ export class SharedSyncImplementation extends BaseObserver setTimeout(resolve, 1000)); + } + } } /** From 91db686f9151c86efbd1233a33577e9501f01569 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 17:50:40 +0200 Subject: [PATCH 15/44] Catch closed errors for hold requests. Use crud throttle time for crud retries. --- .../sync/stream/AbstractStreamingSyncImplementation.ts | 9 ++++----- .../web/src/db/adapters/LockedAsyncDatabaseAdapter.ts | 6 +++++- packages/web/tests/multiple_instances.test.ts | 5 ++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index 0de88f9a6..2b368400c 100644 --- a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -429,7 +429,7 @@ The next upload iteration will be delayed.`); uploadError: ex } }); - await this.delayRetry(controller.signal); + await this.delayRetry(controller.signal, this.options.crudUploadThrottleMs); if (!this.isConnected) { // Exit the upload loop if the sync stream is no longer connected break; @@ -1216,15 +1216,14 @@ The next upload iteration will be delayed.`); this.iterateListeners((cb) => cb.statusUpdated?.(options)); } - private async delayRetry(signal?: AbortSignal): Promise { + private async delayRetry(signal?: AbortSignal, delayMs?: number): Promise { return new Promise((resolve) => { if (signal?.aborted) { // If the signal is already aborted, resolve immediately resolve(); return; } - - const { retryDelayMs } = this.options; + const delay = delayMs ?? this.options.retryDelayMs; let timeoutId: ReturnType | undefined; @@ -1238,7 +1237,7 @@ The next upload iteration will be delayed.`); }; signal?.addEventListener('abort', endDelay, { once: true }); - timeoutId = setTimeout(endDelay, retryDelayMs); + timeoutId = setTimeout(endDelay, delay); }); } diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 41c56c93b..c80dc39cc 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -251,12 +251,16 @@ export class LockedAsyncDatabaseAdapter if (timeoutId) { clearTimeout(timeoutId); } - const holdId = this.requiresHolds ? await this.baseDB.markHold() : null; + let holdId: string | null = null; try { if (this.requiresReOpen) { + this.logger.debug('Re-opening database'); await this.openInternalDB(); + this.logger.debug('Database re-opened'); this.requiresReOpen = false; } + + holdId = this.requiresHolds ? await this.baseDB.markHold() : null; return await callback(); } catch (ex) { if (ex instanceof WorkerConnectionClosedError) { diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 15e80918c..4411498a5 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -284,11 +284,12 @@ describe('Multiple Instances', { sequential: true }, () => { await stream2.dispose(); }); - it('should trigger uploads from last connected clients', async () => { + it('should trigger uploads from last connected clients', { timeout: Infinity }, async () => { // Generate the first streaming sync implementation const connector1 = new TestConnector(); const spy1 = vi.spyOn(connector1, 'uploadData'); + await new Promise((resolve) => setTimeout(resolve, 5000)); const db = openDatabase(); await db.init(); // They need to use the same identifier to use the same shared worker. @@ -380,8 +381,6 @@ describe('Multiple Instances', { sequential: true }, () => { // Close the second client, leaving only the first one await stream2.dispose(); - // The dispose above will disconnect, but we need to wait for the sync stream to be created before we can update the status - await vi.waitFor(() => expect(stream1.syncStatus.connecting).true); // Hack, set the status to connected in order to trigger the upload await (stream1 as any)['_testUpdateStatus'](new SyncStatus({ connected: true })); stream1.triggerCrudUpload(); From deaf83c7fda07d7f01d27748e9425123c87145ac Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Fri, 28 Nov 2025 18:09:25 +0200 Subject: [PATCH 16/44] Synchronize lock requests better. --- packages/web/src/worker/sync/WorkerClient.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/web/src/worker/sync/WorkerClient.ts b/packages/web/src/worker/sync/WorkerClient.ts index b88b190ad..a5a8ded86 100644 --- a/packages/web/src/worker/sync/WorkerClient.ts +++ b/packages/web/src/worker/sync/WorkerClient.ts @@ -69,19 +69,18 @@ export class WorkerClient { * When the client tab is closed, its lock will be returned. So when the shared worker attempts to acquire the lock, * it can consider the connection to be closed. */ - addLockBasedCloseSignal(name: string) { + async addLockBasedCloseSignal(name: string) { if (!this.resolvedPortPromise) { // The init logic above is actually synchronous, so this should not happen. this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered'); } else { - this.resolvedPortPromise.then((wrappedPort) => { - /** - * The client registered a navigator lock. We now can guarantee detecting if the client has closed. - * E.g. before this point: It's possible some ports might have been created and closed before the - * lock based close signal is added. We should not trust those ports. - */ - wrappedPort.isProtectedFromClose = true; - }); + const wrappedPort = await this.resolvedPortPromise; + /** + * The client registered a navigator lock. We now can guarantee detecting if the client has closed. + * E.g. before this point: It's possible some ports might have been created and closed before the + * lock based close signal is added. We should not trust those ports. + */ + wrappedPort.isProtectedFromClose = true; } getNavigatorLocks().request(name, async () => { await this.removePort(); From 37802231183550e6dd60f3d3b841b413c9a256a6 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Sat, 29 Nov 2025 07:56:48 +0200 Subject: [PATCH 17/44] Re-open database as soon as it's closed. Trigger uploads if database has closed. --- .../db/adapters/AsyncDatabaseConnection.ts | 7 ++ .../db/adapters/LockedAsyncDatabaseAdapter.ts | 42 +++++++---- .../WorkerWrappedAsyncDatabaseConnection.ts | 12 +--- .../worker/sync/SharedSyncImplementation.ts | 71 ++++++++++++++++--- 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/packages/web/src/db/adapters/AsyncDatabaseConnection.ts b/packages/web/src/db/adapters/AsyncDatabaseConnection.ts index 1b983802c..1bf791f63 100644 --- a/packages/web/src/db/adapters/AsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/AsyncDatabaseConnection.ts @@ -17,6 +17,13 @@ export type ProxiedQueryResult = Omit & { */ export type OnTableChangeCallback = (event: BatchedUpdateNotification) => void; +export class ConnectionClosedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ConnectionClosedError'; + } +} + /** * @internal * An async Database connection which provides basic async SQL methods. diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index c80dc39cc..9d48adc7b 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -11,12 +11,9 @@ import { type ILogger } from '@powersync/common'; import { getNavigatorLocks } from '../..//shared/navigator'; -import { AsyncDatabaseConnection } from './AsyncDatabaseConnection'; +import { AsyncDatabaseConnection, ConnectionClosedError } from './AsyncDatabaseConnection'; import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter'; -import { - WorkerConnectionClosedError, - WorkerWrappedAsyncDatabaseConnection -} from './WorkerWrappedAsyncDatabaseConnection'; +import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection'; import { WASQLiteVFS } from './wa-sqlite/WASQLiteConnection'; import { ResolvedWASQLiteOpenFactoryOptions } from './wa-sqlite/WASQLiteOpenFactory'; import { ResolvedWebSQLOpenOptions } from './web-sql-flags'; @@ -30,10 +27,15 @@ export interface LockedAsyncDatabaseAdapterOptions { debugMode?: boolean; logger?: ILogger; defaultLockTimeoutMs?: number; + reOpenOnConnectionClosed?: boolean; } export type LockedAsyncDatabaseAdapterListener = DBAdapterListener & { initialized?: () => void; + /** + * Fired when the database is re-opened after being closed. + */ + databaseReOpened?: () => void; }; /** @@ -56,6 +58,7 @@ export class LockedAsyncDatabaseAdapter protected pendingAbortControllers: Set; protected requiresHolds: boolean | null; protected requiresReOpen: boolean; + protected databaseOpenPromise: Promise | null = null; closing: boolean; closed: boolean; @@ -116,10 +119,15 @@ export class LockedAsyncDatabaseAdapter this._disposeTableChangeListener?.(); this._disposeTableChangeListener = null; + const isReOpen = !!this._db; + this._db = await this.options.openConnection(); await this._db.init(); this._config = await this._db.getConfig(); await this.registerOnChangeListener(this._db); + if (isReOpen) { + this.iterateListeners((cb) => cb.databaseReOpened?.()); + } /** * This is only required for the long-lived shared IndexedDB connections. */ @@ -253,18 +261,28 @@ export class LockedAsyncDatabaseAdapter } let holdId: string | null = null; try { - if (this.requiresReOpen) { - this.logger.debug('Re-opening database'); - await this.openInternalDB(); - this.logger.debug('Database re-opened'); - this.requiresReOpen = false; + // The database is being opened in the background. Wait for it here. + if (this.databaseOpenPromise) { + try { + await this.databaseOpenPromise; + } catch (ex) { + // This will cause a retry of opening the database. + const wrappedError = new ConnectionClosedError('Could not open database'); + wrappedError.cause = ex; + throw wrappedError; + } } holdId = this.requiresHolds ? await this.baseDB.markHold() : null; return await callback(); } catch (ex) { - if (ex instanceof WorkerConnectionClosedError) { - this.requiresReOpen = true; + if (ex instanceof ConnectionClosedError) { + if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) { + // Immediately re-open the database. We need to miss as little table updates as possible. + this.databaseOpenPromise = this.openInternalDB().finally(() => { + this.databaseOpenPromise = null; + }); + } } throw ex; } finally { diff --git a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts index 8eda58c41..81caf8445 100644 --- a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts @@ -1,6 +1,7 @@ import * as Comlink from 'comlink'; import { AsyncDatabaseConnection, + ConnectionClosedError, OnTableChangeCallback, OpenAsyncDatabaseConnection, ProxiedQueryResult @@ -23,13 +24,6 @@ export type WrappedWorkerConnectionOptions void; }; -export class WorkerConnectionClosedError extends Error { - constructor(message: string) { - super(message); - this.name = 'WorkerConnectionClosedError'; - } -} - /** * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy * functions for worker listeners. @@ -84,13 +78,13 @@ export class WorkerWrappedAsyncDatabaseConnection { if (controller.signal.aborted) { - reject(new WorkerConnectionClosedError('Called operation on closed remote')); + reject(new ConnectionClosedError('Called operation on closed remote')); // Don't run the operation if we're going to reject return; } function handleAbort() { - reject(new WorkerConnectionClosedError('Remote peer closed with request in flight')); + reject(new ConnectionClosedError('Remote peer closed with request in flight')); } function completePromise(action: () => void) { diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 68557d556..e218b75bb 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -249,11 +249,20 @@ export class SharedSyncImplementation extends BaseObserver { + // We may have missed some table updates while the database was closed. + // We can poke the crud in case we missed any updates. + this.connectionManager.syncStreamImplementation?.triggerCrudUpload(); + } + }); + self.onerror = (event) => { // Share any uncaught events on the broadcast logger this.logger.error('Uncaught exception in PowerSync shared sync worker', event); @@ -480,7 +489,31 @@ export class SharedSyncImplementation extends BaseObserver lastClient.clientProvider.getDBWorkerPort(), 5_000); + /** + * Handle cases where the client might close while opening a connection. + */ + const abortController = new AbortController(); + const closeListener = () => { + abortController.abort(); + }; + + const removeCloseListener = () => { + const index = lastClient.closeListeners.indexOf(closeListener); + if (index >= 0) { + lastClient.closeListeners.splice(index, 1); + } + }; + + lastClient.closeListeners.push(closeListener); + + const workerPort = await withAbort( + () => lastClient.clientProvider.getDBWorkerPort(), + abortController.signal + ).catch((ex) => { + removeCloseListener(); + throw ex; + }); + const remote = Comlink.wrap(workerPort); const identifier = this.syncParams!.dbParams.dbFilename; @@ -490,7 +523,10 @@ export class SharedSyncImplementation extends BaseObserver remote(this.syncParams!.dbParams), 5_000); + const db = await withAbort(() => remote(this.syncParams!.dbParams), abortController.signal).finally(() => { + // We can remove the close listener here since we no longer need it past this point. + removeCloseListener(); + }); const wrapped = new WorkerWrappedAsyncDatabaseConnection({ remote, @@ -543,16 +579,29 @@ export class SharedSyncImplementation extends BaseObserver(action: () => Promise, timeoutMs: number): Promise { +function withAbort(action: () => Promise, signal: AbortSignal): Promise { return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Timeout waiting for action')); - }, timeoutMs); + if (signal.aborted) { + reject(new AbortOperation('Operation aborted by abort controller')); + return; + } + + function handleAbort() { + signal.removeEventListener('abort', handleAbort); + reject(new AbortOperation('Operation aborted by abort controller')); + } + + signal.addEventListener('abort', handleAbort, { once: true }); + + function completePromise(action: () => void) { + signal.removeEventListener('abort', handleAbort); + action(); + } + action() - .then(resolve) - .catch(reject) - .finally(() => clearTimeout(timeout)); + .then((data) => completePromise(() => resolve(data))) + .catch((e) => completePromise(() => reject(e))); }); } From 8f95e1080f3556a391bb51f9e33fe33df898240a Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Sat, 29 Nov 2025 08:07:13 +0200 Subject: [PATCH 18/44] Update tabs test to be more indicative of the actual issues. --- .../web/tests/multiple_tabs_iframe.test.ts | 257 ++++++++---------- 1 file changed, 119 insertions(+), 138 deletions(-) diff --git a/packages/web/tests/multiple_tabs_iframe.test.ts b/packages/web/tests/multiple_tabs_iframe.test.ts index 9817de990..df6159a29 100644 --- a/packages/web/tests/multiple_tabs_iframe.test.ts +++ b/packages/web/tests/multiple_tabs_iframe.test.ts @@ -1,6 +1,6 @@ import { WASQLiteVFS } from '@powersync/web'; import { v4 as uuid } from 'uuid'; -import { describe, expect, it, onTestFinished, vi } from 'vitest'; +import { describe, expect, it, onTestFinished } from 'vitest'; /** * Creates an iframe with a PowerSync client that connects using the same database. @@ -16,16 +16,22 @@ interface IframeClient { getCredentialsFetchCount: () => Promise; } +interface IframeClientResult { + iframe: HTMLIFrameElement; + cleanup: () => Promise; + ready: Promise; +} + // Run tests for both IndexedDB and OPFS createMultipleTabsTest(); // IndexedDB (default) createMultipleTabsTest(WASQLiteVFS.OPFSCoopSyncVFS); -async function createIframeWithPowerSyncClient( +function createIframeWithPowerSyncClient( dbFilename: string, identifier: string, vfs?: WASQLiteVFS, waitForConnection?: boolean -): Promise { +): IframeClientResult { const iframe = document.createElement('iframe'); // Make iframe visible for debugging iframe.style.display = 'block'; @@ -91,33 +97,60 @@ async function createIframeWithPowerSyncClient( const url = URL.createObjectURL(blob); iframe.src = url; - return new Promise((resolve, reject) => { - let requestIdCounter = 0; - const pendingRequests = new Map< - string, - { - resolve: (value: any) => void; - reject: (error: Error) => void; + let requestIdCounter = 0; + const pendingRequests = new Map< + string, + { + resolve: (value: any) => void; + reject: (error: Error) => void; + } + >(); + + let messageHandler: ((event: MessageEvent) => void) | null = null; + let isCleanedUp = false; + + // Create cleanup function that can be called immediately + const cleanup = async (): Promise => { + if (isCleanedUp) { + return; + } + isCleanedUp = true; + + // Remove message handler if it was added + if (messageHandler) { + window.removeEventListener('message', messageHandler); + messageHandler = null; + } + + // Simulate abrupt tab closure - just remove the iframe without calling + // disconnect/close on the PowerSync client. This tests dead tab detection. + URL.revokeObjectURL(url); + if (iframe.parentNode) { + iframe.remove(); + } + }; + + // Create promise that resolves when powersync-ready is received + const ready = new Promise((resolve, reject) => { + messageHandler = async (event: MessageEvent) => { + if (isCleanedUp) { + return; } - >(); - const messageHandler = async (event: MessageEvent) => { const data = event.data; if (data?.type === 'powersync-ready' && data.identifier === identifier) { // Don't remove the message handler - we need it to receive query results! resolve({ iframe, - cleanup: async () => { - // Simulate abrupt tab closure - just remove the iframe without calling - // disconnect/close on the PowerSync client. This tests dead tab detection. - URL.revokeObjectURL(url); - if (iframe.parentNode) { - iframe.remove(); - } - }, + cleanup, executeQuery: (query: string, parameters?: unknown[]): Promise => { return new Promise((resolveQuery, rejectQuery) => { + if (isCleanedUp) { + rejectQuery(new Error('Iframe has been cleaned up')); + return; + } + const requestId = `query-${identifier}-${++requestIdCounter}`; pendingRequests.set(requestId, { resolve: resolveQuery, @@ -126,6 +159,7 @@ async function createIframeWithPowerSyncClient( const iframeWindow = iframe.contentWindow; if (!iframeWindow) { + pendingRequests.delete(requestId); rejectQuery(new Error('Iframe window not available')); return; } @@ -151,6 +185,11 @@ async function createIframeWithPowerSyncClient( }, getCredentialsFetchCount: (): Promise => { return new Promise((resolveCount, rejectCount) => { + if (isCleanedUp) { + rejectCount(new Error('Iframe has been cleaned up')); + return; + } + const requestId = `credentials-count-${identifier}-${++requestIdCounter}`; pendingRequests.set(requestId, { resolve: resolveCount, @@ -159,6 +198,7 @@ async function createIframeWithPowerSyncClient( const iframeWindow = iframe.contentWindow; if (!iframeWindow) { + pendingRequests.delete(requestId); rejectCount(new Error('Iframe window not available')); return; } @@ -182,7 +222,10 @@ async function createIframeWithPowerSyncClient( } }); } else if (data?.type === 'powersync-error' && data.identifier === identifier) { - window.removeEventListener('message', messageHandler); + if (messageHandler) { + window.removeEventListener('message', messageHandler); + messageHandler = null; + } URL.revokeObjectURL(url); if (iframe.parentNode) { iframe.remove(); @@ -212,6 +255,12 @@ async function createIframeWithPowerSyncClient( }; window.addEventListener('message', messageHandler); }); + + return { + iframe, + cleanup, + ready + }; } /** @@ -236,51 +285,33 @@ async function createIframeWithPowerSyncClient( * enableMultiTabs is true). * * Test Scenarios: - * - Opening a long-lived reference tab that remains open throughout the test - * - Opening multiple additional tabs simultaneously - * - Simultaneously closing half of the tabs (simulating abrupt tab closures) - * - Simultaneously reopening the closed tabs - * - Verifying that all tabs remain functional and the shared database connection - * is properly maintained across tab lifecycle events + * - Opening 100 tabs simultaneously + * - Waiting 1 second for all tabs to initialize + * - Simultaneously closing all tabs except the middle (50th) tab + * - Verifying that the remaining tab is still functional and the shared database + * connection is properly maintained after closing 99 tabs * * This test suite runs for both IndexedDB and OPFS VFS backends to ensure dead tab * detection works correctly across different storage mechanisms. */ function createMultipleTabsTest(vfs?: WASQLiteVFS) { const vfsName = vfs || 'IndexedDB'; - describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 20_000 }, () => { + describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 60_000 }, () => { const dbFilename = `test-multi-tab-${uuid()}.db`; - // Configurable number of tabs to create (excluding the long-lived tab) - const NUM_TABS = 20; - - it('should handle simultaneous close and reopen of tabs', async () => { - // Step 1: Open a long-lived reference tab that stays open throughout the test - const longLivedTab = await createIframeWithPowerSyncClient(dbFilename, 'long-lived-tab', vfs); - onTestFinished(async () => { - try { - await longLivedTab.cleanup(); - } catch (e) { - // Ignore cleanup errors - } - }); - - // Test query execution right after creating the long-lived tab - const initialQueryResult = await longLivedTab.executeQuery('SELECT 1 as value'); - expect(initialQueryResult).toBeDefined(); - expect(Array.isArray(initialQueryResult)).toBe(true); - expect(initialQueryResult.length).toBe(1); - expect((initialQueryResult[0] as { value: number }).value).toBe(1); + // Number of tabs to create + const NUM_TABS = 100; + // Index of the middle tab to keep (0-indexed, so 49 is the 50th tab) + const MIDDLE_TAB_INDEX = 49; - // Step 2: Open a configurable number of other tabs - const tabs: IframeClient[] = []; - const tabIdentifiers: string[] = []; + it('should handle opening and closing many tabs quickly', async () => { + // Step 1: Open 100 tabs (don't wait for them to be ready) + const tabResults: IframeClientResult[] = []; for (let i = 0; i < NUM_TABS; i++) { const identifier = `tab-${i}`; - tabIdentifiers.push(identifier); - const result = await createIframeWithPowerSyncClient(dbFilename, identifier, vfs); - tabs.push(result); + const result = createIframeWithPowerSyncClient(dbFilename, identifier, vfs); + tabResults.push(result); // Register cleanup for each tab onTestFinished(async () => { @@ -292,87 +323,43 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { }); } - expect(tabs.length).toBe(NUM_TABS); + expect(tabResults.length).toBe(NUM_TABS); - // Verify all tabs are connected - for (const tab of tabs) { - expect(tab.iframe.isConnected).toBe(true); + // Verify all iframes are created (they're created immediately) + for (const result of tabResults) { + expect(result.iframe.isConnected).toBe(true); } - expect(longLivedTab.iframe.isConnected).toBe(true); - - // Step 3: Simultaneously close the first and last quarters of the tabs (simulating abrupt closure) - const quarterCount = Math.floor(NUM_TABS / 4); - const firstQuarterEnd = quarterCount; - const lastQuarterStart = NUM_TABS - quarterCount; - // Close the first quarter and last quarter of tabs - const firstQuarter = tabs.slice(0, firstQuarterEnd); - const lastQuarter = tabs.slice(lastQuarterStart); - const tabsToClose = [...firstQuarter, ...lastQuarter]; + // Step 2: Wait 1 second + await new Promise((resolve) => setTimeout(resolve, 1000)); - // Keep the middle two quarters - const tabsToKeep = tabs.slice(firstQuarterEnd, lastQuarterStart); + // Step 3: Close all tabs except the middle (50th) tab + const tabsToClose: IframeClientResult[] = []; + for (let i = 0; i < NUM_TABS; i++) { + if (i !== MIDDLE_TAB_INDEX) { + tabsToClose.push(tabResults[i]); + } + } - // Close the first and last quarters of tabs simultaneously (without proper cleanup) - // Do this in reverse order to ensure the last connected tab is closed first. - const closePromises = tabsToClose.reverse().map((tab) => tab.cleanup()); + // Close all tabs except the middle one simultaneously (without waiting for ready) + const closePromises = tabsToClose.map((result) => result.cleanup()); await Promise.all(closePromises); // Verify closed tabs are removed - for (const tab of tabsToClose) { - expect(tab.iframe.isConnected).toBe(false); - expect(document.body.contains(tab.iframe)).toBe(false); - } - - // Verify remaining tabs and long-lived tab are still connected - for (const tab of tabsToKeep) { - expect(tab.iframe.isConnected).toBe(true); - } - expect(longLivedTab.iframe.isConnected).toBe(true); - - // Step 4: Reopen the closed tabs - const reopenedTabs: IframeClient[] = []; - // Get the identifiers for the closed tabs by finding their indices in the original tabs array - const closedTabIdentifiers = tabsToClose.map((closedTab) => { - const index = tabs.indexOf(closedTab); - return tabIdentifiers[index]; - }); - - const reopenPromises = closedTabIdentifiers.map(async (identifier) => { - const result = await createIframeWithPowerSyncClient(dbFilename, identifier, vfs); - reopenedTabs.push(result); - - // Register cleanup for reopened tabs - onTestFinished(async () => { - try { - await result.cleanup(); - } catch (e) { - // Ignore cleanup errors - } - }); - return result; - }); - - // Reopen all closed tabs simultaneously - await Promise.all(reopenPromises); - - // Verify all reopened tabs are connected - for (const tab of reopenedTabs) { - expect(tab.iframe.isConnected).toBe(true); - } - - // Verify tabs that were kept open are still connected - for (const tab of tabsToKeep) { - expect(tab.iframe.isConnected).toBe(true); + for (let i = 0; i < NUM_TABS; i++) { + if (i !== MIDDLE_TAB_INDEX) { + expect(tabResults[i].iframe.isConnected).toBe(false); + expect(document.body.contains(tabResults[i].iframe)).toBe(false); + } } - // Final verification: all tabs should be mounted - const allTabs = [...tabsToKeep, ...reopenedTabs]; - expect(allTabs.length).toBe(NUM_TABS); - expect(longLivedTab.iframe.isConnected).toBe(true); + // Verify the middle tab is still present + expect(tabResults[MIDDLE_TAB_INDEX].iframe.isConnected).toBe(true); + expect(document.body.contains(tabResults[MIDDLE_TAB_INDEX].iframe)).toBe(true); - // Step 5: Execute a test query in the long-lived tab to verify its DB is still functional - const queryResult = await longLivedTab.executeQuery('SELECT 1 as value'); + // Step 4: Wait for the middle tab to be ready, then execute a test query to verify its DB is still functional + const middleTabClient = await tabResults[MIDDLE_TAB_INDEX].ready; + const queryResult = await middleTabClient.executeQuery('SELECT 1 as value'); // Verify the query result expect(queryResult).toBeDefined(); @@ -380,28 +367,22 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { expect(queryResult.length).toBe(1); expect((queryResult[0] as { value: number }).value).toBe(1); - // Step 6: Create a new tab which should trigger a connect. The shared sync worker should reconnect. - // This ensures the shared sync worker is not stuck and is properly handling new connections + // Step 5: Create another tab, wait for it to be ready, and verify its credentialsFetchCount is 1 const newTabIdentifier = `new-tab-${Date.now()}`; - const newTab = await createIframeWithPowerSyncClient(dbFilename, newTabIdentifier, vfs, true); + const newTabResult = createIframeWithPowerSyncClient(dbFilename, newTabIdentifier, vfs, true); onTestFinished(async () => { try { - await newTab.cleanup(); + await newTabResult.cleanup(); } catch (e) { // Ignore cleanup errors } }); + const newTabClient = await newTabResult.ready; - // Wait for the new tab's credentials to be fetched (indicating the shared sync worker is active) - // The mocked remote always returns 401, so the shared sync worker should try and fetch credentials again. - await vi.waitFor(async () => { - const credentialsFetchCount = await newTab.getCredentialsFetchCount(); - expect( - credentialsFetchCount, - 'The new client should have been asked for credentials by the shared sync worker. ' + - 'This indicates the shared sync worker may be stuck or not processing new connections.' - ).toBeGreaterThanOrEqual(1); - }); + // Verify the new tab's credentials fetch count is 1 + // This means the shared worker is using the db and attempting to connect to the PowerSync server. + const credentialsFetchCount = await newTabClient.getCredentialsFetchCount(); + expect(credentialsFetchCount).toBe(1); }); }); } From 1e880d6b2999e7a66d276149aeebc5a03d2f2fe0 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 08:53:37 +0200 Subject: [PATCH 19/44] Add mocked sync tests for shared webworkers --- .../web/src/worker/sync/MockSyncService.ts | 3 + .../src/worker/sync/MockSyncServiceTypes.ts | 58 ++++ .../src/worker/sync/MockSyncServiceWorker.ts | 289 ++++++++++++++++++ .../web/tests/mockSyncServiceExample.test.ts | 110 +++++++ packages/web/tests/mocks/MockWebRemote.ts | 148 ++++++++- packages/web/tests/utils/MockSyncService.ts | 10 + .../web/tests/utils/MockSyncServiceClient.ts | 185 +++++++++++ .../web/tests/utils/mockSyncServiceTest.ts | 135 ++++++++ 8 files changed, 933 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/worker/sync/MockSyncService.ts create mode 100644 packages/web/src/worker/sync/MockSyncServiceTypes.ts create mode 100644 packages/web/src/worker/sync/MockSyncServiceWorker.ts create mode 100644 packages/web/tests/mockSyncServiceExample.test.ts create mode 100644 packages/web/tests/utils/MockSyncService.ts create mode 100644 packages/web/tests/utils/MockSyncServiceClient.ts create mode 100644 packages/web/tests/utils/mockSyncServiceTest.ts diff --git a/packages/web/src/worker/sync/MockSyncService.ts b/packages/web/src/worker/sync/MockSyncService.ts new file mode 100644 index 000000000..d6755b18e --- /dev/null +++ b/packages/web/src/worker/sync/MockSyncService.ts @@ -0,0 +1,3 @@ +// Re-export types and worker-side implementation +export * from './MockSyncServiceTypes'; +export * from './MockSyncServiceWorker'; diff --git a/packages/web/src/worker/sync/MockSyncServiceTypes.ts b/packages/web/src/worker/sync/MockSyncServiceTypes.ts new file mode 100644 index 000000000..707c501ac --- /dev/null +++ b/packages/web/src/worker/sync/MockSyncServiceTypes.ts @@ -0,0 +1,58 @@ +/** + * Representation of a pending request + */ +export interface PendingRequest { + id: string; + url: string; + method: string; + headers: Record; + body: any; +} + +/** + * Message types for communication via MessagePort + */ +export type MockSyncServiceMessage = + | { type: 'getPendingRequests'; requestId: string } + | { + type: 'createResponse'; + requestId: string; + pendingRequestId: string; + status: number; + headers: Record; + } + | { type: 'pushBodyData'; requestId: string; pendingRequestId: string; data: string | ArrayBuffer | Uint8Array } + | { type: 'completeResponse'; requestId: string; pendingRequestId: string }; + +export type MockSyncServiceResponse = + | { type: 'getPendingRequests'; requestId: string; requests: PendingRequest[] } + | { type: 'createResponse'; requestId: string; success: boolean } + | { type: 'pushBodyData'; requestId: string; success: boolean } + | { type: 'completeResponse'; requestId: string; success: boolean } + | { type: 'error'; requestId?: string; error: string }; + +/** + * Internal representation of a pending request with response promise + */ +export interface PendingRequestInternal { + id: string; + url: string; + method: string; + headers: Record; + body: any; + responsePromise: { + resolve: (response: Response) => void; + reject: (error: Error) => void; + }; + streamController?: ReadableStreamDefaultController; +} + +/** + * Internal representation of an active response + */ +export interface ActiveResponse { + id: string; + status: number; + headers: Record; + stream: ReadableStreamDefaultController; +} diff --git a/packages/web/src/worker/sync/MockSyncServiceWorker.ts b/packages/web/src/worker/sync/MockSyncServiceWorker.ts new file mode 100644 index 000000000..54ed208a5 --- /dev/null +++ b/packages/web/src/worker/sync/MockSyncServiceWorker.ts @@ -0,0 +1,289 @@ +import type { MockSyncServiceMessage, MockSyncServiceResponse } from './MockSyncServiceTypes'; +import { ActiveResponse, PendingRequest, PendingRequestInternal } from './MockSyncServiceTypes'; + +/** + * Mock sync service implementation for shared worker environments. + * This allows tests to mock sync responses when using enableMultipleTabs: true. + * Requests are kept pending until a client explicitly creates a response. + */ +export class MockSyncService { + private pendingRequests: Map = new Map(); + private activeResponses: Map = new Map(); + private nextId = 0; + + /** + * Register a new pending request (called by WebRemote when a sync stream is requested). + * Returns a promise that resolves when a client creates a response for this request. + */ + registerPendingRequest(url: string, method: string, headers: Record, body: any): Promise { + const id = `pending-${++this.nextId}`; + + let resolveResponse: (response: Response) => void; + let rejectResponse: (error: Error) => void; + + const responsePromise = new Promise((resolve, reject) => { + resolveResponse = resolve; + rejectResponse = reject; + }); + + const pendingRequest: PendingRequestInternal = { + id, + url, + method, + headers, + body, + responsePromise: { + resolve: resolveResponse!, + reject: rejectResponse! + } + }; + + this.pendingRequests.set(id, pendingRequest); + + // Return the promise - it will resolve when createResponse is called + return responsePromise; + } + + /** + * Get all pending requests + */ + getPendingRequestsSync(): PendingRequest[] { + return Array.from(this.pendingRequests.values()).map((pr) => ({ + id: pr.id, + url: pr.url, + method: pr.method, + headers: pr.headers, + body: pr.body + })); + } + + /** + * Create a response for a pending request. + * This resolves the response promise and allows pushing body lines. + */ + createResponse(pendingRequestId: string, status: number, headers: Record): void { + const pendingRequest = this.pendingRequests.get(pendingRequestId); + if (!pendingRequest) { + throw new Error(`Pending request ${pendingRequestId} not found`); + } + + // Create a readable stream that the mock service can control + // Response.body is always ReadableStream, so we use Uint8Array + const stream = new ReadableStream({ + start: (controller) => { + // Store the active response once the controller is available + // The start callback is called synchronously, so this is safe + const activeResponse: ActiveResponse = { + id: pendingRequestId, + status, + headers, + stream: controller + }; + this.activeResponses.set(pendingRequestId, activeResponse); + }, + cancel: () => { + // Remove response when stream is cancelled + this.activeResponses.delete(pendingRequestId); + this.pendingRequests.delete(pendingRequestId); + } + }); + + // Create the Response object + const response = new Response(stream, { + status, + headers + }); + + // Resolve the pending request's promise + pendingRequest.responsePromise.resolve(response); + + // Remove from pending (it's now active) + this.pendingRequests.delete(pendingRequestId); + } + + /** + * Push body data to an active response. + * Accepts either text (string) or binary data (ArrayBuffer or Uint8Array). + * All data is encoded to Uint8Array before enqueueing (required by ReadableStream). + */ + pushBodyData(pendingRequestId: string, data: string | ArrayBuffer | Uint8Array): void { + const activeResponse = this.activeResponses.get(pendingRequestId); + if (!activeResponse) { + throw new Error(`Active response ${pendingRequestId} not found`); + } + + try { + let encoded: Uint8Array; + + if (typeof data === 'string') { + // Encode string to Uint8Array (required by ReadableStream) + const encoder = new TextEncoder(); + encoded = encoder.encode(data); + } else if (data instanceof ArrayBuffer) { + // Convert ArrayBuffer to Uint8Array + encoded = new Uint8Array(data); + } else { + // Already Uint8Array, use directly + encoded = data; + } + + activeResponse.stream.enqueue(encoded); + } catch (e) { + // Stream might be closed, remove it + this.activeResponses.delete(pendingRequestId); + throw new Error(`Failed to push data to response ${pendingRequestId}: ${e}`); + } + } + + /** + * Complete an active response (close the stream) + */ + completeResponse(pendingRequestId: string): void { + const activeResponse = this.activeResponses.get(pendingRequestId); + if (!activeResponse) { + throw new Error(`Active response ${pendingRequestId} not found`); + } + + try { + activeResponse.stream.close(); + } catch (e) { + // Stream might already be closed + } finally { + this.activeResponses.delete(pendingRequestId); + } + } +} + +/** + * Global mock service instance (only available in shared worker context) + */ +let globalMockService: MockSyncService | null = null; + +/** + * Get or create the global mock service instance + */ +export function getMockSyncService(): MockSyncService | null { + // Only available in shared worker context + if (typeof SharedWorkerGlobalScope === 'undefined') { + return null; + } + + if (!globalMockService) { + globalMockService = new MockSyncService(); + } + + return globalMockService; +} + +/** + * Set up message handler for the mock service on a MessagePort + */ +export function setupMockServiceMessageHandler(port: MessagePort) { + const service = getMockSyncService(); + if (!service) { + return; + } + + port.addEventListener('message', (event: MessageEvent) => { + const message = event.data; + + if (!message || typeof message !== 'object' || !('type' in message)) { + return; + } + + try { + switch (message.type) { + case 'getPendingRequests': { + try { + const requests = service.getPendingRequestsSync(); + port.postMessage({ + type: 'getPendingRequests', + requestId: message.requestId, + requests + } satisfies MockSyncServiceResponse); + } catch (error) { + port.postMessage({ + type: 'error', + requestId: message.requestId, + error: error instanceof Error ? error.message : String(error) + } satisfies MockSyncServiceResponse); + } + break; + } + case 'createResponse': { + try { + service.createResponse(message.pendingRequestId, message.status, message.headers); + port.postMessage({ + type: 'createResponse', + requestId: message.requestId, + success: true + } satisfies MockSyncServiceResponse); + } catch (error) { + port.postMessage({ + type: 'error', + requestId: message.requestId, + error: error instanceof Error ? error.message : String(error) + } satisfies MockSyncServiceResponse); + } + break; + } + case 'pushBodyData': { + try { + service.pushBodyData(message.pendingRequestId, message.data); + port.postMessage({ + type: 'pushBodyData', + requestId: message.requestId, + success: true + } satisfies MockSyncServiceResponse); + } catch (error) { + port.postMessage({ + type: 'error', + requestId: message.requestId, + error: error instanceof Error ? error.message : String(error) + } satisfies MockSyncServiceResponse); + } + break; + } + case 'completeResponse': { + try { + service.completeResponse(message.pendingRequestId); + port.postMessage({ + type: 'completeResponse', + requestId: message.requestId, + success: true + } satisfies MockSyncServiceResponse); + } catch (error) { + port.postMessage({ + type: 'error', + requestId: message.requestId, + error: error instanceof Error ? error.message : String(error) + } satisfies MockSyncServiceResponse); + } + break; + } + default: { + const requestId = + 'requestId' in message && typeof message === 'object' && message !== null + ? (message as { requestId?: string }).requestId + : undefined; + port.postMessage({ + type: 'error', + requestId, + error: `Unknown message type: ${(message as any).type}` + } satisfies MockSyncServiceResponse); + break; + } + } + } catch (error) { + // Fallback for any unexpected errors + const requestId = 'requestId' in message ? message.requestId : undefined; + port.postMessage({ + type: 'error', + requestId, + error: error instanceof Error ? error.message : String(error) + } satisfies MockSyncServiceResponse); + } + }); + + port.start(); +} diff --git a/packages/web/tests/mockSyncServiceExample.test.ts b/packages/web/tests/mockSyncServiceExample.test.ts new file mode 100644 index 000000000..6f1b4c58a --- /dev/null +++ b/packages/web/tests/mockSyncServiceExample.test.ts @@ -0,0 +1,110 @@ +/** + * Example test demonstrating how to use the mock sync service for shared worker environments. + * + * This example shows how to: + * 1. Use the sharedMockSyncServiceTest utility to set up the test environment + * 2. Use the mock service to get pending requests and create responses + * 3. Send data via sync stream and query it in the database + * + * Note: This is an example file - rename to .test.ts to use it in actual tests. + */ + +import { StreamingSyncCheckpoint } from '@powersync/common'; +import { describe, expect, vi } from 'vitest'; +import { sharedMockSyncServiceTest } from './utils/mockSyncServiceTest'; + +describe('Mock Sync Service Example', { timeout: 100000 }, () => { + sharedMockSyncServiceTest( + 'should allow mocking sync responses in shared worker', + { timeout: 100000 }, + async ({ database, connect }) => { + // Call connect to start the sync worker and get the sync service + const { syncService, connectionPromise } = await connect(); + + // Wait for a pending request to appear + let pendingRequestId: string; + await vi.waitFor(async () => { + const requests = await syncService.getPendingRequests(); + expect(requests.length).toBeGreaterThan(0); + pendingRequestId = requests[0].id; + }); + + // Get the pending request to inspect it + const requests = await syncService.getPendingRequests(); + expect(requests).toHaveLength(1); + expect(requests[0].url).toContain('/sync/stream'); + expect(requests[0].method).toBe('POST'); + expect(requests[0].headers).toBeDefined(); + expect(requests[0].body).toBeDefined(); + + // Create a response for the pending request + await syncService.createResponse(pendingRequestId!, 200, { 'Content-Type': 'application/json' }); + + // Push a checkpoint with buckets (following node test pattern) + const checkpoint: StreamingSyncCheckpoint = { + checkpoint: { + last_op_id: '1', + buckets: [ + { + bucket: 'a', + count: 1, + checksum: 0, + priority: 3 + } + ], + write_checkpoint: undefined + } + }; + + await syncService.pushBodyLine(pendingRequestId!, checkpoint); + + // The connect call should resolve by now + await connectionPromise; + + // Push data line with PUT operation to save data (following node test pattern) + await syncService.pushBodyLine(pendingRequestId!, { + data: { + bucket: 'a', + data: [ + { + checksum: 0, + op_id: '1', + op: 'PUT', + object_id: '1', + object_type: 'lists', + data: '{"name": "from server"}' + } + ] + } + }); + + // Push checkpoint_complete to finish the sync + await syncService.pushBodyLine(pendingRequestId!, { + checkpoint_complete: { + last_op_id: '1' + } + }); + + // Complete the response + await syncService.completeResponse(pendingRequestId!); + + // Wait for sync to complete and verify the data was saved + await vi.waitFor(async () => { + const rows = await database.getAll('SELECT * FROM lists WHERE id = ?', ['1']); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + id: '1', + name: 'from server' + }); + }); + + // Verify the data by querying the database + const allRows = await database.getAll('SELECT * FROM lists'); + expect(allRows).toHaveLength(1); + expect(allRows[0]).toMatchObject({ + id: '1', + name: 'from server' + }); + } + ); +}); diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts index 0c3c655e2..68a6be8ad 100644 --- a/packages/web/tests/mocks/MockWebRemote.ts +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -2,17 +2,30 @@ import { AbstractRemote, AbstractRemoteOptions, BSONImplementation, + DataStream, DEFAULT_REMOTE_LOGGER, FetchImplementation, FetchImplementationProvider, ILogger, - RemoteConnector + RemoteConnector, + SocketSyncStreamOptions } from '@powersync/common'; +import type { BSON } from 'bson'; +import { getMockSyncService, setupMockServiceMessageHandler } from '../utils/MockSyncService'; /** - * Mock WebRemote that throws 401 Unauthorized errors for all HTTP requests. - * Used for testing error handling in the shared sync worker. - * Other tests may override this for managed streams. + * Check if we're running in a shared worker context + */ +function isSharedWorkerContext(): boolean { + const isSharedWorker = + typeof SharedWorkerGlobalScope !== 'undefined' && + typeof self !== 'undefined' && + (self as any).constructor?.name === 'SharedWorkerGlobalScope'; + return isSharedWorker; +} + +/** + * Mock fetch provider that returns 401 for non-stream requests */ class MockFetchProvider extends FetchImplementationProvider { getFetch(): FetchImplementation { @@ -27,6 +40,111 @@ class MockFetchProvider extends FetchImplementationProvider { } } +/** + * Mock fetch provider that intercepts all requests and routes them to the mock sync service. + * Used for testing in shared worker environments with enableMultipleTabs: true. + * + * When running in a shared worker context, this will: + * 1. Intercept all requests and register them as pending requests + * 2. Wait for a client to create a response before returning + * 3. Set up message handler for the mock service when onconnect is called + */ +class MockSyncServiceFetchProvider extends FetchImplementationProvider { + getFetch(): FetchImplementation { + return async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const request = new Request(input, init); + const mockService = getMockSyncService(); + + if (mockService) { + // Read the request body (if any) + let body: any = null; + try { + if (request.body) { + const clonedRequest = request.clone(); + body = await clonedRequest.json().catch(() => { + // If JSON parsing fails, try text + return clonedRequest.text().catch(() => null); + }); + } + } catch (e) { + // Body might not be readable, that's okay + } + + // Extract headers from the request + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; + }); + + // Register as a pending request and wait for client to create response + return await mockService.registerPendingRequest(request.url, request.method, headers, body); + } + + // Fallback if mock service is not available + return new Response(null, { + status: 401, + statusText: 'Unauthorized' + }); + }; + } +} + +// Track if we've already set up the onconnect handler +let onconnectHandlerSetup = false; + +/** + * Create the mock sync service globally upfront when in shared worker context. + * This ensures the service is available before any requests come in. + */ +function initializeMockSyncService() { + if (!isSharedWorkerContext()) { + return; + } + + // Create the mock service upfront (getMockSyncService creates it if it doesn't exist) + getMockSyncService(); +} + +/** + * Set up the onconnect listener lazily when the first WebRemote is created. + * This ensures deterministic initialization order regardless of import order. + * The mock service is created upfront, but message handlers are only set up on connect. + */ +function setupOnconnectHandlerIfNeeded() { + if (onconnectHandlerSetup || !isSharedWorkerContext()) { + return; + } + + // Create the mock sync service globally upfront + initializeMockSyncService(); + + const _self: SharedWorkerGlobalScope = self as any; + + // Store the original onconnect if it exists (may be set by SharedSyncImplementation.worker.ts) + const originalOnConnect = _self.onconnect; + + _self.onconnect = async function (event: MessageEvent) { + const port = event.ports[0]; + + // Get the mock service (already created upfront) and set up message handler lazily + const mockService = getMockSyncService(); + if (mockService) { + // Set up message handler for the mock service on this port + // Tests can create a separate SharedWorker connection to access this + setupMockServiceMessageHandler(port); + } + + // Call the original handler if it exists (this will set up WorkerClient) + // Note: The mock service message handler and WorkerClient can coexist + // since they use different message types + if (originalOnConnect) { + await originalOnConnect.call(this, event); + } + }; + + onconnectHandlerSetup = true; +} + export class WebRemote extends AbstractRemote { private _bson: BSONImplementation | undefined; @@ -35,9 +153,16 @@ export class WebRemote extends AbstractRemote { protected logger: ILogger = DEFAULT_REMOTE_LOGGER, options?: Partial ) { + // Lazy initialize the onconnect handler when the first WebRemote is created + // This ensures deterministic initialization order regardless of import order + setupOnconnectHandlerIfNeeded(); + + // Use mock service fetch provider if we're in a shared worker context + const fetchProvider = isSharedWorkerContext() ? new MockSyncServiceFetchProvider() : new MockFetchProvider(); + super(connector, logger, { ...(options ?? {}), - fetchImplementation: options?.fetchImplementation ?? new MockFetchProvider() + fetchImplementation: options?.fetchImplementation ?? fetchProvider }); } @@ -53,4 +178,17 @@ export class WebRemote extends AbstractRemote { this._bson = BSON; return this._bson; } + + /** + * Override socketStreamRaw to use HTTP method (postStreamRaw) instead. + * This allows us to use the same mocks for both socket and HTTP streaming. + */ + async socketStreamRaw( + options: SocketSyncStreamOptions, + map: (buffer: Uint8Array) => T, + bson?: typeof BSON + ): Promise> { + // postStreamRaw decodes to strings, so convert back to Uint8Array for the map function + return await this.postStreamRaw(options, (line: string) => map(new TextEncoder().encode(line))); + } } diff --git a/packages/web/tests/utils/MockSyncService.ts b/packages/web/tests/utils/MockSyncService.ts new file mode 100644 index 000000000..95a5859e8 --- /dev/null +++ b/packages/web/tests/utils/MockSyncService.ts @@ -0,0 +1,10 @@ +// Re-export types and worker-side implementation for backward compatibility +export * from '../../src/worker/sync/MockSyncServiceTypes'; +export { + MockSyncService, + getMockSyncService, + setupMockServiceMessageHandler +} from '../../src/worker/sync/MockSyncServiceWorker'; + +// Re-export client-side utilities +export * from './MockSyncServiceClient'; diff --git a/packages/web/tests/utils/MockSyncServiceClient.ts b/packages/web/tests/utils/MockSyncServiceClient.ts new file mode 100644 index 000000000..923ac6120 --- /dev/null +++ b/packages/web/tests/utils/MockSyncServiceClient.ts @@ -0,0 +1,185 @@ +import type { + MockSyncServiceMessage, + MockSyncServiceResponse, + PendingRequest +} from '../../src/worker/sync/MockSyncServiceTypes'; + +/** + * Interface for mocking sync service responses in shared worker environments. + * Similar to MockSyncService in the node SDK package. + */ +export interface MockSyncService { + /** + * Get all pending requests (requests waiting for a response to be created) + */ + getPendingRequests(): Promise; + + /** + * Create a response for a pending request with the specified status and headers. + * This resolves the pending request and allows pushing body data. + */ + createResponse(pendingRequestId: string, status: number, headers: Record): Promise; + + /** + * Push body data to an active response. + * Accepts either text (string) or binary data (ArrayBuffer or Uint8Array). + * Strings are encoded to Uint8Array before sending. + * The response must have been created first using createResponse. + */ + pushBodyData(pendingRequestId: string, data: string | ArrayBuffer | Uint8Array): Promise; + + /** + * Push a streaming sync line as NDJSON to an active response. + * This is a convenience method that encodes the line as JSON with a newline. + * The response must have been created first using createResponse. + */ + pushBodyLine(pendingRequestId: string, line: any): Promise; + + /** + * Complete an active response (close the stream). + * The response must have been created first using createResponse. + */ + completeResponse(pendingRequestId: string): Promise; +} + +/** + * Connect to the shared worker and get access to the mock sync service. + * This function creates a separate SharedWorker connection to the same shared sync worker + * just to access the mock service, without interfering with the normal sync implementation. + * + * @param identifier - The database identifier (used to construct the worker name) + * @param workerUrl - Optional custom worker URL. If not provided, uses the default shared sync worker. + * @returns The mock sync service interface, or null if not available + */ +export async function getMockSyncServiceFromWorker( + identifier: string, + workerUrl?: string | URL +): Promise { + // Create a separate SharedWorker connection to the same shared sync worker + // This connection is only used to access the mock service + // Note the URL and identifier should match in order for the correct worker to be used + const worker = workerUrl + ? new SharedWorker(typeof workerUrl === 'string' ? workerUrl : workerUrl.href, { + name: `shared-sync-${identifier}` + }) + : new SharedWorker(new URL('../../lib/src/worker/sync/SharedSyncImplementation.worker.js', import.meta.url), { + /* @vite-ignore */ + name: `shared-sync-${identifier}`, + type: 'module' + }); + + const port = worker.port; + port.start(); + + // Generic helper to send a message and wait for a response + const sendMessage = ( + message: MockSyncServiceMessage, + expectedType: T['type'], + timeout = 5000, + transfer?: Transferable[] + ): Promise => { + return new Promise((resolve, reject) => { + const requestId = 'requestId' in message ? message.requestId : undefined; + + const handler = (event: MessageEvent) => { + const response = event.data; + + if (response.type === expectedType && response.requestId === requestId) { + port.removeEventListener('message', handler); + if ('success' in response && !response.success) { + reject(new Error('Operation failed')); + } else { + resolve(response as T); + } + } else if (response.type === 'error' && response.requestId === requestId) { + port.removeEventListener('message', handler); + reject(new Error(response.error)); + } + }; + + port.addEventListener('message', handler); + if (transfer && transfer.length > 0) { + port.postMessage(message, transfer); + } else { + port.postMessage(message); + } + + // Timeout + setTimeout(() => { + port.removeEventListener('message', handler); + reject(new Error(`Timeout waiting for ${expectedType} response`)); + }, timeout); + }); + }; + + // Define pushBodyData first so it can be used by pushBodyLine + const pushBodyData = async (pendingRequestId: string, data: string | ArrayBuffer | Uint8Array): Promise => { + const requestId = crypto.randomUUID(); + + // Handle transferable objects for ArrayBuffer + const transfer: Transferable[] = []; + if (data instanceof ArrayBuffer) { + transfer.push(data); + } else if (data instanceof Uint8Array && data.buffer instanceof ArrayBuffer) { + transfer.push(data.buffer); + } + // Strings are passed as-is, no transfer needed + + await sendMessage( + { + type: 'pushBodyData', + requestId, + pendingRequestId, + data + } satisfies MockSyncServiceMessage, + 'pushBodyData', + 5000, + transfer.length > 0 ? transfer : undefined + ); + }; + + return { + async getPendingRequests(): Promise { + const requestId = crypto.randomUUID(); + const response = await sendMessage<{ type: 'getPendingRequests'; requestId: string; requests: PendingRequest[] }>( + { type: 'getPendingRequests', requestId } satisfies MockSyncServiceMessage, + 'getPendingRequests' + ); + return response.requests; + }, + + async createResponse(pendingRequestId: string, status: number, headers: Record): Promise { + const requestId = crypto.randomUUID(); + await sendMessage( + { + type: 'createResponse', + requestId, + pendingRequestId, + status, + headers + } satisfies MockSyncServiceMessage, + 'createResponse' + ); + }, + + pushBodyData, + + async pushBodyLine(pendingRequestId: string, line: any): Promise { + // Encode as NDJSON: JSON.stringify + newline + const lineStr = `${JSON.stringify(line)}\n`; + await pushBodyData(pendingRequestId, lineStr); + }, + + async completeResponse(pendingRequestId: string): Promise { + const requestId = crypto.randomUUID(); + await sendMessage( + { + type: 'completeResponse', + requestId, + pendingRequestId + } satisfies MockSyncServiceMessage, + 'completeResponse' + ); + } + }; +} diff --git a/packages/web/tests/utils/mockSyncServiceTest.ts b/packages/web/tests/utils/mockSyncServiceTest.ts new file mode 100644 index 000000000..047ac65cc --- /dev/null +++ b/packages/web/tests/utils/mockSyncServiceTest.ts @@ -0,0 +1,135 @@ +import { + LogLevel, + PowerSyncBackendConnector, + PowerSyncCredentials, + Schema, + SyncStreamConnectionMethod, + Table, + column, + createBaseLogger +} from '@powersync/common'; +import { PowerSyncDatabase } from '@powersync/web'; +import { expect, onTestFinished, test, vi } from 'vitest'; +import { MockSyncService, getMockSyncServiceFromWorker } from './MockSyncServiceClient'; + +// Define schema similar to node tests +const lists = new Table({ + name: column.text +}); + +export const AppSchema = new Schema({ + lists +}); + +/** + * Creates a test connector with vi.fn implementations for testing. + */ +export function createTestConnector(): PowerSyncBackendConnector { + return { + fetchCredentials: vi.fn().mockResolvedValue({ + endpoint: 'http://localhost:3000', + token: 'test-token' + } as PowerSyncCredentials), + uploadData: vi.fn().mockResolvedValue(undefined) + }; +} + +/** + * Result of calling the connect function + */ +export interface ConnectResult { + syncService: MockSyncService; + connectionPromise: Promise; +} + +/** + * Vitest test extension for mocking sync service in shared worker environments. + * Similar to mockSyncServiceTest in the node SDK package. + * + * This extension: + * - Sets up a PowerSync database with the lists schema + * - Exposes a connect function that calls powersync.connect(), waits for connecting: true, + * creates the sync service, and returns both + * - Exposes the database and test connector + */ +export const sharedMockSyncServiceTest = test.extend<{ + database: PowerSyncDatabase; + connector: PowerSyncBackendConnector; + connect: (customConnector?: PowerSyncBackendConnector) => Promise; +}>({ + database: async ({}, use) => { + // Uses a unique database identifier per test to avoid conflicts + const identifier = `test-${crypto.randomUUID()}.db`; + + // Create a logger with defaults enabled + const logger = createBaseLogger(); + logger.setLevel(LogLevel.DEBUG); + logger.useDefaults(); + + // Create a PowerSync database with enableMultipleTabs: true + const db = new PowerSyncDatabase({ + database: { + dbFilename: identifier + }, + flags: { + enableMultiTabs: true + }, + schema: AppSchema, + logger + }); + + await db.init(); + + onTestFinished(async () => { + if (!db.closed) { + await db.disconnect(); + await db.close(); + } + }); + + await use(db); + }, + + connector: async ({}, use) => { + const connector = createTestConnector(); + await use(connector); + }, + + connect: async ({ database, connector }, use) => { + const connectFn = async (customConnector?: PowerSyncBackendConnector): Promise => { + const connectorToUse = customConnector ?? connector; + + // Call powersync.connect() to start the sync worker + const connectionPromise = database.connect(connectorToUse, { + connectionMethod: SyncStreamConnectionMethod.HTTP + }); + + // Wait for the database to report connecting: true before using the sync service + await vi.waitFor( + () => { + expect(database.connecting).toBe(true); + }, + { timeout: 10000 } + ); + + // Get the identifier from the database.name property + const identifier = database.database.name; + + // Connect to the shared worker to get the mock service + const mockService = await getMockSyncServiceFromWorker(identifier); + + if (!mockService) { + throw new Error( + 'Mock service not available - ensure enableMultiTabs is true and running in a test environment' + ); + } + + return { + syncService: mockService, + connectionPromise + }; + }; + + await use(connectFn); + } +}); From da533969f8a898539e9276138a25ef938eab3263 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 08:53:48 +0200 Subject: [PATCH 20/44] Update tests for shared web workers --- packages/web/src/db/PowerSyncDatabase.ts | 28 +-- .../SharedWebStreamingSyncImplementation.ts | 5 +- .../src/worker/sync/MockSyncServiceWorker.ts | 22 +- .../web/tests/mockSyncServiceExample.test.ts | 34 +-- packages/web/tests/mocks/MockWebRemote.ts | 6 +- packages/web/tests/multiple_instances.test.ts | 227 +++++------------- .../web/tests/utils/MockSyncServiceClient.ts | 3 +- .../web/tests/utils/mockSyncServiceTest.ts | 105 +++++--- 8 files changed, 172 insertions(+), 258 deletions(-) diff --git a/packages/web/src/db/PowerSyncDatabase.ts b/packages/web/src/db/PowerSyncDatabase.ts index 0e60de018..fe88ac71d 100644 --- a/packages/web/src/db/PowerSyncDatabase.ts +++ b/packages/web/src/db/PowerSyncDatabase.ts @@ -1,32 +1,31 @@ import { - type BucketStorageAdapter, - type PowerSyncBackendConnector, - type PowerSyncCloseOptions, - type RequiredAdditionalConnectionOptions, AbstractPowerSyncDatabase, DBAdapter, - DEFAULT_POWERSYNC_CLOSE_OPTIONS, - isDBAdapter, - isSQLOpenFactory, PowerSyncDatabaseOptions, PowerSyncDatabaseOptionsWithDBAdapter, PowerSyncDatabaseOptionsWithOpenFactory, PowerSyncDatabaseOptionsWithSettings, SqliteBucketStorage, - StreamingSyncImplementation + StreamingSyncImplementation, + isDBAdapter, + isSQLOpenFactory, + type BucketStorageAdapter, + type PowerSyncBackendConnector, + type PowerSyncCloseOptions, + type RequiredAdditionalConnectionOptions } from '@powersync/common'; import { Mutex } from 'async-mutex'; import { getNavigatorLocks } from '../shared/navigator'; +import { WebDBAdapter } from './adapters/WebDBAdapter'; import { WASQLiteOpenFactory } from './adapters/wa-sqlite/WASQLiteOpenFactory'; import { DEFAULT_WEB_SQL_FLAGS, ResolvedWebSQLOpenOptions, - resolveWebSQLFlags, - WebSQLFlags + WebSQLFlags, + resolveWebSQLFlags } from './adapters/web-sql-flags'; -import { WebDBAdapter } from './adapters/WebDBAdapter'; -import { SharedWebStreamingSyncImplementation } from './sync/SharedWebStreamingSyncImplementation'; import { SSRStreamingSyncImplementation } from './sync/SSRWebStreamingSyncImplementation'; +import { SharedWebStreamingSyncImplementation } from './sync/SharedWebStreamingSyncImplementation'; import { WebRemote } from './sync/WebRemote'; import { WebStreamingSyncImplementation, @@ -160,14 +159,13 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase { * By default the sync stream client is only disconnected if * multiple tabs are not enabled. */ - close(options: PowerSyncCloseOptions = DEFAULT_POWERSYNC_CLOSE_OPTIONS): Promise { + close(options?: PowerSyncCloseOptions): Promise { if (this.unloadListener) { window.removeEventListener('unload', this.unloadListener); } - return super.close({ // Don't disconnect by default if multiple tabs are enabled - disconnect: options.disconnect ?? !this.resolvedFlags.enableMultiTabs + disconnect: options?.disconnect ?? !this.resolvedFlags.enableMultiTabs }); } diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 9e4d1e315..218c71019 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -255,8 +255,6 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem async dispose(): Promise { await this.waitForReady(); - await super.dispose(); - await new Promise((resolve) => { // Listen for the close acknowledgment from the worker this.messagePort.addEventListener('message', (event) => { @@ -273,6 +271,9 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem }; this.messagePort.postMessage(closeMessagePayload); }); + + await super.dispose(); + this.abortOnClose.abort(); // Release the proxy diff --git a/packages/web/src/worker/sync/MockSyncServiceWorker.ts b/packages/web/src/worker/sync/MockSyncServiceWorker.ts index 54ed208a5..08a81a68e 100644 --- a/packages/web/src/worker/sync/MockSyncServiceWorker.ts +++ b/packages/web/src/worker/sync/MockSyncServiceWorker.ts @@ -15,7 +15,13 @@ export class MockSyncService { * Register a new pending request (called by WebRemote when a sync stream is requested). * Returns a promise that resolves when a client creates a response for this request. */ - registerPendingRequest(url: string, method: string, headers: Record, body: any): Promise { + registerPendingRequest( + url: string, + method: string, + headers: Record, + body: any, + signal?: AbortSignal + ): Promise { const id = `pending-${++this.nextId}`; let resolveResponse: (response: Response) => void; @@ -40,6 +46,20 @@ export class MockSyncService { this.pendingRequests.set(id, pendingRequest); + signal?.addEventListener('abort', () => { + this.pendingRequests.delete(id); + rejectResponse(new Error('Request aborted')); + + // if already in active responses, remove it + if (this.activeResponses.has(id)) { + const response = this.activeResponses.get(id); + if (response) { + response.stream.close(); + } + this.activeResponses.delete(id); + } + }); + // Return the promise - it will resolve when createResponse is called return responsePromise; } diff --git a/packages/web/tests/mockSyncServiceExample.test.ts b/packages/web/tests/mockSyncServiceExample.test.ts index 6f1b4c58a..b4d8edb47 100644 --- a/packages/web/tests/mockSyncServiceExample.test.ts +++ b/packages/web/tests/mockSyncServiceExample.test.ts @@ -17,28 +17,9 @@ describe('Mock Sync Service Example', { timeout: 100000 }, () => { sharedMockSyncServiceTest( 'should allow mocking sync responses in shared worker', { timeout: 100000 }, - async ({ database, connect }) => { + async ({ context: { database, connect } }) => { // Call connect to start the sync worker and get the sync service - const { syncService, connectionPromise } = await connect(); - - // Wait for a pending request to appear - let pendingRequestId: string; - await vi.waitFor(async () => { - const requests = await syncService.getPendingRequests(); - expect(requests.length).toBeGreaterThan(0); - pendingRequestId = requests[0].id; - }); - - // Get the pending request to inspect it - const requests = await syncService.getPendingRequests(); - expect(requests).toHaveLength(1); - expect(requests[0].url).toContain('/sync/stream'); - expect(requests[0].method).toBe('POST'); - expect(requests[0].headers).toBeDefined(); - expect(requests[0].body).toBeDefined(); - - // Create a response for the pending request - await syncService.createResponse(pendingRequestId!, 200, { 'Content-Type': 'application/json' }); + const { syncService, syncRequestId } = await connect(); // Push a checkpoint with buckets (following node test pattern) const checkpoint: StreamingSyncCheckpoint = { @@ -56,13 +37,10 @@ describe('Mock Sync Service Example', { timeout: 100000 }, () => { } }; - await syncService.pushBodyLine(pendingRequestId!, checkpoint); + await syncService.pushBodyLine(syncRequestId, checkpoint); // The connect call should resolve by now - await connectionPromise; - - // Push data line with PUT operation to save data (following node test pattern) - await syncService.pushBodyLine(pendingRequestId!, { + await syncService.pushBodyLine(syncRequestId, { data: { bucket: 'a', data: [ @@ -79,14 +57,14 @@ describe('Mock Sync Service Example', { timeout: 100000 }, () => { }); // Push checkpoint_complete to finish the sync - await syncService.pushBodyLine(pendingRequestId!, { + await syncService.pushBodyLine(syncRequestId, { checkpoint_complete: { last_op_id: '1' } }); // Complete the response - await syncService.completeResponse(pendingRequestId!); + await syncService.completeResponse(syncRequestId); // Wait for sync to complete and verify the data was saved await vi.waitFor(async () => { diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts index 68a6be8ad..02a03d6bb 100644 --- a/packages/web/tests/mocks/MockWebRemote.ts +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -10,7 +10,7 @@ import { RemoteConnector, SocketSyncStreamOptions } from '@powersync/common'; -import type { BSON } from 'bson'; +import { serialize, type BSON } from 'bson'; import { getMockSyncService, setupMockServiceMessageHandler } from '../utils/MockSyncService'; /** @@ -77,7 +77,7 @@ class MockSyncServiceFetchProvider extends FetchImplementationProvider { }); // Register as a pending request and wait for client to create response - return await mockService.registerPendingRequest(request.url, request.method, headers, body); + return await mockService.registerPendingRequest(request.url, request.method, headers, body, request.signal); } // Fallback if mock service is not available @@ -189,6 +189,6 @@ export class WebRemote extends AbstractRemote { bson?: typeof BSON ): Promise> { // postStreamRaw decodes to strings, so convert back to Uint8Array for the map function - return await this.postStreamRaw(options, (line: string) => map(new TextEncoder().encode(line))); + return await this.postStreamRaw(options, (line: string) => map(serialize(JSON.parse(line)))); } } diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 4411498a5..fc0d90162 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -1,24 +1,11 @@ -import { - AbstractPowerSyncDatabase, - createBaseLogger, - createLogger, - DEFAULT_CRUD_UPLOAD_THROTTLE_MS, - SqliteBucketStorage, - SyncStatus -} from '@powersync/common'; -import { - OpenAsyncDatabaseConnection, - SharedWebStreamingSyncImplementation, - SharedWebStreamingSyncImplementationOptions, - WASqliteConnection, - WebRemote -} from '@powersync/web'; +import { AbstractPowerSyncDatabase, createBaseLogger, createLogger } from '@powersync/common'; +import { OpenAsyncDatabaseConnection, WASqliteConnection } from '@powersync/web'; import * as Comlink from 'comlink'; import { beforeAll, describe, expect, it, onTestFinished, vi } from 'vitest'; import { LockedAsyncDatabaseAdapter } from '../src/db/adapters/LockedAsyncDatabaseAdapter'; import { WebDBAdapter } from '../src/db/adapters/WebDBAdapter'; import { WorkerWrappedAsyncDatabaseConnection } from '../src/db/adapters/WorkerWrappedAsyncDatabaseConnection'; -import { TestConnector } from './utils/MockStreamOpenFactory'; +import { createTestConnector, sharedMockSyncServiceTest } from './utils/mockSyncServiceTest'; import { generateTestDb, testSchema } from './utils/testDb'; const DB_FILENAME = 'test-multiple-instances.db'; @@ -104,7 +91,7 @@ describe('Multiple Instances', { sequential: true }, () => { await createAsset(powersync2); }); - it('should handled interrupted transactions', { timeout: Infinity }, async () => { + it('should handled interrupted transactions', async () => { //Create a shared PowerSync database. We'll just use this for internally managing connections. const powersync = openDatabase(); await powersync.init(); @@ -223,171 +210,75 @@ describe('Multiple Instances', { sequential: true }, () => { await watchedPromise; }); - it('should share sync updates', async () => { - // Generate the first streaming sync implementation - const connector1 = new TestConnector(); - const db = openDatabase(); - await db.init(); - - // They need to use the same identifier to use the same shared worker. - const identifier = 'streaming-sync-shared'; - const syncOptions1: SharedWebStreamingSyncImplementationOptions = { - adapter: new SqliteBucketStorage(db.database), - remote: new WebRemote(connector1), - uploadCrud: async () => { - await connector1.uploadData(db); - }, - identifier, - crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS, - retryDelayMs: 90_000, // Large delay to allow for testing - db: db.database as WebDBAdapter, - subscriptions: [] - }; - - const stream1 = new SharedWebStreamingSyncImplementation(syncOptions1); - await stream1.connect(); - // Generate the second streaming sync implementation - const connector2 = new TestConnector(); - const syncOptions2: SharedWebStreamingSyncImplementationOptions = { - adapter: new SqliteBucketStorage(db.database), - remote: new WebRemote(connector1), - uploadCrud: async () => { - await connector2.uploadData(db); - }, - identifier, - crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS, - retryDelayMs: 90_000, // Large delay to allow for testing - db: db.database as WebDBAdapter, - subscriptions: [] - }; - - const stream2 = new SharedWebStreamingSyncImplementation(syncOptions2); - - const stream2UpdatedPromise = new Promise((resolve, reject) => { - const l = stream2.registerListener({ - statusChanged: (status) => { - if (status.connected) { - resolve(); - l(); - } - } - }); - }); - - // hack to set the status to a new one for tests - (stream1 as any)['_testUpdateStatus'](new SyncStatus({ connected: true })); + sharedMockSyncServiceTest('should share sync updates', async ({ context: { database, connect, openDatabase } }) => { + const secondDatabase = openDatabase(); - await stream2UpdatedPromise; - expect(stream2.isConnected).true; + expect(database.currentStatus.connected).false; + expect(secondDatabase.currentStatus.connected).false; + // connect the first database + await connect(); - await stream1.dispose(); - await stream2.dispose(); + expect(database.currentStatus.connected).true; + expect(secondDatabase.currentStatus.connected).true; }); - it('should trigger uploads from last connected clients', { timeout: Infinity }, async () => { - // Generate the first streaming sync implementation - const connector1 = new TestConnector(); - const spy1 = vi.spyOn(connector1, 'uploadData'); - - await new Promise((resolve) => setTimeout(resolve, 5000)); - const db = openDatabase(); - await db.init(); - // They need to use the same identifier to use the same shared worker. - const identifier = db.database.name; - - // Resolves once the first connector has been called to upload data - let triggerUpload1: () => void; - const upload1TriggeredPromise = new Promise((resolve) => { - triggerUpload1 = resolve; - }); - - const sharedSyncOptions = { - adapter: new SqliteBucketStorage(db.database), - remote: new WebRemote(connector1), - db: db.database as WebDBAdapter, - identifier, - // The large delay here allows us to test between connection retries - retryDelayMs: 90_000, - crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS, - subscriptions: [], - flags: { - broadcastLogs: true - } - }; - - // Create the first streaming client - const stream1 = new SharedWebStreamingSyncImplementation({ - ...sharedSyncOptions, - uploadCrud: async () => { - triggerUpload1(); - connector1.uploadData(db); - } - }); - - // Generate the second streaming sync implementation - const connector2 = new TestConnector(); - // The second connector will be called first to upload, we don't want it to actually upload - // This will cause the sync uploads to be delayed as the CRUD queue did not change - const spy2 = vi.spyOn(connector2, 'uploadData').mockImplementation(async () => {}); - - let triggerUpload2: () => void; - const upload2TriggeredPromise = new Promise((resolve) => { - triggerUpload2 = resolve; - }); + sharedMockSyncServiceTest( + 'should trigger uploads from last connected clients', + { timeout: Infinity }, + async ({ context: { database, openDatabase, connect, connector } }) => { + const secondDatabase = openDatabase(); - const stream2 = new SharedWebStreamingSyncImplementation({ - ...sharedSyncOptions, - uploadCrud: async () => { - triggerUpload2(); - connector2.uploadData(db); - } - }); + expect(database.currentStatus.connected).false; + expect(secondDatabase.currentStatus.connected).false; - // Waits for the stream to be marked as connected - const stream2UpdatedPromise = new Promise((resolve, reject) => { - const l = stream2.registerListener({ - statusChanged: (status) => { - if (status.connected) { - resolve(); - l(); - } - } + // Don't actually upload data + connector.uploadData.mockImplementation(async (db) => { + console.log('uploading from first client'); }); - }); - // hack to set the status to connected for tests - await stream1.connect(); - // Hack, set the status to connected in order to trigger the upload - await (stream1 as any)['_testUpdateStatus'](new SyncStatus({ connected: true })); + // Create something with CRUD in it. + await database.execute('INSERT into lists (id, name) VALUES (uuid(), ?)', ['steven']); - // The status in the second stream client should be updated - await stream2UpdatedPromise; + // connect from the first database + const { syncService } = await connect(); - expect(stream2.isConnected).true; + await vi.waitFor(() => expect(database.currentStatus.connected).true); - // Create something with CRUD in it. - await db.execute('INSERT into customers (id, name, email) VALUES (uuid(), ?, ?)', [ - 'steven', - 'steven@journeyapps.com' - ]); + // It should initially try and upload from the first client + await vi.waitFor(() => expect(connector.uploadData).toHaveBeenCalledOnce(), { timeout: 2000 }); - stream1.triggerCrudUpload(); - // The second connector should be called to upload - await upload2TriggeredPromise; + const secondConnector = createTestConnector(); + // Don't actually upload data + secondConnector.uploadData.mockImplementation(async (db) => { + console.log('uploading from second client'); + }); - // It should call the latest connected client - expect(spy2).toHaveBeenCalledOnce(); + // Connect the second database and wait for a pending request to appear + const secondConnectPromise = secondDatabase.connect(secondConnector); + let _pendingRequestId: string; + await vi.waitFor(async () => { + const requests = await syncService.getPendingRequests(); + expect(requests.length).toBeGreaterThan(0); + _pendingRequestId = requests[0].id; + }); + const pendingRequestId = _pendingRequestId!; + await syncService.createResponse(pendingRequestId, 200, { 'Content-Type': 'application/json' }); + await syncService.pushBodyLine(pendingRequestId, { + token_expires_in: 10000000 + }); + await secondConnectPromise; - // Close the second client, leaving only the first one - await stream2.dispose(); + // It should now upload from the second client + await vi.waitFor(() => expect(secondConnector.uploadData).toHaveBeenCalledOnce()); + await new Promise((resolve) => setTimeout(resolve, 5000)); + debugger; + // Now disconnect and close the second client + await secondDatabase.close(); - // Hack, set the status to connected in order to trigger the upload - await (stream1 as any)['_testUpdateStatus'](new SyncStatus({ connected: true })); - stream1.triggerCrudUpload(); - // It should now upload from the first client - await upload1TriggeredPromise; + expect(database.currentStatus.connected).true; - expect(spy1).toHaveBeenCalledOnce(); - await stream1.dispose(); - }); + // It should now upload from the first client + await vi.waitFor(() => expect(connector.uploadData.mock.calls.length).greaterThanOrEqual(2), { timeout: 3000 }); + } + ); }); diff --git a/packages/web/tests/utils/MockSyncServiceClient.ts b/packages/web/tests/utils/MockSyncServiceClient.ts index 923ac6120..ad8c17bf4 100644 --- a/packages/web/tests/utils/MockSyncServiceClient.ts +++ b/packages/web/tests/utils/MockSyncServiceClient.ts @@ -1,3 +1,4 @@ +import { StreamingSyncLine } from '@powersync/common'; import type { MockSyncServiceMessage, MockSyncServiceResponse, @@ -33,7 +34,7 @@ export interface MockSyncService { * This is a convenience method that encodes the line as JSON with a newline. * The response must have been created first using createResponse. */ - pushBodyLine(pendingRequestId: string, line: any): Promise; + pushBodyLine(pendingRequestId: string, line: StreamingSyncLine): Promise; /** * Complete an active response (close the stream). diff --git a/packages/web/tests/utils/mockSyncServiceTest.ts b/packages/web/tests/utils/mockSyncServiceTest.ts index 047ac65cc..48ecdb4c5 100644 --- a/packages/web/tests/utils/mockSyncServiceTest.ts +++ b/packages/web/tests/utils/mockSyncServiceTest.ts @@ -9,7 +9,7 @@ import { createBaseLogger } from '@powersync/common'; import { PowerSyncDatabase } from '@powersync/web'; -import { expect, onTestFinished, test, vi } from 'vitest'; +import { MockedFunction, expect, onTestFinished, test, vi } from 'vitest'; import { MockSyncService, getMockSyncServiceFromWorker } from './MockSyncServiceClient'; // Define schema similar to node tests @@ -21,10 +21,13 @@ export const AppSchema = new Schema({ lists }); +export type MockedTestConnector = { + [Key in keyof PowerSyncBackendConnector]: MockedFunction; +}; /** * Creates a test connector with vi.fn implementations for testing. */ -export function createTestConnector(): PowerSyncBackendConnector { +export function createTestConnector(): MockedTestConnector { return { fetchCredentials: vi.fn().mockResolvedValue({ endpoint: 'http://localhost:3000', @@ -39,7 +42,7 @@ export function createTestConnector(): PowerSyncBackendConnector { */ export interface ConnectResult { syncService: MockSyncService; - connectionPromise: Promise; + syncRequestId: string; } /** @@ -53,49 +56,47 @@ export interface ConnectResult { * - Exposes the database and test connector */ export const sharedMockSyncServiceTest = test.extend<{ - database: PowerSyncDatabase; - connector: PowerSyncBackendConnector; - connect: (customConnector?: PowerSyncBackendConnector) => Promise; + context: { + /** An automatically opened database */ + connector: MockedTestConnector; + connect: (customConnector?: PowerSyncBackendConnector) => Promise; + database: PowerSyncDatabase; + databaseName: string; + openDatabase: () => PowerSyncDatabase; + }; }>({ - database: async ({}, use) => { - // Uses a unique database identifier per test to avoid conflicts - const identifier = `test-${crypto.randomUUID()}.db`; - - // Create a logger with defaults enabled + context: async ({}, use) => { + const dbFilename = `test-${crypto.randomUUID()}.db`; const logger = createBaseLogger(); logger.setLevel(LogLevel.DEBUG); logger.useDefaults(); - // Create a PowerSync database with enableMultipleTabs: true - const db = new PowerSyncDatabase({ - database: { - dbFilename: identifier - }, - flags: { - enableMultiTabs: true - }, - schema: AppSchema, - logger - }); - - await db.init(); - - onTestFinished(async () => { - if (!db.closed) { - await db.disconnect(); - await db.close(); - } - }); + const openDatabase = () => { + const db = new PowerSyncDatabase({ + database: { + dbFilename + }, + flags: { + enableMultiTabs: true + }, + retryDelayMs: 1000, + crudUploadThrottleMs: 1000, + schema: AppSchema, + logger + }); + onTestFinished(async () => { + if (!db.closed) { + await db.disconnect(); + await db.close(); + } + }); + return db; + }; - await use(db); - }, + const database = openDatabase(); - connector: async ({}, use) => { const connector = createTestConnector(); - await use(connector); - }, - connect: async ({ database, connector }, use) => { const connectFn = async (customConnector?: PowerSyncBackendConnector): Promise => { const connectorToUse = customConnector ?? connector; @@ -109,7 +110,7 @@ export const sharedMockSyncServiceTest = test.extend<{ () => { expect(database.connecting).toBe(true); }, - { timeout: 10000 } + { timeout: 1000 } ); // Get the identifier from the database.name property @@ -124,12 +125,36 @@ export const sharedMockSyncServiceTest = test.extend<{ ); } + let _syncRequestId: string; + await vi.waitFor(async () => { + const requests = await mockService.getPendingRequests(); + expect(requests.length).toBeGreaterThan(0); + _syncRequestId = requests[0].id; + }); + + const syncRequestId = _syncRequestId!; + + await mockService.createResponse(syncRequestId, 200, { 'Content-Type': 'application/json' }); + + // Send a Keepalive just as the first message + await mockService.pushBodyLine(syncRequestId, { + token_expires_in: 10_000_000 + }); + + await connectionPromise; + return { syncService: mockService, - connectionPromise + syncRequestId }; }; - await use(connectFn); + await use({ + connector, + connect: connectFn, + database, + databaseName: dbFilename, + openDatabase + }); } }); From 28472e3d6ad98ac5bc081632c61e26a2b3a5b93c Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 09:22:47 +0200 Subject: [PATCH 21/44] Add automatic responses or mocked sync service. --- .../src/worker/sync/MockSyncServiceTypes.ts | 15 ++- .../src/worker/sync/MockSyncServiceWorker.ts | 118 +++++++++++++++++- .../web/tests/multiple_tabs_iframe.test.ts | 49 +++++--- .../web/tests/utils/MockSyncServiceClient.ts | 42 +++++++ packages/web/tests/utils/iframeInitializer.ts | 27 +++- 5 files changed, 232 insertions(+), 19 deletions(-) diff --git a/packages/web/src/worker/sync/MockSyncServiceTypes.ts b/packages/web/src/worker/sync/MockSyncServiceTypes.ts index 707c501ac..8f0d61d79 100644 --- a/packages/web/src/worker/sync/MockSyncServiceTypes.ts +++ b/packages/web/src/worker/sync/MockSyncServiceTypes.ts @@ -9,6 +9,15 @@ export interface PendingRequest { body: any; } +/** + * Automatic response configuration + */ +export interface AutomaticResponseConfig { + status: number; + headers: Record; + bodyLines?: any[]; +} + /** * Message types for communication via MessagePort */ @@ -22,13 +31,17 @@ export type MockSyncServiceMessage = headers: Record; } | { type: 'pushBodyData'; requestId: string; pendingRequestId: string; data: string | ArrayBuffer | Uint8Array } - | { type: 'completeResponse'; requestId: string; pendingRequestId: string }; + | { type: 'completeResponse'; requestId: string; pendingRequestId: string } + | { type: 'setAutomaticResponse'; requestId: string; config: AutomaticResponseConfig | null } + | { type: 'replyToAllPendingRequests'; requestId: string }; export type MockSyncServiceResponse = | { type: 'getPendingRequests'; requestId: string; requests: PendingRequest[] } | { type: 'createResponse'; requestId: string; success: boolean } | { type: 'pushBodyData'; requestId: string; success: boolean } | { type: 'completeResponse'; requestId: string; success: boolean } + | { type: 'setAutomaticResponse'; requestId: string; success: boolean } + | { type: 'replyToAllPendingRequests'; requestId: string; success: boolean; count: number } | { type: 'error'; requestId?: string; error: string }; /** diff --git a/packages/web/src/worker/sync/MockSyncServiceWorker.ts b/packages/web/src/worker/sync/MockSyncServiceWorker.ts index 08a81a68e..2ff5c2d05 100644 --- a/packages/web/src/worker/sync/MockSyncServiceWorker.ts +++ b/packages/web/src/worker/sync/MockSyncServiceWorker.ts @@ -1,5 +1,10 @@ import type { MockSyncServiceMessage, MockSyncServiceResponse } from './MockSyncServiceTypes'; -import { ActiveResponse, PendingRequest, PendingRequestInternal } from './MockSyncServiceTypes'; +import { + ActiveResponse, + AutomaticResponseConfig, + PendingRequest, + PendingRequestInternal +} from './MockSyncServiceTypes'; /** * Mock sync service implementation for shared worker environments. @@ -10,6 +15,7 @@ export class MockSyncService { private pendingRequests: Map = new Map(); private activeResponses: Map = new Map(); private nextId = 0; + private automaticResponse: AutomaticResponseConfig | null = null; /** * Register a new pending request (called by WebRemote when a sync stream is requested). @@ -60,7 +66,34 @@ export class MockSyncService { } }); - // Return the promise - it will resolve when createResponse is called + // If automatic response is configured, apply it immediately + if (this.automaticResponse) { + // Use setTimeout to ensure the response is created asynchronously + // This prevents issues if the response creation happens synchronously + setTimeout(() => { + try { + // Create response with automatic config + this.createResponse(id, this.automaticResponse!.status, this.automaticResponse!.headers); + + // Push body lines if provided + if (this.automaticResponse!.bodyLines) { + for (const line of this.automaticResponse!.bodyLines) { + const lineStr = `${JSON.stringify(line)}\n`; + const encoder = new TextEncoder(); + this.pushBodyData(id, encoder.encode(lineStr)); + } + } + + // Complete the response + this.completeResponse(id); + } catch (e) { + // If automatic response fails, reject the promise + rejectResponse!(e instanceof Error ? e : new Error(String(e))); + } + }, 0); + } + + // Return the promise - it will resolve when createResponse is called (or immediately if auto-response is set) return responsePromise; } @@ -172,6 +205,52 @@ export class MockSyncService { this.activeResponses.delete(pendingRequestId); } } + + /** + * Set the automatic response configuration. + * When set, this will be used to automatically reply to all pending requests. + */ + setAutomaticResponse(config: AutomaticResponseConfig | null): void { + this.automaticResponse = config; + } + + /** + * Automatically reply to all pending requests using the automatic response configuration. + * Returns the number of requests that were replied to. + */ + replyToAllPendingRequests(): number { + if (!this.automaticResponse) { + throw new Error('Automatic response not set. Call setAutomaticResponse first.'); + } + + const pendingRequestIds = Array.from(this.pendingRequests.keys()); + let count = 0; + + for (const requestId of pendingRequestIds) { + try { + // Create response with automatic config + this.createResponse(requestId, this.automaticResponse.status, this.automaticResponse.headers); + + // Push body lines if provided + if (this.automaticResponse.bodyLines) { + for (const line of this.automaticResponse.bodyLines) { + const lineStr = `${JSON.stringify(line)}\n`; + const encoder = new TextEncoder(); + this.pushBodyData(requestId, encoder.encode(lineStr)); + } + } + + // Complete the response + this.completeResponse(requestId); + count++; + } catch (e) { + // Skip requests that fail (might already be handled) + continue; + } + } + + return count; + } } /** @@ -281,6 +360,41 @@ export function setupMockServiceMessageHandler(port: MessagePort) { } break; } + case 'setAutomaticResponse': { + try { + service.setAutomaticResponse(message.config); + port.postMessage({ + type: 'setAutomaticResponse', + requestId: message.requestId, + success: true + } satisfies MockSyncServiceResponse); + } catch (error) { + port.postMessage({ + type: 'error', + requestId: message.requestId, + error: error instanceof Error ? error.message : String(error) + } satisfies MockSyncServiceResponse); + } + break; + } + case 'replyToAllPendingRequests': { + try { + const count = service.replyToAllPendingRequests(); + port.postMessage({ + type: 'replyToAllPendingRequests', + requestId: message.requestId, + success: true, + count + } satisfies MockSyncServiceResponse); + } catch (error) { + port.postMessage({ + type: 'error', + requestId: message.requestId, + error: error instanceof Error ? error.message : String(error) + } satisfies MockSyncServiceResponse); + } + break; + } default: { const requestId = 'requestId' in message && typeof message === 'object' && message !== null diff --git a/packages/web/tests/multiple_tabs_iframe.test.ts b/packages/web/tests/multiple_tabs_iframe.test.ts index df6159a29..038425932 100644 --- a/packages/web/tests/multiple_tabs_iframe.test.ts +++ b/packages/web/tests/multiple_tabs_iframe.test.ts @@ -30,7 +30,8 @@ function createIframeWithPowerSyncClient( dbFilename: string, identifier: string, vfs?: WASQLiteVFS, - waitForConnection?: boolean + waitForConnection?: boolean, + configureMockResponses?: boolean ): IframeClientResult { const iframe = document.createElement('iframe'); // Make iframe visible for debugging @@ -88,7 +89,7 @@ function createIframeWithPowerSyncClient( `; @@ -296,7 +297,7 @@ function createIframeWithPowerSyncClient( */ function createMultipleTabsTest(vfs?: WASQLiteVFS) { const vfsName = vfs || 'IndexedDB'; - describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 60_000 }, () => { + describe(`Multiple Tabs with Iframes (${vfsName})`, { sequential: true, timeout: 30_000 }, () => { const dbFilename = `test-multi-tab-${uuid()}.db`; // Number of tabs to create @@ -305,8 +306,20 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { const MIDDLE_TAB_INDEX = 49; it('should handle opening and closing many tabs quickly', async () => { + // Step 0: Create an iframe to set up PowerSync and configure mock responses (401) + const setupIdentifier = `setup-${uuid()}`; + const setupIframe = createIframeWithPowerSyncClient(dbFilename, setupIdentifier, vfs, false, true); + onTestFinished(async () => { + try { + await setupIframe.cleanup(); + } catch (e) { + // Ignore cleanup errors + } + }); + // Wait for the setup iframe to be ready (this ensures PowerSync is initialized and mock responses are configured) + await setupIframe.ready; // Step 1: Open 100 tabs (don't wait for them to be ready) - const tabResults: IframeClientResult[] = []; + const tabResults: IframeClientResult[] = [setupIframe]; for (let i = 0; i < NUM_TABS; i++) { const identifier = `tab-${i}`; @@ -323,7 +336,8 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { }); } - expect(tabResults.length).toBe(NUM_TABS); + // Total iframes: 1 setup + NUM_TABS tabs + expect(tabResults.length).toBe(NUM_TABS + 1); // Verify all iframes are created (they're created immediately) for (const result of tabResults) { @@ -333,32 +347,37 @@ function createMultipleTabsTest(vfs?: WASQLiteVFS) { // Step 2: Wait 1 second await new Promise((resolve) => setTimeout(resolve, 1000)); - // Step 3: Close all tabs except the middle (50th) tab + // Step 3: Close all tabs except the setup iframe (index 0) and the middle (50th) tab + // The middle tab is at index 1 + MIDDLE_TAB_INDEX (since index 0 is the setup iframe) + const middleTabArrayIndex = 1 + MIDDLE_TAB_INDEX; const tabsToClose: IframeClientResult[] = []; - for (let i = 0; i < NUM_TABS; i++) { - if (i !== MIDDLE_TAB_INDEX) { + for (let i = 0; i < tabResults.length; i++) { + // Skip the setup iframe (index 0) and the middle tab + if (i !== 0 && i !== middleTabArrayIndex) { tabsToClose.push(tabResults[i]); } } - // Close all tabs except the middle one simultaneously (without waiting for ready) + // Close all tabs except the setup iframe and middle one simultaneously (without waiting for ready) const closePromises = tabsToClose.map((result) => result.cleanup()); await Promise.all(closePromises); // Verify closed tabs are removed - for (let i = 0; i < NUM_TABS; i++) { - if (i !== MIDDLE_TAB_INDEX) { + for (let i = 0; i < tabResults.length; i++) { + if (i !== 0 && i !== middleTabArrayIndex) { expect(tabResults[i].iframe.isConnected).toBe(false); expect(document.body.contains(tabResults[i].iframe)).toBe(false); } } - // Verify the middle tab is still present - expect(tabResults[MIDDLE_TAB_INDEX].iframe.isConnected).toBe(true); - expect(document.body.contains(tabResults[MIDDLE_TAB_INDEX].iframe)).toBe(true); + // Verify the setup iframe and middle tab are still present + expect(tabResults[0].iframe.isConnected).toBe(true); + expect(document.body.contains(tabResults[0].iframe)).toBe(true); + expect(tabResults[middleTabArrayIndex].iframe.isConnected).toBe(true); + expect(document.body.contains(tabResults[middleTabArrayIndex].iframe)).toBe(true); // Step 4: Wait for the middle tab to be ready, then execute a test query to verify its DB is still functional - const middleTabClient = await tabResults[MIDDLE_TAB_INDEX].ready; + const middleTabClient = await tabResults[middleTabArrayIndex].ready; const queryResult = await middleTabClient.executeQuery('SELECT 1 as value'); // Verify the query result diff --git a/packages/web/tests/utils/MockSyncServiceClient.ts b/packages/web/tests/utils/MockSyncServiceClient.ts index ad8c17bf4..74804b857 100644 --- a/packages/web/tests/utils/MockSyncServiceClient.ts +++ b/packages/web/tests/utils/MockSyncServiceClient.ts @@ -1,5 +1,6 @@ import { StreamingSyncLine } from '@powersync/common'; import type { + AutomaticResponseConfig, MockSyncServiceMessage, MockSyncServiceResponse, PendingRequest @@ -41,6 +42,18 @@ export interface MockSyncService { * The response must have been created first using createResponse. */ completeResponse(pendingRequestId: string): Promise; + + /** + * Set the automatic response configuration. + * When set, this will be used to automatically reply to all pending requests. + */ + setAutomaticResponse(config: AutomaticResponseConfig | null): Promise; + + /** + * Automatically reply to all pending requests using the automatic response configuration. + * Returns the number of requests that were replied to. + */ + replyToAllPendingRequests(): Promise; } /** @@ -181,6 +194,35 @@ export async function getMockSyncServiceFromWorker( } satisfies MockSyncServiceMessage, 'completeResponse' ); + }, + + async setAutomaticResponse(config: AutomaticResponseConfig | null): Promise { + const requestId = crypto.randomUUID(); + await sendMessage( + { + type: 'setAutomaticResponse', + requestId, + config + } satisfies MockSyncServiceMessage, + 'setAutomaticResponse' + ); + }, + + async replyToAllPendingRequests(): Promise { + const requestId = crypto.randomUUID(); + const response = await sendMessage<{ + type: 'replyToAllPendingRequests'; + requestId: string; + success: boolean; + count: number; + }>( + { + type: 'replyToAllPendingRequests', + requestId + } satisfies MockSyncServiceMessage, + 'replyToAllPendingRequests' + ); + return response.count; } }; } diff --git a/packages/web/tests/utils/iframeInitializer.ts b/packages/web/tests/utils/iframeInitializer.ts index 3f9e7b91e..4c1c8da11 100644 --- a/packages/web/tests/utils/iframeInitializer.ts +++ b/packages/web/tests/utils/iframeInitializer.ts @@ -1,5 +1,6 @@ import { LogLevel, Schema, SyncStreamConnectionMethod, TableV2, column, createBaseLogger } from '@powersync/common'; import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web'; +import { getMockSyncServiceFromWorker } from './MockSyncServiceClient'; /** * Initializes a PowerSync client in the current iframe context and notifies the parent. @@ -11,7 +12,8 @@ export async function setupPowerSyncInIframe( dbFilename: string, identifier: string, vfs?: string, - waitForConnection?: boolean + waitForConnection?: boolean, + configureMockResponses?: boolean ): Promise { try { // Track the number of times fetchCredentials has been called @@ -50,6 +52,7 @@ export async function setupPowerSyncInIframe( const db = new PowerSyncDatabase({ database: databaseOptions, schema: schema, + retryDelayMs: 100, flags: { enableMultiTabs: true, useWebWorker: true }, logger }); @@ -61,6 +64,28 @@ export async function setupPowerSyncInIframe( await connectionPromise; } + if (configureMockResponses) { + // Wait for connecting:true before setting up mock responses + const maxAttempts = 100; + const delayMs = 50; + for (let i = 0; i < maxAttempts; i++) { + if (db.currentStatus.connecting) { + break; + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + + const mockSyncService = await getMockSyncServiceFromWorker(dbFilename); + if (mockSyncService) { + await mockSyncService.setAutomaticResponse({ + // We want to confirm credentials are fetched due to invalidation. + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + await mockSyncService.replyToAllPendingRequests(); + } + } + // Store reference for cleanup (window as any).powersyncClient = db; From 11b7a36ee04cc8b592ed43b12753561e8c85b3fb Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 09:45:27 +0200 Subject: [PATCH 22/44] Update more tests to use mocked sync service --- packages/web/tests/multiple_instances.test.ts | 95 +++++++++++-------- .../web/tests/utils/mockSyncServiceTest.ts | 15 +-- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index fc0d90162..a40375ae0 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -5,6 +5,7 @@ import { beforeAll, describe, expect, it, onTestFinished, vi } from 'vitest'; import { LockedAsyncDatabaseAdapter } from '../src/db/adapters/LockedAsyncDatabaseAdapter'; import { WebDBAdapter } from '../src/db/adapters/WebDBAdapter'; import { WorkerWrappedAsyncDatabaseConnection } from '../src/db/adapters/WorkerWrappedAsyncDatabaseConnection'; +import { getMockSyncServiceFromWorker } from './utils/MockSyncService'; import { createTestConnector, sharedMockSyncServiceTest } from './utils/mockSyncServiceTest'; import { generateTestDb, testSchema } from './utils/testDb'; @@ -37,45 +38,58 @@ describe('Multiple Instances', { sequential: true }, () => { expect(assets.length).equals(1); }); - it('should broadcast logs from shared sync worker', { timeout: 20000 }, async () => { - const logger = createLogger('test-logger'); - const spiedErrorLogger = vi.spyOn(logger, 'error'); - const spiedDebugLogger = vi.spyOn(logger, 'debug'); + sharedMockSyncServiceTest( + 'should broadcast logs from shared sync worker', + { timeout: 10_000 }, + async ({ context: { database, connect, openDatabase } }) => { + const logger = createLogger('test-logger'); + const spiedErrorLogger = vi.spyOn(logger, 'error'); + const spiedDebugLogger = vi.spyOn(logger, 'debug'); + + // Open an additional database which we can spy on the logs. + const powersync = openDatabase({ + logger + }); - const powersync = generateTestDb({ - logger, - database: { - dbFilename: 'broadcast-logger-test.sqlite' - }, - schema: testSchema - }); + powersync.connect({ + fetchCredentials: async () => { + return { + endpoint: 'http://localhost/does-not-exist', + token: 'none' + }; + }, + uploadData: async (db) => {} + }); - powersync.connect({ - fetchCredentials: async () => { - return { - endpoint: 'http://localhost/does-not-exist', - token: 'none' - }; - }, - uploadData: async (db) => {} - }); + // Should log that a connection attempt has been made + const message = 'Streaming sync iteration started'; + await vi.waitFor( + () => + expect( + spiedDebugLogger.mock.calls + .flat(1) + .find((argument) => typeof argument == 'string' && argument.includes(message)) + ).exist, + { timeout: 2000 } + ); - // Should log that a connection attempt has been made - const message = 'Streaming sync iteration started'; - await vi.waitFor( - () => - expect( - spiedDebugLogger.mock.calls - .flat(1) - .find((argument) => typeof argument == 'string' && argument.includes(message)) - ).exist, - { timeout: 2000 } - ); - - // The connection should fail with an error - await vi.waitFor(() => expect(spiedErrorLogger.mock.calls.length).gt(0), { timeout: 2000 }); - // This test seems to take quite long while waiting for this disconnect call - }); + await vi.waitFor(async () => { + const syncService = await getMockSyncServiceFromWorker(powersync.database.name); + if (!syncService) { + throw new Error('Sync service not found'); + } + const requests = await syncService.getPendingRequests(); + expect(requests.length).toBeGreaterThan(0); + const pendingRequestId = requests[0].id; + // Generate an error + await syncService.createResponse(pendingRequestId, 401, { 'Content-Type': 'application/json' }); + await syncService.completeResponse(pendingRequestId); + }); + + // The connection should fail with an error + await vi.waitFor(() => expect(spiedErrorLogger.mock.calls.length).gt(0), { timeout: 2000 }); + } + ); it('should maintain DB connections if instances call close', async () => { /** @@ -215,11 +229,14 @@ describe('Multiple Instances', { sequential: true }, () => { expect(database.currentStatus.connected).false; expect(secondDatabase.currentStatus.connected).false; - // connect the first database - await connect(); + // connect the second database in order for it to have access to the sync service. + secondDatabase.connect(createTestConnector()); + // connect the first database - this will actually connect to the sync service. + const { syncService } = await connect(); expect(database.currentStatus.connected).true; - expect(secondDatabase.currentStatus.connected).true; + + await vi.waitFor(() => expect(secondDatabase.currentStatus.connected).true); }); sharedMockSyncServiceTest( diff --git a/packages/web/tests/utils/mockSyncServiceTest.ts b/packages/web/tests/utils/mockSyncServiceTest.ts index 48ecdb4c5..6e55163af 100644 --- a/packages/web/tests/utils/mockSyncServiceTest.ts +++ b/packages/web/tests/utils/mockSyncServiceTest.ts @@ -8,7 +8,7 @@ import { column, createBaseLogger } from '@powersync/common'; -import { PowerSyncDatabase } from '@powersync/web'; +import { PowerSyncDatabase, WebPowerSyncDatabaseOptions } from '@powersync/web'; import { MockedFunction, expect, onTestFinished, test, vi } from 'vitest'; import { MockSyncService, getMockSyncServiceFromWorker } from './MockSyncServiceClient'; @@ -62,7 +62,7 @@ export const sharedMockSyncServiceTest = test.extend<{ connect: (customConnector?: PowerSyncBackendConnector) => Promise; database: PowerSyncDatabase; databaseName: string; - openDatabase: () => PowerSyncDatabase; + openDatabase: (customConfig?: Partial) => PowerSyncDatabase; }; }>({ context: async ({}, use) => { @@ -71,18 +71,21 @@ export const sharedMockSyncServiceTest = test.extend<{ logger.setLevel(LogLevel.DEBUG); logger.useDefaults(); - const openDatabase = () => { + const openDatabase = (customConfig: Partial = {}) => { const db = new PowerSyncDatabase({ database: { - dbFilename + dbFilename, + ...(customConfig.database ?? {}) }, flags: { - enableMultiTabs: true + enableMultiTabs: true, + ...(customConfig.flags ?? {}) }, retryDelayMs: 1000, crudUploadThrottleMs: 1000, schema: AppSchema, - logger + logger, + ...customConfig }); onTestFinished(async () => { if (!db.closed) { From 7e7ec91a56d76e98976f824b2323877cd9afb219 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 10:28:26 +0200 Subject: [PATCH 23/44] increase timeout for ci --- packages/web/tests/multiple_instances.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index a40375ae0..33d8452cf 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -232,11 +232,11 @@ describe('Multiple Instances', { sequential: true }, () => { // connect the second database in order for it to have access to the sync service. secondDatabase.connect(createTestConnector()); // connect the first database - this will actually connect to the sync service. - const { syncService } = await connect(); + await connect(); expect(database.currentStatus.connected).true; - await vi.waitFor(() => expect(secondDatabase.currentStatus.connected).true); + await vi.waitFor(() => expect(secondDatabase.currentStatus.connected).true, { timeout: 3000 }); }); sharedMockSyncServiceTest( From 1e9fa3f7961317fa4cc9d96b45895fe4eeb6aaaf Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 11:06:17 +0200 Subject: [PATCH 24/44] Improve test stability --- packages/web/tests/multiple_instances.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 33d8452cf..4edf86cc0 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -231,6 +231,8 @@ describe('Multiple Instances', { sequential: true }, () => { expect(secondDatabase.currentStatus.connected).false; // connect the second database in order for it to have access to the sync service. secondDatabase.connect(createTestConnector()); + // Timing of this can be tricky due to the need for responding to a pending request. + await vi.waitFor(() => expect(secondDatabase.currentStatus.connecting).true); // connect the first database - this will actually connect to the sync service. await connect(); @@ -241,7 +243,6 @@ describe('Multiple Instances', { sequential: true }, () => { sharedMockSyncServiceTest( 'should trigger uploads from last connected clients', - { timeout: Infinity }, async ({ context: { database, openDatabase, connect, connector } }) => { const secondDatabase = openDatabase(); @@ -288,7 +289,6 @@ describe('Multiple Instances', { sequential: true }, () => { // It should now upload from the second client await vi.waitFor(() => expect(secondConnector.uploadData).toHaveBeenCalledOnce()); await new Promise((resolve) => setTimeout(resolve, 5000)); - debugger; // Now disconnect and close the second client await secondDatabase.close(); From 07231cb429a1af8c6fb99ad98057653aa0faa14b Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 11:28:57 +0200 Subject: [PATCH 25/44] cleanup port assignments and init flow --- .../SharedWebStreamingSyncImplementation.ts | 11 +- .../worker/sync/SharedSyncImplementation.ts | 111 ++++++++---------- .../sync/SharedSyncImplementation.worker.ts | 2 +- packages/web/src/worker/sync/WorkerClient.ts | 34 +----- 4 files changed, 55 insertions(+), 103 deletions(-) diff --git a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts index 218c71019..fe2bb3ad2 100644 --- a/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts +++ b/packages/web/src/db/sync/SharedWebStreamingSyncImplementation.ts @@ -2,7 +2,6 @@ import { PowerSyncConnectionOptions, PowerSyncCredentials, SubscribedStream, - SyncStatus, SyncStatusOptions } from '@powersync/common'; import * as Comlink from 'comlink'; @@ -187,6 +186,8 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem * - The shared worker can then request the same lock. The client has been closed if the shared worker can acquire the lock. * - Once the shared worker knows the client's lock, we can guarentee that the shared worker will detect if the client has been closed. * - This makes the client safe for the shared worker to use. + * - The client is only added to the SharedSyncImplementation once the lock has been registered. + * This ensures we don't ever keep track of dead clients (tabs that closed before the lock was registered). * - The client side lock is held until the client is disposed. * - We resolve the top-level promise after the lock has been registered with the shared worker. * - The client sends the params to the shared worker after locks have been registered. @@ -288,12 +289,4 @@ export class SharedWebStreamingSyncImplementation extends WebStreamingSyncImplem updateSubscriptions(subscriptions: SubscribedStream[]): void { this.syncManager.updateSubscriptions(subscriptions); } - - /** - * Used in tests to force a connection states - */ - private async _testUpdateStatus(status: SyncStatus) { - await this.isInitialized; - return this.syncManager._testUpdateAllStatuses(status.toJSON()); - } } diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index e218b75bb..b01438bc3 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -76,10 +76,6 @@ export type WrappedSyncPort = { db?: DBAdapter; currentSubscriptions: SubscribedStream[]; closeListeners: (() => void | Promise)[]; - /** - * If we can use Navigator locks to detect if the client has closed. - */ - isProtectedFromClose: boolean; isClosing: boolean; }; @@ -176,14 +172,16 @@ export class SharedSyncImplementation extends BaseObserver= 0; i--) { - if (this.ports[i].isProtectedFromClose && !this.ports[i].isClosing) { - return this.ports[i]; + protected async getLastWrappedPort(): Promise { + // Find the last port which is not closing + return await this.portMutex.runExclusive(() => { + for (let i = this.ports.length - 1; i >= 0; i--) { + if (!this.ports[i].isClosing) { + return this.ports[i]; + } } - } - return; + return; + }); } async waitForStatus(status: SyncStatusOptions): Promise { @@ -232,44 +230,45 @@ export class SharedSyncImplementation extends BaseObserver { this.collectActiveSubscriptions(); - if (this.syncParams) { - // Cannot modify already existing sync implementation params - return; - } - - // First time setting params - this.syncParams = params; - if (params.streamOptions?.flags?.broadcastLogs) { - this.logger = this.broadCastLogger; - } + }); - const lockedAdapter = new LockedAsyncDatabaseAdapter({ - name: params.dbParams.dbFilename, - openConnection: async () => { - // Gets a connection from the clients when a new connection is requested. - return await this.openInternalDB(); - }, - logger: this.logger, - reOpenOnConnectionClosed: true - }); - this.distributedDB = lockedAdapter; - await lockedAdapter.init(); - - lockedAdapter.registerListener({ - databaseReOpened: () => { - // We may have missed some table updates while the database was closed. - // We can poke the crud in case we missed any updates. - this.connectionManager.syncStreamImplementation?.triggerCrudUpload(); - } - }); + if (this.syncParams) { + // Cannot modify already existing sync implementation params + return; + } - self.onerror = (event) => { - // Share any uncaught events on the broadcast logger - this.logger.error('Uncaught exception in PowerSync shared sync worker', event); - }; + // First time setting params + this.syncParams = params; + if (params.streamOptions?.flags?.broadcastLogs) { + this.logger = this.broadCastLogger; + } - this.iterateListeners((l) => l.initialized?.()); + const lockedAdapter = new LockedAsyncDatabaseAdapter({ + name: params.dbParams.dbFilename, + openConnection: async () => { + // Gets a connection from the clients when a new connection is requested. + return await this.openInternalDB(); + }, + logger: this.logger, + reOpenOnConnectionClosed: true + }); + this.distributedDB = lockedAdapter; + await lockedAdapter.init(); + + lockedAdapter.registerListener({ + databaseReOpened: () => { + // We may have missed some table updates while the database was closed. + // We can poke the crud in case we missed any updates. + this.connectionManager.syncStreamImplementation?.triggerCrudUpload(); + } }); + + self.onerror = (event) => { + // Share any uncaught events on the broadcast logger + this.logger.error('Uncaught exception in PowerSync shared sync worker', event); + }; + + this.iterateListeners((l) => l.initialized?.()); } async dispose() { @@ -303,7 +302,6 @@ export class SharedSyncImplementation extends BaseObserver(port), currentSubscriptions: [], closeListeners: [], - isProtectedFromClose: false, isClosing: false } satisfies WrappedSyncPort; this.ports.push(portProvider); @@ -409,7 +407,7 @@ export class SharedSyncImplementation extends BaseObserver { - const lastPort = this.lastWrappedPort; + const lastPort = await this.getLastWrappedPort(); if (!lastPort) { throw new Error('No client port found to invalidate credentials'); } @@ -421,7 +419,7 @@ export class SharedSyncImplementation extends BaseObserver { - const lastPort = this.lastWrappedPort; + const lastPort = await this.getLastWrappedPort(); if (!lastPort) { throw new Error('No client port found to fetch credentials'); } @@ -447,7 +445,7 @@ export class SharedSyncImplementation extends BaseObserver { - const lastPort = this.lastWrappedPort; + const lastPort = await this.getLastWrappedPort(); if (!lastPort) { throw new Error('No client port found to upload crud'); } @@ -483,7 +481,7 @@ export class SharedSyncImplementation extends BaseObserver p.clientProvider.statusChanged(status)); } - - /** - * A function only used for unit tests which updates the internal - * sync stream client and all tab client's sync status - */ - async _testUpdateAllStatuses(status: SyncStatusOptions) { - if (!this.connectionManager.syncStreamImplementation) { - throw new Error('Cannot update status without a sync stream implementation'); - } - // Only assigning, don't call listeners for this test - this.connectionManager.syncStreamImplementation!.syncStatus = new SyncStatus(status); - this.updateAllStatuses(status); - } } /** diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts b/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts index 157d7c1cb..8693575b6 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.worker.ts @@ -10,5 +10,5 @@ const sharedSyncImplementation = new SharedSyncImplementation(); _self.onconnect = async function (event: MessageEvent) { const port = event.ports[0]; - await new WorkerClient(sharedSyncImplementation, port).initialize(); + new WorkerClient(sharedSyncImplementation, port); }; diff --git a/packages/web/src/worker/sync/WorkerClient.ts b/packages/web/src/worker/sync/WorkerClient.ts index a5a8ded86..519d42119 100644 --- a/packages/web/src/worker/sync/WorkerClient.ts +++ b/packages/web/src/worker/sync/WorkerClient.ts @@ -1,4 +1,4 @@ -import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream, SyncStatusOptions } from '@powersync/common'; +import { ILogLevel, PowerSyncConnectionOptions, SubscribedStream } from '@powersync/common'; import * as Comlink from 'comlink'; import { getNavigatorLocks } from '../../shared/navigator'; import { @@ -24,9 +24,6 @@ export class WorkerClient { private readonly port: MessagePort ) { Comlink.expose(this, this.port); - } - - async initialize() { /** * Adds an extra listener which can remove this port * from the list of monitored ports. @@ -37,16 +34,6 @@ export class WorkerClient { await this.removePort(); } }); - - /** - * Keep a reference to the resolved port promise. - * The init timing is difficult to predict due to the async message passing. - * We only want to use a port if we are know it's been protected from being closed. - * The lock based close signal will be added asynchronously. We need to use the - * added port once the lock is configured. - */ - this.resolvedPortPromise = this.sync.addPort(this.port); - this.resolvedPort = await this.resolvedPortPromise; } private async removePort() { @@ -70,18 +57,9 @@ export class WorkerClient { * it can consider the connection to be closed. */ async addLockBasedCloseSignal(name: string) { - if (!this.resolvedPortPromise) { - // The init logic above is actually synchronous, so this should not happen. - this.sync.broadCastLogger.warn('addLockBasedCloseSignal called before port promise registered'); - } else { - const wrappedPort = await this.resolvedPortPromise; - /** - * The client registered a navigator lock. We now can guarantee detecting if the client has closed. - * E.g. before this point: It's possible some ports might have been created and closed before the - * lock based close signal is added. We should not trust those ports. - */ - wrappedPort.isProtectedFromClose = true; - } + // Only add the port once the lock has been obtained on the client. + this.resolvedPort = await this.sync.addPort(this.port); + // Don't await this lock request getNavigatorLocks().request(name, async () => { await this.removePort(); }); @@ -121,8 +99,4 @@ export class WorkerClient { disconnect() { return this.sync.disconnect(); } - - async _testUpdateAllStatuses(status: SyncStatusOptions) { - return this.sync._testUpdateAllStatuses(status); - } } From cbfb68303d1aab31d99ef95c456aa58db63f3f33 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 11:58:36 +0200 Subject: [PATCH 26/44] cleanup test code --- .../db/adapters/AsyncDatabaseConnection.ts | 5 + .../db/adapters/LockedAsyncDatabaseAdapter.ts | 2 - .../src/worker/sync/MockSyncServiceWorker.ts | 35 ++--- packages/web/tests/mocks/MockWebRemote.ts | 146 +++++------------- packages/web/tests/multiple_instances.test.ts | 2 +- packages/web/tests/utils/MockSyncService.ts | 10 -- 6 files changed, 55 insertions(+), 145 deletions(-) delete mode 100644 packages/web/tests/utils/MockSyncService.ts diff --git a/packages/web/src/db/adapters/AsyncDatabaseConnection.ts b/packages/web/src/db/adapters/AsyncDatabaseConnection.ts index 1bf791f63..0ae2dc481 100644 --- a/packages/web/src/db/adapters/AsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/AsyncDatabaseConnection.ts @@ -17,6 +17,11 @@ export type ProxiedQueryResult = Omit & { */ export type OnTableChangeCallback = (event: BatchedUpdateNotification) => void; +/** + * Thrown when an underlying database connection is closed. + * This is particularly relevant when worker connections are marked as closed while + * operations are still in progress. + */ export class ConnectionClosedError extends Error { constructor(message: string) { super(message); diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 9d48adc7b..70db10009 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -57,7 +57,6 @@ export class LockedAsyncDatabaseAdapter private _config: ResolvedWebSQLOpenOptions | null = null; protected pendingAbortControllers: Set; protected requiresHolds: boolean | null; - protected requiresReOpen: boolean; protected databaseOpenPromise: Promise | null = null; closing: boolean; @@ -71,7 +70,6 @@ export class LockedAsyncDatabaseAdapter this.closed = false; this.closing = false; this.requiresHolds = null; - this.requiresReOpen = false; // Set the name if provided. We can query for the name if not available yet this.debugMode = options.debugMode ?? false; if (this.debugMode) { diff --git a/packages/web/src/worker/sync/MockSyncServiceWorker.ts b/packages/web/src/worker/sync/MockSyncServiceWorker.ts index 2ff5c2d05..9387d3064 100644 --- a/packages/web/src/worker/sync/MockSyncServiceWorker.ts +++ b/packages/web/src/worker/sync/MockSyncServiceWorker.ts @@ -17,6 +17,13 @@ export class MockSyncService { private nextId = 0; private automaticResponse: AutomaticResponseConfig | null = null; + /** + * A Static instance of the mock sync service. + * This can be used directly for non-worker environments. + * A proxy is required for worker environments. + */ + static readonly GLOBAL_INSTANCE = new MockSyncService(); + /** * Register a new pending request (called by WebRemote when a sync stream is requested). * Returns a promise that resolves when a client creates a response for this request. @@ -253,36 +260,10 @@ export class MockSyncService { } } -/** - * Global mock service instance (only available in shared worker context) - */ -let globalMockService: MockSyncService | null = null; - -/** - * Get or create the global mock service instance - */ -export function getMockSyncService(): MockSyncService | null { - // Only available in shared worker context - if (typeof SharedWorkerGlobalScope === 'undefined') { - return null; - } - - if (!globalMockService) { - globalMockService = new MockSyncService(); - } - - return globalMockService; -} - /** * Set up message handler for the mock service on a MessagePort */ export function setupMockServiceMessageHandler(port: MessagePort) { - const service = getMockSyncService(); - if (!service) { - return; - } - port.addEventListener('message', (event: MessageEvent) => { const message = event.data; @@ -290,6 +271,8 @@ export function setupMockServiceMessageHandler(port: MessagePort) { return; } + const service = MockSyncService.GLOBAL_INSTANCE; + try { switch (message.type) { case 'getPendingRequests': { diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts index 02a03d6bb..83471a202 100644 --- a/packages/web/tests/mocks/MockWebRemote.ts +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -11,34 +11,7 @@ import { SocketSyncStreamOptions } from '@powersync/common'; import { serialize, type BSON } from 'bson'; -import { getMockSyncService, setupMockServiceMessageHandler } from '../utils/MockSyncService'; - -/** - * Check if we're running in a shared worker context - */ -function isSharedWorkerContext(): boolean { - const isSharedWorker = - typeof SharedWorkerGlobalScope !== 'undefined' && - typeof self !== 'undefined' && - (self as any).constructor?.name === 'SharedWorkerGlobalScope'; - return isSharedWorker; -} - -/** - * Mock fetch provider that returns 401 for non-stream requests - */ -class MockFetchProvider extends FetchImplementationProvider { - getFetch(): FetchImplementation { - // Return a mock fetch that always returns 401 - return async (input: RequestInfo | URL, init?: RequestInit): Promise => { - const response = new Response(null, { - status: 401, - statusText: 'Unauthorized' - }); - return response; - }; - } -} +import { MockSyncService, setupMockServiceMessageHandler } from '../../src/worker/sync/MockSyncServiceWorker'; /** * Mock fetch provider that intercepts all requests and routes them to the mock sync service. @@ -53,96 +26,61 @@ class MockSyncServiceFetchProvider extends FetchImplementationProvider { getFetch(): FetchImplementation { return async (input: RequestInfo | URL, init?: RequestInit): Promise => { const request = new Request(input, init); - const mockService = getMockSyncService(); - if (mockService) { - // Read the request body (if any) - let body: any = null; - try { - if (request.body) { - const clonedRequest = request.clone(); - body = await clonedRequest.json().catch(() => { - // If JSON parsing fails, try text - return clonedRequest.text().catch(() => null); - }); - } - } catch (e) { - // Body might not be readable, that's okay + const mockService = MockSyncService.GLOBAL_INSTANCE; + + // Read the request body (if any) + let body: any = null; + try { + if (request.body) { + const clonedRequest = request.clone(); + body = await clonedRequest.json().catch(() => { + // If JSON parsing fails, try text + return clonedRequest.text().catch(() => null); + }); } - - // Extract headers from the request - const headers: Record = {}; - request.headers.forEach((value, key) => { - headers[key] = value; - }); - - // Register as a pending request and wait for client to create response - return await mockService.registerPendingRequest(request.url, request.method, headers, body, request.signal); + } catch (e) { + // Body might not be readable, that's okay } - // Fallback if mock service is not available - return new Response(null, { - status: 401, - statusText: 'Unauthorized' + // Extract headers from the request + const headers: Record = {}; + request.headers.forEach((value, key) => { + headers[key] = value; }); + + // Register as a pending request and wait for client to create response + return await mockService.registerPendingRequest(request.url, request.method, headers, body, request.signal); }; } } -// Track if we've already set up the onconnect handler -let onconnectHandlerSetup = false; - /** - * Create the mock sync service globally upfront when in shared worker context. - * This ensures the service is available before any requests come in. + * Check if we're running in a shared worker context */ -function initializeMockSyncService() { - if (!isSharedWorkerContext()) { - return; - } - - // Create the mock service upfront (getMockSyncService creates it if it doesn't exist) - getMockSyncService(); +function isSharedWorkerContext(): boolean { + const isSharedWorker = + typeof SharedWorkerGlobalScope !== 'undefined' && + typeof self !== 'undefined' && + (self as any).constructor?.name === 'SharedWorkerGlobalScope'; + return isSharedWorker; } -/** - * Set up the onconnect listener lazily when the first WebRemote is created. - * This ensures deterministic initialization order regardless of import order. - * The mock service is created upfront, but message handlers are only set up on connect. - */ -function setupOnconnectHandlerIfNeeded() { - if (onconnectHandlerSetup || !isSharedWorkerContext()) { - return; - } - - // Create the mock sync service globally upfront - initializeMockSyncService(); - +if (isSharedWorkerContext()) { const _self: SharedWorkerGlobalScope = self as any; + console.log('MockWebRemote: setting up connect listener'); - // Store the original onconnect if it exists (may be set by SharedSyncImplementation.worker.ts) - const originalOnConnect = _self.onconnect; - - _self.onconnect = async function (event: MessageEvent) { + /** + * This listener should be called in tandem with the shared sync worker's listener. + */ + _self.addEventListener('connect', async function (event: MessageEvent) { + console.log('MockWebRemote: connect listener called'); const port = event.ports[0]; - // Get the mock service (already created upfront) and set up message handler lazily - const mockService = getMockSyncService(); - if (mockService) { - // Set up message handler for the mock service on this port - // Tests can create a separate SharedWorker connection to access this - setupMockServiceMessageHandler(port); - } - - // Call the original handler if it exists (this will set up WorkerClient) - // Note: The mock service message handler and WorkerClient can coexist - // since they use different message types - if (originalOnConnect) { - await originalOnConnect.call(this, event); - } - }; - - onconnectHandlerSetup = true; + // Set up message handler for the mock service on this port + // Tests can create a separate SharedWorker connection to access this + setupMockServiceMessageHandler(port); + }); } export class WebRemote extends AbstractRemote { @@ -153,12 +91,8 @@ export class WebRemote extends AbstractRemote { protected logger: ILogger = DEFAULT_REMOTE_LOGGER, options?: Partial ) { - // Lazy initialize the onconnect handler when the first WebRemote is created - // This ensures deterministic initialization order regardless of import order - setupOnconnectHandlerIfNeeded(); - // Use mock service fetch provider if we're in a shared worker context - const fetchProvider = isSharedWorkerContext() ? new MockSyncServiceFetchProvider() : new MockFetchProvider(); + const fetchProvider = new MockSyncServiceFetchProvider(); super(connector, logger, { ...(options ?? {}), diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 4edf86cc0..d15d6406c 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -5,7 +5,7 @@ import { beforeAll, describe, expect, it, onTestFinished, vi } from 'vitest'; import { LockedAsyncDatabaseAdapter } from '../src/db/adapters/LockedAsyncDatabaseAdapter'; import { WebDBAdapter } from '../src/db/adapters/WebDBAdapter'; import { WorkerWrappedAsyncDatabaseConnection } from '../src/db/adapters/WorkerWrappedAsyncDatabaseConnection'; -import { getMockSyncServiceFromWorker } from './utils/MockSyncService'; +import { getMockSyncServiceFromWorker } from './utils/MockSyncServiceClient'; import { createTestConnector, sharedMockSyncServiceTest } from './utils/mockSyncServiceTest'; import { generateTestDb, testSchema } from './utils/testDb'; diff --git a/packages/web/tests/utils/MockSyncService.ts b/packages/web/tests/utils/MockSyncService.ts deleted file mode 100644 index 95a5859e8..000000000 --- a/packages/web/tests/utils/MockSyncService.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Re-export types and worker-side implementation for backward compatibility -export * from '../../src/worker/sync/MockSyncServiceTypes'; -export { - MockSyncService, - getMockSyncService, - setupMockServiceMessageHandler -} from '../../src/worker/sync/MockSyncServiceWorker'; - -// Re-export client-side utilities -export * from './MockSyncServiceClient'; From f88199215711bb067fbf7b1822940459a0b1d9be Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 12:15:40 +0200 Subject: [PATCH 27/44] Listen for database close events to catch closed items earlier. --- .../db/adapters/LockedAsyncDatabaseAdapter.ts | 25 ++++++++++++++++--- .../WorkerWrappedAsyncDatabaseConnection.ts | 8 ++++++ .../worker/sync/SharedSyncImplementation.ts | 8 +++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 70db10009..e44d04287 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -132,6 +132,27 @@ export class LockedAsyncDatabaseAdapter this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS; } + protected _reOpen() { + this.databaseOpenPromise = this.openInternalDB().finally(() => { + this.databaseOpenPromise = null; + }); + return this.databaseOpenPromise; + } + + /** + * Re-opens the underlying database. + * Returns a pending operation if one is already in progress. + */ + async reOpenInternalDB(): Promise { + if (!this.options.reOpenOnConnectionClosed) { + throw new Error(`Cannot re-open underlying database, reOpenOnConnectionClosed is not enabled`); + } + if (this.databaseOpenPromise) { + return this.databaseOpenPromise; + } + return this._reOpen(); + } + protected async _init() { await this.openInternalDB(); this.iterateListeners((cb) => cb.initialized?.()); @@ -277,9 +298,7 @@ export class LockedAsyncDatabaseAdapter if (ex instanceof ConnectionClosedError) { if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) { // Immediately re-open the database. We need to miss as little table updates as possible. - this.databaseOpenPromise = this.openInternalDB().finally(() => { - this.databaseOpenPromise = null; - }); + this.reOpenInternalDB(); } } throw ex; diff --git a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts index 81caf8445..0e25dbe5f 100644 --- a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts @@ -1,3 +1,4 @@ +import { BaseObserver } from '@powersync/common'; import * as Comlink from 'comlink'; import { AsyncDatabaseConnection, @@ -24,17 +25,23 @@ export type WrappedWorkerConnectionOptions void; }; +export type WorkerWrappedAsyncDatabaseConnectionListener = { + closing: () => void; +}; /** * Wraps a provided instance of {@link AsyncDatabaseConnection}, providing necessary proxy * functions for worker listeners. */ export class WorkerWrappedAsyncDatabaseConnection + extends BaseObserver implements AsyncDatabaseConnection { protected lockAbortController = new AbortController(); protected notifyRemoteClosed: AbortController | undefined; constructor(protected options: WrappedWorkerConnectionOptions) { + super(); + if (options.remoteCanCloseUnexpectedly) { this.notifyRemoteClosed = new AbortController(); } @@ -169,6 +176,7 @@ export class WorkerWrappedAsyncDatabaseConnection l.closing?.()); } } diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index b01438bc3..560a694eb 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -247,7 +247,13 @@ export class SharedSyncImplementation extends BaseObserver { // Gets a connection from the clients when a new connection is requested. - return await this.openInternalDB(); + const db = await this.openInternalDB(); + db.registerListener({ + closing: () => { + lockedAdapter.reOpenInternalDB(); + } + }); + return db; }, logger: this.logger, reOpenOnConnectionClosed: true From ffe5abe6aacf5929f1c96129953218f663b1def8 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Mon, 1 Dec 2025 15:45:49 +0200 Subject: [PATCH 28/44] Fire and forget close operations. Serialize all sync status errors. --- package.json | 5 + packages/common/src/db/crud/SyncStatus.ts | 21 +- .../WorkerWrappedAsyncDatabaseConnection.ts | 12 +- .../worker/sync/SharedSyncImplementation.ts | 48 +- .../web/tests/error_serialization.test.ts | 43 + .../web/tests/mockSyncServiceExample.test.ts | 12 +- packages/web/tests/multiple_instances.test.ts | 10 +- .../web/tests/utils/mockSyncServiceTest.ts | 27 +- pnpm-lock.yaml | 1295 ++++++++++------- 9 files changed, 914 insertions(+), 559 deletions(-) create mode 100644 packages/web/tests/error_serialization.test.ts diff --git a/package.json b/package.json index fd438ab2e..141435fe5 100644 --- a/package.json +++ b/package.json @@ -49,5 +49,10 @@ "rollup-plugin-dts": "^6.2.1", "typescript": "^5.7.2", "vitest": "^3.2.4" + }, + "pnpm": { + "overrides": { + "@journeyapps/wa-sqlite": "0.0.0-dev-20251201120934" + } } } diff --git a/packages/common/src/db/crud/SyncStatus.ts b/packages/common/src/db/crud/SyncStatus.ts index f5686c037..5b8c45990 100644 --- a/packages/common/src/db/crud/SyncStatus.ts +++ b/packages/common/src/db/crud/SyncStatus.ts @@ -1,7 +1,7 @@ -import { CoreStreamSubscription } from '../../client/sync/stream/core-instruction.js'; import { SyncClientImplementation } from '../../client/sync/stream/AbstractStreamingSyncImplementation.js'; -import { InternalProgressInformation, ProgressWithOperations, SyncProgress } from './SyncProgress.js'; +import { CoreStreamSubscription } from '../../client/sync/stream/core-instruction.js'; import { SyncStreamDescription, SyncSubscriptionDescription } from '../../client/sync/sync-streams.js'; +import { InternalProgressInformation, ProgressWithOperations, SyncProgress } from './SyncProgress.js'; export type SyncDataFlowStatus = Partial<{ downloading: boolean; @@ -250,13 +250,28 @@ export class SyncStatus { return { connected: this.connected, connecting: this.connecting, - dataFlow: this.dataFlowStatus, + dataFlow: { + ...this.dataFlowStatus, + uploadError: this.serializeError(this.dataFlowStatus.uploadError), + downloadError: this.serializeError(this.dataFlowStatus.downloadError) + }, lastSyncedAt: this.lastSyncedAt, hasSynced: this.hasSynced, priorityStatusEntries: this.priorityStatusEntries }; } + protected serializeError(error?: Error) { + if (typeof error == 'undefined') { + return undefined; + } + return { + name: error.name, + message: error.message, + stack: error.stack + }; + } + private static comparePriorities(a: SyncPriorityStatus, b: SyncPriorityStatus) { return b.priority - a.priority; // Reverse because higher priorities have lower numbers } diff --git a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts index 0e25dbe5f..57557edb8 100644 --- a/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts +++ b/packages/web/src/db/adapters/WorkerWrappedAsyncDatabaseConnection.ts @@ -80,14 +80,17 @@ export class WorkerWrappedAsyncDatabaseConnection this.baseConnection.isAutoCommit()); } - private withRemote(workerPromise: () => Promise): Promise { + private withRemote(workerPromise: () => Promise, fireActionOnAbort = false): Promise { const controller = this.notifyRemoteClosed; if (controller) { return new Promise((resolve, reject) => { if (controller.signal.aborted) { reject(new ConnectionClosedError('Called operation on closed remote')); - // Don't run the operation if we're going to reject - return; + if (!fireActionOnAbort) { + // Don't run the operation if we're going to reject + // We might want to fire-and-forget the operation in some cases (like a close operation) + return; + } } function handleAbort() { @@ -172,7 +175,8 @@ export class WorkerWrappedAsyncDatabaseConnection this.baseConnection.close()); + // fire and forget the close operation + await this.withRemote(() => this.baseConnection.close(), true); } finally { this.options.remote[Comlink.releaseProxy](); this.options.onClose?.(); diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 560a694eb..0dee7a711 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -184,6 +184,16 @@ export class SharedSyncImplementation extends BaseObserver { + return await this.portMutex.runExclusive(() => { + return this.ports[Math.floor(Math.random() * this.ports.length)]; + }); + } + async waitForStatus(status: SyncStatusOptions): Promise { return this.withSyncImplementation(async (sync) => { return sync.waitForStatus(status); @@ -487,12 +497,17 @@ export class SharedSyncImplementation extends BaseObserver { + abortController.abort(); + }, 10_000); + /** * Handle cases where the client might close while opening a connection. */ @@ -502,21 +517,20 @@ export class SharedSyncImplementation extends BaseObserver { - const index = lastClient.closeListeners.indexOf(closeListener); + const index = client.closeListeners.indexOf(closeListener); if (index >= 0) { - lastClient.closeListeners.splice(index, 1); + client.closeListeners.splice(index, 1); } }; - lastClient.closeListeners.push(closeListener); + client.closeListeners.push(closeListener); - const workerPort = await withAbort( - () => lastClient.clientProvider.getDBWorkerPort(), - abortController.signal - ).catch((ex) => { - removeCloseListener(); - throw ex; - }); + const workerPort = await withAbort(() => client.clientProvider.getDBWorkerPort(), abortController.signal).catch( + (ex) => { + removeCloseListener(); + throw ex; + } + ); const remote = Comlink.wrap(workerPort); const identifier = this.syncParams!.dbParams.dbFilename; @@ -532,6 +546,8 @@ export class SharedSyncImplementation extends BaseObserver { + client.closeListeners.push(async () => { this.logger.info('Aborting open connection because associated tab closed.'); /** * Don't await this close operation. It might never resolve if the tab is closed. - * We run the close operation first, before marking the remote as closed. This gives the database some chance - * to close the connection. + * We mark the remote as closed first, this will reject any pending requests. + * We then call close. The close operation is configured to fire-and-forget, the main promise will reject immediately. */ - wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex)); wrapped.markRemoteClosed(); + wrapped.close().catch((ex) => this.logger.warn('error closing database connection', ex)); }); return wrapped; diff --git a/packages/web/tests/error_serialization.test.ts b/packages/web/tests/error_serialization.test.ts new file mode 100644 index 000000000..df192378c --- /dev/null +++ b/packages/web/tests/error_serialization.test.ts @@ -0,0 +1,43 @@ +import { SyncStreamConnectionMethod } from '@powersync/common'; +import { describe, expect } from 'vitest'; +import { sharedMockSyncServiceTest } from './utils/mockSyncServiceTest'; + +/** + * Test to verify that Error instances are properly serialized when passed through MessagePorts. + * When errors occur in the shared worker and are reported via statusChanged, they should + * be properly serialized and deserialized to appear in the sync status. + */ +describe('Error Serialization through MessagePorts', { sequential: true }, () => { + sharedMockSyncServiceTest( + 'should serialize and deserialize Error in sync status when connection fails', + { timeout: 10_000 }, + async ({ context: { database, mockService } }) => { + await mockService.setAutomaticResponse({ + status: 401, + headers: { 'Content-Type': 'application/json' }, + bodyLines: ['Unauthorized'] + }); + + // Start connection attempt + await database.connect( + { + fetchCredentials: async () => { + return { + endpoint: 'http://localhost:3000', + token: 'test-token' + }; + }, + uploadData: async () => {} + }, + { + connectionMethod: SyncStreamConnectionMethod.HTTP + } + ); + + expect(database.currentStatus.dataFlowStatus?.downloadError).toBeDefined(); + expect(database.currentStatus.dataFlowStatus?.downloadError?.name).toBe('Error'); + expect(database.currentStatus.dataFlowStatus?.downloadError?.message).toBe('HTTP : "Unauthorized"\n'); + expect(database.currentStatus.dataFlowStatus?.downloadError?.stack).toBeDefined(); + } + ); +}); diff --git a/packages/web/tests/mockSyncServiceExample.test.ts b/packages/web/tests/mockSyncServiceExample.test.ts index b4d8edb47..e7165e944 100644 --- a/packages/web/tests/mockSyncServiceExample.test.ts +++ b/packages/web/tests/mockSyncServiceExample.test.ts @@ -17,9 +17,9 @@ describe('Mock Sync Service Example', { timeout: 100000 }, () => { sharedMockSyncServiceTest( 'should allow mocking sync responses in shared worker', { timeout: 100000 }, - async ({ context: { database, connect } }) => { + async ({ context: { database, connect, mockService } }) => { // Call connect to start the sync worker and get the sync service - const { syncService, syncRequestId } = await connect(); + const { syncRequestId } = await connect(); // Push a checkpoint with buckets (following node test pattern) const checkpoint: StreamingSyncCheckpoint = { @@ -37,10 +37,10 @@ describe('Mock Sync Service Example', { timeout: 100000 }, () => { } }; - await syncService.pushBodyLine(syncRequestId, checkpoint); + await mockService.pushBodyLine(syncRequestId, checkpoint); // The connect call should resolve by now - await syncService.pushBodyLine(syncRequestId, { + await mockService.pushBodyLine(syncRequestId, { data: { bucket: 'a', data: [ @@ -57,14 +57,14 @@ describe('Mock Sync Service Example', { timeout: 100000 }, () => { }); // Push checkpoint_complete to finish the sync - await syncService.pushBodyLine(syncRequestId, { + await mockService.pushBodyLine(syncRequestId, { checkpoint_complete: { last_op_id: '1' } }); // Complete the response - await syncService.completeResponse(syncRequestId); + await mockService.completeResponse(syncRequestId); // Wait for sync to complete and verify the data was saved await vi.waitFor(async () => { diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index d15d6406c..f83a9e230 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -243,7 +243,7 @@ describe('Multiple Instances', { sequential: true }, () => { sharedMockSyncServiceTest( 'should trigger uploads from last connected clients', - async ({ context: { database, openDatabase, connect, connector } }) => { + async ({ context: { database, openDatabase, connect, connector, mockService } }) => { const secondDatabase = openDatabase(); expect(database.currentStatus.connected).false; @@ -258,7 +258,7 @@ describe('Multiple Instances', { sequential: true }, () => { await database.execute('INSERT into lists (id, name) VALUES (uuid(), ?)', ['steven']); // connect from the first database - const { syncService } = await connect(); + await connect(); await vi.waitFor(() => expect(database.currentStatus.connected).true); @@ -275,13 +275,13 @@ describe('Multiple Instances', { sequential: true }, () => { const secondConnectPromise = secondDatabase.connect(secondConnector); let _pendingRequestId: string; await vi.waitFor(async () => { - const requests = await syncService.getPendingRequests(); + const requests = await mockService.getPendingRequests(); expect(requests.length).toBeGreaterThan(0); _pendingRequestId = requests[0].id; }); const pendingRequestId = _pendingRequestId!; - await syncService.createResponse(pendingRequestId, 200, { 'Content-Type': 'application/json' }); - await syncService.pushBodyLine(pendingRequestId, { + await mockService.createResponse(pendingRequestId, 200, { 'Content-Type': 'application/json' }); + await mockService.pushBodyLine(pendingRequestId, { token_expires_in: 10000000 }); await secondConnectPromise; diff --git a/packages/web/tests/utils/mockSyncServiceTest.ts b/packages/web/tests/utils/mockSyncServiceTest.ts index 6e55163af..2cac92e53 100644 --- a/packages/web/tests/utils/mockSyncServiceTest.ts +++ b/packages/web/tests/utils/mockSyncServiceTest.ts @@ -41,7 +41,6 @@ export function createTestConnector(): MockedTestConnector { * Result of calling the connect function */ export interface ConnectResult { - syncService: MockSyncService; syncRequestId: string; } @@ -63,6 +62,7 @@ export const sharedMockSyncServiceTest = test.extend<{ database: PowerSyncDatabase; databaseName: string; openDatabase: (customConfig?: Partial) => PowerSyncDatabase; + mockService: MockSyncService; }; }>({ context: async ({}, use) => { @@ -98,6 +98,15 @@ export const sharedMockSyncServiceTest = test.extend<{ const database = openDatabase(); + // Get the identifier from the database.name property + const identifier = database.database.name; + + // Connect to the shared worker to get the mock service + const mockService = await getMockSyncServiceFromWorker(identifier); + if (!mockService) { + throw new Error('Mock service not available'); + } + const connector = createTestConnector(); const connectFn = async (customConnector?: PowerSyncBackendConnector): Promise => { @@ -116,18 +125,6 @@ export const sharedMockSyncServiceTest = test.extend<{ { timeout: 1000 } ); - // Get the identifier from the database.name property - const identifier = database.database.name; - - // Connect to the shared worker to get the mock service - const mockService = await getMockSyncServiceFromWorker(identifier); - - if (!mockService) { - throw new Error( - 'Mock service not available - ensure enableMultiTabs is true and running in a test environment' - ); - } - let _syncRequestId: string; await vi.waitFor(async () => { const requests = await mockService.getPendingRequests(); @@ -147,7 +144,6 @@ export const sharedMockSyncServiceTest = test.extend<{ await connectionPromise; return { - syncService: mockService, syncRequestId }; }; @@ -157,7 +153,8 @@ export const sharedMockSyncServiceTest = test.extend<{ connect: connectFn, database, databaseName: dbFilename, - openDatabase + openDatabase, + mockService }); } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf18515e9..9e64f34b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@journeyapps/wa-sqlite': 0.0.0-dev-20251201120934 + importers: .: @@ -31,7 +34,7 @@ importers: version: 12.1.4(rollup@4.14.3)(tslib@2.8.1)(typescript@5.9.2) '@vitest/browser': specifier: ^3.2.4 - version: 3.2.4(playwright@1.52.0)(vite@6.3.5(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0))(vitest@3.2.4) + version: 3.2.4(playwright@1.52.0)(vite@6.3.5(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.4) husky: specifier: ^9.0.11 version: 9.1.7 @@ -93,8 +96,8 @@ importers: specifier: ^19.2.4 version: 19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@powersync/web': specifier: workspace:* version: link:../../packages/web @@ -113,10 +116,10 @@ importers: devDependencies: '@angular-builders/custom-webpack': specifier: ^19.0.0 - version: 19.0.1(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17)(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0) + version: 19.0.1(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0) '@angular-devkit/build-angular': specifier: ^19.2.5 - version: 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17)(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0) + version: 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0) '@angular/cli': specifier: ^19.2.5 version: 19.2.14(@types/node@24.2.0)(chokidar@4.0.3) @@ -143,7 +146,7 @@ importers: version: 4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) '@expo/vector-icons': specifier: ^14.0.0 - version: 14.1.0(wm3bvfp4qcetscjld4hplpimri) + version: 14.1.0(a6850416216e8b64df60af23d5183c0b) '@journeyapps/react-native-quick-sqlite': specifier: ^2.4.9 version: 2.4.9(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -164,7 +167,7 @@ importers: version: 0.1.11(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@react-navigation/drawer': specifier: ^7.1.1 - version: 7.4.1(j6abyuabi5plzpedpvxbnwhrsi) + version: 7.4.1(1d85788bd68a0e12619f848d71cbac62) '@react-navigation/native': specifier: ^7.0.14 version: 7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -188,7 +191,7 @@ importers: version: 2.1.10 expo-router: specifier: 4.0.21 - version: 4.0.21(xdzi7taj2dri7edfzwov6a63va) + version: 4.0.21(e063c8109134fcdd1c97e4d6a4caf625) expo-splash-screen: specifier: ~0.29.22 version: 0.29.24(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)) @@ -236,7 +239,7 @@ importers: version: 10.2.0 react-navigation-stack: specifier: ^2.10.4 - version: 2.10.4(4a23q4g4mav7ddr6jlhxnyzzo4) + version: 2.10.4(1b7f2cbbd098c1646b3c5f57acc57915) typed-async-storage: specifier: ^3.1.2 version: 3.1.2 @@ -281,8 +284,8 @@ importers: specifier: latest version: 7.0.3(@capacitor/core@7.4.4) '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@powersync/capacitor': specifier: workspace:* version: link:../../packages/capacitor @@ -348,8 +351,8 @@ importers: specifier: ^11.13.0 version: 11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@mui/icons-material': specifier: ^5.15.16 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -542,8 +545,8 @@ importers: specifier: ^5.0.13 version: 5.2.5 '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@lexical/react': specifier: ^0.15.0 version: 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.27) @@ -589,10 +592,10 @@ importers: version: 10.4.21(postcss@8.5.4) babel-loader: specifier: ^9.1.3 - version: 9.2.1(@babel/core@7.26.10)(webpack@5.99.9) + version: 9.2.1(@babel/core@7.26.10)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) css-loader: specifier: ^6.11.0 - version: 6.11.0(@rspack/core@1.3.13)(webpack@5.99.9) + version: 6.11.0(@rspack/core@1.3.13(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) eslint: specifier: ^8.57.0 version: 8.57.1 @@ -607,13 +610,13 @@ importers: version: 1.89.1 sass-loader: specifier: ^13.3.3 - version: 13.3.3(sass@1.89.1)(webpack@5.99.9) + version: 13.3.3(sass@1.89.1)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) style-loader: specifier: ^3.3.4 - version: 3.3.4(webpack@5.99.9) + version: 3.3.4(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) tailwindcss: specifier: ^3.4.3 - version: 3.4.17(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + version: 3.4.17(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/node@20.17.57)(typescript@5.9.2)) demos/example-node: dependencies: @@ -698,10 +701,10 @@ importers: devDependencies: '@types/webpack': specifier: ^5.28.5 - version: 5.28.5(webpack-cli@5.1.4(webpack@5.99.9)) + version: 5.28.5(webpack-cli@5.1.4) html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.3(@rspack/core@1.3.13)(webpack@5.99.9(webpack-cli@5.1.4)) + version: 5.6.3(@rspack/core@1.3.13(@swc/helpers@0.5.13))(webpack@5.99.9) serve: specifier: ^14.2.1 version: 14.2.4 @@ -715,8 +718,8 @@ importers: demos/react-multi-client: dependencies: '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@powersync/react': specifier: workspace:* version: link:../../packages/react @@ -844,7 +847,7 @@ importers: version: 0.77.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10)) '@react-native/eslint-config': specifier: 0.77.0 - version: 0.77.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(prettier@3.5.3)(typescript@5.9.2) + version: 0.77.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)))(prettier@3.5.3)(typescript@5.9.2) '@react-native/metro-config': specifier: 0.77.0 version: 0.77.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10)) @@ -928,7 +931,7 @@ importers: version: 7.0.5(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) expo-router: specifier: 4.0.21 - version: 4.0.21(cpo3xaw6yrjernjvkkkt7bisia) + version: 4.0.21(b0bddf53ba1689b30337428eee4dc275) expo-splash-screen: specifier: ~0.29.22 version: 0.29.24(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)) @@ -1001,7 +1004,7 @@ importers: version: 1.0.2 '@expo/vector-icons': specifier: ^14.0.3 - version: 14.1.0(wm3bvfp4qcetscjld4hplpimri) + version: 14.1.0(a6850416216e8b64df60af23d5183c0b) '@journeyapps/react-native-quick-sqlite': specifier: ^2.4.9 version: 2.4.9(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -1022,7 +1025,7 @@ importers: version: 0.1.11(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@react-navigation/drawer': specifier: ^7.1.1 - version: 7.4.1(j6abyuabi5plzpedpvxbnwhrsi) + version: 7.4.1(1d85788bd68a0e12619f848d71cbac62) '@react-navigation/native': specifier: ^7.0.14 version: 7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -1046,7 +1049,7 @@ importers: version: 0.13.3(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)) expo-camera: specifier: ~16.0.18 - version: 16.0.18(hml277kvlorqbj6gijmq6joh24) + version: 16.0.18(55c6da9df988ca7f1678935d82e9238e) expo-constants: specifier: ~17.0.8 version: 17.0.8(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) @@ -1061,7 +1064,7 @@ importers: version: 7.0.5(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) expo-router: specifier: 4.0.21 - version: 4.0.21(xdzi7taj2dri7edfzwov6a63va) + version: 4.0.21(e063c8109134fcdd1c97e4d6a4caf625) expo-secure-store: specifier: ~14.0.1 version: 14.0.1(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)) @@ -1103,7 +1106,7 @@ importers: version: 4.4.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) react-navigation-stack: specifier: ^2.10.4 - version: 2.10.4(4a23q4g4mav7ddr6jlhxnyzzo4) + version: 2.10.4(1b7f2cbbd098c1646b3c5f57acc57915) devDependencies: '@babel/core': specifier: ^7.26.10 @@ -1140,13 +1143,13 @@ importers: version: 4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) '@expo/vector-icons': specifier: ^14.0.2 - version: 14.1.0(wm3bvfp4qcetscjld4hplpimri) + version: 14.1.0(a6850416216e8b64df60af23d5183c0b) '@journeyapps/react-native-quick-sqlite': specifier: ^2.4.9 version: 2.4.9(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@powersync/attachments': specifier: workspace:* version: link:../../packages/attachments @@ -1167,7 +1170,7 @@ importers: version: 7.3.14(@react-navigation/native@7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-screens@4.4.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@react-navigation/drawer': specifier: ^7.1.1 - version: 7.4.1(j6abyuabi5plzpedpvxbnwhrsi) + version: 7.4.1(1d85788bd68a0e12619f848d71cbac62) '@react-navigation/native': specifier: ^7.0.14 version: 7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -1188,7 +1191,7 @@ importers: version: 14.0.3(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) expo-camera: specifier: ~16.0.18 - version: 16.0.18(hml277kvlorqbj6gijmq6joh24) + version: 16.0.18(55c6da9df988ca7f1678935d82e9238e) expo-constants: specifier: ~17.0.5 version: 17.0.8(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) @@ -1206,7 +1209,7 @@ importers: version: 7.0.5(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) expo-router: specifier: 4.0.21 - version: 4.0.21(xdzi7taj2dri7edfzwov6a63va) + version: 4.0.21(e063c8109134fcdd1c97e4d6a4caf625) expo-secure-store: specifier: ^14.0.1 version: 14.0.1(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)) @@ -1221,7 +1224,7 @@ importers: version: 0.2.2(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)) expo-system-ui: specifier: ~4.0.8 - version: 4.0.9(l76mjoke3yk4s56nokhxoxxpou) + version: 4.0.9(fa4ab2ddb2d13a20299c682fc53644db) expo-web-browser: specifier: ~14.0.2 version: 14.0.2(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) @@ -1279,10 +1282,10 @@ importers: version: 18.3.1 jest: specifier: ^29.2.1 - version: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + version: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) jest-expo: specifier: ~52.0.3 - version: 52.0.6(hjrfme3xxu7xcbl6wzt3m2hgh4) + version: 52.0.6(3635c191458c5fa90af52243d15b5fda) react-test-renderer: specifier: 18.3.1 version: 18.3.1(react@18.3.1) @@ -1299,8 +1302,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1384,8 +1387,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1469,8 +1472,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: ^1.3.1 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1554,8 +1557,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1718,8 +1721,8 @@ importers: demos/yjs-react-supabase-text-collab: dependencies: '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@mui/material': specifier: ^5.15.12 version: 5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1828,10 +1831,10 @@ importers: dependencies: '@docusaurus/core': specifier: ^3.7.0 - version: 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + version: 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/preset-classic': specifier: ^3.7.0 - version: 3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) + version: 3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) '@mdx-js/react': specifier: ^3.1.0 version: 3.1.0(@types/react@19.1.6)(react@18.3.1) @@ -1850,19 +1853,19 @@ importers: devDependencies: '@docusaurus/faster': specifier: ^3.7.0 - version: 3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13) '@docusaurus/module-type-aliases': specifier: ^3.7.0 - version: 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-classic': specifier: ^3.7.0 - version: 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + version: 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/tsconfig': specifier: 3.7.0 version: 3.7.0 '@docusaurus/types': specifier: 3.7.0 - version: 3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/node': specifier: ^20.17.12 version: 20.17.57 @@ -2033,8 +2036,8 @@ importers: version: link:../common devDependencies: '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@powersync/web': specifier: workspace:* version: link:../web @@ -2043,7 +2046,7 @@ importers: version: 20.17.57 drizzle-orm: specifier: ^0.44.7 - version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2)(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) + version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) vite: specifier: ^6.1.0 version: 6.3.5(@types/node@20.17.57)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) @@ -2064,8 +2067,8 @@ importers: version: 0.28.2 devDependencies: '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@powersync/web': specifier: workspace:* version: link:../web @@ -2111,7 +2114,7 @@ importers: version: 12.2.0 drizzle-orm: specifier: ^0.44.7 - version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2)(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) + version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) rollup: specifier: 4.14.3 version: 4.14.3 @@ -2327,8 +2330,8 @@ importers: version: 12.1.0 devDependencies: '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@types/uuid': specifier: ^9.0.6 version: 9.0.8 @@ -2340,13 +2343,13 @@ importers: version: 4.0.1 source-map-loader: specifier: ^5.0.0 - version: 5.0.0(webpack@5.99.9(webpack-cli@5.1.4)) + version: 5.0.0(webpack@5.99.9) stream-browserify: specifier: ^3.0.0 version: 3.0.0 terser-webpack-plugin: specifier: ^5.3.9 - version: 5.3.14(webpack@5.99.9(webpack-cli@5.1.4)) + version: 5.3.14(webpack@5.99.9) uuid: specifier: ^9.0.1 version: 9.0.1 @@ -2375,8 +2378,8 @@ importers: tools/diagnostics-app: dependencies: '@journeyapps/wa-sqlite': - specifier: ^1.3.2 - version: 1.3.2 + specifier: 0.0.0-dev-20251201120934 + version: 0.0.0-dev-20251201120934 '@mui/material': specifier: ^5.15.12 version: 5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2546,7 +2549,7 @@ importers: version: 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10)) '@react-native/eslint-config': specifier: 0.78.0 - version: 0.78.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) + version: 0.78.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4) '@react-native/metro-config': specifier: 0.78.0 version: 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10)) @@ -2588,7 +2591,7 @@ importers: version: 4.1.0 detox: specifier: ^20.34.4 - version: 20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))) + version: 20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))) eslint: specifier: ^8.19.0 version: 8.57.1 @@ -2597,7 +2600,7 @@ importers: version: 3.3.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + version: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) prettier: specifier: 2.8.8 version: 2.8.8 @@ -2606,7 +2609,7 @@ importers: version: 19.0.0(react@19.0.0) ts-jest: specifier: ^29.2.6 - version: 29.3.4(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4) + version: 29.3.4(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4) typescript: specifier: 5.0.4 version: 5.0.4 @@ -5878,8 +5881,8 @@ packages: react: '*' react-native: '*' - '@journeyapps/wa-sqlite@1.3.2': - resolution: {integrity: sha512-zZ0KF01an940DV2jJHHzeTvGlXZ8il/sWnI6a2hH3eV/LHtb5H0c6TB7WursE3c8BvNLifXre3R0mLM5OyNhgw==} + '@journeyapps/wa-sqlite@0.0.0-dev-20251201120934': + resolution: {integrity: sha512-Ira+2+IqPUyBqewcS4yPw1DPF/pi8EuXcxHf4w4+TcW4xfV2KnI9ZNA6+IOrLlVnScxoGFitlLbAgb3gWCPciA==} '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} @@ -21700,11 +21703,11 @@ snapshots: - chokidar - typescript - '@angular-builders/custom-webpack@19.0.1(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17)(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0)': + '@angular-builders/custom-webpack@19.0.1(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0)': dependencies: '@angular-builders/common': 3.0.1(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(typescript@5.5.4) '@angular-devkit/architect': 0.1902.14(chokidar@4.0.3) - '@angular-devkit/build-angular': 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17)(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0) + '@angular-devkit/build-angular': 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0) '@angular-devkit/core': 19.2.14(chokidar@4.0.3) '@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4) lodash: 4.17.21 @@ -21753,13 +21756,13 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17)(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0)': + '@angular-devkit/build-angular@19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/node@24.2.0)(chokidar@4.0.3)(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(jiti@2.4.2)(lightningcss@1.30.1)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(yaml@2.8.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.14(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.14(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)))(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + '@angular-devkit/build-webpack': 0.1902.14(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(@swc/core@1.11.29)))(webpack@5.98.0(@swc/core@1.11.29)) '@angular-devkit/core': 19.2.14(chokidar@4.0.3) - '@angular/build': 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@types/node@24.2.0)(chokidar@4.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(postcss@8.5.2)(tailwindcss@3.4.17)(terser@5.39.0)(tsx@4.19.4)(typescript@5.5.4)(yaml@2.8.0) + '@angular/build': 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@types/node@24.2.0)(chokidar@4.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(postcss@8.5.2)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(terser@5.39.0)(tsx@4.19.4)(typescript@5.5.4)(yaml@2.8.0) '@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4) '@babel/core': 7.26.10 '@babel/generator': 7.26.10 @@ -21771,14 +21774,14 @@ snapshots: '@babel/preset-env': 7.26.9(@babel/core@7.26.10) '@babel/runtime': 7.26.10 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + '@ngtools/webpack': 19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29)) '@vitejs/plugin-basic-ssl': 1.2.0(vite@6.2.7(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.85.0)(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) - babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.98.0(@swc/core@1.11.29)) browserslist: 4.25.0 - copy-webpack-plugin: 12.0.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) - css-loader: 7.1.2(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + copy-webpack-plugin: 12.0.2(webpack@5.98.0(@swc/core@1.11.29)) + css-loader: 7.1.2(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)) esbuild-wasm: 0.25.4 fast-glob: 3.3.3 http-proxy-middleware: 3.0.5 @@ -21786,38 +21789,38 @@ snapshots: jsonc-parser: 3.3.1 karma-source-map-support: 1.4.0 less: 4.2.2 - less-loader: 12.2.0(@rspack/core@1.3.13)(less@4.2.2)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) - license-webpack-plugin: 4.0.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + less-loader: 12.2.0(@rspack/core@1.3.13)(less@4.2.2)(webpack@5.98.0(@swc/core@1.11.29)) + license-webpack-plugin: 4.0.2(webpack@5.98.0(@swc/core@1.11.29)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.9.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + mini-css-extract-plugin: 2.9.2(webpack@5.98.0(@swc/core@1.11.29)) open: 10.1.0 ora: 5.4.1 picomatch: 4.0.2 piscina: 4.8.0 postcss: 8.5.2 - postcss-loader: 8.1.1(@rspack/core@1.3.13)(postcss@8.5.2)(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + postcss-loader: 8.1.1(@rspack/core@1.3.13)(postcss@8.5.2)(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 - sass-loader: 16.0.5(@rspack/core@1.3.13)(sass@1.85.0)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + sass-loader: 16.0.5(@rspack/core@1.3.13)(sass@1.85.0)(webpack@5.98.0(@swc/core@1.11.29)) semver: 7.7.1 - source-map-loader: 5.0.0(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + source-map-loader: 5.0.0(webpack@5.98.0(@swc/core@1.11.29)) source-map-support: 0.5.21 terser: 5.39.0 tree-kill: 1.2.2 tslib: 2.8.1 typescript: 5.5.4 webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) - webpack-dev-middleware: 7.4.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) - webpack-dev-server: 5.2.0(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(@swc/core@1.11.29)) + webpack-dev-server: 5.2.0(webpack@5.98.0(@swc/core@1.11.29)) webpack-merge: 6.0.1 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(webpack@5.98.0(@swc/core@1.11.29)) optionalDependencies: '@angular/service-worker': 19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) esbuild: 0.25.4 - jest: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0) + jest: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) jest-environment-jsdom: 29.7.0 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) transitivePeerDependencies: - '@angular/compiler' - '@rspack/core' @@ -21841,12 +21844,12 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-webpack@0.1902.14(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)))(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4))': + '@angular-devkit/build-webpack@0.1902.14(chokidar@4.0.3)(webpack-dev-server@5.2.0(webpack@5.98.0(@swc/core@1.11.29)))(webpack@5.98.0(@swc/core@1.11.29))': dependencies: '@angular-devkit/architect': 0.1902.14(chokidar@4.0.3) rxjs: 7.8.1 webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) - webpack-dev-server: 5.2.0(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + webpack-dev-server: 5.2.0(webpack@5.98.0(@swc/core@1.11.29)) transitivePeerDependencies: - chokidar @@ -21877,7 +21880,7 @@ snapshots: '@angular/core': 19.2.14(rxjs@7.8.2)(zone.js@0.15.1) tslib: 2.8.1 - '@angular/build@19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@types/node@24.2.0)(chokidar@4.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(postcss@8.5.2)(tailwindcss@3.4.17)(terser@5.39.0)(tsx@4.19.4)(typescript@5.5.4)(yaml@2.8.0)': + '@angular/build@19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(@angular/compiler@19.2.14)(@angular/service-worker@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@types/node@24.2.0)(chokidar@4.0.3)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(postcss@8.5.2)(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)))(terser@5.39.0)(tsx@4.19.4)(typescript@5.5.4)(yaml@2.8.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.14(chokidar@4.0.3) @@ -21913,7 +21916,7 @@ snapshots: less: 4.2.2 lmdb: 3.2.6 postcss: 8.5.2 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) transitivePeerDependencies: - '@types/node' - chokidar @@ -23709,7 +23712,7 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@docusaurus/babel@3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/babel@3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/core': 7.26.10 '@babel/generator': 7.27.3 @@ -23722,7 +23725,7 @@ snapshots: '@babel/runtime-corejs3': 7.27.4 '@babel/traverse': 7.27.4 '@docusaurus/logger': 3.8.0 - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.0 tslib: 2.8.1 @@ -23736,34 +23739,34 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/bundler@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: '@babel/core': 7.26.10 - '@docusaurus/babel': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/babel': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/cssnano-preset': 3.8.0 '@docusaurus/logger': 3.8.0 - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.99.9(@swc/core@1.11.29)) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.99.9(@swc/core@1.11.29)) - css-loader: 6.11.0(@rspack/core@1.3.13)(webpack@5.99.9(@swc/core@1.11.29)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(lightningcss@1.30.1)(webpack@5.99.9(@swc/core@1.11.29)) + copy-webpack-plugin: 11.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) + css-loader: 6.11.0(@rspack/core@1.3.13(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(lightningcss@1.30.1)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) cssnano: 6.1.2(postcss@8.5.4) - file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29)) + file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.2(webpack@5.99.9(@swc/core@1.11.29)) - null-loader: 4.0.1(webpack@5.99.9(@swc/core@1.11.29)) + mini-css-extract-plugin: 2.9.2(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) + null-loader: 4.0.1(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) postcss: 8.5.4 - postcss-loader: 7.3.4(postcss@8.5.4)(typescript@5.9.2)(webpack@5.99.9(@swc/core@1.11.29)) + postcss-loader: 7.3.4(postcss@8.5.4)(typescript@5.9.2)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) postcss-preset-env: 10.2.0(postcss@8.5.4) - terser-webpack-plugin: 5.3.14(@swc/core@1.11.29)(webpack@5.99.9(@swc/core@1.11.29)) + terser-webpack-plugin: 5.3.14(@swc/core@1.11.29(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29)))(webpack@5.99.9(@swc/core@1.11.29)) - webpack: 5.99.9(@swc/core@1.11.29) - webpackbar: 6.0.1(webpack@5.99.9(@swc/core@1.11.29)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) + webpackbar: 6.0.1(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) optionalDependencies: - '@docusaurus/faster': 3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@docusaurus/faster': 3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -23780,15 +23783,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/core@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/babel': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/bundler': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/babel': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/bundler': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.0 - '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/react': 3.1.0(@types/react@19.1.6)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 @@ -23804,7 +23807,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.0 html-tags: 3.3.1 - html-webpack-plugin: 5.6.3(@rspack/core@1.3.13)(webpack@5.99.9(@swc/core@1.11.29)) + html-webpack-plugin: 5.6.3(@rspack/core@1.3.13(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) leven: 3.1.0 lodash: 4.17.21 open: 8.4.2 @@ -23814,7 +23817,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.99.9(@swc/core@1.11.29)) + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) react-router: 5.3.4(react@18.3.1) react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) react-router-dom: 5.3.4(react@18.3.1) @@ -23823,9 +23826,9 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 4.15.2(debug@4.4.1)(webpack@5.99.9(@swc/core@1.11.29)) + webpack-dev-server: 4.15.2(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) webpack-merge: 6.0.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -23852,17 +23855,17 @@ snapshots: postcss-sort-media-queries: 5.2.0(postcss@8.5.4) tslib: 2.8.1 - '@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13)': dependencies: - '@docusaurus/types': 3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@rspack/core': 1.3.13 - '@swc/core': 1.11.29 + '@docusaurus/types': 3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) + '@swc/core': 1.11.29(@swc/helpers@0.5.13) '@swc/html': 1.11.29 browserslist: 4.25.0 lightningcss: 1.30.1 - swc-loader: 0.2.6(@swc/core@1.11.29)(webpack@5.99.9(@swc/core@1.11.29)) + swc-loader: 0.2.6(@swc/core@1.11.29(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) tslib: 2.8.1 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - '@swc/helpers' - esbuild @@ -23874,16 +23877,16 @@ snapshots: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/mdx-loader@3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/logger': 3.8.0 - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/mdx': 3.1.0(acorn@8.14.1) '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.4.0 - file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29)) + file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) fs-extra: 11.3.0 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -23899,9 +23902,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29)))(webpack@5.99.9(@swc/core@1.11.29)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) vfile: 6.0.3 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - '@swc/core' - acorn @@ -23910,9 +23913,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/module-type-aliases@3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 '@types/react': 18.3.23 '@types/react-router-config': 5.0.11 @@ -23929,17 +23932,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-content-blog@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.0 - '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) cheerio: 1.0.0-rc.12 feed: 4.2.2 fs-extra: 11.3.0 @@ -23951,7 +23954,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -23971,17 +23974,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.0 - '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/module-type-aliases': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.0 @@ -23992,7 +23995,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -24012,18 +24015,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-content-pages@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -24043,11 +24046,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-css-cascade-layers@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -24070,11 +24073,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-debug@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -24099,11 +24102,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-google-analytics@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 @@ -24126,11 +24129,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-google-gtag@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gtag.js': 0.0.12 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -24154,11 +24157,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-google-tag-manager@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 @@ -24181,14 +24184,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-sitemap@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.0 - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -24213,18 +24216,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/plugin-svgr@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@svgr/core': 8.1.0(typescript@5.9.2) '@svgr/webpack': 8.1.0(typescript@5.9.2) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -24244,23 +24247,23 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': - dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-blog': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-pages': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-css-cascade-layers': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-debug': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-google-analytics': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-google-gtag': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-google-tag-manager': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-sitemap': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-svgr': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-classic': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-search-algolia': 3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/preset-classic@3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': + dependencies: + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-blog': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-pages': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-css-cascade-layers': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-debug': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-google-analytics': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-google-gtag': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-google-tag-manager': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-sitemap': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-svgr': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-classic': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -24290,21 +24293,21 @@ snapshots: '@types/react': 18.3.23 react: 18.3.1 - '@docusaurus/theme-classic@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': + '@docusaurus/theme-classic@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)': dependencies: - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.0 - '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/module-type-aliases': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-blog': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/plugin-content-pages': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/plugin-content-pages': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.8.0 - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/react': 3.1.0(@types/react@19.1.6)(react@18.3.1) clsx: 2.1.1 copy-text-to-clipboard: 3.2.0 @@ -24339,13 +24342,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/module-type-aliases': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/mdx-loader': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 '@types/react': 18.3.23 '@types/react-router-config': 5.0.11 @@ -24364,16 +24367,16 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': + '@docusaurus/theme-search-algolia@3.8.0(@algolia/client-search@5.25.0)(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/react@19.1.6)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.2)': dependencies: '@docsearch/react': 3.9.0(@algolia/client-search@5.25.0)(@types/react@19.1.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) - '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/core': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) '@docusaurus/logger': 3.8.0 - '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) - '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13)(@swc/core@1.11.29)(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) + '@docusaurus/theme-common': 3.8.0(@docusaurus/plugin-content-docs@3.8.0(@docusaurus/faster@3.8.0(@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.13))(@mdx-js/react@3.1.0(@types/react@19.1.6)(react@18.3.1))(@rspack/core@1.3.13(@swc/helpers@0.5.13))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(lightningcss@1.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2))(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.8.0 - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) algoliasearch: 5.25.0 algoliasearch-helper: 3.25.0(algoliasearch@5.25.0) clsx: 2.1.1 @@ -24413,7 +24416,7 @@ snapshots: '@docusaurus/tsconfig@3.7.0': {} - '@docusaurus/types@3.7.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/types@3.7.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@mdx-js/mdx': 3.1.0(acorn@8.14.1) '@types/history': 4.7.11 @@ -24424,7 +24427,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' utility-types: 3.11.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -24434,7 +24437,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/types@3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/types@3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@mdx-js/mdx': 3.1.0(acorn@8.14.1) '@types/history': 4.7.11 @@ -24445,7 +24448,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' utility-types: 3.11.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -24455,9 +24458,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/utils-common@3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' @@ -24469,11 +24472,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-validation@3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/utils-validation@3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/logger': 3.8.0 - '@docusaurus/utils': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.0 joi: 17.13.3 js-yaml: 4.1.0 @@ -24489,14 +24492,14 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/utils@3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/logger': 3.8.0 - '@docusaurus/types': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29)(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.0(@swc/core@1.11.29(@swc/helpers@0.5.13))(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29)) + file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) fs-extra: 11.3.0 github-slugger: 1.5.0 globby: 11.1.0 @@ -24509,9 +24512,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29)))(webpack@5.99.9(@swc/core@1.11.29)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) utility-types: 3.11.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - '@swc/core' - acorn @@ -25963,13 +25966,13 @@ snapshots: '@expo/timeago.js@1.0.0': {} - '@expo/vector-icons@14.1.0(ka6rgkktlsuut5gotrymd2sdni)': + '@expo/vector-icons@14.1.0(99f35dc9d27b76831378288730881035)': dependencies: expo-font: 13.0.4(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) react: 18.3.1 react-native: 0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1) - '@expo/vector-icons@14.1.0(wm3bvfp4qcetscjld4hplpimri)': + '@expo/vector-icons@14.1.0(a6850416216e8b64df60af23d5183c0b)': dependencies: expo-font: 13.0.4(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -26321,7 +26324,42 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.17.57 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -26335,7 +26373,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -26356,7 +26394,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -26370,7 +26408,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -26390,6 +26428,43 @@ snapshots: - babel-plugin-macros - supports-color - ts-node + optional: true + + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.17.57 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + optional: true '@jest/create-cache-key-function@29.7.0': dependencies: @@ -26544,7 +26619,7 @@ snapshots: react: 18.3.1 react-native: 0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1) - '@journeyapps/wa-sqlite@1.3.2': {} + '@journeyapps/wa-sqlite@0.0.0-dev-20251201120934': {} '@jridgewell/gen-mapping@0.3.8': dependencies: @@ -26975,7 +27050,7 @@ snapshots: '@module-federation/manifest': 0.13.1(typescript@5.9.2)(vue-tsc@2.0.6(typescript@5.9.2)) '@module-federation/runtime-tools': 0.13.1 '@module-federation/sdk': 0.13.1 - '@rspack/core': 1.3.13 + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) btoa: 1.2.1 optionalDependencies: typescript: 5.9.2 @@ -27366,7 +27441,7 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.3': optional: true - '@ngtools/webpack@19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4))': + '@ngtools/webpack@19.2.14(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4))(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29))': dependencies: '@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.5.4) typescript: 5.5.4 @@ -29117,7 +29192,7 @@ snapshots: - supports-color - typescript - '@react-native/eslint-config@0.77.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(prettier@3.5.3)(typescript@5.9.2)': + '@react-native/eslint-config@0.77.0(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)))(prettier@3.5.3)(typescript@5.9.2)': dependencies: '@babel/core': 7.26.10 '@babel/eslint-parser': 7.27.1(@babel/core@7.26.10)(eslint@8.57.1) @@ -29128,7 +29203,7 @@ snapshots: eslint-config-prettier: 8.10.0(eslint@8.57.1) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.1) eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.27.1(@babel/core@7.26.10)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(typescript@5.9.2) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)))(typescript@5.9.2) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) eslint-plugin-react-native: 4.1.0(eslint@8.57.1) @@ -29138,7 +29213,7 @@ snapshots: - supports-color - typescript - '@react-native/eslint-config@0.78.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4)': + '@react-native/eslint-config@0.78.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)))(prettier@2.8.8)(typescript@5.0.4)': dependencies: '@babel/core': 7.26.10 '@babel/eslint-parser': 7.27.1(@babel/core@7.26.10)(eslint@8.57.1) @@ -29149,7 +29224,7 @@ snapshots: eslint-config-prettier: 8.10.0(eslint@8.57.1) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.1) eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.27.1(@babel/core@7.26.10)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) eslint-plugin-react-native: 4.1.0(eslint@8.57.1) @@ -29234,9 +29309,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - - bufferutil - supports-color - - utf-8-validate '@react-native/metro-config@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))': dependencies: @@ -29372,7 +29445,7 @@ snapshots: use-latest-callback: 0.2.3(react@18.3.1) use-sync-external-store: 1.5.0(react@18.3.1) - '@react-navigation/drawer@7.4.1(j6abyuabi5plzpedpvxbnwhrsi)': + '@react-navigation/drawer@7.4.1(1d85788bd68a0e12619f848d71cbac62)': dependencies: '@react-navigation/elements': 2.4.3(@react-navigation/native@7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@react-navigation/native': 7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -29388,7 +29461,7 @@ snapshots: transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/drawer@7.4.1(nyxmcqdttlojx3ihgax6eihdpu)': + '@react-navigation/drawer@7.4.1(f2502081aada8c22c3fd2dbf46b9d114)': dependencies: '@react-navigation/elements': 2.4.3(@react-navigation/native@7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@react-navigation/native': 7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -29970,11 +30043,13 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 1.3.13 '@rspack/binding-win32-x64-msvc': 1.3.13 - '@rspack/core@1.3.13': + '@rspack/core@1.3.13(@swc/helpers@0.5.13)': dependencies: '@module-federation/runtime-tools': 0.14.3 '@rspack/binding': 1.3.13 '@rspack/lite-tapable': 1.0.1 + optionalDependencies: + '@swc/helpers': 0.5.13 '@rspack/lite-tapable@1.0.1': {} @@ -30338,7 +30413,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.6.13': optional: true - '@swc/core@1.11.29': + '@swc/core@1.11.29(@swc/helpers@0.5.13)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.21 @@ -30353,6 +30428,7 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.11.29 '@swc/core-win32-ia32-msvc': 1.11.29 '@swc/core-win32-x64-msvc': 1.11.29 + '@swc/helpers': 0.5.13 '@swc/core@1.6.13': dependencies: @@ -31951,7 +32027,7 @@ snapshots: dependencies: vue: 2.7.16 - '@types/webpack@5.28.5(webpack-cli@5.1.4(webpack@5.99.9))': + '@types/webpack@5.28.5(webpack-cli@5.1.4)': dependencies: '@types/node': 20.17.57 tapable: 2.2.2 @@ -32487,11 +32563,11 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@3.2.4(playwright@1.52.0)(vite@6.3.5(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.52.0)(vite@6.3.5(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 sirv: 3.0.1 @@ -32522,7 +32598,7 @@ snapshots: optionalDependencies: vite: 5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 @@ -32686,7 +32762,7 @@ snapshots: vue: 3.4.21(typescript@5.9.2) vue-demi: 0.13.11(vue@3.4.21(typescript@5.9.2)) - '@vuetify/loader-shared@2.1.0(vue@3.4.21(typescript@5.9.2))(vuetify@3.6.8(typescript@5.9.2)(vite-plugin-vuetify@2.1.1)(vue@3.4.21(typescript@5.9.2)))': + '@vuetify/loader-shared@2.1.0(vue@3.4.21(typescript@5.9.2))(vuetify@3.6.8)': dependencies: upath: 2.0.1 vue: 3.4.21(typescript@5.9.2) @@ -32817,17 +32893,17 @@ snapshots: - vue-tsc - webpack-cli - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(webpack-cli@5.1.4))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.99.9)': dependencies: webpack: 5.99.9(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.99.9) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(webpack-cli@5.1.4))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.99.9)': dependencies: webpack: 5.99.9(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.99.9) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(webpack-cli@5.1.4))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.99.9)': dependencies: webpack: 5.99.9(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.99.9) @@ -32840,10 +32916,10 @@ snapshots: optionalDependencies: expect: 29.7.0 - '@wix-pilot/detox@1.0.11(@wix-pilot/core@3.3.2(expect@29.7.0))(detox@20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))))(expect@29.7.0)': + '@wix-pilot/detox@1.0.11(@wix-pilot/core@3.3.2(expect@29.7.0))(detox@20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))))(expect@29.7.0)': dependencies: '@wix-pilot/core': 3.3.2(expect@29.7.0) - detox: 20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))) + detox: 20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))) expect: 29.7.0 '@xmldom/xmldom@0.7.13': {} @@ -33337,19 +33413,19 @@ snapshots: find-up: 5.0.0 webpack: 5.99.9(@swc/core@1.6.13) - babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.98.0(@swc/core@1.11.29)): dependencies: '@babel/core': 7.26.10 find-cache-dir: 4.0.0 schema-utils: 4.3.2 webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) - babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.99.9(@swc/core@1.11.29)): + babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: '@babel/core': 7.26.10 find-cache-dir: 4.0.0 schema-utils: 4.3.2 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.99.9(@swc/core@1.6.13)): dependencies: @@ -33358,13 +33434,6 @@ snapshots: schema-utils: 4.3.2 webpack: 5.99.9(@swc/core@1.6.13) - babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.99.9): - dependencies: - '@babel/core': 7.26.10 - find-cache-dir: 4.0.0 - schema-utils: 4.3.2 - webpack: 5.99.9 - babel-plugin-dynamic-import-node@2.3.3: dependencies: object.assign: 4.1.7 @@ -34483,7 +34552,7 @@ snapshots: copy-text-to-clipboard@3.2.0: {} - copy-webpack-plugin@11.0.0(webpack@5.99.9(@swc/core@1.11.29)): + copy-webpack-plugin@11.0.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -34491,9 +34560,9 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) - copy-webpack-plugin@12.0.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + copy-webpack-plugin@12.0.2(webpack@5.98.0(@swc/core@1.11.29)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -34614,13 +34683,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 - create-jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)): + create-jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -34629,13 +34698,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)): + create-jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest-config: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -34644,13 +34713,29 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0): + create-jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + optional: true + + create-jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -34749,7 +34834,7 @@ snapshots: dependencies: hyphenate-style-name: 1.1.0 - css-loader@6.11.0(@rspack/core@1.3.13)(webpack@5.99.9(@swc/core@1.11.29)): + css-loader@6.11.0(@rspack/core@1.3.13(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -34760,10 +34845,10 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - '@rspack/core': 1.3.13 - webpack: 5.99.9(@swc/core@1.11.29) + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) - css-loader@6.11.0(@rspack/core@1.3.13)(webpack@5.99.9): + css-loader@6.11.0(@rspack/core@1.3.13)(webpack@5.99.9(@swc/core@1.11.29)): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -34774,10 +34859,10 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - '@rspack/core': 1.3.13 - webpack: 5.99.9 + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) + webpack: 5.99.9(@swc/core@1.11.29) - css-loader@7.1.2(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + css-loader@7.1.2(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)): dependencies: icss-utils: 5.1.0(postcss@8.5.4) postcss: 8.5.4 @@ -34788,7 +34873,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - '@rspack/core': 1.3.13 + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) css-loader@7.1.2(@rspack/core@1.3.13)(webpack@5.99.9(@swc/core@1.6.13)): @@ -34802,10 +34887,10 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - '@rspack/core': 1.3.13 + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) webpack: 5.99.9(@swc/core@1.6.13) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(lightningcss@1.30.1)(webpack@5.99.9(@swc/core@1.11.29)): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(lightningcss@1.30.1)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: '@jridgewell/trace-mapping': 0.3.25 cssnano: 6.1.2(postcss@8.5.4) @@ -34813,7 +34898,7 @@ snapshots: postcss: 8.5.4 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) optionalDependencies: clean-css: 5.3.3 lightningcss: 1.30.1 @@ -35169,10 +35254,10 @@ snapshots: transitivePeerDependencies: - supports-color - detox@20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))): + detox@20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))): dependencies: '@wix-pilot/core': 3.3.2(expect@29.7.0) - '@wix-pilot/detox': 1.0.11(@wix-pilot/core@3.3.2(expect@29.7.0))(detox@20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))))(expect@29.7.0) + '@wix-pilot/detox': 1.0.11(@wix-pilot/core@3.3.2(expect@29.7.0))(detox@20.39.0(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(expect@29.7.0)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))))(expect@29.7.0) ajv: 8.17.1 bunyan: 1.8.15 bunyan-debug-stream: 3.1.1(bunyan@1.8.15) @@ -35184,7 +35269,7 @@ snapshots: funpermaproxy: 1.1.0 glob: 8.1.0 ini: 1.3.8 - jest-environment-emit: 1.0.8(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))) + jest-environment-emit: 1.0.8(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))) json-cycle: 1.5.0 lodash: 4.17.21 multi-sort-stream: 1.0.4 @@ -35209,7 +35294,7 @@ snapshots: yargs-parser: 21.1.1 yargs-unparser: 2.0.0 optionalDependencies: - jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) transitivePeerDependencies: - '@jest/environment' - '@jest/types' @@ -35342,7 +35427,7 @@ snapshots: dotenv@16.5.0: {} - drizzle-orm@0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2)(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0): + drizzle-orm@0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0): optionalDependencies: '@libsql/client-wasm': 0.15.8 '@op-engineering/op-sqlite': 14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0) @@ -36169,24 +36254,24 @@ snapshots: - supports-color - typescript - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.0.4) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.0.4))(eslint@8.57.1)(typescript@5.0.4) - jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0))(typescript@5.9.2): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)))(typescript@5.9.2): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.9.2) eslint: 8.57.1 optionalDependencies: '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) - jest: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0) + jest: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) transitivePeerDependencies: - supports-color - typescript @@ -36616,7 +36701,7 @@ snapshots: expo: 52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) semver: 7.7.2 - expo-camera@16.0.18(hml277kvlorqbj6gijmq6joh24): + expo-camera@16.0.18(55c6da9df988ca7f1678935d82e9238e): dependencies: expo: 52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) invariant: 2.2.4 @@ -36776,7 +36861,7 @@ snapshots: dependencies: invariant: 2.2.4 - expo-router@4.0.21(cpo3xaw6yrjernjvkkkt7bisia): + expo-router@4.0.21(b0bddf53ba1689b30337428eee4dc275): dependencies: '@expo/metro-runtime': 4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) '@expo/server': 0.5.3 @@ -36797,7 +36882,7 @@ snapshots: semver: 7.6.3 server-only: 0.0.1 optionalDependencies: - '@react-navigation/drawer': 7.4.1(nyxmcqdttlojx3ihgax6eihdpu) + '@react-navigation/drawer': 7.4.1(f2502081aada8c22c3fd2dbf46b9d114) react-native-reanimated: 3.16.7(@babel/core@7.26.10)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - '@react-native-masked-view/masked-view' @@ -36806,7 +36891,7 @@ snapshots: - react-native - supports-color - expo-router@4.0.21(xdzi7taj2dri7edfzwov6a63va): + expo-router@4.0.21(e063c8109134fcdd1c97e4d6a4caf625): dependencies: '@expo/metro-runtime': 4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) '@expo/server': 0.5.3 @@ -36827,7 +36912,7 @@ snapshots: semver: 7.6.3 server-only: 0.0.1 optionalDependencies: - '@react-navigation/drawer': 7.4.1(j6abyuabi5plzpedpvxbnwhrsi) + '@react-navigation/drawer': 7.4.1(1d85788bd68a0e12619f848d71cbac62) react-native-reanimated: 3.16.7(@babel/core@7.26.10)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - '@react-native-masked-view/masked-view' @@ -36869,7 +36954,7 @@ snapshots: expo: 52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) sf-symbols-typescript: 2.1.0 - expo-system-ui@4.0.9(l76mjoke3yk4s56nokhxoxxpou): + expo-system-ui@4.0.9(fa4ab2ddb2d13a20299c682fc53644db): dependencies: '@react-native/normalize-colors': 0.76.8 debug: 4.4.1(supports-color@8.1.1) @@ -36897,7 +36982,7 @@ snapshots: '@expo/config-plugins': 9.0.17 '@expo/fingerprint': 0.11.11 '@expo/metro-config': 0.19.12 - '@expo/vector-icons': 14.1.0(ka6rgkktlsuut5gotrymd2sdni) + '@expo/vector-icons': 14.1.0(99f35dc9d27b76831378288730881035) babel-preset-expo: 12.0.11(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10)) expo-asset: 11.0.5(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) expo-constants: 17.0.8(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) @@ -36933,7 +37018,7 @@ snapshots: '@expo/config-plugins': 9.0.17 '@expo/fingerprint': 0.11.11 '@expo/metro-config': 0.19.12 - '@expo/vector-icons': 14.1.0(wm3bvfp4qcetscjld4hplpimri) + '@expo/vector-icons': 14.1.0(a6850416216e8b64df60af23d5183c0b) babel-preset-expo: 12.0.11(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10)) expo-asset: 11.0.5(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) expo-constants: 17.0.8(expo@52.0.46(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) @@ -37136,11 +37221,11 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29)): + file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) file-uri-to-path@1.0.0: {} @@ -38029,7 +38114,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)): + html-webpack-plugin@5.6.3(@rspack/core@1.3.13(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -38037,11 +38122,10 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - '@rspack/core': 1.3.13 - webpack: 5.98.0(@swc/core@1.11.29) - optional: true + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) - html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.99.9(@swc/core@1.11.29)): + html-webpack-plugin@5.6.3(@rspack/core@1.3.13(@swc/helpers@0.5.13))(webpack@5.99.9): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -38049,10 +38133,10 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - '@rspack/core': 1.3.13 - webpack: 5.99.9(@swc/core@1.11.29) + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) + webpack: 5.99.9(webpack-cli@5.1.4) - html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.99.9(webpack-cli@5.1.4)): + html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -38060,8 +38144,20 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.2 optionalDependencies: - '@rspack/core': 1.3.13 - webpack: 5.99.9(webpack-cli@5.1.4) + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) + webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) + optional: true + + html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.99.9(@swc/core@1.11.29)): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.2 + optionalDependencies: + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) + webpack: 5.99.9(@swc/core@1.11.29) htmlparser2@10.0.0: dependencies: @@ -38785,16 +38881,35 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)): + jest-cli@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-cli@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + create-jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + jest-config: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -38804,16 +38919,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)): + jest-cli@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + create-jest: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest-config: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -38822,17 +38937,18 @@ snapshots: - babel-plugin-macros - supports-color - ts-node + optional: true - jest-cli@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0): + jest-cli@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0) + create-jest: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -38843,7 +38959,38 @@ snapshots: - ts-node optional: true - jest-config@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)): + jest-config@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.17.57 + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -38869,12 +39016,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.57 - ts-node: 10.9.2(@types/node@20.17.57)(typescript@5.9.2) + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)): + jest-config@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -38900,12 +39047,45 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.57 - ts-node: 10.9.2(@types/node@22.15.29)(typescript@5.0.4) + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4) transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true + + jest-config@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.17.57 + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true - jest-config@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)): + jest-config@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -38931,12 +39111,12 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.15.29 - ts-node: 10.9.2(@types/node@22.15.29)(typescript@5.0.4) + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0): + jest-config@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -38962,6 +39142,39 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 24.2.0 + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + + jest-config@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.2.0 + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -38986,7 +39199,7 @@ snapshots: jest-util: 29.7.0 pretty-format: 29.7.0 - jest-environment-emit@1.0.8(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4))): + jest-environment-emit@1.0.8(@jest/environment@29.7.0)(@jest/types@29.6.3)(@types/bunyan@1.8.11)(jest-environment-jsdom@29.7.0)(jest-environment-node@29.7.0)(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4))): dependencies: bunyamin: 1.6.3(@types/bunyan@1.8.11)(bunyan@2.0.5) bunyan: 2.0.5 @@ -38999,7 +39212,7 @@ snapshots: optionalDependencies: '@jest/environment': 29.7.0 '@jest/types': 29.6.3 - jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) jest-environment-jsdom: 29.7.0 jest-environment-node: 29.7.0 transitivePeerDependencies: @@ -39029,7 +39242,7 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@52.0.6(hjrfme3xxu7xcbl6wzt3m2hgh4): + jest-expo@52.0.6(3635c191458c5fa90af52243d15b5fda): dependencies: '@expo/config': 10.0.11 '@expo/json-file': 9.1.4 @@ -39042,11 +39255,11 @@ snapshots: jest-environment-jsdom: 29.7.0 jest-snapshot: 29.7.0 jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2))) + jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2))) json5: 2.2.3 lodash: 4.17.21 react-native: 0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1) - react-server-dom-webpack: 19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.99.9) + react-server-dom-webpack: 19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.99.9(@swc/core@1.11.29)) react-test-renderer: 18.3.1(react@18.3.1) server-only: 0.0.1 stacktrace-js: 2.0.2 @@ -39248,11 +39461,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2))): + jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2))): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + jest: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -39283,36 +39496,49 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)): + jest@29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.57)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + jest-cli: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)): + jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest-cli: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node + optional: true - jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0): + jest@29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0) + jest-cli: 29.7.0(@types/node@24.2.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -39657,11 +39883,11 @@ snapshots: dependencies: readable-stream: 2.3.8 - less-loader@12.2.0(@rspack/core@1.3.13)(less@4.2.2)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + less-loader@12.2.0(@rspack/core@1.3.13)(less@4.2.2)(webpack@5.98.0(@swc/core@1.11.29)): dependencies: less: 4.2.2 optionalDependencies: - '@rspack/core': 1.3.13 + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) less@4.2.2: @@ -39691,7 +39917,7 @@ snapshots: dependencies: isomorphic.js: 0.2.5 - license-webpack-plugin@4.0.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + license-webpack-plugin@4.0.2(webpack@5.98.0(@swc/core@1.11.29)): dependencies: webpack-sources: 3.3.0 optionalDependencies: @@ -41425,17 +41651,17 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + mini-css-extract-plugin@2.9.2(webpack@5.98.0(@swc/core@1.11.29)): dependencies: schema-utils: 4.3.2 tapable: 2.2.2 webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) - mini-css-extract-plugin@2.9.2(webpack@5.99.9(@swc/core@1.11.29)): + mini-css-extract-plugin@2.9.2(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: schema-utils: 4.3.2 tapable: 2.2.2 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) mini-css-extract-plugin@2.9.2(webpack@5.99.9(@swc/core@1.6.13)): dependencies: @@ -41983,11 +42209,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.99.9(@swc/core@1.11.29)): + null-loader@4.0.1(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) nullthrows@1.1.1: {} @@ -42700,32 +42926,41 @@ snapshots: '@csstools/utilities': 2.0.0(postcss@8.5.4) postcss: 8.5.4 - postcss-load-config@4.0.2(postcss@8.5.4)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)): + postcss-load-config@4.0.2(postcss@8.5.4)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/node@20.17.57)(typescript@5.9.2)): dependencies: lilconfig: 3.1.3 yaml: 2.8.0 optionalDependencies: postcss: 8.5.4 - ts-node: 10.9.2(@types/node@20.17.57)(typescript@5.9.2) + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2) - postcss-loader@7.3.4(postcss@8.5.4)(typescript@5.9.2)(webpack@5.99.9(@swc/core@1.11.29)): + postcss-load-config@4.0.2(postcss@8.5.4)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.0 + optionalDependencies: + postcss: 8.5.4 + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4) + optional: true + + postcss-loader@7.3.4(postcss@8.5.4)(typescript@5.9.2)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: cosmiconfig: 8.3.6(typescript@5.9.2) jiti: 1.21.7 postcss: 8.5.4 semver: 7.7.2 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) transitivePeerDependencies: - typescript - postcss-loader@8.1.1(@rspack/core@1.3.13)(postcss@8.5.2)(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + postcss-loader@8.1.1(@rspack/core@1.3.13)(postcss@8.5.2)(typescript@5.5.4)(webpack@5.98.0(@swc/core@1.11.29)): dependencies: cosmiconfig: 9.0.0(typescript@5.5.4) jiti: 1.21.7 postcss: 8.5.2 semver: 7.7.2 optionalDependencies: - '@rspack/core': 1.3.13 + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) transitivePeerDependencies: - typescript @@ -43492,11 +43727,11 @@ snapshots: dependencies: react: 18.3.1 - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.99.9(@swc/core@1.11.29)): + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: '@babel/runtime': 7.27.6 react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) react-native-builder-bob@0.30.3(typescript@5.9.2): dependencies: @@ -44162,7 +44397,7 @@ snapshots: - supports-color - utf-8-validate - react-navigation-stack@2.10.4(4a23q4g4mav7ddr6jlhxnyzzo4): + react-navigation-stack@2.10.4(1b7f2cbbd098c1646b3c5f57acc57915): dependencies: '@react-native-community/masked-view': 0.1.11(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) color: 3.2.1 @@ -44248,13 +44483,13 @@ snapshots: '@remix-run/router': 1.23.0 react: 18.3.1 - react-server-dom-webpack@19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.99.9): + react-server-dom-webpack@19.0.0-rc-6230622a1a-20240610(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.99.9(@swc/core@1.11.29)): dependencies: acorn-loose: 8.5.0 neo-async: 2.6.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - webpack: 5.99.9 + webpack: 5.99.9(@swc/core@1.11.29) react-shallow-renderer@16.15.0(react@18.3.1): dependencies: @@ -44989,18 +45224,18 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sass-loader@13.3.3(sass@1.89.1)(webpack@5.99.9): + sass-loader@13.3.3(sass@1.89.1)(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: neo-async: 2.6.2 - webpack: 5.99.9 + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) optionalDependencies: sass: 1.89.1 - sass-loader@16.0.5(@rspack/core@1.3.13)(sass@1.85.0)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + sass-loader@16.0.5(@rspack/core@1.3.13)(sass@1.85.0)(webpack@5.98.0(@swc/core@1.11.29)): dependencies: neo-async: 2.6.2 optionalDependencies: - '@rspack/core': 1.3.13 + '@rspack/core': 1.3.13(@swc/helpers@0.5.13) sass: 1.85.0 webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) @@ -45438,13 +45673,13 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + source-map-loader@5.0.0(webpack@5.98.0(@swc/core@1.11.29)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) - source-map-loader@5.0.0(webpack@5.99.9(webpack-cli@5.1.4)): + source-map-loader@5.0.0(webpack@5.99.9): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 @@ -45787,6 +46022,10 @@ snapshots: structured-headers@0.4.1: {} + style-loader@3.3.4(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): + dependencies: + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) + style-loader@3.3.4(webpack@5.99.9(@swc/core@1.11.29)): dependencies: webpack: 5.99.9(@swc/core@1.11.29) @@ -45795,10 +46034,6 @@ snapshots: dependencies: webpack: 5.99.9(@swc/core@1.6.13) - style-loader@3.3.4(webpack@5.99.9): - dependencies: - webpack: 5.99.9 - style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -45914,11 +46149,11 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - swc-loader@0.2.6(@swc/core@1.11.29)(webpack@5.99.9(@swc/core@1.11.29)): + swc-loader@0.2.6(@swc/core@1.11.29(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) '@swc/counter': 0.1.3 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) swiftlint@2.0.0(typescript@5.9.2): dependencies: @@ -45939,7 +46174,7 @@ snapshots: tabbable@6.2.0: {} - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)): + tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/node@20.17.57)(typescript@5.9.2)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -45958,7 +46193,7 @@ snapshots: postcss: 8.5.4 postcss-import: 15.1.0(postcss@8.5.4) postcss-js: 4.0.1(postcss@8.5.4) - postcss-load-config: 4.0.2(postcss@8.5.4)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)) + postcss-load-config: 4.0.2(postcss@8.5.4)(ts-node@10.9.2(@swc/core@1.11.29(@swc/helpers@0.5.13))(@types/node@20.17.57)(typescript@5.9.2)) postcss-nested: 6.2.0(postcss@8.5.4) postcss-selector-parser: 6.1.2 resolve: 1.22.10 @@ -45966,6 +46201,34 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.4 + postcss-import: 15.1.0(postcss@8.5.4) + postcss-js: 4.0.1(postcss@8.5.4) + postcss-load-config: 4.0.2(postcss@8.5.4)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4)) + postcss-nested: 6.2.0(postcss@8.5.4) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + optional: true + tamagui@1.79.6(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react-native-web@0.19.13(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): dependencies: '@tamagui/accordion': 1.79.6(react@18.3.1) @@ -46122,29 +46385,28 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(@swc/core@1.11.29)(esbuild@0.25.4)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + terser-webpack-plugin@5.3.14(@swc/core@1.11.29(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.40.0 - webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) optionalDependencies: - '@swc/core': 1.11.29 - esbuild: 0.25.4 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) - terser-webpack-plugin@5.3.14(@swc/core@1.11.29)(webpack@5.98.0(@swc/core@1.11.29)): + terser-webpack-plugin@5.3.14(@swc/core@1.11.29)(esbuild@0.25.4)(webpack@5.98.0(@swc/core@1.11.29)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.40.0 - webpack: 5.98.0(@swc/core@1.11.29) + webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) optionalDependencies: - '@swc/core': 1.11.29 - optional: true + '@swc/core': 1.11.29(@swc/helpers@0.5.13) + esbuild: 0.25.4 terser-webpack-plugin@5.3.14(@swc/core@1.11.29)(webpack@5.99.9(@swc/core@1.11.29)): dependencies: @@ -46155,7 +46417,7 @@ snapshots: terser: 5.40.0 webpack: 5.99.9(@swc/core@1.11.29) optionalDependencies: - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) terser-webpack-plugin@5.3.14(@swc/core@1.6.13)(webpack@5.99.9(@swc/core@1.6.13)): dependencies: @@ -46168,15 +46430,6 @@ snapshots: optionalDependencies: '@swc/core': 1.6.13 - terser-webpack-plugin@5.3.14(webpack@5.99.9(webpack-cli@5.1.4)): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 4.3.2 - serialize-javascript: 6.0.2 - terser: 5.40.0 - webpack: 5.99.9(webpack-cli@5.1.4) - terser-webpack-plugin@5.3.14(webpack@5.99.9): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -46184,7 +46437,7 @@ snapshots: schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.40.0 - webpack: 5.99.9 + webpack: 5.99.9(webpack-cli@5.1.4) terser@5.39.0: dependencies: @@ -46353,12 +46606,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.3.4(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4): + ts-jest@29.3.4(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)))(typescript@5.0.4): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4)) + jest: 29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -46383,47 +46636,49 @@ snapshots: typescript: 5.9.2 webpack: 5.99.9(@swc/core@1.11.29) - ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.3.3): + ts-node@10.9.2(@swc/core@1.11.29)(@types/node@20.17.57)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.2.0 + '@types/node': 20.17.57 acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.3.3 + typescript: 5.9.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) + optional: true - ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4): + ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.0.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.2.0 + '@types/node': 22.15.29 acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.4 + typescript: 5.0.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) + optional: true - ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2): + ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.3.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -46437,40 +46692,40 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.9.2 + typescript: 5.3.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) - ts-node@10.9.2(@swc/core@1.6.13)(@types/node@20.17.57)(typescript@4.5.5): + ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.5.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.57 + '@types/node': 24.2.0 acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.5.5 + typescript: 5.5.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.6.13 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) - ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2): + ts-node@10.9.2(@swc/core@1.11.29)(@types/node@24.2.0)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.17.57 + '@types/node': 24.2.0 acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 @@ -46480,26 +46735,28 @@ snapshots: typescript: 5.9.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - optional: true + optionalDependencies: + '@swc/core': 1.11.29(@swc/helpers@0.5.13) - ts-node@10.9.2(@types/node@22.15.29)(typescript@5.0.4): + ts-node@10.9.2(@swc/core@1.6.13)(@types/node@20.17.57)(typescript@4.5.5): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.15.29 + '@types/node': 20.17.57 acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.0.4 + typescript: 4.5.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - optional: true + optionalDependencies: + '@swc/core': 1.6.13 ts-object-utils@0.0.5: {} @@ -46940,14 +47197,14 @@ snapshots: url-join@4.0.1: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29)))(webpack@5.99.9(@swc/core@1.11.29)): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) optionalDependencies: - file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29)) + file-loader: 6.2.0(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) url-parse@1.5.10: dependencies: @@ -47125,7 +47382,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@2.79.2)(vite@5.4.19(@types/node@20.17.57)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@2.79.2) - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) uuid: 10.0.0 vite: 5.4.19(@types/node@20.17.57)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) transitivePeerDependencies: @@ -47135,7 +47392,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.52.5)(vite@5.4.19(@types/node@20.17.57)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.52.5) - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) uuid: 10.0.0 vite: 5.4.19(@types/node@20.17.57)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) transitivePeerDependencies: @@ -47145,7 +47402,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.52.5)(vite@5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.52.5) - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) uuid: 10.0.0 vite: 5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) transitivePeerDependencies: @@ -47155,7 +47412,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.52.5)(vite@6.3.5(@types/node@20.17.57)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.52.5) - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) uuid: 10.0.0 vite: 6.3.5(@types/node@20.17.57)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: @@ -47165,7 +47422,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.52.5)(vite@6.3.5(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.52.5) - '@swc/core': 1.11.29 + '@swc/core': 1.11.29(@swc/helpers@0.5.13) uuid: 10.0.0 vite: 6.3.5(@types/node@24.2.0)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: @@ -47174,7 +47431,7 @@ snapshots: vite-plugin-vuetify@2.1.1(vite@5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0))(vue@3.4.21(typescript@5.9.2))(vuetify@3.6.8): dependencies: - '@vuetify/loader-shared': 2.1.0(vue@3.4.21(typescript@5.9.2))(vuetify@3.6.8(typescript@5.9.2)(vite-plugin-vuetify@2.1.1)(vue@3.4.21(typescript@5.9.2))) + '@vuetify/loader-shared': 2.1.0(vue@3.4.21(typescript@5.9.2))(vuetify@3.6.8) debug: 4.4.1(supports-color@8.1.1) upath: 2.0.1 vite: 5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) @@ -47461,9 +47718,9 @@ snapshots: webpack-cli@5.1.4(webpack@5.99.9): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack@5.99.9))(webpack@5.99.9(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.99.9) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.99.9) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.99.9) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.6 @@ -47475,6 +47732,15 @@ snapshots: webpack: 5.99.9(webpack-cli@5.1.4) webpack-merge: 5.10.0 + webpack-dev-middleware@5.3.4(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.2 + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) + webpack-dev-middleware@5.3.4(webpack@5.99.9(@swc/core@1.11.29)): dependencies: colorette: 2.0.20 @@ -47484,7 +47750,7 @@ snapshots: schema-utils: 4.3.2 webpack: 5.99.9(@swc/core@1.11.29) - webpack-dev-middleware@7.4.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + webpack-dev-middleware@7.4.2(webpack@5.98.0(@swc/core@1.11.29)): dependencies: colorette: 2.0.20 memfs: 4.17.2 @@ -47535,7 +47801,47 @@ snapshots: - supports-color - utf-8-validate - webpack-dev-server@5.2.0(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + webpack-dev-server@4.15.2(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.22 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.7 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.0 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.21.2 + graceful-fs: 4.2.11 + html-entities: 2.6.0 + http-proxy-middleware: 2.0.9(@types/express@4.17.22)(debug@4.4.1) + ipaddr.js: 2.2.0 + launch-editor: 2.10.0 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.3.2 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 5.3.4(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) + ws: 8.18.2 + optionalDependencies: + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-dev-server@5.2.0(webpack@5.98.0(@swc/core@1.11.29)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -47562,7 +47868,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + webpack-dev-middleware: 7.4.2(webpack@5.98.0(@swc/core@1.11.29)) ws: 8.18.2 optionalDependencies: webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) @@ -47588,7 +47894,7 @@ snapshots: webpack-sources@3.3.0: {} - webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)): + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.3(@rspack/core@1.3.13)(webpack@5.98.0(@swc/core@1.11.29)))(webpack@5.98.0(@swc/core@1.11.29)): dependencies: typed-assert: 1.0.9 webpack: 5.98.0(@swc/core@1.11.29)(esbuild@0.25.4) @@ -47597,37 +47903,6 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.98.0(@swc/core@1.11.29): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 - browserslist: 4.25.0 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(@swc/core@1.11.29)(webpack@5.98.0(@swc/core@1.11.29)) - watchpack: 2.4.4 - webpack-sources: 3.3.0 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - optional: true - webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4): dependencies: '@types/eslint-scope': 3.7.7 @@ -47650,7 +47925,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(@swc/core@1.11.29)(esbuild@0.25.4)(webpack@5.98.0(@swc/core@1.11.29)(esbuild@0.25.4)) + terser-webpack-plugin: 5.3.14(@swc/core@1.11.29)(esbuild@0.25.4)(webpack@5.98.0(@swc/core@1.11.29)) watchpack: 2.4.4 webpack-sources: 3.3.0 transitivePeerDependencies: @@ -47658,7 +47933,7 @@ snapshots: - esbuild - uglify-js - webpack@5.99.9: + webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.7 @@ -47681,7 +47956,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(webpack@5.99.9) + terser-webpack-plugin: 5.3.14(@swc/core@1.11.29(@swc/helpers@0.5.13))(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))) watchpack: 2.4.4 webpack-sources: 3.3.0 transitivePeerDependencies: @@ -47774,7 +48049,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(webpack@5.99.9(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.14(webpack@5.99.9) watchpack: 2.4.4 webpack-sources: 3.3.0 optionalDependencies: @@ -47784,7 +48059,7 @@ snapshots: - esbuild - uglify-js - webpackbar@6.0.1(webpack@5.99.9(@swc/core@1.11.29)): + webpackbar@6.0.1(webpack@5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13))): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -47793,7 +48068,7 @@ snapshots: markdown-table: 2.0.0 pretty-time: 1.1.0 std-env: 3.9.0 - webpack: 5.99.9(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29(@swc/helpers@0.5.13)) wrap-ansi: 7.0.0 websocket-driver@0.7.4: From 58d91945abc399805b818224a1001d8c81ec1bfe Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Mon, 1 Dec 2025 17:04:15 +0200 Subject: [PATCH 29/44] Common changeset. --- .changeset/witty-steaks-worry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/witty-steaks-worry.md diff --git a/.changeset/witty-steaks-worry.md b/.changeset/witty-steaks-worry.md new file mode 100644 index 000000000..d7ec22d0a --- /dev/null +++ b/.changeset/witty-steaks-worry.md @@ -0,0 +1,5 @@ +--- +'@powersync/common': minor +--- + +Serializing upload and download errors for SyncStatus events. Small changes to how delay values are passed to the sync implementation internally. From 1ef44a3796b39e90810759cb91e1bbff9e579686 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 2 Dec 2025 10:48:34 +0200 Subject: [PATCH 30/44] Try and catch Accesshandle errors. Fork OPFSCoopSyncVFS for potentially lock bug fix. --- .../db/adapters/LockedAsyncDatabaseAdapter.ts | 5 +- .../adapters/wa-sqlite/WASQLiteConnection.ts | 8 +- .../adapters/wa-sqlite/WASQLiteOpenFactory.ts | 6 +- .../web/src/worker/db/WASQLiteDB.worker.ts | 4 +- packages/web/src/worker/db/opfs.ts | 618 ++++++++++++++++++ 5 files changed, 631 insertions(+), 10 deletions(-) create mode 100644 packages/web/src/worker/db/opfs.ts diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index e44d04287..806f01917 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -295,7 +295,10 @@ export class LockedAsyncDatabaseAdapter holdId = this.requiresHolds ? await this.baseDB.markHold() : null; return await callback(); } catch (ex) { - if (ex instanceof ConnectionClosedError) { + if ( + ex instanceof ConnectionClosedError || + (ex instanceof Error && ex.name === 'NoModificationAllowedError') + ) { if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) { // Immediately re-open the database. We need to miss as little table updates as possible. this.reOpenInternalDB(); diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts index 8860a7233..02841c0ca 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts @@ -1,9 +1,9 @@ import * as SQLite from '@journeyapps/wa-sqlite'; import { BaseObserver, BatchedUpdateNotification } from '@powersync/common'; import { Mutex } from 'async-mutex'; +import { OPFSCoopSyncVFS } from '../../../worker/db/opfs'; import { AsyncDatabaseConnection, OnTableChangeCallback, ProxiedQueryResult } from '../AsyncDatabaseConnection'; import { ResolvedWASQLiteOpenFactoryOptions } from './WASQLiteOpenFactory'; - /** * List of currently tested virtual filesystems */ @@ -124,11 +124,11 @@ export const DEFAULT_MODULE_FACTORIES = { } else { module = await SyncWASQLiteModuleFactory(); } - // @ts-expect-error The types for this static method are missing upstream - const { OPFSCoopSyncVFS } = await import('@journeyapps/wa-sqlite/src/examples/OPFSCoopSyncVFS.js'); + const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module); + (vfs as any).log = (...args: any[]) => console.log('[OPFSCoopSyncVFS]', ...args); return { module, - vfs: await OPFSCoopSyncVFS.create(options.dbFileName, module) + vfs: vfs as any }; } }; diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts index 487d121aa..fa4528907 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteOpenFactory.ts @@ -1,17 +1,17 @@ -import { type ILogLevel, DBAdapter } from '@powersync/common'; +import { DBAdapter, type ILogLevel } from '@powersync/common'; import * as Comlink from 'comlink'; import { openWorkerDatabasePort, resolveWorkerDatabasePortFactory } from '../../../worker/db/open-worker-database'; import { AbstractWebSQLOpenFactory } from '../AbstractWebSQLOpenFactory'; import { AsyncDatabaseConnection, OpenAsyncDatabaseConnection } from '../AsyncDatabaseConnection'; import { LockedAsyncDatabaseAdapter } from '../LockedAsyncDatabaseAdapter'; +import { WorkerWrappedAsyncDatabaseConnection } from '../WorkerWrappedAsyncDatabaseConnection'; import { DEFAULT_CACHE_SIZE_KB, ResolvedWebSQLOpenOptions, TemporaryStorageOption, WebSQLOpenFactoryOptions } from '../web-sql-flags'; -import { WorkerWrappedAsyncDatabaseConnection } from '../WorkerWrappedAsyncDatabaseConnection'; -import { WASqliteConnection, WASQLiteVFS } from './WASQLiteConnection'; +import { WASQLiteVFS, WASqliteConnection } from './WASQLiteConnection'; export interface WASQLiteOpenFactoryOptions extends WebSQLOpenFactoryOptions { vfs?: WASQLiteVFS; diff --git a/packages/web/src/worker/db/WASQLiteDB.worker.ts b/packages/web/src/worker/db/WASQLiteDB.worker.ts index 4f40fdad4..c09d4fd06 100644 --- a/packages/web/src/worker/db/WASQLiteDB.worker.ts +++ b/packages/web/src/worker/db/WASQLiteDB.worker.ts @@ -16,13 +16,13 @@ baseLogger.useDefaults(); const logger = createLogger('db-worker'); const DBMap = new Map(); -const OPEN_DB_LOCK = 'open-wasqlite-db'; let nextClientId = 1; const openDBShared = async (options: WorkerDBOpenerOptions): Promise => { // Prevent multiple simultaneous opens from causing race conditions - return getNavigatorLocks().request(OPEN_DB_LOCK, async () => { + // Use the same lock as a write lock to prevent concurrent writes to the database. + return getNavigatorLocks().request(`db-lock-${options.dbFilename}`, async () => { const clientId = nextClientId++; const { dbFilename, logLevel } = options; diff --git a/packages/web/src/worker/db/opfs.ts b/packages/web/src/worker/db/opfs.ts new file mode 100644 index 000000000..a102b177d --- /dev/null +++ b/packages/web/src/worker/db/opfs.ts @@ -0,0 +1,618 @@ +// Copyright 2024 Roy T. Hashimoto. All Rights Reserved. +// @ts-nocheck +import { FacadeVFS } from '@journeyapps/wa-sqlite/src/FacadeVFS.js'; +import * as VFS from '@journeyapps/wa-sqlite/src/VFS.js'; + +const DEFAULT_TEMPORARY_FILES = 10; +const LOCK_NOTIFY_INTERVAL = 1000; + +const DB_RELATED_FILE_SUFFIXES = ['', '-journal', '-wal']; + +const finalizationRegistry = new FinalizationRegistry((releaser) => releaser()); + +class File { + /** @type {string} */ path; + /** @type {number} */ flags; + /** @type {FileSystemSyncAccessHandle} */ accessHandle; + + /** @type {PersistentFile?} */ persistentFile; + + constructor(path, flags) { + this.path = path; + this.flags = flags; + } +} + +class PersistentFile { + /** @type {FileSystemFileHandle} */ fileHandle; + /** @type {FileSystemSyncAccessHandle} */ accessHandle = null; + + // The following properties are for main database files. + + /** @type {boolean} */ isLockBusy = false; + /** @type {boolean} */ isFileLocked = false; + /** @type {boolean} */ isRequestInProgress = false; + /** @type {function} */ handleLockReleaser = null; + + /** @type {BroadcastChannel} */ handleRequestChannel; + /** @type {boolean} */ isHandleRequested = false; + + constructor(fileHandle) { + this.fileHandle = fileHandle; + } +} + +export class OPFSCoopSyncVFS extends FacadeVFS { + /** @type {Map} */ mapIdToFile = new Map(); + + lastError = null; + log = null; //function(...args) { console.log(`[${contextName}]`, ...args) }; + + /** @type {Map} */ persistentFiles = new Map(); + /** @type {Map} */ boundAccessHandles = new Map(); + /** @type {Set} */ unboundAccessHandles = new Set(); + /** @type {Set} */ accessiblePaths = new Set(); + releaser = null; + + static async create(name, module) { + const vfs = new OPFSCoopSyncVFS(name, module); + await Promise.all([vfs.isReady(), vfs.#initialize(DEFAULT_TEMPORARY_FILES)]); + return vfs; + } + + constructor(name, module) { + super(name, module); + } + + async #initialize(nTemporaryFiles) { + // Delete temporary directories no longer in use. + const root = await navigator.storage.getDirectory(); + // @ts-ignore + for await (const entry of root.values()) { + if (entry.kind === 'directory' && entry.name.startsWith('.ahp-')) { + // A lock with the same name as the directory protects it from + // being deleted. + await navigator.locks.request(entry.name, { ifAvailable: true }, async (lock) => { + if (lock) { + this.log?.(`Deleting temporary directory ${entry.name}`); + await root.removeEntry(entry.name, { recursive: true }); + } else { + this.log?.(`Temporary directory ${entry.name} is in use`); + } + }); + } + } + + // Create our temporary directory. + const tmpDirName = `.ahp-${Math.random().toString(36).slice(2)}`; + this.releaser = await new Promise((resolve) => { + navigator.locks.request(tmpDirName, () => { + return new Promise((release) => { + resolve(release); + }); + }); + }); + finalizationRegistry.register(this, this.releaser); + const tmpDir = await root.getDirectoryHandle(tmpDirName, { create: true }); + + // Populate temporary directory. + for (let i = 0; i < nTemporaryFiles; i++) { + const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true }); + const tmpAccessHandle = await tmpFile.createSyncAccessHandle(); + this.unboundAccessHandles.add(tmpAccessHandle); + } + } + + /** + * @param {string?} zName + * @param {number} fileId + * @param {number} flags + * @param {DataView} pOutFlags + * @returns {number} + */ + jOpen(zName, fileId, flags, pOutFlags) { + try { + const url = new URL(zName || Math.random().toString(36).slice(2), 'file://'); + const path = url.pathname; + + if (flags & VFS.SQLITE_OPEN_MAIN_DB) { + const persistentFile = this.persistentFiles.get(path); + if (persistentFile?.isRequestInProgress) { + // Should not reach here unless SQLite itself retries an open. + // Otherwise, asynchronous operations started on a previous + // open try should have completed. + return VFS.SQLITE_BUSY; + } else if (!persistentFile) { + // This is the usual starting point for opening a database. + // Register a Promise that resolves when the database and related + // files are ready to be used. + this.log?.(`creating persistent file for ${path}`); + const create = !!(flags & VFS.SQLITE_OPEN_CREATE); + this._module.retryOps.push( + (async () => { + try { + // Get the path directory handle. + let dirHandle = await navigator.storage.getDirectory(); + const directories = path.split('/').filter((d) => d); + const filename = directories.pop(); + for (const directory of directories) { + dirHandle = await dirHandle.getDirectoryHandle(directory, { create }); + } + + // Get file handles for the database and related files, + // and create persistent file instances. + for (const suffix of DB_RELATED_FILE_SUFFIXES) { + const fileHandle = await dirHandle.getFileHandle(filename + suffix, { create }); + await this.#createPersistentFile(fileHandle); + } + + // Get access handles for the files. + const file = new File(path, flags); + file.persistentFile = this.persistentFiles.get(path); + await this.#requestAccessHandle(file); + } catch (e) { + // Use an invalid persistent file to signal this error + // for the retried open. + const persistentFile = new PersistentFile(null); + this.persistentFiles.set(path, persistentFile); + console.error(e); + } + })() + ); + return VFS.SQLITE_BUSY; + } else if (!persistentFile.fileHandle) { + // The asynchronous open operation failed. + this.persistentFiles.delete(path); + return VFS.SQLITE_CANTOPEN; + } else if (!persistentFile.accessHandle) { + // This branch is reached if the database was previously opened + // and closed. + this._module.retryOps.push( + (async () => { + const file = new File(path, flags); + file.persistentFile = this.persistentFiles.get(path); + await this.#requestAccessHandle(file); + })() + ); + return VFS.SQLITE_BUSY; + } + } + + if (!this.accessiblePaths.has(path) && !(flags & VFS.SQLITE_OPEN_CREATE)) { + throw new Error(`File ${path} not found`); + } + + const file = new File(path, flags); + this.mapIdToFile.set(fileId, file); + + if (this.persistentFiles.has(path)) { + file.persistentFile = this.persistentFiles.get(path); + } else if (this.boundAccessHandles.has(path)) { + // This temporary file was previously created and closed. Reopen + // the same access handle. + file.accessHandle = this.boundAccessHandles.get(path); + } else if (this.unboundAccessHandles.size) { + // Associate an unbound access handle to this file. + file.accessHandle = this.unboundAccessHandles.values().next().value; + file.accessHandle.truncate(0); + this.unboundAccessHandles.delete(file.accessHandle); + this.boundAccessHandles.set(path, file.accessHandle); + } + this.accessiblePaths.add(path); + + pOutFlags.setInt32(0, flags, true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_CANTOPEN; + } + } + + /** + * @param {string} zName + * @param {number} syncDir + * @returns {number} + */ + jDelete(zName, syncDir) { + try { + const url = new URL(zName, 'file://'); + const path = url.pathname; + if (this.persistentFiles.has(path)) { + const persistentFile = this.persistentFiles.get(path); + persistentFile.accessHandle.truncate(0); + } else { + this.boundAccessHandles.get(path)?.truncate(0); + } + this.accessiblePaths.delete(path); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_DELETE; + } + } + + /** + * @param {string} zName + * @param {number} flags + * @param {DataView} pResOut + * @returns {number} + */ + jAccess(zName, flags, pResOut) { + try { + const url = new URL(zName, 'file://'); + const path = url.pathname; + pResOut.setInt32(0, this.accessiblePaths.has(path) ? 1 : 0, true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_ACCESS; + } + } + + /** + * @param {number} fileId + * @returns {number} + */ + jClose(fileId) { + try { + const file = this.mapIdToFile.get(fileId); + this.mapIdToFile.delete(fileId); + + if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) { + // Always close handles directly as well to ensure cleanup + // This handles edge cases where the lock mechanism might not work properly + if (file.persistentFile) { + DB_RELATED_FILE_SUFFIXES.forEach((suffix) => { + const persistentFile = this.persistentFiles.get(file.path + suffix); + if (persistentFile?.accessHandle) { + persistentFile.accessHandle.close(); + persistentFile.accessHandle = null; + } + }); + } + if (file.persistentFile?.handleLockReleaser) { + // Normal case: release via the lock mechanism + this.#releaseAccessHandle(file); + } + } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { + file.accessHandle.truncate(0); + this.accessiblePaths.delete(file.path); + if (!this.persistentFiles.has(file.path)) { + this.boundAccessHandles.delete(file.path); + this.unboundAccessHandles.add(file.accessHandle); + } + } + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_CLOSE; + } + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + jRead(fileId, pData, iOffset) { + try { + const file = this.mapIdToFile.get(fileId); + + // On Chrome (at least), passing pData to accessHandle.read() is + // an error because pData is a Proxy of a Uint8Array. Calling + // subarray() produces a real Uint8Array and that works. + const accessHandle = file.accessHandle || file.persistentFile.accessHandle; + const bytesRead = accessHandle.read(pData.subarray(), { at: iOffset }); + + // Opening a database file performs one read without a xLock call. + if (file.flags & VFS.SQLITE_OPEN_MAIN_DB && !file.persistentFile.isFileLocked) { + this.#releaseAccessHandle(file); + } + + if (bytesRead < pData.byteLength) { + pData.fill(0, bytesRead); + return VFS.SQLITE_IOERR_SHORT_READ; + } + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_READ; + } + } + + /** + * @param {number} fileId + * @param {Uint8Array} pData + * @param {number} iOffset + * @returns {number} + */ + jWrite(fileId, pData, iOffset) { + try { + const file = this.mapIdToFile.get(fileId); + + // On Chrome (at least), passing pData to accessHandle.write() is + // an error because pData is a Proxy of a Uint8Array. Calling + // subarray() produces a real Uint8Array and that works. + const accessHandle = file.accessHandle || file.persistentFile.accessHandle; + const nBytes = accessHandle.write(pData.subarray(), { at: iOffset }); + if (nBytes !== pData.byteLength) throw new Error('short write'); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_WRITE; + } + } + + /** + * @param {number} fileId + * @param {number} iSize + * @returns {number} + */ + jTruncate(fileId, iSize) { + try { + const file = this.mapIdToFile.get(fileId); + const accessHandle = file.accessHandle || file.persistentFile.accessHandle; + accessHandle.truncate(iSize); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_TRUNCATE; + } + } + + /** + * @param {number} fileId + * @param {number} flags + * @returns {number} + */ + jSync(fileId, flags) { + try { + const file = this.mapIdToFile.get(fileId); + const accessHandle = file.accessHandle || file.persistentFile.accessHandle; + accessHandle.flush(); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_FSYNC; + } + } + + /** + * @param {number} fileId + * @param {DataView} pSize64 + * @returns {number} + */ + jFileSize(fileId, pSize64) { + try { + const file = this.mapIdToFile.get(fileId); + const accessHandle = file.accessHandle || file.persistentFile.accessHandle; + const size = accessHandle.getSize(); + pSize64.setBigInt64(0, BigInt(size), true); + return VFS.SQLITE_OK; + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR_FSTAT; + } + } + + /** + * @param {number} fileId + * @param {number} lockType + * @returns {number} + */ + jLock(fileId, lockType) { + const file = this.mapIdToFile.get(fileId); + if (file.persistentFile.isRequestInProgress) { + file.persistentFile.isLockBusy = true; + return VFS.SQLITE_BUSY; + } + + file.persistentFile.isFileLocked = true; + if (!file.persistentFile.handleLockReleaser) { + // Start listening for notifications from other connections. + // This is before we actually get access handles, but waiting to + // listen until then allows a race condition where notifications + // are missed. + file.persistentFile.handleRequestChannel.onmessage = () => { + this.log?.(`received notification for ${file.path}`); + if (file.persistentFile.isFileLocked) { + // We're still using the access handle, so mark it to be + // released when we're done. + file.persistentFile.isHandleRequested = true; + } else { + // Release the access handles immediately. + this.#releaseAccessHandle(file); + } + file.persistentFile.handleRequestChannel.onmessage = null; + }; + + this.#requestAccessHandle(file); + this.log?.('returning SQLITE_BUSY'); + file.persistentFile.isLockBusy = true; + return VFS.SQLITE_BUSY; + } + file.persistentFile.isLockBusy = false; + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {number} lockType + * @returns {number} + */ + jUnlock(fileId, lockType) { + const file = this.mapIdToFile.get(fileId); + if (lockType === VFS.SQLITE_LOCK_NONE) { + // Don't change any state if this unlock is because xLock returned + // SQLITE_BUSY. + if (!file.persistentFile.isLockBusy) { + if (file.persistentFile.isHandleRequested) { + // Another connection wants the access handle. + this.#releaseAccessHandle(file); + file.persistentFile.isHandleRequested = false; + } + file.persistentFile.isFileLocked = false; + } + } + return VFS.SQLITE_OK; + } + + /** + * @param {number} fileId + * @param {number} op + * @param {DataView} pArg + * @returns {number|Promise} + */ + jFileControl(fileId, op, pArg) { + try { + const file = this.mapIdToFile.get(fileId); + switch (op) { + case VFS.SQLITE_FCNTL_PRAGMA: + const key = extractString(pArg, 4); + const value = extractString(pArg, 8); + this.log?.('xFileControl', file.path, 'PRAGMA', key, value); + switch (key.toLowerCase()) { + case 'journal_mode': + if (value && !['off', 'memory', 'delete', 'wal'].includes(value.toLowerCase())) { + throw new Error('journal_mode must be "off", "memory", "delete", or "wal"'); + } + break; + } + break; + } + } catch (e) { + this.lastError = e; + return VFS.SQLITE_IOERR; + } + return VFS.SQLITE_NOTFOUND; + } + + /** + * @param {Uint8Array} zBuf + * @returns + */ + jGetLastError(zBuf) { + if (this.lastError) { + console.error(this.lastError); + const outputArray = zBuf.subarray(0, zBuf.byteLength - 1); + const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray); + zBuf[written] = 0; + } + return VFS.SQLITE_OK; + } + + /** + * @param {FileSystemFileHandle} fileHandle + * @returns {Promise} + */ + async #createPersistentFile(fileHandle) { + const persistentFile = new PersistentFile(fileHandle); + const root = await navigator.storage.getDirectory(); + const relativePath = await root.resolve(fileHandle); + const path = `/${relativePath.join('/')}`; + persistentFile.handleRequestChannel = new BroadcastChannel(`ahp:${path}`); + this.persistentFiles.set(path, persistentFile); + + const f = await fileHandle.getFile(); + if (f.size) { + this.accessiblePaths.add(path); + } + return persistentFile; + } + + /** + * @param {File} file + */ + #requestAccessHandle(file) { + console.assert(!file.persistentFile.handleLockReleaser); + if (!file.persistentFile.isRequestInProgress) { + file.persistentFile.isRequestInProgress = true; + this._module.retryOps.push( + (async () => { + // Acquire the Web Lock. + file.persistentFile.handleLockReleaser = await this.#acquireLock(file.persistentFile); + + try { + // Get access handles for the database and releated files in parallel. + this.log?.(`creating access handles for ${file.path}`); + await Promise.all( + DB_RELATED_FILE_SUFFIXES.map(async (suffix) => { + const persistentFile = this.persistentFiles.get(file.path + suffix); + if (persistentFile) { + persistentFile.accessHandle = await persistentFile.fileHandle.createSyncAccessHandle(); + } + }) + ); + file.persistentFile.isRequestInProgress = false; + } catch (e) { + this.log?.(`failed to create access handles for ${file.path}`, e); + file.persistentFile.handleLockReleaser(); + throw e; + } + })() + ); + return this._module.retryOps.at(-1); + } + return Promise.resolve(); + } + + /** + * @param {File} file + */ + async #releaseAccessHandle(file) { + DB_RELATED_FILE_SUFFIXES.forEach(async (suffix) => { + const persistentFile = this.persistentFiles.get(file.path + suffix); + if (persistentFile) { + persistentFile.accessHandle?.close(); + persistentFile.accessHandle = null; + } + }); + this.log?.(`access handles closed for ${file.path}`); + + file.persistentFile.handleLockReleaser?.(); + file.persistentFile.handleLockReleaser = null; + this.log?.(`lock released for ${file.path}`); + } + + /** + * @param {PersistentFile} persistentFile + * @returns {Promise} lock releaser + */ + #acquireLock(persistentFile) { + return new Promise((resolve) => { + const lockName = persistentFile.handleRequestChannel.name; + const notify = () => { + this.log?.(`notifying for ${lockName}`); + persistentFile.handleRequestChannel.postMessage(null); + }; + const notifyId = setInterval(notify, LOCK_NOTIFY_INTERVAL); + setTimeout(notify); + + this.log?.(`lock requested: ${lockName}`); + let releaseLock; // This will hold the resolver for the lock Promise + const lockPromise = new Promise((release) => { + releaseLock = release; // Capture the resolver + }); + + navigator.locks.request(lockName, (lock) => { + this.log?.(`lock acquired: ${lockName}`, lock); + clearInterval(notifyId); + // Resolve the outer Promise with a function that releases the lock + resolve(() => { + releaseLock(); // This resolves lockPromise, releasing the lock + }); + return lockPromise; // Return the Promise that controls the lock + }); + }); + } +} + +function extractString(dataView, offset) { + const p = dataView.getUint32(offset, true); + if (p) { + const chars = new Uint8Array(dataView.buffer, p); + return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0))); + } + return null; +} From d73d9d265091232bb9d5701f66268f3128098a95 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 2 Dec 2025 14:02:41 +0200 Subject: [PATCH 31/44] Cleanup aborted operations if aborted. Try and gracefully handle Accesshandle errors. --- .../db/adapters/LockedAsyncDatabaseAdapter.ts | 12 ++---- packages/web/src/worker/db/opfs.ts | 30 +++------------ .../worker/sync/SharedSyncImplementation.ts | 37 +++++++++++++++---- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 806f01917..7958afba9 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -282,14 +282,10 @@ export class LockedAsyncDatabaseAdapter try { // The database is being opened in the background. Wait for it here. if (this.databaseOpenPromise) { - try { - await this.databaseOpenPromise; - } catch (ex) { - // This will cause a retry of opening the database. - const wrappedError = new ConnectionClosedError('Could not open database'); - wrappedError.cause = ex; - throw wrappedError; - } + /** + * We can't await this since it uses the same lock as we're in now. + */ + throw new ConnectionClosedError('Connection is busy re-opening'); } holdId = this.requiresHolds ? await this.baseDB.markHold() : null; diff --git a/packages/web/src/worker/db/opfs.ts b/packages/web/src/worker/db/opfs.ts index a102b177d..341c20415 100644 --- a/packages/web/src/worker/db/opfs.ts +++ b/packages/web/src/worker/db/opfs.ts @@ -259,19 +259,7 @@ export class OPFSCoopSyncVFS extends FacadeVFS { this.mapIdToFile.delete(fileId); if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) { - // Always close handles directly as well to ensure cleanup - // This handles edge cases where the lock mechanism might not work properly - if (file.persistentFile) { - DB_RELATED_FILE_SUFFIXES.forEach((suffix) => { - const persistentFile = this.persistentFiles.get(file.path + suffix); - if (persistentFile?.accessHandle) { - persistentFile.accessHandle.close(); - persistentFile.accessHandle = null; - } - }); - } if (file.persistentFile?.handleLockReleaser) { - // Normal case: release via the lock mechanism this.#releaseAccessHandle(file); } } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { @@ -532,7 +520,6 @@ export class OPFSCoopSyncVFS extends FacadeVFS { (async () => { // Acquire the Web Lock. file.persistentFile.handleLockReleaser = await this.#acquireLock(file.persistentFile); - try { // Get access handles for the database and releated files in parallel. this.log?.(`creating access handles for ${file.path}`); @@ -544,11 +531,13 @@ export class OPFSCoopSyncVFS extends FacadeVFS { } }) ); - file.persistentFile.isRequestInProgress = false; } catch (e) { this.log?.(`failed to create access handles for ${file.path}`, e); + // Release the lock, if we failed here, we'd need to obtain the lock later in order to retry file.persistentFile.handleLockReleaser(); throw e; + } finally { + file.persistentFile.isRequestInProgress = false; } })() ); @@ -581,6 +570,7 @@ export class OPFSCoopSyncVFS extends FacadeVFS { */ #acquireLock(persistentFile) { return new Promise((resolve) => { + // Tell other connections we want the access handle. const lockName = persistentFile.handleRequestChannel.name; const notify = () => { this.log?.(`notifying for ${lockName}`); @@ -590,19 +580,11 @@ export class OPFSCoopSyncVFS extends FacadeVFS { setTimeout(notify); this.log?.(`lock requested: ${lockName}`); - let releaseLock; // This will hold the resolver for the lock Promise - const lockPromise = new Promise((release) => { - releaseLock = release; // Capture the resolver - }); - navigator.locks.request(lockName, (lock) => { + // We have the lock. Stop asking other connections for it. this.log?.(`lock acquired: ${lockName}`, lock); clearInterval(notifyId); - // Resolve the outer Promise with a function that releases the lock - resolve(() => { - releaseLock(); // This resolves lockPromise, releasing the lock - }); - return lockPromise; // Return the Promise that controls the lock + return new Promise(resolve); }); }); } diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 0dee7a711..39e0a82f2 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -525,12 +525,16 @@ export class SharedSyncImplementation extends BaseObserver client.clientProvider.getDBWorkerPort(), abortController.signal).catch( - (ex) => { - removeCloseListener(); - throw ex; + const workerPort = await withAbort({ + action: () => client.clientProvider.getDBWorkerPort(), + signal: abortController.signal, + cleanupOnAbort: (port) => { + port.close(); } - ); + }).catch((ex) => { + removeCloseListener(); + throw ex; + }); const remote = Comlink.wrap(workerPort); const identifier = this.syncParams!.dbParams.dbFilename; @@ -541,7 +545,13 @@ export class SharedSyncImplementation extends BaseObserver remote(this.syncParams!.dbParams), abortController.signal).finally(() => { + const db = await withAbort({ + action: () => remote(this.syncParams!.dbParams), + signal: abortController.signal, + cleanupOnAbort: (db) => { + db.close(); + } + }).finally(() => { // We can remove the close listener here since we no longer need it past this point. removeCloseListener(); }); @@ -588,7 +598,12 @@ export class SharedSyncImplementation extends BaseObserver(action: () => Promise, signal: AbortSignal): Promise { +function withAbort(options: { + action: () => Promise; + signal: AbortSignal; + cleanupOnAbort?: (result: T) => void; +}): Promise { + const { action, signal, cleanupOnAbort } = options; return new Promise((resolve, reject) => { if (signal.aborted) { reject(new AbortOperation('Operation aborted by abort controller')); @@ -608,7 +623,13 @@ function withAbort(action: () => Promise, signal: AbortSignal): Promise } action() - .then((data) => completePromise(() => resolve(data))) + .then((data) => { + completePromise(() => resolve(data)); + // We already rejected due to the abort, allow for cleanup + if (signal.aborted) { + cleanupOnAbort?.(data); + } + }) .catch((e) => completePromise(() => reject(e))); }); } From a1fd0bbc6d9a4aa5f1d0eef080afbaf4f300f51b Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 2 Dec 2025 14:14:15 +0200 Subject: [PATCH 32/44] cleanup --- packages/web/src/worker/sync/SharedSyncImplementation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/worker/sync/SharedSyncImplementation.ts b/packages/web/src/worker/sync/SharedSyncImplementation.ts index 39e0a82f2..9c352dd64 100644 --- a/packages/web/src/worker/sync/SharedSyncImplementation.ts +++ b/packages/web/src/worker/sync/SharedSyncImplementation.ts @@ -624,11 +624,11 @@ function withAbort(options: { action() .then((data) => { - completePromise(() => resolve(data)); // We already rejected due to the abort, allow for cleanup if (signal.aborted) { - cleanupOnAbort?.(data); + return completePromise(() => cleanupOnAbort?.(data)); } + completePromise(() => resolve(data)); }) .catch((e) => completePromise(() => reject(e))); }); From 445ec6912e726300149341331129cc1bcef19fc3 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Tue, 2 Dec 2025 16:38:41 +0200 Subject: [PATCH 33/44] add a finalization registry entry to release access handles --- packages/web/src/worker/db/opfs.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/web/src/worker/db/opfs.ts b/packages/web/src/worker/db/opfs.ts index 341c20415..40a8a3648 100644 --- a/packages/web/src/worker/db/opfs.ts +++ b/packages/web/src/worker/db/opfs.ts @@ -92,6 +92,19 @@ export class OPFSCoopSyncVFS extends FacadeVFS { }); }); }); + + finalizationRegistry.register(this, async () => { + for (const file of this.persistentFiles.values()) { + const release = this.#releaseAccessHandle(file); + try { + await this.#releaseAccessHandle(file); + } catch (e) { + this.log?.('error releasing access handle', e); + } finally { + release(); + } + } + }); finalizationRegistry.register(this, this.releaser); const tmpDir = await root.getDirectoryHandle(tmpDirName, { create: true }); From 66300976165f2d0811f8cb5fd2cf72e7051a1095 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 08:51:58 +0200 Subject: [PATCH 34/44] Wrap entire open operation init in writeLock. --- .../db/adapters/LockedAsyncDatabaseAdapter.ts | 97 ++++++++++--------- .../web/src/worker/db/WASQLiteDB.worker.ts | 6 +- packages/web/src/worker/db/opfs.ts | 34 ++++--- 3 files changed, 73 insertions(+), 64 deletions(-) diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 7958afba9..77a56fcd0 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -113,23 +113,28 @@ export class LockedAsyncDatabaseAdapter } protected async openInternalDB() { - // Dispose any previous table change listener. - this._disposeTableChangeListener?.(); - this._disposeTableChangeListener = null; - - const isReOpen = !!this._db; - - this._db = await this.options.openConnection(); - await this._db.init(); - this._config = await this._db.getConfig(); - await this.registerOnChangeListener(this._db); - if (isReOpen) { - this.iterateListeners((cb) => cb.databaseReOpened?.()); - } /** - * This is only required for the long-lived shared IndexedDB connections. + * Execute opening of the db in a lock in order not to interfere with other operations. */ - this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS; + return this._acquireLock(async () => { + // Dispose any previous table change listener. + this._disposeTableChangeListener?.(); + this._disposeTableChangeListener = null; + + const isReOpen = !!this._db; + + this._db = await this.options.openConnection(); + await this._db.init(); + this._config = await this._db.getConfig(); + await this.registerOnChangeListener(this._db); + if (isReOpen) { + this.iterateListeners((cb) => cb.databaseReOpened?.()); + } + /** + * This is only required for the long-lived shared IndexedDB connections. + */ + this.requiresHolds = (this._config as ResolvedWASQLiteOpenFactoryOptions).vfs == WASQLiteVFS.IDBBatchAtomicVFS; + }); } protected _reOpen() { @@ -252,13 +257,10 @@ export class LockedAsyncDatabaseAdapter ); } - protected async acquireLock(callback: () => Promise, options?: { timeoutMs?: number }): Promise { - await this.waitForInitialized(); - + protected async _acquireLock(callback: () => Promise, options?: { timeoutMs?: number }): Promise { if (this.closing) { throw new Error(`Cannot acquire lock, the database is closing`); } - const abortController = new AbortController(); this.pendingAbortControllers.add(abortController); const { timeoutMs } = options ?? {}; @@ -278,36 +280,41 @@ export class LockedAsyncDatabaseAdapter if (timeoutId) { clearTimeout(timeoutId); } - let holdId: string | null = null; - try { - // The database is being opened in the background. Wait for it here. - if (this.databaseOpenPromise) { - /** - * We can't await this since it uses the same lock as we're in now. - */ - throw new ConnectionClosedError('Connection is busy re-opening'); - } + return await callback(); + } + ); + } - holdId = this.requiresHolds ? await this.baseDB.markHold() : null; - return await callback(); - } catch (ex) { - if ( - ex instanceof ConnectionClosedError || - (ex instanceof Error && ex.name === 'NoModificationAllowedError') - ) { - if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) { - // Immediately re-open the database. We need to miss as little table updates as possible. - this.reOpenInternalDB(); - } - } - throw ex; - } finally { - if (holdId) { - await this.baseDB.releaseHold(holdId); + protected async acquireLock(callback: () => Promise, options?: { timeoutMs?: number }): Promise { + await this.waitForInitialized(); + + return this._acquireLock(async () => { + let holdId: string | null = null; + try { + // The database is being opened in the background. Wait for it here. + if (this.databaseOpenPromise) { + /** + * We can't await this since it uses the same lock as we're in now. + */ + throw new ConnectionClosedError('Connection is busy re-opening'); + } + + holdId = this.requiresHolds ? await this.baseDB.markHold() : null; + return await callback(); + } catch (ex) { + if (ex instanceof ConnectionClosedError || (ex instanceof Error && ex.name === 'NoModificationAllowedError')) { + if (this.options.reOpenOnConnectionClosed && !this.databaseOpenPromise && !this.closing) { + // Immediately re-open the database. We need to miss as little table updates as possible. + this.reOpenInternalDB(); } } + throw ex; + } finally { + if (holdId) { + await this.baseDB.releaseHold(holdId); + } } - ); + }, options); } async readTransaction(fn: (tx: Transaction) => Promise, options?: DBLockOptions | undefined): Promise { diff --git a/packages/web/src/worker/db/WASQLiteDB.worker.ts b/packages/web/src/worker/db/WASQLiteDB.worker.ts index c09d4fd06..13fa68871 100644 --- a/packages/web/src/worker/db/WASQLiteDB.worker.ts +++ b/packages/web/src/worker/db/WASQLiteDB.worker.ts @@ -16,13 +16,11 @@ baseLogger.useDefaults(); const logger = createLogger('db-worker'); const DBMap = new Map(); - +const OPEN_DB_LOCK = 'open-wasqlite-db'; let nextClientId = 1; const openDBShared = async (options: WorkerDBOpenerOptions): Promise => { - // Prevent multiple simultaneous opens from causing race conditions - // Use the same lock as a write lock to prevent concurrent writes to the database. - return getNavigatorLocks().request(`db-lock-${options.dbFilename}`, async () => { + return getNavigatorLocks().request(OPEN_DB_LOCK, async () => { const clientId = nextClientId++; const { dbFilename, logLevel } = options; diff --git a/packages/web/src/worker/db/opfs.ts b/packages/web/src/worker/db/opfs.ts index 40a8a3648..dfcb07e13 100644 --- a/packages/web/src/worker/db/opfs.ts +++ b/packages/web/src/worker/db/opfs.ts @@ -93,18 +93,21 @@ export class OPFSCoopSyncVFS extends FacadeVFS { }); }); - finalizationRegistry.register(this, async () => { - for (const file of this.persistentFiles.values()) { - const release = this.#releaseAccessHandle(file); - try { - await this.#releaseAccessHandle(file); - } catch (e) { - this.log?.('error releasing access handle', e); - } finally { - release(); - } - } - }); + const releaseHandle = async () => { + await Promise.all( + this.persistentFiles.values().map(async (file) => { + try { + await this.#releaseAccessHandle(file); + } catch (e) { + this.log?.('error releasing access handle', e); + } finally { + release(); + } + }) + ); + }; + + finalizationRegistry.register(this, releaseHandle); finalizationRegistry.register(this, this.releaser); const tmpDir = await root.getDirectoryHandle(tmpDirName, { create: true }); @@ -272,9 +275,10 @@ export class OPFSCoopSyncVFS extends FacadeVFS { this.mapIdToFile.delete(fileId); if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) { - if (file.persistentFile?.handleLockReleaser) { - this.#releaseAccessHandle(file); - } + // Release this either way + // if (file.persistentFile?.handleLockReleaser) { + this.#releaseAccessHandle(file); + // } } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { file.accessHandle.truncate(0); this.accessiblePaths.delete(file.path); From bbfdcf738c5ff05c42b56af172bde62e4e0840b6 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 09:14:42 +0200 Subject: [PATCH 35/44] revert opfs test --- packages/web/src/worker/db/opfs.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/web/src/worker/db/opfs.ts b/packages/web/src/worker/db/opfs.ts index dfcb07e13..0f4b40799 100644 --- a/packages/web/src/worker/db/opfs.ts +++ b/packages/web/src/worker/db/opfs.ts @@ -275,10 +275,9 @@ export class OPFSCoopSyncVFS extends FacadeVFS { this.mapIdToFile.delete(fileId); if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) { - // Release this either way - // if (file.persistentFile?.handleLockReleaser) { - this.#releaseAccessHandle(file); - // } + if (file.persistentFile?.accessHandle) { + this.#releaseAccessHandle(file); + } } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) { file.accessHandle.truncate(0); this.accessiblePaths.delete(file.path); From 8cfc0e6fe91405b024d401d5745096da4235636a Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 10:07:23 +0200 Subject: [PATCH 36/44] update wa-sqlite --- demos/angular-supabase-todolist/package.json | 2 +- demos/example-capacitor/package.json | 6 +- demos/example-electron/package.json | 4 +- demos/example-nextjs/package.json | 2 +- demos/react-multi-client/package.json | 2 +- .../package.json | 2 +- .../package.json | 6 +- .../package.json | 6 +- .../package.json | 4 +- demos/react-supabase-todolist/package.json | 6 +- .../package.json | 6 +- package.json | 5 - packages/drizzle-driver/package.json | 2 +- packages/kysely-driver/package.json | 2 +- packages/web/package.json | 4 +- pnpm-lock.yaml | 1136 ++++++++++++++--- tools/diagnostics-app/package.json | 6 +- 17 files changed, 985 insertions(+), 216 deletions(-) diff --git a/demos/angular-supabase-todolist/package.json b/demos/angular-supabase-todolist/package.json index 075eb38ac..86e8e54a6 100644 --- a/demos/angular-supabase-todolist/package.json +++ b/demos/angular-supabase-todolist/package.json @@ -23,7 +23,7 @@ "@angular/platform-browser-dynamic": "^19.2.4", "@angular/router": "^19.2.4", "@angular/service-worker": "^19.2.4", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@powersync/web": "workspace:*", "@supabase/supabase-js": "^2.44.4", "rxjs": "~7.8.1", diff --git a/demos/example-capacitor/package.json b/demos/example-capacitor/package.json index 9ba20f697..045e7aedd 100644 --- a/demos/example-capacitor/package.json +++ b/demos/example-capacitor/package.json @@ -25,8 +25,8 @@ "@capacitor/core": "latest", "@capacitor/ios": "^7.4.3", "@capacitor/splash-screen": "latest", + "@journeyapps/wa-sqlite": "^1.3.4", "@powersync/capacitor": "workspace:*", - "@journeyapps/wa-sqlite": "^1.3.2", "@powersync/react": "workspace:*", "@powersync/web": "workspace:*", "react": "^18.2.0", @@ -35,10 +35,10 @@ }, "devDependencies": { "@capacitor/cli": "^7.4.3", - "@swc/core": "~1.6.0", - "@mui/material": "^5.15.12", "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", + "@mui/material": "^5.15.12", + "@swc/core": "~1.6.0", "@types/node": "^20.12.12", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", diff --git a/demos/example-electron/package.json b/demos/example-electron/package.json index 4b9f6b397..757a8eb7e 100644 --- a/demos/example-electron/package.json +++ b/demos/example-electron/package.json @@ -21,14 +21,14 @@ "dependencies": { "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@mui/icons-material": "^5.15.16", "@mui/material": "^5.15.16", "@mui/x-data-grid": "^6.19.11", - "express": "^4.19.2", "@powersync/react": "workspace:*", "@powersync/web": "workspace:*", "electron-squirrel-startup": "^1.0.0", + "express": "^4.19.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.23.0" diff --git a/demos/example-nextjs/package.json b/demos/example-nextjs/package.json index 9cdf7a9b1..88ffeedd3 100644 --- a/demos/example-nextjs/package.json +++ b/demos/example-nextjs/package.json @@ -14,7 +14,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fontsource/roboto": "^5.0.13", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@lexical/react": "^0.15.0", "@mui/icons-material": "^5.15.18", "@mui/material": "^5.15.18", diff --git a/demos/react-multi-client/package.json b/demos/react-multi-client/package.json index 1d7f1735b..a1191a806 100644 --- a/demos/react-multi-client/package.json +++ b/demos/react-multi-client/package.json @@ -10,7 +10,7 @@ "test:build": "pnpm build" }, "dependencies": { - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@powersync/react": "workspace:*", "@powersync/web": "workspace:*", "@supabase/supabase-js": "^2.43.1", diff --git a/demos/react-native-web-supabase-todolist/package.json b/demos/react-native-web-supabase-todolist/package.json index a88d0993f..de309d206 100644 --- a/demos/react-native-web-supabase-todolist/package.json +++ b/demos/react-native-web-supabase-todolist/package.json @@ -14,7 +14,7 @@ "@expo/metro-runtime": "^4.0.1", "@expo/vector-icons": "^14.0.2", "@journeyapps/react-native-quick-sqlite": "^2.4.9", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@powersync/attachments": "workspace:*", "@powersync/react": "workspace:*", "@powersync/react-native": "workspace:*", diff --git a/demos/react-supabase-todolist-optional-sync/package.json b/demos/react-supabase-todolist-optional-sync/package.json index 7c51c0e54..75e3ecc68 100644 --- a/demos/react-supabase-todolist-optional-sync/package.json +++ b/demos/react-supabase-todolist-optional-sync/package.json @@ -9,14 +9,14 @@ "start": "pnpm build && pnpm preview" }, "dependencies": { - "@powersync/react": "workspace:*", - "@powersync/web": "workspace:*", "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", "@mui/x-data-grid": "^6.19.6", + "@powersync/react": "workspace:*", + "@powersync/web": "workspace:*", "@supabase/supabase-js": "^2.39.7", "formik": "^2.4.6", "lodash": "^4.17.21", diff --git a/demos/react-supabase-todolist-sync-streams/package.json b/demos/react-supabase-todolist-sync-streams/package.json index 41b859460..3014ae1e3 100644 --- a/demos/react-supabase-todolist-sync-streams/package.json +++ b/demos/react-supabase-todolist-sync-streams/package.json @@ -9,14 +9,14 @@ "start": "pnpm build && pnpm preview" }, "dependencies": { - "@powersync/react": "workspace:*", - "@powersync/web": "workspace:*", "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", - "@journeyapps/wa-sqlite": "^1.3.1", + "@journeyapps/wa-sqlite": "^1.3.4", "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", "@mui/x-data-grid": "^6.19.6", + "@powersync/react": "workspace:*", + "@powersync/web": "workspace:*", "@supabase/supabase-js": "^2.39.7", "formik": "^2.4.6", "lodash": "^4.17.21", diff --git a/demos/react-supabase-todolist-tanstackdb/package.json b/demos/react-supabase-todolist-tanstackdb/package.json index 33ea05e19..8843786f0 100644 --- a/demos/react-supabase-todolist-tanstackdb/package.json +++ b/demos/react-supabase-todolist-tanstackdb/package.json @@ -11,13 +11,13 @@ "dependencies": { "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", "@mui/x-data-grid": "^6.19.6", + "@powersync/common": "workspace:*", "@powersync/react": "workspace:*", "@powersync/web": "workspace:*", - "@powersync/common": "workspace:*", "@supabase/supabase-js": "^2.39.7", "@tanstack/db": "^0.4.17", "@tanstack/powersync-db-collection": "^0.1.0", diff --git a/demos/react-supabase-todolist/package.json b/demos/react-supabase-todolist/package.json index bc2e32340..3014ae1e3 100644 --- a/demos/react-supabase-todolist/package.json +++ b/demos/react-supabase-todolist/package.json @@ -9,14 +9,14 @@ "start": "pnpm build && pnpm preview" }, "dependencies": { - "@powersync/react": "workspace:*", - "@powersync/web": "workspace:*", "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", "@mui/x-data-grid": "^6.19.6", + "@powersync/react": "workspace:*", + "@powersync/web": "workspace:*", "@supabase/supabase-js": "^2.39.7", "formik": "^2.4.6", "lodash": "^4.17.21", diff --git a/demos/yjs-react-supabase-text-collab/package.json b/demos/yjs-react-supabase-text-collab/package.json index 77c8d90f0..79cc8560e 100644 --- a/demos/yjs-react-supabase-text-collab/package.json +++ b/demos/yjs-react-supabase-text-collab/package.json @@ -9,11 +9,11 @@ "start": "pnpm build && pnpm preview" }, "dependencies": { - "@powersync/react": "workspace:*", - "@powersync/web": "workspace:*", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@mui/material": "^5.15.12", "@mui/x-data-grid": "^6.19.6", + "@powersync/react": "workspace:*", + "@powersync/web": "workspace:*", "@supabase/supabase-js": "^2.39.8", "@tiptap/extension-collaboration": "2.2.2", "@tiptap/extension-collaboration-cursor": "2.2.2", diff --git a/package.json b/package.json index 141435fe5..fd438ab2e 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,5 @@ "rollup-plugin-dts": "^6.2.1", "typescript": "^5.7.2", "vitest": "^3.2.4" - }, - "pnpm": { - "overrides": { - "@journeyapps/wa-sqlite": "0.0.0-dev-20251201120934" - } } } diff --git a/packages/drizzle-driver/package.json b/packages/drizzle-driver/package.json index 9b9628249..4ba8d533b 100644 --- a/packages/drizzle-driver/package.json +++ b/packages/drizzle-driver/package.json @@ -47,8 +47,8 @@ "drizzle-orm": "<1.0.0" }, "devDependencies": { + "@journeyapps/wa-sqlite": "^1.3.4", "@powersync/web": "workspace:*", - "@journeyapps/wa-sqlite": "^1.3.2", "@types/node": "^20.17.6", "drizzle-orm": "^0.44.7", "vite": "^6.1.0", diff --git a/packages/kysely-driver/package.json b/packages/kysely-driver/package.json index 86a602f3e..f2a51ea1b 100644 --- a/packages/kysely-driver/package.json +++ b/packages/kysely-driver/package.json @@ -49,8 +49,8 @@ "kysely": "^0.28.0" }, "devDependencies": { + "@journeyapps/wa-sqlite": "^1.3.4", "@powersync/web": "workspace:*", - "@journeyapps/wa-sqlite": "^1.3.2", "@types/node": "^20.17.6", "vite": "^6.1.0", "vite-plugin-top-level-await": "^1.4.4", diff --git a/packages/web/package.json b/packages/web/package.json index 2ae4a30a0..5ef61ba77 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -61,7 +61,7 @@ "author": "JOURNEYAPPS", "license": "Apache-2.0", "peerDependencies": { - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@powersync/common": "workspace:^1.43.1" }, "dependencies": { @@ -72,7 +72,7 @@ "commander": "^12.1.0" }, "devDependencies": { - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@types/uuid": "^9.0.6", "crypto-browserify": "^3.12.0", "p-defer": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e64f34b7..2f4c0aad5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - '@journeyapps/wa-sqlite': 0.0.0-dev-20251201120934 - importers: .: @@ -96,8 +93,8 @@ importers: specifier: ^19.2.4 version: 19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@powersync/web': specifier: workspace:* version: link:../../packages/web @@ -284,8 +281,8 @@ importers: specifier: latest version: 7.0.3(@capacitor/core@7.4.4) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@powersync/capacitor': specifier: workspace:* version: link:../../packages/capacitor @@ -351,8 +348,8 @@ importers: specifier: ^11.13.0 version: 11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@mui/icons-material': specifier: ^5.15.16 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -545,8 +542,8 @@ importers: specifier: ^5.0.13 version: 5.2.5 '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@lexical/react': specifier: ^0.15.0 version: 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.27) @@ -718,8 +715,8 @@ importers: demos/react-multi-client: dependencies: '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@powersync/react': specifier: workspace:* version: link:../../packages/react @@ -889,7 +886,7 @@ importers: version: 1.23.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1)) '@shopify/flash-list': specifier: 1.7.3 - version: 1.7.3(@babel/runtime@7.27.6)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + version: 1.7.3(@babel/runtime@7.28.4)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@supabase/supabase-js': specifier: 2.39.0 version: 2.39.0 @@ -1148,8 +1145,8 @@ importers: specifier: ^2.4.9 version: 2.4.9(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@powersync/attachments': specifier: workspace:* version: link:../../packages/attachments @@ -1302,8 +1299,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1387,8 +1384,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1472,8 +1469,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1557,8 +1554,8 @@ importers: specifier: 11.11.5 version: 11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@mui/icons-material': specifier: ^5.15.12 version: 5.17.1(@mui/material@5.17.1(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) @@ -1698,7 +1695,7 @@ importers: version: 1.3.1(vite@5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)) unplugin-vue-components: specifier: ^0.26.0 - version: 0.26.0(@babel/parser@7.27.4)(rollup@4.52.5)(vue@3.4.21(typescript@5.9.2)) + version: 0.26.0(@babel/parser@7.28.5)(rollup@4.52.5)(vue@3.4.21(typescript@5.9.2)) vite: specifier: ^5.2.0 version: 5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) @@ -1721,8 +1718,8 @@ importers: demos/yjs-react-supabase-text-collab: dependencies: '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@mui/material': specifier: ^5.15.12 version: 5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1740,10 +1737,10 @@ importers: version: 2.49.9 '@tiptap/extension-collaboration': specifier: 2.2.2 - version: 2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(y-prosemirror@1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) + version: 2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(y-prosemirror@1.3.5(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) '@tiptap/extension-collaboration-cursor': specifier: 2.2.2 - version: 2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(y-prosemirror@1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) + version: 2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(y-prosemirror@1.3.5(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) '@tiptap/extension-highlight': specifier: 2.2.2 version: 2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0)) @@ -2036,8 +2033,8 @@ importers: version: link:../common devDependencies: '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@powersync/web': specifier: workspace:* version: link:../web @@ -2046,7 +2043,7 @@ importers: version: 20.17.57 drizzle-orm: specifier: ^0.44.7 - version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) + version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) vite: specifier: ^6.1.0 version: 6.3.5(@types/node@20.17.57)(jiti@2.4.2)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0) @@ -2067,8 +2064,8 @@ importers: version: 0.28.2 devDependencies: '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@powersync/web': specifier: workspace:* version: link:../web @@ -2114,7 +2111,7 @@ importers: version: 12.2.0 drizzle-orm: specifier: ^0.44.7 - version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) + version: 0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0) rollup: specifier: 4.14.3 version: 4.14.3 @@ -2330,8 +2327,8 @@ importers: version: 12.1.0 devDependencies: '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@types/uuid': specifier: ^9.0.6 version: 9.0.8 @@ -2378,8 +2375,8 @@ importers: tools/diagnostics-app: dependencies: '@journeyapps/wa-sqlite': - specifier: 0.0.0-dev-20251201120934 - version: 0.0.0-dev-20251201120934 + specifier: ^1.3.4 + version: 1.3.4 '@mui/material': specifier: ^5.15.12 version: 5.17.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2963,6 +2960,10 @@ packages: resolution: {integrity: sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} + engines: {node: '>=6.9.0'} + '@babel/core@7.26.10': resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} engines: {node: '>=6.9.0'} @@ -2986,6 +2987,10 @@ packages: resolution: {integrity: sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.25.9': resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} @@ -3004,6 +3009,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-class-features-plugin@7.28.5': + resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.27.1': resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} engines: {node: '>=6.9.0'} @@ -3015,14 +3026,27 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-define-polyfill-provider@0.6.5': + resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-environment-visitor@7.24.7': resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.27.1': resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} @@ -3033,6 +3057,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.27.1': resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} @@ -3069,6 +3099,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -3090,12 +3124,23 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} engines: {node: '>=6.9.0'} @@ -3120,6 +3165,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': + resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/plugin-proposal-async-generator-functions@7.20.7': resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} @@ -3337,6 +3388,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-async-generator-functions@7.28.0': + resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-async-to-generator@7.25.9': resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} engines: {node: '>=6.9.0'} @@ -3361,6 +3418,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-block-scoping@7.28.5': + resolution: {integrity: sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-class-properties@7.27.1': resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} engines: {node: '>=6.9.0'} @@ -3373,12 +3436,24 @@ packages: peerDependencies: '@babel/core': ^7.12.0 + '@babel/plugin-transform-class-static-block@7.28.3': + resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + '@babel/plugin-transform-classes@7.27.1': resolution: {integrity: sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-classes@7.28.4': + resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-computed-properties@7.27.1': resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} engines: {node: '>=6.9.0'} @@ -3391,6 +3466,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-dotall-regex@7.27.1': resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} engines: {node: '>=6.9.0'} @@ -3415,12 +3496,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-explicit-resource-management@7.28.0': + resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-exponentiation-operator@7.27.1': resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-exponentiation-operator@7.28.5': + resolution: {integrity: sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-export-namespace-from@7.27.1': resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} engines: {node: '>=6.9.0'} @@ -3463,6 +3556,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-logical-assignment-operators@7.28.5': + resolution: {integrity: sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-member-expression-literals@7.27.1': resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} engines: {node: '>=6.9.0'} @@ -3487,6 +3586,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-systemjs@7.28.5': + resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-umd@7.27.1': resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} engines: {node: '>=6.9.0'} @@ -3523,6 +3628,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-object-rest-spread@7.28.4': + resolution: {integrity: sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-object-super@7.27.1': resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} engines: {node: '>=6.9.0'} @@ -3541,12 +3652,24 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-optional-chaining@7.28.5': + resolution: {integrity: sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-parameters@7.27.1': resolution: {integrity: sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-private-methods@7.27.1': resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} engines: {node: '>=6.9.0'} @@ -3613,6 +3736,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-regenerator@7.28.4': + resolution: {integrity: sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-regexp-modifiers@7.27.1': resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} engines: {node: '>=6.9.0'} @@ -3715,6 +3844,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/preset-env@7.28.5': + resolution: {integrity: sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/preset-flow@7.27.1': resolution: {integrity: sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg==} engines: {node: '>=6.9.0'} @@ -3756,6 +3891,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -3764,10 +3903,18 @@ packages: resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.27.3': resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -5881,8 +6028,11 @@ packages: react: '*' react-native: '*' - '@journeyapps/wa-sqlite@0.0.0-dev-20251201120934': - resolution: {integrity: sha512-Ira+2+IqPUyBqewcS4yPw1DPF/pi8EuXcxHf4w4+TcW4xfV2KnI9ZNA6+IOrLlVnScxoGFitlLbAgb3gWCPciA==} + '@journeyapps/wa-sqlite@1.3.4': + resolution: {integrity: sha512-oaacwXDjqI8N9zsGbO13Z6EnxiJarlC2mcnwFjpF3atadq3dck8UmaPe/GpjyRlQJWUyqP50YRjA8FluOhuOSw==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} @@ -5902,9 +6052,15 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -6008,8 +6164,8 @@ packages: bundledDependencies: - '@libsql/libsql-wasm-experimental' - '@libsql/core@0.15.9': - resolution: {integrity: sha512-4OVdeAmuaCUq5hYT8NNn0nxlO9AcA/eTjXfUZ+QK8MT3Dz7Z76m73x7KxjU6I64WyXX98dauVH2b9XM+d84npw==} + '@libsql/core@0.15.15': + resolution: {integrity: sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA==} '@listr2/prompt-adapter-inquirer@2.0.18': resolution: {integrity: sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==} @@ -7572,6 +7728,15 @@ packages: rollup: optional: true + '@rollup/plugin-node-resolve@15.3.1': + resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/plugin-node-resolve@16.0.3': resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} engines: {node: '>=14.0.0'} @@ -7641,6 +7806,15 @@ packages: rollup: optional: true + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.14.3': resolution: {integrity: sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==} cpu: [arm] @@ -10640,16 +10814,31 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs2@0.4.14: + resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.11.1: resolution: {integrity: sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.4: resolution: {integrity: sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-polyfill-regenerator@0.6.5: + resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-plugin-react-native-web@0.19.13: resolution: {integrity: sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==} @@ -10705,6 +10894,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.8.32: + resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} + hasBin: true + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -10770,8 +10963,8 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - bole@5.0.19: - resolution: {integrity: sha512-OgMuI8erST2t4K/Y+tSsn4SOxlKj4JR2wluQgLYadQFPIhj0r3jcmnp0OthgiyNO91CnxR8woKeLQmnMPgl1Ug==} + bole@5.0.23: + resolution: {integrity: sha512-xpiwhM4hEpfpasv+CIVUJJB/GisrqcysIBwBJ6rU069rUMXfy0DZKYJ1M1IS1fZ6yb65CPKBWzMs72Pop+Cgfg==} bonjour-service@1.3.0: resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} @@ -10860,6 +11053,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} @@ -11064,6 +11262,9 @@ packages: caniuse-lite@1.0.30001720: resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + caniuse-lite@1.0.30001759: + resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==} + cardinal@2.1.1: resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} hasBin: true @@ -11547,6 +11748,9 @@ packages: core-js-compat@3.42.0: resolution: {integrity: sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==} + core-js-compat@3.47.0: + resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + core-js-pure@3.42.0: resolution: {integrity: sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==} @@ -12379,6 +12583,9 @@ packages: electron-to-chromium@1.5.161: resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==} + electron-to-chromium@1.5.263: + resolution: {integrity: sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==} + electron-winstaller@5.4.0: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} @@ -13165,6 +13372,9 @@ packages: exponential-backoff@3.1.2: resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + express-ws@5.0.2: resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} engines: {node: '>=4.5.0'} @@ -14914,8 +15124,8 @@ packages: js-base64@3.7.2: resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==} - js-base64@3.7.7: - resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} js-logger@1.6.1: resolution: {integrity: sha512-yTgMCPXVjhmg28CuUH8CKjU+cIKL/G+zTu4Fn4lQxs8mRFH/03QTNvEFngcxfg/gRDiQAOoyCKmMTOm9ayOzXA==} @@ -15182,6 +15392,11 @@ packages: engines: {node: '>=16'} hasBin: true + lib0@0.2.114: + resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} + engines: {node: '>=16'} + hasBin: true + license-webpack-plugin@4.0.2: resolution: {integrity: sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==} peerDependencies: @@ -16554,6 +16769,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-rsa@1.1.1: resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==} @@ -17160,6 +17378,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -17847,14 +18069,14 @@ packages: prosemirror-dropcursor@1.8.2: resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} - prosemirror-gapcursor@1.3.2: - resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + prosemirror-gapcursor@1.4.0: + resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==} - prosemirror-history@1.4.1: - resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} - prosemirror-inputrules@1.5.0: - resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==} + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} prosemirror-keymap@1.2.3: resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} @@ -17865,8 +18087,8 @@ packages: prosemirror-menu@1.2.5: resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} - prosemirror-model@1.25.1: - resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==} + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} prosemirror-schema-basic@1.2.4: resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} @@ -17874,11 +18096,11 @@ packages: prosemirror-schema-list@1.5.1: resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} - prosemirror-state@1.4.3: - resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} - prosemirror-tables@1.7.1: - resolution: {integrity: sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==} + prosemirror-tables@1.8.1: + resolution: {integrity: sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==} prosemirror-trailing-node@3.0.0: resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} @@ -17887,11 +18109,11 @@ packages: prosemirror-state: ^1.4.2 prosemirror-view: ^1.33.8 - prosemirror-transform@1.10.4: - resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} + prosemirror-transform@1.10.5: + resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==} - prosemirror-view@1.40.0: - resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==} + prosemirror-view@1.41.4: + resolution: {integrity: sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==} proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -18739,6 +18961,11 @@ packages: engines: {node: '>= 0.4'} hasBin: true + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -20518,6 +20745,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + update-check@1.5.4: resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} @@ -22080,6 +22313,8 @@ snapshots: '@babel/compat-data@7.27.3': {} + '@babel/compat-data@7.28.5': {} + '@babel/core@7.26.10': dependencies: '@ampproject/remapping': 2.3.0 @@ -22144,6 +22379,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 + '@babel/generator@7.28.5': + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.25.9': dependencies: '@babel/types': 7.27.3 @@ -22173,6 +22416,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.26.10) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22191,9 +22447,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + '@babel/helper-environment-visitor@7.24.7': dependencies: - '@babel/types': 7.27.3 + '@babel/types': 7.28.5 + + '@babel/helper-globals@7.28.0': {} '@babel/helper-member-expression-to-functions@7.27.1': dependencies: @@ -22202,6 +22471,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.27.4 @@ -22227,6 +22503,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.27.1': dependencies: '@babel/types': 7.27.3 @@ -22266,6 +22551,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.27.1': @@ -22292,6 +22579,10 @@ snapshots: dependencies: '@babel/types': 7.27.3 + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22300,6 +22591,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22327,6 +22626,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22543,6 +22850,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.26.10) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22571,6 +22887,11 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-block-scoping@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22587,6 +22908,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-classes@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22599,6 +22928,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-classes@7.28.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.26.10) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22610,6 +22951,14 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22632,11 +22981,24 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-exponentiation-operator@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22680,6 +23042,11 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-logical-assignment-operators@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22711,6 +23078,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.26.10) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22748,6 +23125,17 @@ snapshots: '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.26.10) '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.26.10) + '@babel/traverse': 7.28.5 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22769,11 +23157,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-optional-chaining@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-parameters@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -22845,6 +23246,11 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -23097,6 +23503,82 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-env@7.28.5(@babel/core@7.26.10)': + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/core': 7.26.10 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.26.10) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.10) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.10) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.26.10) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-block-scoping': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.26.10) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.26.10) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.26.10) + '@babel/plugin-transform-exponentiation-operator': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-logical-assignment-operators': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-object-rest-spread': 7.28.4(@babel/core@7.26.10) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-optional-chaining': 7.28.5(@babel/core@7.26.10) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.26.10) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.26.10) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.26.10) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.10) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.26.10) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.26.10) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.26.10) + core-js-compat: 3.47.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/preset-flow@7.27.1(@babel/core@7.26.10)': dependencies: '@babel/core': 7.26.10 @@ -23153,6 +23635,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -23171,11 +23655,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.27.3': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@0.2.3': {} '@braidai/lang@1.1.1': {} @@ -24844,7 +25345,7 @@ snapshots: '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': dependencies: env-paths: 2.2.1 - exponential-backoff: 3.1.2 + exponential-backoff: 3.1.3 glob: 8.1.0 graceful-fs: 4.2.11 make-fetch-happen: 10.2.1 @@ -26619,7 +27120,12 @@ snapshots: react: 18.3.1 react-native: 0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1) - '@journeyapps/wa-sqlite@0.0.0-dev-20251201120934': {} + '@journeyapps/wa-sqlite@1.3.4': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/gen-mapping@0.3.8': dependencies: @@ -26638,11 +27144,18 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@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 @@ -26815,13 +27328,13 @@ snapshots: '@libsql/client-wasm@0.15.8': dependencies: - '@libsql/core': 0.15.9 - js-base64: 3.7.7 + '@libsql/core': 0.15.15 + js-base64: 3.7.8 optional: true - '@libsql/core@0.15.9': + '@libsql/core@0.15.15': dependencies: - js-base64: 3.7.7 + js-base64: 3.7.8 optional: true '@listr2/prompt-adapter-inquirer@2.0.18(@inquirer/prompts@7.3.2(@types/node@24.2.0))': @@ -27636,6 +28149,12 @@ snapshots: react: 19.0.0 react-native: 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0) + '@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0)': + dependencies: + react: 19.0.0 + react-native: 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0) + optional: true + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -27858,7 +28377,7 @@ snapshots: '@pnpm/logger@5.2.0': dependencies: - bole: 5.0.19 + bole: 5.0.23 ndjson: 2.0.0 '@pnpm/manifest-utils@6.0.9(@pnpm/logger@5.2.0)': @@ -28706,6 +29225,15 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/babel-plugin-codegen@0.78.0(@babel/preset-env@7.28.5(@babel/core@7.26.10))': + dependencies: + '@babel/traverse': 7.27.4 + '@react-native/codegen': 0.78.0(@babel/preset-env@7.28.5(@babel/core@7.26.10)) + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + '@react-native/babel-preset@0.75.3(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))': dependencies: '@babel/core': 7.26.10 @@ -28910,6 +29438,58 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/babel-preset@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))': + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.10) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-async-generator-functions': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-block-scoping': 7.27.3(@babel/core@7.26.10) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-classes': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-destructuring': 7.27.3(@babel/core@7.26.10) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-object-rest-spread': 7.27.3(@babel/core@7.26.10) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-parameters': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-react-display-name': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-regenerator': 7.27.4(@babel/core@7.26.10) + '@babel/plugin-transform-runtime': 7.27.4(@babel/core@7.26.10) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.26.10) + '@babel/template': 7.27.2 + '@react-native/babel-plugin-codegen': 0.78.0(@babel/preset-env@7.28.5(@babel/core@7.26.10)) + babel-plugin-syntax-hermes-parser: 0.25.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.26.10) + react-refresh: 0.14.2 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + '@react-native/codegen@0.72.8(@babel/preset-env@7.27.2(@babel/core@7.26.10))': dependencies: '@babel/parser': 7.27.4 @@ -28977,6 +29557,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@react-native/codegen@0.78.0(@babel/preset-env@7.28.5(@babel/core@7.26.10))': + dependencies: + '@babel/parser': 7.27.4 + '@babel/preset-env': 7.28.5(@babel/core@7.26.10) + glob: 7.2.3 + hermes-parser: 0.25.1 + invariant: 2.2.4 + jscodeshift: 17.3.0(@babel/preset-env@7.28.5(@babel/core@7.26.10)) + nullthrows: 1.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + optional: true + '@react-native/community-cli-plugin@0.75.3(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(encoding@0.1.13)': dependencies: '@react-native-community/cli-server-api': 14.1.0 @@ -29086,6 +29680,28 @@ snapshots: - supports-color - utf-8-validate + '@react-native/community-cli-plugin@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)': + dependencies: + '@react-native/dev-middleware': 0.78.0 + '@react-native/metro-babel-transformer': 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10)) + chalk: 4.1.2 + debug: 2.6.9 + invariant: 2.2.4 + metro: 0.81.5 + metro-config: 0.81.5 + metro-core: 0.81.5 + readline: 1.3.0 + semver: 7.7.2 + optionalDependencies: + '@react-native-community/cli-server-api': 15.1.3 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - supports-color + - utf-8-validate + optional: true + '@react-native/debugger-frontend@0.75.3': {} '@react-native/debugger-frontend@0.76.9': {} @@ -29300,6 +29916,17 @@ snapshots: - '@babel/preset-env' - supports-color + '@react-native/metro-babel-transformer@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))': + dependencies: + '@babel/core': 7.26.10 + '@react-native/babel-preset': 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10)) + hermes-parser: 0.25.1 + nullthrows: 1.1.1 + transitivePeerDependencies: + - '@babel/preset-env' + - supports-color + optional: true + '@react-native/metro-config@0.77.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))': dependencies: '@react-native/js-polyfills': 0.77.0 @@ -29402,6 +30029,16 @@ snapshots: optionalDependencies: '@types/react': 19.1.6 + '@react-native/virtualized-lists@0.78.0(@types/react@19.1.6)(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.0.0 + react-native: 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0) + optionalDependencies: + '@types/react': 19.1.6 + optional: true + '@react-navigation/bottom-tabs@7.3.14(@react-navigation/native@7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-screens@4.4.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: '@react-navigation/elements': 2.4.3(@react-navigation/native@7.1.10(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -29640,27 +30277,26 @@ snapshots: optionalDependencies: rollup: 4.52.5 - '@rollup/plugin-node-resolve@15.2.3(rollup@2.79.2)': + '@rollup/plugin-node-resolve@15.2.3(rollup@4.14.3)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@2.79.2) + '@rollup/pluginutils': 5.1.4(rollup@4.14.3) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 2.79.2 + rollup: 4.14.3 - '@rollup/plugin-node-resolve@15.2.3(rollup@4.14.3)': + '@rollup/plugin-node-resolve@15.3.1(rollup@2.79.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.14.3) + '@rollup/pluginutils': 5.3.0(rollup@2.79.2) '@types/resolve': 1.20.2 deepmerge: 4.3.1 - is-builtin-module: 3.2.1 is-module: 1.0.0 - resolve: 1.22.10 + resolve: 1.22.11 optionalDependencies: - rollup: 4.14.3 + rollup: 2.79.2 '@rollup/plugin-node-resolve@16.0.3(rollup@4.52.5)': dependencies: @@ -29725,14 +30361,6 @@ snapshots: picomatch: 2.3.1 rollup: 2.79.2 - '@rollup/pluginutils@5.1.4(rollup@2.79.2)': - dependencies: - '@types/estree': 1.0.7 - estree-walker: 2.0.2 - picomatch: 4.0.2 - optionalDependencies: - rollup: 2.79.2 - '@rollup/pluginutils@5.1.4(rollup@4.14.3)': dependencies: '@types/estree': 1.0.7 @@ -29749,6 +30377,14 @@ snapshots: optionalDependencies: rollup: 4.52.5 + '@rollup/pluginutils@5.3.0(rollup@2.79.2)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 2.79.2 + '@rollup/rollup-android-arm-eabi@4.14.3': optional: true @@ -30086,9 +30722,9 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@shopify/flash-list@1.7.3(@babel/runtime@7.27.6)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': + '@shopify/flash-list@1.7.3(@babel/runtime@7.28.4)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 react: 18.3.1 react-native: 0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1) recyclerlistview: 4.2.1(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -30152,7 +30788,7 @@ snapshots: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 invariant: 2.2.4 prop-types: 15.8.1 react: 18.3.1 @@ -30317,7 +30953,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.27.3 + '@babel/types': 7.28.5 entities: 4.5.0 '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))': @@ -31465,16 +32101,16 @@ snapshots: dependencies: '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) - '@tiptap/extension-collaboration-cursor@2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(y-prosemirror@1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + '@tiptap/extension-collaboration-cursor@2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(y-prosemirror@1.3.5(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': dependencies: '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) - y-prosemirror: 1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + y-prosemirror: 1.3.5(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - '@tiptap/extension-collaboration@2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(y-prosemirror@1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + '@tiptap/extension-collaboration@2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(y-prosemirror@1.3.5(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': dependencies: '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/pm': 2.12.0 - y-prosemirror: 1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + y-prosemirror: 1.3.5(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) '@tiptap/extension-document@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))': dependencies: @@ -31557,20 +32193,20 @@ snapshots: prosemirror-collab: 1.3.1 prosemirror-commands: 1.7.1 prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.3.2 - prosemirror-history: 1.4.1 - prosemirror-inputrules: 1.5.0 + prosemirror-gapcursor: 1.4.0 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 prosemirror-keymap: 1.2.3 prosemirror-markdown: 1.13.2 prosemirror-menu: 1.2.5 - prosemirror-model: 1.25.1 + prosemirror-model: 1.25.4 prosemirror-schema-basic: 1.2.4 prosemirror-schema-list: 1.5.1 - prosemirror-state: 1.4.3 - prosemirror-tables: 1.7.1 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0) - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4) + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.4 '@tiptap/react@2.2.2(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -33486,6 +34122,15 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.26.10): + dependencies: + '@babel/compat-data': 7.28.5 + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.26.10) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -33494,6 +34139,14 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.26.10) + core-js-compat: 3.47.0 + transitivePeerDependencies: + - supports-color + babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -33501,6 +34154,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.26.10): + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + babel-plugin-react-native-web@0.19.13: {} babel-plugin-syntax-hermes-parser@0.23.1: @@ -33601,6 +34261,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.8.32: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -33690,7 +34352,7 @@ snapshots: transitivePeerDependencies: - supports-color - bole@5.0.19: + bole@5.0.23: dependencies: fast-safe-stringify: 2.1.1 individual: 3.0.0 @@ -33838,6 +34500,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.8.32 + caniuse-lite: 1.0.30001759 + electron-to-chromium: 1.5.263 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) + bs-logger@0.2.6: dependencies: fast-json-stable-stringify: 2.1.0 @@ -34089,6 +34759,8 @@ snapshots: caniuse-lite@1.0.30001720: {} + caniuse-lite@1.0.30001759: {} + cardinal@2.1.1: dependencies: ansicolors: 0.3.2 @@ -34585,6 +35257,10 @@ snapshots: dependencies: browserslist: 4.25.0 + core-js-compat@3.47.0: + dependencies: + browserslist: 4.28.0 + core-js-pure@3.42.0: {} core-js@3.42.0: {} @@ -35427,10 +36103,10 @@ snapshots: dotenv@16.5.0: {} - drizzle-orm@0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0): + drizzle-orm@0.44.7(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-sqlite3@12.2.0)(kysely@0.28.2)(sql.js@1.13.0): optionalDependencies: '@libsql/client-wasm': 0.15.8 - '@op-engineering/op-sqlite': 14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0) + '@op-engineering/op-sqlite': 14.0.2(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0) '@types/better-sqlite3': 7.6.13 '@types/sql.js': 1.4.9 better-sqlite3: 12.2.0 @@ -35627,6 +36303,8 @@ snapshots: electron-to-chromium@1.5.161: {} + electron-to-chromium@1.5.263: {} + electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.4.1 @@ -37048,6 +37726,8 @@ snapshots: exponential-backoff@3.1.2: {} + exponential-backoff@3.1.3: {} + express-ws@5.0.2(express@4.21.2): dependencies: express: 4.21.2 @@ -39578,7 +40258,7 @@ snapshots: js-base64@3.7.2: {} - js-base64@3.7.7: + js-base64@3.7.8: optional: true js-logger@1.6.1: {} @@ -39658,6 +40338,32 @@ snapshots: transitivePeerDependencies: - supports-color + jscodeshift@17.3.0(@babel/preset-env@7.28.5(@babel/core@7.26.10)): + dependencies: + '@babel/core': 7.26.10 + '@babel/parser': 7.27.4 + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.26.10) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.26.10) + '@babel/preset-flow': 7.27.1(@babel/core@7.26.10) + '@babel/preset-typescript': 7.27.1(@babel/core@7.26.10) + '@babel/register': 7.27.1(@babel/core@7.26.10) + flow-parser: 0.272.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + neo-async: 2.6.2 + picocolors: 1.1.1 + recast: 0.23.11 + tmp: 0.2.3 + write-file-atomic: 5.0.1 + optionalDependencies: + '@babel/preset-env': 7.28.5(@babel/core@7.26.10) + transitivePeerDependencies: + - supports-color + optional: true + jsdom@20.0.3: dependencies: abab: 2.0.6 @@ -39917,6 +40623,10 @@ snapshots: dependencies: isomorphic.js: 0.2.5 + lib0@0.2.114: + dependencies: + isomorphic.js: 0.2.5 + license-webpack-plugin@4.0.2(webpack@5.98.0(@swc/core@1.11.29)): dependencies: webpack-sources: 3.3.0 @@ -40944,7 +41654,7 @@ snapshots: metro-runtime@0.76.7: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 react-refresh: 0.4.3 metro-runtime@0.76.8: @@ -40965,7 +41675,7 @@ snapshots: metro-source-map@0.76.7: dependencies: '@babel/traverse': 7.27.4 - '@babel/types': 7.27.3 + '@babel/types': 7.28.5 invariant: 2.2.4 metro-symbolicate: 0.76.7 nullthrows: 1.1.1 @@ -41005,7 +41715,7 @@ snapshots: metro-source-map@0.81.5: dependencies: '@babel/traverse': 7.27.4 - '@babel/traverse--for-generate-function-map': '@babel/traverse@7.27.4' + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.28.5' '@babel/types': 7.27.3 flow-enums-runtime: 0.0.6 invariant: 2.2.4 @@ -41065,7 +41775,7 @@ snapshots: metro-transform-plugins@0.76.7: dependencies: '@babel/core': 7.26.10 - '@babel/generator': 7.27.3 + '@babel/generator': 7.28.5 '@babel/template': 7.27.2 '@babel/traverse': 7.27.4 nullthrows: 1.1.1 @@ -41097,9 +41807,9 @@ snapshots: metro-transform-worker@0.76.7(encoding@0.1.13): dependencies: '@babel/core': 7.26.10 - '@babel/generator': 7.27.3 - '@babel/parser': 7.27.4 - '@babel/types': 7.27.3 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 babel-preset-fbjs: 3.4.0(@babel/core@7.26.10) metro: 0.76.7(encoding@0.1.13) metro-babel-transformer: 0.76.7 @@ -41158,11 +41868,11 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.26.10 - '@babel/generator': 7.27.3 - '@babel/parser': 7.27.4 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/traverse': 7.27.4 - '@babel/types': 7.27.3 + '@babel/types': 7.28.5 accepts: 1.3.8 async: 3.2.6 chalk: 4.1.2 @@ -41584,7 +42294,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -42079,6 +42789,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.27: {} + node-rsa@1.1.1: dependencies: asn1: 0.2.6 @@ -42710,6 +43422,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pidtree@0.6.0: {} pify@2.3.0: {} @@ -43436,106 +44150,106 @@ snapshots: prosemirror-changeset@2.3.1: dependencies: - prosemirror-transform: 1.10.4 + prosemirror-transform: 1.10.5 prosemirror-collab@1.3.1: dependencies: - prosemirror-state: 1.4.3 + prosemirror-state: 1.4.4 prosemirror-commands@1.7.1: dependencies: - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 prosemirror-dropcursor@1.8.2: dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.4 - prosemirror-gapcursor@1.3.2: + prosemirror-gapcursor@1.4.0: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-view: 1.40.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.4 - prosemirror-history@1.4.1: + prosemirror-history@1.5.0: dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.4 rope-sequence: 1.3.4 - prosemirror-inputrules@1.5.0: + prosemirror-inputrules@1.5.1: dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 prosemirror-keymap@1.2.3: dependencies: - prosemirror-state: 1.4.3 + prosemirror-state: 1.4.4 w3c-keyname: 2.2.8 prosemirror-markdown@1.13.2: dependencies: '@types/markdown-it': 14.1.2 markdown-it: 14.1.0 - prosemirror-model: 1.25.1 + prosemirror-model: 1.25.4 prosemirror-menu@1.2.5: dependencies: crelt: 1.0.6 prosemirror-commands: 1.7.1 - prosemirror-history: 1.4.1 - prosemirror-state: 1.4.3 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 - prosemirror-model@1.25.1: + prosemirror-model@1.25.4: dependencies: orderedmap: 2.1.1 prosemirror-schema-basic@1.2.4: dependencies: - prosemirror-model: 1.25.1 + prosemirror-model: 1.25.4 prosemirror-schema-list@1.5.1: dependencies: - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 - prosemirror-state@1.4.3: + prosemirror-state@1.4.4: dependencies: - prosemirror-model: 1.25.1 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 + prosemirror-model: 1.25.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.4 - prosemirror-tables@1.7.1: + prosemirror-tables@1.8.1: dependencies: prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 + prosemirror-view: 1.41.4 - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0): + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4): dependencies: '@remirror/core-constants': 3.0.0 escape-string-regexp: 4.0.0 - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-view: 1.40.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.4 - prosemirror-transform@1.10.4: + prosemirror-transform@1.10.5: dependencies: - prosemirror-model: 1.25.1 + prosemirror-model: 1.25.4 - prosemirror-view@1.40.0: + prosemirror-view@1.41.4: dependencies: - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.10.5 proto-list@1.2.4: {} @@ -43758,10 +44472,8 @@ snapshots: which: 2.0.2 yargs: 17.7.2 transitivePeerDependencies: - - bufferutil - supports-color - typescript - - utf-8-validate react-native-drawer-layout@4.1.10(react-native-gesture-handler@2.20.2(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native-reanimated@3.16.7(@babel/core@7.26.10)(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.3.3))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): dependencies: @@ -44397,6 +45109,56 @@ snapshots: - supports-color - utf-8-validate + react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.78.0 + '@react-native/codegen': 0.78.0(@babel/preset-env@7.28.5(@babel/core@7.26.10)) + '@react-native/community-cli-plugin': 0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3) + '@react-native/gradle-plugin': 0.78.0 + '@react-native/js-polyfills': 0.78.0 + '@react-native/normalize-colors': 0.78.0 + '@react-native/virtualized-lists': 0.78.0(@types/react@19.1.6)(react-native@0.78.0(@babel/core@7.26.10)(@babel/preset-env@7.28.5(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(@types/react@19.1.6)(react@19.0.0))(react@19.0.0) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.26.10) + babel-plugin-syntax-hermes-parser: 0.25.1 + base64-js: 1.5.1 + chalk: 4.1.2 + commander: 12.1.0 + event-target-shim: 5.0.1 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + memoize-one: 5.2.1 + metro-runtime: 0.81.5 + metro-source-map: 0.81.5 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.0.0 + react-devtools-core: 6.1.2 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.25.0 + semver: 7.7.2 + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.1.6 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - '@react-native-community/cli-server-api' + - bufferutil + - supports-color + - utf-8-validate + optional: true + react-navigation-stack@2.10.4(1b7f2cbbd098c1646b3c5f57acc57915): dependencies: '@react-native-community/masked-view': 0.1.11(react-native@0.76.9(@babel/core@7.26.10)(@babel/preset-env@7.27.2(@babel/core@7.26.10))(@react-native-community/cli@15.1.3(typescript@5.9.2))(@types/react@18.3.23)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) @@ -44945,6 +45707,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.8: dependencies: is-core-module: 2.16.1 @@ -45651,7 +46419,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -46813,7 +47581,7 @@ snapshots: tuf-js@3.0.1: dependencies: '@tufjs/models': 3.0.1 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3 make-fetch-happen: 14.0.3 transitivePeerDependencies: - supports-color @@ -47106,7 +47874,7 @@ snapshots: unplugin: 2.0.0-beta.1 vite: 5.4.19(@types/node@24.2.0)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) - unplugin-vue-components@0.26.0(@babel/parser@7.27.4)(rollup@4.52.5)(vue@3.4.21(typescript@5.9.2)): + unplugin-vue-components@0.26.0(@babel/parser@7.28.5)(rollup@4.52.5)(vue@3.4.21(typescript@5.9.2)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.4(rollup@4.52.5) @@ -47120,7 +47888,7 @@ snapshots: unplugin: 1.16.1 vue: 3.4.21(typescript@5.9.2) optionalDependencies: - '@babel/parser': 7.27.4 + '@babel/parser': 7.28.5 transitivePeerDependencies: - rollup - supports-color @@ -47169,6 +47937,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.1.4(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + update-check@1.5.4: dependencies: registry-auth-token: 3.3.2 @@ -48238,10 +49012,10 @@ snapshots: dependencies: '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) '@babel/core': 7.26.10 - '@babel/preset-env': 7.27.2(@babel/core@7.26.10) - '@babel/runtime': 7.27.6 + '@babel/preset-env': 7.28.5(@babel/core@7.26.10) + '@babel/runtime': 7.28.4 '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@2.79.2) - '@rollup/plugin-node-resolve': 15.2.3(rollup@2.79.2) + '@rollup/plugin-node-resolve': 15.3.1(rollup@2.79.2) '@rollup/plugin-replace': 2.4.2(rollup@2.79.2) '@rollup/plugin-terser': 0.4.4(rollup@2.79.2) '@surma/rollup-plugin-off-main-thread': 2.2.3 @@ -48454,18 +49228,18 @@ snapshots: xterm@4.19.0: {} - y-prosemirror@1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27): + y-prosemirror@1.3.5(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27): dependencies: - lib0: 0.2.109 - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-view: 1.40.0 + lib0: 0.2.114 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.4 y-protocols: 1.0.6(yjs@13.6.27) yjs: 13.6.27 y-protocols@1.0.6(yjs@13.6.27): dependencies: - lib0: 0.2.109 + lib0: 0.2.114 yjs: 13.6.27 y18n@4.0.3: {} diff --git a/tools/diagnostics-app/package.json b/tools/diagnostics-app/package.json index 8434d3af6..1eab49aed 100644 --- a/tools/diagnostics-app/package.json +++ b/tools/diagnostics-app/package.json @@ -9,11 +9,11 @@ "start": "pnpm build && pnpm preview" }, "dependencies": { - "@powersync/react": "workspace:*", - "@powersync/web": "workspace:*", - "@journeyapps/wa-sqlite": "^1.3.2", + "@journeyapps/wa-sqlite": "^1.3.4", "@mui/material": "^5.15.12", "@mui/x-data-grid": "^6.19.6", + "@powersync/react": "workspace:*", + "@powersync/web": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.3" From abdec984db00bf8c6f5b8244d87b8571df97b7e5 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 10:13:25 +0200 Subject: [PATCH 37/44] Update changesets --- .changeset/rare-windows-argue.md | 4 +++- .changeset/witty-steaks-worry.md | 2 +- .../sync/stream/AbstractStreamingSyncImplementation.ts | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.changeset/rare-windows-argue.md b/.changeset/rare-windows-argue.md index c7b9b3bf3..3cc520b86 100644 --- a/.changeset/rare-windows-argue.md +++ b/.changeset/rare-windows-argue.md @@ -2,4 +2,6 @@ '@powersync/web': patch --- -No longer awaiting when aborting connection on tab closure. Fixes some edge cases where multiple tabs with OPFS/Safari can cause deadlocks. +- Fixed some edge cases where multiple tabs with OPFS can cause sync deadlocks. +- Fixed issue where calling `powerSync.close()` would cause a disconnect if using multiple tabs (the default should not be to disconnect if using multiple tabs) +- Improved shared sync implementation database delegation and opening strategy. diff --git a/.changeset/witty-steaks-worry.md b/.changeset/witty-steaks-worry.md index d7ec22d0a..f92c5e058 100644 --- a/.changeset/witty-steaks-worry.md +++ b/.changeset/witty-steaks-worry.md @@ -2,4 +2,4 @@ '@powersync/common': minor --- -Serializing upload and download errors for SyncStatus events. Small changes to how delay values are passed to the sync implementation internally. +- Improved serializing of upload and download errors for SyncStatus events. Some JS `Error`s are not cloneable, the JSON representation of a SyncStatus should now always be cloneable. diff --git a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts index 2b368400c..78955858b 100644 --- a/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +++ b/packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts @@ -429,7 +429,7 @@ The next upload iteration will be delayed.`); uploadError: ex } }); - await this.delayRetry(controller.signal, this.options.crudUploadThrottleMs); + await this.delayRetry(controller.signal); if (!this.isConnected) { // Exit the upload loop if the sync stream is no longer connected break; @@ -1216,14 +1216,14 @@ The next upload iteration will be delayed.`); this.iterateListeners((cb) => cb.statusUpdated?.(options)); } - private async delayRetry(signal?: AbortSignal, delayMs?: number): Promise { + private async delayRetry(signal?: AbortSignal): Promise { return new Promise((resolve) => { if (signal?.aborted) { // If the signal is already aborted, resolve immediately resolve(); return; } - const delay = delayMs ?? this.options.retryDelayMs; + const { retryDelayMs } = this.options; let timeoutId: ReturnType | undefined; @@ -1237,7 +1237,7 @@ The next upload iteration will be delayed.`); }; signal?.addEventListener('abort', endDelay, { once: true }); - timeoutId = setTimeout(endDelay, delay); + timeoutId = setTimeout(endDelay, retryDelayMs); }); } From 9cadf24dad959afb52ee3e1f93b2a4d212336cc0 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 11:49:11 +0200 Subject: [PATCH 38/44] try stabilize test --- packages/web/tests/multiple_instances.test.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index f83a9e230..24814f29a 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -41,7 +41,7 @@ describe('Multiple Instances', { sequential: true }, () => { sharedMockSyncServiceTest( 'should broadcast logs from shared sync worker', { timeout: 10_000 }, - async ({ context: { database, connect, openDatabase } }) => { + async ({ context: { openDatabase } }) => { const logger = createLogger('test-logger'); const spiedErrorLogger = vi.spyOn(logger, 'error'); const spiedDebugLogger = vi.spyOn(logger, 'debug'); @@ -61,18 +61,6 @@ describe('Multiple Instances', { sequential: true }, () => { uploadData: async (db) => {} }); - // Should log that a connection attempt has been made - const message = 'Streaming sync iteration started'; - await vi.waitFor( - () => - expect( - spiedDebugLogger.mock.calls - .flat(1) - .find((argument) => typeof argument == 'string' && argument.includes(message)) - ).exist, - { timeout: 2000 } - ); - await vi.waitFor(async () => { const syncService = await getMockSyncServiceFromWorker(powersync.database.name); if (!syncService) { @@ -86,6 +74,18 @@ describe('Multiple Instances', { sequential: true }, () => { await syncService.completeResponse(pendingRequestId); }); + // Should log that a connection attempt has been made + const message = 'Streaming sync iteration started'; + await vi.waitFor( + () => + expect( + spiedDebugLogger.mock.calls + .flat(1) + .find((argument) => typeof argument == 'string' && argument.includes(message)) + ).exist, + { timeout: 2000 } + ); + // The connection should fail with an error await vi.waitFor(() => expect(spiedErrorLogger.mock.calls.length).gt(0), { timeout: 2000 }); } @@ -232,7 +232,7 @@ describe('Multiple Instances', { sequential: true }, () => { // connect the second database in order for it to have access to the sync service. secondDatabase.connect(createTestConnector()); // Timing of this can be tricky due to the need for responding to a pending request. - await vi.waitFor(() => expect(secondDatabase.currentStatus.connecting).true); + await vi.waitFor(() => expect(secondDatabase.currentStatus.connecting).true, { timeout: 2000 }); // connect the first database - this will actually connect to the sync service. await connect(); From 4a12be8da62171202f1ef1cbf69008d89246a4ed Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 12:02:40 +0200 Subject: [PATCH 39/44] increase timeouts --- packages/web/tests/multiple_instances.test.ts | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/web/tests/multiple_instances.test.ts b/packages/web/tests/multiple_instances.test.ts index 24814f29a..a8a239fc4 100644 --- a/packages/web/tests/multiple_instances.test.ts +++ b/packages/web/tests/multiple_instances.test.ts @@ -5,7 +5,6 @@ import { beforeAll, describe, expect, it, onTestFinished, vi } from 'vitest'; import { LockedAsyncDatabaseAdapter } from '../src/db/adapters/LockedAsyncDatabaseAdapter'; import { WebDBAdapter } from '../src/db/adapters/WebDBAdapter'; import { WorkerWrappedAsyncDatabaseConnection } from '../src/db/adapters/WorkerWrappedAsyncDatabaseConnection'; -import { getMockSyncServiceFromWorker } from './utils/MockSyncServiceClient'; import { createTestConnector, sharedMockSyncServiceTest } from './utils/mockSyncServiceTest'; import { generateTestDb, testSchema } from './utils/testDb'; @@ -41,7 +40,7 @@ describe('Multiple Instances', { sequential: true }, () => { sharedMockSyncServiceTest( 'should broadcast logs from shared sync worker', { timeout: 10_000 }, - async ({ context: { openDatabase } }) => { + async ({ context: { openDatabase, mockService } }) => { const logger = createLogger('test-logger'); const spiedErrorLogger = vi.spyOn(logger, 'error'); const spiedDebugLogger = vi.spyOn(logger, 'debug'); @@ -61,18 +60,17 @@ describe('Multiple Instances', { sequential: true }, () => { uploadData: async (db) => {} }); - await vi.waitFor(async () => { - const syncService = await getMockSyncServiceFromWorker(powersync.database.name); - if (!syncService) { - throw new Error('Sync service not found'); - } - const requests = await syncService.getPendingRequests(); - expect(requests.length).toBeGreaterThan(0); - const pendingRequestId = requests[0].id; - // Generate an error - await syncService.createResponse(pendingRequestId, 401, { 'Content-Type': 'application/json' }); - await syncService.completeResponse(pendingRequestId); - }); + await vi.waitFor( + async () => { + const requests = await mockService.getPendingRequests(); + expect(requests.length).toBeGreaterThan(0); + const pendingRequestId = requests[0].id; + // Generate an error + await mockService.createResponse(pendingRequestId, 401, { 'Content-Type': 'application/json' }); + await mockService.completeResponse(pendingRequestId); + }, + { timeout: 3_000 } + ); // Should log that a connection attempt has been made const message = 'Streaming sync iteration started'; @@ -224,22 +222,26 @@ describe('Multiple Instances', { sequential: true }, () => { await watchedPromise; }); - sharedMockSyncServiceTest('should share sync updates', async ({ context: { database, connect, openDatabase } }) => { - const secondDatabase = openDatabase(); + sharedMockSyncServiceTest( + 'should share sync updates', + { timeout: 10_000 }, + async ({ context: { database, connect, openDatabase } }) => { + const secondDatabase = openDatabase(); - expect(database.currentStatus.connected).false; - expect(secondDatabase.currentStatus.connected).false; - // connect the second database in order for it to have access to the sync service. - secondDatabase.connect(createTestConnector()); - // Timing of this can be tricky due to the need for responding to a pending request. - await vi.waitFor(() => expect(secondDatabase.currentStatus.connecting).true, { timeout: 2000 }); - // connect the first database - this will actually connect to the sync service. - await connect(); + expect(database.currentStatus.connected).false; + expect(secondDatabase.currentStatus.connected).false; + // connect the second database in order for it to have access to the sync service. + secondDatabase.connect(createTestConnector()); + // Timing of this can be tricky due to the need for responding to a pending request. + await vi.waitFor(() => expect(secondDatabase.currentStatus.connecting).true, { timeout: 5_000 }); + // connect the first database - this will actually connect to the sync service. + await connect(); - expect(database.currentStatus.connected).true; + expect(database.currentStatus.connected).true; - await vi.waitFor(() => expect(secondDatabase.currentStatus.connected).true, { timeout: 3000 }); - }); + await vi.waitFor(() => expect(secondDatabase.currentStatus.connected).true, { timeout: 5_000 }); + } + ); sharedMockSyncServiceTest( 'should trigger uploads from last connected clients', From eb88fde8de6cbeed5fe2db316ec7ed7b97d07cb4 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 16:22:30 +0200 Subject: [PATCH 40/44] cleanup any opened access handles if an error is caught. --- .../db/adapters/LockedAsyncDatabaseAdapter.ts | 23 ++++++++++++++++--- packages/web/src/worker/db/opfs.ts | 7 ++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 77a56fcd0..9fae2dffa 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -10,7 +10,7 @@ import { createLogger, type ILogger } from '@powersync/common'; -import { getNavigatorLocks } from '../..//shared/navigator'; +import { getNavigatorLocks } from '../../shared/navigator'; import { AsyncDatabaseConnection, ConnectionClosedError } from './AsyncDatabaseConnection'; import { SharedConnectionWorker, WebDBAdapter } from './WebDBAdapter'; import { WorkerWrappedAsyncDatabaseConnection } from './WorkerWrappedAsyncDatabaseConnection'; @@ -120,8 +120,9 @@ export class LockedAsyncDatabaseAdapter // Dispose any previous table change listener. this._disposeTableChangeListener?.(); this._disposeTableChangeListener = null; - + this._db?.close().catch((ex) => this.logger.warn(`Error closing database before opening new instance`, ex)); const isReOpen = !!this._db; + this._db = null; this._db = await this.options.openConnection(); await this._db.init(); @@ -159,7 +160,23 @@ export class LockedAsyncDatabaseAdapter } protected async _init() { - await this.openInternalDB(); + /** + * For OPFS, we can see this open call sometimes fail due to NoModificationAllowedError. + * We should be able to recover from this by re-opening the database. + */ + const maxAttempts = 3; + for (let count = 0; count < maxAttempts; count++) { + try { + await this.openInternalDB(); + break; + } catch (ex) { + if (count == maxAttempts - 1) { + throw ex; + } + this.logger.warn(`Attempt ${count + 1} of ${maxAttempts} to open database failed, retrying in 1 second...`, ex); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } this.iterateListeners((cb) => cb.initialized?.()); } diff --git a/packages/web/src/worker/db/opfs.ts b/packages/web/src/worker/db/opfs.ts index 0f4b40799..34daceaad 100644 --- a/packages/web/src/worker/db/opfs.ts +++ b/packages/web/src/worker/db/opfs.ts @@ -549,6 +549,13 @@ export class OPFSCoopSyncVFS extends FacadeVFS { ); } catch (e) { this.log?.(`failed to create access handles for ${file.path}`, e); + // Close any of the potentially opened access handles + DB_RELATED_FILE_SUFFIXES.forEach(async (suffix) => { + const persistentFile = this.persistentFiles.get(file.path + suffix); + if (persistentFile) { + persistentFile.accessHandle?.close(); + } + }); // Release the lock, if we failed here, we'd need to obtain the lock later in order to retry file.persistentFile.handleLockReleaser(); throw e; From 423e4e4a8b5669bb22dc25138137219ccdfd1938 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 16:23:57 +0200 Subject: [PATCH 41/44] cleanup tests structure --- packages/web/tests/mocks/MockWebRemote.ts | 2 +- .../web/{src/worker/sync => tests/utils}/MockSyncService.ts | 0 packages/web/tests/utils/MockSyncServiceClient.ts | 2 +- .../{src/worker/sync => tests/utils}/MockSyncServiceTypes.ts | 0 .../{src/worker/sync => tests/utils}/MockSyncServiceWorker.ts | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename packages/web/{src/worker/sync => tests/utils}/MockSyncService.ts (100%) rename packages/web/{src/worker/sync => tests/utils}/MockSyncServiceTypes.ts (100%) rename packages/web/{src/worker/sync => tests/utils}/MockSyncServiceWorker.ts (100%) diff --git a/packages/web/tests/mocks/MockWebRemote.ts b/packages/web/tests/mocks/MockWebRemote.ts index 83471a202..5613a3f98 100644 --- a/packages/web/tests/mocks/MockWebRemote.ts +++ b/packages/web/tests/mocks/MockWebRemote.ts @@ -11,7 +11,7 @@ import { SocketSyncStreamOptions } from '@powersync/common'; import { serialize, type BSON } from 'bson'; -import { MockSyncService, setupMockServiceMessageHandler } from '../../src/worker/sync/MockSyncServiceWorker'; +import { MockSyncService, setupMockServiceMessageHandler } from '../utils/MockSyncServiceWorker'; /** * Mock fetch provider that intercepts all requests and routes them to the mock sync service. diff --git a/packages/web/src/worker/sync/MockSyncService.ts b/packages/web/tests/utils/MockSyncService.ts similarity index 100% rename from packages/web/src/worker/sync/MockSyncService.ts rename to packages/web/tests/utils/MockSyncService.ts diff --git a/packages/web/tests/utils/MockSyncServiceClient.ts b/packages/web/tests/utils/MockSyncServiceClient.ts index 74804b857..c099e5b77 100644 --- a/packages/web/tests/utils/MockSyncServiceClient.ts +++ b/packages/web/tests/utils/MockSyncServiceClient.ts @@ -4,7 +4,7 @@ import type { MockSyncServiceMessage, MockSyncServiceResponse, PendingRequest -} from '../../src/worker/sync/MockSyncServiceTypes'; +} from './MockSyncServiceTypes'; /** * Interface for mocking sync service responses in shared worker environments. diff --git a/packages/web/src/worker/sync/MockSyncServiceTypes.ts b/packages/web/tests/utils/MockSyncServiceTypes.ts similarity index 100% rename from packages/web/src/worker/sync/MockSyncServiceTypes.ts rename to packages/web/tests/utils/MockSyncServiceTypes.ts diff --git a/packages/web/src/worker/sync/MockSyncServiceWorker.ts b/packages/web/tests/utils/MockSyncServiceWorker.ts similarity index 100% rename from packages/web/src/worker/sync/MockSyncServiceWorker.ts rename to packages/web/tests/utils/MockSyncServiceWorker.ts From 59b9ce9bebdd064ba1b20491a7ade47a8c27f3d5 Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 16:31:59 +0200 Subject: [PATCH 42/44] remove dev logs --- packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts index 02841c0ca..1d3afc47a 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts @@ -125,10 +125,9 @@ export const DEFAULT_MODULE_FACTORIES = { module = await SyncWASQLiteModuleFactory(); } const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module); - (vfs as any).log = (...args: any[]) => console.log('[OPFSCoopSyncVFS]', ...args); return { module, - vfs: vfs as any + vfs }; } }; From 23ae289b6f7353ea2973f580bd999f9e1ca10ccf Mon Sep 17 00:00:00 2001 From: stevensJourney Date: Wed, 3 Dec 2025 16:34:52 +0200 Subject: [PATCH 43/44] fix types --- packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts index 1d3afc47a..6e09699b0 100644 --- a/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts +++ b/packages/web/src/db/adapters/wa-sqlite/WASQLiteConnection.ts @@ -127,7 +127,7 @@ export const DEFAULT_MODULE_FACTORIES = { const vfs = await OPFSCoopSyncVFS.create(options.dbFileName, module); return { module, - vfs + vfs: vfs as any }; } }; From 4108c8e2eb08d35d6bb61e54ab97cae64f189b49 Mon Sep 17 00:00:00 2001 From: Christiaan Landman Date: Wed, 3 Dec 2025 16:40:00 +0200 Subject: [PATCH 44/44] Changesets for Drizzle/Kysely. --- .changeset/unlucky-jokes-doubt.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/unlucky-jokes-doubt.md diff --git a/.changeset/unlucky-jokes-doubt.md b/.changeset/unlucky-jokes-doubt.md new file mode 100644 index 000000000..78677899f --- /dev/null +++ b/.changeset/unlucky-jokes-doubt.md @@ -0,0 +1,6 @@ +--- +'@powersync/drizzle-driver': patch +'@powersync/kysely-driver': patch +--- + +Updating `wa-sqlite` to ^1.3.4.