@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 +205 -530
- 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 +205 -522
- 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,144 +131,85 @@ function useQuery(query, options) {
|
|
|
204
131
|
}
|
|
205
132
|
|
|
206
133
|
// src/hooks/use-mutation.ts
|
|
207
|
-
import {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
134
|
+
import { createMutationController } from "@korajs/core/bindings";
|
|
135
|
+
import { useRef as useRef4, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
136
|
+
|
|
137
|
+
// src/hooks/use-controller.ts
|
|
138
|
+
import { useEffect as useEffect3, useReducer, useRef as useRef3 } from "react";
|
|
139
|
+
function useController(create, destroy, deps) {
|
|
140
|
+
const controllerRef = useRef3(null);
|
|
141
|
+
const createRef = useRef3(create);
|
|
142
|
+
createRef.current = create;
|
|
143
|
+
const [, forceRender] = useReducer((count) => count + 1, 0);
|
|
144
|
+
const getController = useRef3(() => {
|
|
145
|
+
if (controllerRef.current === null) {
|
|
146
|
+
controllerRef.current = createRef.current();
|
|
147
|
+
}
|
|
148
|
+
return controllerRef.current;
|
|
149
|
+
}).current;
|
|
150
|
+
useEffect3(() => {
|
|
151
|
+
if (controllerRef.current === null) {
|
|
152
|
+
controllerRef.current = createRef.current();
|
|
153
|
+
forceRender();
|
|
154
|
+
}
|
|
216
155
|
return () => {
|
|
217
|
-
|
|
156
|
+
const controller = controllerRef.current;
|
|
157
|
+
controllerRef.current = null;
|
|
158
|
+
if (controller !== null) {
|
|
159
|
+
destroy(controller);
|
|
160
|
+
}
|
|
218
161
|
};
|
|
219
|
-
},
|
|
220
|
-
|
|
162
|
+
}, deps);
|
|
163
|
+
return getController;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/hooks/use-mutation.ts
|
|
167
|
+
function useMutation(mutationFn, options) {
|
|
168
|
+
const fnRef = useRef4(mutationFn);
|
|
221
169
|
fnRef.current = mutationFn;
|
|
222
|
-
const optionsRef =
|
|
170
|
+
const optionsRef = useRef4(options);
|
|
223
171
|
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]
|
|
172
|
+
const getController = useController(
|
|
173
|
+
() => createMutationController({
|
|
174
|
+
mutationFn: (...args) => fnRef.current(...args),
|
|
175
|
+
resolveOptions: () => optionsRef.current
|
|
176
|
+
}),
|
|
177
|
+
(controller) => controller.destroy(),
|
|
178
|
+
[]
|
|
179
|
+
);
|
|
180
|
+
const state = useSyncExternalStore2(
|
|
181
|
+
(onStoreChange) => getController().subscribe(onStoreChange),
|
|
182
|
+
() => getController().getSnapshot(),
|
|
183
|
+
() => getController().getSnapshot()
|
|
260
184
|
);
|
|
261
|
-
const reset = useCallback(() => {
|
|
262
|
-
if (mountedRef.current) {
|
|
263
|
-
setState({ isLoading: false, error: null });
|
|
264
|
-
}
|
|
265
|
-
}, []);
|
|
266
185
|
return {
|
|
267
|
-
mutate,
|
|
268
|
-
mutateAsync,
|
|
186
|
+
mutate: (...args) => getController().mutate(...args),
|
|
187
|
+
mutateAsync: (...args) => getController().mutateAsync(...args),
|
|
269
188
|
isLoading: state.isLoading,
|
|
270
189
|
error: state.error,
|
|
271
|
-
reset
|
|
190
|
+
reset: () => getController().reset()
|
|
272
191
|
};
|
|
273
192
|
}
|
|
274
193
|
|
|
275
194
|
// 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
|
-
];
|
|
195
|
+
import { createSyncStatusController } from "@korajs/sync";
|
|
196
|
+
import { useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
297
197
|
function useSyncStatus() {
|
|
298
198
|
const { syncEngine, subscribeSyncStatus, events } = useKoraContext();
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
},
|
|
199
|
+
const getController = useController(
|
|
200
|
+
() => createSyncStatusController({
|
|
201
|
+
syncEngine,
|
|
202
|
+
subscribeSyncStatus,
|
|
203
|
+
events: subscribeSyncStatus ? null : events
|
|
204
|
+
}),
|
|
205
|
+
(controller) => controller.destroy(),
|
|
336
206
|
[syncEngine, subscribeSyncStatus, events]
|
|
337
207
|
);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}, [syncEngine, subscribeSyncStatus]);
|
|
344
|
-
return useSyncExternalStore2(subscribe, getSnapshot);
|
|
208
|
+
return useSyncExternalStore3(
|
|
209
|
+
(onStoreChange) => getController().subscribe(onStoreChange),
|
|
210
|
+
() => getController().getSnapshot(),
|
|
211
|
+
() => getController().getSnapshot()
|
|
212
|
+
);
|
|
345
213
|
}
|
|
346
214
|
|
|
347
215
|
// src/hooks/use-collection.ts
|
|
@@ -354,303 +222,118 @@ function useCollection(name) {
|
|
|
354
222
|
}
|
|
355
223
|
|
|
356
224
|
// 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;
|
|
225
|
+
import { asRichTextSyncEngine, createRichTextController } from "@korajs/store";
|
|
226
|
+
import { useCallback, useEffect as useEffect4, useMemo as useMemo3, useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
366
227
|
function useRichText(collectionName, recordId, fieldName, options) {
|
|
367
228
|
const { store, syncEngine } = useKoraContext();
|
|
368
|
-
const collection = useMemo3(
|
|
369
|
-
|
|
370
|
-
|
|
229
|
+
const collection = useMemo3(() => store.collection(collectionName), [store, collectionName]);
|
|
230
|
+
const getController = useController(
|
|
231
|
+
() => createRichTextController({
|
|
232
|
+
collection,
|
|
233
|
+
collectionName,
|
|
234
|
+
recordId,
|
|
235
|
+
fieldName,
|
|
236
|
+
store,
|
|
237
|
+
syncEngine: asRichTextSyncEngine(syncEngine),
|
|
238
|
+
useDocChannel: options?.useDocChannel,
|
|
239
|
+
user: options?.user
|
|
240
|
+
}),
|
|
241
|
+
(controller) => controller.destroy(),
|
|
242
|
+
[collection, collectionName, fieldName, options?.useDocChannel, recordId, store, syncEngine]
|
|
371
243
|
);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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]
|
|
244
|
+
useEffect4(() => {
|
|
245
|
+
getController().setUser(options?.user);
|
|
246
|
+
}, [getController, options?.user]);
|
|
247
|
+
const snapshot = useSyncExternalStore4(
|
|
248
|
+
(onStoreChange) => getController().subscribe(onStoreChange),
|
|
249
|
+
() => getController().getSnapshot(),
|
|
250
|
+
() => getController().getSnapshot()
|
|
434
251
|
);
|
|
435
|
-
const
|
|
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]
|
|
252
|
+
const undo = useCallback(() => {
|
|
253
|
+
getController().undo();
|
|
254
|
+
}, [getController]);
|
|
255
|
+
const redo = useCallback(() => {
|
|
256
|
+
getController().redo();
|
|
257
|
+
}, [getController]);
|
|
258
|
+
const setCursor = useCallback(
|
|
259
|
+
(anchor, head) => getController().setCursor(anchor, head),
|
|
260
|
+
[getController]
|
|
465
261
|
);
|
|
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
|
-
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;
|
|
262
|
+
const clearCursor = useCallback(() => getController().clearCursor(), [getController]);
|
|
263
|
+
return buildResult(getController(), snapshot, undo, redo, setCursor, clearCursor);
|
|
264
|
+
}
|
|
265
|
+
function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
|
|
266
|
+
return {
|
|
267
|
+
doc: controller.doc,
|
|
268
|
+
text: controller.text,
|
|
269
|
+
undo,
|
|
270
|
+
redo,
|
|
271
|
+
canUndo: snapshot.canUndo,
|
|
272
|
+
canRedo: snapshot.canRedo,
|
|
273
|
+
ready: snapshot.ready,
|
|
274
|
+
error: snapshot.error,
|
|
275
|
+
cursors: [...snapshot.cursors],
|
|
276
|
+
setCursor,
|
|
277
|
+
clearCursor
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/hooks/use-presence.ts
|
|
282
|
+
import { useEffect as useEffect5 } from "react";
|
|
283
|
+
function usePresence(user) {
|
|
284
|
+
const { syncEngine } = useKoraContext();
|
|
285
|
+
const name = user?.name ?? null;
|
|
286
|
+
const color = user?.color ?? null;
|
|
287
|
+
const avatar = user?.avatar ?? null;
|
|
288
|
+
useEffect5(() => {
|
|
289
|
+
if (!syncEngine || !name || !color) return;
|
|
581
290
|
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();
|
|
291
|
+
awareness.setLocalState({
|
|
292
|
+
user: { name, color, avatar: avatar ?? void 0 }
|
|
604
293
|
});
|
|
605
|
-
updateCursors();
|
|
606
294
|
return () => {
|
|
607
|
-
|
|
608
|
-
clearCursor();
|
|
295
|
+
awareness.setLocalState(null);
|
|
609
296
|
};
|
|
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
|
-
}
|
|
297
|
+
}, [syncEngine, name, color, avatar]);
|
|
627
298
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return
|
|
299
|
+
|
|
300
|
+
// src/hooks/use-collaborators.ts
|
|
301
|
+
import { subscribeRemoteAwarenessStates } from "@korajs/sync";
|
|
302
|
+
import { useCallback as useCallback2, useEffect as useEffect6, useRef as useRef5, useSyncExternalStore as useSyncExternalStore5 } from "react";
|
|
303
|
+
var EMPTY_ARRAY2 = [];
|
|
304
|
+
function useCollaborators() {
|
|
305
|
+
const { syncEngine } = useKoraContext();
|
|
306
|
+
const snapshotRef = useRef5(EMPTY_ARRAY2);
|
|
307
|
+
const subscribe = useCallback2(
|
|
308
|
+
(onStoreChange) => {
|
|
309
|
+
if (!syncEngine) {
|
|
310
|
+
snapshotRef.current = EMPTY_ARRAY2;
|
|
311
|
+
return () => {
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const awareness = syncEngine.getAwarenessManager();
|
|
315
|
+
return subscribeRemoteAwarenessStates(awareness, (states) => {
|
|
316
|
+
snapshotRef.current = states;
|
|
317
|
+
onStoreChange();
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
[syncEngine]
|
|
321
|
+
);
|
|
322
|
+
const getSnapshot = useCallback2(() => snapshotRef.current, []);
|
|
323
|
+
useEffect6(() => {
|
|
324
|
+
if (!syncEngine) {
|
|
325
|
+
snapshotRef.current = EMPTY_ARRAY2;
|
|
645
326
|
}
|
|
646
|
-
}
|
|
647
|
-
return
|
|
327
|
+
}, [syncEngine]);
|
|
328
|
+
return useSyncExternalStore5(subscribe, getSnapshot);
|
|
648
329
|
}
|
|
649
330
|
export {
|
|
650
331
|
KoraProvider,
|
|
651
332
|
useApp,
|
|
333
|
+
useCollaborators,
|
|
652
334
|
useCollection,
|
|
653
335
|
useMutation,
|
|
336
|
+
usePresence,
|
|
654
337
|
useQuery,
|
|
655
338
|
useRichText,
|
|
656
339
|
useSyncStatus
|