diff --git a/apps/events/package.json b/apps/events/package.json index 1942b5fc..20dde2b7 100644 --- a/apps/events/package.json +++ b/apps/events/package.json @@ -27,6 +27,7 @@ "lucide-react": "^0.471.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-select": "^5.10.0", "siwe": "^2.3.2", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", diff --git a/apps/events/src/components/todos-read-only.tsx b/apps/events/src/components/todos-read-only.tsx new file mode 100644 index 00000000..411887cb --- /dev/null +++ b/apps/events/src/components/todos-read-only.tsx @@ -0,0 +1,18 @@ +import { useQueryEntities } from '@graphprotocol/hypergraph-react'; +import { Todo } from '../schema'; + +export const TodosReadOnly = () => { + const todos = useQueryEntities(Todo); + + return ( + <> +

Todos (read only)

+ {todos.map((todo) => ( +
+

{todo.name}

+ +
+ ))} + + ); +}; diff --git a/apps/events/src/components/todos.tsx b/apps/events/src/components/todos.tsx index 50bfa0ef..94c2ae49 100644 --- a/apps/events/src/components/todos.tsx +++ b/apps/events/src/components/todos.tsx @@ -1,25 +1,41 @@ import { useCreateEntity, useDeleteEntity, useQueryEntities, useUpdateEntity } from '@graphprotocol/hypergraph-react'; -import { useState } from 'react'; -import { Todo } from '../schema'; +import { useEffect, useState } from 'react'; +import Select from 'react-select'; +import { Todo, User } from '../schema'; import { Button } from './ui/button'; import { Input } from './ui/input'; export const Todos = () => { const todos = useQueryEntities(Todo); + const users = useQueryEntities(User); const createEntity = useCreateEntity(Todo); const updateEntity = useUpdateEntity(Todo); const deleteEntity = useDeleteEntity(); - const [newTodoTitle, setNewTodoTitle] = useState(''); + const [newTodoName, setNewTodoName] = useState(''); + const [assignees, setAssignees] = useState<{ value: string; label: string }[]>([]); + useEffect(() => { + setAssignees((prevFilteredAssignees) => { + // filter out assignees that are not in the users array whenever users change + return prevFilteredAssignees.filter((assignee) => users.some((user) => user.id === assignee.value)); + }); + }, [users]); + + const userOptions = users.map((user) => ({ value: user.id, label: user.name })); return ( <>

Todos

-
- setNewTodoTitle(e.target.value)} /> +
+ setNewTodoName(e.target.value)} /> + { + const deleteEntity = useDeleteEntity(); + const updateEntity = useUpdateEntity(User); + const [editMode, setEditMode] = useState(false); + + return ( +
+

+ {user.name} ({user.id}) +

+ + + + {editMode && ( + updateEntity(user.id, { name: e.target.value })} /> + )} +
+ ); +}; diff --git a/apps/events/src/components/users.tsx b/apps/events/src/components/users.tsx new file mode 100644 index 00000000..5f892526 --- /dev/null +++ b/apps/events/src/components/users.tsx @@ -0,0 +1,32 @@ +import { useCreateEntity, useQueryEntities } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { User } from '../schema.js'; +import { Button } from './ui/button.js'; +import { Input } from './ui/input.js'; +import { UserEntry } from './user-entry.js'; + +export const Users = () => { + const users = useQueryEntities(User); + const createEntity = useCreateEntity(User); + const [newUserName, setNewUserName] = useState(''); + + return ( + <> +

Users

+
+ setNewUserName(e.target.value)} /> + +
+ {users.map((user) => ( + + ))} + + ); +}; diff --git a/apps/events/src/routes/space/$spaceId.tsx b/apps/events/src/routes/space/$spaceId.tsx index 9cc26434..7e5551ec 100644 --- a/apps/events/src/routes/space/$spaceId.tsx +++ b/apps/events/src/routes/space/$spaceId.tsx @@ -5,9 +5,11 @@ import { useSelector } from '@xstate/store/react'; import { DevTool } from '@/components/dev-tool'; import { Todos } from '@/components/todos'; +import { TodosReadOnly } from '@/components/todos-read-only'; import { Button } from '@/components/ui/button'; +import { Users } from '@/components/users'; import { availableAccounts } from '@/lib/availableAccounts'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { getAddress } from 'viem'; export const Route = createFileRoute('/space/$spaceId')({ @@ -23,6 +25,7 @@ function Space() { subscribeToSpace({ spaceId }); } }, [loading, subscribeToSpace, spaceId]); + const [show2ndTodos, setShow2ndTodos] = useState(false); const space = spaces.find((space) => space.id === spaceId); @@ -37,7 +40,10 @@ function Space() { return (
+ + + {show2ndTodos && }

Invite people

{availableAccounts.map((invitee) => { @@ -56,8 +62,9 @@ function Space() { ); })}
-
+
+
diff --git a/apps/events/src/schema.ts b/apps/events/src/schema.ts index fe408045..c6397ead 100644 --- a/apps/events/src/schema.ts +++ b/apps/events/src/schema.ts @@ -1,7 +1,13 @@ import { Entity } from '@graphprotocol/hypergraph'; +export class User extends Entity.Class('User')({ + id: Entity.Generated(Entity.Text), + name: Entity.Text, +}) {} + export class Todo extends Entity.Class('Todo')({ id: Entity.Generated(Entity.Text), name: Entity.Text, completed: Entity.Checkbox, + assignees: Entity.Reference(Entity.ReferenceArray(User)), }) {} diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index 2fc6b7da..2eb2dd6a 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -4,7 +4,7 @@ import type { AnyDocumentId, DocHandle, Repo } from '@automerge/automerge-repo'; import { useRepo } from '@automerge/automerge-repo-react-hooks'; import { Entity, Utils } from '@graphprotocol/hypergraph'; import * as Schema from 'effect/Schema'; -import { type ReactNode, createContext, useContext, useRef, useSyncExternalStore } from 'react'; +import { type ReactNode, createContext, useContext, useRef, useState, useSyncExternalStore } from 'react'; export type HypergraphContext = { space: string; @@ -31,11 +31,13 @@ export function HypergraphSpaceProvider({ space, children }: { space: string; ch let current = ref.current; if (current === undefined || space !== current.space || repo !== current.repo) { const id = Utils.idToAutomergeId(space) as AnyDocumentId; + const handle = repo.find(id); + current = ref.current = { space, repo, id, - handle: repo.find(id), + handle, }; } @@ -59,37 +61,11 @@ export function useDeleteEntity() { export function useQueryEntities(type: S) { const hypergraph = useHypergraph(); - const equal = isEqual(type); - - // store as a map of type to array of entities of the type - const prevEntitiesRef = useRef>>>([]); - - const subscribe = (callback: () => void) => { - const handleChange = () => { - callback(); - }; - - const handleDelete = () => { - callback(); - }; - - hypergraph.handle.on('change', handleChange); - hypergraph.handle.on('delete', handleDelete); - - return () => { - hypergraph.handle.off('change', handleChange); - hypergraph.handle.off('delete', handleDelete); - }; - }; - - return useSyncExternalStore>>>(subscribe, () => { - const filtered = Entity.findMany(hypergraph.handle, type); - if (!equal(filtered, prevEntitiesRef.current)) { - prevEntitiesRef.current = filtered; - } - - return prevEntitiesRef.current; + const [subscription] = useState(() => { + return Entity.subscribeToFindMany(hypergraph.handle, type); }); + + return useSyncExternalStore(subscription.subscribe, subscription.getEntities, () => []); } export function useQueryEntity(type: S, id: string) { @@ -135,22 +111,3 @@ export function useQueryEntity(type: S, id: return prevEntityRef.current; }); } - -/** @internal */ -const isEqual = (type: Schema.Schema) => { - const equals = Schema.equivalence(type); - - return (a: ReadonlyArray, b: ReadonlyArray) => { - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i++) { - if (!equals(a[i], b[i])) { - return false; - } - } - - return true; - }; -}; diff --git a/packages/hypergraph/src/Entity.ts b/packages/hypergraph/src/Entity.ts deleted file mode 100644 index bf241ff8..00000000 --- a/packages/hypergraph/src/Entity.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { DocHandle } from '@automerge/automerge-repo'; -import * as VariantSchema from '@effect/experimental/VariantSchema'; -import * as Data from 'effect/Data'; -import * as Schema from 'effect/Schema'; -import { generateId } from './utils/generateId.js'; - -const { - Class, - Field, - // FieldExcept, - // FieldOnly, - // Struct, - // Union, - // extract, - // fieldEvolve, - // fieldFromKey -} = VariantSchema.make({ - variants: ['select', 'insert', 'update'], - defaultVariant: 'select', -}); - -export { Class }; - -export type Any = Schema.Schema.Any & { - readonly fields: Schema.Struct.Fields; - readonly insert: Schema.Schema.Any; - readonly update: Schema.Schema.Any; -}; - -export type AnyNoContext = Schema.Schema.AnyNoContext & { - readonly fields: Schema.Struct.Fields; - readonly insert: Schema.Schema.AnyNoContext; - readonly update: Schema.Schema.AnyNoContext; -}; - -export type Update = S['update']; -export type Insert = S['insert']; - -export interface Generated - extends VariantSchema.Field<{ - readonly select: S; - }> {} - -/** - * A field that represents a column that is generated by the store. - */ -export const Generated = (schema: S): Generated => - Field({ - select: schema, - }); - -export const Text = Schema.String; -// biome-ignore lint/suspicious/noShadowRestrictedNames: is part of a namespaces module and therefor ok -export const Number = Schema.Number; -export const Checkbox = Schema.Boolean; - -export type DocumentContent = { - entities?: Record; -}; - -export class EntityNotFoundError extends Data.TaggedError('EntityNotFoundError')<{ - id: string; - type: AnyNoContext; - cause?: unknown; -}> {} - -export type Entity = Schema.Schema.Type & { type: string }; - -/** - * Creates an entity model of given type and stores it in the repo. - */ -export const create = (handle: DocHandle, type: S) => { - // TODO: what's the right way to get the name of the type? - // @ts-expect-error name is defined - const typeName = type.name; - const entityId = generateId(); - const encode = Schema.encodeSync(type.insert); - - return (data: Readonly>>): Entity => { - const encoded = encode(data); - // apply changes to the repo -> adds the entity to the repo entites document - handle.change((doc) => { - doc.entities ??= {}; - doc.entities[entityId] = { ...encoded, '@@types@@': [typeName] }; - }); - - return { id: entityId, ...encoded, type: typeName }; - }; -}; - -/** - * Update an existing entity model of given type in the repo. - */ -export const update = (handle: DocHandle, type: S) => { - const validate = Schema.validateSync(Schema.partial(type.update)); - const encode = Schema.encodeSync(type.update); - const decode = Schema.decodeUnknownSync(type.update); - - // TODO: what's the right way to get the name of the type? - // @ts-expect-error name is defined - const typeName = type.name; - - return (id: string, data: Schema.Simplify>>>): Entity => { - validate(data); - - // apply changes to the repo -> updates the existing entity to the repo entites document - let updated: Schema.Schema.Type | undefined = undefined; - handle.change((doc) => { - if (doc.entities === undefined) { - return; - } - - // TODO: Fetch the pre-decoded value from the local cache. - const entity = doc.entities[id] ?? undefined; - if (entity === undefined || typeof entity !== 'object') { - return; - } - - // TODO: Try to get a diff of the entitiy properties and only override the changed ones. - updated = { ...decode(entity), ...data }; - doc.entities[id] = { ...encode(updated), '@@types@@': [typeName] }; - }); - - if (updated === undefined) { - throw new EntityNotFoundError({ id, type }); - } - - return { id, type: typeName, ...(updated as Schema.Schema.Type) }; - }; -}; - -/** - * Deletes the exiting entity from the repo. - */ -const delete$ = (handle: DocHandle) => { - return (id: string): boolean => { - let result = false; - - // apply changes to the repo -> removes the existing entity by its id - handle.change((doc) => { - if (doc.entities?.[id] !== undefined) { - delete doc.entities[id]; - result = true; - } - }); - - return result; - }; -}; - -export { delete$ as delete }; - -/** - * Queries for a list of entities of the given type from the repo. - */ -export function findMany( - handle: DocHandle, - type: S, -): Readonly>> { - const decode = Schema.decodeUnknownSync(type); - // TODO: what's the right way to get the name of the type? - // @ts-expect-error name is defined - const typeName = type.name; - - // TODO: Instead of this insane filtering logic, we should be keeping track of the entities in - // an index and store the decoded valeus instead of re-decoding over and over again. - const entities = handle.docSync()?.entities ?? {}; - const filtered: Array> = []; - for (const id in entities) { - const entity = entities[id]; - if (typeof entity === 'object' && entity != null && '@@types@@' in entity) { - const types = entity['@@types@@']; - if (Array.isArray(types) && types.includes(typeName)) { - filtered.push({ ...decode({ ...entity, id }), type: typeName }); - } - } - } - - return filtered; -} - -/** - * Find the entity of the given type, with the given id, from the repo. - */ -export const findOne = - (handle: DocHandle, type: S) => - (id: string): Entity | undefined => { - const decode = Schema.decodeUnknownSync(type); - - // TODO: what's the right way to get the name of the type? - // @ts-expect-error name is defined - const typeName = type.name; - - // TODO: Instead of this insane filtering logic, we should be keeping track of the entities in - // an index and store the decoded valeus instead of re-decoding over and over again. - const entity = handle.docSync()?.entities?.[id]; - if (typeof entity === 'object' && entity != null && '@@types@@' in entity) { - const types = entity['@@types@@']; - if (Array.isArray(types) && types.includes(typeName)) { - return { ...decode({ ...entity, id }), type: typeName }; - } - } - - return undefined; - }; diff --git a/packages/hypergraph/src/entity/create.ts b/packages/hypergraph/src/entity/create.ts new file mode 100644 index 00000000..6c25355e --- /dev/null +++ b/packages/hypergraph/src/entity/create.ts @@ -0,0 +1,26 @@ +import type { DocHandle } from '@automerge/automerge-repo'; +import * as Schema from 'effect/Schema'; +import { generateId } from '../utils/generateId.js'; +import type { AnyNoContext, DocumentContent, Entity, Insert } from './types.js'; + +/** + * Creates an entity model of given type and stores it in the repo. + */ +export const create = (handle: DocHandle, type: S) => { + // TODO: what's the right way to get the name of the type? + // @ts-expect-error name is defined + const typeName = type.name; + const entityId = generateId(); + const encode = Schema.encodeSync(type.insert); + + return (data: Readonly>>): Entity => { + const encoded = encode(data); + // apply changes to the repo -> adds the entity to the repo entities document + handle.change((doc) => { + doc.entities ??= {}; + doc.entities[entityId] = { ...encoded, '@@types@@': [typeName] }; + }); + + return { id: entityId, ...encoded, type: typeName }; + }; +}; diff --git a/packages/hypergraph/src/entity/decodedEntitiesCache.ts b/packages/hypergraph/src/entity/decodedEntitiesCache.ts new file mode 100644 index 00000000..734fd633 --- /dev/null +++ b/packages/hypergraph/src/entity/decodedEntitiesCache.ts @@ -0,0 +1,36 @@ +import type { AnyNoContext, Entity } from './types.js'; + +export type QueryEntry = { + data: Array>; // holds the decoded entities of this query and must be a stable reference and use the same reference for the `entities` array + listeners: Array<() => void>; // listeners to this query + isInvalidated: boolean; +}; + +export type DecodedEntitiesCacheEntry = { + decoder: (data: unknown) => unknown; + type: AnyNoContext; // TODO should be the type of the entity + entities: Map>; // holds all entities of this type + queries: Map< + string, // instead of serializedQueryKey as string we could also have the actual params + QueryEntry + >; + isInvalidated: boolean; +}; + +/* +/* + * Note: Currently we only use one global cache for all entities. + * In the future we probably want a build function that creates a cache and returns the + * functions (create, update, findMany, …) that use this specific cache. + * + * How does it work? + * + * We store all decoded entities in a cache and for each query we reference the entities relevant to this query. + * Whenever a query is registered we add it to the cache and add a listener to the query. Whenever a query is unregistered we remove the listener from the query. + */ +type DecodedEntitiesCache = Map< + string, // type name + DecodedEntitiesCacheEntry +>; + +export const decodedEntitiesCache: DecodedEntitiesCache = new Map(); diff --git a/packages/hypergraph/src/entity/delete.ts b/packages/hypergraph/src/entity/delete.ts new file mode 100644 index 00000000..d51610c5 --- /dev/null +++ b/packages/hypergraph/src/entity/delete.ts @@ -0,0 +1,23 @@ +import type { DocHandle } from '@automerge/automerge-repo'; +import type { DocumentContent } from './types.js'; + +/** + * Deletes the exiting entity from the repo. + */ +export const delete$ = (handle: DocHandle) => { + return (id: string): boolean => { + let result = false; + + // apply changes to the repo -> removes the existing entity by its id + handle.change((doc) => { + if (doc.entities?.[id] !== undefined) { + delete doc.entities[id]; + result = true; + } + }); + + return result; + }; +}; + +export { delete$ as delete }; diff --git a/packages/hypergraph/src/entity/entity.ts b/packages/hypergraph/src/entity/entity.ts new file mode 100644 index 00000000..be63afa9 --- /dev/null +++ b/packages/hypergraph/src/entity/entity.ts @@ -0,0 +1,54 @@ +import * as VariantSchema from '@effect/experimental/VariantSchema'; +import * as Data from 'effect/Data'; +import * as Schema from 'effect/Schema'; +import type { AnyNoContext } from './types.js'; + +const { + Class, + Field, + // FieldExcept, + // FieldOnly, + // Struct, + // Union, + // extract, + // fieldEvolve, + // fieldFromKey +} = VariantSchema.make({ + variants: ['select', 'insert', 'update'], + defaultVariant: 'select', +}); + +export { Class }; + +export interface Generated + extends VariantSchema.Field<{ + readonly select: S; + }> {} + +/** + * A field that represents a column that is generated by the store. + */ +export const Generated = (schema: S): Generated => + Field({ + select: schema, + }); + +export const Text = Schema.String; +// biome-ignore lint/suspicious/noShadowRestrictedNames: is part of a namespaces module and therefor ok +export const Number = Schema.Number; +export const Checkbox = Schema.Boolean; + +export class EntityNotFoundError extends Data.TaggedError('EntityNotFoundError')<{ + id: string; + type: AnyNoContext; + cause?: unknown; +}> {} + +export const Reference = (schema: S) => + Field({ + select: schema, + insert: Schema.optional(Schema.Array(Schema.String)), + update: Schema.optional(Schema.Array(Schema.String)), + }); + +export const ReferenceArray = Schema.Array; diff --git a/packages/hypergraph/src/entity/entityRelationParentsMap.ts b/packages/hypergraph/src/entity/entityRelationParentsMap.ts new file mode 100644 index 00000000..f4738092 --- /dev/null +++ b/packages/hypergraph/src/entity/entityRelationParentsMap.ts @@ -0,0 +1,6 @@ +import type { DecodedEntitiesCacheEntry } from './decodedEntitiesCache.js'; + +export const entityRelationParentsMap: Map< + string, // entity ID + Map +> = new Map(); diff --git a/packages/hypergraph/src/entity/findMany.ts b/packages/hypergraph/src/entity/findMany.ts new file mode 100644 index 00000000..973382a6 --- /dev/null +++ b/packages/hypergraph/src/entity/findMany.ts @@ -0,0 +1,380 @@ +import type { DocHandle, Patch } from '@automerge/automerge-repo'; +import * as Schema from 'effect/Schema'; +import { type DecodedEntitiesCacheEntry, type QueryEntry, decodedEntitiesCache } from './decodedEntitiesCache.js'; +import { entityRelationParentsMap } from './entityRelationParentsMap.js'; +import { getEntityRelations } from './getEntityRelations.js'; +import { hasValidTypesProperty } from './hasValidTypesProperty.js'; +import { isReferenceField } from './isReferenceField.js'; +import type { AnyNoContext, DocumentContent, Entity } from './types.js'; + +const documentChangeListener: { + subscribedQueriesCount: number; + unsubscribe: undefined | (() => void); +} = { + subscribedQueriesCount: 0, + unsubscribe: undefined, +}; + +const subscribeToDocumentChanges = (handle: DocHandle) => { + const onChange = ({ patches, doc }: { patches: Array; doc: DocumentContent }) => { + const changedEntities = new Set(); + const deletedEntities = new Set(); + + for (const patch of patches) { + switch (patch.action) { + case 'put': + case 'insert': + case 'splice': { + if (patch.path.length > 2 && patch.path[0] === 'entities' && typeof patch.path[1] === 'string') { + changedEntities.add(patch.path[1]); + } + break; + } + case 'del': { + if (patch.path.length === 2 && patch.path[0] === 'entities' && typeof patch.path[1] === 'string') { + deletedEntities.add(patch.path[1]); + } + break; + } + } + } + + const entityTypes = new Set(); + // collect all query entries that changed and only at the end make one copy to change the + // reference to reduce the amount of O(n) operations per query to 1 + const touchedQueries = new Set>(); + + // collect all entities that used this entity as a entry in on of their relation fields + const touchedRelationParents = new Set(); + + // loop over all changed entities and update the cache + for (const entityId of changedEntities) { + const entity = doc.entities?.[entityId]; + if (!hasValidTypesProperty(entity)) continue; + for (const typeName of entity['@@types@@']) { + if (typeof typeName !== 'string') continue; + const cacheEntry = decodedEntitiesCache.get(typeName); + if (!cacheEntry) continue; + + const oldDecodedEntry = cacheEntry.entities.get(entityId); + const relations = getEntityRelations(entity, cacheEntry.type, doc); + const decoded = cacheEntry.decoder({ + ...entity, + ...relations, + id: entityId, + }); + cacheEntry.entities.set(entityId, decoded); + + if (oldDecodedEntry) { + // collect all the Ids for relation entries in the `oldDecodedEntry` + const deletedRelationIds = new Set(); + for (const [, value] of Object.entries(oldDecodedEntry)) { + if (Array.isArray(value)) { + for (const relationEntity of value) { + deletedRelationIds.add(relationEntity.id); + } + } + } + + // it's fine to remove all of them since they are re-added below + for (const deletedRelationId of deletedRelationIds) { + const deletedRelationEntry = entityRelationParentsMap.get(deletedRelationId); + if (deletedRelationEntry) { + deletedRelationEntry.set(cacheEntry, (deletedRelationEntry.get(cacheEntry) ?? 0) - 1); + if (deletedRelationEntry.get(cacheEntry) === 0) { + deletedRelationEntry.delete(cacheEntry); + } + if (deletedRelationEntry.size === 0) { + entityRelationParentsMap.delete(deletedRelationId); + } + } + } + } + + // @ts-expect-error decoded is a valid object + for (const [key, value] of Object.entries(decoded)) { + if (Array.isArray(value)) { + for (const relationEntity of value) { + let relationParentEntry = entityRelationParentsMap.get(relationEntity.id); + if (relationParentEntry) { + relationParentEntry.set(cacheEntry, (relationParentEntry.get(cacheEntry) ?? 0) + 1); + } else { + relationParentEntry = new Map(); + entityRelationParentsMap.set(relationEntity.id, relationParentEntry); + relationParentEntry.set(cacheEntry, 1); + } + } + } + } + + const query = cacheEntry.queries.get('all'); + if (query) { + const index = query.data.findIndex((entity) => entity.id === entityId); + if (index !== -1) { + query.data[index] = decoded; + } else { + query.data.push(decoded); + } + touchedQueries.add([typeName, 'all']); + } + + entityTypes.add(typeName); + + // gather all the decodedEntitiesCacheEntries that have a relation to this entity to + // invoke their query listeners below + if (entityRelationParentsMap.has(entityId)) { + const decodedEntitiesCacheEntries = entityRelationParentsMap.get(entityId); + if (!decodedEntitiesCacheEntries) return; + + for (const [entry] of decodedEntitiesCacheEntries) { + touchedRelationParents.add(entry); + } + } + } + } + + // loop over all deleted entities and remove them from the cache + for (const entityId of deletedEntities) { + for (const [affectedTypeName, cacheEntry] of decodedEntitiesCache) { + if (cacheEntry.entities.has(entityId)) { + entityTypes.add(affectedTypeName); + cacheEntry.entities.delete(entityId); + + for (const [, query] of cacheEntry.queries) { + // find the entity in the query and remove it using splice + const index = query.data.findIndex((entity) => entity.id === entityId); + if (index !== -1) { + query.data.splice(index, 1); + touchedQueries.add([affectedTypeName, 'all']); + } + } + } + } + + // gather all the queries of impacted parent relation queries and then remove the cacheEntry + if (entityRelationParentsMap.has(entityId)) { + const decodedEntitiesCacheEntries = entityRelationParentsMap.get(entityId); + if (!decodedEntitiesCacheEntries) return; + + for (const [entry] of decodedEntitiesCacheEntries) { + touchedRelationParents.add(entry); + } + + entityRelationParentsMap.delete(entityId); + } + } + + // update the queries affected queries + for (const [typeName, queryKey] of touchedQueries) { + const cacheEntry = decodedEntitiesCache.get(typeName); + if (!cacheEntry) continue; + + const query = cacheEntry.queries.get(queryKey); + if (!query) continue; + + query.data = [...query.data]; // must be a new reference for React.useSyncExternalStore + } + + // invoke all the listeners per type + for (const typeName of entityTypes) { + const cacheEntry = decodedEntitiesCache.get(typeName); + if (!cacheEntry) continue; + + for (const query of cacheEntry.queries.values()) { + for (const listener of query.listeners) { + listener(); + } + } + } + + // trigger all the listeners of the parent relation queries + for (const decodedEntitiesCacheEntry of touchedRelationParents) { + decodedEntitiesCacheEntry.isInvalidated = true; + for (const query of decodedEntitiesCacheEntry.queries.values()) { + query.isInvalidated = true; + for (const listener of query.listeners) { + listener(); + } + } + } + }; + + handle.on('change', onChange); + + return () => { + handle.off('change', onChange); + decodedEntitiesCache.clear(); // currently we only support exactly one space + }; +}; + +/** + * Queries for a list of entities of the given type from the repo. + */ +export function findMany( + handle: DocHandle, + type: S, +): Readonly>> { + const decode = Schema.decodeUnknownSync(type); + // TODO: what's the right way to get the name of the type? + // @ts-expect-error name is defined + const typeName = type.name; + + const doc = handle.docSync(); + if (!doc) { + return []; + } + const entities = doc.entities ?? {}; + const filtered: Array> = []; + for (const id in entities) { + const entity = entities[id]; + if (hasValidTypesProperty(entity) && entity['@@types@@'].includes(typeName)) { + const relations = getEntityRelations(entity, type, doc); + filtered.push({ ...decode({ ...entity, ...relations, id }), type: typeName }); + } + } + + return filtered; +} + +const stableEmptyArray: Array = []; + +export function subscribeToFindMany( + handle: DocHandle, + type: S, +): { + subscribe: (callback: () => void) => () => void; + getEntities: () => Readonly>>; +} { + const queryKey = 'all'; + const decode = Schema.decodeUnknownSync(type); + // TODO: what's the right way to get the name of the type? + // @ts-expect-error name is defined + const typeName = type.name; + + const getEntities = () => { + const cacheEntry = decodedEntitiesCache.get(typeName); + if (!cacheEntry) return stableEmptyArray; + const query = cacheEntry.queries.get(queryKey); + if (!query) return stableEmptyArray; + + if (!cacheEntry.isInvalidated && !query.isInvalidated) { + return query.data; + } + + const entities = findMany(handle, type); + for (const entity of entities) { + cacheEntry?.entities.set(entity.id, entity); + + if (!query) continue; + + const index = query.data.findIndex((e) => e.id === entity.id); + if (index !== -1) { + query.data[index] = entity; + } else { + query.data.push(entity); + } + } + + cacheEntry.isInvalidated = false; + query.isInvalidated = false; + return query.data; + }; + + if (!decodedEntitiesCache.has(typeName)) { + const entities = findMany(handle, type); + const entitiesMap = new Map(); + for (const entity of entities) { + entitiesMap.set(entity.id, entity); + } + + const queries = new Map(); + + queries.set(queryKey, { + data: [...entities], + listeners: [], + isInvalidated: false, + }); + + const cacheEntry: DecodedEntitiesCacheEntry = { + decoder: decode, + type, + entities: entitiesMap, + queries, + isInvalidated: false, + }; + + decodedEntitiesCache.set(typeName, cacheEntry); + + for (const entity of entities) { + for (const [, value] of Object.entries(entity)) { + if (Array.isArray(value)) { + for (const relationEntity of value) { + let relationParentEntry = entityRelationParentsMap.get(relationEntity.id); + if (relationParentEntry) { + relationParentEntry.set(cacheEntry, (relationParentEntry.get(cacheEntry) ?? 0) + 1); + } else { + relationParentEntry = new Map(); + entityRelationParentsMap.set(relationEntity.id, relationParentEntry); + relationParentEntry.set(cacheEntry, 1); + } + } + } + } + } + } + + const allTypes = new Set(); + for (const [_key, field] of Object.entries(type.fields)) { + if (isReferenceField(field)) { + allTypes.add(field as S); + } + } + + const subscribe = (callback: () => void) => { + const query = decodedEntitiesCache.get(typeName)?.queries.get(queryKey); + if (query?.listeners) { + query.listeners.push(callback); + } + + return () => { + const cacheEntry = decodedEntitiesCache.get(typeName); + if (cacheEntry) { + // first cleanup the queries + const query = cacheEntry.queries.get(queryKey); + if (query) { + query.listeners = query?.listeners?.filter((cachedListener) => cachedListener !== callback); + if (query.listeners.length === 0) { + cacheEntry.queries.delete(queryKey); + } + } + // if the last query is removed, cleanup the entityRelationParentsMap and remove the decodedEntitiesCacheEntry + if (cacheEntry.queries.size === 0) { + entityRelationParentsMap.forEach((relationCacheEntries, key) => { + for (const [relationCacheEntry, counter] of relationCacheEntries) { + if (relationCacheEntry === cacheEntry && counter === 0) { + relationCacheEntries.delete(cacheEntry); + } + } + if (relationCacheEntries.size === 0) { + entityRelationParentsMap.delete(key); + } + }); + decodedEntitiesCache.delete(typeName); + } + } + + documentChangeListener.subscribedQueriesCount--; + if (documentChangeListener.subscribedQueriesCount === 0) { + documentChangeListener.unsubscribe?.(); + documentChangeListener.unsubscribe = undefined; + } + }; + }; + + if (documentChangeListener.subscribedQueriesCount === 0) { + documentChangeListener.unsubscribe = subscribeToDocumentChanges(handle); + } + documentChangeListener.subscribedQueriesCount++; + + return { subscribe, getEntities }; +} diff --git a/packages/hypergraph/src/entity/findOne.ts b/packages/hypergraph/src/entity/findOne.ts new file mode 100644 index 00000000..f2c3d2ba --- /dev/null +++ b/packages/hypergraph/src/entity/findOne.ts @@ -0,0 +1,26 @@ +import type { DocHandle } from '@automerge/automerge-repo'; +import * as Schema from 'effect/Schema'; +import { hasValidTypesProperty } from './hasValidTypesProperty.js'; +import type { AnyNoContext, DocumentContent, Entity } from './types.js'; + +/** + * Find the entity of the given type, with the given id, from the repo. + */ +export const findOne = + (handle: DocHandle, type: S) => + (id: string): Entity | undefined => { + const decode = Schema.decodeUnknownSync(type); + + // TODO: what's the right way to get the name of the type? + // @ts-expect-error name is defined + const typeName = type.name; + + // TODO: Instead of this insane filtering logic, we should be keeping track of the entities in + // an index and store the decoded values instead of re-decoding over and over again. + const entity = handle.docSync()?.entities?.[id]; + if (hasValidTypesProperty(entity) && entity['@@types@@'].includes(typeName)) { + return { ...decode({ ...entity, id }), type: typeName }; + } + + return undefined; + }; diff --git a/packages/hypergraph/src/entity/getEntityRelations.ts b/packages/hypergraph/src/entity/getEntityRelations.ts new file mode 100644 index 00000000..7c71a681 --- /dev/null +++ b/packages/hypergraph/src/entity/getEntityRelations.ts @@ -0,0 +1,30 @@ +import { hasArrayField } from '../utils/hasArrayField.js'; +import { hasValidTypesProperty } from './hasValidTypesProperty.js'; +import { isReferenceField } from './isReferenceField.js'; +import type { AnyNoContext, DocumentContent, Entity } from './types.js'; + +export const getEntityRelations = ( + entity: Entity, + type: S, + doc: DocumentContent, +) => { + const relations: Record> = {}; + for (const [fieldName, field] of Object.entries(type.fields)) { + if (!isReferenceField(field)) continue; + + const relationEntities: Array> = []; + + if (hasArrayField(entity, fieldName)) { + for (const relationEntityId of entity[fieldName]) { + const relationEntity = doc.entities?.[relationEntityId]; + if (!hasValidTypesProperty(relationEntity)) continue; + + relationEntities.push({ ...relationEntity, id: relationEntityId }); + } + } + + relations[fieldName] = relationEntities; + } + + return relations; +}; diff --git a/packages/hypergraph/src/entity/hasValidTypesProperty.ts b/packages/hypergraph/src/entity/hasValidTypesProperty.ts new file mode 100644 index 00000000..8aa4ed63 --- /dev/null +++ b/packages/hypergraph/src/entity/hasValidTypesProperty.ts @@ -0,0 +1,8 @@ +export const hasValidTypesProperty = (value: unknown): value is Record<'@@types@@', unknown[]> => { + return ( + value !== null && + typeof value === 'object' && + '@@types@@' in value && + Array.isArray((value as { '@@types@@': unknown })['@@types@@']) + ); +}; diff --git a/packages/hypergraph/src/entity/index.ts b/packages/hypergraph/src/entity/index.ts new file mode 100644 index 00000000..92f4dd64 --- /dev/null +++ b/packages/hypergraph/src/entity/index.ts @@ -0,0 +1,7 @@ +export * from './create.js'; +export * from './delete.js'; +export * from './entity.js'; +export * from './findMany.js'; +export * from './findOne.js'; +export * from './types.js'; +export * from './update.js'; diff --git a/packages/hypergraph/src/entity/isReferenceField.ts b/packages/hypergraph/src/entity/isReferenceField.ts new file mode 100644 index 00000000..b84c03e2 --- /dev/null +++ b/packages/hypergraph/src/entity/isReferenceField.ts @@ -0,0 +1,10 @@ +import type * as Schema from 'effect/Schema'; + +export const isReferenceField = (field: Schema.Schema.All | Schema.PropertySignature.All) => { + // TODO: instead we should check that the field in the array is an Entity.Class + // @ts-expect-error name is defined + if (field.name === 'ArrayClass') { + return true; + } + return false; +}; diff --git a/packages/hypergraph/src/entity/types.ts b/packages/hypergraph/src/entity/types.ts new file mode 100644 index 00000000..bde6c249 --- /dev/null +++ b/packages/hypergraph/src/entity/types.ts @@ -0,0 +1,22 @@ +import type * as Schema from 'effect/Schema'; + +export type Any = Schema.Schema.Any & { + readonly fields: Schema.Struct.Fields; + readonly insert: Schema.Schema.Any; + readonly update: Schema.Schema.Any; +}; + +export type AnyNoContext = Schema.Schema.AnyNoContext & { + readonly fields: Schema.Struct.Fields; + readonly insert: Schema.Schema.AnyNoContext; + readonly update: Schema.Schema.AnyNoContext; +}; + +export type Update = S['update']; +export type Insert = S['insert']; + +export type Entity = Schema.Schema.Type & { type: string }; + +export type DocumentContent = { + entities?: Record; +}; diff --git a/packages/hypergraph/src/entity/update.ts b/packages/hypergraph/src/entity/update.ts new file mode 100644 index 00000000..d8b2ff5f --- /dev/null +++ b/packages/hypergraph/src/entity/update.ts @@ -0,0 +1,45 @@ +import type { DocHandle } from '@automerge/automerge-repo'; +import * as Schema from 'effect/Schema'; +import { EntityNotFoundError } from './entity.js'; +import type { AnyNoContext, DocumentContent, Entity, Update } from './types.js'; + +/** + * Update an existing entity model of given type in the repo. + */ +export const update = (handle: DocHandle, type: S) => { + const validate = Schema.validateSync(Schema.partial(type.update)); + const encode = Schema.encodeSync(type.update); + const decode = Schema.decodeUnknownSync(type.update); + + // TODO: what's the right way to get the name of the type? + // @ts-expect-error name is defined + const typeName = type.name; + + return (id: string, data: Schema.Simplify>>>): Entity => { + validate(data); + + // apply changes to the repo -> updates the existing entity to the repo entities document + let updated: Schema.Schema.Type | undefined = undefined; + handle.change((doc) => { + if (doc.entities === undefined) { + return; + } + + // TODO: Fetch the pre-decoded value from the local cache. + const entity = doc.entities[id] ?? undefined; + if (entity === undefined || typeof entity !== 'object') { + return; + } + + // TODO: Try to get a diff of the entity properties and only override the changed ones. + updated = { ...decode(entity), ...data }; + doc.entities[id] = { ...encode(updated), '@@types@@': [typeName] }; + }); + + if (updated === undefined) { + throw new EntityNotFoundError({ id, type }); + } + + return { id, type: typeName, ...(updated as Schema.Schema.Type) }; + }; +}; diff --git a/packages/hypergraph/src/index.ts b/packages/hypergraph/src/index.ts index fe73c09f..9a995eca 100644 --- a/packages/hypergraph/src/index.ts +++ b/packages/hypergraph/src/index.ts @@ -1,7 +1,7 @@ +export * as Entity from './entity/index.js'; export * as Identity from './identity/index.js'; export * as Key from './key/index.js'; export * as Messages from './messages/index.js'; -export * as Entity from './Entity.js'; export * as SpaceEvents from './space-events/index.js'; export * from './store.js'; export * as Utils from './utils/index.js'; diff --git a/packages/hypergraph/src/utils/hasArrayField.ts b/packages/hypergraph/src/utils/hasArrayField.ts new file mode 100644 index 00000000..be78e221 --- /dev/null +++ b/packages/hypergraph/src/utils/hasArrayField.ts @@ -0,0 +1,4 @@ +export const hasArrayField = (obj: unknown, key: string): obj is { [K in string]: string[] } => { + // biome-ignore lint/suspicious/noExplicitAny: any is fine here + return obj !== null && typeof obj === 'object' && key in obj && Array.isArray((obj as any)[key]); +}; diff --git a/packages/hypergraph/test/entity/entity.test.ts b/packages/hypergraph/test/entity/entity.test.ts index e487148e..23a1ad60 100644 --- a/packages/hypergraph/test/entity/entity.test.ts +++ b/packages/hypergraph/test/entity/entity.test.ts @@ -2,7 +2,7 @@ import type { AnyDocumentId, DocHandle } from '@automerge/automerge-repo'; import { Repo } from '@automerge/automerge-repo'; import { beforeEach, describe, expect, it } from 'vitest'; -import * as Entity from '../../src/Entity.js'; +import * as Entity from '../../src/entity/index.js'; import { idToAutomergeId } from '../../src/utils/automergeId.js'; describe('Entity', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff8056ad..3f71ad73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-select: + specifier: ^5.10.0 + version: 5.10.0(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) siwe: specifier: ^2.3.2 version: 2.3.2(ethers@6.13.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -648,15 +651,56 @@ packages: peerDependencies: effect: ^3.12.2 + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + '@emotion/is-prop-valid@1.2.2': resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} '@emotion/memoize@0.8.1': resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} @@ -1970,6 +2014,9 @@ packages: '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/pg@8.11.10': resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==} @@ -1984,6 +2031,11 @@ packages: peerDependencies: '@types/react': ^19.0.0 + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@19.0.7': resolution: {integrity: sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==} @@ -2295,6 +2347,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2441,6 +2497,10 @@ packages: call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -2569,6 +2629,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2590,6 +2653,10 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -2795,6 +2862,9 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + domain-browser@4.23.0: resolution: {integrity: sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==} engines: {node: '>=10'} @@ -2858,6 +2928,9 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-define-property@1.0.0: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} @@ -2892,6 +2965,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -3020,6 +3097,9 @@ packages: find-my-way-ts@0.1.5: resolution: {integrity: sha512-4GOTMrpGQVzsCH2ruUn2vmwzV/02zF4q+ybhCIrw/Rkt3L8KWcycdC6aJMctJzwN4fXD4SD5F/4B9Sksh5rE0A==} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3181,6 +3261,9 @@ packages: hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -3228,6 +3311,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -3250,6 +3337,9 @@ packages: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -3395,6 +3485,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -3463,6 +3556,10 @@ packages: lokijs@1.5.12: resolution: {integrity: sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -3512,6 +3609,9 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -3783,10 +3883,18 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-asn1@5.1.7: resolution: {integrity: sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==} engines: {node: '>= 0.10'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} @@ -4038,6 +4146,9 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -4129,6 +4240,9 @@ packages: peerDependencies: react: ^19.0.0 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -4136,6 +4250,18 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react-select@5.10.0: + resolution: {integrity: sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react-usestateref@1.0.9: resolution: {integrity: sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw==} peerDependencies: @@ -4189,6 +4315,10 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -4345,6 +4475,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -4439,6 +4573,9 @@ packages: react: '>= 16.8.0' react-dom: '>= 16.8.0' + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + stylis@4.3.2: resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} @@ -4782,6 +4919,15 @@ packages: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} + use-isomorphic-layout-effect@1.2.0: + resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -5079,6 +5225,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + yaml@2.7.0: resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} engines: {node: '>= 14'} @@ -5574,14 +5724,78 @@ snapshots: find-my-way-ts: 0.1.5 multipasta: 0.2.5 + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.25.9 + '@babel/runtime': 7.26.0 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + '@emotion/is-prop-valid@1.2.2': dependencies: '@emotion/memoize': 0.8.1 '@emotion/memoize@0.8.1': {} + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.0.0) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.7 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + '@emotion/unitless@0.8.1': {} + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.0.0)': + dependencies: + react: 19.0.0 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + '@esbuild/aix-ppc64@0.23.1': optional: true @@ -7080,6 +7294,8 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/parse-json@4.0.2': {} + '@types/pg@8.11.10': dependencies: '@types/node': 22.10.7 @@ -7094,6 +7310,10 @@ snapshots: dependencies: '@types/react': 19.0.7 + '@types/react-transition-group@4.4.12(@types/react@19.0.7)': + dependencies: + '@types/react': 19.0.7 + '@types/react@19.0.7': dependencies: csstype: 3.1.3 @@ -7671,6 +7891,12 @@ snapshots: dependencies: '@babel/core': 7.26.0 + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.26.0 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + balanced-match@1.0.2: {} base-x@3.0.10: @@ -7863,6 +8089,8 @@ snapshots: call-me-maybe@1.0.2: {} + callsites@3.1.0: {} + camelcase-css@2.0.1: {} camelcase@5.3.1: {} @@ -7986,6 +8214,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} cookie-es@1.2.2: {} @@ -8001,6 +8231,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + crc-32@1.2.2: {} create-ecdh@4.0.4: @@ -8179,6 +8417,11 @@ snapshots: dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.26.0 + csstype: 3.1.3 + domain-browser@4.23.0: {} dotenv@16.4.7: {} @@ -8247,6 +8490,10 @@ snapshots: entities@4.5.0: {} + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + es-define-property@1.0.0: dependencies: get-intrinsic: 1.2.4 @@ -8320,6 +8567,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + esprima@4.0.1: {} estree-walker@2.0.2: {} @@ -8511,6 +8760,8 @@ snapshots: find-my-way-ts@0.1.5: {} + find-root@1.1.0: {} + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -8696,6 +8947,10 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -8748,6 +9003,11 @@ snapshots: ignore@5.3.2: {} + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + indent-string@4.0.0: {} inflight@1.0.6: @@ -8766,6 +9026,8 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-arrayish@0.2.1: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -8912,6 +9174,8 @@ snapshots: jsesc@3.1.0: {} + json-parse-even-better-errors@2.3.1: {} + json-stringify-safe@5.0.1: {} json5@2.2.3: {} @@ -8972,6 +9236,10 @@ snapshots: lokijs@1.5.12: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + loupe@3.1.2: {} lru-cache@10.4.3: {} @@ -9019,6 +9287,8 @@ snapshots: media-typer@1.1.0: {} + memoize-one@6.0.0: {} + merge-descriptors@2.0.0: {} merge2@1.4.1: {} @@ -9283,6 +9553,10 @@ snapshots: pako@1.0.11: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-asn1@5.1.7: dependencies: asn1.js: 4.10.1 @@ -9292,6 +9566,13 @@ snapshots: pbkdf2: 3.1.2 safe-buffer: 5.2.1 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + parse5@7.2.1: dependencies: entities: 4.5.0 @@ -9527,6 +9808,12 @@ snapshots: process@0.11.10: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -9632,10 +9919,38 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-is@16.13.1: {} + react-is@17.0.2: {} react-refresh@0.14.2: {} + react-select@5.10.0(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.0 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@floating-ui/dom': 1.6.13 + '@types/react-transition-group': 4.4.12(@types/react@19.0.7) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + use-isomorphic-layout-effect: 1.2.0(@types/react@19.0.7)(react@19.0.0) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.0 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-usestateref@1.0.9(react@19.0.0): dependencies: react: 19.0.0 @@ -9696,6 +10011,8 @@ snapshots: require-main-filename@2.0.0: {} + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -9888,6 +10205,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.5.7: {} + source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 @@ -9983,6 +10302,8 @@ snapshots: stylis: 4.3.2 tslib: 2.6.2 + stylis@4.2.0: {} + stylis@4.3.2: {} stylis@4.3.5: {} @@ -10278,6 +10599,12 @@ snapshots: punycode: 1.4.1 qs: 6.13.0 + use-isomorphic-layout-effect@1.2.0(@types/react@19.0.7)(react@19.0.0): + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.7 + use-sync-external-store@1.2.0(react@19.0.0): dependencies: react: 19.0.0 @@ -10545,6 +10872,8 @@ snapshots: yallist@3.1.1: {} + yaml@1.10.2: {} + yaml@2.7.0: {} yargs-parser@18.1.3: