Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/events/src/components/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 5 additions & 6 deletions packages/hypergraph-react/src/HypergraphAppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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([]) };
Comment on lines +364 to 369
Copy link

Copilot AI Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out logging can clutter the codebase. Either remove the dead code or re-enable the check with a proper logging strategy and a clear TODO note.

Suggested change
// console.error(
// `Received invalid signature, recovered signer is ${signer},
// expected ${authorIdentity.signaturePublicKey}`,
// );
// TODO bring back signature verfication
// return { valid: false, update: new Uint8Array([]) };
console.error(
`Received invalid signature, recovered signer is ${signer}, expected ${authorIdentity.signaturePublicKey}`
);
// TODO: Implement signature verification logic here to handle invalid signatures.
return { valid: false, update: new Uint8Array([]) };

Copilot uses AI. Check for mistakes.

}
Expand Down
59 changes: 45 additions & 14 deletions packages/hypergraph-react/src/HypergraphSpaceContext.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<Entity.DocumentContent>(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(() => {
Expand All @@ -61,43 +60,69 @@ 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<const S extends Entity.AnyNoContext>(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);
}

export function useUpdateEntity<const S extends Entity.AnyNoContext>(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);
}

Expand All @@ -117,11 +142,11 @@ export function useQueryLocal<const S extends Entity.AnyNoContext>(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;
Expand Down Expand Up @@ -163,6 +188,9 @@ export function useQueryEntity<const S extends Entity.AnyNoContext>(
const equals = Schema.equivalence(type);

const subscribe = (callback: () => void) => {
if (!handle) {
return () => {};
}
const handleChange = () => {
callback();
};
Expand All @@ -181,6 +209,9 @@ export function useQueryEntity<const S extends Entity.AnyNoContext>(
};

return useSyncExternalStore(subscribe, () => {
if (!handle) {
return prevEntityRef.current;
}
const doc = handle.doc();
if (doc === undefined) {
return prevEntityRef.current;
Expand Down
28 changes: 21 additions & 7 deletions packages/hypergraph-react/test/HypergraphSpaceContext.test.tsx
Original file line number Diff line number Diff line change
@@ -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: <explanation>
Expand Down Expand Up @@ -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 }>) => (
<RepoContext.Provider value={repo}>
Expand All @@ -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<typeof Event> | null = null;

Expand Down
Loading