@korajs/react 1.0.0-beta.12 → 1.0.0-beta.14
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/README.md +48 -40
- package/dist/index.cjs +169 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -5
- package/dist/index.d.ts +159 -5
- package/dist/index.js +167 -63
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KoraAppLike as KoraAppLike$1, KoraContextValue as KoraContextValue$1,
|
|
1
|
+
import { KoraAppLike as KoraAppLike$1, KoraContextValue as KoraContextValue$1, UseQueryOptions as UseQueryOptions$1, UseMutationOptions as UseMutationOptions$1, UseMutationResultBase, AuthSyncBinding, AuthSyncState } from '@korajs/core/bindings';
|
|
2
2
|
import { Store, QueryStoreCache, CollectionRecord, QueryBuilder, CollectionAccessor } from '@korajs/store';
|
|
3
3
|
import { SyncEngine, CursorInfo, SyncStatusInfo, AwarenessUser, AwarenessState } from '@korajs/sync';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
@@ -6,7 +6,24 @@ import * as Y from 'yjs';
|
|
|
6
6
|
|
|
7
7
|
type KoraAppLike = KoraAppLike$1<Store, SyncEngine, QueryStoreCache>;
|
|
8
8
|
type KoraContextValue = KoraContextValue$1<Store, SyncEngine, QueryStoreCache>;
|
|
9
|
-
|
|
9
|
+
/** Options for {@link useQuery} and {@link useQueryState}. */
|
|
10
|
+
interface UseQueryOptions extends UseQueryOptions$1 {
|
|
11
|
+
/**
|
|
12
|
+
* When true (default), `useQuery` throws a failed query to the nearest error
|
|
13
|
+
* boundary. When false it keeps returning the last good rows; read the error with
|
|
14
|
+
* `useQueryState`. Ignored by `useQueryState`, which always returns the error.
|
|
15
|
+
*/
|
|
16
|
+
throwOnError?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Result of `useQueryState`. */
|
|
19
|
+
interface UseQueryStateResult<T> {
|
|
20
|
+
/** The current rows (the last good rows while `error` is set). */
|
|
21
|
+
data: readonly T[];
|
|
22
|
+
/** The query's failure, or null. Cleared when results flow again. */
|
|
23
|
+
error: Error | null;
|
|
24
|
+
/** False until the first result for the current query arrived (always false on the server). */
|
|
25
|
+
ready: boolean;
|
|
26
|
+
}
|
|
10
27
|
type UseMutationOptions<TData, TArgs extends unknown[], TContext = void> = UseMutationOptions$1<TData, TArgs, TContext>;
|
|
11
28
|
/**
|
|
12
29
|
* Props for the KoraProvider component.
|
|
@@ -37,6 +54,15 @@ interface UseRichTextResult {
|
|
|
37
54
|
cursors: CursorInfo[];
|
|
38
55
|
setCursor: (anchor: number, head: number) => void;
|
|
39
56
|
clearCursor: () => void;
|
|
57
|
+
/**
|
|
58
|
+
* True while local edits are not saved yet (a save pending or refused; see
|
|
59
|
+
* `error`). The edits stay in the document and the next edit saves them again.
|
|
60
|
+
*/
|
|
61
|
+
hasUnsavedChanges: boolean;
|
|
62
|
+
/** Save the document now, for example after a refused save. */
|
|
63
|
+
retrySave: () => Promise<void>;
|
|
64
|
+
/** The full document state while edits are unsaved (for a recovery copy), else null. */
|
|
65
|
+
getUnsavedState: () => Uint8Array | null;
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
/**
|
|
@@ -53,6 +79,13 @@ interface AuthBoundKoraProviderProps {
|
|
|
53
79
|
close(): Promise<void>;
|
|
54
80
|
};
|
|
55
81
|
signedOut?: ReactNode;
|
|
82
|
+
/**
|
|
83
|
+
* Rendered instead of the app while the session is locked (offline longer
|
|
84
|
+
* than the auth client's `maxOfflineGraceMs`, or the device clock moved
|
|
85
|
+
* backwards). The app and its local data stay open underneath; nothing is
|
|
86
|
+
* wiped. Defaults to `signedOut`.
|
|
87
|
+
*/
|
|
88
|
+
locked?: ReactNode;
|
|
56
89
|
fallback?: ReactNode;
|
|
57
90
|
error?: (context: AuthBoundKoraErrorContext) => ReactNode;
|
|
58
91
|
children?: ReactNode;
|
|
@@ -83,7 +116,7 @@ declare function classifyKoraInitializationError(error: unknown): AuthBoundKoraI
|
|
|
83
116
|
* The previous app is fully closed before a different user's app is created,
|
|
84
117
|
* and no stale provider tree is rendered across that boundary.
|
|
85
118
|
*/
|
|
86
|
-
declare function AuthBoundKoraProvider({ authClient, createApp, signedOut, fallback, error: renderError, children, }: AuthBoundKoraProviderProps): ReactNode;
|
|
119
|
+
declare function AuthBoundKoraProvider({ authClient, createApp, signedOut, locked, fallback, error: renderError, children, }: AuthBoundKoraProviderProps): ReactNode;
|
|
87
120
|
|
|
88
121
|
/**
|
|
89
122
|
* React hook that returns the Kora app instance from context.
|
|
@@ -110,19 +143,137 @@ declare function useApp<T extends KoraAppLike = KoraAppLike>(): T;
|
|
|
110
143
|
|
|
111
144
|
/**
|
|
112
145
|
* React hook for reactive queries against the local Kora store.
|
|
146
|
+
*
|
|
147
|
+
* Returns the current rows synchronously (no loading state for local data) and
|
|
148
|
+
* re-renders only when the result set changes. The array keeps its identity while
|
|
149
|
+
* the result is unchanged. On the server (`renderToString`, Next.js App Router) and
|
|
150
|
+
* during hydration it returns `[]`; the rows arrive right after hydration.
|
|
151
|
+
*
|
|
152
|
+
* A query that fails (for example a where/orderBy on an unknown field) is thrown to
|
|
153
|
+
* the nearest React error boundary, so the failure is visible instead of an empty
|
|
154
|
+
* list. Pass `throwOnError: false`, or use {@link useQueryState}, to handle it inline.
|
|
155
|
+
*
|
|
156
|
+
* @param query - A query builder, for example `app.todos.where({ completed: false })`
|
|
157
|
+
* @param options - `enabled` (default true) and `throwOnError` (default true)
|
|
158
|
+
* @returns The current result rows
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```tsx
|
|
162
|
+
* const todos = useQuery(app.todos.where({ completed: false }).orderBy('createdAt'))
|
|
163
|
+
* ```
|
|
113
164
|
*/
|
|
114
165
|
declare function useQuery<T = CollectionRecord>(query: QueryBuilder<T>, options?: UseQueryOptions): readonly T[];
|
|
166
|
+
/**
|
|
167
|
+
* Like {@link useQuery}, but returns the error instead of throwing it.
|
|
168
|
+
*
|
|
169
|
+
* @param query - A query builder
|
|
170
|
+
* @param options - `enabled` (default true); `throwOnError` is ignored
|
|
171
|
+
* @returns `{ data, error, ready }`, the same object until one of them changes.
|
|
172
|
+
* `ready` is false until the first result for the current query has arrived.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```tsx
|
|
176
|
+
* const { data: todos, error } = useQueryState(app.todos.where({ completed: false }))
|
|
177
|
+
* if (error) return <p role="alert">{error.message}</p>
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
declare function useQueryState<T = CollectionRecord>(query: QueryBuilder<T>, options?: UseQueryOptions): UseQueryStateResult<T>;
|
|
115
181
|
|
|
116
182
|
/**
|
|
117
183
|
* React hook for performing mutations against the local Kora store.
|
|
184
|
+
*
|
|
185
|
+
* `mutate`, `mutateAsync` and `reset` keep their identity for the component's lifetime
|
|
186
|
+
* (safe in effect deps and as memoized props), and the result object only changes when
|
|
187
|
+
* `isLoading` or `error` does. The latest `mutationFn` and `options` are always used.
|
|
188
|
+
*
|
|
189
|
+
* @param mutationFn - The mutation to run, for example `app.todos.insert`
|
|
190
|
+
* @param options - Optional lifecycle callbacks (`onMutate`, `onSuccess`, `onError`, `onSettled`)
|
|
191
|
+
* @returns `{ mutate, mutateAsync, isLoading, error, reset }`
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* const { mutate: addTodo, isLoading } = useMutation(app.todos.insert)
|
|
196
|
+
* return <button disabled={isLoading} onClick={() => addTodo({ title: 'New' })}>Add</button>
|
|
197
|
+
* ```
|
|
118
198
|
*/
|
|
119
199
|
declare function useMutation<TData, TArgs extends unknown[], TContext = void>(mutationFn: (...args: TArgs) => Promise<TData>, options?: UseMutationOptions<TData, TArgs, TContext>): UseMutationResult<TData, TArgs>;
|
|
120
200
|
|
|
121
201
|
/**
|
|
122
202
|
* React hook for monitoring the sync engine's connection status.
|
|
203
|
+
*
|
|
204
|
+
* Re-renders only when the status payload changes. The returned object, and its nested
|
|
205
|
+
* `heldNodes`, `initialSync` and `blockedFailure` values, keep their identity while
|
|
206
|
+
* unchanged, so they are safe as effect or memo dependencies. Beyond `status` and
|
|
207
|
+
* `pendingOperations` it reports `heldOperations` / `heldNodes` (writes of another
|
|
208
|
+
* user waiting on this device), `localDurability` (`'degraded'` when the local
|
|
209
|
+
* database cannot persist) and `serverProtocolVersion` / `protocolDeprecated`.
|
|
210
|
+
*
|
|
211
|
+
* @returns The current {@link SyncStatusInfo}
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```tsx
|
|
215
|
+
* const { status, pendingOperations, localDurability } = useSyncStatus()
|
|
216
|
+
* ```
|
|
123
217
|
*/
|
|
124
218
|
declare function useSyncStatus(): SyncStatusInfo;
|
|
125
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The collections an app exposes, read from its `collections` namespace. Purely
|
|
222
|
+
* structural, so it follows whatever accessor types `createApp` infers from the schema.
|
|
223
|
+
*/
|
|
224
|
+
type AppCollections<TApp> = TApp extends {
|
|
225
|
+
collections: infer C;
|
|
226
|
+
} ? C : never;
|
|
227
|
+
/** The collection names of an app, as string literals. */
|
|
228
|
+
type AppCollectionName<TApp> = Extract<keyof AppCollections<TApp>, string>;
|
|
229
|
+
/** The record type of a collection accessor, inferred from what its queries return. */
|
|
230
|
+
type AccessorRecord<TAccessor> = TAccessor extends {
|
|
231
|
+
where(...args: never[]): QueryBuilder<infer R>;
|
|
232
|
+
} ? R : CollectionRecord;
|
|
233
|
+
/** The record type of collection `N` of an app. */
|
|
234
|
+
type AppRecord<TApp, N extends AppCollectionName<TApp>> = AccessorRecord<AppCollections<TApp>[N]>;
|
|
235
|
+
/**
|
|
236
|
+
* Hooks bound to one app type, returned by {@link createKoraHooks}.
|
|
237
|
+
*/
|
|
238
|
+
interface KoraHooks<TApp> {
|
|
239
|
+
/** The app from the nearest `<KoraProvider app={app}>`, typed as `TApp`. */
|
|
240
|
+
useApp: () => TApp;
|
|
241
|
+
/**
|
|
242
|
+
* The typed accessor of one collection: `insert`, `update` and `where` are checked
|
|
243
|
+
* against the schema. The accessor keeps its identity across renders.
|
|
244
|
+
*/
|
|
245
|
+
useCollection: <N extends AppCollectionName<TApp>>(name: N) => AppCollections<TApp>[N];
|
|
246
|
+
/** {@link useQuery}: rows are typed from the query builder. */
|
|
247
|
+
useQuery: typeof useQuery;
|
|
248
|
+
/** {@link useQueryState}: rows are typed from the query builder. */
|
|
249
|
+
useQueryState: typeof useQueryState;
|
|
250
|
+
/** {@link useMutation}. */
|
|
251
|
+
useMutation: typeof useMutation;
|
|
252
|
+
/** {@link useSyncStatus}. */
|
|
253
|
+
useSyncStatus: typeof useSyncStatus;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Creates React hooks typed for one app, so components get schema-checked
|
|
257
|
+
* collection names, inserts, updates and rows without passing generics around.
|
|
258
|
+
* Call it once next to `createApp` and import the hooks from there. Nothing runs at
|
|
259
|
+
* call time; the hooks read the app from `<KoraProvider app={app}>`.
|
|
260
|
+
*
|
|
261
|
+
* @returns {@link KoraHooks} for `TApp`
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* // kora.ts
|
|
266
|
+
* export const app = createApp({ schema })
|
|
267
|
+
* export const { useCollection, useQuery, useMutation } = createKoraHooks<typeof app>()
|
|
268
|
+
*
|
|
269
|
+
* // TodoList.tsx
|
|
270
|
+
* const todos = useCollection('todos') // 'todoz' is a type error
|
|
271
|
+
* const rows = useQuery(todos.where({ completed: false }))
|
|
272
|
+
* const { mutate: add } = useMutation(todos.insert)
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
declare function createKoraHooks<TApp extends KoraAppLike>(): KoraHooks<TApp>;
|
|
276
|
+
|
|
126
277
|
/**
|
|
127
278
|
* React hook that returns a CollectionAccessor for the given collection name.
|
|
128
279
|
* Convenience hook for accessing a collection without going through the store directly.
|
|
@@ -161,8 +312,11 @@ declare function usePresence(user: {
|
|
|
161
312
|
/**
|
|
162
313
|
* Returns all currently connected collaborators' awareness states.
|
|
163
314
|
*
|
|
164
|
-
* Excludes the local user — only returns remote peers.
|
|
315
|
+
* Excludes the local user — only returns remote peers. Safe under server rendering
|
|
316
|
+
* (`renderToString`, Next.js App Router): the server and the hydration pass see `[]`.
|
|
317
|
+
*
|
|
318
|
+
* @returns The remote peers' awareness states; the same array until they change
|
|
165
319
|
*/
|
|
166
320
|
declare function useCollaborators(): AwarenessState[];
|
|
167
321
|
|
|
168
|
-
export { type AuthBoundKoraErrorContext, type AuthBoundKoraInitializationError, AuthBoundKoraProvider, type AuthBoundKoraProviderProps, type AuthBoundKoraSession, type KoraAppLike, type KoraContextValue, KoraProvider, type KoraProviderProps, type UseMutationOptions, type UseMutationResult, type UseQueryOptions, type UseRichTextOptions, type UseRichTextResult, classifyKoraInitializationError, useApp, useCollaborators, useCollection, useMutation, usePresence, useQuery, useRichText, useSyncStatus };
|
|
322
|
+
export { type AccessorRecord, type AppCollectionName, type AppCollections, type AppRecord, type AuthBoundKoraErrorContext, type AuthBoundKoraInitializationError, AuthBoundKoraProvider, type AuthBoundKoraProviderProps, type AuthBoundKoraSession, type KoraAppLike, type KoraContextValue, type KoraHooks, KoraProvider, type KoraProviderProps, type UseMutationOptions, type UseMutationResult, type UseQueryOptions, type UseQueryStateResult, type UseRichTextOptions, type UseRichTextResult, classifyKoraInitializationError, createKoraHooks, useApp, useCollaborators, useCollection, useMutation, usePresence, useQuery, useQueryState, useRichText, useSyncStatus };
|
package/dist/index.js
CHANGED
|
@@ -123,6 +123,7 @@ function AuthBoundKoraProvider({
|
|
|
123
123
|
authClient,
|
|
124
124
|
createApp,
|
|
125
125
|
signedOut = null,
|
|
126
|
+
locked,
|
|
126
127
|
fallback = null,
|
|
127
128
|
error: renderError,
|
|
128
129
|
children
|
|
@@ -216,6 +217,7 @@ function AuthBoundKoraProvider({
|
|
|
216
217
|
);
|
|
217
218
|
}
|
|
218
219
|
if (state.state === "signed-out" || state.state === "anonymous") return signedOut;
|
|
220
|
+
if (state.state === "authenticated" && state.locked) return locked ?? signedOut;
|
|
219
221
|
if (!app) return fallback;
|
|
220
222
|
return createElement2(KoraProvider, { app, fallback }, children);
|
|
221
223
|
}
|
|
@@ -232,17 +234,24 @@ function useApp() {
|
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
// src/hooks/use-query.ts
|
|
235
|
-
import { assertQueryReady } from "@korajs/store";
|
|
236
|
-
import { useCallback, useEffect as useEffect3, useRef as useRef3, useState as useState3, useSyncExternalStore } from "react";
|
|
237
|
+
import { assertQueryReady, queryKey } from "@korajs/store";
|
|
238
|
+
import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3, useSyncExternalStore } from "react";
|
|
237
239
|
var EMPTY_ARRAY = Object.freeze([]);
|
|
238
240
|
var noopSubscribe = (_onStoreChange) => {
|
|
239
241
|
return () => {
|
|
240
242
|
};
|
|
241
243
|
};
|
|
242
|
-
|
|
244
|
+
var getServerSnapshot = () => EMPTY_ARRAY;
|
|
245
|
+
var getNoError = () => null;
|
|
246
|
+
function readQueryError(queryStore) {
|
|
247
|
+
if (!queryStore) return null;
|
|
248
|
+
const channel = queryStore;
|
|
249
|
+
return typeof channel.getError === "function" ? channel.getError() : null;
|
|
250
|
+
}
|
|
251
|
+
function useQuerySubscription(query, options) {
|
|
243
252
|
const { queryStoreCache } = useKoraContext();
|
|
244
253
|
const enabled = options?.enabled !== false;
|
|
245
|
-
const descriptorKey =
|
|
254
|
+
const descriptorKey = queryKey(query.getDescriptor());
|
|
246
255
|
const queryRef = useRef3(query);
|
|
247
256
|
queryRef.current = query;
|
|
248
257
|
const lastSnapshotRef = useRef3(EMPTY_ARRAY);
|
|
@@ -260,122 +269,204 @@ function useQuery(query, options) {
|
|
|
260
269
|
queryStoreCache.release(currentQuery);
|
|
261
270
|
};
|
|
262
271
|
}, [descriptorKey, enabled, queryStoreCache]);
|
|
272
|
+
const subscribe = enabled && queryStore ? queryStore.subscribe : noopSubscribe;
|
|
263
273
|
const getSnapshot = useCallback(() => {
|
|
264
274
|
if (!enabled) {
|
|
265
275
|
lastSnapshotRef.current = EMPTY_ARRAY;
|
|
266
276
|
return lastSnapshotRef.current;
|
|
267
277
|
}
|
|
268
|
-
if (!queryStore) {
|
|
269
|
-
return lastSnapshotRef.current;
|
|
270
|
-
}
|
|
271
|
-
if (!queryStore.hasSnapshot()) {
|
|
278
|
+
if (!queryStore || !queryStore.hasSnapshot()) {
|
|
272
279
|
return lastSnapshotRef.current;
|
|
273
280
|
}
|
|
274
281
|
const snapshot = queryStore.getSnapshot();
|
|
275
282
|
lastSnapshotRef.current = snapshot;
|
|
276
283
|
return snapshot;
|
|
277
284
|
}, [enabled, queryStore]);
|
|
278
|
-
|
|
285
|
+
const getError = useCallback(
|
|
286
|
+
() => enabled ? readQueryError(queryStore) : null,
|
|
287
|
+
[enabled, queryStore]
|
|
288
|
+
);
|
|
289
|
+
const data = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
290
|
+
const error = useSyncExternalStore(subscribe, getError, getNoError);
|
|
291
|
+
const ready = enabled && queryStore !== null && queryStore.hasSnapshot();
|
|
292
|
+
return { data, error, ready };
|
|
293
|
+
}
|
|
294
|
+
function useQuery(query, options) {
|
|
295
|
+
const { data, error } = useQuerySubscription(query, options);
|
|
296
|
+
if (error && options?.throwOnError !== false) {
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
return data;
|
|
279
300
|
}
|
|
301
|
+
function useQueryState(query, options) {
|
|
302
|
+
const { data, error, ready } = useQuerySubscription(query, options);
|
|
303
|
+
return useMemo2(() => ({ data, error, ready }), [data, error, ready]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/hooks/create-kora-hooks.ts
|
|
307
|
+
import { useMemo as useMemo4 } from "react";
|
|
280
308
|
|
|
281
309
|
// src/hooks/use-mutation.ts
|
|
282
310
|
import { createMutationController } from "@korajs/core/bindings";
|
|
283
|
-
import { useRef as useRef5, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
311
|
+
import { useCallback as useCallback2, useMemo as useMemo3, useRef as useRef5, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
284
312
|
|
|
285
313
|
// src/hooks/use-controller.ts
|
|
286
314
|
import { useEffect as useEffect4, useReducer, useRef as useRef4 } from "react";
|
|
315
|
+
var ORPHAN_REAP_MS = 1e3;
|
|
287
316
|
function useController(create, destroy, deps) {
|
|
288
|
-
const
|
|
317
|
+
const entryRef = useRef4(null);
|
|
289
318
|
const createRef = useRef4(create);
|
|
290
319
|
createRef.current = create;
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
const destroyRef = useRef4(destroy);
|
|
321
|
+
destroyRef.current = destroy;
|
|
322
|
+
const [version, bumpVersion] = useReducer((count) => count + 1, 0);
|
|
323
|
+
const handle = useRef4({
|
|
324
|
+
get: () => {
|
|
325
|
+
const current = entryRef.current;
|
|
326
|
+
if (current !== null && !current.reaped) {
|
|
327
|
+
return current.controller;
|
|
328
|
+
}
|
|
329
|
+
const entry = {
|
|
330
|
+
controller: createRef.current(),
|
|
331
|
+
claimed: false,
|
|
332
|
+
reaped: false
|
|
333
|
+
};
|
|
334
|
+
entryRef.current = entry;
|
|
335
|
+
setTimeout(() => {
|
|
336
|
+
if (!entry.claimed && !entry.reaped) {
|
|
337
|
+
entry.reaped = true;
|
|
338
|
+
destroyRef.current(entry.controller);
|
|
339
|
+
}
|
|
340
|
+
}, ORPHAN_REAP_MS);
|
|
341
|
+
return entry.controller;
|
|
342
|
+
},
|
|
343
|
+
peek: () => {
|
|
344
|
+
const current = entryRef.current;
|
|
345
|
+
return current?.claimed && !current.reaped ? current.controller : null;
|
|
295
346
|
}
|
|
296
|
-
return controllerRef.current;
|
|
297
347
|
}).current;
|
|
298
348
|
useEffect4(() => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
349
|
+
const current = entryRef.current;
|
|
350
|
+
if (current !== null && !current.reaped && !current.claimed) {
|
|
351
|
+
current.claimed = true;
|
|
352
|
+
} else if (current === null || current.reaped) {
|
|
353
|
+
entryRef.current = { controller: createRef.current(), claimed: true, reaped: false };
|
|
354
|
+
bumpVersion();
|
|
302
355
|
}
|
|
303
356
|
return () => {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
if (
|
|
307
|
-
|
|
357
|
+
const entry = entryRef.current;
|
|
358
|
+
entryRef.current = null;
|
|
359
|
+
if (entry !== null && !entry.reaped) {
|
|
360
|
+
entry.reaped = true;
|
|
361
|
+
destroyRef.current(entry.controller);
|
|
308
362
|
}
|
|
309
363
|
};
|
|
310
364
|
}, deps);
|
|
311
|
-
return
|
|
365
|
+
return { get: handle.get, peek: handle.peek, version };
|
|
312
366
|
}
|
|
313
367
|
|
|
314
368
|
// src/hooks/use-mutation.ts
|
|
369
|
+
var IDLE = Object.freeze({ isLoading: false, error: null });
|
|
370
|
+
var getServerSnapshot2 = () => IDLE;
|
|
315
371
|
function useMutation(mutationFn, options) {
|
|
316
372
|
const fnRef = useRef5(mutationFn);
|
|
317
373
|
fnRef.current = mutationFn;
|
|
318
374
|
const optionsRef = useRef5(options);
|
|
319
375
|
optionsRef.current = options;
|
|
320
|
-
const
|
|
376
|
+
const controller = useController(
|
|
321
377
|
() => createMutationController({
|
|
322
378
|
mutationFn: (...args) => fnRef.current(...args),
|
|
323
379
|
resolveOptions: () => optionsRef.current
|
|
324
380
|
}),
|
|
325
|
-
(
|
|
381
|
+
(instance) => instance.destroy(),
|
|
326
382
|
[]
|
|
327
383
|
);
|
|
328
|
-
const
|
|
384
|
+
const getController = controller.get;
|
|
385
|
+
const subscribe = useCallback2(
|
|
329
386
|
(onStoreChange) => getController().subscribe(onStoreChange),
|
|
330
|
-
|
|
331
|
-
|
|
387
|
+
[getController, controller.version]
|
|
388
|
+
);
|
|
389
|
+
const getSnapshot = useCallback2(() => getController().getSnapshot(), [getController]);
|
|
390
|
+
const state = useSyncExternalStore2(subscribe, getSnapshot, getServerSnapshot2);
|
|
391
|
+
const mutate = useCallback2((...args) => getController().mutate(...args), [getController]);
|
|
392
|
+
const mutateAsync = useCallback2(
|
|
393
|
+
(...args) => getController().mutateAsync(...args),
|
|
394
|
+
[getController]
|
|
395
|
+
);
|
|
396
|
+
const reset = useCallback2(() => getController().reset(), [getController]);
|
|
397
|
+
return useMemo3(
|
|
398
|
+
() => ({ mutate, mutateAsync, isLoading: state.isLoading, error: state.error, reset }),
|
|
399
|
+
[mutate, mutateAsync, reset, state.isLoading, state.error]
|
|
332
400
|
);
|
|
333
|
-
return {
|
|
334
|
-
mutate: (...args) => getController().mutate(...args),
|
|
335
|
-
mutateAsync: (...args) => getController().mutateAsync(...args),
|
|
336
|
-
isLoading: state.isLoading,
|
|
337
|
-
error: state.error,
|
|
338
|
-
reset: () => getController().reset()
|
|
339
|
-
};
|
|
340
401
|
}
|
|
341
402
|
|
|
342
403
|
// src/hooks/use-sync-status.ts
|
|
343
|
-
import { createSyncStatusController } from "@korajs/sync";
|
|
344
|
-
import { useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
404
|
+
import { OFFLINE_SYNC_STATUS, createSyncStatusController } from "@korajs/sync";
|
|
405
|
+
import { useCallback as useCallback3, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
406
|
+
var getServerSnapshot3 = () => OFFLINE_SYNC_STATUS;
|
|
345
407
|
function useSyncStatus() {
|
|
346
408
|
const { syncEngine, subscribeSyncStatus, events } = useKoraContext();
|
|
347
|
-
const
|
|
409
|
+
const controller = useController(
|
|
348
410
|
() => createSyncStatusController({
|
|
349
411
|
syncEngine,
|
|
350
412
|
subscribeSyncStatus,
|
|
351
413
|
events: subscribeSyncStatus ? null : events
|
|
352
414
|
}),
|
|
353
|
-
(
|
|
415
|
+
(instance) => instance.destroy(),
|
|
354
416
|
[syncEngine, subscribeSyncStatus, events]
|
|
355
417
|
);
|
|
356
|
-
|
|
418
|
+
const getController = controller.get;
|
|
419
|
+
const subscribe = useCallback3(
|
|
357
420
|
(onStoreChange) => getController().subscribe(onStoreChange),
|
|
358
|
-
|
|
359
|
-
() => getController().getSnapshot()
|
|
421
|
+
[getController, controller.version]
|
|
360
422
|
);
|
|
423
|
+
const peek = controller.peek;
|
|
424
|
+
const getSnapshot = useCallback3(
|
|
425
|
+
() => peek()?.getSnapshot() ?? OFFLINE_SYNC_STATUS,
|
|
426
|
+
[peek]
|
|
427
|
+
);
|
|
428
|
+
return useSyncExternalStore3(subscribe, getSnapshot, getServerSnapshot3);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/hooks/create-kora-hooks.ts
|
|
432
|
+
function createKoraHooks() {
|
|
433
|
+
function useTypedApp() {
|
|
434
|
+
return useApp();
|
|
435
|
+
}
|
|
436
|
+
function useTypedCollection(name) {
|
|
437
|
+
const { app, store } = useKoraContext();
|
|
438
|
+
return useMemo4(() => {
|
|
439
|
+
const collections = app?.collections;
|
|
440
|
+
const accessor = collections && name in collections ? collections[name] : store.collection(name);
|
|
441
|
+
return accessor;
|
|
442
|
+
}, [app, store, name]);
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
useApp: useTypedApp,
|
|
446
|
+
useCollection: useTypedCollection,
|
|
447
|
+
useQuery,
|
|
448
|
+
useQueryState,
|
|
449
|
+
useMutation,
|
|
450
|
+
useSyncStatus
|
|
451
|
+
};
|
|
361
452
|
}
|
|
362
453
|
|
|
363
454
|
// src/hooks/use-collection.ts
|
|
364
|
-
import { useMemo as
|
|
455
|
+
import { useMemo as useMemo5 } from "react";
|
|
365
456
|
function useCollection(name) {
|
|
366
457
|
const { store } = useKoraContext();
|
|
367
|
-
return
|
|
458
|
+
return useMemo5(() => {
|
|
368
459
|
return store.collection(name);
|
|
369
460
|
}, [store, name]);
|
|
370
461
|
}
|
|
371
462
|
|
|
372
463
|
// src/hooks/use-rich-text.ts
|
|
373
464
|
import { asRichTextSyncEngine, createRichTextController } from "@korajs/store";
|
|
374
|
-
import { useCallback as
|
|
465
|
+
import { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo6, useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
375
466
|
function useRichText(collectionName, recordId, fieldName, options) {
|
|
376
467
|
const { store, syncEngine } = useKoraContext();
|
|
377
|
-
const collection =
|
|
378
|
-
const
|
|
468
|
+
const collection = useMemo6(() => store.collection(collectionName), [store, collectionName]);
|
|
469
|
+
const controller = useController(
|
|
379
470
|
() => createRichTextController({
|
|
380
471
|
collection,
|
|
381
472
|
collectionName,
|
|
@@ -386,29 +477,35 @@ function useRichText(collectionName, recordId, fieldName, options) {
|
|
|
386
477
|
useDocChannel: options?.useDocChannel,
|
|
387
478
|
user: options?.user
|
|
388
479
|
}),
|
|
389
|
-
(
|
|
480
|
+
(instance) => instance.destroy(),
|
|
390
481
|
[collection, collectionName, fieldName, options?.useDocChannel, recordId, store, syncEngine]
|
|
391
482
|
);
|
|
483
|
+
const getController = controller.get;
|
|
392
484
|
useEffect5(() => {
|
|
393
485
|
getController().setUser(options?.user);
|
|
394
|
-
}, [getController, options?.user]);
|
|
395
|
-
const
|
|
486
|
+
}, [getController, options?.user, controller.version]);
|
|
487
|
+
const subscribe = useCallback4(
|
|
396
488
|
(onStoreChange) => getController().subscribe(onStoreChange),
|
|
397
|
-
|
|
398
|
-
() => getController().getSnapshot()
|
|
489
|
+
[getController, controller.version]
|
|
399
490
|
);
|
|
400
|
-
const
|
|
491
|
+
const getSnapshot = useCallback4(() => getController().getSnapshot(), [getController]);
|
|
492
|
+
const snapshot = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
|
|
493
|
+
const undo = useCallback4(() => {
|
|
401
494
|
getController().undo();
|
|
402
495
|
}, [getController]);
|
|
403
|
-
const redo =
|
|
496
|
+
const redo = useCallback4(() => {
|
|
404
497
|
getController().redo();
|
|
405
498
|
}, [getController]);
|
|
406
|
-
const setCursor =
|
|
499
|
+
const setCursor = useCallback4(
|
|
407
500
|
(anchor, head) => getController().setCursor(anchor, head),
|
|
408
501
|
[getController]
|
|
409
502
|
);
|
|
410
|
-
const clearCursor =
|
|
411
|
-
|
|
503
|
+
const clearCursor = useCallback4(() => getController().clearCursor(), [getController]);
|
|
504
|
+
const live = getController();
|
|
505
|
+
return useMemo6(
|
|
506
|
+
() => buildResult(live, snapshot, undo, redo, setCursor, clearCursor),
|
|
507
|
+
[live, snapshot, undo, redo, setCursor, clearCursor, controller.version]
|
|
508
|
+
);
|
|
412
509
|
}
|
|
413
510
|
function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
|
|
414
511
|
return {
|
|
@@ -422,7 +519,10 @@ function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
|
|
|
422
519
|
error: snapshot.error,
|
|
423
520
|
cursors: [...snapshot.cursors],
|
|
424
521
|
setCursor,
|
|
425
|
-
clearCursor
|
|
522
|
+
clearCursor,
|
|
523
|
+
hasUnsavedChanges: snapshot.hasUnsavedChanges,
|
|
524
|
+
retrySave: () => controller.retrySave(),
|
|
525
|
+
getUnsavedState: () => controller.getUnsavedState()
|
|
426
526
|
};
|
|
427
527
|
}
|
|
428
528
|
|
|
@@ -447,12 +547,13 @@ function usePresence(user) {
|
|
|
447
547
|
|
|
448
548
|
// src/hooks/use-collaborators.ts
|
|
449
549
|
import { subscribeRemoteAwarenessStates } from "@korajs/sync";
|
|
450
|
-
import { useCallback as
|
|
550
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef6, useSyncExternalStore as useSyncExternalStore5 } from "react";
|
|
451
551
|
var EMPTY_ARRAY2 = [];
|
|
552
|
+
var getServerSnapshot4 = () => EMPTY_ARRAY2;
|
|
452
553
|
function useCollaborators() {
|
|
453
554
|
const { syncEngine } = useKoraContext();
|
|
454
555
|
const snapshotRef = useRef6(EMPTY_ARRAY2);
|
|
455
|
-
const subscribe =
|
|
556
|
+
const subscribe = useCallback5(
|
|
456
557
|
(onStoreChange) => {
|
|
457
558
|
if (!syncEngine) {
|
|
458
559
|
snapshotRef.current = EMPTY_ARRAY2;
|
|
@@ -460,6 +561,7 @@ function useCollaborators() {
|
|
|
460
561
|
};
|
|
461
562
|
}
|
|
462
563
|
const awareness = syncEngine.getAwarenessManager();
|
|
564
|
+
snapshotRef.current = EMPTY_ARRAY2;
|
|
463
565
|
return subscribeRemoteAwarenessStates(awareness, (states) => {
|
|
464
566
|
snapshotRef.current = states;
|
|
465
567
|
onStoreChange();
|
|
@@ -467,24 +569,26 @@ function useCollaborators() {
|
|
|
467
569
|
},
|
|
468
570
|
[syncEngine]
|
|
469
571
|
);
|
|
470
|
-
const getSnapshot =
|
|
572
|
+
const getSnapshot = useCallback5(() => snapshotRef.current, []);
|
|
471
573
|
useEffect7(() => {
|
|
472
574
|
if (!syncEngine) {
|
|
473
575
|
snapshotRef.current = EMPTY_ARRAY2;
|
|
474
576
|
}
|
|
475
577
|
}, [syncEngine]);
|
|
476
|
-
return useSyncExternalStore5(subscribe, getSnapshot);
|
|
578
|
+
return useSyncExternalStore5(subscribe, getSnapshot, getServerSnapshot4);
|
|
477
579
|
}
|
|
478
580
|
export {
|
|
479
581
|
AuthBoundKoraProvider,
|
|
480
582
|
KoraProvider,
|
|
481
583
|
classifyKoraInitializationError,
|
|
584
|
+
createKoraHooks,
|
|
482
585
|
useApp,
|
|
483
586
|
useCollaborators,
|
|
484
587
|
useCollection,
|
|
485
588
|
useMutation,
|
|
486
589
|
usePresence,
|
|
487
590
|
useQuery,
|
|
591
|
+
useQueryState,
|
|
488
592
|
useRichText,
|
|
489
593
|
useSyncStatus
|
|
490
594
|
};
|