diff --git a/.changeset/beige-snails-brush.md b/.changeset/beige-snails-brush.md new file mode 100644 index 0000000000..8b6dc13a40 --- /dev/null +++ b/.changeset/beige-snails-brush.md @@ -0,0 +1,5 @@ +--- +"@electric-sql/client": patch +--- + +Expose `shape.stream` as public readonly property. diff --git a/.changeset/tender-seas-stare.md b/.changeset/tender-seas-stare.md new file mode 100644 index 0000000000..ef18216bb1 --- /dev/null +++ b/.changeset/tender-seas-stare.md @@ -0,0 +1,5 @@ +--- +"@electric-sql/react": patch +--- + +Expose `stream` in the useShape result data. This allows React components to easily access the stream to match on. diff --git a/examples/nextjs-example/app/match-stream.ts b/examples/nextjs-example/app/match-stream.ts index 6c5372bf9b..c9b8b9e68e 100644 --- a/examples/nextjs-example/app/match-stream.ts +++ b/examples/nextjs-example/app/match-stream.ts @@ -23,6 +23,10 @@ export async function matchStream({ timeout?: number }): Promise> { return new Promise>((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(`matchStream timed out after ${timeout}ms`) + }, timeout) + const unsubscribe = stream.subscribe((messages) => { const message = messages.filter(isChangeMessage).find( (message) => @@ -32,18 +36,12 @@ export async function matchStream({ message: message, }) ) - if (message) return finish(message) - }) - const timeoutId = setTimeout(() => { - console.error(`matchStream timed out after ${timeout}ms`) - reject(`matchStream timed out after ${timeout}ms`) - }, timeout) - - function finish(message: ChangeMessage) { - clearTimeout(timeoutId) - unsubscribe() - return resolve(message) - } + if (message) { + clearTimeout(timeoutId) + unsubscribe() + return resolve(message) + } + }) }) } diff --git a/examples/write-patterns/.eslintrc.cjs b/examples/write-patterns/.eslintrc.cjs new file mode 100644 index 0000000000..b630f873ab --- /dev/null +++ b/examples/write-patterns/.eslintrc.cjs @@ -0,0 +1,41 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + parserOptions: { + ecmaVersion: 2022, + requireConfigFile: false, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + parser: '@typescript-eslint/parser', + plugins: ['prettier'], + rules: { + quotes: ['error', 'single'], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + }, + ignorePatterns: [ + '**/node_modules/**', + '**/dist/**', + 'tsup.config.ts', + 'vitest.config.ts', + '.eslintrc.js', + ], +} diff --git a/examples/write-patterns/.gitignore b/examples/write-patterns/.gitignore new file mode 100644 index 0000000000..d97f3f9c91 --- /dev/null +++ b/examples/write-patterns/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +.env.local diff --git a/examples/write-patterns/.prettierrc b/examples/write-patterns/.prettierrc new file mode 100644 index 0000000000..ad4d895523 --- /dev/null +++ b/examples/write-patterns/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/examples/write-patterns/README.md b/examples/write-patterns/README.md new file mode 100644 index 0000000000..64ad8635ed --- /dev/null +++ b/examples/write-patterns/README.md @@ -0,0 +1,71 @@ + +# Write patterns example + +This example implements and describes four different patterns for handling writes in an application built with [ElectricSQL](https://electric-sql.com). + +These patterns are described in the [Writes guide](https://electric-sql.com/docs/guides/writes) from the ElectricSQL documentation. It's worth reading the guide for context. The idea is that if you walk through these patterns in turn, you can get a sense of the range of techniques and their evolution in both power and complexity. + +The example is set up to run all the patterns together, in the page, at the same time, as components of a single React application. So you can also evaluate their behaviour side-by-side and and with different network connectivity. + +[![Screenshot of the application running](./public/screenshot.png)](https://write-patterns.electric-sql.com) + +You can see the example deployed and running online at: +https://write-patterns.examples.electric-sql.com + +## Patterns + +The main code is in the [`./patterns`](./patterns) folder, which has a subfolder for each pattern. There's also some shared code, including an API server and some app boilerplate in [`./shared`](./shared). + +All of the patterns use [Electric](https://electric-sql.com/product/sync) for the read-path (i.e.: syncing data from Postgres into the local app) and implement a different approach to the write-path (i.e.: how they handle local writes and get data from the local app back into Postgres). + +### [1. Online writes](./patterns/1-online-writes) + +The first pattern is in [`./patterns/1-online-writes`](./patterns/1-online-writes). + +This is the simplest approach, which just sends writes to an API and only works if you're online. It has a resilient client that will retry in the event of network failure but the app doesn't update until the write goes through. + +### [2. Optimistic state](./patterns/2-optimistic-state) + +The second pattern is in [`./patterns/2-optimistic-state`](./patterns/2-optimistic-state). + +It extends the first pattern with support for local offline writes with simple optimistic state. The optimistic state is "simple" in the sense that it's only available within the component that makes the write and it's not persisted if the page reloads or the component unmounts. + +### [3. Shared persistent optimistic state](./patterns/3-shared-persistent) + +The third pattern is in [`./patterns/3-shared-persistent`](./patterns/3-shared-persistent). + +It extends the second pattern by storing the optimistic state in a shared, persistent local store. This makes offline writes more resilient and avoids components getting out of sync. It's a compelling point in the design space: providing good UX and DX without introducing too much complexity or any heavy dependencies. + +### [4. Through-the-database sync](./patterns/4-database-sync) + +The fourth pattern is in [`./patterns/4-database-sync`](./patterns/4-database-sync). + +It extends the concept of shared, persistent optimistic state all the way to a local embedded database. Specifically, it: + +1. syncs data from Electric into an immutable table +2. persists local optimistic state in a shadow table +2. combines the two into a view that provides a unified interface for reads and writes +4. automatically detects local changes and syncs them to the server + +This provides a pure local-first development experience, where the application code talks directly to a single database "table" and changes sync automatically in the background. However, this "power" does come at the cost of increased complexity in the form of an embedded database, complex local schema and loss of context when handling rollbacks. + +## How to run + +Make sure you've installed all dependencies for the monorepo and built the packages (from the monorepo root directory): + +```shell +pnpm install +pnpm run -r build +``` + +Start the docker containers (in this directory): + +```shell +pnpm backend:up +``` + +Start the dev server: + +```shell +pnpm dev +``` diff --git a/examples/write-patterns/index.html b/examples/write-patterns/index.html new file mode 100644 index 0000000000..b9b77ebd17 --- /dev/null +++ b/examples/write-patterns/index.html @@ -0,0 +1,16 @@ + + + + + + Write Patterns - ElectricSQL + + + + + + +
+ + + diff --git a/examples/write-patterns/package.json b/examples/write-patterns/package.json new file mode 100644 index 0000000000..fa1108790b --- /dev/null +++ b/examples/write-patterns/package.json @@ -0,0 +1,59 @@ +{ + "name": "@electric-examples/write-patterns", + "private": true, + "version": "0.0.1", + "author": "ElectricSQL", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/electric-sql/electric/issues" + }, + "type": "module", + "scripts": { + "backend:up": "PROJECT_NAME=write-patterns pnpm -C ../../ run example-backend:up && pnpm db:migrate", + "backend:down": "PROJECT_NAME=write-patterns pnpm -C ../../ run example-backend:down", + "db:migrate": "dotenv -e ../../.env.dev -- pnpm exec pg-migrations apply --directory ./shared/migrations", + "dev": "concurrently \"vite\" \"node shared/backend/api.js\"", + "build": "vite build", + "format": "eslint . --ext ts,tsx --fix", + "stylecheck": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@electric-sql/client": "workspace:*", + "@electric-sql/experimental": "workspace:*", + "@electric-sql/pglite": "^0.2.14", + "@electric-sql/pglite-react": "^0.2.14", + "@electric-sql/pglite-sync": "^0.2.16", + "@electric-sql/react": "workspace:*", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "express": "^4.19.2", + "pg": "^8.12.0", + "react": "19.0.0-rc.1", + "react-dom": "19.0.0-rc.1", + "uuid": "^10.0.0", + "valtio": "^2.1.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@databases/pg-migrations": "^5.0.3", + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^4.3.1", + "concurrently": "^8.2.2", + "dotenv": "^16.4.5", + "eslint": "^8.57.0", + "rollup": "2.79.2", + "typescript": "^5.5.3", + "vite": "^5.3.4", + "vite-plugin-pwa": "^0.21.0" + }, + "overrides": { + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "react": "19.0.0-rc.1", + "react-dom": "19.0.0-rc.1" + } +} diff --git a/examples/write-patterns/patterns/1-online-writes/README.md b/examples/write-patterns/patterns/1-online-writes/README.md new file mode 100644 index 0000000000..d1dd60f04c --- /dev/null +++ b/examples/write-patterns/patterns/1-online-writes/README.md @@ -0,0 +1,32 @@ + +# Online writes pattern + +This is an example of an application using: + +- Electric for read-path sync, to sync data from into a local app +- online writes to write data back into Postgres from the local app + +It's the simplest of the [write-patterns](https://electric-sql.com/docs/guides/writes#patterns) introduced in the [Writes](https://electric-sql.com/docs/guides/writes#patterns) guide. + +> [!TIP] Other examples +> The [Phoenix LiveView example](../../../phoenix-liveview) also implements this pattern — using Electric to stream data into the LiveView client and normal Phoenix APIs to handle writes. + +## Benefits + +It's very simple to implement. It allows you [use your existing API](https://electric-sql.com/blog/2024/11/21/local-first-with-your-existing-api). It allows you to create apps that are fast and available offline for reading data. + +Good use-cases include: + +- live dashboards, data analytics and data visualisation +- AI applications that generate embeddings in the cloud +- systems where writes require online integration anyway, e.g.: making payments + +## Drawbacks + +You have the network on the write path — slow, laggy, loading spinners. + +Interactive applications won't work offline without implementing [optimistic writes with local optimistic state](../2-optimistic-state). + +## How to run + +See the [How to run](../../README.md#how-to-run) section in the example README. diff --git a/examples/write-patterns/patterns/1-online-writes/index.tsx b/examples/write-patterns/patterns/1-online-writes/index.tsx new file mode 100644 index 0000000000..10d9b79938 --- /dev/null +++ b/examples/write-patterns/patterns/1-online-writes/index.tsx @@ -0,0 +1,109 @@ +import React from 'react' +import { v4 as uuidv4 } from 'uuid' +import { useShape } from '@electric-sql/react' +import api from '../../shared/app/client' + +const ELECTRIC_URL = import.meta.env.ELECTRIC_URL || 'http://localhost:3000' + +type Todo = { + id: string + title: string + completed: boolean + created_at: Date +} + +export default function OnlineWrites() { + // Use Electric's `useShape` hook to sync data from Postgres + // into a React state variable. + const { isLoading, data } = useShape({ + url: `${ELECTRIC_URL}/v1/shape`, + params: { + table: 'todos', + }, + parser: { + timestamptz: (value: string) => new Date(value), + }, + }) + + const todos = data ? data.sort((a, b) => +a.created_at - +b.created_at) : [] + + // Handle user input events by making requests to the backend + // API to create, update and delete todos. + + async function createTodo(event: React.FormEvent) { + event.preventDefault() + + const form = event.target as HTMLFormElement + const formData = new FormData(form) + const title = formData.get('todo') as string + + const path = '/todos' + const data = { + id: uuidv4(), + title: title, + created_at: new Date(), + } + + await api.request(path, 'POST', data) + + form.reset() + } + + async function updateTodo(todo: Todo) { + const path = `/todos/${todo.id}` + + const data = { + completed: !todo.completed, + } + + await api.request(path, 'PUT', data) + } + + async function deleteTodo(event: React.MouseEvent, todo: Todo) { + event.preventDefault() + + const path = `/todos/${todo.id}` + + await api.request(path, 'DELETE') + } + + if (isLoading) { + return
Loading …
+ } + + // prettier-ignore + return ( +
+

1. Online writes

+
    + {todos.map((todo) => ( +
  • + + deleteTodo(event, todo)}> + ✕ +
  • + ))} + {todos.length === 0 && ( +
  • All done 🎉
  • + )} +
+
+ + +
+
+ ) +} diff --git a/examples/write-patterns/patterns/2-optimistic-state/README.md b/examples/write-patterns/patterns/2-optimistic-state/README.md new file mode 100644 index 0000000000..cd52e3d7ec --- /dev/null +++ b/examples/write-patterns/patterns/2-optimistic-state/README.md @@ -0,0 +1,31 @@ + +# Optimistic state pattern + +This is an example of an application using: + +- Electric for read-path sync, to sync data from into a local app +- local-optimistic writes using React's built-in [`useOptimistic`](https://react.dev/reference/react/useOptimistic) hook + +This allows writes to be displayed locally immediately, by merging temporary optimistic state into the synced todo list before rendering. If the app (or API) is offline, then the writes are retried following a backoff algorithm and should eventually succeed when the app (or API) comes back online. + +When the writes do succeed, they are automatically synced back to the app via Electric and the local optimistic state is discarded. + +## Benefits + +Simple to implement. Allows you [use your existing API](https://electric-sql.com/blog/2024/11/21/local-first-with-your-existing-api). Takes the network off the write path. Allows you to create apps that are fast and available offline for both reading and writing data. + +Good use-cases include: + +- management apps and interactive dashboards +- apps that want to feel fast and avoid loading spinners on write +- mobile apps that want to be resilient to patchy connectivity + +## Drawbacks + +The optimistic state is only available within the component that makes the write. This means that other components rendering the same state may not see it and may display stale data. The optimistic state is also not peristent. So it's lost if you unmount the component or reload the page. + +These limitations are addressed by the [shared persistent optimistic state](../../3-shared-persistent) pattern. + +## How to run + +See the [How to run](../../README.md#how-to-run) section in the example README. diff --git a/examples/write-patterns/patterns/2-optimistic-state/index.tsx b/examples/write-patterns/patterns/2-optimistic-state/index.tsx new file mode 100644 index 0000000000..1c8a3d221e --- /dev/null +++ b/examples/write-patterns/patterns/2-optimistic-state/index.tsx @@ -0,0 +1,193 @@ +import React, { useOptimistic, useTransition } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { matchBy, matchStream } from '@electric-sql/experimental' +import { useShape } from '@electric-sql/react' +import api from '../../shared/app/client' + +const ELECTRIC_URL = import.meta.env.ELECTRIC_URL || 'http://localhost:3000' + +type Todo = { + id: string + title: string + completed: boolean + created_at: Date +} +type PartialTodo = Partial & { + id: string +} + +type Write = { + operation: 'insert' | 'update' | 'delete' + value: PartialTodo +} + +export default function OptimisticState() { + const [isPending, startTransition] = useTransition() + + // Use Electric's `useShape` hook to sync data from Postgres + // into a React state variable. + // + // Note that we also unpack the `stream` from the useShape + // return value, so that we can monitor it below to detect + // local writes syncing back from the server. + const { isLoading, data, stream } = useShape({ + url: `${ELECTRIC_URL}/v1/shape`, + params: { + table: 'todos', + }, + parser: { + timestamptz: (value: string) => new Date(value), + }, + }) + + const sorted = data ? data.sort((a, b) => +a.created_at - +b.created_at) : [] + + // Use React's built in `useOptimistic` hook. This provides + // a mechanism to apply local optimistic state whilst writes + // are being sent-to and syncing-back-from the server. + const [todos, addOptimisticState] = useOptimistic( + sorted, + (synced: Todo[], { operation, value }: Write) => { + switch (operation) { + case 'insert': + return synced.some((todo) => todo.id === value.id) + ? synced + : [...synced, value as Todo] + + case 'update': + return synced.map((todo) => + todo.id === value.id ? { ...todo, ...value } : todo + ) + + case 'delete': + return synced.filter((todo) => todo.id !== value.id) + } + } + ) + + // These are the same event handler functions from the online + // example, extended with `startTransition` -> `addOptimisticState` + // to apply local optimistic state. + // + // Note that the local state is applied: + // + // 1. whilst the HTTP request is being made to the API server; and + // 2. until the write syncs back through the Electric shape stream + // + // This is slightly different from most optimistic state examples + // because we wait for the sync as well as the api request. + + async function createTodo(event: React.FormEvent) { + event.preventDefault() + + const form = event.target as HTMLFormElement + const formData = new FormData(form) + const title = formData.get('todo') as string + + const path = '/todos' + const data = { + id: uuidv4(), + title: title, + created_at: new Date(), + completed: false, + } + + startTransition(async () => { + addOptimisticState({ operation: 'insert', value: data }) + + const fetchPromise = api.request(path, 'POST', data) + const syncPromise = matchStream( + stream, + ['insert'], + matchBy('id', data.id) + ) + + await Promise.all([fetchPromise, syncPromise]) + }) + + form.reset() + } + + async function updateTodo(todo: Todo) { + const { id, completed } = todo + + const path = `/todos/${id}` + const data = { + id, + completed: !completed, + } + + startTransition(async () => { + addOptimisticState({ operation: 'update', value: data }) + + const fetchPromise = api.request(path, 'PUT', data) + const syncPromise = matchStream(stream, ['update'], matchBy('id', id)) + + await Promise.all([fetchPromise, syncPromise]) + }) + } + + async function deleteTodo(event: React.MouseEvent, todo: Todo) { + event.preventDefault() + + const { id } = todo + + const path = `/todos/${id}` + + startTransition(async () => { + addOptimisticState({ operation: 'delete', value: { id } }) + + const fetchPromise = api.request(path, 'DELETE') + const syncPromise = matchStream(stream, ['delete'], matchBy('id', id)) + + await Promise.all([fetchPromise, syncPromise]) + }) + } + + if (isLoading) { + return
Loading …
+ } + + // The template below the heading is identical to the other patterns. + + // prettier-ignore + return ( +
+

+ + 2. Optimistic state + + +

+
    + {todos.map((todo) => ( +
  • + + deleteTodo(event, todo)}> + ✕ +
  • + ))} + {todos.length === 0 && ( +
  • All done 🎉
  • + )} +
+
+ + +
+
+ ) +} diff --git a/examples/write-patterns/patterns/3-shared-persistent/README.md b/examples/write-patterns/patterns/3-shared-persistent/README.md new file mode 100644 index 0000000000..8b1951dcdf --- /dev/null +++ b/examples/write-patterns/patterns/3-shared-persistent/README.md @@ -0,0 +1,42 @@ + +# Shared persistent optimistic state pattern + +This is an example of an application using: + +- Electric for read-path sync +- local optimistic writes with shared, persistent optimistic state + +This pattern can be implemented with a variety of client-side state management and storage mechanisms. This example uses [valtio](https://valtio.dev) for a shared reactive store and persists this store to localStorage on any change. This allows us to keep the code very similar to the previous [`../2-optimistic-state`](../2-optimistic-state) pattern (with a valtio `useSnapshot` and a custom reduce function playing almost exactly the same role as the React `useOptimistic` hook). + +## Benefits + +This is a powerful and pragmatic pattern, occupying a compelling point in the design space. It's relatively simple to implement. Persisting optimistic state makes local writes more resilient. + +Storing optimistic state in a shared store allows all your components to see and react to it. This avoids one of the weaknesses with component-scoped optimistic state with a [more naive optimistic state pattern](../2-optimistic-state) and makes this pattern more suitable for more complex, real world apps. + +Seperating immutable synced state from mutable local state makes it easy to reason about and implement rollback strategies. The entrypoint for handling rollbacks has the local write context as well as the shared store, so it's easy to make rollbacks relatively surgical. + +Good use-cases include: + +- building local-first software +- interactive SaaS applications +- collaboration and authoring software + +## Drawbacks + +Combining data on-read makes local reads slightly slower. + +Writes are still made via an API. This can often be helpful and pragmatic, allowing you to [re-use your existing API](https://electric-sql.com/blog/2024/11/21/local-first-with-your-existing-api). However, you may want to avoid running an API and leverage [through the DB sync](../4-through-the-db) for a purer local-first approach. + +## Implementation notes + +The merge logic in the `matchWrite` function supports rebasing local optimistic state on concurrent updates from other users. + +This differs from the previous optimistic state example, in that it matches inserts and updates on the `write_id`, rather than the `id`. This means that concurrent updates to the same row will not +clear the optimistic state, which allows it to be rebased on changes made concurrently to the same data by other users. + +Note that we still match deletes by `id`, because delete operations can't update the `write_id` column. If you'd like to support revertable concurrent deletes, you can use soft deletes (which are obviously actually updates). + +## How to run + +See the [How to run](../../README.md#how-to-run) section in the example README. diff --git a/examples/write-patterns/patterns/3-shared-persistent/index.tsx b/examples/write-patterns/patterns/3-shared-persistent/index.tsx new file mode 100644 index 0000000000..4c3c5b6f45 --- /dev/null +++ b/examples/write-patterns/patterns/3-shared-persistent/index.tsx @@ -0,0 +1,258 @@ +import React, { useTransition } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { subscribe, useSnapshot } from 'valtio' +import { proxyMap } from 'valtio/utils' + +import { type Operation, ShapeStream } from '@electric-sql/client' +import { matchBy, matchStream } from '@electric-sql/experimental' +import { useShape } from '@electric-sql/react' + +import api from '../../shared/app/client' + +const ELECTRIC_URL = import.meta.env.ELECTRIC_URL || 'http://localhost:3000' +const KEY = 'electric-sql/examples/write-patterns/shared-persistent' + +type Todo = { + id: string + title: string + completed: boolean + created_at: Date +} +type PartialTodo = Partial & { + id: string +} + +type LocalWrite = { + id: string + operation: Operation + value: PartialTodo +} + +// Define a shared, persistent, reactive store for local optimistic state. +const optimisticState = proxyMap( + JSON.parse(localStorage.getItem(KEY) || '[]') +) +subscribe(optimisticState, () => { + localStorage.setItem(KEY, JSON.stringify([...optimisticState])) +}) + +/* + * Add a local write to the optimistic state + */ +function addLocalWrite(operation: Operation, value: PartialTodo): LocalWrite { + const id = uuidv4() + + const write: LocalWrite = { + id, + operation, + value, + } + + optimisticState.set(id, write) + + return write +} + +/* + * Subscribe to the shape `stream` until the local write syncs back through it. + * At which point, delete the local write from the optimistic state. + */ +async function matchWrite( + stream: ShapeStream, + write: LocalWrite +): Promise { + const { operation, value } = write + + const matchFn = + operation === 'delete' + ? matchBy('id', value.id) + : matchBy('write_id', write.id) + + try { + await matchStream(stream, [operation], matchFn) + } catch (_err) { + return + } + + optimisticState.delete(write.id) +} + +/* + * Make an HTTP request to send the write to the API server. + * If the request fails, delete the local write from the optimistic state. + * If it succeeds, return the `txid` of the write from the response data. + */ +async function sendRequest( + path: string, + method: string, + { id, value }: LocalWrite +): Promise { + const data = { + ...value, + write_id: id, + } + + let response: Response | undefined + try { + response = await api.request(path, method, data) + } catch (err) { + // ignore + } + + if (response === undefined || !response.ok) { + optimisticState.delete(id) + } +} + +export default function SharedPersistent() { + const [isPending, startTransition] = useTransition() + + // Use Electric's `useShape` hook to sync data from Postgres. + const { isLoading, data, stream } = useShape({ + url: `${ELECTRIC_URL}/v1/shape`, + params: { + table: 'todos', + }, + parser: { + timestamptz: (value: string) => new Date(value), + }, + }) + + const sorted = data ? data.sort((a, b) => +a.created_at - +b.created_at) : [] + + // Get the local optimistic state. + const localWrites = useSnapshot>(optimisticState) + + // Merge the synced state with the local state. + const todos = localWrites + .values() + .reduce((synced: Todo[], { operation, value }: LocalWrite) => { + switch (operation) { + case 'insert': + return synced.some((todo) => todo.id === value.id) + ? synced + : [...synced, value as Todo] + + case 'update': + return synced.map((todo) => + todo.id === value.id ? { ...todo, ...value } : todo + ) + + case 'delete': + return synced.filter((todo) => todo.id !== value.id) + } + }, sorted) + + // These are the same event handler functions from the previous optimistic + // state pattern, adapted to add the state to the shared, persistent store. + + async function createTodo(event: React.FormEvent) { + event.preventDefault() + + const form = event.target as HTMLFormElement + const formData = new FormData(form) + const title = formData.get('todo') as string + + const path = '/todos' + const data = { + id: uuidv4(), + title: title, + completed: false, + created_at: new Date(), + } + + startTransition(async () => { + const write = addLocalWrite('insert', data) + const fetchPromise = sendRequest(path, 'POST', write) + const syncPromise = matchWrite(stream, write) + + await Promise.all([fetchPromise, syncPromise]) + }) + + form.reset() + } + + async function updateTodo(todo: Todo) { + const { id, completed } = todo + + const path = `/todos/${id}` + const data = { + id, + completed: !completed, + } + + startTransition(async () => { + const write = addLocalWrite('update', data) + const fetchPromise = sendRequest(path, 'PUT', write) + const syncPromise = matchWrite(stream, write) + + await Promise.all([fetchPromise, syncPromise]) + }) + } + + async function deleteTodo(event: React.MouseEvent, todo: Todo) { + event.preventDefault() + + const { id } = todo + + const path = `/todos/${id}` + const data = { + id, + } + + startTransition(async () => { + const write = addLocalWrite('delete', data) + const fetchPromise = sendRequest(path, 'DELETE', write) + const syncPromise = matchWrite(stream, write) + + await Promise.all([fetchPromise, syncPromise]) + }) + } + + if (isLoading) { + return
Loading …
+ } + + // The template below the heading is identical to the other patterns. + + // prettier-ignore + return ( +
+

+ + 3. Shared persistent + + +

+
    + {todos.map((todo) => ( +
  • + + deleteTodo(event, todo)}> + ✕ +
  • + ))} + {todos.length === 0 && ( +
  • All done 🎉
  • + )} +
+
+ + +
+
+ ) +} diff --git a/examples/write-patterns/patterns/4-through-the-db/README.md b/examples/write-patterns/patterns/4-through-the-db/README.md new file mode 100644 index 0000000000..726e396243 --- /dev/null +++ b/examples/write-patterns/patterns/4-through-the-db/README.md @@ -0,0 +1,51 @@ + +# Through-the-database sync pattern + +This is an example of an application using: + +- Electric for read-path sync +- local reads and writes to and from a single DB table +- shared, persistent optimistic state +- automatic change detection and background sync + +The implementation uses a local embedded [PGlite](https://electric-sql.com/product/pglite) database, to store both synced and local optimistic state. It automatically manages optimistic state lifecycle, presents a single table interface for reads and writes and auto-syncs the local writes. + +Specifically, we: + +1. sync data into an immutable table +2. persist optimistic state in a shadow table +3. combine the two on read using a view + +Plus for the write path sync, we: + +4. detect local writes +5. write them into a change log table +6. POST the changes to the API server + +## Benefits + +This provides full offline support, shared optimistic state and allows your components to interact purely with the local database. No coding over the network is needed. Data fetching and sending is abstracted away behind the Electric sync (for reads) and the change message log (for writes). + +Good use-cases include: + +- building local-first software +- mobile and desktop applications +- collaboration and authoring software + +## Drawbacks + +Using a local embedded database adds a relatively-heavy dependency to your app. The shadow table and trigger machinery complicate your client side schema definition. + +Syncing changes in the background complicates any potential rollback handling. In the [shared persistent optimistic state](../../3-shared-persistent) pattern, you can detect a write being rejected by the server whilst still in context, handling user input. With through the database sync, this context is harder to reconstruct. + +## Implementation notes + +The merge logic in the `delete_local_on_synced_insert_and_update_trigger` in [`./local-schema.sql`](./local-schema.sql) supports rebasing local optimistic state on concurrent updates from other users. + +The rollback strategy in the `rollback` method of the `ChangeLogSynchronizer` in [`./sync.ts`](./sync.ts) is very naive: clearing all local state and writes in the event of any write being rejected by the server. You may want to implement a more nuanced strategy. For example, to provide information to the user about what is happening and / or minimise data loss by only clearing local-state that's causally dependent on a rejected write. + +This opens the door to a lot of complexity that may best be addressed by using an existing framework. See the [Writes guide](https://electric-sql.com/docs/guides/writes) for more information and links to [existing frameworks](https://electric-sql.com/docs/guides/writes#tools). + +## How to run + +See the [How to run](../../README.md#how-to-run) section in the example README. diff --git a/examples/write-patterns/patterns/4-through-the-db/db.ts b/examples/write-patterns/patterns/4-through-the-db/db.ts new file mode 100644 index 0000000000..3ac8c56162 --- /dev/null +++ b/examples/write-patterns/patterns/4-through-the-db/db.ts @@ -0,0 +1,43 @@ +import { PGlite } from '@electric-sql/pglite' +import { type PGliteWithLive, live } from '@electric-sql/pglite/live' +import { electricSync } from '@electric-sql/pglite-sync' + +import localSchemaMigrations from './local-schema.sql?raw' + +const DATA_DIR = 'idb://electric-write-patterns-example' +const ELECTRIC_URL = import.meta.env.ELECTRIC_URL || 'http://localhost:3000' + +const registry = new Map>() + +export default async function loadPGlite(): Promise { + const loadingPromise = registry.get('loadingPromise') + + if (loadingPromise === undefined) { + registry.set('loadingPromise', _loadPGlite()) + } + + return loadingPromise as Promise +} + +async function _loadPGlite(): Promise { + const pglite: PGliteWithLive = await PGlite.create(DATA_DIR, { + extensions: { + electric: electricSync(), + live, + }, + }) + + await pglite.exec(localSchemaMigrations) + + await pglite.electric.syncShapeToTable({ + shape: { + url: `${ELECTRIC_URL}/v1/shape`, + table: 'todos', + }, + shapeKey: 'todos', + table: 'todos_synced', + primaryKey: ['id'], + }) + + return pglite +} diff --git a/examples/write-patterns/patterns/4-through-the-db/index.tsx b/examples/write-patterns/patterns/4-through-the-db/index.tsx new file mode 100644 index 0000000000..05f17a7af4 --- /dev/null +++ b/examples/write-patterns/patterns/4-through-the-db/index.tsx @@ -0,0 +1,167 @@ +import React, { useEffect, useState } from 'react' +import { v4 as uuidv4 } from 'uuid' + +import { + PGliteProvider, + useLiveQuery, + usePGlite, +} from '@electric-sql/pglite-react' +import { type PGliteWithLive } from '@electric-sql/pglite/live' + +import loadPGlite from './db' +import ChangeLogSynchronizer from './sync' + +type Todo = { + id: string + title: string + completed: boolean + created_at: Date +} + +/* + * Setup the local PGlite database, with automatic change detection and syncing. + * + * See `./local-schema.sql` for the local database schema, including view + * and trigger machinery. + * + * See `./sync.ts` for the write-path sync utility, which listens to changes + * using pg_notify, as per https://pglite.dev/docs/api#listen + */ +export default function Wrapper() { + const [db, setDb] = useState() + + useEffect(() => { + let isMounted = true + let writePathSync: ChangeLogSynchronizer + + async function init() { + const pglite = await loadPGlite() + + if (!isMounted) { + return + } + + writePathSync = new ChangeLogSynchronizer(pglite) + writePathSync.start() + + setDb(pglite) + } + + init() + + return () => { + isMounted = false + + if (writePathSync !== undefined) { + writePathSync.stop() + } + } + }, []) + + if (db === undefined) { + return
Loading …
+ } + + return ( + + + + ) +} + +function ThroughTheDB() { + const db = usePGlite() + const results = useLiveQuery('SELECT * FROM todos ORDER BY created_at') + + async function createTodo(event: React.FormEvent) { + event.preventDefault() + + const form = event.target as HTMLFormElement + const formData = new FormData(form) + const title = formData.get('todo') as string + + await db.sql` + INSERT INTO todos ( + id, + title, + completed, + created_at + ) + VALUES ( + ${uuidv4()}, + ${title}, + ${false}, + ${new Date()} + ) + ` + + form.reset() + } + + async function updateTodo(todo: Todo) { + const { id, completed } = todo + + await db.sql` + UPDATE todos + SET completed = ${!completed} + WHERE id = ${id} + ` + } + + async function deleteTodo(event: React.MouseEvent, todo: Todo) { + event.preventDefault() + + await db.sql` + DELETE FROM todos + WHERE id = ${todo.id} + ` + } + + if (results === undefined) { + return
Loading …
+ } + + const todos = results.rows + + // The template below the heading is identical to the other patterns. + + // prettier-ignore + return ( +
+

+ + 4. Through the DB + +

+
    + {todos.map((todo: Todo) => ( +
  • + + deleteTodo(event, todo)}> + ✕ +
  • + ))} + {todos.length === 0 && ( +
  • All done 🎉
  • + )} +
+
+ + +
+
+ ) +} diff --git a/examples/write-patterns/patterns/4-through-the-db/local-schema.sql b/examples/write-patterns/patterns/4-through-the-db/local-schema.sql new file mode 100644 index 0000000000..d20a2133e1 --- /dev/null +++ b/examples/write-patterns/patterns/4-through-the-db/local-schema.sql @@ -0,0 +1,345 @@ +-- This is the local database schema for PGlite. + +-- It uses two tables: `todos_synced` and `todos_local`. These are combined +-- into a `todos` view that provides a merged view on both tables and supports +-- local live queries. Writes to the `todos` view are redirected using +-- `INSTEAD OF` triggers to the `todos_local` and `changes` tables. + +-- The `todos_synced` table for immutable, synced state from the server. +CREATE TABLE IF NOT EXISTS todos_synced ( + id UUID PRIMARY KEY, + title TEXT NOT NULL, + completed BOOLEAN NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + -- Bookkeeping column. + write_id UUID +); + +-- The `todos_local` table for local optimistic state. +CREATE TABLE IF NOT EXISTS todos_local ( + id UUID PRIMARY KEY, + title TEXT, + completed BOOLEAN, + created_at TIMESTAMP WITH TIME ZONE, + -- Bookkeeping columns. + changed_columns TEXT[], + is_deleted BOOLEAN NOT NULL DEFAULT FALSE, + write_id UUID NOT NULL +); + +-- The `todos` view to combine the two tables on read. +CREATE OR REPLACE VIEW todos AS + SELECT + COALESCE(local.id, synced.id) AS id, + CASE + WHEN 'title' = ANY(local.changed_columns) + THEN local.title + ELSE synced.title + END AS title, + CASE + WHEN 'completed' = ANY(local.changed_columns) + THEN local.completed + ELSE synced.completed + END AS completed, + CASE + WHEN 'created_at' = ANY(local.changed_columns) + THEN local.created_at + ELSE synced.created_at + END AS created_at + FROM todos_synced AS synced + FULL OUTER JOIN todos_local AS local + ON synced.id = local.id + WHERE local.id IS NULL OR local.is_deleted = FALSE; + +-- Triggers to automatically remove local optimistic state when the corresponding +-- row syncs over the replication stream. Match on `write_id`, to allow local +-- state to be rebased on concurrent changes to the same row. +CREATE OR REPLACE FUNCTION delete_local_on_synced_insert_and_update_trigger() +RETURNS TRIGGER AS $$ +BEGIN + DELETE FROM todos_local + WHERE id = NEW.id + AND write_id IS NOT NULL + AND write_id = NEW.write_id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- N.b.: deletes can be concurrent, but can't update the `write_id` and aren't +-- revertable (once a row is deleted, it would be re-created with an insert), +-- so its safe to just match on ID. You could implement revertable concurrent +-- deletes using soft deletes (which are actually updates). +CREATE OR REPLACE FUNCTION delete_local_on_synced_delete_trigger() +RETURNS TRIGGER AS $$ +BEGIN + DELETE FROM todos_local WHERE id = OLD.id; + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER delete_local_on_synced_insert +AFTER INSERT ON todos_synced +FOR EACH ROW +EXECUTE FUNCTION delete_local_on_synced_insert_trigger(); + +CREATE OR REPLACE TRIGGER delete_local_on_synced_insert_and_update +AFTER UPDATE ON todos_synced +FOR EACH ROW +EXECUTE FUNCTION delete_local_on_synced_insert_and_update_trigger(); + +-- The local `changes` table for capturing and persisting a log +-- of local write operations that we want to sync to the server. +CREATE TABLE IF NOT EXISTS changes ( + id BIGSERIAL PRIMARY KEY, + operation TEXT NOT NULL, + value JSONB NOT NULL, + write_id UUID NOT NULL, + transaction_id XID8 NOT NULL +); + +-- The following `INSTEAD OF` triggers: +-- 1. allow the app code to write directly to the view +-- 2. to capture write operations and write change messages into the + +-- The insert trigger +CREATE OR REPLACE FUNCTION todos_insert_trigger() +RETURNS TRIGGER AS $$ +DECLARE + local_write_id UUID := gen_random_uuid(); +BEGIN + IF EXISTS (SELECT 1 FROM todos_synced WHERE id = NEW.id) THEN + RAISE EXCEPTION 'Cannot insert: id already exists in the synced table'; + END IF; + IF EXISTS (SELECT 1 FROM todos_local WHERE id = NEW.id) THEN + RAISE EXCEPTION 'Cannot insert: id already exists in the local table'; + END IF; + + -- Insert into the local table. + INSERT INTO todos_local ( + id, + title, + completed, + created_at, + changed_columns, + write_id + ) + VALUES ( + NEW.id, + NEW.title, + NEW.completed, + NEW.created_at, + ARRAY['title', 'completed', 'created_at'], + local_write_id + ); + + -- Record the write operation in the change log. + INSERT INTO changes ( + operation, + value, + write_id, + transaction_id + ) + VALUES ( + 'insert', + jsonb_build_object( + 'id', NEW.id, + 'title', NEW.title, + 'completed', NEW.completed, + 'created_at', NEW.created_at + ), + local_write_id, + pg_current_xact_id() + ); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- The update trigger +CREATE OR REPLACE FUNCTION todos_update_trigger() +RETURNS TRIGGER AS $$ +DECLARE + synced todos_synced%ROWTYPE; + local todos_local%ROWTYPE; + changed_cols TEXT[] := '{}'; + local_write_id UUID := gen_random_uuid(); +BEGIN + -- Fetch the corresponding rows from the synced and local tables + SELECT * INTO synced FROM todos_synced WHERE id = NEW.id; + SELECT * INTO local FROM todos_local WHERE id = NEW.id; + + -- If the row is not present in the local table, insert it + IF NOT FOUND THEN + -- Compare each column with the synced table and add to changed_cols if different + IF NEW.title IS DISTINCT FROM synced.title THEN + changed_cols := array_append(changed_cols, 'title'); + END IF; + IF NEW.completed IS DISTINCT FROM synced.completed THEN + changed_cols := array_append(changed_cols, 'completed'); + END IF; + IF NEW.created_at IS DISTINCT FROM synced.created_at THEN + changed_cols := array_append(changed_cols, 'created_at'); + END IF; + + INSERT INTO todos_local ( + id, + title, + completed, + created_at, + changed_columns, + write_id + ) + VALUES ( + NEW.id, + NEW.title, + NEW.completed, + NEW.created_at, + changed_cols, + local_write_id + ); + + -- Otherwise, if the row is already in the local table, update it and adjust + -- the changed_columns + ELSE + UPDATE todos_local + SET + title = + CASE + WHEN NEW.title IS DISTINCT FROM synced.title + THEN NEW.title + ELSE local.title + END, + completed = + CASE + WHEN NEW.completed IS DISTINCT FROM synced.completed + THEN NEW.completed + ELSE local.completed + END, + created_at = + CASE + WHEN NEW.created_at IS DISTINCT FROM synced.created_at + THEN NEW.created_at + ELSE local.created_at + END, + -- Set the changed_columns to columes that have both been marked as changed + -- and have values that have actually changed. + changed_columns = ( + SELECT array_agg(DISTINCT col) FROM ( + SELECT unnest(local.changed_columns) AS col + UNION + SELECT unnest(ARRAY['title', 'completed', 'created_at']) AS col + ) AS cols + WHERE ( + CASE + WHEN col = 'title' + THEN COALESCE(NEW.title, local.title) IS DISTINCT FROM synced.title + WHEN col = 'completed' + THEN COALESCE(NEW.completed, local.completed) IS DISTINCT FROM synced.completed + WHEN col = 'created_at' + THEN COALESCE(NEW.created_at, local.created_at) IS DISTINCT FROM synced.created_at + END + ) + ), + write_id = local_write_id + WHERE id = NEW.id; + END IF; + + -- Record the update into the change log. + INSERT INTO changes ( + operation, + value, + write_id, + transaction_id + ) + VALUES ( + 'update', + jsonb_strip_nulls( + jsonb_build_object( + 'id', NEW.id, + 'title', NEW.title, + 'completed', NEW.completed, + 'created_at', NEW.created_at + ) + ), + local_write_id, + pg_current_xact_id() + ); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- The delete trigger +CREATE OR REPLACE FUNCTION todos_delete_trigger() +RETURNS TRIGGER AS $$ +DECLARE + local_write_id UUID := gen_random_uuid(); +BEGIN + -- Upsert a soft-deletion record in the local table. + IF EXISTS (SELECT 1 FROM todos_local WHERE id = OLD.id) THEN + UPDATE todos_local + SET + is_deleted = TRUE, + write_id = local_write_id + WHERE id = OLD.id; + ELSE + INSERT INTO todos_local ( + id, + is_deleted, + write_id + ) + VALUES ( + OLD.id, + TRUE, + local_write_id + ); + END IF; + + -- Record in the change log. + INSERT INTO changes ( + operation, + value, + write_id, + transaction_id + ) + VALUES ( + 'delete', + jsonb_build_object( + 'id', OLD.id + ), + local_write_id, + pg_current_xact_id() + ); + + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER todos_insert +INSTEAD OF INSERT ON todos +FOR EACH ROW +EXECUTE FUNCTION todos_insert_trigger(); + +CREATE OR REPLACE TRIGGER todos_update +INSTEAD OF UPDATE ON todos +FOR EACH ROW +EXECUTE FUNCTION todos_update_trigger(); + +CREATE OR REPLACE TRIGGER todos_delete +INSTEAD OF DELETE ON todos +FOR EACH ROW +EXECUTE FUNCTION todos_delete_trigger(); + +-- Notify on a `changes` topic whenever anything is added to the change log. +CREATE OR REPLACE FUNCTION changes_notify_trigger() +RETURNS TRIGGER AS $$ +BEGIN + NOTIFY changes; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER changes_notify +AFTER INSERT ON changes +FOR EACH ROW +EXECUTE FUNCTION changes_notify_trigger(); diff --git a/examples/write-patterns/patterns/4-through-the-db/sync.ts b/examples/write-patterns/patterns/4-through-the-db/sync.ts new file mode 100644 index 0000000000..98b832a776 --- /dev/null +++ b/examples/write-patterns/patterns/4-through-the-db/sync.ts @@ -0,0 +1,194 @@ +import { type Operation } from '@electric-sql/client' +import { type PGliteWithLive } from '@electric-sql/pglite/live' + +import api from '../../shared/app/client' + +type Change = { + id: number + operation: Operation + value: { + id: string + title?: string + completed?: boolean + created_at?: Date + } + write_id: string + transaction_id: string +} + +type SendResult = 'accepted' | 'rejected' | 'retry' + +/* + * Minimal, naive synchronization utility, just to illustrate the pattern of + * `listen`ing to `changes` and `POST`ing them to the api server. + */ +export default class ChangeLogSynchronizer { + #db: PGliteWithLive + #position: number + + #hasChangedWhileProcessing: boolean = false + #shouldContinue: boolean = true + #status: 'idle' | 'processing' = 'idle' + + #abortController?: AbortController + #unsubscribe?: () => Promise + + constructor(db: PGliteWithLive, position = 0) { + this.#db = db + this.#position = position + } + + /* + * Start by listening for notifications. + */ + async start(): Promise { + this.#abortController = new AbortController() + this.#unsubscribe = await this.#db.listen('changes', this.handle.bind(this)) + + this.process() + } + + /* + * On notify, either kick off processing or note down that there were changes + * so we can process them straightaway on the next loop. + */ + async handle(): Promise { + if (this.#status === 'processing') { + this.#hasChangedWhileProcessing = true + + return + } + + this.#status = 'processing' + + this.process() + } + + // Process the changes by fetching them and posting them to the server. + // If the changes are accepted then proceed, otherwise rollback or retry. + async process(): Promise { + this.#hasChangedWhileProcessing = false + + const { changes, position } = await this.query() + + if (changes.length) { + const result: SendResult = await this.send(changes) + + switch (result) { + case 'accepted': + await this.proceed(position) + + break + + case 'rejected': + await this.rollback() + + break + + case 'retry': + this.#hasChangedWhileProcessing = true + + break + } + } + + if (this.#hasChangedWhileProcessing && this.#shouldContinue) { + return await this.process() + } + + this.#status = 'idle' + } + + /* + * Fetch the current batch of changes + */ + async query(): Promise<{ changes: Change[]; position: number }> { + const { rows } = await this.#db.sql` + SELECT * from changes + WHERE id > ${this.#position} + ORDER BY id asc + ` + + const position = rows.length ? rows.at(-1)!.id : this.#position + + return { + changes: rows, + position, + } + } + + /* + * Send the current batch of changes to the server, grouped by transaction. + */ + async send(changes: Change[]): Promise { + const path = '/changes' + + const groups = Object.groupBy(changes, (x) => x.transaction_id) + const sorted = Object.entries(groups).sort((a, b) => + a[0].localeCompare(b[0]) + ) + const transactions = sorted.map(([transaction_id, changes]) => { + return { + id: transaction_id, + changes: changes, + } + }) + + const signal = this.#abortController?.signal + + let response: Response | undefined + try { + response = await api.request(path, 'POST', transactions, signal) + } catch (_err) { + return 'retry' + } + + if (response === undefined) { + return 'retry' + } + + if (response.ok) { + return 'accepted' + } + + return response.status < 500 ? 'rejected' : 'retry' + } + + /* + * Proceed by clearing the processed changes and moving the position forward. + */ + async proceed(position: number): Promise { + await this.#db.sql` + DELETE from changes + WHERE id <= ${position} + ` + + this.#position = position + } + + /* + * Rollback with an extremely naive strategy: if any write is rejected, simply + * wipe the entire local state. + */ + async rollback(): Promise { + await this.#db.transaction(async (tx) => { + await tx.sql`DELETE from changes` + await tx.sql`DELETE from todos_local` + }) + } + + /* + * Stop synchronizing + */ + async stop(): Promise { + this.#shouldContinue = false + + if (this.#abortController !== undefined) { + this.#abortController.abort() + } + + if (this.#unsubscribe !== undefined) { + await this.#unsubscribe() + } + } +} diff --git a/examples/write-patterns/patterns/index.ts b/examples/write-patterns/patterns/index.ts new file mode 100644 index 0000000000..653ca5bfcd --- /dev/null +++ b/examples/write-patterns/patterns/index.ts @@ -0,0 +1,4 @@ +export { default as OnlineWrites } from './1-online-writes' +export { default as OptimisticState } from './2-optimistic-state' +export { default as SharedPersistent } from './3-shared-persistent' +export { default as ThroughTheDB } from './4-through-the-db' diff --git a/examples/write-patterns/public/robots.txt b/examples/write-patterns/public/robots.txt new file mode 100644 index 0000000000..14267e9032 --- /dev/null +++ b/examples/write-patterns/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / \ No newline at end of file diff --git a/examples/write-patterns/public/screenshot.png b/examples/write-patterns/public/screenshot.png new file mode 100644 index 0000000000..9ecade6e99 Binary files /dev/null and b/examples/write-patterns/public/screenshot.png differ diff --git a/examples/write-patterns/shared/app/App.tsx b/examples/write-patterns/shared/app/App.tsx new file mode 100644 index 0000000000..3e42d9cfcf --- /dev/null +++ b/examples/write-patterns/shared/app/App.tsx @@ -0,0 +1,21 @@ +import './style.css' + +import { + OnlineWrites, + OptimisticState, + SharedPersistent, + ThroughTheDB, +} from '../../patterns' + +const App = () => { + return ( +
+ + + + +
+ ) +} + +export default App diff --git a/examples/write-patterns/shared/app/client.ts b/examples/write-patterns/shared/app/client.ts new file mode 100644 index 0000000000..967cca7419 --- /dev/null +++ b/examples/write-patterns/shared/app/client.ts @@ -0,0 +1,74 @@ +const API_URL = import.meta.env.API_URL || 'http://localhost:3001' + +type RequestOptions = { + method: string + headers: HeadersInit + body?: string + signal?: AbortSignal +} + +// Keeps trying for 3 minutes, with the delay +// increasing slowly from 1 to 20 seconds. +const maxRetries = 32 +const backoffMultiplier = 1.1 +const initialDelayMs = 1_000 + +async function retryFetch( + url: string, + options: RequestOptions, + retryCount: number +): Promise { + if (retryCount > maxRetries) { + return + } + + const delay = retryCount * backoffMultiplier * initialDelayMs + + return await new Promise((resolve) => { + setTimeout(async () => { + resolve(await resilientFetch(url, options, retryCount)) + }, delay) + }) +} + +async function resilientFetch( + url: string, + options: RequestOptions, + retryCount: number +): Promise { + try { + // Could also check the status and retry before returning if you want to be + // resilient to 4xx and 5xx responses as well as network errors + return await fetch(url, options) + } catch (err) { + return await retryFetch(url, options, retryCount + 1) + } +} + +async function request( + path: string, + method: string, + data?: object, + signal?: AbortSignal +): Promise { + const url = `${API_URL}${path}` + + const options: RequestOptions = { + method: method, + headers: { + 'Content-Type': 'application/json', + }, + } + + if (data !== undefined) { + options.body = JSON.stringify(data) + } + + if (signal !== undefined) { + options.signal = signal + } + + return await resilientFetch(url, options, 0) +} + +export default { request } diff --git a/examples/write-patterns/shared/app/icons/favicon.ico b/examples/write-patterns/shared/app/icons/favicon.ico new file mode 100644 index 0000000000..55765b6d89 Binary files /dev/null and b/examples/write-patterns/shared/app/icons/favicon.ico differ diff --git a/examples/write-patterns/shared/app/icons/icon-180.png b/examples/write-patterns/shared/app/icons/icon-180.png new file mode 100644 index 0000000000..054fc797fd Binary files /dev/null and b/examples/write-patterns/shared/app/icons/icon-180.png differ diff --git a/examples/write-patterns/shared/app/icons/icon-192.png b/examples/write-patterns/shared/app/icons/icon-192.png new file mode 100644 index 0000000000..ac2d52f73b Binary files /dev/null and b/examples/write-patterns/shared/app/icons/icon-192.png differ diff --git a/examples/write-patterns/shared/app/icons/icon-512.png b/examples/write-patterns/shared/app/icons/icon-512.png new file mode 100644 index 0000000000..9e98631798 Binary files /dev/null and b/examples/write-patterns/shared/app/icons/icon-512.png differ diff --git a/examples/write-patterns/shared/app/icons/icon.svg b/examples/write-patterns/shared/app/icons/icon.svg new file mode 100644 index 0000000000..b0a4278d35 --- /dev/null +++ b/examples/write-patterns/shared/app/icons/icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/write-patterns/shared/app/icons/spinner.gif b/examples/write-patterns/shared/app/icons/spinner.gif new file mode 100644 index 0000000000..5fcd7f2f21 Binary files /dev/null and b/examples/write-patterns/shared/app/icons/spinner.gif differ diff --git a/examples/write-patterns/shared/app/main.tsx b/examples/write-patterns/shared/app/main.tsx new file mode 100644 index 0000000000..6201bd4023 --- /dev/null +++ b/examples/write-patterns/shared/app/main.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' + +import App from './App' +import './style.css' + +const root = document.getElementById('root')! + +ReactDOM.createRoot(root).render( + + + +) diff --git a/examples/write-patterns/shared/app/style.css b/examples/write-patterns/shared/app/style.css new file mode 100644 index 0000000000..d77fc923dc --- /dev/null +++ b/examples/write-patterns/shared/app/style.css @@ -0,0 +1,133 @@ +body { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + + background: #1c1e20; + color: rgba(227, 227, 239, 0.92); + font-family: "Helvetica Neue", Helvetica, sans-serif; + font-size: 15px; + margin: 0; +} + +code { + font-family: source-code-pro, Menlo, Monaco, "Courier New", monospace; +} + +.app { + margin: 10px; + padding: 10px; + width: 100%; + + display:grid; + grid-template-columns: calc(50% - 30px) calc(50% - 30px); + grid-row: auto auto; + grid-column-gap: 20px; + grid-row-gap: 20px; +} + +.example { + padding: 65px 20px 20px 20px; + + background-color: #262628; + border-radius:10px; + + min-height: calc(50vh - 115px); + + background-image: url(/shared/app/icons/icon.svg); + background-position: 17px 23px; + background-repeat: no-repeat; + background-size: 34px; +} + +.example h3 { + margin: 5px 0 10px; +} + +.example ul { + list-style: none; + margin: 0; + padding: 20px 0 10px; + border-top: 1px solid #404049; + border-bottom: 1px solid #404049; +} + +.example ul li { + width: calc(100%); + margin-bottom: 10px; +} + +.example label { + display: inline-flex; + cursor: pointer; +} + +.example label input[type=checkbox] { + margin-right: 7px; + + accent-color: rgb(208, 188, 255); +} + +.example label .title.completed { + text-decoration: line-through; +} + +.example a.close { + float: right; + margin-right: 5px; + text-decoration: none; + color: inherit; + font-size: 13px; + margin-top: 1px; +} + +.example form { + margin-top: 18px; +} + +.example form input[type=text] { + padding: 9px; + margin-bottom: 12px; + background: #363638; + border: 0.2px solid #404049; + border-radius: 3px; + color: rgba(227, 227, 239, 0.92); + outline: none; + font-size: 14px; + display: block; + width: calc(100% - 20px); +} + +.example form button[type=submit] { + background: #404049; + border: none; + padding: 6px 9px; + border-radius: 7px; + color: rgba(227, 227, 239, 0.92); + font-size: 14.5px; + cursor: pointer; +} + +span.title { + display: inline; +} + +span.pending { + display: inline; + opacity: 1; + + width: 50px; + height: 50px; + position: absolute; + margin: -14px 0 0 -7px; + + background-image: url(/shared/app/icons/spinner.gif); + background-repeat: no-repeat; + background-size: 50px 50px; + + -webkit-transition: opacity 0.5s ease-in-out; + -moz-transition: opacity 0.5s ease-in-out; + transition: opacity 0.5s ease-in-out; +} +span.pending.hidden { + opacity: 0; +} diff --git a/examples/write-patterns/shared/app/vite-env.d.ts b/examples/write-patterns/shared/app/vite-env.d.ts new file mode 100644 index 0000000000..a08026d9c9 --- /dev/null +++ b/examples/write-patterns/shared/app/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/write-patterns/shared/backend/api.js b/examples/write-patterns/shared/backend/api.js new file mode 100644 index 0000000000..6b4f4a8798 --- /dev/null +++ b/examples/write-patterns/shared/backend/api.js @@ -0,0 +1,201 @@ +import bodyParser from 'body-parser' +import cors from 'cors' +import express from 'express' +import pg from 'pg' + +import { z } from 'zod' + +// Connect to Postgres. +const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:password@localhost:54321/electric' +const DATABASE_USE_SSL = process.env.DATABASE_USE_SSL === 'true' || false +const pool = new pg.Pool({connectionString: DATABASE_URL, ssl: DATABASE_USE_SSL}) +const db = await pool.connect() + +// Expose an HTTP server. +const PORT = parseInt(process.env.PORT || '3001') +const app = express() +app.use(bodyParser.json()) +app.use(cors()) + +// Validate user input +const idSchema = z.string().uuid() +const createSchema = z.object({ + id: z.string().uuid(), + title: z.string(), + created_at: z.string(), + write_id: z.string().optional() +}) +const updateSchema = z.object({ + completed: z.boolean(), + write_id: z.string().optional() +}) +const deleteSchema = z.object({ + write_id: z.string().optional() +}) + +// Define functions to create, update and delete todos +// using the `db` client. + +const createTodo = async (id, title, created_at, write_id) => { + const sql = ` + INSERT INTO todos (id, title, completed, created_at, write_id) + VALUES ($1, $2, false, $3, $4) + ` + + const params = [ + id, + title, + created_at, + write_id || null + ] + + await db.query(sql, params) +} + +const updateTodo = async (id, completed, write_id) => { + const sql = ` + UPDATE todos SET completed = $1, write_id = $2 + WHERE id = $3 + ` + + const params = [ + completed ? '1' : '0', + write_id || null, + id + ] + + await db.query(sql, params) +} + +const deleteTodo = async (id) => { + const sql = `DELETE from todos where id = $1` + const params = [id] + await db.query(sql, params) +} + +// Expose the shared REST API to create, update and delete todos. + +app.post(`/todos`, async (req, res) => { + let data + try { + data = createSchema.parse(req.body) + } + catch (err) { + return res.status(400).json({ errors: err.errors }) + } + + try { + await createTodo(data.id, data.title, data.created_at, data.write_id) + } + catch (err) { + return res.status(500).json({ errors: err }) + } + + return res.status(200).json({ status: 'OK' }) +}) + +app.put(`/todos/:id`, async (req, res) => { + let id, data + try { + id = idSchema.parse(req.params.id) + data = updateSchema.parse(req.body) + } + catch (err) { + return res.status(400).json({ errors: err.errors }) + } + + try { + await updateTodo(id, data.completed, data.write_id) + } + catch (err) { + return res.status(500).json({ errors: err }) + } + + return res.status(200).json({ status: 'OK' }) +}) + +app.delete(`/todos/:id`, async (req, res) => { + let id + try { + id = idSchema.parse(req.params.id) + } + catch (err) { + return res.status(400).json({ errors: err.errors }) + } + + try { + await deleteTodo(id) + } + catch (err) { + return res.status(500).json({ errors: err }) + } + + return res.status(200).json({ status: 'OK' }) +}) + +// And expose a `POST /changes` route specifically to support the +// through the DB sync pattern. + +const transactionsSchema = z.array( + z.object({ + id: z.string(), + changes: z.array( + z.object({ + operation: z.string(), + value: z.object({ + id: z.string().uuid(), + title: z.string().optional(), + completed: z.boolean().optional(), + created_at: z.string().optional(), + }), + write_id: z.string() + }) + ) + }) +) + +app.post(`/changes`, async (req, res) => { + let data + try { + data = transactionsSchema.parse(req.body) + } + catch (err) { + return res.status(400).json({ errors: err.errors }) + } + + try { + await db.query('BEGIN') + + data.forEach((tx) => { + tx.changes.forEach(({operation, value, write_id}) => { + switch (operation) { + case 'insert': + createTodo(value.id, value.title, value.created_at, write_id) + break + + case 'update': + updateTodo(value.id, value.completed, write_id) + break + + case 'delete': + deleteTodo(value.id) + break + } + }) + }) + + await db.query('COMMIT') + } + catch (err) { + await db.query('ROLLBACK') + + return res.status(500).json({ errors: err }) + } + + return res.status(200).json({ status: 'OK' }) +}) + +// Start the server +app.listen(PORT, () => { + console.log(`Server listening at http://localhost:${PORT}`) +}) diff --git a/examples/write-patterns/shared/migrations/01-create-todos.sql b/examples/write-patterns/shared/migrations/01-create-todos.sql new file mode 100644 index 0000000000..8bdd835474 --- /dev/null +++ b/examples/write-patterns/shared/migrations/01-create-todos.sql @@ -0,0 +1,23 @@ +-- Basic todos table. +CREATE TABLE IF NOT EXISTS todos ( + id UUID PRIMARY KEY, + title TEXT NOT NULL, + completed BOOLEAN NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL +); + +-- Insert some todos to get started. +INSERT INTO todos ( + id, + title, + completed, + created_at +) +SELECT + gen_random_uuid(), + 'Get stuff done', + '0', + CURRENT_TIMESTAMP +WHERE NOT EXISTS ( + SELECT * FROM todos +); diff --git a/examples/write-patterns/shared/migrations/02-add-write-id.sql b/examples/write-patterns/shared/migrations/02-add-write-id.sql new file mode 100644 index 0000000000..489d39be46 --- /dev/null +++ b/examples/write-patterns/shared/migrations/02-add-write-id.sql @@ -0,0 +1,11 @@ +-- Add an optional `write_id` field to the table. +-- +-- This is not necessary for simpler patterns but provides an option for more +-- advanced patterns to match on when monitoring the Electric replication +-- stream in order to invalidate local state. +-- +-- Matching on a per-operation update key and not, say, just the row `id`, +-- allows you to rebase local optimistic state on top of concurrent changes to +-- the same row made by other users. (Because you only clear the local state +-- when *your* local write syncs through, not anyone else's). +ALTER TABLE todos ADD COLUMN write_id UUID; diff --git a/examples/write-patterns/tsconfig.json b/examples/write-patterns/tsconfig.json new file mode 100644 index 0000000000..27245b362f --- /dev/null +++ b/examples/write-patterns/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["shared/app", "patterns"] +} diff --git a/examples/write-patterns/vite.config.ts b/examples/write-patterns/vite.config.ts new file mode 100644 index 0000000000..3ed5dd7be0 --- /dev/null +++ b/examples/write-patterns/vite.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'vite' +import { VitePWA } from 'vite-plugin-pwa' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { + target: 'esnext', + }, + optimizeDeps: { + exclude: ['@electric-sql/pglite'], + }, + plugins: [ + react(), + VitePWA({ + registerType: 'autoUpdate', + workbox: { + maximumFileSizeToCacheInBytes: 10 * 1024 ** 2, + }, + devOptions: { + enabled: false, + }, + includeAssets: ['shared/app/icons/*'], + manifest: { + name: 'Write Patterns Example - ElectricSQL ', + short_name: 'Writes', + description: 'Four different write-patterns that work with Electric.', + theme_color: '#1c1e20', + icons: [ + { + src: './shared/app/icons/icon-192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: './shared/app/icons/icon-512.png', + sizes: '512x512', + type: 'image/png', + }, + ], + }, + }), + ], +}) diff --git a/packages/experimental/.eslintrc.cjs b/packages/experimental/.eslintrc.cjs new file mode 100644 index 0000000000..5bc530843d --- /dev/null +++ b/packages/experimental/.eslintrc.cjs @@ -0,0 +1,41 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + `eslint:recommended`, + `plugin:@typescript-eslint/recommended`, + `plugin:prettier/recommended`, + ], + parserOptions: { + ecmaVersion: 2022, + requireConfigFile: false, + sourceType: `module`, + ecmaFeatures: { + jsx: true, + }, + }, + parser: `@typescript-eslint/parser`, + plugins: [`prettier`], + rules: { + quotes: [`error`, `backtick`], + 'no-unused-vars': `off`, + '@typescript-eslint/no-unused-vars': [ + `error`, + { + argsIgnorePattern: `^_`, + varsIgnorePattern: `^_`, + caughtErrorsIgnorePattern: `^_`, + }, + ], + }, + ignorePatterns: [ + '**/node_modules/**', + '**/dist/**', + 'tsup.config.ts', + 'vitest.config.ts', + '.eslintrc.js' + ], +} diff --git a/packages/experimental/.prettierignore b/packages/experimental/.prettierignore new file mode 100644 index 0000000000..55371e5c85 --- /dev/null +++ b/packages/experimental/.prettierignore @@ -0,0 +1,2 @@ +node_modules +.vscode \ No newline at end of file diff --git a/packages/experimental/.prettierrc b/packages/experimental/.prettierrc new file mode 100644 index 0000000000..f685078fff --- /dev/null +++ b/packages/experimental/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "semi": false, + "tabWidth": 2, + "singleQuote": true +} diff --git a/packages/experimental/README.md b/packages/experimental/README.md new file mode 100644 index 0000000000..77e87bf61a --- /dev/null +++ b/packages/experimental/README.md @@ -0,0 +1,41 @@ + +# Experimental TypeScript features for ElectricSQL + +## Install + +The client is published on NPM as [`@electric-sql/experimental`](https://www.npmjs.com/package/@electric-sql/experimental): + +```sh +npm i @electric-sql/experimental +``` + +## Develop + +Install the pnpm workspace at the repo root: + +```shell +pnpm install +``` + +Build the package: + +```shell +cd packages/typescript-client +pnpm build +``` + +## Test + +In one terminal, start the backend running: + +```shell +cd ../sync-service +mix deps.get +mix stop_dev && mix compile && mix start_dev && ies -S mix +``` + +Then in this folder: + +```shell +pnpm test +``` diff --git a/packages/experimental/package.json b/packages/experimental/package.json new file mode 100644 index 0000000000..ad3e3c6ced --- /dev/null +++ b/packages/experimental/package.json @@ -0,0 +1,71 @@ +{ + "name": "@electric-sql/experimental", + "version": "0.1.0", + "description": "Experimental TypeScript features for ElectricSQL.", + "type": "module", + "main": "dist/cjs/index.cjs", + "module": "dist/index.legacy-esm.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/cjs/index.cjs" + } + }, + "files": [ + "dist", + "src" + ], + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/electric-sql/electric.git" + }, + "author": "ElectricSQL team and contributors.", + "license": "Apache-2", + "bugs": { + "url": "https://github.com/electric-sql/electric/issues" + }, + "homepage": "https://electric-sql.com", + "dependencies": { + "@electric-sql/client": "workspace:*" + }, + "devDependencies": { + "@types/pg": "^8.11.6", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", + "concurrently": "^8.2.2", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "glob": "^10.3.10", + "pg": "^8.12.0", + "prettier": "^3.3.2", + "shx": "^0.3.4", + "tsup": "^8.0.1", + "typescript": "^5.5.2", + "uuid": "^10.0.0", + "vitest": "^2.0.2" + }, + "optionalDependencies": { + "@rollup/rollup-darwin-arm64": "^4.18.1" + }, + "typesVersions": { + "*": { + "*": [ + "./dist/index.d.ts" + ] + } + }, + "scripts": { + "test": "pnpm exec vitest", + "typecheck": "tsc -p tsconfig.json", + "build": "shx rm -rf dist && concurrently \"tsup\" \"tsc -p tsconfig.build.json\"", + "prepack": "pnpm build", + "stylecheck": "eslint . --quiet", + "format": "eslint . --fix" + } +} diff --git a/packages/experimental/src/index.ts b/packages/experimental/src/index.ts new file mode 100644 index 0000000000..e9f564c2b1 --- /dev/null +++ b/packages/experimental/src/index.ts @@ -0,0 +1 @@ +export * from './match' diff --git a/packages/experimental/src/match.ts b/packages/experimental/src/match.ts new file mode 100644 index 0000000000..f0200f41cb --- /dev/null +++ b/packages/experimental/src/match.ts @@ -0,0 +1,53 @@ +import { + isChangeMessage, + type ShapeStreamInterface, + type ChangeMessage, + type GetExtensions, + type Operation, + type Row, + type Value, +} from '@electric-sql/client' + +export function matchStream>( + stream: ShapeStreamInterface, + operations: Array, + matchFn: (message: ChangeMessage) => boolean, + timeout = 60000 // ms +): Promise> { + return new Promise>((resolve, reject) => { + const unsubscribe: () => void = stream.subscribe((messages) => { + const message = messages.filter(isChangeMessage).find((message) => { + const operation = message.headers.operation + + return operations.includes(operation) && matchFn(message) + }) + + if (message) { + return finish(message) + } + }) + + const timeoutId = setTimeout(() => { + const msg = `matchStream timed out after ${timeout}ms` + + console.error(msg) + + reject(msg) + }, timeout) + + function finish(message: ChangeMessage) { + clearTimeout(timeoutId) + + unsubscribe() + + return resolve(message) + } + }) +} + +export function matchBy>( + column: string, + value: Value> +): (message: ChangeMessage) => boolean { + return (message) => message.value[column] === value +} diff --git a/packages/experimental/test/match.test.ts b/packages/experimental/test/match.test.ts new file mode 100644 index 0000000000..d3a9d23fb7 --- /dev/null +++ b/packages/experimental/test/match.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, inject } from 'vitest' +import { v4 as uuidv4 } from 'uuid' +import { ShapeStream } from '@electric-sql/client' +import { testWithIssuesTable as it } from './support/test-context' + +import { matchBy, matchStream } from '../src/match' + +const BASE_URL = inject(`baseUrl`) + +describe(`matchStream`, () => { + it(`should match`, async ({ insertIssues, issuesTableUrl }) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-nocheck + const stream = new ShapeStream({ + url: `${BASE_URL}/v1/shape`, + params: { + table: issuesTableUrl, + }, + }) + + const id = uuidv4() + const issue = { + id: id, + title: `test title`, + } + + setTimeout(() => { + insertIssues(issue) + }, 10) + + const matchFn = matchBy(`id`, id) + const result = await matchStream(stream, [`insert`], matchFn, 200) + + expect(result.value.title).toEqual(`test title`) + }) +}) diff --git a/packages/experimental/test/support/global-setup.ts b/packages/experimental/test/support/global-setup.ts new file mode 100644 index 0000000000..a97f2e2667 --- /dev/null +++ b/packages/experimental/test/support/global-setup.ts @@ -0,0 +1,71 @@ +import type { GlobalSetupContext } from 'vitest/node' +import { makePgClient } from './test-helpers' + +const url = process.env.ELECTRIC_URL ?? `http://localhost:3000` +const proxyUrl = process.env.ELECTRIC_PROXY_CACHE_URL ?? `http://localhost:3002` + +// name of proxy cache container to execute commands against, +// see docker-compose.yml that spins it up for details +const proxyCacheContainerName = `electric_dev-nginx-1` +// path pattern for cache files inside proxy cache to clear +const proxyCachePath = `/var/cache/nginx/*` + +// eslint-disable-next-line quotes -- eslint is acting dumb with enforce backtick quotes mode, and is trying to use it here where it's not allowed. +declare module 'vitest' { + export interface ProvidedContext { + baseUrl: string + proxyCacheBaseUrl: string + testPgSchema: string + proxyCacheContainerName: string + proxyCachePath: string + } +} + +function waitForElectric(url: string): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(`Timed out waiting for Electric to be active`), + 10000 + ) + + const tryHealth = async () => + fetch(`${url}/v1/health`) + .then(async (res): Promise => { + if (!res.ok) return tryHealth() + const { status } = (await res.json()) as { status: string } + if (status !== `active`) return tryHealth() + clearTimeout(timeout) + resolve() + }) + .catch((err) => { + clearTimeout(timeout) + reject(err) + }) + + return tryHealth() + }) +} + +/** + * Global setup for the test suite. Validates that our server is running, and creates and tears down a + * special schema in Postgres to ensure clean slate between runs. + */ +export default async function ({ provide }: GlobalSetupContext) { + await waitForElectric(url) + + const client = makePgClient() + + await client.connect() + await client.query(`CREATE SCHEMA IF NOT EXISTS electric_test`) + + provide(`baseUrl`, url) + provide(`testPgSchema`, `electric_test`) + provide(`proxyCacheBaseUrl`, proxyUrl) + provide(`proxyCacheContainerName`, proxyCacheContainerName) + provide(`proxyCachePath`, proxyCachePath) + + return async () => { + await client.query(`DROP SCHEMA electric_test CASCADE`) + await client.end() + } +} diff --git a/packages/experimental/test/support/test-context.ts b/packages/experimental/test/support/test-context.ts new file mode 100644 index 0000000000..24211f4089 --- /dev/null +++ b/packages/experimental/test/support/test-context.ts @@ -0,0 +1,193 @@ +/* eslint-disable no-empty-pattern */ +import { v4 as uuidv4 } from 'uuid' +import { Client, QueryResult } from 'pg' +import { inject, test } from 'vitest' +import { makePgClient } from './test-helpers' +import { FetchError } from '@electric-sql/client' + +const SHAPE_HANDLE_QUERY_PARAM = `handle` + +export type IssueRow = { id: string; title: string; priority?: string } +export type GeneratedIssueRow = { id?: string; title: string } +export type UpdateIssueFn = (row: IssueRow) => Promise> +export type DeleteIssueFn = (row: IssueRow) => Promise> +export type InsertIssuesFn = (...rows: GeneratedIssueRow[]) => Promise +export type ClearIssuesShapeFn = (handle?: string) => Promise +export type ClearShapeFn = ( + table: string, + options?: { handle?: string } +) => Promise + +export const testWithDbClient = test.extend<{ + dbClient: Client + aborter: AbortController + baseUrl: string + pgSchema: string + clearShape: ClearShapeFn +}>({ + dbClient: async ({}, use) => { + const searchOption = `-csearch_path=${inject(`testPgSchema`)}` + const client = makePgClient({ options: searchOption }) + await client.connect() + await use(client) + await client.end() + }, + aborter: async ({}, use) => { + const controller = new AbortController() + await use(controller) + controller.abort(`Test complete`) + }, + baseUrl: async ({}, use) => use(inject(`baseUrl`)), + pgSchema: async ({}, use) => use(inject(`testPgSchema`)), + clearShape: async ({}, use) => { + await use( + async ( + table: string, + options: { + handle?: string + } = {} + ) => { + const baseUrl = inject(`baseUrl`) + const url = new URL(`${baseUrl}/v1/shape`) + url.searchParams.set(`table`, table) + + if (options.handle) { + url.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, options.handle) + } + + const resp = await fetch(url.toString(), { method: `DELETE` }) + if (!resp.ok) { + console.error( + await FetchError.fromResponse(resp, `DELETE ${url.toString()}`) + ) + throw new Error( + `Could not delete shape ${table} with ID ${options.handle}` + ) + } + } + ) + }, +}) + +export const testWithIssuesTable = testWithDbClient.extend<{ + issuesTableSql: string + issuesTableUrl: string + issuesTableKey: string + updateIssue: UpdateIssueFn + deleteIssue: DeleteIssueFn + insertIssues: InsertIssuesFn + clearIssuesShape: ClearIssuesShapeFn +}>({ + issuesTableSql: async ({ dbClient, task }, use) => { + const tableName = `"issues for ${task.id}_${Math.random().toString(16)}"` + await dbClient.query(` + DROP TABLE IF EXISTS ${tableName}; + CREATE TABLE ${tableName} ( + id UUID PRIMARY KEY, + title TEXT NOT NULL, + priority INTEGER NOT NULL + ); + COMMENT ON TABLE ${tableName} IS 'Created for ${task.file?.name.replace(/'/g, `\``) ?? `unknown`} - ${task.name.replace(`'`, `\``)}'; + `) + await use(tableName) + await dbClient.query(`DROP TABLE ${tableName}`) + }, + issuesTableUrl: async ({ issuesTableSql, pgSchema, clearShape }, use) => { + const urlAppropriateTable = pgSchema + `.` + issuesTableSql + await use(urlAppropriateTable) + try { + await clearShape(urlAppropriateTable) + } catch (_) { + // ignore - clearShape has its own logging + // we don't want to interrupt cleanup + } + }, + issuesTableKey: ({ issuesTableSql, pgSchema }, use) => + use(`"${pgSchema}".${issuesTableSql}`), + updateIssue: ({ issuesTableSql, dbClient }, use) => + use(({ id, title }) => + dbClient.query(`UPDATE ${issuesTableSql} SET title = $2 WHERE id = $1`, [ + id, + title, + ]) + ), + deleteIssue: ({ issuesTableSql, dbClient }, use) => + use(({ id }) => + dbClient.query(`DELETE FROM ${issuesTableSql} WHERE id = $1`, [id]) + ), + insertIssues: ({ issuesTableSql, dbClient }, use) => + use(async (...rows) => { + const placeholders = rows.map( + (_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})` + ) + const { rows: rows_1 } = await dbClient.query( + `INSERT INTO ${issuesTableSql} (id, title, priority) VALUES ${placeholders} RETURNING id`, + rows.flatMap((x) => [x.id ?? uuidv4(), x.title, 10]) + ) + return rows_1.map((x) => x.id) + }), + + clearIssuesShape: async ({ clearShape, issuesTableUrl }, use) => { + use((handle?: string) => clearShape(issuesTableUrl, { handle })) + }, +}) + +export const testWithMultitypeTable = testWithDbClient.extend<{ + tableSql: string + tableUrl: string +}>({ + tableSql: async ({ dbClient, task }, use) => { + const tableName = `"multitype table for ${task.id}_${Math.random().toString(16)}"` + + await dbClient.query(` + DROP TABLE IF EXISTS ${tableName}; + DROP TYPE IF EXISTS mood; + DROP TYPE IF EXISTS complex; + DROP DOMAIN IF EXISTS posint; + CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); + CREATE TYPE complex AS (r double precision, i double precision); + CREATE DOMAIN posint AS integer CHECK (VALUE > 0); + CREATE TABLE ${tableName} ( + txt VARCHAR, + i2 INT2 PRIMARY KEY, + i4 INT4, + i8 INT8, + f8 FLOAT8, + b BOOLEAN, + json JSON, + jsonb JSONB, + ints INT8[], + ints2 INT8[][], + int4s INT4[], + doubles FLOAT8[], + bools BOOLEAN[], + moods mood[], + moods2 mood[][], + complexes complex[], + posints posint[], + jsons JSONB[], + txts TEXT[], + value JSON + )`) + + await use(tableName) + + // Cleanup + await dbClient.query(` + DROP TABLE ${tableName}; + DROP TYPE IF EXISTS mood; + DROP TYPE IF EXISTS complex; + DROP DOMAIN IF EXISTS posint; + `) + }, + tableUrl: async ({ tableSql, clearShape, pgSchema }, use) => { + const urlAppropriateTable = pgSchema + `.` + tableSql + await use(urlAppropriateTable) + try { + await clearShape(urlAppropriateTable) + } catch (_) { + // ignore - clearShape has its own logging + // we don't want to interrupt cleanup + } + }, +}) diff --git a/packages/experimental/test/support/test-helpers.ts b/packages/experimental/test/support/test-helpers.ts new file mode 100644 index 0000000000..3c5999ef3b --- /dev/null +++ b/packages/experimental/test/support/test-helpers.ts @@ -0,0 +1,51 @@ +import { Client, ClientConfig } from 'pg' +import { + ShapeStreamInterface, + type Message, + type Row, +} from '@electric-sql/client' + +export function makePgClient(overrides: ClientConfig = {}) { + return new Client({ + host: `localhost`, + port: 54321, + password: `password`, + user: `postgres`, + database: `electric`, + options: `-csearch_path=electric_test`, + ...overrides, + }) +} + +export function forEachMessage>( + stream: ShapeStreamInterface, + controller: AbortController, + handler: ( + resolve: () => void, + message: Message, + nthDataMessage: number + ) => Promise | void +) { + return new Promise((resolve, reject) => { + let messageIdx = 0 + + stream.subscribe(async (messages) => { + for (const message of messages) { + try { + await handler( + () => { + controller.abort() + return resolve() + }, + message as Message, + messageIdx + ) + if (`operation` in message.headers) messageIdx++ + } catch (e) { + controller.abort() + return reject(e) + } + } + }, reject) + }) +} diff --git a/packages/experimental/tsconfig.build.json b/packages/experimental/tsconfig.build.json new file mode 100644 index 0000000000..94732df2ed --- /dev/null +++ b/packages/experimental/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "test", "dist"] +} diff --git a/packages/experimental/tsconfig.json b/packages/experimental/tsconfig.json new file mode 100644 index 0000000000..5bd3117ad9 --- /dev/null +++ b/packages/experimental/tsconfig.json @@ -0,0 +1,105 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + // "incremental": true, /* Enable incremental compilation */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": [ + "ESNext" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "jsx": "preserve" /* Specify what JSX code is generated. */, + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/experimental/tsup.config.ts b/packages/experimental/tsup.config.ts new file mode 100644 index 0000000000..075298b6c3 --- /dev/null +++ b/packages/experimental/tsup.config.ts @@ -0,0 +1,60 @@ +import type { Options } from 'tsup' +import { defineConfig } from 'tsup' + +export default defineConfig((options) => { + const entry = { + index: 'src/index.ts', + } + const commonOptions: Partial = { + entry, + tsconfig: `./tsconfig.build.json`, + sourcemap: true, + ...options, + } + + return [ + // Standard ESM, embedded `process.env.NODE_ENV` checks + { + ...commonOptions, + format: ['esm'], + outExtension: () => ({ js: '.mjs' }), // Add dts: '.d.ts' when egoist/tsup#1053 lands + dts: true, + clean: true, + }, + // Support Webpack 4 by pointing `"module"` to a file with a `.js` extension + { + ...commonOptions, + format: ['esm'], + target: 'es2017', + dts: false, + outExtension: () => ({ js: '.js' }), + entry: Object.fromEntries( + Object.entries(entry).map(([key, value]) => [ + `${key}.legacy-esm`, + value, + ]) + ), + }, + // Browser-ready ESM, production + minified + { + ...commonOptions, + + entry: Object.fromEntries( + Object.entries(entry).map(([key, value]) => [`${key}.browser`, value]) + ), + + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + format: ['esm'], + outExtension: () => ({ js: '.mjs' }), + minify: true, + }, + { + ...commonOptions, + format: 'cjs', + outDir: './dist/cjs/', + outExtension: () => ({ js: '.cjs' }), + }, + ] +}) diff --git a/packages/experimental/vitest.config.ts b/packages/experimental/vitest.config.ts new file mode 100644 index 0000000000..9ca6c004fe --- /dev/null +++ b/packages/experimental/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globalSetup: `test/support/global-setup.ts`, + typecheck: { enabled: true }, + fileParallelism: false, + }, +}) diff --git a/packages/react-hooks/package.json b/packages/react-hooks/package.json index 5af6e0f280..c5a74aebdc 100644 --- a/packages/react-hooks/package.json +++ b/packages/react-hooks/package.json @@ -50,6 +50,7 @@ "pg": "^8.12.0", "prettier": "^3.3.2", "react": "^18.3.1", + "react-dom": "^18.3.1", "shx": "^0.3.4", "tsup": "^8.0.1", "typescript": "^5.5.2", diff --git a/packages/react-hooks/src/react-hooks.tsx b/packages/react-hooks/src/react-hooks.tsx index 0e8a97df3b..96aa9a11ec 100644 --- a/packages/react-hooks/src/react-hooks.tsx +++ b/packages/react-hooks/src/react-hooks.tsx @@ -102,6 +102,11 @@ export interface UseShapeResult = Row> { * @type {Shape} */ shape: Shape + /** + * The ShapeStream instance used by this Shape + * @type {ShapeStream} + */ + stream: ShapeStream /** True during initial fetch. False afterwise. */ isLoading: boolean /** Unix time at which we last synced. Undefined when `isLoading` is true. */ @@ -129,6 +134,7 @@ function parseShapeData>( lastSyncedAt: shape.lastSyncedAt(), isError: shape.error !== false, shape, + stream: shape.stream as ShapeStream, error: shape.error, } } diff --git a/packages/typescript-client/src/client.ts b/packages/typescript-client/src/client.ts index d2944bf06c..1ee875535b 100644 --- a/packages/typescript-client/src/client.ts +++ b/packages/typescript-client/src/client.ts @@ -190,7 +190,7 @@ export interface ShapeStreamInterface = Row> { subscribe( callback: (messages: Message[]) => MaybePromise, onError?: (error: FetchError | Error) => void - ): void + ): () => void unsubscribeAll(): void isLoading(): boolean diff --git a/packages/typescript-client/src/shape.ts b/packages/typescript-client/src/shape.ts index 553ee91878..0fe5a11b21 100644 --- a/packages/typescript-client/src/shape.ts +++ b/packages/typescript-client/src/shape.ts @@ -46,7 +46,7 @@ export type ShapeChangedCallback = Row> = (data: { * }) */ export class Shape = Row> { - readonly #stream: ShapeStreamInterface + readonly stream: ShapeStreamInterface readonly #data: ShapeData = new Map() readonly #subscribers = new Map>() @@ -55,23 +55,23 @@ export class Shape = Row> { #error: FetchError | false = false constructor(stream: ShapeStreamInterface) { - this.#stream = stream - this.#stream.subscribe( + this.stream = stream + this.stream.subscribe( this.#process.bind(this), this.#handleError.bind(this) ) } get isUpToDate(): boolean { - return this.#stream.isUpToDate + return this.stream.isUpToDate } get lastOffset(): Offset { - return this.#stream.lastOffset + return this.stream.lastOffset } get handle(): string | undefined { - return this.#stream.shapeHandle + return this.stream.shapeHandle } get rows(): Promise { @@ -84,7 +84,7 @@ export class Shape = Row> { get value(): Promise> { return new Promise((resolve, reject) => { - if (this.#stream.isUpToDate) { + if (this.stream.isUpToDate) { resolve(this.currentValue) } else { const unsubscribe = this.subscribe(({ value }) => { @@ -106,22 +106,22 @@ export class Shape = Row> { /** Unix time at which we last synced. Undefined when `isLoading` is true. */ lastSyncedAt(): number | undefined { - return this.#stream.lastSyncedAt() + return this.stream.lastSyncedAt() } /** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */ lastSynced() { - return this.#stream.lastSynced() + return this.stream.lastSynced() } /** True during initial fetch. False afterwise. */ isLoading() { - return this.#stream.isLoading() + return this.stream.isLoading() } /** Indicates if we are connected to the Electric sync service. */ isConnected(): boolean { - return this.#stream.isConnected() + return this.stream.isConnected() } subscribe(callback: ShapeChangedCallback): () => void { diff --git a/packages/typescript-client/src/types.ts b/packages/typescript-client/src/types.ts index 2332a4e706..9de8c68e8e 100644 --- a/packages/typescript-client/src/types.ts +++ b/packages/typescript-client/src/types.ts @@ -23,6 +23,8 @@ interface Header { [key: Exclude]: Value } +export type Operation = `insert` | `update` | `delete` + export type ControlMessage = { headers: Header & { control: `up-to-date` | `must-refetch` } } @@ -30,7 +32,7 @@ export type ControlMessage = { export type ChangeMessage = Row> = { key: string value: T - headers: Header & { operation: `insert` | `update` | `delete` } + headers: Header & { operation: Operation } offset: Offset } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40755304df..1d0f179b37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 18.3.1 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@5.4.10(@types/node@20.17.6)) + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -52,7 +52,7 @@ importers: version: 5.6.3 vite: specifier: ^5.3.4 - version: 5.4.10(@types/node@20.17.6) + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) examples/encryption: dependencies: @@ -101,7 +101,7 @@ importers: version: 10.0.0 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@5.4.10(@types/node@20.17.6)) + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -116,7 +116,7 @@ importers: version: 5.6.3 vite: specifier: ^5.3.4 - version: 5.4.10(@types/node@20.17.6) + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) examples/gatekeeper-auth: dependencies: @@ -237,7 +237,7 @@ importers: version: 9.0.1 vite-plugin-svgr: specifier: ^3.2.0 - version: 3.3.0(rollup@4.24.4)(typescript@5.6.3)(vite@4.5.5(@types/node@20.17.6)) + version: 3.3.0(rollup@4.24.4)(typescript@5.6.3)(vite@4.5.5(@types/node@20.17.6)(terser@5.36.0)) devDependencies: '@databases/pg': specifier: ^5.5.0 @@ -289,7 +289,7 @@ importers: version: 6.21.0(eslint@8.57.1)(typescript@5.6.3) '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@4.5.5(@types/node@20.17.6)) + version: 4.3.3(vite@4.5.5(@types/node@20.17.6)(terser@5.36.0)) autoprefixer: specifier: ^10.4.19 version: 10.4.20(postcss@8.4.47) @@ -328,7 +328,7 @@ importers: version: 5.6.3 vite: specifier: ^4.4.5 - version: 4.5.5(@types/node@20.17.6) + version: 4.5.5(@types/node@20.17.6)(terser@5.36.0) examples/nextjs-example: dependencies: @@ -374,7 +374,7 @@ importers: version: 10.0.0 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@5.4.10(@types/node@20.17.6)) + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -389,7 +389,7 @@ importers: version: 5.6.3 vite: specifier: ^5.3.4 - version: 5.4.10(@types/node@20.17.6) + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) examples/proxy-auth: dependencies: @@ -435,7 +435,7 @@ importers: version: 10.0.0 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@5.4.10(@types/node@20.17.6)) + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -447,7 +447,7 @@ importers: version: 5.6.3 vite: specifier: ^5.3.4 - version: 5.4.10(@types/node@20.17.6) + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) examples/redis-sync: dependencies: @@ -515,7 +515,7 @@ importers: version: link:../../packages/react-hooks '@remix-run/dev': specifier: ^2.11.0 - version: 2.13.1(@remix-run/react@2.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@remix-run/serve@2.13.1(typescript@5.6.3))(@types/node@20.17.6)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)) + version: 2.13.1(@remix-run/react@2.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@remix-run/serve@2.13.1(typescript@5.6.3))(@types/node@20.17.6)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) '@remix-run/node': specifier: ^2.11.0 version: 2.13.1(typescript@5.6.3) @@ -558,7 +558,7 @@ importers: version: 10.0.0 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@5.4.10(@types/node@20.17.6)) + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -570,7 +570,7 @@ importers: version: 5.6.3 vite: specifier: ^5.3.4 - version: 5.4.10(@types/node@20.17.6) + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) examples/tanstack-example: dependencies: @@ -616,7 +616,7 @@ importers: version: 10.0.0 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@5.4.10(@types/node@20.17.6)) + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -631,7 +631,7 @@ importers: version: 5.6.3 vite: specifier: ^5.3.4 - version: 5.4.10(@types/node@20.17.6) + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) examples/todo-app: dependencies: @@ -692,7 +692,7 @@ importers: version: 7.18.0(eslint@8.57.1)(typescript@5.6.3) '@vitejs/plugin-react-swc': specifier: ^3.7.0 - version: 3.7.1(@swc/helpers@0.5.5)(vite@5.4.10(@types/node@20.17.6)) + version: 3.7.1(@swc/helpers@0.5.5)(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -725,13 +725,157 @@ importers: version: 5.6.3 vite: specifier: ^5.3.1 - version: 5.4.10(@types/node@20.17.6) + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) vite-plugin-capsize-radix: specifier: ^0.0.9 version: 0.0.9 + examples/write-patterns: + dependencies: + '@electric-sql/client': + specifier: workspace:* + version: link:../../packages/typescript-client + '@electric-sql/experimental': + specifier: workspace:* + version: link:../../packages/experimental + '@electric-sql/pglite': + specifier: ^0.2.14 + version: 0.2.14 + '@electric-sql/pglite-react': + specifier: ^0.2.14 + version: 0.2.14(@electric-sql/pglite@0.2.14)(react@19.0.0-rc.1) + '@electric-sql/pglite-sync': + specifier: ^0.2.16 + version: 0.2.16(@electric-sql/pglite@0.2.14) + '@electric-sql/react': + specifier: workspace:* + version: link:../../packages/react-hooks + body-parser: + specifier: ^1.20.2 + version: 1.20.3 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.19.2 + version: 4.21.1 + pg: + specifier: ^8.12.0 + version: 8.13.1 + react: + specifier: 19.0.0-rc.1 + version: 19.0.0-rc.1 + react-dom: + specifier: 19.0.0-rc.1 + version: 19.0.0-rc.1(react@19.0.0-rc.1) + uuid: + specifier: ^10.0.0 + version: 10.0.0 + valtio: + specifier: ^2.1.2 + version: 2.1.2(react@19.0.0-rc.1)(types-react@19.0.0-rc.1) + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@databases/pg-migrations': + specifier: ^5.0.3 + version: 5.0.3(typescript@5.6.3) + '@types/react': + specifier: npm:types-react@rc + version: types-react@19.0.0-rc.1 + '@types/react-dom': + specifier: npm:types-react-dom@rc + version: types-react-dom@19.0.0-rc.1 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + eslint: + specifier: ^8.57.0 + version: 8.57.1 + rollup: + specifier: 2.79.2 + version: 2.79.2 + typescript: + specifier: ^5.5.3 + version: 5.6.3 + vite: + specifier: ^5.3.4 + version: 5.4.10(@types/node@20.17.6)(terser@5.36.0) + vite-plugin-pwa: + specifier: ^0.21.0 + version: 0.21.0(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) + packages/elixir-client: {} + packages/experimental: + dependencies: + '@electric-sql/client': + specifier: workspace:* + version: link:../typescript-client + optionalDependencies: + '@rollup/rollup-darwin-arm64': + specifier: ^4.18.1 + version: 4.24.4 + devDependencies: + '@types/pg': + specifier: ^8.11.6 + version: 8.11.10 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^7.14.1 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^7.14.1 + version: 7.18.0(eslint@8.57.1)(typescript@5.6.3) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + eslint: + specifier: ^8.57.0 + version: 8.57.1 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@8.57.1) + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.2.1(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.3.3) + glob: + specifier: ^10.3.10 + version: 10.4.5 + pg: + specifier: ^8.12.0 + version: 8.13.1 + prettier: + specifier: ^3.3.2 + version: 3.3.3 + shx: + specifier: ^0.3.4 + version: 0.3.4 + tsup: + specifier: ^8.0.1 + version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) + typescript: + specifier: ^5.5.2 + version: 5.6.3 + uuid: + specifier: ^10.0.0 + version: 10.0.0 + vitest: + specifier: ^2.0.2 + version: 2.1.4(@types/node@20.17.6)(jsdom@25.0.1)(terser@5.36.0) + packages/react-hooks: dependencies: '@electric-sql/client': @@ -789,6 +933,9 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) shx: specifier: ^0.3.4 version: 0.3.4 @@ -803,7 +950,7 @@ importers: version: 10.0.0 vitest: specifier: ^2.0.2 - version: 2.1.4(@types/node@20.17.6)(jsdom@25.0.1) + version: 2.1.4(@types/node@20.17.6)(jsdom@25.0.1)(terser@5.36.0) packages/sync-service: {} @@ -863,7 +1010,7 @@ importers: version: 10.0.0 vitest: specifier: ^2.0.2 - version: 2.1.4(@types/node@20.17.6)(jsdom@25.0.1) + version: 2.1.4(@types/node@20.17.6)(jsdom@25.0.1)(terser@5.36.0) website: devDependencies: @@ -875,7 +1022,10 @@ importers: version: 3.1.7 vitepress: specifier: ^1.3.1 - version: 1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(typescript@5.6.3) + version: 1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3) + vitepress-plugin-tabs: + specifier: ^0.5.0 + version: 0.5.0(vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3))(vue@3.5.12(typescript@5.6.3)) vue-tweet: specifier: ^2.3.1 version: 2.3.1(vue@3.5.12(typescript@5.6.3)) @@ -965,6 +1115,12 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@apideck/better-ajv-errors@0.3.6': + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -985,6 +1141,10 @@ packages: resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': + resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.25.9': resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} @@ -995,6 +1155,17 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-create-regexp-features-plugin@7.25.9': + resolution: {integrity: sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.3': + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + '@babel/helper-member-expression-to-functions@7.25.9': resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} engines: {node: '>=6.9.0'} @@ -1017,6 +1188,12 @@ packages: resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-replace-supers@7.25.9': resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} engines: {node: '>=6.9.0'} @@ -1043,6 +1220,10 @@ packages: resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} + engines: {node: '>=6.9.0'} + '@babel/helpers@7.26.0': resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} @@ -1052,12 +1233,60 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-decorators@7.25.9': resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.25.9': resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} engines: {node: '>=6.9.0'} @@ -1070,94 +1299,405 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-commonjs@7.25.9': - resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-self@7.25.9': - resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + '@babel/plugin-transform-async-generator-functions@7.25.9': + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.25.9': - resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.25.9': - resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} + '@babel/plugin-transform-block-scoped-functions@7.25.9': + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.26.0': - resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + '@babel/plugin-transform-block-scoping@7.25.9': + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.26.0': - resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@babel/template@7.25.9': - resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 - '@babel/traverse@7.25.9': - resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@babel/types@7.26.0': - resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@capsizecss/core@4.1.2': - resolution: {integrity: sha512-5tMjLsVsaEEwJ816y3eTfhhTIyUWNFt58x6YcHni0eV5tta8MGDOAIe+CV5ICb5pguXgDpNGLprqhPqBWtkFSg==} + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@capsizecss/metrics@2.2.0': - resolution: {integrity: sha512-DkFIser1KbGxWyG2hhQQeCit72TnOQDx5pr9bkA7+XlIy7qv+4lYtslH3bidVxm2qkY2guAgypSIPYuQQuk70A==} + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@capsizecss/metrics@3.3.0': - resolution: {integrity: sha512-WAQtKgyz7fZDEMuERSLPmWXuV53O/HDJZLof8BMWEX1GTWYiiNdqGA6j56+GCSSeVyzYDxkBnm5taIh0YyW6fQ==} + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@cfaester/enzyme-adapter-react-18@0.8.0': - resolution: {integrity: sha512-3Z3ThTUouHwz8oIyhTYQljEMNRFtlVyc3VOOHCbxs47U6cnXs8K9ygi/c1tv49s7MBlTXeIcuN+Ttd9aPtILFQ==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} + engines: {node: '>=6.9.0'} peerDependencies: - enzyme: ^3.11.0 - react: '>=18' - react-dom: '>=18' + '@babel/core': ^7.0.0 - '@changesets/apply-release-plan@7.0.5': - resolution: {integrity: sha512-1cWCk+ZshEkSVEZrm2fSj1Gz8sYvxgUL4Q78+1ZZqeqfuevPTPk033/yUZ3df8BKMohkqqHfzj0HOOrG0KtXTw==} + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/assemble-release-plan@6.0.4': - resolution: {integrity: sha512-nqICnvmrwWj4w2x0fOhVj2QEGdlUuwVAwESrUo5HLzWMI1rE5SWfsr9ln+rDqWB6RQ2ZyaMZHUcU7/IRaUJS+Q==} + '@babel/plugin-transform-exponentiation-operator@7.25.9': + resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/changelog-git@0.2.0': - resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/cli@2.27.9': - resolution: {integrity: sha512-q42a/ZbDnxPpCb5Wkm6tMVIxgeI9C/bexntzTeCFBrQEdpisQqk8kCHllYZMDjYtEc1ZzumbMJAG8H0Z4rdvjg==} - hasBin: true + '@babel/plugin-transform-for-of@7.25.9': + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/config@3.0.3': - resolution: {integrity: sha512-vqgQZMyIcuIpw9nqFIpTSNyc/wgm/Lu1zKN5vECy74u95Qx/Wa9g27HdgO4NkVAaq+BGA8wUc/qvbvVNs93n6A==} + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/errors@0.2.0': - resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/get-dependents-graph@2.1.2': - resolution: {integrity: sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ==} + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/get-release-plan@4.0.4': - resolution: {integrity: sha512-SicG/S67JmPTrdcc9Vpu0wSQt7IiuN0dc8iR5VScnnTVPfIaLvKmEGRvIaF0kcn8u5ZqLbormZNTO77bCEvyWw==} + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - '@changesets/get-version-range-type@0.4.0': + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.25.9': + resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9': + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.25.9': + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.25.9': + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.25.9': + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.25.9': + resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.26.0': + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-typescript@7.26.0': + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@capsizecss/core@4.1.2': + resolution: {integrity: sha512-5tMjLsVsaEEwJ816y3eTfhhTIyUWNFt58x6YcHni0eV5tta8MGDOAIe+CV5ICb5pguXgDpNGLprqhPqBWtkFSg==} + + '@capsizecss/metrics@2.2.0': + resolution: {integrity: sha512-DkFIser1KbGxWyG2hhQQeCit72TnOQDx5pr9bkA7+XlIy7qv+4lYtslH3bidVxm2qkY2guAgypSIPYuQQuk70A==} + + '@capsizecss/metrics@3.3.0': + resolution: {integrity: sha512-WAQtKgyz7fZDEMuERSLPmWXuV53O/HDJZLof8BMWEX1GTWYiiNdqGA6j56+GCSSeVyzYDxkBnm5taIh0YyW6fQ==} + + '@cfaester/enzyme-adapter-react-18@0.8.0': + resolution: {integrity: sha512-3Z3ThTUouHwz8oIyhTYQljEMNRFtlVyc3VOOHCbxs47U6cnXs8K9ygi/c1tv49s7MBlTXeIcuN+Ttd9aPtILFQ==} + peerDependencies: + enzyme: ^3.11.0 + react: '>=18' + react-dom: '>=18' + + '@changesets/apply-release-plan@7.0.5': + resolution: {integrity: sha512-1cWCk+ZshEkSVEZrm2fSj1Gz8sYvxgUL4Q78+1ZZqeqfuevPTPk033/yUZ3df8BKMohkqqHfzj0HOOrG0KtXTw==} + + '@changesets/assemble-release-plan@6.0.4': + resolution: {integrity: sha512-nqICnvmrwWj4w2x0fOhVj2QEGdlUuwVAwESrUo5HLzWMI1rE5SWfsr9ln+rDqWB6RQ2ZyaMZHUcU7/IRaUJS+Q==} + + '@changesets/changelog-git@0.2.0': + resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} + + '@changesets/cli@2.27.9': + resolution: {integrity: sha512-q42a/ZbDnxPpCb5Wkm6tMVIxgeI9C/bexntzTeCFBrQEdpisQqk8kCHllYZMDjYtEc1ZzumbMJAG8H0Z4rdvjg==} + hasBin: true + + '@changesets/config@3.0.3': + resolution: {integrity: sha512-vqgQZMyIcuIpw9nqFIpTSNyc/wgm/Lu1zKN5vECy74u95Qx/Wa9g27HdgO4NkVAaq+BGA8wUc/qvbvVNs93n6A==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.2': + resolution: {integrity: sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ==} + + '@changesets/get-release-plan@4.0.4': + resolution: {integrity: sha512-SicG/S67JmPTrdcc9Vpu0wSQt7IiuN0dc8iR5VScnnTVPfIaLvKmEGRvIaF0kcn8u5ZqLbormZNTO77bCEvyWw==} + + '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} '@changesets/git@3.0.1': @@ -1261,6 +1801,23 @@ packages: search-insights: optional: true + '@electric-sql/client@0.8.0': + resolution: {integrity: sha512-M4VnuL2q2i1yhsjc9DEQtf4GEkXoaMjlfm0Lq7KqLDjj2nqPhbUTo8IeWhf3OJSZ7j+GyFd/YlLg4rlBDrE/6Q==} + + '@electric-sql/pglite-react@0.2.14': + resolution: {integrity: sha512-QseE5Y2a7+9G1gj0Xffg3nKpKQ0KIN/6euofmRMMwTUWxK8eok6sVus2XtUY+Hy8TkzhoLa8boEOJ+xTU8/bZQ==} + peerDependencies: + '@electric-sql/pglite': ^0.2.14 + react: ^18.0.0 + + '@electric-sql/pglite-sync@0.2.16': + resolution: {integrity: sha512-jv/86TRBAPHA273Dt0af/mf/kARHkzsmjTMVhmSs8FAn6UKR1fcqIIAeC3EyI0ihP2H3E2M3ivpMMvO8DULGxQ==} + peerDependencies: + '@electric-sql/pglite': ^0.2.14 + + '@electric-sql/pglite@0.2.14': + resolution: {integrity: sha512-ZMYZL/yFu5sCewYecdX4OjyOPcrI2OmQ6598e/tyke4Rpgeekd4+pINf9jjzJNJk1Kq5dtuB6buqZsBQf0sx8A==} + '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} @@ -2077,6 +2634,9 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -2910,6 +3470,46 @@ packages: '@remix-run/web-stream@1.1.0': resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} + '@rollup/plugin-babel@5.3.1': + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + + '@rollup/plugin-node-resolve@15.3.0': + resolution: {integrity: sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@2.4.2': + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + + '@rollup/plugin-terser@0.4.4': + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@3.1.0': + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + '@rollup/pluginutils@5.1.3': resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} engines: {node: '>=14.0.0'} @@ -3030,6 +3630,9 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@surma/rollup-plugin-off-main-thread@2.2.3': + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -3429,6 +4032,9 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@0.0.39': + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -3528,6 +4134,9 @@ packages: '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -3540,6 +4149,9 @@ packages: '@types/svgo@2.6.4': resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3868,6 +4480,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + algoliasearch@5.13.0: resolution: {integrity: sha512-04lyQX3Ev/oLYQx+aagamQDXvkUUfX1mwrLrus15+9fNaYj28GDxxEzbwaRfvmHFcZyoxvup7mMtDTTw8SrTEQ==} engines: {node: '>= 14.0.0'} @@ -3960,9 +4575,16 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -3977,6 +4599,21 @@ packages: aws4fetch@1.0.20: resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + babel-plugin-polyfill-corejs2@0.4.12: + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.3: + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4220,6 +4857,10 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -4273,6 +4914,9 @@ packages: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + core-js@3.39.0: resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} @@ -4302,6 +4946,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + css-box-model@1.2.1: resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} @@ -4530,6 +5178,11 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + electron-to-chromium@1.5.52: resolution: {integrity: sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==} @@ -4871,6 +5524,9 @@ packages: estree-util-visit@1.2.1: resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -4945,6 +5601,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -4963,6 +5622,9 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -5037,6 +5699,10 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -5089,6 +5755,9 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} @@ -5262,6 +5931,9 @@ packages: peerDependencies: postcss: ^8.1.0 + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -5401,6 +6073,9 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -5413,6 +6088,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -5438,6 +6117,10 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -5508,6 +6191,11 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + javascript-stringify@2.1.0: resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} @@ -5593,6 +6281,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -5607,6 +6298,10 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -5624,6 +6319,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -5754,6 +6453,9 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -6692,6 +7394,14 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6795,6 +7505,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-compare@3.0.1: + resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} @@ -6861,6 +7574,11 @@ packages: peerDependencies: react: ^18.3.1 + react-dom@19.0.0-rc.1: + resolution: {integrity: sha512-k8MfDX+4G+eaa1cXXI9QF4d+pQtYol3nx8vauqRWUEOPqC7NQn2qmEqUsLoSd28rrZUL+R3T2VC+kZ2Hyx1geQ==} + peerDependencies: + react: 19.0.0-rc.1 + react-icons@4.12.0: resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==} peerDependencies: @@ -6986,6 +7704,10 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + react@19.0.0-rc.1: + resolution: {integrity: sha512-NZKln+uyPuyHchzP07I6GGYFxdAoaKhehgpCa3ltJGzwE31OYumLeshGaitA1R/fS5d9D2qpZVwTFAr6zCLM9w==} + engines: {node: '>=0.10.0'} + read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} @@ -7031,9 +7753,19 @@ packages: reftools@1.1.9: resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==} + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + regex@4.4.0: resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} @@ -7041,6 +7773,17 @@ packages: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + remark-frontmatter@4.0.1: resolution: {integrity: sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==} @@ -7116,6 +7859,11 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + rollup@3.29.5: resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -7173,6 +7921,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0-rc.1: + resolution: {integrity: sha512-fVinv2lXqYpKConAMdergOl5owd0rY1O4P/QTe0aWKCqGtu7VsCt1iqQFxSJtqK4Lci/upVSBpGwVC7eWcuS9Q==} + search-insights@2.17.2: resolution: {integrity: sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==} @@ -7189,6 +7940,9 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} @@ -7289,6 +8043,9 @@ packages: resolution: {integrity: sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==} engines: {node: '>=8.0.0'} + smob@1.5.0: + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} @@ -7311,6 +8068,10 @@ packages: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -7426,6 +8187,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + string.prototype.trim@1.2.9: resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} engines: {node: '>= 0.4'} @@ -7446,6 +8211,10 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -7458,6 +8227,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -7555,10 +8328,23 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + terser@5.36.0: + resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} + engines: {node: '>=10'} + hasBin: true + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -7705,6 +8491,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -7733,6 +8523,12 @@ packages: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} + types-react-dom@19.0.0-rc.1: + resolution: {integrity: sha512-VSLZJl8VXCD0fAWp7DUTFUDCcZ8DVXOQmjhJMD03odgeFmu14ZQJHCXeETm3BEAhJqfgJaFkLnGkQv88sRx0fQ==} + + types-react@19.0.0-rc.1: + resolution: {integrity: sha512-RshndUfqTW6K3STLPis8BtAYCGOkMbtvYsi90gmVNDZBXUyUc5juf2PE9LfS/JmOlUIRO8cWTS/1MTnmhjDqyQ==} + typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} @@ -7762,6 +8558,22 @@ packages: resolution: {integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==} engines: {node: '>=18.17'} + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} @@ -7773,6 +8585,10 @@ packages: resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + unist-util-generated@2.0.1: resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} @@ -7824,6 +8640,10 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -7899,6 +8719,18 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + valtio@2.1.2: + resolution: {integrity: sha512-fhekN5Rq7dvHULHHBlJeXHrQDl0Jj9GXfNavCm3gkD06crGchaG1nf/J7gSlfZU2wPcRdVS5jBKWHtE2NNz97A==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + react: '>=18.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -7928,6 +8760,18 @@ packages: vite-plugin-capsize-radix@0.0.9: resolution: {integrity: sha512-pd9qgkxyTXkWstMy5yri65mbgCZrmQm/VHcPzbfdLs0jUEP0/C6LUtupEMzxcjYP2jqQyic4tBNocyVf060Ccw==} + vite-plugin-pwa@0.21.0: + resolution: {integrity: sha512-gnDE5sN2hdxA4vTl0pe6PCTPXqChk175jH8dZVVTBjFhWarZZoXaAdoTIKCIa8Zbx94sC0CnCOyERBWpxvry+g==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vite-pwa/assets-generator': ^0.2.6 + vite: ^3.1.0 || ^4.0.0 || ^5.0.0 + workbox-build: ^7.3.0 + workbox-window: ^7.3.0 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + vite-plugin-svgr@3.3.0: resolution: {integrity: sha512-vWZMCcGNdPqgziYFKQ3Y95XP0d0YGp28+MM3Dp9cTa/px5CKcHHrIoPl2Jw81rgVm6/ZUNONzjXbZQZ7Kw66og==} peerDependencies: @@ -8008,6 +8852,12 @@ packages: terser: optional: true + vitepress-plugin-tabs@0.5.0: + resolution: {integrity: sha512-SIhFWwGsUkTByfc2b279ray/E0Jt8vDTsM1LiHxmCOBAEMmvzIBZSuYYT1DpdDTiS3SuJieBheJkYnwCq/yD9A==} + peerDependencies: + vitepress: ^1.0.0-rc.27 + vue: ^3.3.8 + vitepress@1.5.0: resolution: {integrity: sha512-q4Q/G2zjvynvizdB3/bupdYkCJe2umSAMv9Ju4d92E6/NXJ59z70xB0q5p/4lpRyAwflDsbwy1mLV9Q5+nlB+g==} hasBin: true @@ -8151,6 +9001,55 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workbox-background-sync@7.3.0: + resolution: {integrity: sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==} + + workbox-broadcast-update@7.3.0: + resolution: {integrity: sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==} + + workbox-build@7.3.0: + resolution: {integrity: sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==} + engines: {node: '>=16.0.0'} + + workbox-cacheable-response@7.3.0: + resolution: {integrity: sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==} + + workbox-core@7.3.0: + resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==} + + workbox-expiration@7.3.0: + resolution: {integrity: sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==} + + workbox-google-analytics@7.3.0: + resolution: {integrity: sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==} + + workbox-navigation-preload@7.3.0: + resolution: {integrity: sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==} + + workbox-precaching@7.3.0: + resolution: {integrity: sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==} + + workbox-range-requests@7.3.0: + resolution: {integrity: sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==} + + workbox-recipes@7.3.0: + resolution: {integrity: sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==} + + workbox-routing@7.3.0: + resolution: {integrity: sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==} + + workbox-strategies@7.3.0: + resolution: {integrity: sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==} + + workbox-streams@7.3.0: + resolution: {integrity: sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==} + + workbox-sw@7.3.0: + resolution: {integrity: sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==} + + workbox-window@7.3.0: + resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -8370,6 +9269,13 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': + dependencies: + ajv: 8.17.1 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -8410,6 +9316,13 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-compilation-targets@7.25.9': dependencies: '@babel/compat-data': 7.26.2 @@ -8431,6 +9344,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.3.7(supports-color@5.5.0) + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.25.9 @@ -8460,6 +9391,15 @@ snapshots: '@babel/helper-plugin-utils@7.25.9': {} + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -8489,6 +9429,14 @@ snapshots: '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + '@babel/helpers@7.26.0': dependencies: '@babel/template': 7.25.9 @@ -8498,51 +9446,502 @@ snapshots: dependencies: '@babel/types': 7.26.0 - '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + + '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.25.9 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 + + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-simple-access': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-simple-access': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) '@babel/helper-plugin-utils': 7.25.9 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/preset-env@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + semver: 6.3.1 transitivePeerDependencies: - supports-color + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.0 + esutils: 2.0.3 + '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -8862,6 +10261,22 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@electric-sql/client@0.8.0': + optionalDependencies: + '@rollup/rollup-darwin-arm64': 4.24.4 + + '@electric-sql/pglite-react@0.2.14(@electric-sql/pglite@0.2.14)(react@19.0.0-rc.1)': + dependencies: + '@electric-sql/pglite': 0.2.14 + react: 19.0.0-rc.1 + + '@electric-sql/pglite-sync@0.2.16(@electric-sql/pglite@0.2.14)': + dependencies: + '@electric-sql/client': 0.8.0 + '@electric-sql/pglite': 0.2.14 + + '@electric-sql/pglite@0.2.14': {} + '@emotion/hash@0.9.2': {} '@emotion/is-prop-valid@1.2.2': @@ -9337,6 +10752,11 @@ snapshots: '@jridgewell/set-array@1.2.1': {} + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': @@ -10193,7 +11613,7 @@ snapshots: '@remirror/core-constants@3.0.0': {} - '@remix-run/dev@2.13.1(@remix-run/react@2.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@remix-run/serve@2.13.1(typescript@5.6.3))(@types/node@20.17.6)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6))': + '@remix-run/dev@2.13.1(@remix-run/react@2.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@remix-run/serve@2.13.1(typescript@5.6.3))(@types/node@20.17.6)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))': dependencies: '@babel/core': 7.26.0 '@babel/generator': 7.26.2 @@ -10210,7 +11630,7 @@ snapshots: '@remix-run/router': 1.20.0 '@remix-run/server-runtime': 2.13.1(typescript@5.6.3) '@types/mdx': 2.0.13 - '@vanilla-extract/integration': 6.5.0(@types/node@20.17.6) + '@vanilla-extract/integration': 6.5.0(@types/node@20.17.6)(terser@5.36.0) arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 @@ -10252,7 +11672,7 @@ snapshots: optionalDependencies: '@remix-run/serve': 2.13.1(typescript@5.6.3) typescript: 5.6.3 - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -10358,6 +11778,56 @@ snapshots: dependencies: web-streams-polyfill: 3.3.3 + '@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@2.79.2)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + rollup: 2.79.2 + optionalDependencies: + '@types/babel__core': 7.20.5 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-node-resolve@15.3.0(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@2.79.2) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.8 + optionalDependencies: + rollup: 2.79.2 + + '@rollup/plugin-replace@2.4.2(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + magic-string: 0.25.9 + rollup: 2.79.2 + + '@rollup/plugin-terser@0.4.4(rollup@2.79.2)': + dependencies: + serialize-javascript: 6.0.2 + smob: 1.5.0 + terser: 5.36.0 + optionalDependencies: + rollup: 2.79.2 + + '@rollup/pluginutils@3.1.0(rollup@2.79.2)': + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.2 + + '@rollup/pluginutils@5.1.3(rollup@2.79.2)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 2.79.2 + '@rollup/pluginutils@5.1.3(rollup@4.24.4)': dependencies: '@types/estree': 1.0.6 @@ -10453,6 +11923,13 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@surma/rollup-plugin-off-main-thread@2.2.3': + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.11 + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -10869,6 +12346,8 @@ snapshots: dependencies: '@types/estree': 1.0.6 + '@types/estree@0.0.39': {} + '@types/estree@1.0.6': {} '@types/hast@2.3.10': @@ -10988,6 +12467,8 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 + '@types/resolve@1.20.2': {} + '@types/semver@7.5.8': {} '@types/stack-utils@2.0.3': {} @@ -10998,6 +12479,8 @@ snapshots: dependencies: '@types/node': 20.17.6 + '@types/trusted-types@2.0.7': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -11219,7 +12702,7 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@vanilla-extract/integration@6.5.0(@types/node@20.17.6)': + '@vanilla-extract/integration@6.5.0(@types/node@20.17.6)(terser@5.36.0)': dependencies: '@babel/core': 7.26.0 '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) @@ -11232,8 +12715,8 @@ snapshots: lodash: 4.17.21 mlly: 1.7.2 outdent: 0.8.0 - vite: 5.4.10(@types/node@20.17.6) - vite-node: 1.6.0(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) + vite-node: 1.6.0(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -11248,38 +12731,38 @@ snapshots: '@vanilla-extract/private@1.0.6': {} - '@vitejs/plugin-react-swc@3.7.1(@swc/helpers@0.5.5)(vite@5.4.10(@types/node@20.17.6))': + '@vitejs/plugin-react-swc@3.7.1(@swc/helpers@0.5.5)(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))': dependencies: '@swc/core': 1.9.1(@swc/helpers@0.5.5) - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.3.3(vite@4.5.5(@types/node@20.17.6))': + '@vitejs/plugin-react@4.3.3(vite@4.5.5(@types/node@20.17.6)(terser@5.36.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 4.5.5(@types/node@20.17.6) + vite: 4.5.5(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.3(vite@5.4.10(@types/node@20.17.6))': + '@vitejs/plugin-react@4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.1.4(vite@5.4.10(@types/node@20.17.6))(vue@3.5.12(typescript@5.6.3))': + '@vitejs/plugin-vue@5.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))': dependencies: - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) vue: 3.5.12(typescript@5.6.3) '@vitest/expect@2.1.4': @@ -11289,13 +12772,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.4(vite@5.4.10(@types/node@20.17.6))': + '@vitest/mocker@2.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))': dependencies: '@vitest/spy': 2.1.4 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) '@vitest/pretty-format@2.1.4': dependencies: @@ -11462,6 +12945,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch@5.13.0: dependencies: '@algolia/client-abtesting': 5.13.0 @@ -11563,8 +13053,12 @@ snapshots: astring@1.9.0: {} + async@3.2.6: {} + asynckit@0.4.0: {} + at-least-node@1.0.0: {} + autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.2 @@ -11581,6 +13075,30 @@ snapshots: aws4fetch@1.0.20: {} + babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0): + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -11835,6 +13353,8 @@ snapshots: commander@7.2.0: {} + common-tags@1.8.2: {} + compressible@2.0.18: dependencies: mime-db: 1.53.0 @@ -11889,6 +13409,10 @@ snapshots: dependencies: is-what: 4.1.16 + core-js-compat@3.39.0: + dependencies: + browserslist: 4.24.2 + core-js@3.39.0: {} core-util-is@1.0.3: {} @@ -11921,6 +13445,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-random-string@2.0.0: {} + css-box-model@1.2.1: dependencies: tiny-invariant: 1.3.3 @@ -12148,6 +13674,10 @@ snapshots: ee-first@1.1.1: {} + ejs@3.1.10: + dependencies: + jake: 10.9.2 + electron-to-chromium@1.5.52: {} emoji-regex@8.0.0: {} @@ -12650,6 +14180,8 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 2.0.11 + estree-walker@1.0.1: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -12757,6 +14289,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-uri@3.0.3: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -12773,6 +14307,10 @@ snapshots: dependencies: flat-cache: 3.2.0 + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -12858,6 +14396,13 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs-minipass@2.1.0: dependencies: minipass: 3.3.6 @@ -12904,6 +14449,8 @@ snapshots: get-nonce@1.0.1: {} + get-own-enumerable-property-symbols@3.0.2: {} + get-port-please@3.1.2: {} get-port@5.1.1: {} @@ -13124,6 +14671,8 @@ snapshots: dependencies: postcss: 8.4.47 + idb@7.1.1: {} + ieee754@1.2.1: {} ignore-by-default@1.0.1: {} @@ -13253,6 +14802,8 @@ snapshots: is-map@2.0.3: {} + is-module@1.0.0: {} + is-negative-zero@2.0.3: {} is-number-object@1.0.7: @@ -13261,6 +14812,8 @@ snapshots: is-number@7.0.0: {} + is-obj@1.0.1: {} + is-path-inside@3.0.3: {} is-plain-obj@3.0.0: {} @@ -13280,6 +14833,8 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-regexp@1.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.3: @@ -13337,6 +14892,13 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + javascript-stringify@2.1.0: {} jest-diff@29.7.0: @@ -13441,6 +15003,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -13455,6 +15019,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpointer@5.0.1: {} + jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -13485,6 +15051,8 @@ snapshots: kleur@4.1.5: {} + leven@3.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -13589,6 +15157,10 @@ snapshots: lz-string@1.5.0: {} + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -14684,6 +16256,10 @@ snapshots: prettier@3.3.3: {} + pretty-bytes@5.6.0: {} + + pretty-bytes@6.1.1: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -14829,6 +16405,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-compare@3.0.1: {} + pseudomap@1.0.2: {} pstree.remy@1.1.8: {} @@ -14905,6 +16483,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@19.0.0-rc.1(react@19.0.0-rc.1): + dependencies: + react: 19.0.0-rc.1 + scheduler: 0.25.0-rc.1 + react-icons@4.12.0(react@18.3.1): dependencies: react: 18.3.1 @@ -15037,6 +16620,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + react@19.0.0-rc.1: {} + read-cache@1.0.0: dependencies: pify: 2.3.0 @@ -15124,8 +16709,18 @@ snapshots: reftools@1.1.9: {} + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + regenerator-runtime@0.14.1: {} + regenerator-transform@0.15.2: + dependencies: + '@babel/runtime': 7.26.0 + regex@4.4.0: {} regexp.prototype.flags@1.5.3: @@ -15135,6 +16730,21 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + remark-frontmatter@4.0.1: dependencies: '@types/mdast': 3.0.15 @@ -15212,6 +16822,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + rollup@2.79.2: + optionalDependencies: + fsevents: 2.3.3 + rollup@3.29.5: optionalDependencies: fsevents: 2.3.3 @@ -15290,6 +16904,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + scheduler@0.25.0-rc.1: {} + search-insights@2.17.2: {} semver@6.3.1: {} @@ -15314,6 +16930,10 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -15438,6 +17058,8 @@ snapshots: slugify@1.4.7: {} + smob@1.5.0: {} + snake-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -15458,6 +17080,8 @@ snapshots: dependencies: whatwg-url: 7.1.0 + sourcemap-codec@1.4.8: {} + space-separated-tokens@2.0.2: {} spawn-command@0.0.2: {} @@ -15554,6 +17178,21 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string.prototype.matchall@4.0.11: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.3 + set-function-name: 2.0.2 + side-channel: 1.0.6 + string.prototype.trim@1.2.9: dependencies: call-bind: 1.0.7 @@ -15586,6 +17225,12 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -15596,6 +17241,8 @@ snapshots: strip-bom@3.0.0: {} + strip-comments@2.0.1: {} + strip-final-newline@2.0.0: {} strip-json-comments@3.1.1: {} @@ -15741,8 +17388,24 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + temp-dir@2.0.0: {} + + tempy@0.6.0: + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + term-size@2.2.1: {} + terser@5.36.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -15884,6 +17547,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.16.0: {} + type-fest@0.20.2: {} type-fest@0.21.3: {} @@ -15925,6 +17590,14 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 + types-react-dom@19.0.0-rc.1: + dependencies: + '@types/react': 18.3.12 + + types-react@19.0.0-rc.1: + dependencies: + csstype: 3.1.3 + typescript@5.6.3: {} uc.micro@2.1.0: {} @@ -15947,6 +17620,17 @@ snapshots: undici@6.20.1: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + unified@10.1.2: dependencies: '@types/unist': 2.0.11 @@ -15965,6 +17649,10 @@ snapshots: dependencies: imurmurhash: 0.1.4 + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + unist-util-generated@2.0.1: {} unist-util-is@5.2.1: @@ -16028,6 +17716,8 @@ snapshots: unpipe@1.0.0: {} + upath@1.2.0: {} + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 @@ -16095,6 +17785,13 @@ snapshots: validate-npm-package-name@5.0.1: {} + valtio@2.1.2(react@19.0.0-rc.1)(types-react@19.0.0-rc.1): + dependencies: + proxy-compare: 3.0.1 + optionalDependencies: + '@types/react': types-react@19.0.0-rc.1 + react: 19.0.0-rc.1 + vary@1.1.2: {} vfile-message@3.1.4: @@ -16119,13 +17816,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@1.6.0(@types/node@20.17.6): + vite-node@1.6.0(@types/node@20.17.6)(terser@5.36.0): dependencies: cac: 6.7.14 debug: 4.3.7(supports-color@5.5.0) pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - '@types/node' - less @@ -16137,12 +17834,12 @@ snapshots: - supports-color - terser - vite-node@2.1.4(@types/node@20.17.6): + vite-node@2.1.4(@types/node@20.17.6)(terser@5.36.0): dependencies: cac: 6.7.14 debug: 4.3.7(supports-color@5.5.0) pathe: 1.1.2 - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - '@types/node' - less @@ -16160,12 +17857,23 @@ snapshots: '@capsizecss/metrics': 2.2.0 mustache: 4.2.0 - vite-plugin-svgr@3.3.0(rollup@4.24.4)(typescript@5.6.3)(vite@4.5.5(@types/node@20.17.6)): + vite-plugin-pwa@0.21.0(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): + dependencies: + debug: 4.3.7(supports-color@5.5.0) + pretty-bytes: 6.1.1 + tinyglobby: 0.2.10 + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) + workbox-build: 7.3.0(@types/babel__core@7.20.5) + workbox-window: 7.3.0 + transitivePeerDependencies: + - supports-color + + vite-plugin-svgr@3.3.0(rollup@4.24.4)(typescript@5.6.3)(vite@4.5.5(@types/node@20.17.6)(terser@5.36.0)): dependencies: '@rollup/pluginutils': 5.1.3(rollup@4.24.4) '@svgr/core': 8.1.0(typescript@5.6.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.6.3)) - vite: 4.5.5(@types/node@20.17.6) + vite: 4.5.5(@types/node@20.17.6)(terser@5.36.0) transitivePeerDependencies: - rollup - supports-color @@ -16180,7 +17888,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vite@4.5.5(@types/node@20.17.6): + vite@4.5.5(@types/node@20.17.6)(terser@5.36.0): dependencies: esbuild: 0.18.20 postcss: 8.4.47 @@ -16188,8 +17896,9 @@ snapshots: optionalDependencies: '@types/node': 20.17.6 fsevents: 2.3.3 + terser: 5.36.0 - vite@5.4.10(@types/node@20.17.6): + vite@5.4.10(@types/node@20.17.6)(terser@5.36.0): dependencies: esbuild: 0.21.5 postcss: 8.4.47 @@ -16197,8 +17906,14 @@ snapshots: optionalDependencies: '@types/node': 20.17.6 fsevents: 2.3.3 + terser: 5.36.0 + + vitepress-plugin-tabs@0.5.0(vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3))(vue@3.5.12(typescript@5.6.3)): + dependencies: + vitepress: 1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3) + vue: 3.5.12(typescript@5.6.3) - vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(typescript@5.6.3): + vitepress@1.5.0(@algolia/client-search@5.13.0)(@types/node@20.17.6)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2)(terser@5.36.0)(typescript@5.6.3): dependencies: '@docsearch/css': 3.7.0 '@docsearch/js': 3.7.0(@algolia/client-search@5.13.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.2) @@ -16207,7 +17922,7 @@ snapshots: '@shikijs/transformers': 1.22.2 '@shikijs/types': 1.22.2 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.1.4(vite@5.4.10(@types/node@20.17.6))(vue@3.5.12(typescript@5.6.3)) + '@vitejs/plugin-vue': 5.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3)) '@vue/devtools-api': 7.6.3 '@vue/shared': 3.5.12 '@vueuse/core': 11.2.0(vue@3.5.12(typescript@5.6.3)) @@ -16216,7 +17931,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.1.0 shiki: 1.22.2 - vite: 5.4.10(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) vue: 3.5.12(typescript@5.6.3) optionalDependencies: postcss: 8.4.47 @@ -16248,10 +17963,10 @@ snapshots: - typescript - universal-cookie - vitest@2.1.4(@types/node@20.17.6)(jsdom@25.0.1): + vitest@2.1.4(@types/node@20.17.6)(jsdom@25.0.1)(terser@5.36.0): dependencies: '@vitest/expect': 2.1.4 - '@vitest/mocker': 2.1.4(vite@5.4.10(@types/node@20.17.6)) + '@vitest/mocker': 2.1.4(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0)) '@vitest/pretty-format': 2.1.4 '@vitest/runner': 2.1.4 '@vitest/snapshot': 2.1.4 @@ -16267,8 +17982,8 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.10(@types/node@20.17.6) - vite-node: 2.1.4(@types/node@20.17.6) + vite: 5.4.10(@types/node@20.17.6)(terser@5.36.0) + vite-node: 2.1.4(@types/node@20.17.6)(terser@5.36.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.17.6 @@ -16392,6 +18107,119 @@ snapshots: wordwrap@1.0.0: {} + workbox-background-sync@7.3.0: + dependencies: + idb: 7.1.1 + workbox-core: 7.3.0 + + workbox-broadcast-update@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-build@7.3.0(@types/babel__core@7.20.5): + dependencies: + '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) + '@babel/core': 7.26.0 + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/runtime': 7.26.0 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@2.79.2) + '@rollup/plugin-node-resolve': 15.3.0(rollup@2.79.2) + '@rollup/plugin-replace': 2.4.2(rollup@2.79.2) + '@rollup/plugin-terser': 0.4.4(rollup@2.79.2) + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.17.1 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.2 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 7.3.0 + workbox-broadcast-update: 7.3.0 + workbox-cacheable-response: 7.3.0 + workbox-core: 7.3.0 + workbox-expiration: 7.3.0 + workbox-google-analytics: 7.3.0 + workbox-navigation-preload: 7.3.0 + workbox-precaching: 7.3.0 + workbox-range-requests: 7.3.0 + workbox-recipes: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + workbox-streams: 7.3.0 + workbox-sw: 7.3.0 + workbox-window: 7.3.0 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + + workbox-cacheable-response@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-core@7.3.0: {} + + workbox-expiration@7.3.0: + dependencies: + idb: 7.1.1 + workbox-core: 7.3.0 + + workbox-google-analytics@7.3.0: + dependencies: + workbox-background-sync: 7.3.0 + workbox-core: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + + workbox-navigation-preload@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-precaching@7.3.0: + dependencies: + workbox-core: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + + workbox-range-requests@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-recipes@7.3.0: + dependencies: + workbox-cacheable-response: 7.3.0 + workbox-core: 7.3.0 + workbox-expiration: 7.3.0 + workbox-precaching: 7.3.0 + workbox-routing: 7.3.0 + workbox-strategies: 7.3.0 + + workbox-routing@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-strategies@7.3.0: + dependencies: + workbox-core: 7.3.0 + + workbox-streams@7.3.0: + dependencies: + workbox-core: 7.3.0 + workbox-routing: 7.3.0 + + workbox-sw@7.3.0: {} + + workbox-window@7.3.0: + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 7.3.0 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index 191db818ea..67da270852 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -1,5 +1,6 @@ import fs from 'node:fs' import { defineConfig } from 'vitepress' +import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' import postsData from '../data/posts.data.ts' @@ -51,7 +52,10 @@ export default defineConfig({ 'sql', 'tsx', 'typescript' - ] + ], + config(md) { + md.use(tabsMarkdownPlugin) + } }, rewrites(id) { if (id.startsWith('blog/posts')) { @@ -154,10 +158,11 @@ export default defineConfig({ items: [ { text: 'Auth', link: '/docs/guides/auth' }, { text: 'Shapes', link: '/docs/guides/shapes' }, + { text: 'Writes', link: '/docs/guides/writes' }, { text: 'Installation', link: '/docs/guides/installation' }, { text: 'Deployment', link: '/docs/guides/deployment' }, { text: 'Troubleshooting', link: '/docs/guides/troubleshooting' }, - { text: 'Writing your own client', link: '/docs/guides/writing-your-own-client' }, + { text: 'Client development', link: '/docs/guides/client-development' }, ] }, { diff --git a/website/.vitepress/theme/custom.css b/website/.vitepress/theme/custom.css index 80130bc115..8d718413a3 100644 --- a/website/.vitepress/theme/custom.css +++ b/website/.vitepress/theme/custom.css @@ -129,6 +129,9 @@ display: inline-flex !important; align-items: center; } +.vpi-social-github { + --icon: url('https://api.iconify.design/simple-icons/github.svg'); +} .action a .vpi-social-discord, .action a .vpi-social-github, .feature-cta a .vpi-social-github, diff --git a/website/.vitepress/theme/index.js b/website/.vitepress/theme/index.js index e2aeca1448..b377e7c808 100644 --- a/website/.vitepress/theme/index.js +++ b/website/.vitepress/theme/index.js @@ -1,12 +1,14 @@ import DefaultTheme, { VPButton } from 'vitepress/theme-without-fonts' -import Layout from './Layout.vue' +import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' import YoutubeEmbed from '../../src/components/YoutubeEmbed.vue' +import Layout from './Layout.vue' import './custom.css' export default { enhanceApp({ app }) { app.component('VPButton', VPButton) app.component('YoutubeEmbed', YoutubeEmbed) + enhanceAppWithTabs(app) }, extends: DefaultTheme, Layout: Layout diff --git a/website/blog/posts/2024-11-21-local-first-with-your-existing-api.md b/website/blog/posts/2024-11-21-local-first-with-your-existing-api.md new file mode 100644 index 0000000000..252592329d --- /dev/null +++ b/website/blog/posts/2024-11-21-local-first-with-your-existing-api.md @@ -0,0 +1,17 @@ +--- +title: Local-first with your existing API +description: >- + How to develop local-first apps incrementally, using your existing API. +excerpt: >- + Local-first is often seen as eliminating your API. But what if you like + your API or need to keep it because of other code paths and integrations? + This post shows how you can develop local-first apps incrementally, + using your existing API. +authors: [thruflo] +image: /img/blog/local-first-with-your-existing-api/humble-toaster.jpg +tags: [local-first example] +outline: [2, 4] +post: true +--- + +This post will be published shortly. Bear with us. \ No newline at end of file diff --git a/website/docs/api/http.md b/website/docs/api/http.md index 1c63824956..98affc7893 100644 --- a/website/docs/api/http.md +++ b/website/docs/api/http.md @@ -109,7 +109,7 @@ The server holds open the request until either a timeout (returning `204 No cont The algorithm for consuming the HTTP API described above can be implemented from scratch for your application. Howerver, it's typically implemented by clients that can be re-used and provide a simpler interface for application code. -There are a number of existing clients, such as the [TypeScript](/docs/api/clients/typescript) and [Elixir](/docs/api/clients/elixir) clients. If one doesn't exist for your language or environment, we hope that the pattern is simple enough that you should be able to [write your own client](/docs/guides/writing-your-own-client) relatively easily. +There are a number of existing clients, such as the [TypeScript](/docs/api/clients/typescript) and [Elixir](/docs/api/clients/elixir) clients. If one doesn't exist for your language or environment, we hope that the pattern is simple enough that you should be able to [write your own client](/docs/guides/client-development) relatively easily. ## Caching diff --git a/website/docs/guides/auth.md b/website/docs/guides/auth.md index e947a32f17..140ec395e0 100644 --- a/website/docs/guides/auth.md +++ b/website/docs/guides/auth.md @@ -14,6 +14,10 @@ import GatekeeperFlow from '/static/img/docs/guides/auth/gatekeeper-flow.dark.pn import GatekeeperFlowJPG from '/static/img/docs/guides/auth/gatekeeper-flow.jpg?url' + + # Auth