@liveblocks/react 0.18.0-beta0 → 0.18.0-beta3
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.
- package/.built-by-link-script +1 -1
- package/index.d.ts +542 -47
- package/index.js +218 -103
- package/index.mjs +2 -0
- package/package.json +2 -2
package/.built-by-link-script
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ee7b9e79d3549611f64024b4247e9a862cfddd34
|
package/index.d.ts
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import { JsonObject, LsonObject, BaseUserMeta, LiveObject, User, Others, Json, Room, BroadcastOptions, History, Client } from '@liveblocks/client';
|
|
3
|
+
export { Json, JsonObject, shallow } from '@liveblocks/client';
|
|
3
4
|
import { ToImmutable, Resolve, RoomInitializers } from '@liveblocks/client/internal';
|
|
4
|
-
import * as React from 'react';
|
|
5
5
|
|
|
6
|
+
declare type Props = {
|
|
7
|
+
fallback: NonNullable<ReactNode> | null;
|
|
8
|
+
children: () => ReactNode | undefined;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Almost like a normal <Suspense> component, except that for server-side
|
|
12
|
+
* renders, the fallback will be used.
|
|
13
|
+
*
|
|
14
|
+
* The child props will have to be provided in a function, i.e. change:
|
|
15
|
+
*
|
|
16
|
+
* <Suspense fallback={<Loading />}>
|
|
17
|
+
* <MyRealComponent a={1} />
|
|
18
|
+
* </Suspense>
|
|
19
|
+
*
|
|
20
|
+
* To:
|
|
21
|
+
*
|
|
22
|
+
* <ClientSideSuspense fallback={<Loading />}>
|
|
23
|
+
* {() => <MyRealComponent a={1} />}
|
|
24
|
+
* </ClientSideSuspense>
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
declare function ClientSideSuspense(props: Props): ReactElement;
|
|
28
|
+
|
|
29
|
+
declare type RoomProviderProps<TPresence extends JsonObject, TStorage extends LsonObject> = Resolve<{
|
|
30
|
+
/**
|
|
31
|
+
* The id of the room you want to connect to
|
|
32
|
+
*/
|
|
33
|
+
id: string;
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
} & RoomInitializers<TPresence, TStorage>>;
|
|
6
36
|
/**
|
|
7
37
|
* For any function type, returns a similar function type, but without the
|
|
8
38
|
* first argument.
|
|
9
39
|
*/
|
|
10
40
|
declare type OmitFirstArg<F> = F extends (first: any, ...rest: infer A) => infer R ? (...args: A) => R : never;
|
|
11
|
-
declare type MutationContext<TPresence extends JsonObject, TStorage extends LsonObject> = {
|
|
41
|
+
declare type MutationContext<TPresence extends JsonObject, TStorage extends LsonObject, TUserMeta extends BaseUserMeta> = {
|
|
12
42
|
root: LiveObject<TStorage>;
|
|
43
|
+
self: User<TPresence, TUserMeta>;
|
|
44
|
+
others: Others<TPresence, TUserMeta>;
|
|
13
45
|
setMyPresence: (patch: Partial<TPresence>, options?: {
|
|
14
46
|
addToHistory: boolean;
|
|
15
47
|
}) => void;
|
|
16
48
|
};
|
|
17
|
-
declare type
|
|
49
|
+
declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends LsonObject, TUserMeta extends BaseUserMeta, TRoomEvent extends Json> = {
|
|
18
50
|
/**
|
|
19
|
-
*
|
|
51
|
+
* You normally don't need to directly interact with the RoomContext, but
|
|
52
|
+
* it can be necessary if you're building an advanced app where you need to
|
|
53
|
+
* set up a context bridge between two React renderers.
|
|
20
54
|
*/
|
|
21
|
-
id: string;
|
|
22
|
-
children: React.ReactNode;
|
|
23
|
-
} & RoomInitializers<TPresence, TStorage>>;
|
|
24
|
-
declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends LsonObject, TUserMeta extends BaseUserMeta, TRoomEvent extends Json> = {
|
|
25
55
|
RoomContext: React.Context<Room<TPresence, TStorage, TUserMeta, TRoomEvent> | null>;
|
|
26
56
|
/**
|
|
27
57
|
* Makes a Room available in the component hierarchy below.
|
|
@@ -29,13 +59,18 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
|
|
|
29
59
|
* That means that you can't have 2 RoomProvider with the same room id in your react tree.
|
|
30
60
|
*/
|
|
31
61
|
RoomProvider(props: RoomProviderProps<TPresence, TStorage>): JSX.Element;
|
|
62
|
+
/**
|
|
63
|
+
* Returns the Room of the nearest RoomProvider above in the React component
|
|
64
|
+
* tree.
|
|
65
|
+
*/
|
|
66
|
+
useRoom(): Room<TPresence, TStorage, TUserMeta, TRoomEvent>;
|
|
32
67
|
/**
|
|
33
68
|
* Returns a function that batches modifications made during the given function.
|
|
34
69
|
* All the modifications are sent to other clients in a single message.
|
|
35
70
|
* All the modifications are merged in a single history item (undo/redo).
|
|
36
71
|
* All the subscribers are called only after the batch is over.
|
|
37
72
|
*/
|
|
38
|
-
useBatch(): (callback: () =>
|
|
73
|
+
useBatch<T>(): (callback: () => T) => T;
|
|
39
74
|
/**
|
|
40
75
|
* Returns a callback that lets you broadcast custom events to other users in the room
|
|
41
76
|
*
|
|
@@ -123,6 +158,14 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
|
|
|
123
158
|
* const object = useObject("obj");
|
|
124
159
|
*/
|
|
125
160
|
useObject<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey] | null;
|
|
161
|
+
/**
|
|
162
|
+
* Returns the mutable (!) Storage root. This hook exists for
|
|
163
|
+
* backward-compatible reasons.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* const [root] = useStorageRoot();
|
|
167
|
+
*/
|
|
168
|
+
useStorageRoot(): [root: LiveObject<TStorage> | null];
|
|
126
169
|
/**
|
|
127
170
|
* Returns your entire Liveblocks Storage as an immutable data structure.
|
|
128
171
|
*
|
|
@@ -149,7 +192,39 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
|
|
|
149
192
|
* return the result of a .map() or .filter() call from the selector. In
|
|
150
193
|
* those cases, you'll probably want to use a `shallow` comparison check.
|
|
151
194
|
*/
|
|
152
|
-
useStorage<T>(selector: (root: ToImmutable<TStorage>) => T, isEqual?: (
|
|
195
|
+
useStorage<T>(selector: (root: ToImmutable<TStorage>) => T, isEqual?: (prev: T, curr: T) => boolean): T | null;
|
|
196
|
+
/**
|
|
197
|
+
* Gets the current user once it is connected to the room.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* const me = useSelf();
|
|
201
|
+
* const { x, y } = me.presence.cursor;
|
|
202
|
+
*/
|
|
203
|
+
useSelf(): User<TPresence, TUserMeta> | null;
|
|
204
|
+
/**
|
|
205
|
+
* Extract arbitrary data based on the current user.
|
|
206
|
+
*
|
|
207
|
+
* The selector function will get re-evaluated any time your presence data
|
|
208
|
+
* changes.
|
|
209
|
+
*
|
|
210
|
+
* The component that uses this hook will automatically re-render if your
|
|
211
|
+
* selector function returns a different value from its previous run.
|
|
212
|
+
*
|
|
213
|
+
* By default `useSelf()` uses strict `===` to check for equality. Take extra
|
|
214
|
+
* care when returning a computed object or list, for example when you return
|
|
215
|
+
* the result of a .map() or .filter() call from the selector. In those
|
|
216
|
+
* cases, you'll probably want to use a `shallow` comparison check.
|
|
217
|
+
*
|
|
218
|
+
* Will return `null` while Liveblocks isn't connected to a room yet.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* const cursor = useSelf(me => me.presence.cursor);
|
|
222
|
+
* if (cursor !== null) {
|
|
223
|
+
* const { x, y } = cursor;
|
|
224
|
+
* }
|
|
225
|
+
*
|
|
226
|
+
*/
|
|
227
|
+
useSelf<T>(selector: (me: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T | null;
|
|
153
228
|
/**
|
|
154
229
|
* Returns the presence of the current user of the current room, and a function to update it.
|
|
155
230
|
* It is different from the setState function returned by the useState hook from React.
|
|
@@ -209,52 +284,70 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
|
|
|
209
284
|
* const someoneIsTyping = useOthers(users => users.some(u => u.presence.isTyping));
|
|
210
285
|
*
|
|
211
286
|
*/
|
|
212
|
-
useOthers<T>(selector: (others: Others<TPresence, TUserMeta>) => T, isEqual?: (
|
|
287
|
+
useOthers<T>(selector: (others: Others<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
|
|
213
288
|
/**
|
|
214
|
-
* Returns
|
|
215
|
-
*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
*
|
|
289
|
+
* Returns an array of connection IDs. This matches the values you'll get by
|
|
290
|
+
* using the `useOthers()` hook.
|
|
291
|
+
*
|
|
292
|
+
* Roughly equivalent to:
|
|
293
|
+
* useOthers((others) => others.map(other => other.connectionId), shallow)
|
|
294
|
+
*
|
|
295
|
+
* This is useful in particular to implement efficiently rendering components
|
|
296
|
+
* for each user in the room, e.g. cursors.
|
|
220
297
|
*
|
|
221
298
|
* @example
|
|
222
|
-
* const
|
|
223
|
-
*
|
|
299
|
+
* const ids = useConnectionIds();
|
|
300
|
+
* // [2, 4, 7]
|
|
224
301
|
*/
|
|
225
|
-
|
|
302
|
+
useConnectionIds(): readonly number[];
|
|
226
303
|
/**
|
|
227
|
-
*
|
|
304
|
+
* Related to useOthers(), but optimized for selecting only "subsets" of
|
|
305
|
+
* others. This is useful for performance reasons in particular, because
|
|
306
|
+
* selecting only a subset of users also means limiting the number of
|
|
307
|
+
* re-renders that will be triggered.
|
|
228
308
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* selector function returns a different value from its previous run.
|
|
309
|
+
* @example
|
|
310
|
+
* const avatars = useOthersWithData(user => user.info.avatar);
|
|
311
|
+
* // ^^^^^^^
|
|
312
|
+
* // { connectionId: number; data: string }[]
|
|
234
313
|
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
314
|
+
* The selector function you pass to useOthersWithData() is called an "item
|
|
315
|
+
* selector", and operates on a single user at a time. If you provide an
|
|
316
|
+
* (optional) "item comparison" function, it will be used to compare each
|
|
317
|
+
* item pairwise.
|
|
239
318
|
*
|
|
240
|
-
*
|
|
319
|
+
* For example, to select multiple properties:
|
|
241
320
|
*
|
|
242
321
|
* @example
|
|
243
|
-
* const
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
322
|
+
* const avatarsAndCursors = useOthersWithData(
|
|
323
|
+
* user => [u.info.avatar, u.presence.cursor],
|
|
324
|
+
* shallow, // 👈
|
|
325
|
+
* );
|
|
326
|
+
*/
|
|
327
|
+
useOthersWithData<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, itemIsEqual?: (prev: T, curr: T) => boolean): readonly {
|
|
328
|
+
readonly connectionId: number;
|
|
329
|
+
readonly data: T;
|
|
330
|
+
}[];
|
|
331
|
+
/**
|
|
332
|
+
* Given a connection ID (as obtained by using `useConnectionIds()`), you can
|
|
333
|
+
* call this selector deep down in your component stack to only have the
|
|
334
|
+
* component re-render if properties for this particular user change.
|
|
247
335
|
*
|
|
336
|
+
* @example
|
|
337
|
+
* // Returns full user and re-renders whenever anything on the user changes
|
|
338
|
+
* const secondUser = useOther(2);
|
|
248
339
|
*/
|
|
249
|
-
|
|
340
|
+
useOther(connectionId: number): User<TPresence, TUserMeta>;
|
|
250
341
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
342
|
+
* Given a connection ID (as obtained by using `useConnectionIds()`), you can
|
|
343
|
+
* call this selector deep down in your component stack to only have the
|
|
344
|
+
* component re-render if properties for this particular user change.
|
|
253
345
|
*
|
|
254
346
|
* @example
|
|
255
|
-
*
|
|
347
|
+
* // Returns only the selected values re-renders whenever that selection changes)
|
|
348
|
+
* const { x, y } = useOther(2, user => user.presence.cursor);
|
|
256
349
|
*/
|
|
257
|
-
|
|
350
|
+
useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
|
|
258
351
|
/**
|
|
259
352
|
* useUpdateMyPresence is similar to useMyPresence but it only returns the function to update the current user presence.
|
|
260
353
|
* If you don't use the current user presence in your component, but you need to update it (e.g. live cursor), it's better to use useUpdateMyPresence to avoid unnecessary renders.
|
|
@@ -269,16 +362,418 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
|
|
|
269
362
|
useUpdateMyPresence(): (patch: Partial<TPresence>, options?: {
|
|
270
363
|
addToHistory: boolean;
|
|
271
364
|
}) => void;
|
|
272
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Create a callback function that can be called to mutate Liveblocks state.
|
|
367
|
+
*
|
|
368
|
+
* The first argument that gets passed into your callback will be a "mutation
|
|
369
|
+
* context", which exposes the following:
|
|
370
|
+
*
|
|
371
|
+
* - `root` - The mutable Storage root.
|
|
372
|
+
* You can normal mutation on Live structures with this, for
|
|
373
|
+
* example: root.get('layers').get('layer1').set('fill', 'red')
|
|
374
|
+
*
|
|
375
|
+
* - `setMyPresence` - Call this with a new (partial) Presence value.
|
|
376
|
+
*
|
|
377
|
+
* - `self` - A read-only version of the latest self, if you need it to
|
|
378
|
+
* compute the next state.
|
|
379
|
+
*
|
|
380
|
+
* - `others` - A read-only version of the latest others list, if you need
|
|
381
|
+
* it to compute the next state.
|
|
382
|
+
*
|
|
383
|
+
* useMutation is like React's useCallback, except that the first argument
|
|
384
|
+
* that gets passed into your callback will be a "mutation context".
|
|
385
|
+
*
|
|
386
|
+
* If you want get access to the immutable root somewhere in your mutation,
|
|
387
|
+
* you can use `root.ToImmutable()`.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* const fillLayers = useMutation(
|
|
391
|
+
* ({ root }, color: Color) => {
|
|
392
|
+
* ...
|
|
393
|
+
* },
|
|
394
|
+
* [],
|
|
395
|
+
* );
|
|
396
|
+
*
|
|
397
|
+
* fillLayers('red');
|
|
398
|
+
*
|
|
399
|
+
* const deleteLayers = useMutation(
|
|
400
|
+
* ({ root }) => {
|
|
401
|
+
* ...
|
|
402
|
+
* },
|
|
403
|
+
* [],
|
|
404
|
+
* );
|
|
405
|
+
*
|
|
406
|
+
* deleteLayers();
|
|
407
|
+
*/
|
|
408
|
+
useMutation<F extends (context: MutationContext<TPresence, TStorage, TUserMeta>, ...args: any[]) => any>(callback: F, deps: readonly unknown[]): OmitFirstArg<F>;
|
|
273
409
|
suspense: {
|
|
410
|
+
/**
|
|
411
|
+
* You normally don't need to directly interact with the RoomContext, but
|
|
412
|
+
* it can be necessary if you're building an advanced app where you need to
|
|
413
|
+
* set up a context bridge between two React renderers.
|
|
414
|
+
*/
|
|
415
|
+
RoomContext: React.Context<Room<TPresence, TStorage, TUserMeta, TRoomEvent> | null>;
|
|
416
|
+
/**
|
|
417
|
+
* Makes a Room available in the component hierarchy below.
|
|
418
|
+
* When this component is unmounted, the current user leave the room.
|
|
419
|
+
* That means that you can't have 2 RoomProvider with the same room id in your react tree.
|
|
420
|
+
*/
|
|
421
|
+
RoomProvider(props: RoomProviderProps<TPresence, TStorage>): JSX.Element;
|
|
422
|
+
/**
|
|
423
|
+
* Returns the Room of the nearest RoomProvider above in the React component
|
|
424
|
+
* tree.
|
|
425
|
+
*/
|
|
426
|
+
useRoom(): Room<TPresence, TStorage, TUserMeta, TRoomEvent>;
|
|
427
|
+
/**
|
|
428
|
+
* Returns a function that batches modifications made during the given function.
|
|
429
|
+
* All the modifications are sent to other clients in a single message.
|
|
430
|
+
* All the modifications are merged in a single history item (undo/redo).
|
|
431
|
+
* All the subscribers are called only after the batch is over.
|
|
432
|
+
*/
|
|
433
|
+
useBatch<T>(): (callback: () => T) => T;
|
|
434
|
+
/**
|
|
435
|
+
* Returns a callback that lets you broadcast custom events to other users in the room
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* const broadcast = useBroadcastEvent();
|
|
439
|
+
*
|
|
440
|
+
* broadcast({ type: "CUSTOM_EVENT", data: { x: 0, y: 0 } });
|
|
441
|
+
*/
|
|
442
|
+
useBroadcastEvent(): (event: TRoomEvent, options?: BroadcastOptions) => void;
|
|
443
|
+
/**
|
|
444
|
+
* useErrorListener is a react hook that lets you react to potential room connection errors.
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* useErrorListener(er => {
|
|
448
|
+
* console.error(er);
|
|
449
|
+
* })
|
|
450
|
+
*/
|
|
451
|
+
useErrorListener(callback: (err: Error) => void): void;
|
|
452
|
+
/**
|
|
453
|
+
* useEventListener is a react hook that lets you react to event broadcasted by other users in the room.
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* useEventListener(({ connectionId, event }) => {
|
|
457
|
+
* if (event.type === "CUSTOM_EVENT") {
|
|
458
|
+
* // Do something
|
|
459
|
+
* }
|
|
460
|
+
* });
|
|
461
|
+
*/
|
|
462
|
+
useEventListener(callback: (eventData: {
|
|
463
|
+
connectionId: number;
|
|
464
|
+
event: TRoomEvent;
|
|
465
|
+
}) => void): void;
|
|
466
|
+
/**
|
|
467
|
+
* Returns the room.history
|
|
468
|
+
*/
|
|
469
|
+
useHistory(): History;
|
|
470
|
+
/**
|
|
471
|
+
* Returns a function that undoes the last operation executed by the current client.
|
|
472
|
+
* It does not impact operations made by other clients.
|
|
473
|
+
*/
|
|
474
|
+
useUndo(): () => void;
|
|
475
|
+
/**
|
|
476
|
+
* Returns a function that redoes the last operation executed by the current client.
|
|
477
|
+
* It does not impact operations made by other clients.
|
|
478
|
+
*/
|
|
479
|
+
useRedo(): () => void;
|
|
480
|
+
/**
|
|
481
|
+
* Returns whether there are any operations to undo.
|
|
482
|
+
*/
|
|
483
|
+
useCanUndo(): boolean;
|
|
484
|
+
/**
|
|
485
|
+
* Returns whether there are any operations to redo.
|
|
486
|
+
*/
|
|
487
|
+
useCanRedo(): boolean;
|
|
488
|
+
/**
|
|
489
|
+
* Returns the mutable (!) Storage root. This hook exists for
|
|
490
|
+
* backward-compatible reasons.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* const [root] = useStorageRoot();
|
|
494
|
+
*/
|
|
495
|
+
useStorageRoot(): [root: LiveObject<TStorage> | null];
|
|
496
|
+
/**
|
|
497
|
+
* Returns your entire Liveblocks Storage as an immutable data structure.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* const root = useStorage();
|
|
501
|
+
*/
|
|
274
502
|
useStorage(): ToImmutable<TStorage>;
|
|
275
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Extract arbitrary data from the Liveblocks Storage state, using an
|
|
505
|
+
* arbitrary selector function.
|
|
506
|
+
*
|
|
507
|
+
* The selector function will get re-evaluated any time something changes in
|
|
508
|
+
* Storage. The value returned by your selector function will also be the
|
|
509
|
+
* value returned by the hook.
|
|
510
|
+
*
|
|
511
|
+
* The `root` value that gets passed to your selector function is
|
|
512
|
+
* a immutable/readonly version of your Liveblocks storage root.
|
|
513
|
+
*
|
|
514
|
+
* The component that uses this hook will automatically re-render if the
|
|
515
|
+
* returned value changes.
|
|
516
|
+
*
|
|
517
|
+
* By default `useStorage()` uses strict `===` to check for equality. Take
|
|
518
|
+
* extra care when returning a computed object or list, for example when you
|
|
519
|
+
* return the result of a .map() or .filter() call from the selector. In
|
|
520
|
+
* those cases, you'll probably want to use a `shallow` comparison check.
|
|
521
|
+
*/
|
|
522
|
+
useStorage<T>(selector: (root: ToImmutable<TStorage>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
|
|
523
|
+
/**
|
|
524
|
+
* Gets the current user once it is connected to the room.
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* const me = useSelf();
|
|
528
|
+
* const { x, y } = me.presence.cursor;
|
|
529
|
+
*/
|
|
276
530
|
useSelf(): User<TPresence, TUserMeta>;
|
|
277
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Extract arbitrary data based on the current user.
|
|
533
|
+
*
|
|
534
|
+
* The selector function will get re-evaluated any time your presence data
|
|
535
|
+
* changes.
|
|
536
|
+
*
|
|
537
|
+
* The component that uses this hook will automatically re-render if your
|
|
538
|
+
* selector function returns a different value from its previous run.
|
|
539
|
+
*
|
|
540
|
+
* By default `useSelf()` uses strict `===` to check for equality. Take extra
|
|
541
|
+
* care when returning a computed object or list, for example when you return
|
|
542
|
+
* the result of a .map() or .filter() call from the selector. In those
|
|
543
|
+
* cases, you'll probably want to use a `shallow` comparison check.
|
|
544
|
+
*
|
|
545
|
+
* Will return `null` while Liveblocks isn't connected to a room yet.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* const cursor = useSelf(me => me.presence.cursor);
|
|
549
|
+
* if (cursor !== null) {
|
|
550
|
+
* const { x, y } = cursor;
|
|
551
|
+
* }
|
|
552
|
+
*
|
|
553
|
+
*/
|
|
554
|
+
useSelf<T>(selector: (me: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
|
|
555
|
+
/**
|
|
556
|
+
* Returns the presence of the current user of the current room, and a function to update it.
|
|
557
|
+
* It is different from the setState function returned by the useState hook from React.
|
|
558
|
+
* You don't need to pass the full presence object to update it.
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* const [myPresence, updateMyPresence] = useMyPresence();
|
|
562
|
+
* updateMyPresence({ x: 0 });
|
|
563
|
+
* updateMyPresence({ y: 0 });
|
|
564
|
+
*
|
|
565
|
+
* // At the next render, "myPresence" will be equal to "{ x: 0, y: 0 }"
|
|
566
|
+
*/
|
|
567
|
+
useMyPresence(): [
|
|
568
|
+
TPresence,
|
|
569
|
+
(patch: Partial<TPresence>, options?: {
|
|
570
|
+
addToHistory: boolean;
|
|
571
|
+
}) => void
|
|
572
|
+
];
|
|
573
|
+
/**
|
|
574
|
+
* Returns an object that lets you get information about all the users
|
|
575
|
+
* currently connected in the room.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* const others = useOthers();
|
|
579
|
+
*
|
|
580
|
+
* // Example to map all cursors in JSX
|
|
581
|
+
* return (
|
|
582
|
+
* <>
|
|
583
|
+
* {others.map((user) => {
|
|
584
|
+
* if (user.presence.cursor == null) {
|
|
585
|
+
* return null;
|
|
586
|
+
* }
|
|
587
|
+
* return <Cursor key={user.connectionId} cursor={user.presence.cursor} />
|
|
588
|
+
* })}
|
|
589
|
+
* </>
|
|
590
|
+
* )
|
|
591
|
+
*/
|
|
278
592
|
useOthers(): Others<TPresence, TUserMeta>;
|
|
279
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Extract arbitrary data based on all the users currently connected in the
|
|
595
|
+
* room (except yourself).
|
|
596
|
+
*
|
|
597
|
+
* The selector function will get re-evaluated any time a user enters or
|
|
598
|
+
* leaves the room, as well as whenever their presence data changes.
|
|
599
|
+
*
|
|
600
|
+
* The component that uses this hook will automatically re-render if your
|
|
601
|
+
* selector function returns a different value from its previous run.
|
|
602
|
+
*
|
|
603
|
+
* By default `useOthers()` uses strict `===` to check for equality. Take
|
|
604
|
+
* extra care when returning a computed object or list, for example when you
|
|
605
|
+
* return the result of a .map() or .filter() call from the selector. In
|
|
606
|
+
* those cases, you'll probably want to use a `shallow` comparison check.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* const avatars = useOthers(users => users.map(u => u.info.avatar), shallow);
|
|
610
|
+
* const cursors = useOthers(users => users.map(u => u.presence.cursor), shallow);
|
|
611
|
+
* const someoneIsTyping = useOthers(users => users.some(u => u.presence.isTyping));
|
|
612
|
+
*
|
|
613
|
+
*/
|
|
614
|
+
useOthers<T>(selector: (others: Others<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
|
|
615
|
+
/**
|
|
616
|
+
* Returns an array of connection IDs. This matches the values you'll get by
|
|
617
|
+
* using the `useOthers()` hook.
|
|
618
|
+
*
|
|
619
|
+
* Roughly equivalent to:
|
|
620
|
+
* useOthers((others) => others.map(other => other.connectionId), shallow)
|
|
621
|
+
*
|
|
622
|
+
* This is useful in particular to implement efficiently rendering components
|
|
623
|
+
* for each user in the room, e.g. cursors.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* const ids = useConnectionIds();
|
|
627
|
+
* // [2, 4, 7]
|
|
628
|
+
*/
|
|
629
|
+
useConnectionIds(): readonly number[];
|
|
630
|
+
/**
|
|
631
|
+
* Related to useOthers(), but optimized for selecting only "subsets" of
|
|
632
|
+
* others. This is useful for performance reasons in particular, because
|
|
633
|
+
* selecting only a subset of users also means limiting the number of
|
|
634
|
+
* re-renders that will be triggered.
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* const avatars = useOthersWithData(user => user.info.avatar);
|
|
638
|
+
* // ^^^^^^^
|
|
639
|
+
* // { connectionId: number; data: string }[]
|
|
640
|
+
*
|
|
641
|
+
* The selector function you pass to useOthersWithData() is called an "item
|
|
642
|
+
* selector", and operates on a single user at a time. If you provide an
|
|
643
|
+
* (optional) "item comparison" function, it will be used to compare each
|
|
644
|
+
* item pairwise.
|
|
645
|
+
*
|
|
646
|
+
* For example, to select multiple properties:
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* const avatarsAndCursors = useOthersWithData(
|
|
650
|
+
* user => [u.info.avatar, u.presence.cursor],
|
|
651
|
+
* shallow, // 👈
|
|
652
|
+
* );
|
|
653
|
+
*/
|
|
654
|
+
useOthersWithData<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, itemIsEqual?: (prev: T, curr: T) => boolean): readonly {
|
|
655
|
+
readonly connectionId: number;
|
|
656
|
+
readonly data: T;
|
|
657
|
+
}[];
|
|
658
|
+
/**
|
|
659
|
+
* Given a connection ID (as obtained by using `useConnectionIds()`), you
|
|
660
|
+
* can call this selector deep down in your component stack to only have
|
|
661
|
+
* the component re-render if properties for this particular user change.
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* // Returns full user and re-renders whenever anything on the user changes
|
|
665
|
+
* const secondUser = useOther(2);
|
|
666
|
+
*/
|
|
667
|
+
useOther(connectionId: number): User<TPresence, TUserMeta>;
|
|
668
|
+
/**
|
|
669
|
+
* Given a connection ID (as obtained by using `useConnectionIds()`), you
|
|
670
|
+
* can call this selector deep down in your component stack to only have
|
|
671
|
+
* the component re-render if properties for this particular user change.
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* // Returns only the selected values re-renders whenever that selection changes)
|
|
675
|
+
* const { x, y } = useOther(2, user => user.presence.cursor);
|
|
676
|
+
*/
|
|
677
|
+
useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
|
|
678
|
+
/**
|
|
679
|
+
* useUpdateMyPresence is similar to useMyPresence but it only returns the function to update the current user presence.
|
|
680
|
+
* If you don't use the current user presence in your component, but you need to update it (e.g. live cursor), it's better to use useUpdateMyPresence to avoid unnecessary renders.
|
|
681
|
+
*
|
|
682
|
+
* @example
|
|
683
|
+
* const updateMyPresence = useUpdateMyPresence();
|
|
684
|
+
* updateMyPresence({ x: 0 });
|
|
685
|
+
* updateMyPresence({ y: 0 });
|
|
686
|
+
*
|
|
687
|
+
* // At the next render, the presence of the current user will be equal to "{ x: 0, y: 0 }"
|
|
688
|
+
*/
|
|
689
|
+
useUpdateMyPresence(): (patch: Partial<TPresence>, options?: {
|
|
690
|
+
addToHistory: boolean;
|
|
691
|
+
}) => void;
|
|
692
|
+
/**
|
|
693
|
+
* Create a callback function that can be called to mutate Liveblocks
|
|
694
|
+
* state.
|
|
695
|
+
*
|
|
696
|
+
* The first argument that gets passed into your callback will be
|
|
697
|
+
* a "mutation context", which exposes the following:
|
|
698
|
+
*
|
|
699
|
+
* - `root` - The mutable Storage root.
|
|
700
|
+
* You can normal mutation on Live structures with this, for
|
|
701
|
+
* example: root.get('layers').get('layer1').set('fill',
|
|
702
|
+
* 'red')
|
|
703
|
+
*
|
|
704
|
+
* - `setMyPresence` - Call this with a new (partial) Presence value.
|
|
705
|
+
*
|
|
706
|
+
* - `self` - A read-only version of the latest self, if you need it to
|
|
707
|
+
* compute the next state.
|
|
708
|
+
*
|
|
709
|
+
* - `others` - A read-only version of the latest others list, if you
|
|
710
|
+
* need it to compute the next state.
|
|
711
|
+
*
|
|
712
|
+
* useMutation is like React's useCallback, except that the first argument
|
|
713
|
+
* that gets passed into your callback will be a "mutation context".
|
|
714
|
+
*
|
|
715
|
+
* If you want get access to the immutable root somewhere in your mutation,
|
|
716
|
+
* you can use `root.ToImmutable()`.
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* const fillLayers = useMutation(
|
|
720
|
+
* ({ root }, color: Color) => {
|
|
721
|
+
* ...
|
|
722
|
+
* },
|
|
723
|
+
* [],
|
|
724
|
+
* );
|
|
725
|
+
*
|
|
726
|
+
* fillLayers('red');
|
|
727
|
+
*
|
|
728
|
+
* const deleteLayers = useMutation(
|
|
729
|
+
* ({ root }) => {
|
|
730
|
+
* ...
|
|
731
|
+
* },
|
|
732
|
+
* [],
|
|
733
|
+
* );
|
|
734
|
+
*
|
|
735
|
+
* deleteLayers();
|
|
736
|
+
*/
|
|
737
|
+
useMutation<F extends (context: MutationContext<TPresence, TStorage, TUserMeta>, ...args: any[]) => any>(callback: F, deps: readonly unknown[]): OmitFirstArg<F>;
|
|
738
|
+
/**
|
|
739
|
+
* Returns the LiveList associated with the provided key. The hook triggers
|
|
740
|
+
* a re-render if the LiveList is updated, however it does not triggers
|
|
741
|
+
* a re-render if a nested CRDT is updated.
|
|
742
|
+
*
|
|
743
|
+
* @param key The storage key associated with the LiveList
|
|
744
|
+
* @returns null while the storage is loading, otherwise, returns the LiveList associated to the storage
|
|
745
|
+
*
|
|
746
|
+
* @example
|
|
747
|
+
* const animals = useList("animals"); // e.g. [] or ["🦁", "🐍", "🦍"]
|
|
748
|
+
*/
|
|
749
|
+
useList<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
|
|
750
|
+
/**
|
|
751
|
+
* Returns the LiveMap associated with the provided key. If the LiveMap
|
|
752
|
+
* does not exist, a new empty LiveMap will be created. The hook triggers
|
|
753
|
+
* a re-render if the LiveMap is updated, however it does not triggers
|
|
754
|
+
* a re-render if a nested CRDT is updated.
|
|
755
|
+
*
|
|
756
|
+
* @param key The storage key associated with the LiveMap
|
|
757
|
+
* @returns null while the storage is loading, otherwise, returns the LiveMap associated to the storage
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* const shapesById = useMap("shapes");
|
|
761
|
+
*/
|
|
762
|
+
useMap<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
|
|
763
|
+
/**
|
|
764
|
+
* Returns the LiveObject associated with the provided key.
|
|
765
|
+
* The hook triggers a re-render if the LiveObject is updated, however it does not triggers a re-render if a nested CRDT is updated.
|
|
766
|
+
*
|
|
767
|
+
* @param key The storage key associated with the LiveObject
|
|
768
|
+
* @returns null while the storage is loading, otherwise, returns the LveObject associated to the storage
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* const object = useObject("obj");
|
|
772
|
+
*/
|
|
773
|
+
useObject<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
|
|
280
774
|
};
|
|
281
775
|
};
|
|
776
|
+
|
|
282
777
|
declare function createRoomContext<TPresence extends JsonObject, TStorage extends LsonObject = LsonObject, TUserMeta extends BaseUserMeta = BaseUserMeta, TRoomEvent extends Json = never>(client: Client): RoomContextBundle<TPresence, TStorage, TUserMeta, TRoomEvent>;
|
|
283
778
|
|
|
284
|
-
export { MutationContext, createRoomContext };
|
|
779
|
+
export { ClientSideSuspense, MutationContext, createRoomContext };
|
package/index.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }// src/
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }// src/ClientSideSuspense.tsx
|
|
2
|
+
var _react = require('react'); var React = _interopRequireWildcard(_react); var React2 = _interopRequireWildcard(_react);
|
|
3
|
+
function ClientSideSuspense(props) {
|
|
4
|
+
const [mounted, setMounted] = React.useState(false);
|
|
5
|
+
React.useEffect(() => {
|
|
6
|
+
setMounted(true);
|
|
7
|
+
}, []);
|
|
8
|
+
return /* @__PURE__ */ React.createElement(React.Suspense, {
|
|
9
|
+
fallback: props.fallback
|
|
10
|
+
}, mounted ? props.children() : props.fallback);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/factory.tsx
|
|
14
|
+
var _client = require('@liveblocks/client');
|
|
2
15
|
var _internal = require('@liveblocks/client/internal');
|
|
3
|
-
|
|
16
|
+
|
|
4
17
|
var _withselector = require('use-sync-external-store/shim/with-selector');
|
|
5
18
|
|
|
6
19
|
// src/hooks.ts
|
|
@@ -20,25 +33,46 @@ function useInitial(value) {
|
|
|
20
33
|
var noop = () => {
|
|
21
34
|
};
|
|
22
35
|
var identity = (x) => x;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
Object.defineProperty(EMPTY_OTHERS, "toArray", {
|
|
29
|
-
value: () => EMPTY_OTHERS,
|
|
30
|
-
enumerable: false
|
|
31
|
-
});
|
|
32
|
-
_internal.freeze.call(void 0, EMPTY_OTHERS);
|
|
36
|
+
function useSyncExternalStore(s, g, gg) {
|
|
37
|
+
return _withselector.useSyncExternalStoreWithSelector.call(void 0, s, g, gg, identity);
|
|
38
|
+
}
|
|
39
|
+
var EMPTY_OTHERS = _internal.asArrayWithLegacyMethods.call(void 0, []);
|
|
33
40
|
function getEmptyOthers() {
|
|
34
41
|
return EMPTY_OTHERS;
|
|
35
42
|
}
|
|
43
|
+
function makeMutationContext(room) {
|
|
44
|
+
const errmsg = "This mutation cannot be used until connected to the Liveblocks room";
|
|
45
|
+
return {
|
|
46
|
+
get root() {
|
|
47
|
+
const root = room.getStorageSnapshot();
|
|
48
|
+
if (root === null) {
|
|
49
|
+
throw new Error(errmsg);
|
|
50
|
+
}
|
|
51
|
+
return root;
|
|
52
|
+
},
|
|
53
|
+
get self() {
|
|
54
|
+
const self = room.getSelf();
|
|
55
|
+
if (self === null) {
|
|
56
|
+
throw new Error(errmsg);
|
|
57
|
+
}
|
|
58
|
+
return self;
|
|
59
|
+
},
|
|
60
|
+
get others() {
|
|
61
|
+
const others = room.getOthers();
|
|
62
|
+
if (!room.isSelfAware()) {
|
|
63
|
+
throw new Error(errmsg);
|
|
64
|
+
}
|
|
65
|
+
return others;
|
|
66
|
+
},
|
|
67
|
+
setMyPresence: room.updatePresence
|
|
68
|
+
};
|
|
69
|
+
}
|
|
36
70
|
function createRoomContext(client) {
|
|
37
|
-
const RoomContext =
|
|
71
|
+
const RoomContext = React2.createContext(null);
|
|
38
72
|
function RoomProvider(props) {
|
|
39
73
|
const { id: roomId, initialPresence, initialStorage } = props;
|
|
40
74
|
if (process.env.NODE_ENV !== "production") {
|
|
41
|
-
if (roomId
|
|
75
|
+
if (!roomId) {
|
|
42
76
|
throw new Error(
|
|
43
77
|
"RoomProvider id property is required. For more information: https://liveblocks.io/docs/errors/liveblocks-react/RoomProvider-id-property-is-required"
|
|
44
78
|
);
|
|
@@ -51,14 +85,14 @@ function createRoomContext(client) {
|
|
|
51
85
|
initialPresence,
|
|
52
86
|
initialStorage
|
|
53
87
|
});
|
|
54
|
-
const [room, setRoom] =
|
|
88
|
+
const [room, setRoom] = React2.useState(
|
|
55
89
|
() => client.enter(roomId, {
|
|
56
90
|
initialPresence,
|
|
57
91
|
initialStorage,
|
|
58
92
|
DO_NOT_USE_withoutConnecting: typeof window === "undefined"
|
|
59
93
|
})
|
|
60
94
|
);
|
|
61
|
-
|
|
95
|
+
React2.useEffect(() => {
|
|
62
96
|
setRoom(
|
|
63
97
|
client.enter(roomId, {
|
|
64
98
|
initialPresence: frozen.initialPresence,
|
|
@@ -70,26 +104,26 @@ function createRoomContext(client) {
|
|
|
70
104
|
client.leave(roomId);
|
|
71
105
|
};
|
|
72
106
|
}, [roomId, frozen]);
|
|
73
|
-
return /* @__PURE__ */
|
|
107
|
+
return /* @__PURE__ */ React2.createElement(RoomContext.Provider, {
|
|
74
108
|
value: room
|
|
75
109
|
}, props.children);
|
|
76
110
|
}
|
|
111
|
+
function connectionIdSelector(others) {
|
|
112
|
+
return others.map((user) => user.connectionId);
|
|
113
|
+
}
|
|
77
114
|
function useRoom() {
|
|
78
|
-
const room =
|
|
79
|
-
if (room
|
|
115
|
+
const room = React2.useContext(RoomContext);
|
|
116
|
+
if (room === null) {
|
|
80
117
|
throw new Error("RoomProvider is missing from the react tree");
|
|
81
118
|
}
|
|
82
119
|
return room;
|
|
83
120
|
}
|
|
84
121
|
function useMyPresence() {
|
|
85
122
|
const room = useRoom();
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const setPresence =
|
|
90
|
-
(patch, options) => room.updatePresence(patch, options),
|
|
91
|
-
[room]
|
|
92
|
-
);
|
|
123
|
+
const subscribe = room.events.me.subscribe;
|
|
124
|
+
const getSnapshot = room.getPresence;
|
|
125
|
+
const presence = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
126
|
+
const setPresence = room.updatePresence;
|
|
93
127
|
return [presence, setPresence];
|
|
94
128
|
}
|
|
95
129
|
function useUpdateMyPresence() {
|
|
@@ -98,10 +132,7 @@ function createRoomContext(client) {
|
|
|
98
132
|
function useOthers(selector, isEqual) {
|
|
99
133
|
const room = useRoom();
|
|
100
134
|
const subscribe = room.events.others.subscribe;
|
|
101
|
-
const getSnapshot =
|
|
102
|
-
() => room.getOthers(),
|
|
103
|
-
[room]
|
|
104
|
-
);
|
|
135
|
+
const getSnapshot = room.getOthers;
|
|
105
136
|
const getServerSnapshot = getEmptyOthers;
|
|
106
137
|
return _withselector.useSyncExternalStoreWithSelector.call(void 0,
|
|
107
138
|
subscribe,
|
|
@@ -111,9 +142,77 @@ function createRoomContext(client) {
|
|
|
111
142
|
isEqual
|
|
112
143
|
);
|
|
113
144
|
}
|
|
145
|
+
function useConnectionIds() {
|
|
146
|
+
return useOthers(connectionIdSelector, _client.shallow);
|
|
147
|
+
}
|
|
148
|
+
function useOthersWithData(itemSelector, itemIsEqual) {
|
|
149
|
+
const wrappedSelector = React2.useCallback(
|
|
150
|
+
(others) => others.map((other) => ({
|
|
151
|
+
connectionId: other.connectionId,
|
|
152
|
+
data: itemSelector(other)
|
|
153
|
+
})),
|
|
154
|
+
[itemSelector]
|
|
155
|
+
);
|
|
156
|
+
const wrappedIsEqual = React2.useCallback(
|
|
157
|
+
(a, b) => {
|
|
158
|
+
const eq = itemIsEqual != null ? itemIsEqual : Object.is;
|
|
159
|
+
return a.length === b.length && a.every((atuple, index) => {
|
|
160
|
+
const btuple = b[index];
|
|
161
|
+
return atuple.connectionId === btuple.connectionId && eq(atuple.data, btuple.data);
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
[itemIsEqual]
|
|
165
|
+
);
|
|
166
|
+
return useOthers(wrappedSelector, wrappedIsEqual);
|
|
167
|
+
}
|
|
168
|
+
const sentinel = Symbol();
|
|
169
|
+
function useOther(connectionId, selector, isEqual) {
|
|
170
|
+
const _useCallback = React2.useCallback;
|
|
171
|
+
const _useOthers = useOthers;
|
|
172
|
+
if (selector === void 0) {
|
|
173
|
+
const selector2 = _useCallback(
|
|
174
|
+
(others) => others.find((other2) => other2.connectionId === connectionId),
|
|
175
|
+
[connectionId]
|
|
176
|
+
);
|
|
177
|
+
const other = _useOthers(selector2, _client.shallow);
|
|
178
|
+
if (other === void 0) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`No such other user with connection id ${connectionId} exists`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return other;
|
|
184
|
+
} else {
|
|
185
|
+
const wrappedSelector = _useCallback(
|
|
186
|
+
(others) => {
|
|
187
|
+
const other2 = others.find(
|
|
188
|
+
(other3) => other3.connectionId === connectionId
|
|
189
|
+
);
|
|
190
|
+
return other2 !== void 0 ? selector(other2) : sentinel;
|
|
191
|
+
},
|
|
192
|
+
[connectionId, selector]
|
|
193
|
+
);
|
|
194
|
+
const wrappedIsEqual = _useCallback(
|
|
195
|
+
(prev, curr) => {
|
|
196
|
+
if (prev === sentinel || curr === sentinel) {
|
|
197
|
+
return prev === curr;
|
|
198
|
+
}
|
|
199
|
+
const eq = isEqual != null ? isEqual : Object.is;
|
|
200
|
+
return eq(prev, curr);
|
|
201
|
+
},
|
|
202
|
+
[isEqual]
|
|
203
|
+
);
|
|
204
|
+
const other = _useOthers(wrappedSelector, wrappedIsEqual);
|
|
205
|
+
if (other === sentinel) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`No such other user with connection id ${connectionId} exists`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return other;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
114
213
|
function useBroadcastEvent() {
|
|
115
214
|
const room = useRoom();
|
|
116
|
-
return
|
|
215
|
+
return React2.useCallback(
|
|
117
216
|
(event, options = { shouldQueueEventIfNotReady: false }) => {
|
|
118
217
|
room.broadcastEvent(event, options);
|
|
119
218
|
},
|
|
@@ -122,22 +221,22 @@ function createRoomContext(client) {
|
|
|
122
221
|
}
|
|
123
222
|
function useErrorListener(callback) {
|
|
124
223
|
const room = useRoom();
|
|
125
|
-
const savedCallback =
|
|
126
|
-
|
|
224
|
+
const savedCallback = React2.useRef(callback);
|
|
225
|
+
React2.useEffect(() => {
|
|
127
226
|
savedCallback.current = callback;
|
|
128
227
|
});
|
|
129
|
-
|
|
228
|
+
React2.useEffect(
|
|
130
229
|
() => room.events.error.subscribe((e) => savedCallback.current(e)),
|
|
131
230
|
[room]
|
|
132
231
|
);
|
|
133
232
|
}
|
|
134
233
|
function useEventListener(callback) {
|
|
135
234
|
const room = useRoom();
|
|
136
|
-
const savedCallback =
|
|
137
|
-
|
|
235
|
+
const savedCallback = React2.useRef(callback);
|
|
236
|
+
React2.useEffect(() => {
|
|
138
237
|
savedCallback.current = callback;
|
|
139
238
|
});
|
|
140
|
-
|
|
239
|
+
React2.useEffect(() => {
|
|
141
240
|
const listener = (eventData) => {
|
|
142
241
|
savedCallback.current(eventData);
|
|
143
242
|
};
|
|
@@ -146,7 +245,7 @@ function createRoomContext(client) {
|
|
|
146
245
|
}
|
|
147
246
|
function useSelf(maybeSelector, isEqual) {
|
|
148
247
|
const room = useRoom();
|
|
149
|
-
const subscribe =
|
|
248
|
+
const subscribe = React2.useCallback(
|
|
150
249
|
(onChange) => {
|
|
151
250
|
const unsub1 = room.events.me.subscribe(onChange);
|
|
152
251
|
const unsub2 = room.events.connection.subscribe(onChange);
|
|
@@ -159,11 +258,11 @@ function createRoomContext(client) {
|
|
|
159
258
|
);
|
|
160
259
|
const getSnapshot = room.getSelf;
|
|
161
260
|
const selector = maybeSelector != null ? maybeSelector : identity;
|
|
162
|
-
const wrappedSelector =
|
|
261
|
+
const wrappedSelector = React2.useCallback(
|
|
163
262
|
(me) => me !== null ? selector(me) : null,
|
|
164
263
|
[selector]
|
|
165
264
|
);
|
|
166
|
-
const getServerSnapshot =
|
|
265
|
+
const getServerSnapshot = React2.useCallback(() => null, []);
|
|
167
266
|
return _withselector.useSyncExternalStoreWithSelector.call(void 0,
|
|
168
267
|
subscribe,
|
|
169
268
|
getSnapshot,
|
|
@@ -176,14 +275,8 @@ function createRoomContext(client) {
|
|
|
176
275
|
const room = useRoom();
|
|
177
276
|
const subscribe = room.events.storageDidLoad.subscribeOnce;
|
|
178
277
|
const getSnapshot = room.getStorageSnapshot;
|
|
179
|
-
const getServerSnapshot =
|
|
180
|
-
|
|
181
|
-
return _withselector.useSyncExternalStoreWithSelector.call(void 0,
|
|
182
|
-
subscribe,
|
|
183
|
-
getSnapshot,
|
|
184
|
-
getServerSnapshot,
|
|
185
|
-
selector
|
|
186
|
-
);
|
|
278
|
+
const getServerSnapshot = React2.useCallback(() => null, []);
|
|
279
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
187
280
|
}
|
|
188
281
|
function useStorageRoot() {
|
|
189
282
|
return [useMutableStorageRoot()];
|
|
@@ -199,21 +292,15 @@ function createRoomContext(client) {
|
|
|
199
292
|
}
|
|
200
293
|
function useCanUndo() {
|
|
201
294
|
const room = useRoom();
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
[room]
|
|
206
|
-
);
|
|
207
|
-
return canUndo;
|
|
295
|
+
const subscribe = room.events.history.subscribe;
|
|
296
|
+
const canUndo = room.history.canUndo;
|
|
297
|
+
return useSyncExternalStore(subscribe, canUndo, canUndo);
|
|
208
298
|
}
|
|
209
299
|
function useCanRedo() {
|
|
210
300
|
const room = useRoom();
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
[room]
|
|
215
|
-
);
|
|
216
|
-
return canRedo;
|
|
301
|
+
const subscribe = room.events.history.subscribe;
|
|
302
|
+
const canRedo = room.history.canRedo;
|
|
303
|
+
return useSyncExternalStore(subscribe, canRedo, canRedo);
|
|
217
304
|
}
|
|
218
305
|
function useBatch() {
|
|
219
306
|
return useRoom().batch;
|
|
@@ -222,8 +309,8 @@ function createRoomContext(client) {
|
|
|
222
309
|
const room = useRoom();
|
|
223
310
|
const root = useMutableStorageRoot();
|
|
224
311
|
const rerender = useRerender();
|
|
225
|
-
|
|
226
|
-
if (root
|
|
312
|
+
React2.useEffect(() => {
|
|
313
|
+
if (root === null) {
|
|
227
314
|
return;
|
|
228
315
|
}
|
|
229
316
|
let liveValue = root.get(key);
|
|
@@ -253,7 +340,7 @@ function createRoomContext(client) {
|
|
|
253
340
|
unsubscribeCrdt();
|
|
254
341
|
};
|
|
255
342
|
}, [root, room, key, rerender]);
|
|
256
|
-
if (root
|
|
343
|
+
if (root === null) {
|
|
257
344
|
return null;
|
|
258
345
|
} else {
|
|
259
346
|
return root.get(key);
|
|
@@ -263,15 +350,15 @@ function createRoomContext(client) {
|
|
|
263
350
|
const room = useRoom();
|
|
264
351
|
const rootOrNull = useMutableStorageRoot();
|
|
265
352
|
const selector = maybeSelector != null ? maybeSelector : identity;
|
|
266
|
-
const wrappedSelector =
|
|
353
|
+
const wrappedSelector = React2.useCallback(
|
|
267
354
|
(rootOrNull2) => rootOrNull2 !== null ? selector(rootOrNull2) : null,
|
|
268
355
|
[selector]
|
|
269
356
|
);
|
|
270
|
-
const subscribe =
|
|
357
|
+
const subscribe = React2.useCallback(
|
|
271
358
|
(onStoreChange) => rootOrNull !== null ? room.subscribe(rootOrNull, onStoreChange, { isDeep: true }) : noop,
|
|
272
359
|
[room, rootOrNull]
|
|
273
360
|
);
|
|
274
|
-
const getSnapshot =
|
|
361
|
+
const getSnapshot = React2.useCallback(() => {
|
|
275
362
|
if (rootOrNull === null) {
|
|
276
363
|
return null;
|
|
277
364
|
} else {
|
|
@@ -280,7 +367,7 @@ function createRoomContext(client) {
|
|
|
280
367
|
return imm;
|
|
281
368
|
}
|
|
282
369
|
}, [rootOrNull]);
|
|
283
|
-
const getServerSnapshot =
|
|
370
|
+
const getServerSnapshot = React2.useCallback(() => null, []);
|
|
284
371
|
return _withselector.useSyncExternalStoreWithSelector.call(void 0,
|
|
285
372
|
subscribe,
|
|
286
373
|
getSnapshot,
|
|
@@ -318,31 +405,13 @@ function createRoomContext(client) {
|
|
|
318
405
|
}
|
|
319
406
|
function useMutation(callback, deps) {
|
|
320
407
|
const room = useRoom();
|
|
321
|
-
|
|
322
|
-
const setMyPresence = room.updatePresence;
|
|
323
|
-
return React.useMemo(
|
|
408
|
+
return React2.useMemo(
|
|
324
409
|
() => {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
setMyPresence
|
|
329
|
-
};
|
|
330
|
-
return (...args) => {
|
|
331
|
-
let rv;
|
|
332
|
-
room.batch(() => {
|
|
333
|
-
rv = callback(mutationCtx, ...args);
|
|
334
|
-
});
|
|
335
|
-
return rv;
|
|
336
|
-
};
|
|
337
|
-
} else {
|
|
338
|
-
return () => {
|
|
339
|
-
throw new Error(
|
|
340
|
-
"Mutation cannot be called while Liveblocks Storage has not loaded yet"
|
|
341
|
-
);
|
|
342
|
-
};
|
|
343
|
-
}
|
|
410
|
+
return (...args) => room.batch(
|
|
411
|
+
() => callback(makeMutationContext(room), ...args)
|
|
412
|
+
);
|
|
344
413
|
},
|
|
345
|
-
|
|
414
|
+
[room, ...deps]
|
|
346
415
|
);
|
|
347
416
|
}
|
|
348
417
|
function useStorageSuspense(selector, isEqual) {
|
|
@@ -366,36 +435,82 @@ function createRoomContext(client) {
|
|
|
366
435
|
isEqual
|
|
367
436
|
);
|
|
368
437
|
}
|
|
438
|
+
function useOthersWithDataSuspense(itemSelector, itemIsEqual) {
|
|
439
|
+
useSuspendUntilPresenceLoaded();
|
|
440
|
+
return useOthersWithData(itemSelector, itemIsEqual);
|
|
441
|
+
}
|
|
442
|
+
function useOtherSuspense(connectionId, selector, isEqual) {
|
|
443
|
+
useSuspendUntilPresenceLoaded();
|
|
444
|
+
return useOther(
|
|
445
|
+
connectionId,
|
|
446
|
+
selector,
|
|
447
|
+
isEqual
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
function useLegacyKeySuspense(key) {
|
|
451
|
+
useSuspendUntilStorageLoaded();
|
|
452
|
+
return useLegacyKey(key);
|
|
453
|
+
}
|
|
369
454
|
return {
|
|
455
|
+
RoomContext,
|
|
370
456
|
RoomProvider,
|
|
457
|
+
useRoom,
|
|
371
458
|
useBatch,
|
|
372
459
|
useBroadcastEvent,
|
|
373
|
-
useCanRedo,
|
|
374
|
-
useCanUndo,
|
|
375
460
|
useErrorListener,
|
|
376
461
|
useEventListener,
|
|
377
462
|
useHistory,
|
|
378
|
-
|
|
379
|
-
useOthers,
|
|
463
|
+
useUndo,
|
|
380
464
|
useRedo,
|
|
381
|
-
|
|
382
|
-
|
|
465
|
+
useCanRedo,
|
|
466
|
+
useCanUndo,
|
|
467
|
+
useList: useLegacyKey,
|
|
468
|
+
useMap: useLegacyKey,
|
|
469
|
+
useObject: useLegacyKey,
|
|
383
470
|
useStorageRoot,
|
|
384
471
|
useStorage,
|
|
385
|
-
|
|
472
|
+
useSelf,
|
|
473
|
+
useMyPresence,
|
|
386
474
|
useUpdateMyPresence,
|
|
475
|
+
useOthers,
|
|
476
|
+
useOthersWithData,
|
|
477
|
+
useConnectionIds,
|
|
478
|
+
useOther,
|
|
387
479
|
useMutation,
|
|
388
|
-
useList: useLegacyKey,
|
|
389
|
-
useMap: useLegacyKey,
|
|
390
|
-
useObject: useLegacyKey,
|
|
391
|
-
RoomContext,
|
|
392
480
|
suspense: {
|
|
481
|
+
RoomContext,
|
|
482
|
+
RoomProvider,
|
|
483
|
+
useRoom,
|
|
484
|
+
useBatch,
|
|
485
|
+
useBroadcastEvent,
|
|
486
|
+
useErrorListener,
|
|
487
|
+
useEventListener,
|
|
488
|
+
useHistory,
|
|
489
|
+
useUndo,
|
|
490
|
+
useRedo,
|
|
491
|
+
useCanRedo,
|
|
492
|
+
useCanUndo,
|
|
493
|
+
useList: useLegacyKeySuspense,
|
|
494
|
+
useMap: useLegacyKeySuspense,
|
|
495
|
+
useObject: useLegacyKeySuspense,
|
|
496
|
+
useStorageRoot,
|
|
393
497
|
useStorage: useStorageSuspense,
|
|
394
498
|
useSelf: useSelfSuspense,
|
|
395
|
-
|
|
499
|
+
useMyPresence,
|
|
500
|
+
useUpdateMyPresence,
|
|
501
|
+
useOthers: useOthersSuspense,
|
|
502
|
+
useOthersWithData: useOthersWithDataSuspense,
|
|
503
|
+
useConnectionIds,
|
|
504
|
+
useOther: useOtherSuspense,
|
|
505
|
+
useMutation
|
|
396
506
|
}
|
|
397
507
|
};
|
|
398
508
|
}
|
|
399
509
|
|
|
510
|
+
// src/index.ts
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
400
515
|
|
|
401
|
-
exports.createRoomContext = createRoomContext;
|
|
516
|
+
exports.ClientSideSuspense = ClientSideSuspense; exports.createRoomContext = createRoomContext; exports.shallow = _client.shallow;
|
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liveblocks/react",
|
|
3
|
-
"version": "0.18.0-
|
|
3
|
+
"version": "0.18.0-beta3",
|
|
4
4
|
"description": "A set of React hooks and providers to use Liveblocks declaratively.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.mjs",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"use-sync-external-store": "^1.2.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@liveblocks/client": "0.18.0-
|
|
30
|
+
"@liveblocks/client": "0.18.0-beta3",
|
|
31
31
|
"react": "^16.14.0 || ^17 || ^18"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|