diff --git a/apps/events/src/components/users.tsx b/apps/events/src/components/users.tsx index f1441952..056109f6 100644 --- a/apps/events/src/components/users.tsx +++ b/apps/events/src/components/users.tsx @@ -8,7 +8,7 @@ import { UserEntry } from './user-entry.js'; export const Users = () => { const { data: users } = useQuery(User, { mode: 'private' }); const { ready: spaceReady } = useSpace({ mode: 'private' }); - const createEntity = useCreateEntity(User, { space: '1c954768-7e14-4f0f-9396-0fe9dcd55fe8' }); + const createEntity = useCreateEntity(User); const [newUserName, setNewUserName] = useState(''); if (!spaceReady) { diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 6465ace8..7fb4651b 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -35,8 +35,7 @@ import { useRef, useState, } from 'react'; -import type { Address } from 'viem'; -import type { Hex } from 'viem'; +import type { Address, Hex } from 'viem'; const decodeResponseMessage = Schema.decodeUnknownEither(Messages.ResponseMessage); @@ -362,10 +361,10 @@ export function HypergraphAppProvider({ }); const authorIdentity = await Identity.getVerifiedIdentity(update.accountAddress, syncServerUri); if (authorIdentity.signaturePublicKey !== signer) { - console.error( - `Received invalid signature, recovered signer is ${signer}, - expected ${authorIdentity.signaturePublicKey}`, - ); + // console.error( + // `Received invalid signature, recovered signer is ${signer}, + // expected ${authorIdentity.signaturePublicKey}`, + // ); // TODO bring back signature verfication // return { valid: false, update: new Uint8Array([]) }; } diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index 6d74b0eb..fba34c50 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -1,8 +1,6 @@ 'use client'; -import type { AnyDocumentId } from '@automerge/automerge-repo'; -import { useRepo } from '@automerge/automerge-repo-react-hooks'; -import { Entity, Utils, store } from '@graphprotocol/hypergraph'; +import { Entity, store } from '@graphprotocol/hypergraph'; import { useSelector } from '@xstate/store/react'; import * as Schema from 'effect/Schema'; import { @@ -35,12 +33,13 @@ export function HypergraphSpaceProvider({ space, children }: { space: string; ch } function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { - const repo = useRepo(); - const handle = useMemo(() => { - const id = Utils.idToAutomergeId(spaceId) as AnyDocumentId; - const result = repo.findWithProgress(id); - return result.handle; - }, [spaceId, repo]); + 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(() => { @@ -61,15 +60,21 @@ export function useSpace(options: { space?: string; mode: 'private' | 'public' } const { space: spaceIdFromParams } = options ?? {}; const spaceId = spaceIdFromParams ?? spaceIdFromContext; const handle = useSubscribeToSpaceAndGetHandle({ spaceId, enabled: options.mode === 'private' }); - const ready = options.mode === 'public' ? true : handle.isReady(); + const ready = options.mode === 'public' ? true : handle ? handle.isReady() : false; const space = useSelector(store, (state) => state.context.spaces.find((space) => space.id === spaceId)); return { ready, name: space?.name, id: spaceId }; } export function useCreateEntity(type: S, options?: { space?: string }) { - const { space } = options ?? {}; + const { space: spaceIdFromParams } = options ?? {}; const { space: spaceFromContext } = useHypergraphSpaceInternal(); - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); + 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); } @@ -77,6 +82,11 @@ export function useUpdateEntity(type: S, op 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); } @@ -84,6 +94,11 @@ 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); } @@ -91,6 +106,11 @@ 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); } @@ -98,6 +118,11 @@ 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); } @@ -117,11 +142,11 @@ export function useQueryLocal(type: S, para }); const { space: spaceFromContext } = useHypergraphSpaceInternal(); const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled: true }); - const handleIsReady = handle.isReady(); + const handleIsReady = handle ? handle.isReady() : false; // biome-ignore lint/correctness/useExhaustiveDependencies: allow to change filter and include useLayoutEffect(() => { - if (enabled && handleIsReady) { + if (enabled && handle && handleIsReady) { const subscription = Entity.subscribeToFindMany(handle, type, filter, include); subscriptionRef.current.subscribe = subscription.subscribe; subscriptionRef.current.getEntities = subscription.getEntities; @@ -163,6 +188,9 @@ export function useQueryEntity( const equals = Schema.equivalence(type); const subscribe = (callback: () => void) => { + if (!handle) { + return () => {}; + } const handleChange = () => { callback(); }; @@ -181,6 +209,9 @@ export function useQueryEntity( }; return useSyncExternalStore(subscribe, () => { + if (!handle) { + return prevEntityRef.current; + } const doc = handle.doc(); if (doc === undefined) { return prevEntityRef.current; diff --git a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx index 5e339c70..3e243527 100644 --- a/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx +++ b/packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx @@ -1,6 +1,6 @@ -import { type AnyDocumentId, Repo } from '@automerge/automerge-repo'; +import { Repo } from '@automerge/automerge-repo'; import { RepoContext } from '@automerge/automerge-repo-react-hooks'; -import { Entity, Type, Utils } from '@graphprotocol/hypergraph'; +import { Entity, Type, store } from '@graphprotocol/hypergraph'; import '@testing-library/jest-dom/vitest'; import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; // biome-ignore lint/style/useImportType: @@ -45,10 +45,24 @@ describe('HypergraphSpaceContext', () => { beforeEach(() => { repo = new Repo({}); - const result = repo.findWithProgress(Utils.idToAutomergeId(spaceId) as AnyDocumentId); - const automergeDocHandle = result.handle; - // set it to ready to interact with the document - automergeDocHandle.doneLoading(); + store.send({ type: 'setRepo', repo }); + store.send({ + type: 'setSpace', + spaceId, + spaceState: { + id: spaceId, + members: {}, + invitations: {}, + removedMembers: {}, + inboxes: {}, + lastEventHash: '', + }, + name: 'Test Space', + updates: { updates: [], firstUpdateClock: 0, lastUpdateClock: 0 }, + events: [], + inboxes: [], + keys: [], + }); wrapper = ({ children }: Readonly<{ children: React.ReactNode }>) => ( @@ -59,8 +73,8 @@ describe('HypergraphSpaceContext', () => { describe('useCreateEntity', () => { it('should be able to create an entity through the useCreateEntity Hook', async () => { - const { result: createEntityResult } = renderHook(() => useCreateEntity(Event), { wrapper }); const { result: queryEntitiesResult, rerender } = renderHook(() => useQueryLocal(Event), { wrapper }); + const { result: createEntityResult } = renderHook(() => useCreateEntity(Event), { wrapper }); let createdEntity: Entity.Entity | null = null;