@roko-app/instant-react-common 0.0.0-stage → 1.0.67-roko.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.tshy/build.json +8 -0
  2. package/.tshy/commonjs.json +16 -0
  3. package/.tshy/esm.json +15 -0
  4. package/LICENSE +202 -0
  5. package/README.md +59 -2
  6. package/dist/commonjs/InstantReactAbstractDatabase.d.ts +283 -0
  7. package/dist/commonjs/InstantReactAbstractDatabase.d.ts.map +1 -0
  8. package/dist/commonjs/InstantReactAbstractDatabase.js +390 -0
  9. package/dist/commonjs/InstantReactAbstractDatabase.js.map +1 -0
  10. package/dist/commonjs/InstantReactRoom.d.ts +183 -0
  11. package/dist/commonjs/InstantReactRoom.d.ts.map +1 -0
  12. package/dist/commonjs/InstantReactRoom.js +290 -0
  13. package/dist/commonjs/InstantReactRoom.js.map +1 -0
  14. package/dist/commonjs/index.d.ts +5 -0
  15. package/dist/commonjs/index.d.ts.map +1 -0
  16. package/dist/commonjs/index.js +11 -0
  17. package/dist/commonjs/index.js.map +1 -0
  18. package/dist/commonjs/package.json +3 -0
  19. package/dist/commonjs/useInfiniteQuerySubscription.d.ts +28 -0
  20. package/dist/commonjs/useInfiniteQuerySubscription.d.ts.map +1 -0
  21. package/dist/commonjs/useInfiniteQuerySubscription.js +57 -0
  22. package/dist/commonjs/useInfiniteQuerySubscription.js.map +1 -0
  23. package/dist/commonjs/useQuery.d.ts +7 -0
  24. package/dist/commonjs/useQuery.d.ts.map +1 -0
  25. package/dist/commonjs/useQuery.js +79 -0
  26. package/dist/commonjs/useQuery.js.map +1 -0
  27. package/dist/commonjs/useTimeout.d.ts +5 -0
  28. package/dist/commonjs/useTimeout.d.ts.map +1 -0
  29. package/dist/commonjs/useTimeout.js +21 -0
  30. package/dist/commonjs/useTimeout.js.map +1 -0
  31. package/dist/commonjs/version.d.ts +3 -0
  32. package/dist/commonjs/version.d.ts.map +1 -0
  33. package/dist/commonjs/version.js +5 -0
  34. package/dist/commonjs/version.js.map +1 -0
  35. package/dist/esm/InstantReactAbstractDatabase.d.ts +283 -0
  36. package/dist/esm/InstantReactAbstractDatabase.d.ts.map +1 -0
  37. package/dist/esm/InstantReactAbstractDatabase.js +387 -0
  38. package/dist/esm/InstantReactAbstractDatabase.js.map +1 -0
  39. package/dist/esm/InstantReactRoom.d.ts +183 -0
  40. package/dist/esm/InstantReactRoom.d.ts.map +1 -0
  41. package/dist/esm/InstantReactRoom.js +281 -0
  42. package/dist/esm/InstantReactRoom.js.map +1 -0
  43. package/dist/esm/index.d.ts +5 -0
  44. package/dist/esm/index.d.ts.map +1 -0
  45. package/dist/esm/index.js +4 -0
  46. package/dist/esm/index.js.map +1 -0
  47. package/dist/esm/package.json +3 -0
  48. package/dist/esm/useInfiniteQuerySubscription.d.ts +28 -0
  49. package/dist/esm/useInfiniteQuerySubscription.d.ts.map +1 -0
  50. package/dist/esm/useInfiniteQuerySubscription.js +54 -0
  51. package/dist/esm/useInfiniteQuerySubscription.js.map +1 -0
  52. package/dist/esm/useQuery.d.ts +7 -0
  53. package/dist/esm/useQuery.d.ts.map +1 -0
  54. package/dist/esm/useQuery.js +76 -0
  55. package/dist/esm/useQuery.js.map +1 -0
  56. package/dist/esm/useTimeout.d.ts +5 -0
  57. package/dist/esm/useTimeout.d.ts.map +1 -0
  58. package/dist/esm/useTimeout.js +18 -0
  59. package/dist/esm/useTimeout.js.map +1 -0
  60. package/dist/esm/version.d.ts +3 -0
  61. package/dist/esm/version.d.ts.map +1 -0
  62. package/dist/esm/version.js +3 -0
  63. package/dist/esm/version.js.map +1 -0
  64. package/dist/standalone/index.js +8718 -0
  65. package/dist/standalone/index.umd.cjs +36 -0
  66. package/package.json +43 -3
  67. package/src/InstantReactAbstractDatabase.tsx +510 -0
  68. package/src/InstantReactRoom.ts +451 -0
  69. package/src/index.ts +9 -0
  70. package/src/useInfiniteQuerySubscription.ts +128 -0
  71. package/src/useQuery.ts +124 -0
  72. package/src/useTimeout.ts +22 -0
  73. package/src/version.ts +3 -0
  74. package/tsconfig.cjs.dev.json +15 -0
  75. package/tsconfig.dev.json +14 -0
  76. package/tsconfig.json +12 -0
  77. package/vite.config.ts +35 -0
@@ -0,0 +1,451 @@
1
+ import {
2
+ // types
3
+ type PresenceOpts,
4
+ type PresenceResponse,
5
+ type RoomSchemaShape,
6
+ InstantCoreDatabase,
7
+ InstantSchemaDef,
8
+ } from '@instantdb/core';
9
+
10
+ import {
11
+ KeyboardEvent,
12
+ useCallback,
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ } from 'react';
18
+
19
+ import { useTimeout } from './useTimeout.ts';
20
+
21
+ export type PresenceHandle<
22
+ PresenceShape,
23
+ Keys extends keyof PresenceShape,
24
+ > = PresenceResponse<PresenceShape, Keys> & {
25
+ publishPresence: (data: Partial<PresenceShape>) => void;
26
+ };
27
+
28
+ export type TypingIndicatorOpts = {
29
+ timeout?: number | null;
30
+ stopOnEnter?: boolean;
31
+ // Perf opt - `active` will always be an empty array
32
+ writeOnly?: boolean;
33
+ };
34
+
35
+ export type TypingIndicatorHandle<PresenceShape> = {
36
+ active: PresenceShape[];
37
+ setActive(active: boolean): void;
38
+ inputProps: {
39
+ onKeyDown: (e: KeyboardEvent) => void;
40
+ onBlur: () => void;
41
+ };
42
+ };
43
+
44
+ export const defaultActivityStopTimeout = 1_000;
45
+
46
+ // ------
47
+ // Topics
48
+
49
+ /**
50
+ * Listen for broadcasted events given a room and topic.
51
+ *
52
+ * @see https://instantdb.com/docs/presence-and-topics
53
+ * @example
54
+ * function App({ roomId }) {
55
+ * const room = db.room('chats', roomId);
56
+ * db.rooms.useTopicEffect(room, 'emoji', (message, peer) => {
57
+ * console.log(peer.name, 'sent', message);
58
+ * });
59
+ * // ...
60
+ * }
61
+ */
62
+ export function useTopicEffect<
63
+ RoomSchema extends RoomSchemaShape,
64
+ RoomType extends string & keyof RoomSchema,
65
+ TopicType extends keyof RoomSchema[RoomType]['topics'],
66
+ >(
67
+ room: InstantReactRoom<any, RoomSchema, RoomType>,
68
+ topic: TopicType,
69
+ onEvent: (
70
+ event: RoomSchema[RoomType]['topics'][TopicType],
71
+ peer: RoomSchema[RoomType]['presence'],
72
+ ) => any,
73
+ ): void {
74
+ const onEventRef = useRef(onEvent);
75
+ onEventRef.current = onEvent;
76
+
77
+ useEffect(() => {
78
+ const unsub = room.core._reactor.subscribeTopic(
79
+ room.type,
80
+ room.id,
81
+ topic,
82
+ (event, peer) => {
83
+ onEventRef.current(event, peer);
84
+ },
85
+ );
86
+
87
+ return unsub;
88
+ }, [room.id, topic]);
89
+ }
90
+
91
+ /**
92
+ * Broadcast an event to a room.
93
+ *
94
+ * @see https://instantdb.com/docs/presence-and-topics
95
+ * @example
96
+ * function App({ roomId }) {
97
+ * const room = db.room('chat', roomId);
98
+ * const publishTopic = db.rooms.usePublishTopic(room, "emoji");
99
+ *
100
+ * return (
101
+ * <button onClick={() => publishTopic({ emoji: "🔥" })}>Send emoji</button>
102
+ * );
103
+ * }
104
+ *
105
+ */
106
+ export function usePublishTopic<
107
+ RoomSchema extends RoomSchemaShape,
108
+ RoomType extends string & keyof RoomSchema,
109
+ TopicType extends keyof RoomSchema[RoomType]['topics'],
110
+ >(
111
+ room: InstantReactRoom<any, RoomSchema, RoomType>,
112
+ topic: TopicType,
113
+ ): (data: RoomSchema[RoomType]['topics'][TopicType]) => void {
114
+ useEffect(() => room.core._reactor.joinRoom(room.type, room.id), [room.id]);
115
+
116
+ const publishTopic = useCallback(
117
+ (data) => {
118
+ room.core._reactor.publishTopic({
119
+ roomType: room.type,
120
+ roomId: room.id,
121
+ topic,
122
+ data,
123
+ });
124
+ },
125
+ [room.id, topic],
126
+ );
127
+
128
+ return publishTopic;
129
+ }
130
+
131
+ // ---------
132
+ // Presence
133
+
134
+ /**
135
+ * Listen for peer's presence data in a room, and publish the current user's presence.
136
+ *
137
+ * @see https://instantdb.com/docs/presence-and-topics
138
+ * @example
139
+ * function App({ roomId }) {
140
+ * const {
141
+ * peers,
142
+ * publishPresence
143
+ * } = db.room(roomType, roomId).usePresence({ keys: ["name", "avatar"] });
144
+ *
145
+ * // ...
146
+ * }
147
+ */
148
+ export function usePresence<
149
+ RoomSchema extends RoomSchemaShape,
150
+ RoomType extends string & keyof RoomSchema,
151
+ Keys extends keyof RoomSchema[RoomType]['presence'],
152
+ >(
153
+ room: InstantReactRoom<any, RoomSchema, RoomType>,
154
+ opts: PresenceOpts<RoomSchema[RoomType]['presence'], Keys> = {},
155
+ ): PresenceHandle<RoomSchema[RoomType]['presence'], Keys> {
156
+ const [state, setState] = useState<
157
+ PresenceResponse<RoomSchema[RoomType]['presence'], Keys>
158
+ >(() => {
159
+ return (
160
+ room.core._reactor.getPresence(room.type, room.id, opts) ?? {
161
+ peers: {},
162
+ isLoading: true,
163
+ }
164
+ );
165
+ });
166
+
167
+ useEffect(() => {
168
+ const unsub = room.core._reactor.subscribePresence(
169
+ room.type,
170
+ room.id,
171
+ opts,
172
+ (data) => {
173
+ setState(data);
174
+ },
175
+ );
176
+
177
+ return unsub;
178
+ }, [room.id, opts.user, opts.peers?.join(), opts.keys?.join()]);
179
+
180
+ const publishPresence = useCallback(
181
+ (data) => {
182
+ room.core._reactor.publishPresence(room.type, room.id, data);
183
+ },
184
+ [room.type, room.id],
185
+ );
186
+ const ret = useMemo(() => {
187
+ return {
188
+ ...state,
189
+ publishPresence,
190
+ };
191
+ }, [state, publishPresence]);
192
+ return ret;
193
+ }
194
+
195
+ /**
196
+ * Publishes presence data to a room
197
+ *
198
+ * @see https://instantdb.com/docs/presence-and-topics
199
+ * @example
200
+ * function App({ roomId, nickname }) {
201
+ * const room = db.room('chat', roomId);
202
+ * db.rooms.useSyncPresence(room, { nickname });
203
+ * }
204
+ */
205
+ export function useSyncPresence<
206
+ RoomSchema extends RoomSchemaShape,
207
+ RoomType extends string & keyof RoomSchema,
208
+ >(
209
+ room: InstantReactRoom<any, RoomSchema, RoomType>,
210
+ data: Partial<RoomSchema[RoomType]['presence']>,
211
+ deps?: any[],
212
+ ): void {
213
+ useEffect(
214
+ () => room.core._reactor.joinRoom(room.type, room.id, data),
215
+ [room.id],
216
+ );
217
+ useEffect(() => {
218
+ return room.core._reactor.publishPresence(room.type, room.id, data);
219
+ }, [room.type, room.id, deps ?? JSON.stringify(data)]);
220
+ }
221
+
222
+ // -----------------
223
+ // Typing Indicator
224
+
225
+ /**
226
+ * Manage typing indicator state
227
+ *
228
+ * @see https://instantdb.com/docs/presence-and-topics
229
+ * @example
230
+ * function App({ roomId }) {
231
+ * const room = db.room('chat', roomId);
232
+ * const {
233
+ * active,
234
+ * setActive,
235
+ * inputProps,
236
+ * } = db.rooms.useTypingIndicator(room, "chat-input");
237
+ *
238
+ * return <input {...inputProps} />;
239
+ * }
240
+ */
241
+ export function useTypingIndicator<
242
+ RoomSchema extends RoomSchemaShape,
243
+ RoomType extends string & keyof RoomSchema,
244
+ >(
245
+ room: InstantReactRoom<any, RoomSchema, RoomType>,
246
+ inputName: string,
247
+ opts: TypingIndicatorOpts = {},
248
+ ): TypingIndicatorHandle<RoomSchema[RoomType]['presence']> {
249
+ const timeout = useTimeout();
250
+
251
+ const observedPresence = rooms.usePresence(room, {
252
+ keys: [inputName] as (keyof RoomSchema[RoomType]['presence'])[],
253
+ });
254
+
255
+ const active = useMemo(() => {
256
+ const presenceSnapshot = room._core._reactor.getPresence(
257
+ room.type,
258
+ room.id,
259
+ );
260
+
261
+ return opts?.writeOnly
262
+ ? []
263
+ : Object.values(presenceSnapshot?.peers ?? {}).filter(
264
+ (p) => p[inputName] === true,
265
+ );
266
+ }, [opts?.writeOnly, observedPresence]);
267
+
268
+ const setActive = useCallback(
269
+ (isActive: boolean) => {
270
+ room.core._reactor.publishPresence(room.type, room.id, {
271
+ [inputName]: isActive,
272
+ } as unknown as Partial<RoomSchema[RoomType]>);
273
+
274
+ if (!isActive) return;
275
+
276
+ if (opts?.timeout === null || opts?.timeout === 0) return;
277
+
278
+ timeout.set(opts?.timeout ?? defaultActivityStopTimeout, () => {
279
+ room.core._reactor.publishPresence(room.type, room.id, {
280
+ [inputName]: null,
281
+ } as Partial<RoomSchema[RoomType]>);
282
+ });
283
+ },
284
+ [room.type, room.id, inputName, opts?.timeout, timeout],
285
+ );
286
+ const onKeyDown = useCallback(
287
+ (e: KeyboardEvent) => {
288
+ const isEnter = opts?.stopOnEnter && e.key === 'Enter';
289
+ const isActive = !isEnter;
290
+
291
+ setActive(isActive);
292
+ },
293
+ [opts.stopOnEnter, setActive],
294
+ );
295
+ const onBlur = useCallback(() => {
296
+ setActive(false);
297
+ }, [setActive]);
298
+
299
+ const inputProps = useMemo(() => {
300
+ return { onKeyDown, onBlur };
301
+ }, [onKeyDown, onBlur]);
302
+
303
+ return {
304
+ active,
305
+ setActive,
306
+ inputProps,
307
+ };
308
+ }
309
+
310
+ // --------------
311
+ // Hooks
312
+
313
+ export const rooms = {
314
+ useTopicEffect,
315
+ usePublishTopic,
316
+ usePresence,
317
+ useSyncPresence,
318
+ useTypingIndicator,
319
+ };
320
+
321
+ // ------------
322
+ // Class
323
+
324
+ export class InstantReactRoom<
325
+ Schema extends InstantSchemaDef<any, any, any>,
326
+ RoomSchema extends RoomSchemaShape,
327
+ RoomType extends string & keyof RoomSchema,
328
+ > {
329
+ core: InstantCoreDatabase<Schema, boolean>;
330
+ /** @deprecated use `core` instead */
331
+ _core: InstantCoreDatabase<Schema, boolean>;
332
+ type: RoomType;
333
+ id: string;
334
+
335
+ constructor(
336
+ core: InstantCoreDatabase<Schema, boolean>,
337
+ type: RoomType,
338
+ id: string,
339
+ ) {
340
+ this.core = core;
341
+ this._core = this.core;
342
+ this.type = type;
343
+ this.id = id;
344
+ }
345
+
346
+ /**
347
+ * @deprecated
348
+ * `db.room(...).useTopicEffect` is deprecated. You can replace it with `db.rooms.useTopicEffect`.
349
+ *
350
+ * @example
351
+ *
352
+ * // Before
353
+ * const room = db.room('chat', 'room-id');
354
+ * room.useTopicEffect('emoji', (message, peer) => { });
355
+ *
356
+ * // After
357
+ * const room = db.room('chat', 'room-id');
358
+ * db.rooms.useTopicEffect(room, 'emoji', (message, peer) => { });
359
+ */
360
+ useTopicEffect = <TopicType extends keyof RoomSchema[RoomType]['topics']>(
361
+ topic: TopicType,
362
+ onEvent: (
363
+ event: RoomSchema[RoomType]['topics'][TopicType],
364
+ peer: RoomSchema[RoomType]['presence'],
365
+ ) => any,
366
+ ): void => {
367
+ rooms.useTopicEffect(this, topic, onEvent);
368
+ };
369
+
370
+ /**
371
+ * @deprecated
372
+ * `db.room(...).usePublishTopic` is deprecated. You can replace it with `db.rooms.usePublishTopic`.
373
+ *
374
+ * @example
375
+ *
376
+ * // Before
377
+ * const room = db.room('chat', 'room-id');
378
+ * const publish = room.usePublishTopic('emoji');
379
+ *
380
+ * // After
381
+ * const room = db.room('chat', 'room-id');
382
+ * const publish = db.rooms.usePublishTopic(room, 'emoji');
383
+ */
384
+ usePublishTopic = <Topic extends keyof RoomSchema[RoomType]['topics']>(
385
+ topic: Topic,
386
+ ): ((data: RoomSchema[RoomType]['topics'][Topic]) => void) => {
387
+ return rooms.usePublishTopic(this, topic);
388
+ };
389
+
390
+ /**
391
+ * @deprecated
392
+ * `db.room(...).usePresence` is deprecated. You can replace it with `db.rooms.usePresence`.
393
+ *
394
+ * @example
395
+ *
396
+ * // Before
397
+ * const room = db.room('chat', 'room-id');
398
+ * const { peers } = room.usePresence({ keys: ["name", "avatar"] });
399
+ *
400
+ * // After
401
+ * const room = db.room('chat', 'room-id');
402
+ * const { peers } = db.rooms.usePresence(room, { keys: ["name", "avatar"] });
403
+ */
404
+ usePresence = <Keys extends keyof RoomSchema[RoomType]['presence']>(
405
+ opts: PresenceOpts<RoomSchema[RoomType]['presence'], Keys> = {},
406
+ ): PresenceHandle<RoomSchema[RoomType]['presence'], Keys> => {
407
+ return rooms.usePresence(this, opts);
408
+ };
409
+
410
+ /**
411
+ * @deprecated
412
+ * `db.room(...).useSyncPresence` is deprecated. You can replace it with `db.rooms.useSyncPresence`.
413
+ *
414
+ * @example
415
+ *
416
+ * // Before
417
+ * const room = db.room('chat', 'room-id');
418
+ * room.useSyncPresence(room, { nickname });
419
+ *
420
+ * // After
421
+ * const room = db.room('chat', 'room-id');
422
+ * db.rooms.useSyncPresence(room, { nickname });
423
+ */
424
+ useSyncPresence = (
425
+ data: Partial<RoomSchema[RoomType]['presence']>,
426
+ deps?: any[],
427
+ ): void => {
428
+ return rooms.useSyncPresence(this, data, deps);
429
+ };
430
+
431
+ /**
432
+ * @deprecated
433
+ * `db.room(...).useTypingIndicator` is deprecated. You can replace it with `db.rooms.useTypingIndicator`.
434
+ *
435
+ * @example
436
+ *
437
+ * // Before
438
+ * const room = db.room('chat', 'room-id');
439
+ * const typing = room.useTypingIndiactor(room, 'chat-input');
440
+ *
441
+ * // After
442
+ * const room = db.room('chat', 'room-id');
443
+ * const typing = db.rooms.useTypingIndiactor(room, 'chat-input');
444
+ */
445
+ useTypingIndicator = (
446
+ inputName: string,
447
+ opts: TypingIndicatorOpts = {},
448
+ ): TypingIndicatorHandle<RoomSchema[RoomType]['presence']> => {
449
+ return rooms.useTypingIndicator(this, inputName, opts);
450
+ };
451
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import InstantReactAbstractDatabase from './InstantReactAbstractDatabase.tsx';
2
+ import type { InstantReactRoom } from './InstantReactRoom.ts';
3
+ import { useQueryInternal } from './useQuery.ts';
4
+
5
+ export {
6
+ InstantReactAbstractDatabase,
7
+ type InstantReactRoom,
8
+ useQueryInternal,
9
+ };
@@ -0,0 +1,128 @@
1
+ import {
2
+ type InfiniteQuerySubscription,
3
+ type InstantCoreDatabase,
4
+ type InstantSchemaDef,
5
+ type InstaQLOptions,
6
+ InstaQLResponse,
7
+ ValidQuery,
8
+ weakHash,
9
+ getInfiniteQueryInitialSnapshot,
10
+ } from '@instantdb/core';
11
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
12
+
13
+ export type InfiniteQueryResult<
14
+ Schema extends InstantSchemaDef<any, any, any>,
15
+ Q extends ValidQuery<Q, Schema>,
16
+ UseDates extends boolean,
17
+ > =
18
+ | {
19
+ error: { message: string };
20
+ data: undefined;
21
+ isLoading: false;
22
+ canLoadNextPage: boolean;
23
+ loadNextPage: () => void;
24
+ }
25
+ | {
26
+ error: undefined;
27
+ data: undefined;
28
+ isLoading: true;
29
+ canLoadNextPage: boolean;
30
+ loadNextPage: () => void;
31
+ }
32
+ | {
33
+ error: undefined;
34
+ data: InstaQLResponse<Schema, Q, UseDates>;
35
+ isLoading: false;
36
+ canLoadNextPage: boolean;
37
+ loadNextPage: () => void;
38
+ };
39
+
40
+ const defaultState = {
41
+ error: undefined,
42
+ data: undefined,
43
+ isLoading: true,
44
+ canLoadNextPage: false,
45
+ };
46
+
47
+ export function useInfiniteQuerySubscription<
48
+ Schema extends InstantSchemaDef<any, any, any>,
49
+ Q extends ValidQuery<Q, Schema>,
50
+ UseDates extends boolean,
51
+ >({
52
+ core,
53
+ query,
54
+ opts,
55
+ }: {
56
+ core: InstantCoreDatabase<Schema, UseDates>;
57
+ query: Q | null;
58
+ opts?: InstaQLOptions;
59
+ }): InfiniteQueryResult<Schema, Q, UseDates> {
60
+ const subRef = useRef<InfiniteQuerySubscription | null>(null);
61
+
62
+ const queryHash = weakHash(query);
63
+ const optsHash = weakHash(opts);
64
+ const snapshot = getInfiniteQueryInitialSnapshot(core, query, opts);
65
+
66
+ const stateCacheRef = useRef<
67
+ Omit<InfiniteQueryResult<Schema, Q, UseDates>, 'loadNextPage'>
68
+ >({
69
+ ...snapshot,
70
+ isLoading: !snapshot.data && !snapshot.error,
71
+ });
72
+
73
+ const subscribe = useCallback(
74
+ (cb: () => void) => {
75
+ subRef.current = null;
76
+
77
+ const nextSnapshot = getInfiniteQueryInitialSnapshot(core, query, opts);
78
+ stateCacheRef.current = {
79
+ ...nextSnapshot,
80
+ isLoading: !nextSnapshot.data && !nextSnapshot.error,
81
+ };
82
+
83
+ cb();
84
+
85
+ if (!query) {
86
+ return () => {};
87
+ }
88
+
89
+ const sub = core.subscribeInfiniteQuery(
90
+ query,
91
+ (resp) => {
92
+ stateCacheRef.current = {
93
+ ...resp,
94
+ isLoading: false,
95
+ };
96
+ cb();
97
+ },
98
+ opts,
99
+ );
100
+
101
+ subRef.current = sub;
102
+
103
+ return () => {
104
+ sub.unsubscribe();
105
+ if (subRef.current === sub) {
106
+ subRef.current = null;
107
+ }
108
+ };
109
+ },
110
+ [queryHash, optsHash],
111
+ );
112
+
113
+ const state = useSyncExternalStore(
114
+ subscribe,
115
+ () => stateCacheRef.current,
116
+ () => defaultState,
117
+ );
118
+
119
+ const loadNextPage = () => {
120
+ subRef.current?.loadNextPage();
121
+ };
122
+
123
+ // @ts-expect-error union type
124
+ return {
125
+ ...state,
126
+ loadNextPage,
127
+ };
128
+ }
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import {
4
+ weakHash,
5
+ coerceQuery,
6
+ type InstaQLOptions,
7
+ InstantCoreDatabase,
8
+ InstaQLLifecycleState,
9
+ InstantSchemaDef,
10
+ ValidQuery,
11
+ } from '@instantdb/core';
12
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
13
+
14
+ const defaultState = {
15
+ isLoading: true,
16
+ data: undefined,
17
+ pageInfo: undefined,
18
+ error: undefined,
19
+ } as const;
20
+
21
+ function stateForResult(result: any) {
22
+ return {
23
+ isLoading: !Boolean(result),
24
+ data: undefined,
25
+ pageInfo: undefined,
26
+ error: undefined,
27
+ ...(result ? result : {}),
28
+ };
29
+ }
30
+
31
+ export function useQueryInternal<
32
+ Q extends ValidQuery<Q, Schema>,
33
+ Schema extends InstantSchemaDef<any, any, any>,
34
+ UseDates extends boolean,
35
+ >(
36
+ _core: InstantCoreDatabase<Schema, UseDates>,
37
+ _query: null | Q,
38
+ _opts?: InstaQLOptions,
39
+ serverSnapshot?: () => InstaQLLifecycleState<Schema, Q, UseDates> | undefined,
40
+ ): {
41
+ state: InstaQLLifecycleState<Schema, Q, UseDates>;
42
+ query: any;
43
+ queryHash: string;
44
+ } {
45
+ if (_query && _opts && 'ruleParams' in _opts) {
46
+ _query = { $$ruleParams: _opts['ruleParams'], ..._query };
47
+ }
48
+ const query = _query ? coerceQuery(_query) : null;
49
+ const queryHash = weakHash(query);
50
+
51
+ // We use a ref to store the result of the query.
52
+ // This is becuase `useSyncExternalStore` uses `Object.is`
53
+ // to compare the previous and next state.
54
+ // If we don't use a ref, the state will always be considered different, so
55
+ // the component will always re-render.
56
+ const resultCacheRef = useRef<InstaQLLifecycleState<Schema, Q, UseDates>>(
57
+ stateForResult(_core._reactor.getPreviousResult(query)),
58
+ );
59
+
60
+ // Cache the server snapshot to satisfy useSyncExternalStore's requirement
61
+ // that getServerSnapshot returns the same reference across calls.
62
+ const serverSnapshotCacheRef = useRef<
63
+ InstaQLLifecycleState<Schema, Q, UseDates>
64
+ >(defaultState as any);
65
+
66
+ // Similar to `resultCacheRef`, `useSyncExternalStore` will unsubscribe
67
+ // if `subscribe` changes, so we use `useCallback` to memoize the function.
68
+ const subscribe = useCallback(
69
+ (cb) => {
70
+ // Update the ref when the query changes to avoid showing stale data
71
+ resultCacheRef.current = stateForResult(
72
+ _core._reactor.getPreviousResult(query),
73
+ );
74
+ // before the subscribeQuery is connected and calls it's own callback,
75
+ // we might have data in the store via SSR, we need to notify useSyncExternalStore that
76
+ // the data has changed.
77
+ cb();
78
+
79
+ // Don't subscribe if query is null
80
+ if (!query) {
81
+ const unsubscribe = () => {};
82
+ return unsubscribe;
83
+ }
84
+
85
+ const unsubscribe = _core.subscribeQuery<Q, UseDates>(query, (result) => {
86
+ resultCacheRef.current = {
87
+ isLoading: !Boolean(result),
88
+ // @ts-expect-error: ts thinks this will always be overwritten
89
+ data: undefined,
90
+ // @ts-expect-error: ts thinks this will always be overwritten
91
+ pageInfo: undefined,
92
+ // @ts-expect-error: ts thinks this will always be overwritten
93
+ error: undefined,
94
+ ...result,
95
+ };
96
+
97
+ cb();
98
+ });
99
+
100
+ return unsubscribe;
101
+ },
102
+ // Build a new subscribe function if the query changes
103
+ [queryHash],
104
+ );
105
+
106
+ const state = useSyncExternalStore<
107
+ InstaQLLifecycleState<Schema, Q, UseDates>
108
+ >(
109
+ subscribe,
110
+ () => resultCacheRef.current,
111
+ () => {
112
+ if (serverSnapshot && serverSnapshotCacheRef.current === defaultState) {
113
+ const result = serverSnapshot();
114
+ if (result) {
115
+ serverSnapshotCacheRef.current = result;
116
+ }
117
+ }
118
+
119
+ return serverSnapshotCacheRef.current;
120
+ },
121
+ );
122
+
123
+ return { state, query, queryHash };
124
+ }