@pluv/react 0.38.13 → 0.39.0

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/dist/index.mjs CHANGED
@@ -1,14 +1,136 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+ var __async = (__this, __arguments, generator) => {
21
+ return new Promise((resolve, reject) => {
22
+ var fulfilled = (value) => {
23
+ try {
24
+ step(generator.next(value));
25
+ } catch (e) {
26
+ reject(e);
27
+ }
28
+ };
29
+ var rejected = (value) => {
30
+ try {
31
+ step(generator.throw(value));
32
+ } catch (e) {
33
+ reject(e);
34
+ }
35
+ };
36
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
37
+ step((generator = generator.apply(__this, __arguments)).next());
38
+ });
39
+ };
40
+
1
41
  // src/createBundle.tsx
2
42
  import { MockedRoom } from "@pluv/client";
43
+ import fastDeepEqual2 from "fast-deep-equal";
44
+ import { createContext, memo, useCallback as useCallback4, useContext, useEffect as useEffect3, useMemo as useMemo2, useState as useState4 } from "react";
45
+
46
+ // src/internal/hooks/useAsyncQueue.ts
47
+ import { useCallback, useMemo, useRef } from "react";
48
+ var useAsyncQueue = () => {
49
+ const queueRef = useRef(Promise.resolve(void 0));
50
+ const push = useCallback((promise) => __async(void 0, null, function* () {
51
+ queueRef.current = queueRef.current.then(() => __async(void 0, null, function* () {
52
+ return yield promise;
53
+ }));
54
+ return yield queueRef.current;
55
+ }), []);
56
+ return useMemo(() => ({ push }), [push]);
57
+ };
58
+
59
+ // src/internal/hooks/useDeepAsyncMemo.ts
60
+ import { useEffect, useRef as useRef2, useState } from "react";
3
61
  import fastDeepEqual from "fast-deep-equal";
4
- import { createContext, memo, useCallback as useCallback3, useContext, useEffect as useEffect2, useMemo, useState as useState3 } from "react";
62
+ var useDeepAsyncMemo = (factory, isEqual = fastDeepEqual) => {
63
+ const [state, setState] = useState({
64
+ isInitialized: false,
65
+ isLoading: true,
66
+ value: void 0,
67
+ error: null
68
+ });
69
+ const lastValueRef = useRef2(void 0);
70
+ const hasValueRef = useRef2(false);
71
+ const inflightRef = useRef2(null);
72
+ const isMountedRef = useRef2(true);
73
+ useEffect(() => {
74
+ isMountedRef.current = true;
75
+ const promise = factory();
76
+ inflightRef.current = promise;
77
+ setState((prev) => {
78
+ return __spreadProps(__spreadValues({}, prev), {
79
+ isLoading: true,
80
+ error: null
81
+ });
82
+ });
83
+ promise.then((result) => {
84
+ if (!isMountedRef.current || inflightRef.current !== promise) return;
85
+ const isSame = hasValueRef.current && isEqual(lastValueRef.current, result);
86
+ if (isSame) {
87
+ setState((prev) => {
88
+ return __spreadProps(__spreadValues({}, prev), {
89
+ isInitialized: true,
90
+ isLoading: false
91
+ });
92
+ });
93
+ return;
94
+ }
95
+ lastValueRef.current = result;
96
+ hasValueRef.current = true;
97
+ setState({
98
+ isInitialized: true,
99
+ isLoading: false,
100
+ value: result,
101
+ error: null
102
+ });
103
+ }).catch((err) => {
104
+ if (!isMountedRef.current || inflightRef.current !== promise) return;
105
+ setState({
106
+ isInitialized: hasValueRef.current,
107
+ isLoading: false,
108
+ value: void 0,
109
+ error: err instanceof Error ? err : new Error("Unknown error")
110
+ });
111
+ });
112
+ return () => {
113
+ isMountedRef.current = false;
114
+ };
115
+ }, [
116
+ /**
117
+ * !HACK
118
+ * @description We're only going to use the `isEqual` function to determine if this hook
119
+ * should re-run. This is basically our hook's version of a dependency array
120
+ * @date April 13, 2025
121
+ */
122
+ // factory,
123
+ isEqual
124
+ ]);
125
+ return state;
126
+ };
5
127
 
6
128
  // src/internal/hooks/useNoSsr.ts
7
- import { useCallback, useEffect, useState } from "react";
129
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
8
130
  var useNoSsr = () => {
9
- const [didMount, setDidMount] = useState(false);
10
- useEffect(() => setDidMount(typeof window !== "undefined"), []);
11
- const noSsr = useCallback(
131
+ const [didMount, setDidMount] = useState2(false);
132
+ useEffect2(() => setDidMount(typeof window !== "undefined"), []);
133
+ const noSsr = useCallback2(
12
134
  (value, fallback = null) => {
13
135
  return didMount ? typeof value !== "function" ? value : value() : fallback;
14
136
  },
@@ -18,16 +140,16 @@ var useNoSsr = () => {
18
140
  };
19
141
 
20
142
  // src/internal/hooks/useRerender.ts
21
- import { useCallback as useCallback2, useState as useState2 } from "react";
143
+ import { useCallback as useCallback3, useState as useState3 } from "react";
22
144
  var useRerender = () => {
23
- const [, setCount] = useState2(0);
24
- return useCallback2(() => {
145
+ const [, setCount] = useState3(0);
146
+ return useCallback3(() => {
25
147
  setCount((oldCount) => (oldCount + 1) % Number.MAX_SAFE_INTEGER);
26
148
  }, []);
27
149
  };
28
150
 
29
151
  // src/internal/hooks/useSyncExternalStoreWithSelector.ts
30
- import { useRef, useSyncExternalStore } from "react";
152
+ import { useRef as useRef3, useSyncExternalStore } from "react";
31
153
 
32
154
  // src/internal/utils/identity.ts
33
155
  var identity = (x) => x;
@@ -43,7 +165,7 @@ var shallowEqual = (a, b) => a === b;
43
165
  // src/internal/hooks/useSyncExternalStoreWithSelector.ts
44
166
  var useSyncExternalStoreWithSelector = (subscribe, getClientSnapshot, getServerSnapshot, selector, isEqual) => {
45
167
  const noSsr = useNoSsr();
46
- const cacheRef = useRef(null);
168
+ const cacheRef = useRef3(null);
47
169
  const _getServerSnapshot = getServerSnapshot ? () => {
48
170
  const data = getServerSnapshot();
49
171
  const snapshot = selector(data);
@@ -71,10 +193,10 @@ import { jsx } from "react/jsx-runtime";
71
193
  var createBundle = (client, options = {}) => {
72
194
  const PluvContext = createContext(null);
73
195
  const PluvRoomContext = createContext(null);
74
- const MockedRoomContext = createContext({ room: null });
196
+ const MockedRoomContext = createContext(null);
75
197
  const MockedRoomProvider = memo((props) => {
76
198
  const { children, events, initialPresence, initialStorage, room: _room } = props;
77
- const [room] = useState3(() => {
199
+ const [room] = useState4(() => {
78
200
  var _a;
79
201
  return new MockedRoom(_room, {
80
202
  events,
@@ -82,14 +204,28 @@ var createBundle = (client, options = {}) => {
82
204
  initialStorage: typeof initialStorage === "function" ? (_a = client._defs.initialStorage) == null ? void 0 : _a.getFactory(initialStorage) : client._defs.initialStorage
83
205
  });
84
206
  });
85
- return /* @__PURE__ */ jsx(MockedRoomContext.Provider, { value: { room }, children: /* @__PURE__ */ jsx(PluvRoomContext.Provider, { value: room, children }) });
207
+ return /* @__PURE__ */ jsx(MockedRoomContext.Provider, { value: room, children: /* @__PURE__ */ jsx(PluvRoomContext.Provider, { value: room, children }) });
86
208
  });
87
209
  MockedRoomProvider.displayName = "MockedRoomProvider";
88
210
  const PluvRoomProvider = memo((props) => {
89
- const { children, debug, initialPresence, initialStorage, metadata, onAuthorizationFail, room: _room } = props;
211
+ const {
212
+ children,
213
+ connect = true,
214
+ debug,
215
+ initialPresence,
216
+ initialStorage,
217
+ metadata,
218
+ onAuthorizationFail,
219
+ room: _room
220
+ } = props;
221
+ const queue = useAsyncQueue();
90
222
  const rerender = useRerender();
91
- const { room: mockedRoom } = useContext(MockedRoomContext);
92
- const [room] = useState3(() => {
223
+ const mockedRoom = useContext(MockedRoomContext);
224
+ const resolvedMeta = useDeepAsyncMemo(() => __async(void 0, null, function* () {
225
+ const resolved = yield Promise.resolve(typeof metadata === "function" ? metadata() : metadata);
226
+ return !!room.metadata ? room.metadata.parse(metadata) : resolved;
227
+ }));
228
+ const [room] = useState4(() => {
93
229
  var _a;
94
230
  return client.createRoom(_room, {
95
231
  addons: options.addons,
@@ -101,7 +237,7 @@ var createBundle = (client, options = {}) => {
101
237
  router: options.router
102
238
  });
103
239
  });
104
- useEffect2(() => {
240
+ useEffect3(() => {
105
241
  const unsubscribe = room.subscribe("connection", () => {
106
242
  rerender();
107
243
  });
@@ -109,12 +245,33 @@ var createBundle = (client, options = {}) => {
109
245
  unsubscribe();
110
246
  };
111
247
  }, [rerender, room]);
112
- useEffect2(() => {
113
- client.enter(room);
248
+ useEffect3(() => {
249
+ const leaveRoom = () => __async(void 0, null, function* () {
250
+ yield queue.push(
251
+ client.leave(room).catch((error) => {
252
+ console.error(error);
253
+ })
254
+ );
255
+ });
256
+ if (!connect) {
257
+ leaveRoom();
258
+ return;
259
+ }
260
+ if (!resolvedMeta.isInitialized) {
261
+ leaveRoom();
262
+ return;
263
+ }
264
+ const resolved = resolvedMeta.value;
265
+ queue.push(
266
+ client.enter(room, ...[{ metadata: resolved }]).catch((error) => __async(void 0, null, function* () {
267
+ console.error(error);
268
+ yield leaveRoom();
269
+ }))
270
+ );
114
271
  return () => {
115
- client.leave(room);
272
+ leaveRoom();
116
273
  };
117
- }, [room]);
274
+ }, [connect, queue, resolvedMeta.isInitialized, resolvedMeta.value, room]);
118
275
  return /* @__PURE__ */ jsx(PluvRoomContext.Provider, { value: mockedRoom != null ? mockedRoom : room, children });
119
276
  });
120
277
  PluvRoomProvider.displayName = "PluvRoomProvider";
@@ -131,13 +288,13 @@ var createBundle = (client, options = {}) => {
131
288
  };
132
289
  const useBroadcast = () => {
133
290
  const room = useRoom();
134
- const broadcast = useCallback3(
291
+ const broadcast = useCallback4(
135
292
  (event2, data) => {
136
293
  room.broadcast(event2, data);
137
294
  },
138
295
  [room]
139
296
  );
140
- return useMemo(() => {
297
+ return useMemo2(() => {
141
298
  return new Proxy(broadcast, {
142
299
  get(fn, prop) {
143
300
  return (data) => {
@@ -149,35 +306,35 @@ var createBundle = (client, options = {}) => {
149
306
  };
150
307
  const useCanRedo = () => {
151
308
  const room = useRoom();
152
- const subscribe = useCallback3((onStoreChange) => room.storageRoot(onStoreChange), [room]);
153
- const getSnapshot = useCallback3(() => room.canRedo(), [room]);
309
+ const subscribe = useCallback4((onStoreChange) => room.storageRoot(onStoreChange), [room]);
310
+ const getSnapshot = useCallback4(() => room.canRedo(), [room]);
154
311
  const canRedo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
155
312
  return canRedo;
156
313
  };
157
314
  const useCanUndo = () => {
158
315
  const room = useRoom();
159
- const subscribe = useCallback3((onStoreChange) => room.storageRoot(onStoreChange), [room]);
160
- const getSnapshot = useCallback3(() => room.canUndo(), [room]);
316
+ const subscribe = useCallback4((onStoreChange) => room.storageRoot(onStoreChange), [room]);
317
+ const getSnapshot = useCallback4(() => room.canUndo(), [room]);
161
318
  const canUndo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
162
319
  return canUndo;
163
320
  };
164
321
  const useConnection = (selector = identity, options2) => {
165
322
  var _a;
166
323
  const room = useRoom();
167
- const subscribe = useCallback3(
324
+ const subscribe = useCallback4(
168
325
  (onStoreChange) => {
169
326
  return room.subscribe("connection", onStoreChange);
170
327
  },
171
328
  [room]
172
329
  );
173
330
  const getSnapshot = room.getConnection;
174
- const _selector = useCallback3((snapshot) => selector(snapshot), [selector]);
331
+ const _selector = useCallback4((snapshot) => selector(snapshot), [selector]);
175
332
  return useSyncExternalStoreWithSelector(
176
333
  subscribe,
177
334
  getSnapshot,
178
335
  getSnapshot,
179
336
  _selector,
180
- (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual
337
+ (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual2
181
338
  );
182
339
  };
183
340
  const useDoc = () => {
@@ -186,7 +343,7 @@ var createBundle = (client, options = {}) => {
186
343
  };
187
344
  const useEvent = (type, callback) => {
188
345
  const room = useRoom();
189
- useEffect2(() => {
346
+ useEffect3(() => {
190
347
  const unsubscribe = room.event(type, callback);
191
348
  return () => {
192
349
  unsubscribe();
@@ -207,34 +364,34 @@ var createBundle = (client, options = {}) => {
207
364
  const useMyPresence = (selector = identity, options2) => {
208
365
  var _a;
209
366
  const room = useRoom();
210
- const subscribe = useCallback3(
367
+ const subscribe = useCallback4(
211
368
  (onStoreChange) => {
212
369
  return room.subscribe("my-presence", onStoreChange);
213
370
  },
214
371
  [room]
215
372
  );
216
373
  const getSnapshot = room.getMyPresence;
217
- const _selector = useCallback3((snapshot) => selector(snapshot), [selector]);
374
+ const _selector = useCallback4((snapshot) => selector(snapshot), [selector]);
218
375
  const myPresence = useSyncExternalStoreWithSelector(
219
376
  subscribe,
220
377
  getSnapshot,
221
378
  getSnapshot,
222
379
  _selector,
223
- (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual
380
+ (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual2
224
381
  );
225
382
  return [myPresence, room.updateMyPresence];
226
383
  };
227
384
  const useMyself = (selector = identity, options2) => {
228
385
  var _a;
229
386
  const room = useRoom();
230
- const subscribe = useCallback3(
387
+ const subscribe = useCallback4(
231
388
  (onStoreChange) => {
232
389
  return room.subscribe("myself", onStoreChange);
233
390
  },
234
391
  [room]
235
392
  );
236
393
  const getSnapshot = room.getMyself;
237
- const _selector = useCallback3(
394
+ const _selector = useCallback4(
238
395
  (snapshot) => {
239
396
  return !snapshot ? null : selector(snapshot);
240
397
  },
@@ -245,20 +402,20 @@ var createBundle = (client, options = {}) => {
245
402
  getSnapshot,
246
403
  getSnapshot,
247
404
  _selector,
248
- (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual
405
+ (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual2
249
406
  );
250
407
  };
251
408
  const useOther = (connectionId, selector = identity, options2) => {
252
409
  var _a;
253
410
  const room = useRoom();
254
- const subscribe = useCallback3(
411
+ const subscribe = useCallback4(
255
412
  (onStoreChange) => {
256
413
  return room.other(connectionId, onStoreChange);
257
414
  },
258
415
  [room, connectionId]
259
416
  );
260
- const getSnapshot = useCallback3(() => room.getOther(connectionId), [room, connectionId]);
261
- const _selector = useCallback3(
417
+ const getSnapshot = useCallback4(() => room.getOther(connectionId), [room, connectionId]);
418
+ const _selector = useCallback4(
262
419
  (snapshot) => {
263
420
  return !snapshot ? null : selector(snapshot);
264
421
  },
@@ -269,13 +426,13 @@ var createBundle = (client, options = {}) => {
269
426
  getSnapshot,
270
427
  getSnapshot,
271
428
  _selector,
272
- (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual
429
+ (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual2
273
430
  );
274
431
  };
275
432
  const useOthers = (selector = identity, options2) => {
276
433
  var _a;
277
434
  const room = useRoom();
278
- const subscribe = useCallback3(
435
+ const subscribe = useCallback4(
279
436
  (onStoreChange) => {
280
437
  return room.subscribe("others", onStoreChange);
281
438
  },
@@ -291,7 +448,7 @@ var createBundle = (client, options = {}) => {
291
448
  if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
292
449
  return shallowArrayEqual(a, b);
293
450
  }
294
- return fastDeepEqual(a, b);
451
+ return fastDeepEqual2(a, b);
295
452
  }
296
453
  );
297
454
  };
@@ -306,11 +463,11 @@ var createBundle = (client, options = {}) => {
306
463
  room.subscribe("storage-loaded", () => {
307
464
  rerender();
308
465
  });
309
- const subscribe = useCallback3((onStoreChange) => room.storage(key, onStoreChange), [key, room]);
310
- const getSnapshot = useCallback3(() => {
466
+ const subscribe = useCallback4((onStoreChange) => room.storage(key, onStoreChange), [key, room]);
467
+ const getSnapshot = useCallback4(() => {
311
468
  return room.getStorageJson(key);
312
469
  }, [key, room]);
313
- const _selector = useCallback3(
470
+ const _selector = useCallback4(
314
471
  (snapshot) => {
315
472
  return snapshot === null ? null : selector(snapshot);
316
473
  },
@@ -321,7 +478,7 @@ var createBundle = (client, options = {}) => {
321
478
  getSnapshot,
322
479
  getSnapshot,
323
480
  _selector,
324
- (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual
481
+ (_a = options2 == null ? void 0 : options2.isEqual) != null ? _a : fastDeepEqual2
325
482
  );
326
483
  const sharedType = (_b = room.getStorage(key)) != null ? _b : null;
327
484
  return [data, sharedType];
@@ -0,0 +1,4 @@
1
+ import { reactConfig } from "eslint-config-pluv/react";
2
+
3
+ /** @type {import("eslint").Linter.Config} */
4
+ export default reactConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/react",
3
- "version": "0.38.13",
3
+ "version": "0.39.0",
4
4
  "description": "@pluv/client React.js bindings",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -18,27 +18,27 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "fast-deep-equal": "^3.1.3",
21
- "@pluv/client": "^0.38.13",
22
- "@pluv/crdt": "^0.38.13",
23
- "@pluv/types": "^0.38.13"
21
+ "@pluv/client": "^0.39.0",
22
+ "@pluv/types": "^0.39.0",
23
+ "@pluv/crdt": "^0.39.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "@types/react": "^18.0.0 || ^19.0.0",
27
27
  "react": "^18.0.0 || ^19.0.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/react": "^19.1.0",
31
- "eslint": "^8.57.1",
30
+ "@types/react": "^19.1.1",
31
+ "eslint": "^9.24.0",
32
32
  "react": "^19.1.0",
33
33
  "tsup": "^8.4.0",
34
34
  "typescript": "^5.8.3",
35
- "@pluv/tsconfig": "^0.38.13",
36
- "eslint-config-pluv": "^0.38.13"
35
+ "@pluv/tsconfig": "^0.39.0",
36
+ "eslint-config-pluv": "^0.39.0"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsup src/index.ts --format esm,cjs --dts --external react",
40
40
  "dev": "tsup src/index.ts --format esm,cjs --watch --dts",
41
- "lint": "eslint src/**/*.ts* --fix",
41
+ "lint": "eslint src/**/*.ts* --fix --max-warnings 0",
42
42
  "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
43
43
  }
44
44
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AbstractRoom,
3
3
  CreateRoomOptions,
4
+ EnterRoomParams,
4
5
  MergeEvents,
5
6
  MockedRoomEvents,
6
7
  PluvClient,
@@ -13,11 +14,18 @@ import type {
13
14
  } from "@pluv/client";
14
15
  import { MockedRoom, PluvRoom } from "@pluv/client";
15
16
  import type { AbstractCrdtDoc, CrdtType, InferCrdtJson } from "@pluv/crdt";
16
- import type { Id, InferIOInput, InferIOOutput, IOEventMessage, IOLike, JsonObject } from "@pluv/types";
17
+ import type { Id, InferIOInput, InferIOOutput, IOEventMessage, IOLike, JsonObject, MaybePromise } from "@pluv/types";
17
18
  import fastDeepEqual from "fast-deep-equal";
18
19
  import type { Dispatch, FC, ReactNode } from "react";
19
20
  import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
20
- import { identity, shallowArrayEqual, useRerender, useSyncExternalStoreWithSelector } from "./internal";
21
+ import {
22
+ identity,
23
+ shallowArrayEqual,
24
+ useAsyncQueue,
25
+ useDeepAsyncMemo,
26
+ useRerender,
27
+ useSyncExternalStoreWithSelector,
28
+ } from "./internal";
21
29
 
22
30
  export interface PluvProviderProps {
23
31
  children?: ReactNode;
@@ -38,15 +46,18 @@ export type MockedRoomProviderProps<
38
46
  events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
39
47
  };
40
48
 
49
+ export type MetadataGetter<TMetadata extends JsonObject> = TMetadata | (() => MaybePromise<TMetadata>);
50
+
41
51
  export type PluvRoomProviderProps<
42
52
  TIO extends IOLike<any, any>,
43
53
  TMetadata extends JsonObject,
44
54
  TPresence extends JsonObject,
45
55
  TStorage extends Record<string, CrdtType<any, any>>,
46
56
  > = BaseRoomProviderProps<TPresence, TStorage> & {
57
+ connect?: boolean;
47
58
  debug?: boolean | PluvRoomDebug<TIO>;
48
59
  onAuthorizationFail?: (error: Error) => void;
49
- } & (keyof TMetadata extends never ? { metadata?: undefined } : { metadata: TMetadata });
60
+ } & (keyof TMetadata extends never ? { metadata?: undefined } : { metadata: MetadataGetter<TMetadata> });
50
61
 
51
62
  export interface SubscriptionHookOptions<T extends unknown> {
52
63
  isEqual?: (a: T, b: T) => boolean;
@@ -151,7 +162,6 @@ export const createBundle = <
151
162
  * !HACK
152
163
  * @description We'll let the context error out if client is not provided,
153
164
  * and let the users deal with it.
154
- * @author leedavidcs
155
165
  * @date October 27, 2022
156
166
  */
157
167
  const PluvContext = createContext<PluvClient<TIO, TPresence, TStorage, TMetadata>>(null as any);
@@ -160,14 +170,11 @@ export const createBundle = <
160
170
  * !HACK
161
171
  * @description We'll let the context error out if the room is not provided,
162
172
  * and let the users deal with it.
163
- * @author leedavidcs
164
173
  * @date November 11, 2022
165
174
  */
166
175
  const PluvRoomContext = createContext<AbstractRoom<TIO, TPresence, TStorage>>(null as any);
167
176
 
168
- const MockedRoomContext = createContext<{
169
- room: AbstractRoom<TIO, TPresence, TStorage> | null;
170
- }>({ room: null });
177
+ const MockedRoomContext = createContext<AbstractRoom<TIO, TPresence, TStorage> | null>(null);
171
178
 
172
179
  const MockedRoomProvider = memo<MockedRoomProviderProps<TIO, TPresence, TStorage>>((props) => {
173
180
  const { children, events, initialPresence, initialStorage, room: _room } = props;
@@ -184,7 +191,7 @@ export const createBundle = <
184
191
  });
185
192
 
186
193
  return (
187
- <MockedRoomContext.Provider value={{ room }}>
194
+ <MockedRoomContext.Provider value={room}>
188
195
  <PluvRoomContext.Provider value={room}>{children}</PluvRoomContext.Provider>
189
196
  </MockedRoomContext.Provider>
190
197
  );
@@ -193,10 +200,26 @@ export const createBundle = <
193
200
  MockedRoomProvider.displayName = "MockedRoomProvider";
194
201
 
195
202
  const PluvRoomProvider = memo<PluvRoomProviderProps<TIO, TMetadata, TPresence, TStorage>>((props) => {
196
- const { children, debug, initialPresence, initialStorage, metadata, onAuthorizationFail, room: _room } = props;
197
-
203
+ const {
204
+ children,
205
+ connect = true,
206
+ debug,
207
+ initialPresence,
208
+ initialStorage,
209
+ metadata,
210
+ onAuthorizationFail,
211
+ room: _room,
212
+ } = props;
213
+
214
+ const queue = useAsyncQueue();
198
215
  const rerender = useRerender();
199
- const { room: mockedRoom } = useContext(MockedRoomContext);
216
+ const mockedRoom = useContext(MockedRoomContext);
217
+
218
+ const resolvedMeta = useDeepAsyncMemo(async () => {
219
+ const resolved = await Promise.resolve(typeof metadata === "function" ? metadata() : metadata);
220
+
221
+ return !!room.metadata ? room.metadata.parse(metadata) : resolved;
222
+ });
200
223
 
201
224
  const [room] = useState<PluvRoom<TIO, TMetadata, TPresence, TStorage, TEvents>>(() => {
202
225
  return client.createRoom(_room, {
@@ -224,12 +247,37 @@ export const createBundle = <
224
247
  }, [rerender, room]);
225
248
 
226
249
  useEffect(() => {
227
- client.enter(room);
250
+ const leaveRoom = async (): Promise<void> => {
251
+ await queue.push(
252
+ client.leave(room).catch((error) => {
253
+ console.error(error);
254
+ }),
255
+ );
256
+ };
257
+
258
+ if (!connect) {
259
+ leaveRoom();
260
+ return;
261
+ }
262
+
263
+ if (!resolvedMeta.isInitialized) {
264
+ leaveRoom();
265
+ return;
266
+ }
267
+
268
+ const resolved = resolvedMeta.value as TMetadata;
269
+
270
+ queue.push(
271
+ client.enter(room, ...([{ metadata: resolved }] as EnterRoomParams<TMetadata>)).catch(async (error) => {
272
+ console.error(error);
273
+ await leaveRoom();
274
+ }),
275
+ );
228
276
 
229
277
  return () => {
230
- client.leave(room);
278
+ leaveRoom();
231
279
  };
232
- }, [room]);
280
+ }, [connect, queue, resolvedMeta.isInitialized, resolvedMeta.value, room]);
233
281
 
234
282
  return <PluvRoomContext.Provider value={mockedRoom ?? room}>{children}</PluvRoomContext.Provider>;
235
283
  });
@@ -1,3 +1,5 @@
1
+ export { useAsyncQueue } from "./useAsyncQueue";
2
+ export { useDeepAsyncMemo } from "./useDeepAsyncMemo";
1
3
  export { useNoSsr } from "./useNoSsr";
2
4
  export { useRerender } from "./useRerender";
3
5
  export { useSyncExternalStoreWithSelector } from "./useSyncExternalStoreWithSelector";
@@ -0,0 +1,18 @@
1
+ import { useCallback, useMemo, useRef } from "react";
2
+
3
+ /**
4
+ * @description This is to handle race conditions, so that our async operations that can
5
+ * happen between re-renders will happen sequentially.
6
+ * @date April 13, 2025
7
+ */
8
+ export const useAsyncQueue = () => {
9
+ const queueRef = useRef<Promise<void>>(Promise.resolve(undefined));
10
+
11
+ const push = useCallback(async (promise: Promise<any>) => {
12
+ queueRef.current = queueRef.current.then(async () => await promise);
13
+
14
+ return await queueRef.current;
15
+ }, []);
16
+
17
+ return useMemo(() => ({ push }), [push]);
18
+ };