From 625937202c90a5bc5e592682a5918e7123ce91cd Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 08:15:21 +0200 Subject: [PATCH 01/13] move useEntityPrivate to separate function --- .../src/HypergraphSpaceContext.tsx | 70 +----------- .../src/internal/use-entity-private.tsx | 106 ++++++++++++++++++ 2 files changed, 107 insertions(+), 69 deletions(-) create mode 100644 packages/hypergraph-react/src/internal/use-entity-private.tsx diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index f912ae98..119984b9 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -14,6 +14,7 @@ import { useSyncExternalStore, } from 'react'; import { useHypergraphApp } from './HypergraphAppContext.js'; +import { useEntityPrivate } from './internal/use-entity-private.js'; import { useEntityPublic } from './internal/use-entity-public.js'; import { usePublicSpace } from './internal/use-public-space.js'; @@ -182,75 +183,6 @@ export function useQueryLocal(type: S, para return { entities, deletedEntities }; } -function useEntityPrivate( - type: S, - params: { - id: string | Id; - enabled?: boolean; - space?: string; - include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined; - }, -) { - const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const { space: spaceFromParams, include, id, enabled = true } = params; - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled }); - const prevEntityRef = useRef<{ - data: Entity.Entity | undefined; - invalidEntity: Record | undefined; - isPending: boolean; - isError: boolean; - }>({ data: undefined, invalidEntity: undefined, isPending: false, isError: false }); - const equals = Schema.equivalence(type); - - const subscribe = (callback: () => void) => { - if (!handle || !enabled) { - return () => {}; - } - const handleChange = () => { - callback(); - }; - - const handleDelete = () => { - callback(); - }; - - handle.on('change', handleChange); - handle.on('delete', handleDelete); - - return () => { - handle.off('change', handleChange); - handle.off('delete', handleDelete); - }; - }; - - return useSyncExternalStore(subscribe, () => { - if (!handle || !enabled) { - return prevEntityRef.current; - } - const doc = handle.doc(); - if (doc === undefined) { - return prevEntityRef.current; - } - - const found = Entity.findOne(handle, type, include)(id); - if (found === undefined && prevEntityRef.current.data !== undefined) { - // entity was maybe deleted, delete from the ref - prevEntityRef.current = { data: undefined, invalidEntity: undefined, isPending: false, isError: false }; - } else if (found !== undefined && prevEntityRef.current.data === undefined) { - prevEntityRef.current = { data: found, invalidEntity: undefined, isPending: false, isError: false }; - } else if ( - found !== undefined && - prevEntityRef.current.data !== undefined && - !equals(found, prevEntityRef.current.data) - ) { - // found and ref have a value, compare for equality, if they are not equal, update the ref and return - prevEntityRef.current = { data: found, invalidEntity: undefined, isPending: false, isError: false }; - } - - return prevEntityRef.current; - }); -} - export function useEntity( type: S, params: { diff --git a/packages/hypergraph-react/src/internal/use-entity-private.tsx b/packages/hypergraph-react/src/internal/use-entity-private.tsx new file mode 100644 index 00000000..c31c69e5 --- /dev/null +++ b/packages/hypergraph-react/src/internal/use-entity-private.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { Entity, type Id, store } from '@graphprotocol/hypergraph'; +import { useSelector } from '@xstate/store/react'; +import * as Schema from 'effect/Schema'; +import { useEffect, useRef, useSyncExternalStore } from 'react'; +import { useHypergraphApp } from '../HypergraphAppContext.js'; +import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; + +const subscribeToSpaceCache = new Map(); + +function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { + const handle = useSelector(store, (state) => { + const space = state.context.spaces.find((space) => space.id === spaceId); + if (!space) { + return undefined; + } + return space.automergeDocHandle; + }); + + const { subscribeToSpace, isConnecting } = useHypergraphApp(); + useEffect(() => { + if (!isConnecting && enabled) { + if (subscribeToSpaceCache.has(spaceId)) { + return; + } + subscribeToSpaceCache.set(spaceId, true); + subscribeToSpace({ spaceId }); + } + return () => { + // TODO: unsubscribe from space in case the space ID changes + subscribeToSpaceCache.delete(spaceId); + }; + }, [isConnecting, subscribeToSpace, spaceId, enabled]); + + return handle; +} + +export function useEntityPrivate( + type: S, + params: { + id: string | Id; + enabled?: boolean; + space?: string; + include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined; + }, +) { + const { space: spaceFromContext } = useHypergraphSpaceInternal(); + const { space: spaceFromParams, include, id, enabled = true } = params; + const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled }); + const prevEntityRef = useRef<{ + data: Entity.Entity | undefined; + invalidEntity: Record | undefined; + isPending: boolean; + isError: boolean; + }>({ data: undefined, invalidEntity: undefined, isPending: false, isError: false }); + const equals = Schema.equivalence(type); + + const subscribe = (callback: () => void) => { + if (!handle || !enabled) { + return () => {}; + } + const handleChange = () => { + callback(); + }; + + const handleDelete = () => { + callback(); + }; + + handle.on('change', handleChange); + handle.on('delete', handleDelete); + + return () => { + handle.off('change', handleChange); + handle.off('delete', handleDelete); + }; + }; + + return useSyncExternalStore(subscribe, () => { + if (!handle || !enabled) { + return prevEntityRef.current; + } + const doc = handle.doc(); + if (doc === undefined) { + return prevEntityRef.current; + } + + const found = Entity.findOne(handle, type, include)(id); + if (found === undefined && prevEntityRef.current.data !== undefined) { + // entity was maybe deleted, delete from the ref + prevEntityRef.current = { data: undefined, invalidEntity: undefined, isPending: false, isError: false }; + } else if (found !== undefined && prevEntityRef.current.data === undefined) { + prevEntityRef.current = { data: found, invalidEntity: undefined, isPending: false, isError: false }; + } else if ( + found !== undefined && + prevEntityRef.current.data !== undefined && + !equals(found, prevEntityRef.current.data) + ) { + // found and ref have a value, compare for equality, if they are not equal, update the ref and return + prevEntityRef.current = { data: found, invalidEntity: undefined, isPending: false, isError: false }; + } + + return prevEntityRef.current; + }); +} From 5048af015f15b9793c01ce2d50c5bdaa07469dfb Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 08:20:44 +0200 Subject: [PATCH 02/13] move useEntity to separate file --- .../src/HypergraphSpaceContext.tsx | 25 ++----------------- packages/hypergraph-react/src/index.ts | 2 +- .../src/internal/use-entity-private.tsx | 2 -- packages/hypergraph-react/src/use-entity.tsx | 25 +++++++++++++++++++ .../test/HypergraphSpaceContext.test.tsx | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) create mode 100644 packages/hypergraph-react/src/use-entity.tsx diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index 119984b9..40b06caf 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -1,8 +1,8 @@ 'use client'; -import { Entity, type Id, store } from '@graphprotocol/hypergraph'; +import { Entity, store } from '@graphprotocol/hypergraph'; import { useSelector } from '@xstate/store/react'; -import * as Schema from 'effect/Schema'; +import type * as Schema from 'effect/Schema'; import { createContext, type ReactNode, @@ -14,8 +14,6 @@ import { useSyncExternalStore, } from 'react'; import { useHypergraphApp } from './HypergraphAppContext.js'; -import { useEntityPrivate } from './internal/use-entity-private.js'; -import { useEntityPublic } from './internal/use-entity-public.js'; import { usePublicSpace } from './internal/use-public-space.js'; // TODO space can be undefined @@ -182,22 +180,3 @@ export function useQueryLocal(type: S, para return { entities, deletedEntities }; } - -export function useEntity( - type: S, - params: { - id: string | Id; - space?: string; - mode: 'private' | 'public'; - include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined; - }, -) { - const resultPublic = useEntityPublic(type, { ...params, enabled: params.mode === 'public' }); - const resultPrivate = useEntityPrivate(type, { ...params, enabled: params.mode === 'private' }); - - if (params.mode === 'public') { - return resultPublic; - } - - return resultPrivate; -} diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index 26da47dc..957eaf3e 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -9,7 +9,6 @@ export { HypergraphSpaceProvider, useCreateEntity, useDeleteEntity, - useEntity, useHardDeleteEntity, useQueryLocal as _useQueryLocal, useRemoveRelation, @@ -31,4 +30,5 @@ export { useQueryPublic as _useQueryPublic } from './internal/use-query-public.j export { preparePublish } from './prepare-publish.js'; export { publishOps } from './publish-ops.js'; export type * from './types.js'; +export { useEntity } from './use-entity.js'; export { useQuery } from './use-query.js'; diff --git a/packages/hypergraph-react/src/internal/use-entity-private.tsx b/packages/hypergraph-react/src/internal/use-entity-private.tsx index c31c69e5..2181f541 100644 --- a/packages/hypergraph-react/src/internal/use-entity-private.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-private.tsx @@ -1,5 +1,3 @@ -'use client'; - import { Entity, type Id, store } from '@graphprotocol/hypergraph'; import { useSelector } from '@xstate/store/react'; import * as Schema from 'effect/Schema'; diff --git a/packages/hypergraph-react/src/use-entity.tsx b/packages/hypergraph-react/src/use-entity.tsx new file mode 100644 index 00000000..afb2b215 --- /dev/null +++ b/packages/hypergraph-react/src/use-entity.tsx @@ -0,0 +1,25 @@ +'use client'; + +import type { Entity, Id } from '@graphprotocol/hypergraph'; +import type * as Schema from 'effect/Schema'; +import { useEntityPrivate } from './internal/use-entity-private.js'; +import { useEntityPublic } from './internal/use-entity-public.js'; + +export function useEntity( + type: S, + params: { + id: string | Id; + space?: string; + mode: 'private' | 'public'; + include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined; + }, +) { + const resultPublic = useEntityPublic(type, { ...params, enabled: params.mode === 'public' }); + const resultPrivate = useEntityPrivate(type, { ...params, enabled: params.mode === 'private' }); + + if (params.mode === 'public') { + return resultPublic; + } + + return resultPrivate; +} diff --git a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx index ced02a44..7fe967e4 100644 --- a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx +++ b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx @@ -10,10 +10,10 @@ import { HypergraphSpaceProvider, useCreateEntity, useDeleteEntity, - useEntity, useQueryLocal, useUpdateEntity, } from '../src/HypergraphSpaceContext.js'; +import { useEntity } from '../src/use-entity.js'; afterEach(() => { cleanup(); From 22788ee9b993e19f94341ff2358e2304148157b4 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 08:24:16 +0200 Subject: [PATCH 03/13] rename useQueryLocal to useQueryPrivate --- packages/hypergraph-react/src/HypergraphSpaceContext.tsx | 2 +- packages/hypergraph-react/src/index.ts | 2 +- packages/hypergraph-react/src/use-query.tsx | 4 ++-- .../hypergraph-react/test/HypergraphSpaceContext.test.tsx | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index 40b06caf..eac2e1a1 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -138,7 +138,7 @@ type QueryParams = { include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined; }; -export function useQueryLocal(type: S, params?: QueryParams) { +export function useQueryPrivate(type: S, params?: QueryParams) { const { enabled = true, filter, include, space: spaceFromParams } = params ?? {}; const entitiesRef = useRef[]>([]); const subscriptionRef = useRef>({ diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index 957eaf3e..f74e6dea 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -10,7 +10,7 @@ export { useCreateEntity, useDeleteEntity, useHardDeleteEntity, - useQueryLocal as _useQueryLocal, + useQueryPrivate as _useQueryPrivate, useRemoveRelation, useSpace, useUpdateEntity, diff --git a/packages/hypergraph-react/src/use-query.tsx b/packages/hypergraph-react/src/use-query.tsx index 11e03548..ac583e64 100644 --- a/packages/hypergraph-react/src/use-query.tsx +++ b/packages/hypergraph-react/src/use-query.tsx @@ -1,6 +1,6 @@ import type { Entity } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; -import { useQueryLocal } from './HypergraphSpaceContext.js'; +import { useQueryPrivate } from './HypergraphSpaceContext.js'; import { useQueryPublic } from './internal/use-query-public.js'; type QueryParams = { @@ -17,7 +17,7 @@ const preparePublishDummy = () => undefined; export function useQuery(type: S, params: QueryParams) { const { mode, filter, include, space, first } = params; const publicResult = useQueryPublic(type, { enabled: mode === 'public', filter, include, first, space }); - const localResult = useQueryLocal(type, { enabled: mode === 'private', filter, include, space }); + const localResult = useQueryPrivate(type, { enabled: mode === 'private', filter, include, space }); if (mode === 'public') { return { diff --git a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx index 7fe967e4..701359c6 100644 --- a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx +++ b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx @@ -10,7 +10,7 @@ import { HypergraphSpaceProvider, useCreateEntity, useDeleteEntity, - useQueryLocal, + useQueryPrivate, useUpdateEntity, } from '../src/HypergraphSpaceContext.js'; import { useEntity } from '../src/use-entity.js'; @@ -76,7 +76,7 @@ describe('HypergraphSpaceContext', () => { describe('useCreateEntity', () => { it('should be able to create an entity through the useCreateEntity Hook', async () => { - const { result: queryEntitiesResult, rerender } = renderHook(() => useQueryLocal(Event), { wrapper }); + const { result: queryEntitiesResult, rerender } = renderHook(() => useQueryPrivate(Event), { wrapper }); const { result: createEntityResult } = renderHook(() => useCreateEntity(Event), { wrapper }); let createdEntity: Entity.Entity | null = null; @@ -165,7 +165,7 @@ describe('HypergraphSpaceContext', () => { isError: false, }); - const { result: queryEntitiesResult, rerender } = renderHook(() => useQueryLocal(Person), { wrapper }); + const { result: queryEntitiesResult, rerender } = renderHook(() => useQueryPrivate(Person), { wrapper }); rerender(); @@ -194,7 +194,7 @@ describe('HypergraphSpaceContext', () => { ); }); - const { result: queryEntitiesResult, rerender: rerenderQueryEntities } = renderHook(() => useQueryLocal(User), { + const { result: queryEntitiesResult, rerender: rerenderQueryEntities } = renderHook(() => useQueryPrivate(User), { wrapper, }); rerenderQueryEntities(); From f0617d8a25b86fd47d77cb727e2c56b6fa44f429 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 08:29:23 +0200 Subject: [PATCH 04/13] move useQueryPrivate to a separate file --- .../src/HypergraphSpaceContext.tsx | 62 +------------ packages/hypergraph-react/src/index.ts | 16 ++-- .../src/internal/use-query-private.tsx | 87 +++++++++++++++++++ packages/hypergraph-react/src/use-query.tsx | 2 +- .../test/HypergraphSpaceContext.test.tsx | 2 +- 5 files changed, 98 insertions(+), 71 deletions(-) create mode 100644 packages/hypergraph-react/src/internal/use-query-private.tsx diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index eac2e1a1..810f5fb5 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -2,17 +2,7 @@ import { Entity, store } from '@graphprotocol/hypergraph'; import { useSelector } from '@xstate/store/react'; -import type * as Schema from 'effect/Schema'; -import { - createContext, - type ReactNode, - useContext, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useSyncExternalStore, -} from 'react'; +import { createContext, type ReactNode, useContext, useEffect } from 'react'; import { useHypergraphApp } from './HypergraphAppContext.js'; import { usePublicSpace } from './internal/use-public-space.js'; @@ -130,53 +120,3 @@ export function useHardDeleteEntity(options?: { space?: string }) { } return Entity.delete(handle); } - -type QueryParams = { - space?: string | undefined; - enabled: boolean; - filter?: Entity.EntityFilter> | undefined; - include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined; -}; - -export function useQueryPrivate(type: S, params?: QueryParams) { - const { enabled = true, filter, include, space: spaceFromParams } = params ?? {}; - const entitiesRef = useRef[]>([]); - const subscriptionRef = useRef>({ - subscribe: () => () => undefined, - getEntities: () => entitiesRef.current, - }); - const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled }); - const handleIsReady = handle ? handle.isReady() : false; - - // biome-ignore lint/correctness/useExhaustiveDependencies: allow to change filter and include - useLayoutEffect(() => { - if (enabled && handle && handleIsReady) { - const subscription = Entity.subscribeToFindMany(handle, type, filter, include); - subscriptionRef.current.subscribe = subscription.subscribe; - subscriptionRef.current.getEntities = subscription.getEntities; - } - }, [enabled, handleIsReady, handle, type]); - - // TODO: allow to change the enabled state - const allEntities = useSyncExternalStore( - subscriptionRef.current.subscribe, - subscriptionRef.current.getEntities, - () => entitiesRef.current, - ); - - const { entities, deletedEntities } = useMemo(() => { - const entities: Entity.Entity[] = []; - const deletedEntities: Entity.Entity[] = []; - for (const entity of allEntities) { - if (entity.__deleted === true) { - deletedEntities.push(entity); - } else { - entities.push(entity); - } - } - return { entities, deletedEntities }; - }, [allEntities]); - - return { entities, deletedEntities }; -} diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index f74e6dea..b58a857a 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -1,5 +1,12 @@ export { PublishDiff } from './components/publish-diff/publish-diff.js'; export { createWalletClient } from './create-wallet-client.js'; +export { useSpaces } from './hooks/use-spaces.js'; +export { useExternalAccountInbox } from './hooks/useExternalAccountInbox.js'; +export { useExternalSpaceInbox } from './hooks/useExternalSpaceInbox.js'; +export { useOwnAccountInbox } from './hooks/useOwnAccountInbox.js'; +export { useOwnSpaceInbox } from './hooks/useOwnSpaceInbox.js'; +export { usePublicAccountInboxes } from './hooks/usePublicAccountInboxes.js'; +export { usePublishToPublicSpace } from './hooks/usePublishToSpace.js'; export { HypergraphAppProvider, useHypergraphApp, @@ -10,22 +17,15 @@ export { useCreateEntity, useDeleteEntity, useHardDeleteEntity, - useQueryPrivate as _useQueryPrivate, useRemoveRelation, useSpace, useUpdateEntity, } from './HypergraphSpaceContext.js'; -export { useSpaces } from './hooks/use-spaces.js'; -export { useExternalAccountInbox } from './hooks/useExternalAccountInbox.js'; -export { useExternalSpaceInbox } from './hooks/useExternalSpaceInbox.js'; -export { useOwnAccountInbox } from './hooks/useOwnAccountInbox.js'; -export { useOwnSpaceInbox } from './hooks/useOwnSpaceInbox.js'; -export { usePublicAccountInboxes } from './hooks/usePublicAccountInboxes.js'; -export { usePublishToPublicSpace } from './hooks/usePublishToSpace.js'; export { generateDeleteOps as _generateDeleteOps } from './internal/generate-delete-ops.js'; export { useCreateEntityPublic as _useCreateEntityPublic } from './internal/use-create-entity-public.js'; export { useDeleteEntityPublic as _useDeleteEntityPublic } from './internal/use-delete-entity-public.js'; export { useEntityPublic as _useEntityPublic } from './internal/use-entity-public.js'; +export { useQueryPrivate as _useQueryPrivate } from './internal/use-query-private.js'; export { useQueryPublic as _useQueryPublic } from './internal/use-query-public.js'; export { preparePublish } from './prepare-publish.js'; export { publishOps } from './publish-ops.js'; diff --git a/packages/hypergraph-react/src/internal/use-query-private.tsx b/packages/hypergraph-react/src/internal/use-query-private.tsx new file mode 100644 index 00000000..19af436a --- /dev/null +++ b/packages/hypergraph-react/src/internal/use-query-private.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { Entity, store } from '@graphprotocol/hypergraph'; +import { useSelector } from '@xstate/store/react'; +import type * as Schema from 'effect/Schema'; +import { useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react'; +import { useHypergraphApp } from '../HypergraphAppContext.js'; +import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; + +const subscribeToSpaceCache = new Map(); + +function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { + const handle = useSelector(store, (state) => { + const space = state.context.spaces.find((space) => space.id === spaceId); + if (!space) { + return undefined; + } + return space.automergeDocHandle; + }); + + const { subscribeToSpace, isConnecting } = useHypergraphApp(); + useEffect(() => { + if (!isConnecting && enabled) { + if (subscribeToSpaceCache.has(spaceId)) { + return; + } + subscribeToSpaceCache.set(spaceId, true); + subscribeToSpace({ spaceId }); + } + return () => { + // TODO: unsubscribe from space in case the space ID changes + subscribeToSpaceCache.delete(spaceId); + }; + }, [isConnecting, subscribeToSpace, spaceId, enabled]); + + return handle; +} + +type QueryParams = { + space?: string | undefined; + enabled: boolean; + filter?: Entity.EntityFilter> | undefined; + include?: { [K in keyof Schema.Schema.Type]?: Record> } | undefined; +}; + +export function useQueryPrivate(type: S, params?: QueryParams) { + const { enabled = true, filter, include, space: spaceFromParams } = params ?? {}; + const entitiesRef = useRef[]>([]); + const subscriptionRef = useRef>({ + subscribe: () => () => undefined, + getEntities: () => entitiesRef.current, + }); + const { space: spaceFromContext } = useHypergraphSpaceInternal(); + const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled }); + const handleIsReady = handle ? handle.isReady() : false; + + // biome-ignore lint/correctness/useExhaustiveDependencies: allow to change filter and include + useLayoutEffect(() => { + if (enabled && handle && handleIsReady) { + const subscription = Entity.subscribeToFindMany(handle, type, filter, include); + subscriptionRef.current.subscribe = subscription.subscribe; + subscriptionRef.current.getEntities = subscription.getEntities; + } + }, [enabled, handleIsReady, handle, type]); + + // TODO: allow to change the enabled state + const allEntities = useSyncExternalStore( + subscriptionRef.current.subscribe, + subscriptionRef.current.getEntities, + () => entitiesRef.current, + ); + + const { entities, deletedEntities } = useMemo(() => { + const entities: Entity.Entity[] = []; + const deletedEntities: Entity.Entity[] = []; + for (const entity of allEntities) { + if (entity.__deleted === true) { + deletedEntities.push(entity); + } else { + entities.push(entity); + } + } + return { entities, deletedEntities }; + }, [allEntities]); + + return { entities, deletedEntities }; +} diff --git a/packages/hypergraph-react/src/use-query.tsx b/packages/hypergraph-react/src/use-query.tsx index ac583e64..e6ecbc44 100644 --- a/packages/hypergraph-react/src/use-query.tsx +++ b/packages/hypergraph-react/src/use-query.tsx @@ -1,6 +1,6 @@ import type { Entity } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; -import { useQueryPrivate } from './HypergraphSpaceContext.js'; +import { useQueryPrivate } from './internal/use-query-private.js'; import { useQueryPublic } from './internal/use-query-public.js'; type QueryParams = { diff --git a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx index 701359c6..f1fe8036 100644 --- a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx +++ b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx @@ -10,9 +10,9 @@ import { HypergraphSpaceProvider, useCreateEntity, useDeleteEntity, - useQueryPrivate, useUpdateEntity, } from '../src/HypergraphSpaceContext.js'; +import { useQueryPrivate } from '../src/internal/use-query-private.js'; import { useEntity } from '../src/use-entity.js'; afterEach(() => { From 92acb54688bf6853ea1455fa10c5c1daae43003e Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 08:33:11 +0200 Subject: [PATCH 05/13] extract use-subscribe-to-space --- .../src/HypergraphSpaceContext.tsx | 33 ++--------------- .../src/internal/use-entity-private.tsx | 36 ++----------------- .../src/internal/use-query-private.tsx | 36 ++----------------- .../src/internal/use-subscribe-to-space.tsx | 33 +++++++++++++++++ 4 files changed, 41 insertions(+), 97 deletions(-) create mode 100644 packages/hypergraph-react/src/internal/use-subscribe-to-space.tsx diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index 810f5fb5..5f822d0e 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -2,9 +2,9 @@ import { Entity, store } from '@graphprotocol/hypergraph'; import { useSelector } from '@xstate/store/react'; -import { createContext, type ReactNode, useContext, useEffect } from 'react'; -import { useHypergraphApp } from './HypergraphAppContext.js'; +import { createContext, type ReactNode, useContext } from 'react'; import { usePublicSpace } from './internal/use-public-space.js'; +import { useSubscribeToSpaceAndGetHandle } from './internal/use-subscribe-to-space.js'; // TODO space can be undefined export type HypergraphContext = { space: string }; @@ -20,35 +20,6 @@ export function HypergraphSpaceProvider({ space, children }: { space: string; ch return {children}; } -const subscribeToSpaceCache = new Map(); - -function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { - const handle = useSelector(store, (state) => { - const space = state.context.spaces.find((space) => space.id === spaceId); - if (!space) { - return undefined; - } - return space.automergeDocHandle; - }); - - const { subscribeToSpace, isConnecting } = useHypergraphApp(); - useEffect(() => { - if (!isConnecting && enabled) { - if (subscribeToSpaceCache.has(spaceId)) { - return; - } - subscribeToSpaceCache.set(spaceId, true); - subscribeToSpace({ spaceId }); - } - return () => { - // TODO: unsubscribe from space in case the space ID changes - subscribeToSpaceCache.delete(spaceId); - }; - }, [isConnecting, subscribeToSpace, spaceId, enabled]); - - return handle; -} - export function useSpace(options: { space?: string; mode: 'private' | 'public' }) { const { space: spaceIdFromContext } = useHypergraphSpaceInternal(); const { space: spaceIdFromParams } = options ?? {}; diff --git a/packages/hypergraph-react/src/internal/use-entity-private.tsx b/packages/hypergraph-react/src/internal/use-entity-private.tsx index 2181f541..6c34ac7f 100644 --- a/packages/hypergraph-react/src/internal/use-entity-private.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-private.tsx @@ -1,38 +1,8 @@ -import { Entity, type Id, store } from '@graphprotocol/hypergraph'; -import { useSelector } from '@xstate/store/react'; +import { Entity, type Id } from '@graphprotocol/hypergraph'; import * as Schema from 'effect/Schema'; -import { useEffect, useRef, useSyncExternalStore } from 'react'; -import { useHypergraphApp } from '../HypergraphAppContext.js'; +import { useRef, useSyncExternalStore } from 'react'; import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; - -const subscribeToSpaceCache = new Map(); - -function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { - const handle = useSelector(store, (state) => { - const space = state.context.spaces.find((space) => space.id === spaceId); - if (!space) { - return undefined; - } - return space.automergeDocHandle; - }); - - const { subscribeToSpace, isConnecting } = useHypergraphApp(); - useEffect(() => { - if (!isConnecting && enabled) { - if (subscribeToSpaceCache.has(spaceId)) { - return; - } - subscribeToSpaceCache.set(spaceId, true); - subscribeToSpace({ spaceId }); - } - return () => { - // TODO: unsubscribe from space in case the space ID changes - subscribeToSpaceCache.delete(spaceId); - }; - }, [isConnecting, subscribeToSpace, spaceId, enabled]); - - return handle; -} +import { useSubscribeToSpaceAndGetHandle } from './use-subscribe-to-space.js'; export function useEntityPrivate( type: S, diff --git a/packages/hypergraph-react/src/internal/use-query-private.tsx b/packages/hypergraph-react/src/internal/use-query-private.tsx index 19af436a..dd9390df 100644 --- a/packages/hypergraph-react/src/internal/use-query-private.tsx +++ b/packages/hypergraph-react/src/internal/use-query-private.tsx @@ -1,40 +1,10 @@ 'use client'; -import { Entity, store } from '@graphprotocol/hypergraph'; -import { useSelector } from '@xstate/store/react'; +import { Entity } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; -import { useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react'; -import { useHypergraphApp } from '../HypergraphAppContext.js'; +import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react'; import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; - -const subscribeToSpaceCache = new Map(); - -function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { - const handle = useSelector(store, (state) => { - const space = state.context.spaces.find((space) => space.id === spaceId); - if (!space) { - return undefined; - } - return space.automergeDocHandle; - }); - - const { subscribeToSpace, isConnecting } = useHypergraphApp(); - useEffect(() => { - if (!isConnecting && enabled) { - if (subscribeToSpaceCache.has(spaceId)) { - return; - } - subscribeToSpaceCache.set(spaceId, true); - subscribeToSpace({ spaceId }); - } - return () => { - // TODO: unsubscribe from space in case the space ID changes - subscribeToSpaceCache.delete(spaceId); - }; - }, [isConnecting, subscribeToSpace, spaceId, enabled]); - - return handle; -} +import { useSubscribeToSpaceAndGetHandle } from './use-subscribe-to-space.js'; type QueryParams = { space?: string | undefined; diff --git a/packages/hypergraph-react/src/internal/use-subscribe-to-space.tsx b/packages/hypergraph-react/src/internal/use-subscribe-to-space.tsx new file mode 100644 index 00000000..b002a29b --- /dev/null +++ b/packages/hypergraph-react/src/internal/use-subscribe-to-space.tsx @@ -0,0 +1,33 @@ +import { store } from '@graphprotocol/hypergraph'; +import { useSelector } from '@xstate/store/react'; +import { useEffect } from 'react'; +import { useHypergraphApp } from '../HypergraphAppContext.js'; + +const subscribeToSpaceCache = new Map(); + +export function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { + const handle = useSelector(store, (state) => { + const space = state.context.spaces.find((space) => space.id === spaceId); + if (!space) { + return undefined; + } + return space.automergeDocHandle; + }); + + const { subscribeToSpace, isConnecting } = useHypergraphApp(); + useEffect(() => { + if (!isConnecting && enabled) { + if (subscribeToSpaceCache.has(spaceId)) { + return; + } + subscribeToSpaceCache.set(spaceId, true); + subscribeToSpace({ spaceId }); + } + return () => { + // TODO: unsubscribe from space in case the space ID changes + subscribeToSpaceCache.delete(spaceId); + }; + }, [isConnecting, subscribeToSpace, spaceId, enabled]); + + return handle; +} From fea5c5d4b7b9f931b673ea3d5cb6df8fde3ff72d Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 08:39:14 +0200 Subject: [PATCH 06/13] fix lint --- packages/hypergraph-react/src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index b58a857a..f81b0093 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -1,12 +1,5 @@ export { PublishDiff } from './components/publish-diff/publish-diff.js'; export { createWalletClient } from './create-wallet-client.js'; -export { useSpaces } from './hooks/use-spaces.js'; -export { useExternalAccountInbox } from './hooks/useExternalAccountInbox.js'; -export { useExternalSpaceInbox } from './hooks/useExternalSpaceInbox.js'; -export { useOwnAccountInbox } from './hooks/useOwnAccountInbox.js'; -export { useOwnSpaceInbox } from './hooks/useOwnSpaceInbox.js'; -export { usePublicAccountInboxes } from './hooks/usePublicAccountInboxes.js'; -export { usePublishToPublicSpace } from './hooks/usePublishToSpace.js'; export { HypergraphAppProvider, useHypergraphApp, @@ -21,6 +14,13 @@ export { useSpace, useUpdateEntity, } from './HypergraphSpaceContext.js'; +export { useSpaces } from './hooks/use-spaces.js'; +export { useExternalAccountInbox } from './hooks/useExternalAccountInbox.js'; +export { useExternalSpaceInbox } from './hooks/useExternalSpaceInbox.js'; +export { useOwnAccountInbox } from './hooks/useOwnAccountInbox.js'; +export { useOwnSpaceInbox } from './hooks/useOwnSpaceInbox.js'; +export { usePublicAccountInboxes } from './hooks/usePublicAccountInboxes.js'; +export { usePublishToPublicSpace } from './hooks/usePublishToSpace.js'; export { generateDeleteOps as _generateDeleteOps } from './internal/generate-delete-ops.js'; export { useCreateEntityPublic as _useCreateEntityPublic } from './internal/use-create-entity-public.js'; export { useDeleteEntityPublic as _useDeleteEntityPublic } from './internal/use-delete-entity-public.js'; From a5c5bb61353340aaf421f5e6b5b767a4d32d84f2 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 08:54:24 +0200 Subject: [PATCH 07/13] extract use-hypergraph-space-internal --- packages/hypergraph-react/src/HypergraphSpaceContext.tsx | 8 ++------ .../hypergraph-react/src/internal/use-entity-private.tsx | 2 +- .../hypergraph-react/src/internal/use-entity-public.tsx | 2 +- .../src/internal/use-hypergraph-space-internal.tsx | 7 +++++++ .../hypergraph-react/src/internal/use-query-private.tsx | 4 +--- .../hypergraph-react/src/internal/use-query-public.tsx | 2 +- 6 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 packages/hypergraph-react/src/internal/use-hypergraph-space-internal.tsx diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index 5f822d0e..5eee8d46 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -2,7 +2,8 @@ import { Entity, store } from '@graphprotocol/hypergraph'; import { useSelector } from '@xstate/store/react'; -import { createContext, type ReactNode, useContext } from 'react'; +import { createContext, type ReactNode } from 'react'; +import { useHypergraphSpaceInternal } from './internal/use-hypergraph-space-internal.js'; import { usePublicSpace } from './internal/use-public-space.js'; import { useSubscribeToSpaceAndGetHandle } from './internal/use-subscribe-to-space.js'; @@ -11,11 +12,6 @@ export type HypergraphContext = { space: string }; export const HypergraphReactContext = createContext(undefined); -export function useHypergraphSpaceInternal() { - const context = useContext(HypergraphReactContext); - return (context as HypergraphContext) || { space: '' }; -} - export function HypergraphSpaceProvider({ space, children }: { space: string; children: ReactNode }) { return {children}; } diff --git a/packages/hypergraph-react/src/internal/use-entity-private.tsx b/packages/hypergraph-react/src/internal/use-entity-private.tsx index 6c34ac7f..9dacf179 100644 --- a/packages/hypergraph-react/src/internal/use-entity-private.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-private.tsx @@ -1,7 +1,7 @@ import { Entity, type Id } from '@graphprotocol/hypergraph'; import * as Schema from 'effect/Schema'; import { useRef, useSyncExternalStore } from 'react'; -import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; +import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; import { useSubscribeToSpaceAndGetHandle } from './use-subscribe-to-space.js'; export function useEntityPrivate( diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index 7ccd744d..ce608107 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -6,7 +6,7 @@ import * as Either from 'effect/Either'; import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; -import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; +import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; const entityQueryDocumentLevel0 = gql` query entity($id: UUID!, $spaceId: UUID!) { diff --git a/packages/hypergraph-react/src/internal/use-hypergraph-space-internal.tsx b/packages/hypergraph-react/src/internal/use-hypergraph-space-internal.tsx new file mode 100644 index 00000000..6f9c37ba --- /dev/null +++ b/packages/hypergraph-react/src/internal/use-hypergraph-space-internal.tsx @@ -0,0 +1,7 @@ +import { useContext } from 'react'; +import { type HypergraphContext, HypergraphReactContext } from '../HypergraphSpaceContext.js'; + +export function useHypergraphSpaceInternal() { + const context = useContext(HypergraphReactContext); + return (context as HypergraphContext) || { space: '' }; +} diff --git a/packages/hypergraph-react/src/internal/use-query-private.tsx b/packages/hypergraph-react/src/internal/use-query-private.tsx index dd9390df..7424a50d 100644 --- a/packages/hypergraph-react/src/internal/use-query-private.tsx +++ b/packages/hypergraph-react/src/internal/use-query-private.tsx @@ -1,9 +1,7 @@ -'use client'; - import { Entity } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react'; -import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; +import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; import { useSubscribeToSpaceAndGetHandle } from './use-subscribe-to-space.js'; type QueryParams = { diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index fe64a7ba..89f31c7c 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -6,9 +6,9 @@ import * as Either from 'effect/Either'; import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; -import { useHypergraphSpaceInternal } from '../HypergraphSpaceContext.js'; import { translateFilterToGraphql } from './translate-filter-to-graphql.js'; import type { QueryPublicParams } from './types.js'; +import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; const entitiesQueryDocumentLevel0 = gql` query entities($spaceId: UUID!, $typeIds: [UUID!]!, $first: Int, $filter: EntityFilter!) { From 4076c1884281ed65fc3738280a8eec2676048540 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 09:24:43 +0200 Subject: [PATCH 08/13] extract hooks into separate files --- .../src/HypergraphSpaceContext.tsx | 77 ------------------- .../src/hooks/use-create-entity.ts | 18 +++++ .../src/hooks/use-delete-entity.ts | 17 ++++ .../src/hooks/use-hard-delete-entity.ts | 17 ++++ .../src/hooks/use-remove-relation.ts | 17 ++++ .../hypergraph-react/src/hooks/use-space.ts | 18 +++++ .../src/hooks/use-update-entity.ts | 17 ++++ packages/hypergraph-react/src/index.ts | 16 ++-- .../test/HypergraphSpaceContext.test.tsx | 10 +-- 9 files changed, 115 insertions(+), 92 deletions(-) create mode 100644 packages/hypergraph-react/src/hooks/use-create-entity.ts create mode 100644 packages/hypergraph-react/src/hooks/use-delete-entity.ts create mode 100644 packages/hypergraph-react/src/hooks/use-hard-delete-entity.ts create mode 100644 packages/hypergraph-react/src/hooks/use-remove-relation.ts create mode 100644 packages/hypergraph-react/src/hooks/use-space.ts create mode 100644 packages/hypergraph-react/src/hooks/use-update-entity.ts diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index 5eee8d46..21b3196c 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -1,11 +1,6 @@ 'use client'; -import { Entity, store } from '@graphprotocol/hypergraph'; -import { useSelector } from '@xstate/store/react'; import { createContext, type ReactNode } from 'react'; -import { useHypergraphSpaceInternal } from './internal/use-hypergraph-space-internal.js'; -import { usePublicSpace } from './internal/use-public-space.js'; -import { useSubscribeToSpaceAndGetHandle } from './internal/use-subscribe-to-space.js'; // TODO space can be undefined export type HypergraphContext = { space: string }; @@ -15,75 +10,3 @@ export const HypergraphReactContext = createContext{children}; } - -export function useSpace(options: { space?: string; mode: 'private' | 'public' }) { - const { space: spaceIdFromContext } = useHypergraphSpaceInternal(); - const { space: spaceIdFromParams } = options ?? {}; - const spaceId = spaceIdFromParams ?? spaceIdFromContext; - const handle = useSubscribeToSpaceAndGetHandle({ spaceId, enabled: options.mode === 'private' }); - const ready = options.mode === 'public' ? true : handle ? handle.isReady() : false; - const privateSpace = useSelector(store, (state) => state.context.spaces.find((space) => space.id === spaceId)); - const publicSpace = usePublicSpace({ spaceId, enabled: options.mode === 'public' }); - return { ready, name: options.mode === 'private' ? privateSpace?.name : publicSpace?.name, id: spaceId }; -} - -export function useCreateEntity(type: S, options?: { space?: string }) { - const { space: spaceIdFromParams } = options ?? {}; - const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const spaceId = spaceIdFromParams ?? spaceFromContext; - const handle = useSubscribeToSpaceAndGetHandle({ spaceId, enabled: true }); - if (!handle) { - return () => { - throw new Error('Space not found or not ready'); - }; - } - return Entity.create(handle, type); -} - -export function useUpdateEntity(type: S, options?: { space?: string }) { - const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const { space } = options ?? {}; - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); - if (!handle) { - return () => { - throw new Error('Space not found or not ready'); - }; - } - return Entity.update(handle, type); -} - -export function useDeleteEntity(options?: { space?: string }) { - const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const { space } = options ?? {}; - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); - if (!handle) { - return () => { - throw new Error('Space not found or not ready'); - }; - } - return Entity.markAsDeleted(handle); -} - -export function useRemoveRelation(options?: { space?: string }) { - const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const { space } = options ?? {}; - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); - if (!handle) { - return () => { - throw new Error('Space not found or not ready'); - }; - } - return Entity.removeRelation(handle); -} - -export function useHardDeleteEntity(options?: { space?: string }) { - const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const { space } = options ?? {}; - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); - if (!handle) { - return () => { - throw new Error('Space not found or not ready'); - }; - } - return Entity.delete(handle); -} diff --git a/packages/hypergraph-react/src/hooks/use-create-entity.ts b/packages/hypergraph-react/src/hooks/use-create-entity.ts new file mode 100644 index 00000000..8f9d6a47 --- /dev/null +++ b/packages/hypergraph-react/src/hooks/use-create-entity.ts @@ -0,0 +1,18 @@ +'use client'; + +import { Entity } from '@graphprotocol/hypergraph'; +import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js'; +import { useSubscribeToSpaceAndGetHandle } from '../internal/use-subscribe-to-space.js'; + +export function useCreateEntity(type: S, options?: { space?: string }) { + const { space: spaceIdFromParams } = options ?? {}; + const { space: spaceFromContext } = useHypergraphSpaceInternal(); + const spaceId = spaceIdFromParams ?? spaceFromContext; + const handle = useSubscribeToSpaceAndGetHandle({ spaceId, enabled: true }); + if (!handle) { + return () => { + throw new Error('Space not found or not ready'); + }; + } + return Entity.create(handle, type); +} diff --git a/packages/hypergraph-react/src/hooks/use-delete-entity.ts b/packages/hypergraph-react/src/hooks/use-delete-entity.ts new file mode 100644 index 00000000..c7667ebc --- /dev/null +++ b/packages/hypergraph-react/src/hooks/use-delete-entity.ts @@ -0,0 +1,17 @@ +'use client'; + +import { Entity } from '@graphprotocol/hypergraph'; +import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js'; +import { useSubscribeToSpaceAndGetHandle } from '../internal/use-subscribe-to-space.js'; + +export function useDeleteEntity(options?: { space?: string }) { + const { space: spaceFromContext } = useHypergraphSpaceInternal(); + const { space } = options ?? {}; + const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); + if (!handle) { + return () => { + throw new Error('Space not found or not ready'); + }; + } + return Entity.markAsDeleted(handle); +} diff --git a/packages/hypergraph-react/src/hooks/use-hard-delete-entity.ts b/packages/hypergraph-react/src/hooks/use-hard-delete-entity.ts new file mode 100644 index 00000000..df33d2e1 --- /dev/null +++ b/packages/hypergraph-react/src/hooks/use-hard-delete-entity.ts @@ -0,0 +1,17 @@ +'use client'; + +import { Entity } from '@graphprotocol/hypergraph'; +import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js'; +import { useSubscribeToSpaceAndGetHandle } from '../internal/use-subscribe-to-space.js'; + +export function useHardDeleteEntity(options?: { space?: string }) { + const { space: spaceFromContext } = useHypergraphSpaceInternal(); + const { space } = options ?? {}; + const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); + if (!handle) { + return () => { + throw new Error('Space not found or not ready'); + }; + } + return Entity.delete(handle); +} diff --git a/packages/hypergraph-react/src/hooks/use-remove-relation.ts b/packages/hypergraph-react/src/hooks/use-remove-relation.ts new file mode 100644 index 00000000..35841b81 --- /dev/null +++ b/packages/hypergraph-react/src/hooks/use-remove-relation.ts @@ -0,0 +1,17 @@ +'use client'; + +import { Entity } from '@graphprotocol/hypergraph'; +import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js'; +import { useSubscribeToSpaceAndGetHandle } from '../internal/use-subscribe-to-space.js'; + +export function useRemoveRelation(options?: { space?: string }) { + const { space: spaceFromContext } = useHypergraphSpaceInternal(); + const { space } = options ?? {}; + const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); + if (!handle) { + return () => { + throw new Error('Space not found or not ready'); + }; + } + return Entity.removeRelation(handle); +} diff --git a/packages/hypergraph-react/src/hooks/use-space.ts b/packages/hypergraph-react/src/hooks/use-space.ts new file mode 100644 index 00000000..e994e158 --- /dev/null +++ b/packages/hypergraph-react/src/hooks/use-space.ts @@ -0,0 +1,18 @@ +'use client'; + +import { store } from '@graphprotocol/hypergraph'; +import { useSelector } from '@xstate/store/react'; +import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js'; +import { usePublicSpace } from '../internal/use-public-space.js'; +import { useSubscribeToSpaceAndGetHandle } from '../internal/use-subscribe-to-space.js'; + +export function useSpace(options: { space?: string; mode: 'private' | 'public' }) { + const { space: spaceIdFromContext } = useHypergraphSpaceInternal(); + const { space: spaceIdFromParams } = options ?? {}; + const spaceId = spaceIdFromParams ?? spaceIdFromContext; + const handle = useSubscribeToSpaceAndGetHandle({ spaceId, enabled: options.mode === 'private' }); + const ready = options.mode === 'public' ? true : handle ? handle.isReady() : false; + const privateSpace = useSelector(store, (state) => state.context.spaces.find((space) => space.id === spaceId)); + const publicSpace = usePublicSpace({ spaceId, enabled: options.mode === 'public' }); + return { ready, name: options.mode === 'private' ? privateSpace?.name : publicSpace?.name, id: spaceId }; +} diff --git a/packages/hypergraph-react/src/hooks/use-update-entity.ts b/packages/hypergraph-react/src/hooks/use-update-entity.ts new file mode 100644 index 00000000..414f3d2f --- /dev/null +++ b/packages/hypergraph-react/src/hooks/use-update-entity.ts @@ -0,0 +1,17 @@ +'use client'; + +import { Entity } from '@graphprotocol/hypergraph'; +import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js'; +import { useSubscribeToSpaceAndGetHandle } from '../internal/use-subscribe-to-space.js'; + +export function useUpdateEntity(type: S, options?: { space?: string }) { + const { space: spaceFromContext } = useHypergraphSpaceInternal(); + const { space } = options ?? {}; + const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); + if (!handle) { + return () => { + throw new Error('Space not found or not ready'); + }; + } + return Entity.update(handle, type); +} diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index f81b0093..03f1dd68 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -5,16 +5,14 @@ export { useHypergraphApp, useHypergraphAuth, } from './HypergraphAppContext.js'; -export { - HypergraphSpaceProvider, - useCreateEntity, - useDeleteEntity, - useHardDeleteEntity, - useRemoveRelation, - useSpace, - useUpdateEntity, -} from './HypergraphSpaceContext.js'; +export { HypergraphSpaceProvider } from './HypergraphSpaceContext.js'; +export { useCreateEntity } from './hooks/use-create-entity.js'; +export { useDeleteEntity } from './hooks/use-delete-entity.js'; +export { useHardDeleteEntity } from './hooks/use-hard-delete-entity.js'; +export { useRemoveRelation } from './hooks/use-remove-relation.js'; +export { useSpace } from './hooks/use-space.js'; export { useSpaces } from './hooks/use-spaces.js'; +export { useUpdateEntity } from './hooks/use-update-entity.js'; export { useExternalAccountInbox } from './hooks/useExternalAccountInbox.js'; export { useExternalSpaceInbox } from './hooks/useExternalSpaceInbox.js'; export { useOwnAccountInbox } from './hooks/useOwnAccountInbox.js'; diff --git a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx index f1fe8036..31fab5ca 100644 --- a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx +++ b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx @@ -6,12 +6,10 @@ import '@testing-library/jest-dom/vitest'; import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import type React from 'react'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { - HypergraphSpaceProvider, - useCreateEntity, - useDeleteEntity, - useUpdateEntity, -} from '../src/HypergraphSpaceContext.js'; +import { HypergraphSpaceProvider } from '../src/HypergraphSpaceContext.js'; +import { useCreateEntity } from '../src/hooks/use-create-entity.js'; +import { useDeleteEntity } from '../src/hooks/use-delete-entity.js'; +import { useUpdateEntity } from '../src/hooks/use-update-entity.js'; import { useQueryPrivate } from '../src/internal/use-query-private.js'; import { useEntity } from '../src/use-entity.js'; From 028e4120f85bc8ef691004c24727be26f45a97b9 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 16:05:00 +0200 Subject: [PATCH 09/13] move translate-filter-to-graphql test --- .../{src => test}/internal/translate-filter-to-graphql.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/hypergraph-react/{src => test}/internal/translate-filter-to-graphql.test.ts (99%) diff --git a/packages/hypergraph-react/src/internal/translate-filter-to-graphql.test.ts b/packages/hypergraph-react/test/internal/translate-filter-to-graphql.test.ts similarity index 99% rename from packages/hypergraph-react/src/internal/translate-filter-to-graphql.test.ts rename to packages/hypergraph-react/test/internal/translate-filter-to-graphql.test.ts index 434102dd..59e35708 100644 --- a/packages/hypergraph-react/src/internal/translate-filter-to-graphql.test.ts +++ b/packages/hypergraph-react/test/internal/translate-filter-to-graphql.test.ts @@ -1,7 +1,7 @@ import { Graph, Id } from '@graphprotocol/grc-20'; import { Entity, type Mapping, Type } from '@graphprotocol/hypergraph'; import { describe, expect, it } from 'vitest'; -import { translateFilterToGraphql } from './translate-filter-to-graphql.js'; +import { translateFilterToGraphql } from '../../src/internal/translate-filter-to-graphql.js'; export class Todo extends Entity.Class('Todo')({ name: Type.String, From 7197e7819790e8f376c9e71ea352f628c4d52ecd Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 16:21:59 +0200 Subject: [PATCH 10/13] extract convertPropertyValue --- .../src/internal/convert-property-value.ts | 21 +++++++++++++++++ .../src/internal/use-entity-public.tsx | 23 ++----------------- .../src/internal/use-query-public.tsx | 23 ++----------------- 3 files changed, 25 insertions(+), 42 deletions(-) create mode 100644 packages/hypergraph-react/src/internal/convert-property-value.ts diff --git a/packages/hypergraph-react/src/internal/convert-property-value.ts b/packages/hypergraph-react/src/internal/convert-property-value.ts new file mode 100644 index 00000000..9601a352 --- /dev/null +++ b/packages/hypergraph-react/src/internal/convert-property-value.ts @@ -0,0 +1,21 @@ +import { type Entity, TypeUtils } from '@graphprotocol/hypergraph'; + +export const convertPropertyValue = ( + property: { propertyId: string; string: string; boolean: boolean; number: number; time: string; point: string }, + key: string, + type: Entity.AnyNoContext, +) => { + if (TypeUtils.isBooleanOrOptionalBooleanType(type.fields[key]) && property.boolean !== undefined) { + return Boolean(property.boolean); + } + if (TypeUtils.isPointOrOptionalPointType(type.fields[key]) && property.point !== undefined) { + return property.point; + } + if (TypeUtils.isDateOrOptionalDateType(type.fields[key]) && property.time !== undefined) { + return property.time; + } + if (TypeUtils.isNumberOrOptionalNumberType(type.fields[key]) && property.number !== undefined) { + return Number(property.number); + } + return property.string; +}; diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index ce608107..755e33b3 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -1,11 +1,12 @@ import { Graph } from '@graphprotocol/grc-20'; -import { type Entity, type Mapping, store, TypeUtils } from '@graphprotocol/hypergraph'; +import { type Entity, type Mapping, store } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import { useSelector } from '@xstate/store/react'; import * as Either from 'effect/Either'; import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; +import { convertPropertyValue } from './convert-property-value.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; const entityQueryDocumentLevel0 = gql` @@ -261,26 +262,6 @@ const convertRelations = ( return rawEntity; }; -const convertPropertyValue = ( - property: { propertyId: string; string: string; boolean: boolean; number: number; time: string; point: string }, - key: string, - type: Entity.AnyNoContext, -) => { - if (TypeUtils.isBooleanOrOptionalBooleanType(type.fields[key]) && property.boolean !== undefined) { - return Boolean(property.boolean); - } - if (TypeUtils.isPointOrOptionalPointType(type.fields[key]) && property.point !== undefined) { - return property.point; - } - if (TypeUtils.isDateOrOptionalDateType(type.fields[key]) && property.time !== undefined) { - return property.time; - } - if (TypeUtils.isNumberOrOptionalNumberType(type.fields[key]) && property.number !== undefined) { - return Number(property.number); - } - return property.string; -}; - export const parseResult = ( queryData: EntityQueryResult, type: S, diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index 89f31c7c..c4648a25 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -1,11 +1,12 @@ import { Graph } from '@graphprotocol/grc-20'; -import { type Entity, type Mapping, store, TypeUtils } from '@graphprotocol/hypergraph'; +import { type Entity, type Mapping, store } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import { useSelector } from '@xstate/store/react'; import * as Either from 'effect/Either'; import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; +import { convertPropertyValue } from './convert-property-value.js'; import { translateFilterToGraphql } from './translate-filter-to-graphql.js'; import type { QueryPublicParams } from './types.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; @@ -197,26 +198,6 @@ type RecursiveQueryEntity = { }[]; }; -const convertPropertyValue = ( - property: { propertyId: string; string: string; boolean: boolean; number: number; time: string; point: string }, - key: string, - type: Entity.AnyNoContext, -) => { - if (TypeUtils.isBooleanOrOptionalBooleanType(type.fields[key]) && property.boolean !== undefined) { - return Boolean(property.boolean); - } - if (TypeUtils.isPointOrOptionalPointType(type.fields[key]) && property.point !== undefined) { - return property.point; - } - if (TypeUtils.isDateOrOptionalDateType(type.fields[key]) && property.time !== undefined) { - return property.time; - } - if (TypeUtils.isNumberOrOptionalNumberType(type.fields[key]) && property.number !== undefined) { - return Number(property.number); - } - return property.string; -}; - const convertRelations = ( queryEntity: RecursiveQueryEntity, type: S, From 5aa3efa42bba1d99a0237cabeebbe776a3981c63 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 16:28:45 +0200 Subject: [PATCH 11/13] extract convertRelations --- .../src/internal/convert-relations.ts | 98 +++++++++++++++++ .../src/internal/use-entity-public.tsx | 101 +----------------- .../src/internal/use-query-public.tsx | 97 +---------------- 3 files changed, 100 insertions(+), 196 deletions(-) create mode 100644 packages/hypergraph-react/src/internal/convert-relations.ts diff --git a/packages/hypergraph-react/src/internal/convert-relations.ts b/packages/hypergraph-react/src/internal/convert-relations.ts new file mode 100644 index 00000000..9a74a81d --- /dev/null +++ b/packages/hypergraph-react/src/internal/convert-relations.ts @@ -0,0 +1,98 @@ +import type { Entity, Mapping } from '@graphprotocol/hypergraph'; +import { convertPropertyValue } from './convert-property-value.js'; + +// A recursive representation of the entity structure returned by the public GraphQL +// endpoint. `values` and `relations` are optional because the nested `to` selections +// get slimmer the deeper we traverse in the query. This type intentionally mirrors +// only the fields we actually consume inside `convertRelations`. +type RecursiveQueryEntity = { + id: string; + name: string; + valuesList?: { + propertyId: string; + string: string; + boolean: boolean; + number: number; + time: string; + point: string; + }[]; + relationsList?: { + toEntity: RecursiveQueryEntity; + typeId: string; + }[]; +}; + +export const convertRelations = ( + queryEntity: RecursiveQueryEntity, + type: S, + mappingEntry: Mapping.MappingEntry, + mapping: Mapping.Mapping, +) => { + const rawEntity: Record = {}; + + for (const [key, relationId] of Object.entries(mappingEntry?.relations ?? {})) { + const properties = (queryEntity.relationsList ?? []).filter((a) => a.typeId === relationId); + if (properties.length === 0) { + rawEntity[key] = [] as unknown[]; + continue; + } + + const field = type.fields[key]; + if (!field) { + // @ts-expect-error TODO: properly access the type.name + console.error(`Field ${key} not found in ${type.name}`); + continue; + } + // @ts-expect-error TODO: properly access the type.name + const annotations = field.ast.rest[0].type.to.annotations; + + // TODO: fix this access using proper effect types + const relationTypeName = + annotations[ + Object.getOwnPropertySymbols(annotations).find((sym) => sym.description === 'effect/annotation/Identifier') + ]; + + const relationMappingEntry = mapping[relationTypeName]; + if (!relationMappingEntry) { + console.error(`Relation mapping entry for ${relationTypeName} not found`); + continue; + } + + const newRelationEntities = properties.map((propertyEntry) => { + // @ts-expect-error TODO: properly access the type.name + const type = field.value; + + let rawEntity: Record = { + id: propertyEntry.toEntity.id, + name: propertyEntry.toEntity.name, + // TODO: should be determined by the actual value + __deleted: false, + // TODO: should be determined by the actual value + __version: '', + }; + + // take the mappingEntry and assign the attributes to the rawEntity + for (const [key, value] of Object.entries(relationMappingEntry?.properties ?? {})) { + const property = propertyEntry.toEntity.valuesList?.find((a) => a.propertyId === value); + if (property) { + rawEntity[key] = convertPropertyValue(property, key, type); + } + } + + rawEntity = { + ...rawEntity, + ...convertRelations(propertyEntry.toEntity, type, relationMappingEntry, mapping), + }; + + return rawEntity; + }); + + if (rawEntity[key]) { + rawEntity[key] = [...(rawEntity[key] as unknown[]), ...newRelationEntities]; + } else { + rawEntity[key] = newRelationEntities; + } + } + + return rawEntity; +}; diff --git a/packages/hypergraph-react/src/internal/use-entity-public.tsx b/packages/hypergraph-react/src/internal/use-entity-public.tsx index 755e33b3..3799edd1 100644 --- a/packages/hypergraph-react/src/internal/use-entity-public.tsx +++ b/packages/hypergraph-react/src/internal/use-entity-public.tsx @@ -7,6 +7,7 @@ import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; import { convertPropertyValue } from './convert-property-value.js'; +import { convertRelations } from './convert-relations.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; const entityQueryDocumentLevel0 = gql` @@ -162,106 +163,6 @@ type EntityQueryResult = { } | null; }; -// A recursive representation of the entity structure returned by the public GraphQL -// endpoint. `values` and `relations` are optional because the nested `to` selections -// get slimmer the deeper we traverse in the query. This type intentionally mirrors -// only the fields we actually consume inside `convertRelations`. -type RecursiveQueryEntity = { - id: string; - name: string; - valuesList?: { - propertyId: string; - string: string; - boolean: boolean; - number: number; - time: string; - point: string; - }[]; - relationsList?: { - toEntity: RecursiveQueryEntity; - typeId: string; - }[]; -}; - -const convertRelations = ( - queryEntity: RecursiveQueryEntity, - type: S, - mappingEntry: Mapping.MappingEntry, - mapping: Mapping.Mapping, -) => { - const rawEntity: Record = {}; - - for (const [key, relationId] of Object.entries(mappingEntry?.relations ?? {})) { - const properties = (queryEntity.relationsList ?? []).filter((a) => a.typeId === relationId); - if (properties.length === 0) { - rawEntity[key] = [] as unknown[]; - continue; - } - - const field = type.fields[key]; - if (!field) { - // @ts-expect-error TODO: properly access the type.name - console.error(`Field ${key} not found in ${type.name}`); - continue; - } - // @ts-expect-error TODO: properly access the type.name - const annotations = field.ast.rest[0].type.to.annotations; - - // TODO: fix this access using proper effect types - const relationTypeName = - annotations[ - Object.getOwnPropertySymbols(annotations).find((sym) => sym.description === 'effect/annotation/Identifier') - ]; - - const relationMappingEntry = mapping[relationTypeName]; - if (!relationMappingEntry) { - console.error(`Relation mapping entry for ${relationTypeName} not found`); - continue; - } - - const newRelationEntities = properties.map((propertyEntry) => { - // @ts-expect-error TODO: properly access the type.name - const type = field.value; - - let rawEntity: Record = { - id: propertyEntry.toEntity.id, - name: propertyEntry.toEntity.name, - // TODO: should be determined by the actual value - __deleted: false, - // TODO: should be determined by the actual value - __version: '', - }; - - // take the mappingEntry and assign the attributes to the rawEntity - for (const [key, value] of Object.entries(relationMappingEntry?.properties ?? {})) { - const property = propertyEntry.toEntity.valuesList?.find((a) => a.propertyId === value); - if (property) { - rawEntity[key] = convertPropertyValue(property, key, type); - } - } - - rawEntity = { - ...rawEntity, - ...convertRelations(propertyEntry.toEntity, type, relationMappingEntry, mapping), - }; - - return rawEntity; - }); - - if (rawEntity[key]) { - rawEntity[key] = [ - // @ts-expect-error TODO: properly access the type.name - ...rawEntity[key], - ...newRelationEntities, - ]; - } else { - rawEntity[key] = newRelationEntities; - } - } - - return rawEntity; -}; - export const parseResult = ( queryData: EntityQueryResult, type: S, diff --git a/packages/hypergraph-react/src/internal/use-query-public.tsx b/packages/hypergraph-react/src/internal/use-query-public.tsx index c4648a25..14242d42 100644 --- a/packages/hypergraph-react/src/internal/use-query-public.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public.tsx @@ -7,6 +7,7 @@ import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; import { useMemo } from 'react'; import { convertPropertyValue } from './convert-property-value.js'; +import { convertRelations } from './convert-relations.js'; import { translateFilterToGraphql } from './translate-filter-to-graphql.js'; import type { QueryPublicParams } from './types.js'; import { useHypergraphSpaceInternal } from './use-hypergraph-space-internal.js'; @@ -177,102 +178,6 @@ type EntityQueryResult = { }[]; }; -// A recursive representation of the entity structure returned by the public GraphQL -// endpoint. `values` and `relations` are optional because the nested `to` selections -// get slimmer the deeper we traverse in the query. This type intentionally mirrors -// only the fields we actually consume inside `convertRelations`. -type RecursiveQueryEntity = { - id: string; - name: string; - valuesList?: { - propertyId: string; - string: string; - boolean: boolean; - number: number; - time: string; - point: string; - }[]; - relationsList?: { - toEntity: RecursiveQueryEntity; - typeId: string; - }[]; -}; - -const convertRelations = ( - queryEntity: RecursiveQueryEntity, - type: S, - mappingEntry: Mapping.MappingEntry, - mapping: Mapping.Mapping, -) => { - const rawEntity: Record = {}; - - for (const [key, relationId] of Object.entries(mappingEntry?.relations ?? {})) { - const properties = (queryEntity.relationsList ?? []).filter((a) => a.typeId === relationId); - if (properties.length === 0) { - rawEntity[key] = [] as unknown[]; - continue; - } - - const field = type.fields[key]; - if (!field) { - // @ts-expect-error TODO: properly access the type.name - console.error(`Field ${key} not found in ${type.name}`); - continue; - } - // @ts-expect-error TODO: properly access the type.name - const annotations = field.ast.rest[0].type.to.annotations; - - // TODO: fix this access using proper effect types - const relationTypeName = - annotations[ - Object.getOwnPropertySymbols(annotations).find((sym) => sym.description === 'effect/annotation/Identifier') - ]; - - const relationMappingEntry = mapping[relationTypeName]; - if (!relationMappingEntry) { - console.error(`Relation mapping entry for ${relationTypeName} not found`); - continue; - } - - const newRelationEntities = properties.map((propertyEntry) => { - // @ts-expect-error TODO: properly access the type.name - const type = field.value; - - let rawEntity: Record = { - id: propertyEntry.toEntity.id, - name: propertyEntry.toEntity.name, - // TODO: should be determined by the actual value - __deleted: false, - // TODO: should be determined by the actual value - __version: '', - }; - - // take the mappingEntry and assign the attributes to the rawEntity - for (const [key, value] of Object.entries(relationMappingEntry?.properties ?? {})) { - const property = propertyEntry.toEntity.valuesList?.find((a) => a.propertyId === value); - if (property) { - rawEntity[key] = convertPropertyValue(property, key, type); - } - } - - rawEntity = { - ...rawEntity, - ...convertRelations(propertyEntry.toEntity, type, relationMappingEntry, mapping), - }; - - return rawEntity; - }); - - if (rawEntity[key]) { - rawEntity[key] = [...rawEntity[key], ...newRelationEntities]; - } else { - rawEntity[key] = newRelationEntities; - } - } - - return rawEntity; -}; - export const parseResult = ( queryData: EntityQueryResult, type: S, From a9548d690967cfa2ea41f64f72bd037b95eac1e0 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 16:44:28 +0200 Subject: [PATCH 12/13] add vitest config to connect and events --- apps/connect/vitest.config.ts | 13 +++++++++++++ apps/events/vitest.config.ts | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 apps/connect/vitest.config.ts create mode 100644 apps/events/vitest.config.ts diff --git a/apps/connect/vitest.config.ts b/apps/connect/vitest.config.ts new file mode 100644 index 00000000..60f4dc1f --- /dev/null +++ b/apps/connect/vitest.config.ts @@ -0,0 +1,13 @@ +import react from '@vitejs/plugin-react'; +import { mergeConfig } from 'vitest/config'; + +import shared from '../../vitest.shared.js'; + +const config = { + plugins: [react()], + test: { + environment: 'jsdom', + }, +}; + +export default mergeConfig(shared, config); diff --git a/apps/events/vitest.config.ts b/apps/events/vitest.config.ts new file mode 100644 index 00000000..60f4dc1f --- /dev/null +++ b/apps/events/vitest.config.ts @@ -0,0 +1,13 @@ +import react from '@vitejs/plugin-react'; +import { mergeConfig } from 'vitest/config'; + +import shared from '../../vitest.shared.js'; + +const config = { + plugins: [react()], + test: { + environment: 'jsdom', + }, +}; + +export default mergeConfig(shared, config); From 920b880622dc0ec10551965b908646f3ef98aa4e Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sat, 13 Sep 2025 16:48:06 +0200 Subject: [PATCH 13/13] move hooks into hooks directory --- packages/hypergraph-react/src/{ => hooks}/use-entity.tsx | 6 ++---- packages/hypergraph-react/src/{ => hooks}/use-query.tsx | 4 ++-- packages/hypergraph-react/src/index.ts | 4 ++-- .../hypergraph-react/test/HypergraphSpaceContext.test.tsx | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) rename packages/hypergraph-react/src/{ => hooks}/use-entity.tsx (81%) rename packages/hypergraph-react/src/{ => hooks}/use-query.tsx (90%) diff --git a/packages/hypergraph-react/src/use-entity.tsx b/packages/hypergraph-react/src/hooks/use-entity.tsx similarity index 81% rename from packages/hypergraph-react/src/use-entity.tsx rename to packages/hypergraph-react/src/hooks/use-entity.tsx index afb2b215..25d5d5eb 100644 --- a/packages/hypergraph-react/src/use-entity.tsx +++ b/packages/hypergraph-react/src/hooks/use-entity.tsx @@ -1,9 +1,7 @@ -'use client'; - import type { Entity, Id } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; -import { useEntityPrivate } from './internal/use-entity-private.js'; -import { useEntityPublic } from './internal/use-entity-public.js'; +import { useEntityPrivate } from '../internal/use-entity-private.js'; +import { useEntityPublic } from '../internal/use-entity-public.js'; export function useEntity( type: S, diff --git a/packages/hypergraph-react/src/use-query.tsx b/packages/hypergraph-react/src/hooks/use-query.tsx similarity index 90% rename from packages/hypergraph-react/src/use-query.tsx rename to packages/hypergraph-react/src/hooks/use-query.tsx index e6ecbc44..0a866c94 100644 --- a/packages/hypergraph-react/src/use-query.tsx +++ b/packages/hypergraph-react/src/hooks/use-query.tsx @@ -1,7 +1,7 @@ import type { Entity } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; -import { useQueryPrivate } from './internal/use-query-private.js'; -import { useQueryPublic } from './internal/use-query-public.js'; +import { useQueryPrivate } from '../internal/use-query-private.js'; +import { useQueryPublic } from '../internal/use-query-public.js'; type QueryParams = { mode: 'public' | 'private'; diff --git a/packages/hypergraph-react/src/index.ts b/packages/hypergraph-react/src/index.ts index 03f1dd68..2987c20a 100644 --- a/packages/hypergraph-react/src/index.ts +++ b/packages/hypergraph-react/src/index.ts @@ -8,7 +8,9 @@ export { export { HypergraphSpaceProvider } from './HypergraphSpaceContext.js'; export { useCreateEntity } from './hooks/use-create-entity.js'; export { useDeleteEntity } from './hooks/use-delete-entity.js'; +export { useEntity } from './hooks/use-entity.js'; export { useHardDeleteEntity } from './hooks/use-hard-delete-entity.js'; +export { useQuery } from './hooks/use-query.js'; export { useRemoveRelation } from './hooks/use-remove-relation.js'; export { useSpace } from './hooks/use-space.js'; export { useSpaces } from './hooks/use-spaces.js'; @@ -28,5 +30,3 @@ export { useQueryPublic as _useQueryPublic } from './internal/use-query-public.j export { preparePublish } from './prepare-publish.js'; export { publishOps } from './publish-ops.js'; export type * from './types.js'; -export { useEntity } from './use-entity.js'; -export { useQuery } from './use-query.js'; diff --git a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx index 31fab5ca..35cf1891 100644 --- a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx +++ b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx @@ -9,9 +9,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { HypergraphSpaceProvider } from '../src/HypergraphSpaceContext.js'; import { useCreateEntity } from '../src/hooks/use-create-entity.js'; import { useDeleteEntity } from '../src/hooks/use-delete-entity.js'; +import { useEntity } from '../src/hooks/use-entity.js'; import { useUpdateEntity } from '../src/hooks/use-update-entity.js'; import { useQueryPrivate } from '../src/internal/use-query-private.js'; -import { useEntity } from '../src/use-entity.js'; afterEach(() => { cleanup();