@korajs/react 0.5.0 → 0.6.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.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/context/kora-context.ts
2
- import { createContext, createElement, useContext, useEffect, useState } from "react";
2
+ import { QueryStoreCache } from "@korajs/store";
3
+ import { createContext, createElement, useContext, useEffect, useMemo, useRef, useState } from "react";
3
4
  var KoraContext = createContext(null);
4
5
  function KoraProvider({
5
6
  app,
@@ -12,6 +13,10 @@ function KoraProvider({
12
13
  const [resolvedSync, setResolvedSync] = useState(syncEngine ?? null);
13
14
  const [ready, setReady] = useState(!app);
14
15
  const [initError, setInitError] = useState(null);
16
+ const fallbackQueryStoreCache = useRef(null);
17
+ if (!fallbackQueryStoreCache.current) {
18
+ fallbackQueryStoreCache.current = new QueryStoreCache();
19
+ }
15
20
  useEffect(() => {
16
21
  if (!app) return;
17
22
  let cancelled = false;
@@ -30,6 +35,26 @@ function KoraProvider({
30
35
  cancelled = true;
31
36
  };
32
37
  }, [app]);
38
+ useEffect(() => {
39
+ return () => {
40
+ if (!app) {
41
+ fallbackQueryStoreCache.current?.clear();
42
+ }
43
+ };
44
+ }, [app]);
45
+ const contextValue = useMemo(() => {
46
+ if (!resolvedStore) {
47
+ return null;
48
+ }
49
+ return {
50
+ store: resolvedStore,
51
+ syncEngine: resolvedSync,
52
+ app: app ?? null,
53
+ events: app?.events ?? null,
54
+ subscribeSyncStatus: app?.sync?.subscribeStatus ?? null,
55
+ queryStoreCache: app && typeof app.getQueryStoreCache === "function" ? app.getQueryStoreCache() : fallbackQueryStoreCache.current
56
+ };
57
+ }, [resolvedStore, resolvedSync, app]);
33
58
  if (initError) {
34
59
  return createElement(
35
60
  "div",
@@ -41,19 +66,12 @@ function KoraProvider({
41
66
  if (!ready) {
42
67
  return fallback ?? null;
43
68
  }
44
- if (!resolvedStore) {
69
+ if (!contextValue) {
45
70
  throw new Error(
46
71
  'KoraProvider requires either an "app" or "store" prop. Pass a KoraApp from createApp() or a Store instance.'
47
72
  );
48
73
  }
49
- const value = {
50
- store: resolvedStore,
51
- syncEngine: resolvedSync,
52
- app: app ?? null,
53
- events: app?.events ?? null,
54
- subscribeSyncStatus: app?.sync?.subscribeStatus ?? null
55
- };
56
- return createElement(KoraContext.Provider, { value }, children);
74
+ return createElement(KoraContext.Provider, { value: contextValue }, children);
57
75
  }
58
76
  function useKoraContext() {
59
77
  const context = useContext(KoraContext);
@@ -77,126 +95,35 @@ function useApp() {
77
95
  }
78
96
 
79
97
  // src/hooks/use-query.ts
80
- import { useMemo, useRef, useSyncExternalStore } from "react";
81
-
82
- // src/query-store/query-store.ts
98
+ import { assertQueryReady } from "@korajs/store";
99
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
83
100
  var EMPTY_ARRAY = Object.freeze([]);
84
- var QueryStore = class {
85
- snapshot = EMPTY_ARRAY;
86
- listeners = /* @__PURE__ */ new Set();
87
- unsubscribeQuery = null;
88
- active = false;
89
- queryBuilder;
90
- constructor(queryBuilder) {
91
- this.queryBuilder = queryBuilder;
92
- }
93
- /**
94
- * Subscribe to snapshot changes. Compatible with useSyncExternalStore.
95
- *
96
- * Lazily starts the underlying query subscription when the first listener
97
- * attaches, and stops it when the last listener detaches. This allows
98
- * React StrictMode to unmount/remount without permanently killing the subscription.
99
- *
100
- * @returns Unsubscribe function
101
- */
102
- subscribe = (onStoreChange) => {
103
- this.listeners.add(onStoreChange);
104
- if (!this.active) {
105
- this.startSubscription();
106
- }
107
- return () => {
108
- this.listeners.delete(onStoreChange);
109
- if (this.listeners.size === 0) {
110
- this.stopSubscription();
111
- }
112
- };
113
- };
114
- /**
115
- * Get the current snapshot synchronously. Compatible with useSyncExternalStore.
116
- * Returns EMPTY_ARRAY before the first async fetch completes.
117
- */
118
- getSnapshot = () => {
119
- return this.snapshot;
120
- };
121
- /**
122
- * Clean up the underlying subscription and release resources.
123
- * Called by useMemo when the query descriptor changes.
124
- */
125
- destroy() {
126
- this.stopSubscription();
127
- this.listeners.clear();
128
- this.snapshot = EMPTY_ARRAY;
129
- }
130
- startSubscription() {
131
- this.active = true;
132
- this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
133
- if (!this.active) return;
134
- const newSnapshot = Object.freeze([...results]);
135
- this.snapshot = newSnapshot;
136
- this.notifyListeners();
137
- });
138
- }
139
- stopSubscription() {
140
- this.active = false;
141
- if (this.unsubscribeQuery) {
142
- this.unsubscribeQuery();
143
- this.unsubscribeQuery = null;
144
- }
145
- }
146
- notifyListeners() {
147
- for (const listener of this.listeners) {
148
- listener();
149
- }
150
- }
151
- };
152
-
153
- // src/hooks/use-query.ts
154
- var APP_NOT_READY_CODE = "APP_NOT_READY";
155
- function assertQueryReady(query) {
156
- const descriptor = query.getDescriptor();
157
- if (descriptor.collection === "__pending__") {
158
- const err = new Error(
159
- "Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
160
- );
161
- err.name = "AppNotReadyError";
162
- Object.assign(err, { code: APP_NOT_READY_CODE });
163
- throw err;
164
- }
165
- }
166
- var EMPTY_ARRAY2 = Object.freeze([]);
167
101
  var noopSubscribe = (_onStoreChange) => {
168
102
  return () => {
169
103
  };
170
104
  };
171
105
  function useQuery(query, options) {
106
+ const { queryStoreCache } = useKoraContext();
172
107
  const enabled = options?.enabled !== false;
173
- if (enabled) {
174
- assertQueryReady(query);
175
- }
176
108
  const descriptorKey = JSON.stringify(query.getDescriptor());
177
- const queryStoreRef = useRef(null);
178
- const prevKeyRef = useRef(null);
179
- const queryStore = useMemo(() => {
109
+ const queryRef = useRef2(query);
110
+ queryRef.current = query;
111
+ const [queryStore, setQueryStore] = useState2(null);
112
+ useEffect2(() => {
180
113
  if (!enabled) {
181
- if (queryStoreRef.current) {
182
- queryStoreRef.current.destroy();
183
- queryStoreRef.current = null;
184
- }
185
- prevKeyRef.current = null;
186
- return null;
187
- }
188
- if (prevKeyRef.current !== descriptorKey) {
189
- if (queryStoreRef.current) {
190
- queryStoreRef.current.destroy();
191
- }
192
- const newStore = new QueryStore(query);
193
- queryStoreRef.current = newStore;
194
- prevKeyRef.current = descriptorKey;
195
- return newStore;
114
+ setQueryStore(null);
115
+ return;
196
116
  }
197
- return queryStoreRef.current;
198
- }, [descriptorKey, enabled, query]);
199
- const disabledGetSnapshot = () => EMPTY_ARRAY2;
117
+ const currentQuery = queryRef.current;
118
+ assertQueryReady(currentQuery);
119
+ const store = queryStoreCache.getOrCreate(currentQuery);
120
+ setQueryStore(store);
121
+ return () => {
122
+ queryStoreCache.release(currentQuery);
123
+ setQueryStore(null);
124
+ };
125
+ }, [descriptorKey, enabled, queryStoreCache]);
126
+ const disabledGetSnapshot = () => EMPTY_ARRAY;
200
127
  return useSyncExternalStore(
201
128
  queryStore ? queryStore.subscribe : noopSubscribe,
202
129
  queryStore ? queryStore.getSnapshot : disabledGetSnapshot
@@ -204,453 +131,177 @@ function useQuery(query, options) {
204
131
  }
205
132
 
206
133
  // src/hooks/use-mutation.ts
207
- import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
134
+ import { createMutationController } from "@korajs/core/bindings";
135
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useSyncExternalStore as useSyncExternalStore2 } from "react";
208
136
  function useMutation(mutationFn, options) {
209
- const [state, setState] = useState2({
210
- isLoading: false,
211
- error: null
212
- });
213
- const mountedRef = useRef2(true);
214
- useEffect2(() => {
215
- mountedRef.current = true;
216
- return () => {
217
- mountedRef.current = false;
218
- };
219
- }, []);
220
- const fnRef = useRef2(mutationFn);
137
+ const fnRef = useRef3(mutationFn);
221
138
  fnRef.current = mutationFn;
222
- const optionsRef = useRef2(options);
139
+ const optionsRef = useRef3(options);
223
140
  optionsRef.current = options;
224
- const mutateAsync = useCallback(async (...args) => {
225
- const opts = optionsRef.current;
226
- let context;
227
- if (mountedRef.current) {
228
- setState({ isLoading: true, error: null });
229
- }
230
- try {
231
- if (opts?.onMutate) {
232
- context = await opts.onMutate(...args);
233
- }
234
- const result = await fnRef.current(...args);
235
- if (mountedRef.current) {
236
- setState({ isLoading: false, error: null });
237
- }
238
- opts?.onSuccess?.(result, ...args);
239
- opts?.onSettled?.(result, null, ...args);
240
- return result;
241
- } catch (err) {
242
- const error = err instanceof Error ? err : new Error(String(err));
243
- if (context !== void 0 && opts?.onRollback) {
244
- await opts.onRollback(context, ...args);
245
- }
246
- if (mountedRef.current) {
247
- setState({ isLoading: false, error });
248
- }
249
- opts?.onError?.(error, ...args);
250
- opts?.onSettled?.(void 0, error, ...args);
251
- throw error;
252
- }
253
- }, []);
254
- const mutate = useCallback(
255
- (...args) => {
256
- mutateAsync(...args).catch(() => {
257
- });
258
- },
259
- [mutateAsync]
141
+ const controller = useMemo2(
142
+ () => createMutationController({
143
+ mutationFn: (...args) => fnRef.current(...args),
144
+ resolveOptions: () => optionsRef.current
145
+ }),
146
+ []
147
+ );
148
+ useEffect3(() => () => controller.destroy(), [controller]);
149
+ const state = useSyncExternalStore2(
150
+ (onStoreChange) => controller.subscribe(onStoreChange),
151
+ () => controller.getSnapshot(),
152
+ () => controller.getSnapshot()
260
153
  );
261
- const reset = useCallback(() => {
262
- if (mountedRef.current) {
263
- setState({ isLoading: false, error: null });
264
- }
265
- }, []);
266
154
  return {
267
- mutate,
268
- mutateAsync,
155
+ mutate: (...args) => controller.mutate(...args),
156
+ mutateAsync: (...args) => controller.mutateAsync(...args),
269
157
  isLoading: state.isLoading,
270
158
  error: state.error,
271
- reset
159
+ reset: () => controller.reset()
272
160
  };
273
161
  }
274
162
 
275
163
  // src/hooks/use-sync-status.ts
276
- import { useCallback as useCallback2, useRef as useRef3, useSyncExternalStore as useSyncExternalStore2 } from "react";
277
- var OFFLINE_STATUS = Object.freeze({
278
- status: "offline",
279
- pendingOperations: 0,
280
- lastSyncedAt: null,
281
- lastSuccessfulPush: null,
282
- lastSuccessfulPull: null,
283
- conflicts: 0
284
- });
285
- var SYNC_STATUS_EVENT_TYPES = [
286
- "sync:connected",
287
- "sync:disconnected",
288
- "sync:schema-mismatch",
289
- "sync:auth-failed",
290
- "sync:sent",
291
- "sync:received",
292
- "sync:acknowledged",
293
- "sync:apply-failed",
294
- "sync:diagnostics",
295
- "sync:initial-sync-progress"
296
- ];
164
+ import { createSyncStatusController } from "@korajs/sync";
165
+ import { useEffect as useEffect4, useMemo as useMemo3, useSyncExternalStore as useSyncExternalStore3 } from "react";
297
166
  function useSyncStatus() {
298
167
  const { syncEngine, subscribeSyncStatus, events } = useKoraContext();
299
- const snapshotRef = useRef3(OFFLINE_STATUS);
300
- const serializedRef = useRef3(JSON.stringify(OFFLINE_STATUS));
301
- const subscribe = useCallback2(
302
- (onStoreChange) => {
303
- if (subscribeSyncStatus) {
304
- return subscribeSyncStatus((status) => {
305
- snapshotRef.current = status;
306
- serializedRef.current = JSON.stringify(status);
307
- onStoreChange();
308
- });
309
- }
310
- if (!syncEngine) {
311
- return () => {
312
- };
313
- }
314
- const refresh = () => {
315
- const newStatus = syncEngine.getStatus();
316
- const newSerialized = JSON.stringify(newStatus);
317
- if (newSerialized !== serializedRef.current) {
318
- snapshotRef.current = newStatus;
319
- serializedRef.current = newSerialized;
320
- onStoreChange();
321
- }
322
- };
323
- if (events) {
324
- const unsubs = SYNC_STATUS_EVENT_TYPES.map((type) => events.on(type, refresh));
325
- refresh();
326
- return () => {
327
- for (const unsub of unsubs) {
328
- unsub();
329
- }
330
- };
331
- }
332
- refresh();
333
- return () => {
334
- };
335
- },
168
+ const controller = useMemo3(
169
+ () => createSyncStatusController({
170
+ syncEngine,
171
+ subscribeSyncStatus,
172
+ events: subscribeSyncStatus ? null : events
173
+ }),
336
174
  [syncEngine, subscribeSyncStatus, events]
337
175
  );
338
- const getSnapshot = useCallback2(() => {
339
- if (subscribeSyncStatus) {
340
- return snapshotRef.current;
341
- }
342
- return syncEngine ? syncEngine.getStatus() : OFFLINE_STATUS;
343
- }, [syncEngine, subscribeSyncStatus]);
344
- return useSyncExternalStore2(subscribe, getSnapshot);
176
+ useEffect4(() => () => controller.destroy(), [controller]);
177
+ return useSyncExternalStore3(
178
+ (onStoreChange) => controller.subscribe(onStoreChange),
179
+ () => controller.getSnapshot(),
180
+ () => controller.getSnapshot()
181
+ );
345
182
  }
346
183
 
347
184
  // src/hooks/use-collection.ts
348
- import { useMemo as useMemo2 } from "react";
185
+ import { useMemo as useMemo4 } from "react";
349
186
  function useCollection(name) {
350
187
  const { store } = useKoraContext();
351
- return useMemo2(() => {
188
+ return useMemo4(() => {
352
189
  return store.collection(name);
353
190
  }, [store, name]);
354
191
  }
355
192
 
356
193
  // src/hooks/use-rich-text.ts
357
- import { encodeRichtext } from "@korajs/store";
358
- import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
359
- import * as Y from "yjs";
360
- var LOAD_ORIGIN = "kora-load";
361
- var REMOTE_ORIGIN = "kora-remote";
362
- var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
363
- var TEXT_KEY = "content";
364
- var COMPACT_AFTER_DELTAS = 20;
365
- var PERSIST_DEBOUNCE_MS = 400;
194
+ import { asRichTextSyncEngine, createRichTextController } from "@korajs/store";
195
+ import { useCallback, useEffect as useEffect5, useMemo as useMemo5, useSyncExternalStore as useSyncExternalStore4 } from "react";
366
196
  function useRichText(collectionName, recordId, fieldName, options) {
367
197
  const { store, syncEngine } = useKoraContext();
368
- const collection = useMemo3(
198
+ const collection = useMemo5(
369
199
  () => store.collection(collectionName),
370
200
  [store, collectionName]
371
201
  );
372
- const [doc] = useState3(() => new Y.Doc());
373
- const [ready, setReady] = useState3(false);
374
- const [error, setError] = useState3(null);
375
- const [canUndo, setCanUndo] = useState3(false);
376
- const [canRedo, setCanRedo] = useState3(false);
377
- const [cursors, setCursors] = useState3([]);
378
- const baseUpdateRef = useRef4(null);
379
- const pendingDeltasRef = useRef4([]);
380
- const docChannelActiveRef = useRef4(false);
381
- const persistTimerRef = useRef4(null);
382
- const userRef = useRef4(options?.user);
383
- useEffect3(() => {
384
- userRef.current = options?.user;
385
- }, [options?.user]);
386
- const text = useMemo3(() => doc.getText(TEXT_KEY), [doc]);
387
- const undoManager = useMemo3(() => new Y.UndoManager(text), [text]);
388
- const syncHistoryState = useCallback3(() => {
389
- setCanUndo(undoManager.undoStack.length > 0);
390
- setCanRedo(undoManager.redoStack.length > 0);
391
- }, [undoManager]);
392
- const undo = useCallback3(() => {
393
- undoManager.undo();
394
- syncHistoryState();
395
- }, [syncHistoryState, undoManager]);
396
- const redo = useCallback3(() => {
397
- undoManager.redo();
398
- syncHistoryState();
399
- }, [syncHistoryState, undoManager]);
400
- const resolveAwarenessUser = useCallback3(() => {
401
- if (userRef.current) {
402
- return userRef.current;
403
- }
404
- if (syncEngine) {
405
- const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
406
- if (existing) {
407
- return existing;
408
- }
409
- }
410
- return {
411
- name: "Anonymous",
412
- color: "#6366f1"
413
- };
414
- }, [syncEngine]);
415
- const setCursor = useCallback3(
416
- (anchor, head) => {
417
- if (!syncEngine) {
418
- return;
419
- }
420
- const awareness = syncEngine.getAwarenessManager();
421
- const state = {
422
- user: resolveAwarenessUser(),
423
- cursor: {
424
- collection: collectionName,
425
- recordId,
426
- field: fieldName,
427
- anchor,
428
- head
429
- }
430
- };
431
- awareness.setLocalState(state);
432
- },
433
- [collectionName, fieldName, recordId, resolveAwarenessUser, syncEngine]
202
+ const controller = useMemo5(
203
+ () => createRichTextController({
204
+ collection,
205
+ collectionName,
206
+ recordId,
207
+ fieldName,
208
+ store,
209
+ syncEngine: asRichTextSyncEngine(syncEngine),
210
+ useDocChannel: options?.useDocChannel,
211
+ user: options?.user
212
+ }),
213
+ [collection, collectionName, fieldName, options?.useDocChannel, recordId, store, syncEngine]
434
214
  );
435
- const clearCursor = useCallback3(() => {
436
- if (!syncEngine) {
437
- return;
438
- }
439
- const awareness = syncEngine.getAwarenessManager();
440
- const current = awareness.getLocalState();
441
- if (!current?.cursor) {
442
- return;
443
- }
444
- if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
445
- return;
446
- }
447
- awareness.setLocalState({ user: current.user });
448
- }, [collectionName, fieldName, recordId, syncEngine]);
449
- const applyRemoteSnapshot = useCallback3(
450
- (value) => {
451
- const encoded = encodeRichtextInput(value);
452
- if (!encoded) {
453
- return;
454
- }
455
- const currentSnapshot = Y.encodeStateAsUpdate(doc);
456
- if (updatesEqual(currentSnapshot, encoded)) {
457
- return;
458
- }
459
- Y.applyUpdate(doc, encoded, REMOTE_ORIGIN);
460
- baseUpdateRef.current = encoded;
461
- pendingDeltasRef.current = [];
462
- syncHistoryState();
463
- },
464
- [doc, syncHistoryState]
215
+ useEffect5(() => {
216
+ controller.setUser(options?.user);
217
+ }, [controller, options?.user]);
218
+ useEffect5(() => () => controller.destroy(), [controller]);
219
+ const snapshot = useSyncExternalStore4(
220
+ (onStoreChange) => controller.subscribe(onStoreChange),
221
+ () => controller.getSnapshot(),
222
+ () => controller.getSnapshot()
465
223
  );
466
- useEffect3(() => {
467
- let disposed = false;
468
- const initialize = async () => {
469
- setReady(false);
470
- setError(null);
471
- try {
472
- const record = await collection.findById(recordId);
473
- if (disposed) return;
474
- doc.transact(() => {
475
- const target = doc.getText(TEXT_KEY);
476
- target.delete(0, target.length);
477
- }, LOAD_ORIGIN);
478
- const encoded = encodeRichtextInput(record?.[fieldName]);
479
- baseUpdateRef.current = encoded;
480
- pendingDeltasRef.current = [];
481
- if (encoded) {
482
- Y.applyUpdate(doc, encoded, LOAD_ORIGIN);
483
- }
484
- const channel = syncEngine?.getRichtextDocChannel?.();
485
- docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, options?.useDocChannel) ?? false;
486
- setReady(true);
487
- } catch (cause) {
488
- if (disposed) return;
489
- setError(cause instanceof Error ? cause : new Error(String(cause)));
490
- }
491
- };
492
- const flushPersist = async () => {
493
- const snapshot = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
494
- if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
495
- baseUpdateRef.current = snapshot;
496
- pendingDeltasRef.current = [];
497
- }
498
- try {
499
- await collection.update(recordId, {
500
- [fieldName]: snapshot
501
- });
502
- } catch (cause) {
503
- if (!disposed) {
504
- setError(cause instanceof Error ? cause : new Error(String(cause)));
505
- }
506
- }
507
- };
508
- const schedulePersist = () => {
509
- if (persistTimerRef.current) {
510
- clearTimeout(persistTimerRef.current);
511
- }
512
- persistTimerRef.current = setTimeout(() => {
513
- persistTimerRef.current = null;
514
- void flushPersist();
515
- }, PERSIST_DEBOUNCE_MS);
516
- };
517
- const persist = (_update, origin) => {
518
- syncHistoryState();
519
- if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
520
- return;
521
- }
522
- pendingDeltasRef.current.push(_update);
523
- if (docChannelActiveRef.current && syncEngine) {
524
- syncEngine.getRichtextDocChannel().send(collectionName, recordId, fieldName, _update);
525
- schedulePersist();
526
- return;
527
- }
528
- void flushPersist();
529
- };
530
- doc.on("update", persist);
531
- void initialize();
532
- syncHistoryState();
533
- return () => {
534
- disposed = true;
535
- if (persistTimerRef.current) {
536
- clearTimeout(persistTimerRef.current);
537
- persistTimerRef.current = null;
538
- }
539
- doc.off("update", persist);
540
- undoManager.destroy();
541
- baseUpdateRef.current = null;
542
- pendingDeltasRef.current = [];
543
- docChannelActiveRef.current = false;
544
- };
545
- }, [
546
- collection,
547
- collectionName,
548
- doc,
549
- fieldName,
550
- options?.useDocChannel,
551
- recordId,
552
- syncEngine,
553
- syncHistoryState,
554
- undoManager
555
- ]);
556
- useEffect3(() => {
557
- if (!ready || !syncEngine || !docChannelActiveRef.current) {
558
- return;
559
- }
560
- const channel = syncEngine.getRichtextDocChannel();
561
- return channel.subscribe(collectionName, recordId, fieldName, (update) => {
562
- Y.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
563
- syncHistoryState();
564
- });
565
- }, [collectionName, doc, fieldName, ready, recordId, syncEngine, syncHistoryState]);
566
- useEffect3(() => {
567
- if (!ready) {
568
- return;
569
- }
570
- const unsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
571
- const record = results[0];
572
- if (!record) {
573
- return;
574
- }
575
- applyRemoteSnapshot(record[fieldName]);
576
- });
577
- return unsubscribe;
578
- }, [applyRemoteSnapshot, collectionName, fieldName, ready, recordId, store]);
579
- useEffect3(() => {
580
- if (!syncEngine) return;
224
+ const undo = useCallback(() => controller.undo(), [controller]);
225
+ const redo = useCallback(() => controller.redo(), [controller]);
226
+ const setCursor = useCallback(
227
+ (anchor, head) => controller.setCursor(anchor, head),
228
+ [controller]
229
+ );
230
+ const clearCursor = useCallback(() => controller.clearCursor(), [controller]);
231
+ return buildResult(controller, snapshot, undo, redo, setCursor, clearCursor);
232
+ }
233
+ function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
234
+ return {
235
+ doc: controller.doc,
236
+ text: controller.text,
237
+ undo,
238
+ redo,
239
+ canUndo: snapshot.canUndo,
240
+ canRedo: snapshot.canRedo,
241
+ ready: snapshot.ready,
242
+ error: snapshot.error,
243
+ cursors: [...snapshot.cursors],
244
+ setCursor,
245
+ clearCursor
246
+ };
247
+ }
248
+
249
+ // src/hooks/use-presence.ts
250
+ import { useEffect as useEffect6 } from "react";
251
+ function usePresence(user) {
252
+ const { syncEngine } = useKoraContext();
253
+ const name = user?.name ?? null;
254
+ const color = user?.color ?? null;
255
+ const avatar = user?.avatar ?? null;
256
+ useEffect6(() => {
257
+ if (!syncEngine || !name || !color) return;
581
258
  const awareness = syncEngine.getAwarenessManager();
582
- const localClientId = awareness.clientId;
583
- const updateCursors = () => {
584
- const states = awareness.getStates();
585
- const fieldCursors = [];
586
- for (const [clientId, state] of states) {
587
- if (clientId === localClientId) continue;
588
- if (!state.cursor) continue;
589
- if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
590
- continue;
591
- }
592
- fieldCursors.push({
593
- clientId,
594
- userName: state.user.name,
595
- color: state.user.color,
596
- anchor: state.cursor.anchor,
597
- head: state.cursor.head
598
- });
599
- }
600
- setCursors(fieldCursors);
601
- };
602
- const unsubscribe = awareness.on("change", () => {
603
- updateCursors();
259
+ awareness.setLocalState({
260
+ user: { name, color, avatar: avatar ?? void 0 }
604
261
  });
605
- updateCursors();
606
262
  return () => {
607
- unsubscribe();
608
- clearCursor();
263
+ awareness.setLocalState(null);
609
264
  };
610
- }, [clearCursor, collectionName, fieldName, recordId, syncEngine]);
611
- return { doc, text, undo, redo, canUndo, canRedo, ready, error, cursors, setCursor, clearCursor };
612
- }
613
- function encodeRichtextInput(value) {
614
- if (value === null || value === void 0) {
615
- return null;
616
- }
617
- try {
618
- return encodeRichtext(value);
619
- } catch {
620
- if (typeof value === "string") {
621
- const fallbackDoc = new Y.Doc();
622
- fallbackDoc.getText(TEXT_KEY).insert(0, value);
623
- return Y.encodeStateAsUpdate(fallbackDoc);
624
- }
625
- throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
626
- }
627
- }
628
- function composeRichtextSnapshot(base, deltas) {
629
- const mergedDoc = new Y.Doc();
630
- if (base) {
631
- Y.applyUpdate(mergedDoc, base);
632
- }
633
- for (const delta of deltas) {
634
- Y.applyUpdate(mergedDoc, delta);
635
- }
636
- return Y.encodeStateAsUpdate(mergedDoc);
265
+ }, [syncEngine, name, color, avatar]);
637
266
  }
638
- function updatesEqual(left, right) {
639
- if (left.length !== right.length) {
640
- return false;
641
- }
642
- for (let index = 0; index < left.length; index++) {
643
- if (left[index] !== right[index]) {
644
- return false;
267
+
268
+ // src/hooks/use-collaborators.ts
269
+ import { subscribeRemoteAwarenessStates } from "@korajs/sync";
270
+ import { useCallback as useCallback2, useEffect as useEffect7, useRef as useRef4, useSyncExternalStore as useSyncExternalStore5 } from "react";
271
+ var EMPTY_ARRAY2 = [];
272
+ function useCollaborators() {
273
+ const { syncEngine } = useKoraContext();
274
+ const snapshotRef = useRef4(EMPTY_ARRAY2);
275
+ const subscribe = useCallback2(
276
+ (onStoreChange) => {
277
+ if (!syncEngine) {
278
+ snapshotRef.current = EMPTY_ARRAY2;
279
+ return () => {
280
+ };
281
+ }
282
+ const awareness = syncEngine.getAwarenessManager();
283
+ return subscribeRemoteAwarenessStates(awareness, (states) => {
284
+ snapshotRef.current = states;
285
+ onStoreChange();
286
+ });
287
+ },
288
+ [syncEngine]
289
+ );
290
+ const getSnapshot = useCallback2(() => snapshotRef.current, []);
291
+ useEffect7(() => {
292
+ if (!syncEngine) {
293
+ snapshotRef.current = EMPTY_ARRAY2;
645
294
  }
646
- }
647
- return true;
295
+ }, [syncEngine]);
296
+ return useSyncExternalStore5(subscribe, getSnapshot);
648
297
  }
649
298
  export {
650
299
  KoraProvider,
651
300
  useApp,
301
+ useCollaborators,
652
302
  useCollection,
653
303
  useMutation,
304
+ usePresence,
654
305
  useQuery,
655
306
  useRichText,
656
307
  useSyncStatus