@korajs/react 0.5.0 → 0.6.1

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