From 01070e136ecd3d4202e77e683649dd4f78b504ce Mon Sep 17 00:00:00 2001 From: joshua-journey-apps Date: Mon, 6 Oct 2025 15:40:26 +0200 Subject: [PATCH 01/19] initial implementation --- .../src/AbstractAttachmentQueue.ts | 539 ------------------ packages/attachments/src/AttachmentContext.ts | 159 ++++++ packages/attachments/src/AttachmentQueue.ts | 243 ++++++++ .../attachments/src/LocalStorageAdapter.ts | 49 ++ .../attachments/src/RemoteStorageAdapter.ts | 27 + packages/attachments/src/Schema.ts | 66 ++- packages/attachments/src/StorageAdapter.ts | 28 - packages/attachments/src/StorageService.ts | 158 +++++ packages/attachments/src/SyncErrorHandler.ts | 28 + .../attachments/src/WatchedAttachmentItem.ts | 14 + packages/attachments/src/index.ts | 8 +- .../IndexDBFileSystemAdapter.ts | 113 ++++ .../storageAdapters/NodeFileSystemAdapter.ts | 71 +++ .../tests/attachments/AttachmentQueue.test.ts | 116 ++-- 14 files changed, 972 insertions(+), 647 deletions(-) delete mode 100644 packages/attachments/src/AbstractAttachmentQueue.ts create mode 100644 packages/attachments/src/AttachmentContext.ts create mode 100644 packages/attachments/src/AttachmentQueue.ts create mode 100644 packages/attachments/src/LocalStorageAdapter.ts create mode 100644 packages/attachments/src/RemoteStorageAdapter.ts delete mode 100644 packages/attachments/src/StorageAdapter.ts create mode 100644 packages/attachments/src/StorageService.ts create mode 100644 packages/attachments/src/SyncErrorHandler.ts create mode 100644 packages/attachments/src/WatchedAttachmentItem.ts create mode 100644 packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts create mode 100644 packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts diff --git a/packages/attachments/src/AbstractAttachmentQueue.ts b/packages/attachments/src/AbstractAttachmentQueue.ts deleted file mode 100644 index de835d44e..000000000 --- a/packages/attachments/src/AbstractAttachmentQueue.ts +++ /dev/null @@ -1,539 +0,0 @@ -import { AbstractPowerSyncDatabase, Transaction } from '@powersync/common'; -import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js'; -import { EncodingType, StorageAdapter } from './StorageAdapter.js'; - -export interface AttachmentQueueOptions { - powersync: AbstractPowerSyncDatabase; - storage: StorageAdapter; - /** - * How often to check for new attachments to sync, in milliseconds. Set to 0 or undefined to disable. - */ - syncInterval?: number; - /** - * How many attachments to keep in the cache - */ - cacheLimit?: number; - /** - * The name of the directory where attachments are stored on the device, not the full path. Defaults to `attachments`. - */ - attachmentDirectoryName?: string; - - /** - * The name of the table where attachments are stored, defaults to `attachments` table. - */ - attachmentTableName?: string; - /** - * Whether to mark the initial watched attachment IDs to be synced - */ - performInitialSync?: boolean; - /** - * Should attachments be downloaded - */ - downloadAttachments?: boolean; - /** - * How to handle download errors, return { retry: false } to ignore the download - */ - onDownloadError?: (attachment: AttachmentRecord, exception: any) => Promise<{ retry?: boolean }>; - /** - * How to handle upload errors, return { retry: false } to ignore the upload - */ - onUploadError?: (attachment: AttachmentRecord, exception: any) => Promise<{ retry?: boolean }>; -} - -export const DEFAULT_ATTACHMENT_QUEUE_OPTIONS: Partial = { - attachmentDirectoryName: ATTACHMENT_TABLE, - attachmentTableName: ATTACHMENT_TABLE, - syncInterval: 30_000, - cacheLimit: 100, - performInitialSync: true, - downloadAttachments: true -}; - -export abstract class AbstractAttachmentQueue { - uploading: boolean; - downloading: boolean; - initialSync: boolean; - options: T; - downloadQueue: Set; - - constructor(options: T) { - this.options = { - ...DEFAULT_ATTACHMENT_QUEUE_OPTIONS, - ...options - }; - this.downloadQueue = new Set(); - this.uploading = false; - this.downloading = false; - this.initialSync = this.options.performInitialSync; - } - - /** - * Takes in a callback that gets invoked with attachment IDs that need to be synced. - * In most cases this will contain a watch query. - * - * @example - * ```javascript - * onAttachmentIdsChange(onUpdate) { - * this.powersync.watch('SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL', [], { - * onResult: (result) => onUpdate(result.rows?._array.map((r) => r.id) ?? []) - * }); - * } - * ``` - */ - abstract onAttachmentIdsChange(onUpdate: (ids: string[]) => void): void; - - /** - * Create a new AttachmentRecord, this gets called when the attachment id is not found in the database. - */ - abstract newAttachmentRecord(record?: Partial): Promise; - - protected get powersync() { - return this.options.powersync; - } - - get logger() { - return this.powersync.logger ?? console; - } - - protected get storage() { - return this.options.storage; - } - - get table() { - return this.options.attachmentTableName!; - } - - async init() { - // Ensure the directory where attachments are downloaded, exists - await this.storage.makeDir(this.storageDirectory); - - this.watchAttachmentIds(); - this.watchUploads(); - this.watchDownloads(); - - if (this.options.syncInterval > 0) { - // In addition to watching for changes, we also trigger a sync every few seconds (30 seconds, by default) - // This will retry any failed uploads/downloads, in particular after the app was offline - setInterval(() => this.trigger(), this.options.syncInterval); - } - } - - trigger() { - this.uploadRecords(); - this.downloadRecords(); - this.expireCache(); - } - - async watchAttachmentIds() { - this.onAttachmentIdsChange(async (ids) => { - const _ids = `${ids.map((id) => `'${id}'`).join(',')}`; - this.logger.debug(`Queuing for sync, attachment IDs: [${_ids}]`); - - if (this.initialSync) { - this.initialSync = false; - // Mark AttachmentIds for sync - await this.powersync.execute( - `UPDATE - ${this.table} - SET state = ${AttachmentState.QUEUED_SYNC} - WHERE - state < ${AttachmentState.SYNCED} - AND - id IN (${_ids})` - ); - } - - const attachmentsInDatabase = await this.powersync.getAll( - `SELECT * FROM ${this.table} WHERE state < ${AttachmentState.ARCHIVED}` - ); - - for (const id of ids) { - const record = attachmentsInDatabase.find((r) => r.id == id); - // 1. ID is not in the database - if (!record) { - const newRecord = await this.newAttachmentRecord({ - id: id, - state: AttachmentState.QUEUED_SYNC - }); - this.logger.debug(`Attachment (${id}) not found in database, creating new record`); - await this.saveToQueue(newRecord); - } else if (record.local_uri == null || !(await this.storage.fileExists(this.getLocalUri(record.local_uri)))) { - // 2. Attachment in database but no local file, mark as queued download - this.logger.debug(`Attachment (${id}) found in database but no local file, marking as queued download`); - await this.update({ - ...record, - state: AttachmentState.QUEUED_DOWNLOAD - }); - } - } - - // 3. Attachment in database and not in AttachmentIds, mark as archived - await this.powersync.execute( - `UPDATE ${this.table} - SET state = ${AttachmentState.ARCHIVED} - WHERE - state < ${AttachmentState.ARCHIVED} - AND - id NOT IN (${ids.map((id) => `'${id}'`).join(',')})` - ); - }); - } - - async saveToQueue(record: Omit): Promise { - const updatedRecord: AttachmentRecord = { - ...record, - timestamp: new Date().getTime() - }; - - await this.powersync.execute( - `INSERT OR REPLACE INTO ${this.table} (id, timestamp, filename, local_uri, media_type, size, state) VALUES (?, ?, ?, ?, ?, ?, ?)`, - [ - updatedRecord.id, - updatedRecord.timestamp, - updatedRecord.filename, - updatedRecord.local_uri || null, - updatedRecord.media_type || null, - updatedRecord.size || null, - updatedRecord.state - ] - ); - - return updatedRecord; - } - - async record(id: string): Promise { - return this.powersync.getOptional(`SELECT * FROM ${this.table} WHERE id = ?`, [id]); - } - - async update(record: Omit): Promise { - const timestamp = new Date().getTime(); - await this.powersync.execute( - `UPDATE ${this.table} - SET - timestamp = ?, - filename = ?, - local_uri = ?, - size = ?, - media_type = ?, - state = ? - WHERE id = ?`, - [timestamp, record.filename, record.local_uri || null, record.size, record.media_type, record.state, record.id] - ); - } - - async delete(record: AttachmentRecord, tx?: Transaction): Promise { - const deleteRecord = async (tx: Transaction) => { - await tx.execute( - `DELETE - FROM ${this.table} - WHERE id = ?`, - [record.id] - ); - }; - - if (tx) { - await deleteRecord(tx); - } else { - await this.powersync.writeTransaction(deleteRecord); - } - - const localFilePathUri = this.getLocalUri(record.local_uri || this.getLocalFilePathSuffix(record.filename)); - - try { - // Delete file on storage - await this.storage.deleteFile(localFilePathUri, { - filename: record.filename - }); - } catch (e) { - this.logger.error(e); - } - } - - async getNextUploadRecord(): Promise { - return this.powersync.getOptional( - `SELECT * - FROM ${this.table} - WHERE - local_uri IS NOT NULL - AND - (state = ${AttachmentState.QUEUED_UPLOAD} - OR - state = ${AttachmentState.QUEUED_SYNC}) - ORDER BY timestamp ASC` - ); - } - - async uploadAttachment(record: AttachmentRecord) { - if (!record.local_uri) { - throw new Error(`No local_uri for record ${JSON.stringify(record, null, 2)}`); - } - - const localFilePathUri = this.getLocalUri(record.local_uri); - try { - if (!(await this.storage.fileExists(localFilePathUri))) { - this.logger.warn(`File for ${record.id} does not exist, skipping upload`); - await this.update({ - ...record, - state: AttachmentState.QUEUED_DOWNLOAD - }); - return true; - } - - const fileBuffer = await this.storage.readFile(localFilePathUri, { - encoding: EncodingType.Base64, - mediaType: record.media_type - }); - - await this.storage.uploadFile(record.filename, fileBuffer, { - mediaType: record.media_type - }); - // Mark as uploaded - await this.update({ ...record, state: AttachmentState.SYNCED }); - this.logger.debug(`Uploaded attachment "${record.id}" to Cloud Storage`); - return true; - } catch (e: any) { - if (e.error == 'Duplicate') { - this.logger.debug(`File already uploaded, marking ${record.id} as synced`); - await this.update({ ...record, state: AttachmentState.SYNCED }); - return false; - } - if (this.options.onUploadError) { - const { retry } = await this.options.onUploadError(record, e); - if (!retry) { - await this.update({ ...record, state: AttachmentState.ARCHIVED }); - return true; - } - } - this.logger.error(`UploadAttachment error for record ${JSON.stringify(record, null, 2)}`); - return false; - } - } - - async downloadRecord(record: AttachmentRecord) { - if (!this.options.downloadAttachments) { - return false; - } - if (!record.local_uri) { - record.local_uri = this.getLocalFilePathSuffix(record.filename); - } - const localFilePathUri = this.getLocalUri(record.local_uri); - if (await this.storage.fileExists(localFilePathUri)) { - this.logger.debug(`Local file already downloaded, marking "${record.id}" as synced`); - await this.update({ ...record, state: AttachmentState.SYNCED }); - return true; - } - - try { - const fileBlob = await this.storage.downloadFile(record.filename); - - // Convert the blob data into a base64 string - const base64Data = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - // remove the header from the result: 'data:*/*;base64,' - resolve(reader.result?.toString().replace(/^data:.+;base64,/, '') || ''); - }; - reader.onerror = reject; - reader.readAsDataURL(fileBlob); - }); - - // Ensure directory exists - await this.storage.makeDir(localFilePathUri.replace(record.filename, '')); - // Write the file - await this.storage.writeFile(localFilePathUri, base64Data, { - encoding: EncodingType.Base64 - }); - - await this.update({ - ...record, - media_type: fileBlob.type, - state: AttachmentState.SYNCED - }); - this.logger.debug(`Downloaded attachment "${record.id}"`); - return true; - } catch (e) { - if (this.options.onDownloadError) { - const { retry } = await this.options.onDownloadError(record, e); - if (!retry) { - await this.update({ ...record, state: AttachmentState.ARCHIVED }); - return true; - } - } - this.logger.error(`Download attachment error for record ${JSON.stringify(record, null, 2)}`, e); - } - return false; - } - - idsToUpload(onResult: (ids: string[]) => void): void { - this.powersync.watch( - `SELECT id - FROM ${this.table} - WHERE - local_uri IS NOT NULL - AND - (state = ${AttachmentState.QUEUED_UPLOAD} - OR - state = ${AttachmentState.QUEUED_SYNC})`, - [], - { onResult: (result) => onResult(result.rows?._array.map((r) => r.id) || []) } - ); - } - - watchUploads() { - this.idsToUpload(async (ids) => { - if (ids.length > 0) { - await this.uploadRecords(); - } - }); - } - - /** - * Returns immediately if another loop is in progress. - */ - private async uploadRecords() { - if (this.uploading) { - return; - } - this.uploading = true; - try { - let record = await this.getNextUploadRecord(); - if (!record) { - return; - } - this.logger.debug(`Uploading attachments...`); - while (record) { - const uploaded = await this.uploadAttachment(record); - if (!uploaded) { - // Then attachment failed to upload. We try all uploads when the next trigger() is called - break; - } - record = await this.getNextUploadRecord(); - } - this.logger.debug('Finished uploading attachments'); - } catch (error) { - this.logger.error('Upload failed:', error); - } finally { - this.uploading = false; - } - } - - async getIdsToDownload(): Promise { - const res = await this.powersync.getAll<{ id: string }>( - `SELECT id - FROM ${this.table} - WHERE - state = ${AttachmentState.QUEUED_DOWNLOAD} - OR - state = ${AttachmentState.QUEUED_SYNC} - ORDER BY timestamp ASC` - ); - return res.map((r) => r.id); - } - - idsToDownload(onResult: (ids: string[]) => void): void { - this.powersync.watch( - `SELECT id - FROM ${this.table} - WHERE - state = ${AttachmentState.QUEUED_DOWNLOAD} - OR - state = ${AttachmentState.QUEUED_SYNC}`, - [], - { onResult: (result) => onResult(result.rows?._array.map((r) => r.id) || []) } - ); - } - - watchDownloads() { - if (!this.options.downloadAttachments) { - return; - } - this.idsToDownload(async (ids) => { - ids.map((id) => this.downloadQueue.add(id)); - // No need to await this, the lock will ensure only one loop is running at a time - this.downloadRecords(); - }); - } - - private async downloadRecords() { - if (!this.options.downloadAttachments) { - return; - } - if (this.downloading) { - return; - } - (await this.getIdsToDownload()).map((id) => this.downloadQueue.add(id)); - if (this.downloadQueue.size == 0) { - return; - } - - this.downloading = true; - try { - this.logger.debug(`Downloading ${this.downloadQueue.size} attachments...`); - while (this.downloadQueue.size > 0) { - const id = this.downloadQueue.values().next().value; - this.downloadQueue.delete(id); - const record = await this.record(id); - if (!record) { - continue; - } - await this.downloadRecord(record); - } - this.logger.debug('Finished downloading attachments'); - } catch (e) { - this.logger.error('Downloads failed:', e); - } finally { - this.downloading = false; - } - } - - /** - * Returns the local file path for the given filename, used to store in the database. - * Example: filename: "attachment-1.jpg" returns "attachments/attachment-1.jpg" - */ - getLocalFilePathSuffix(filename: string): string { - return `${this.options.attachmentDirectoryName}/${filename}`; - } - - /** - * Return users storage directory with the attachmentPath use to load the file. - * Example: filePath: "attachments/attachment-1.jpg" returns "/var/mobile/Containers/Data/Application/.../Library/attachments/attachment-1.jpg" - */ - getLocalUri(filePath: string): string { - return `${this.storage.getUserStorageDirectory()}/${filePath}`; - } - - /** - * Returns the directory where attachments are stored on the device, used to make dir - * Example: "/var/mobile/Containers/Data/Application/.../Library/attachments/" - */ - get storageDirectory() { - return `${this.storage.getUserStorageDirectory()}${this.options.attachmentDirectoryName}`; - } - - async expireCache() { - const res = await this.powersync.getAll(`SELECT * FROM ${this.table} - WHERE - state = ${AttachmentState.SYNCED} OR state = ${AttachmentState.ARCHIVED} - ORDER BY - timestamp DESC - LIMIT 100 OFFSET ${this.options.cacheLimit}`); - - if (res.length == 0) { - return; - } - - this.logger.debug(`Deleting ${res.length} attachments from cache...`); - await this.powersync.writeTransaction(async (tx) => { - for (const record of res) { - await this.delete(record, tx); - } - }); - } - - async clearQueue(): Promise { - this.logger.debug(`Clearing attachment queue...`); - await this.powersync.writeTransaction(async (tx) => { - await tx.execute(`DELETE FROM ${this.table}`); - }); - } -} diff --git a/packages/attachments/src/AttachmentContext.ts b/packages/attachments/src/AttachmentContext.ts new file mode 100644 index 000000000..e6d13cea8 --- /dev/null +++ b/packages/attachments/src/AttachmentContext.ts @@ -0,0 +1,159 @@ +import { AbstractPowerSyncDatabase, ILogger, Transaction } from '@powersync/common'; +import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.js'; + +export class AttachmentContext { + db: AbstractPowerSyncDatabase; + tableName: string; + logger: ILogger; + + constructor(db: AbstractPowerSyncDatabase, tableName: string = 'attachments', logger: ILogger) { + this.db = db; + this.tableName = tableName; + this.logger = logger; + } + + watchActiveAttachments(onUpdate: () => void): AbortController { + const abortController = new AbortController(); + this.db.watchWithCallback( + /* sql */ + ` + SELECT + * + FROM + ${this.tableName} + WHERE + state = ? + OR state = ? + OR state = ? + ORDER BY + timestamp ASC + `, + [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE], + { + onResult: () => { + onUpdate(); + } + }, + { + signal: abortController.signal + } + ); + + return abortController; + } + + async getActiveAttachments(): Promise { + const attachments = await this.db.getAll( + /* sql */ + ` + SELECT + * + FROM + ${this.tableName} + WHERE + state = ? + OR state = ? + OR state = ? + ORDER BY + timestamp ASC + `, + [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE] + ); + + return attachments.map(attachmentFromSql); + } + + async getArchivedAttachments(): Promise { + const attachments = await this.db.getAll( + /* sql */ + ` + SELECT + * + FROM + ${this.tableName} + WHERE + state = ? + ORDER BY + timestamp ASC + `, + [AttachmentState.ARCHIVED] + ); + + return attachments.map(attachmentFromSql); + } + + async getAttachments(): Promise { + const attachments = await this.db.getAll( + /* sql */ + ` + SELECT + * + FROM + ${this.tableName} + ORDER BY + timestamp ASC + `, + [] + ); + + return attachments.map(attachmentFromSql); + } + + upsertAttachment(attachment: AttachmentRecord, context: Transaction): void { + context.execute( + /* sql */ + ` + INSERT + OR REPLACE INTO ${this.tableName} ( + id, + filename, + local_uri, + size, + media_type, + timestamp, + state, + has_synced, + meta_data + ) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + [ + attachment.id, + attachment.filename, + attachment.localUri || null, + attachment.size || null, + attachment.mediaType || null, + attachment.timestamp, + attachment.state, + attachment.hasSynced ? 1 : 0, + attachment.metaData || null + ] + ); + } + + async deleteAttachment(attachmentId: string): Promise { + await this.db.writeTransaction((tx) => + tx.execute( + /* sql */ + ` + DELETE FROM ${this.tableName} + WHERE + id = ? + `, + [attachmentId] + ) + ); + } + + async saveAttachments(attachments: AttachmentRecord[]): Promise { + if (attachments.length === 0) { + return; + } + await this.db.writeTransaction(async (tx) => { + for (const attachment of attachments) { + this.upsertAttachment(attachment, tx); + } + }); + } +} diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts new file mode 100644 index 000000000..2689e1c3f --- /dev/null +++ b/packages/attachments/src/AttachmentQueue.ts @@ -0,0 +1,243 @@ +import { AbstractPowerSyncDatabase, ILogger } from '@powersync/common'; +import { AttachmentContext } from './AttachmentContext.js'; +import { LocalStorageAdapter } from './LocalStorageAdapter.js'; +import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; +import { AttachmentRecord, AttachmentState } from './Schema.js'; +import { StorageService } from './StorageService.js'; +import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; + +export class AttachmentQueue { + periodicSyncTimer?: ReturnType; + syncInterval: number = 30 * 1000; + context: AttachmentContext; + storageService: StorageService; + localStorage: LocalStorageAdapter; + remoteStorage: RemoteStorageAdapter; + downloadAttachments: boolean = true; + watchActiveAbortController?: AbortController; + + constructor({ + db, + localStorage, + remoteStorage, + watchAttachments, + tableName, + logger, + options + }: { + db: AbstractPowerSyncDatabase; + remoteStorage: RemoteStorageAdapter; + localStorage: LocalStorageAdapter; + watchAttachments?: (onUpdate: (attachement: WatchedAttachmentItem[]) => void) => void; + tableName?: string; + logger?: ILogger; + options?: { syncInterval?: number; downloadAttachments?: boolean }; + }) { + this.context = new AttachmentContext(db, tableName, logger ?? db.logger); + this.storageService = new StorageService(this.context, localStorage, remoteStorage, logger ?? db.logger); + if (options?.syncInterval != null) { + this.syncInterval = options.syncInterval; + } + if (options?.downloadAttachments != null) { + this.downloadAttachments = options.downloadAttachments; + } + + this.watchAttachments = watchAttachments ?? this.watchAttachments; + } + + watchAttachments(onUpdate: (attachement: WatchedAttachmentItem[]) => void): void { + throw new Error('watchAttachments not implemented'); + } + + async startSync(): Promise { + await this.stopSync(); + + // Sync storage periodically + this.periodicSyncTimer = setInterval(async () => { + await this.syncStorage(); + }, this.syncInterval); + + // Sync storage when there is a change in active attachments + this.watchActiveAbortController = this.context.watchActiveAttachments(async () => { + await this.syncStorage(); + }); + + // Process attachments when there is a change in watched attachments + this.watchAttachments(async (watchedAttachments) => { + // Need to get all the attachments which are tracked in the DB. + // We might need to restore an archived attachment. + const currentAttachments = await this.context.getAttachments(); + const attachmentUpdates: AttachmentRecord[] = []; + + for (const watchedAttachment of watchedAttachments) { + const existingQueueItem = currentAttachments.find((a) => a.id === watchedAttachment.id); + if (!existingQueueItem) { + // Item is watched but not in the queue yet. Need to add it. + + if (!this.downloadAttachments) { + continue; + } + + const filename = `${watchedAttachment.id}.${watchedAttachment.fileExtension}`; + + attachmentUpdates.push({ + id: watchedAttachment.id, + filename, + state: AttachmentState.QUEUED_DOWNLOAD, + hasSynced: false, + metaData: watchedAttachment.metaData + }); + continue; + } + + if (existingQueueItem.state === AttachmentState.ARCHIVED) { + // The attachment is present again. Need to queue it for sync. + // We might be able to optimize this in future + if (existingQueueItem.hasSynced === true) { + // No remote action required, we can restore the record (avoids deletion) + attachmentUpdates.push({ + ...existingQueueItem, + state: AttachmentState.SYNCED + }); + } else { + // The localURI should be set if the record was meant to be downloaded + // and hasSynced is false then + // it must be an upload operation + const newState = + existingQueueItem.localUri == null ? AttachmentState.QUEUED_DOWNLOAD : AttachmentState.QUEUED_UPLOAD; + + attachmentUpdates.push({ + ...existingQueueItem, + state: newState + }); + } + } + } + + for (const attachment of currentAttachments) { + const notInWatchedItems = watchedAttachments.find((i) => i.id === attachment.id) == null; + if (notInWatchedItems) { + switch (attachment.state) { + case AttachmentState.QUEUED_DELETE: + case AttachmentState.QUEUED_UPLOAD: + // Only archive if it has synced + if (attachment.hasSynced === true) { + attachmentUpdates.push({ + ...attachment, + state: AttachmentState.ARCHIVED + }); + } + break; + default: + // Archive other states such as QUEUED_DOWNLOAD + attachmentUpdates.push({ + ...attachment, + state: AttachmentState.ARCHIVED + }); + } + } + } + + if (attachmentUpdates.length > 0) { + await this.context.saveAttachments(attachmentUpdates); + } + }); + } + + // Sync storage with all active attachments + async syncStorage(): Promise { + const activeAttachments = await this.context.getActiveAttachments(); + await this.localStorage.initialize(); + await this.storageService.processAttachments(activeAttachments); + await this.storageService.deleteArchivedAttachments(); + } + + async stopSync(): Promise { + clearInterval(this.periodicSyncTimer); + this.periodicSyncTimer = undefined; + this.watchActiveAbortController?.abort(); + } + + getLocalUri(filePath: string): string { + return `${this.localStorage.getUserStorageDirectory()}/${filePath}`; + } + + async saveFile({ + data, + fileExtension, + mediaType, + metaData, + id + }: { + data: ArrayBuffer | Blob | string; + fileExtension: string; + mediaType?: string; + metaData?: string; + id?: string; + }): Promise { + const resolvedId = id ?? (await this.context.db.get<{ id: string }>('SELECT uuid() as id')).id; + const filename = `${resolvedId}.${fileExtension}`; + const localUri = this.getLocalUri(filename); + const size = await this.localStorage.saveFile(localUri, data); + + const attachment: AttachmentRecord = { + id: resolvedId, + filename, + mediaType, + localUri, + state: AttachmentState.QUEUED_UPLOAD, + hasSynced: false, + size, + timestamp: new Date().getTime(), + metaData + }; + + await this.context.db.writeTransaction(async (tx) => { + this.context.upsertAttachment(attachment, tx); + }); + + return attachment; + } + + verifyAttachments = async (): Promise => { + const attachments = await this.context.getAttachments(); + const updates: AttachmentRecord[] = []; + + for (const attachment of attachments) { + if (attachment.localUri == null) { + continue; + } + + const exists = await this.localStorage.fileExists(attachment.localUri); + if (exists) { + // The file exists, this is correct + continue; + } + + const newLocalUri = this.getLocalUri(attachment.filename); + const newExists = await this.localStorage.fileExists(newLocalUri); + if (newExists) { + // The file exists but the localUri is broken, lets update it. + updates.push({ + ...attachment, + localUri: newLocalUri + }); + } else if (attachment.state === AttachmentState.QUEUED_UPLOAD || attachment.state === AttachmentState.ARCHIVED) { + // The file must have been removed from the local storage before upload was completed + updates.push({ + ...attachment, + state: AttachmentState.ARCHIVED, + localUri: undefined // Clears the value + }); + } else if (attachment.state === AttachmentState.SYNCED) { + // The file was downloaded, but removed - trigger redownload + updates.push({ + ...attachment, + state: AttachmentState.QUEUED_DOWNLOAD + }); + } + } + + await this.context.saveAttachments(updates); + }; +} diff --git a/packages/attachments/src/LocalStorageAdapter.ts b/packages/attachments/src/LocalStorageAdapter.ts new file mode 100644 index 000000000..03de44df4 --- /dev/null +++ b/packages/attachments/src/LocalStorageAdapter.ts @@ -0,0 +1,49 @@ +export enum EncodingType { + UTF8 = 'utf8', + Base64 = 'base64' +} + +export interface LocalStorageAdapter { + /** + * Saves buffer data to a local file. + * @param filePath Path where the file will be stored + * @param data Data string to store + * @returns number of bytes written + */ + saveFile(filePath: string, data: ArrayBuffer | Blob | string): Promise; + + /** + * Retrieves an ArrayBuffer with the file data from the given path. + * @param filePath Path where the file is stored + * @returns ArrayBuffer with the file data + */ + readFile(filePath: string): Promise; + + /** + * Deletes the file at the given path. + * @param filePath Path where the file is stored + */ + deleteFile(filePath: string): Promise; + + /** + * Checks if a file exists at the given path. + * @param filePath Path where the file is stored + */ + fileExists(filePath: string): Promise; + + /** + * Initializes the storage adapter (e.g., creating necessary directories). + */ + initialize(): Promise; + + /** + * Clears all files in the storage. + */ + clear(): Promise; + + /** + * Get the base directory used by the storage adapter. + * @returns The base directory path as a string. + */ + getUserStorageDirectory(): string; +} \ No newline at end of file diff --git a/packages/attachments/src/RemoteStorageAdapter.ts b/packages/attachments/src/RemoteStorageAdapter.ts new file mode 100644 index 000000000..853aac4d0 --- /dev/null +++ b/packages/attachments/src/RemoteStorageAdapter.ts @@ -0,0 +1,27 @@ +import { AttachmentRecord } from "./Schema.js"; + +export interface RemoteStorageAdapter { + /** + * Uploads a file to remote storage. + * + * @param fileData The binary content of the file to upload. + * @param attachment The associated `Attachment` metadata describing the file. + * @throws An error if the upload fails. + */ + uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise; + /** + * Downloads a file from remote storage. + * + * @param attachment The `Attachment` describing the file to download. + * @returns The binary data of the downloaded file. + * @throws An error if the download fails or the file is not found. + */ + downloadFile(attachment: AttachmentRecord): Promise; + /** + * Deletes a file from remote storage. + * + * @param attachment The `Attachment` describing the file to delete. + * @throws An error if the deletion fails or the file does not exist. + */ + deleteFile(attachment: AttachmentRecord): Promise; +} \ No newline at end of file diff --git a/packages/attachments/src/Schema.ts b/packages/attachments/src/Schema.ts index 93811c1f4..b295cdfcf 100644 --- a/packages/attachments/src/Schema.ts +++ b/packages/attachments/src/Schema.ts @@ -1,46 +1,64 @@ -import { Column, ColumnType, Table, TableOptions } from '@powersync/common'; +import { column, Table, TableV2Options } from '@powersync/common'; export const ATTACHMENT_TABLE = 'attachments'; export interface AttachmentRecord { id: string; filename: string; - local_uri?: string; + localUri?: string; size?: number; - media_type?: string; + mediaType?: string; timestamp?: number; + metaData?: string; + hasSynced?: boolean; state: AttachmentState; } +// map from db to record +export function attachmentFromSql(row: any): AttachmentRecord { + return { + id: row.id, + filename: row.filename, + localUri: row.local_uri, + size: row.size, + mediaType: row.media_type, + timestamp: row.timestamp, + metaData: row.meta_data, + hasSynced: row.has_synced === 1, + state: row.state + }; +} + export enum AttachmentState { QUEUED_SYNC = 0, // Check if the attachment needs to be uploaded or downloaded QUEUED_UPLOAD = 1, // Attachment to be uploaded QUEUED_DOWNLOAD = 2, // Attachment to be downloaded - SYNCED = 3, // Attachment has been synced - ARCHIVED = 4 // Attachment has been orphaned, i.e. the associated record has been deleted + QUEUED_DELETE = 3, // Attachment to be deleted + SYNCED = 4, // Attachment has been synced + ARCHIVED = 5 // Attachment has been orphaned, i.e. the associated record has been deleted } -export interface AttachmentTableOptions extends Omit { - name?: string; - additionalColumns?: Column[]; -} +export interface AttachmentTableOptions extends Omit {} export class AttachmentTable extends Table { constructor(options?: AttachmentTableOptions) { - super({ - ...options, - name: options?.name ?? ATTACHMENT_TABLE, - localOnly: true, - insertOnly: false, - columns: [ - new Column({ name: 'filename', type: ColumnType.TEXT }), - new Column({ name: 'local_uri', type: ColumnType.TEXT }), - new Column({ name: 'timestamp', type: ColumnType.INTEGER }), - new Column({ name: 'size', type: ColumnType.INTEGER }), - new Column({ name: 'media_type', type: ColumnType.TEXT }), - new Column({ name: 'state', type: ColumnType.INTEGER }), // Corresponds to AttachmentState - ...(options?.additionalColumns ?? []) - ] - }); + super( + { + filename: column.text, + local_uri: column.text, + timestamp: column.integer, + size: column.integer, + media_type: column.text, + state: column.integer, // Corresponds to AttachmentState + has_synced: column.integer, + meta_data: column.text + }, + { + ...options, + viewName: options?.viewName ?? ATTACHMENT_TABLE, + localOnly: true, + insertOnly: false + } + ); } } diff --git a/packages/attachments/src/StorageAdapter.ts b/packages/attachments/src/StorageAdapter.ts deleted file mode 100644 index 96ed3ec01..000000000 --- a/packages/attachments/src/StorageAdapter.ts +++ /dev/null @@ -1,28 +0,0 @@ -export enum EncodingType { - UTF8 = 'utf8', - Base64 = 'base64' -} - -export interface StorageAdapter { - uploadFile(filePath: string, data: ArrayBuffer, options?: { mediaType?: string }): Promise; - - downloadFile(filePath: string): Promise; - - writeFile(fileUri: string, base64Data: string, options?: { encoding?: EncodingType }): Promise; - - readFile(fileUri: string, options?: { encoding?: EncodingType; mediaType?: string }): Promise; - - deleteFile(uri: string, options?: { filename?: string }): Promise; - - fileExists(fileUri: string): Promise; - - makeDir(uri: string): Promise; - - copyFile(sourceUri: string, targetUri: string): Promise; - - /** - * Returns the directory where user data is stored. - * Should end with a '/' - */ - getUserStorageDirectory(): string; -} diff --git a/packages/attachments/src/StorageService.ts b/packages/attachments/src/StorageService.ts new file mode 100644 index 000000000..47a9b0523 --- /dev/null +++ b/packages/attachments/src/StorageService.ts @@ -0,0 +1,158 @@ +import { ILogger } from '@powersync/common'; +import { AttachmentContext } from './AttachmentContext.js'; +import { EncodingType, LocalStorageAdapter } from './LocalStorageAdapter.js'; +import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; +import { AttachmentRecord, AttachmentState } from './Schema.js'; +import { SyncErrorHandler } from './SyncErrorHandler.js'; + +export class StorageService { + context: AttachmentContext; + localStorage: LocalStorageAdapter; + remoteStorage: RemoteStorageAdapter; + logger: ILogger; + errorHandler?: SyncErrorHandler; + + constructor( + context: AttachmentContext, + localStorage: LocalStorageAdapter, + remoteStorage: RemoteStorageAdapter, + logger: ILogger, + errorHandler?: SyncErrorHandler + ) { + this.context = context; + this.localStorage = localStorage; + this.remoteStorage = remoteStorage; + this.logger = logger; + this.errorHandler = errorHandler; + } + + async processAttachments(attachments: AttachmentRecord[]): Promise { + const updatedAttachments: AttachmentRecord[] = []; + for (const attachment of attachments) { + switch (attachment.state) { + case AttachmentState.QUEUED_UPLOAD: + const uploaded = await this.uploadAttachment(attachment); + updatedAttachments.push(uploaded); + break; + case AttachmentState.QUEUED_DOWNLOAD: + const downloaded = await this.downloadAttachment(attachment); + updatedAttachments.push(downloaded); + break; + case AttachmentState.QUEUED_DELETE: + const deleted = await this.deleteAttachment(attachment); + updatedAttachments.push(deleted); + break; + + default: + break; + } + } + + await this.context.saveAttachments(updatedAttachments); + } + + async uploadAttachment(attachment: AttachmentRecord): Promise { + this.logger.info(`Uploading attachment ${attachment.filename}`); + try { + if (attachment.localUri == null) { + throw new Error(`No localUri for attachment ${attachment.id}`); + } + + const fileBlob = await this.localStorage.readFile(attachment.localUri); + await this.remoteStorage.uploadFile(fileBlob, attachment); + + return { + ...attachment, + state: AttachmentState.SYNCED, + hasSynced: true + }; + } catch (error) { + const shouldRetry = this.errorHandler?.onUploadError(attachment, error) ?? false; + if (!shouldRetry) { + return { + ...attachment, + state: AttachmentState.ARCHIVED + }; + } + + return attachment; + } + } + + async downloadAttachment(attachment: AttachmentRecord): Promise { + try { + const fileBlob = await this.remoteStorage.downloadFile(attachment); + + const base64Data = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + // remove the header from the result: 'data:*/*;base64,' + resolve(reader.result?.toString().replace(/^data:.+;base64,/, '') || ''); + }; + reader.onerror = reject; + reader.readAsDataURL(fileBlob); + }); + const userDir = this.localStorage.getUserStorageDirectory(); + const localUri = `${userDir}${attachment.id}`; + + await this.localStorage.saveFile(localUri, base64Data); + + return { + ...attachment, + state: AttachmentState.SYNCED, + localUri: localUri + }; + } catch (error) { + const shouldRetry = this.errorHandler?.onDownloadError(attachment, error) ?? false; + if (!shouldRetry) { + return { + ...attachment, + state: AttachmentState.ARCHIVED + }; + } + + return attachment; + } + } + + async deleteAttachment(attachment: AttachmentRecord): Promise { + try { + await this.remoteStorage.deleteFile(attachment); + if (attachment.localUri) { + await this.localStorage.deleteFile(attachment.localUri); + } + + await this.context.deleteAttachment(attachment.id); + + return { + ...attachment, + state: AttachmentState.QUEUED_DELETE, + localUri: null + }; + } catch (error) { + const shouldRetry = this.errorHandler?.onDeleteError(attachment, error) ?? false; + if (!shouldRetry) { + return { + ...attachment, + state: AttachmentState.ARCHIVED + }; + } + + return attachment; + } + } + + async deleteArchivedAttachments(): Promise { + const archivedAttachments = await this.context.getArchivedAttachments(); + for (const attachment of archivedAttachments) { + if (attachment.localUri) { + try { + await this.localStorage.deleteFile(attachment.localUri); + } catch (error) { + this.logger.error('Error deleting local file for archived attachment', error); + } + } + await this.context.deleteAttachment(attachment.id); + } + } +} diff --git a/packages/attachments/src/SyncErrorHandler.ts b/packages/attachments/src/SyncErrorHandler.ts new file mode 100644 index 000000000..d71ea7db1 --- /dev/null +++ b/packages/attachments/src/SyncErrorHandler.ts @@ -0,0 +1,28 @@ +import { AttachmentRecord } from './Schema.js'; + +/// If an operation fails and should not be retried, the attachment record is archived. +export abstract class SyncErrorHandler { + /** + * Handles a download error for a specific attachment. + * @param attachment The `Attachment` that failed to be downloaded. + * @param error The error encountered during the download operation. + * @returns `true` if the operation should be retried, `false` if it should be archived. + */ + abstract onDownloadError(attachment: AttachmentRecord, error: Error): Promise; + + /** + * Handles an upload error for a specific attachment. + * @param attachment The `Attachment` that failed to be uploaded. + * @param error The error encountered during the upload operation. + * @returns `true` if the operation should be retried, `false` if it should be archived. + */ + abstract onUploadError(attachment: AttachmentRecord, error: Error): Promise; + + /** + * Handles a delete error for a specific attachment. + * @param attachment The `Attachment` that failed to be deleted. + * @param error The error encountered during the delete operation. + * @returns `true` if the operation should be retried, `false` if it should be archived. + */ + abstract onDeleteError(attachment: AttachmentRecord, error: Error): Promise; +} diff --git a/packages/attachments/src/WatchedAttachmentItem.ts b/packages/attachments/src/WatchedAttachmentItem.ts new file mode 100644 index 000000000..70fefea44 --- /dev/null +++ b/packages/attachments/src/WatchedAttachmentItem.ts @@ -0,0 +1,14 @@ +// A watched attachment record item. +export type WatchedAttachmentItem = + | { + id: string; + filename: string; + fileExtension?: never; + metaData?: string; + } + | { + id: string; + fileExtension: string; + filename?: never; + metaData?: string; + }; diff --git a/packages/attachments/src/index.ts b/packages/attachments/src/index.ts index 04eee58ed..3ed1ad989 100644 --- a/packages/attachments/src/index.ts +++ b/packages/attachments/src/index.ts @@ -1,4 +1,6 @@ export * from './Schema.js'; -export * from './StorageAdapter.js'; - -export * from './AbstractAttachmentQueue.js'; +export * from './LocalStorageAdapter.js'; +export * from './RemoteStorageAdapter.js'; +export * from './AttachmentContext.js'; +export * from './StorageService.js'; +export * from './AttachmentQueue.js'; \ No newline at end of file diff --git a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts new file mode 100644 index 000000000..a01f25dff --- /dev/null +++ b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts @@ -0,0 +1,113 @@ +import { EncodingType, LocalStorageAdapter } from 'src/LocalStorageAdapter.js'; + +export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { + private dbPromise: Promise; + + async initialize(): Promise { + this.dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open('PowerSyncFiles', 1); + request.onupgradeneeded = () => { + request.result.createObjectStore('files'); + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + private async getStore(mode: IDBTransactionMode = 'readonly'): Promise { + const db = await this.dbPromise; + const tx = db.transaction('files', mode); + return tx.objectStore('files'); + } + + async saveFile(filePath: string, data: string): Promise { + const store = await this.getStore('readwrite'); + return await new Promise((resolve, reject) => { + const req = store.put(data, filePath); + req.onsuccess = () => resolve(data.length); + req.onerror = () => reject(req.error); + }); + } + + async downloadFile(filePath: string): Promise { + const store = await this.getStore(); + return new Promise((resolve, reject) => { + const req = store.get(filePath); + req.onsuccess = () => { + if (req.result) { + resolve(new Blob([req.result])); + } else { + reject(new Error('File not found')); + } + }; + req.onerror = () => reject(req.error); + }); + } + + async readFile(fileUri: string, options?: { encoding?: EncodingType; mediaType?: string }): Promise { + const store = await this.getStore(); + return new Promise((resolve, reject) => { + const req = store.get(fileUri); + req.onsuccess = async () => { + if (!req.result) { + reject(new Error('File not found')); + return; + } + + if (options?.encoding === EncodingType.Base64) { + const base64String = req.result.replace(/^data:\w+;base64,/, ''); + const binaryString = atob(base64String); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + resolve(bytes.buffer); + } + + if (options?.encoding === EncodingType.UTF8) { + const encoder = new TextEncoder(); + const arrayBuffer = encoder.encode(req.result).buffer; + resolve(arrayBuffer); + } + + reject(new Error('Unsupported encoding')); + }; + req.onerror = () => reject(req.error); + }); + } + + async deleteFile(uri: string, options?: { filename?: string }): Promise { + const store = await this.getStore('readwrite'); + await new Promise((resolve, reject) => { + const req = store.delete(uri); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } + + async fileExists(fileUri: string): Promise { + const store = await this.getStore(); + return new Promise((resolve, reject) => { + const req = store.get(fileUri); + req.onsuccess = () => resolve(!!req.result); + req.onerror = () => reject(req.error); + }); + } + + getUserStorageDirectory(): string { + // Not applicable for web, but return a logical root + return 'indexeddb://PowerSyncFiles/files'; + } + + clear(): Promise { + return new Promise(async (resolve, reject) => { + const db = await this.dbPromise; + const tx = db.transaction('files', 'readwrite'); + const store = tx.objectStore('files'); + const req = store.clear(); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } +} diff --git a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts new file mode 100644 index 000000000..dfa8f967a --- /dev/null +++ b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts @@ -0,0 +1,71 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { EncodingType, LocalStorageAdapter } from 'src/LocalStorageAdapter.js'; + +export class NodeFileSystemAdapter implements LocalStorageAdapter { + async uploadFile(filePath: string, data: ArrayBuffer, options?: { encoding: EncodingType }): Promise { + const buffer = Buffer.from(data); + await fs.writeFile(filePath, buffer, { + encoding: options.encoding + }); + } + + async downloadFile(filePath: string): Promise { + const data = await fs.readFile(filePath); + return new Blob([new Uint8Array(data)]); + } + + async saveFile( + filePath: string, + data: string, + options?: { encoding?: EncodingType; mediaType?: string } + ): Promise { + const buffer = options?.encoding === EncodingType.Base64 ? Buffer.from(data, 'base64') : Buffer.from(data, 'utf8'); + await fs.writeFile(filePath, buffer, { + encoding: options?.encoding + }); + return buffer.length; + } + + async readFile(filePath: string, options?: { encoding?: EncodingType; mediaType?: string }): Promise { + const data = await fs.readFile(filePath); + if (options?.encoding === EncodingType.Base64) { + return Buffer.from(data.toString(), 'base64').buffer; + } else { + return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer; + } + } + + async deleteFile(path: string, options?: { filename?: string }): Promise { + await fs.unlink(path).catch((err) => { + if (err.code !== 'ENOENT') { + throw err; + } + }); + } + + async fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + } + + async makeDir(path: string): Promise { + await fs.mkdir(path, { recursive: true }); + } + + async rmDir(filePath: string): Promise { + await fs.rmdir(filePath, { recursive: true }); + } + + async copyFile(sourcePath: string, targetPath: string): Promise { + await fs.copyFile(sourcePath, targetPath); + } + + getUserStorageDirectory(): string { + return path.resolve('./user_data') + path.sep; + } +} diff --git a/packages/attachments/tests/attachments/AttachmentQueue.test.ts b/packages/attachments/tests/attachments/AttachmentQueue.test.ts index 69e8c1593..bf0252472 100644 --- a/packages/attachments/tests/attachments/AttachmentQueue.test.ts +++ b/packages/attachments/tests/attachments/AttachmentQueue.test.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { AbstractAttachmentQueue } from '../../src/AbstractAttachmentQueue.js'; -import { AttachmentRecord, AttachmentState } from '../../src/Schema.js'; -import { StorageAdapter } from '../../src/StorageAdapter.js'; +import { AttachmentQueue } from '../../src/AttachmentQueue.js'; +import { AttachmentState } from '../../src/Schema.js'; +import { LocalStorageAdapter } from '../../src/LocalStorageAdapter.js'; +import { RemoteStorageAdapter } from '../../src/RemoteStorageAdapter.js'; const record = { id: 'test-1', @@ -18,7 +19,14 @@ const mockPowerSync = { execute: vi.fn(() => Promise.resolve()), getOptional: vi.fn((_query, params) => Promise.resolve(record)), watch: vi.fn((query, params, callbacks) => { - callbacks?.onResult?.({ rows: { _array: [{ id: 'test-1' }, { id: 'test-2' }] } }); + callbacks?.onResult?.({ + rows: { + _array: [ + { id: 'test-1', fileExtension: 'jpg' }, + { id: 'test-2', fileExtension: 'jpg' } + ] + } + }); }), writeTransaction: vi.fn(async (callback) => { await callback({ @@ -27,67 +35,69 @@ const mockPowerSync = { }) }; -const mockStorage: StorageAdapter = { - downloadFile: vi.fn(), - uploadFile: vi.fn(), +const mockLocalStorage: LocalStorageAdapter = { deleteFile: vi.fn(), - writeFile: vi.fn(), + saveFile: vi.fn(), readFile: vi.fn(), fileExists: vi.fn(), - makeDir: vi.fn(), - copyFile: vi.fn(), - getUserStorageDirectory: vi.fn() + getUserStorageDirectory: vi.fn(), + initialize: vi.fn(), + clear: vi.fn() }; -class TestAttachmentQueue extends AbstractAttachmentQueue { - onAttachmentIdsChange(onUpdate: (ids: string[]) => void): void { - throw new Error('Method not implemented.'); - } - newAttachmentRecord(record?: Partial): Promise { - throw new Error('Method not implemented.'); - } -} +const mockRemoteStorage: RemoteStorageAdapter = { + uploadFile: vi.fn(), + downloadFile: vi.fn(), + deleteFile: vi.fn() +}; -describe('attachments', () => { - beforeEach(() => { - vi.clearAllMocks(); +const watchAttachments = (onUpdate: (attachments: any[]) => void) => { + mockPowerSync.watch('SELECT id FROM table1', [], { + onResult: (result) => onUpdate(result.rows?._array.map((r) => ({ id: r.id, fileExtension: 'jpg' })) ?? []) }); +}; - it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => { - const queue = new TestAttachmentQueue({ - powersync: mockPowerSync as any, - storage: mockStorage, - downloadAttachments: false - }); +// describe('attachments', () => { +// beforeEach(() => { +// vi.clearAllMocks(); +// }); - await queue.downloadRecord(record); +// it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => { +// const queue = new AttachmentQueue({ +// db: mockPowerSync as any, +// watchAttachments: watchAttachments, +// remoteStorage: mockRemoteStorage, +// localStorage: mockLocalStorage +// }); - expect(mockStorage.downloadFile).not.toHaveBeenCalled(); - }); +// await queue.saveFile; - it('should download attachments when downloadRecord is called with downloadAttachments true', async () => { - const queue = new TestAttachmentQueue({ - powersync: mockPowerSync as any, - storage: mockStorage, - downloadAttachments: true - }); +// expect(mockLocalStorage.downloadFile).not.toHaveBeenCalled(); +// }); - await queue.downloadRecord(record); +// it('should download attachments when downloadRecord is called with downloadAttachments true', async () => { +// const queue = new TestAttachmentQueue({ +// powersync: mockPowerSync as any, +// storage: mockLocalStorage, +// downloadAttachments: true +// }); - expect(mockStorage.downloadFile).toHaveBeenCalled(); - }); +// await queue.downloadRecord(record); - // Testing the inverse of this test, i.e. when downloadAttachments is false, is not required as you can't wait for something that does not happen - it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => { - const queue = new TestAttachmentQueue({ - powersync: mockPowerSync as any, - storage: mockStorage, - downloadAttachments: true - }); +// expect(mockLocalStorage.downloadFile).toHaveBeenCalled(); +// }); - queue.watchDownloads(); - await vi.waitFor(() => { - expect(mockStorage.downloadFile).toBeCalledTimes(2); - }); - }); -}); +// // Testing the inverse of this test, i.e. when downloadAttachments is false, is not required as you can't wait for something that does not happen +// it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => { +// const queue = new TestAttachmentQueue({ +// powersync: mockPowerSync as any, +// storage: mockLocalStorage, +// downloadAttachments: true +// }); + +// queue.watchDownloads(); +// await vi.waitFor(() => { +// expect(mockLocalStorage.downloadFile).toBeCalledTimes(2); +// }); +// }); +// }); From 23dc8e41531b1ab5cf574bc435241e28f7642c5d Mon Sep 17 00:00:00 2001 From: joshua-journey-apps Date: Tue, 7 Oct 2025 12:23:03 +0200 Subject: [PATCH 02/19] Add make, remove and get uri endpoints to local storage --- packages/attachments/src/AttachmentQueue.ts | 8 +-- .../attachments/src/LocalStorageAdapter.ts | 26 +++++++-- packages/attachments/src/StorageService.ts | 3 +- .../IndexDBFileSystemAdapter.ts | 57 +++++++++++-------- .../storageAdapters/NodeFileSystemAdapter.ts | 30 ++++++---- 5 files changed, 76 insertions(+), 48 deletions(-) diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index 2689e1c3f..ab75a1c3e 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -158,10 +158,6 @@ export class AttachmentQueue { this.watchActiveAbortController?.abort(); } - getLocalUri(filePath: string): string { - return `${this.localStorage.getUserStorageDirectory()}/${filePath}`; - } - async saveFile({ data, fileExtension, @@ -177,7 +173,7 @@ export class AttachmentQueue { }): Promise { const resolvedId = id ?? (await this.context.db.get<{ id: string }>('SELECT uuid() as id')).id; const filename = `${resolvedId}.${fileExtension}`; - const localUri = this.getLocalUri(filename); + const localUri = this.localStorage.getLocalUri(filename); const size = await this.localStorage.saveFile(localUri, data); const attachment: AttachmentRecord = { @@ -214,7 +210,7 @@ export class AttachmentQueue { continue; } - const newLocalUri = this.getLocalUri(attachment.filename); + const newLocalUri = this.localStorage.getLocalUri(attachment.filename); const newExists = await this.localStorage.fileExists(newLocalUri); if (newExists) { // The file exists but the localUri is broken, lets update it. diff --git a/packages/attachments/src/LocalStorageAdapter.ts b/packages/attachments/src/LocalStorageAdapter.ts index 03de44df4..73d707c09 100644 --- a/packages/attachments/src/LocalStorageAdapter.ts +++ b/packages/attachments/src/LocalStorageAdapter.ts @@ -8,7 +8,7 @@ export interface LocalStorageAdapter { * Saves buffer data to a local file. * @param filePath Path where the file will be stored * @param data Data string to store - * @returns number of bytes written + * @returns Number of bytes written */ saveFile(filePath: string, data: ArrayBuffer | Blob | string): Promise; @@ -28,9 +28,24 @@ export interface LocalStorageAdapter { /** * Checks if a file exists at the given path. * @param filePath Path where the file is stored + * @returns True if the file exists, false otherwise */ fileExists(filePath: string): Promise; + /** + * Creates a directory at the specified path. + * @param path The full path to the directory + * @throws PowerSyncAttachmentError if creation fails + */ + makeDir(path: string): Promise; + + /** + * Removes a directory at the specified path. + * @param path The full path to the directory + * @throws PowerSyncAttachmentError if removal fails + */ + rmDir(path: string): Promise; + /** * Initializes the storage adapter (e.g., creating necessary directories). */ @@ -42,8 +57,9 @@ export interface LocalStorageAdapter { clear(): Promise; /** - * Get the base directory used by the storage adapter. - * @returns The base directory path as a string. + * Returns the file path of the provided filename in the user storage directory. + * @param filename The filename to get the path for + * @returns The full file path */ - getUserStorageDirectory(): string; -} \ No newline at end of file + getLocalUri(filename: string): string; +} diff --git a/packages/attachments/src/StorageService.ts b/packages/attachments/src/StorageService.ts index 47a9b0523..ede346239 100644 --- a/packages/attachments/src/StorageService.ts +++ b/packages/attachments/src/StorageService.ts @@ -92,9 +92,8 @@ export class StorageService { reader.onerror = reject; reader.readAsDataURL(fileBlob); }); - const userDir = this.localStorage.getUserStorageDirectory(); - const localUri = `${userDir}${attachment.id}`; + const localUri = this.localStorage.getLocalUri(attachment.filename); await this.localStorage.saveFile(localUri, base64Data); return { diff --git a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts index a01f25dff..7de9b1aa0 100644 --- a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts @@ -1,4 +1,4 @@ -import { EncodingType, LocalStorageAdapter } from 'src/LocalStorageAdapter.js'; +import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { private dbPromise: Promise; @@ -14,6 +14,21 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { }); } + clear(): Promise { + return new Promise(async (resolve, reject) => { + const db = await this.dbPromise; + const tx = db.transaction('files', 'readwrite'); + const store = tx.objectStore('files'); + const req = store.clear(); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } + + getLocalUri(filename: string): string { + return `indexeddb://PowerSyncFiles/files/${filename}`; + } + private async getStore(mode: IDBTransactionMode = 'readonly'): Promise { const db = await this.dbPromise; const tx = db.transaction('files', mode); @@ -54,16 +69,8 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { return; } - if (options?.encoding === EncodingType.Base64) { - const base64String = req.result.replace(/^data:\w+;base64,/, ''); - const binaryString = atob(base64String); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - resolve(bytes.buffer); - } + // if (options?.encoding === EncodingType.Base64) { + // } if (options?.encoding === EncodingType.UTF8) { const encoder = new TextEncoder(); @@ -71,7 +78,17 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { resolve(arrayBuffer); } - reject(new Error('Unsupported encoding')); + // Default base64 encoding + const base64String = req.result.replace(/^data:\w+;base64,/, ''); + const binaryString = atob(base64String); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + resolve(bytes.buffer); + + // reject(new Error('Unsupported encoding')); }; req.onerror = () => reject(req.error); }); @@ -95,19 +112,11 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { }); } - getUserStorageDirectory(): string { - // Not applicable for web, but return a logical root - return 'indexeddb://PowerSyncFiles/files'; + async makeDir(path: string): Promise { + // No-op for IndexedDB } - clear(): Promise { - return new Promise(async (resolve, reject) => { - const db = await this.dbPromise; - const tx = db.transaction('files', 'readwrite'); - const store = tx.objectStore('files'); - const req = store.clear(); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); + async rmDir(path: string): Promise { + // No-op for IndexedDB } } diff --git a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts index dfa8f967a..9ce26ea67 100644 --- a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts @@ -1,8 +1,24 @@ import { promises as fs } from 'fs'; import * as path from 'path'; -import { EncodingType, LocalStorageAdapter } from 'src/LocalStorageAdapter.js'; +import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; export class NodeFileSystemAdapter implements LocalStorageAdapter { + async initialize(): Promise { + // const dir = this.getUserStorageDirectory(); + const dir = path.resolve('./user_data'); + await fs.mkdir(dir, { recursive: true }); + } + + async clear(): Promise { + // const dir = this.getUserStorageDirectory(); + const dir = path.resolve('./user_data'); + await fs.rmdir(dir, { recursive: true }); + } + + getLocalUri(filename: string): string { + return path.join(path.resolve('./user_data'), filename); + } + async uploadFile(filePath: string, data: ArrayBuffer, options?: { encoding: EncodingType }): Promise { const buffer = Buffer.from(data); await fs.writeFile(filePath, buffer, { @@ -57,15 +73,7 @@ export class NodeFileSystemAdapter implements LocalStorageAdapter { await fs.mkdir(path, { recursive: true }); } - async rmDir(filePath: string): Promise { - await fs.rmdir(filePath, { recursive: true }); - } - - async copyFile(sourcePath: string, targetPath: string): Promise { - await fs.copyFile(sourcePath, targetPath); - } - - getUserStorageDirectory(): string { - return path.resolve('./user_data') + path.sep; + async rmDir(path: string): Promise { + await fs.rmdir(path, { recursive: true }); } } From ecb71cba0e0ec162e6826e718bf5205ee7bab3b7 Mon Sep 17 00:00:00 2001 From: joshua-journey-apps Date: Tue, 7 Oct 2025 12:23:31 +0200 Subject: [PATCH 03/19] Fix exporting node and index db storage adapters --- packages/attachments/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/attachments/src/index.ts b/packages/attachments/src/index.ts index 3ed1ad989..7174fa7ca 100644 --- a/packages/attachments/src/index.ts +++ b/packages/attachments/src/index.ts @@ -1,6 +1,8 @@ export * from './Schema.js'; export * from './LocalStorageAdapter.js'; +export * from './storageAdapters/NodeFileSystemAdapter.js'; +export * from './storageAdapters/IndexDBFileSystemAdapter.js'; export * from './RemoteStorageAdapter.js'; export * from './AttachmentContext.js'; export * from './StorageService.js'; -export * from './AttachmentQueue.js'; \ No newline at end of file +export * from './AttachmentQueue.js'; From 029df9d4cdcfff87c62bdc5315d081963fd20610 Mon Sep 17 00:00:00 2001 From: joshua-journey-apps Date: Tue, 7 Oct 2025 12:25:03 +0200 Subject: [PATCH 04/19] Wip add sync throttle & cache limiting --- packages/attachments/src/AttachmentQueue.ts | 39 +++++++++++++-------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index ab75a1c3e..ea008b5b4 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -2,47 +2,58 @@ import { AbstractPowerSyncDatabase, ILogger } from '@powersync/common'; import { AttachmentContext } from './AttachmentContext.js'; import { LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; -import { AttachmentRecord, AttachmentState } from './Schema.js'; +import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js'; import { StorageService } from './StorageService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; export class AttachmentQueue { periodicSyncTimer?: ReturnType; - syncInterval: number = 30 * 1000; context: AttachmentContext; storageService: StorageService; localStorage: LocalStorageAdapter; remoteStorage: RemoteStorageAdapter; + attachmentsDirectory?: string; + tableName?: string; + logger?: ILogger; + syncInterval: number = 30 * 1000; + syncThrottleDuration: number; downloadAttachments: boolean = true; watchActiveAbortController?: AbortController; + archivedCacheLimit: number; constructor({ db, localStorage, remoteStorage, watchAttachments, - tableName, logger, - options + tableName = ATTACHMENT_TABLE, + syncInterval = 30 * 1000, + syncThrottleDuration = 1000, + downloadAttachments = true, + archivedCacheLimit = 100 }: { db: AbstractPowerSyncDatabase; remoteStorage: RemoteStorageAdapter; localStorage: LocalStorageAdapter; - watchAttachments?: (onUpdate: (attachement: WatchedAttachmentItem[]) => void) => void; + watchAttachments: (onUpdate: (attachement: WatchedAttachmentItem[]) => void) => void; tableName?: string; logger?: ILogger; - options?: { syncInterval?: number; downloadAttachments?: boolean }; + syncInterval?: number; + syncThrottleDuration?: number; + downloadAttachments?: boolean; + archivedCacheLimit?: number; }) { this.context = new AttachmentContext(db, tableName, logger ?? db.logger); + this.remoteStorage = remoteStorage; + this.localStorage = localStorage; + this.watchAttachments = watchAttachments; + this.tableName = tableName; this.storageService = new StorageService(this.context, localStorage, remoteStorage, logger ?? db.logger); - if (options?.syncInterval != null) { - this.syncInterval = options.syncInterval; - } - if (options?.downloadAttachments != null) { - this.downloadAttachments = options.downloadAttachments; - } - - this.watchAttachments = watchAttachments ?? this.watchAttachments; + this.syncInterval = syncInterval; + this.syncThrottleDuration = syncThrottleDuration; + this.downloadAttachments = downloadAttachments; + this.archivedCacheLimit = archivedCacheLimit; } watchAttachments(onUpdate: (attachement: WatchedAttachmentItem[]) => void): void { From 958675a6656ec606eb47144cb4c3fa5910a806cd Mon Sep 17 00:00:00 2001 From: joshua-journey-apps Date: Tue, 7 Oct 2025 12:28:31 +0200 Subject: [PATCH 05/19] Add downloading attachment test --- packages/attachments/package.json | 4 +- .../attachments/tests/attachments.test.ts | 202 ++++++++++++++++++ .../tests/attachments/AttachmentQueue.test.ts | 103 --------- packages/attachments/tsconfig.json | 2 +- packages/attachments/vitest.config.ts | 13 +- 5 files changed, 218 insertions(+), 106 deletions(-) create mode 100644 packages/attachments/tests/attachments.test.ts delete mode 100644 packages/attachments/tests/attachments/AttachmentQueue.test.ts diff --git a/packages/attachments/package.json b/packages/attachments/package.json index 9bcb3a9b5..1bd86678f 100644 --- a/packages/attachments/package.json +++ b/packages/attachments/package.json @@ -49,8 +49,10 @@ }, "devDependencies": { "@powersync/common": "workspace:*", + "@powersync/web": "workspace:*", "@types/node": "^20.17.6", "vite": "^6.1.0", - "vite-plugin-top-level-await": "^1.4.4" + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0" } } diff --git a/packages/attachments/tests/attachments.test.ts b/packages/attachments/tests/attachments.test.ts new file mode 100644 index 000000000..41f049102 --- /dev/null +++ b/packages/attachments/tests/attachments.test.ts @@ -0,0 +1,202 @@ +import { describe, expect, it, vi } from 'vitest'; +import { PowerSyncDatabase, Schema, Table, column } from '@powersync/web'; +import { AbstractPowerSyncDatabase } from '@powersync/common'; +import { AttachmentQueue } from '../src/AttachmentQueue.js'; +import { AttachmentState, AttachmentTable } from '../src/Schema.js'; +import { RemoteStorageAdapter } from '../src/RemoteStorageAdapter.js'; +import { WatchedAttachmentItem } from '../src/WatchedAttachmentItem.js'; +import { IndexDBFileSystemStorageAdapter } from '../src/storageAdapters/IndexDBFileSystemAdapter.js'; + +const mockRemoteStorage: RemoteStorageAdapter = { + downloadFile: (attachment) => { + return Promise.resolve(new Blob(['data:image/jpeg;base64,FAKE_BASE64_DATA'], { type: 'image/jpeg' })); + }, + uploadFile: vi.fn(), + deleteFile: vi.fn() +}; + +const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => void) => { + db.watch( + /* sql */ + ` + SELECT + photo_id + FROM + users + WHERE + photo_id IS NOT NULL + `, + [], + { + onResult: (result: any) => + onUpdate( + result.rows?._array.map((r: any) => ({ + id: r.photo_id, + fileExtension: 'jpg' + })) ?? [] + ) + } + ); +}; + +let db: AbstractPowerSyncDatabase; + +beforeAll(async () => { + db = new PowerSyncDatabase({ + schema: new Schema({ + users: new Table({ + name: column.text, + email: column.text, + photo_id: column.text + }), + attachments: new AttachmentTable() + }), + database: { + dbFilename: 'example.db' + } + }); + + await db.disconnectAndClear(); +}); + +afterAll(async () => { + await db.disconnectAndClear(); +}); + +describe('attachment queue', () => { + it('should download attachments when a new record with an attachment is added', async () => { + const queue = new AttachmentQueue({ + db: db, + watchAttachments, + remoteStorage: mockRemoteStorage, + localStorage: new IndexDBFileSystemStorageAdapter(), + }); + + await queue.startSync(); + + await db.execute( + /* sql */ + ` + INSERT INTO + users (id, name, email, photo_id) + VALUES + ( + uuid (), + 'example', + 'example@example.com', + uuid () + ) + `, + [] + ); + + const attachmentRecords = await waitForMatch( + () => + db.watch( + /* sql */ + ` + SELECT + * + FROM + attachments + `, + [] + ), + (results) => { + return results?.rows?._array.some((r: any) => r.state === AttachmentState.SYNCED); + }, + 5 + ); + + const attachmentRecord = attachmentRecords.rows._array.at(0); + + const localData = await queue.localStorage.readFile(attachmentRecord.local_uri!); + const localDataString = new TextDecoder().decode(localData); + expect(localDataString).toBe('data:image/jpeg;base64,FAKE_BASE64_DATA'); + + await queue.stopSync(); + }); +}); + +async function waitForMatch( + iteratorGenerator: () => AsyncIterable, + predicate: (value: any) => boolean, + timeout: number +) { + const timeoutMs = timeout * 1000; + const abortController = new AbortController(); + + const matchPromise = (async () => { + const asyncIterable = iteratorGenerator(); + try { + for await (const value of asyncIterable) { + if (abortController.signal.aborted) { + throw new Error('Timeout'); + } + if (predicate(value)) { + return value; + } + } + throw new Error('Stream ended without match'); + } finally { + const iterator = asyncIterable[Symbol.asyncIterator](); + if (iterator.return) { + await iterator.return(); + } + } + })(); + + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => { + abortController.abort(); + reject(new Error('Timeout')); + }, timeoutMs) + ); + + return Promise.race([matchPromise, timeoutPromise]); +} + +// describe('attachments', () => { +// beforeEach(() => { +// vi.clearAllMocks(); +// }); + +// it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => { +// const queue = new AttachmentQueue({ +// db: mockPowerSync as any, +// watchAttachments: watchAttachments, +// remoteStorage: mockRemoteStorage, +// localStorage: mockLocalStorage +// }); + +// await queue.saveFile; + +// expect(mockLocalStorage.downloadFile).not.toHaveBeenCalled(); +// }); + +// it('should download attachments when downloadRecord is called with downloadAttachments true', async () => { +// const queue = new TestAttachmentQueue({ +// powersync: mockPowerSync as any, +// storage: mockLocalStorage, +// downloadAttachments: true +// }); + +// await queue.downloadRecord(record); + +// expect(mockLocalStorage.downloadFile).toHaveBeenCalled(); +// }); + +// // Testing the inverse of this test, i.e. when downloadAttachments is false, is not required as you can't wait for something that does not happen +// it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => { +// const queue = new TestAttachmentQueue({ +// powersync: mockPowerSync as any, +// storage: mockLocalStorage, +// downloadAttachments: true +// }); + +// queue.watchDownloads(); +// await vi.waitFor(() => { +// expect(mockLocalStorage.downloadFile).toBeCalledTimes(2); +// }); +// }); +// }); diff --git a/packages/attachments/tests/attachments/AttachmentQueue.test.ts b/packages/attachments/tests/attachments/AttachmentQueue.test.ts deleted file mode 100644 index bf0252472..000000000 --- a/packages/attachments/tests/attachments/AttachmentQueue.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { AttachmentQueue } from '../../src/AttachmentQueue.js'; -import { AttachmentState } from '../../src/Schema.js'; -import { LocalStorageAdapter } from '../../src/LocalStorageAdapter.js'; -import { RemoteStorageAdapter } from '../../src/RemoteStorageAdapter.js'; - -const record = { - id: 'test-1', - filename: 'test.jpg', - state: AttachmentState.QUEUED_DOWNLOAD -}; - -const mockPowerSync = { - currentStatus: { status: 'initial' }, - registerListener: vi.fn(() => {}), - resolveTables: vi.fn(() => ['table1', 'table2']), - onChangeWithCallback: vi.fn(), - getAll: vi.fn(() => Promise.resolve([{ id: 'test-1' }, { id: 'test-2' }])), - execute: vi.fn(() => Promise.resolve()), - getOptional: vi.fn((_query, params) => Promise.resolve(record)), - watch: vi.fn((query, params, callbacks) => { - callbacks?.onResult?.({ - rows: { - _array: [ - { id: 'test-1', fileExtension: 'jpg' }, - { id: 'test-2', fileExtension: 'jpg' } - ] - } - }); - }), - writeTransaction: vi.fn(async (callback) => { - await callback({ - execute: vi.fn(() => Promise.resolve()) - }); - }) -}; - -const mockLocalStorage: LocalStorageAdapter = { - deleteFile: vi.fn(), - saveFile: vi.fn(), - readFile: vi.fn(), - fileExists: vi.fn(), - getUserStorageDirectory: vi.fn(), - initialize: vi.fn(), - clear: vi.fn() -}; - -const mockRemoteStorage: RemoteStorageAdapter = { - uploadFile: vi.fn(), - downloadFile: vi.fn(), - deleteFile: vi.fn() -}; - -const watchAttachments = (onUpdate: (attachments: any[]) => void) => { - mockPowerSync.watch('SELECT id FROM table1', [], { - onResult: (result) => onUpdate(result.rows?._array.map((r) => ({ id: r.id, fileExtension: 'jpg' })) ?? []) - }); -}; - -// describe('attachments', () => { -// beforeEach(() => { -// vi.clearAllMocks(); -// }); - -// it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => { -// const queue = new AttachmentQueue({ -// db: mockPowerSync as any, -// watchAttachments: watchAttachments, -// remoteStorage: mockRemoteStorage, -// localStorage: mockLocalStorage -// }); - -// await queue.saveFile; - -// expect(mockLocalStorage.downloadFile).not.toHaveBeenCalled(); -// }); - -// it('should download attachments when downloadRecord is called with downloadAttachments true', async () => { -// const queue = new TestAttachmentQueue({ -// powersync: mockPowerSync as any, -// storage: mockLocalStorage, -// downloadAttachments: true -// }); - -// await queue.downloadRecord(record); - -// expect(mockLocalStorage.downloadFile).toHaveBeenCalled(); -// }); - -// // Testing the inverse of this test, i.e. when downloadAttachments is false, is not required as you can't wait for something that does not happen -// it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => { -// const queue = new TestAttachmentQueue({ -// powersync: mockPowerSync as any, -// storage: mockLocalStorage, -// downloadAttachments: true -// }); - -// queue.watchDownloads(); -// await vi.waitFor(() => { -// expect(mockLocalStorage.downloadFile).toBeCalledTimes(2); -// }); -// }); -// }); diff --git a/packages/attachments/tsconfig.json b/packages/attachments/tsconfig.json index 04be1f2f0..9c31f1952 100644 --- a/packages/attachments/tsconfig.json +++ b/packages/attachments/tsconfig.json @@ -13,5 +13,5 @@ "path": "../common" } ], - "include": ["src/**/*"] + "include": ["src/**/*", "tests/**/*", "package.json"] } diff --git a/packages/attachments/vitest.config.ts b/packages/attachments/vitest.config.ts index d8f9fd351..e26633c68 100644 --- a/packages/attachments/vitest.config.ts +++ b/packages/attachments/vitest.config.ts @@ -1,8 +1,19 @@ import topLevelAwait from 'vite-plugin-top-level-await'; +import wasm from 'vite-plugin-wasm'; import { defineConfig, UserConfigExport } from 'vitest/config'; const config: UserConfigExport = { - plugins: [topLevelAwait()], + worker: { + format: 'es', + plugins: () => [wasm(), topLevelAwait()] + }, + optimizeDeps: { + // 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: ['async-mutex', 'comlink', 'bson'] + }, + plugins: [wasm(), topLevelAwait()], test: { isolate: false, globals: true, From 45773aae19cc81cbf1bafcc1873e5ac8e0508984 Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Mon, 27 Oct 2025 15:49:23 +0200 Subject: [PATCH 06/19] Add user defined storage adapter path --- .../src/storageAdapters/IndexDBFileSystemAdapter.ts | 6 ++++-- .../src/storageAdapters/NodeFileSystemAdapter.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts index 7de9b1aa0..90e999830 100644 --- a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts @@ -2,10 +2,12 @@ import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { private dbPromise: Promise; + + constructor(private databaseName: string = 'PowerSyncFiles') {} async initialize(): Promise { this.dbPromise = new Promise((resolve, reject) => { - const request = indexedDB.open('PowerSyncFiles', 1); + const request = indexedDB.open(this.databaseName, 1); request.onupgradeneeded = () => { request.result.createObjectStore('files'); }; @@ -26,7 +28,7 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { } getLocalUri(filename: string): string { - return `indexeddb://PowerSyncFiles/files/${filename}`; + return `indexeddb://${this.databaseName}/files/${filename}`; } private async getStore(mode: IDBTransactionMode = 'readonly'): Promise { diff --git a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts index 9ce26ea67..f267c214f 100644 --- a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts @@ -3,20 +3,23 @@ import * as path from 'path'; import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; export class NodeFileSystemAdapter implements LocalStorageAdapter { + + constructor(private storageDirectory: string = './user_data') {} + async initialize(): Promise { // const dir = this.getUserStorageDirectory(); - const dir = path.resolve('./user_data'); + const dir = path.resolve(this.storageDirectory); await fs.mkdir(dir, { recursive: true }); } async clear(): Promise { // const dir = this.getUserStorageDirectory(); - const dir = path.resolve('./user_data'); + const dir = path.resolve(this.storageDirectory); await fs.rmdir(dir, { recursive: true }); } getLocalUri(filename: string): string { - return path.join(path.resolve('./user_data'), filename); + return path.join(path.resolve(this.storageDirectory), filename); } async uploadFile(filePath: string, data: ArrayBuffer, options?: { encoding: EncodingType }): Promise { From 533dfab54e7c16caefb19b0a81cc44d1a1383fdf Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Mon, 27 Oct 2025 15:53:15 +0200 Subject: [PATCH 07/19] Refactor watch active observer into dedicated service --- packages/attachments/src/AttachmentContext.ts | 30 --------------- packages/attachments/src/AttachmentQueue.ts | 11 ++++-- packages/attachments/src/AttachmentService.ts | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 packages/attachments/src/AttachmentService.ts diff --git a/packages/attachments/src/AttachmentContext.ts b/packages/attachments/src/AttachmentContext.ts index e6d13cea8..b312a88bd 100644 --- a/packages/attachments/src/AttachmentContext.ts +++ b/packages/attachments/src/AttachmentContext.ts @@ -12,36 +12,6 @@ export class AttachmentContext { this.logger = logger; } - watchActiveAttachments(onUpdate: () => void): AbortController { - const abortController = new AbortController(); - this.db.watchWithCallback( - /* sql */ - ` - SELECT - * - FROM - ${this.tableName} - WHERE - state = ? - OR state = ? - OR state = ? - ORDER BY - timestamp ASC - `, - [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE], - { - onResult: () => { - onUpdate(); - } - }, - { - signal: abortController.signal - } - ); - - return abortController; - } - async getActiveAttachments(): Promise { const attachments = await this.db.getAll( /* sql */ diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index ea008b5b4..dc12ec81f 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -5,6 +5,7 @@ import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js'; import { StorageService } from './StorageService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; +import { AttachmentService } from './AttachmentService.js'; export class AttachmentQueue { periodicSyncTimer?: ReturnType; @@ -20,6 +21,7 @@ export class AttachmentQueue { downloadAttachments: boolean = true; watchActiveAbortController?: AbortController; archivedCacheLimit: number; + attachmentService: AttachmentService; constructor({ db, @@ -51,6 +53,7 @@ export class AttachmentQueue { this.tableName = tableName; this.storageService = new StorageService(this.context, localStorage, remoteStorage, logger ?? db.logger); this.syncInterval = syncInterval; + this.attachmentService = new AttachmentService(tableName, db); this.syncThrottleDuration = syncThrottleDuration; this.downloadAttachments = downloadAttachments; this.archivedCacheLimit = archivedCacheLimit; @@ -69,8 +72,10 @@ export class AttachmentQueue { }, this.syncInterval); // Sync storage when there is a change in active attachments - this.watchActiveAbortController = this.context.watchActiveAttachments(async () => { - await this.syncStorage(); + this.attachmentService.watchActiveAttachments().registerListener({ + onDiff: async () => { + await this.syncStorage(); + } }); // Process attachments when there is a change in watched attachments @@ -166,7 +171,7 @@ export class AttachmentQueue { async stopSync(): Promise { clearInterval(this.periodicSyncTimer); this.periodicSyncTimer = undefined; - this.watchActiveAbortController?.abort(); + await this.attachmentService.watchActiveAttachments().close(); } async saveFile({ diff --git a/packages/attachments/src/AttachmentService.ts b/packages/attachments/src/AttachmentService.ts new file mode 100644 index 000000000..50a6b8983 --- /dev/null +++ b/packages/attachments/src/AttachmentService.ts @@ -0,0 +1,38 @@ +import { AbstractPowerSyncDatabase, DifferentialWatchedQuery } from '@powersync/common'; +import { AttachmentRecord, AttachmentState } from './Schema.js'; + +/** + * Service for querying and watching attachment records in the database. + */ +export class AttachmentService { + constructor( + private tableName: string = 'attachments', + private db: AbstractPowerSyncDatabase + ) {} + + /** + * Creates a differential watch query for active attachments requiring synchronization. + * @returns Watch query that emits changes for queued uploads, downloads, and deletes + */ + watchActiveAttachments(): DifferentialWatchedQuery { + const watch = this.db + .query({ + sql: /* sql */ ` + SELECT + * + FROM + ${this.tableName} + WHERE + state = ? + OR state = ? + OR state = ? + ORDER BY + timestamp ASC + `, + parameters: [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE] + }) + .differentialWatch(); + + return watch; + } +} From 643289d07adfcb2ca4fef30b86da8c6f4f22491a Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Tue, 28 Oct 2025 14:39:47 +0200 Subject: [PATCH 08/19] Add temporal units to variable name --- packages/attachments/src/AttachmentQueue.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index dc12ec81f..ebb72619e 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -16,7 +16,6 @@ export class AttachmentQueue { attachmentsDirectory?: string; tableName?: string; logger?: ILogger; - syncInterval: number = 30 * 1000; syncThrottleDuration: number; downloadAttachments: boolean = true; watchActiveAbortController?: AbortController; @@ -30,7 +29,7 @@ export class AttachmentQueue { watchAttachments, logger, tableName = ATTACHMENT_TABLE, - syncInterval = 30 * 1000, + syncIntervalMs = 30 * 1000, syncThrottleDuration = 1000, downloadAttachments = true, archivedCacheLimit = 100 @@ -41,7 +40,7 @@ export class AttachmentQueue { watchAttachments: (onUpdate: (attachement: WatchedAttachmentItem[]) => void) => void; tableName?: string; logger?: ILogger; - syncInterval?: number; + syncIntervalMs?: number; syncThrottleDuration?: number; downloadAttachments?: boolean; archivedCacheLimit?: number; @@ -52,8 +51,8 @@ export class AttachmentQueue { this.watchAttachments = watchAttachments; this.tableName = tableName; this.storageService = new StorageService(this.context, localStorage, remoteStorage, logger ?? db.logger); - this.syncInterval = syncInterval; this.attachmentService = new AttachmentService(tableName, db); + this.syncIntervalMs = syncIntervalMs; this.syncThrottleDuration = syncThrottleDuration; this.downloadAttachments = downloadAttachments; this.archivedCacheLimit = archivedCacheLimit; @@ -69,7 +68,7 @@ export class AttachmentQueue { // Sync storage periodically this.periodicSyncTimer = setInterval(async () => { await this.syncStorage(); - }, this.syncInterval); + }, this.syncIntervalMs); // Sync storage when there is a change in active attachments this.attachmentService.watchActiveAttachments().registerListener({ From 6840fb0b75e9054c130910ed17696566c79f3df8 Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Tue, 28 Oct 2025 14:48:04 +0200 Subject: [PATCH 09/19] Rename storage -> syncing service --- packages/attachments/src/AttachmentQueue.ts | 10 ++-- .../{StorageService.ts => SyncingService.ts} | 47 +++++++++++++++++-- packages/attachments/src/index.ts | 2 +- 3 files changed, 49 insertions(+), 10 deletions(-) rename packages/attachments/src/{StorageService.ts => SyncingService.ts} (71%) diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index ebb72619e..1760399c6 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -3,14 +3,14 @@ import { AttachmentContext } from './AttachmentContext.js'; import { LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js'; -import { StorageService } from './StorageService.js'; +import { SyncingService } from './SyncingService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; import { AttachmentService } from './AttachmentService.js'; export class AttachmentQueue { periodicSyncTimer?: ReturnType; context: AttachmentContext; - storageService: StorageService; + syncingService: SyncingService; localStorage: LocalStorageAdapter; remoteStorage: RemoteStorageAdapter; attachmentsDirectory?: string; @@ -50,7 +50,7 @@ export class AttachmentQueue { this.localStorage = localStorage; this.watchAttachments = watchAttachments; this.tableName = tableName; - this.storageService = new StorageService(this.context, localStorage, remoteStorage, logger ?? db.logger); + this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger); this.attachmentService = new AttachmentService(tableName, db); this.syncIntervalMs = syncIntervalMs; this.syncThrottleDuration = syncThrottleDuration; @@ -163,8 +163,8 @@ export class AttachmentQueue { async syncStorage(): Promise { const activeAttachments = await this.context.getActiveAttachments(); await this.localStorage.initialize(); - await this.storageService.processAttachments(activeAttachments); - await this.storageService.deleteArchivedAttachments(); + await this.syncingService.processAttachments(activeAttachments); + await this.syncingService.deleteArchivedAttachments(); } async stopSync(): Promise { diff --git a/packages/attachments/src/StorageService.ts b/packages/attachments/src/SyncingService.ts similarity index 71% rename from packages/attachments/src/StorageService.ts rename to packages/attachments/src/SyncingService.ts index ede346239..e69e518f1 100644 --- a/packages/attachments/src/StorageService.ts +++ b/packages/attachments/src/SyncingService.ts @@ -1,11 +1,15 @@ import { ILogger } from '@powersync/common'; import { AttachmentContext } from './AttachmentContext.js'; -import { EncodingType, LocalStorageAdapter } from './LocalStorageAdapter.js'; +import { LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; import { AttachmentRecord, AttachmentState } from './Schema.js'; import { SyncErrorHandler } from './SyncErrorHandler.js'; -export class StorageService { +/** + * Orchestrates attachment synchronization between local and remote storage. + * Handles uploads, downloads, deletions, and state transitions. + */ +export class SyncingService { context: AttachmentContext; localStorage: LocalStorageAdapter; remoteStorage: RemoteStorageAdapter; @@ -26,6 +30,13 @@ export class StorageService { this.errorHandler = errorHandler; } + /** + * Processes attachments based on their state (upload, download, or delete). + * All updates are saved in a single batch after processing. + * + * @param attachments - Array of attachment records to process + * @returns Promise that resolves when all attachments have been processed and saved + */ async processAttachments(attachments: AttachmentRecord[]): Promise { const updatedAttachments: AttachmentRecord[] = []; for (const attachment of attachments) { @@ -51,6 +62,14 @@ export class StorageService { await this.context.saveAttachments(updatedAttachments); } + /** + * Uploads an attachment from local storage to remote storage. + * On success, marks as SYNCED. On failure, defers to error handler or archives. + * + * @param attachment - The attachment record to upload + * @returns Updated attachment record with new state + * @throws Error if the attachment has no localUri + */ async uploadAttachment(attachment: AttachmentRecord): Promise { this.logger.info(`Uploading attachment ${attachment.filename}`); try { @@ -79,9 +98,17 @@ export class StorageService { } } + /** + * Downloads an attachment from remote storage to local storage. + * Retrieves the file, converts to base64, and saves locally. + * On success, marks as SYNCED. On failure, defers to error handler or archives. + * + * @param attachment - The attachment record to download + * @returns Updated attachment record with local URI and new state + */ async downloadAttachment(attachment: AttachmentRecord): Promise { try { - const fileBlob = await this.remoteStorage.downloadFile(attachment); + const file = await this.remoteStorage.downloadFile(attachment); const base64Data = await new Promise((resolve, reject) => { const reader = new FileReader(); @@ -90,7 +117,7 @@ export class StorageService { resolve(reader.result?.toString().replace(/^data:.+;base64,/, '') || ''); }; reader.onerror = reject; - reader.readAsDataURL(fileBlob); + reader.readAsDataURL(new File([file], attachment.filename)); }); const localUri = this.localStorage.getLocalUri(attachment.filename); @@ -114,6 +141,14 @@ export class StorageService { } } + /** + * Deletes an attachment from both remote and local storage. + * Removes the remote file, local file (if exists), and the attachment record. + * On failure, defers to error handler or archives. + * + * @param attachment - The attachment record to delete + * @returns Updated attachment record + */ async deleteAttachment(attachment: AttachmentRecord): Promise { try { await this.remoteStorage.deleteFile(attachment); @@ -141,6 +176,10 @@ export class StorageService { } } + /** + * Performs cleanup of archived attachments by removing their local files and records. + * Errors during local file deletion are logged but do not prevent record deletion. + */ async deleteArchivedAttachments(): Promise { const archivedAttachments = await this.context.getArchivedAttachments(); for (const attachment of archivedAttachments) { diff --git a/packages/attachments/src/index.ts b/packages/attachments/src/index.ts index 7174fa7ca..f0cd91a8e 100644 --- a/packages/attachments/src/index.ts +++ b/packages/attachments/src/index.ts @@ -4,5 +4,5 @@ export * from './storageAdapters/NodeFileSystemAdapter.js'; export * from './storageAdapters/IndexDBFileSystemAdapter.js'; export * from './RemoteStorageAdapter.js'; export * from './AttachmentContext.js'; -export * from './StorageService.js'; +export * from './SyncingService.js'; export * from './AttachmentQueue.js'; From d8d4ad99caeb4d11243f3aed5f76d1ad57890cee Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Tue, 28 Oct 2025 14:49:15 +0200 Subject: [PATCH 10/19] Add updateHook to save file --- packages/attachments/src/AttachmentQueue.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index 1760399c6..ccb8cea3f 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -178,13 +178,15 @@ export class AttachmentQueue { fileExtension, mediaType, metaData, - id + id, + updateHook }: { data: ArrayBuffer | Blob | string; fileExtension: string; mediaType?: string; metaData?: string; id?: string; + updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => void; }): Promise { const resolvedId = id ?? (await this.context.db.get<{ id: string }>('SELECT uuid() as id')).id; const filename = `${resolvedId}.${fileExtension}`; @@ -204,6 +206,7 @@ export class AttachmentQueue { }; await this.context.db.writeTransaction(async (tx) => { + updateHook?.(tx, attachment); this.context.upsertAttachment(attachment, tx); }); From 9509aeb1b56d0732776b89cad60e5c0ab9aab3a7 Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Tue, 28 Oct 2025 14:54:15 +0200 Subject: [PATCH 11/19] Use async onUpdate callback --- packages/attachments/src/AttachmentQueue.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index ccb8cea3f..92b34512f 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -18,7 +18,6 @@ export class AttachmentQueue { logger?: ILogger; syncThrottleDuration: number; downloadAttachments: boolean = true; - watchActiveAbortController?: AbortController; archivedCacheLimit: number; attachmentService: AttachmentService; @@ -37,7 +36,7 @@ export class AttachmentQueue { db: AbstractPowerSyncDatabase; remoteStorage: RemoteStorageAdapter; localStorage: LocalStorageAdapter; - watchAttachments: (onUpdate: (attachement: WatchedAttachmentItem[]) => void) => void; + watchAttachments: (onUpdate: (attachement: WatchedAttachmentItem[]) => Promise) => void; tableName?: string; logger?: ILogger; syncIntervalMs?: number; @@ -58,8 +57,6 @@ export class AttachmentQueue { this.archivedCacheLimit = archivedCacheLimit; } - watchAttachments(onUpdate: (attachement: WatchedAttachmentItem[]) => void): void { - throw new Error('watchAttachments not implemented'); } async startSync(): Promise { From 9ac685c0474ac08924ce0782564e386b7bf00312 Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Tue, 28 Oct 2025 14:54:41 +0200 Subject: [PATCH 12/19] Improve comments --- packages/attachments/src/AttachmentContext.ts | 67 ++++++++++- packages/attachments/src/AttachmentQueue.ts | 106 +++++++++++++++++- .../attachments/src/LocalStorageAdapter.ts | 16 +-- .../attachments/src/RemoteStorageAdapter.ts | 24 ++-- packages/attachments/src/Schema.ts | 13 ++- packages/attachments/src/SyncErrorHandler.ts | 31 ++--- .../attachments/src/WatchedAttachmentItem.ts | 5 +- .../IndexDBFileSystemAdapter.ts | 19 ++-- .../storageAdapters/NodeFileSystemAdapter.ts | 4 + 9 files changed, 240 insertions(+), 45 deletions(-) diff --git a/packages/attachments/src/AttachmentContext.ts b/packages/attachments/src/AttachmentContext.ts index b312a88bd..bc4cfe51d 100644 --- a/packages/attachments/src/AttachmentContext.ts +++ b/packages/attachments/src/AttachmentContext.ts @@ -1,18 +1,43 @@ import { AbstractPowerSyncDatabase, ILogger, Transaction } from '@powersync/common'; import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.js'; +/** + * AttachmentContext provides database operations for managing attachment records. + * + * Provides methods to query, insert, update, and delete attachment records with + * proper transaction management through PowerSync. + */ export class AttachmentContext { + /** PowerSync database instance for executing queries */ db: AbstractPowerSyncDatabase; + + /** Name of the database table storing attachment records */ tableName: string; + + /** Logger instance for diagnostic information */ logger: ILogger; + /** + * Creates a new AttachmentContext instance. + * + * @param db - PowerSync database instance + * @param tableName - Name of the table storing attachment records. Default: 'attachments' + * @param logger - Logger instance for diagnostic output + */ constructor(db: AbstractPowerSyncDatabase, tableName: string = 'attachments', logger: ILogger) { this.db = db; this.tableName = tableName; this.logger = logger; } - async getActiveAttachments(): Promise { + /** + * Retrieves all active attachments that require synchronization. + * Active attachments include those queued for upload, download, or delete. + * Results are ordered by timestamp in ascending order. + * + * @returns Promise resolving to an array of active attachment records + */ + async getActiveAttachments(): Promise { const attachments = await this.db.getAll( /* sql */ ` @@ -33,6 +58,14 @@ export class AttachmentContext { return attachments.map(attachmentFromSql); } + /** + * Retrieves all archived attachments. + * + * Archived attachments are no longer referenced but haven't been permanently deleted. + * These are candidates for cleanup operations to free up storage space. + * + * @returns Promise resolving to an array of archived attachment records + */ async getArchivedAttachments(): Promise { const attachments = await this.db.getAll( /* sql */ @@ -52,6 +85,12 @@ export class AttachmentContext { return attachments.map(attachmentFromSql); } + /** + * Retrieves all attachment records regardless of state. + * Results are ordered by timestamp in ascending order. + * + * @returns Promise resolving to an array of all attachment records + */ async getAttachments(): Promise { const attachments = await this.db.getAll( /* sql */ @@ -69,6 +108,15 @@ export class AttachmentContext { return attachments.map(attachmentFromSql); } + /** + * Inserts or updates an attachment record within an existing transaction. + * + * Performs an upsert operation (INSERT OR REPLACE). Must be called within + * an active database transaction context. + * + * @param attachment - The attachment record to upsert + * @param context - Active database transaction context + */ upsertAttachment(attachment: AttachmentRecord, context: Transaction): void { context.execute( /* sql */ @@ -102,6 +150,15 @@ export class AttachmentContext { ); } + /** + * Permanently deletes an attachment record from the database. + * + * This operation removes the attachment record but does not delete + * the associated local or remote files. File deletion should be handled + * separately through the appropriate storage adapters. + * + * @param attachmentId - Unique identifier of the attachment to delete + */ async deleteAttachment(attachmentId: string): Promise { await this.db.writeTransaction((tx) => tx.execute( @@ -116,6 +173,14 @@ export class AttachmentContext { ); } + /** + * Saves multiple attachment records in a single transaction. + * + * All updates are saved in a single batch after processing. + * If the attachments array is empty, no database operations are performed. + * + * @param attachments - Array of attachment records to save + */ async saveAttachments(attachments: AttachmentRecord[]): Promise { if (attachments.length === 0) { return; diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index 92b34512f..f2abf096f 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, ILogger } from '@powersync/common'; +import { AbstractPowerSyncDatabase, ILogger, Transaction } from '@powersync/common'; import { AttachmentContext } from './AttachmentContext.js'; import { LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; @@ -7,20 +7,68 @@ import { SyncingService } from './SyncingService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; import { AttachmentService } from './AttachmentService.js'; +/** + * AttachmentQueue manages the lifecycle and synchronization of attachments + * between local and remote storage. + * + * Provides automatic synchronization, upload/download queuing, attachment monitoring, + * verification and repair of local files, and cleanup of archived attachments. + */ export class AttachmentQueue { + /** Timer for periodic synchronization operations */ periodicSyncTimer?: ReturnType; + + /** Context for managing attachment records in the database */ context: AttachmentContext; + + /** Service for synchronizing attachments between local and remote storage */ syncingService: SyncingService; + + /** Adapter for local file storage operations */ localStorage: LocalStorageAdapter; + + /** Adapter for remote file storage operations */ remoteStorage: RemoteStorageAdapter; + + /** @deprecated Directory path for storing attachments */ attachmentsDirectory?: string; + + /** Name of the database table storing attachment records */ tableName?: string; + + /** Logger instance for diagnostic information */ logger?: ILogger; + + /** Interval in milliseconds between periodic sync operations. Default: 30000 (30 seconds) */ + syncIntervalMs: number = 30 * 1000; + + /** Duration in milliseconds to throttle sync operations */ syncThrottleDuration: number; + + /** Whether to automatically download remote attachments. Default: true */ downloadAttachments: boolean = true; + + /** Maximum number of archived attachments to keep before cleanup. Default: 100 */ archivedCacheLimit: number; + + /** Service for managing attachment-related database operations */ attachmentService: AttachmentService; + /** + * Creates a new AttachmentQueue instance. + * + * @param options - Configuration options + * @param options.db - PowerSync database instance + * @param options.remoteStorage - Remote storage adapter for upload/download operations + * @param options.localStorage - Local storage adapter for file persistence + * @param options.watchAttachments - Callback for monitoring attachment changes in your data model + * @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue' + * @param options.logger - Logger instance. Defaults to db.logger + * @param options.syncIntervalMs - Interval between automatic syncs in milliseconds. Default: 30000 + * @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000 + * @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true + * @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100 + */ constructor({ db, localStorage, @@ -57,8 +105,30 @@ export class AttachmentQueue { this.archivedCacheLimit = archivedCacheLimit; } + /** + * Callback function to watch for changes in attachment references in your data model. + * + * This method should be implemented to monitor changes in your application's + * data that reference attachments. When attachments are added, removed, or modified, + * this callback should trigger the onUpdate function with the current set of attachments. + * + * @param onUpdate - Callback to invoke when attachment references change + * @throws Error indicating this method must be implemented by the user + */ + watchAttachments(onUpdate: (attachement: WatchedAttachmentItem[]) => Promise): void { + throw new Error('watchAttachments should be implemented by the user of AttachmentQueue'); } + /** + * Starts the attachment synchronization process. + * + * This method: + * - Stops any existing sync operations + * - Sets up periodic synchronization based on syncIntervalMs + * - Registers listeners for active attachment changes + * - Processes watched attachments to queue uploads/downloads + * - Handles state transitions for archived and new attachments + */ async startSync(): Promise { await this.stopSync(); @@ -156,7 +226,12 @@ export class AttachmentQueue { }); } - // Sync storage with all active attachments + /** + * Synchronizes all active attachments between local and remote storage. + * + * This is called automatically at regular intervals when sync is started, + * but can also be called manually to trigger an immediate sync. + */ async syncStorage(): Promise { const activeAttachments = await this.context.getActiveAttachments(); await this.localStorage.initialize(); @@ -164,12 +239,30 @@ export class AttachmentQueue { await this.syncingService.deleteArchivedAttachments(); } + /** + * Stops the attachment synchronization process. + * + * Clears the periodic sync timer and closes all active attachment watchers. + */ async stopSync(): Promise { clearInterval(this.periodicSyncTimer); this.periodicSyncTimer = undefined; await this.attachmentService.watchActiveAttachments().close(); } + /** + * Saves a file to local storage and queues it for upload to remote storage. + * + * @param options - File save options + * @param options.data - The file data as ArrayBuffer, Blob, or base64 string + * @param options.fileExtension - File extension (e.g., 'jpg', 'pdf') + * @param options.mediaType - MIME type of the file (e.g., 'image/jpeg') + * @param options.metaData - Optional metadata to associate with the attachment + * @param options.id - Optional custom ID. If not provided, a UUID will be generated + * @param options.updateHook - Optional callback to execute additional database operations + * within the same transaction as the attachment creation + * @returns Promise resolving to the created attachment record + */ async saveFile({ data, fileExtension, @@ -178,6 +271,7 @@ export class AttachmentQueue { id, updateHook }: { + // TODO: create a dedicated type for data data: ArrayBuffer | Blob | string; fileExtension: string; mediaType?: string; @@ -210,6 +304,14 @@ export class AttachmentQueue { return attachment; } + /** + * Verifies the integrity of all attachment records and repairs inconsistencies. + * + * This method checks each attachment record against the local filesystem and: + * - Updates localUri if the file exists at a different path + * - Archives attachments with missing local files that haven't been uploaded + * - Requeues synced attachments for download if their local files are missing + */ verifyAttachments = async (): Promise => { const attachments = await this.context.getAttachments(); const updates: AttachmentRecord[] = []; diff --git a/packages/attachments/src/LocalStorageAdapter.ts b/packages/attachments/src/LocalStorageAdapter.ts index 73d707c09..feb9df05d 100644 --- a/packages/attachments/src/LocalStorageAdapter.ts +++ b/packages/attachments/src/LocalStorageAdapter.ts @@ -3,19 +3,23 @@ export enum EncodingType { Base64 = 'base64' } +/** + * LocalStorageAdapter defines the interface for local file storage operations. + * Implementations handle file I/O, directory management, and storage initialization. + */ export interface LocalStorageAdapter { /** - * Saves buffer data to a local file. + * Saves data to a local file. * @param filePath Path where the file will be stored - * @param data Data string to store + * @param data Data to store (ArrayBuffer, Blob, or string) * @returns Number of bytes written */ saveFile(filePath: string, data: ArrayBuffer | Blob | string): Promise; /** - * Retrieves an ArrayBuffer with the file data from the given path. + * Retrieves file data as an ArrayBuffer. * @param filePath Path where the file is stored - * @returns ArrayBuffer with the file data + * @returns ArrayBuffer containing the file data */ readFile(filePath: string): Promise; @@ -35,14 +39,12 @@ export interface LocalStorageAdapter { /** * Creates a directory at the specified path. * @param path The full path to the directory - * @throws PowerSyncAttachmentError if creation fails */ makeDir(path: string): Promise; /** * Removes a directory at the specified path. * @param path The full path to the directory - * @throws PowerSyncAttachmentError if removal fails */ rmDir(path: string): Promise; @@ -57,7 +59,7 @@ export interface LocalStorageAdapter { clear(): Promise; /** - * Returns the file path of the provided filename in the user storage directory. + * Returns the file path for the provided filename in the storage directory. * @param filename The filename to get the path for * @returns The full file path */ diff --git a/packages/attachments/src/RemoteStorageAdapter.ts b/packages/attachments/src/RemoteStorageAdapter.ts index 853aac4d0..8e8e13813 100644 --- a/packages/attachments/src/RemoteStorageAdapter.ts +++ b/packages/attachments/src/RemoteStorageAdapter.ts @@ -1,27 +1,27 @@ import { AttachmentRecord } from "./Schema.js"; +/** + * RemoteStorageAdapter defines the interface for remote storage operations. + * Implementations handle uploading, downloading, and deleting files from remote storage. + */ export interface RemoteStorageAdapter { /** * Uploads a file to remote storage. - * - * @param fileData The binary content of the file to upload. - * @param attachment The associated `Attachment` metadata describing the file. - * @throws An error if the upload fails. + * @param fileData The binary content of the file to upload + * @param attachment The associated attachment metadata */ uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise; + /** * Downloads a file from remote storage. - * - * @param attachment The `Attachment` describing the file to download. - * @returns The binary data of the downloaded file. - * @throws An error if the download fails or the file is not found. + * @param attachment The attachment describing the file to download + * @returns The binary data of the downloaded file */ - downloadFile(attachment: AttachmentRecord): Promise; + downloadFile(attachment: AttachmentRecord): Promise; + /** * Deletes a file from remote storage. - * - * @param attachment The `Attachment` describing the file to delete. - * @throws An error if the deletion fails or the file does not exist. + * @param attachment The attachment describing the file to delete */ deleteFile(attachment: AttachmentRecord): Promise; } \ No newline at end of file diff --git a/packages/attachments/src/Schema.ts b/packages/attachments/src/Schema.ts index b295cdfcf..04ffba9ca 100644 --- a/packages/attachments/src/Schema.ts +++ b/packages/attachments/src/Schema.ts @@ -2,6 +2,9 @@ import { column, Table, TableV2Options } from '@powersync/common'; export const ATTACHMENT_TABLE = 'attachments'; +/** + * AttachmentRecord represents an attachment in the local database. + */ export interface AttachmentRecord { id: string; filename: string; @@ -14,7 +17,9 @@ export interface AttachmentRecord { state: AttachmentState; } -// map from db to record +/** + * Maps a database row to an AttachmentRecord. + */ export function attachmentFromSql(row: any): AttachmentRecord { return { id: row.id, @@ -29,6 +34,9 @@ export function attachmentFromSql(row: any): AttachmentRecord { }; } +/** + * AttachmentState represents the current synchronization state of an attachment. + */ export enum AttachmentState { QUEUED_SYNC = 0, // Check if the attachment needs to be uploaded or downloaded QUEUED_UPLOAD = 1, // Attachment to be uploaded @@ -40,6 +48,9 @@ export enum AttachmentState { export interface AttachmentTableOptions extends Omit {} +/** + * AttachmentTable defines the schema for the attachment queue table. + */ export class AttachmentTable extends Table { constructor(options?: AttachmentTableOptions) { super( diff --git a/packages/attachments/src/SyncErrorHandler.ts b/packages/attachments/src/SyncErrorHandler.ts index d71ea7db1..cbe49f245 100644 --- a/packages/attachments/src/SyncErrorHandler.ts +++ b/packages/attachments/src/SyncErrorHandler.ts @@ -1,28 +1,31 @@ import { AttachmentRecord } from './Schema.js'; -/// If an operation fails and should not be retried, the attachment record is archived. -export abstract class SyncErrorHandler { +/** + * SyncErrorHandler provides custom error handling for attachment sync operations. + * Implementations determine whether failed operations should be retried or archived. + */ +export interface SyncErrorHandler { /** * Handles a download error for a specific attachment. - * @param attachment The `Attachment` that failed to be downloaded. - * @param error The error encountered during the download operation. - * @returns `true` if the operation should be retried, `false` if it should be archived. + * @param attachment The attachment that failed to download + * @param error The error encountered during the download + * @returns `true` to retry the operation, `false` to archive the attachment */ - abstract onDownloadError(attachment: AttachmentRecord, error: Error): Promise; + onDownloadError(attachment: AttachmentRecord, error: Error): Promise; /** * Handles an upload error for a specific attachment. - * @param attachment The `Attachment` that failed to be uploaded. - * @param error The error encountered during the upload operation. - * @returns `true` if the operation should be retried, `false` if it should be archived. + * @param attachment The attachment that failed to upload + * @param error The error encountered during the upload + * @returns `true` to retry the operation, `false` to archive the attachment */ - abstract onUploadError(attachment: AttachmentRecord, error: Error): Promise; + onUploadError(attachment: AttachmentRecord, error: Error): Promise; /** * Handles a delete error for a specific attachment. - * @param attachment The `Attachment` that failed to be deleted. - * @param error The error encountered during the delete operation. - * @returns `true` if the operation should be retried, `false` if it should be archived. + * @param attachment The attachment that failed to delete + * @param error The error encountered during the delete + * @returns `true` to retry the operation, `false` to archive the attachment */ - abstract onDeleteError(attachment: AttachmentRecord, error: Error): Promise; + onDeleteError(attachment: AttachmentRecord, error: Error): Promise; } diff --git a/packages/attachments/src/WatchedAttachmentItem.ts b/packages/attachments/src/WatchedAttachmentItem.ts index 70fefea44..2944d7e40 100644 --- a/packages/attachments/src/WatchedAttachmentItem.ts +++ b/packages/attachments/src/WatchedAttachmentItem.ts @@ -1,4 +1,7 @@ -// A watched attachment record item. +/** + * WatchedAttachmentItem represents an attachment reference in your application's data model. + * Use either filename OR fileExtension (not both). + */ export type WatchedAttachmentItem = | { id: string; diff --git a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts index 90e999830..1a31c1d02 100644 --- a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts @@ -1,5 +1,9 @@ import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; +/** + * IndexDBFileSystemStorageAdapter implements LocalStorageAdapter using IndexedDB. + * Suitable for web browsers and web-based environments. + */ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { private dbPromise: Promise; @@ -71,9 +75,6 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { return; } - // if (options?.encoding === EncodingType.Base64) { - // } - if (options?.encoding === EncodingType.UTF8) { const encoder = new TextEncoder(); const arrayBuffer = encoder.encode(req.result).buffer; @@ -89,8 +90,6 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { bytes[i] = binaryString.charCodeAt(i); } resolve(bytes.buffer); - - // reject(new Error('Unsupported encoding')); }; req.onerror = () => reject(req.error); }); @@ -115,10 +114,16 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { } async makeDir(path: string): Promise { - // No-op for IndexedDB + // No-op for IndexedDB as it does not have a directory structure } async rmDir(path: string): Promise { - // No-op for IndexedDB + const store = await this.getStore('readwrite'); + // TODO: Test this to ensure it deletes all files under the directory + await new Promise((resolve, reject) => { + const req = store.delete(path); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); } } diff --git a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts index f267c214f..3a1c96a37 100644 --- a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts @@ -2,6 +2,10 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; +/** + * NodeFileSystemAdapter implements LocalStorageAdapter using Node.js filesystem. + * Suitable for Node.js environments and Electron applications. + */ export class NodeFileSystemAdapter implements LocalStorageAdapter { constructor(private storageDirectory: string = './user_data') {} From ce33bab96947368edcca43d09234ec3175aebd91 Mon Sep 17 00:00:00 2001 From: joshuabrink Date: Tue, 28 Oct 2025 15:15:52 +0200 Subject: [PATCH 13/19] Fix closing the watch active attachments listener --- packages/attachments/src/AttachmentQueue.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index f2abf096f..1733b5425 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, ILogger, Transaction } from '@powersync/common'; +import { AbstractPowerSyncDatabase, DifferentialWatchedQuery, ILogger, Transaction } from '@powersync/common'; import { AttachmentContext } from './AttachmentContext.js'; import { LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; @@ -54,6 +54,8 @@ export class AttachmentQueue { /** Service for managing attachment-related database operations */ attachmentService: AttachmentService; + watchActiveAttachments: DifferentialWatchedQuery; + /** * Creates a new AttachmentQueue instance. * @@ -99,6 +101,7 @@ export class AttachmentQueue { this.tableName = tableName; this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger); this.attachmentService = new AttachmentService(tableName, db); + this.watchActiveAttachments = this.attachmentService.watchActiveAttachments(); this.syncIntervalMs = syncIntervalMs; this.syncThrottleDuration = syncThrottleDuration; this.downloadAttachments = downloadAttachments; @@ -138,7 +141,7 @@ export class AttachmentQueue { }, this.syncIntervalMs); // Sync storage when there is a change in active attachments - this.attachmentService.watchActiveAttachments().registerListener({ + this.watchActiveAttachments.registerListener({ onDiff: async () => { await this.syncStorage(); } @@ -247,7 +250,7 @@ export class AttachmentQueue { async stopSync(): Promise { clearInterval(this.periodicSyncTimer); this.periodicSyncTimer = undefined; - await this.attachmentService.watchActiveAttachments().close(); + await this.watchActiveAttachments.close(); } /** From 704c2a82c43dd2a53924396ea15fb3ad5c6a21d6 Mon Sep 17 00:00:00 2001 From: Amine Date: Fri, 31 Oct 2025 18:22:42 +0800 Subject: [PATCH 14/19] tests(WIP:) initial node setup and few bug fixes --- .gitignore | 2 + packages/attachments/package.json | 3 + packages/attachments/src/AttachmentContext.ts | 78 +- packages/attachments/src/AttachmentQueue.ts | 16 +- packages/attachments/src/SyncingService.ts | 3 +- .../attachments/tests/attachments.test.ts | 343 ++-- packages/attachments/tsconfig.json | 2 +- packages/attachments/vitest.config.ts | 20 +- pnpm-lock.yaml | 1440 +++++++++++------ 9 files changed, 1227 insertions(+), 680 deletions(-) diff --git a/.gitignore b/.gitignore index cbf8f9efd..3e7968508 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ dist # Useful if running repository in VSCode dev container .pnpm-store __screenshots__ +**/tests/temp/**/* +**/tests/temp/**/.* diff --git a/packages/attachments/package.json b/packages/attachments/package.json index 1bd86678f..70d7eb837 100644 --- a/packages/attachments/package.json +++ b/packages/attachments/package.json @@ -50,7 +50,10 @@ "devDependencies": { "@powersync/common": "workspace:*", "@powersync/web": "workspace:*", + "@powersync/node": "workspace:*", + "@powersync/better-sqlite3": "^0.2.0", "@types/node": "^20.17.6", + "memfs": "^4.50.0", "vite": "^6.1.0", "vite-plugin-top-level-await": "^1.4.4", "vite-plugin-wasm": "^3.3.0" diff --git a/packages/attachments/src/AttachmentContext.ts b/packages/attachments/src/AttachmentContext.ts index bc4cfe51d..515ae5bd5 100644 --- a/packages/attachments/src/AttachmentContext.ts +++ b/packages/attachments/src/AttachmentContext.ts @@ -118,36 +118,52 @@ export class AttachmentContext { * @param context - Active database transaction context */ upsertAttachment(attachment: AttachmentRecord, context: Transaction): void { - context.execute( - /* sql */ - ` - INSERT - OR REPLACE INTO ${this.tableName} ( - id, - filename, - local_uri, - size, - media_type, - timestamp, - state, - has_synced, - meta_data - ) - VALUES - (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, - [ - attachment.id, - attachment.filename, - attachment.localUri || null, - attachment.size || null, - attachment.mediaType || null, - attachment.timestamp, - attachment.state, - attachment.hasSynced ? 1 : 0, - attachment.metaData || null - ] - ); + console.debug('[ATTACHMENT CONTEXT] Upserting attachment:', [ + attachment.id, + attachment.filename, + attachment.localUri || null, + attachment.size || null, + attachment.mediaType || null, + attachment.timestamp, + attachment.state, + attachment.hasSynced ? 1 : 0, + attachment.metaData || null + ]); + try { + context.execute( + /* sql */ + ` + INSERT + OR REPLACE INTO ${this.tableName} ( + id, + filename, + local_uri, + size, + media_type, + timestamp, + state, + has_synced, + meta_data + ) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + [ + attachment.id, + attachment.filename, + attachment.localUri || 'dummy', + attachment.size || 1, + attachment.mediaType || 'dummy', + attachment.timestamp || Date.now(), + attachment.state, + attachment.hasSynced ? 1 : 0, + attachment.metaData || 'dummy' + ] + ); + } catch (error) { + console.error('[ATTACHMENT CONTEXT] Error upserting attachment:', attachment.id?.substring(0,8), attachment.state, error); + throw error; + } } /** @@ -182,7 +198,9 @@ export class AttachmentContext { * @param attachments - Array of attachment records to save */ async saveAttachments(attachments: AttachmentRecord[]): Promise { + console.debug('[ATTACHMENT CONTEXT] Saving attachments:', attachments.map(a => ({ id: a.id?.substring(0,8), state: a.state }))); if (attachments.length === 0) { + console.debug('[ATTACHMENT CONTEXT] No attachments to save'); return; } await this.db.writeTransaction(async (tx) => { diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index 1733b5425..17938ce77 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -7,6 +7,8 @@ import { SyncingService } from './SyncingService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; import { AttachmentService } from './AttachmentService.js'; +export type AttachmentData = ArrayBuffer | Blob | string; + /** * AttachmentQueue manages the lifecycle and synchronization of attachments * between local and remote storage. @@ -94,6 +96,7 @@ export class AttachmentQueue { downloadAttachments?: boolean; archivedCacheLimit?: number; }) { + console.debug('AttachmentQueue constructor') this.context = new AttachmentContext(db, tableName, logger ?? db.logger); this.remoteStorage = remoteStorage; this.localStorage = localStorage; @@ -133,7 +136,12 @@ export class AttachmentQueue { * - Handles state transitions for archived and new attachments */ async startSync(): Promise { - await this.stopSync(); + console.debug('[QUEUE] AttachmentQueue startSync') + if (this.attachmentService.watchActiveAttachments) { + await this.stopSync(); + // re-create the watch after it was stopped + this.watchActiveAttachments = this.attachmentService.watchActiveAttachments(); + } // Sync storage periodically this.periodicSyncTimer = setInterval(async () => { @@ -143,12 +151,14 @@ export class AttachmentQueue { // Sync storage when there is a change in active attachments this.watchActiveAttachments.registerListener({ onDiff: async () => { + console.debug('[QUEUE] watchActiveAttachments: diff detected, syncing storage'); await this.syncStorage(); } }); // Process attachments when there is a change in watched attachments this.watchAttachments(async (watchedAttachments) => { + console.debug('[QUEUE] watchAttachments callback:', watchedAttachments.length, 'items'); // Need to get all the attachments which are tracked in the DB. // We might need to restore an archived attachment. const currentAttachments = await this.context.getAttachments(); @@ -224,6 +234,7 @@ export class AttachmentQueue { } if (attachmentUpdates.length > 0) { + console.debug('[QUEUE] Saving attachments:', attachmentUpdates); await this.context.saveAttachments(attachmentUpdates); } }); @@ -237,6 +248,7 @@ export class AttachmentQueue { */ async syncStorage(): Promise { const activeAttachments = await this.context.getActiveAttachments(); + console.debug('[QUEUE] syncStorage: processing', activeAttachments.length, 'active attachments'); await this.localStorage.initialize(); await this.syncingService.processAttachments(activeAttachments); await this.syncingService.deleteArchivedAttachments(); @@ -275,7 +287,7 @@ export class AttachmentQueue { updateHook }: { // TODO: create a dedicated type for data - data: ArrayBuffer | Blob | string; + data: AttachmentData; fileExtension: string; mediaType?: string; metaData?: string; diff --git a/packages/attachments/src/SyncingService.ts b/packages/attachments/src/SyncingService.ts index e69e518f1..c77a8c3ae 100644 --- a/packages/attachments/src/SyncingService.ts +++ b/packages/attachments/src/SyncingService.ts @@ -107,9 +107,10 @@ export class SyncingService { * @returns Updated attachment record with local URI and new state */ async downloadAttachment(attachment: AttachmentRecord): Promise { + console.debug('[SYNC] Downloading:', attachment.id); try { const file = await this.remoteStorage.downloadFile(attachment); - + console.debug('[SYNC] Downloaded, converting to base64'); const base64Data = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { diff --git a/packages/attachments/tests/attachments.test.ts b/packages/attachments/tests/attachments.test.ts index 41f049102..b9611a52d 100644 --- a/packages/attachments/tests/attachments.test.ts +++ b/packages/attachments/tests/attachments.test.ts @@ -1,20 +1,80 @@ import { describe, expect, it, vi } from 'vitest'; -import { PowerSyncDatabase, Schema, Table, column } from '@powersync/web'; +import { vol } from 'memfs' +import { PowerSyncDatabase, Schema, Table, column } from '@powersync/node'; import { AbstractPowerSyncDatabase } from '@powersync/common'; import { AttachmentQueue } from '../src/AttachmentQueue.js'; -import { AttachmentState, AttachmentTable } from '../src/Schema.js'; +import { attachmentFromSql, AttachmentRecord, AttachmentState, AttachmentTable } from '../src/Schema.js'; import { RemoteStorageAdapter } from '../src/RemoteStorageAdapter.js'; import { WatchedAttachmentItem } from '../src/WatchedAttachmentItem.js'; -import { IndexDBFileSystemStorageAdapter } from '../src/storageAdapters/IndexDBFileSystemAdapter.js'; +import { NodeFileSystemAdapter } from '../src/storageAdapters/NodeFileSystemAdapter.js'; + +const MOCK_JPEG_U8A = [ + 0xFF, 0xD8, 0xFF, 0xE0, + 0x00, 0x10, + 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, + 0x00, + 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, + 0xFF, 0xD9 +]; +// This creates a 1x1 pixel JPEG that's base64 encoded +const createMockJpegBuffer = (): ArrayBuffer => { + return new Uint8Array(MOCK_JPEG_U8A).buffer; +}; + +const mockUploadFile = vi.fn().mockResolvedValue(undefined); +const mockDownloadFile = vi.fn().mockResolvedValue(createMockJpegBuffer()); +const mockDeleteFile = vi.fn().mockResolvedValue(undefined); const mockRemoteStorage: RemoteStorageAdapter = { - downloadFile: (attachment) => { - return Promise.resolve(new Blob(['data:image/jpeg;base64,FAKE_BASE64_DATA'], { type: 'image/jpeg' })); - }, - uploadFile: vi.fn(), - deleteFile: vi.fn() + downloadFile: mockDownloadFile, + uploadFile: mockUploadFile, + deleteFile: mockDeleteFile }; +// we mock the local file system and use the NodeFileSystemAdapter to save and read files in memory +// see more: https://vitest.dev/guide/mocking/file-system +vi.mock('fs', () => { + const { fs } = require('memfs'); + return fs; +}); + +const mockLocalStorage = new NodeFileSystemAdapter('./temp/attachments'); + +let db: AbstractPowerSyncDatabase; + +beforeAll(async () => { + db = new PowerSyncDatabase({ + schema: new Schema({ + users: new Table({ + name: column.text, + email: column.text, + photo_id: column.text + }), + attachments: new AttachmentTable() + }), + database: { + dbFilename: 'testing.db', + dbLocation: './tests/temp' + } + }); + +}); + +beforeEach(() => { + // reset the state of in-memory fs + vol.reset() + // Reset mock call history + mockUploadFile.mockClear(); + mockDownloadFile.mockClear(); + mockDeleteFile.mockClear(); +}) + +afterEach(async () => { + await db.disconnectAndClear(); +}); + const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => void) => { db.watch( /* sql */ @@ -39,164 +99,151 @@ const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => vo ); }; -let db: AbstractPowerSyncDatabase; +// Helper to watch the attachments table +async function* watchAttachmentsTable(): AsyncGenerator { + console.debug('[TEST] watchAttachmentsTable: watching attachments table'); + const watcher = db.watch( + ` + SELECT + * + FROM + attachments; + `, + // [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE], + ); -beforeAll(async () => { - db = new PowerSyncDatabase({ - schema: new Schema({ - users: new Table({ - name: column.text, - email: column.text, - photo_id: column.text - }), - attachments: new AttachmentTable() - }), - database: { - dbFilename: 'example.db' - } - }); + for await (const result of watcher) { + console.debug('[TEST] watchAttachmentsTable: result', result); + const attachments = result.rows?._array.map((r: any) => attachmentFromSql(r)) ?? []; + console.debug('[TEST] watchAttachmentsTable: attachments', attachments); + console.debug('[TEST] Mapped attachments:', attachments.map(a => ({ id: a.id?.substring(0,8), state: a.state, hasSynced: a.hasSynced }))); + yield attachments; + } +} - await db.disconnectAndClear(); -}); +async function waitForMatchCondition( + iteratorGenerator: () => AsyncGenerator, + predicate: (attachments: AttachmentRecord[]) => boolean, + timeoutSeconds: number = 5 +): Promise { + console.debug('[TEST] waitForMatchCondition: waiting for condition'); + const timeoutMs = timeoutSeconds * 1000; + const abortController = new AbortController(); + const startTime = Date.now(); -afterAll(async () => { - await db.disconnectAndClear(); -}); + const generator = iteratorGenerator(); + + try { + for await (const value of generator) { + console.debug('[TEST] waitForMatchCondition: generator value', value); + if (Date.now() - startTime > timeoutMs) { + console.debug('[TEST] waitForMatchCondition: timeout'); + throw new Error(`Timeout waiting for condition after ${timeoutSeconds}s`); + } + + if (predicate(value)) { + console.debug('[TEST] waitForMatchCondition: match found!'); + return value; + } + } + console.debug('[TEST] waitForMatchCondition: for await loop ended without match'); + throw new Error('Stream ended without match'); + } finally { + console.debug('[TEST] waitForMatchCondition: finally finaling'); + await generator.return?.(undefined); + abortController.abort(); + } +} describe('attachment queue', () => { - it('should download attachments when a new record with an attachment is added', async () => { + it('should download attachments when a new record with an attachment is added', { + timeout: 10000 // 10 seconds + }, async () => { const queue = new AttachmentQueue({ db: db, watchAttachments, remoteStorage: mockRemoteStorage, - localStorage: new IndexDBFileSystemStorageAdapter(), + localStorage: mockLocalStorage, }); await queue.startSync(); + await db.execute( /* sql */ ` - INSERT INTO - users (id, name, email, photo_id) - VALUES - ( - uuid (), - 'example', - 'example@example.com', - uuid () - ) - `, - [] - ); - - const attachmentRecords = await waitForMatch( - () => - db.watch( - /* sql */ - ` - SELECT - * - FROM - attachments - `, - [] - ), - (results) => { - return results?.rows?._array.some((r: any) => r.state === AttachmentState.SYNCED); - }, - 5 - ); - - const attachmentRecord = attachmentRecords.rows._array.at(0); - - const localData = await queue.localStorage.readFile(attachmentRecord.local_uri!); - const localDataString = new TextDecoder().decode(localData); - expect(localDataString).toBe('data:image/jpeg;base64,FAKE_BASE64_DATA'); + INSERT INTO + users (id, name, email, photo_id) + VALUES + ( + uuid (), + 'example', + 'example@example.com', + uuid () + ) + `, + [] + ); + + const attachments = await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => { + console.debug('[TEST] Predicate checking:', results.map(r => ({ id: r.id?.substring(0,8), state: r.state }))); + return results.some((r) => r.state === AttachmentState.SYNCED) + }, + 5 + ); + + const attachmentRecord = attachments.find((r) => r.state === AttachmentState.SYNCED); + if (!attachmentRecord) { + throw new Error('No attachment record found'); + } + + // Verify the download was called + expect(mockDownloadFile).toHaveBeenCalled(); + + // Verify local file exists + const localData = await queue.localStorage.readFile(attachmentRecord.localUri!); + expect(localData).toEqual(MOCK_JPEG_U8A); await queue.stopSync(); }); }); -async function waitForMatch( - iteratorGenerator: () => AsyncIterable, - predicate: (value: any) => boolean, - timeout: number -) { - const timeoutMs = timeout * 1000; - const abortController = new AbortController(); - - const matchPromise = (async () => { - const asyncIterable = iteratorGenerator(); - try { - for await (const value of asyncIterable) { - if (abortController.signal.aborted) { - throw new Error('Timeout'); - } - if (predicate(value)) { - return value; - } - } - throw new Error('Stream ended without match'); - } finally { - const iterator = asyncIterable[Symbol.asyncIterator](); - if (iterator.return) { - await iterator.return(); - } - } - })(); - - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => { - abortController.abort(); - reject(new Error('Timeout')); - }, timeoutMs) - ); - - return Promise.race([matchPromise, timeoutPromise]); -} - -// describe('attachments', () => { -// beforeEach(() => { -// vi.clearAllMocks(); -// }); - -// it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => { -// const queue = new AttachmentQueue({ -// db: mockPowerSync as any, -// watchAttachments: watchAttachments, -// remoteStorage: mockRemoteStorage, -// localStorage: mockLocalStorage -// }); - -// await queue.saveFile; - -// expect(mockLocalStorage.downloadFile).not.toHaveBeenCalled(); -// }); - -// it('should download attachments when downloadRecord is called with downloadAttachments true', async () => { -// const queue = new TestAttachmentQueue({ -// powersync: mockPowerSync as any, -// storage: mockLocalStorage, -// downloadAttachments: true -// }); - -// await queue.downloadRecord(record); - -// expect(mockLocalStorage.downloadFile).toHaveBeenCalled(); -// }); - -// // Testing the inverse of this test, i.e. when downloadAttachments is false, is not required as you can't wait for something that does not happen -// it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => { -// const queue = new TestAttachmentQueue({ -// powersync: mockPowerSync as any, -// storage: mockLocalStorage, -// downloadAttachments: true -// }); - -// queue.watchDownloads(); -// await vi.waitFor(() => { -// expect(mockLocalStorage.downloadFile).toBeCalledTimes(2); -// }); -// }); -// }); +// async function waitForMatch( +// iteratorGenerator: () => AsyncGenerator, +// predicate: (attachments: AttachmentRecord[]) => boolean, +// timeoutSeconds: number = 5 +// ): Promise { +// const timeoutMs = timeoutSeconds * 1000; +// const abortController = new AbortController(); + +// const matchPromise = (async () => { +// const asyncIterable = iteratorGenerator(); +// try { +// for await (const value of asyncIterable) { +// if (abortController.signal.aborted) { +// throw new Error('Timeout'); +// } +// if (predicate(value)) { +// return value; +// } +// } +// throw new Error('Stream ended without match'); +// } finally { +// const iterator = asyncIterable[Symbol.asyncIterator](); +// if (iterator.return) { +// await iterator.return(); +// } +// } +// })(); + +// const timeoutPromise = new Promise((_, reject) => +// setTimeout(() => { +// abortController.abort(); +// reject(new Error('Timeout')); +// }, timeoutMs) +// ); + +// return Promise.race([matchPromise, timeoutPromise]); +// } \ No newline at end of file diff --git a/packages/attachments/tsconfig.json b/packages/attachments/tsconfig.json index 9c31f1952..fe19835ed 100644 --- a/packages/attachments/tsconfig.json +++ b/packages/attachments/tsconfig.json @@ -13,5 +13,5 @@ "path": "../common" } ], - "include": ["src/**/*", "tests/**/*", "package.json"] + "include": ["src/**/*", "package.json"] } diff --git a/packages/attachments/vitest.config.ts b/packages/attachments/vitest.config.ts index e26633c68..3ee2de12c 100644 --- a/packages/attachments/vitest.config.ts +++ b/packages/attachments/vitest.config.ts @@ -18,16 +18,16 @@ const config: UserConfigExport = { isolate: false, globals: true, include: ['tests/**/*.test.ts'], - browser: { - enabled: true, - headless: true, - provider: 'playwright', - instances: [ - { - browser: 'chromium' - } - ] - } + // browser: { + // enabled: true, + // headless: true, + // provider: 'playwright', + // instances: [ + // { + // browser: 'chromium' + // } + // ] + // } } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e594b631c..767c1c281 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,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@22.15.29)(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@22.15.29)(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 @@ -113,10 +113,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@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@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.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@22.15.29)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.3.5(@types/node@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@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.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@22.15.29)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.3.5(@types/node@22.15.29)(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@22.15.29)(chokidar@4.0.3) @@ -143,7 +143,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 +164,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 +188,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 +236,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 @@ -267,16 +267,16 @@ importers: dependencies: '@capacitor/android': specifier: ^6.0.0 - version: 6.2.1(@capacitor/core@7.4.3) + version: 6.2.1(@capacitor/core@7.4.4) '@capacitor/core': specifier: latest - version: 7.4.3 + version: 7.4.4 '@capacitor/ios': specifier: ^6.0.0 - version: 6.2.1(@capacitor/core@7.4.3) + version: 6.2.1(@capacitor/core@7.4.4) '@capacitor/splash-screen': specifier: latest - version: 7.0.3(@capacitor/core@7.4.3) + version: 7.0.3(@capacitor/core@7.4.4) '@journeyapps/wa-sqlite': specifier: ^1.3.2 version: 1.3.2 @@ -568,10 +568,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 @@ -586,13 +586,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: @@ -659,10 +659,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 @@ -805,7 +805,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@22.15.29)(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@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.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)) @@ -889,7 +889,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)) @@ -962,7 +962,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) @@ -983,7 +983,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) @@ -1007,7 +1007,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)) @@ -1022,7 +1022,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)) @@ -1064,7 +1064,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 @@ -1101,7 +1101,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.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) @@ -1128,7 +1128,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) @@ -1149,7 +1149,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)) @@ -1167,7 +1167,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)) @@ -1182,7 +1182,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)) @@ -1240,10 +1240,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) @@ -1689,10 +1689,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) @@ -1711,19 +1711,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 @@ -1773,18 +1773,33 @@ importers: packages/attachments: devDependencies: + '@powersync/better-sqlite3': + specifier: ^0.2.0 + version: 0.2.0 '@powersync/common': specifier: workspace:* version: link:../common + '@powersync/node': + specifier: workspace:* + version: link:../node + '@powersync/web': + specifier: workspace:* + version: link:../web '@types/node': specifier: ^20.17.6 version: 20.17.57 + memfs: + specifier: ^4.50.0 + version: 4.50.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) vite-plugin-top-level-await: specifier: ^1.4.4 version: 1.5.0(rollup@4.41.1)(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)) + vite-plugin-wasm: + specifier: ^3.3.0 + version: 3.4.1(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)) packages/common: dependencies: @@ -1861,7 +1876,7 @@ importers: version: 20.17.57 drizzle-orm: specifier: ^0.35.2 - version: 0.35.3(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react@19.0.0))(@types/react@19.1.6)(@types/sql.js@1.4.9)(kysely@0.28.2)(react@19.0.0)(sql.js@1.13.0) + version: 0.35.3(@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/react@19.1.6)(@types/sql.js@1.4.9)(kysely@0.28.2)(react@19.0.0)(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) @@ -1929,7 +1944,7 @@ importers: version: 1.4.2 drizzle-orm: specifier: ^0.35.2 - version: 0.35.3(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react@19.0.0))(@types/react@19.1.6)(@types/sql.js@1.4.9)(kysely@0.28.2)(react@19.0.0)(sql.js@1.13.0) + version: 0.35.3(@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/react@19.1.6)(@types/sql.js@1.4.9)(kysely@0.28.2)(react@19.0.0)(sql.js@1.13.0) rollup: specifier: 4.14.3 version: 4.14.3 @@ -2161,13 +2176,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 @@ -2367,7 +2382,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)) @@ -2409,7 +2424,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 @@ -2418,7 +2433,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 @@ -2427,7 +2442,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 @@ -3607,8 +3622,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@capacitor/core@7.4.3': - resolution: {integrity: sha512-wCWr8fQ9Wxn0466vPg7nMn0tivbNVjNy1yL4GvDSIZuZx7UpU2HeVGNe9QjN/quEd+YLRFeKEBLBw619VqUiNg==} + '@capacitor/core@7.4.4': + resolution: {integrity: sha512-xzjxpr+d2zwTpCaN0k+C6wKSZzWFAb9OVEUtmO72ihjr/NEDoLvsGl4WLfjWPcCO2zOy0b2X52tfRWjECFUjtw==} '@capacitor/ios@6.2.1': resolution: {integrity: sha512-tbMlQdQjxe1wyaBvYVU1yTojKJjgluZQsJkALuJxv/6F8QTw5b6vd7X785O/O7cMpIAZfUWo/vtAHzFkRV+kXw==} @@ -5693,18 +5708,48 @@ packages: peerDependencies: tslib: '2' + '@jsonjoy.com/buffers@1.2.1': + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@jsonjoy.com/json-pack@1.2.0': resolution: {integrity: sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' + '@jsonjoy.com/json-pack@1.21.0': + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@jsonjoy.com/util@1.6.0': resolution: {integrity: sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} @@ -13197,6 +13242,12 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regex.js@1.2.0: + resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -14591,6 +14642,7 @@ packages: keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -15220,6 +15272,9 @@ packages: resolution: {integrity: sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==} engines: {node: '>= 4.0.0'} + memfs@4.50.0: + resolution: {integrity: sha512-N0LUYQMUA1yS5tJKmMtU9yprPm6ZIg24yr/OVv/7t6q0kKDIho4cBbXRi1XKttUmNYDYgF/q45qrKE/UhGO0CA==} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -19341,6 +19396,12 @@ packages: peerDependencies: tslib: ^2 + thingies@2.5.0: + resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + this-file@2.0.3: resolution: {integrity: sha512-IdMH1bUkVJdJjM7o8v83Mv4QvVPdkAofur20STl2Bbw9uMuuS/bT/PZURkEdZsy9XC/1ZXWgZ1wIL9nvouGaEg==} engines: {node: '>=14.15.0'} @@ -19451,6 +19512,12 @@ packages: peerDependencies: tslib: '2' + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -21131,11 +21198,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@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@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.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@22.15.29)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.3.5(@types/node@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@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.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@22.15.29)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.3.5(@types/node@22.15.29)(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 @@ -21184,13 +21251,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@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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@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.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@22.15.29)(typescript@5.5.4)))(tsx@4.19.4)(typescript@5.5.4)(vite@6.3.5(@types/node@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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 @@ -21202,14 +21269,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)) - '@vitejs/plugin-basic-ssl': 1.2.0(vite@6.2.7(@types/node@22.15.29)(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)) + '@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.3.5(@types/node@22.15.29)(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 @@ -21217,38 +21284,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@22.15.29)(babel-plugin-macros@3.1.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.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@22.15.29)(typescript@5.5.4)) transitivePeerDependencies: - '@angular/compiler' - '@rspack/core' @@ -21272,12 +21339,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 @@ -21308,7 +21375,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@22.15.29)(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@22.15.29)(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@22.15.29)(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) @@ -21344,7 +21411,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@22.15.29)(typescript@5.5.4)) transitivePeerDependencies: - '@types/node' - chokidar @@ -22614,9 +22681,9 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.0.0 - '@capacitor/android@6.2.1(@capacitor/core@7.4.3)': + '@capacitor/android@6.2.1(@capacitor/core@7.4.4)': dependencies: - '@capacitor/core': 7.4.3 + '@capacitor/core': 7.4.4 '@capacitor/cli@6.2.1': dependencies: @@ -22640,17 +22707,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@capacitor/core@7.4.3': + '@capacitor/core@7.4.4': dependencies: tslib: 2.8.1 - '@capacitor/ios@6.2.1(@capacitor/core@7.4.3)': + '@capacitor/ios@6.2.1(@capacitor/core@7.4.4)': dependencies: - '@capacitor/core': 7.4.3 + '@capacitor/core': 7.4.4 - '@capacitor/splash-screen@7.0.3(@capacitor/core@7.4.3)': + '@capacitor/splash-screen@7.0.3(@capacitor/core@7.4.4)': dependencies: - '@capacitor/core': 7.4.3 + '@capacitor/core': 7.4.4 '@changesets/apply-release-plan@7.0.12': dependencies: @@ -23110,7 +23177,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 @@ -23123,7 +23190,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 @@ -23137,34 +23204,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' @@ -23181,15 +23248,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 @@ -23205,7 +23272,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 @@ -23215,7 +23282,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) @@ -23224,9 +23291,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' @@ -23253,17 +23320,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 @@ -23275,16 +23342,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 @@ -23300,9 +23367,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 @@ -23311,9 +23378,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 @@ -23330,17 +23397,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 @@ -23352,7 +23419,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' @@ -23372,17 +23439,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 @@ -23393,7 +23460,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' @@ -23413,18 +23480,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' @@ -23444,11 +23511,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' @@ -23471,11 +23538,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) @@ -23500,11 +23567,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 @@ -23527,11 +23594,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) @@ -23555,11 +23622,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 @@ -23582,14 +23649,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) @@ -23614,18 +23681,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' @@ -23645,23 +23712,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: @@ -23691,21 +23758,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 @@ -23740,13 +23807,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 @@ -23765,16 +23832,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 @@ -23814,7 +23881,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 @@ -23825,7 +23892,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' @@ -23835,7 +23902,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 @@ -23846,7 +23913,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' @@ -23856,9 +23923,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' @@ -23870,11 +23937,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 @@ -23890,14 +23957,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 @@ -23910,9 +23977,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 @@ -25364,13 +25431,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 @@ -25720,7 +25787,77 @@ 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 + '@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@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 + 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.5.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -25734,7 +25871,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.5.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -25754,8 +25891,9 @@ 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(@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@22.15.29)(typescript@5.9.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -25769,7 +25907,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@22.15.29)(typescript@5.9.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -25789,6 +25927,7 @@ snapshots: - babel-plugin-macros - supports-color - ts-node + optional: true '@jest/create-cache-key-function@29.7.0': dependencies: @@ -25976,6 +26115,14 @@ snapshots: dependencies: tslib: 2.8.1 + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + '@jsonjoy.com/json-pack@1.2.0(tslib@2.8.1)': dependencies: '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) @@ -25984,10 +26131,34 @@ snapshots: thingies: 1.21.0(tslib@2.8.1) tslib: 2.8.1 + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.5.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + '@jsonjoy.com/util@1.6.0(tslib@2.8.1)': dependencies: tslib: 2.8.1 + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + '@kurkle/color@0.3.4': {} '@leichtgewicht/ip-codec@2.0.5': {} @@ -26372,7 +26543,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 @@ -26763,7 +26934,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 @@ -28518,7 +28689,7 @@ snapshots: - supports-color - typescript - '@react-native/eslint-config@0.77.0(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.29)(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@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.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) @@ -28529,7 +28700,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@22.15.29)(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@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.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) @@ -28539,7 +28710,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) @@ -28550,7 +28721,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) @@ -28771,7 +28942,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) @@ -28787,7 +28958,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) @@ -29243,11 +29414,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': {} @@ -29598,7 +29771,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 @@ -29613,6 +29786,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: @@ -31162,7 +31336,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 @@ -31662,6 +31836,10 @@ snapshots: dependencies: vite: 6.2.7(@types/node@22.15.29)(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) + '@vitejs/plugin-basic-ssl@1.2.0(vite@6.3.5(@types/node@22.15.29)(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))': + dependencies: + vite: 6.3.5(@types/node@22.15.29)(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) + '@vitejs/plugin-react@4.5.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))': dependencies: '@babel/core': 7.26.10 @@ -31699,11 +31877,11 @@ snapshots: - vite optional: true - '@vitest/browser@3.2.4(playwright@1.52.0)(vite@6.3.5(@types/node@22.15.29)(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@22.15.29)(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@22.15.29)(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@22.15.29)(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 @@ -31734,7 +31912,7 @@ snapshots: optionalDependencies: vite: 5.4.19(@types/node@22.15.29)(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@22.15.29)(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@22.15.29)(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 @@ -31898,7 +32076,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) @@ -32029,17 +32207,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) @@ -32052,10 +32230,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': {} @@ -32553,19 +32731,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: @@ -32574,13 +32752,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 @@ -33671,7 +33842,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 @@ -33679,9 +33850,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 @@ -33802,13 +33973,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: @@ -33817,13 +33988,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: @@ -33832,6 +34003,38 @@ 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(@swc/core@1.11.29)(@types/node@22.15.29)(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@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.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@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.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@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.9.2)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + optional: true + create-require@1.1.1: {} crelt@1.0.6: {} @@ -33921,7 +34124,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 @@ -33932,10 +34135,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 @@ -33946,10 +34149,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 @@ -33960,7 +34163,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)): @@ -33974,10 +34177,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) @@ -33985,7 +34188,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 @@ -34337,10 +34540,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) @@ -34352,7 +34555,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 @@ -34377,7 +34580,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' @@ -34510,7 +34713,7 @@ snapshots: dotenv@16.5.0: {} - drizzle-orm@0.35.3(@libsql/client-wasm@0.15.8)(@op-engineering/op-sqlite@14.0.2(react@19.0.0))(@types/react@19.1.6)(@types/sql.js@1.4.9)(kysely@0.28.2)(react@19.0.0)(sql.js@1.13.0): + drizzle-orm@0.35.3(@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/react@19.1.6)(@types/sql.js@1.4.9)(kysely@0.28.2)(react@19.0.0)(sql.js@1.13.0): dependencies: '@libsql/client-wasm': 0.15.8 optionalDependencies: @@ -35117,7 +35320,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -35179,7 +35382,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.7.8 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -35252,7 +35455,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -35291,24 +35494,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@22.15.29)(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@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.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@22.15.29)(babel-plugin-macros@3.1.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.9.2)) transitivePeerDependencies: - supports-color - typescript @@ -35736,7 +35939,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 @@ -35896,7 +36099,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 @@ -35917,7 +36120,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' @@ -35926,7 +36129,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 @@ -35947,7 +36150,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' @@ -35989,7 +36192,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) @@ -36017,7 +36220,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)) @@ -36053,7 +36256,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)) @@ -36256,11 +36459,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: {} @@ -36689,6 +36892,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regex.js@1.2.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + glob-to-regexp@0.4.1: {} glob@10.4.5: @@ -37136,7 +37343,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 @@ -37144,11 +37351,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 @@ -37156,10 +37362,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 @@ -37167,8 +37373,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: @@ -37872,16 +38090,16 @@ 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(@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)) '@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@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(@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 jest-validate: 29.7.0 yargs: 17.7.2 @@ -37891,16 +38109,35 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@22.15.29)(babel-plugin-macros@3.1.0): + 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@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)) 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@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 + 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.5.4)): + dependencies: + '@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.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(@swc/core@1.11.29)(@types/node@22.15.29)(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(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.5.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -37911,16 +38148,16 @@ snapshots: - ts-node optional: true - 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@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.9.2)): 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@22.15.29)(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@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.9.2)) 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@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.9.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -37929,8 +38166,71 @@ snapshots: - babel-plugin-macros - supports-color - 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(@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 + '@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@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@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.5.4)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -37956,12 +38256,13 @@ 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.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(@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@22.15.29)(typescript@5.9.2)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -37987,12 +38288,76 @@ 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@22.15.29)(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(@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 + '@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': 22.15.29 + 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@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.5.4)): + 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': 22.15.29 + ts-node: 10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.5.4) + 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.9.2)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -38018,10 +38383,11 @@ 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.9.2) transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true jest-diff@29.7.0: dependencies: @@ -38042,7 +38408,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 @@ -38055,7 +38421,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: @@ -38085,7 +38451,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 @@ -38098,11 +38464,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 @@ -38304,11 +38670,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 @@ -38339,24 +38705,36 @@ 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(@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)) '@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@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): + 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@22.15.29)(babel-plugin-macros@3.1.0) + 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(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.5.4)): + dependencies: + '@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.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(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.5.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -38364,17 +38742,18 @@ snapshots: - ts-node optional: true - 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.9.2)): 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@22.15.29)(typescript@5.9.2)) '@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@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.9.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node + optional: true jimp-compact@0.16.1: {} @@ -38705,11 +39084,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: @@ -38739,7 +39118,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: @@ -39446,6 +39825,15 @@ snapshots: tree-dump: 1.0.3(tslib@2.8.1) tslib: 2.8.1 + memfs@4.50.0: + dependencies: + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.5.0(tslib@2.8.1) + tree-dump: 1.0.3(tslib@2.8.1) + tslib: 2.8.1 + memoize-one@5.2.1: {} memoize-one@6.0.0: {} @@ -40461,17 +40849,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: @@ -41011,11 +41399,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: {} @@ -41721,32 +42109,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(@swc/core@1.11.29)(@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)(@types/node@22.15.29)(typescript@5.5.4)): 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@22.15.29)(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)): + 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 @@ -42493,11 +42890,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: @@ -43163,7 +43560,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 @@ -43249,13 +43646,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: @@ -43961,18 +44358,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) @@ -44400,13 +44797,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 @@ -44750,6 +45147,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) @@ -44758,10 +45159,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 @@ -44877,11 +45274,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)) symbol-observable@4.0.0: {} @@ -44893,7 +45290,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 @@ -44912,7 +45309,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 @@ -44920,6 +45317,34 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(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@22.15.29)(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) @@ -45069,29 +45494,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: @@ -45102,7 +45526,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: @@ -45115,15 +45539,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 @@ -45131,7 +45546,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: @@ -45171,6 +45586,10 @@ snapshots: dependencies: tslib: 2.8.1 + thingies@2.5.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + this-file@2.0.3: {} throat@5.0.0: {} @@ -45266,6 +45685,10 @@ snapshots: dependencies: tslib: 2.8.1 + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -45300,12 +45723,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 @@ -45330,27 +45753,28 @@ 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@22.15.29)(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': 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.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@22.15.29)(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 @@ -45364,13 +45788,14 @@ snapshots: 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@22.15.29)(typescript@5.9.2): + ts-node@10.9.2(@swc/core@1.11.29)(@types/node@22.15.29)(typescript@5.3.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -45384,40 +45809,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@22.15.29)(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': 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: 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@22.15.29)(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': 22.15.29 acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 @@ -45427,26 +45852,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: {} @@ -45883,14 +46310,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: @@ -46064,7 +46491,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: @@ -46074,7 +46501,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.41.1)(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.41.1) - '@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: @@ -46084,7 +46511,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.41.1)(vite@5.4.19(@types/node@22.15.29)(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.41.1) - '@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@22.15.29)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) transitivePeerDependencies: @@ -46094,7 +46521,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.41.1)(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.41.1) - '@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: @@ -46104,7 +46531,7 @@ snapshots: vite-plugin-top-level-await@1.5.0(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.29)(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.41.1) - '@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@22.15.29)(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: @@ -46113,7 +46540,7 @@ snapshots: vite-plugin-vuetify@2.1.1(vite@5.4.19(@types/node@22.15.29)(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@22.15.29)(less@4.2.2)(lightningcss@1.30.1)(sass@1.89.1)(terser@5.40.0) @@ -46199,6 +46626,25 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 + vite@6.3.5(@types/node@22.15.29)(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): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.5(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.4 + rollup: 4.41.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 22.15.29 + fsevents: 2.3.3 + 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 + vite@6.3.5(@types/node@22.15.29)(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: esbuild: 0.25.5 @@ -46400,9 +46846,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 @@ -46414,6 +46860,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 @@ -46423,7 +46878,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 @@ -46474,7 +46929,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 @@ -46501,7 +46996,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) @@ -46527,7 +47022,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) @@ -46536,37 +47031,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.7 - '@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 @@ -46589,7 +47053,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: @@ -46597,7 +47061,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 @@ -46620,7 +47084,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: @@ -46713,7 +47177,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: @@ -46723,7 +47187,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 @@ -46732,7 +47196,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 ff25b2b8364a0467d138700a526f98ee467b344a Mon Sep 17 00:00:00 2001 From: Amine Date: Fri, 31 Oct 2025 22:24:07 +0800 Subject: [PATCH 15/19] tests: fixed SyncService to rely on LocalStorage and be agnostic to file saving. --- .gitignore | 4 +-- packages/attachments/src/AttachmentContext.ts | 18 ++----------- packages/attachments/src/AttachmentQueue.ts | 10 +------- .../attachments/src/LocalStorageAdapter.ts | 5 +++- packages/attachments/src/SyncingService.ts | 15 ++--------- .../IndexDBFileSystemAdapter.ts | 25 ++++++++++++++++--- .../storageAdapters/NodeFileSystemAdapter.ts | 16 +++++++----- .../attachments/tests/attachments.test.ts | 14 +---------- 8 files changed, 43 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index 3e7968508..1bf4f9aed 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ dist # Useful if running repository in VSCode dev container .pnpm-store __screenshots__ -**/tests/temp/**/* -**/tests/temp/**/.* +testing.db +testing.db-* diff --git a/packages/attachments/src/AttachmentContext.ts b/packages/attachments/src/AttachmentContext.ts index 515ae5bd5..7f43709d5 100644 --- a/packages/attachments/src/AttachmentContext.ts +++ b/packages/attachments/src/AttachmentContext.ts @@ -117,20 +117,9 @@ export class AttachmentContext { * @param attachment - The attachment record to upsert * @param context - Active database transaction context */ - upsertAttachment(attachment: AttachmentRecord, context: Transaction): void { - console.debug('[ATTACHMENT CONTEXT] Upserting attachment:', [ - attachment.id, - attachment.filename, - attachment.localUri || null, - attachment.size || null, - attachment.mediaType || null, - attachment.timestamp, - attachment.state, - attachment.hasSynced ? 1 : 0, - attachment.metaData || null - ]); + async upsertAttachment(attachment: AttachmentRecord, context: Transaction): Promise { try { - context.execute( + const result = await context.execute( /* sql */ ` INSERT @@ -161,7 +150,6 @@ export class AttachmentContext { ] ); } catch (error) { - console.error('[ATTACHMENT CONTEXT] Error upserting attachment:', attachment.id?.substring(0,8), attachment.state, error); throw error; } } @@ -198,9 +186,7 @@ export class AttachmentContext { * @param attachments - Array of attachment records to save */ async saveAttachments(attachments: AttachmentRecord[]): Promise { - console.debug('[ATTACHMENT CONTEXT] Saving attachments:', attachments.map(a => ({ id: a.id?.substring(0,8), state: a.state }))); if (attachments.length === 0) { - console.debug('[ATTACHMENT CONTEXT] No attachments to save'); return; } await this.db.writeTransaction(async (tx) => { diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index 17938ce77..f4f831b41 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -1,14 +1,12 @@ import { AbstractPowerSyncDatabase, DifferentialWatchedQuery, ILogger, Transaction } from '@powersync/common'; import { AttachmentContext } from './AttachmentContext.js'; -import { LocalStorageAdapter } from './LocalStorageAdapter.js'; +import { AttachmentData, LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js'; import { SyncingService } from './SyncingService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; import { AttachmentService } from './AttachmentService.js'; -export type AttachmentData = ArrayBuffer | Blob | string; - /** * AttachmentQueue manages the lifecycle and synchronization of attachments * between local and remote storage. @@ -96,7 +94,6 @@ export class AttachmentQueue { downloadAttachments?: boolean; archivedCacheLimit?: number; }) { - console.debug('AttachmentQueue constructor') this.context = new AttachmentContext(db, tableName, logger ?? db.logger); this.remoteStorage = remoteStorage; this.localStorage = localStorage; @@ -136,7 +133,6 @@ export class AttachmentQueue { * - Handles state transitions for archived and new attachments */ async startSync(): Promise { - console.debug('[QUEUE] AttachmentQueue startSync') if (this.attachmentService.watchActiveAttachments) { await this.stopSync(); // re-create the watch after it was stopped @@ -151,14 +147,12 @@ export class AttachmentQueue { // Sync storage when there is a change in active attachments this.watchActiveAttachments.registerListener({ onDiff: async () => { - console.debug('[QUEUE] watchActiveAttachments: diff detected, syncing storage'); await this.syncStorage(); } }); // Process attachments when there is a change in watched attachments this.watchAttachments(async (watchedAttachments) => { - console.debug('[QUEUE] watchAttachments callback:', watchedAttachments.length, 'items'); // Need to get all the attachments which are tracked in the DB. // We might need to restore an archived attachment. const currentAttachments = await this.context.getAttachments(); @@ -234,7 +228,6 @@ export class AttachmentQueue { } if (attachmentUpdates.length > 0) { - console.debug('[QUEUE] Saving attachments:', attachmentUpdates); await this.context.saveAttachments(attachmentUpdates); } }); @@ -248,7 +241,6 @@ export class AttachmentQueue { */ async syncStorage(): Promise { const activeAttachments = await this.context.getActiveAttachments(); - console.debug('[QUEUE] syncStorage: processing', activeAttachments.length, 'active attachments'); await this.localStorage.initialize(); await this.syncingService.processAttachments(activeAttachments); await this.syncingService.deleteArchivedAttachments(); diff --git a/packages/attachments/src/LocalStorageAdapter.ts b/packages/attachments/src/LocalStorageAdapter.ts index feb9df05d..5f53f1f0a 100644 --- a/packages/attachments/src/LocalStorageAdapter.ts +++ b/packages/attachments/src/LocalStorageAdapter.ts @@ -1,8 +1,11 @@ +export type AttachmentData = ArrayBuffer | string; + export enum EncodingType { UTF8 = 'utf8', Base64 = 'base64' } + /** * LocalStorageAdapter defines the interface for local file storage operations. * Implementations handle file I/O, directory management, and storage initialization. @@ -14,7 +17,7 @@ export interface LocalStorageAdapter { * @param data Data to store (ArrayBuffer, Blob, or string) * @returns Number of bytes written */ - saveFile(filePath: string, data: ArrayBuffer | Blob | string): Promise; + saveFile(filePath: string, data: AttachmentData): Promise; /** * Retrieves file data as an ArrayBuffer. diff --git a/packages/attachments/src/SyncingService.ts b/packages/attachments/src/SyncingService.ts index c77a8c3ae..0b029d480 100644 --- a/packages/attachments/src/SyncingService.ts +++ b/packages/attachments/src/SyncingService.ts @@ -107,22 +107,11 @@ export class SyncingService { * @returns Updated attachment record with local URI and new state */ async downloadAttachment(attachment: AttachmentRecord): Promise { - console.debug('[SYNC] Downloading:', attachment.id); try { - const file = await this.remoteStorage.downloadFile(attachment); - console.debug('[SYNC] Downloaded, converting to base64'); - const base64Data = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - // remove the header from the result: 'data:*/*;base64,' - resolve(reader.result?.toString().replace(/^data:.+;base64,/, '') || ''); - }; - reader.onerror = reject; - reader.readAsDataURL(new File([file], attachment.filename)); - }); + const fileData = await this.remoteStorage.downloadFile(attachment); const localUri = this.localStorage.getLocalUri(attachment.filename); - await this.localStorage.saveFile(localUri, base64Data); + await this.localStorage.saveFile(localUri, fileData); return { ...attachment, diff --git a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts index 1a31c1d02..97625557f 100644 --- a/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/IndexDBFileSystemAdapter.ts @@ -1,4 +1,4 @@ -import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; +import { AttachmentData, EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; /** * IndexDBFileSystemStorageAdapter implements LocalStorageAdapter using IndexedDB. @@ -41,11 +41,28 @@ export class IndexDBFileSystemStorageAdapter implements LocalStorageAdapter { return tx.objectStore('files'); } - async saveFile(filePath: string, data: string): Promise { + async saveFile(filePath: string, data: AttachmentData): Promise { const store = await this.getStore('readwrite'); + + let dataToStore: ArrayBuffer; + let size: number; + + if (typeof data === 'string') { + const binaryString = atob(data); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + dataToStore = bytes.buffer; + size = bytes.byteLength; + } else { + dataToStore = data; + size = dataToStore.byteLength; + } + return await new Promise((resolve, reject) => { - const req = store.put(data, filePath); - req.onsuccess = () => resolve(data.length); + const req = store.put(dataToStore, filePath); + req.onsuccess = () => resolve(size); req.onerror = () => reject(req.error); }); } diff --git a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts index 3a1c96a37..49b87d94d 100644 --- a/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts +++ b/packages/attachments/src/storageAdapters/NodeFileSystemAdapter.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'fs'; import * as path from 'path'; -import { EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; +import { AttachmentData, EncodingType, LocalStorageAdapter } from '../LocalStorageAdapter.js'; /** * NodeFileSystemAdapter implements LocalStorageAdapter using Node.js filesystem. @@ -40,13 +40,17 @@ export class NodeFileSystemAdapter implements LocalStorageAdapter { async saveFile( filePath: string, - data: string, + data: AttachmentData, options?: { encoding?: EncodingType; mediaType?: string } ): Promise { - const buffer = options?.encoding === EncodingType.Base64 ? Buffer.from(data, 'base64') : Buffer.from(data, 'utf8'); - await fs.writeFile(filePath, buffer, { - encoding: options?.encoding - }); + let buffer: Buffer; + + if (typeof data === 'string') { + buffer = Buffer.from(data, options?.encoding ?? EncodingType.Base64); + } else { + buffer = Buffer.from(data); + } + await fs.writeFile(filePath, buffer); return buffer.length; } diff --git a/packages/attachments/tests/attachments.test.ts b/packages/attachments/tests/attachments.test.ts index b9611a52d..3d68dc158 100644 --- a/packages/attachments/tests/attachments.test.ts +++ b/packages/attachments/tests/attachments.test.ts @@ -56,7 +56,6 @@ beforeAll(async () => { }), database: { dbFilename: 'testing.db', - dbLocation: './tests/temp' } }); @@ -101,7 +100,6 @@ const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => vo // Helper to watch the attachments table async function* watchAttachmentsTable(): AsyncGenerator { - console.debug('[TEST] watchAttachmentsTable: watching attachments table'); const watcher = db.watch( ` SELECT @@ -113,10 +111,7 @@ async function* watchAttachmentsTable(): AsyncGenerator { ); for await (const result of watcher) { - console.debug('[TEST] watchAttachmentsTable: result', result); const attachments = result.rows?._array.map((r: any) => attachmentFromSql(r)) ?? []; - console.debug('[TEST] watchAttachmentsTable: attachments', attachments); - console.debug('[TEST] Mapped attachments:', attachments.map(a => ({ id: a.id?.substring(0,8), state: a.state, hasSynced: a.hasSynced }))); yield attachments; } } @@ -126,7 +121,6 @@ async function waitForMatchCondition( predicate: (attachments: AttachmentRecord[]) => boolean, timeoutSeconds: number = 5 ): Promise { - console.debug('[TEST] waitForMatchCondition: waiting for condition'); const timeoutMs = timeoutSeconds * 1000; const abortController = new AbortController(); const startTime = Date.now(); @@ -135,21 +129,16 @@ async function waitForMatchCondition( try { for await (const value of generator) { - console.debug('[TEST] waitForMatchCondition: generator value', value); if (Date.now() - startTime > timeoutMs) { - console.debug('[TEST] waitForMatchCondition: timeout'); throw new Error(`Timeout waiting for condition after ${timeoutSeconds}s`); } if (predicate(value)) { - console.debug('[TEST] waitForMatchCondition: match found!'); return value; } } - console.debug('[TEST] waitForMatchCondition: for await loop ended without match'); throw new Error('Stream ended without match'); } finally { - console.debug('[TEST] waitForMatchCondition: finally finaling'); await generator.return?.(undefined); abortController.abort(); } @@ -188,7 +177,6 @@ describe('attachment queue', () => { const attachments = await waitForMatchCondition( () => watchAttachmentsTable(), (results) => { - console.debug('[TEST] Predicate checking:', results.map(r => ({ id: r.id?.substring(0,8), state: r.state }))); return results.some((r) => r.state === AttachmentState.SYNCED) }, 5 @@ -204,7 +192,7 @@ describe('attachment queue', () => { // Verify local file exists const localData = await queue.localStorage.readFile(attachmentRecord.localUri!); - expect(localData).toEqual(MOCK_JPEG_U8A); + expect(Array.from(new Uint8Array(localData))).toEqual(MOCK_JPEG_U8A); await queue.stopSync(); }); From fae7f2755e69c0740dafdfae04e0aeb8b8fa5e16 Mon Sep 17 00:00:00 2001 From: Amine Date: Wed, 5 Nov 2025 09:02:59 +0800 Subject: [PATCH 16/19] test: workflow tests working and passing - Added `getAttachment` method to retrieve an attachment by ID in AttachmentContext. - Updated `upsertAttachment` to handle null values for optional fields. - Introduced `generateAttachmentId` method in AttachmentQueue for generating unique IDs. - Modified `watchActiveAttachments` to accept a throttle parameter. - Added `deleteFile` method to have attachment deletion. - Updated tests to cover new functionality and ensure reliability. --- packages/attachments/src/AttachmentContext.ts | 28 +- packages/attachments/src/AttachmentQueue.ts | 81 ++-- packages/attachments/src/AttachmentService.ts | 6 +- .../attachments/tests/attachments.test.ts | 368 ++++++++++++++---- 4 files changed, 370 insertions(+), 113 deletions(-) diff --git a/packages/attachments/src/AttachmentContext.ts b/packages/attachments/src/AttachmentContext.ts index 7f43709d5..45e368538 100644 --- a/packages/attachments/src/AttachmentContext.ts +++ b/packages/attachments/src/AttachmentContext.ts @@ -119,7 +119,7 @@ export class AttachmentContext { */ async upsertAttachment(attachment: AttachmentRecord, context: Transaction): Promise { try { - const result = await context.execute( + await context.execute( /* sql */ ` INSERT @@ -140,13 +140,13 @@ export class AttachmentContext { [ attachment.id, attachment.filename, - attachment.localUri || 'dummy', - attachment.size || 1, - attachment.mediaType || 'dummy', - attachment.timestamp || Date.now(), + attachment.localUri || null, + attachment.size || null, + attachment.mediaType || null, + attachment.timestamp, attachment.state, attachment.hasSynced ? 1 : 0, - attachment.metaData || 'dummy' + attachment.metaData || null ] ); } catch (error) { @@ -154,6 +154,20 @@ export class AttachmentContext { } } + async getAttachment(id: string): Promise { + const attachment = await this.db.get( + /* sql */ + ` + SELECT * FROM ${this.tableName} + WHERE + id = ? + `, + [id] + ); + + return attachment ? attachmentFromSql(attachment) : undefined; + } + /** * Permanently deletes an attachment record from the database. * @@ -191,7 +205,7 @@ export class AttachmentContext { } await this.db.writeTransaction(async (tx) => { for (const attachment of attachments) { - this.upsertAttachment(attachment, tx); + await this.upsertAttachment(attachment, tx); } }); } diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index f4f831b41..0baf198b0 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, DifferentialWatchedQuery, ILogger, Transaction } from '@powersync/common'; +import { AbstractPowerSyncDatabase, DEFAULT_WATCH_THROTTLE_MS, DifferentialWatchedQuery, ILogger, Transaction } from '@powersync/common'; import { AttachmentContext } from './AttachmentContext.js'; import { AttachmentData, LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; @@ -79,14 +79,14 @@ export class AttachmentQueue { logger, tableName = ATTACHMENT_TABLE, syncIntervalMs = 30 * 1000, - syncThrottleDuration = 1000, + syncThrottleDuration = DEFAULT_WATCH_THROTTLE_MS, downloadAttachments = true, archivedCacheLimit = 100 }: { db: AbstractPowerSyncDatabase; remoteStorage: RemoteStorageAdapter; localStorage: LocalStorageAdapter; - watchAttachments: (onUpdate: (attachement: WatchedAttachmentItem[]) => Promise) => void; + watchAttachments: (onUpdate: (attachment: WatchedAttachmentItem[]) => Promise) => void; tableName?: string; logger?: ILogger; syncIntervalMs?: number; @@ -101,9 +101,9 @@ export class AttachmentQueue { this.tableName = tableName; this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger); this.attachmentService = new AttachmentService(tableName, db); - this.watchActiveAttachments = this.attachmentService.watchActiveAttachments(); this.syncIntervalMs = syncIntervalMs; this.syncThrottleDuration = syncThrottleDuration; + this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({ throttleMs: this.syncThrottleDuration }); this.downloadAttachments = downloadAttachments; this.archivedCacheLimit = archivedCacheLimit; } @@ -118,10 +118,19 @@ export class AttachmentQueue { * @param onUpdate - Callback to invoke when attachment references change * @throws Error indicating this method must be implemented by the user */ - watchAttachments(onUpdate: (attachement: WatchedAttachmentItem[]) => Promise): void { + watchAttachments(onUpdate: (attachment: WatchedAttachmentItem[]) => Promise): void { throw new Error('watchAttachments should be implemented by the user of AttachmentQueue'); } + /** + * Generates a new attachment ID using a SQLite UUID function. + * + * @returns Promise resolving to the new attachment ID + */ + async generateAttachmentId(): Promise { + return (await this.context.db.get<{ id: string }>('SELECT uuid() as id')).id; + } + /** * Starts the attachment synchronization process. * @@ -136,9 +145,14 @@ export class AttachmentQueue { if (this.attachmentService.watchActiveAttachments) { await this.stopSync(); // re-create the watch after it was stopped - this.watchActiveAttachments = this.attachmentService.watchActiveAttachments(); + this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({ throttleMs: this.syncThrottleDuration }); } + // immediately invoke the sync storage to initialize local storage + await this.localStorage.initialize(); + + await this.verifyAttachments(); + // Sync storage periodically this.periodicSyncTimer = setInterval(async () => { await this.syncStorage(); @@ -162,7 +176,6 @@ export class AttachmentQueue { const existingQueueItem = currentAttachments.find((a) => a.id === watchedAttachment.id); if (!existingQueueItem) { // Item is watched but not in the queue yet. Need to add it. - if (!this.downloadAttachments) { continue; } @@ -284,9 +297,9 @@ export class AttachmentQueue { mediaType?: string; metaData?: string; id?: string; - updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => void; + updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise; }): Promise { - const resolvedId = id ?? (await this.context.db.get<{ id: string }>('SELECT uuid() as id')).id; + const resolvedId = id ?? await this.generateAttachmentId(); const filename = `${resolvedId}.${fileExtension}`; const localUri = this.localStorage.getLocalUri(filename); const size = await this.localStorage.saveFile(localUri, data); @@ -304,13 +317,32 @@ export class AttachmentQueue { }; await this.context.db.writeTransaction(async (tx) => { - updateHook?.(tx, attachment); - this.context.upsertAttachment(attachment, tx); + await updateHook?.(tx, attachment); + await this.context.upsertAttachment(attachment, tx); }); return attachment; } + async deleteFile({ id, updateHook }: { + id: string, + updateHook?: (transaction: Transaction, attachment: AttachmentRecord) => Promise + }): Promise { + const attachment = await this.context.getAttachment(id); + if (!attachment) { + throw new Error(`Attachment with id ${id} not found`); + } + + await this.context.db.writeTransaction(async (tx) => { + await updateHook?.(tx, attachment); + await this.context.upsertAttachment({ + ...attachment, + state: AttachmentState.QUEUED_DELETE, + hasSynced: false, + }, tx); + }); + } + /** * Verifies the integrity of all attachment records and repairs inconsistencies. * @@ -322,12 +354,12 @@ export class AttachmentQueue { verifyAttachments = async (): Promise => { const attachments = await this.context.getAttachments(); const updates: AttachmentRecord[] = []; - + for (const attachment of attachments) { if (attachment.localUri == null) { continue; } - + const exists = await this.localStorage.fileExists(attachment.localUri); if (exists) { // The file exists, this is correct @@ -342,19 +374,16 @@ export class AttachmentQueue { ...attachment, localUri: newLocalUri }); - } else if (attachment.state === AttachmentState.QUEUED_UPLOAD || attachment.state === AttachmentState.ARCHIVED) { - // The file must have been removed from the local storage before upload was completed - updates.push({ - ...attachment, - state: AttachmentState.ARCHIVED, - localUri: undefined // Clears the value - }); - } else if (attachment.state === AttachmentState.SYNCED) { - // The file was downloaded, but removed - trigger redownload - updates.push({ - ...attachment, - state: AttachmentState.QUEUED_DOWNLOAD - }); + } else { + // no new exists + if (attachment.state === AttachmentState.QUEUED_UPLOAD || attachment.state === AttachmentState.SYNCED) { + // The file must have been removed from the local storage before upload was completed + updates.push({ + ...attachment, + state: AttachmentState.ARCHIVED, + localUri: undefined // Clears the value + }); + } } } diff --git a/packages/attachments/src/AttachmentService.ts b/packages/attachments/src/AttachmentService.ts index 50a6b8983..37e83b97b 100644 --- a/packages/attachments/src/AttachmentService.ts +++ b/packages/attachments/src/AttachmentService.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, DifferentialWatchedQuery } from '@powersync/common'; +import { AbstractPowerSyncDatabase, DEFAULT_WATCH_THROTTLE_MS, DifferentialWatchedQuery } from '@powersync/common'; import { AttachmentRecord, AttachmentState } from './Schema.js'; /** @@ -14,7 +14,7 @@ export class AttachmentService { * Creates a differential watch query for active attachments requiring synchronization. * @returns Watch query that emits changes for queued uploads, downloads, and deletes */ - watchActiveAttachments(): DifferentialWatchedQuery { + watchActiveAttachments({ throttleMs }: { throttleMs?: number } = {}): DifferentialWatchedQuery { const watch = this.db .query({ sql: /* sql */ ` @@ -31,7 +31,7 @@ export class AttachmentService { `, parameters: [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE] }) - .differentialWatch(); + .differentialWatch({ throttleMs }); return watch; } diff --git a/packages/attachments/tests/attachments.test.ts b/packages/attachments/tests/attachments.test.ts index 3d68dc158..bcf3744f4 100644 --- a/packages/attachments/tests/attachments.test.ts +++ b/packages/attachments/tests/attachments.test.ts @@ -43,38 +43,19 @@ vi.mock('fs', () => { const mockLocalStorage = new NodeFileSystemAdapter('./temp/attachments'); let db: AbstractPowerSyncDatabase; - -beforeAll(async () => { - db = new PowerSyncDatabase({ - schema: new Schema({ - users: new Table({ - name: column.text, - email: column.text, - photo_id: column.text - }), - attachments: new AttachmentTable() - }), - database: { - dbFilename: 'testing.db', - } - }); - +let queue: AttachmentQueue; +const schema = new Schema({ + users: new Table({ + name: column.text, + email: column.text, + photo_id: column.text + }), + attachments: new AttachmentTable() }); -beforeEach(() => { - // reset the state of in-memory fs - vol.reset() - // Reset mock call history - mockUploadFile.mockClear(); - mockDownloadFile.mockClear(); - mockDeleteFile.mockClear(); -}) +const INTERVAL_MILLISECONDS = 1000; -afterEach(async () => { - await db.disconnectAndClear(); -}); - -const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => void) => { +const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => Promise) => { db.watch( /* sql */ ` @@ -87,8 +68,8 @@ const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => vo `, [], { - onResult: (result: any) => - onUpdate( + onResult: async (result: any) => + await onUpdate( result.rows?._array.map((r: any) => ({ id: r.photo_id, fileExtension: 'jpg' @@ -98,6 +79,34 @@ const watchAttachments = (onUpdate: (attachments: WatchedAttachmentItem[]) => vo ); }; +beforeEach(() => { + db = new PowerSyncDatabase({ + schema, + database: { + dbFilename: 'testing.db', + } + }); + // reset the state of in-memory fs + vol.reset() + // Reset mock call history + mockUploadFile.mockClear(); + mockDownloadFile.mockClear(); + mockDeleteFile.mockClear(); + queue = new AttachmentQueue({ + db: db, + watchAttachments, + remoteStorage: mockRemoteStorage, + localStorage: mockLocalStorage, + syncIntervalMs: INTERVAL_MILLISECONDS, + }); +}) + +afterEach(async () => { + await queue.stopSync(); + await db.disconnectAndClear(); + await db.close(); +}); + // Helper to watch the attachments table async function* watchAttachmentsTable(): AsyncGenerator { const watcher = db.watch( @@ -107,7 +116,6 @@ async function* watchAttachmentsTable(): AsyncGenerator { FROM attachments; `, - // [AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE], ); for await (const result of watcher) { @@ -145,16 +153,30 @@ async function waitForMatchCondition( } describe('attachment queue', () => { + it('should use the correct relative path for the local file', async () => { + await queue.startSync(); + const id = await queue.generateAttachmentId(); + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)', + ['steven', 'steven@journeyapps.com', id], + ); + + // wait for the file to be synced + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.id === id && r.state === AttachmentState.SYNCED), + 5 + ); + const expectedLocalUri = await queue.localStorage.getLocalUri(`${id}.jpg`); + + expect(await mockLocalStorage.fileExists(expectedLocalUri)).toBe(true); + + await queue.stopSync(); + }) + it('should download attachments when a new record with an attachment is added', { timeout: 10000 // 10 seconds }, async () => { - const queue = new AttachmentQueue({ - db: db, - watchAttachments, - remoteStorage: mockRemoteStorage, - localStorage: mockLocalStorage, - }); - await queue.startSync(); @@ -196,42 +218,234 @@ describe('attachment queue', () => { await queue.stopSync(); }); -}); -// async function waitForMatch( -// iteratorGenerator: () => AsyncGenerator, -// predicate: (attachments: AttachmentRecord[]) => boolean, -// timeoutSeconds: number = 5 -// ): Promise { -// const timeoutMs = timeoutSeconds * 1000; -// const abortController = new AbortController(); - -// const matchPromise = (async () => { -// const asyncIterable = iteratorGenerator(); -// try { -// for await (const value of asyncIterable) { -// if (abortController.signal.aborted) { -// throw new Error('Timeout'); -// } -// if (predicate(value)) { -// return value; -// } -// } -// throw new Error('Stream ended without match'); -// } finally { -// const iterator = asyncIterable[Symbol.asyncIterator](); -// if (iterator.return) { -// await iterator.return(); -// } -// } -// })(); - -// const timeoutPromise = new Promise((_, reject) => -// setTimeout(() => { -// abortController.abort(); -// reject(new Error('Timeout')); -// }, timeoutMs) -// ); - -// return Promise.race([matchPromise, timeoutPromise]); -// } \ No newline at end of file + it('should upload attachments when a new file is saved', { + timeout: 10000 + }, async () => { + await queue.startSync(); + + // Create mock file data (123 bytes) + const mockFileData = new Uint8Array(123).fill(42); // Fill with some data + + // Save file with updateHook to link to user + const record = await queue.saveFile({ + data: mockFileData.buffer, + fileExtension: 'jpg', + mediaType: 'image/jpeg', + updateHook: async (tx, attachment) => { + await tx.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)', + ['testuser', 'testuser@journeyapps.com', attachment.id] + ); + } + }); + + expect(record.size).toBe(123); + expect(record.state).toBe(AttachmentState.QUEUED_UPLOAD); + + // Wait for attachment to be uploaded and synced + const attachments = await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.id === record.id && r.state === AttachmentState.SYNCED), + 5 + ); + + const attachmentRecord = attachments.find((r) => r.id === record.id); + expect(attachmentRecord?.state).toBe(AttachmentState.SYNCED); + + // Verify upload was called + expect(mockUploadFile).toHaveBeenCalled(); + const uploadCall = mockUploadFile.mock.calls[0]; + expect(uploadCall[1].id).toBe(record.id); + + // Verify local file exists + expect(await mockLocalStorage.fileExists(record.localUri!)).toBe(true); + + // Clear the user's photo_id to archive the attachment + await db.execute('UPDATE users SET photo_id = NULL'); + + + // Wait for attachment to be archived + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.id === record.id && r.state === AttachmentState.ARCHIVED), + 5 + ) + + // Wait for attachment to be deleted (not just archived) + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => !results.some(r => r.id === record.id), + 5 + ); + + // await queue.syncStorage(); // <-- explicitly delete + + // File should be deleted too + expect(await mockLocalStorage.fileExists(record.localUri!)).toBe(false); + + await queue.stopSync(); + }); + + it('should delete attachments', async () => { + await queue.startSync(); + + const id = await queue.generateAttachmentId(); + + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)', + ['steven', 'steven@journeyapps.com', id], + ); + + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.id === id && r.state === AttachmentState.SYNCED), + 5 + ); + + await queue.deleteFile({ + id, + updateHook: async (tx, attachment) => { + await tx.execute( + 'UPDATE users SET photo_id = NULL WHERE photo_id = ?', + [attachment.id], + ); + } + }); + + const toBeDeletedAttachments = await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.id === id && r.state === AttachmentState.QUEUED_DELETE), + 5 + ); + + expect(toBeDeletedAttachments.length).toBe(1); + expect(toBeDeletedAttachments[0].id).toBe(id); + expect(toBeDeletedAttachments[0].state).toBe(AttachmentState.QUEUED_DELETE); + expect(toBeDeletedAttachments[0].hasSynced).toBe(false); + + // wait for the file to be deleted + await new Promise(resolve => setTimeout(resolve, 1500)); + + expect(await mockLocalStorage.fileExists(toBeDeletedAttachments[0].localUri!)).toBe(false); + + await queue.stopSync(); + }) + + it('should recover from deleted local file', async () => { + // create an attachment record that has an invalid localUri + await db.execute( + ` + INSERT + OR REPLACE INTO attachments ( + id, + timestamp, + filename, + local_uri, + size, + media_type, + has_synced, + state + ) + VALUES + (uuid(), current_timestamp, ?, ?, ?, ?, ?, ?)`, + [ + 'test.jpg', + 'invalid/dir/test.jpg', + 100, + 'image/jpeg', + 1, + AttachmentState.SYNCED + ], + ); + + await queue.startSync(); + + const attachmentRecord = await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.state === AttachmentState.ARCHIVED), + 5 + ); + + expect(attachmentRecord[0].filename).toBe('test.jpg'); + // it seems that the localUri is not set to null + expect(attachmentRecord[0].localUri).toBe(null); + expect(attachmentRecord[0].state).toBe(AttachmentState.ARCHIVED); + + await queue.stopSync(); + }); + + it('should cache downloaded attachments', async () => { + await queue.startSync(); + + const id = await queue.generateAttachmentId(); + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)', + ['testuser', 'testuser@journeyapps.com', id], + ); + + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.state === AttachmentState.SYNCED), + 5 + ); + + expect(mockDownloadFile).toHaveBeenCalled(); + expect(mockDownloadFile).toHaveBeenCalledWith({ + filename: `${id}.jpg`, + hasSynced: false, + id: id, + localUri: null, + mediaType: null, + metaData: null, + size: null, + state: AttachmentState.QUEUED_DOWNLOAD, + timestamp: null, + }); + + // Archive attachment by not referencing it anymore. + await db.execute('UPDATE users SET photo_id = NULL'); + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.state === AttachmentState.ARCHIVED), + 5 + ); + + // Restore from cache + await db.execute('UPDATE users SET photo_id = ?', [id]); + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.state === AttachmentState.SYNCED), + 5 + ); + + const localUri = await queue.localStorage.getLocalUri(`${id}.jpg`); + expect(await mockLocalStorage.fileExists(localUri)).toBe(true); + + // Verify the download was not called again + expect(mockDownloadFile).toHaveBeenCalledExactlyOnceWith({ + filename: `${id}.jpg`, + hasSynced: false, + id: id, + localUri: null, + mediaType: null, + metaData: null, + size: null, + state: AttachmentState.QUEUED_DOWNLOAD, + timestamp: null, + }); + + await queue.stopSync(); + }) + + it.todo('should skip failed download and retry it in the next sync', async () => { + // no error handling yet expose error handling + const localeQueue = new AttachmentQueue({ + db: db, + watchAttachments, + remoteStorage: mockRemoteStorage, + localStorage: mockLocalStorage, + syncIntervalMs: INTERVAL_MILLISECONDS, + }); + }) +}); \ No newline at end of file From eedd22425317bffebb688c204ccb399c2a74c337 Mon Sep 17 00:00:00 2001 From: Amine Date: Wed, 5 Nov 2025 10:14:42 +0800 Subject: [PATCH 17/19] feat: introduce AttachmentErrorHandler for custom error handling in attachment sync operations - Added AttachmentErrorHandler interface to manage download, upload, and delete errors for attachments. - Updated AttachmentQueue and SyncingService to utilize the new error handler. - Enhanced tests to verify error handling behavior during attachment sync processes. --- ...orHandler.ts => AttachmentErrorHandler.ts} | 2 +- packages/attachments/src/AttachmentQueue.ts | 7 +++- packages/attachments/src/AttachmentService.ts | 2 +- packages/attachments/src/SyncingService.ts | 6 +-- .../attachments/tests/attachments.test.ts | 39 ++++++++++++++++++- 5 files changed, 48 insertions(+), 8 deletions(-) rename packages/attachments/src/{SyncErrorHandler.ts => AttachmentErrorHandler.ts} (96%) diff --git a/packages/attachments/src/SyncErrorHandler.ts b/packages/attachments/src/AttachmentErrorHandler.ts similarity index 96% rename from packages/attachments/src/SyncErrorHandler.ts rename to packages/attachments/src/AttachmentErrorHandler.ts index cbe49f245..6a673c347 100644 --- a/packages/attachments/src/SyncErrorHandler.ts +++ b/packages/attachments/src/AttachmentErrorHandler.ts @@ -4,7 +4,7 @@ import { AttachmentRecord } from './Schema.js'; * SyncErrorHandler provides custom error handling for attachment sync operations. * Implementations determine whether failed operations should be retried or archived. */ -export interface SyncErrorHandler { +export interface AttachmentErrorHandler { /** * Handles a download error for a specific attachment. * @param attachment The attachment that failed to download diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index 0baf198b0..6b5636265 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -6,6 +6,7 @@ import { ATTACHMENT_TABLE, AttachmentRecord, AttachmentState } from './Schema.js import { SyncingService } from './SyncingService.js'; import { WatchedAttachmentItem } from './WatchedAttachmentItem.js'; import { AttachmentService } from './AttachmentService.js'; +import { AttachmentErrorHandler } from './AttachmentErrorHandler.js'; /** * AttachmentQueue manages the lifecycle and synchronization of attachments @@ -81,7 +82,8 @@ export class AttachmentQueue { syncIntervalMs = 30 * 1000, syncThrottleDuration = DEFAULT_WATCH_THROTTLE_MS, downloadAttachments = true, - archivedCacheLimit = 100 + archivedCacheLimit = 100, + errorHandler }: { db: AbstractPowerSyncDatabase; remoteStorage: RemoteStorageAdapter; @@ -93,13 +95,14 @@ export class AttachmentQueue { syncThrottleDuration?: number; downloadAttachments?: boolean; archivedCacheLimit?: number; + errorHandler?: AttachmentErrorHandler; }) { this.context = new AttachmentContext(db, tableName, logger ?? db.logger); this.remoteStorage = remoteStorage; this.localStorage = localStorage; this.watchAttachments = watchAttachments; this.tableName = tableName; - this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger); + this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger, errorHandler); this.attachmentService = new AttachmentService(tableName, db); this.syncIntervalMs = syncIntervalMs; this.syncThrottleDuration = syncThrottleDuration; diff --git a/packages/attachments/src/AttachmentService.ts b/packages/attachments/src/AttachmentService.ts index 37e83b97b..0e6d71504 100644 --- a/packages/attachments/src/AttachmentService.ts +++ b/packages/attachments/src/AttachmentService.ts @@ -1,4 +1,4 @@ -import { AbstractPowerSyncDatabase, DEFAULT_WATCH_THROTTLE_MS, DifferentialWatchedQuery } from '@powersync/common'; +import { AbstractPowerSyncDatabase, DifferentialWatchedQuery } from '@powersync/common'; import { AttachmentRecord, AttachmentState } from './Schema.js'; /** diff --git a/packages/attachments/src/SyncingService.ts b/packages/attachments/src/SyncingService.ts index 0b029d480..73327dd0b 100644 --- a/packages/attachments/src/SyncingService.ts +++ b/packages/attachments/src/SyncingService.ts @@ -3,7 +3,7 @@ import { AttachmentContext } from './AttachmentContext.js'; import { LocalStorageAdapter } from './LocalStorageAdapter.js'; import { RemoteStorageAdapter } from './RemoteStorageAdapter.js'; import { AttachmentRecord, AttachmentState } from './Schema.js'; -import { SyncErrorHandler } from './SyncErrorHandler.js'; +import { AttachmentErrorHandler } from './AttachmentErrorHandler.js'; /** * Orchestrates attachment synchronization between local and remote storage. @@ -14,14 +14,14 @@ export class SyncingService { localStorage: LocalStorageAdapter; remoteStorage: RemoteStorageAdapter; logger: ILogger; - errorHandler?: SyncErrorHandler; + errorHandler?: AttachmentErrorHandler; constructor( context: AttachmentContext, localStorage: LocalStorageAdapter, remoteStorage: RemoteStorageAdapter, logger: ILogger, - errorHandler?: SyncErrorHandler + errorHandler?: AttachmentErrorHandler ) { this.context = context; this.localStorage = localStorage; diff --git a/packages/attachments/tests/attachments.test.ts b/packages/attachments/tests/attachments.test.ts index bcf3744f4..2ef91e623 100644 --- a/packages/attachments/tests/attachments.test.ts +++ b/packages/attachments/tests/attachments.test.ts @@ -7,6 +7,7 @@ import { attachmentFromSql, AttachmentRecord, AttachmentState, AttachmentTable } import { RemoteStorageAdapter } from '../src/RemoteStorageAdapter.js'; import { WatchedAttachmentItem } from '../src/WatchedAttachmentItem.js'; import { NodeFileSystemAdapter } from '../src/storageAdapters/NodeFileSystemAdapter.js'; +import { AttachmentErrorHandler } from '../src/AttachmentErrorHandler.js'; const MOCK_JPEG_U8A = [ 0xFF, 0xD8, 0xFF, 0xE0, @@ -438,7 +439,23 @@ describe('attachment queue', () => { await queue.stopSync(); }) - it.todo('should skip failed download and retry it in the next sync', async () => { + it('should skip failed download and retry it in the next sync', async () => { + const mockErrorHandler = vi.fn().mockRejectedValue(false); + const errorHandler: AttachmentErrorHandler = { + onDeleteError: mockErrorHandler, + onDownloadError: mockErrorHandler, + onUploadError: mockErrorHandler, + } + const mockDownloadFile = vi.fn() + .mockRejectedValueOnce(new Error('Download failed')) + .mockResolvedValueOnce(createMockJpegBuffer()); + + const mockRemoteStorage: RemoteStorageAdapter = { + downloadFile: mockDownloadFile, + uploadFile: mockUploadFile, + deleteFile: mockDeleteFile + }; + // no error handling yet expose error handling const localeQueue = new AttachmentQueue({ db: db, @@ -446,6 +463,26 @@ describe('attachment queue', () => { remoteStorage: mockRemoteStorage, localStorage: mockLocalStorage, syncIntervalMs: INTERVAL_MILLISECONDS, + errorHandler, }); + + const id = await localeQueue.generateAttachmentId(); + + await localeQueue.startSync() + + await db.execute( + 'INSERT INTO users (id, name, email, photo_id) VALUES (uuid(), ?, ?, ?)', + ['testuser', 'testuser@journeyapps.com', id], + ); + + await waitForMatchCondition( + () => watchAttachmentsTable(), + (results) => results.some((r) => r.id === id && r.state === AttachmentState.SYNCED), + 5 + ); + + expect(mockErrorHandler).toHaveBeenCalledOnce(); + expect(mockDownloadFile).toHaveBeenCalledTimes(2); + }) }); \ No newline at end of file From ec1a7335811c04a3b593c443e4837e3af9f233ac Mon Sep 17 00:00:00 2001 From: Amine Date: Thu, 6 Nov 2025 13:40:03 +0800 Subject: [PATCH 18/19] feat: added archival management to AttachmentQueue via AttachmentContext --- packages/attachments/src/AttachmentContext.ts | 53 ++++++++++++++++++- packages/attachments/src/AttachmentQueue.ts | 38 +++++++++---- packages/attachments/src/AttachmentService.ts | 7 ++- packages/attachments/src/SyncingService.ts | 21 ++++---- .../attachments/tests/attachments.test.ts | 3 +- 5 files changed, 99 insertions(+), 23 deletions(-) diff --git a/packages/attachments/src/AttachmentContext.ts b/packages/attachments/src/AttachmentContext.ts index 45e368538..bb81e413f 100644 --- a/packages/attachments/src/AttachmentContext.ts +++ b/packages/attachments/src/AttachmentContext.ts @@ -17,6 +17,9 @@ export class AttachmentContext { /** Logger instance for diagnostic information */ logger: ILogger; + /** Maximum number of archived attachments to keep before cleanup */ + archivedCacheLimit: number = 100; + /** * Creates a new AttachmentContext instance. * @@ -24,10 +27,11 @@ export class AttachmentContext { * @param tableName - Name of the table storing attachment records. Default: 'attachments' * @param logger - Logger instance for diagnostic output */ - constructor(db: AbstractPowerSyncDatabase, tableName: string = 'attachments', logger: ILogger) { + constructor(db: AbstractPowerSyncDatabase, tableName: string = 'attachments', logger: ILogger, archivedCacheLimit: number) { this.db = db; this.tableName = tableName; this.logger = logger; + this.archivedCacheLimit = archivedCacheLimit; } /** @@ -191,6 +195,53 @@ export class AttachmentContext { ); } + async clearQueue(): Promise { + await this.db.writeTransaction((tx) => + tx.execute( + /* sql */ + ` + DELETE FROM ${this.tableName} + ` + ) + ); + } + + async deleteArchivedAttachments(callback?: (attachments: AttachmentRecord[]) => Promise): Promise { + const limit = 1000; + + const results = await this.db.getAll( + /* sql */ + ` + SELECT * FROM ${this.tableName} WHERE state = ? ORDER BY timestamp DESC LIMIT ? OFFSET ? + `, + [ + AttachmentState.ARCHIVED, + limit, + this.archivedCacheLimit, + ], + ); + + const archivedAttachments = results.map(attachmentFromSql); + if (archivedAttachments.length === 0) return false; + + this.logger.info(`Deleting ${archivedAttachments.length} archived attachments. Archived attachment exceeds cache archiveCacheLimit of ${this.archivedCacheLimit}.`); + + await callback?.(archivedAttachments); + + const ids = archivedAttachments.map(attachment => attachment.id); + + await this.db.executeBatch( + /* sql */ + ` + DELETE FROM ${this.tableName} WHERE id IN (?) + `, + [ids] + ); + + this.logger.info(`Deleted ${archivedAttachments.length} archived attachments`); + return archivedAttachments.length < limit; + } + /** * Saves multiple attachment records in a single transaction. * diff --git a/packages/attachments/src/AttachmentQueue.ts b/packages/attachments/src/AttachmentQueue.ts index 6b5636265..33e2716b5 100644 --- a/packages/attachments/src/AttachmentQueue.ts +++ b/packages/attachments/src/AttachmentQueue.ts @@ -97,18 +97,18 @@ export class AttachmentQueue { archivedCacheLimit?: number; errorHandler?: AttachmentErrorHandler; }) { - this.context = new AttachmentContext(db, tableName, logger ?? db.logger); this.remoteStorage = remoteStorage; this.localStorage = localStorage; this.watchAttachments = watchAttachments; this.tableName = tableName; - this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger, errorHandler); - this.attachmentService = new AttachmentService(tableName, db); this.syncIntervalMs = syncIntervalMs; this.syncThrottleDuration = syncThrottleDuration; - this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({ throttleMs: this.syncThrottleDuration }); - this.downloadAttachments = downloadAttachments; this.archivedCacheLimit = archivedCacheLimit; + this.downloadAttachments = downloadAttachments; + this.context = new AttachmentContext(db, tableName, logger ?? db.logger, archivedCacheLimit); + this.attachmentService = new AttachmentService(db, logger ?? db.logger, tableName); + this.syncingService = new SyncingService(this.context, localStorage, remoteStorage, logger ?? db.logger, errorHandler); + this.watchActiveAttachments = this.attachmentService.watchActiveAttachments({ throttleMs: this.syncThrottleDuration }); } /** @@ -346,6 +346,18 @@ export class AttachmentQueue { }); } + async expireCache(): Promise { + let isDone = false; + while (!isDone) { + isDone = await this.syncingService.deleteArchivedAttachments(); + } + } + + async clearQueue(): Promise { + await this.context.clearQueue(); + await this.localStorage.clear(); + } + /** * Verifies the integrity of all attachment records and repairs inconsistencies. * @@ -372,15 +384,23 @@ export class AttachmentQueue { const newLocalUri = this.localStorage.getLocalUri(attachment.filename); const newExists = await this.localStorage.fileExists(newLocalUri); if (newExists) { - // The file exists but the localUri is broken, lets update it. + // The file exists locally but the localUri is broken, we update it. updates.push({ ...attachment, localUri: newLocalUri }); } else { - // no new exists - if (attachment.state === AttachmentState.QUEUED_UPLOAD || attachment.state === AttachmentState.SYNCED) { - // The file must have been removed from the local storage before upload was completed + // the file doesn't exist locally. + if (attachment.state === AttachmentState.SYNCED) { + // the file has been successfully synced to remote storage but is missing + // we download it again + updates.push({ + ...attachment, + state: AttachmentState.QUEUED_DOWNLOAD, + localUri: undefined + }); + } else { + // the file wasn't successfully synced to remote storage, we archive it updates.push({ ...attachment, state: AttachmentState.ARCHIVED, diff --git a/packages/attachments/src/AttachmentService.ts b/packages/attachments/src/AttachmentService.ts index 0e6d71504..75220428b 100644 --- a/packages/attachments/src/AttachmentService.ts +++ b/packages/attachments/src/AttachmentService.ts @@ -1,13 +1,15 @@ -import { AbstractPowerSyncDatabase, DifferentialWatchedQuery } from '@powersync/common'; +import { AbstractPowerSyncDatabase, DifferentialWatchedQuery, ILogger } from '@powersync/common'; import { AttachmentRecord, AttachmentState } from './Schema.js'; +import { AttachmentContext } from './AttachmentContext.js'; /** * Service for querying and watching attachment records in the database. */ export class AttachmentService { constructor( + private db: AbstractPowerSyncDatabase, + private logger: ILogger, private tableName: string = 'attachments', - private db: AbstractPowerSyncDatabase ) {} /** @@ -15,6 +17,7 @@ export class AttachmentService { * @returns Watch query that emits changes for queued uploads, downloads, and deletes */ watchActiveAttachments({ throttleMs }: { throttleMs?: number } = {}): DifferentialWatchedQuery { + this.logger.info('Watching active attachments...'); const watch = this.db .query({ sql: /* sql */ ` diff --git a/packages/attachments/src/SyncingService.ts b/packages/attachments/src/SyncingService.ts index 73327dd0b..79004384a 100644 --- a/packages/attachments/src/SyncingService.ts +++ b/packages/attachments/src/SyncingService.ts @@ -170,17 +170,18 @@ export class SyncingService { * Performs cleanup of archived attachments by removing their local files and records. * Errors during local file deletion are logged but do not prevent record deletion. */ - async deleteArchivedAttachments(): Promise { - const archivedAttachments = await this.context.getArchivedAttachments(); - for (const attachment of archivedAttachments) { - if (attachment.localUri) { - try { - await this.localStorage.deleteFile(attachment.localUri); - } catch (error) { - this.logger.error('Error deleting local file for archived attachment', error); + async deleteArchivedAttachments(): Promise { + return await this.context.deleteArchivedAttachments(async (archivedAttachments) => { + for (const attachment of archivedAttachments) { + if (attachment.localUri) { + try { + await this.localStorage.deleteFile(attachment.localUri); + } catch (error) { + this.logger.error('Error deleting local file for archived attachment', error); + } } + await this.context.deleteAttachment(attachment.id); } - await this.context.deleteAttachment(attachment.id); - } + }); } } diff --git a/packages/attachments/tests/attachments.test.ts b/packages/attachments/tests/attachments.test.ts index 2ef91e623..443fe9a51 100644 --- a/packages/attachments/tests/attachments.test.ts +++ b/packages/attachments/tests/attachments.test.ts @@ -99,6 +99,7 @@ beforeEach(() => { remoteStorage: mockRemoteStorage, localStorage: mockLocalStorage, syncIntervalMs: INTERVAL_MILLISECONDS, + archivedCacheLimit: 0, }); }) @@ -280,7 +281,7 @@ describe('attachment queue', () => { 5 ); - // await queue.syncStorage(); // <-- explicitly delete + // await new Promise(resolve => setTimeout(resolve, 1500)); // File should be deleted too expect(await mockLocalStorage.fileExists(record.localUri!)).toBe(false); From f6b2343be17ef6302a5f4f0c69006e1dcddc3b84 Mon Sep 17 00:00:00 2001 From: Amine Date: Thu, 6 Nov 2025 13:40:34 +0800 Subject: [PATCH 19/19] docs: update README.md to document usage --- packages/attachments/README.md | 781 ++++++++++++++++++++++++++------- 1 file changed, 632 insertions(+), 149 deletions(-) diff --git a/packages/attachments/README.md b/packages/attachments/README.md index 2ed7d8199..dd325bb5e 100644 --- a/packages/attachments/README.md +++ b/packages/attachments/README.md @@ -1,236 +1,719 @@ # @powersync/attachments -A [PowerSync](https://powersync.com) library to manage attachments in React Native and JavaScript/TypeScript apps. +PowerSync SDK for managing file attachments in JavaScript/TypeScript applications. Automatically handles synchronization of files between local storage and remote storage (S3, Supabase Storage, etc.), with support for upload/download queuing, offline functionality, and cache management. + +For detailed concepts and guides, see the [PowerSync documentation](https://docs.powersync.com). ## Installation -**yarn** +```bash +npm install @powersync/attachments +``` ```bash yarn add @powersync/attachments ``` -**pnpm** - ```bash pnpm add @powersync/attachments ``` -**npm** +## Quick Start -```bash -npm install @powersync/attachments +This example shows a web application where users have profile photos stored as attachments. + +### 1. Add AttachmentTable to your schema + +```typescript +import { Schema, Table, column } from '@powersync/web'; +import { AttachmentTable } from '@powersync/attachments'; + +const appSchema = new Schema({ + users: new Table({ + name: column.text, + email: column.text, + photo_id: column.text + }), + attachments: new AttachmentTable() +}); ``` -## Usage +### 2. Set up storage adapters + +```typescript +import { IndexDBFileSystemStorageAdapter } from '@powersync/attachments'; + +// Local storage for the browser (IndexedDB) +const localStorage = new IndexDBFileSystemStorageAdapter('my-app-files'); + +// Remote storage adapter for your cloud storage (e.g., S3, Supabase) +const remoteStorage = { + async uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise { + // Get signed upload URL from your backend + const { uploadUrl } = await fetch('/api/attachments/upload-url', { + method: 'POST', + body: JSON.stringify({ filename: attachment.filename }) + }).then(r => r.json()); + + // Upload file to cloud storage + await fetch(uploadUrl, { + method: 'PUT', + body: fileData, + headers: { 'Content-Type': attachment.mediaType || 'application/octet-stream' } + }); + }, + + async downloadFile(attachment: AttachmentRecord): Promise { + // Get signed download URL from your backend + const { downloadUrl } = await fetch(`/api/attachments/download-url/${attachment.id}`) + .then(r => r.json()); + + // Download file from cloud storage + const response = await fetch(downloadUrl); + return response.arrayBuffer(); + }, + + async deleteFile(attachment: AttachmentRecord): Promise { + // Delete from cloud storage via your backend + await fetch(`/api/attachments/${attachment.id}`, { method: 'DELETE' }); + } +}; +``` -The `AttachmentQueue` class is used to manage and sync attachments in your app. +> **Note:** For Node.js or Electron apps, use `NodeFileSystemAdapter` instead: +> ```typescript +> import { NodeFileSystemAdapter } from '@powersync/attachments'; +> const localStorage = new NodeFileSystemAdapter('./user-attachments'); +> ``` + +### 3. Create and start AttachmentQueue + +```typescript +import { AttachmentQueue } from '@powersync/attachments'; + +const profilePicturesQueue = new AttachmentQueue({ + db: powersync, + localStorage, + remoteStorage, + // Determine what attachments the queue should handle + // in this case it handles only the user profile pictures + watchAttachments: (onUpdate) => { + powersync.watch( + 'SELECT photo_id FROM users WHERE photo_id IS NOT NULL', + [], + { + onResult: (result) => { + const attachments = result.rows?._array.map(row => ({ + id: row.photo_id, + fileExtension: 'jpg' + })) ?? []; + onUpdate(attachments); + } + } + ); + } +}); -### Example +// Start automatic syncing +await profilePicturesQueue.startSync(); +``` -In this example, the user captures photos when checklist items are completed as part of an inspection workflow. +### 4. Save files with atomic updates + +```typescript +// When user uploads a profile photo +async function uploadProfilePhoto(imageBlob: Blob) { + const arrayBuffer = await imageBlob.arrayBuffer(); + + const attachment = await queue.saveFile({ + data: arrayBuffer, + fileExtension: 'jpg', + mediaType: 'image/jpeg', + // Atomically update the user record in the same transaction + updateHook: async (tx, attachment) => { + await tx.execute( + 'UPDATE users SET photo_id = ? WHERE id = ?', + [attachment.id, currentUserId] + ); + } + }); + + console.log('Photo queued for upload:', attachment.id); + // File will automatically upload in the background +} +``` -The schema for the `checklist` table: +## Storage Adapters -```javascript -const checklists = new Table( - { - photo_id: column.text, - description: column.text, - completed: column.integer, - completed_at: column.text, - completed_by: column.text - }, - { indexes: { inspections: ['checklist_id'] } } -); +### Local Storage Adapters -const AppSchema = new Schema({ - checklists -}); +Local storage adapters handle file persistence on the device. + +#### IndexDBFileSystemStorageAdapter + +For web browsers using IndexedDB: + +```typescript +import { IndexDBFileSystemStorageAdapter } from '@powersync/attachments'; + +const localStorage = new IndexDBFileSystemStorageAdapter('database-name'); ``` -### Steps to implement +**Constructor Parameters:** +- `databaseName` (string, optional): IndexedDB database name. Default: `'PowerSyncFiles'` + +#### NodeFileSystemAdapter -1. Create a new class `AttachmentQueue` that extends `AbstractAttachmentQueue` from `@powersync/attachments`. +For Node.js and Electron using the filesystem: -```javascript -import { AbstractAttachmentQueue } from '@powersync/attachments'; +```typescript +import { NodeFileSystemAdapter } from '@powersync/attachments'; -export class AttachmentQueue extends AbstractAttachmentQueue {} +const localStorage = new NodeFileSystemAdapter('./attachments'); ``` -2. Implement `onAttachmentIdsChange`, which takes in a callback to handle an array of `string` values of IDs that relate to attachments in your app. We recommend using `PowerSync`'s `watch` query to return the all IDs of attachments in your app. +**Constructor Parameters:** +- `storageDirectory` (string, optional): Directory path for storing files. Default: `'./user_data'` - In this example, we query all photos that have been captured as part of an inspection and map these to an array of `string` values. +#### Custom Local Storage Adapter -```javascript -import { AbstractAttachmentQueue } from '@powersync/attachments'; +Implement the `LocalStorageAdapter` interface for other environments: -export class AttachmentQueue extends AbstractAttachmentQueue { - onAttachmentIdsChange(onUpdate) { - this.powersync.watch('SELECT photo_id as id FROM checklists WHERE photo_id IS NOT NULL', [], { - onResult: (result) => onUpdate(result.rows?._array.map((r) => r.id) ?? []) - }); - } +```typescript +interface LocalStorageAdapter { + initialize(): Promise; + clear(): Promise; + getLocalUri(filename: string): string; + saveFile(filePath: string, data: ArrayBuffer | string): Promise; + readFile(filePath: string): Promise; + deleteFile(filePath: string): Promise; + fileExists(filePath: string): Promise; + makeDir(path: string): Promise; + rmDir(path: string): Promise; } ``` -3. Implement `newAttachmentRecord` to return an object that represents the attachment record in your app. +### Remote Storage Adapter - In this example we always work with `JPEG` images, but you can use any media type that is supported by your app and storage solution. Note: we are set the state to `QUEUED_UPLOAD` when creating a new photo record which assumes that the photo data is already on the device. +Remote storage adapters handle communication with your cloud storage (S3, Supabase Storage, Cloudflare R2, etc.). -```javascript -import { AbstractAttachmentQueue } from '@powersync/attachments'; +#### Interface -export class AttachmentQueue extends AbstractAttachmentQueue { - // ... - async newAttachmentRecord(record) { - const photoId = record?.id ?? uuid(); - const filename = record?.filename ?? `${photoId}.jpg`; - return { - id: photoId, - filename, - media_type: 'image/jpeg', - state: AttachmentState.QUEUED_UPLOAD, - ...record - }; - } +```typescript +interface RemoteStorageAdapter { + uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise; + downloadFile(attachment: AttachmentRecord): Promise; + deleteFile(attachment: AttachmentRecord): Promise; } ``` -4. Add an `AttachmentTable` to your app's PowerSync Schema: +#### Example: S3-Compatible Storage with Signed URLs + +```typescript +import { RemoteStorageAdapter, AttachmentRecord } from '@powersync/attachments'; + +const remoteStorage: RemoteStorageAdapter = { + async uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise { + // Request signed upload URL from your backend + const response = await fetch('https://api.example.com/attachments/upload-url', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getAuthToken()}` + }, + body: JSON.stringify({ + filename: attachment.filename, + contentType: attachment.mediaType + }) + }); + + const { uploadUrl } = await response.json(); + + // Upload directly to S3 using signed URL + await fetch(uploadUrl, { + method: 'PUT', + body: fileData, + headers: { 'Content-Type': attachment.mediaType || 'application/octet-stream' } + }); + }, -```javascript -import { AttachmentTable } from '@powersync/attachments'; + async downloadFile(attachment: AttachmentRecord): Promise { + // Request signed download URL from your backend + const response = await fetch( + `https://api.example.com/attachments/${attachment.id}/download-url`, + { headers: { 'Authorization': `Bearer ${getAuthToken()}` } } + ); + + const { downloadUrl } = await response.json(); + + // Download from S3 using signed URL + const fileResponse = await fetch(downloadUrl); + if (!fileResponse.ok) { + throw new Error(`Download failed: ${fileResponse.statusText}`); + } + + return fileResponse.arrayBuffer(); + }, -const AppSchema = new Schema({ - // ... other tables - attachments: new AttachmentTable({ - name: 'attachments', - }), -}); + async deleteFile(attachment: AttachmentRecord): Promise { + // Delete via your backend (backend handles S3 deletion) + await fetch(`https://api.example.com/attachments/${attachment.id}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${getAuthToken()}` } + }); + } +}; +``` + +> **Security Note:** Always use your backend to generate signed URLs and validate permissions. Never expose storage credentials to the client. + +## API Reference + +### AttachmentQueue + +Main class for managing attachment synchronization. + +#### Constructor + +```typescript +new AttachmentQueue(options: AttachmentQueueOptions) ``` -In addition to `Table` options, the `AttachmentTable` can optionally be configured with the following options: +**Options:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `db` | `AbstractPowerSyncDatabase` | Yes | - | PowerSync database instance | +| `remoteStorage` | `RemoteStorageAdapter` | Yes | - | Remote storage adapter implementation | +| `localStorage` | `LocalStorageAdapter` | Yes | - | Local storage adapter implementation | +| `watchAttachments` | `(onUpdate: (attachments: WatchedAttachmentItem[]) => Promise) => void` | Yes | - | Callback to determine which attachments to handle by the queue from your user defined query | +| `tableName` | `string` | No | `'attachments'` | Name of the attachments table | +| `logger` | `ILogger` | No | `db.logger` | Logger instance for diagnostic output | +| `syncIntervalMs` | `number` | No | `30000` | Interval between automatic syncs in milliseconds | +| `syncThrottleDuration` | `number` | No | `30` | Throttle duration for sync operations in milliseconds | +| `downloadAttachments` | `boolean` | No | `true` | Whether to automatically download remote attachments | +| `archivedCacheLimit` | `number` | No | `100` | Maximum number of archived attachments before cleanup | +| `errorHandler` | `AttachmentErrorHandler` | No | `undefined` | Custom error handler for upload/download/delete operations | -| Option | Description | Default | -| ------------------- | ------------------------------------------------------------------------------- | ----------------------------- | -| `name` | The name of the table | `attachments` | -| `additionalColumns` | An array of addition `Column` objects added to the default columns in the table | See below for default columns | +#### Methods + +##### `startSync()` + +Starts automatic attachment synchronization. + +```typescript +await queue.startSync(); +``` -The default columns in `AttachmentTable`: +This will: +- Initialize local storage +- Set up periodic sync based on `syncIntervalMs` +- Watch for changes in active attachments +- Process queued uploads, downloads, and deletes -| Column Name | Type | Description | -| ------------ | --------- | ----------------------------------------------------------------- | -| `id` | `TEXT` | The ID of the attachment record | -| `filename` | `TEXT` | The filename of the attachment | -| `media_type` | `TEXT` | The media type of the attachment | -| `state` | `INTEGER` | The state of the attachment, one of `AttachmentState` enum values | -| `timestamp` | `INTEGER` | The timestamp of last update to the attachment record | -| `size` | `INTEGER` | The size of the attachment in bytes | +##### `stopSync()` -5. To instantiate an `AttachmentQueue`, one needs to provide an instance of `AbstractPowerSyncDatabase` from PowerSync and an instance of `StorageAdapter`. - See the `StorageAdapter` interface definition [here](https://github.com/powersync-ja/powersync-js/blob/main/packages/attachments/src/StorageAdapter.ts). +Stops automatic attachment synchronization. -6. Instantiate a new `AttachmentQueue` and call `init()` to start syncing attachments. Our example, uses a `StorageAdapter` that integrates with Supabase Storage. +```typescript +await queue.stopSync(); +``` -```javascript -this.storage = this.supabaseConnector.storage; -this.powersync = new PowerSyncDatabase({ - schema: AppSchema, - database: { - dbFilename: 'sqlite.db' +##### `saveFile(options)` + +Saves a file locally and queues it for upload to remote storage. + +```typescript +const attachment = await queue.saveFile({ + data: arrayBuffer, + fileExtension: 'pdf', + mediaType: 'application/pdf', + id: 'custom-id', // optional + metaData: '{"description": "Invoice"}', // optional + updateHook: async (tx, attachment) => { + // Update your data model in the same transaction + await tx.execute( + 'INSERT INTO documents (id, attachment_id) VALUES (?, ?)', + [documentId, attachment.id] + ); } }); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `data` | `ArrayBuffer \| string` | Yes | File data as ArrayBuffer or base64 string | +| `fileExtension` | `string` | Yes | File extension (e.g., 'jpg', 'pdf') | +| `mediaType` | `string` | No | MIME type (e.g., 'image/jpeg') | +| `id` | `string` | No | Custom attachment ID (UUID generated if not provided) | +| `metaData` | `string` | No | Optional metadata JSON string | +| `updateHook` | `(tx: Transaction, attachment: AttachmentRecord) => Promise` | No | Callback to update your data model atomically | + +**Returns:** `Promise` - The created attachment record -this.attachmentQueue = new AttachmentQueue({ - powersync: this.powersync, - storage: this.storage +The `updateHook` is executed in the same database transaction as the attachment creation, ensuring atomic operations. This is the recommended way to link attachments to your data model. + +##### `deleteFile(options)` + +Deletes an attachment from both local and remote storage. + +```typescript +await queue.deleteFile({ + id: attachmentId, + updateHook: async (tx, attachment) => { + // Update your data model in the same transaction + await tx.execute( + 'UPDATE users SET photo_id = NULL WHERE photo_id = ?', + [attachment.id] + ); + } }); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `id` | `string` | Yes | Attachment ID to delete | +| `updateHook` | `(tx: Transaction, attachment: AttachmentRecord) => Promise` | No | Callback to update your data model atomically | + +##### `generateAttachmentId()` + +Generates a new UUID for an attachment using SQLite's `uuid()` function. -// Initialize and connect PowerSync ... -// Then initialize the attachment queue -await this.attachmentQueue.init(); +```typescript +const id = await queue.generateAttachmentId(); ``` -7. Finally, to create an attachment and add it to the queue, call `saveToQueue()`. +**Returns:** `Promise` - A new UUID - In our example we added a `savePhoto()` method to our `AttachmentQueue` class, that does this: +##### `syncStorage()` -```javascript -export class AttachmentQueue extends AbstractAttachmentQueue { - // ... - async savePhoto(base64Data) { - const photoAttachment = await this.newAttachmentRecord(); - photoAttachment.local_uri = this.getLocalFilePathSuffix(photoAttachment.filename); +Manually triggers a sync operation. This is called automatically at regular intervals, but can be invoked manually if needed. - const localFilePathUri = this.getLocalUri(photoAttachment.local_uri); +```typescript +await queue.syncStorage(); +``` - await this.storage.writeFile(localFilePathUri, base64Data, { encoding: 'base64' }); +##### `verifyAttachments()` - return this.saveToQueue(photoAttachment); - } +Verifies the integrity of all attachment records and repairs inconsistencies. Checks each attachment against local storage and: +- Updates `localUri` if file exists at a different path +- Archives attachments with missing local files that haven't been uploaded +- Requeues synced attachments for download if local files are missing + +```typescript +await queue.verifyAttachments(); +``` + +This is automatically called when `startSync()` is invoked. + +##### `watchAttachments` callback + +The `watchAttachments` callback is a required parameter that tells the AttachmentQueue which attachments to handle. This tells the queue which attachments to download, upload, or archive. + +**Signature:** + +```typescript +(onUpdate: (attachments: WatchedAttachmentItem[]) => Promise) => void +``` + +**WatchedAttachmentItem:** + +```typescript +type WatchedAttachmentItem = { + id: string; + fileExtension: string; // e.g., 'jpg', 'pdf' + metaData?: string; +} | { + id: string; + filename: string; // e.g., 'document.pdf' + metaData?: string; +}; +``` + +Use either `fileExtension` OR `filename`, not both. + +**Example:** + +```typescript +watchAttachments: (onUpdate) => { + // Watch for photo references in users table + db.watch( + 'SELECT photo_id, metadata FROM users WHERE photo_id IS NOT NULL', + [], + { + onResult: async (result) => { + const attachments = result.rows?._array.map(row => ({ + id: row.photo_id, + fileExtension: 'jpg', + metaData: row.metadata + })) ?? []; + await onUpdate(attachments); + } + } + ); } ``` -# Implementation details +--- -## Attachment State +### AttachmentTable -The `AttachmentQueue` class manages attachments in your app by tracking their state. +PowerSync schema table for storing attachment metadata. -The state of an attachment can be one of the following: +#### Constructor -| State | Description | -| ----------------- | ----------------------------------------------------------------------------- | -| `QUEUED_SYNC` | Check if the attachment needs to be uploaded or downloaded | -| `QUEUED_UPLOAD` | The attachment has been queued for upload to the cloud storage | -| `QUEUED_DOWNLOAD` | The attachment has been queued for download from the cloud storage | -| `SYNCED` | The attachment has been synced | -| `ARCHIVED` | The attachment has been orphaned, i.e. the associated record has been deleted | +```typescript +new AttachmentTable(options?: AttachmentTableOptions) +``` -## Initial sync +**Options:** + +Extends PowerSync `TableV2Options` (excluding `name` and `columns`). + +| Parameter | Type | Description | +|-----------|------|-------------| +| `viewName` | `string` | View name for the table. Default: `'attachments'` | +| `localOnly` | `boolean` | Whether table is local-only. Default: `true` | +| `insertOnly` | `boolean` | Whether table is insert-only. Default: `false` | + +#### Default Columns + +| Column | Type | Description | +|--------|------|-------------| +| `id` | `TEXT` | Attachment ID (primary key) | +| `filename` | `TEXT` | Filename with extension | +| `local_uri` | `TEXT` | Local file path or URI | +| `timestamp` | `INTEGER` | Last update timestamp | +| `size` | `INTEGER` | File size in bytes | +| `media_type` | `TEXT` | MIME type | +| `state` | `INTEGER` | Sync state (see `AttachmentState`) | +| `has_synced` | `INTEGER` | Whether file has synced (0 or 1) | +| `meta_data` | `TEXT` | Optional metadata JSON string | + +--- + +### AttachmentRecord + +Interface representing an attachment record. + +```typescript +interface AttachmentRecord { + id: string; + filename: string; + localUri?: string; + size?: number; + mediaType?: string; + timestamp?: number; + metaData?: string; + hasSynced?: boolean; + state: AttachmentState; +} +``` + +--- -Upon initializing the `AttachmentQueue`, an initial sync of attachments will take place if the `performInitialSync` is set to true. -Any `AttachmentRecord` with `id` in first set of IDs retrieved from the watch query will be marked as `QUEUED_SYNC`, and these records will be rechecked to see if they need to be uploaded or downloaded. +### AttachmentState -## Syncing attachments +Enum representing attachment synchronization states. -The `AttachmentQueue` sets up two watch queries on the `attachments` table, one for records in `QUEUED_UPLOAD` state and one for `QUEUED_DOWNLOAD` state. +```typescript +enum AttachmentState { + QUEUED_SYNC = 0, // Check if upload or download needed + QUEUED_UPLOAD = 1, // Queued for upload + QUEUED_DOWNLOAD = 2, // Queued for download + QUEUED_DELETE = 3, // Queued for deletion + SYNCED = 4, // Successfully synced + ARCHIVED = 5 // No longer referenced (orphaned) +} +``` -In addition to watching for changes, the `AttachmentQueue` also triggers a sync every few seconds. This will retry any failed uploads/downloads, in particular after the app was offline. +--- -By default, this is every 30 seconds, but can be configured by setting `syncInterval` in the `AttachmentQueue` constructor options, or disabled by setting the interval to `0`. +### LocalStorageAdapter + +Interface for local file storage operations. + +```typescript +interface LocalStorageAdapter { + initialize(): Promise; + clear(): Promise; + getLocalUri(filename: string): string; + saveFile(filePath: string, data: ArrayBuffer | string): Promise; + readFile(filePath: string): Promise; + deleteFile(filePath: string): Promise; + fileExists(filePath: string): Promise; + makeDir(path: string): Promise; + rmDir(path: string): Promise; +} +``` -### Uploading +--- + +### RemoteStorageAdapter + +Interface for remote storage operations. + +```typescript +interface RemoteStorageAdapter { + uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise; + downloadFile(attachment: AttachmentRecord): Promise; + deleteFile(attachment: AttachmentRecord): Promise; +} +``` + +--- + +### NodeFileSystemAdapter + +Local storage adapter for Node.js and Electron. + +**Constructor:** + +```typescript +new NodeFileSystemAdapter(storageDirectory?: string) +``` + +- `storageDirectory` (optional): Directory path for storing files. Default: `'./user_data'` + +--- + +### IndexDBFileSystemStorageAdapter + +Local storage adapter for web browsers using IndexedDB. + +**Constructor:** + +```typescript +new IndexDBFileSystemStorageAdapter(databaseName?: string) +``` -- An `AttachmentRecord` is created or updated with a state of `QUEUED_UPLOAD`. -- The `AttachmentQueue` picks this up and upon successful upload to Supabase, sets the state to `SYNCED`. -- If the upload is not successful, the record remains in `QUEUED_UPLOAD` state and uploading will be retried when syncing triggers again. +- `databaseName` (optional): IndexedDB database name. Default: `'PowerSyncFiles'` + +## Error Handling + +The `AttachmentErrorHandler` interface allows you to customize error handling for sync operations. + +### Interface + +```typescript +interface AttachmentErrorHandler { + onDownloadError(attachment: AttachmentRecord, error: Error): Promise; + onUploadError(attachment: AttachmentRecord, error: Error): Promise; + onDeleteError(attachment: AttachmentRecord, error: Error): Promise; +} +``` + +Each method returns: +- `true` to retry the operation +- `false` to archive the attachment and skip retrying + +### Example + +```typescript +const errorHandler: AttachmentErrorHandler = { + async onDownloadError(attachment, error) { + console.error(`Download failed for ${attachment.filename}:`, error); + + // Retry on network errors, archive on 404s + if (error.message.includes('404') || error.message.includes('Not Found')) { + console.log('File not found, archiving attachment'); + return false; // Archive + } + + console.log('Will retry download on next sync'); + return true; // Retry + }, + + async onUploadError(attachment, error) { + console.error(`Upload failed for ${attachment.filename}:`, error); + + // Always retry uploads + return true; + }, + + async onDeleteError(attachment, error) { + console.error(`Delete failed for ${attachment.filename}:`, error); + + // Retry deletes, but archive after too many attempts + const attempts = attachment.metaData ? + JSON.parse(attachment.metaData).deleteAttempts || 0 : 0; + + return attempts < 3; // Retry up to 3 times + } +}; + +const queue = new AttachmentQueue({ + // ... other options + errorHandler +}); +``` + +## Advanced Usage + +### Verification and Recovery + +The `verifyAttachments()` method checks attachment integrity and repairs issues: + +```typescript +// Manually verify all attachments +await queue.verifyAttachments(); +``` + +This is useful if: +- Local files may have been manually deleted +- Storage paths changed +- You suspect data inconsistencies + +Verification is automatically run when `startSync()` is called. + +### Custom Sync Intervals + +Adjust sync frequency based on your needs: + +```typescript +const queue = new AttachmentQueue({ + // ... other options + syncIntervalMs: 60000, // Sync every 60 seconds instead of 30 +}); +``` + +Set to `0` to disable periodic syncing (manual `syncStorage()` calls only). + +### Archive and Cache Management + +Control how many archived attachments are kept before cleanup: + +```typescript +const queue = new AttachmentQueue({ + // ... other options + archivedCacheLimit: 200, // Keep up to 200 archived attachments +}); +``` -### Downloading +Archived attachments are those no longer referenced in your data model but not yet deleted. This allows for: +- Quick restoration if references are added back +- Caching of recently used files +- Gradual cleanup to avoid storage bloat -- An `AttachmentRecord` is created or updated with `QUEUED_DOWNLOAD` state. -- The watch query adds the `id` into a queue of IDs to download and triggers the download process -- This checks whether the photo is already on the device and if so, skips downloading. -- If the photo is not on the device, it is downloaded from cloud storage. -- Writes file to the user's local storage. -- If this is successful, update the `AttachmentRecord` state to `SYNCED`. -- If any of these fail, the download is retried in the next sync trigger. +When the limit is reached, the oldest archived attachments are permanently deleted. -### Deleting attachments +## Examples -When an attachment is deleted by a user action or cache expiration: +See the following demo applications in this repository: -- Related `AttachmentRecord` is removed from attachments table. -- Local file (if exists) is deleted. -- File on cloud storage is deleted. +- **[react-supabase-todolist](../../demos/react-supabase-todolist)** - React web app with Supabase Storage integration +- **[react-native-supabase-todolist](../../demos/react-native-supabase-todolist)** - React Native mobile app with attachment support -### Expire Cache +Each demo shows a complete implementation including: +- Schema setup +- Storage adapter configuration +- File upload/download UI +- Error handling -When PowerSync removes a record, as a result of coming back online or conflict resolution for instance: +## License -- Any associated `AttachmentRecord` is orphaned. -- On the next sync trigger, the `AttachmentQueue` sets all records that are orphaned to `ARCHIVED` state. -- By default, the `AttachmentQueue` only keeps the last `100` attachment records and then expires the rest. -- This can be configured by setting `cacheLimit` in the `AttachmentQueue` constructor options. +Apache 2.0