@korajs/react 1.0.0-beta.11 → 1.0.0-beta.13

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/README.md CHANGED
@@ -1,70 +1,78 @@
1
1
  # @korajs/react
2
2
 
3
- React hooks and bindings for Kora.js. Reactive queries that update automatically, optimistic mutations, sync status tracking, and rich text editing support.
3
+ React bindings for Kora.js: a provider, reactive queries, mutations, sync status, rich text and
4
+ presence. Hooks use `useSyncExternalStore`, so they are safe in concurrent rendering, StrictMode and
5
+ server rendering.
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- pnpm add korajs @korajs/react
10
+ pnpm add korajs@beta @korajs/react@beta
9
11
  ```
10
12
 
11
13
  ## Usage
12
14
 
13
- ### Provider
15
+ <!-- docs-check-prelude
16
+ import { createApp, defineSchema, t } from 'korajs'
17
+ const schema = defineSchema({
18
+ version: 1,
19
+ collections: { todos: { fields: { title: t.string(), completed: t.boolean().default(false), createdAt: t.timestamp().auto() } } },
20
+ })
21
+ const app = createApp({ schema })
22
+ -->
14
23
 
15
24
  ```tsx
16
- import { KoraProvider } from '@korajs/react'
25
+ import { createKoraHooks, KoraProvider } from '@korajs/react'
17
26
 
18
- function App() {
27
+ // Hooks typed by your schema
28
+ const { useCollection, useMutation, useQuery, useSyncStatus } = createKoraHooks<typeof app>()
29
+
30
+ export function Root() {
19
31
  return (
20
- <KoraProvider app={app}>
32
+ <KoraProvider app={app} fallback={<p>Loading...</p>}>
21
33
  <TodoList />
22
34
  </KoraProvider>
23
35
  )
24
36
  }
25
- ```
26
-
27
- ### Reactive Queries
28
-
29
- ```tsx
30
- import { useQuery } from '@korajs/react'
31
37
 
32
38
  function TodoList() {
33
- const todos = useQuery(app.todos.where({ completed: false }).orderBy('createdAt'))
34
- // Always up to date. No loading state needed for local data.
35
-
36
- return todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)
37
- }
38
- ```
39
-
40
- ### Mutations
41
-
42
- ```tsx
43
- import { useMutation } from '@korajs/react'
44
-
45
- function AddTodo() {
46
- const addTodo = useMutation(app.todos.insert)
39
+ const todos = useCollection('todos')
40
+ const open = useQuery(todos.where({ completed: false }).orderBy('createdAt'))
41
+ const addTodo = useMutation(todos.insert)
42
+ const status = useSyncStatus()
47
43
 
48
- return <button onClick={() => addTodo({ title: 'New todo' })}>Add</button>
44
+ return (
45
+ <>
46
+ <button onClick={() => addTodo.mutate({ title: 'New todo' })}>Add</button>
47
+ {addTodo.error && <p role="alert">{addTodo.error.message}</p>}
48
+ <ul>
49
+ {open.map((todo) => (
50
+ <li key={todo.id}>{todo.title}</li>
51
+ ))}
52
+ </ul>
53
+ <small>
54
+ {status.status}, {status.pendingOperations} pending
55
+ </small>
56
+ </>
57
+ )
49
58
  }
50
59
  ```
51
60
 
52
- ### Sync Status
53
-
54
- ```tsx
55
- import { useSyncStatus } from '@korajs/react'
61
+ - `KoraProvider` renders `fallback` until the local database is open.
62
+ - `useQuery` renders `[]` first and the rows right after mount, then re-renders only when the
63
+ result changes. `useQueryState` returns `{ data, error, ready }`.
64
+ - `useMutation` returns `{ mutate, mutateAsync, isLoading, error, reset }`; writes land locally at
65
+ once and upload in the background.
66
+ - `useSyncStatus()` returns the full status (`status`, `pendingOperations`, `lastSyncedAt`, ...)
67
+ and re-renders only when it changes.
68
+ - `useRichText`, `usePresence`, `useCollaborators` and `AuthBoundKoraProvider` cover collaborative
69
+ editing and per-user databases.
56
70
 
57
- function SyncIndicator() {
58
- const status = useSyncStatus()
59
- // 'connected' | 'syncing' | 'synced' | 'offline' | 'error'
60
- return <span>{status}</span>
61
- }
62
- ```
71
+ ## Documentation
63
72
 
64
- All hooks use `useSyncExternalStore` for React 18+ concurrent mode safety.
73
+ [React Hooks guide](https://korajs.dev/guide/react-hooks) and the
74
+ [React API reference](https://korajs.dev/api/react).
65
75
 
66
76
  ## License
67
77
 
68
78
  MIT
69
-
70
- See the [full documentation](https://github.com/ehoneahobed/kora) for guides, API reference, and examples.
package/dist/index.cjs CHANGED
@@ -22,12 +22,15 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthBoundKoraProvider: () => AuthBoundKoraProvider,
24
24
  KoraProvider: () => KoraProvider,
25
+ classifyKoraInitializationError: () => classifyKoraInitializationError,
26
+ createKoraHooks: () => createKoraHooks,
25
27
  useApp: () => useApp,
26
28
  useCollaborators: () => useCollaborators,
27
29
  useCollection: () => useCollection,
28
30
  useMutation: () => useMutation,
29
31
  usePresence: () => usePresence,
30
32
  useQuery: () => useQuery,
33
+ useQueryState: () => useQueryState,
31
34
  useRichText: () => useRichText,
32
35
  useSyncStatus: () => useSyncStatus
33
36
  });
@@ -123,11 +126,36 @@ function useKoraContext() {
123
126
 
124
127
  // src/context/auth-bound-kora-provider.ts
125
128
  var import_react2 = require("react");
129
+ var SENSITIVE_METADATA_KEY = /token|secret|password|authorization|cookie/i;
130
+ function classifyKoraInitializationError(error) {
131
+ const candidate = error;
132
+ const message = typeof candidate?.message === "string" ? candidate.message : String(error);
133
+ const name = typeof candidate?.name === "string" ? candidate.name : "Error";
134
+ const cause = new Error(message);
135
+ cause.name = name;
136
+ if (typeof candidate?.stack === "string") cause.stack = candidate.stack;
137
+ let code = typeof candidate?.code === "string" ? candidate.code : "INITIALIZATION_FAILED";
138
+ if (code === "INITIALIZATION_FAILED" && cause.name === "QuotaExceededError") {
139
+ code = "STORAGE_QUOTA_EXCEEDED";
140
+ }
141
+ const metadata = {};
142
+ if (candidate?.context && typeof candidate.context === "object") {
143
+ for (const [key, value] of Object.entries(candidate.context)) {
144
+ if (SENSITIVE_METADATA_KEY.test(key)) continue;
145
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
146
+ metadata[key] = value;
147
+ }
148
+ }
149
+ }
150
+ return { code, name: cause.name, message: cause.message, metadata, cause };
151
+ }
126
152
  function AuthBoundKoraProvider({
127
153
  authClient,
128
154
  createApp,
129
155
  signedOut = null,
156
+ locked,
130
157
  fallback = null,
158
+ error: renderError,
131
159
  children
132
160
  }) {
133
161
  const [app, setApp] = (0, import_react2.useState)(null);
@@ -139,6 +167,8 @@ function AuthBoundKoraProvider({
139
167
  const activeUserId = (0, import_react2.useRef)(null);
140
168
  const generation = (0, import_react2.useRef)(0);
141
169
  const transition = (0, import_react2.useRef)(Promise.resolve());
170
+ const reconcileRef = (0, import_react2.useRef)(() => {
171
+ });
142
172
  (0, import_react2.useEffect)(() => {
143
173
  let disposed = false;
144
174
  const reconcile = () => {
@@ -176,10 +206,18 @@ function AuthBoundKoraProvider({
176
206
  setApp(created);
177
207
  }).catch((error) => {
178
208
  if (!disposed && currentGeneration === generation.current) {
179
- setInitError(error instanceof Error ? error : new Error(String(error)));
209
+ void authClient.resolveSyncState?.().then((failedSession) => {
210
+ if (!disposed && currentGeneration === generation.current && failedSession?.state === "authenticated") {
211
+ setInitError({
212
+ error: classifyKoraInitializationError(error),
213
+ session: { userId: failedSession.userId }
214
+ });
215
+ }
216
+ });
180
217
  }
181
218
  });
182
219
  };
220
+ reconcileRef.current = reconcile;
183
221
  reconcile();
184
222
  const unsubscribe = authClient.subscribe?.(reconcile) ?? (() => {
185
223
  });
@@ -194,14 +232,22 @@ function AuthBoundKoraProvider({
194
232
  };
195
233
  }, [authClient]);
196
234
  if (initError) {
235
+ if (renderError) {
236
+ return renderError({
237
+ error: initError.error,
238
+ session: initError.session,
239
+ retry: () => reconcileRef.current()
240
+ });
241
+ }
197
242
  return (0, import_react2.createElement)(
198
243
  "div",
199
244
  { style: { color: "red", padding: "1rem", fontFamily: "monospace" } },
200
245
  (0, import_react2.createElement)("strong", null, "Kora initialization error: "),
201
- initError.message
246
+ initError.error.message
202
247
  );
203
248
  }
204
249
  if (state.state === "signed-out" || state.state === "anonymous") return signedOut;
250
+ if (state.state === "authenticated" && state.locked) return locked ?? signedOut;
205
251
  if (!app) return fallback;
206
252
  return (0, import_react2.createElement)(KoraProvider, { app, fallback }, children);
207
253
  }
@@ -225,10 +271,17 @@ var noopSubscribe = (_onStoreChange) => {
225
271
  return () => {
226
272
  };
227
273
  };
228
- function useQuery(query, options) {
274
+ var getServerSnapshot = () => EMPTY_ARRAY;
275
+ var getNoError = () => null;
276
+ function readQueryError(queryStore) {
277
+ if (!queryStore) return null;
278
+ const channel = queryStore;
279
+ return typeof channel.getError === "function" ? channel.getError() : null;
280
+ }
281
+ function useQuerySubscription(query, options) {
229
282
  const { queryStoreCache } = useKoraContext();
230
283
  const enabled = options?.enabled !== false;
231
- const descriptorKey = JSON.stringify(query.getDescriptor());
284
+ const descriptorKey = (0, import_store2.queryKey)(query.getDescriptor());
232
285
  const queryRef = (0, import_react3.useRef)(query);
233
286
  queryRef.current = query;
234
287
  const lastSnapshotRef = (0, import_react3.useRef)(EMPTY_ARRAY);
@@ -246,122 +299,204 @@ function useQuery(query, options) {
246
299
  queryStoreCache.release(currentQuery);
247
300
  };
248
301
  }, [descriptorKey, enabled, queryStoreCache]);
302
+ const subscribe = enabled && queryStore ? queryStore.subscribe : noopSubscribe;
249
303
  const getSnapshot = (0, import_react3.useCallback)(() => {
250
304
  if (!enabled) {
251
305
  lastSnapshotRef.current = EMPTY_ARRAY;
252
306
  return lastSnapshotRef.current;
253
307
  }
254
- if (!queryStore) {
255
- return lastSnapshotRef.current;
256
- }
257
- if (!queryStore.hasSnapshot()) {
308
+ if (!queryStore || !queryStore.hasSnapshot()) {
258
309
  return lastSnapshotRef.current;
259
310
  }
260
311
  const snapshot = queryStore.getSnapshot();
261
312
  lastSnapshotRef.current = snapshot;
262
313
  return snapshot;
263
314
  }, [enabled, queryStore]);
264
- return (0, import_react3.useSyncExternalStore)(queryStore ? queryStore.subscribe : noopSubscribe, getSnapshot);
315
+ const getError = (0, import_react3.useCallback)(
316
+ () => enabled ? readQueryError(queryStore) : null,
317
+ [enabled, queryStore]
318
+ );
319
+ const data = (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot);
320
+ const error = (0, import_react3.useSyncExternalStore)(subscribe, getError, getNoError);
321
+ const ready = enabled && queryStore !== null && queryStore.hasSnapshot();
322
+ return { data, error, ready };
323
+ }
324
+ function useQuery(query, options) {
325
+ const { data, error } = useQuerySubscription(query, options);
326
+ if (error && options?.throwOnError !== false) {
327
+ throw error;
328
+ }
329
+ return data;
330
+ }
331
+ function useQueryState(query, options) {
332
+ const { data, error, ready } = useQuerySubscription(query, options);
333
+ return (0, import_react3.useMemo)(() => ({ data, error, ready }), [data, error, ready]);
265
334
  }
266
335
 
336
+ // src/hooks/create-kora-hooks.ts
337
+ var import_react7 = require("react");
338
+
267
339
  // src/hooks/use-mutation.ts
268
340
  var import_bindings = require("@korajs/core/bindings");
269
341
  var import_react5 = require("react");
270
342
 
271
343
  // src/hooks/use-controller.ts
272
344
  var import_react4 = require("react");
345
+ var ORPHAN_REAP_MS = 1e3;
273
346
  function useController(create, destroy, deps) {
274
- const controllerRef = (0, import_react4.useRef)(null);
347
+ const entryRef = (0, import_react4.useRef)(null);
275
348
  const createRef = (0, import_react4.useRef)(create);
276
349
  createRef.current = create;
277
- const [, forceRender] = (0, import_react4.useReducer)((count) => count + 1, 0);
278
- const getController = (0, import_react4.useRef)(() => {
279
- if (controllerRef.current === null) {
280
- controllerRef.current = createRef.current();
350
+ const destroyRef = (0, import_react4.useRef)(destroy);
351
+ destroyRef.current = destroy;
352
+ const [version, bumpVersion] = (0, import_react4.useReducer)((count) => count + 1, 0);
353
+ const handle = (0, import_react4.useRef)({
354
+ get: () => {
355
+ const current = entryRef.current;
356
+ if (current !== null && !current.reaped) {
357
+ return current.controller;
358
+ }
359
+ const entry = {
360
+ controller: createRef.current(),
361
+ claimed: false,
362
+ reaped: false
363
+ };
364
+ entryRef.current = entry;
365
+ setTimeout(() => {
366
+ if (!entry.claimed && !entry.reaped) {
367
+ entry.reaped = true;
368
+ destroyRef.current(entry.controller);
369
+ }
370
+ }, ORPHAN_REAP_MS);
371
+ return entry.controller;
372
+ },
373
+ peek: () => {
374
+ const current = entryRef.current;
375
+ return current?.claimed && !current.reaped ? current.controller : null;
281
376
  }
282
- return controllerRef.current;
283
377
  }).current;
284
378
  (0, import_react4.useEffect)(() => {
285
- if (controllerRef.current === null) {
286
- controllerRef.current = createRef.current();
287
- forceRender();
379
+ const current = entryRef.current;
380
+ if (current !== null && !current.reaped && !current.claimed) {
381
+ current.claimed = true;
382
+ } else if (current === null || current.reaped) {
383
+ entryRef.current = { controller: createRef.current(), claimed: true, reaped: false };
384
+ bumpVersion();
288
385
  }
289
386
  return () => {
290
- const controller = controllerRef.current;
291
- controllerRef.current = null;
292
- if (controller !== null) {
293
- destroy(controller);
387
+ const entry = entryRef.current;
388
+ entryRef.current = null;
389
+ if (entry !== null && !entry.reaped) {
390
+ entry.reaped = true;
391
+ destroyRef.current(entry.controller);
294
392
  }
295
393
  };
296
394
  }, deps);
297
- return getController;
395
+ return { get: handle.get, peek: handle.peek, version };
298
396
  }
299
397
 
300
398
  // src/hooks/use-mutation.ts
399
+ var IDLE = Object.freeze({ isLoading: false, error: null });
400
+ var getServerSnapshot2 = () => IDLE;
301
401
  function useMutation(mutationFn, options) {
302
402
  const fnRef = (0, import_react5.useRef)(mutationFn);
303
403
  fnRef.current = mutationFn;
304
404
  const optionsRef = (0, import_react5.useRef)(options);
305
405
  optionsRef.current = options;
306
- const getController = useController(
406
+ const controller = useController(
307
407
  () => (0, import_bindings.createMutationController)({
308
408
  mutationFn: (...args) => fnRef.current(...args),
309
409
  resolveOptions: () => optionsRef.current
310
410
  }),
311
- (controller) => controller.destroy(),
411
+ (instance) => instance.destroy(),
312
412
  []
313
413
  );
314
- const state = (0, import_react5.useSyncExternalStore)(
414
+ const getController = controller.get;
415
+ const subscribe = (0, import_react5.useCallback)(
315
416
  (onStoreChange) => getController().subscribe(onStoreChange),
316
- () => getController().getSnapshot(),
317
- () => getController().getSnapshot()
417
+ [getController, controller.version]
418
+ );
419
+ const getSnapshot = (0, import_react5.useCallback)(() => getController().getSnapshot(), [getController]);
420
+ const state = (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot2);
421
+ const mutate = (0, import_react5.useCallback)((...args) => getController().mutate(...args), [getController]);
422
+ const mutateAsync = (0, import_react5.useCallback)(
423
+ (...args) => getController().mutateAsync(...args),
424
+ [getController]
425
+ );
426
+ const reset = (0, import_react5.useCallback)(() => getController().reset(), [getController]);
427
+ return (0, import_react5.useMemo)(
428
+ () => ({ mutate, mutateAsync, isLoading: state.isLoading, error: state.error, reset }),
429
+ [mutate, mutateAsync, reset, state.isLoading, state.error]
318
430
  );
319
- return {
320
- mutate: (...args) => getController().mutate(...args),
321
- mutateAsync: (...args) => getController().mutateAsync(...args),
322
- isLoading: state.isLoading,
323
- error: state.error,
324
- reset: () => getController().reset()
325
- };
326
431
  }
327
432
 
328
433
  // src/hooks/use-sync-status.ts
329
434
  var import_sync = require("@korajs/sync");
330
435
  var import_react6 = require("react");
436
+ var getServerSnapshot3 = () => import_sync.OFFLINE_SYNC_STATUS;
331
437
  function useSyncStatus() {
332
438
  const { syncEngine, subscribeSyncStatus, events } = useKoraContext();
333
- const getController = useController(
439
+ const controller = useController(
334
440
  () => (0, import_sync.createSyncStatusController)({
335
441
  syncEngine,
336
442
  subscribeSyncStatus,
337
443
  events: subscribeSyncStatus ? null : events
338
444
  }),
339
- (controller) => controller.destroy(),
445
+ (instance) => instance.destroy(),
340
446
  [syncEngine, subscribeSyncStatus, events]
341
447
  );
342
- return (0, import_react6.useSyncExternalStore)(
448
+ const getController = controller.get;
449
+ const subscribe = (0, import_react6.useCallback)(
343
450
  (onStoreChange) => getController().subscribe(onStoreChange),
344
- () => getController().getSnapshot(),
345
- () => getController().getSnapshot()
451
+ [getController, controller.version]
346
452
  );
453
+ const peek = controller.peek;
454
+ const getSnapshot = (0, import_react6.useCallback)(
455
+ () => peek()?.getSnapshot() ?? import_sync.OFFLINE_SYNC_STATUS,
456
+ [peek]
457
+ );
458
+ return (0, import_react6.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot3);
459
+ }
460
+
461
+ // src/hooks/create-kora-hooks.ts
462
+ function createKoraHooks() {
463
+ function useTypedApp() {
464
+ return useApp();
465
+ }
466
+ function useTypedCollection(name) {
467
+ const { app, store } = useKoraContext();
468
+ return (0, import_react7.useMemo)(() => {
469
+ const collections = app?.collections;
470
+ const accessor = collections && name in collections ? collections[name] : store.collection(name);
471
+ return accessor;
472
+ }, [app, store, name]);
473
+ }
474
+ return {
475
+ useApp: useTypedApp,
476
+ useCollection: useTypedCollection,
477
+ useQuery,
478
+ useQueryState,
479
+ useMutation,
480
+ useSyncStatus
481
+ };
347
482
  }
348
483
 
349
484
  // src/hooks/use-collection.ts
350
- var import_react7 = require("react");
485
+ var import_react8 = require("react");
351
486
  function useCollection(name) {
352
487
  const { store } = useKoraContext();
353
- return (0, import_react7.useMemo)(() => {
488
+ return (0, import_react8.useMemo)(() => {
354
489
  return store.collection(name);
355
490
  }, [store, name]);
356
491
  }
357
492
 
358
493
  // src/hooks/use-rich-text.ts
359
494
  var import_store3 = require("@korajs/store");
360
- var import_react8 = require("react");
495
+ var import_react9 = require("react");
361
496
  function useRichText(collectionName, recordId, fieldName, options) {
362
497
  const { store, syncEngine } = useKoraContext();
363
- const collection = (0, import_react8.useMemo)(() => store.collection(collectionName), [store, collectionName]);
364
- const getController = useController(
498
+ const collection = (0, import_react9.useMemo)(() => store.collection(collectionName), [store, collectionName]);
499
+ const controller = useController(
365
500
  () => (0, import_store3.createRichTextController)({
366
501
  collection,
367
502
  collectionName,
@@ -372,29 +507,35 @@ function useRichText(collectionName, recordId, fieldName, options) {
372
507
  useDocChannel: options?.useDocChannel,
373
508
  user: options?.user
374
509
  }),
375
- (controller) => controller.destroy(),
510
+ (instance) => instance.destroy(),
376
511
  [collection, collectionName, fieldName, options?.useDocChannel, recordId, store, syncEngine]
377
512
  );
378
- (0, import_react8.useEffect)(() => {
513
+ const getController = controller.get;
514
+ (0, import_react9.useEffect)(() => {
379
515
  getController().setUser(options?.user);
380
- }, [getController, options?.user]);
381
- const snapshot = (0, import_react8.useSyncExternalStore)(
516
+ }, [getController, options?.user, controller.version]);
517
+ const subscribe = (0, import_react9.useCallback)(
382
518
  (onStoreChange) => getController().subscribe(onStoreChange),
383
- () => getController().getSnapshot(),
384
- () => getController().getSnapshot()
519
+ [getController, controller.version]
385
520
  );
386
- const undo = (0, import_react8.useCallback)(() => {
521
+ const getSnapshot = (0, import_react9.useCallback)(() => getController().getSnapshot(), [getController]);
522
+ const snapshot = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
523
+ const undo = (0, import_react9.useCallback)(() => {
387
524
  getController().undo();
388
525
  }, [getController]);
389
- const redo = (0, import_react8.useCallback)(() => {
526
+ const redo = (0, import_react9.useCallback)(() => {
390
527
  getController().redo();
391
528
  }, [getController]);
392
- const setCursor = (0, import_react8.useCallback)(
529
+ const setCursor = (0, import_react9.useCallback)(
393
530
  (anchor, head) => getController().setCursor(anchor, head),
394
531
  [getController]
395
532
  );
396
- const clearCursor = (0, import_react8.useCallback)(() => getController().clearCursor(), [getController]);
397
- return buildResult(getController(), snapshot, undo, redo, setCursor, clearCursor);
533
+ const clearCursor = (0, import_react9.useCallback)(() => getController().clearCursor(), [getController]);
534
+ const live = getController();
535
+ return (0, import_react9.useMemo)(
536
+ () => buildResult(live, snapshot, undo, redo, setCursor, clearCursor),
537
+ [live, snapshot, undo, redo, setCursor, clearCursor, controller.version]
538
+ );
398
539
  }
399
540
  function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
400
541
  return {
@@ -413,13 +554,13 @@ function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
413
554
  }
414
555
 
415
556
  // src/hooks/use-presence.ts
416
- var import_react9 = require("react");
557
+ var import_react10 = require("react");
417
558
  function usePresence(user) {
418
559
  const { syncEngine } = useKoraContext();
419
560
  const name = user?.name ?? null;
420
561
  const color = user?.color ?? null;
421
562
  const avatar = user?.avatar ?? null;
422
- (0, import_react9.useEffect)(() => {
563
+ (0, import_react10.useEffect)(() => {
423
564
  if (!syncEngine || !name || !color) return;
424
565
  const awareness = syncEngine.getAwarenessManager();
425
566
  awareness.setLocalState({
@@ -433,12 +574,13 @@ function usePresence(user) {
433
574
 
434
575
  // src/hooks/use-collaborators.ts
435
576
  var import_sync2 = require("@korajs/sync");
436
- var import_react10 = require("react");
577
+ var import_react11 = require("react");
437
578
  var EMPTY_ARRAY2 = [];
579
+ var getServerSnapshot4 = () => EMPTY_ARRAY2;
438
580
  function useCollaborators() {
439
581
  const { syncEngine } = useKoraContext();
440
- const snapshotRef = (0, import_react10.useRef)(EMPTY_ARRAY2);
441
- const subscribe = (0, import_react10.useCallback)(
582
+ const snapshotRef = (0, import_react11.useRef)(EMPTY_ARRAY2);
583
+ const subscribe = (0, import_react11.useCallback)(
442
584
  (onStoreChange) => {
443
585
  if (!syncEngine) {
444
586
  snapshotRef.current = EMPTY_ARRAY2;
@@ -446,6 +588,7 @@ function useCollaborators() {
446
588
  };
447
589
  }
448
590
  const awareness = syncEngine.getAwarenessManager();
591
+ snapshotRef.current = EMPTY_ARRAY2;
449
592
  return (0, import_sync2.subscribeRemoteAwarenessStates)(awareness, (states) => {
450
593
  snapshotRef.current = states;
451
594
  onStoreChange();
@@ -453,24 +596,27 @@ function useCollaborators() {
453
596
  },
454
597
  [syncEngine]
455
598
  );
456
- const getSnapshot = (0, import_react10.useCallback)(() => snapshotRef.current, []);
457
- (0, import_react10.useEffect)(() => {
599
+ const getSnapshot = (0, import_react11.useCallback)(() => snapshotRef.current, []);
600
+ (0, import_react11.useEffect)(() => {
458
601
  if (!syncEngine) {
459
602
  snapshotRef.current = EMPTY_ARRAY2;
460
603
  }
461
604
  }, [syncEngine]);
462
- return (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot);
605
+ return (0, import_react11.useSyncExternalStore)(subscribe, getSnapshot, getServerSnapshot4);
463
606
  }
464
607
  // Annotate the CommonJS export names for ESM import in node:
465
608
  0 && (module.exports = {
466
609
  AuthBoundKoraProvider,
467
610
  KoraProvider,
611
+ classifyKoraInitializationError,
612
+ createKoraHooks,
468
613
  useApp,
469
614
  useCollaborators,
470
615
  useCollection,
471
616
  useMutation,
472
617
  usePresence,
473
618
  useQuery,
619
+ useQueryState,
474
620
  useRichText,
475
621
  useSyncStatus
476
622
  });