@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.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,144 +165,54 @@ function useQuery(query, options) {
246
165
  }
247
166
 
248
167
  // src/hooks/use-mutation.ts
168
+ var import_bindings = require("@korajs/core/bindings");
249
169
  var import_react3 = require("react");
250
170
  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);
256
- (0, import_react3.useEffect)(() => {
257
- mountedRef.current = true;
258
- return () => {
259
- mountedRef.current = false;
260
- };
261
- }, []);
262
171
  const fnRef = (0, import_react3.useRef)(mutationFn);
263
172
  fnRef.current = mutationFn;
264
173
  const optionsRef = (0, import_react3.useRef)(options);
265
174
  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]
175
+ const controller = (0, import_react3.useMemo)(
176
+ () => (0, import_bindings.createMutationController)({
177
+ mutationFn: (...args) => fnRef.current(...args),
178
+ resolveOptions: () => optionsRef.current
179
+ }),
180
+ []
181
+ );
182
+ (0, import_react3.useEffect)(() => () => controller.destroy(), [controller]);
183
+ const state = (0, import_react3.useSyncExternalStore)(
184
+ (onStoreChange) => controller.subscribe(onStoreChange),
185
+ () => controller.getSnapshot(),
186
+ () => controller.getSnapshot()
302
187
  );
303
- const reset = (0, import_react3.useCallback)(() => {
304
- if (mountedRef.current) {
305
- setState({ isLoading: false, error: null });
306
- }
307
- }, []);
308
188
  return {
309
- mutate,
310
- mutateAsync,
189
+ mutate: (...args) => controller.mutate(...args),
190
+ mutateAsync: (...args) => controller.mutateAsync(...args),
311
191
  isLoading: state.isLoading,
312
192
  error: state.error,
313
- reset
193
+ reset: () => controller.reset()
314
194
  };
315
195
  }
316
196
 
317
197
  // src/hooks/use-sync-status.ts
198
+ var import_sync = require("@korajs/sync");
318
199
  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
- ];
339
200
  function useSyncStatus() {
340
201
  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
- },
202
+ const controller = (0, import_react4.useMemo)(
203
+ () => (0, import_sync.createSyncStatusController)({
204
+ syncEngine,
205
+ subscribeSyncStatus,
206
+ events: subscribeSyncStatus ? null : events
207
+ }),
378
208
  [syncEngine, subscribeSyncStatus, events]
379
209
  );
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);
210
+ (0, import_react4.useEffect)(() => () => controller.destroy(), [controller]);
211
+ return (0, import_react4.useSyncExternalStore)(
212
+ (onStoreChange) => controller.subscribe(onStoreChange),
213
+ () => controller.getSnapshot(),
214
+ () => controller.getSnapshot()
215
+ );
387
216
  }
388
217
 
389
218
  // src/hooks/use-collection.ts
@@ -396,304 +225,118 @@ function useCollection(name) {
396
225
  }
397
226
 
398
227
  // src/hooks/use-rich-text.ts
399
- var import_store = require("@korajs/store");
228
+ var import_store3 = require("@korajs/store");
400
229
  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;
408
230
  function useRichText(collectionName, recordId, fieldName, options) {
409
231
  const { store, syncEngine } = useKoraContext();
410
232
  const collection = (0, import_react6.useMemo)(
411
233
  () => store.collection(collectionName),
412
234
  [store, collectionName]
413
235
  );
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);
236
+ const controller = (0, import_react6.useMemo)(
237
+ () => (0, import_store3.createRichTextController)({
238
+ collection,
239
+ collectionName,
240
+ recordId,
241
+ fieldName,
242
+ store,
243
+ syncEngine: (0, import_store3.asRichTextSyncEngine)(syncEngine),
244
+ useDocChannel: options?.useDocChannel,
245
+ user: options?.user
246
+ }),
247
+ [collection, collectionName, fieldName, options?.useDocChannel, recordId, store, syncEngine]
248
+ );
425
249
  (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]
250
+ controller.setUser(options?.user);
251
+ }, [controller, options?.user]);
252
+ (0, import_react6.useEffect)(() => () => controller.destroy(), [controller]);
253
+ const snapshot = (0, import_react6.useSyncExternalStore)(
254
+ (onStoreChange) => controller.subscribe(onStoreChange),
255
+ () => controller.getSnapshot(),
256
+ () => controller.getSnapshot()
476
257
  );
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]
258
+ const undo = (0, import_react6.useCallback)(() => controller.undo(), [controller]);
259
+ const redo = (0, import_react6.useCallback)(() => controller.redo(), [controller]);
260
+ const setCursor = (0, import_react6.useCallback)(
261
+ (anchor, head) => controller.setCursor(anchor, head),
262
+ [controller]
507
263
  );
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;
264
+ const clearCursor = (0, import_react6.useCallback)(() => controller.clearCursor(), [controller]);
265
+ return buildResult(controller, snapshot, undo, redo, setCursor, clearCursor);
266
+ }
267
+ function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
268
+ return {
269
+ doc: controller.doc,
270
+ text: controller.text,
271
+ undo,
272
+ redo,
273
+ canUndo: snapshot.canUndo,
274
+ canRedo: snapshot.canRedo,
275
+ ready: snapshot.ready,
276
+ error: snapshot.error,
277
+ cursors: [...snapshot.cursors],
278
+ setCursor,
279
+ clearCursor
280
+ };
281
+ }
282
+
283
+ // src/hooks/use-presence.ts
284
+ var import_react7 = require("react");
285
+ function usePresence(user) {
286
+ const { syncEngine } = useKoraContext();
287
+ const name = user?.name ?? null;
288
+ const color = user?.color ?? null;
289
+ const avatar = user?.avatar ?? null;
290
+ (0, import_react7.useEffect)(() => {
291
+ if (!syncEngine || !name || !color) return;
623
292
  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();
293
+ awareness.setLocalState({
294
+ user: { name, color, avatar: avatar ?? void 0 }
646
295
  });
647
- updateCursors();
648
296
  return () => {
649
- unsubscribe();
650
- clearCursor();
297
+ awareness.setLocalState(null);
651
298
  };
652
- }, [clearCursor, collectionName, fieldName, recordId, syncEngine]);
653
- return { doc, text, undo, redo, canUndo, canRedo, ready, error, cursors, setCursor, clearCursor };
654
- }
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
- }
299
+ }, [syncEngine, name, color, avatar]);
669
300
  }
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;
301
+
302
+ // src/hooks/use-collaborators.ts
303
+ var import_sync2 = require("@korajs/sync");
304
+ var import_react8 = require("react");
305
+ var EMPTY_ARRAY2 = [];
306
+ function useCollaborators() {
307
+ const { syncEngine } = useKoraContext();
308
+ const snapshotRef = (0, import_react8.useRef)(EMPTY_ARRAY2);
309
+ const subscribe = (0, import_react8.useCallback)(
310
+ (onStoreChange) => {
311
+ if (!syncEngine) {
312
+ snapshotRef.current = EMPTY_ARRAY2;
313
+ return () => {
314
+ };
315
+ }
316
+ const awareness = syncEngine.getAwarenessManager();
317
+ return (0, import_sync2.subscribeRemoteAwarenessStates)(awareness, (states) => {
318
+ snapshotRef.current = states;
319
+ onStoreChange();
320
+ });
321
+ },
322
+ [syncEngine]
323
+ );
324
+ const getSnapshot = (0, import_react8.useCallback)(() => snapshotRef.current, []);
325
+ (0, import_react8.useEffect)(() => {
326
+ if (!syncEngine) {
327
+ snapshotRef.current = EMPTY_ARRAY2;
687
328
  }
688
- }
689
- return true;
329
+ }, [syncEngine]);
330
+ return (0, import_react8.useSyncExternalStore)(subscribe, getSnapshot);
690
331
  }
691
332
  // Annotate the CommonJS export names for ESM import in node:
692
333
  0 && (module.exports = {
693
334
  KoraProvider,
694
335
  useApp,
336
+ useCollaborators,
695
337
  useCollection,
696
338
  useMutation,
339
+ usePresence,
697
340
  useQuery,
698
341
  useRichText,
699
342
  useSyncStatus