diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c58f00b..948d86b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,12 +8,12 @@ ## Language-Specific Rules -| Language | Rule File | -|----------|-----------| -| **TypeScript/React** | `spacetimedb-typescript.mdc` (MANDATORY) | -| **Rust** | `spacetimedb-rust.mdc` (MANDATORY) | -| **C#** | `spacetimedb-csharp.mdc` (MANDATORY) | -| **Migrating 1.0 → 2.0** | `spacetimedb-migration-2.0.mdc` | +| Language | Rule File | +| ----------------------- | ---------------------------------------- | +| **TypeScript/React** | `spacetimedb-typescript.mdc` (MANDATORY) | +| **Rust** | `spacetimedb-rust.mdc` (MANDATORY) | +| **C#** | `spacetimedb-csharp.mdc` (MANDATORY) | +| **Migrating 1.0 → 2.0** | `spacetimedb-migration-2.0.mdc` | --- @@ -44,6 +44,7 @@ When implementing a feature that spans backend and client: ## Index System SpacetimeDB automatically creates indexes for: + - Primary key columns - Columns marked as unique @@ -52,6 +53,7 @@ You can add explicit indexes on non-unique columns for query performance. **Index names must be unique across your entire module (all tables).** If two tables have indexes with the same declared name → conflict error. **Schema ↔ Code coupling:** + - Your query code references indexes by name - If you add/remove/rename an index in the schema, update all code that uses it - Removing an index without updating queries causes runtime errors @@ -85,7 +87,7 @@ spacetime logs ## Deployment - Maincloud is the spacetimedb hosted cloud and the default location for module publishing -- The default server marked by *** in `spacetime server list` should be used when publishing +- The default server marked by \*\*\* in `spacetime server list` should be used when publishing - If the default server is maincloud you should publish to maincloud - Publishing to maincloud is free of charge - When publishing to maincloud the database dashboard will be at the url: https://spacetimedb.com/@/ @@ -110,7 +112,6 @@ spacetime logs - Do NOT invent new SpacetimeDB APIs — use only what exists in docs or this repo - Do NOT add restrictions the prompt didn't ask for — if "users can do X", implement X for all users - # SpacetimeDB TypeScript SDK ## ⛔ HALLUCINATED APIs — DO NOT USE @@ -139,11 +140,11 @@ tables.user.filter(u => u.name === 'alice'); // No .filter() on tables object! ```typescript // ✅ CORRECT IMPORTS -import { DbConnection, tables } from './module_bindings'; // Generated! -import { SpacetimeDBProvider, useTable, Identity } from 'spacetimedb/react'; +import { DbConnection, tables } from "./module_bindings"; // Generated! +import { SpacetimeDBProvider, useTable, Identity } from "spacetimedb/react"; // ✅ CORRECT REDUCER CALLS — object syntax, not positional! -conn.reducers.doSomething({ value: 'test' }); +conn.reducers.doSomething({ value: "test" }); conn.reducers.updateItem({ itemId: 1n, newValue: 42 }); // ✅ CORRECT DATA ACCESS — useTable returns [rows, isLoading] @@ -151,6 +152,7 @@ const [items, isLoading] = useTable(tables.item); ``` ### ⛔ DO NOT: + - **Invent hooks** like `useItems()`, `useData()` — use `useTable(tables.tableName)` - **Import from fake packages** — only `spacetimedb`, `spacetimedb/react`, `./module_bindings` @@ -160,40 +162,40 @@ const [items, isLoading] = useTable(tables.item); ### Server-side errors -| Wrong | Right | Error | -|-------|-------|-------| -| Missing `package.json` | Create `package.json` | "could not detect language" | -| Missing `tsconfig.json` | Create `tsconfig.json` | "TsconfigNotFound" | -| Entrypoint not at `src/index.ts` | Use `src/index.ts` | Module won't bundle | -| `indexes` in COLUMNS (2nd arg) | `indexes` in OPTIONS (1st arg) | "reading 'tag'" error | -| Index without `algorithm` | `algorithm: 'btree'` | "reading 'tag'" error | -| `filter({ ownerId })` | `filter(ownerId)` | "does not exist in type 'Range'" | -| `.filter()` on unique column | `.find()` on unique column | TypeError | -| `insert({ ...without id })` | `insert({ id: 0n, ... })` | "Property 'id' is missing" | -| `const id = table.insert(...)` | `const row = table.insert(...)` | `.insert()` returns ROW, not ID | -| `.unique()` + explicit index | Just use `.unique()` | "name is used for multiple entities" | -| Index on `.primaryKey()` column | Don't — already indexed | "name is used for multiple entities" | -| Same index name in multiple tables | Prefix with table name | "name is used for multiple entities" | -| `.indexName.filter()` after removing index | Use `.iter()` + manual filter | "Cannot read properties of undefined" | -| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" | -| Multi-column index `.filter()` | **⚠️ BROKEN** — use single-column | PANIC or silent empty results | -| `JSON.stringify({ id: row.id })` | Convert BigInt first: `{ id: row.id.toString() }` | "Do not know how to serialize a BigInt" | -| `ScheduleAt.Time(timestamp)` | `ScheduleAt.time(timestamp)` (lowercase) | "ScheduleAt.Time is not a function" | -| `ctx.db.foo.myIndexName.filter()` | Use exact name: `ctx.db.foo.my_index_name.filter()` | "Cannot read properties of undefined" | -| `.iter()` in views | Use index lookups | Severe performance issues (re-evaluates on any change) | -| `ctx.db` in procedures | `ctx.withTx(tx => tx.db...)` | Procedures need explicit transactions | -| `ctx.myTable` in procedure tx | `tx.db.myTable` | Wrong context variable | +| Wrong | Right | Error | +| ------------------------------------------ | --------------------------------------------------- | ------------------------------------------------------ | +| Missing `package.json` | Create `package.json` | "could not detect language" | +| Missing `tsconfig.json` | Create `tsconfig.json` | "TsconfigNotFound" | +| Entrypoint not at `src/index.ts` | Use `src/index.ts` | Module won't bundle | +| `indexes` in COLUMNS (2nd arg) | `indexes` in OPTIONS (1st arg) | "reading 'tag'" error | +| Index without `algorithm` | `algorithm: 'btree'` | "reading 'tag'" error | +| `filter({ ownerId })` | `filter(ownerId)` | "does not exist in type 'Range'" | +| `.filter()` on unique column | `.find()` on unique column | TypeError | +| `insert({ ...without id })` | `insert({ id: 0n, ... })` | "Property 'id' is missing" | +| `const id = table.insert(...)` | `const row = table.insert(...)` | `.insert()` returns ROW, not ID | +| `.unique()` + explicit index | Just use `.unique()` | "name is used for multiple entities" | +| Index on `.primaryKey()` column | Don't — already indexed | "name is used for multiple entities" | +| Same index name in multiple tables | Prefix with table name | "name is used for multiple entities" | +| `.indexName.filter()` after removing index | Use `.iter()` + manual filter | "Cannot read properties of undefined" | +| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" | +| Multi-column index `.filter()` | **⚠️ BROKEN** — use single-column | PANIC or silent empty results | +| `JSON.stringify({ id: row.id })` | Convert BigInt first: `{ id: row.id.toString() }` | "Do not know how to serialize a BigInt" | +| `ScheduleAt.Time(timestamp)` | `ScheduleAt.time(timestamp)` (lowercase) | "ScheduleAt.Time is not a function" | +| `ctx.db.foo.myIndexName.filter()` | Use exact name: `ctx.db.foo.my_index_name.filter()` | "Cannot read properties of undefined" | +| `.iter()` in views | Use index lookups | Severe performance issues (re-evaluates on any change) | +| `ctx.db` in procedures | `ctx.withTx(tx => tx.db...)` | Procedures need explicit transactions | +| `ctx.myTable` in procedure tx | `tx.db.myTable` | Wrong context variable | ### Client-side errors -| Wrong | Right | Error | -|-------|-------|-------| -| `@spacetimedb/sdk` | `spacetimedb` | 404 / missing subpath | -| `conn.reducers.foo("val")` | `conn.reducers.foo({ param: "val" })` | Wrong reducer syntax | -| Inline `connectionBuilder` | `useMemo(() => ..., [])` | Reconnects every render | -| `const rows = useTable(table)` | `const [rows, isLoading] = useTable(table)` | Tuple destructuring | -| Optimistic UI updates | Let subscriptions drive state | Desync issues | -| `` | `connectionBuilder={...}` | Wrong prop name | +| Wrong | Right | Error | +| ------------------------------------- | ------------------------------------------- | ----------------------- | +| `@spacetimedb/sdk` | `spacetimedb` | 404 / missing subpath | +| `conn.reducers.foo("val")` | `conn.reducers.foo({ param: "val" })` | Wrong reducer syntax | +| Inline `connectionBuilder` | `useMemo(() => ..., [])` | Reconnects every render | +| `const rows = useTable(table)` | `const [rows, isLoading] = useTable(table)` | Tuple destructuring | +| Optimistic UI updates | Let subscriptions drive state | Desync issues | +| `` | `connectionBuilder={...}` | Wrong prop name | --- @@ -202,62 +204,77 @@ const [items, isLoading] = useTable(tables.item); **`table()` takes TWO arguments: `table(OPTIONS, COLUMNS)`** ```typescript -import { schema, table, t } from 'spacetimedb/server'; +import { schema, table, t } from "spacetimedb/server"; // ❌ WRONG — indexes in COLUMNS causes "reading 'tag'" error -export const Task = table({ name: 'task' }, { - id: t.u64().primaryKey().autoInc(), - ownerId: t.identity(), - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] // ❌ WRONG! -}); +export const Task = table( + { name: "task" }, + { + id: t.u64().primaryKey().autoInc(), + ownerId: t.identity(), + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], // ❌ WRONG! + }, +); // ✅ RIGHT — indexes in OPTIONS (first argument) -export const Task = table({ - name: 'task', - public: true, - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] -}, { - id: t.u64().primaryKey().autoInc(), - ownerId: t.identity(), - title: t.string(), - createdAt: t.timestamp(), -}); +export const Task = table( + { + name: "task", + public: true, + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], + }, + { + id: t.u64().primaryKey().autoInc(), + ownerId: t.identity(), + title: t.string(), + createdAt: t.timestamp(), + }, +); ``` ### Column types + ```typescript -t.identity() // User identity (primary key for per-user tables) -t.u64() // Unsigned 64-bit integer (use for IDs) -t.string() // Text -t.bool() // Boolean -t.timestamp() // Timestamp (use ctx.timestamp for current time) -t.scheduleAt() // For scheduled tables only +t.identity(); // User identity (primary key for per-user tables) +t.u64(); // Unsigned 64-bit integer (use for IDs) +t.string(); // Text +t.bool(); // Boolean +t.timestamp(); // Timestamp (use ctx.timestamp for current time) +t.scheduleAt(); // For scheduled tables only // Product types (nested objects) — use t.object, NOT t.struct -const Point = t.object('Point', { x: t.i32(), y: t.i32() }); +const Point = t.object("Point", { x: t.i32(), y: t.i32() }); // Sum types (tagged unions) — use t.enum, NOT t.sum -const Shape = t.enum('Shape', { circle: t.i32(), rectangle: Point }); +const Shape = t.enum("Shape", { circle: t.i32(), rectangle: Point }); // Values use { tag: 'circle', value: 10 } or { tag: 'rectangle', value: { x: 1, y: 2 } } // Modifiers -t.string().optional() // Nullable -t.u64().primaryKey() // Primary key -t.u64().primaryKey().autoInc() // Auto-increment primary key +t.string().optional(); // Nullable +t.u64().primaryKey(); // Primary key +t.u64().primaryKey().autoInc(); // Auto-increment primary key ``` > ⚠️ **BIGINT SYNTAX:** All `u64`, `i64`, and ID fields use JavaScript BigInt. +> > - Literals: `0n`, `1n`, `100n` (NOT `0`, `1`, `100`) > - Comparisons: `row.id === 5n` (NOT `row.id === 5`) > - Arithmetic: `row.count + 1n` (NOT `row.count + 1`) ### Auto-increment placeholder + ```typescript // ✅ MUST provide 0n placeholder for auto-inc fields -ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title: 'New', createdAt: ctx.timestamp }); +ctx.db.task.insert({ + id: 0n, + ownerId: ctx.sender, + title: "New", + createdAt: ctx.timestamp, +}); ``` ### Insert returns ROW, not ID + ```typescript // ❌ WRONG const id = ctx.db.task.insert({ ... }); @@ -268,14 +285,15 @@ const newId = row.id; // Extract .id from returned row ``` ### Schema export (CRITICAL) + ```typescript // At end of schema.ts — schema() takes exactly ONE argument: an object const spacetimedb = schema({ table1, table2, table3 }); export default spacetimedb; // ❌ WRONG — never pass tables directly or as multiple args -schema(myTable); // WRONG! -schema(t1, t2, t3); // WRONG! +schema(myTable); // WRONG! +schema(t1, t2, t3); // WRONG! ``` --- @@ -294,7 +312,9 @@ const msgs = [...ctx.db.message.message_room_id.filter(roomId)]; // 3. NO INDEX — use .iter() + manual filter for (const m of ctx.db.roomMember.iter()) { - if (m.roomId === roomId) { /* ... */ } + if (m.roomId === roomId) { + /* ... */ + } } ``` @@ -302,24 +322,31 @@ for (const m of ctx.db.roomMember.iter()) { ```typescript // In table OPTIONS (first argument), not columns -export const Message = table({ - name: 'message', - public: true, - indexes: [{ name: 'message_room_id', algorithm: 'btree', columns: ['roomId'] }] -}, { - id: t.u64().primaryKey().autoInc(), - roomId: t.u64(), - // ... -}); +export const Message = table( + { + name: "message", + public: true, + indexes: [ + { name: "message_room_id", algorithm: "btree", columns: ["roomId"] }, + ], + }, + { + id: t.u64().primaryKey().autoInc(), + roomId: t.u64(), + // ... + }, +); ``` ### Naming conventions **Table names — automatic transformation:** -- Schema: `table({ name: 'my_messages' })` + +- Schema: `table({ name: 'my_messages' })` - Access: `ctx.db.myMessages` (automatic snake_case → camelCase) **Index names — NO transformation, use EXACTLY as defined:** + ```typescript // Schema definition indexes: [{ name: 'canvas_member_canvas_id', algorithm: 'btree', columns: ['canvasId'] }] @@ -335,6 +362,7 @@ ctx.db.canvasMember.canvas_member_canvas_id.filter(...) > ⚠️ **Index names are used VERBATIM** — pick a convention (snake_case or camelCase) and stick with it. **Index naming pattern — use `{tableName}_{columnName}`:** + ```typescript // ✅ GOOD — unique names across entire module indexes: [{ name: 'message_room_id', algorithm: 'btree', columns: ['roomId'] }] @@ -346,10 +374,12 @@ indexes: [{ name: 'by_owner', ... }] // in Note table — CONFLICT! ``` **Client-side table names:** + - Check generated `module_bindings/index.ts` for exact export names - Usage: `useTable(tables.MyMessages)` or `tables.myMessages` (varies by SDK version) ### Filter vs Find + ```typescript // Filter takes VALUE directly, not object — returns iterator const rows = [...ctx.db.task.by_owner.filter(ownerId)]; @@ -359,13 +389,16 @@ const row = ctx.db.player.identity.find(ctx.sender); ``` ### ⚠️ Multi-column indexes are BROKEN + ```typescript // ❌ DON'T — causes PANIC ctx.db.scores.by_player_level.filter(playerId); // ✅ DO — use single-column index + manual filter for (const row of ctx.db.scores.by_player.filter(playerId)) { - if (row.level === targetLevel) { /* ... */ } + if (row.level === targetLevel) { + /* ... */ + } } ``` @@ -374,6 +407,7 @@ for (const row of ctx.db.scores.by_player.filter(playerId)) { ## 4) Reducers ### Definition syntax (CRITICAL) + **Reducer name comes from the export — NOT from a string argument.** Use `reducer(params, fn)` or `reducer(fn)`. ```typescript @@ -384,10 +418,10 @@ import { t, SenderError } from 'spacetimedb/server'; export const reducer_name = spacetimedb.reducer({ param1: t.string(), param2: t.u64() }, (ctx, { param1, param2 }) => { // Validation if (!param1) throw new SenderError('param1 required'); - + // Access tables via ctx.db const row = ctx.db.myTable.primaryKey.find(param2); - + // Mutations ctx.db.myTable.insert({ ... }); ctx.db.myTable.primaryKey.update({ ...row, newField: value }); @@ -403,24 +437,31 @@ spacetimedb.reducer('reducer_name', { param1: t.string() }, (ctx, { param1 }) => ``` ### Update pattern (CRITICAL) + ```typescript // ✅ CORRECT — spread existing row, override specific fields const existing = ctx.db.task.id.find(taskId); -if (!existing) throw new SenderError('Task not found'); -ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp }); +if (!existing) throw new SenderError("Task not found"); +ctx.db.task.id.update({ + ...existing, + title: newTitle, + updatedAt: ctx.timestamp, +}); // ❌ WRONG — partial update nulls out other fields! ctx.db.task.id.update({ id: taskId, title: newTitle }); ``` ### Delete pattern + ```typescript // Delete by primary key VALUE (not row object) -ctx.db.task.id.delete(taskId); // taskId is the u64 value -ctx.db.player.identity.delete(ctx.sender); // delete by identity +ctx.db.task.id.delete(taskId); // taskId is the u64 value +ctx.db.player.identity.delete(ctx.sender); // delete by identity ``` ### Lifecycle hooks + ```typescript spacetimedb.clientConnected((ctx) => { // ctx.sender is the connecting identity @@ -433,16 +474,18 @@ spacetimedb.clientDisconnected((ctx) => { ``` ### Snake_case to camelCase conversion + - Server: `export const do_something = spacetimedb.reducer(...)` — name from export - Client: `conn.reducers.doSomething({ ... })` ### Object syntax required + ```typescript // ❌ WRONG - positional -conn.reducers.doSomething('value'); +conn.reducers.doSomething("value"); // ✅ RIGHT - object -conn.reducers.doSomething({ param: 'value' }); +conn.reducers.doSomething({ param: "value" }); ``` --- @@ -451,28 +494,34 @@ conn.reducers.doSomething({ param: 'value' }); ```typescript // 1. Define table first (scheduled: () => reducer — pass the exported reducer) -export const CleanupJob = table({ - name: 'cleanup_job', - scheduled: () => run_cleanup // reducer defined below -}, { - scheduledId: t.u64().primaryKey().autoInc(), - scheduledAt: t.scheduleAt(), - targetId: t.u64(), // Your custom data -}); +export const CleanupJob = table( + { + name: "cleanup_job", + scheduled: () => run_cleanup, // reducer defined below + }, + { + scheduledId: t.u64().primaryKey().autoInc(), + scheduledAt: t.scheduleAt(), + targetId: t.u64(), // Your custom data + }, +); // 2. Define scheduled reducer (receives full row as arg) -export const run_cleanup = spacetimedb.reducer({ arg: CleanupJob.rowType }, (ctx, { arg }) => { - // arg.scheduledId, arg.targetId available - // Row is auto-deleted after reducer completes -}); +export const run_cleanup = spacetimedb.reducer( + { arg: CleanupJob.rowType }, + (ctx, { arg }) => { + // arg.scheduledId, arg.targetId available + // Row is auto-deleted after reducer completes + }, +); // Schedule a job -import { ScheduleAt } from 'spacetimedb'; +import { ScheduleAt } from "spacetimedb"; const futureTime = ctx.timestamp.microsSinceUnixEpoch + 60_000_000n; // 60 seconds -ctx.db.cleanupJob.insert({ - scheduledId: 0n, +ctx.db.cleanupJob.insert({ + scheduledId: 0n, scheduledAt: ScheduleAt.time(futureTime), - targetId: someId + targetId: someId, }); // Cancel a job by deleting the row @@ -484,18 +533,21 @@ ctx.db.cleanupJob.scheduledId.delete(jobId); ## 6) Timestamps ### Server-side + ```typescript -import { Timestamp, ScheduleAt } from 'spacetimedb'; +import { Timestamp, ScheduleAt } from "spacetimedb"; // Current time ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp }); // Future time (add microseconds) -const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5 minutes +const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5 minutes ``` ### Client-side (CRITICAL) + **Timestamps are objects, not numbers:** + ```typescript // ❌ WRONG const date = new Date(row.createdAt); @@ -506,9 +558,10 @@ const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n)); ``` ### ScheduleAt on client + ```typescript // ScheduleAt is a tagged union -if (scheduleAt.tag === 'Time') { +if (scheduleAt.tag === "Time") { const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n)); } ``` @@ -519,32 +572,37 @@ if (scheduleAt.tag === 'Time') { **`public: true` exposes ALL rows to ALL clients.** -| Scenario | Pattern | -|----------|---------| -| Everyone sees all rows | `public: true` | +| Scenario | Pattern | +| ------------------------- | ------------------------------------- | +| Everyone sees all rows | `public: true` | | Users see only their data | Private table + filtered subscription | ### Subscription patterns (client-side) + ```typescript // Subscribe to ALL public tables (simplest) conn.subscriptionBuilder().subscribeToAll(); // Subscribe to specific tables with SQL -conn.subscriptionBuilder().subscribe([ - 'SELECT * FROM message', - 'SELECT * FROM room WHERE is_public = true', -]); +conn + .subscriptionBuilder() + .subscribe([ + "SELECT * FROM message", + "SELECT * FROM room WHERE is_public = true", + ]); // Handle subscription lifecycle -conn.subscriptionBuilder() - .onApplied(() => console.log('Initial data loaded')) - .onError((e) => console.error('Subscription failed:', e)) +conn + .subscriptionBuilder() + .onApplied(() => console.log("Initial data loaded")) + .onError((e) => console.error("Subscription failed:", e)) .subscribeToAll(); ``` ### Private table + view pattern (RECOMMENDED) **Views are the recommended approach** for controlling data visibility. They provide: + - Server-side filtering (reduces network traffic) - Real-time updates when underlying data changes - Full control over what data clients can access @@ -557,28 +615,29 @@ conn.subscriptionBuilder() ```typescript // Private table with index on ownerId export const PrivateData = table( - { name: 'private_data', - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] + { + name: "private_data", + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], }, { id: t.u64().primaryKey().autoInc(), ownerId: t.identity(), - secret: t.string() - } + secret: t.string(), + }, ); // ❌ BAD — .iter() causes performance issues (re-evaluates on ANY row change) spacetimedb.view( - { name: 'my_data_slow', public: true }, + { name: "my_data_slow", public: true }, t.array(PrivateData.rowType), - (ctx) => [...ctx.db.privateData.iter()] // Works but VERY slow at scale + (ctx) => [...ctx.db.privateData.iter()], // Works but VERY slow at scale ); // ✅ GOOD — index lookup enables targeted invalidation spacetimedb.view( - { name: 'my_data', public: true }, + { name: "my_data", public: true }, t.array(PrivateData.rowType), - (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)] + (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)], ); ``` @@ -588,32 +647,40 @@ spacetimedb.view( // Query-builder views return a query; the SQL engine maintains the result incrementally. // This can scan the whole table if needed (e.g. leaderboard-style queries). spacetimedb.anonymousView( - { name: 'top_players', public: true }, + { name: "top_players", public: true }, t.array(Player.rowType), - (ctx) => - ctx.from.player - .where(p => p.score.gt(1000)) + (ctx) => ctx.from.player.where((p) => p.score.gt(1000)), ); ``` ### ViewContext vs AnonymousViewContext + ```typescript // ViewContext — has ctx.sender, result varies per user (computed per-subscriber) -spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => { - return [...ctx.db.item.by_owner.filter(ctx.sender)]; -}); +spacetimedb.view( + { name: "my_items", public: true }, + t.array(Item.rowType), + (ctx) => { + return [...ctx.db.item.by_owner.filter(ctx.sender)]; + }, +); // AnonymousViewContext — no ctx.sender, same result for everyone (shared, better perf) -spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(LeaderboardRow), (ctx) => { - return [...ctx.db.player.by_score.filter(/* top scores */)]; -}); +spacetimedb.anonymousView( + { name: "leaderboard", public: true }, + t.array(LeaderboardRow), + (ctx) => { + return [...ctx.db.player.by_score.filter(/* top scores */)]; + }, +); ``` **Views require explicit subscription:** + ```typescript conn.subscriptionBuilder().subscribe([ - 'SELECT * FROM public_table', - 'SELECT * FROM my_data', // Views need explicit SQL! + "SELECT * FROM public_table", + "SELECT * FROM my_data", // Views need explicit SQL! ]); ``` @@ -622,16 +689,18 @@ conn.subscriptionBuilder().subscribe([ ## 8) React Integration ### Key patterns + ```typescript // Memoize connectionBuilder to prevent reconnects on re-render -const builder = useMemo(() => - DbConnection.builder() - .withUri(SPACETIMEDB_URI) - .withDatabaseName(MODULE_NAME) - .withToken(localStorage.getItem('auth_token') || undefined) - .onConnect(onConnect) - .onConnectError(onConnectError), - [] // Empty deps - only create once +const builder = useMemo( + () => + DbConnection.builder() + .withUri(SPACETIMEDB_URI) + .withDatabaseName(MODULE_NAME) + .withToken(localStorage.getItem("auth_token") || undefined) + .onConnect(onConnect) + .onConnectError(onConnectError), + [], // Empty deps - only create once ); // useTable returns tuple [rows, isLoading] @@ -650,17 +719,18 @@ const isOwner = row.ownerId.toHexString() === myIdentity.toHexString(); ⚠️ Procedures are currently in beta. API may change. ### Defining a procedure + **Procedure name comes from the export — NOT from a string argument.** Use `procedure(params, ret, fn)` or `procedure(ret, fn)`. ```typescript // ✅ CORRECT — export const name = spacetimedb.procedure(params, ret, fn) export const fetch_external_data = spacetimedb.procedure( { url: t.string() }, - t.string(), // return type + t.string(), // return type (ctx, { url }) => { const response = ctx.http.fetch(url); return response.text(); - } + }, ); ``` @@ -692,18 +762,20 @@ spacetimedb.procedure({ url: t.string() }, t.unit(), (ctx, { url }) => { ``` ### Key differences from reducers -| Reducers | Procedures | -|----------|------------| + +| Reducers | Procedures | +| --------------------------- | ------------------------------------- | | `ctx.db` available directly | Must use `ctx.withTx(tx => tx.db...)` | -| Automatic transaction | Manual transaction management | -| No HTTP/network | `ctx.http.fetch()` available | -| No return values to caller | Can return data to caller | +| Automatic transaction | Manual transaction management | +| No HTTP/network | `ctx.http.fetch()` available | +| No return values to caller | Can return data to caller | --- ## 10) Project Structure ### Server (`backend/spacetimedb/`) + ``` src/schema.ts → Tables, export spacetimedb src/index.ts → Reducers, lifecycle, import schema @@ -712,12 +784,14 @@ tsconfig.json → Standard config ``` ### Avoiding circular imports + ``` schema.ts → defines tables AND exports spacetimedb index.ts → imports spacetimedb from ./schema, defines reducers ``` ### Client (`client/`) + ``` src/module_bindings/ → Generated (spacetime generate) src/main.tsx → Provider, connection setup diff --git a/AGENTS.md b/AGENTS.md index c58f00b..948d86b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,12 +8,12 @@ ## Language-Specific Rules -| Language | Rule File | -|----------|-----------| -| **TypeScript/React** | `spacetimedb-typescript.mdc` (MANDATORY) | -| **Rust** | `spacetimedb-rust.mdc` (MANDATORY) | -| **C#** | `spacetimedb-csharp.mdc` (MANDATORY) | -| **Migrating 1.0 → 2.0** | `spacetimedb-migration-2.0.mdc` | +| Language | Rule File | +| ----------------------- | ---------------------------------------- | +| **TypeScript/React** | `spacetimedb-typescript.mdc` (MANDATORY) | +| **Rust** | `spacetimedb-rust.mdc` (MANDATORY) | +| **C#** | `spacetimedb-csharp.mdc` (MANDATORY) | +| **Migrating 1.0 → 2.0** | `spacetimedb-migration-2.0.mdc` | --- @@ -44,6 +44,7 @@ When implementing a feature that spans backend and client: ## Index System SpacetimeDB automatically creates indexes for: + - Primary key columns - Columns marked as unique @@ -52,6 +53,7 @@ You can add explicit indexes on non-unique columns for query performance. **Index names must be unique across your entire module (all tables).** If two tables have indexes with the same declared name → conflict error. **Schema ↔ Code coupling:** + - Your query code references indexes by name - If you add/remove/rename an index in the schema, update all code that uses it - Removing an index without updating queries causes runtime errors @@ -85,7 +87,7 @@ spacetime logs ## Deployment - Maincloud is the spacetimedb hosted cloud and the default location for module publishing -- The default server marked by *** in `spacetime server list` should be used when publishing +- The default server marked by \*\*\* in `spacetime server list` should be used when publishing - If the default server is maincloud you should publish to maincloud - Publishing to maincloud is free of charge - When publishing to maincloud the database dashboard will be at the url: https://spacetimedb.com/@/ @@ -110,7 +112,6 @@ spacetime logs - Do NOT invent new SpacetimeDB APIs — use only what exists in docs or this repo - Do NOT add restrictions the prompt didn't ask for — if "users can do X", implement X for all users - # SpacetimeDB TypeScript SDK ## ⛔ HALLUCINATED APIs — DO NOT USE @@ -139,11 +140,11 @@ tables.user.filter(u => u.name === 'alice'); // No .filter() on tables object! ```typescript // ✅ CORRECT IMPORTS -import { DbConnection, tables } from './module_bindings'; // Generated! -import { SpacetimeDBProvider, useTable, Identity } from 'spacetimedb/react'; +import { DbConnection, tables } from "./module_bindings"; // Generated! +import { SpacetimeDBProvider, useTable, Identity } from "spacetimedb/react"; // ✅ CORRECT REDUCER CALLS — object syntax, not positional! -conn.reducers.doSomething({ value: 'test' }); +conn.reducers.doSomething({ value: "test" }); conn.reducers.updateItem({ itemId: 1n, newValue: 42 }); // ✅ CORRECT DATA ACCESS — useTable returns [rows, isLoading] @@ -151,6 +152,7 @@ const [items, isLoading] = useTable(tables.item); ``` ### ⛔ DO NOT: + - **Invent hooks** like `useItems()`, `useData()` — use `useTable(tables.tableName)` - **Import from fake packages** — only `spacetimedb`, `spacetimedb/react`, `./module_bindings` @@ -160,40 +162,40 @@ const [items, isLoading] = useTable(tables.item); ### Server-side errors -| Wrong | Right | Error | -|-------|-------|-------| -| Missing `package.json` | Create `package.json` | "could not detect language" | -| Missing `tsconfig.json` | Create `tsconfig.json` | "TsconfigNotFound" | -| Entrypoint not at `src/index.ts` | Use `src/index.ts` | Module won't bundle | -| `indexes` in COLUMNS (2nd arg) | `indexes` in OPTIONS (1st arg) | "reading 'tag'" error | -| Index without `algorithm` | `algorithm: 'btree'` | "reading 'tag'" error | -| `filter({ ownerId })` | `filter(ownerId)` | "does not exist in type 'Range'" | -| `.filter()` on unique column | `.find()` on unique column | TypeError | -| `insert({ ...without id })` | `insert({ id: 0n, ... })` | "Property 'id' is missing" | -| `const id = table.insert(...)` | `const row = table.insert(...)` | `.insert()` returns ROW, not ID | -| `.unique()` + explicit index | Just use `.unique()` | "name is used for multiple entities" | -| Index on `.primaryKey()` column | Don't — already indexed | "name is used for multiple entities" | -| Same index name in multiple tables | Prefix with table name | "name is used for multiple entities" | -| `.indexName.filter()` after removing index | Use `.iter()` + manual filter | "Cannot read properties of undefined" | -| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" | -| Multi-column index `.filter()` | **⚠️ BROKEN** — use single-column | PANIC or silent empty results | -| `JSON.stringify({ id: row.id })` | Convert BigInt first: `{ id: row.id.toString() }` | "Do not know how to serialize a BigInt" | -| `ScheduleAt.Time(timestamp)` | `ScheduleAt.time(timestamp)` (lowercase) | "ScheduleAt.Time is not a function" | -| `ctx.db.foo.myIndexName.filter()` | Use exact name: `ctx.db.foo.my_index_name.filter()` | "Cannot read properties of undefined" | -| `.iter()` in views | Use index lookups | Severe performance issues (re-evaluates on any change) | -| `ctx.db` in procedures | `ctx.withTx(tx => tx.db...)` | Procedures need explicit transactions | -| `ctx.myTable` in procedure tx | `tx.db.myTable` | Wrong context variable | +| Wrong | Right | Error | +| ------------------------------------------ | --------------------------------------------------- | ------------------------------------------------------ | +| Missing `package.json` | Create `package.json` | "could not detect language" | +| Missing `tsconfig.json` | Create `tsconfig.json` | "TsconfigNotFound" | +| Entrypoint not at `src/index.ts` | Use `src/index.ts` | Module won't bundle | +| `indexes` in COLUMNS (2nd arg) | `indexes` in OPTIONS (1st arg) | "reading 'tag'" error | +| Index without `algorithm` | `algorithm: 'btree'` | "reading 'tag'" error | +| `filter({ ownerId })` | `filter(ownerId)` | "does not exist in type 'Range'" | +| `.filter()` on unique column | `.find()` on unique column | TypeError | +| `insert({ ...without id })` | `insert({ id: 0n, ... })` | "Property 'id' is missing" | +| `const id = table.insert(...)` | `const row = table.insert(...)` | `.insert()` returns ROW, not ID | +| `.unique()` + explicit index | Just use `.unique()` | "name is used for multiple entities" | +| Index on `.primaryKey()` column | Don't — already indexed | "name is used for multiple entities" | +| Same index name in multiple tables | Prefix with table name | "name is used for multiple entities" | +| `.indexName.filter()` after removing index | Use `.iter()` + manual filter | "Cannot read properties of undefined" | +| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" | +| Multi-column index `.filter()` | **⚠️ BROKEN** — use single-column | PANIC or silent empty results | +| `JSON.stringify({ id: row.id })` | Convert BigInt first: `{ id: row.id.toString() }` | "Do not know how to serialize a BigInt" | +| `ScheduleAt.Time(timestamp)` | `ScheduleAt.time(timestamp)` (lowercase) | "ScheduleAt.Time is not a function" | +| `ctx.db.foo.myIndexName.filter()` | Use exact name: `ctx.db.foo.my_index_name.filter()` | "Cannot read properties of undefined" | +| `.iter()` in views | Use index lookups | Severe performance issues (re-evaluates on any change) | +| `ctx.db` in procedures | `ctx.withTx(tx => tx.db...)` | Procedures need explicit transactions | +| `ctx.myTable` in procedure tx | `tx.db.myTable` | Wrong context variable | ### Client-side errors -| Wrong | Right | Error | -|-------|-------|-------| -| `@spacetimedb/sdk` | `spacetimedb` | 404 / missing subpath | -| `conn.reducers.foo("val")` | `conn.reducers.foo({ param: "val" })` | Wrong reducer syntax | -| Inline `connectionBuilder` | `useMemo(() => ..., [])` | Reconnects every render | -| `const rows = useTable(table)` | `const [rows, isLoading] = useTable(table)` | Tuple destructuring | -| Optimistic UI updates | Let subscriptions drive state | Desync issues | -| `` | `connectionBuilder={...}` | Wrong prop name | +| Wrong | Right | Error | +| ------------------------------------- | ------------------------------------------- | ----------------------- | +| `@spacetimedb/sdk` | `spacetimedb` | 404 / missing subpath | +| `conn.reducers.foo("val")` | `conn.reducers.foo({ param: "val" })` | Wrong reducer syntax | +| Inline `connectionBuilder` | `useMemo(() => ..., [])` | Reconnects every render | +| `const rows = useTable(table)` | `const [rows, isLoading] = useTable(table)` | Tuple destructuring | +| Optimistic UI updates | Let subscriptions drive state | Desync issues | +| `` | `connectionBuilder={...}` | Wrong prop name | --- @@ -202,62 +204,77 @@ const [items, isLoading] = useTable(tables.item); **`table()` takes TWO arguments: `table(OPTIONS, COLUMNS)`** ```typescript -import { schema, table, t } from 'spacetimedb/server'; +import { schema, table, t } from "spacetimedb/server"; // ❌ WRONG — indexes in COLUMNS causes "reading 'tag'" error -export const Task = table({ name: 'task' }, { - id: t.u64().primaryKey().autoInc(), - ownerId: t.identity(), - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] // ❌ WRONG! -}); +export const Task = table( + { name: "task" }, + { + id: t.u64().primaryKey().autoInc(), + ownerId: t.identity(), + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], // ❌ WRONG! + }, +); // ✅ RIGHT — indexes in OPTIONS (first argument) -export const Task = table({ - name: 'task', - public: true, - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] -}, { - id: t.u64().primaryKey().autoInc(), - ownerId: t.identity(), - title: t.string(), - createdAt: t.timestamp(), -}); +export const Task = table( + { + name: "task", + public: true, + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], + }, + { + id: t.u64().primaryKey().autoInc(), + ownerId: t.identity(), + title: t.string(), + createdAt: t.timestamp(), + }, +); ``` ### Column types + ```typescript -t.identity() // User identity (primary key for per-user tables) -t.u64() // Unsigned 64-bit integer (use for IDs) -t.string() // Text -t.bool() // Boolean -t.timestamp() // Timestamp (use ctx.timestamp for current time) -t.scheduleAt() // For scheduled tables only +t.identity(); // User identity (primary key for per-user tables) +t.u64(); // Unsigned 64-bit integer (use for IDs) +t.string(); // Text +t.bool(); // Boolean +t.timestamp(); // Timestamp (use ctx.timestamp for current time) +t.scheduleAt(); // For scheduled tables only // Product types (nested objects) — use t.object, NOT t.struct -const Point = t.object('Point', { x: t.i32(), y: t.i32() }); +const Point = t.object("Point", { x: t.i32(), y: t.i32() }); // Sum types (tagged unions) — use t.enum, NOT t.sum -const Shape = t.enum('Shape', { circle: t.i32(), rectangle: Point }); +const Shape = t.enum("Shape", { circle: t.i32(), rectangle: Point }); // Values use { tag: 'circle', value: 10 } or { tag: 'rectangle', value: { x: 1, y: 2 } } // Modifiers -t.string().optional() // Nullable -t.u64().primaryKey() // Primary key -t.u64().primaryKey().autoInc() // Auto-increment primary key +t.string().optional(); // Nullable +t.u64().primaryKey(); // Primary key +t.u64().primaryKey().autoInc(); // Auto-increment primary key ``` > ⚠️ **BIGINT SYNTAX:** All `u64`, `i64`, and ID fields use JavaScript BigInt. +> > - Literals: `0n`, `1n`, `100n` (NOT `0`, `1`, `100`) > - Comparisons: `row.id === 5n` (NOT `row.id === 5`) > - Arithmetic: `row.count + 1n` (NOT `row.count + 1`) ### Auto-increment placeholder + ```typescript // ✅ MUST provide 0n placeholder for auto-inc fields -ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title: 'New', createdAt: ctx.timestamp }); +ctx.db.task.insert({ + id: 0n, + ownerId: ctx.sender, + title: "New", + createdAt: ctx.timestamp, +}); ``` ### Insert returns ROW, not ID + ```typescript // ❌ WRONG const id = ctx.db.task.insert({ ... }); @@ -268,14 +285,15 @@ const newId = row.id; // Extract .id from returned row ``` ### Schema export (CRITICAL) + ```typescript // At end of schema.ts — schema() takes exactly ONE argument: an object const spacetimedb = schema({ table1, table2, table3 }); export default spacetimedb; // ❌ WRONG — never pass tables directly or as multiple args -schema(myTable); // WRONG! -schema(t1, t2, t3); // WRONG! +schema(myTable); // WRONG! +schema(t1, t2, t3); // WRONG! ``` --- @@ -294,7 +312,9 @@ const msgs = [...ctx.db.message.message_room_id.filter(roomId)]; // 3. NO INDEX — use .iter() + manual filter for (const m of ctx.db.roomMember.iter()) { - if (m.roomId === roomId) { /* ... */ } + if (m.roomId === roomId) { + /* ... */ + } } ``` @@ -302,24 +322,31 @@ for (const m of ctx.db.roomMember.iter()) { ```typescript // In table OPTIONS (first argument), not columns -export const Message = table({ - name: 'message', - public: true, - indexes: [{ name: 'message_room_id', algorithm: 'btree', columns: ['roomId'] }] -}, { - id: t.u64().primaryKey().autoInc(), - roomId: t.u64(), - // ... -}); +export const Message = table( + { + name: "message", + public: true, + indexes: [ + { name: "message_room_id", algorithm: "btree", columns: ["roomId"] }, + ], + }, + { + id: t.u64().primaryKey().autoInc(), + roomId: t.u64(), + // ... + }, +); ``` ### Naming conventions **Table names — automatic transformation:** -- Schema: `table({ name: 'my_messages' })` + +- Schema: `table({ name: 'my_messages' })` - Access: `ctx.db.myMessages` (automatic snake_case → camelCase) **Index names — NO transformation, use EXACTLY as defined:** + ```typescript // Schema definition indexes: [{ name: 'canvas_member_canvas_id', algorithm: 'btree', columns: ['canvasId'] }] @@ -335,6 +362,7 @@ ctx.db.canvasMember.canvas_member_canvas_id.filter(...) > ⚠️ **Index names are used VERBATIM** — pick a convention (snake_case or camelCase) and stick with it. **Index naming pattern — use `{tableName}_{columnName}`:** + ```typescript // ✅ GOOD — unique names across entire module indexes: [{ name: 'message_room_id', algorithm: 'btree', columns: ['roomId'] }] @@ -346,10 +374,12 @@ indexes: [{ name: 'by_owner', ... }] // in Note table — CONFLICT! ``` **Client-side table names:** + - Check generated `module_bindings/index.ts` for exact export names - Usage: `useTable(tables.MyMessages)` or `tables.myMessages` (varies by SDK version) ### Filter vs Find + ```typescript // Filter takes VALUE directly, not object — returns iterator const rows = [...ctx.db.task.by_owner.filter(ownerId)]; @@ -359,13 +389,16 @@ const row = ctx.db.player.identity.find(ctx.sender); ``` ### ⚠️ Multi-column indexes are BROKEN + ```typescript // ❌ DON'T — causes PANIC ctx.db.scores.by_player_level.filter(playerId); // ✅ DO — use single-column index + manual filter for (const row of ctx.db.scores.by_player.filter(playerId)) { - if (row.level === targetLevel) { /* ... */ } + if (row.level === targetLevel) { + /* ... */ + } } ``` @@ -374,6 +407,7 @@ for (const row of ctx.db.scores.by_player.filter(playerId)) { ## 4) Reducers ### Definition syntax (CRITICAL) + **Reducer name comes from the export — NOT from a string argument.** Use `reducer(params, fn)` or `reducer(fn)`. ```typescript @@ -384,10 +418,10 @@ import { t, SenderError } from 'spacetimedb/server'; export const reducer_name = spacetimedb.reducer({ param1: t.string(), param2: t.u64() }, (ctx, { param1, param2 }) => { // Validation if (!param1) throw new SenderError('param1 required'); - + // Access tables via ctx.db const row = ctx.db.myTable.primaryKey.find(param2); - + // Mutations ctx.db.myTable.insert({ ... }); ctx.db.myTable.primaryKey.update({ ...row, newField: value }); @@ -403,24 +437,31 @@ spacetimedb.reducer('reducer_name', { param1: t.string() }, (ctx, { param1 }) => ``` ### Update pattern (CRITICAL) + ```typescript // ✅ CORRECT — spread existing row, override specific fields const existing = ctx.db.task.id.find(taskId); -if (!existing) throw new SenderError('Task not found'); -ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp }); +if (!existing) throw new SenderError("Task not found"); +ctx.db.task.id.update({ + ...existing, + title: newTitle, + updatedAt: ctx.timestamp, +}); // ❌ WRONG — partial update nulls out other fields! ctx.db.task.id.update({ id: taskId, title: newTitle }); ``` ### Delete pattern + ```typescript // Delete by primary key VALUE (not row object) -ctx.db.task.id.delete(taskId); // taskId is the u64 value -ctx.db.player.identity.delete(ctx.sender); // delete by identity +ctx.db.task.id.delete(taskId); // taskId is the u64 value +ctx.db.player.identity.delete(ctx.sender); // delete by identity ``` ### Lifecycle hooks + ```typescript spacetimedb.clientConnected((ctx) => { // ctx.sender is the connecting identity @@ -433,16 +474,18 @@ spacetimedb.clientDisconnected((ctx) => { ``` ### Snake_case to camelCase conversion + - Server: `export const do_something = spacetimedb.reducer(...)` — name from export - Client: `conn.reducers.doSomething({ ... })` ### Object syntax required + ```typescript // ❌ WRONG - positional -conn.reducers.doSomething('value'); +conn.reducers.doSomething("value"); // ✅ RIGHT - object -conn.reducers.doSomething({ param: 'value' }); +conn.reducers.doSomething({ param: "value" }); ``` --- @@ -451,28 +494,34 @@ conn.reducers.doSomething({ param: 'value' }); ```typescript // 1. Define table first (scheduled: () => reducer — pass the exported reducer) -export const CleanupJob = table({ - name: 'cleanup_job', - scheduled: () => run_cleanup // reducer defined below -}, { - scheduledId: t.u64().primaryKey().autoInc(), - scheduledAt: t.scheduleAt(), - targetId: t.u64(), // Your custom data -}); +export const CleanupJob = table( + { + name: "cleanup_job", + scheduled: () => run_cleanup, // reducer defined below + }, + { + scheduledId: t.u64().primaryKey().autoInc(), + scheduledAt: t.scheduleAt(), + targetId: t.u64(), // Your custom data + }, +); // 2. Define scheduled reducer (receives full row as arg) -export const run_cleanup = spacetimedb.reducer({ arg: CleanupJob.rowType }, (ctx, { arg }) => { - // arg.scheduledId, arg.targetId available - // Row is auto-deleted after reducer completes -}); +export const run_cleanup = spacetimedb.reducer( + { arg: CleanupJob.rowType }, + (ctx, { arg }) => { + // arg.scheduledId, arg.targetId available + // Row is auto-deleted after reducer completes + }, +); // Schedule a job -import { ScheduleAt } from 'spacetimedb'; +import { ScheduleAt } from "spacetimedb"; const futureTime = ctx.timestamp.microsSinceUnixEpoch + 60_000_000n; // 60 seconds -ctx.db.cleanupJob.insert({ - scheduledId: 0n, +ctx.db.cleanupJob.insert({ + scheduledId: 0n, scheduledAt: ScheduleAt.time(futureTime), - targetId: someId + targetId: someId, }); // Cancel a job by deleting the row @@ -484,18 +533,21 @@ ctx.db.cleanupJob.scheduledId.delete(jobId); ## 6) Timestamps ### Server-side + ```typescript -import { Timestamp, ScheduleAt } from 'spacetimedb'; +import { Timestamp, ScheduleAt } from "spacetimedb"; // Current time ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp }); // Future time (add microseconds) -const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5 minutes +const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5 minutes ``` ### Client-side (CRITICAL) + **Timestamps are objects, not numbers:** + ```typescript // ❌ WRONG const date = new Date(row.createdAt); @@ -506,9 +558,10 @@ const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n)); ``` ### ScheduleAt on client + ```typescript // ScheduleAt is a tagged union -if (scheduleAt.tag === 'Time') { +if (scheduleAt.tag === "Time") { const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n)); } ``` @@ -519,32 +572,37 @@ if (scheduleAt.tag === 'Time') { **`public: true` exposes ALL rows to ALL clients.** -| Scenario | Pattern | -|----------|---------| -| Everyone sees all rows | `public: true` | +| Scenario | Pattern | +| ------------------------- | ------------------------------------- | +| Everyone sees all rows | `public: true` | | Users see only their data | Private table + filtered subscription | ### Subscription patterns (client-side) + ```typescript // Subscribe to ALL public tables (simplest) conn.subscriptionBuilder().subscribeToAll(); // Subscribe to specific tables with SQL -conn.subscriptionBuilder().subscribe([ - 'SELECT * FROM message', - 'SELECT * FROM room WHERE is_public = true', -]); +conn + .subscriptionBuilder() + .subscribe([ + "SELECT * FROM message", + "SELECT * FROM room WHERE is_public = true", + ]); // Handle subscription lifecycle -conn.subscriptionBuilder() - .onApplied(() => console.log('Initial data loaded')) - .onError((e) => console.error('Subscription failed:', e)) +conn + .subscriptionBuilder() + .onApplied(() => console.log("Initial data loaded")) + .onError((e) => console.error("Subscription failed:", e)) .subscribeToAll(); ``` ### Private table + view pattern (RECOMMENDED) **Views are the recommended approach** for controlling data visibility. They provide: + - Server-side filtering (reduces network traffic) - Real-time updates when underlying data changes - Full control over what data clients can access @@ -557,28 +615,29 @@ conn.subscriptionBuilder() ```typescript // Private table with index on ownerId export const PrivateData = table( - { name: 'private_data', - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] + { + name: "private_data", + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], }, { id: t.u64().primaryKey().autoInc(), ownerId: t.identity(), - secret: t.string() - } + secret: t.string(), + }, ); // ❌ BAD — .iter() causes performance issues (re-evaluates on ANY row change) spacetimedb.view( - { name: 'my_data_slow', public: true }, + { name: "my_data_slow", public: true }, t.array(PrivateData.rowType), - (ctx) => [...ctx.db.privateData.iter()] // Works but VERY slow at scale + (ctx) => [...ctx.db.privateData.iter()], // Works but VERY slow at scale ); // ✅ GOOD — index lookup enables targeted invalidation spacetimedb.view( - { name: 'my_data', public: true }, + { name: "my_data", public: true }, t.array(PrivateData.rowType), - (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)] + (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)], ); ``` @@ -588,32 +647,40 @@ spacetimedb.view( // Query-builder views return a query; the SQL engine maintains the result incrementally. // This can scan the whole table if needed (e.g. leaderboard-style queries). spacetimedb.anonymousView( - { name: 'top_players', public: true }, + { name: "top_players", public: true }, t.array(Player.rowType), - (ctx) => - ctx.from.player - .where(p => p.score.gt(1000)) + (ctx) => ctx.from.player.where((p) => p.score.gt(1000)), ); ``` ### ViewContext vs AnonymousViewContext + ```typescript // ViewContext — has ctx.sender, result varies per user (computed per-subscriber) -spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => { - return [...ctx.db.item.by_owner.filter(ctx.sender)]; -}); +spacetimedb.view( + { name: "my_items", public: true }, + t.array(Item.rowType), + (ctx) => { + return [...ctx.db.item.by_owner.filter(ctx.sender)]; + }, +); // AnonymousViewContext — no ctx.sender, same result for everyone (shared, better perf) -spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(LeaderboardRow), (ctx) => { - return [...ctx.db.player.by_score.filter(/* top scores */)]; -}); +spacetimedb.anonymousView( + { name: "leaderboard", public: true }, + t.array(LeaderboardRow), + (ctx) => { + return [...ctx.db.player.by_score.filter(/* top scores */)]; + }, +); ``` **Views require explicit subscription:** + ```typescript conn.subscriptionBuilder().subscribe([ - 'SELECT * FROM public_table', - 'SELECT * FROM my_data', // Views need explicit SQL! + "SELECT * FROM public_table", + "SELECT * FROM my_data", // Views need explicit SQL! ]); ``` @@ -622,16 +689,18 @@ conn.subscriptionBuilder().subscribe([ ## 8) React Integration ### Key patterns + ```typescript // Memoize connectionBuilder to prevent reconnects on re-render -const builder = useMemo(() => - DbConnection.builder() - .withUri(SPACETIMEDB_URI) - .withDatabaseName(MODULE_NAME) - .withToken(localStorage.getItem('auth_token') || undefined) - .onConnect(onConnect) - .onConnectError(onConnectError), - [] // Empty deps - only create once +const builder = useMemo( + () => + DbConnection.builder() + .withUri(SPACETIMEDB_URI) + .withDatabaseName(MODULE_NAME) + .withToken(localStorage.getItem("auth_token") || undefined) + .onConnect(onConnect) + .onConnectError(onConnectError), + [], // Empty deps - only create once ); // useTable returns tuple [rows, isLoading] @@ -650,17 +719,18 @@ const isOwner = row.ownerId.toHexString() === myIdentity.toHexString(); ⚠️ Procedures are currently in beta. API may change. ### Defining a procedure + **Procedure name comes from the export — NOT from a string argument.** Use `procedure(params, ret, fn)` or `procedure(ret, fn)`. ```typescript // ✅ CORRECT — export const name = spacetimedb.procedure(params, ret, fn) export const fetch_external_data = spacetimedb.procedure( { url: t.string() }, - t.string(), // return type + t.string(), // return type (ctx, { url }) => { const response = ctx.http.fetch(url); return response.text(); - } + }, ); ``` @@ -692,18 +762,20 @@ spacetimedb.procedure({ url: t.string() }, t.unit(), (ctx, { url }) => { ``` ### Key differences from reducers -| Reducers | Procedures | -|----------|------------| + +| Reducers | Procedures | +| --------------------------- | ------------------------------------- | | `ctx.db` available directly | Must use `ctx.withTx(tx => tx.db...)` | -| Automatic transaction | Manual transaction management | -| No HTTP/network | `ctx.http.fetch()` available | -| No return values to caller | Can return data to caller | +| Automatic transaction | Manual transaction management | +| No HTTP/network | `ctx.http.fetch()` available | +| No return values to caller | Can return data to caller | --- ## 10) Project Structure ### Server (`backend/spacetimedb/`) + ``` src/schema.ts → Tables, export spacetimedb src/index.ts → Reducers, lifecycle, import schema @@ -712,12 +784,14 @@ tsconfig.json → Standard config ``` ### Avoiding circular imports + ``` schema.ts → defines tables AND exports spacetimedb index.ts → imports spacetimedb from ./schema, defines reducers ``` ### Client (`client/`) + ``` src/module_bindings/ → Generated (spacetime generate) src/main.tsx → Provider, connection setup diff --git a/CLAUDE.md b/CLAUDE.md index c58f00b..948d86b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,12 +8,12 @@ ## Language-Specific Rules -| Language | Rule File | -|----------|-----------| -| **TypeScript/React** | `spacetimedb-typescript.mdc` (MANDATORY) | -| **Rust** | `spacetimedb-rust.mdc` (MANDATORY) | -| **C#** | `spacetimedb-csharp.mdc` (MANDATORY) | -| **Migrating 1.0 → 2.0** | `spacetimedb-migration-2.0.mdc` | +| Language | Rule File | +| ----------------------- | ---------------------------------------- | +| **TypeScript/React** | `spacetimedb-typescript.mdc` (MANDATORY) | +| **Rust** | `spacetimedb-rust.mdc` (MANDATORY) | +| **C#** | `spacetimedb-csharp.mdc` (MANDATORY) | +| **Migrating 1.0 → 2.0** | `spacetimedb-migration-2.0.mdc` | --- @@ -44,6 +44,7 @@ When implementing a feature that spans backend and client: ## Index System SpacetimeDB automatically creates indexes for: + - Primary key columns - Columns marked as unique @@ -52,6 +53,7 @@ You can add explicit indexes on non-unique columns for query performance. **Index names must be unique across your entire module (all tables).** If two tables have indexes with the same declared name → conflict error. **Schema ↔ Code coupling:** + - Your query code references indexes by name - If you add/remove/rename an index in the schema, update all code that uses it - Removing an index without updating queries causes runtime errors @@ -85,7 +87,7 @@ spacetime logs ## Deployment - Maincloud is the spacetimedb hosted cloud and the default location for module publishing -- The default server marked by *** in `spacetime server list` should be used when publishing +- The default server marked by \*\*\* in `spacetime server list` should be used when publishing - If the default server is maincloud you should publish to maincloud - Publishing to maincloud is free of charge - When publishing to maincloud the database dashboard will be at the url: https://spacetimedb.com/@/ @@ -110,7 +112,6 @@ spacetime logs - Do NOT invent new SpacetimeDB APIs — use only what exists in docs or this repo - Do NOT add restrictions the prompt didn't ask for — if "users can do X", implement X for all users - # SpacetimeDB TypeScript SDK ## ⛔ HALLUCINATED APIs — DO NOT USE @@ -139,11 +140,11 @@ tables.user.filter(u => u.name === 'alice'); // No .filter() on tables object! ```typescript // ✅ CORRECT IMPORTS -import { DbConnection, tables } from './module_bindings'; // Generated! -import { SpacetimeDBProvider, useTable, Identity } from 'spacetimedb/react'; +import { DbConnection, tables } from "./module_bindings"; // Generated! +import { SpacetimeDBProvider, useTable, Identity } from "spacetimedb/react"; // ✅ CORRECT REDUCER CALLS — object syntax, not positional! -conn.reducers.doSomething({ value: 'test' }); +conn.reducers.doSomething({ value: "test" }); conn.reducers.updateItem({ itemId: 1n, newValue: 42 }); // ✅ CORRECT DATA ACCESS — useTable returns [rows, isLoading] @@ -151,6 +152,7 @@ const [items, isLoading] = useTable(tables.item); ``` ### ⛔ DO NOT: + - **Invent hooks** like `useItems()`, `useData()` — use `useTable(tables.tableName)` - **Import from fake packages** — only `spacetimedb`, `spacetimedb/react`, `./module_bindings` @@ -160,40 +162,40 @@ const [items, isLoading] = useTable(tables.item); ### Server-side errors -| Wrong | Right | Error | -|-------|-------|-------| -| Missing `package.json` | Create `package.json` | "could not detect language" | -| Missing `tsconfig.json` | Create `tsconfig.json` | "TsconfigNotFound" | -| Entrypoint not at `src/index.ts` | Use `src/index.ts` | Module won't bundle | -| `indexes` in COLUMNS (2nd arg) | `indexes` in OPTIONS (1st arg) | "reading 'tag'" error | -| Index without `algorithm` | `algorithm: 'btree'` | "reading 'tag'" error | -| `filter({ ownerId })` | `filter(ownerId)` | "does not exist in type 'Range'" | -| `.filter()` on unique column | `.find()` on unique column | TypeError | -| `insert({ ...without id })` | `insert({ id: 0n, ... })` | "Property 'id' is missing" | -| `const id = table.insert(...)` | `const row = table.insert(...)` | `.insert()` returns ROW, not ID | -| `.unique()` + explicit index | Just use `.unique()` | "name is used for multiple entities" | -| Index on `.primaryKey()` column | Don't — already indexed | "name is used for multiple entities" | -| Same index name in multiple tables | Prefix with table name | "name is used for multiple entities" | -| `.indexName.filter()` after removing index | Use `.iter()` + manual filter | "Cannot read properties of undefined" | -| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" | -| Multi-column index `.filter()` | **⚠️ BROKEN** — use single-column | PANIC or silent empty results | -| `JSON.stringify({ id: row.id })` | Convert BigInt first: `{ id: row.id.toString() }` | "Do not know how to serialize a BigInt" | -| `ScheduleAt.Time(timestamp)` | `ScheduleAt.time(timestamp)` (lowercase) | "ScheduleAt.Time is not a function" | -| `ctx.db.foo.myIndexName.filter()` | Use exact name: `ctx.db.foo.my_index_name.filter()` | "Cannot read properties of undefined" | -| `.iter()` in views | Use index lookups | Severe performance issues (re-evaluates on any change) | -| `ctx.db` in procedures | `ctx.withTx(tx => tx.db...)` | Procedures need explicit transactions | -| `ctx.myTable` in procedure tx | `tx.db.myTable` | Wrong context variable | +| Wrong | Right | Error | +| ------------------------------------------ | --------------------------------------------------- | ------------------------------------------------------ | +| Missing `package.json` | Create `package.json` | "could not detect language" | +| Missing `tsconfig.json` | Create `tsconfig.json` | "TsconfigNotFound" | +| Entrypoint not at `src/index.ts` | Use `src/index.ts` | Module won't bundle | +| `indexes` in COLUMNS (2nd arg) | `indexes` in OPTIONS (1st arg) | "reading 'tag'" error | +| Index without `algorithm` | `algorithm: 'btree'` | "reading 'tag'" error | +| `filter({ ownerId })` | `filter(ownerId)` | "does not exist in type 'Range'" | +| `.filter()` on unique column | `.find()` on unique column | TypeError | +| `insert({ ...without id })` | `insert({ id: 0n, ... })` | "Property 'id' is missing" | +| `const id = table.insert(...)` | `const row = table.insert(...)` | `.insert()` returns ROW, not ID | +| `.unique()` + explicit index | Just use `.unique()` | "name is used for multiple entities" | +| Index on `.primaryKey()` column | Don't — already indexed | "name is used for multiple entities" | +| Same index name in multiple tables | Prefix with table name | "name is used for multiple entities" | +| `.indexName.filter()` after removing index | Use `.iter()` + manual filter | "Cannot read properties of undefined" | +| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" | +| Multi-column index `.filter()` | **⚠️ BROKEN** — use single-column | PANIC or silent empty results | +| `JSON.stringify({ id: row.id })` | Convert BigInt first: `{ id: row.id.toString() }` | "Do not know how to serialize a BigInt" | +| `ScheduleAt.Time(timestamp)` | `ScheduleAt.time(timestamp)` (lowercase) | "ScheduleAt.Time is not a function" | +| `ctx.db.foo.myIndexName.filter()` | Use exact name: `ctx.db.foo.my_index_name.filter()` | "Cannot read properties of undefined" | +| `.iter()` in views | Use index lookups | Severe performance issues (re-evaluates on any change) | +| `ctx.db` in procedures | `ctx.withTx(tx => tx.db...)` | Procedures need explicit transactions | +| `ctx.myTable` in procedure tx | `tx.db.myTable` | Wrong context variable | ### Client-side errors -| Wrong | Right | Error | -|-------|-------|-------| -| `@spacetimedb/sdk` | `spacetimedb` | 404 / missing subpath | -| `conn.reducers.foo("val")` | `conn.reducers.foo({ param: "val" })` | Wrong reducer syntax | -| Inline `connectionBuilder` | `useMemo(() => ..., [])` | Reconnects every render | -| `const rows = useTable(table)` | `const [rows, isLoading] = useTable(table)` | Tuple destructuring | -| Optimistic UI updates | Let subscriptions drive state | Desync issues | -| `` | `connectionBuilder={...}` | Wrong prop name | +| Wrong | Right | Error | +| ------------------------------------- | ------------------------------------------- | ----------------------- | +| `@spacetimedb/sdk` | `spacetimedb` | 404 / missing subpath | +| `conn.reducers.foo("val")` | `conn.reducers.foo({ param: "val" })` | Wrong reducer syntax | +| Inline `connectionBuilder` | `useMemo(() => ..., [])` | Reconnects every render | +| `const rows = useTable(table)` | `const [rows, isLoading] = useTable(table)` | Tuple destructuring | +| Optimistic UI updates | Let subscriptions drive state | Desync issues | +| `` | `connectionBuilder={...}` | Wrong prop name | --- @@ -202,62 +204,77 @@ const [items, isLoading] = useTable(tables.item); **`table()` takes TWO arguments: `table(OPTIONS, COLUMNS)`** ```typescript -import { schema, table, t } from 'spacetimedb/server'; +import { schema, table, t } from "spacetimedb/server"; // ❌ WRONG — indexes in COLUMNS causes "reading 'tag'" error -export const Task = table({ name: 'task' }, { - id: t.u64().primaryKey().autoInc(), - ownerId: t.identity(), - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] // ❌ WRONG! -}); +export const Task = table( + { name: "task" }, + { + id: t.u64().primaryKey().autoInc(), + ownerId: t.identity(), + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], // ❌ WRONG! + }, +); // ✅ RIGHT — indexes in OPTIONS (first argument) -export const Task = table({ - name: 'task', - public: true, - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] -}, { - id: t.u64().primaryKey().autoInc(), - ownerId: t.identity(), - title: t.string(), - createdAt: t.timestamp(), -}); +export const Task = table( + { + name: "task", + public: true, + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], + }, + { + id: t.u64().primaryKey().autoInc(), + ownerId: t.identity(), + title: t.string(), + createdAt: t.timestamp(), + }, +); ``` ### Column types + ```typescript -t.identity() // User identity (primary key for per-user tables) -t.u64() // Unsigned 64-bit integer (use for IDs) -t.string() // Text -t.bool() // Boolean -t.timestamp() // Timestamp (use ctx.timestamp for current time) -t.scheduleAt() // For scheduled tables only +t.identity(); // User identity (primary key for per-user tables) +t.u64(); // Unsigned 64-bit integer (use for IDs) +t.string(); // Text +t.bool(); // Boolean +t.timestamp(); // Timestamp (use ctx.timestamp for current time) +t.scheduleAt(); // For scheduled tables only // Product types (nested objects) — use t.object, NOT t.struct -const Point = t.object('Point', { x: t.i32(), y: t.i32() }); +const Point = t.object("Point", { x: t.i32(), y: t.i32() }); // Sum types (tagged unions) — use t.enum, NOT t.sum -const Shape = t.enum('Shape', { circle: t.i32(), rectangle: Point }); +const Shape = t.enum("Shape", { circle: t.i32(), rectangle: Point }); // Values use { tag: 'circle', value: 10 } or { tag: 'rectangle', value: { x: 1, y: 2 } } // Modifiers -t.string().optional() // Nullable -t.u64().primaryKey() // Primary key -t.u64().primaryKey().autoInc() // Auto-increment primary key +t.string().optional(); // Nullable +t.u64().primaryKey(); // Primary key +t.u64().primaryKey().autoInc(); // Auto-increment primary key ``` > ⚠️ **BIGINT SYNTAX:** All `u64`, `i64`, and ID fields use JavaScript BigInt. +> > - Literals: `0n`, `1n`, `100n` (NOT `0`, `1`, `100`) > - Comparisons: `row.id === 5n` (NOT `row.id === 5`) > - Arithmetic: `row.count + 1n` (NOT `row.count + 1`) ### Auto-increment placeholder + ```typescript // ✅ MUST provide 0n placeholder for auto-inc fields -ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title: 'New', createdAt: ctx.timestamp }); +ctx.db.task.insert({ + id: 0n, + ownerId: ctx.sender, + title: "New", + createdAt: ctx.timestamp, +}); ``` ### Insert returns ROW, not ID + ```typescript // ❌ WRONG const id = ctx.db.task.insert({ ... }); @@ -268,14 +285,15 @@ const newId = row.id; // Extract .id from returned row ``` ### Schema export (CRITICAL) + ```typescript // At end of schema.ts — schema() takes exactly ONE argument: an object const spacetimedb = schema({ table1, table2, table3 }); export default spacetimedb; // ❌ WRONG — never pass tables directly or as multiple args -schema(myTable); // WRONG! -schema(t1, t2, t3); // WRONG! +schema(myTable); // WRONG! +schema(t1, t2, t3); // WRONG! ``` --- @@ -294,7 +312,9 @@ const msgs = [...ctx.db.message.message_room_id.filter(roomId)]; // 3. NO INDEX — use .iter() + manual filter for (const m of ctx.db.roomMember.iter()) { - if (m.roomId === roomId) { /* ... */ } + if (m.roomId === roomId) { + /* ... */ + } } ``` @@ -302,24 +322,31 @@ for (const m of ctx.db.roomMember.iter()) { ```typescript // In table OPTIONS (first argument), not columns -export const Message = table({ - name: 'message', - public: true, - indexes: [{ name: 'message_room_id', algorithm: 'btree', columns: ['roomId'] }] -}, { - id: t.u64().primaryKey().autoInc(), - roomId: t.u64(), - // ... -}); +export const Message = table( + { + name: "message", + public: true, + indexes: [ + { name: "message_room_id", algorithm: "btree", columns: ["roomId"] }, + ], + }, + { + id: t.u64().primaryKey().autoInc(), + roomId: t.u64(), + // ... + }, +); ``` ### Naming conventions **Table names — automatic transformation:** -- Schema: `table({ name: 'my_messages' })` + +- Schema: `table({ name: 'my_messages' })` - Access: `ctx.db.myMessages` (automatic snake_case → camelCase) **Index names — NO transformation, use EXACTLY as defined:** + ```typescript // Schema definition indexes: [{ name: 'canvas_member_canvas_id', algorithm: 'btree', columns: ['canvasId'] }] @@ -335,6 +362,7 @@ ctx.db.canvasMember.canvas_member_canvas_id.filter(...) > ⚠️ **Index names are used VERBATIM** — pick a convention (snake_case or camelCase) and stick with it. **Index naming pattern — use `{tableName}_{columnName}`:** + ```typescript // ✅ GOOD — unique names across entire module indexes: [{ name: 'message_room_id', algorithm: 'btree', columns: ['roomId'] }] @@ -346,10 +374,12 @@ indexes: [{ name: 'by_owner', ... }] // in Note table — CONFLICT! ``` **Client-side table names:** + - Check generated `module_bindings/index.ts` for exact export names - Usage: `useTable(tables.MyMessages)` or `tables.myMessages` (varies by SDK version) ### Filter vs Find + ```typescript // Filter takes VALUE directly, not object — returns iterator const rows = [...ctx.db.task.by_owner.filter(ownerId)]; @@ -359,13 +389,16 @@ const row = ctx.db.player.identity.find(ctx.sender); ``` ### ⚠️ Multi-column indexes are BROKEN + ```typescript // ❌ DON'T — causes PANIC ctx.db.scores.by_player_level.filter(playerId); // ✅ DO — use single-column index + manual filter for (const row of ctx.db.scores.by_player.filter(playerId)) { - if (row.level === targetLevel) { /* ... */ } + if (row.level === targetLevel) { + /* ... */ + } } ``` @@ -374,6 +407,7 @@ for (const row of ctx.db.scores.by_player.filter(playerId)) { ## 4) Reducers ### Definition syntax (CRITICAL) + **Reducer name comes from the export — NOT from a string argument.** Use `reducer(params, fn)` or `reducer(fn)`. ```typescript @@ -384,10 +418,10 @@ import { t, SenderError } from 'spacetimedb/server'; export const reducer_name = spacetimedb.reducer({ param1: t.string(), param2: t.u64() }, (ctx, { param1, param2 }) => { // Validation if (!param1) throw new SenderError('param1 required'); - + // Access tables via ctx.db const row = ctx.db.myTable.primaryKey.find(param2); - + // Mutations ctx.db.myTable.insert({ ... }); ctx.db.myTable.primaryKey.update({ ...row, newField: value }); @@ -403,24 +437,31 @@ spacetimedb.reducer('reducer_name', { param1: t.string() }, (ctx, { param1 }) => ``` ### Update pattern (CRITICAL) + ```typescript // ✅ CORRECT — spread existing row, override specific fields const existing = ctx.db.task.id.find(taskId); -if (!existing) throw new SenderError('Task not found'); -ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp }); +if (!existing) throw new SenderError("Task not found"); +ctx.db.task.id.update({ + ...existing, + title: newTitle, + updatedAt: ctx.timestamp, +}); // ❌ WRONG — partial update nulls out other fields! ctx.db.task.id.update({ id: taskId, title: newTitle }); ``` ### Delete pattern + ```typescript // Delete by primary key VALUE (not row object) -ctx.db.task.id.delete(taskId); // taskId is the u64 value -ctx.db.player.identity.delete(ctx.sender); // delete by identity +ctx.db.task.id.delete(taskId); // taskId is the u64 value +ctx.db.player.identity.delete(ctx.sender); // delete by identity ``` ### Lifecycle hooks + ```typescript spacetimedb.clientConnected((ctx) => { // ctx.sender is the connecting identity @@ -433,16 +474,18 @@ spacetimedb.clientDisconnected((ctx) => { ``` ### Snake_case to camelCase conversion + - Server: `export const do_something = spacetimedb.reducer(...)` — name from export - Client: `conn.reducers.doSomething({ ... })` ### Object syntax required + ```typescript // ❌ WRONG - positional -conn.reducers.doSomething('value'); +conn.reducers.doSomething("value"); // ✅ RIGHT - object -conn.reducers.doSomething({ param: 'value' }); +conn.reducers.doSomething({ param: "value" }); ``` --- @@ -451,28 +494,34 @@ conn.reducers.doSomething({ param: 'value' }); ```typescript // 1. Define table first (scheduled: () => reducer — pass the exported reducer) -export const CleanupJob = table({ - name: 'cleanup_job', - scheduled: () => run_cleanup // reducer defined below -}, { - scheduledId: t.u64().primaryKey().autoInc(), - scheduledAt: t.scheduleAt(), - targetId: t.u64(), // Your custom data -}); +export const CleanupJob = table( + { + name: "cleanup_job", + scheduled: () => run_cleanup, // reducer defined below + }, + { + scheduledId: t.u64().primaryKey().autoInc(), + scheduledAt: t.scheduleAt(), + targetId: t.u64(), // Your custom data + }, +); // 2. Define scheduled reducer (receives full row as arg) -export const run_cleanup = spacetimedb.reducer({ arg: CleanupJob.rowType }, (ctx, { arg }) => { - // arg.scheduledId, arg.targetId available - // Row is auto-deleted after reducer completes -}); +export const run_cleanup = spacetimedb.reducer( + { arg: CleanupJob.rowType }, + (ctx, { arg }) => { + // arg.scheduledId, arg.targetId available + // Row is auto-deleted after reducer completes + }, +); // Schedule a job -import { ScheduleAt } from 'spacetimedb'; +import { ScheduleAt } from "spacetimedb"; const futureTime = ctx.timestamp.microsSinceUnixEpoch + 60_000_000n; // 60 seconds -ctx.db.cleanupJob.insert({ - scheduledId: 0n, +ctx.db.cleanupJob.insert({ + scheduledId: 0n, scheduledAt: ScheduleAt.time(futureTime), - targetId: someId + targetId: someId, }); // Cancel a job by deleting the row @@ -484,18 +533,21 @@ ctx.db.cleanupJob.scheduledId.delete(jobId); ## 6) Timestamps ### Server-side + ```typescript -import { Timestamp, ScheduleAt } from 'spacetimedb'; +import { Timestamp, ScheduleAt } from "spacetimedb"; // Current time ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp }); // Future time (add microseconds) -const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5 minutes +const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n; // 5 minutes ``` ### Client-side (CRITICAL) + **Timestamps are objects, not numbers:** + ```typescript // ❌ WRONG const date = new Date(row.createdAt); @@ -506,9 +558,10 @@ const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n)); ``` ### ScheduleAt on client + ```typescript // ScheduleAt is a tagged union -if (scheduleAt.tag === 'Time') { +if (scheduleAt.tag === "Time") { const date = new Date(Number(scheduleAt.value.microsSinceUnixEpoch / 1000n)); } ``` @@ -519,32 +572,37 @@ if (scheduleAt.tag === 'Time') { **`public: true` exposes ALL rows to ALL clients.** -| Scenario | Pattern | -|----------|---------| -| Everyone sees all rows | `public: true` | +| Scenario | Pattern | +| ------------------------- | ------------------------------------- | +| Everyone sees all rows | `public: true` | | Users see only their data | Private table + filtered subscription | ### Subscription patterns (client-side) + ```typescript // Subscribe to ALL public tables (simplest) conn.subscriptionBuilder().subscribeToAll(); // Subscribe to specific tables with SQL -conn.subscriptionBuilder().subscribe([ - 'SELECT * FROM message', - 'SELECT * FROM room WHERE is_public = true', -]); +conn + .subscriptionBuilder() + .subscribe([ + "SELECT * FROM message", + "SELECT * FROM room WHERE is_public = true", + ]); // Handle subscription lifecycle -conn.subscriptionBuilder() - .onApplied(() => console.log('Initial data loaded')) - .onError((e) => console.error('Subscription failed:', e)) +conn + .subscriptionBuilder() + .onApplied(() => console.log("Initial data loaded")) + .onError((e) => console.error("Subscription failed:", e)) .subscribeToAll(); ``` ### Private table + view pattern (RECOMMENDED) **Views are the recommended approach** for controlling data visibility. They provide: + - Server-side filtering (reduces network traffic) - Real-time updates when underlying data changes - Full control over what data clients can access @@ -557,28 +615,29 @@ conn.subscriptionBuilder() ```typescript // Private table with index on ownerId export const PrivateData = table( - { name: 'private_data', - indexes: [{ name: 'by_owner', algorithm: 'btree', columns: ['ownerId'] }] + { + name: "private_data", + indexes: [{ name: "by_owner", algorithm: "btree", columns: ["ownerId"] }], }, { id: t.u64().primaryKey().autoInc(), ownerId: t.identity(), - secret: t.string() - } + secret: t.string(), + }, ); // ❌ BAD — .iter() causes performance issues (re-evaluates on ANY row change) spacetimedb.view( - { name: 'my_data_slow', public: true }, + { name: "my_data_slow", public: true }, t.array(PrivateData.rowType), - (ctx) => [...ctx.db.privateData.iter()] // Works but VERY slow at scale + (ctx) => [...ctx.db.privateData.iter()], // Works but VERY slow at scale ); // ✅ GOOD — index lookup enables targeted invalidation spacetimedb.view( - { name: 'my_data', public: true }, + { name: "my_data", public: true }, t.array(PrivateData.rowType), - (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)] + (ctx) => [...ctx.db.privateData.by_owner.filter(ctx.sender)], ); ``` @@ -588,32 +647,40 @@ spacetimedb.view( // Query-builder views return a query; the SQL engine maintains the result incrementally. // This can scan the whole table if needed (e.g. leaderboard-style queries). spacetimedb.anonymousView( - { name: 'top_players', public: true }, + { name: "top_players", public: true }, t.array(Player.rowType), - (ctx) => - ctx.from.player - .where(p => p.score.gt(1000)) + (ctx) => ctx.from.player.where((p) => p.score.gt(1000)), ); ``` ### ViewContext vs AnonymousViewContext + ```typescript // ViewContext — has ctx.sender, result varies per user (computed per-subscriber) -spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => { - return [...ctx.db.item.by_owner.filter(ctx.sender)]; -}); +spacetimedb.view( + { name: "my_items", public: true }, + t.array(Item.rowType), + (ctx) => { + return [...ctx.db.item.by_owner.filter(ctx.sender)]; + }, +); // AnonymousViewContext — no ctx.sender, same result for everyone (shared, better perf) -spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(LeaderboardRow), (ctx) => { - return [...ctx.db.player.by_score.filter(/* top scores */)]; -}); +spacetimedb.anonymousView( + { name: "leaderboard", public: true }, + t.array(LeaderboardRow), + (ctx) => { + return [...ctx.db.player.by_score.filter(/* top scores */)]; + }, +); ``` **Views require explicit subscription:** + ```typescript conn.subscriptionBuilder().subscribe([ - 'SELECT * FROM public_table', - 'SELECT * FROM my_data', // Views need explicit SQL! + "SELECT * FROM public_table", + "SELECT * FROM my_data", // Views need explicit SQL! ]); ``` @@ -622,16 +689,18 @@ conn.subscriptionBuilder().subscribe([ ## 8) React Integration ### Key patterns + ```typescript // Memoize connectionBuilder to prevent reconnects on re-render -const builder = useMemo(() => - DbConnection.builder() - .withUri(SPACETIMEDB_URI) - .withDatabaseName(MODULE_NAME) - .withToken(localStorage.getItem('auth_token') || undefined) - .onConnect(onConnect) - .onConnectError(onConnectError), - [] // Empty deps - only create once +const builder = useMemo( + () => + DbConnection.builder() + .withUri(SPACETIMEDB_URI) + .withDatabaseName(MODULE_NAME) + .withToken(localStorage.getItem("auth_token") || undefined) + .onConnect(onConnect) + .onConnectError(onConnectError), + [], // Empty deps - only create once ); // useTable returns tuple [rows, isLoading] @@ -650,17 +719,18 @@ const isOwner = row.ownerId.toHexString() === myIdentity.toHexString(); ⚠️ Procedures are currently in beta. API may change. ### Defining a procedure + **Procedure name comes from the export — NOT from a string argument.** Use `procedure(params, ret, fn)` or `procedure(ret, fn)`. ```typescript // ✅ CORRECT — export const name = spacetimedb.procedure(params, ret, fn) export const fetch_external_data = spacetimedb.procedure( { url: t.string() }, - t.string(), // return type + t.string(), // return type (ctx, { url }) => { const response = ctx.http.fetch(url); return response.text(); - } + }, ); ``` @@ -692,18 +762,20 @@ spacetimedb.procedure({ url: t.string() }, t.unit(), (ctx, { url }) => { ``` ### Key differences from reducers -| Reducers | Procedures | -|----------|------------| + +| Reducers | Procedures | +| --------------------------- | ------------------------------------- | | `ctx.db` available directly | Must use `ctx.withTx(tx => tx.db...)` | -| Automatic transaction | Manual transaction management | -| No HTTP/network | `ctx.http.fetch()` available | -| No return values to caller | Can return data to caller | +| Automatic transaction | Manual transaction management | +| No HTTP/network | `ctx.http.fetch()` available | +| No return values to caller | Can return data to caller | --- ## 10) Project Structure ### Server (`backend/spacetimedb/`) + ``` src/schema.ts → Tables, export spacetimedb src/index.ts → Reducers, lifecycle, import schema @@ -712,12 +784,14 @@ tsconfig.json → Standard config ``` ### Avoiding circular imports + ``` schema.ts → defines tables AND exports spacetimedb index.ts → imports spacetimedb from ./schema, defines reducers ``` ### Client (`client/`) + ``` src/module_bindings/ → Generated (spacetime generate) src/main.tsx → Provider, connection setup diff --git a/README.md b/README.md index 92a1c58..e3e8d0b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ export default tseslint.config({ languageOptions: { // other options... parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, }, @@ -48,11 +48,11 @@ export default tseslint.config({ ```js // eslint.config.js -import react from 'eslint-plugin-react'; +import react from "eslint-plugin-react"; export default tseslint.config({ // Set the react version - settings: { react: { version: '18.3' } }, + settings: { react: { version: "18.3" } }, plugins: { // Add the react plugin react, @@ -61,7 +61,7 @@ export default tseslint.config({ // other rules... // Enable its recommended rules ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, + ...react.configs["jsx-runtime"].rules, }, }); ``` diff --git a/dist/index.html b/dist/index.html index 56c6c05..8a51fe7 100644 --- a/dist/index.html +++ b/dist/index.html @@ -6,7 +6,7 @@ Vite + React + TS - +
diff --git a/package.json b/package.json index 50032c1..d19ec91 100644 --- a/package.json +++ b/package.json @@ -45,4 +45,4 @@ "vite": "^7.1.5", "vitest": "3.2.4" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b27ded0..7aa0640 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,11 +1,10 @@ -lockfileVersion: '9.0' +lockfileVersion: "9.0" settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: - .: dependencies: oidc-client-ts: @@ -24,34 +23,34 @@ importers: specifier: ^2.1.0 version: 2.1.0(react@18.3.1) devDependencies: - '@eslint/js': + "@eslint/js": specifier: ^9.17.0 version: 9.39.4 - '@testing-library/jest-dom': + "@testing-library/jest-dom": specifier: ^6.6.3 version: 6.9.1 - '@testing-library/react': + "@testing-library/react": specifier: ^16.2.0 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@testing-library/user-event': + "@testing-library/user-event": specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) - '@types/react': + "@types/react": specifier: ^18.3.18 version: 18.3.28 - '@types/react-dom': + "@types/react-dom": specifier: ^18.3.5 version: 18.3.7(@types/react@18.3.28) - '@typescript-eslint/eslint-plugin': + "@typescript-eslint/eslint-plugin": specifier: ^8.57.2 version: 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.6.3))(eslint@9.39.4)(typescript@5.6.3) - '@typescript-eslint/parser': + "@typescript-eslint/parser": specifier: ^8.57.2 version: 8.57.2(eslint@9.39.4)(typescript@5.6.3) - '@vitejs/plugin-basic-ssl': + "@vitejs/plugin-basic-ssl": specifier: ^2.3.0 version: 2.3.0(vite@7.3.1) - '@vitejs/plugin-react': + "@vitejs/plugin-react": specifier: ^5.0.2 version: 5.2.0(vite@7.3.1) eslint: @@ -89,638 +88,1018 @@ importers: version: 3.2.4(jsdom@26.1.0) packages: + "@adobe/css-tools@4.4.4": + resolution: + { + integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==, + } - '@adobe/css-tools@4.4.4': - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + "@asamuzakjp/css-color@3.2.0": + resolution: + { + integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==, + } - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + "@babel/code-frame@7.29.0": + resolution: + { + integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==, + } + engines: { node: ">=6.9.0" } - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} + "@babel/compat-data@7.29.0": + resolution: + { + integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==, + } + engines: { node: ">=6.9.0" } - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} - engines: {node: '>=6.9.0'} + "@babel/core@7.29.0": + resolution: + { + integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==, + } + engines: { node: ">=6.9.0" } - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} + "@babel/generator@7.29.1": + resolution: + { + integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==, + } + engines: { node: ">=6.9.0" } - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} + "@babel/helper-compilation-targets@7.28.6": + resolution: + { + integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} + "@babel/helper-globals@7.28.0": + resolution: + { + integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} + "@babel/helper-module-imports@7.28.6": + resolution: + { + integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} + "@babel/helper-module-transforms@7.28.6": + resolution: + { + integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0 + "@babel/core": ^7.0.0 - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} - engines: {node: '>=6.9.0'} + "@babel/helper-plugin-utils@7.28.6": + resolution: + { + integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} + "@babel/helper-string-parser@7.27.1": + resolution: + { + integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} + "@babel/helper-validator-identifier@7.28.5": + resolution: + { + integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==, + } + engines: { node: ">=6.9.0" } - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} + "@babel/helper-validator-option@7.27.1": + resolution: + { + integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, + } + engines: { node: ">=6.9.0" } - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} + "@babel/helpers@7.29.2": + resolution: + { + integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==, + } + engines: { node: ">=6.9.0" } - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} - engines: {node: '>=6.0.0'} + "@babel/parser@7.29.2": + resolution: + { + integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==, + } + engines: { node: ">=6.0.0" } hasBin: true - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} + "@babel/plugin-transform-react-jsx-self@7.27.1": + resolution: + { + integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0-0 + "@babel/core": ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} + "@babel/plugin-transform-react-jsx-source@7.27.1": + resolution: + { + integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==, + } + engines: { node: ">=6.9.0" } peerDependencies: - '@babel/core': ^7.0.0-0 + "@babel/core": ^7.0.0-0 - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} - engines: {node: '>=6.9.0'} + "@babel/runtime@7.29.2": + resolution: + { + integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==, + } + engines: { node: ">=6.9.0" } - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} + "@babel/template@7.28.6": + resolution: + { + integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==, + } + engines: { node: ">=6.9.0" } - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} + "@babel/traverse@7.29.0": + resolution: + { + integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==, + } + engines: { node: ">=6.9.0" } - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} + "@babel/types@7.29.0": + resolution: + { + integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==, + } + engines: { node: ">=6.9.0" } - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} + "@csstools/color-helpers@5.1.0": + resolution: + { + integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==, + } + engines: { node: ">=18" } - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} + "@csstools/css-calc@2.1.4": + resolution: + { + integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==, + } + engines: { node: ">=18" } peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + "@csstools/css-parser-algorithms": ^3.0.5 + "@csstools/css-tokenizer": ^3.0.4 - '@csstools/css-color-parser@3.1.0': - resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} - engines: {node: '>=18'} + "@csstools/css-color-parser@3.1.0": + resolution: + { + integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==, + } + engines: { node: ">=18" } peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + "@csstools/css-parser-algorithms": ^3.0.5 + "@csstools/css-tokenizer": ^3.0.4 - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} + "@csstools/css-parser-algorithms@3.0.5": + resolution: + { + integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==, + } + engines: { node: ">=18" } peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 + "@csstools/css-tokenizer": ^3.0.4 - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} + "@csstools/css-tokenizer@3.0.4": + resolution: + { + integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==, + } + engines: { node: ">=18" } - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} - engines: {node: '>=18'} + "@esbuild/aix-ppc64@0.27.4": + resolution: + { + integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} - engines: {node: '>=18'} + "@esbuild/android-arm64@0.27.4": + resolution: + { + integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==, + } + engines: { node: ">=18" } cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} - engines: {node: '>=18'} + "@esbuild/android-arm@0.27.4": + resolution: + { + integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==, + } + engines: { node: ">=18" } cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} - engines: {node: '>=18'} + "@esbuild/android-x64@0.27.4": + resolution: + { + integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==, + } + engines: { node: ">=18" } cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} - engines: {node: '>=18'} + "@esbuild/darwin-arm64@0.27.4": + resolution: + { + integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==, + } + engines: { node: ">=18" } cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} - engines: {node: '>=18'} + "@esbuild/darwin-x64@0.27.4": + resolution: + { + integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==, + } + engines: { node: ">=18" } cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} - engines: {node: '>=18'} + "@esbuild/freebsd-arm64@0.27.4": + resolution: + { + integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==, + } + engines: { node: ">=18" } cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} - engines: {node: '>=18'} + "@esbuild/freebsd-x64@0.27.4": + resolution: + { + integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} - engines: {node: '>=18'} + "@esbuild/linux-arm64@0.27.4": + resolution: + { + integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==, + } + engines: { node: ">=18" } cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} - engines: {node: '>=18'} + "@esbuild/linux-arm@0.27.4": + resolution: + { + integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==, + } + engines: { node: ">=18" } cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} - engines: {node: '>=18'} + "@esbuild/linux-ia32@0.27.4": + resolution: + { + integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==, + } + engines: { node: ">=18" } cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} - engines: {node: '>=18'} + "@esbuild/linux-loong64@0.27.4": + resolution: + { + integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==, + } + engines: { node: ">=18" } cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} - engines: {node: '>=18'} + "@esbuild/linux-mips64el@0.27.4": + resolution: + { + integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==, + } + engines: { node: ">=18" } cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} - engines: {node: '>=18'} + "@esbuild/linux-ppc64@0.27.4": + resolution: + { + integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==, + } + engines: { node: ">=18" } cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} - engines: {node: '>=18'} + "@esbuild/linux-riscv64@0.27.4": + resolution: + { + integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==, + } + engines: { node: ">=18" } cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} - engines: {node: '>=18'} + "@esbuild/linux-s390x@0.27.4": + resolution: + { + integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==, + } + engines: { node: ">=18" } cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} - engines: {node: '>=18'} + "@esbuild/linux-x64@0.27.4": + resolution: + { + integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==, + } + engines: { node: ">=18" } cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} - engines: {node: '>=18'} + "@esbuild/netbsd-arm64@0.27.4": + resolution: + { + integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==, + } + engines: { node: ">=18" } cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} - engines: {node: '>=18'} + "@esbuild/netbsd-x64@0.27.4": + resolution: + { + integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==, + } + engines: { node: ">=18" } cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} - engines: {node: '>=18'} + "@esbuild/openbsd-arm64@0.27.4": + resolution: + { + integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} - engines: {node: '>=18'} + "@esbuild/openbsd-x64@0.27.4": + resolution: + { + integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==, + } + engines: { node: ">=18" } cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} - engines: {node: '>=18'} + "@esbuild/openharmony-arm64@0.27.4": + resolution: + { + integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} - engines: {node: '>=18'} + "@esbuild/sunos-x64@0.27.4": + resolution: + { + integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==, + } + engines: { node: ">=18" } cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} - engines: {node: '>=18'} + "@esbuild/win32-arm64@0.27.4": + resolution: + { + integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==, + } + engines: { node: ">=18" } cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} - engines: {node: '>=18'} + "@esbuild/win32-ia32@0.27.4": + resolution: + { + integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==, + } + engines: { node: ">=18" } cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} - engines: {node: '>=18'} + "@esbuild/win32-x64@0.27.4": + resolution: + { + integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==, + } + engines: { node: ">=18" } cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + "@eslint-community/eslint-utils@4.9.1": + resolution: + { + integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + "@eslint-community/regexpp@4.12.2": + resolution: + { + integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, + } + engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/config-array@0.21.2": + resolution: + { + integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/config-helpers@0.4.2": + resolution: + { + integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/core@0.17.0": + resolution: + { + integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/eslintrc@3.3.5": + resolution: + { + integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/js@9.39.4": + resolution: + { + integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/object-schema@2.1.7": + resolution: + { + integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@eslint/plugin-kit@0.4.1": + resolution: + { + integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} + "@humanfs/core@0.19.1": + resolution: + { + integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, + } + engines: { node: ">=18.18.0" } - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} + "@humanfs/node@0.16.7": + resolution: + { + integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==, + } + engines: { node: ">=18.18.0" } - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + "@humanwhocodes/module-importer@1.0.1": + resolution: + { + integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, + } + engines: { node: ">=12.22" } - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} + "@humanwhocodes/retry@0.4.3": + resolution: + { + integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, + } + engines: { node: ">=18.18" } - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + "@jridgewell/gen-mapping@0.3.13": + resolution: + { + integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, + } - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + "@jridgewell/remapping@2.3.5": + resolution: + { + integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, + } - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} + "@jridgewell/resolve-uri@3.1.2": + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: ">=6.0.0" } - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + "@jridgewell/sourcemap-codec@1.5.5": + resolution: + { + integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, + } - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + "@jridgewell/trace-mapping@0.3.31": + resolution: + { + integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, + } - '@rolldown/pluginutils@1.0.0-rc.3': - resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + "@rolldown/pluginutils@1.0.0-rc.3": + resolution: + { + integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==, + } - '@rollup/rollup-android-arm-eabi@4.60.0': - resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} + "@rollup/rollup-android-arm-eabi@4.60.0": + resolution: + { + integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==, + } cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.0': - resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} + "@rollup/rollup-android-arm64@4.60.0": + resolution: + { + integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==, + } cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.0': - resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} + "@rollup/rollup-darwin-arm64@4.60.0": + resolution: + { + integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==, + } cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.0': - resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} + "@rollup/rollup-darwin-x64@4.60.0": + resolution: + { + integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==, + } cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.0': - resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} + "@rollup/rollup-freebsd-arm64@4.60.0": + resolution: + { + integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==, + } cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.0': - resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} + "@rollup/rollup-freebsd-x64@4.60.0": + resolution: + { + integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==, + } cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.0': - resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} + "@rollup/rollup-linux-arm-gnueabihf@4.60.0": + resolution: + { + integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==, + } cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.0': - resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} + "@rollup/rollup-linux-arm-musleabihf@4.60.0": + resolution: + { + integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==, + } cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.0': - resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} + "@rollup/rollup-linux-arm64-gnu@4.60.0": + resolution: + { + integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==, + } cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.0': - resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} + "@rollup/rollup-linux-arm64-musl@4.60.0": + resolution: + { + integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==, + } cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.0': - resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} + "@rollup/rollup-linux-loong64-gnu@4.60.0": + resolution: + { + integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==, + } cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.0': - resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} + "@rollup/rollup-linux-loong64-musl@4.60.0": + resolution: + { + integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==, + } cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.0': - resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} + "@rollup/rollup-linux-ppc64-gnu@4.60.0": + resolution: + { + integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==, + } cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.0': - resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} + "@rollup/rollup-linux-ppc64-musl@4.60.0": + resolution: + { + integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==, + } cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.0': - resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} + "@rollup/rollup-linux-riscv64-gnu@4.60.0": + resolution: + { + integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==, + } cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.0': - resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} + "@rollup/rollup-linux-riscv64-musl@4.60.0": + resolution: + { + integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==, + } cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.0': - resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} + "@rollup/rollup-linux-s390x-gnu@4.60.0": + resolution: + { + integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==, + } cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.0': - resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} + "@rollup/rollup-linux-x64-gnu@4.60.0": + resolution: + { + integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==, + } cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.0': - resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} + "@rollup/rollup-linux-x64-musl@4.60.0": + resolution: + { + integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==, + } cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.0': - resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} + "@rollup/rollup-openbsd-x64@4.60.0": + resolution: + { + integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==, + } cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.0': - resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} + "@rollup/rollup-openharmony-arm64@4.60.0": + resolution: + { + integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==, + } cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.0': - resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} + "@rollup/rollup-win32-arm64-msvc@4.60.0": + resolution: + { + integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==, + } cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.0': - resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} + "@rollup/rollup-win32-ia32-msvc@4.60.0": + resolution: + { + integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==, + } cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.0': - resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} + "@rollup/rollup-win32-x64-gnu@4.60.0": + resolution: + { + integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==, + } cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.0': - resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} + "@rollup/rollup-win32-x64-msvc@4.60.0": + resolution: + { + integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==, + } cpu: [x64] os: [win32] - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} + "@testing-library/dom@10.4.1": + resolution: + { + integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==, + } + engines: { node: ">=18" } - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + "@testing-library/jest-dom@6.9.1": + resolution: + { + integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==, + } + engines: { node: ">=14", npm: ">=6", yarn: ">=1" } - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - engines: {node: '>=18'} + "@testing-library/react@16.3.2": + resolution: + { + integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==, + } + engines: { node: ">=18" } peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: - '@types/react': + "@types/react": optional: true - '@types/react-dom': + "@types/react-dom": optional: true - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} - engines: {node: '>=12', npm: '>=6'} + "@testing-library/user-event@14.6.1": + resolution: + { + integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==, + } + engines: { node: ">=12", npm: ">=6" } peerDependencies: - '@testing-library/dom': '>=7.21.4' + "@testing-library/dom": ">=7.21.4" - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + "@types/aria-query@5.0.4": + resolution: + { + integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, + } - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + "@types/babel__core@7.20.5": + resolution: + { + integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, + } - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + "@types/babel__generator@7.27.0": + resolution: + { + integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, + } - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + "@types/babel__template@7.4.4": + resolution: + { + integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, + } - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + "@types/babel__traverse@7.28.0": + resolution: + { + integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, + } - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + "@types/chai@5.2.3": + resolution: + { + integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==, + } - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + "@types/deep-eql@4.0.2": + resolution: + { + integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, + } - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + "@types/estree@1.0.8": + resolution: + { + integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, + } - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + "@types/json-schema@7.0.15": + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + "@types/prop-types@15.7.15": + resolution: + { + integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==, + } - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + "@types/react-dom@18.3.7": + resolution: + { + integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==, + } peerDependencies: - '@types/react': ^18.0.0 + "@types/react": ^18.0.0 - '@types/react@18.3.28': - resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + "@types/react@18.3.28": + resolution: + { + integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==, + } - '@typescript-eslint/eslint-plugin@8.57.2': - resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/eslint-plugin@8.57.2": + resolution: + { + integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: - '@typescript-eslint/parser': ^8.57.2 + "@typescript-eslint/parser": ^8.57.2 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" - '@typescript-eslint/parser@8.57.2': - resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/parser@8.57.2": + resolution: + { + integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" - '@typescript-eslint/project-service@8.57.2': - resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/project-service@8.57.2": + resolution: + { + integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" - '@typescript-eslint/scope-manager@8.57.2': - resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/scope-manager@8.57.2": + resolution: + { + integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@typescript-eslint/tsconfig-utils@8.57.2': - resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/tsconfig-utils@8.57.2": + resolution: + { + integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" - '@typescript-eslint/type-utils@8.57.2': - resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/type-utils@8.57.2": + resolution: + { + integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" - '@typescript-eslint/types@8.57.2': - resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/types@8.57.2": + resolution: + { + integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@typescript-eslint/typescript-estree@8.57.2': - resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/typescript-estree@8.57.2": + resolution: + { + integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" - '@typescript-eslint/utils@8.57.2': - resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/utils@8.57.2": + resolution: + { + integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" - '@typescript-eslint/visitor-keys@8.57.2': - resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + "@typescript-eslint/visitor-keys@8.57.2": + resolution: + { + integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } - '@vitejs/plugin-basic-ssl@2.3.0': - resolution: {integrity: sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + "@vitejs/plugin-basic-ssl@2.3.0": + resolution: + { + integrity: sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==, + } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@vitejs/plugin-react@5.2.0': - resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} - engines: {node: ^20.19.0 || >=22.12.0} + "@vitejs/plugin-react@5.2.0": + resolution: + { + integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==, + } + engines: { node: ^20.19.0 || >=22.12.0 } peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + "@vitest/expect@3.2.4": + resolution: + { + integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==, + } - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + "@vitest/mocker@3.2.4": + resolution: + { + integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==, + } peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 @@ -730,387 +1109,678 @@ packages: vite: optional: true - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + "@vitest/pretty-format@3.2.4": + resolution: + { + integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==, + } - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + "@vitest/runner@3.2.4": + resolution: + { + integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==, + } - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + "@vitest/snapshot@3.2.4": + resolution: + { + integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==, + } - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + "@vitest/spy@3.2.4": + resolution: + { + integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==, + } - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + "@vitest/utils@3.2.4": + resolution: + { + integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==, + } acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} + resolution: + { + integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==, + } + engines: { node: ">=0.4.0" } hasBin: true agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==, + } + engines: { node: ">= 14" } ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + resolution: + { + integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==, + } ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: ">=8" } ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, + } + engines: { node: ">=8" } ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, + } + engines: { node: ">=10" } argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + resolution: + { + integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==, + } aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==, + } + engines: { node: ">= 0.4" } array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==, + } + engines: { node: ">= 0.4" } array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==, + } + engines: { node: ">= 0.4" } array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==, + } + engines: { node: ">= 0.4" } array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==, + } + engines: { node: ">= 0.4" } array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==, + } + engines: { node: ">= 0.4" } array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==, + } + engines: { node: ">= 0.4" } arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==, + } + engines: { node: ">= 0.4" } assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, + } + engines: { node: ">=12" } async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==, + } + engines: { node: ">= 0.4" } available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==, + } + engines: { node: ">= 0.4" } balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} + resolution: + { + integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, + } + engines: { node: 18 || 20 || >=22 } base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + resolution: + { + integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, + } baseline-browser-mapping@2.10.12: - resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} - engines: {node: '>=6.0.0'} + resolution: + { + integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==, + } + engines: { node: ">=6.0.0" } hasBin: true brace-expansion@1.1.13: - resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + resolution: + { + integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==, + } brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} - engines: {node: 18 || 20 || >=22} + resolution: + { + integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==, + } + engines: { node: 18 || 20 || >=22 } browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + resolution: + { + integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==, + } + engines: { node: ">=8" } call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, + } + engines: { node: ">= 0.4" } call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==, + } + engines: { node: ">= 0.4" } call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, + } + engines: { node: ">= 0.4" } callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: ">=6" } caniuse-lite@1.0.30001781: - resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + resolution: + { + integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==, + } chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==, + } + engines: { node: ">=18" } chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, + } + engines: { node: ">=10" } check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} + resolution: + { + integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==, + } + engines: { node: ">= 16" } color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: ">=7.0.0" } color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + resolution: + { + integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==, + } cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==, + } + engines: { node: ">=18" } csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + resolution: + { + integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, + } data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==, + } + engines: { node: ">=18" } data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==, + } + engines: { node: ">= 0.4" } data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==, + } + engines: { node: ">= 0.4" } data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==, + } + engines: { node: ">= 0.4" } debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } peerDependencies: - supports-color: '*' + supports-color: "*" peerDependenciesMeta: supports-color: optional: true decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + resolution: + { + integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==, + } deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==, + } + engines: { node: ">=6" } deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + resolution: + { + integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, + } define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, + } + engines: { node: ">= 0.4" } define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==, + } + engines: { node: ">= 0.4" } dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, + } + engines: { node: ">=6" } doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==, + } + engines: { node: ">=0.10.0" } dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + resolution: + { + integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==, + } dom-accessibility-api@0.6.3: - resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + resolution: + { + integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==, + } dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, + } + engines: { node: ">= 0.4" } electron-to-chromium@1.5.328: - resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} + resolution: + { + integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==, + } entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} + resolution: + { + integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==, + } + engines: { node: ">=0.12" } es-abstract@1.24.1: - resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==, + } + engines: { node: ">= 0.4" } es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, + } + engines: { node: ">= 0.4" } es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, + } + engines: { node: ">= 0.4" } es-iterator-helpers@1.3.1: - resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==, + } + engines: { node: ">= 0.4" } es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + resolution: + { + integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==, + } es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, + } + engines: { node: ">= 0.4" } es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, + } + engines: { node: ">= 0.4" } es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==, + } + engines: { node: ">= 0.4" } es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==, + } + engines: { node: ">= 0.4" } esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==, + } + engines: { node: ">=18" } hasBin: true escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: ">=6" } escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, + } + engines: { node: ">=10" } eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==, + } + engines: { node: ">=10" } peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 eslint-plugin-react-refresh@0.4.26: - resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + resolution: + { + integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==, + } peerDependencies: - eslint: '>=8.40' + eslint: ">=8.40" eslint-plugin-react@7.37.5: - resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==, + } + engines: { node: ">=4" } peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: + { + integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + resolution: + { + integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: + { + integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} + resolution: + { + integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, + } + engines: { node: ^20.19.0 || ^22.13.0 || >=24 } eslint@9.39.4: - resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: + { + integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } hasBin: true peerDependencies: - jiti: '*' + jiti: "*" peerDependenciesMeta: jiti: optional: true espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: + { + integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} + resolution: + { + integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, + } + engines: { node: ">=0.10" } esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, + } + engines: { node: ">=4.0" } estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: ">=4.0" } estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, + } + engines: { node: ">=0.10.0" } expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==, + } + engines: { node: ">=12.0.0" } fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + resolution: + { + integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, + } fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + resolution: + { + integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, + } fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + resolution: + { + integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, + } fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, + } + engines: { node: ">=12.0.0" } peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1118,271 +1788,478 @@ packages: optional: true file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + resolution: + { + integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, + } + engines: { node: ">=16.0.0" } find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, + } + engines: { node: ">=10" } flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, + } + engines: { node: ">=16" } flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + resolution: + { + integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==, + } for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==, + } + engines: { node: ">= 0.4" } fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } os: [darwin] function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==, + } + engines: { node: ">= 0.4" } functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + resolution: + { + integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==, + } generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==, + } + engines: { node: ">= 0.4" } gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, + } + engines: { node: ">= 0.4" } get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, + } + engines: { node: ">= 0.4" } get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==, + } + engines: { node: ">= 0.4" } glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, + } + engines: { node: ">=10.13.0" } globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==, + } + engines: { node: ">=18" } globals@15.15.0: - resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==, + } + engines: { node: ">=18" } globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==, + } + engines: { node: ">= 0.4" } gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, + } + engines: { node: ">= 0.4" } has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==, + } + engines: { node: ">= 0.4" } has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, + } + engines: { node: ">=8" } has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + resolution: + { + integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==, + } has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==, + } + engines: { node: ">= 0.4" } has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, + } + engines: { node: ">= 0.4" } has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, + } + engines: { node: ">= 0.4" } hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, + } + engines: { node: ">= 0.4" } headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + resolution: + { + integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==, + } html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==, + } + engines: { node: ">=18" } http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==, + } + engines: { node: ">= 14" } https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} + resolution: + { + integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==, + } + engines: { node: ">= 14" } iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, + } + engines: { node: ">=0.10.0" } ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: ">= 4" } ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==, + } + engines: { node: ">= 4" } import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, + } + engines: { node: ">=6" } imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + resolution: + { + integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, + } + engines: { node: ">=0.8.19" } indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==, + } + engines: { node: ">=8" } internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==, + } + engines: { node: ">= 0.4" } is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==, + } + engines: { node: ">= 0.4" } is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==, + } + engines: { node: ">= 0.4" } is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==, + } + engines: { node: ">= 0.4" } is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==, + } + engines: { node: ">= 0.4" } is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==, + } + engines: { node: ">= 0.4" } is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==, + } + engines: { node: ">= 0.4" } is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==, + } + engines: { node: ">= 0.4" } is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==, + } + engines: { node: ">= 0.4" } is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==, + } + engines: { node: ">= 0.4" } is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==, + } + engines: { node: ">= 0.4" } is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==, + } + engines: { node: ">= 0.4" } is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==, + } + engines: { node: ">= 0.4" } is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==, + } + engines: { node: ">= 0.4" } is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + resolution: + { + integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==, + } is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==, + } + engines: { node: ">= 0.4" } is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==, + } + engines: { node: ">= 0.4" } is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==, + } + engines: { node: ">= 0.4" } is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==, + } + engines: { node: ">= 0.4" } is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==, + } + engines: { node: ">= 0.4" } is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==, + } + engines: { node: ">= 0.4" } is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==, + } + engines: { node: ">= 0.4" } is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==, + } + engines: { node: ">= 0.4" } is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==, + } + engines: { node: ">= 0.4" } isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + resolution: + { + integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, + } isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==, + } + engines: { node: ">= 0.4" } js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + resolution: + { + integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==, + } js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + resolution: + { + integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, + } hasBin: true jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==, + } + engines: { node: ">=18" } peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -1390,359 +2267,629 @@ packages: optional: true jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, + } + engines: { node: ">=6" } hasBin: true json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + resolution: + { + integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, + } json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + resolution: + { + integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, + } json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + resolution: + { + integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, + } json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } hasBin: true jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} + resolution: + { + integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==, + } + engines: { node: ">=4.0" } jwt-decode@4.0.0: - resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==, + } + engines: { node: ">=18" } keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + resolution: + { + integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, + } levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, + } + engines: { node: ">= 0.8.0" } locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, + } + engines: { node: ">=10" } lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + resolution: + { + integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, + } loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + resolution: + { + integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, + } hasBin: true loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + resolution: + { + integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==, + } lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + resolution: + { + integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, + } lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + resolution: + { + integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, + } hasBin: true magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + resolution: + { + integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, + } math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, + } + engines: { node: ">= 0.4" } min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==, + } + engines: { node: ">=4" } minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} - engines: {node: 18 || 20 || >=22} + resolution: + { + integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==, + } + engines: { node: 18 || 20 || >=22 } minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + resolution: + { + integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, + } ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + resolution: + { + integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, + } node-exports-info@1.6.0: - resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==, + } + engines: { node: ">= 0.4" } node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + resolution: + { + integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==, + } nwsapi@2.2.23: - resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + resolution: + { + integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==, + } object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, + } + engines: { node: ">=0.10.0" } object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, + } + engines: { node: ">= 0.4" } object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==, + } + engines: { node: ">= 0.4" } object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==, + } + engines: { node: ">= 0.4" } object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==, + } + engines: { node: ">= 0.4" } object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==, + } + engines: { node: ">= 0.4" } object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==, + } + engines: { node: ">= 0.4" } oidc-client-ts@3.5.0: - resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==, + } + engines: { node: ">=18" } optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, + } + engines: { node: ">= 0.8.0" } own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==, + } + engines: { node: ">= 0.4" } p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, + } + engines: { node: ">=10" } p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, + } + engines: { node: ">=10" } parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: ">=6" } parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + resolution: + { + integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==, + } path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + resolution: + { + integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, + } pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + resolution: + { + integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, + } pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} + resolution: + { + integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==, + } + engines: { node: ">= 14.16" } picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, + } + engines: { node: ">=12" } possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==, + } + engines: { node: ">= 0.4" } postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} + resolution: + { + integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==, + } + engines: { node: ^10 || ^12 || >=14 } prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, + } + engines: { node: ">= 0.8.0" } prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==, + } + engines: { node: ">=14" } hasBin: true 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} + resolution: + { + integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==, + } + engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 } prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + resolution: + { + integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, + } punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: ">=6" } pure-rand@7.0.1: - resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + resolution: + { + integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==, + } react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + resolution: + { + integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==, + } peerDependencies: react: ^18.3.1 react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + resolution: + { + integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, + } react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + resolution: + { + integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==, + } react-oidc-context@3.3.1: - resolution: {integrity: sha512-/Azvm9W4DhhOtSDBE73kFInh1b6zZRRfILKbgmk2syExMF0PCYJOn/dGdOOi2BFX8x0rCeUe45NXHU+/+xDcrQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-/Azvm9W4DhhOtSDBE73kFInh1b6zZRRfILKbgmk2syExMF0PCYJOn/dGdOOi2BFX8x0rCeUe45NXHU+/+xDcrQ==, + } + engines: { node: ">=18" } peerDependencies: oidc-client-ts: ^3.1.0 - react: '>=16.14.0' + react: ">=16.14.0" react-refresh@0.18.0: - resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==, + } + engines: { node: ">=0.10.0" } react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==, + } + engines: { node: ">=0.10.0" } redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==, + } + engines: { node: ">=8" } reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==, + } + engines: { node: ">= 0.4" } regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==, + } + engines: { node: ">= 0.4" } resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: ">=4" } resolve@2.0.0-next.6: - resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==, + } + engines: { node: ">= 0.4" } hasBin: true rollup@4.60.0: - resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + resolution: + { + integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==, + } + engines: { node: ">=18.0.0", npm: ">=8.0.0" } hasBin: true rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + resolution: + { + integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==, + } safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} + resolution: + { + integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==, + } + engines: { node: ">=0.4" } safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==, + } + engines: { node: ">= 0.4" } safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==, + } + engines: { node: ">= 0.4" } safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==, + } + engines: { node: ">=10" } safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, + } saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} + resolution: + { + integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==, + } + engines: { node: ">=v12.22.7" } scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + resolution: + { + integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==, + } semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } hasBin: true semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, + } + engines: { node: ">=10" } hasBin: true set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==, + } + engines: { node: ">= 0.4" } set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==, + } + engines: { node: ">= 0.4" } set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==, + } + engines: { node: ">= 0.4" } shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==, + } + engines: { node: ">= 0.4" } side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, + } + engines: { node: ">= 0.4" } side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, + } + engines: { node: ">= 0.4" } side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, + } + engines: { node: ">= 0.4" } siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + resolution: + { + integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, + } source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: ">=0.10.0" } spacetimedb@2.1.0: - resolution: {integrity: sha512-Kzs+HXCRj15ryld03ztU4a2uQg0M8ivV/9Bk/gvMpb59lLc/A2/r7UkGCYBePsBL7Zwqgr8gE8FeufoZVXtPnA==} + resolution: + { + integrity: sha512-Kzs+HXCRj15ryld03ztU4a2uQg0M8ivV/9Bk/gvMpb59lLc/A2/r7UkGCYBePsBL7Zwqgr8gE8FeufoZVXtPnA==, + } peerDependencies: - '@angular/core': '>=17.0.0' - '@tanstack/react-query': ^5.0.0 + "@angular/core": ">=17.0.0" + "@tanstack/react-query": ^5.0.0 react: ^18.0.0 || ^19.0.0-0 || ^19.0.0 svelte: ^4.0.0 || ^5.0.0 undici: ^6.19.2 vue: ^3.3.0 peerDependenciesMeta: - '@angular/core': + "@angular/core": optional: true - '@tanstack/react-query': + "@tanstack/react-query": optional: true react: optional: true @@ -1754,174 +2901,291 @@ packages: optional: true stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + resolution: + { + integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, + } statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, + } + engines: { node: ">= 0.8" } std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + resolution: + { + integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==, + } stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==, + } + engines: { node: ">= 0.4" } string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==, + } + engines: { node: ">= 0.4" } string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + resolution: + { + integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==, + } string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==, + } + engines: { node: ">= 0.4" } string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==, + } + engines: { node: ">= 0.4" } string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==, + } + engines: { node: ">= 0.4" } strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==, + } + engines: { node: ">=8" } strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, + } + engines: { node: ">=8" } strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + resolution: + { + integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==, + } supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, + } + engines: { node: ">=8" } supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, + } + engines: { node: ">= 0.4" } symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + resolution: + { + integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, + } tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + resolution: + { + integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, + } tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + resolution: + { + integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==, + } tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} + resolution: + { + integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, + } + engines: { node: ">=12.0.0" } tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} + resolution: + { + integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==, + } + engines: { node: ^18.0.0 || >=20.0.0 } tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==, + } + engines: { node: ">=14.0.0" } tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} + resolution: + { + integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==, + } + engines: { node: ">=14.0.0" } tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + resolution: + { + integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==, + } tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + resolution: + { + integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==, + } hasBin: true tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==, + } + engines: { node: ">=16" } tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==, + } + engines: { node: ">=18" } ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} - engines: {node: '>=18.12'} + resolution: + { + integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==, + } + engines: { node: ">=18.12" } peerDependencies: - typescript: '>=4.8.4' + typescript: ">=4.8.4" type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, + } + engines: { node: ">= 0.8.0" } typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==, + } + engines: { node: ">= 0.4" } typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==, + } + engines: { node: ">= 0.4" } typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==, + } + engines: { node: ">= 0.4" } typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==, + } + engines: { node: ">= 0.4" } typescript-eslint@8.57.2: - resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + resolution: + { + integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==, + } + engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ">=4.8.4 <6.0.0" typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==, + } + engines: { node: ">=14.17" } hasBin: true unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==, + } + engines: { node: ">= 0.4" } update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + resolution: + { + integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, + } hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: ">= 4.21.0" uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + resolution: + { + integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, + } url-polyfill@1.1.14: - resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} + resolution: + { + integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==, + } vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + resolution: + { + integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==, + } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } hasBin: true vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} + resolution: + { + integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==, + } + engines: { node: ^20.19.0 || >=22.12.0 } hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" less: ^4.0.0 lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 - stylus: '>=0.54.8' + stylus: ">=0.54.8" sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: - '@types/node': + "@types/node": optional: true jiti: optional: true @@ -1945,27 +3209,30 @@ packages: optional: true vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + resolution: + { + integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==, + } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } hasBin: true peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 - happy-dom: '*' - jsdom: '*' + "@edge-runtime/vm": "*" + "@types/debug": ^4.1.12 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@vitest/browser": 3.2.4 + "@vitest/ui": 3.2.4 + happy-dom: "*" + jsdom: "*" peerDependenciesMeta: - '@edge-runtime/vm': + "@edge-runtime/vm": optional: true - '@types/debug': + "@types/debug": optional: true - '@types/node': + "@types/node": optional: true - '@vitest/browser': + "@vitest/browser": optional: true - '@vitest/ui': + "@vitest/ui": optional: true happy-dom: optional: true @@ -1973,62 +3240,101 @@ packages: optional: true w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==, + } + engines: { node: ">=18" } webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==, + } + engines: { node: ">=12" } whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==, + } + engines: { node: ">=18" } deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==, + } + engines: { node: ">=18" } whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==, + } + engines: { node: ">=18" } which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==, + } + engines: { node: ">= 0.4" } which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==, + } + engines: { node: ">= 0.4" } which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==, + } + engines: { node: ">= 0.4" } which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==, + } + engines: { node: ">= 0.4" } which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } hasBin: true why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==, + } + engines: { node: ">=8" } hasBin: true word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, + } + engines: { node: ">=0.10.0" } ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} + resolution: + { + integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==, + } + engines: { node: ">=10.0.0" } peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true @@ -2036,51 +3342,62 @@ packages: optional: true xml-name-validator@5.0.0: - resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==, + } + engines: { node: ">=18" } xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + resolution: + { + integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==, + } yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, + } + engines: { node: ">=10" } snapshots: + "@adobe/css-tools@4.4.4": {} - '@adobe/css-tools@4.4.4': {} - - '@asamuzakjp/css-color@3.2.0': + "@asamuzakjp/css-color@3.2.0": dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + "@csstools/css-calc": 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + "@csstools/css-color-parser": 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + "@csstools/css-parser-algorithms": 3.0.5(@csstools/css-tokenizer@3.0.4) + "@csstools/css-tokenizer": 3.0.4 lru-cache: 10.4.3 - '@babel/code-frame@7.29.0': + "@babel/code-frame@7.29.0": dependencies: - '@babel/helper-validator-identifier': 7.28.5 + "@babel/helper-validator-identifier": 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + "@babel/compat-data@7.29.0": {} - '@babel/core@7.29.0': + "@babel/core@7.29.0": dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 + "@babel/code-frame": 7.29.0 + "@babel/generator": 7.29.1 + "@babel/helper-compilation-targets": 7.28.6 + "@babel/helper-module-transforms": 7.28.6(@babel/core@7.29.0) + "@babel/helpers": 7.29.2 + "@babel/parser": 7.29.2 + "@babel/template": 7.28.6 + "@babel/traverse": 7.29.0 + "@babel/types": 7.29.0 + "@jridgewell/remapping": 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -2089,214 +3406,214 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': + "@babel/generator@7.29.1": dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@babel/parser": 7.29.2 + "@babel/types": 7.29.0 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.28.6': + "@babel/helper-compilation-targets@7.28.6": dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 + "@babel/compat-data": 7.29.0 + "@babel/helper-validator-option": 7.27.1 browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + "@babel/helper-globals@7.28.0": {} - '@babel/helper-module-imports@7.28.6': + "@babel/helper-module-imports@7.28.6": dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + "@babel/traverse": 7.29.0 + "@babel/types": 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + "@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)": dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + "@babel/core": 7.29.0 + "@babel/helper-module-imports": 7.28.6 + "@babel/helper-validator-identifier": 7.28.5 + "@babel/traverse": 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.28.6': {} + "@babel/helper-plugin-utils@7.28.6": {} - '@babel/helper-string-parser@7.27.1': {} + "@babel/helper-string-parser@7.27.1": {} - '@babel/helper-validator-identifier@7.28.5': {} + "@babel/helper-validator-identifier@7.28.5": {} - '@babel/helper-validator-option@7.27.1': {} + "@babel/helper-validator-option@7.27.1": {} - '@babel/helpers@7.29.2': + "@babel/helpers@7.29.2": dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + "@babel/template": 7.28.6 + "@babel/types": 7.29.0 - '@babel/parser@7.29.2': + "@babel/parser@7.29.2": dependencies: - '@babel/types': 7.29.0 + "@babel/types": 7.29.0 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + "@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)": dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + "@babel/core": 7.29.0 + "@babel/helper-plugin-utils": 7.28.6 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + "@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)": dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + "@babel/core": 7.29.0 + "@babel/helper-plugin-utils": 7.28.6 - '@babel/runtime@7.29.2': {} + "@babel/runtime@7.29.2": {} - '@babel/template@7.28.6': + "@babel/template@7.28.6": dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + "@babel/code-frame": 7.29.0 + "@babel/parser": 7.29.2 + "@babel/types": 7.29.0 - '@babel/traverse@7.29.0': + "@babel/traverse@7.29.0": dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + "@babel/code-frame": 7.29.0 + "@babel/generator": 7.29.1 + "@babel/helper-globals": 7.28.0 + "@babel/parser": 7.29.2 + "@babel/template": 7.28.6 + "@babel/types": 7.29.0 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + "@babel/types@7.29.0": dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + "@babel/helper-string-parser": 7.27.1 + "@babel/helper-validator-identifier": 7.28.5 - '@csstools/color-helpers@5.1.0': {} + "@csstools/color-helpers@5.1.0": {} - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + "@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)": dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + "@csstools/css-parser-algorithms": 3.0.5(@csstools/css-tokenizer@3.0.4) + "@csstools/css-tokenizer": 3.0.4 - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + "@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)": dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + "@csstools/color-helpers": 5.1.0 + "@csstools/css-calc": 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + "@csstools/css-parser-algorithms": 3.0.5(@csstools/css-tokenizer@3.0.4) + "@csstools/css-tokenizer": 3.0.4 - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + "@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)": dependencies: - '@csstools/css-tokenizer': 3.0.4 + "@csstools/css-tokenizer": 3.0.4 - '@csstools/css-tokenizer@3.0.4': {} + "@csstools/css-tokenizer@3.0.4": {} - '@esbuild/aix-ppc64@0.27.4': + "@esbuild/aix-ppc64@0.27.4": optional: true - '@esbuild/android-arm64@0.27.4': + "@esbuild/android-arm64@0.27.4": optional: true - '@esbuild/android-arm@0.27.4': + "@esbuild/android-arm@0.27.4": optional: true - '@esbuild/android-x64@0.27.4': + "@esbuild/android-x64@0.27.4": optional: true - '@esbuild/darwin-arm64@0.27.4': + "@esbuild/darwin-arm64@0.27.4": optional: true - '@esbuild/darwin-x64@0.27.4': + "@esbuild/darwin-x64@0.27.4": optional: true - '@esbuild/freebsd-arm64@0.27.4': + "@esbuild/freebsd-arm64@0.27.4": optional: true - '@esbuild/freebsd-x64@0.27.4': + "@esbuild/freebsd-x64@0.27.4": optional: true - '@esbuild/linux-arm64@0.27.4': + "@esbuild/linux-arm64@0.27.4": optional: true - '@esbuild/linux-arm@0.27.4': + "@esbuild/linux-arm@0.27.4": optional: true - '@esbuild/linux-ia32@0.27.4': + "@esbuild/linux-ia32@0.27.4": optional: true - '@esbuild/linux-loong64@0.27.4': + "@esbuild/linux-loong64@0.27.4": optional: true - '@esbuild/linux-mips64el@0.27.4': + "@esbuild/linux-mips64el@0.27.4": optional: true - '@esbuild/linux-ppc64@0.27.4': + "@esbuild/linux-ppc64@0.27.4": optional: true - '@esbuild/linux-riscv64@0.27.4': + "@esbuild/linux-riscv64@0.27.4": optional: true - '@esbuild/linux-s390x@0.27.4': + "@esbuild/linux-s390x@0.27.4": optional: true - '@esbuild/linux-x64@0.27.4': + "@esbuild/linux-x64@0.27.4": optional: true - '@esbuild/netbsd-arm64@0.27.4': + "@esbuild/netbsd-arm64@0.27.4": optional: true - '@esbuild/netbsd-x64@0.27.4': + "@esbuild/netbsd-x64@0.27.4": optional: true - '@esbuild/openbsd-arm64@0.27.4': + "@esbuild/openbsd-arm64@0.27.4": optional: true - '@esbuild/openbsd-x64@0.27.4': + "@esbuild/openbsd-x64@0.27.4": optional: true - '@esbuild/openharmony-arm64@0.27.4': + "@esbuild/openharmony-arm64@0.27.4": optional: true - '@esbuild/sunos-x64@0.27.4': + "@esbuild/sunos-x64@0.27.4": optional: true - '@esbuild/win32-arm64@0.27.4': + "@esbuild/win32-arm64@0.27.4": optional: true - '@esbuild/win32-ia32@0.27.4': + "@esbuild/win32-ia32@0.27.4": optional: true - '@esbuild/win32-x64@0.27.4': + "@esbuild/win32-x64@0.27.4": optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + "@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)": dependencies: eslint: 9.39.4 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.2': {} + "@eslint-community/regexpp@4.12.2": {} - '@eslint/config-array@0.21.2': + "@eslint/config-array@0.21.2": dependencies: - '@eslint/object-schema': 2.1.7 + "@eslint/object-schema": 2.1.7 debug: 4.4.3 minimatch: 3.1.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + "@eslint/config-helpers@0.4.2": dependencies: - '@eslint/core': 0.17.0 + "@eslint/core": 0.17.0 - '@eslint/core@0.17.0': + "@eslint/core@0.17.0": dependencies: - '@types/json-schema': 7.0.15 + "@types/json-schema": 7.0.15 - '@eslint/eslintrc@3.3.5': + "@eslint/eslintrc@3.3.5": dependencies: ajv: 6.14.0 debug: 4.4.3 @@ -2310,209 +3627,209 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.4': {} + "@eslint/js@9.39.4": {} - '@eslint/object-schema@2.1.7': {} + "@eslint/object-schema@2.1.7": {} - '@eslint/plugin-kit@0.4.1': + "@eslint/plugin-kit@0.4.1": dependencies: - '@eslint/core': 0.17.0 + "@eslint/core": 0.17.0 levn: 0.4.1 - '@humanfs/core@0.19.1': {} + "@humanfs/core@0.19.1": {} - '@humanfs/node@0.16.7': + "@humanfs/node@0.16.7": dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 + "@humanfs/core": 0.19.1 + "@humanwhocodes/retry": 0.4.3 - '@humanwhocodes/module-importer@1.0.1': {} + "@humanwhocodes/module-importer@1.0.1": {} - '@humanwhocodes/retry@0.4.3': {} + "@humanwhocodes/retry@0.4.3": {} - '@jridgewell/gen-mapping@0.3.13': + "@jridgewell/gen-mapping@0.3.13": dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/sourcemap-codec": 1.5.5 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/remapping@2.3.5': + "@jridgewell/remapping@2.3.5": dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} + "@jridgewell/resolve-uri@3.1.2": {} - '@jridgewell/sourcemap-codec@1.5.5': {} + "@jridgewell/sourcemap-codec@1.5.5": {} - '@jridgewell/trace-mapping@0.3.31': + "@jridgewell/trace-mapping@0.3.31": dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/resolve-uri": 3.1.2 + "@jridgewell/sourcemap-codec": 1.5.5 - '@rolldown/pluginutils@1.0.0-rc.3': {} + "@rolldown/pluginutils@1.0.0-rc.3": {} - '@rollup/rollup-android-arm-eabi@4.60.0': + "@rollup/rollup-android-arm-eabi@4.60.0": optional: true - '@rollup/rollup-android-arm64@4.60.0': + "@rollup/rollup-android-arm64@4.60.0": optional: true - '@rollup/rollup-darwin-arm64@4.60.0': + "@rollup/rollup-darwin-arm64@4.60.0": optional: true - '@rollup/rollup-darwin-x64@4.60.0': + "@rollup/rollup-darwin-x64@4.60.0": optional: true - '@rollup/rollup-freebsd-arm64@4.60.0': + "@rollup/rollup-freebsd-arm64@4.60.0": optional: true - '@rollup/rollup-freebsd-x64@4.60.0': + "@rollup/rollup-freebsd-x64@4.60.0": optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + "@rollup/rollup-linux-arm-gnueabihf@4.60.0": optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.0': + "@rollup/rollup-linux-arm-musleabihf@4.60.0": optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.0': + "@rollup/rollup-linux-arm64-gnu@4.60.0": optional: true - '@rollup/rollup-linux-arm64-musl@4.60.0': + "@rollup/rollup-linux-arm64-musl@4.60.0": optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.0': + "@rollup/rollup-linux-loong64-gnu@4.60.0": optional: true - '@rollup/rollup-linux-loong64-musl@4.60.0': + "@rollup/rollup-linux-loong64-musl@4.60.0": optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.0': + "@rollup/rollup-linux-ppc64-gnu@4.60.0": optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.0': + "@rollup/rollup-linux-ppc64-musl@4.60.0": optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.0': + "@rollup/rollup-linux-riscv64-gnu@4.60.0": optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.0': + "@rollup/rollup-linux-riscv64-musl@4.60.0": optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.0': + "@rollup/rollup-linux-s390x-gnu@4.60.0": optional: true - '@rollup/rollup-linux-x64-gnu@4.60.0': + "@rollup/rollup-linux-x64-gnu@4.60.0": optional: true - '@rollup/rollup-linux-x64-musl@4.60.0': + "@rollup/rollup-linux-x64-musl@4.60.0": optional: true - '@rollup/rollup-openbsd-x64@4.60.0': + "@rollup/rollup-openbsd-x64@4.60.0": optional: true - '@rollup/rollup-openharmony-arm64@4.60.0': + "@rollup/rollup-openharmony-arm64@4.60.0": optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.0': + "@rollup/rollup-win32-arm64-msvc@4.60.0": optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.0': + "@rollup/rollup-win32-ia32-msvc@4.60.0": optional: true - '@rollup/rollup-win32-x64-gnu@4.60.0': + "@rollup/rollup-win32-x64-gnu@4.60.0": optional: true - '@rollup/rollup-win32-x64-msvc@4.60.0': + "@rollup/rollup-win32-x64-msvc@4.60.0": optional: true - '@testing-library/dom@10.4.1': + "@testing-library/dom@10.4.1": dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.29.2 - '@types/aria-query': 5.0.4 + "@babel/code-frame": 7.29.0 + "@babel/runtime": 7.29.2 + "@types/aria-query": 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': + "@testing-library/jest-dom@6.9.1": dependencies: - '@adobe/css-tools': 4.4.4 + "@adobe/css-tools": 4.4.4 aria-query: 5.3.2 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + "@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: - '@babel/runtime': 7.29.2 - '@testing-library/dom': 10.4.1 + "@babel/runtime": 7.29.2 + "@testing-library/dom": 10.4.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.28 - '@types/react-dom': 18.3.7(@types/react@18.3.28) + "@types/react": 18.3.28 + "@types/react-dom": 18.3.7(@types/react@18.3.28) - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + "@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)": dependencies: - '@testing-library/dom': 10.4.1 + "@testing-library/dom": 10.4.1 - '@types/aria-query@5.0.4': {} + "@types/aria-query@5.0.4": {} - '@types/babel__core@7.20.5': + "@types/babel__core@7.20.5": dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + "@babel/parser": 7.29.2 + "@babel/types": 7.29.0 + "@types/babel__generator": 7.27.0 + "@types/babel__template": 7.4.4 + "@types/babel__traverse": 7.28.0 - '@types/babel__generator@7.27.0': + "@types/babel__generator@7.27.0": dependencies: - '@babel/types': 7.29.0 + "@babel/types": 7.29.0 - '@types/babel__template@7.4.4': + "@types/babel__template@7.4.4": dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + "@babel/parser": 7.29.2 + "@babel/types": 7.29.0 - '@types/babel__traverse@7.28.0': + "@types/babel__traverse@7.28.0": dependencies: - '@babel/types': 7.29.0 + "@babel/types": 7.29.0 - '@types/chai@5.2.3': + "@types/chai@5.2.3": dependencies: - '@types/deep-eql': 4.0.2 + "@types/deep-eql": 4.0.2 assertion-error: 2.0.1 - '@types/deep-eql@4.0.2': {} + "@types/deep-eql@4.0.2": {} - '@types/estree@1.0.8': {} + "@types/estree@1.0.8": {} - '@types/json-schema@7.0.15': {} + "@types/json-schema@7.0.15": {} - '@types/prop-types@15.7.15': {} + "@types/prop-types@15.7.15": {} - '@types/react-dom@18.3.7(@types/react@18.3.28)': + "@types/react-dom@18.3.7(@types/react@18.3.28)": dependencies: - '@types/react': 18.3.28 + "@types/react": 18.3.28 - '@types/react@18.3.28': + "@types/react@18.3.28": dependencies: - '@types/prop-types': 15.7.15 + "@types/prop-types": 15.7.15 csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.6.3))(eslint@9.39.4)(typescript@5.6.3)': + "@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.6.3))(eslint@9.39.4)(typescript@5.6.3)": dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/type-utils': 8.57.2(eslint@9.39.4)(typescript@5.6.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.57.2 + "@eslint-community/regexpp": 4.12.2 + "@typescript-eslint/parser": 8.57.2(eslint@9.39.4)(typescript@5.6.3) + "@typescript-eslint/scope-manager": 8.57.2 + "@typescript-eslint/type-utils": 8.57.2(eslint@9.39.4)(typescript@5.6.3) + "@typescript-eslint/utils": 8.57.2(eslint@9.39.4)(typescript@5.6.3) + "@typescript-eslint/visitor-keys": 8.57.2 eslint: 9.39.4 ignore: 7.0.5 natural-compare: 1.4.0 @@ -2521,41 +3838,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.6.3)': + "@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.6.3)": dependencies: - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.57.2 + "@typescript-eslint/scope-manager": 8.57.2 + "@typescript-eslint/types": 8.57.2 + "@typescript-eslint/typescript-estree": 8.57.2(typescript@5.6.3) + "@typescript-eslint/visitor-keys": 8.57.2 debug: 4.4.3 eslint: 9.39.4 typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.2(typescript@5.6.3)': + "@typescript-eslint/project-service@8.57.2(typescript@5.6.3)": dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.6.3) - '@typescript-eslint/types': 8.57.2 + "@typescript-eslint/tsconfig-utils": 8.57.2(typescript@5.6.3) + "@typescript-eslint/types": 8.57.2 debug: 4.4.3 typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.57.2': + "@typescript-eslint/scope-manager@8.57.2": dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/visitor-keys': 8.57.2 + "@typescript-eslint/types": 8.57.2 + "@typescript-eslint/visitor-keys": 8.57.2 - '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.6.3)': + "@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.6.3)": dependencies: typescript: 5.6.3 - '@typescript-eslint/type-utils@8.57.2(eslint@9.39.4)(typescript@5.6.3)': + "@typescript-eslint/type-utils@8.57.2(eslint@9.39.4)(typescript@5.6.3)": dependencies: - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.6.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.6.3) + "@typescript-eslint/types": 8.57.2 + "@typescript-eslint/typescript-estree": 8.57.2(typescript@5.6.3) + "@typescript-eslint/utils": 8.57.2(eslint@9.39.4)(typescript@5.6.3) debug: 4.4.3 eslint: 9.39.4 ts-api-utils: 2.5.0(typescript@5.6.3) @@ -2563,14 +3880,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.57.2': {} + "@typescript-eslint/types@8.57.2": {} - '@typescript-eslint/typescript-estree@8.57.2(typescript@5.6.3)': + "@typescript-eslint/typescript-estree@8.57.2(typescript@5.6.3)": dependencies: - '@typescript-eslint/project-service': 8.57.2(typescript@5.6.3) - '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.6.3) - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/visitor-keys': 8.57.2 + "@typescript-eslint/project-service": 8.57.2(typescript@5.6.3) + "@typescript-eslint/tsconfig-utils": 8.57.2(typescript@5.6.3) + "@typescript-eslint/types": 8.57.2 + "@typescript-eslint/visitor-keys": 8.57.2 debug: 4.4.3 minimatch: 10.2.4 semver: 7.7.4 @@ -2580,77 +3897,77 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.2(eslint@9.39.4)(typescript@5.6.3)': + "@typescript-eslint/utils@8.57.2(eslint@9.39.4)(typescript@5.6.3)": dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) - '@typescript-eslint/scope-manager': 8.57.2 - '@typescript-eslint/types': 8.57.2 - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.6.3) + "@eslint-community/eslint-utils": 4.9.1(eslint@9.39.4) + "@typescript-eslint/scope-manager": 8.57.2 + "@typescript-eslint/types": 8.57.2 + "@typescript-eslint/typescript-estree": 8.57.2(typescript@5.6.3) eslint: 9.39.4 typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.57.2': + "@typescript-eslint/visitor-keys@8.57.2": dependencies: - '@typescript-eslint/types': 8.57.2 + "@typescript-eslint/types": 8.57.2 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-basic-ssl@2.3.0(vite@7.3.1)': + "@vitejs/plugin-basic-ssl@2.3.0(vite@7.3.1)": dependencies: vite: 7.3.1 - '@vitejs/plugin-react@5.2.0(vite@7.3.1)': + "@vitejs/plugin-react@5.2.0(vite@7.3.1)": dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.3 - '@types/babel__core': 7.20.5 + "@babel/core": 7.29.0 + "@babel/plugin-transform-react-jsx-self": 7.27.1(@babel/core@7.29.0) + "@babel/plugin-transform-react-jsx-source": 7.27.1(@babel/core@7.29.0) + "@rolldown/pluginutils": 1.0.0-rc.3 + "@types/babel__core": 7.20.5 react-refresh: 0.18.0 vite: 7.3.1 transitivePeerDependencies: - supports-color - '@vitest/expect@3.2.4': + "@vitest/expect@3.2.4": dependencies: - '@types/chai': 5.2.3 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 + "@types/chai": 5.2.3 + "@vitest/spy": 3.2.4 + "@vitest/utils": 3.2.4 chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.1)': + "@vitest/mocker@3.2.4(vite@7.3.1)": dependencies: - '@vitest/spy': 3.2.4 + "@vitest/spy": 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.1 - '@vitest/pretty-format@3.2.4': + "@vitest/pretty-format@3.2.4": dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.2.4': + "@vitest/runner@3.2.4": dependencies: - '@vitest/utils': 3.2.4 + "@vitest/utils": 3.2.4 pathe: 2.0.3 strip-literal: 3.1.0 - '@vitest/snapshot@3.2.4': + "@vitest/snapshot@3.2.4": dependencies: - '@vitest/pretty-format': 3.2.4 + "@vitest/pretty-format": 3.2.4 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@3.2.4': + "@vitest/spy@3.2.4": dependencies: tinyspy: 4.0.4 - '@vitest/utils@3.2.4': + "@vitest/utils@3.2.4": dependencies: - '@vitest/pretty-format': 3.2.4 + "@vitest/pretty-format": 3.2.4 loupe: 3.2.1 tinyrainbow: 2.0.0 @@ -2833,7 +4150,7 @@ snapshots: cssstyle@4.6.0: dependencies: - '@asamuzakjp/css-color': 3.2.0 + "@asamuzakjp/css-color": 3.2.0 rrweb-cssom: 0.8.0 csstype@3.2.3: {} @@ -3009,32 +4326,32 @@ snapshots: esbuild@0.27.4: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 + "@esbuild/aix-ppc64": 0.27.4 + "@esbuild/android-arm": 0.27.4 + "@esbuild/android-arm64": 0.27.4 + "@esbuild/android-x64": 0.27.4 + "@esbuild/darwin-arm64": 0.27.4 + "@esbuild/darwin-x64": 0.27.4 + "@esbuild/freebsd-arm64": 0.27.4 + "@esbuild/freebsd-x64": 0.27.4 + "@esbuild/linux-arm": 0.27.4 + "@esbuild/linux-arm64": 0.27.4 + "@esbuild/linux-ia32": 0.27.4 + "@esbuild/linux-loong64": 0.27.4 + "@esbuild/linux-mips64el": 0.27.4 + "@esbuild/linux-ppc64": 0.27.4 + "@esbuild/linux-riscv64": 0.27.4 + "@esbuild/linux-s390x": 0.27.4 + "@esbuild/linux-x64": 0.27.4 + "@esbuild/netbsd-arm64": 0.27.4 + "@esbuild/netbsd-x64": 0.27.4 + "@esbuild/openbsd-arm64": 0.27.4 + "@esbuild/openbsd-x64": 0.27.4 + "@esbuild/openharmony-arm64": 0.27.4 + "@esbuild/sunos-x64": 0.27.4 + "@esbuild/win32-arm64": 0.27.4 + "@esbuild/win32-ia32": 0.27.4 + "@esbuild/win32-x64": 0.27.4 escalade@3.2.0: {} @@ -3083,18 +4400,18 @@ snapshots: eslint@9.39.4: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + "@eslint-community/eslint-utils": 4.9.1(eslint@9.39.4) + "@eslint-community/regexpp": 4.12.2 + "@eslint/config-array": 0.21.2 + "@eslint/config-helpers": 0.4.2 + "@eslint/core": 0.17.0 + "@eslint/eslintrc": 3.3.5 + "@eslint/js": 9.39.4 + "@eslint/plugin-kit": 0.4.1 + "@humanfs/node": 0.16.7 + "@humanwhocodes/module-importer": 1.0.1 + "@humanwhocodes/retry": 0.4.3 + "@types/estree": 1.0.8 ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 @@ -3138,7 +4455,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + "@types/estree": 1.0.8 esutils@2.0.3: {} @@ -3504,7 +4821,7 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + "@jridgewell/sourcemap-codec": 1.5.5 math-intrinsics@1.1.0: {} @@ -3707,33 +5024,33 @@ snapshots: rollup@4.60.0: dependencies: - '@types/estree': 1.0.8 + "@types/estree": 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.0 - '@rollup/rollup-android-arm64': 4.60.0 - '@rollup/rollup-darwin-arm64': 4.60.0 - '@rollup/rollup-darwin-x64': 4.60.0 - '@rollup/rollup-freebsd-arm64': 4.60.0 - '@rollup/rollup-freebsd-x64': 4.60.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 - '@rollup/rollup-linux-arm-musleabihf': 4.60.0 - '@rollup/rollup-linux-arm64-gnu': 4.60.0 - '@rollup/rollup-linux-arm64-musl': 4.60.0 - '@rollup/rollup-linux-loong64-gnu': 4.60.0 - '@rollup/rollup-linux-loong64-musl': 4.60.0 - '@rollup/rollup-linux-ppc64-gnu': 4.60.0 - '@rollup/rollup-linux-ppc64-musl': 4.60.0 - '@rollup/rollup-linux-riscv64-gnu': 4.60.0 - '@rollup/rollup-linux-riscv64-musl': 4.60.0 - '@rollup/rollup-linux-s390x-gnu': 4.60.0 - '@rollup/rollup-linux-x64-gnu': 4.60.0 - '@rollup/rollup-linux-x64-musl': 4.60.0 - '@rollup/rollup-openbsd-x64': 4.60.0 - '@rollup/rollup-openharmony-arm64': 4.60.0 - '@rollup/rollup-win32-arm64-msvc': 4.60.0 - '@rollup/rollup-win32-ia32-msvc': 4.60.0 - '@rollup/rollup-win32-x64-gnu': 4.60.0 - '@rollup/rollup-win32-x64-msvc': 4.60.0 + "@rollup/rollup-android-arm-eabi": 4.60.0 + "@rollup/rollup-android-arm64": 4.60.0 + "@rollup/rollup-darwin-arm64": 4.60.0 + "@rollup/rollup-darwin-x64": 4.60.0 + "@rollup/rollup-freebsd-arm64": 4.60.0 + "@rollup/rollup-freebsd-x64": 4.60.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.60.0 + "@rollup/rollup-linux-arm-musleabihf": 4.60.0 + "@rollup/rollup-linux-arm64-gnu": 4.60.0 + "@rollup/rollup-linux-arm64-musl": 4.60.0 + "@rollup/rollup-linux-loong64-gnu": 4.60.0 + "@rollup/rollup-linux-loong64-musl": 4.60.0 + "@rollup/rollup-linux-ppc64-gnu": 4.60.0 + "@rollup/rollup-linux-ppc64-musl": 4.60.0 + "@rollup/rollup-linux-riscv64-gnu": 4.60.0 + "@rollup/rollup-linux-riscv64-musl": 4.60.0 + "@rollup/rollup-linux-s390x-gnu": 4.60.0 + "@rollup/rollup-linux-x64-gnu": 4.60.0 + "@rollup/rollup-linux-x64-musl": 4.60.0 + "@rollup/rollup-openbsd-x64": 4.60.0 + "@rollup/rollup-openharmony-arm64": 4.60.0 + "@rollup/rollup-win32-arm64-msvc": 4.60.0 + "@rollup/rollup-win32-ia32-msvc": 4.60.0 + "@rollup/rollup-win32-x64-gnu": 4.60.0 + "@rollup/rollup-win32-x64-msvc": 4.60.0 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -3991,10 +5308,10 @@ snapshots: typescript-eslint@8.57.2(eslint@9.39.4)(typescript@5.6.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.6.3))(eslint@9.39.4)(typescript@5.6.3) - '@typescript-eslint/parser': 8.57.2(eslint@9.39.4)(typescript@5.6.3) - '@typescript-eslint/typescript-estree': 8.57.2(typescript@5.6.3) - '@typescript-eslint/utils': 8.57.2(eslint@9.39.4)(typescript@5.6.3) + "@typescript-eslint/eslint-plugin": 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4)(typescript@5.6.3))(eslint@9.39.4)(typescript@5.6.3) + "@typescript-eslint/parser": 8.57.2(eslint@9.39.4)(typescript@5.6.3) + "@typescript-eslint/typescript-estree": 8.57.2(typescript@5.6.3) + "@typescript-eslint/utils": 8.57.2(eslint@9.39.4)(typescript@5.6.3) eslint: 9.39.4 typescript: 5.6.3 transitivePeerDependencies: @@ -4029,7 +5346,7 @@ snapshots: pathe: 2.0.3 vite: 7.3.1 transitivePeerDependencies: - - '@types/node' + - "@types/node" - jiti - less - lightningcss @@ -4055,14 +5372,14 @@ snapshots: vitest@3.2.4(jsdom@26.1.0): dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 + "@types/chai": 5.2.3 + "@vitest/expect": 3.2.4 + "@vitest/mocker": 3.2.4(vite@7.3.1) + "@vitest/pretty-format": 3.2.4 + "@vitest/runner": 3.2.4 + "@vitest/snapshot": 3.2.4 + "@vitest/spy": 3.2.4 + "@vitest/utils": 3.2.4 chai: 5.3.3 debug: 4.4.3 expect-type: 1.3.0 diff --git a/spacetime.json b/spacetime.json index e5af965..4da2d32 100644 --- a/spacetime.json +++ b/spacetime.json @@ -5,4 +5,4 @@ "server": "maincloud", "database": "my-spacetime-app-jdhdg", "module-path": "./spacetimedb" -} \ No newline at end of file +} diff --git a/spacetime.local.json b/spacetime.local.json index c174128..0f8c635 100644 --- a/spacetime.local.json +++ b/spacetime.local.json @@ -1,3 +1,3 @@ { "database": "my-spacetime-app-jdhdg" -} \ No newline at end of file +} diff --git a/spacetimedb/package.json b/spacetimedb/package.json index 3adcce6..e10b776 100644 --- a/spacetimedb/package.json +++ b/spacetimedb/package.json @@ -13,4 +13,4 @@ "devDependencies": { "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/spacetimedb/pnpm-lock.yaml b/spacetimedb/pnpm-lock.yaml index ce33884..65a0dfe 100644 --- a/spacetimedb/pnpm-lock.yaml +++ b/spacetimedb/pnpm-lock.yaml @@ -1,11 +1,10 @@ -lockfileVersion: '9.0' +lockfileVersion: "9.0" settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: - .: dependencies: spacetimedb: @@ -17,42 +16,62 @@ importers: version: 5.9.3 packages: - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + resolution: + { + integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, + } headers-polyfill@4.0.3: - resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + resolution: + { + integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==, + } object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, + } + engines: { node: ">= 0.4" } prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==, + } + engines: { node: ">=14" } hasBin: true pure-rand@7.0.1: - resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + resolution: + { + integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==, + } safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==, + } + engines: { node: ">=10" } spacetimedb@2.1.0: - resolution: {integrity: sha512-Kzs+HXCRj15ryld03ztU4a2uQg0M8ivV/9Bk/gvMpb59lLc/A2/r7UkGCYBePsBL7Zwqgr8gE8FeufoZVXtPnA==} + resolution: + { + integrity: sha512-Kzs+HXCRj15ryld03ztU4a2uQg0M8ivV/9Bk/gvMpb59lLc/A2/r7UkGCYBePsBL7Zwqgr8gE8FeufoZVXtPnA==, + } peerDependencies: - '@angular/core': '>=17.0.0' - '@tanstack/react-query': ^5.0.0 + "@angular/core": ">=17.0.0" + "@tanstack/react-query": ^5.0.0 react: ^18.0.0 || ^19.0.0-0 || ^19.0.0 svelte: ^4.0.0 || ^5.0.0 undici: ^6.19.2 vue: ^3.3.0 peerDependenciesMeta: - '@angular/core': + "@angular/core": optional: true - '@tanstack/react-query': + "@tanstack/react-query": optional: true react: optional: true @@ -64,19 +83,27 @@ packages: optional: true statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, + } + engines: { node: ">= 0.8" } typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, + } + engines: { node: ">=14.17" } hasBin: true url-polyfill@1.1.14: - resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} + resolution: + { + integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==, + } snapshots: - base64-js@1.5.1: {} headers-polyfill@4.0.3: {} diff --git a/spacetimedb/src/index.ts b/spacetimedb/src/index.ts index 3367763..3314611 100644 --- a/spacetimedb/src/index.ts +++ b/spacetimedb/src/index.ts @@ -1,10 +1,10 @@ -import { schema, t, table, SenderError } from 'spacetimedb/server'; +import { schema, t, table, SenderError } from "spacetimedb/server"; -const channel_kind = t.enum('ChannelKind', { Text: t.unit(), Voice: t.unit() }); +const channel_kind = t.enum("ChannelKind", { Text: t.unit(), Voice: t.unit() }); const user = table( { - name: 'user', + name: "user", public: true, }, { @@ -16,90 +16,95 @@ const user = table( subject: t.string().optional(), username: t.string().optional(), // For creds-based auth password: t.string().optional(), // For creds-based auth (Note: plain text for MVP) - } + }, ); const server = table( - { name: 'server', public: true }, + { name: "server", public: true }, { id: t.u64().primaryKey().autoInc(), name: t.string(), owner: t.identity().optional(), - } + }, ); const server_member = table( { - name: 'server_member', + name: "server_member", public: true, indexes: [ - { accessor: 'by_identity', algorithm: 'btree', columns: ['identity'] }, - { accessor: 'by_server_id', algorithm: 'btree', columns: ['server_id'] } - ] + { accessor: "by_identity", algorithm: "btree", columns: ["identity"] }, + { accessor: "by_server_id", algorithm: "btree", columns: ["server_id"] }, + ], }, { id: t.u64().primaryKey().autoInc(), identity: t.identity(), server_id: t.u64(), - } + }, ); const channel = table( { - name: 'channel', + name: "channel", public: true, indexes: [ - { accessor: 'by_server_id', algorithm: 'btree', columns: ['server_id'] } - ] + { accessor: "by_server_id", algorithm: "btree", columns: ["server_id"] }, + ], }, { id: t.u64().primaryKey().autoInc(), server_id: t.u64(), name: t.string(), kind: channel_kind, - } + }, ); const voice_state = table( { - name: 'voice_state', + name: "voice_state", public: true, indexes: [ - { accessor: 'by_channel_id', algorithm: 'btree', columns: ['channel_id'] } - ] + { + accessor: "by_channel_id", + algorithm: "btree", + columns: ["channel_id"], + }, + ], }, { identity: t.identity().primaryKey(), channel_id: t.u64(), is_sharing_screen: t.bool(), - } + }, ); const watching = table( { - name: 'watching', + name: "watching", public: true, indexes: [ - { accessor: 'by_watcher', algorithm: 'btree', columns: ['watcher'] }, - { accessor: 'by_watchee', algorithm: 'btree', columns: ['watchee'] } - ] + { accessor: "by_watcher", algorithm: "btree", columns: ["watcher"] }, + { accessor: "by_watchee", algorithm: "btree", columns: ["watchee"] }, + ], }, { id: t.u64().primaryKey().autoInc(), watcher: t.identity(), watchee: t.identity(), channel_id: t.u64(), - } + }, ); -const sdp_offer = table( +// --- Voice Signaling Tables --- +const voice_sdp_offer = table( { - name: 'sdp_offer', + name: "voice_sdp_offer", public: true, indexes: [ - { accessor: 'by_receiver', algorithm: 'btree', columns: ['receiver'] }, - { accessor: 'by_sender', algorithm: 'btree', columns: ['sender'] } - ] + { accessor: "by_receiver", algorithm: "btree", columns: ["receiver"] }, + { accessor: "by_sender", algorithm: "btree", columns: ["sender"] }, + ], }, { id: t.u64().primaryKey().autoInc(), @@ -107,17 +112,17 @@ const sdp_offer = table( receiver: t.identity(), sdp: t.string(), channel_id: t.u64(), - } + }, ); -const sdp_answer = table( +const voice_sdp_answer = table( { - name: 'sdp_answer', + name: "voice_sdp_answer", public: true, indexes: [ - { accessor: 'by_receiver', algorithm: 'btree', columns: ['receiver'] }, - { accessor: 'by_sender', algorithm: 'btree', columns: ['sender'] } - ] + { accessor: "by_receiver", algorithm: "btree", columns: ["receiver"] }, + { accessor: "by_sender", algorithm: "btree", columns: ["sender"] }, + ], }, { id: t.u64().primaryKey().autoInc(), @@ -125,17 +130,17 @@ const sdp_answer = table( receiver: t.identity(), sdp: t.string(), channel_id: t.u64(), - } + }, ); -const ice_candidate = table( +const voice_ice_candidate = table( { - name: 'ice_candidate', + name: "voice_ice_candidate", public: true, indexes: [ - { accessor: 'by_receiver', algorithm: 'btree', columns: ['receiver'] }, - { accessor: 'by_sender', algorithm: 'btree', columns: ['sender'] } - ] + { accessor: "by_receiver", algorithm: "btree", columns: ["receiver"] }, + { accessor: "by_sender", algorithm: "btree", columns: ["sender"] }, + ], }, { id: t.u64().primaryKey().autoInc(), @@ -143,33 +148,96 @@ const ice_candidate = table( receiver: t.identity(), candidate: t.string(), channel_id: t.u64(), - } + }, +); + +// --- Screen Signaling Tables --- +const screen_sdp_offer = table( + { + name: "screen_sdp_offer", + public: true, + indexes: [ + { accessor: "by_receiver", algorithm: "btree", columns: ["receiver"] }, + { accessor: "by_sender", algorithm: "btree", columns: ["sender"] }, + ], + }, + { + id: t.u64().primaryKey().autoInc(), + sender: t.identity(), + receiver: t.identity(), + sdp: t.string(), + channel_id: t.u64(), + }, +); + +const screen_sdp_answer = table( + { + name: "screen_sdp_answer", + public: true, + indexes: [ + { accessor: "by_receiver", algorithm: "btree", columns: ["receiver"] }, + { accessor: "by_sender", algorithm: "btree", columns: ["sender"] }, + ], + }, + { + id: t.u64().primaryKey().autoInc(), + sender: t.identity(), + receiver: t.identity(), + sdp: t.string(), + channel_id: t.u64(), + }, +); + +const screen_ice_candidate = table( + { + name: "screen_ice_candidate", + public: true, + indexes: [ + { accessor: "by_receiver", algorithm: "btree", columns: ["receiver"] }, + { accessor: "by_sender", algorithm: "btree", columns: ["sender"] }, + ], + }, + { + id: t.u64().primaryKey().autoInc(), + sender: t.identity(), + receiver: t.identity(), + candidate: t.string(), + channel_id: t.u64(), + }, ); const thread = table( { - name: 'thread', + name: "thread", public: true, indexes: [ - { accessor: 'by_channel_id', algorithm: 'btree', columns: ['channel_id'] } - ] + { + accessor: "by_channel_id", + algorithm: "btree", + columns: ["channel_id"], + }, + ], }, { id: t.u64().primaryKey().autoInc(), channel_id: t.u64(), parent_message_id: t.u64().unique(), name: t.string(), - } + }, ); const message = table( { - name: 'message', + name: "message", public: true, indexes: [ - { accessor: 'by_channel_id', algorithm: 'btree', columns: ['channel_id'] }, - { accessor: 'by_thread_id', algorithm: 'btree', columns: ['thread_id'] } - ] + { + accessor: "by_channel_id", + algorithm: "btree", + columns: ["channel_id"], + }, + { accessor: "by_thread_id", algorithm: "btree", columns: ["thread_id"] }, + ], }, { id: t.u64().primaryKey().autoInc(), @@ -178,14 +246,30 @@ const message = table( text: t.string(), channel_id: t.u64(), thread_id: t.u64().optional(), - } + }, ); -const spacetimedb = schema({ user, server, server_member, channel, voice_state, watching, sdp_offer, sdp_answer, ice_candidate, thread, message }); +const spacetimedb = schema({ + user, + server, + server_member, + channel, + voice_state, + watching, + voice_sdp_offer, + voice_sdp_answer, + voice_ice_candidate, + screen_sdp_offer, + screen_sdp_answer, + screen_ice_candidate, + thread, + message, +}); export default spacetimedb; function validateName(name: string) { - if (!name || name.trim().length === 0) throw new SenderError('Names must not be empty'); + if (!name || name.trim().length === 0) + throw new SenderError("Names must not be empty"); } export const set_name = spacetimedb.reducer( @@ -193,29 +277,31 @@ export const set_name = spacetimedb.reducer( (ctx, { name }) => { validateName(name); const user = ctx.db.user.identity.find(ctx.sender); - if (!user) throw new SenderError('Cannot set name for unknown user'); + if (!user) throw new SenderError("Cannot set name for unknown user"); ctx.db.user.identity.update({ ...user, name }); - } + }, ); export const set_talking = spacetimedb.reducer( { talking: t.bool() }, (ctx, { talking }) => { const user = ctx.db.user.identity.find(ctx.sender); - if (!user) throw new SenderError('Cannot set talking status for unknown user'); + if (!user) + throw new SenderError("Cannot set talking status for unknown user"); ctx.db.user.identity.update({ ...user, talking }); - } + }, ); export const register = spacetimedb.reducer( { username: t.string(), password: t.string() }, (ctx, { username, password }) => { validateName(username); - if (!password || password.length < 4) throw new SenderError('Password must be at least 4 characters'); + if (!password || password.length < 4) + throw new SenderError("Password must be at least 4 characters"); for (const u of ctx.db.user.iter()) { if (u.username === username) { - throw new SenderError('Username already taken'); + throw new SenderError("Username already taken"); } } @@ -225,7 +311,7 @@ export const register = spacetimedb.reducer( ...user, username, password, - name: user.name || username + name: user.name || username, }); } else { ctx.db.user.insert({ @@ -236,10 +322,10 @@ export const register = spacetimedb.reducer( online: true, talking: false, issuer: undefined, - subject: undefined + subject: undefined, }); } - } + }, ); export const login = spacetimedb.reducer( @@ -254,12 +340,16 @@ export const login = spacetimedb.reducer( } if (!foundUser) { - throw new SenderError('Invalid username or password'); + throw new SenderError("Invalid username or password"); } const currentIdentityUser = ctx.db.user.identity.find(ctx.sender); - if (currentIdentityUser && currentIdentityUser.identity.toHexString() !== foundUser.identity.toHexString()) { - ctx.db.user.identity.delete(ctx.sender); + if ( + currentIdentityUser && + currentIdentityUser.identity.toHexString() !== + foundUser.identity.toHexString() + ) { + ctx.db.user.identity.delete(ctx.sender); } if (foundUser.identity.toHexString() !== ctx.sender.toHexString()) { @@ -267,15 +357,15 @@ export const login = spacetimedb.reducer( ctx.db.user.insert({ ...foundUser, identity: ctx.sender, - online: true + online: true, }); } else { ctx.db.user.identity.update({ ...foundUser, - online: true + online: true, }); } - } + }, ); export const create_server = spacetimedb.reducer( @@ -284,13 +374,27 @@ export const create_server = spacetimedb.reducer( validateName(name); const user = ctx.db.user.identity.find(ctx.sender); if (!user || (!user.username && !user.subject)) { - throw new SenderError('You must be logged in to create a server'); + throw new SenderError("You must be logged in to create a server"); } const s = ctx.db.server.insert({ id: 0n, name, owner: ctx.sender }); - ctx.db.server_member.insert({ id: 0n, identity: ctx.sender, server_id: s.id }); - ctx.db.channel.insert({ id: 0n, server_id: s.id, name: 'general', kind: { tag: 'Text' } }); - ctx.db.channel.insert({ id: 0n, server_id: s.id, name: 'Voice General', kind: { tag: 'Voice' } }); - } + ctx.db.server_member.insert({ + id: 0n, + identity: ctx.sender, + server_id: s.id, + }); + ctx.db.channel.insert({ + id: 0n, + server_id: s.id, + name: "general", + kind: { tag: "Text" }, + }); + ctx.db.channel.insert({ + id: 0n, + server_id: s.id, + name: "Voice General", + kind: { tag: "Voice" }, + }); + }, ); export const join_server = spacetimedb.reducer( @@ -298,18 +402,22 @@ export const join_server = spacetimedb.reducer( (ctx, { serverId }) => { const user = ctx.db.user.identity.find(ctx.sender); if (!user || (!user.username && !user.subject)) { - throw new SenderError('You must be logged in to join a server'); + throw new SenderError("You must be logged in to join a server"); } const s = ctx.db.server.id.find(serverId); - if (!s) throw new SenderError('Server not found'); + if (!s) throw new SenderError("Server not found"); // Check if already a member for (const m of ctx.db.server_member.by_identity.filter(ctx.sender)) { if (m.server_id === serverId) return; } - ctx.db.server_member.insert({ id: 0n, identity: ctx.sender, server_id: serverId }); - } + ctx.db.server_member.insert({ + id: 0n, + identity: ctx.sender, + server_id: serverId, + }); + }, ); export const leave_server = spacetimedb.reducer( @@ -323,7 +431,7 @@ export const leave_server = spacetimedb.reducer( ctx.db.server_member.id.delete(m.id); } } - } + }, ); export const create_channel = spacetimedb.reducer( @@ -332,17 +440,17 @@ export const create_channel = spacetimedb.reducer( validateName(name); const user = ctx.db.user.identity.find(ctx.sender); if (!user || (!user.username && !user.subject)) { - throw new SenderError('You must be logged in to create a channel'); + throw new SenderError("You must be logged in to create a channel"); } const s = ctx.db.server.id.find(serverId); - if (!s) throw new SenderError('Server not found'); + if (!s) throw new SenderError("Server not found"); ctx.db.channel.insert({ id: 0n, server_id: serverId, name, - kind: isVoice ? { tag: 'Voice' } : { tag: 'Text' } + kind: isVoice ? { tag: "Voice" } : { tag: "Text" }, }); - } + }, ); export const join_voice = spacetimedb.reducer( @@ -350,21 +458,30 @@ export const join_voice = spacetimedb.reducer( (ctx, { channelId }) => { const user = ctx.db.user.identity.find(ctx.sender); if (!user || (!user.username && !user.subject)) { - throw new SenderError('You must be logged in to join voice'); + throw new SenderError("You must be logged in to join voice"); } const chan = ctx.db.channel.id.find(channelId); - if (!chan || chan.kind.tag !== 'Voice') throw new SenderError('Invalid voice channel'); + if (!chan || chan.kind.tag !== "Voice") + throw new SenderError("Invalid voice channel"); const existing = ctx.db.voice_state.identity.find(ctx.sender); if (existing) { if (existing.channel_id !== channelId) { clearSignalingForUser(ctx, ctx.sender); - ctx.db.voice_state.identity.update({ identity: ctx.sender, channel_id: channelId, is_sharing_screen: false }); + ctx.db.voice_state.identity.update({ + identity: ctx.sender, + channel_id: channelId, + is_sharing_screen: false, + }); } } else { - ctx.db.voice_state.insert({ identity: ctx.sender, channel_id: channelId, is_sharing_screen: false }); + ctx.db.voice_state.insert({ + identity: ctx.sender, + channel_id: channelId, + is_sharing_screen: false, + }); } - } + }, ); export const set_sharing_screen = spacetimedb.reducer( @@ -372,9 +489,12 @@ export const set_sharing_screen = spacetimedb.reducer( (ctx, { sharing }) => { const state = ctx.db.voice_state.identity.find(ctx.sender); if (state) { - ctx.db.voice_state.identity.update({ ...state, is_sharing_screen: sharing }); + ctx.db.voice_state.identity.update({ + ...state, + is_sharing_screen: sharing, + }); } - } + }, ); export const start_watching = spacetimedb.reducer( @@ -386,8 +506,13 @@ export const start_watching = spacetimedb.reducer( for (const w of ctx.db.watching.by_watcher.filter(ctx.sender)) { if (w.watchee.isEqual(watchee)) return; } - ctx.db.watching.insert({ id: 0n, watcher: ctx.sender, watchee, channel_id: channelId }); - } + ctx.db.watching.insert({ + id: 0n, + watcher: ctx.sender, + watchee, + channel_id: channelId, + }); + }, ); export const stop_watching = spacetimedb.reducer( @@ -398,7 +523,7 @@ export const stop_watching = spacetimedb.reducer( ctx.db.watching.id.delete(w.id); } } - } + }, ); export const leave_voice = spacetimedb.reducer((ctx) => { @@ -409,60 +534,149 @@ export const leave_voice = spacetimedb.reducer((ctx) => { clearSignalingForUser(ctx, ctx.sender); }); -export const send_sdp_offer = spacetimedb.reducer( +// --- Voice Signaling Reducers --- +export const send_voice_sdp_offer = spacetimedb.reducer( { receiver: t.identity(), sdp: t.string(), channelId: t.u64() }, (ctx, { receiver, sdp, channelId }) => { - // Clear any existing offers/answers/candidates between this pair in both directions - // to ensure a fresh negotiation state. - - // Outgoing from sender to receiver - for (const offer of ctx.db.sdp_offer.by_sender.filter(ctx.sender)) { - if (offer.receiver.isEqual(receiver)) ctx.db.sdp_offer.id.delete(offer.id); + for (const row of ctx.db.voice_sdp_offer.by_sender.filter(ctx.sender)) { + if (row.receiver.isEqual(receiver)) + ctx.db.voice_sdp_offer.id.delete(row.id); } - for (const answer of ctx.db.sdp_answer.by_sender.filter(ctx.sender)) { - if (answer.receiver.isEqual(receiver)) ctx.db.sdp_answer.id.delete(answer.id); + for (const row of ctx.db.voice_sdp_answer.by_sender.filter(ctx.sender)) { + if (row.receiver.isEqual(receiver)) + ctx.db.voice_sdp_answer.id.delete(row.id); } - for (const cand of ctx.db.ice_candidate.by_sender.filter(ctx.sender)) { - if (cand.receiver.isEqual(receiver)) ctx.db.ice_candidate.id.delete(cand.id); + for (const row of ctx.db.voice_ice_candidate.by_sender.filter(ctx.sender)) { + if (row.receiver.isEqual(receiver)) + ctx.db.voice_ice_candidate.id.delete(row.id); } - - // Incoming to sender from receiver (stale messages from previous negotiations) - for (const offer of ctx.db.sdp_offer.by_receiver.filter(ctx.sender)) { - if (offer.sender.isEqual(receiver)) ctx.db.sdp_offer.id.delete(offer.id); + for (const row of ctx.db.voice_sdp_offer.by_receiver.filter(ctx.sender)) { + if (row.sender.isEqual(receiver)) + ctx.db.voice_sdp_offer.id.delete(row.id); } - for (const answer of ctx.db.sdp_answer.by_receiver.filter(ctx.sender)) { - if (answer.sender.isEqual(receiver)) ctx.db.sdp_answer.id.delete(answer.id); + for (const row of ctx.db.voice_sdp_answer.by_receiver.filter(ctx.sender)) { + if (row.sender.isEqual(receiver)) + ctx.db.voice_sdp_answer.id.delete(row.id); } - for (const cand of ctx.db.ice_candidate.by_receiver.filter(ctx.sender)) { - if (cand.sender.isEqual(receiver)) ctx.db.ice_candidate.id.delete(cand.id); + for (const row of ctx.db.voice_ice_candidate.by_receiver.filter( + ctx.sender, + )) { + if (row.sender.isEqual(receiver)) + ctx.db.voice_ice_candidate.id.delete(row.id); } - - ctx.db.sdp_offer.insert({ id: 0n, sender: ctx.sender, receiver, sdp, channel_id: channelId }); - } + ctx.db.voice_sdp_offer.insert({ + id: 0n, + sender: ctx.sender, + receiver, + sdp, + channel_id: channelId, + }); + }, ); -export const send_sdp_answer = spacetimedb.reducer( +export const send_voice_sdp_answer = spacetimedb.reducer( { receiver: t.identity(), sdp: t.string(), channelId: t.u64() }, (ctx, { receiver, sdp, channelId }) => { - for (const answer of ctx.db.sdp_answer.by_sender.filter(ctx.sender)) { - if (answer.receiver.isEqual(receiver)) { - ctx.db.sdp_answer.id.delete(answer.id); - } + for (const row of ctx.db.voice_sdp_answer.by_sender.filter(ctx.sender)) { + if (row.receiver.isEqual(receiver)) + ctx.db.voice_sdp_answer.id.delete(row.id); } - ctx.db.sdp_answer.insert({ id: 0n, sender: ctx.sender, receiver, sdp, channel_id: channelId }); - } + ctx.db.voice_sdp_answer.insert({ + id: 0n, + sender: ctx.sender, + receiver, + sdp, + channel_id: channelId, + }); + }, ); -export const send_ice_candidate = spacetimedb.reducer( +export const send_voice_ice_candidate = spacetimedb.reducer( { receiver: t.identity(), candidate: t.string(), channelId: t.u64() }, (ctx, { receiver, candidate, channelId }) => { - ctx.db.ice_candidate.insert({ id: 0n, sender: ctx.sender, receiver, candidate, channel_id: channelId }); - } + ctx.db.voice_ice_candidate.insert({ + id: 0n, + sender: ctx.sender, + receiver, + candidate, + channel_id: channelId, + }); + }, +); + +// --- Screen Signaling Reducers --- +export const send_screen_sdp_offer = spacetimedb.reducer( + { receiver: t.identity(), sdp: t.string(), channelId: t.u64() }, + (ctx, { receiver, sdp, channelId }) => { + for (const row of ctx.db.screen_sdp_offer.by_sender.filter(ctx.sender)) { + if (row.receiver.isEqual(receiver)) + ctx.db.screen_sdp_offer.id.delete(row.id); + } + for (const row of ctx.db.screen_sdp_answer.by_sender.filter(ctx.sender)) { + if (row.receiver.isEqual(receiver)) + ctx.db.screen_sdp_answer.id.delete(row.id); + } + for (const row of ctx.db.screen_ice_candidate.by_sender.filter( + ctx.sender, + )) { + if (row.receiver.isEqual(receiver)) + ctx.db.screen_ice_candidate.id.delete(row.id); + } + for (const row of ctx.db.screen_sdp_offer.by_receiver.filter(ctx.sender)) { + if (row.sender.isEqual(receiver)) + ctx.db.screen_sdp_offer.id.delete(row.id); + } + for (const row of ctx.db.screen_sdp_answer.by_receiver.filter(ctx.sender)) { + if (row.sender.isEqual(receiver)) + ctx.db.screen_sdp_answer.id.delete(row.id); + } + for (const row of ctx.db.screen_ice_candidate.by_receiver.filter( + ctx.sender, + )) { + if (row.sender.isEqual(receiver)) + ctx.db.screen_ice_candidate.id.delete(row.id); + } + ctx.db.screen_sdp_offer.insert({ + id: 0n, + sender: ctx.sender, + receiver, + sdp, + channel_id: channelId, + }); + }, +); + +export const send_screen_sdp_answer = spacetimedb.reducer( + { receiver: t.identity(), sdp: t.string(), channelId: t.u64() }, + (ctx, { receiver, sdp, channelId }) => { + for (const row of ctx.db.screen_sdp_answer.by_sender.filter(ctx.sender)) { + if (row.receiver.isEqual(receiver)) + ctx.db.screen_sdp_answer.id.delete(row.id); + } + ctx.db.screen_sdp_answer.insert({ + id: 0n, + sender: ctx.sender, + receiver, + sdp, + channel_id: channelId, + }); + }, +); + +export const send_screen_ice_candidate = spacetimedb.reducer( + { receiver: t.identity(), candidate: t.string(), channelId: t.u64() }, + (ctx, { receiver, candidate, channelId }) => { + ctx.db.screen_ice_candidate.insert({ + id: 0n, + sender: ctx.sender, + receiver, + candidate, + channel_id: channelId, + }); + }, ); function clearSignalingForUser(ctx: any, identity: any) { - // Clean up stale signaling messages for the user - // Clean up watching status for (const w of ctx.db.watching.by_watcher.filter(identity)) { ctx.db.watching.id.delete(w.id); @@ -471,26 +685,33 @@ function clearSignalingForUser(ctx: any, identity: any) { ctx.db.watching.id.delete(w.id); } - for (const offer of ctx.db.sdp_offer.by_sender.filter(identity)) { - ctx.db.sdp_offer.id.delete(offer.id); - } - for (const offer of ctx.db.sdp_offer.by_receiver.filter(identity)) { - ctx.db.sdp_offer.id.delete(offer.id); - } + // Voice Cleanup + for (const row of ctx.db.voice_sdp_offer.by_sender.filter(identity)) + ctx.db.voice_sdp_offer.id.delete(row.id); + for (const row of ctx.db.voice_sdp_offer.by_receiver.filter(identity)) + ctx.db.voice_sdp_offer.id.delete(row.id); + for (const row of ctx.db.voice_sdp_answer.by_sender.filter(identity)) + ctx.db.voice_sdp_answer.id.delete(row.id); + for (const row of ctx.db.voice_sdp_answer.by_receiver.filter(identity)) + ctx.db.voice_sdp_answer.id.delete(row.id); + for (const row of ctx.db.voice_ice_candidate.by_sender.filter(identity)) + ctx.db.voice_ice_candidate.id.delete(row.id); + for (const row of ctx.db.voice_ice_candidate.by_receiver.filter(identity)) + ctx.db.voice_ice_candidate.id.delete(row.id); - for (const answer of ctx.db.sdp_answer.by_sender.filter(identity)) { - ctx.db.sdp_answer.id.delete(answer.id); - } - for (const answer of ctx.db.sdp_answer.by_receiver.filter(identity)) { - ctx.db.sdp_answer.id.delete(answer.id); - } - - for (const candidate of ctx.db.ice_candidate.by_sender.filter(identity)) { - ctx.db.ice_candidate.id.delete(candidate.id); - } - for (const candidate of ctx.db.ice_candidate.by_receiver.filter(identity)) { - ctx.db.ice_candidate.id.delete(candidate.id); - } + // Screen Cleanup + for (const row of ctx.db.screen_sdp_offer.by_sender.filter(identity)) + ctx.db.screen_sdp_offer.id.delete(row.id); + for (const row of ctx.db.screen_sdp_offer.by_receiver.filter(identity)) + ctx.db.screen_sdp_offer.id.delete(row.id); + for (const row of ctx.db.screen_sdp_answer.by_sender.filter(identity)) + ctx.db.screen_sdp_answer.id.delete(row.id); + for (const row of ctx.db.screen_sdp_answer.by_receiver.filter(identity)) + ctx.db.screen_sdp_answer.id.delete(row.id); + for (const row of ctx.db.screen_ice_candidate.by_sender.filter(identity)) + ctx.db.screen_ice_candidate.id.delete(row.id); + for (const row of ctx.db.screen_ice_candidate.by_receiver.filter(identity)) + ctx.db.screen_ice_candidate.id.delete(row.id); } export const create_thread = spacetimedb.reducer( @@ -499,23 +720,29 @@ export const create_thread = spacetimedb.reducer( validateName(name); const user = ctx.db.user.identity.find(ctx.sender); if (!user || (!user.username && !user.subject)) { - throw new SenderError('You must be logged in to create a thread'); + throw new SenderError("You must be logged in to create a thread"); } const parentMsg = ctx.db.message.id.find(parentMessageId); - if (!parentMsg) throw new SenderError('Parent message not found'); + if (!parentMsg) throw new SenderError("Parent message not found"); - ctx.db.thread.insert({ id: 0n, channel_id: channelId, parent_message_id: parentMessageId, name }); - } + ctx.db.thread.insert({ + id: 0n, + channel_id: channelId, + parent_message_id: parentMessageId, + name, + }); + }, ); export const send_message = spacetimedb.reducer( { text: t.string(), channelId: t.u64(), threadId: t.u64().optional() }, (ctx, { text, channelId, threadId }) => { - if (!text || text.trim().length === 0) throw new SenderError('Messages must not be empty'); + if (!text || text.trim().length === 0) + throw new SenderError("Messages must not be empty"); const user = ctx.db.user.identity.find(ctx.sender); if (!user || (!user.username && !user.subject)) { - throw new SenderError('You must be logged in to send messages'); + throw new SenderError("You must be logged in to send messages"); } ctx.db.message.insert({ @@ -526,23 +753,37 @@ export const send_message = spacetimedb.reducer( channel_id: channelId, thread_id: threadId, }); - } + }, ); -export const init = spacetimedb.init(ctx => { +export const init = spacetimedb.init((ctx) => { let hasServers = false; for (const _ of ctx.db.server.iter()) { hasServers = true; break; } if (!hasServers) { - const s = ctx.db.server.insert({ id: 0n, name: 'Spacetime Community', owner: undefined }); - ctx.db.channel.insert({ id: 0n, server_id: s.id, name: 'general', kind: { tag: 'Text' } }); - ctx.db.channel.insert({ id: 0n, server_id: s.id, name: 'Voice General', kind: { tag: 'Voice' } }); + const s = ctx.db.server.insert({ + id: 0n, + name: "Spacetime Community", + owner: undefined, + }); + ctx.db.channel.insert({ + id: 0n, + server_id: s.id, + name: "general", + kind: { tag: "Text" }, + }); + ctx.db.channel.insert({ + id: 0n, + server_id: s.id, + name: "Voice General", + kind: { tag: "Voice" }, + }); } }); -export const onConnect = spacetimedb.clientConnected(ctx => { +export const onConnect = spacetimedb.clientConnected((ctx) => { const user = ctx.db.user.identity.find(ctx.sender); if (ctx.senderAuth.hasJWT && ctx.senderAuth.jwt) { @@ -550,7 +791,11 @@ export const onConnect = spacetimedb.clientConnected(ctx => { const issuer = jwt.issuer; const subject = jwt.subject; const payload = jwt.fullPayload; - const name = (payload.name as string) || (payload.nickname as string) || (payload.preferred_username as string) || (payload.email as string); + const name = + (payload.name as string) || + (payload.nickname as string) || + (payload.preferred_username as string) || + (payload.email as string); if (user) { ctx.db.user.identity.update({ @@ -559,7 +804,7 @@ export const onConnect = spacetimedb.clientConnected(ctx => { talking: false, name: user.name || name, issuer, - subject + subject, }); } else { ctx.db.user.insert({ @@ -570,7 +815,7 @@ export const onConnect = spacetimedb.clientConnected(ctx => { issuer, subject, username: undefined, - password: undefined + password: undefined, }); } } else if (user) { @@ -585,21 +830,29 @@ export const onConnect = spacetimedb.clientConnected(ctx => { issuer: undefined, subject: undefined, username: undefined, - password: undefined + password: undefined, }); } // Auto-join the "Spacetime Community" server if it exists - const communityServer = [...ctx.db.server.iter()].find(s => s.name === 'Spacetime Community'); + const communityServer = [...ctx.db.server.iter()].find( + (s) => s.name === "Spacetime Community", + ); if (communityServer) { - const alreadyMember = [...ctx.db.server_member.by_identity.filter(ctx.sender)].some(m => m.server_id === communityServer.id); + const alreadyMember = [ + ...ctx.db.server_member.by_identity.filter(ctx.sender), + ].some((m) => m.server_id === communityServer.id); if (!alreadyMember) { - ctx.db.server_member.insert({ id: 0n, identity: ctx.sender, server_id: communityServer.id }); + ctx.db.server_member.insert({ + id: 0n, + identity: ctx.sender, + server_id: communityServer.id, + }); } } }); -export const onDisconnect = spacetimedb.clientDisconnected(ctx => { +export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { const user = ctx.db.user.identity.find(ctx.sender); if (user) { ctx.db.user.identity.update({ ...user, online: false, talking: false }); diff --git a/src/App.css b/src/App.css index b3bacdc..327dcc3 100644 --- a/src/App.css +++ b/src/App.css @@ -20,7 +20,8 @@ body { margin: 0; - font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-family: + "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; background-color: var(--background-tertiary); color: var(--text-normal); height: 100vh; @@ -58,7 +59,9 @@ body { align-items: center; justify-content: center; cursor: pointer; - transition: border-radius 0.2s, background-color 0.2s; + transition: + border-radius 0.2s, + background-color 0.2s; font-weight: bold; color: var(--text-normal); position: relative; @@ -77,7 +80,7 @@ body { } .server-icon.active::before { - content: ''; + content: ""; position: absolute; left: -12px; width: 8px; @@ -111,7 +114,7 @@ body { padding: 0 16px; display: flex; align-items: center; - box-shadow: 0 1px 0 rgba(0,0,0,0.2); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); font-weight: bold; } @@ -271,7 +274,7 @@ body { display: flex; align-items: center; gap: 8px; - box-shadow: 0 1px 0 rgba(0,0,0,0.2); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); font-weight: bold; z-index: 10; } @@ -290,7 +293,7 @@ body { } .message-item:hover { - background-color: rgba(0,0,0,0.05); + background-color: rgba(0, 0, 0, 0.05); } .message-avatar { @@ -460,7 +463,6 @@ body { flex: 1; } - .video-tile.talking { border-color: #23a559; } @@ -472,7 +474,8 @@ body { background-color: black; } -.video-controls, .tile-actions-right { +.video-controls, +.tile-actions-right { position: absolute; display: flex; gap: 8px; @@ -496,7 +499,8 @@ body { opacity: 1; } -.watch-btn, .mute-tile-btn { +.watch-btn, +.mute-tile-btn { background-color: var(--brand); color: white; border: none; @@ -509,17 +513,18 @@ body { justify-content: center; } -.watch-btn.active, .mute-tile-btn { +.watch-btn.active, +.mute-tile-btn { background-color: rgba(30, 31, 34, 0.7); } -.watch-btn.active:hover, .mute-tile-btn:hover { +.watch-btn.active:hover, +.mute-tile-btn:hover { background-color: rgba(43, 45, 49, 0.9); } - .fullscreen-btn { - background-color: rgba(0,0,0,0.5); + background-color: rgba(0, 0, 0, 0.5); color: white; border: none; padding: 4px 8px; @@ -550,7 +555,7 @@ body { position: absolute; bottom: 8px; left: 8px; - background-color: rgba(0,0,0,0.5); + background-color: rgba(0, 0, 0, 0.5); padding: 2px 8px; border-radius: 4px; display: flex; @@ -633,15 +638,15 @@ body { background-color: var(--background-floating, #1e1f22); border-radius: 8px; padding: 12px; - box-shadow: 0 8px 16px rgba(0,0,0,0.24); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.24); z-index: 2000; color: var(--text-normal, #dbdee1); - font-family: 'gg sans', sans-serif; + font-family: "gg sans", sans-serif; pointer-events: none; } .connection-popover::before { - content: ''; + content: ""; position: absolute; top: -6px; left: 20px; @@ -657,7 +662,7 @@ body { align-items: center; margin-bottom: 12px; padding-bottom: 8px; - border-bottom: 1px solid rgba(255,255,255,0.1); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .popover-name { @@ -671,9 +676,15 @@ body { font-weight: 800; } -.popover-status.green { color: #23a559; } -.popover-status.yellow { color: #f0b232; } -.popover-status.red { color: #f23f43; } +.popover-status.green { + color: #23a559; +} +.popover-status.yellow { + color: #f0b232; +} +.popover-status.red { + color: #f23f43; +} .popover-info { font-size: 0.8rem; @@ -709,7 +720,6 @@ body { } .member-name { -... font-size: 0.9rem; font-weight: 500; white-space: nowrap; @@ -724,7 +734,7 @@ body { display: flex; flex-direction: column; flex-shrink: 0; - border-left: 1px solid rgba(0,0,0,0.2); + border-left: 1px solid rgba(0, 0, 0, 0.2); } .thread-view { @@ -738,7 +748,7 @@ body { padding: 0 16px; display: flex; align-items: center; - box-shadow: 0 1px 0 rgba(0,0,0,0.2); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); font-weight: bold; } @@ -755,7 +765,7 @@ body { left: 0; right: 0; bottom: 0; - background-color: rgba(0,0,0,0.8); + background-color: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; @@ -806,7 +816,7 @@ body { display: flex; justify-content: space-between; align-items: center; - border-top: 1px solid rgba(255,255,255,0.1); + border-top: 1px solid rgba(255, 255, 255, 0.1); } .voice-info { diff --git a/src/App.integration.test.tsx b/src/App.integration.test.tsx index 6e2bcd7..b478052 100644 --- a/src/App.integration.test.tsx +++ b/src/App.integration.test.tsx @@ -1,24 +1,24 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { describe, it, expect } from 'vitest'; -import App from './App'; -import { SpacetimeDBProvider } from 'spacetimedb/react'; -import { DbConnection } from './module_bindings'; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect } from "vitest"; +import App from "./App"; +import { SpacetimeDBProvider } from "spacetimedb/react"; +import { DbConnection } from "./module_bindings"; -describe('App Integration Test', () => { - it('connects to the DB, allows name change and message sending', async () => { +describe("App Integration Test", () => { + it("connects to the DB, allows name change and message sending", async () => { const connectionBuilder = DbConnection.builder() - .withUri('ws://localhost:3000') - .withDatabaseName('quickstart-chat') + .withUri("ws://localhost:3000") + .withDatabaseName("quickstart-chat") .withToken( localStorage.getItem( - 'ws://localhost:3000/quickstart-chat/auth_token' - ) || '' + "ws://localhost:3000/quickstart-chat/auth_token", + ) || "", ); render( - +
, ); // Initially, we should see "Connecting..." @@ -29,7 +29,7 @@ describe('App Integration Test', () => { await waitFor( () => expect(screen.queryByText(/Connecting.../i)).not.toBeInTheDocument(), - { timeout: 10000 } + { timeout: 10000 }, ); // The profile section should show the default name or truncated identity @@ -37,40 +37,40 @@ describe('App Integration Test', () => { // If your default identity is something like 'abcdef12' or 'Unknown' // we do a generic check: expect( - screen.getByRole('heading', { name: /profile/i }) + screen.getByRole("heading", { name: /profile/i }), ).toBeInTheDocument(); // Let's change the user's name const editNameButton = screen.getByText(/Edit Name/i); await userEvent.click(editNameButton); - const nameInput = screen.getByRole('textbox', { name: /name input/i }); + const nameInput = screen.getByRole("textbox", { name: /name input/i }); await userEvent.clear(nameInput); - await userEvent.type(nameInput, 'TestUser'); - const submitNameButton = screen.getByRole('button', { name: /submit/i }); + await userEvent.type(nameInput, "TestUser"); + const submitNameButton = screen.getByRole("button", { name: /submit/i }); await userEvent.click(submitNameButton); // If your DB or UI updates instantly, we can check that the new name shows up await waitFor( () => { - expect(screen.getByText('TestUser')).toBeInTheDocument(); + expect(screen.getByText("TestUser")).toBeInTheDocument(); }, - { timeout: 10000 } + { timeout: 10000 }, ); // Now let's send a message - const textarea = screen.getByRole('textbox', { name: /message input/i }); - await userEvent.type(textarea, 'Hello from GH Actions!'); + const textarea = screen.getByRole("textbox", { name: /message input/i }); + await userEvent.type(textarea, "Hello from GH Actions!"); - const sendButton = screen.getByRole('button', { name: /send/i }); + const sendButton = screen.getByRole("button", { name: /send/i }); await userEvent.click(sendButton); // Wait for message to appear in the UI await waitFor( () => { - expect(screen.getByText('Hello from GH Actions!')).toBeInTheDocument(); + expect(screen.getByText("Hello from GH Actions!")).toBeInTheDocument(); }, - { timeout: 10000 } + { timeout: 10000 }, ); }); }); diff --git a/src/App.tsx b/src/App.tsx index b3731f9..b5e5971 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import './App.css'; +import React from "react"; +import "./App.css"; // Remove all imports related to SpacetimeDB, auth, and chat logic that are now in ChatContainer or other modules // import { tables, reducers } from './module_bindings'; // import type * as Types from './module_bindings/types'; @@ -9,16 +9,14 @@ import './App.css'; // import { TOKEN_KEY } from './main'; // Import the new ChatContainer component -import { ChatContainer } from './chat'; // Import from index.ts +import { ChatContainer } from "./chat"; // Import from index.ts function App() { // All the state, effects, reducers, table fetches, and UI rendering logic // related to chat and authentication have been moved to ChatContainer and its sub-components. // App.tsx now simply renders the ChatContainer. - return ( - - ); + return ; } export default App; diff --git a/src/auth/AuthGate.tsx b/src/auth/AuthGate.tsx index 90b61a9..d34a868 100644 --- a/src/auth/AuthGate.tsx +++ b/src/auth/AuthGate.tsx @@ -1,9 +1,9 @@ // src/auth/AuthGate.tsx -import React, { useState, useContext, useEffect } from 'react'; -import { useAuth } from 'react-oidc-context'; -import UsernamePasswordAuth from './UsernamePasswordAuth'; -import App from '../App'; -import { TOKEN_KEY } from '../main.tsx'; +import React, { useState, useContext, useEffect } from "react"; +import { useAuth } from "react-oidc-context"; +import UsernamePasswordAuth from "./UsernamePasswordAuth"; +import App from "../App"; +import { TOKEN_KEY } from "../main.tsx"; interface AuthGateProps { children: React.ReactNode; // This will be SpacetimeDBWrapper @@ -12,12 +12,14 @@ interface AuthGateProps { function AuthGate({ children }: AuthGateProps) { const auth = useAuth(); const [authError, setAuthError] = useState(null); - const [hasStoredToken, setHasStoredToken] = useState(!!localStorage.getItem(TOKEN_KEY)); + const [hasStoredToken, setHasStoredToken] = useState( + !!localStorage.getItem(TOKEN_KEY), + ); // Logging authentication state - console.log('AuthGate: auth.isLoading:', auth.isLoading); - console.log('AuthGate: auth.isAuthenticated:', auth.isAuthenticated); - console.log('AuthGate: hasStoredToken:', hasStoredToken); + console.log("AuthGate: auth.isLoading:", auth.isLoading); + console.log("AuthGate: auth.isAuthenticated:", auth.isAuthenticated); + console.log("AuthGate: hasStoredToken:", hasStoredToken); const handleUsernamePasswordLoginSuccess = () => { console.log("Username/Password login successful. AuthGate will re-render."); @@ -25,7 +27,9 @@ function AuthGate({ children }: AuthGateProps) { }; const handleUsernamePasswordRegisterSuccess = () => { - console.log("Username/Password registration successful. AuthGate will re-render."); + console.log( + "Username/Password registration successful. AuthGate will re-render.", + ); setHasStoredToken(true); }; @@ -34,7 +38,7 @@ function AuthGate({ children }: AuthGateProps) { }; const isAuthenticated = auth.isAuthenticated || hasStoredToken; - + if (auth.isLoading) { // Show a loading indicator instead of a blank white screen console.log("AuthGate: Authentication is loading..."); @@ -47,7 +51,7 @@ function AuthGate({ children }: AuthGateProps) { if (isAuthenticated) { console.log("AuthGate: Authenticated. Rendering children."); - return <>{children}; + return <>{children}; } // If not authenticated, show login options @@ -57,15 +61,17 @@ function AuthGate({ children }: AuthGateProps) {

Welcome!

Please log in to continue to the chat.

- {authError &&

{authError}

} + {authError && ( +

{authError}

+ )} { window.history.replaceState({}, document.title, window.location.pathname); - } + }, }; interface OidcProviderProps { @@ -19,9 +20,5 @@ interface OidcProviderProps { } export function OidcProvider({ children }: OidcProviderProps) { - return ( - - {children} - - ); + return {children}; } diff --git a/src/auth/UsernamePasswordAuth.tsx b/src/auth/UsernamePasswordAuth.tsx index e5d59de..e4a5db1 100644 --- a/src/auth/UsernamePasswordAuth.tsx +++ b/src/auth/UsernamePasswordAuth.tsx @@ -1,8 +1,8 @@ // src/auth/UsernamePasswordAuth.tsx -import React, { useState, useEffect, useContext } from 'react'; -import { Identity } from 'spacetimedb'; -import { useSpacetimeDB } from 'spacetimedb/react'; // Correct hook for SpacetimeDB connection -import { TOKEN_KEY } from '../main.tsx'; // Import the token key +import React, { useState, useEffect, useContext } from "react"; +import { Identity } from "spacetimedb"; +import { useSpacetimeDB } from "spacetimedb/react"; // Correct hook for SpacetimeDB connection +import { TOKEN_KEY } from "../main.tsx"; // Import the token key // Define the expected shape of the DbConnection instance from the hook interface SpacetimeDBConnection { @@ -16,9 +16,13 @@ interface UsernamePasswordAuthProps { onError: (error: string | null) => void; // Callback for errors } -function UsernamePasswordAuth({ onLoginSuccess, onRegisterSuccess, onError }: UsernamePasswordAuthProps) { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); +function UsernamePasswordAuth({ + onLoginSuccess, + onRegisterSuccess, + onError, +}: UsernamePasswordAuthProps) { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); // Get the SpacetimeDB connection instance using the correct hook @@ -26,11 +30,11 @@ function UsernamePasswordAuth({ onLoginSuccess, onRegisterSuccess, onError }: Us const handleLogin = async () => { if (!conn) { - onError('Database connection not available.'); + onError("Database connection not available."); return; } if (!username || !password) { - onError('Please enter both username and password.'); + onError("Please enter both username and password."); return; } setIsLoading(true); @@ -39,14 +43,14 @@ function UsernamePasswordAuth({ onLoginSuccess, onRegisterSuccess, onError }: Us // Correct way to call reducers in SpacetimeDB TS SDK: // conn.reducers.reducerName({ arg1: value1, ... }) conn.reducers.login({ username, password }); - + // Since reducers are asynchronous and don't return values to the caller, // we assume success if no error is thrown immediately. // The actual state update will come through the subscription. onLoginSuccess(); } catch (e: any) { - onError(`Login error: ${e.message || 'Unknown error'}`); - console.error('Login error:', e); + onError(`Login error: ${e.message || "Unknown error"}`); + console.error("Login error:", e); } finally { setIsLoading(false); } @@ -54,11 +58,11 @@ function UsernamePasswordAuth({ onLoginSuccess, onRegisterSuccess, onError }: Us const handleRegister = async () => { if (!conn) { - onError('Database connection not available.'); + onError("Database connection not available."); return; } if (!username || !password) { - onError('Please enter both username and password.'); + onError("Please enter both username and password."); return; } setIsLoading(true); @@ -67,41 +71,75 @@ function UsernamePasswordAuth({ onLoginSuccess, onRegisterSuccess, onError }: Us conn.reducers.register({ username, password }); onRegisterSuccess(); } catch (e: any) { - onError(`Registration error: ${e.message || 'Unknown error'}`); - console.error('Registration error:', e); + onError(`Registration error: ${e.message || "Unknown error"}`); + console.error("Registration error:", e); } finally { setIsLoading(false); } }; return ( -
-

Username/Password Authentication

-
+
+

Username/Password Authentication

+
setUsername(e.target.value)} - style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ccc' }} + style={{ + padding: "8px", + marginRight: "10px", + borderRadius: "4px", + border: "1px solid #ccc", + }} disabled={isLoading} />
-
+
setPassword(e.target.value)} - style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ccc' }} + style={{ + padding: "8px", + marginRight: "10px", + borderRadius: "4px", + border: "1px solid #ccc", + }} disabled={isLoading} />
- -
); diff --git a/src/auth/index.ts b/src/auth/index.ts index 7b273ad..8bc491a 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,4 +1,4 @@ // src/auth/index.ts -export { OidcProvider, oidcConfig } from './OidcProvider'; -export { default as AuthGate } from './AuthGate'; -export { default as UsernamePasswordAuth } from './UsernamePasswordAuth'; +export { OidcProvider, oidcConfig } from "./OidcProvider"; +export { default as AuthGate } from "./AuthGate"; +export { default as UsernamePasswordAuth } from "./UsernamePasswordAuth"; diff --git a/src/chat/ChatContainer.tsx b/src/chat/ChatContainer.tsx index ee9790f..9e28af3 100644 --- a/src/chat/ChatContainer.tsx +++ b/src/chat/ChatContainer.tsx @@ -1,15 +1,15 @@ -import React, { useState, useEffect } from 'react'; -import { useChat, useWebRTC } from './services'; -import ServerList from './components/ServerList'; -import ChannelList from './components/ChannelList'; -import MessageList from './components/MessageList'; -import MessageInput from './components/MessageInput'; -import MemberList from './components/MemberList'; -import ThreadView from './components/ThreadView'; -import ServerDiscovery from './components/ServerDiscovery'; -import { VideoGrid } from './components/VideoGrid'; -import { SettingsPanel } from './components/SettingsPanel'; -import { useSpacetimeDB } from 'spacetimedb/react'; +import React, { useState, useEffect } from "react"; +import { useChat, useWebRTC } from "./services"; +import ServerList from "./components/ServerList"; +import ChannelList from "./components/ChannelList"; +import MessageList from "./components/MessageList"; +import MessageInput from "./components/MessageInput"; +import MemberList from "./components/MemberList"; +import ThreadView from "./components/ThreadView"; +import ServerDiscovery from "./components/ServerDiscovery"; +import { VideoGrid } from "./components/VideoGrid"; +import { SettingsPanel } from "./components/SettingsPanel"; +import { useSpacetimeDB } from "spacetimedb/react"; const ChatContainer: React.FC = () => { const chat = useChat(); @@ -31,7 +31,7 @@ const ChatContainer: React.FC = () => { isDeafened, toggleMute, toggleDeafen, - peerStats + peerStats, } = useWebRTC(chat.connectedVoiceChannel?.id); useEffect(() => { @@ -89,7 +89,7 @@ const ChatContainer: React.FC = () => {
- 📶 + 📶 Voice Connected
@@ -101,9 +101,9 @@ const ChatContainer: React.FC = () => { className="icon-btn" onClick={chat.handleLeaveVoice} title="Disconnect" - style={{ color: '#f23f43' }} + style={{ color: "#f23f43" }} > - +
@@ -113,29 +113,31 @@ const ChatContainer: React.FC = () => {
- {chat.currentUser?.name?.[0]?.toUpperCase() || identity?.toHexString().substring(0, 2).toUpperCase()} + {chat.currentUser?.name?.[0]?.toUpperCase() || + identity?.toHexString().substring(0, 2).toUpperCase()}
- {chat.currentUser?.name || identity?.toHexString().substring(0, 8)} + {chat.currentUser?.name || + identity?.toHexString().substring(0, 8)}
Online
-
+
-
+
- {chat.isActiveChannelVoice ? '🔊' : '#'} + {chat.isActiveChannelVoice ? "🔊" : "#"} - {chat.activeChannel?.name || 'Select a channel'} + {chat.activeChannel?.name || "Select a channel"}
-
- {chat.isActiveChannelVoice && chat.connectedVoiceChannel?.id === chat.activeChannel?.id && ( - - )} +
+ {chat.isActiveChannelVoice && + chat.connectedVoiceChannel?.id === chat.activeChannel?.id && ( + + )} {!chat.activeThreadId && (
); }; diff --git a/src/chat/components/ChannelList.tsx b/src/chat/components/ChannelList.tsx index 7f1429b..62771a5 100644 --- a/src/chat/components/ChannelList.tsx +++ b/src/chat/components/ChannelList.tsx @@ -33,16 +33,27 @@ interface ChannelListProps { } // Helper function (extracted from App.tsx) -const getUsername = (userIdentity: Identity | null, users: readonly Types.User[]) => { +const getUsername = ( + userIdentity: Identity | null, + users: readonly Types.User[], +) => { if (!userIdentity) return "Unknown"; - const user = users.find(u => u.identity?.isEqual(userIdentity)); + const user = users.find((u) => u.identity?.isEqual(userIdentity)); return user?.name || userIdentity.toHexString().substring(0, 8); }; -const getStatusColor = (status: string | undefined): "green" | "yellow" | "red" => { - if (status === 'connected' || status === 'completed') return 'green'; - if (status === 'connecting' || status === 'checking' || status === 'new' || !status) return 'yellow'; - return 'red'; +const getStatusColor = ( + status: string | undefined, +): "green" | "yellow" | "red" => { + if (status === "connected" || status === "completed") return "green"; + if ( + status === "connecting" || + status === "checking" || + status === "new" || + !status + ) + return "yellow"; + return "red"; }; const formatBitrate = (bps: number) => { @@ -51,12 +62,19 @@ const formatBitrate = (bps: number) => { return `${bps.toFixed(0)} bps`; }; -const ConnectionPopover: React.FC<{ stats?: WebRTCStats, status: string, name: string, isMe: boolean }> = ({ stats, status, name, isMe }) => { +const ConnectionPopover: React.FC<{ + stats?: WebRTCStats; + status: string; + name: string; + isMe: boolean; +}> = ({ stats, status, name, isMe }) => { return (
{name} - {status} + + {status} +
{isMe ? ( @@ -65,16 +83,36 @@ const ConnectionPopover: React.FC<{ stats?: WebRTCStats, status: string, name: s
AUDIO
-
Bitrate{formatBitrate(stats.audio.bitrate)}
-
Jitter{(stats.audio.jitter * 1000).toFixed(2)} ms
-
Loss{stats.audio.packetsLost} pkts
+
+ Bitrate + {formatBitrate(stats.audio.bitrate)} +
+
+ Jitter + {(stats.audio.jitter * 1000).toFixed(2)} ms +
+
+ Loss + {stats.audio.packetsLost} pkts +
{stats.video.bitrate > 0 && (
VIDEO
-
Bitrate{formatBitrate(stats.video.bitrate)}
-
Res{stats.video.frameWidth}x{stats.video.frameHeight}
-
FPS{stats.video.framesPerSecond.toFixed(0)}
+
+ Bitrate + {formatBitrate(stats.video.bitrate)} +
+
+ Res + + {stats.video.frameWidth}x{stats.video.frameHeight} + +
+
+ FPS + {stats.video.framesPerSecond.toFixed(0)} +
)}
@@ -86,25 +124,51 @@ const ConnectionPopover: React.FC<{ stats?: WebRTCStats, status: string, name: s }; export const ChannelList: React.FC = ({ - activeServerId, activeChannelId, setActiveChannelId, setActiveThreadId, - channels, servers, users, identity, voiceStates, currentVoiceState, connectedVoiceChannel, isFullyAuthenticated, - showCreateChannelModal, setShowCreateChannelModal, newChannelName, setNewChannelName, isVoiceChannel, setIsVoiceChannel, - handleCreateChannel, handleJoinVoice, handleLeaveVoice, peerStatuses, watching, peerStats + activeServerId, + activeChannelId, + setActiveChannelId, + setActiveThreadId, + channels, + servers, + users, + identity, + voiceStates, + currentVoiceState, + connectedVoiceChannel, + isFullyAuthenticated, + showCreateChannelModal, + setShowCreateChannelModal, + newChannelName, + setNewChannelName, + isVoiceChannel, + setIsVoiceChannel, + handleCreateChannel, + handleJoinVoice, + handleLeaveVoice, + peerStatuses, + watching, + peerStats, }) => { const [hoveredPeer, setHoveredPeer] = React.useState(null); - const activeServer = React.useMemo(() => - servers.find(s => s.id === activeServerId), - [servers, activeServerId] + const activeServer = React.useMemo( + () => servers.find((s) => s.id === activeServerId), + [servers, activeServerId], ); - const textChannels = React.useMemo(() => - channels.filter(c => c.serverId === activeServerId && c.kind.tag === 'Text'), - [channels, activeServerId] + const textChannels = React.useMemo( + () => + channels.filter( + (c) => c.serverId === activeServerId && c.kind.tag === "Text", + ), + [channels, activeServerId], ); - const voiceChannels = React.useMemo(() => - channels.filter(c => c.serverId === activeServerId && c.kind.tag === 'Voice'), - [channels, activeServerId] + const voiceChannels = React.useMemo( + () => + channels.filter( + (c) => c.serverId === activeServerId && c.kind.tag === "Voice", + ), + [channels, activeServerId], ); if (!activeServer) { @@ -124,12 +188,20 @@ export const ChannelList: React.FC = ({
TEXT CHANNELS - +
- {textChannels.map(channel => ( + {textChannels.map((channel) => (
{ setActiveChannelId(channel.id); setActiveThreadId(null); @@ -144,112 +216,164 @@ export const ChannelList: React.FC = ({
VOICE CHANNELS - +
- {voiceChannels.map(channel => ( + {voiceChannels.map((channel) => (
{ handleJoinVoice(channel.id); setActiveChannelId(channel.id); setActiveThreadId(null); }} - style={{cursor: isFullyAuthenticated ? 'pointer' : 'not-allowed'}} + style={{ + cursor: isFullyAuthenticated ? "pointer" : "not-allowed", + }} > 🔊 {channel.name}
{/* Voice Channel Members */} -
- {voiceStates.filter(vs => vs.channelId === channel.id).map(vs => { - const peerIdHex = vs.identity.toHexString(); - const isMe = identity?.isEqual(vs.identity); - const status = peerStatuses.get(peerIdHex); - const user = users.find(u => u.identity?.isEqual(vs.identity)); - const isTalking = user?.talking || false; - const isSharing = vs.isSharingScreen; - const amIWatching = watching.some(w => w.watcher.isEqual(identity!) && w.watchee.isEqual(vs.identity)); +
+ {voiceStates + .filter((vs) => vs.channelId === channel.id) + .map((vs) => { + const peerIdHex = vs.identity.toHexString(); + const isMe = identity?.isEqual(vs.identity); + const status = peerStatuses.get(peerIdHex); + const user = users.find((u) => + u.identity?.isEqual(vs.identity), + ); + const isTalking = user?.talking || false; + const isSharing = vs.isSharingScreen; + const amIWatching = watching.some( + (w) => + w.watcher.isEqual(identity!) && + w.watchee.isEqual(vs.identity), + ); - const voiceStatusColor = isMe ? 'green' : getStatusColor(status); - const videoStatusColor = isMe ? (isSharing ? 'green' : undefined) : (isSharing ? getStatusColor(status) : undefined); + const voiceStatusColor = isMe + ? "green" + : getStatusColor(status); + const videoStatusColor = isMe + ? isSharing + ? "green" + : undefined + : isSharing + ? getStatusColor(status) + : undefined; - // Consolidate into one dot: priority Red > Yellow > Green - let finalStatusColor: "green" | "yellow" | "red" = voiceStatusColor; - if (videoStatusColor === 'red') finalStatusColor = 'red'; - else if (videoStatusColor === 'yellow' && finalStatusColor === 'green') finalStatusColor = 'yellow'; + // Consolidate into one dot: priority Red > Yellow > Green + let finalStatusColor: "green" | "yellow" | "red" = + voiceStatusColor; + if (videoStatusColor === "red") finalStatusColor = "red"; + else if ( + videoStatusColor === "yellow" && + finalStatusColor === "green" + ) + finalStatusColor = "yellow"; - return ( -
+ return (
- {getUsername(vs.identity, users).substring(0, 2).toUpperCase()} -
- {getUsername(vs.identity, users)} - {isSharing && ( - LIVE - )} -
setHoveredPeer(peerIdHex)} - onMouseLeave={() => setHoveredPeer(null)} - > -
-
+
+ {getUsername(vs.identity, users) + .substring(0, 2) + .toUpperCase()} +
+ + {getUsername(vs.identity, users)} + + {isSharing && ( + + LIVE + + )} +
setHoveredPeer(peerIdHex)} + onMouseLeave={() => setHoveredPeer(null)} + > +
+
- {hoveredPeer === peerIdHex && ( - - )} -
- ); - })} + {hoveredPeer === peerIdHex && ( + + )} +
+ ); + })}
))} @@ -258,7 +382,7 @@ export const ChannelList: React.FC = ({ {showCreateChannelModal && (
-

Create {isVoiceChannel ? 'Voice' : 'Text'} Channel

+

Create {isVoiceChannel ? "Voice" : "Text"} Channel

= ({ onChange={(e) => setNewChannelName(e.target.value)} />
- - + +
diff --git a/src/chat/components/MemberList.tsx b/src/chat/components/MemberList.tsx index e8d3681..a834f37 100644 --- a/src/chat/components/MemberList.tsx +++ b/src/chat/components/MemberList.tsx @@ -1,13 +1,16 @@ // src/chat/components/MemberList.tsx -import React, { useMemo } from 'react'; -import { Identity } from 'spacetimedb'; -import type * as Types from '../../module_bindings/types'; -import { tables } from '../../module_bindings'; +import React, { useMemo } from "react"; +import { Identity } from "spacetimedb"; +import type * as Types from "../../module_bindings/types"; +import { tables } from "../../module_bindings"; // Helper function (extracted from App.tsx) -const getUsername = (userIdentity: Identity | null, users: readonly Types.User[]) => { +const getUsername = ( + userIdentity: Identity | null, + users: readonly Types.User[], +) => { if (!userIdentity) return "Unknown"; - const user = users.find(u => u.identity?.isEqual(userIdentity)); + const user = users.find((u) => u.identity?.isEqual(userIdentity)); return user?.name || userIdentity.toHexString().substring(0, 8); }; @@ -21,54 +24,89 @@ interface MemberListProps { connectedVoiceChannel: Types.Channel | undefined; } -function MemberList({ activeServerMembers, users, identity, activeServer, voiceStates, currentVoiceState, connectedVoiceChannel }: MemberListProps) { - +function MemberList({ + activeServerMembers, + users, + identity, + activeServer, + voiceStates, + currentVoiceState, + connectedVoiceChannel, +}: MemberListProps) { // Categorize members into Online and Offline - const onlineMembers = useMemo(() => - activeServerMembers.filter(m => m.online), - [activeServerMembers] + const onlineMembers = useMemo( + () => activeServerMembers.filter((m) => m.online), + [activeServerMembers], ); - const offlineMembers = useMemo(() => - activeServerMembers.filter(m => !m.online), - [activeServerMembers] + const offlineMembers = useMemo( + () => activeServerMembers.filter((m) => !m.online), + [activeServerMembers], ); const renderMember = (user: Types.User, isOffline: boolean = false) => { - const userVoiceState = voiceStates.find(vs => vs.identity.isEqual(user.identity)); + const userVoiceState = voiceStates.find((vs) => + vs.identity.isEqual(user.identity), + ); const isTalking = user.talking || false; const isSharing = userVoiceState?.isSharingScreen || false; const isMe = identity?.isEqual(user.identity); return ( -
-
+
- {(user.name || user.identity.toHexString()).substring(0, 2).toUpperCase()} + {(user.name || user.identity.toHexString()) + .substring(0, 2) + .toUpperCase()}
- + {user.name || user.identity.toHexString().substring(0, 8)} - {isMe && (You)} + {isMe && ( + + (You) + + )} {isSharing && !isOffline && ( - LIVE + + LIVE + )}
); @@ -79,19 +117,33 @@ function MemberList({ activeServerMembers, users, identity, activeServer, voiceS
{onlineMembers.length > 0 && ( <> -
+
ONLINE — {onlineMembers.length}
- {onlineMembers.map(user => renderMember(user))} + {onlineMembers.map((user) => renderMember(user))} )} {offlineMembers.length > 0 && ( <> -
+
OFFLINE — {offlineMembers.length}
- {offlineMembers.map(user => renderMember(user, true))} + {offlineMembers.map((user) => renderMember(user, true))} )}
diff --git a/src/chat/components/MessageInput.tsx b/src/chat/components/MessageInput.tsx index 9015dae..0fe28a8 100644 --- a/src/chat/components/MessageInput.tsx +++ b/src/chat/components/MessageInput.tsx @@ -1,5 +1,5 @@ // src/chat/components/MessageInput.tsx -import React, { useState } from 'react'; +import React, { useState } from "react"; interface MessageInputProps { activeChannelId: bigint | null; @@ -8,23 +8,36 @@ interface MessageInputProps { sendMessageReducer: (args: any) => void; } -function MessageInput({ activeChannelId, activeThreadId, isFullyAuthenticated, sendMessageReducer }: MessageInputProps) { - const [messageText, setMessageText] = useState(''); - +function MessageInput({ + activeChannelId, + activeThreadId, + isFullyAuthenticated, + sendMessageReducer, +}: MessageInputProps) { + const [messageText, setMessageText] = useState(""); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!messageText.trim() || !activeChannelId) return; - + // Call the sendMessage reducer - sendMessageReducer({ text: messageText, channelId: activeChannelId, threadId: activeThreadId }); - setMessageText(''); + sendMessageReducer({ + text: messageText, + channelId: activeChannelId, + threadId: activeThreadId, + }); + setMessageText(""); }; return (
setMessageText(e.target.value)} diff --git a/src/chat/components/MessageList.tsx b/src/chat/components/MessageList.tsx index 70e1a1f..0bab53a 100644 --- a/src/chat/components/MessageList.tsx +++ b/src/chat/components/MessageList.tsx @@ -1,22 +1,25 @@ // src/chat/components/MessageList.tsx -import React, { useRef, useEffect, useMemo } from 'react'; -import { Identity } from 'spacetimedb'; -import type * as Types from '../../module_bindings/types'; -import { useTable } from 'spacetimedb/react'; -import { tables } from '../../module_bindings'; +import React, { useRef, useEffect, useMemo } from "react"; +import { Identity } from "spacetimedb"; +import type * as Types from "../../module_bindings/types"; +import { useTable } from "spacetimedb/react"; +import { tables } from "../../module_bindings"; -import RichText from './RichText'; +import RichText from "./RichText"; // Helper function (extracted from App.tsx) -const getUsername = (userIdentity: Identity | null, users: readonly Types.User[]) => { +const getUsername = ( + userIdentity: Identity | null, + users: readonly Types.User[], +) => { if (!userIdentity) return "Unknown"; - const user = users.find(u => u.identity?.isEqual(userIdentity)); + const user = users.find((u) => u.identity?.isEqual(userIdentity)); return user?.name || userIdentity.toHexString().substring(0, 8); }; const formatTime = (ts: any) => { const date = new Date(Number(ts.microsSinceUnixEpoch / 1000n)); - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; interface MessageListProps { @@ -29,12 +32,20 @@ interface MessageListProps { isFullyAuthenticated: boolean; } -function MessageList({ messages, activeThreadId, setActiveThreadId, users, identity, handleStartThread, isFullyAuthenticated }: MessageListProps) { +function MessageList({ + messages, + activeThreadId, + setActiveThreadId, + users, + identity, + handleStartThread, + isFullyAuthenticated, +}: MessageListProps) { const messagesEndRef = useRef(null); // Auto-scroll to bottom when messages change useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Fetch threads to display thread links @@ -42,13 +53,25 @@ function MessageList({ messages, activeThreadId, setActiveThreadId, users, ident return (
- {messages.map(msg => { + {messages.map((msg) => { const msgUsername = getUsername(msg.sender, users); - const existingThread = allThreads.find(t => t.parentMessageId === msg.id); + const existingThread = allThreads.find( + (t) => t.parentMessageId === msg.id, + ); return (
-
+
{msgUsername.substring(0, 2).toUpperCase()}
@@ -56,19 +79,19 @@ function MessageList({ messages, activeThreadId, setActiveThreadId, users, ident {msgUsername} {formatTime(msg.sent)} {!existingThread && isFullyAuthenticated && ( -
); diff --git a/src/chat/components/ServerDiscovery.tsx b/src/chat/components/ServerDiscovery.tsx index c456349..4a747cc 100644 --- a/src/chat/components/ServerDiscovery.tsx +++ b/src/chat/components/ServerDiscovery.tsx @@ -1,5 +1,5 @@ -import React, { useState, useMemo } from 'react'; -import type * as Types from '../../module_bindings/types'; +import React, { useState, useMemo } from "react"; +import type * as Types from "../../module_bindings/types"; interface ServerDiscoveryProps { availableServers: readonly Types.Server[]; @@ -8,55 +8,96 @@ interface ServerDiscoveryProps { isFullyAuthenticated: boolean; } -function ServerDiscovery({ availableServers, handleJoinServer, onClose, isFullyAuthenticated }: ServerDiscoveryProps) { - const [searchTerm, setSearchTerm] = useState(''); +function ServerDiscovery({ + availableServers, + handleJoinServer, + onClose, + isFullyAuthenticated, +}: ServerDiscoveryProps) { + const [searchTerm, setSearchTerm] = useState(""); const filteredServers = useMemo(() => { - return availableServers.filter(s => - s.name.toLowerCase().includes(searchTerm.toLowerCase()) + return availableServers.filter((s) => + s.name.toLowerCase().includes(searchTerm.toLowerCase()), ); }, [availableServers, searchTerm]); return (
-
-
+
+

Discover Servers

- +
- + setSearchTerm(e.target.value)} - style={{ marginBottom: '16px' }} + style={{ marginBottom: "16px" }} /> -
+
{filteredServers.length === 0 ? ( -

No servers found.

+

+ No servers found. +

) : ( - filteredServers.map(server => ( -
( +
-
-
+
+
{server.name.substring(0, 2).toUpperCase()}
- {server.name} + {server.name}
- - + +
diff --git a/src/chat/components/SettingsPanel.tsx b/src/chat/components/SettingsPanel.tsx index 5c00472..5da319b 100644 --- a/src/chat/components/SettingsPanel.tsx +++ b/src/chat/components/SettingsPanel.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; -import { useReducer, useSpacetimeDB, useTable } from 'spacetimedb/react'; -import { reducers, tables } from '../../module_bindings'; +import React, { useState } from "react"; +import { useReducer, useSpacetimeDB, useTable } from "spacetimedb/react"; +import { reducers, tables } from "../../module_bindings"; interface SettingsPanelProps { onClose: () => void; @@ -9,9 +9,9 @@ interface SettingsPanelProps { export const SettingsPanel: React.FC = ({ onClose }) => { const { identity } = useSpacetimeDB(); const [users] = useTable(tables.user); - const currentUser = users.find(u => u.identity.isEqual(identity!)); - - const [name, setNameInput] = useState(currentUser?.name || ''); + const currentUser = users.find((u) => u.identity.isEqual(identity!)); + + const [name, setNameInput] = useState(currentUser?.name || ""); const setNameReducer = useReducer(reducers.setName); const handleSave = () => { @@ -23,24 +23,41 @@ export const SettingsPanel: React.FC = ({ onClose }) => { return (
-
e.stopPropagation()}> +
e.stopPropagation()}>

User Settings

-
- - + +
diff --git a/src/chat/components/ThreadMessageInput.tsx b/src/chat/components/ThreadMessageInput.tsx index cb46b78..0c6cc19 100644 --- a/src/chat/components/ThreadMessageInput.tsx +++ b/src/chat/components/ThreadMessageInput.tsx @@ -1,6 +1,6 @@ // src/chat/components/ThreadMessageInput.tsx -import React, { useState } from 'react'; -import { tables, reducers } from '../../module_bindings'; +import React, { useState } from "react"; +import { tables, reducers } from "../../module_bindings"; interface ThreadMessageInputProps { activeChannelId: bigint | null; // Still needed by sendMessage reducer @@ -9,25 +9,39 @@ interface ThreadMessageInputProps { sendMessageReducer: (args: any) => void; } -function ThreadMessageInput({ activeChannelId, activeThreadId, isFullyAuthenticated, sendMessageReducer }: ThreadMessageInputProps) { - const [threadMessageText, setThreadMessageText] = useState(''); +function ThreadMessageInput({ + activeChannelId, + activeThreadId, + isFullyAuthenticated, + sendMessageReducer, +}: ThreadMessageInputProps) { + const [threadMessageText, setThreadMessageText] = useState(""); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (!threadMessageText.trim() || !activeChannelId || !activeThreadId) return; - - sendMessageReducer({ text: threadMessageText, channelId: activeChannelId, threadId: activeThreadId }); - setThreadMessageText(''); + if (!threadMessageText.trim() || !activeChannelId || !activeThreadId) + return; + + sendMessageReducer({ + text: threadMessageText, + channelId: activeChannelId, + threadId: activeThreadId, + }); + setThreadMessageText(""); }; return ( -
+
setThreadMessageText(e.target.value)} /> diff --git a/src/chat/components/ThreadMessageList.tsx b/src/chat/components/ThreadMessageList.tsx index 776b656..ae48451 100644 --- a/src/chat/components/ThreadMessageList.tsx +++ b/src/chat/components/ThreadMessageList.tsx @@ -1,13 +1,16 @@ // src/chat/components/ThreadMessageList.tsx -import React, { useRef, useEffect } from 'react'; -import { Identity } from 'spacetimedb'; -import type * as Types from '../../module_bindings/types'; -import RichText from './RichText'; +import React, { useRef, useEffect } from "react"; +import { Identity } from "spacetimedb"; +import type * as Types from "../../module_bindings/types"; +import RichText from "./RichText"; // Helper function (extracted from App.tsx) -const getUsername = (userIdentity: Identity | null, users: readonly Types.User[]) => { +const getUsername = ( + userIdentity: Identity | null, + users: readonly Types.User[], +) => { if (!userIdentity) return "Unknown"; - const user = users.find(u => u.identity?.isEqual(userIdentity)); + const user = users.find((u) => u.identity?.isEqual(userIdentity)); return user?.name || userIdentity.toHexString().substring(0, 8); }; @@ -17,29 +20,54 @@ interface ThreadMessageListProps { identity: Identity | null; } -function ThreadMessageList({ threadMessages, users, identity }: ThreadMessageListProps) { +function ThreadMessageList({ + threadMessages, + users, + identity, +}: ThreadMessageListProps) { const threadMessagesEndRef = useRef(null); // Auto-scroll to bottom when messages change useEffect(() => { - threadMessagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + threadMessagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [threadMessages]); return ( -
- {threadMessages.map(msg => { +
+ {threadMessages.map((msg) => { const msgUsername = getUsername(msg.sender, users); - + return ( -
-
+
+
{msgUsername.substring(0, 2).toUpperCase()}
- {msgUsername} + + {msgUsername} +
-
+
diff --git a/src/chat/components/ThreadView.tsx b/src/chat/components/ThreadView.tsx index 044438f..4c38838 100644 --- a/src/chat/components/ThreadView.tsx +++ b/src/chat/components/ThreadView.tsx @@ -1,11 +1,11 @@ // src/chat/components/ThreadView.tsx -import React, { useState, useMemo } from 'react'; -import { Identity } from 'spacetimedb'; -import type * as Types from '../../module_bindings/types'; -import { useTable, useReducer } from 'spacetimedb/react'; // Assuming useTable and useReducer are available -import { tables, reducers } from '../../module_bindings'; -import ThreadMessageList from './ThreadMessageList'; -import ThreadMessageInput from './ThreadMessageInput'; +import React, { useState, useMemo } from "react"; +import { Identity } from "spacetimedb"; +import type * as Types from "../../module_bindings/types"; +import { useTable, useReducer } from "spacetimedb/react"; // Assuming useTable and useReducer are available +import { tables, reducers } from "../../module_bindings"; +import ThreadMessageList from "./ThreadMessageList"; +import ThreadMessageInput from "./ThreadMessageInput"; interface ThreadViewProps { activeThreadId: bigint; @@ -17,23 +17,35 @@ interface ThreadViewProps { identity: Identity | null; } -function ThreadView({ activeThreadId, setActiveThreadId, activeChannelId, activeServer, isFullyAuthenticated, users, identity }: ThreadViewProps) { - const sendMessageReducer = useReducer(useMemo(() => reducers.sendMessage, [])); // Assuming reducers are accessible +function ThreadView({ + activeThreadId, + setActiveThreadId, + activeChannelId, + activeServer, + isFullyAuthenticated, + users, + identity, +}: ThreadViewProps) { + const sendMessageReducer = useReducer( + useMemo(() => reducers.sendMessage, []), + ); // Assuming reducers are accessible // Fetch all threads and messages const [allThreads] = useTable(useMemo(() => tables.thread, [])); const [allMessages] = useTable(useMemo(() => tables.message, [])); - const activeThread = useMemo(() => - allThreads.find(t => t.id === activeThreadId), - [allThreads, activeThreadId] + const activeThread = useMemo( + () => allThreads.find((t) => t.id === activeThreadId), + [allThreads, activeThreadId], ); const threadMessages = useMemo(() => { if (!activeThreadId) return []; return allMessages - .filter(m => m.threadId === activeThreadId) - .sort((a, b) => a.sent.microsSinceUnixEpoch < b.sent.microsSinceUnixEpoch ? -1 : 1); + .filter((m) => m.threadId === activeThreadId) + .sort((a, b) => + a.sent.microsSinceUnixEpoch < b.sent.microsSinceUnixEpoch ? -1 : 1, + ); }, [allMessages, activeThreadId]); if (!activeThreadId || !activeThread) { @@ -42,21 +54,47 @@ function ThreadView({ activeThreadId, setActiveThreadId, activeChannelId, active return (
-
-
- setActiveThreadId(null)}>← - {activeThread.name} +
+
+ setActiveThreadId(null)} + > + ← + + + {activeThread.name} +
- +
- - - - + +
); diff --git a/src/chat/components/VideoGrid.tsx b/src/chat/components/VideoGrid.tsx index 5fd4fc8..795bb02 100644 --- a/src/chat/components/VideoGrid.tsx +++ b/src/chat/components/VideoGrid.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef } from 'react'; -import { Identity } from 'spacetimedb'; -import { useTable, useSpacetimeDB } from 'spacetimedb/react'; -import { tables } from '../../module_bindings'; -import * as Types from '../../module_bindings/types'; +import React, { useEffect, useRef } from "react"; +import { Identity } from "spacetimedb"; +import { useTable, useSpacetimeDB } from "spacetimedb/react"; +import { tables } from "../../module_bindings"; +import * as Types from "../../module_bindings/types"; interface VideoGridProps { peers: Map; @@ -13,18 +13,18 @@ interface VideoGridProps { watching: readonly Types.Watching[]; } -const VideoTile = ({ - identity, - stream, - isLocal, +const VideoTile = ({ + identity, + stream, + isLocal, isTalking, onToggleWatch, isWatching, isSharing, - isHero -}: { - identity: Identity; - stream?: MediaStream; + isHero, +}: { + identity: Identity; + stream?: MediaStream; isLocal?: boolean; isTalking?: boolean; onToggleWatch?: () => void; @@ -36,26 +36,31 @@ const VideoTile = ({ const containerRef = useRef(null); const [users] = useTable(tables.user); const [isMuted, setIsMuted] = React.useState(true); - const user = users.find(u => u.identity.isEqual(identity)); - const name = user?.name || user?.username || identity.toHexString().substring(0, 8); + const user = users.find((u) => u.identity.isEqual(identity)); + const name = + user?.name || user?.username || identity.toHexString().substring(0, 8); useEffect(() => { const video = videoRef.current; const shouldShow = isLocal || isWatching; - - console.log(`[VideoTile] ${name}: isLocal=${isLocal}, isWatching=${isWatching}, isSharing=${isSharing}, hasStream=${!!stream}, isMuted=${isMuted}`); + + console.log( + `[VideoTile] ${name}: isLocal=${isLocal}, isWatching=${isWatching}, isSharing=${isSharing}, hasStream=${!!stream}, isMuted=${isMuted}`, + ); if (video && stream && shouldShow) { if (video.srcObject !== stream) { - console.log(`[VideoTile] Linking stream to ${name} (${isLocal ? 'local' : 'remote'})`); + console.log( + `[VideoTile] Linking stream to ${name} (${isLocal ? "local" : "remote"})`, + ); video.srcObject = stream; } - - // Muted is usually required for autoplay, but if the user has interacted + + // Muted is usually required for autoplay, but if the user has interacted // (e.g. clicked unmute), we can change it. - video.muted = isMuted; - video.play().catch(err => { - if (err.name !== 'AbortError') { + video.muted = isMuted; + video.play().catch((err) => { + if (err.name !== "AbortError") { console.warn(`[VideoTile] Play failed for ${name}:`, err); } }); @@ -76,88 +81,112 @@ const VideoTile = ({ const toggleMute = (e: React.MouseEvent) => { e.stopPropagation(); - setIsMuted(prev => !prev); + setIsMuted((prev) => !prev); }; const showStream = (isLocal || isWatching) && stream; return ( -
{showStream ? ( <> -
); }; - - -export const VideoGrid: React.FC = ({ - peers, - localScreenStream, +export const VideoGrid: React.FC = ({ + peers, + localScreenStream, connectedChannelId, startWatching, stopWatching, - watching + watching, }) => { const { identity: localIdentity } = useSpacetimeDB(); const [voiceStates] = useTable(tables.voice_state); const [users] = useTable(tables.user); - const [focusedIdentity, setFocusedIdentity] = React.useState(null); - - const participants = voiceStates.filter(vs => vs.channelId === connectedChannelId); + const [focusedIdentity, setFocusedIdentity] = React.useState( + null, + ); + + const participants = voiceStates.filter( + (vs) => vs.channelId === connectedChannelId, + ); const isWatchingPeer = (peerIdHex: string) => { - return watching.some(w => - w.watcher.isEqual(localIdentity!) && w.watchee.toHexString() === peerIdHex + return watching.some( + (w) => + w.watcher.isEqual(localIdentity!) && + w.watchee.toHexString() === peerIdHex, ); }; @@ -168,15 +197,15 @@ export const VideoGrid: React.FC = ({ startWatching(peerIdentity); } }; - + const localSharing = !!localScreenStream; - const remoteSharerVs = participants.find(vs => { + const remoteSharerVs = participants.find((vs) => { if (vs.identity.isEqual(localIdentity!)) return false; return vs.isSharingScreen; }); - const defaultSharerIdentity = localSharing - ? localIdentity + const defaultSharerIdentity = localSharing + ? localIdentity : remoteSharerVs?.identity; const primarySharerIdentity = focusedIdentity || defaultSharerIdentity; @@ -185,15 +214,15 @@ export const VideoGrid: React.FC = ({ const isLocal = vs.identity.isEqual(localIdentity!); const peerIdHex = vs.identity.toHexString(); const peer = peers.get(peerIdHex); - const user = users.find(u => u.identity.isEqual(vs.identity)); + const user = users.find((u) => u.identity.isEqual(vs.identity)); const isHero = primarySharerIdentity?.isEqual(vs.identity); - + return ( -
setFocusedIdentity(vs.identity)} - style={{ cursor: 'pointer' }} + style={{ cursor: "pointer" }} > = ({ ); }; - const heroVs = participants.find(vs => vs.identity.isEqual(primarySharerIdentity || Identity.zero())); - const rowParticipants = participants.filter(vs => !vs.identity.isEqual(primarySharerIdentity || Identity.zero())); + const heroVs = participants.find((vs) => + vs.identity.isEqual(primarySharerIdentity || Identity.zero()), + ); + const rowParticipants = participants.filter( + (vs) => !vs.identity.isEqual(primarySharerIdentity || Identity.zero()), + ); return ( -
+
{primarySharerIdentity ? ( <> {heroVs && renderTile(heroVs)} {rowParticipants.length > 0 && (
- {rowParticipants.map(vs => renderTile(vs))} + {rowParticipants.map((vs) => renderTile(vs))}
)} ) : ( - participants.map(vs => renderTile(vs)) + participants.map((vs) => renderTile(vs)) )}
diff --git a/src/chat/index.ts b/src/chat/index.ts index fd024e1..7da2c34 100644 --- a/src/chat/index.ts +++ b/src/chat/index.ts @@ -1,3 +1,3 @@ // src/chat/index.ts -export { default as ChatContainer } from './ChatContainer'; -export { useChat } from './services/useChat'; +export { default as ChatContainer } from "./ChatContainer"; +export { useChat } from "./services/useChat"; diff --git a/src/chat/services/useChat.ts b/src/chat/services/useChat.ts index 1bb863c..010f80b 100644 --- a/src/chat/services/useChat.ts +++ b/src/chat/services/useChat.ts @@ -1,24 +1,30 @@ // src/chat/services/useChat.ts -import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; -import { useTable, useReducer, useSpacetimeDB } from 'spacetimedb/react'; -import { Identity } from 'spacetimedb'; -import * as Types from '../../module_bindings/types'; +import React, { + useState, + useMemo, + useEffect, + useCallback, + useRef, +} from "react"; +import { useTable, useReducer, useSpacetimeDB } from "spacetimedb/react"; +import { Identity } from "spacetimedb"; +import * as Types from "../../module_bindings/types"; // Import tables and reducers from module_bindings -import { tables, reducers } from '../../module_bindings'; +import { tables, reducers } from "../../module_bindings"; // Import the useAuth hook (assuming it's defined and exported from src/auth/index.ts or similar) import { useAuth } from "react-oidc-context"; -import { TOKEN_KEY } from '../../main.tsx'; // Import TOKEN_KEY for auth status +import { TOKEN_KEY } from "../../main.tsx"; // Import TOKEN_KEY for auth status // Helper functions (extracted from App.tsx) const getUsername = (userIdentity: Identity | null, users: Types.User[]) => { if (!userIdentity) return "Unknown"; - const user = users.find(u => u.identity?.isEqual(userIdentity)); + const user = users.find((u) => u.identity?.isEqual(userIdentity)); return user?.name || userIdentity.toHexString().substring(0, 8); }; const formatTime = (ts: any) => { const date = new Date(Number(ts.microsSinceUnixEpoch / 1000n)); - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; // Define interface for the hook's return value @@ -62,9 +68,21 @@ interface ChatState { voiceChannels: readonly Types.Channel[]; createServerReducer: (params: { name: string }) => void; - createChannelReducer: (params: { serverId: bigint, name: string, isVoice: boolean }) => void; - createThreadReducer: (params: { name: string, channelId: bigint, parentMessageId: bigint }) => void; - sendMessageReducer: (params: { channelId: bigint, text: string, threadId: bigint | undefined }) => void; + createChannelReducer: (params: { + serverId: bigint; + name: string; + isVoice: boolean; + }) => void; + createThreadReducer: (params: { + name: string; + channelId: bigint; + parentMessageId: bigint; + }) => void; + sendMessageReducer: (params: { + channelId: bigint; + text: string; + threadId: bigint | undefined; + }) => void; joinVoiceReducer: (params: { channelId: bigint }) => void; leaveVoiceReducer: () => void; setNameReducer: (params: { name: string }) => void; @@ -110,17 +128,17 @@ export function useChat(): ChatState { const [activeServerId, setActiveServerId] = useState(null); const [activeChannelId, setActiveChannelId] = useState(null); const [activeThreadId, setActiveThreadId] = useState(null); - const [messageText, setMessageText] = useState(''); - const [threadMessageText, setThreadMessageText] = useState(''); + const [messageText, setMessageText] = useState(""); + const [threadMessageText, setThreadMessageText] = useState(""); const [showCreateServerModal, setShowCreateServerModal] = useState(false); - const [newServerName, setNewServerName] = useState(''); + const [newServerName, setNewServerName] = useState(""); const [showCreateChannelModal, setShowCreateChannelModal] = useState(false); - const [newChannelName, setNewChannelName] = useState(''); + const [newChannelName, setNewChannelName] = useState(""); const [isVoiceChannel, setIsVoiceChannel] = useState(false); const [showSetNameModal, setShowSetNameModal] = useState(false); - const [newName, setNewName] = useState(''); + const [newName, setNewName] = useState(""); const [showDiscoveryModal, setShowDiscoveryModal] = useState(false); - const [authError, setAuthError] = useState(''); // Consider if this should be part of auth hook + const [authError, setAuthError] = useState(""); // Consider if this should be part of auth hook // Fetching data from tables const [servers] = useTable(useMemo(() => tables.server, [])); @@ -132,15 +150,25 @@ export function useChat(): ChatState { const [voiceStates] = useTable(useMemo(() => tables.voice_state, [])); // Reducers - const createServerReducer = useReducer(useMemo(() => reducers.createServer, [])); - const createChannelReducer = useReducer(useMemo(() => reducers.createChannel, [])); - const createThreadReducer = useReducer(useMemo(() => reducers.createThread, [])); - const sendMessageReducer = useReducer(useMemo(() => reducers.sendMessage, [])); + const createServerReducer = useReducer( + useMemo(() => reducers.createServer, []), + ); + const createChannelReducer = useReducer( + useMemo(() => reducers.createChannel, []), + ); + const createThreadReducer = useReducer( + useMemo(() => reducers.createThread, []), + ); + const sendMessageReducer = useReducer( + useMemo(() => reducers.sendMessage, []), + ); const joinVoiceReducer = useReducer(useMemo(() => reducers.joinVoice, [])); const leaveVoiceReducer = useReducer(useMemo(() => reducers.leaveVoice, [])); const setNameReducer = useReducer(useMemo(() => reducers.setName, [])); const joinServerReducer = useReducer(useMemo(() => reducers.joinServer, [])); - const leaveServerReducer = useReducer(useMemo(() => reducers.leaveServer, [])); + const leaveServerReducer = useReducer( + useMemo(() => reducers.leaveServer, []), + ); // Get current identity from SpacetimeDB const { identity } = useSpacetimeDB(); @@ -149,39 +177,49 @@ export function useChat(): ChatState { const isFullyAuthenticated = useMemo(() => { // If we have an identity, we are at least partially authenticated (guest) // but the app logic seems to want OIDC or username/password for "full" auth - const user = users.find(u => u.identity?.isEqual(identity || Identity.zero())); + const user = users.find((u) => + u.identity?.isEqual(identity || Identity.zero()), + ); if (!user) return false; const hasOidc = !!(user.issuer && user.subject); const hasCreds = !!(user.username && user.password); - console.log('useChat: isFullyAuthenticated check - hasOidc:', hasOidc, 'hasCreds:', hasCreds); + console.log( + "useChat: isFullyAuthenticated check - hasOidc:", + hasOidc, + "hasCreds:", + hasCreds, + ); return hasOidc || hasCreds; }, [users, identity]); // Logging fetched data and auth status - console.log('useChat: servers:', servers?.length); - console.log('useChat: channels:', channels?.length); - console.log('useChat: users:', users?.length, 'isUsersReady:', isUsersReady); - console.log('useChat: messages:', allMessages?.length); - console.log('useChat: threads:', allThreads?.length); - console.log('useChat: voiceStates:', voiceStates?.length); - console.log('useChat: auth.isAuthenticated:', auth.isAuthenticated); - console.log('useChat: auth.user:', auth.user); - console.log('useChat: identity:', identity?.toHexString()); - console.log('useChat: isFullyAuthenticated:', isFullyAuthenticated); - + console.log("useChat: servers:", servers?.length); + console.log("useChat: channels:", channels?.length); + console.log("useChat: users:", users?.length, "isUsersReady:", isUsersReady); + console.log("useChat: messages:", allMessages?.length); + console.log("useChat: threads:", allThreads?.length); + console.log("useChat: voiceStates:", voiceStates?.length); + console.log("useChat: auth.isAuthenticated:", auth.isAuthenticated); + console.log("useChat: auth.user:", auth.user); + console.log("useChat: identity:", identity?.toHexString()); + console.log("useChat: isFullyAuthenticated:", isFullyAuthenticated); // Initialization logic for active server/channel const joinedServerIds = useMemo(() => { if (!identity) return new Set(); - return new Set(serverMembers.filter(m => m.identity.isEqual(identity)).map(m => m.serverId)); + return new Set( + serverMembers + .filter((m) => m.identity.isEqual(identity)) + .map((m) => m.serverId), + ); }, [serverMembers, identity]); const joinedServers = useMemo(() => { - return servers.filter(s => joinedServerIds.has(s.id)); + return servers.filter((s) => joinedServerIds.has(s.id)); }, [servers, joinedServerIds]); const availableServers = useMemo(() => { - return servers.filter(s => !joinedServerIds.has(s.id)); + return servers.filter((s) => !joinedServerIds.has(s.id)); }, [servers, joinedServerIds]); useEffect(() => { @@ -192,9 +230,16 @@ export function useChat(): ChatState { useEffect(() => { if (activeServerId) { - const serverChannels = channels.filter(c => c.serverId === activeServerId && c.kind.tag === 'Text'); + const serverChannels = channels.filter( + (c) => c.serverId === activeServerId && c.kind.tag === "Text", + ); if (serverChannels.length > 0) { - if (!activeChannelId || !channels.some(c => c.id === activeChannelId && c.serverId === activeServerId)) { + if ( + !activeChannelId || + !channels.some( + (c) => c.id === activeChannelId && c.serverId === activeServerId, + ) + ) { setActiveChannelId(serverChannels[0].id); } } else { @@ -204,121 +249,178 @@ export function useChat(): ChatState { }, [activeServerId, channels, activeChannelId]); // Derived Data - const activeServer = useMemo(() => - servers.find(s => s.id === activeServerId), - [servers, activeServerId] + const activeServer = useMemo( + () => servers.find((s) => s.id === activeServerId), + [servers, activeServerId], ); - const activeChannel = useMemo(() => - channels.find(c => c.id === activeChannelId), - [channels, activeChannelId] + const activeChannel = useMemo( + () => channels.find((c) => c.id === activeChannelId), + [channels, activeChannelId], ); - const activeThread = useMemo(() => - allThreads.find(t => t.id === activeThreadId), - [allThreads, activeThreadId] + const activeThread = useMemo( + () => allThreads.find((t) => t.id === activeThreadId), + [allThreads, activeThreadId], ); - const isActiveChannelVoice = useMemo(() => activeChannel?.kind.tag === 'Voice', [activeChannel]); - const isActiveChannelText = useMemo(() => activeChannel?.kind.tag === 'Text', [activeChannel]); + const isActiveChannelVoice = useMemo( + () => activeChannel?.kind.tag === "Voice", + [activeChannel], + ); + const isActiveChannelText = useMemo( + () => activeChannel?.kind.tag === "Text", + [activeChannel], + ); const textChannels = useMemo(() => { - if (!activeServerId) return []; - return channels.filter(c => c.serverId === activeServerId && c.kind.tag === 'Text'); - }, [channels, activeServerId]); + if (!activeServerId) return []; + return channels.filter( + (c) => c.serverId === activeServerId && c.kind.tag === "Text", + ); + }, [channels, activeServerId]); const voiceChannels = useMemo(() => { - if (!activeServerId) return []; - return channels.filter(c => c.serverId === activeServerId && c.kind.tag === 'Voice'); - }, [channels, activeServerId]); + if (!activeServerId) return []; + return channels.filter( + (c) => c.serverId === activeServerId && c.kind.tag === "Voice", + ); + }, [channels, activeServerId]); const channelMessages = useMemo(() => { if (!activeChannelId) return []; return allMessages - .filter(m => m.channelId === activeChannelId && m.threadId === undefined) - .sort((a, b) => a.sent.microsSinceUnixEpoch < b.sent.microsSinceUnixEpoch ? -1 : 1); + .filter( + (m) => m.channelId === activeChannelId && m.threadId === undefined, + ) + .sort((a, b) => + a.sent.microsSinceUnixEpoch < b.sent.microsSinceUnixEpoch ? -1 : 1, + ); }, [allMessages, activeChannelId]); const threadMessages = useMemo(() => { if (!activeThreadId) return []; return allMessages - .filter(m => m.threadId === activeThreadId) - .sort((a, b) => a.sent.microsSinceUnixEpoch < b.sent.microsSinceUnixEpoch ? -1 : 1); + .filter((m) => m.threadId === activeThreadId) + .sort((a, b) => + a.sent.microsSinceUnixEpoch < b.sent.microsSinceUnixEpoch ? -1 : 1, + ); }, [allMessages, activeThreadId]); // Updated to use identity from useSpacetimeDB - const currentUser = useMemo(() => - users.find(u => u.identity?.isEqual(identity || Identity.zero())), - [users, identity] + const currentUser = useMemo( + () => users.find((u) => u.identity?.isEqual(identity || Identity.zero())), + [users, identity], ); // Updated to use identity from useSpacetimeDB - const currentVoiceState = useMemo(() => - voiceStates.find(vs => vs.identity?.isEqual(identity || Identity.zero())), - [voiceStates, identity] + const currentVoiceState = useMemo( + () => + voiceStates.find((vs) => + vs.identity?.isEqual(identity || Identity.zero()), + ), + [voiceStates, identity], ); - const connectedVoiceChannel = useMemo(() => - channels.find(c => c.id === currentVoiceState?.channelId), - [channels, currentVoiceState] + const connectedVoiceChannel = useMemo( + () => channels.find((c) => c.id === currentVoiceState?.channelId), + [channels, currentVoiceState], ); // Updated to use identity from useSpacetimeDB - const onlineUsers = useMemo(() => - users.filter(u => u.online || (identity?.isEqual(u.identity))), - [users, identity] + const onlineUsers = useMemo( + () => users.filter((u) => u.online || identity?.isEqual(u.identity)), + [users, identity], ); // Check if user has linked OIDC or username/password credentials // (Moved isFullyAuthenticated up earlier in the file already) // Event Handlers - const handleSendMessage = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (!messageText.trim() || !activeChannelId) return; - sendMessageReducer({ text: messageText, channelId: activeChannelId, threadId: undefined }); - setMessageText(''); - }, [messageText, activeChannelId, sendMessageReducer]); + const handleSendMessage = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!messageText.trim() || !activeChannelId) return; + sendMessageReducer({ + text: messageText, + channelId: activeChannelId, + threadId: undefined, + }); + setMessageText(""); + }, + [messageText, activeChannelId, sendMessageReducer], + ); - const handleSendThreadMessage = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (!threadMessageText.trim() || !activeThreadId || !activeChannelId) return; - sendMessageReducer({ text: threadMessageText, channelId: activeChannelId, threadId: activeThreadId }); - setThreadMessageText(''); - }, [threadMessageText, activeThreadId, activeChannelId, sendMessageReducer]); + const handleSendThreadMessage = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!threadMessageText.trim() || !activeThreadId || !activeChannelId) + return; + sendMessageReducer({ + text: threadMessageText, + channelId: activeChannelId, + threadId: activeThreadId, + }); + setThreadMessageText(""); + }, + [threadMessageText, activeThreadId, activeChannelId, sendMessageReducer], + ); - const handleCreateServer = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (!newServerName.trim()) return; - createServerReducer({ name: newServerName }); - setNewServerName(''); - setShowCreateServerModal(false); - }, [newServerName, createServerReducer]); + const handleCreateServer = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!newServerName.trim()) return; + createServerReducer({ name: newServerName }); + setNewServerName(""); + setShowCreateServerModal(false); + }, + [newServerName, createServerReducer], + ); - const handleCreateChannel = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (!newChannelName.trim() || !activeServerId) return; - createChannelReducer({ name: newChannelName, serverId: activeServerId, isVoice: isVoiceChannel }); - setNewChannelName(''); - setIsVoiceChannel(false); - setShowCreateChannelModal(false); - }, [newChannelName, activeServerId, isVoiceChannel, createChannelReducer]); + const handleCreateChannel = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!newChannelName.trim() || !activeServerId) return; + createChannelReducer({ + name: newChannelName, + serverId: activeServerId, + isVoice: isVoiceChannel, + }); + setNewChannelName(""); + setIsVoiceChannel(false); + setShowCreateChannelModal(false); + }, + [newChannelName, activeServerId, isVoiceChannel, createChannelReducer], + ); - const handleSetName = useCallback((e: React.FormEvent) => { - e.preventDefault(); - if (!newName.trim()) return; - setNameReducer({ name: newName }); - setShowSetNameModal(false); - }, [newName, setNameReducer]); + const handleSetName = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (!newName.trim()) return; + setNameReducer({ name: newName }); + setShowSetNameModal(false); + }, + [newName, setNameReducer], + ); - const handleStartThread = useCallback((msg: Types.Message) => { - const threadName = `Thread on: ${msg.text.substring(0, 20)}...`; - createThreadReducer({ name: threadName, channelId: msg.channelId, parentMessageId: msg.id }); - }, [createThreadReducer]); + const handleStartThread = useCallback( + (msg: Types.Message) => { + const threadName = `Thread on: ${msg.text.substring(0, 20)}...`; + createThreadReducer({ + name: threadName, + channelId: msg.channelId, + parentMessageId: msg.id, + }); + }, + [createThreadReducer], + ); - const handleJoinVoice = useCallback((channelId: bigint) => { - joinVoiceReducer({ channelId }); - }, [joinVoiceReducer]); + const handleJoinVoice = useCallback( + (channelId: bigint) => { + joinVoiceReducer({ channelId }); + }, + [joinVoiceReducer], + ); const handleLeaveVoice = useCallback(() => { if (currentVoiceState) { @@ -326,55 +428,123 @@ export function useChat(): ChatState { } }, [currentVoiceState, leaveVoiceReducer]); - const handleJoinServer = useCallback((serverId: bigint) => { - joinServerReducer({ serverId }); - setShowDiscoveryModal(false); - }, [joinServerReducer]); + const handleJoinServer = useCallback( + (serverId: bigint) => { + joinServerReducer({ serverId }); + setShowDiscoveryModal(false); + }, + [joinServerReducer], + ); - const handleLeaveServer = useCallback((serverId: bigint) => { - leaveServerReducer({ serverId }); - if (activeServerId === serverId) { - setActiveServerId(null); - } - }, [activeServerId, leaveServerReducer]); + const handleLeaveServer = useCallback( + (serverId: bigint) => { + leaveServerReducer({ serverId }); + if (activeServerId === serverId) { + setActiveServerId(null); + } + }, + [activeServerId, leaveServerReducer], + ); return { // State variables - activeServerId, activeChannelId, activeThreadId, messageText, threadMessageText, - showCreateServerModal, newServerName, showCreateChannelModal, newChannelName, - isVoiceChannel, showSetNameModal, newName, showDiscoveryModal, authError, + activeServerId, + activeChannelId, + activeThreadId, + messageText, + threadMessageText, + showCreateServerModal, + newServerName, + showCreateChannelModal, + newChannelName, + isVoiceChannel, + showSetNameModal, + newName, + showDiscoveryModal, + authError, // Data fetched from tables - servers, joinedServers, availableServers, channels, users, allMessages, allThreads, voiceStates, - currentVoiceState, connectedVoiceChannel, onlineUsers, + servers, + joinedServers, + availableServers, + channels, + users, + allMessages, + allThreads, + voiceStates, + currentVoiceState, + connectedVoiceChannel, + onlineUsers, activeServerMembers: useMemo(() => { if (!activeServerId) return []; - const memberIdentities = new Set(serverMembers.filter(m => m.serverId === activeServerId).map(m => m.identity.toHexString())); - return users.filter(u => memberIdentities.has(u.identity.toHexString())); + const memberIdentities = new Set( + serverMembers + .filter((m) => m.serverId === activeServerId) + .map((m) => m.identity.toHexString()), + ); + return users.filter((u) => + memberIdentities.has(u.identity.toHexString()), + ); }, [serverMembers, users, activeServerId]), currentUser, - activeServer, activeChannel, activeThread, isActiveChannelVoice, isActiveChannelText, channelMessages, threadMessages, - textChannels, voiceChannels, + activeServer, + activeChannel, + activeThread, + isActiveChannelVoice, + isActiveChannelText, + channelMessages, + threadMessages, + textChannels, + voiceChannels, // Reducers - createServerReducer, createChannelReducer, createThreadReducer, sendMessageReducer, - joinVoiceReducer, leaveVoiceReducer, setNameReducer, joinServerReducer, leaveServerReducer, + createServerReducer, + createChannelReducer, + createThreadReducer, + sendMessageReducer, + joinVoiceReducer, + leaveVoiceReducer, + setNameReducer, + joinServerReducer, + leaveServerReducer, // State setters - setActiveServerId, setActiveChannelId, setActiveThreadId, setMessageText, - setThreadMessageText, setShowCreateServerModal, setNewServerName, setShowCreateChannelModal, - setNewChannelName, setIsVoiceChannel, setShowSetNameModal, setNewName, setShowDiscoveryModal, setAuthError, + setActiveServerId, + setActiveChannelId, + setActiveThreadId, + setMessageText, + setThreadMessageText, + setShowCreateServerModal, + setNewServerName, + setShowCreateChannelModal, + setNewChannelName, + setIsVoiceChannel, + setShowSetNameModal, + setNewName, + setShowDiscoveryModal, + setAuthError, // Event handlers - handleSendMessage, handleSendThreadMessage, handleCreateServer, handleCreateChannel, - handleStartThread, handleJoinVoice, handleLeaveVoice, handleSetName, - handleJoinServer, handleLeaveServer, + handleSendMessage, + handleSendThreadMessage, + handleCreateServer, + handleCreateChannel, + handleStartThread, + handleJoinVoice, + handleLeaveVoice, + handleSetName, + handleJoinServer, + handleLeaveServer, // Derived status isFullyAuthenticated, // Helper functions - getUsername: useCallback((userIdentity: Identity | null) => getUsername(userIdentity, users as Types.User[]), [users]), + getUsername: useCallback( + (userIdentity: Identity | null) => + getUsername(userIdentity, users as Types.User[]), + [users], + ), formatTime: useCallback((ts: any) => formatTime(ts), []), }; } diff --git a/src/chat/services/webrtc/useChannelAudioWebRTC.ts b/src/chat/services/webrtc/useChannelAudioWebRTC.ts new file mode 100644 index 0000000..1251751 --- /dev/null +++ b/src/chat/services/webrtc/useChannelAudioWebRTC.ts @@ -0,0 +1,226 @@ +import { useEffect, useCallback, useMemo, useRef } from "react"; +import { Identity } from "spacetimedb"; +import { useTable, useReducer } from "spacetimedb/react"; +import { tables, reducers } from "../../../module_bindings"; +import { usePeerManager } from "./usePeerManager"; + +export const useChannelAudioWebRTC = ( + connectedChannelId: bigint | undefined, + identity: Identity | null, + localStream: MediaStream | null, + isDeafened: boolean +) => { + const [voiceStates] = useTable(tables.voice_state); + const [offers] = useTable(tables.voice_sdp_offer); + const [answers] = useTable(tables.voice_sdp_answer); + const [iceCandidates] = useTable(tables.voice_ice_candidate); + + const sendSdpOffer = useReducer(reducers.sendVoiceSdpOffer); + const sendSdpAnswer = useReducer(reducers.sendVoiceSdpAnswer); + const sendIceCandidate = useReducer(reducers.sendVoiceIceCandidate); + + const makingOfferRef = useRef>(new Map()); + const ignoreOfferRef = useRef>(new Map()); + const processedOffersRef = useRef>(new Set()); + const processedAnswersRef = useRef>(new Set()); + const processedCandidatesRef = useRef>(new Set()); + const candidateQueueRef = useRef>(new Map()); + + const connectedChannelIdRef = useRef(connectedChannelId); + useEffect(() => { connectedChannelIdRef.current = connectedChannelId; }, [connectedChannelId]); + + const drainCandidateQueue = useCallback(async (peerIdHex: string, pc: RTCPeerConnection) => { + const queue = candidateQueueRef.current.get(peerIdHex) || []; + if (queue.length === 0 || !pc.remoteDescription) return; + console.log(`[WebRTC][voice] Draining ${queue.length} candidates for ${peerIdHex}`); + for (const cand of queue) { + try { await pc.addIceCandidate(new RTCIceCandidate(cand)); } + catch (e) { console.warn(`[WebRTC][voice] Error adding queued ICE for ${peerIdHex}`, e); } + } + candidateQueueRef.current.set(peerIdHex, []); + }, []); + + const onNegotiationNeeded = useCallback(async (peerIdHex: string, pc: RTCPeerConnection) => { + const channelId = connectedChannelIdRef.current; + if (!channelId || pc.signalingState !== 'stable' || makingOfferRef.current.get(peerIdHex)) { + console.log(`[WebRTC][voice] Skipping negotiation for ${peerIdHex}: state=${pc.signalingState}, makingOffer=${makingOfferRef.current.get(peerIdHex)}`); + return; + } + try { + makingOfferRef.current.set(peerIdHex, true); + console.log(`[WebRTC][voice] Creating offer for ${peerIdHex}...`); + await pc.setLocalDescription(); + sendSdpOffer({ + receiver: Identity.fromString(peerIdHex), + sdp: JSON.stringify(pc.localDescription), + channelId + }); + } catch (e) { console.error(`[WebRTC][voice] Negotiation error for ${peerIdHex}`, e); } + finally { makingOfferRef.current.set(peerIdHex, false); } + }, [sendSdpOffer]); + + const onIceCandidate = useCallback((peerIdHex: string, candidate: RTCIceCandidate) => { + const channelId = connectedChannelIdRef.current; + if (channelId) { + sendIceCandidate({ + receiver: Identity.fromString(peerIdHex), + candidate: JSON.stringify(candidate), + channelId + }); + } + }, [sendIceCandidate]); + + const peerManager = usePeerManager( + identity, + "voice", + isDeafened, + onNegotiationNeeded, + onIceCandidate + ); + + // Handle Incoming Signaling + useEffect(() => { + if (!connectedChannelId || !identity) return; + + // Offers + const myOffers = offers.filter(o => o.receiver.isEqual(identity) && !o.sender.isEqual(identity) && o.channelId === connectedChannelId); + (async () => { + for (const offerRow of myOffers) { + if (processedOffersRef.current.has(offerRow.id)) continue; + processedOffersRef.current.add(offerRow.id); + + const peerIdHex = offerRow.sender.toHexString(); + console.log(`[WebRTC][voice] Received offer from ${peerIdHex}`); + const pc = peerManager.createPeerConnection(peerIdHex); + if (!pc) continue; + + try { + const isPolite = identity.toHexString() < peerIdHex; + const makingOffer = makingOfferRef.current.get(peerIdHex) || false; + const offerCollision = (pc.signalingState !== "stable") || makingOffer; + const ignoreOffer = !isPolite && offerCollision; + + ignoreOfferRef.current.set(peerIdHex, ignoreOffer); + if (ignoreOffer) { + console.log(`[WebRTC][voice] Ignoring offer collision from ${peerIdHex} (impolite)`); + continue; + } + + if (offerCollision) { + console.log(`[WebRTC][voice] Handling offer collision from ${peerIdHex} (polite), rolling back...`); + await pc.setLocalDescription({ type: "rollback" as RTCSdpType }); + } + + console.log(`[WebRTC][voice] Setting remote description from ${peerIdHex}`); + await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(offerRow.sdp))); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + console.log(`[WebRTC][voice] Sending answer to ${peerIdHex}`); + sendSdpAnswer({ receiver: offerRow.sender, sdp: JSON.stringify(answer), channelId: connectedChannelId }); + await drainCandidateQueue(peerIdHex, pc); + } catch (e) { console.error(`[WebRTC][voice] Error handling offer from ${peerIdHex}`, e); } + } + })(); + + // Answers + const myAnswers = answers.filter(a => a.receiver.isEqual(identity) && !a.sender.isEqual(identity) && a.channelId === connectedChannelId); + (async () => { + for (const answerRow of myAnswers) { + if (processedAnswersRef.current.has(answerRow.id)) continue; + processedAnswersRef.current.add(answerRow.id); + + const peerIdHex = answerRow.sender.toHexString(); + const peer = peerManager.getPeer(peerIdHex); + if (peer) { + try { + console.log(`[WebRTC][voice] Received answer from ${peerIdHex}, setting remote description`); + await peer.pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(answerRow.sdp))); + await drainCandidateQueue(peerIdHex, peer.pc); + } catch (e) { console.error(`[WebRTC][voice] Error handling answer from ${peerIdHex}`, e); } + } + } + })(); + + // ICE Candidates + const myCandidates = iceCandidates.filter(c => c.receiver.isEqual(identity) && !c.sender.isEqual(identity) && c.channelId === connectedChannelId); + (async () => { + for (const candRow of myCandidates) { + if (processedCandidatesRef.current.has(candRow.id)) continue; + processedCandidatesRef.current.add(candRow.id); + + const peerIdHex = candRow.sender.toHexString(); + const pc = peerManager.createPeerConnection(peerIdHex); + if (!pc) continue; + + try { + const candidate = JSON.parse(candRow.candidate); + const ignoreOffer = ignoreOfferRef.current.get(peerIdHex) || false; + + if (pc.remoteDescription) { + console.log(`[WebRTC][voice] Adding ICE candidate from ${peerIdHex}`); + await pc.addIceCandidate(new RTCIceCandidate(candidate)); + } else if (!ignoreOffer) { + console.log(`[WebRTC][voice] Queueing ICE candidate from ${peerIdHex}`); + const queue = candidateQueueRef.current.get(peerIdHex) || []; + queue.push(candidate); + candidateQueueRef.current.set(peerIdHex, queue); + } + } catch (e) { console.error(`[WebRTC][voice] Error handling ICE from ${peerIdHex}`, e); } + } + })(); + }, [offers, answers, iceCandidates, connectedChannelId, identity, peerManager, sendSdpAnswer, drainCandidateQueue]); + + // Track Syncing + useEffect(() => { + const audioTrack = localStream?.getAudioTracks()[0] || null; + peerManager.peersRef.current.forEach(async (peer, peerIdHex) => { + const transceivers = peer.pc.getTransceivers(); + if (transceivers[0] && transceivers[0].sender.track !== audioTrack) { + try { + console.log(`[WebRTC][voice] Syncing audio track for ${peerIdHex}`); + await transceivers[0].sender.replaceTrack(audioTrack); + if (peer.pc.signalingState === 'stable') onNegotiationNeeded(peerIdHex, peer.pc); + } catch (e) { console.error(`[WebRTC][voice] Error syncing track for ${peerIdHex}`, e); } + } + }); + }, [localStream, peerManager.peers, onNegotiationNeeded, peerManager.peersRef]); + + // Lifecycle + const voicePeersToConnect = useMemo(() => { + if (!identity || !connectedChannelId) return new Set(); + return new Set(voiceStates + .filter(vs => vs.channelId === connectedChannelId && !vs.identity.isEqual(identity)) + .map(vs => vs.identity.toHexString())); + }, [voiceStates, identity, connectedChannelId]); + + useEffect(() => { + if (!connectedChannelId || !identity) { + console.log(`[WebRTC][voice] Cleaning up connections (channel=${connectedChannelId}, identity=${!!identity})`); + peerManager.peersRef.current.forEach((_, id) => peerManager.closePeer(id)); + processedOffersRef.current.clear(); + processedAnswersRef.current.clear(); + processedCandidatesRef.current.clear(); + return; + } + + voicePeersToConnect.forEach(id => { + if (!peerManager.peersRef.current.has(id)) { + console.log(`[WebRTC][voice] Proactively connecting to ${id}`); + peerManager.createPeerConnection(id, [localStream?.getAudioTracks()[0] || null]); + } + }); + + peerManager.peersRef.current.forEach((_, id) => { + if (!voicePeersToConnect.has(id)) { + console.log(`[WebRTC][voice] Peer ${id} no longer in channel, closing`); + peerManager.closePeer(id); + } + }); + }, [voicePeersToConnect, connectedChannelId, identity, peerManager, localStream]); + + return { + peerStatuses: peerManager.peerStatuses, + peerStats: peerManager.peerStats, + peers: peerManager.peers + }; +}; diff --git a/src/chat/services/webrtc/useLocalMedia.ts b/src/chat/services/webrtc/useLocalMedia.ts index 4066f5a..df68eaa 100644 --- a/src/chat/services/webrtc/useLocalMedia.ts +++ b/src/chat/services/webrtc/useLocalMedia.ts @@ -4,7 +4,8 @@ import { reducers } from "../../../module_bindings"; export const useLocalMedia = () => { const [localStream, setLocalStream] = useState(null); - const [localScreenStream, setLocalScreenStream] = useState(null); + const [localScreenStream, setLocalScreenStream] = + useState(null); const [isMuted, setIsMuted] = useState(false); const [isDeafened, setIsDeafened] = useState(false); const [isTalking, setIsTalking] = useState(false); @@ -16,14 +17,17 @@ export const useLocalMedia = () => { const setTalking = useReducer(reducers.setTalking); const setSharingScreen = useReducer(reducers.setSharingScreen); - const toggleMute = useCallback(() => setIsMuted(prev => !prev), []); - const toggleDeafen = useCallback(() => setIsDeafened(prev => !prev), []); + const toggleMute = useCallback(() => setIsMuted((prev) => !prev), []); + const toggleDeafen = useCallback(() => setIsDeafened((prev) => !prev), []); const requestMic = useCallback(async () => { if (localStreamRef.current) return; try { console.log("[WebRTC] Requesting mic permission..."); - const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: false, + }); setLocalStream(stream); localStreamRef.current = stream; } catch (err) { @@ -33,44 +37,56 @@ export const useLocalMedia = () => { const releaseMic = useCallback(() => { if (localStreamRef.current) { - localStreamRef.current.getTracks().forEach(track => track.stop()); + localStreamRef.current.getTracks().forEach((track) => track.stop()); setLocalStream(null); localStreamRef.current = null; } }, []); - const startScreenShare = useCallback(async (onTrackReady: (track: MediaStreamTrack) => void) => { - try { - console.log("[WebRTC] Requesting screen share..."); - const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); - setLocalScreenStream(stream); - localScreenStreamRef.current = stream; - setSharingScreen({ sharing: true }); + const startScreenShare = useCallback( + async (onTrackReady: (track: MediaStreamTrack) => void) => { + try { + console.log("[WebRTC] Requesting screen share..."); + const stream = await navigator.mediaDevices.getDisplayMedia({ + video: true, + audio: true, + }); + setLocalScreenStream(stream); + localScreenStreamRef.current = stream; + setSharingScreen({ sharing: true }); - const videoTrack = stream.getVideoTracks()[0]; - if (videoTrack) { - onTrackReady(videoTrack); - videoTrack.onended = () => stopScreenShare(() => onTrackReady(null as any)); + const videoTrack = stream.getVideoTracks()[0]; + if (videoTrack) { + onTrackReady(videoTrack); + videoTrack.onended = () => + stopScreenShare(() => onTrackReady(null as any)); + } + } catch (err) { + console.error("[WebRTC] Failed to start screen share:", err); } - } catch (err) { - console.error("[WebRTC] Failed to start screen share:", err); - } - }, [setSharingScreen]); + }, + [setSharingScreen], + ); - const stopScreenShare = useCallback((onTrackCleared: (track: MediaStreamTrack | null) => void) => { - if (localScreenStreamRef.current) { - localScreenStreamRef.current.getTracks().forEach(track => track.stop()); - setLocalScreenStream(null); - localScreenStreamRef.current = null; - setSharingScreen({ sharing: false }); - onTrackCleared(null); - } - }, [setSharingScreen]); + const stopScreenShare = useCallback( + (onTrackCleared: (track: MediaStreamTrack | null) => void) => { + if (localScreenStreamRef.current) { + localScreenStreamRef.current + .getTracks() + .forEach((track) => track.stop()); + setLocalScreenStream(null); + localScreenStreamRef.current = null; + setSharingScreen({ sharing: false }); + onTrackCleared(null); + } + }, + [setSharingScreen], + ); // Handle Mute/Deafen effect on tracks useEffect(() => { if (localStreamRef.current) { - localStreamRef.current.getAudioTracks().forEach(track => { + localStreamRef.current.getAudioTracks().forEach((track) => { track.enabled = !isMuted && !isDeafened; }); } @@ -140,6 +156,6 @@ export const useLocalMedia = () => { requestMic, releaseMic, localStreamRef, - localScreenStreamRef + localScreenStreamRef, }; }; diff --git a/src/chat/services/webrtc/usePeerManager.ts b/src/chat/services/webrtc/usePeerManager.ts index 8900d13..2fe3a93 100644 --- a/src/chat/services/webrtc/usePeerManager.ts +++ b/src/chat/services/webrtc/usePeerManager.ts @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from "react"; +import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { Identity } from "spacetimedb"; import { Peer, WebRTCStats } from "./types"; @@ -8,166 +8,238 @@ const ICE_SERVERS: RTCConfiguration = { export const usePeerManager = ( identity: Identity | null, - isDeafened: boolean, - localStreamRef: React.MutableRefObject, - localScreenStreamRef: React.MutableRefObject, + mediaType: "voice" | "screen", + isDeafened: boolean, // Only relevant for voice onNegotiationNeeded: (peerIdHex: string, pc: RTCPeerConnection) => void, - onIceCandidate: (peerIdHex: string, candidate: RTCIceCandidate) => void + onIceCandidate: (peerIdHex: string, candidate: RTCIceCandidate) => void, ) => { const [peers, setPeers] = useState>(new Map()); - const [peerStatuses, setPeerStatuses] = useState>(new Map()); - const [peerStats, setPeerStats] = useState>(new Map()); + const [peerStatuses, setPeerStatuses] = useState>( + new Map(), + ); + const [peerStats, setPeerStats] = useState>( + new Map(), + ); const peersRef = useRef>(new Map()); const peerStatsRef = useRef>(new Map()); - const getPeer = useCallback((peerIdHex: string) => peersRef.current.get(peerIdHex), []); + // Use refs for callbacks to avoid re-creating PC when UI state changes + const onNegotiationNeededRef = useRef(onNegotiationNeeded); + const onIceCandidateRef = useRef(onIceCandidate); + const isDeafenedRef = useRef(isDeafened); - const createPeerConnection = useCallback((peerIdHex: string) => { - if (peersRef.current.has(peerIdHex)) return peersRef.current.get(peerIdHex)!.pc; - - if (identity && peerIdHex === identity.toHexString()) { - console.warn(`[WebRTC] Attempted to create a PeerConnection to self (${peerIdHex}). Ignoring.`); - return null as any; // Should not happen with proper filtering - } - - console.log(`[WebRTC] Creating new PeerConnection for ${peerIdHex}`); - - const pc = new RTCPeerConnection(ICE_SERVERS); - - // Bind handlers BEFORE adding transceivers to catch early negotiationneeded - pc.onnegotiationneeded = () => { - console.log(`[WebRTC] onnegotiationneeded fired for ${peerIdHex}`); - onNegotiationNeeded(peerIdHex, pc); - }; - - pc.onicecandidate = (event) => { - if (event.candidate) { - onIceCandidate(peerIdHex, event.candidate); - } - }; - - pc.oniceconnectionstatechange = () => { - console.log(`[WebRTC] ICE state for ${peerIdHex}: ${pc.iceConnectionState}`); - setPeerStatuses(prev => { - const next = new Map(prev); - next.set(peerIdHex, pc.iceConnectionState); - return next; - }); - - if (pc.iceConnectionState === 'failed') { - console.log(`[WebRTC] ICE failed for ${peerIdHex}, closing peer for retry`); - closePeer(peerIdHex); - } - }; - - pc.onconnectionstatechange = () => { - console.log(`[WebRTC] Connection state for ${peerIdHex}: ${pc.connectionState}`); - if (pc.connectionState === 'failed' || pc.connectionState === 'closed') { - console.log(`[WebRTC] Connection ${pc.connectionState} for ${peerIdHex}, cleaning up`); - closePeer(peerIdHex); - } - }; - - pc.ontrack = (event) => { - console.log(`[WebRTC] Received track from ${peerIdHex}: ${event.track.kind} (id: ${event.track.id})`); - setPeers(prev => { - const next = new Map(prev); - const existingPeer = { ...(next.get(peerIdHex) || { pc }) }; - - if (event.track.kind === 'audio') { - if (!existingPeer.audio) { - existingPeer.audio = new Audio(); - existingPeer.audio.autoplay = true; - existingPeer.audio.muted = isDeafened; - } - - const currentAudioStream = (existingPeer.audio.srcObject instanceof MediaStream) - ? existingPeer.audio.srcObject - : new MediaStream(); - - if (!currentAudioStream.getTracks().find(t => t.id === event.track.id)) { - currentAudioStream.addTrack(event.track); - } - - if (existingPeer.audio.srcObject !== currentAudioStream) { - existingPeer.audio.srcObject = currentAudioStream; - } - - existingPeer.audio.play().catch(e => { - if (e.name !== 'AbortError') console.error(`[WebRTC] Error playing audio for ${peerIdHex}`, e); - }); - } else if (event.track.kind === 'video') { - - const currentVideoStream = existingPeer.videoStream || new MediaStream(); - if (!currentVideoStream.getTracks().find(t => t.id === event.track.id)) { - currentVideoStream.addTrack(event.track); - } - // Force a new MediaStream object to trigger React re-render - existingPeer.videoStream = new MediaStream(currentVideoStream.getTracks()); - } - - next.set(peerIdHex, existingPeer); - peersRef.current = next; - return next; - }); - }; - - // Fixed transceivers for stability - pc.addTransceiver('audio', { direction: 'sendrecv' }); - pc.addTransceiver('video', { direction: 'sendrecv' }); - - // Initialize transceivers with local tracks if available - const transceivers = pc.getTransceivers(); - if (localStreamRef.current) { - const audioTrack = localStreamRef.current.getAudioTracks()[0]; - if (audioTrack) { - console.log(`[WebRTC] Attaching local audio track to new connection for ${peerIdHex}`); - transceivers[0].sender.replaceTrack(audioTrack); - } - } - if (localScreenStreamRef.current) { - const videoTrack = localScreenStreamRef.current.getVideoTracks()[0]; - if (videoTrack) { - console.log(`[WebRTC] Attaching local video track to new connection for ${peerIdHex}`); - transceivers[1].sender.replaceTrack(videoTrack); - } - } - - peersRef.current.set(peerIdHex, { pc }); - setPeers(new Map(peersRef.current)); - return pc; - }, [localStreamRef, localScreenStreamRef, onNegotiationNeeded, onIceCandidate]); - - - // Sync isDeafened state to all peer audio elements useEffect(() => { - peersRef.current.forEach(peer => { - if (peer.audio) { - peer.audio.muted = isDeafened; - } - }); + onNegotiationNeededRef.current = onNegotiationNeeded; + }, [onNegotiationNeeded]); + useEffect(() => { + onIceCandidateRef.current = onIceCandidate; + }, [onIceCandidate]); + useEffect(() => { + isDeafenedRef.current = isDeafened; }, [isDeafened]); - const closePeer = useCallback((peerIdHex: string) => { - - const peer = peersRef.current.get(peerIdHex); - if (peer) { - peer.pc.close(); - if (peer.audio) { - peer.audio.pause(); - peer.audio.srcObject = null; + const closePeer = useCallback( + (peerIdHex: string) => { + const peer = peersRef.current.get(peerIdHex); + if (peer) { + console.log( + `[WebRTC][${mediaType}] Closing peer connection for ${peerIdHex}`, + ); + peer.pc.close(); + if (peer.audio) { + peer.audio.pause(); + peer.audio.srcObject = null; + } + peersRef.current.delete(peerIdHex); + setPeers(new Map(peersRef.current)); + setPeerStatuses((prev) => { + const next = new Map(prev); + next.delete(peerIdHex); + return next; + }); } - peersRef.current.delete(peerIdHex); + }, + [mediaType], + ); + + const createPeerConnection = useCallback( + (peerIdHex: string, initialTracks: (MediaStreamTrack | null)[] = []) => { + if (peersRef.current.has(peerIdHex)) + return peersRef.current.get(peerIdHex)!.pc; + + if (identity && peerIdHex === identity.toHexString()) { + console.warn( + `[WebRTC][${mediaType}] Attempted to create a PeerConnection to self (${peerIdHex}). Ignoring.`, + ); + return null as any; + } + + console.log( + `[WebRTC][${mediaType}] Creating new PeerConnection for ${peerIdHex}`, + ); + const pc = new RTCPeerConnection(ICE_SERVERS); + + pc.onnegotiationneeded = () => { + console.log( + `[WebRTC][${mediaType}] onnegotiationneeded fired for ${peerIdHex}`, + ); + onNegotiationNeededRef.current(peerIdHex, pc); + }; + + pc.onicecandidate = (event) => { + if (event.candidate) { + onIceCandidateRef.current(peerIdHex, event.candidate); + } + }; + + pc.oniceconnectionstatechange = () => { + console.log( + `[WebRTC][${mediaType}] ICE state for ${peerIdHex}: ${pc.iceConnectionState}`, + ); + setPeerStatuses((prev) => { + const next = new Map(prev); + next.set(peerIdHex, pc.iceConnectionState); + return next; + }); + + if (pc.iceConnectionState === "failed") { + console.log( + `[WebRTC][${mediaType}] ICE failed for ${peerIdHex}, closing peer for retry`, + ); + closePeer(peerIdHex); + } + }; + + pc.onconnectionstatechange = () => { + console.log( + `[WebRTC][${mediaType}] Connection state for ${peerIdHex}: ${pc.connectionState}`, + ); + if ( + pc.connectionState === "failed" || + pc.connectionState === "closed" + ) { + console.log( + `[WebRTC][${mediaType}] Connection ${pc.connectionState} for ${peerIdHex}, cleaning up`, + ); + closePeer(peerIdHex); + } + }; + + pc.ontrack = (event) => { + console.log( + `[WebRTC][${mediaType}] Received track from ${peerIdHex}: ${event.track.kind} (id: ${event.track.id})`, + ); + setPeers((prev) => { + const next = new Map(prev); + const existingPeer = { ...(next.get(peerIdHex) || { pc }) }; + + if (event.track.kind === "audio") { + if (!existingPeer.audio) { + existingPeer.audio = new Audio(); + existingPeer.audio.autoplay = true; + existingPeer.audio.muted = + mediaType === "voice" ? isDeafenedRef.current : false; + } + + const currentAudioStream = + existingPeer.audio.srcObject instanceof MediaStream + ? existingPeer.audio.srcObject + : new MediaStream(); + + if ( + !currentAudioStream + .getTracks() + .find((t) => t.id === event.track.id) + ) { + currentAudioStream.addTrack(event.track); + } + + if (existingPeer.audio.srcObject !== currentAudioStream) { + existingPeer.audio.srcObject = currentAudioStream; + } + + existingPeer.audio.play().catch((e) => { + if (e.name !== "AbortError") + console.error( + `[WebRTC][${mediaType}] Error playing audio for ${peerIdHex}`, + e, + ); + }); + + if (mediaType === "screen") { + const currentVideoStream = + existingPeer.videoStream || new MediaStream(); + if ( + !currentVideoStream + .getTracks() + .find((t) => t.id === event.track.id) + ) { + currentVideoStream.addTrack(event.track); + } + existingPeer.videoStream = new MediaStream( + currentVideoStream.getTracks(), + ); + } + } else if (event.track.kind === "video") { + const currentVideoStream = + existingPeer.videoStream || new MediaStream(); + if ( + !currentVideoStream + .getTracks() + .find((t) => t.id === event.track.id) + ) { + currentVideoStream.addTrack(event.track); + } + existingPeer.videoStream = new MediaStream( + currentVideoStream.getTracks(), + ); + } + + next.set(peerIdHex, existingPeer); + peersRef.current = next; + return next; + }); + }; + + if (mediaType === "voice") { + pc.addTransceiver("audio", { direction: "sendrecv" }); + } else { + pc.addTransceiver("video", { direction: "sendrecv" }); + pc.addTransceiver("audio", { direction: "sendrecv" }); + } + + const transceivers = pc.getTransceivers(); + initialTracks.forEach((track, i) => { + if (track && transceivers[i]) { + console.log( + `[WebRTC][${mediaType}] Attaching initial track ${i} to ${peerIdHex}`, + ); + transceivers[i].sender.replaceTrack(track); + } + }); + + peersRef.current.set(peerIdHex, { pc }); setPeers(new Map(peersRef.current)); - setPeerStatuses(prev => { - const next = new Map(prev); - next.delete(peerIdHex); - return next; + return pc; + }, + [identity, mediaType, closePeer], + ); + + const getPeer = useCallback( + (peerIdHex: string) => peersRef.current.get(peerIdHex), + [], + ); + + useEffect(() => { + if (mediaType === "voice") { + peersRef.current.forEach((peer) => { + if (peer.audio) { + peer.audio.muted = isDeafened; + } }); } - }, []); + }, [isDeafened, mediaType]); - // Stats Polling useEffect(() => { if (peers.size === 0) { if (peerStatsRef.current.size > 0) { @@ -185,31 +257,42 @@ export const usePeerManager = ( const prevStats = peerStatsRef.current.get(peerIdHex); const currentStats: WebRTCStats = { audio: { bytesReceived: 0, jitter: 0, packetsLost: 0, bitrate: 0 }, - video: { bytesReceived: 0, frameWidth: 0, frameHeight: 0, framesPerSecond: 0, bitrate: 0 }, - timestamp: Date.now() + video: { + bytesReceived: 0, + frameWidth: 0, + frameHeight: 0, + framesPerSecond: 0, + bitrate: 0, + }, + timestamp: Date.now(), }; - stats.forEach(report => { - if (report.type === 'inbound-rtp') { + stats.forEach((report) => { + if (report.type === "inbound-rtp") { const kind = report.kind; - if (kind === 'audio' || kind === 'video') { - const target = kind === 'audio' ? currentStats.audio : currentStats.video; + if (kind === "audio" || kind === "video") { + const target = + kind === "audio" ? currentStats.audio : currentStats.video; target.bytesReceived = report.bytesReceived || 0; - if (kind === 'audio') { + if (kind === "audio") { currentStats.audio.jitter = report.jitter || 0; currentStats.audio.packetsLost = report.packetsLost || 0; } else { currentStats.video.frameWidth = report.frameWidth || 0; currentStats.video.frameHeight = report.frameHeight || 0; - currentStats.video.framesPerSecond = report.framesPerSecond || 0; + currentStats.video.framesPerSecond = + report.framesPerSecond || 0; } if (prevStats) { - const prevTarget = kind === 'audio' ? prevStats.audio : prevStats.video; - const deltaBytes = target.bytesReceived - prevTarget.bytesReceived; - const deltaTime = (currentStats.timestamp - prevStats.timestamp) / 1000; + const prevTarget = + kind === "audio" ? prevStats.audio : prevStats.video; + const deltaBytes = + target.bytesReceived - prevTarget.bytesReceived; + const deltaTime = + (currentStats.timestamp - prevStats.timestamp) / 1000; if (deltaTime > 0) { - target.bitrate = Math.max(0, deltaBytes * 8 / deltaTime); + target.bitrate = Math.max(0, (deltaBytes * 8) / deltaTime); } } } @@ -217,7 +300,10 @@ export const usePeerManager = ( }); newStats.set(peerIdHex, currentStats); } catch (e) { - console.warn(`[WebRTC] Failed to get stats for ${peerIdHex}`, e); + console.warn( + `[WebRTC][${mediaType}] Failed to get stats for ${peerIdHex}`, + e, + ); } } peerStatsRef.current = newStats; @@ -225,15 +311,18 @@ export const usePeerManager = ( }, 2000); return () => clearInterval(interval); - }, [peers]); + }, [peers, mediaType]); - return { - peers, - peerStatuses, - peerStats, - createPeerConnection, - closePeer, - getPeer, - peersRef - }; + return useMemo( + () => ({ + peers, + peerStatuses, + peerStats, + createPeerConnection, + closePeer, + getPeer, + peersRef, + }), + [peers, peerStatuses, peerStats, createPeerConnection, closePeer, getPeer], + ); }; diff --git a/src/chat/services/webrtc/useScreenSharingWebRTC.ts b/src/chat/services/webrtc/useScreenSharingWebRTC.ts new file mode 100644 index 0000000..2da8dc9 --- /dev/null +++ b/src/chat/services/webrtc/useScreenSharingWebRTC.ts @@ -0,0 +1,235 @@ +import { useEffect, useCallback, useMemo, useRef } from "react"; +import { Identity } from "spacetimedb"; +import { useTable, useReducer } from "spacetimedb/react"; +import { tables, reducers } from "../../../module_bindings"; +import { usePeerManager } from "./usePeerManager"; + +export const useScreenSharingWebRTC = ( + connectedChannelId: bigint | undefined, + identity: Identity | null, + localScreenStream: MediaStream | null +) => { + const [watching] = useTable(tables.watching); + const [offers] = useTable(tables.screen_sdp_offer); + const [answers] = useTable(tables.screen_sdp_answer); + const [iceCandidates] = useTable(tables.screen_ice_candidate); + + const sendSdpOffer = useReducer(reducers.sendScreenSdpOffer); + const sendSdpAnswer = useReducer(reducers.sendScreenSdpAnswer); + const sendIceCandidate = useReducer(reducers.sendScreenIceCandidate); + + const makingOfferRef = useRef>(new Map()); + const ignoreOfferRef = useRef>(new Map()); + const processedOffersRef = useRef>(new Set()); + const processedAnswersRef = useRef>(new Set()); + const processedCandidatesRef = useRef>(new Set()); + const candidateQueueRef = useRef>(new Map()); + + const connectedChannelIdRef = useRef(connectedChannelId); + useEffect(() => { connectedChannelIdRef.current = connectedChannelId; }, [connectedChannelId]); + + const drainCandidateQueue = useCallback(async (peerIdHex: string, pc: RTCPeerConnection) => { + const queue = candidateQueueRef.current.get(peerIdHex) || []; + if (queue.length === 0 || !pc.remoteDescription) return; + console.log(`[WebRTC][screen] Draining ${queue.length} candidates for ${peerIdHex}`); + for (const cand of queue) { + try { await pc.addIceCandidate(new RTCIceCandidate(cand)); } + catch (e) { console.warn(`[WebRTC][screen] Error adding queued ICE for ${peerIdHex}`, e); } + } + candidateQueueRef.current.set(peerIdHex, []); + }, []); + + const onNegotiationNeeded = useCallback(async (peerIdHex: string, pc: RTCPeerConnection) => { + const channelId = connectedChannelIdRef.current; + if (!channelId || pc.signalingState !== 'stable' || makingOfferRef.current.get(peerIdHex)) { + console.log(`[WebRTC][screen] Skipping negotiation for ${peerIdHex}: state=${pc.signalingState}, makingOffer=${makingOfferRef.current.get(peerIdHex)}`); + return; + } + try { + makingOfferRef.current.set(peerIdHex, true); + console.log(`[WebRTC][screen] Creating offer for ${peerIdHex}...`); + await pc.setLocalDescription(); + sendSdpOffer({ + receiver: Identity.fromString(peerIdHex), + sdp: JSON.stringify(pc.localDescription), + channelId + }); + } catch (e) { console.error(`[WebRTC][screen] Negotiation error for ${peerIdHex}`, e); } + finally { makingOfferRef.current.set(peerIdHex, false); } + }, [sendSdpOffer]); + + const onIceCandidate = useCallback((peerIdHex: string, candidate: RTCIceCandidate) => { + const channelId = connectedChannelIdRef.current; + if (channelId) { + sendIceCandidate({ + receiver: Identity.fromString(peerIdHex), + candidate: JSON.stringify(candidate), + channelId + }); + } + }, [sendIceCandidate]); + + const peerManager = usePeerManager( + identity, + "screen", + false, + onNegotiationNeeded, + onIceCandidate + ); + + // Signaling + useEffect(() => { + if (!connectedChannelId || !identity) return; + + const myOffers = offers.filter(o => o.receiver.isEqual(identity) && !o.sender.isEqual(identity) && o.channelId === connectedChannelId); + (async () => { + for (const offerRow of myOffers) { + if (processedOffersRef.current.has(offerRow.id)) continue; + processedOffersRef.current.add(offerRow.id); + + const peerIdHex = offerRow.sender.toHexString(); + console.log(`[WebRTC][screen] Received offer from ${peerIdHex}`); + const pc = peerManager.createPeerConnection(peerIdHex); + if (!pc) continue; + + try { + const isPolite = identity.toHexString() < peerIdHex; + const makingOffer = makingOfferRef.current.get(peerIdHex) || false; + const offerCollision = (pc.signalingState !== "stable") || makingOffer; + const ignoreOffer = !isPolite && offerCollision; + + ignoreOfferRef.current.set(peerIdHex, ignoreOffer); + if (ignoreOffer) { + console.log(`[WebRTC][screen] Ignoring offer collision from ${peerIdHex} (impolite)`); + continue; + } + + if (offerCollision) { + console.log(`[WebRTC][screen] Handling offer collision from ${peerIdHex} (polite), rolling back...`); + await pc.setLocalDescription({ type: "rollback" as RTCSdpType }); + } + + console.log(`[WebRTC][screen] Setting remote description from ${peerIdHex}`); + await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(offerRow.sdp))); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + console.log(`[WebRTC][screen] Sending answer to ${peerIdHex}`); + sendSdpAnswer({ receiver: offerRow.sender, sdp: JSON.stringify(answer), channelId: connectedChannelId }); + await drainCandidateQueue(peerIdHex, pc); + } catch (e) { console.error(`[WebRTC][screen] Error handling offer from ${peerIdHex}`, e); } + } + })(); + + const myAnswers = answers.filter(a => a.receiver.isEqual(identity) && !a.sender.isEqual(identity) && a.channelId === connectedChannelId); + (async () => { + for (const answerRow of myAnswers) { + if (processedAnswersRef.current.has(answerRow.id)) continue; + processedAnswersRef.current.add(answerRow.id); + + const peerIdHex = answerRow.sender.toHexString(); + const peer = peerManager.getPeer(peerIdHex); + if (peer) { + try { + console.log(`[WebRTC][screen] Received answer from ${peerIdHex}, setting remote description`); + await peer.pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(answerRow.sdp))); + await drainCandidateQueue(peerIdHex, peer.pc); + } catch (e) { console.error(`[WebRTC][screen] Error handling answer from ${peerIdHex}`, e); } + } + } + })(); + + const myCandidates = iceCandidates.filter(c => c.receiver.isEqual(identity) && !c.sender.isEqual(identity) && c.channelId === connectedChannelId); + (async () => { + for (const candRow of myCandidates) { + if (processedCandidatesRef.current.has(candRow.id)) continue; + processedCandidatesRef.current.add(candRow.id); + + const peerIdHex = candRow.sender.toHexString(); + const pc = peerManager.createPeerConnection(peerIdHex); + if (!pc) continue; + + try { + const candidate = JSON.parse(candRow.candidate); + const ignoreOffer = ignoreOfferRef.current.get(peerIdHex) || false; + + if (pc.remoteDescription) { + console.log(`[WebRTC][screen] Adding ICE candidate from ${peerIdHex}`); + await pc.addIceCandidate(new RTCIceCandidate(candidate)); + } else if (!ignoreOffer) { + console.log(`[WebRTC][screen] Queueing ICE candidate from ${peerIdHex}`); + const queue = candidateQueueRef.current.get(peerIdHex) || []; + queue.push(candidate); + candidateQueueRef.current.set(peerIdHex, queue); + } + } catch (e) { console.error(`[WebRTC][screen] Error handling ICE from ${peerIdHex}`, e); } + } + })(); + }, [offers, answers, iceCandidates, connectedChannelId, identity, peerManager, sendSdpAnswer, drainCandidateQueue]); + + // Track Syncing + useEffect(() => { + const videoTrack = localScreenStream?.getVideoTracks()[0] || null; + const audioTrack = localScreenStream?.getAudioTracks()[0] || null; + peerManager.peersRef.current.forEach(async (peer, peerIdHex) => { + const transceivers = peer.pc.getTransceivers(); + let changed = false; + if (transceivers[0] && transceivers[0].sender.track !== videoTrack) { + await transceivers[0].sender.replaceTrack(videoTrack); + changed = true; + } + if (transceivers[1] && transceivers[1].sender.track !== audioTrack) { + await transceivers[1].sender.replaceTrack(audioTrack); + changed = true; + } + if (changed && peer.pc.signalingState === 'stable') { + console.log(`[WebRTC][screen] Syncing track for ${peerIdHex}`); + onNegotiationNeeded(peerIdHex, peer.pc); + } + }); + }, [localScreenStream, peerManager.peers, onNegotiationNeeded, peerManager.peersRef]); + + // Lifecycle + const screenPeersToConnect = useMemo(() => { + if (!identity || !connectedChannelId) return new Set(); + const peerIds = new Set(); + watching.forEach(w => { + if (w.channelId === connectedChannelId) { + if (w.watcher.isEqual(identity)) peerIds.add(w.watchee.toHexString()); + else if (w.watchee.isEqual(identity)) peerIds.add(w.watcher.toHexString()); + } + }); + return peerIds; + }, [watching, identity, connectedChannelId]); + + useEffect(() => { + if (!connectedChannelId || !identity) { + console.log(`[WebRTC][screen] Cleaning up connections (channel=${connectedChannelId}, identity=${!!identity})`); + peerManager.peersRef.current.forEach((_, id) => peerManager.closePeer(id)); + processedOffersRef.current.clear(); + processedAnswersRef.current.clear(); + processedCandidatesRef.current.clear(); + return; + } + + screenPeersToConnect.forEach(id => { + if (!peerManager.peersRef.current.has(id)) { + console.log(`[WebRTC][screen] Connecting to watched peer ${id}`); + peerManager.createPeerConnection(id, [ + localScreenStream?.getVideoTracks()[0] || null, + localScreenStream?.getAudioTracks()[0] || null + ]); + } + }); + + peerManager.peersRef.current.forEach((_, id) => { + if (!screenPeersToConnect.has(id)) { + console.log(`[WebRTC][screen] Peer ${id} no longer watched, closing`); + peerManager.closePeer(id); + } + }); + }, [screenPeersToConnect, connectedChannelId, identity, peerManager, localScreenStream]); + + return { + peers: peerManager.peers + }; +}; diff --git a/src/chat/services/webrtc/useSignaling.ts b/src/chat/services/webrtc/useSignaling.ts index a025015..d371a58 100644 --- a/src/chat/services/webrtc/useSignaling.ts +++ b/src/chat/services/webrtc/useSignaling.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback } from "react"; +import { useEffect, useRef, useCallback, useMemo } from "react"; import { Identity } from "spacetimedb"; import { useTable, useReducer } from "spacetimedb/react"; import { tables, reducers } from "../../../module_bindings"; @@ -6,61 +6,71 @@ import { tables, reducers } from "../../../module_bindings"; export const useSignaling = ( identity: Identity | null, connectedChannelId: bigint | undefined, + mediaType: "voice" | "screen", createPeerConnection: (peerIdHex: string) => RTCPeerConnection, getPeer: (peerIdHex: string) => any, makingOfferRef: React.MutableRefObject>, - ignoreOfferRef: React.MutableRefObject> + ignoreOfferRef: React.MutableRefObject>, ) => { const [offers] = useTable(tables.sdp_offer); const [answers] = useTable(tables.sdp_answer); const [iceCandidates] = useTable(tables.ice_candidate); const sendSdpAnswer = useReducer(reducers.sendSdpAnswer); - + const processedOffersRef = useRef>(new Set()); const processedAnswersRef = useRef>(new Set()); const processedCandidatesRef = useRef>(new Set()); const candidateQueueRef = useRef>(new Map()); + const drainCandidateQueue = useCallback( + async (peerIdHex: string, pc: RTCPeerConnection) => { + const queue = candidateQueueRef.current.get(peerIdHex) || []; + if (queue.length === 0) return; - const drainCandidateQueue = useCallback(async (peerIdHex: string, pc: RTCPeerConnection) => { - const queue = candidateQueueRef.current.get(peerIdHex) || []; - if (queue.length === 0) return; - - // Safety: ensure we have a remote description before draining - if (!pc.remoteDescription) { - console.warn(`[WebRTC] Attempted to drain candidates for ${peerIdHex} but no remote description exists`); + if (!pc.remoteDescription) { + console.warn( + `[WebRTC][${mediaType}] Attempted to drain candidates for ${peerIdHex} but no remote description exists`, + ); return; - } - - console.log(`[WebRTC] Draining ${queue.length} queued candidates for ${peerIdHex}`); - for (const cand of queue) { - try { - await pc.addIceCandidate(new RTCIceCandidate(cand)); - } catch (e) { - console.warn(`[WebRTC] Error adding queued ICE for ${peerIdHex}`, e); } - } - candidateQueueRef.current.set(peerIdHex, []); - }, []); + + console.log( + `[WebRTC][${mediaType}] Draining ${queue.length} queued candidates for ${peerIdHex}`, + ); + for (const cand of queue) { + try { + await pc.addIceCandidate(new RTCIceCandidate(cand)); + } catch (e) { + console.warn( + `[WebRTC][${mediaType}] Error adding queued ICE for ${peerIdHex}`, + e, + ); + } + } + candidateQueueRef.current.set(peerIdHex, []); + }, + [mediaType], + ); // Handle Offers useEffect(() => { if (!connectedChannelId || !identity) return; - const myOffers = offers.filter(o => - o.receiver.isEqual(identity) && - !o.sender.isEqual(identity) && - o.channelId === connectedChannelId + const myOffers = offers.filter( + (o) => + o.receiver.isEqual(identity) && + !o.sender.isEqual(identity) && + o.channelId === connectedChannelId && + o.kind === mediaType, ); - + const processOffers = async () => { for (const offerRow of myOffers) { if (processedOffersRef.current.has(offerRow.id)) continue; - // Mark as processed immediately to prevent duplicate processing during async gaps processedOffersRef.current.add(offerRow.id); - + const peerIdHex = offerRow.sender.toHexString(); - console.log(`[WebRTC] Received offer from ${peerIdHex}`); + console.log(`[WebRTC][${mediaType}] Received offer from ${peerIdHex}`); const pc = createPeerConnection(peerIdHex); if (!pc) continue; const offer = JSON.parse(offerRow.sdp); @@ -68,109 +78,165 @@ export const useSignaling = ( try { const isPolite = identity.toHexString() < peerIdHex; const makingOffer = makingOfferRef.current.get(peerIdHex) || false; - const offerCollision = (pc.signalingState !== "stable") || makingOffer; + const offerCollision = pc.signalingState !== "stable" || makingOffer; const ignoreOffer = !isPolite && offerCollision; ignoreOfferRef.current.set(peerIdHex, ignoreOffer); if (ignoreOffer) { - console.log(`[WebRTC] Ignoring offer collision from ${peerIdHex} (Impolite)`); + console.log( + `[WebRTC][${mediaType}] Ignoring offer collision from ${peerIdHex} (Impolite)`, + ); continue; } if (offerCollision) { - console.log(`[WebRTC] Handling offer collision from ${peerIdHex} (Polite), rolling back...`); + console.log( + `[WebRTC][${mediaType}] Handling offer collision from ${peerIdHex} (Polite), rolling back...`, + ); await pc.setLocalDescription({ type: "rollback" as RTCSdpType }); } - console.log(`[WebRTC] Setting remote description for ${peerIdHex}`); + console.log( + `[WebRTC][${mediaType}] Setting remote description for ${peerIdHex}`, + ); await pc.setRemoteDescription(new RTCSessionDescription(offer)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); - console.log(`[WebRTC] Sending answer to ${peerIdHex}`); - sendSdpAnswer({ receiver: offerRow.sender, sdp: JSON.stringify(answer), channelId: connectedChannelId }); + console.log(`[WebRTC][${mediaType}] Sending answer to ${peerIdHex}`); + sendSdpAnswer({ + receiver: offerRow.sender, + sdp: JSON.stringify(answer), + channelId: connectedChannelId, + kind: mediaType, + }); await drainCandidateQueue(peerIdHex, pc); - } catch (e) { - console.error(`[WebRTC] Error handling offer from ${peerIdHex}`, e); + } catch (e) { + console.error( + `[WebRTC][${mediaType}] Error handling offer from ${peerIdHex}`, + e, + ); } } }; processOffers(); - }, [offers, connectedChannelId, identity, createPeerConnection, sendSdpAnswer, drainCandidateQueue]); + }, [ + offers, + connectedChannelId, + identity, + createPeerConnection, + sendSdpAnswer, + drainCandidateQueue, + mediaType, + makingOfferRef, + ignoreOfferRef, + ]); // Handle Answers useEffect(() => { if (!connectedChannelId || !identity) return; - const myAnswers = answers.filter(a => - a.receiver.isEqual(identity) && - !a.sender.isEqual(identity) && - a.channelId === connectedChannelId + const myAnswers = answers.filter( + (a) => + a.receiver.isEqual(identity) && + !a.sender.isEqual(identity) && + a.channelId === connectedChannelId && + a.kind === mediaType, ); - + const processAnswers = async () => { for (const answerRow of myAnswers) { if (processedAnswersRef.current.has(answerRow.id)) continue; processedAnswersRef.current.add(answerRow.id); - + const peerIdHex = answerRow.sender.toHexString(); const peer = getPeer(peerIdHex); if (peer) { try { - console.log(`[WebRTC] Received answer from ${peerIdHex}`); + console.log( + `[WebRTC][${mediaType}] Received answer from ${peerIdHex}`, + ); const answer = JSON.parse(answerRow.sdp); - await peer.pc.setRemoteDescription(new RTCSessionDescription(answer)); + await peer.pc.setRemoteDescription( + new RTCSessionDescription(answer), + ); await drainCandidateQueue(peerIdHex, peer.pc); - } catch (e) { console.error(`[WebRTC] Error handling answer from ${peerIdHex}`, e); } + } catch (e) { + console.error( + `[WebRTC][${mediaType}] Error handling answer from ${peerIdHex}`, + e, + ); + } } else { - console.warn(`[WebRTC] Received answer from ${peerIdHex} but no PeerConnection exists`); + console.warn( + `[WebRTC][${mediaType}] Received answer from ${peerIdHex} but no PeerConnection exists`, + ); } } }; processAnswers(); - }, [answers, connectedChannelId, identity, getPeer, drainCandidateQueue]); + }, [ + answers, + connectedChannelId, + identity, + getPeer, + drainCandidateQueue, + mediaType, + ]); // Handle ICE Candidates useEffect(() => { if (!connectedChannelId || !identity) return; - const myCandidates = iceCandidates.filter(c => - c.receiver.isEqual(identity) && - !c.sender.isEqual(identity) && - c.channelId === connectedChannelId + const myCandidates = iceCandidates.filter( + (c) => + c.receiver.isEqual(identity) && + !c.sender.isEqual(identity) && + c.channelId === connectedChannelId && + c.kind === mediaType, ); - + const processCandidates = async () => { for (const candRow of myCandidates) { if (processedCandidatesRef.current.has(candRow.id)) continue; processedCandidatesRef.current.add(candRow.id); const peerIdHex = candRow.sender.toHexString(); - // Ensure PeerConnection exists if we get a candidate const pc = createPeerConnection(peerIdHex); if (!pc) continue; - + try { const ignoreOffer = ignoreOfferRef.current.get(peerIdHex) || false; const candidate = JSON.parse(candRow.candidate); if (pc.remoteDescription) { - console.log(`[WebRTC] Adding ICE candidate from ${peerIdHex}`); + console.log( + `[WebRTC][${mediaType}] Adding ICE candidate from ${peerIdHex}`, + ); await pc.addIceCandidate(new RTCIceCandidate(candidate)); } else if (!ignoreOffer) { - console.log(`[WebRTC] Queueing ICE candidate from ${peerIdHex}`); + console.log( + `[WebRTC][${mediaType}] Queueing ICE candidate from ${peerIdHex}`, + ); const queue = candidateQueueRef.current.get(peerIdHex) || []; queue.push(candidate); candidateQueueRef.current.set(peerIdHex, queue); - } else { - console.log(`[WebRTC] Ignoring ICE candidate from ${peerIdHex} (ignoreOffer=true)`); } - } catch (e) { console.error(`[WebRTC] Error handling ICE from ${peerIdHex}`, e); } + } catch (e) { + console.error( + `[WebRTC][${mediaType}] Error handling ICE from ${peerIdHex}`, + e, + ); + } } }; processCandidates(); - }, [iceCandidates, connectedChannelId, identity, createPeerConnection]); - - - + }, [ + iceCandidates, + connectedChannelId, + identity, + createPeerConnection, + mediaType, + ignoreOfferRef, + ]); const clearSignalingState = useCallback(() => { processedOffersRef.current.clear(); @@ -179,10 +245,12 @@ export const useSignaling = ( makingOfferRef.current.clear(); ignoreOfferRef.current.clear(); candidateQueueRef.current.clear(); - }, []); + }, [makingOfferRef, ignoreOfferRef]); - return { - makingOfferRef, - clearSignalingState - }; + return useMemo( + () => ({ + clearSignalingState, + }), + [clearSignalingState], + ); }; diff --git a/src/chat/services/webrtc/useWebRTC.ts b/src/chat/services/webrtc/useWebRTC.ts index 67b2c54..2d9cd6d 100644 --- a/src/chat/services/webrtc/useWebRTC.ts +++ b/src/chat/services/webrtc/useWebRTC.ts @@ -1,30 +1,18 @@ -import { useEffect, useCallback, useMemo, useRef } from "react"; +import { useCallback } from "react"; import { Identity } from "spacetimedb"; import { useTable, useReducer, useSpacetimeDB } from "spacetimedb/react"; import { tables, reducers } from "../../../module_bindings"; import { useLocalMedia } from "./useLocalMedia"; -import { usePeerManager } from "./usePeerManager"; -import { useSignaling } from "./useSignaling"; +import { useChannelAudioWebRTC } from "./useChannelAudioWebRTC"; +import { useScreenSharingWebRTC } from "./useScreenSharingWebRTC"; export const useWebRTC = (connectedChannelId: bigint | undefined) => { const { identity } = useSpacetimeDB(); - const [voiceStates] = useTable(tables.voice_state); const [watching] = useTable(tables.watching); - - const sendSdpOffer = useReducer(reducers.sendSdpOffer); - const sendIceCandidate = useReducer(reducers.sendIceCandidate); + const startWatchingReducer = useReducer(reducers.startWatching); const stopWatchingReducer = useReducer(reducers.stopWatching); - // Refs for signaling state to avoid circular dependencies and stale closures - const makingOfferRef = useRef>(new Map()); - const ignoreOfferRef = useRef>(new Map()); - const connectedChannelIdRef = useRef(connectedChannelId); - - useEffect(() => { - connectedChannelIdRef.current = connectedChannelId; - }, [connectedChannelId]); - const { localStream, localScreenStream, @@ -38,179 +26,29 @@ export const useWebRTC = (connectedChannelId: bigint | undefined) => { stopScreenShare: stopLocalScreenShare, requestMic, releaseMic, - localStreamRef, - localScreenStreamRef } = useLocalMedia(); - const onNegotiationNeeded = useCallback(async (peerIdHex: string, pc: RTCPeerConnection) => { - // Always check the LATEST channel ID from ref - const channelId = connectedChannelIdRef.current; - const isMakingOffer = makingOfferRef.current.get(peerIdHex); - - if (!channelId || pc.signalingState !== 'stable' || isMakingOffer) { - console.log(`[WebRTC] Skipping negotiation for ${peerIdHex}: channel=${!!channelId}, state=${pc.signalingState}, makingOffer=${isMakingOffer}`); - return; - } - - try { - makingOfferRef.current.set(peerIdHex, true); - console.log(`[WebRTC] Negotiation needed for ${peerIdHex}, creating offer...`); - await pc.setLocalDescription(); - console.log(`[WebRTC] Sending offer to ${peerIdHex}`); - sendSdpOffer({ - receiver: Identity.fromString(peerIdHex), - sdp: JSON.stringify(pc.localDescription), - channelId - }); - } catch (e) { - console.error(`[WebRTC] Error during negotiation for ${peerIdHex}`, e); - } finally { - makingOfferRef.current.set(peerIdHex, false); - } - }, [sendSdpOffer]); - - - const onIceCandidate = useCallback((peerIdHex: string, candidate: RTCIceCandidate) => { - const channelId = connectedChannelIdRef.current; - if (channelId) { - sendIceCandidate({ - receiver: Identity.fromString(peerIdHex), - candidate: JSON.stringify(candidate), - channelId - }); - } - }, [sendIceCandidate]); - - const { - peers, - peerStatuses, - peerStats, - createPeerConnection, - closePeer, - getPeer, - peersRef - } = usePeerManager( - identity, - isDeafened, - localStreamRef, - localScreenStreamRef, - onNegotiationNeeded, - onIceCandidate - ); - - const { - clearSignalingState - } = useSignaling( - identity, + // --- Specialized Hooks --- + const voice = useChannelAudioWebRTC( connectedChannelId, - createPeerConnection, - getPeer, - makingOfferRef, - ignoreOfferRef + identity, + localStream, + isDeafened ); - // Sync local media to existing peers - useEffect(() => { - const audioTrack = localStream?.getAudioTracks()[0] || null; - peersRef.current.forEach(async (peer, peerIdHex) => { - const transceivers = peer.pc.getTransceivers(); - if (transceivers[0] && transceivers[0].sender.track !== audioTrack) { - console.log(`[WebRTC] Syncing audio track to peer ${peerIdHex}`); - try { - await transceivers[0].sender.replaceTrack(audioTrack); - if (peer.pc.signalingState === 'stable') { - onNegotiationNeeded(peerIdHex, peer.pc); - } - } catch (e) { - console.error(`[WebRTC] Error replacing audio track for ${peerIdHex}`, e); - } - } - }); - }, [localStream, peers, onNegotiationNeeded]); + const screen = useScreenSharingWebRTC( + connectedChannelId, + identity, + localScreenStream + ); - useEffect(() => { - const videoTrack = localScreenStream?.getVideoTracks()[0] || null; - peersRef.current.forEach(async (peer, peerIdHex) => { - const transceivers = peer.pc.getTransceivers(); - if (transceivers[1] && transceivers[1].sender.track !== videoTrack) { - console.log(`[WebRTC] Syncing video track to peer ${peerIdHex}`); - try { - await transceivers[1].sender.replaceTrack(videoTrack); - if (peer.pc.signalingState === 'stable') { - onNegotiationNeeded(peerIdHex, peer.pc); - } - } catch (e) { - console.error(`[WebRTC] Error replacing video track for ${peerIdHex}`, e); - } - } - }); - }, [localScreenStream, peers, onNegotiationNeeded]); - - - - // Determine who to connect to - const peersToConnect = useMemo(() => { - if (!identity || !connectedChannelId) return new Set(); - const peerIds = new Set(); - voiceStates.forEach(vs => { - if (vs.channelId === connectedChannelId && !vs.identity.isEqual(identity)) { - peerIds.add(vs.identity.toHexString()); - } - }); - watching.forEach(w => { - if (w.watcher.isEqual(identity)) { - peerIds.add(w.watchee.toHexString()); - } else if (w.watchee.isEqual(identity)) { - peerIds.add(w.watcher.toHexString()); - } - }); - return peerIds; - }, [voiceStates, watching, identity, connectedChannelId]); - - // Peer Lifecycle Orchestration - useEffect(() => { - if (!connectedChannelId || !identity) { - // Cleanup all - if (peersRef.current.size > 0) { - console.log("[WebRTC] Cleaning up all peer connections"); - peersRef.current.forEach((_, peerIdHex) => closePeer(peerIdHex)); - } - releaseMic(); - clearSignalingState(); - return; - } - - // Always clear signaling state when connectedChannelId changes to avoid stale row processing - clearSignalingState(); - - // Connect to new peers - peersToConnect.forEach(peerIdHex => { - if (!peersRef.current.has(peerIdHex)) { - createPeerConnection(peerIdHex); - } - }); - - // Cleanup disconnected peers - peersRef.current.forEach((_, peerIdHex) => { - if (!peersToConnect.has(peerIdHex)) { - closePeer(peerIdHex); - } - }); - - requestMic(); - }, [peersToConnect, connectedChannelId, identity, createPeerConnection, closePeer, requestMic, releaseMic, clearSignalingState]); - - // Screen Share Actions + // --- Actions --- const startScreenShare = useCallback(() => { - startLocalScreenShare((track) => { - // Handled by localScreenStream effect - }); + startLocalScreenShare(() => {}); }, [startLocalScreenShare]); const stopScreenShare = useCallback(() => { - stopLocalScreenShare((track) => { - // Handled by localScreenStream effect - }); + stopLocalScreenShare(() => {}); }, [stopLocalScreenShare]); const startWatching = useCallback((peerIdentity: Identity) => { @@ -226,8 +64,8 @@ export const useWebRTC = (connectedChannelId: bigint | undefined) => { return { localStream, localScreenStream, - peerStatuses, - peers, + peerStatuses: voice.peerStatuses, + peers: screen.peers, // For VideoGrid to show streams startScreenShare, stopScreenShare, isSharingScreen, @@ -238,9 +76,8 @@ export const useWebRTC = (connectedChannelId: bigint | undefined) => { isDeafened, toggleMute, toggleDeafen, - peerStats + peerStats: voice.peerStats }; }; export default useWebRTC; - diff --git a/src/index.css b/src/index.css index 12a6ed0..e86186e 100644 --- a/src/index.css +++ b/src/index.css @@ -33,15 +33,15 @@ body, body { font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', - 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: - source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } /* ----- Buttons ----- */ diff --git a/src/main.tsx b/src/main.tsx index d9eb728..49adb2b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode, useMemo } from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import App from './App.tsx'; -import { Identity } from 'spacetimedb'; -import { SpacetimeDBProvider } from 'spacetimedb/react'; -import { DbConnection, ErrorContext } from './module_bindings/index.ts'; +import { StrictMode, useMemo } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; +import { Identity } from "spacetimedb"; +import { SpacetimeDBProvider } from "spacetimedb/react"; +import { DbConnection, ErrorContext } from "./module_bindings/index.ts"; import { OidcProvider } from "./auth"; // Import from index.ts import { AuthGate } from "./auth"; // Import from index.ts @@ -12,24 +12,26 @@ import { AuthGate } from "./auth"; // Import from index.ts // which it does. SpacetimeDBWrapper relies on useAuth to get the OIDC token. import { useAuth } from "react-oidc-context"; -const HOST = import.meta.env.VITE_SPACETIMEDB_HOST ?? 'wss://maincloud.spacetimedb.com'; -const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? 'my-spacetime-app-jdhdg'; +const HOST = + import.meta.env.VITE_SPACETIMEDB_HOST ?? "wss://maincloud.spacetimedb.com"; +const DB_NAME = + import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? "my-spacetime-app-jdhdg"; export const TOKEN_KEY = `${HOST}/${DB_NAME}/auth_token`; const onConnect = (conn: DbConnection, identity: Identity, token: string) => { localStorage.setItem(TOKEN_KEY, token); console.log( - 'Connected to SpacetimeDB with identity:', - identity.toHexString() + "Connected to SpacetimeDB with identity:", + identity.toHexString(), ); }; const onDisconnect = () => { - console.log('Disconnected from SpacetimeDB'); + console.log("Disconnected from SpacetimeDB"); }; const onConnectError = (_ctx: ErrorContext, err: Error) => { - console.log('Error connecting to SpacetimeDB:', err); + console.log("Error connecting to SpacetimeDB:", err); }; // This component remains responsible for the SpacetimeDB connection logic. @@ -38,11 +40,14 @@ function SpacetimeDBWrapper({ children }: { children: React.ReactNode }) { const auth = useAuth(); // Logging authentication and token status - console.log('SpacetimeDBWrapper: auth.isLoading:', auth.isLoading); - console.log('SpacetimeDBWrapper: auth.isAuthenticated:', auth.isAuthenticated); - console.log('SpacetimeDBWrapper: auth.user?.id_token:', auth.user?.id_token); + console.log("SpacetimeDBWrapper: auth.isLoading:", auth.isLoading); + console.log( + "SpacetimeDBWrapper: auth.isAuthenticated:", + auth.isAuthenticated, + ); + console.log("SpacetimeDBWrapper: auth.user?.id_token:", auth.user?.id_token); const storedToken = localStorage.getItem(TOKEN_KEY); - console.log('SpacetimeDBWrapper: localStorage TOKEN_KEY:', storedToken); + console.log("SpacetimeDBWrapper: localStorage TOKEN_KEY:", storedToken); const connectionBuilder = useMemo(() => { const builder = DbConnection.builder() @@ -58,16 +63,19 @@ function SpacetimeDBWrapper({ children }: { children: React.ReactNode }) { console.log("SpacetimeDBWrapper: Connecting with OIDC token"); return builder.withToken(auth.user.id_token); } else if (storedToken) { - console.log("SpacetimeDBWrapper: Connecting with stored SpacetimeDB token"); + console.log( + "SpacetimeDBWrapper: Connecting with stored SpacetimeDB token", + ); return builder.withToken(storedToken); } else { - console.log("SpacetimeDBWrapper: No token available, proceeding without."); + console.log( + "SpacetimeDBWrapper: No token available, proceeding without.", + ); return builder; // Proceed without a token if none is available } - }, [auth.isAuthenticated, auth.user?.id_token, storedToken]); // Include storedToken in dependencies - console.log('SpacetimeDBWrapper: connectionBuilder created.'); + console.log("SpacetimeDBWrapper: connectionBuilder created."); return ( @@ -76,7 +84,7 @@ function SpacetimeDBWrapper({ children }: { children: React.ReactNode }) { ); } -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( @@ -86,5 +94,5 @@ createRoot(document.getElementById('root')!).render( - + , ); diff --git a/src/module_bindings/channel_table.ts b/src/module_bindings/channel_table.ts index ef6e429..c805434 100644 --- a/src/module_bindings/channel_table.ts +++ b/src/module_bindings/channel_table.ts @@ -9,10 +9,7 @@ import { type AlgebraicTypeType as __AlgebraicTypeType, type Infer as __Infer, } from "spacetimedb"; -import { - ChannelKind, -} from "./types"; - +import { ChannelKind } from "./types"; export default __t.row({ id: __t.u64().primaryKey(), diff --git a/src/module_bindings/index.ts b/src/module_bindings/index.ts index a07528c..748b150 100644 --- a/src/module_bindings/index.ts +++ b/src/module_bindings/index.ts @@ -43,10 +43,13 @@ import LeaveServerReducer from "./leave_server_reducer"; import LeaveVoiceReducer from "./leave_voice_reducer"; import LoginReducer from "./login_reducer"; import RegisterReducer from "./register_reducer"; -import SendIceCandidateReducer from "./send_ice_candidate_reducer"; import SendMessageReducer from "./send_message_reducer"; -import SendSdpAnswerReducer from "./send_sdp_answer_reducer"; -import SendSdpOfferReducer from "./send_sdp_offer_reducer"; +import SendScreenIceCandidateReducer from "./send_screen_ice_candidate_reducer"; +import SendScreenSdpAnswerReducer from "./send_screen_sdp_answer_reducer"; +import SendScreenSdpOfferReducer from "./send_screen_sdp_offer_reducer"; +import SendVoiceIceCandidateReducer from "./send_voice_ice_candidate_reducer"; +import SendVoiceSdpAnswerReducer from "./send_voice_sdp_answer_reducer"; +import SendVoiceSdpOfferReducer from "./send_voice_sdp_offer_reducer"; import SetNameReducer from "./set_name_reducer"; import SetSharingScreenReducer from "./set_sharing_screen_reducer"; import SetTalkingReducer from "./set_talking_reducer"; @@ -57,14 +60,17 @@ import StopWatchingReducer from "./stop_watching_reducer"; // Import all table schema definitions import ChannelRow from "./channel_table"; -import IceCandidateRow from "./ice_candidate_table"; import MessageRow from "./message_table"; -import SdpAnswerRow from "./sdp_answer_table"; -import SdpOfferRow from "./sdp_offer_table"; +import ScreenIceCandidateRow from "./screen_ice_candidate_table"; +import ScreenSdpAnswerRow from "./screen_sdp_answer_table"; +import ScreenSdpOfferRow from "./screen_sdp_offer_table"; import ServerRow from "./server_table"; import ServerMemberRow from "./server_member_table"; import ThreadRow from "./thread_table"; import UserRow from "./user_table"; +import VoiceIceCandidateRow from "./voice_ice_candidate_table"; +import VoiceSdpAnswerRow from "./voice_sdp_answer_table"; +import VoiceSdpOfferRow from "./voice_sdp_offer_table"; import VoiceStateRow from "./voice_state_table"; import WatchingRow from "./watching_table"; @@ -72,176 +78,413 @@ import WatchingRow from "./watching_table"; /** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ const tablesSchema = __schema({ - channel: __table({ - name: 'channel', - indexes: [ - { accessor: 'id', name: 'channel_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'by_server_id', name: 'channel_server_id_idx_btree', algorithm: 'btree', columns: [ - 'serverId', - ] }, - ], - constraints: [ - { name: 'channel_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, ChannelRow), - ice_candidate: __table({ - name: 'ice_candidate', - indexes: [ - { accessor: 'id', name: 'ice_candidate_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'by_receiver', name: 'ice_candidate_receiver_idx_btree', algorithm: 'btree', columns: [ - 'receiver', - ] }, - { accessor: 'by_sender', name: 'ice_candidate_sender_idx_btree', algorithm: 'btree', columns: [ - 'sender', - ] }, - ], - constraints: [ - { name: 'ice_candidate_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, IceCandidateRow), - message: __table({ - name: 'message', - indexes: [ - { accessor: 'by_channel_id', name: 'message_channel_id_idx_btree', algorithm: 'btree', columns: [ - 'channelId', - ] }, - { accessor: 'id', name: 'message_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'by_thread_id', name: 'message_thread_id_idx_btree', algorithm: 'btree', columns: [ - 'threadId', - ] }, - ], - constraints: [ - { name: 'message_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, MessageRow), - sdp_answer: __table({ - name: 'sdp_answer', - indexes: [ - { accessor: 'id', name: 'sdp_answer_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'by_receiver', name: 'sdp_answer_receiver_idx_btree', algorithm: 'btree', columns: [ - 'receiver', - ] }, - { accessor: 'by_sender', name: 'sdp_answer_sender_idx_btree', algorithm: 'btree', columns: [ - 'sender', - ] }, - ], - constraints: [ - { name: 'sdp_answer_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, SdpAnswerRow), - sdp_offer: __table({ - name: 'sdp_offer', - indexes: [ - { accessor: 'id', name: 'sdp_offer_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'by_receiver', name: 'sdp_offer_receiver_idx_btree', algorithm: 'btree', columns: [ - 'receiver', - ] }, - { accessor: 'by_sender', name: 'sdp_offer_sender_idx_btree', algorithm: 'btree', columns: [ - 'sender', - ] }, - ], - constraints: [ - { name: 'sdp_offer_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, SdpOfferRow), - server: __table({ - name: 'server', - indexes: [ - { accessor: 'id', name: 'server_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - ], - constraints: [ - { name: 'server_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, ServerRow), - server_member: __table({ - name: 'server_member', - indexes: [ - { accessor: 'id', name: 'server_member_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'by_identity', name: 'server_member_identity_idx_btree', algorithm: 'btree', columns: [ - 'identity', - ] }, - { accessor: 'by_server_id', name: 'server_member_server_id_idx_btree', algorithm: 'btree', columns: [ - 'serverId', - ] }, - ], - constraints: [ - { name: 'server_member_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, ServerMemberRow), - thread: __table({ - name: 'thread', - indexes: [ - { accessor: 'by_channel_id', name: 'thread_channel_id_idx_btree', algorithm: 'btree', columns: [ - 'channelId', - ] }, - { accessor: 'id', name: 'thread_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'parent_message_id', name: 'thread_parent_message_id_idx_btree', algorithm: 'btree', columns: [ - 'parentMessageId', - ] }, - ], - constraints: [ - { name: 'thread_id_key', constraint: 'unique', columns: ['id'] }, - { name: 'thread_parent_message_id_key', constraint: 'unique', columns: ['parentMessageId'] }, - ], - }, ThreadRow), - user: __table({ - name: 'user', - indexes: [ - { accessor: 'identity', name: 'user_identity_idx_btree', algorithm: 'btree', columns: [ - 'identity', - ] }, - ], - constraints: [ - { name: 'user_identity_key', constraint: 'unique', columns: ['identity'] }, - ], - }, UserRow), - voice_state: __table({ - name: 'voice_state', - indexes: [ - { accessor: 'by_channel_id', name: 'voice_state_channel_id_idx_btree', algorithm: 'btree', columns: [ - 'channelId', - ] }, - { accessor: 'identity', name: 'voice_state_identity_idx_btree', algorithm: 'btree', columns: [ - 'identity', - ] }, - ], - constraints: [ - { name: 'voice_state_identity_key', constraint: 'unique', columns: ['identity'] }, - ], - }, VoiceStateRow), - watching: __table({ - name: 'watching', - indexes: [ - { accessor: 'id', name: 'watching_id_idx_btree', algorithm: 'btree', columns: [ - 'id', - ] }, - { accessor: 'by_watchee', name: 'watching_watchee_idx_btree', algorithm: 'btree', columns: [ - 'watchee', - ] }, - { accessor: 'by_watcher', name: 'watching_watcher_idx_btree', algorithm: 'btree', columns: [ - 'watcher', - ] }, - ], - constraints: [ - { name: 'watching_id_key', constraint: 'unique', columns: ['id'] }, - ], - }, WatchingRow), + channel: __table( + { + name: "channel", + indexes: [ + { + accessor: "id", + name: "channel_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_server_id", + name: "channel_server_id_idx_btree", + algorithm: "btree", + columns: ["serverId"], + }, + ], + constraints: [ + { name: "channel_id_key", constraint: "unique", columns: ["id"] }, + ], + }, + ChannelRow, + ), + message: __table( + { + name: "message", + indexes: [ + { + accessor: "by_channel_id", + name: "message_channel_id_idx_btree", + algorithm: "btree", + columns: ["channelId"], + }, + { + accessor: "id", + name: "message_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_thread_id", + name: "message_thread_id_idx_btree", + algorithm: "btree", + columns: ["threadId"], + }, + ], + constraints: [ + { name: "message_id_key", constraint: "unique", columns: ["id"] }, + ], + }, + MessageRow, + ), + screen_ice_candidate: __table( + { + name: "screen_ice_candidate", + indexes: [ + { + accessor: "id", + name: "screen_ice_candidate_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_receiver", + name: "screen_ice_candidate_receiver_idx_btree", + algorithm: "btree", + columns: ["receiver"], + }, + { + accessor: "by_sender", + name: "screen_ice_candidate_sender_idx_btree", + algorithm: "btree", + columns: ["sender"], + }, + ], + constraints: [ + { + name: "screen_ice_candidate_id_key", + constraint: "unique", + columns: ["id"], + }, + ], + }, + ScreenIceCandidateRow, + ), + screen_sdp_answer: __table( + { + name: "screen_sdp_answer", + indexes: [ + { + accessor: "id", + name: "screen_sdp_answer_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_receiver", + name: "screen_sdp_answer_receiver_idx_btree", + algorithm: "btree", + columns: ["receiver"], + }, + { + accessor: "by_sender", + name: "screen_sdp_answer_sender_idx_btree", + algorithm: "btree", + columns: ["sender"], + }, + ], + constraints: [ + { + name: "screen_sdp_answer_id_key", + constraint: "unique", + columns: ["id"], + }, + ], + }, + ScreenSdpAnswerRow, + ), + screen_sdp_offer: __table( + { + name: "screen_sdp_offer", + indexes: [ + { + accessor: "id", + name: "screen_sdp_offer_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_receiver", + name: "screen_sdp_offer_receiver_idx_btree", + algorithm: "btree", + columns: ["receiver"], + }, + { + accessor: "by_sender", + name: "screen_sdp_offer_sender_idx_btree", + algorithm: "btree", + columns: ["sender"], + }, + ], + constraints: [ + { + name: "screen_sdp_offer_id_key", + constraint: "unique", + columns: ["id"], + }, + ], + }, + ScreenSdpOfferRow, + ), + server: __table( + { + name: "server", + indexes: [ + { + accessor: "id", + name: "server_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + ], + constraints: [ + { name: "server_id_key", constraint: "unique", columns: ["id"] }, + ], + }, + ServerRow, + ), + server_member: __table( + { + name: "server_member", + indexes: [ + { + accessor: "id", + name: "server_member_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_identity", + name: "server_member_identity_idx_btree", + algorithm: "btree", + columns: ["identity"], + }, + { + accessor: "by_server_id", + name: "server_member_server_id_idx_btree", + algorithm: "btree", + columns: ["serverId"], + }, + ], + constraints: [ + { name: "server_member_id_key", constraint: "unique", columns: ["id"] }, + ], + }, + ServerMemberRow, + ), + thread: __table( + { + name: "thread", + indexes: [ + { + accessor: "by_channel_id", + name: "thread_channel_id_idx_btree", + algorithm: "btree", + columns: ["channelId"], + }, + { + accessor: "id", + name: "thread_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "parent_message_id", + name: "thread_parent_message_id_idx_btree", + algorithm: "btree", + columns: ["parentMessageId"], + }, + ], + constraints: [ + { name: "thread_id_key", constraint: "unique", columns: ["id"] }, + { + name: "thread_parent_message_id_key", + constraint: "unique", + columns: ["parentMessageId"], + }, + ], + }, + ThreadRow, + ), + user: __table( + { + name: "user", + indexes: [ + { + accessor: "identity", + name: "user_identity_idx_btree", + algorithm: "btree", + columns: ["identity"], + }, + ], + constraints: [ + { + name: "user_identity_key", + constraint: "unique", + columns: ["identity"], + }, + ], + }, + UserRow, + ), + voice_ice_candidate: __table( + { + name: "voice_ice_candidate", + indexes: [ + { + accessor: "id", + name: "voice_ice_candidate_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_receiver", + name: "voice_ice_candidate_receiver_idx_btree", + algorithm: "btree", + columns: ["receiver"], + }, + { + accessor: "by_sender", + name: "voice_ice_candidate_sender_idx_btree", + algorithm: "btree", + columns: ["sender"], + }, + ], + constraints: [ + { + name: "voice_ice_candidate_id_key", + constraint: "unique", + columns: ["id"], + }, + ], + }, + VoiceIceCandidateRow, + ), + voice_sdp_answer: __table( + { + name: "voice_sdp_answer", + indexes: [ + { + accessor: "id", + name: "voice_sdp_answer_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_receiver", + name: "voice_sdp_answer_receiver_idx_btree", + algorithm: "btree", + columns: ["receiver"], + }, + { + accessor: "by_sender", + name: "voice_sdp_answer_sender_idx_btree", + algorithm: "btree", + columns: ["sender"], + }, + ], + constraints: [ + { + name: "voice_sdp_answer_id_key", + constraint: "unique", + columns: ["id"], + }, + ], + }, + VoiceSdpAnswerRow, + ), + voice_sdp_offer: __table( + { + name: "voice_sdp_offer", + indexes: [ + { + accessor: "id", + name: "voice_sdp_offer_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_receiver", + name: "voice_sdp_offer_receiver_idx_btree", + algorithm: "btree", + columns: ["receiver"], + }, + { + accessor: "by_sender", + name: "voice_sdp_offer_sender_idx_btree", + algorithm: "btree", + columns: ["sender"], + }, + ], + constraints: [ + { + name: "voice_sdp_offer_id_key", + constraint: "unique", + columns: ["id"], + }, + ], + }, + VoiceSdpOfferRow, + ), + voice_state: __table( + { + name: "voice_state", + indexes: [ + { + accessor: "by_channel_id", + name: "voice_state_channel_id_idx_btree", + algorithm: "btree", + columns: ["channelId"], + }, + { + accessor: "identity", + name: "voice_state_identity_idx_btree", + algorithm: "btree", + columns: ["identity"], + }, + ], + constraints: [ + { + name: "voice_state_identity_key", + constraint: "unique", + columns: ["identity"], + }, + ], + }, + VoiceStateRow, + ), + watching: __table( + { + name: "watching", + indexes: [ + { + accessor: "id", + name: "watching_id_idx_btree", + algorithm: "btree", + columns: ["id"], + }, + { + accessor: "by_watchee", + name: "watching_watchee_idx_btree", + algorithm: "btree", + columns: ["watchee"], + }, + { + accessor: "by_watcher", + name: "watching_watcher_idx_btree", + algorithm: "btree", + columns: ["watcher"], + }, + ], + constraints: [ + { name: "watching_id_key", constraint: "unique", columns: ["id"] }, + ], + }, + WatchingRow, + ), }); /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ @@ -255,10 +498,13 @@ const reducersSchema = __reducers( __reducerSchema("leave_voice", LeaveVoiceReducer), __reducerSchema("login", LoginReducer), __reducerSchema("register", RegisterReducer), - __reducerSchema("send_ice_candidate", SendIceCandidateReducer), __reducerSchema("send_message", SendMessageReducer), - __reducerSchema("send_sdp_answer", SendSdpAnswerReducer), - __reducerSchema("send_sdp_offer", SendSdpOfferReducer), + __reducerSchema("send_screen_ice_candidate", SendScreenIceCandidateReducer), + __reducerSchema("send_screen_sdp_answer", SendScreenSdpAnswerReducer), + __reducerSchema("send_screen_sdp_offer", SendScreenSdpOfferReducer), + __reducerSchema("send_voice_ice_candidate", SendVoiceIceCandidateReducer), + __reducerSchema("send_voice_sdp_answer", SendVoiceSdpAnswerReducer), + __reducerSchema("send_voice_sdp_offer", SendVoiceSdpOfferReducer), __reducerSchema("set_name", SetNameReducer), __reducerSchema("set_sharing_screen", SetSharingScreenReducer), __reducerSchema("set_talking", SetTalkingReducer), @@ -267,8 +513,7 @@ const reducersSchema = __reducers( ); /** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ -const proceduresSchema = __procedures( -); +const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { @@ -285,24 +530,33 @@ const REMOTE_MODULE = { >; /** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ -export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType); +export const tables: __QueryBuilder = + __makeQueryBuilder(tablesSchema.schemaType); /** The reducers available in this remote SpacetimeDB module. */ -export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); +export const reducers = __convertToAccessorMap( + reducersSchema.reducersType.reducers, +); /** The context type returned in callbacks for all possible events. */ export type EventContext = __EventContextInterface; /** The context type returned in callbacks for reducer events. */ -export type ReducerEventContext = __ReducerEventContextInterface; +export type ReducerEventContext = __ReducerEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for subscription events. */ -export type SubscriptionEventContext = __SubscriptionEventContextInterface; +export type SubscriptionEventContext = __SubscriptionEventContextInterface< + typeof REMOTE_MODULE +>; /** The context type returned in callbacks for error events. */ export type ErrorContext = __ErrorContextInterface; /** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ export type SubscriptionHandle = __SubscriptionHandleImpl; /** Builder class to configure a new subscription to the remote SpacetimeDB instance. */ -export class SubscriptionBuilder extends __SubscriptionBuilderImpl {} +export class SubscriptionBuilder extends __SubscriptionBuilderImpl< + typeof REMOTE_MODULE +> {} /** Builder class to configure a new database connection to the remote SpacetimeDB instance. */ export class DbConnectionBuilder extends __DbConnectionBuilder {} @@ -311,7 +565,11 @@ export class DbConnectionBuilder extends __DbConnectionBuilder {} export class DbConnection extends __DbConnectionImpl { /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ static builder = (): DbConnectionBuilder => { - return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig) => new DbConnection(config)); + return new DbConnectionBuilder( + REMOTE_MODULE, + (config: __DbConnectionConfig) => + new DbConnection(config), + ); }; /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */ @@ -319,4 +577,3 @@ export class DbConnection extends __DbConnectionImpl { return new SubscriptionBuilder(this); }; } - diff --git a/src/module_bindings/ice_candidate_table.ts b/src/module_bindings/screen_ice_candidate_table.ts similarity index 100% rename from src/module_bindings/ice_candidate_table.ts rename to src/module_bindings/screen_ice_candidate_table.ts diff --git a/src/module_bindings/sdp_answer_table.ts b/src/module_bindings/screen_sdp_answer_table.ts similarity index 100% rename from src/module_bindings/sdp_answer_table.ts rename to src/module_bindings/screen_sdp_answer_table.ts diff --git a/src/module_bindings/sdp_offer_table.ts b/src/module_bindings/screen_sdp_offer_table.ts similarity index 100% rename from src/module_bindings/sdp_offer_table.ts rename to src/module_bindings/screen_sdp_offer_table.ts diff --git a/src/module_bindings/send_ice_candidate_reducer.ts b/src/module_bindings/send_screen_ice_candidate_reducer.ts similarity index 100% rename from src/module_bindings/send_ice_candidate_reducer.ts rename to src/module_bindings/send_screen_ice_candidate_reducer.ts diff --git a/src/module_bindings/send_sdp_answer_reducer.ts b/src/module_bindings/send_screen_sdp_answer_reducer.ts similarity index 100% rename from src/module_bindings/send_sdp_answer_reducer.ts rename to src/module_bindings/send_screen_sdp_answer_reducer.ts diff --git a/src/module_bindings/send_sdp_offer_reducer.ts b/src/module_bindings/send_screen_sdp_offer_reducer.ts similarity index 100% rename from src/module_bindings/send_sdp_offer_reducer.ts rename to src/module_bindings/send_screen_sdp_offer_reducer.ts diff --git a/src/module_bindings/send_voice_ice_candidate_reducer.ts b/src/module_bindings/send_voice_ice_candidate_reducer.ts new file mode 100644 index 0000000..7e4c0d8 --- /dev/null +++ b/src/module_bindings/send_voice_ice_candidate_reducer.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + receiver: __t.identity(), + candidate: __t.string(), + channelId: __t.u64(), +}; diff --git a/src/module_bindings/send_voice_sdp_answer_reducer.ts b/src/module_bindings/send_voice_sdp_answer_reducer.ts new file mode 100644 index 0000000..629d0f5 --- /dev/null +++ b/src/module_bindings/send_voice_sdp_answer_reducer.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + receiver: __t.identity(), + sdp: __t.string(), + channelId: __t.u64(), +}; diff --git a/src/module_bindings/send_voice_sdp_offer_reducer.ts b/src/module_bindings/send_voice_sdp_offer_reducer.ts new file mode 100644 index 0000000..629d0f5 --- /dev/null +++ b/src/module_bindings/send_voice_sdp_offer_reducer.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + receiver: __t.identity(), + sdp: __t.string(), + channelId: __t.u64(), +}; diff --git a/src/module_bindings/types.ts b/src/module_bindings/types.ts index 250dbd6..ac7de2c 100644 --- a/src/module_bindings/types.ts +++ b/src/module_bindings/types.ts @@ -27,15 +27,6 @@ export const ChannelKind = __t.enum("ChannelKind", { }); export type ChannelKind = __Infer; -export const IceCandidate = __t.object("IceCandidate", { - id: __t.u64(), - sender: __t.identity(), - receiver: __t.identity(), - candidate: __t.string(), - channelId: __t.u64(), -}); -export type IceCandidate = __Infer; - export const Message = __t.object("Message", { id: __t.u64(), sender: __t.identity(), @@ -46,23 +37,32 @@ export const Message = __t.object("Message", { }); export type Message = __Infer; -export const SdpAnswer = __t.object("SdpAnswer", { +export const ScreenIceCandidate = __t.object("ScreenIceCandidate", { id: __t.u64(), sender: __t.identity(), receiver: __t.identity(), - sdp: __t.string(), + candidate: __t.string(), channelId: __t.u64(), }); -export type SdpAnswer = __Infer; +export type ScreenIceCandidate = __Infer; -export const SdpOffer = __t.object("SdpOffer", { +export const ScreenSdpAnswer = __t.object("ScreenSdpAnswer", { id: __t.u64(), sender: __t.identity(), receiver: __t.identity(), sdp: __t.string(), channelId: __t.u64(), }); -export type SdpOffer = __Infer; +export type ScreenSdpAnswer = __Infer; + +export const ScreenSdpOffer = __t.object("ScreenSdpOffer", { + id: __t.u64(), + sender: __t.identity(), + receiver: __t.identity(), + sdp: __t.string(), + channelId: __t.u64(), +}); +export type ScreenSdpOffer = __Infer; export const Server = __t.object("Server", { id: __t.u64(), @@ -98,6 +98,33 @@ export const User = __t.object("User", { }); export type User = __Infer; +export const VoiceIceCandidate = __t.object("VoiceIceCandidate", { + id: __t.u64(), + sender: __t.identity(), + receiver: __t.identity(), + candidate: __t.string(), + channelId: __t.u64(), +}); +export type VoiceIceCandidate = __Infer; + +export const VoiceSdpAnswer = __t.object("VoiceSdpAnswer", { + id: __t.u64(), + sender: __t.identity(), + receiver: __t.identity(), + sdp: __t.string(), + channelId: __t.u64(), +}); +export type VoiceSdpAnswer = __Infer; + +export const VoiceSdpOffer = __t.object("VoiceSdpOffer", { + id: __t.u64(), + sender: __t.identity(), + receiver: __t.identity(), + sdp: __t.string(), + channelId: __t.u64(), +}); +export type VoiceSdpOffer = __Infer; + export const VoiceState = __t.object("VoiceState", { identity: __t.identity(), channelId: __t.u64(), @@ -112,4 +139,3 @@ export const Watching = __t.object("Watching", { channelId: __t.u64(), }); export type Watching = __Infer; - diff --git a/src/module_bindings/types/procedures.ts b/src/module_bindings/types/procedures.ts index d5ac825..4d38205 100644 --- a/src/module_bindings/types/procedures.ts +++ b/src/module_bindings/types/procedures.ts @@ -6,5 +6,3 @@ import { type Infer as __Infer } from "spacetimedb"; // Import all procedure arg schemas - - diff --git a/src/module_bindings/types/reducers.ts b/src/module_bindings/types/reducers.ts index 3846a6a..4a0c23a 100644 --- a/src/module_bindings/types/reducers.ts +++ b/src/module_bindings/types/reducers.ts @@ -15,10 +15,13 @@ import LeaveServerReducer from "../leave_server_reducer"; import LeaveVoiceReducer from "../leave_voice_reducer"; import LoginReducer from "../login_reducer"; import RegisterReducer from "../register_reducer"; -import SendIceCandidateReducer from "../send_ice_candidate_reducer"; import SendMessageReducer from "../send_message_reducer"; -import SendSdpAnswerReducer from "../send_sdp_answer_reducer"; -import SendSdpOfferReducer from "../send_sdp_offer_reducer"; +import SendScreenIceCandidateReducer from "../send_screen_ice_candidate_reducer"; +import SendScreenSdpAnswerReducer from "../send_screen_sdp_answer_reducer"; +import SendScreenSdpOfferReducer from "../send_screen_sdp_offer_reducer"; +import SendVoiceIceCandidateReducer from "../send_voice_ice_candidate_reducer"; +import SendVoiceSdpAnswerReducer from "../send_voice_sdp_answer_reducer"; +import SendVoiceSdpOfferReducer from "../send_voice_sdp_offer_reducer"; import SetNameReducer from "../set_name_reducer"; import SetSharingScreenReducer from "../set_sharing_screen_reducer"; import SetTalkingReducer from "../set_talking_reducer"; @@ -34,13 +37,25 @@ export type LeaveServerParams = __Infer; export type LeaveVoiceParams = __Infer; export type LoginParams = __Infer; export type RegisterParams = __Infer; -export type SendIceCandidateParams = __Infer; export type SendMessageParams = __Infer; -export type SendSdpAnswerParams = __Infer; -export type SendSdpOfferParams = __Infer; +export type SendScreenIceCandidateParams = __Infer< + typeof SendScreenIceCandidateReducer +>; +export type SendScreenSdpAnswerParams = __Infer< + typeof SendScreenSdpAnswerReducer +>; +export type SendScreenSdpOfferParams = __Infer< + typeof SendScreenSdpOfferReducer +>; +export type SendVoiceIceCandidateParams = __Infer< + typeof SendVoiceIceCandidateReducer +>; +export type SendVoiceSdpAnswerParams = __Infer< + typeof SendVoiceSdpAnswerReducer +>; +export type SendVoiceSdpOfferParams = __Infer; export type SetNameParams = __Infer; export type SetSharingScreenParams = __Infer; export type SetTalkingParams = __Infer; export type StartWatchingParams = __Infer; export type StopWatchingParams = __Infer; - diff --git a/src/module_bindings/voice_ice_candidate_table.ts b/src/module_bindings/voice_ice_candidate_table.ts new file mode 100644 index 0000000..ff7e317 --- /dev/null +++ b/src/module_bindings/voice_ice_candidate_table.ts @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + id: __t.u64().primaryKey(), + sender: __t.identity(), + receiver: __t.identity(), + candidate: __t.string(), + channelId: __t.u64().name("channel_id"), +}); diff --git a/src/module_bindings/voice_sdp_answer_table.ts b/src/module_bindings/voice_sdp_answer_table.ts new file mode 100644 index 0000000..daa19fa --- /dev/null +++ b/src/module_bindings/voice_sdp_answer_table.ts @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + id: __t.u64().primaryKey(), + sender: __t.identity(), + receiver: __t.identity(), + sdp: __t.string(), + channelId: __t.u64().name("channel_id"), +}); diff --git a/src/module_bindings/voice_sdp_offer_table.ts b/src/module_bindings/voice_sdp_offer_table.ts new file mode 100644 index 0000000..daa19fa --- /dev/null +++ b/src/module_bindings/voice_sdp_offer_table.ts @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + id: __t.u64().primaryKey(), + sender: __t.identity(), + receiver: __t.identity(), + sdp: __t.string(), + channelId: __t.u64().name("channel_id"), +}); diff --git a/src/setupTests.ts b/src/setupTests.ts index 7b0828b..d0de870 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1 +1 @@ -import '@testing-library/jest-dom'; +import "@testing-library/jest-dom"; diff --git a/vite.config.ts b/vite.config.ts index f2f80ff..577fb18 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,10 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import basicSsl from '@vitejs/plugin-basic-ssl'; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import basicSsl from "@vitejs/plugin-basic-ssl"; // https://vite.dev/config/ export default defineConfig({ - plugins: [ - react(), - basicSsl(), - ], + plugins: [react(), basicSsl()], server: { https: true, }, diff --git a/vitest.config.ts b/vitest.config.ts index 34d8192..38da4a8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from 'vitest/config'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], test: { globals: true, - environment: 'jsdom', // or "node" if you're not testing DOM - setupFiles: './src/setupTests.ts', + environment: "jsdom", // or "node" if you're not testing DOM + setupFiles: "./src/setupTests.ts", testTimeout: 15_000, // give extra time for real connections hookTimeout: 15_000, },