|
1 | 1 | 'use client'; |
2 | 2 |
|
3 | | -import { Entity, type Id, store } from '@graphprotocol/hypergraph'; |
4 | | -import { useSelector } from '@xstate/store/react'; |
5 | | -import * as Schema from 'effect/Schema'; |
6 | | -import { |
7 | | - createContext, |
8 | | - type ReactNode, |
9 | | - useContext, |
10 | | - useEffect, |
11 | | - useLayoutEffect, |
12 | | - useMemo, |
13 | | - useRef, |
14 | | - useSyncExternalStore, |
15 | | -} from 'react'; |
16 | | -import { useHypergraphApp } from './HypergraphAppContext.js'; |
17 | | -import { useEntityPublic } from './internal/use-entity-public.js'; |
18 | | -import { usePublicSpace } from './internal/use-public-space.js'; |
| 3 | +import { createContext, type ReactNode } from 'react'; |
19 | 4 |
|
20 | 5 | // TODO space can be undefined |
21 | 6 | export type HypergraphContext = { space: string }; |
22 | 7 |
|
23 | 8 | export const HypergraphReactContext = createContext<HypergraphContext | undefined>(undefined); |
24 | 9 |
|
25 | | -export function useHypergraphSpaceInternal() { |
26 | | - const context = useContext(HypergraphReactContext); |
27 | | - return (context as HypergraphContext) || { space: '' }; |
28 | | -} |
29 | | - |
30 | 10 | export function HypergraphSpaceProvider({ space, children }: { space: string; children: ReactNode }) { |
31 | 11 | return <HypergraphReactContext.Provider value={{ space }}>{children}</HypergraphReactContext.Provider>; |
32 | 12 | } |
33 | | - |
34 | | -const subscribeToSpaceCache = new Map<string, boolean>(); |
35 | | - |
36 | | -function useSubscribeToSpaceAndGetHandle({ spaceId, enabled }: { spaceId: string; enabled: boolean }) { |
37 | | - const handle = useSelector(store, (state) => { |
38 | | - const space = state.context.spaces.find((space) => space.id === spaceId); |
39 | | - if (!space) { |
40 | | - return undefined; |
41 | | - } |
42 | | - return space.automergeDocHandle; |
43 | | - }); |
44 | | - |
45 | | - const { subscribeToSpace, isConnecting } = useHypergraphApp(); |
46 | | - useEffect(() => { |
47 | | - if (!isConnecting && enabled) { |
48 | | - if (subscribeToSpaceCache.has(spaceId)) { |
49 | | - return; |
50 | | - } |
51 | | - subscribeToSpaceCache.set(spaceId, true); |
52 | | - subscribeToSpace({ spaceId }); |
53 | | - } |
54 | | - return () => { |
55 | | - // TODO: unsubscribe from space in case the space ID changes |
56 | | - subscribeToSpaceCache.delete(spaceId); |
57 | | - }; |
58 | | - }, [isConnecting, subscribeToSpace, spaceId, enabled]); |
59 | | - |
60 | | - return handle; |
61 | | -} |
62 | | - |
63 | | -export function useSpace(options: { space?: string; mode: 'private' | 'public' }) { |
64 | | - const { space: spaceIdFromContext } = useHypergraphSpaceInternal(); |
65 | | - const { space: spaceIdFromParams } = options ?? {}; |
66 | | - const spaceId = spaceIdFromParams ?? spaceIdFromContext; |
67 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId, enabled: options.mode === 'private' }); |
68 | | - const ready = options.mode === 'public' ? true : handle ? handle.isReady() : false; |
69 | | - const privateSpace = useSelector(store, (state) => state.context.spaces.find((space) => space.id === spaceId)); |
70 | | - const publicSpace = usePublicSpace({ spaceId, enabled: options.mode === 'public' }); |
71 | | - return { ready, name: options.mode === 'private' ? privateSpace?.name : publicSpace?.name, id: spaceId }; |
72 | | -} |
73 | | - |
74 | | -export function useCreateEntity<const S extends Entity.AnyNoContext>(type: S, options?: { space?: string }) { |
75 | | - const { space: spaceIdFromParams } = options ?? {}; |
76 | | - const { space: spaceFromContext } = useHypergraphSpaceInternal(); |
77 | | - const spaceId = spaceIdFromParams ?? spaceFromContext; |
78 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId, enabled: true }); |
79 | | - if (!handle) { |
80 | | - return () => { |
81 | | - throw new Error('Space not found or not ready'); |
82 | | - }; |
83 | | - } |
84 | | - return Entity.create(handle, type); |
85 | | -} |
86 | | - |
87 | | -export function useUpdateEntity<const S extends Entity.AnyNoContext>(type: S, options?: { space?: string }) { |
88 | | - const { space: spaceFromContext } = useHypergraphSpaceInternal(); |
89 | | - const { space } = options ?? {}; |
90 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); |
91 | | - if (!handle) { |
92 | | - return () => { |
93 | | - throw new Error('Space not found or not ready'); |
94 | | - }; |
95 | | - } |
96 | | - return Entity.update(handle, type); |
97 | | -} |
98 | | - |
99 | | -export function useDeleteEntity(options?: { space?: string }) { |
100 | | - const { space: spaceFromContext } = useHypergraphSpaceInternal(); |
101 | | - const { space } = options ?? {}; |
102 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); |
103 | | - if (!handle) { |
104 | | - return () => { |
105 | | - throw new Error('Space not found or not ready'); |
106 | | - }; |
107 | | - } |
108 | | - return Entity.markAsDeleted(handle); |
109 | | -} |
110 | | - |
111 | | -export function useRemoveRelation(options?: { space?: string }) { |
112 | | - const { space: spaceFromContext } = useHypergraphSpaceInternal(); |
113 | | - const { space } = options ?? {}; |
114 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); |
115 | | - if (!handle) { |
116 | | - return () => { |
117 | | - throw new Error('Space not found or not ready'); |
118 | | - }; |
119 | | - } |
120 | | - return Entity.removeRelation(handle); |
121 | | -} |
122 | | - |
123 | | -export function useHardDeleteEntity(options?: { space?: string }) { |
124 | | - const { space: spaceFromContext } = useHypergraphSpaceInternal(); |
125 | | - const { space } = options ?? {}; |
126 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: space ?? spaceFromContext, enabled: true }); |
127 | | - if (!handle) { |
128 | | - return () => { |
129 | | - throw new Error('Space not found or not ready'); |
130 | | - }; |
131 | | - } |
132 | | - return Entity.delete(handle); |
133 | | -} |
134 | | - |
135 | | -type QueryParams<S extends Entity.AnyNoContext> = { |
136 | | - space?: string | undefined; |
137 | | - enabled: boolean; |
138 | | - filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined; |
139 | | - include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined; |
140 | | -}; |
141 | | - |
142 | | -export function useQueryLocal<const S extends Entity.AnyNoContext>(type: S, params?: QueryParams<S>) { |
143 | | - const { enabled = true, filter, include, space: spaceFromParams } = params ?? {}; |
144 | | - const entitiesRef = useRef<Entity.Entity<S>[]>([]); |
145 | | - const subscriptionRef = useRef<Entity.FindManySubscription<S>>({ |
146 | | - subscribe: () => () => undefined, |
147 | | - getEntities: () => entitiesRef.current, |
148 | | - }); |
149 | | - const { space: spaceFromContext } = useHypergraphSpaceInternal(); |
150 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled }); |
151 | | - const handleIsReady = handle ? handle.isReady() : false; |
152 | | - |
153 | | - // biome-ignore lint/correctness/useExhaustiveDependencies: allow to change filter and include |
154 | | - useLayoutEffect(() => { |
155 | | - if (enabled && handle && handleIsReady) { |
156 | | - const subscription = Entity.subscribeToFindMany(handle, type, filter, include); |
157 | | - subscriptionRef.current.subscribe = subscription.subscribe; |
158 | | - subscriptionRef.current.getEntities = subscription.getEntities; |
159 | | - } |
160 | | - }, [enabled, handleIsReady, handle, type]); |
161 | | - |
162 | | - // TODO: allow to change the enabled state |
163 | | - const allEntities = useSyncExternalStore( |
164 | | - subscriptionRef.current.subscribe, |
165 | | - subscriptionRef.current.getEntities, |
166 | | - () => entitiesRef.current, |
167 | | - ); |
168 | | - |
169 | | - const { entities, deletedEntities } = useMemo(() => { |
170 | | - const entities: Entity.Entity<S>[] = []; |
171 | | - const deletedEntities: Entity.Entity<S>[] = []; |
172 | | - for (const entity of allEntities) { |
173 | | - if (entity.__deleted === true) { |
174 | | - deletedEntities.push(entity); |
175 | | - } else { |
176 | | - entities.push(entity); |
177 | | - } |
178 | | - } |
179 | | - return { entities, deletedEntities }; |
180 | | - }, [allEntities]); |
181 | | - |
182 | | - return { entities, deletedEntities }; |
183 | | -} |
184 | | - |
185 | | -function useEntityPrivate<const S extends Entity.AnyNoContext>( |
186 | | - type: S, |
187 | | - params: { |
188 | | - id: string | Id; |
189 | | - enabled?: boolean; |
190 | | - space?: string; |
191 | | - include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined; |
192 | | - }, |
193 | | -) { |
194 | | - const { space: spaceFromContext } = useHypergraphSpaceInternal(); |
195 | | - const { space: spaceFromParams, include, id, enabled = true } = params; |
196 | | - const handle = useSubscribeToSpaceAndGetHandle({ spaceId: spaceFromParams ?? spaceFromContext, enabled }); |
197 | | - const prevEntityRef = useRef<{ |
198 | | - data: Entity.Entity<S> | undefined; |
199 | | - invalidEntity: Record<string, string | boolean | number | Date> | undefined; |
200 | | - isPending: boolean; |
201 | | - isError: boolean; |
202 | | - }>({ data: undefined, invalidEntity: undefined, isPending: false, isError: false }); |
203 | | - const equals = Schema.equivalence(type); |
204 | | - |
205 | | - const subscribe = (callback: () => void) => { |
206 | | - if (!handle || !enabled) { |
207 | | - return () => {}; |
208 | | - } |
209 | | - const handleChange = () => { |
210 | | - callback(); |
211 | | - }; |
212 | | - |
213 | | - const handleDelete = () => { |
214 | | - callback(); |
215 | | - }; |
216 | | - |
217 | | - handle.on('change', handleChange); |
218 | | - handle.on('delete', handleDelete); |
219 | | - |
220 | | - return () => { |
221 | | - handle.off('change', handleChange); |
222 | | - handle.off('delete', handleDelete); |
223 | | - }; |
224 | | - }; |
225 | | - |
226 | | - return useSyncExternalStore(subscribe, () => { |
227 | | - if (!handle || !enabled) { |
228 | | - return prevEntityRef.current; |
229 | | - } |
230 | | - const doc = handle.doc(); |
231 | | - if (doc === undefined) { |
232 | | - return prevEntityRef.current; |
233 | | - } |
234 | | - |
235 | | - const found = Entity.findOne(handle, type, include)(id); |
236 | | - if (found === undefined && prevEntityRef.current.data !== undefined) { |
237 | | - // entity was maybe deleted, delete from the ref |
238 | | - prevEntityRef.current = { data: undefined, invalidEntity: undefined, isPending: false, isError: false }; |
239 | | - } else if (found !== undefined && prevEntityRef.current.data === undefined) { |
240 | | - prevEntityRef.current = { data: found, invalidEntity: undefined, isPending: false, isError: false }; |
241 | | - } else if ( |
242 | | - found !== undefined && |
243 | | - prevEntityRef.current.data !== undefined && |
244 | | - !equals(found, prevEntityRef.current.data) |
245 | | - ) { |
246 | | - // found and ref have a value, compare for equality, if they are not equal, update the ref and return |
247 | | - prevEntityRef.current = { data: found, invalidEntity: undefined, isPending: false, isError: false }; |
248 | | - } |
249 | | - |
250 | | - return prevEntityRef.current; |
251 | | - }); |
252 | | -} |
253 | | - |
254 | | -export function useEntity<const S extends Entity.AnyNoContext>( |
255 | | - type: S, |
256 | | - params: { |
257 | | - id: string | Id; |
258 | | - space?: string; |
259 | | - mode: 'private' | 'public'; |
260 | | - include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined; |
261 | | - }, |
262 | | -) { |
263 | | - const resultPublic = useEntityPublic(type, { ...params, enabled: params.mode === 'public' }); |
264 | | - const resultPrivate = useEntityPrivate(type, { ...params, enabled: params.mode === 'private' }); |
265 | | - |
266 | | - if (params.mode === 'public') { |
267 | | - return resultPublic; |
268 | | - } |
269 | | - |
270 | | - return resultPrivate; |
271 | | -} |
0 commit comments