From 16d2296a45c8598de165005723a3923fc9bcbca2 Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Mon, 14 Oct 2024 13:25:17 +0100 Subject: [PATCH 1/3] fix(pglite/live): Fix an issue with live.incrementalQuery where the order would be incorrect with rapid consecutive queries (#378) * Fix an issue with live.incrementalQuery where the order would be incorect with rapid consecutive quearies * Changeset * Fix bug where Firefox was unable to remove OPFS files --- .changeset/gold-moose-shake.md | 5 +++++ .changeset/swift-suns-complain.md | 5 +++++ packages/pglite/src/fs/opfs-ahp.ts | 10 ++++------ packages/pglite/src/live/index.ts | 12 ++++++++---- 4 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 .changeset/gold-moose-shake.md create mode 100644 .changeset/swift-suns-complain.md diff --git a/.changeset/gold-moose-shake.md b/.changeset/gold-moose-shake.md new file mode 100644 index 00000000..7f2e9b91 --- /dev/null +++ b/.changeset/gold-moose-shake.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Fix bug where Firefox was unable to remove OPFS files diff --git a/.changeset/swift-suns-complain.md b/.changeset/swift-suns-complain.md new file mode 100644 index 00000000..dc4ff119 --- /dev/null +++ b/.changeset/swift-suns-complain.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Fix an issue with live.incrementalQuery where the order would be incorrect with rapid consecutive queries diff --git a/packages/pglite/src/fs/opfs-ahp.ts b/packages/pglite/src/fs/opfs-ahp.ts index 5a22c6f9..433af52b 100644 --- a/packages/pglite/src/fs/opfs-ahp.ts +++ b/packages/pglite/src/fs/opfs-ahp.ts @@ -278,12 +278,10 @@ export class OpfsAhpFS extends BaseFilesystem { const fh = this.#fh.get(filename)! const sh = this.#sh.get(filename) sh?.close() - // @ts-ignore outdated type? need to check - await fh.remove().then(() => { - this.#fh.delete(filename) - this.#sh.delete(filename) - resolve() - }) + await this.#dataDirAh.removeEntry(fh.name) + this.#fh.delete(filename) + this.#sh.delete(filename) + resolve() }), ) } diff --git a/packages/pglite/src/live/index.ts b/packages/pglite/src/live/index.ts index 2ade9547..7b7ebed6 100644 --- a/packages/pglite/src/live/index.ts +++ b/packages/pglite/src/live/index.ts @@ -244,7 +244,6 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { await pg.transaction(async (tx) => { // Populate the state table await tx.exec(` - DELETE FROM live_query_${id}_state${stateSwitch}; INSERT INTO live_query_${id}_state${stateSwitch} SELECT * FROM live_query_${id}_view; `) @@ -253,6 +252,14 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { changes = await tx.query( `EXECUTE live_query_${id}_diff${stateSwitch};`, ) + + // Switch state + stateSwitch = stateSwitch === 1 ? 2 : 1 + + // Truncate the old state table + await tx.exec(` + TRUNCATE live_query_${id}_state${stateSwitch}; + `) }) break } catch (e) { @@ -272,9 +279,6 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { } } - // Switch state - stateSwitch = stateSwitch === 1 ? 2 : 1 - callback([ ...(reset ? [ From 5e390365b6ed5b5c122047a248a4117c50390fa4 Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Mon, 14 Oct 2024 14:47:43 +0100 Subject: [PATCH 2/3] fix(pglite/live): Fix querying views, and extend the return value to allow late attachment of a callback (#374) * Improve live plugin * Add tests --- .changeset/chilled-tomatoes-whisper.md | 5 + .changeset/smooth-badgers-move.md | 5 + packages/pglite/src/live/index.ts | 388 +++++++++++---- packages/pglite/src/live/interface.ts | 77 ++- .../tests/{live.test.js => live.test.ts} | 449 +++++++++++++++--- 5 files changed, 756 insertions(+), 168 deletions(-) create mode 100644 .changeset/chilled-tomatoes-whisper.md create mode 100644 .changeset/smooth-badgers-move.md rename packages/pglite/tests/{live.test.js => live.test.ts} (59%) diff --git a/.changeset/chilled-tomatoes-whisper.md b/.changeset/chilled-tomatoes-whisper.md new file mode 100644 index 00000000..c5749609 --- /dev/null +++ b/.changeset/chilled-tomatoes-whisper.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Fix live queries can query a view by recursively finding all tables they depend on. diff --git a/.changeset/smooth-badgers-move.md b/.changeset/smooth-badgers-move.md new file mode 100644 index 00000000..0eef33aa --- /dev/null +++ b/.changeset/smooth-badgers-move.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Extend the return value of live queries to be subscribed to multiple times, and make the callback optional on initiation. diff --git a/packages/pglite/src/live/index.ts b/packages/pglite/src/live/index.ts index 7b7ebed6..853210d1 100644 --- a/packages/pglite/src/live/index.ts +++ b/packages/pglite/src/live/index.ts @@ -5,17 +5,20 @@ import type { Transaction, } from '../interface' import type { + LiveQueryOptions, + LiveIncrementalQueryOptions, + LiveChangesOptions, LiveNamespace, - LiveQueryReturn, - LiveChangesReturn, + LiveQuery, + LiveChanges, Change, } from './interface' import { uuid, formatQuery } from '../utils.js' export type { LiveNamespace, - LiveQueryReturn, - LiveChangesReturn, + LiveQuery, + LiveChanges, Change, } from './interface.js' @@ -28,11 +31,22 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { const namespaceObj: LiveNamespace = { async query( - query: string, - params: any[] | undefined | null, - callback: (results: Results) => void, + query: string | LiveQueryOptions, + params?: any[] | null, + callback?: (results: Results) => void, ) { + let signal: AbortSignal | undefined + if (typeof query !== 'string') { + signal = query.signal + params = query.params + callback = query.callback + query = query.query + } + let callbacks: Array<(results: Results) => void> = callback + ? [callback] + : [] const id = uuid().replace(/-/g, '') + let dead = false let results: Results let tables: { table_name: string; schema_name: string }[] @@ -63,6 +77,9 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { // Function to refresh the query const refresh = async (count = 0) => { + if (callbacks.length === 0) { + return + } try { results = await pg.query(`EXECUTE live_query_${id}_get;`) } catch (e) { @@ -81,7 +98,7 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { throw e } } - callback(results) + runResultCallbacks(callbacks, results) } // Setup the listeners @@ -96,33 +113,83 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { ), ) + // Function to subscribe to the query + const subscribe = (callback: (results: Results) => void) => { + if (dead) { + throw new Error( + 'Live query is no longer active and cannot be subscribed to', + ) + } + callbacks.push(callback) + } + // Function to unsubscribe from the query - const unsubscribe = async () => { - await Promise.all(unsubList.map((unsub) => unsub())) - await pg.exec(` + // If no function is provided, unsubscribe all callbacks + // If there are no callbacks, unsubscribe from the notify triggers + const unsubscribe = async (callback?: (results: Results) => void) => { + if (callback) { + callbacks = callbacks.filter((callback) => callback !== callback) + } else { + callbacks = [] + } + if (callbacks.length === 0) { + dead = true + await Promise.all(unsubList.map((unsub) => unsub())) + await pg.exec(` DROP VIEW IF EXISTS live_query_${id}_view; DEALLOCATE live_query_${id}_get; `) + } + } + + // If the signal has already been aborted, unsubscribe + if (signal?.aborted) { + await unsubscribe() + } else { + // Add an event listener to unsubscribe if the signal is aborted + signal?.addEventListener( + 'abort', + () => { + unsubscribe() + }, + { once: true }, + ) } // Run the callback with the initial results - callback(results!) + runResultCallbacks(callbacks, results!) // Return the initial results return { initialResults: results!, + subscribe, unsubscribe, refresh, - } satisfies LiveQueryReturn + } satisfies LiveQuery }, async changes( - query: string, - params: any[] | undefined | null, - key: string, - callback: (changes: Array>) => void, + query: string | LiveChangesOptions, + params?: any[] | null, + key?: string, + callback?: (changes: Array>) => void, ) { + let signal: AbortSignal | undefined + if (typeof query !== 'string') { + signal = query.signal + params = query.params + key = query.key + callback = query.callback + query = query.query + } + if (!key) { + throw new Error('key is required for changes queries') + } + let callbacks: Array<(changes: Array>) => void> = callback + ? [callback] + : [] const id = uuid().replace(/-/g, '') + let dead = false let tables: { table_name: string; schema_name: string }[] let stateSwitch: 1 | 2 = 1 @@ -238,6 +305,9 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { await init() const refresh = async () => { + if (callbacks.length === 0) { + return + } let reset = false for (let i = 0; i < 5; i++) { try { @@ -279,7 +349,7 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { } } - callback([ + runChangeCallbacks(callbacks, [ ...(reset ? [ { @@ -301,16 +371,50 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { ), ) + // Function to subscribe to the query + const subscribe = (callback: (changes: Array>) => void) => { + if (dead) { + throw new Error( + 'Live query is no longer active and cannot be subscribed to', + ) + } + callbacks.push(callback) + } + // Function to unsubscribe from the query - const unsubscribe = async () => { - await Promise.all(unsubList.map((unsub) => unsub())) - await pg.exec(` - DROP VIEW IF EXISTS live_query_${id}_view; - DROP TABLE IF EXISTS live_query_${id}_state1; - DROP TABLE IF EXISTS live_query_${id}_state2; - DEALLOCATE live_query_${id}_diff1; - DEALLOCATE live_query_${id}_diff2; - `) + const unsubscribe = async ( + callback?: (changes: Array>) => void, + ) => { + if (callback) { + callbacks = callbacks.filter((callback) => callback !== callback) + } else { + callbacks = [] + } + if (callbacks.length === 0) { + dead = true + await Promise.all(unsubList.map((unsub) => unsub())) + await pg.exec(` + DROP VIEW IF EXISTS live_query_${id}_view; + DROP TABLE IF EXISTS live_query_${id}_state1; + DROP TABLE IF EXISTS live_query_${id}_state2; + DEALLOCATE live_query_${id}_diff1; + DEALLOCATE live_query_${id}_diff2; + `) + } + } + + // If the signal has already been aborted, unsubscribe + if (signal?.aborted) { + await unsubscribe() + } else { + // Add an event listener to unsubscribe if the signal is aborted + signal?.addEventListener( + 'abort', + () => { + unsubscribe() + }, + { once: true }, + ) } // Run the callback with the initial changes @@ -326,104 +430,146 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { return { fields, initialChanges: changes!.rows, + subscribe, unsubscribe, refresh, - } satisfies LiveChangesReturn + } satisfies LiveChanges }, async incrementalQuery( - query: string, - params: any[] | undefined | null, - key: string, - callback: (results: Results) => void, + query: string | LiveIncrementalQueryOptions, + params?: any[] | null, + key?: string, + callback?: (results: Results) => void, ) { + let signal: AbortSignal | undefined + if (typeof query !== 'string') { + signal = query.signal + params = query.params + key = query.key + callback = query.callback + query = query.query + } + if (!key) { + throw new Error('key is required for incremental queries') + } + let callbacks: Array<(results: Results) => void> = callback + ? [callback] + : [] const rowsMap: Map = new Map() const afterMap: Map = new Map() let lastRows: T[] = [] let firstRun = true - const { fields, unsubscribe, refresh } = await namespaceObj.changes( - query, - params, - key, - (changes) => { - // Process the changes - for (const change of changes) { - const { - __op__: op, - __changed_columns__: changedColumns, - ...obj - } = change as typeof change & { [key: string]: any } - switch (op) { - case 'RESET': - rowsMap.clear() - afterMap.clear() - break - case 'INSERT': - rowsMap.set(obj[key], obj) - afterMap.set(obj.__after__, obj[key]) - break - case 'DELETE': { - const oldObj = rowsMap.get(obj[key]) - rowsMap.delete(obj[key]) - afterMap.delete(oldObj.__after__) - break - } - case 'UPDATE': { - const newObj = { ...(rowsMap.get(obj[key]) ?? {}) } - for (const columnName of changedColumns) { - newObj[columnName] = obj[columnName] - if (columnName === '__after__') { - afterMap.set(obj.__after__, obj[key]) - } + const { + fields, + unsubscribe: unsubscribeChanges, + refresh, + } = await namespaceObj.changes(query, params, key, (changes) => { + // Process the changes + for (const change of changes) { + const { + __op__: op, + __changed_columns__: changedColumns, + ...obj + } = change as typeof change & { [key: string]: any } + switch (op) { + case 'RESET': + rowsMap.clear() + afterMap.clear() + break + case 'INSERT': + rowsMap.set(obj[key], obj) + afterMap.set(obj.__after__, obj[key]) + break + case 'DELETE': { + const oldObj = rowsMap.get(obj[key]) + rowsMap.delete(obj[key]) + afterMap.delete(oldObj.__after__) + break + } + case 'UPDATE': { + const newObj = { ...(rowsMap.get(obj[key]) ?? {}) } + for (const columnName of changedColumns) { + newObj[columnName] = obj[columnName] + if (columnName === '__after__') { + afterMap.set(obj.__after__, obj[key]) } - rowsMap.set(obj[key], newObj) - break } - } - } - - // Get the rows in order - const rows: T[] = [] - let lastKey: any = null - for (let i = 0; i < rowsMap.size; i++) { - const nextKey = afterMap.get(lastKey) - const obj = rowsMap.get(nextKey) - if (!obj) { + rowsMap.set(obj[key], newObj) break } - // Remove the __after__ key from the exposed row - const cleanObj = { ...obj } - delete cleanObj.__after__ - rows.push(cleanObj) - lastKey = nextKey } - lastRows = rows + } - // Run the callback - if (!firstRun) { - callback({ - rows, - fields, - }) + // Get the rows in order + const rows: T[] = [] + let lastKey: any = null + for (let i = 0; i < rowsMap.size; i++) { + const nextKey = afterMap.get(lastKey) + const obj = rowsMap.get(nextKey) + if (!obj) { + break } - }, - ) + // Remove the __after__ key from the exposed row + const cleanObj = { ...obj } + delete cleanObj.__after__ + rows.push(cleanObj) + lastKey = nextKey + } + lastRows = rows + + // Run the callbacks + if (!firstRun) { + runResultCallbacks(callbacks, { + rows, + fields, + }) + } + }) firstRun = false - callback({ + runResultCallbacks(callbacks, { rows: lastRows, fields, }) + const subscribe = (callback: (results: Results) => void) => { + callbacks.push(callback) + } + + const unsubscribe = async (callback?: (results: Results) => void) => { + if (callback) { + callbacks = callbacks.filter((callback) => callback !== callback) + } else { + callbacks = [] + } + if (callbacks.length === 0) { + await unsubscribeChanges() + } + } + + if (signal?.aborted) { + await unsubscribe() + } else { + signal?.addEventListener( + 'abort', + () => { + unsubscribe() + }, + { once: true }, + ) + } + return { initialResults: { rows: lastRows, fields, }, + subscribe, unsubscribe, refresh, - } satisfies LiveQueryReturn + } satisfies LiveQuery }, } @@ -442,8 +588,8 @@ export type PGliteWithLive = PGliteInterface & { } /** - * Get a list of all the tables used in a view - * @param tx a transaction or or PGlite instance + * Get a list of all the tables used in a view, recursively + * @param tx a transaction or PGlite instance * @param viewName the name of the view * @returns list of tables used in the view */ @@ -451,15 +597,19 @@ async function getTablesForView( tx: Transaction | PGliteInterface, viewName: string, ): Promise<{ table_name: string; schema_name: string }[]> { - return ( - await tx.query<{ + const tables = new Map() + + async function getTablesRecursive(currentViewName: string) { + const result = await tx.query<{ table_name: string schema_name: string + is_view: boolean }>( ` SELECT DISTINCT cl.relname AS table_name, - n.nspname AS schema_name + n.nspname AS schema_name, + cl.relkind = 'v' AS is_view FROM pg_rewrite r JOIN pg_depend d ON r.oid = d.objid JOIN pg_class cl ON d.refobjid = cl.oid @@ -470,9 +620,27 @@ async function getTablesForView( ) AND d.deptype = 'n'; `, - [viewName], + [currentViewName], ) - ).rows.filter((row) => row.table_name !== viewName) + + for (const row of result.rows) { + if (row.table_name !== currentViewName && !row.is_view) { + const tableKey = `"${row.schema_name}"."${row.table_name}"` + if (!tables.has(tableKey)) { + tables.set(tableKey, { + table_name: row.table_name, + schema_name: row.schema_name, + }) + } + } else if (row.is_view) { + await getTablesRecursive(row.table_name) + } + } + } + + await getTablesRecursive(viewName) + + return Array.from(tables.values()) } /** @@ -513,3 +681,21 @@ async function addNotifyTriggersToTables( tableNotifyTriggersAdded.add(`${table.schema_name}_${table.table_name}`), ) } + +const runResultCallbacks = ( + callbacks: Array<(results: Results) => void>, + results: Results, +) => { + for (const callback of callbacks) { + callback(results) + } +} + +const runChangeCallbacks = ( + callbacks: Array<(changes: Array>) => void>, + changes: Array>, +) => { + for (const callback of callbacks) { + callback(changes) + } +} diff --git a/packages/pglite/src/live/interface.ts b/packages/pglite/src/live/interface.ts index a24984c9..fa753475 100644 --- a/packages/pglite/src/live/interface.ts +++ b/packages/pglite/src/live/interface.ts @@ -1,5 +1,28 @@ import type { Results } from '../interface' +export interface LiveQueryOptions { + query: string + params?: any[] | null + callback?: (results: Results) => void + signal?: AbortSignal +} + +export interface LiveChangesOptions { + query: string + params?: any[] | null + key: string + callback?: (changes: Array>) => void + signal?: AbortSignal +} + +export interface LiveIncrementalQueryOptions { + query: string + params?: any[] | null + key: string + callback?: (results: Results) => void + signal?: AbortSignal +} + export interface LiveNamespace { /** * Create a live query @@ -11,9 +34,19 @@ export interface LiveNamespace { */ query( query: string, - params: any[] | undefined | null, - callback: (results: Results) => void, - ): Promise> + params?: any[] | null, + callback?: (results: Results) => void, + ): Promise> + + /** + * Create a live query + * @param options - The options to pass to the query + * @returns A promise that resolves to an object with the initial results, + * an unsubscribe function, and a refresh function + */ + query( + options: LiveQueryOptions, + ): Promise> /** * Create a live query that returns the changes to the query results @@ -27,8 +60,18 @@ export interface LiveNamespace { query: string, params: any[] | undefined | null, key: string, - callback: (changes: Array>) => void, - ): Promise> + callback?: (changes: Array>) => void, + ): Promise> + + /** + * Create a live query that returns the changes to the query results + * @param options - The options to pass to the query + * @returns A promise that resolves to an object with the initial changes, + * an unsubscribe function, and a refresh function + */ + changes( + options: LiveChangesOptions, + ): Promise> /** * Create a live query with incremental updates @@ -42,20 +85,32 @@ export interface LiveNamespace { query: string, params: any[] | undefined | null, key: string, - callback: (results: Results) => void, - ): Promise> + callback?: (results: Results) => void, + ): Promise> + + /** + * Create a live query with incremental updates + * @param options - The options to pass to the query + * @returns A promise that resolves to an object with the initial results, + * an unsubscribe function, and a refresh function + */ + incrementalQuery( + options: LiveIncrementalQueryOptions, + ): Promise> } -export interface LiveQueryReturn { +export interface LiveQuery { initialResults: Results - unsubscribe: () => Promise + subscribe: (callback: (results: Results) => void) => void + unsubscribe: (callback?: (results: Results) => void) => Promise refresh: () => Promise } -export interface LiveChangesReturn { +export interface LiveChanges { fields: { name: string; dataTypeID: number }[] initialChanges: Array> - unsubscribe: () => Promise + subscribe: (callback: (changes: Array>) => void) => void + unsubscribe: (callback?: (changes: Array>) => void) => Promise refresh: () => Promise } diff --git a/packages/pglite/tests/live.test.js b/packages/pglite/tests/live.test.ts similarity index 59% rename from packages/pglite/tests/live.test.js rename to packages/pglite/tests/live.test.ts index 29473bbc..39eaf89f 100644 --- a/packages/pglite/tests/live.test.js +++ b/packages/pglite/tests/live.test.ts @@ -2,10 +2,11 @@ import { describe, it, expect } from 'vitest' import { testEsmAndCjs } from './test-utils.js' await testEsmAndCjs(async (importType) => { - const { PGlite } = + const { PGlite } = ( importType === 'esm' ? await import('../dist/index.js') : await import('../dist/index.cjs') + ) as typeof import('../dist/index.js') const { live } = importType === 'esm' @@ -14,21 +15,21 @@ await testEsmAndCjs(async (importType) => { describe(`live ${importType}`, () => { it('basic live query', async () => { - const db = new PGlite({ + const db = await PGlite.create({ extensions: { live }, }) await db.exec(` - CREATE TABLE IF NOT EXISTS testTable ( - id SERIAL PRIMARY KEY, - number INT - ); - `) + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) await db.exec(` - INSERT INTO testTable (number) - SELECT i*10 FROM generate_series(1, 5) i; - `) + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) let updatedResults const eventTarget = new EventTarget() @@ -108,22 +109,132 @@ await testEsmAndCjs(async (importType) => { ]) }) + it('live query on view', async () => { + const db = await PGlite.create({ + extensions: { live }, + }) + + await db.exec(` + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) + + await db.exec(` + CREATE OR REPLACE VIEW testView2 AS + SELECT * FROM testTable; + `) + + await db.exec(` + CREATE OR REPLACE VIEW testView1 AS + SELECT * FROM testView2; + `) + + await db.exec(` + CREATE OR REPLACE VIEW testView AS + SELECT * FROM testView1; + `) + + await db.exec(` + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) + + let updatedResults + const eventTarget = new EventTarget() + + const { initialResults, unsubscribe } = await db.live.query( + 'SELECT * FROM testView ORDER BY number;', + [], + (result) => { + updatedResults = result + eventTarget.dispatchEvent(new Event('change')) + }, + ) + + expect(initialResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + db.exec('INSERT INTO testTable (number) VALUES (25);') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 6, number: 25 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + db.exec('DELETE FROM testTable WHERE id = 6;') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + db.exec('UPDATE testTable SET number = 15 WHERE id = 3;') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 3, number: 15 }, + { id: 2, number: 20 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + unsubscribe() + + db.exec('INSERT INTO testTable (number) VALUES (35);') + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 3, number: 15 }, + { id: 2, number: 20 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + }) + it('live query with params', async () => { - const db = new PGlite({ + const db = await PGlite.create({ extensions: { live }, }) await db.exec(` - CREATE TABLE IF NOT EXISTS testTable ( - id SERIAL PRIMARY KEY, - number INT - ); - `) + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) await db.exec(` - INSERT INTO testTable (number) - SELECT i*10 FROM generate_series(1, 5) i; - `) + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) let updatedResults const eventTarget = new EventTarget() @@ -194,21 +305,21 @@ await testEsmAndCjs(async (importType) => { }) it('incremental query unordered', async () => { - const db = new PGlite({ + const db = await PGlite.create({ extensions: { live }, }) await db.exec(` - CREATE TABLE IF NOT EXISTS testTable ( - id SERIAL PRIMARY KEY, - number INT - ); - `) + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) await db.exec(` - INSERT INTO testTable (number) - VALUES (1), (2); - `) + INSERT INTO testTable (number) + VALUES (1), (2); + `) let updatedResults const eventTarget = new EventTarget() @@ -243,7 +354,7 @@ await testEsmAndCjs(async (importType) => { }) it('incremental query with non-integer key', async () => { - const db = new PGlite({ + const db = await PGlite.create({ extensions: { live }, }) @@ -292,21 +403,132 @@ await testEsmAndCjs(async (importType) => { }) it('basic live incremental query', async () => { - const db = new PGlite({ + const db = await PGlite.create({ + extensions: { live }, + }) + + await db.exec(` + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) + + await db.exec(` + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) + + let updatedResults + const eventTarget = new EventTarget() + + const { initialResults, unsubscribe } = await db.live.incrementalQuery( + 'SELECT * FROM testTable ORDER BY number;', + [], + 'id', + (result) => { + updatedResults = result + eventTarget.dispatchEvent(new Event('change')) + }, + ) + + expect(initialResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + await db.exec('INSERT INTO testTable (number) VALUES (25);') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 6, number: 25 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + await db.exec('DELETE FROM testTable WHERE id = 6;') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + await db.exec('UPDATE testTable SET number = 15 WHERE id = 3;') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 3, number: 15 }, + { id: 2, number: 20 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + unsubscribe() + + await db.exec('INSERT INTO testTable (number) VALUES (35);') + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 3, number: 15 }, + { id: 2, number: 20 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + }) + + it('live incremental query on view', async () => { + const db = await PGlite.create({ extensions: { live }, }) await db.exec(` - CREATE TABLE IF NOT EXISTS testTable ( - id SERIAL PRIMARY KEY, - number INT - ); - `) + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) + + await db.exec(` + CREATE OR REPLACE VIEW testView2 AS + SELECT * FROM testTable; + `) + + await db.exec(` + CREATE OR REPLACE VIEW testView1 AS + SELECT * FROM testView2; + `) + + await db.exec(` + CREATE OR REPLACE VIEW testView AS + SELECT * FROM testView1; + `) await db.exec(` - INSERT INTO testTable (number) - SELECT i*10 FROM generate_series(1, 5) i; - `) + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) let updatedResults const eventTarget = new EventTarget() @@ -388,21 +610,21 @@ await testEsmAndCjs(async (importType) => { }) it('live incremental query with params', async () => { - const db = new PGlite({ + const db = await PGlite.create({ extensions: { live }, }) await db.exec(` - CREATE TABLE IF NOT EXISTS testTable ( - id SERIAL PRIMARY KEY, - number INT - ); - `) + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) await db.exec(` - INSERT INTO testTable (number) - SELECT i*10 FROM generate_series(1, 5) i; - `) + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) let updatedResults const eventTarget = new EventTarget() @@ -474,21 +696,21 @@ await testEsmAndCjs(async (importType) => { }) it('basic live changes', async () => { - const db = new PGlite({ + const db = await PGlite.create({ extensions: { live }, }) await db.exec(` - CREATE TABLE IF NOT EXISTS testTable ( - id SERIAL PRIMARY KEY, - number INT - ); - `) + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) await db.exec(` - INSERT INTO testTable (number) - SELECT i*10 FROM generate_series(1, 5) i; - `) + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) let updatedChanges const eventTarget = new EventTarget() @@ -647,5 +869,120 @@ await testEsmAndCjs(async (importType) => { }, ]) }) + + it('subscribe to live query after creation', async () => { + const db = await PGlite.create({ + extensions: { live }, + }) + + await db.exec(` + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) + + await db.exec(` + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) + + const eventTarget = new EventTarget() + let updatedResults + + const { initialResults, subscribe, unsubscribe } = await db.live.query( + 'SELECT * FROM testTable ORDER BY number;', + ) + + expect(initialResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + // Subscribe after creation + subscribe((result) => { + updatedResults = result + eventTarget.dispatchEvent(new Event('change')) + }) + + db.exec('INSERT INTO testTable (number) VALUES (25);') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 6, number: 25 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + unsubscribe() + }) + + it('subscribe to live changes after creation', async () => { + const db = await PGlite.create({ + extensions: { live }, + }) + + await db.exec(` + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) + + await db.exec(` + INSERT INTO testTable (number) + SELECT i*10 FROM generate_series(1, 5) i; + `) + + const eventTarget = new EventTarget() + let updatedResults + + const { initialResults, subscribe, unsubscribe } = + await db.live.incrementalQuery( + 'SELECT * FROM testTable ORDER BY number;', + [], + 'id', + ) + + expect(initialResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + // Subscribe after creation + subscribe((result) => { + updatedResults = result + eventTarget.dispatchEvent(new Event('change')) + }) + + db.exec('INSERT INTO testTable (number) VALUES (25);') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([ + { id: 1, number: 10 }, + { id: 2, number: 20 }, + { id: 6, number: 25 }, + { id: 3, number: 30 }, + { id: 4, number: 40 }, + { id: 5, number: 50 }, + ]) + + unsubscribe() + }) }) }) From bd1b3b9ea1051c23f572f47a07eacab8c7acbe4e Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Mon, 14 Oct 2024 22:20:16 +0100 Subject: [PATCH 3/3] feat(pglite-react) Enable passing the return value of a live query directly to useLiveQuery (#375) * Enable passing the return value of a live query directly to * Add tests, and fix bug --- .changeset/early-windows-walk.md | 5 + .changeset/grumpy-turkeys-crash.md | 5 + packages/pglite-react/src/hooks.ts | 64 ++++- packages/pglite-react/src/provider.tsx | 6 +- packages/pglite-react/test/hooks.test.tsx | 111 ++++++++ packages/pglite/package.json | 1 - packages/pglite/src/live/index.ts | 8 +- packages/pglite/tests/live.test.ts | 108 ++++++++ pnpm-lock.yaml | 299 ---------------------- 9 files changed, 292 insertions(+), 315 deletions(-) create mode 100644 .changeset/early-windows-walk.md create mode 100644 .changeset/grumpy-turkeys-crash.md diff --git a/.changeset/early-windows-walk.md b/.changeset/early-windows-walk.md new file mode 100644 index 00000000..e5b3023c --- /dev/null +++ b/.changeset/early-windows-walk.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite-react': patch +--- + +Enable passing the return value of a live query directly to `useLiveQuery`. This allows you to create a live query in a react-router loader, then pass it to the route component where it is then attached with `useLiveQuery`. diff --git a/.changeset/grumpy-turkeys-crash.md b/.changeset/grumpy-turkeys-crash.md new file mode 100644 index 00000000..066cae65 --- /dev/null +++ b/.changeset/grumpy-turkeys-crash.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': patch +--- + +Fix a bug in live.incrementalQuery where if it was set to `limit 1` it would return no rows diff --git a/packages/pglite-react/src/hooks.ts b/packages/pglite-react/src/hooks.ts index 25f625ff..0ef6b654 100644 --- a/packages/pglite-react/src/hooks.ts +++ b/packages/pglite-react/src/hooks.ts @@ -1,5 +1,6 @@ import { useEffect, useState, useRef } from 'react' import { Results } from '@electric-sql/pglite' +import type { LiveQuery } from '@electric-sql/pglite/live' import { usePGlite } from './provider' import { query as buildQuery } from '@electric-sql/pglite/template' @@ -18,13 +19,20 @@ function paramsEqual( } function useLiveQueryImpl( - query: string, + query: string | LiveQuery | Promise>, params: unknown[] | undefined | null, key?: string, ): Omit, 'affectedRows'> | undefined { const db = usePGlite() - const [results, setResults] = useState>() const paramsRef = useRef(params) + const liveQueryRef = useRef | undefined>() + let liveQuery: LiveQuery | undefined + if (!(typeof query === 'string') && !(query instanceof Promise)) { + liveQuery = query + } + const [results, setResults] = useState | undefined>( + liveQuery?.initialResults, + ) let currentParams = paramsRef.current if (!paramsEqual(paramsRef.current, params)) { @@ -38,16 +46,39 @@ function useLiveQueryImpl( if (cancelled) return setResults(results) } - const ret = - key !== undefined - ? db.live.incrementalQuery(query, currentParams, key, cb) - : db.live.query(query, currentParams, cb) + if (typeof query === 'string') { + const ret = + key !== undefined + ? db.live.incrementalQuery(query, currentParams, key, cb) + : db.live.query(query, currentParams, cb) - return () => { - cancelled = true - ret.then(({ unsubscribe }) => unsubscribe()) + return () => { + cancelled = true + ret.then(({ unsubscribe }) => unsubscribe()) + } + } else if (query instanceof Promise) { + query.then((liveQuery) => { + if (cancelled) return + liveQueryRef.current = liveQuery + setResults(liveQuery.initialResults) + liveQuery.subscribe(cb) + }) + return () => { + cancelled = true + liveQueryRef.current?.unsubscribe(cb) + } + } else if (liveQuery) { + setResults(liveQuery.initialResults) + liveQuery.subscribe(cb) + return () => { + cancelled = true + liveQuery.unsubscribe(cb) + } + } else { + throw new Error('Should never happen') } - }, [db, key, query, currentParams]) + }, [db, key, query, currentParams, liveQuery]) + return ( results && { rows: results.rows, @@ -59,6 +90,19 @@ function useLiveQueryImpl( export function useLiveQuery( query: string, params?: unknown[] | null, +): Results | undefined + +export function useLiveQuery( + liveQuery: LiveQuery, +): Results + +export function useLiveQuery( + liveQueryPromise: Promise>, +): Results | undefined + +export function useLiveQuery( + query: string | LiveQuery | Promise>, + params?: unknown[] | null, ): Results | undefined { return useLiveQueryImpl(query, params) } diff --git a/packages/pglite-react/src/provider.tsx b/packages/pglite-react/src/provider.tsx index b55c32b4..e422dd31 100644 --- a/packages/pglite-react/src/provider.tsx +++ b/packages/pglite-react/src/provider.tsx @@ -20,11 +20,11 @@ interface PGliteProviderSet { function makePGliteProvider(): PGliteProviderSet { const ctx = createContext(undefined) return { - usePGlite: (db?: T) => { + usePGlite: ((db?: T) => { const dbProvided = useContext(ctx) // allow providing a db explicitly - if (db) return db + if (db !== undefined) return db if (!dbProvided) throw new Error( @@ -32,7 +32,7 @@ function makePGliteProvider(): PGliteProviderSet { ) return dbProvided - }, + }) as UsePGlite, PGliteProvider: ({ children, db }: Props) => { return {children} }, diff --git a/packages/pglite-react/test/hooks.test.tsx b/packages/pglite-react/test/hooks.test.tsx index 277bf78d..5102ed9f 100644 --- a/packages/pglite-react/test/hooks.test.tsx +++ b/packages/pglite-react/test/hooks.test.tsx @@ -264,5 +264,116 @@ function testLiveQuery(queryHook: 'useLiveQuery' | 'useLiveIncrementalQuery') { ]), ) }) + + if (queryHook !== 'useLiveQuery') { + return + } + + it('can take a live query return value directly', async () => { + await db.exec(` + CREATE TABLE live_test ( + id SERIAL PRIMARY KEY, + name TEXT + ); + `) + await db.exec(`INSERT INTO live_test (name) VALUES ('initial');`) + + const liveQuery = await db.live.query( + `SELECT * FROM live_test ORDER BY id DESC LIMIT 1;`, + ) + const { result } = renderHook(() => useLiveQuery(liveQuery), { wrapper }) + + await waitFor(() => expect(result.current?.rows).toHaveLength(1)) + expect(result.current?.rows[0]).toEqual({ id: 1, name: 'initial' }) + + // Trigger an update + await db.exec(`INSERT INTO live_test (name) VALUES ('updated');`) + await waitFor(() => expect(result.current?.rows[0].name).toBe('updated')) + expect(result.current?.rows[0]).toEqual({ id: 2, name: 'updated' }) + }) + + it('can take a live query returned promise directly', async () => { + await db.exec(` + CREATE TABLE live_test ( + id SERIAL PRIMARY KEY, + name TEXT + ); + `) + await db.exec(`INSERT INTO live_test (name) VALUES ('initial');`) + + const liveQueryPromise = db.live.query( + `SELECT * FROM live_test ORDER BY id DESC LIMIT 1;`, + ) + const { result } = renderHook(() => useLiveQuery(liveQueryPromise), { + wrapper, + }) + + expect(result.current).toBe(undefined) + + await waitFor(() => expect(result.current).not.toBe(undefined)) + + await waitFor(() => expect(result.current?.rows).toHaveLength(1)) + expect(result.current?.rows[0]).toEqual({ id: 1, name: 'initial' }) + + // Trigger an update + await db.exec(`INSERT INTO live_test (name) VALUES ('updated');`) + await waitFor(() => expect(result.current?.rows[0].name).toBe('updated')) + expect(result.current?.rows[0]).toEqual({ id: 2, name: 'updated' }) + }) + + it('can take a live incremental query return value directly', async () => { + await db.exec(` + CREATE TABLE live_test ( + id SERIAL PRIMARY KEY, + name TEXT + ); + `) + await db.exec(`INSERT INTO live_test (name) VALUES ('initial');`) + + const liveQuery = await db.live.incrementalQuery( + `SELECT * FROM live_test ORDER BY id DESC LIMIT 1;`, + [], + incKey, + ) + const { result } = renderHook(() => useLiveQuery(liveQuery), { wrapper }) + + await waitFor(() => expect(result.current?.rows).toHaveLength(1)) + expect(result.current?.rows[0]).toEqual({ id: 1, name: 'initial' }) + + // Trigger an update + await db.exec(`INSERT INTO live_test (name) VALUES ('updated');`) + await waitFor(() => expect(result.current?.rows[0].name).toBe('updated')) + expect(result.current?.rows[0]).toEqual({ id: 2, name: 'updated' }) + }) + + it('can take a live incremental query returned promise directly', async () => { + await db.exec(` + CREATE TABLE live_test ( + id SERIAL PRIMARY KEY, + name TEXT + ); + `) + await db.exec(`INSERT INTO live_test (name) VALUES ('initial');`) + + const liveQueryPromise = db.live.incrementalQuery( + `SELECT * FROM live_test ORDER BY id DESC LIMIT 1;`, + [], + incKey, + ) + const { result } = renderHook(() => useLiveQuery(liveQueryPromise), { + wrapper, + }) + + expect(result.current).toBe(undefined) + + await waitFor(() => expect(result.current).not.toBe(undefined)) + + await waitFor(() => expect(result.current?.rows).toHaveLength(1)) + expect(result.current?.rows[0]).toEqual({ id: 1, name: 'initial' }) + + // Trigger an update + await db.exec(`INSERT INTO live_test (name) VALUES ('updated');`) + await waitFor(() => expect(result.current?.rows[0].name).toBe('updated')) + }) }) } diff --git a/packages/pglite/package.json b/packages/pglite/package.json index 1651216d..d79fe0de 100644 --- a/packages/pglite/package.json +++ b/packages/pglite/package.json @@ -100,7 +100,6 @@ "concurrently": "^8.2.2", "http-server": "^14.1.1", "playwright": "^1.48.0", - "serve": "^14.2.3", "tinytar": "^0.1.0", "vitest": "^2.1.2" }, diff --git a/packages/pglite/src/live/index.ts b/packages/pglite/src/live/index.ts index 853210d1..6467cf0e 100644 --- a/packages/pglite/src/live/index.ts +++ b/packages/pglite/src/live/index.ts @@ -305,7 +305,7 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { await init() const refresh = async () => { - if (callbacks.length === 0) { + if (callbacks.length === 0 && changes) { return } let reset = false @@ -485,7 +485,11 @@ const setup = async (pg: PGliteInterface, _emscriptenOpts: any) => { case 'DELETE': { const oldObj = rowsMap.get(obj[key]) rowsMap.delete(obj[key]) - afterMap.delete(oldObj.__after__) + // null is the starting point, we don't delete it as another insert + // may have happened thats replacing it + if (oldObj.__after__ !== null) { + afterMap.delete(oldObj.__after__) + } break } case 'UPDATE': { diff --git a/packages/pglite/tests/live.test.ts b/packages/pglite/tests/live.test.ts index 39eaf89f..7c86771a 100644 --- a/packages/pglite/tests/live.test.ts +++ b/packages/pglite/tests/live.test.ts @@ -498,6 +498,48 @@ await testEsmAndCjs(async (importType) => { ]) }) + it('basic live incremental query with limit 1', async () => { + const db = await PGlite.create({ + extensions: { live }, + }) + + await db.exec(` + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) + + await db.exec(` + INSERT INTO testTable (number) VALUES (10); + `) + + let updatedResults + const eventTarget = new EventTarget() + + const { initialResults, unsubscribe } = await db.live.incrementalQuery( + 'SELECT * FROM testTable ORDER BY number ASC LIMIT 1;', + [], + 'id', + (result) => { + updatedResults = result + eventTarget.dispatchEvent(new Event('change')) + }, + ) + + expect(initialResults.rows).toEqual([{ id: 1, number: 10 }]) + + await db.exec('INSERT INTO testTable (number) VALUES (5);') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedResults.rows).toEqual([{ id: 2, number: 5 }]) + + unsubscribe() + }) + it('live incremental query on view', async () => { const db = await PGlite.create({ extensions: { live }, @@ -926,6 +968,72 @@ await testEsmAndCjs(async (importType) => { unsubscribe() }) + it('live changes limit 1', async () => { + const db = await PGlite.create({ + extensions: { live }, + }) + + await db.exec(` + CREATE TABLE IF NOT EXISTS testTable ( + id SERIAL PRIMARY KEY, + number INT + ); + `) + + await db.exec(` + INSERT INTO testTable (number) VALUES (10); + `) + + let updatedChanges + const eventTarget = new EventTarget() + + const { initialChanges, subscribe, unsubscribe } = await db.live.changes({ + query: 'SELECT * FROM testTable ORDER BY number ASC LIMIT 1;', + params: [], + key: 'id', + }) + + expect(initialChanges).toEqual([ + { + __op__: 'INSERT', + id: 1, + number: 10, + __after__: null, + __changed_columns__: [], + }, + ]) + + subscribe((changes) => { + updatedChanges = changes + eventTarget.dispatchEvent(new Event('change')) + }) + + await db.exec('INSERT INTO testTable (number) VALUES (5);') + + await new Promise((resolve) => + eventTarget.addEventListener('change', resolve, { once: true }), + ) + + expect(updatedChanges).toEqual([ + { + __op__: 'INSERT', + id: 2, + number: 5, + __after__: null, + __changed_columns__: [], + }, + { + __op__: 'DELETE', + id: 1, + number: null, + __after__: null, + __changed_columns__: [], + }, + ]) + + unsubscribe() + }) + it('subscribe to live changes after creation', async () => { const db = await PGlite.create({ extensions: { live }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f274284e..b9fc3b6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,9 +118,6 @@ importers: playwright: specifier: ^1.48.0 version: 1.48.0 - serve: - specifier: ^14.2.3 - version: 14.2.3 tinytar: specifier: ^0.1.0 version: 0.1.0 @@ -1654,17 +1651,10 @@ packages: '@vueuse/shared@11.1.0': resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} - '@zeit/schemas@2.36.0': - resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} - abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1707,9 +1697,6 @@ packages: algoliasearch@4.24.0: resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} - ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1745,12 +1732,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - arch@2.2.0: - resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1832,10 +1813,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - boxen@7.0.0: - resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} - engines: {node: '>=14.16'} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1862,7 +1839,6 @@ packages: bun@1.1.30: resolution: {integrity: sha512-ysRL1pq10Xba0jqVLPrKU3YIv0ohfp3cTajCPtpjCyppbn3lfiAVNpGoHfyaxS17OlPmWmR67UZRPw/EueQuug==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true @@ -1872,10 +1848,6 @@ packages: peerDependencies: esbuild: '>=0.18' - bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1888,10 +1860,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase@7.0.1: - resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} - engines: {node: '>=14.16'} - caniuse-lite@1.0.30001668: resolution: {integrity: sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==} @@ -1902,10 +1870,6 @@ packages: resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} engines: {node: '>=12'} - chalk-template@0.4.0: - resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} - engines: {node: '>=12'} - chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1914,10 +1878,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.0.1: - resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -1942,14 +1902,6 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - - clipboardy@3.0.0: - resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1991,14 +1943,6 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} - - compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} - computeds@0.0.1: resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} @@ -2020,10 +1964,6 @@ packages: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.2: - resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} - engines: {node: '>= 0.6'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2068,14 +2008,6 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2392,9 +2324,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-url-parser@1.1.3: - resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} - fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -2708,11 +2637,6 @@ packages: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2747,10 +2671,6 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-port-reachable@4.0.0: - resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -2798,10 +2718,6 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2976,22 +2892,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.33.0: - resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} - engines: {node: '>= 0.6'} - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.53.0: - resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.18: - resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -3054,9 +2958,6 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3077,10 +2978,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - node-abi@3.68.0: resolution: {integrity: sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==} engines: {node: '>=10'} @@ -3127,10 +3024,6 @@ packages: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} engines: {node: '>= 0.4'} - on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3208,9 +3101,6 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - path-is-inside@1.0.2: - resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3222,9 +3112,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@2.2.1: - resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -3402,9 +3289,6 @@ packages: pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} - punycode@1.4.1: - resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3419,10 +3303,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - range-parser@1.2.0: - resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} - engines: {node: '>= 0.6'} - rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -3465,13 +3345,6 @@ packages: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} - registry-auth-token@3.3.2: - resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==} - - registry-url@3.1.0: - resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} - engines: {node: '>=0.10.0'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3560,14 +3433,6 @@ packages: engines: {node: '>=10'} hasBin: true - serve-handler@6.1.5: - resolution: {integrity: sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==} - - serve@14.2.3: - resolution: {integrity: sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ==} - engines: {node: '>= 14'} - hasBin: true - set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3887,10 +3752,6 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} @@ -3940,9 +3801,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-check@1.5.4: - resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -3955,10 +3813,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -4154,10 +4008,6 @@ packages: engines: {node: '>=8'} hasBin: true - widest-line@4.0.1: - resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} - engines: {node: '>=12'} - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5841,15 +5691,8 @@ snapshots: - '@vue/composition-api' - vue - '@zeit/schemas@2.36.0': {} - abbrev@2.0.0: {} - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 @@ -5909,10 +5752,6 @@ snapshots: '@algolia/requester-node-http': 4.24.0 '@algolia/transporter': 4.24.0 - ansi-align@3.0.1: - dependencies: - string-width: 4.2.3 - ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -5938,10 +5777,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - arch@2.2.0: {} - - arg@5.0.2: {} - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -6020,17 +5855,6 @@ snapshots: boolbase@1.0.0: {} - boxen@7.0.0: - dependencies: - ansi-align: 3.0.1 - camelcase: 7.0.1 - chalk: 5.0.1 - cli-boxes: 3.0.0 - string-width: 5.1.2 - type-fest: 2.19.0 - widest-line: 4.0.1 - wrap-ansi: 8.1.0 - brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -6079,8 +5903,6 @@ snapshots: esbuild: 0.23.1 load-tsconfig: 0.2.5 - bytes@3.0.0: {} - cac@6.7.14: {} call-bind@1.0.7: @@ -6093,8 +5915,6 @@ snapshots: callsites@3.1.0: {} - camelcase@7.0.1: {} - caniuse-lite@1.0.30001668: {} ccount@2.0.1: {} @@ -6107,10 +5927,6 @@ snapshots: loupe: 3.1.2 pathval: 2.0.0 - chalk-template@0.4.0: - dependencies: - chalk: 4.1.2 - chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -6122,8 +5938,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.0.1: {} - character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -6148,14 +5962,6 @@ snapshots: ci-info@3.9.0: {} - cli-boxes@3.0.0: {} - - clipboardy@3.0.0: - dependencies: - arch: 2.2.0 - execa: 5.1.1 - is-wsl: 2.2.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -6200,22 +6006,6 @@ snapshots: compare-versions@6.1.1: {} - compressible@2.0.18: - dependencies: - mime-db: 1.53.0 - - compression@1.7.4: - dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - computeds@0.0.1: {} concat-map@0.0.1: {} @@ -6241,8 +6031,6 @@ snapshots: consola@3.2.3: {} - content-disposition@0.5.2: {} - convert-source-map@2.0.0: {} copy-anything@3.0.5: @@ -6284,10 +6072,6 @@ snapshots: de-indent@1.0.2: {} - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@3.2.7: dependencies: ms: 2.1.3 @@ -6760,10 +6544,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-url-parser@1.1.3: - dependencies: - punycode: 1.4.1 - fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -7094,8 +6874,6 @@ snapshots: dependencies: has-tostringtag: 1.0.2 - is-docker@2.2.1: {} - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -7124,8 +6902,6 @@ snapshots: is-path-inside@3.0.3: {} - is-port-reachable@4.0.0: {} - is-potential-custom-element-name@1.0.1: {} is-regex@1.1.4: @@ -7164,10 +6940,6 @@ snapshots: is-windows@1.0.2: {} - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - isarray@2.0.5: {} isexe@2.0.0: {} @@ -7352,16 +7124,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.33.0: {} - mime-db@1.52.0: {} - mime-db@1.53.0: {} - - mime-types@2.1.18: - dependencies: - mime-db: 1.33.0 - mime-types@2.1.35: dependencies: mime-db: 1.52.0 @@ -7415,8 +7179,6 @@ snapshots: mri@1.2.0: {} - ms@2.0.0: {} - ms@2.1.3: {} muggle-string@0.4.1: {} @@ -7433,8 +7195,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} - node-abi@3.68.0: dependencies: semver: 7.6.3 @@ -7475,8 +7235,6 @@ snapshots: has-symbols: 1.0.3 object-keys: 1.1.1 - on-headers@1.0.2: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -7546,8 +7304,6 @@ snapshots: path-is-absolute@1.0.1: {} - path-is-inside@1.0.2: {} - path-key@3.1.1: {} path-parse@1.0.7: {} @@ -7557,8 +7313,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@2.2.1: {} - path-type@4.0.0: {} pathe@1.1.2: {} @@ -7710,8 +7464,6 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 - punycode@1.4.1: {} - punycode@2.3.1: {} qs@6.13.0: @@ -7722,8 +7474,6 @@ snapshots: queue-microtask@1.2.3: {} - range-parser@1.2.0: {} - rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -7773,15 +7523,6 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 - registry-auth-token@3.3.2: - dependencies: - rc: 1.2.8 - safe-buffer: 5.2.1 - - registry-url@3.1.0: - dependencies: - rc: 1.2.8 - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -7866,33 +7607,6 @@ snapshots: semver@7.6.3: {} - serve-handler@6.1.5: - dependencies: - bytes: 3.0.0 - content-disposition: 0.5.2 - fast-url-parser: 1.1.3 - mime-types: 2.1.18 - minimatch: 3.1.2 - path-is-inside: 1.0.2 - path-to-regexp: 2.2.1 - range-parser: 1.2.0 - - serve@14.2.3: - dependencies: - '@zeit/schemas': 2.36.0 - ajv: 8.12.0 - arg: 5.0.2 - boxen: 7.0.0 - chalk: 5.0.1 - chalk-template: 0.4.0 - clipboardy: 3.0.0 - compression: 1.7.4 - is-port-reachable: 4.0.0 - serve-handler: 6.1.5 - update-check: 1.5.4 - transitivePeerDependencies: - - supports-color - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8208,8 +7922,6 @@ snapshots: type-fest@0.20.2: {} - type-fest@2.19.0: {} - typescript@5.4.2: {} typescript@5.6.3: {} @@ -8255,11 +7967,6 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.0 - update-check@1.5.4: - dependencies: - registry-auth-token: 3.3.2 - registry-url: 3.1.0 - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -8273,8 +7980,6 @@ snapshots: util-deprecate@1.0.2: {} - vary@1.1.2: {} - vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -8529,10 +8234,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - widest-line@4.0.1: - dependencies: - string-width: 5.1.2 - word-wrap@1.2.5: {} wrap-ansi@7.0.0: