Skip to content

Commit 8ddea57

Browse files
committed
fix change subscription
1 parent 1609442 commit 8ddea57

File tree

4 files changed

+63
-36
lines changed

4 files changed

+63
-36
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useQueryEntities } from '@graphprotocol/hypergraph-react';
2+
import { Todo } from '../schema';
3+
4+
export const TodosReadOnly = () => {
5+
const todos = useQueryEntities(Todo);
6+
7+
return (
8+
<>
9+
<h1 className="text-2xl font-bold">Todos (read only)</h1>
10+
{todos.map((todo) => (
11+
<div key={todo.id} className="flex flex-row items-center gap-2">
12+
<h2>{todo.name}</h2>
13+
<input type="checkbox" checked={todo.completed} readOnly />
14+
</div>
15+
))}
16+
</>
17+
);
18+
};

apps/events/src/routes/space/$spaceId.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSelector } from '@xstate/store/react';
55

66
import { DevTool } from '@/components/dev-tool';
77
import { Todos } from '@/components/todos';
8+
import { TodosReadOnly } from '@/components/todos-read-only';
89
import { Button } from '@/components/ui/button';
910
import { availableAccounts } from '@/lib/availableAccounts';
1011
import { useEffect, useState } from 'react';
@@ -39,6 +40,7 @@ function Space() {
3940
<div className="flex flex-col gap-4 max-w-screen-sm mx-auto py-8">
4041
<HypergraphSpaceProvider space={spaceId}>
4142
<Todos />
43+
<TodosReadOnly />
4244
{show2ndTodos && <Todos />}
4345
<h3 className="text-xl font-bold">Invite people</h3>
4446
<div className="flex flex-row gap-2">

packages/hypergraph-react/src/HypergraphSpaceContext.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useRepo } from '@automerge/automerge-repo-react-hooks';
55
import { Entity, Utils } from '@graphprotocol/hypergraph';
66
import type { DocumentContent } from '@graphprotocol/hypergraph/Entity';
77
import * as Schema from 'effect/Schema';
8-
import { type ReactNode, createContext, useContext, useEffect, useRef, useState, useSyncExternalStore } from 'react';
8+
import { type ReactNode, createContext, useContext, useRef, useState, useSyncExternalStore } from 'react';
99

1010
export type HypergraphContext = {
1111
space: string;
@@ -66,13 +66,7 @@ export function useQueryEntities<const S extends Entity.AnyNoContext>(type: S) {
6666
return Entity.subscribeToFindMany(hypergraph.handle, type);
6767
});
6868

69-
useEffect(() => {
70-
return () => {
71-
subscription.unsubscribe();
72-
};
73-
}, [subscription]);
74-
75-
return useSyncExternalStore(subscription.listener, subscription.getEntities, () => []);
69+
return useSyncExternalStore(subscription.subscribe, subscription.getEntities, () => []);
7670
}
7771

7872
export function useQueryEntity<const S extends Entity.AnyNoContext>(type: S, id: string) {

packages/hypergraph/src/Entity.ts

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ export const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) =
134134
}
135135

136136
const entityTypes = new Set<string>();
137+
// collect all query entries that changed and only at the end make one copy to change the
138+
// reference to reduce the amount of O(n) operations per query to 1
139+
const touchedQueries = new Set<Array<string>>();
137140

138141
// loop over all changed entities and update the cache
139142
for (const entityId of changedEntities) {
@@ -155,6 +158,7 @@ export const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) =
155158
} else {
156159
query.data.push(decoded);
157160
}
161+
touchedQueries.add([typeName, 'all']);
158162
}
159163

160164
entityTypes.add(typeName);
@@ -173,12 +177,23 @@ export const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) =
173177
const index = query.data.findIndex((entity) => entity.id === entityId);
174178
if (index !== -1) {
175179
query.data.splice(index, 1);
180+
touchedQueries.add([affectedTypeName, 'all']);
176181
}
177182
}
178183
}
179184
}
180185
}
181186

187+
for (const [typeName, queryKey] of touchedQueries) {
188+
const cacheEntry = decodedEntitiesCache.get(typeName);
189+
if (!cacheEntry) continue;
190+
191+
const query = cacheEntry.queries.get(queryKey);
192+
if (!query) continue;
193+
194+
query.data = [...query.data]; // must be a new reference for React.useSyncExternalStore
195+
}
196+
182197
// invoke all the listeners per type
183198
for (const typeName of entityTypes) {
184199
const cacheEntry = decodedEntitiesCache.get(typeName);
@@ -195,7 +210,6 @@ export const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) =
195210
handle.on('change', onChange);
196211

197212
return () => {
198-
console.log('unsubscribe document changes');
199213
handle.off('change', onChange);
200214
decodedEntitiesCache.clear(); // currently we only support exactly one space
201215
};
@@ -317,34 +331,18 @@ export function findMany<const S extends AnyNoContext>(
317331
export function subscribeToFindMany<const S extends AnyNoContext>(
318332
handle: DocHandle<DocumentContent>,
319333
type: S,
320-
): { listener: () => () => void; getEntities: () => Readonly<Array<Entity<S>>>; unsubscribe: () => void } {
334+
): {
335+
subscribe: (callback: () => void) => () => void;
336+
getEntities: () => Readonly<Array<Entity<S>>>;
337+
} {
321338
const queryKey = 'all';
322339
const decode = Schema.decodeUnknownSync(type);
323340
// TODO: what's the right way to get the name of the type?
324341
// @ts-expect-error name is defined
325342
const typeName = type.name;
326343

327344
const getEntities = () => {
328-
const entities = decodedEntitiesCache.get(typeName)?.queries.get(queryKey)?.data ?? [];
329-
return entities;
330-
};
331-
332-
const listener = () => {
333-
return () => undefined;
334-
};
335-
336-
const unsubscribe = () => {
337-
const query = decodedEntitiesCache.get(typeName)?.queries.get(queryKey);
338-
if (query?.listeners) {
339-
query.listeners = query.listeners.filter((cachedListener) => cachedListener !== listener);
340-
console.log('unsubscribe query', query.listeners);
341-
}
342-
343-
documentChangeListener.subscribedQueriesCount--;
344-
if (documentChangeListener.subscribedQueriesCount === 0) {
345-
documentChangeListener.unsubscribe?.();
346-
documentChangeListener.unsubscribe = undefined;
347-
}
345+
return decodedEntitiesCache.get(typeName)?.queries.get(queryKey)?.data ?? [];
348346
};
349347

350348
const entities = findMany(handle, type);
@@ -366,10 +364,6 @@ export function subscribeToFindMany<const S extends AnyNoContext>(
366364
query.data.push(entity);
367365
}
368366
}
369-
370-
if (query?.listeners) {
371-
query.listeners.push(listener);
372-
}
373367
} else {
374368
const entitiesMap = new Map();
375369
for (const entity of entities) {
@@ -380,7 +374,7 @@ export function subscribeToFindMany<const S extends AnyNoContext>(
380374

381375
queries.set(queryKey, {
382376
data: entities,
383-
listeners: [listener],
377+
listeners: [],
384378
});
385379

386380
decodedEntitiesCache.set(typeName, {
@@ -390,12 +384,31 @@ export function subscribeToFindMany<const S extends AnyNoContext>(
390384
});
391385
}
392386

387+
const subscribe = (callback: () => void) => {
388+
const query = decodedEntitiesCache.get(typeName)?.queries.get(queryKey);
389+
if (query?.listeners) {
390+
query.listeners.push(callback);
391+
}
392+
return () => {
393+
const query = decodedEntitiesCache.get(typeName)?.queries.get(queryKey);
394+
if (query?.listeners) {
395+
query.listeners = query?.listeners?.filter((cachedListener) => cachedListener !== callback);
396+
}
397+
398+
documentChangeListener.subscribedQueriesCount--;
399+
if (documentChangeListener.subscribedQueriesCount === 0) {
400+
documentChangeListener.unsubscribe?.();
401+
documentChangeListener.unsubscribe = undefined;
402+
}
403+
};
404+
};
405+
393406
if (documentChangeListener.subscribedQueriesCount === 0) {
394407
documentChangeListener.unsubscribe = subscribeToDocumentChanges(handle);
395408
}
396409
documentChangeListener.subscribedQueriesCount++;
397410

398-
return { listener, getEntities, unsubscribe };
411+
return { subscribe, getEntities };
399412
}
400413

401414
/**

0 commit comments

Comments
 (0)