@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 +165 -522
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -162
- package/dist/index.d.ts +27 -162
- package/dist/index.js +174 -523
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/context/kora-context.ts
|
|
2
|
-
import {
|
|
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 (!
|
|
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
|
-
|
|
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 {
|
|
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
|
|
178
|
-
|
|
179
|
-
const queryStore =
|
|
109
|
+
const queryRef = useRef2(query);
|
|
110
|
+
queryRef.current = query;
|
|
111
|
+
const [queryStore, setQueryStore] = useState2(null);
|
|
112
|
+
useEffect2(() => {
|
|
180
113
|
if (!enabled) {
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
139
|
+
const optionsRef = useRef3(options);
|
|
223
140
|
optionsRef.current = options;
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 {
|
|
277
|
-
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
185
|
+
import { useMemo as useMemo4 } from "react";
|
|
349
186
|
function useCollection(name) {
|
|
350
187
|
const { store } = useKoraContext();
|
|
351
|
-
return
|
|
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 {
|
|
358
|
-
import { useCallback
|
|
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 =
|
|
198
|
+
const collection = useMemo5(
|
|
369
199
|
() => store.collection(collectionName),
|
|
370
200
|
[store, collectionName]
|
|
371
201
|
);
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
583
|
-
|
|
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
|
-
|
|
608
|
-
clearCursor();
|
|
263
|
+
awareness.setLocalState(null);
|
|
609
264
|
};
|
|
610
|
-
}, [
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|
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
|