@korajs/react 1.0.0-beta.11 → 1.0.0-beta.13

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.js CHANGED
@@ -96,11 +96,36 @@ function useKoraContext() {
96
96
 
97
97
  // src/context/auth-bound-kora-provider.ts
98
98
  import { createElement as createElement2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
99
+ var SENSITIVE_METADATA_KEY = /token|secret|password|authorization|cookie/i;
100
+ function classifyKoraInitializationError(error) {
101
+ const candidate = error;
102
+ const message = typeof candidate?.message === "string" ? candidate.message : String(error);
103
+ const name = typeof candidate?.name === "string" ? candidate.name : "Error";
104
+ const cause = new Error(message);
105
+ cause.name = name;
106
+ if (typeof candidate?.stack === "string") cause.stack = candidate.stack;
107
+ let code = typeof candidate?.code === "string" ? candidate.code : "INITIALIZATION_FAILED";
108
+ if (code === "INITIALIZATION_FAILED" && cause.name === "QuotaExceededError") {
109
+ code = "STORAGE_QUOTA_EXCEEDED";
110
+ }
111
+ const metadata = {};
112
+ if (candidate?.context && typeof candidate.context === "object") {
113
+ for (const [key, value] of Object.entries(candidate.context)) {
114
+ if (SENSITIVE_METADATA_KEY.test(key)) continue;
115
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
116
+ metadata[key] = value;
117
+ }
118
+ }
119
+ }
120
+ return { code, name: cause.name, message: cause.message, metadata, cause };
121
+ }
99
122
  function AuthBoundKoraProvider({
100
123
  authClient,
101
124
  createApp,
102
125
  signedOut = null,
126
+ locked,
103
127
  fallback = null,
128
+ error: renderError,
104
129
  children
105
130
  }) {
106
131
  const [app, setApp] = useState2(null);
@@ -112,6 +137,8 @@ function AuthBoundKoraProvider({
112
137
  const activeUserId = useRef2(null);
113
138
  const generation = useRef2(0);
114
139
  const transition = useRef2(Promise.resolve());
140
+ const reconcileRef = useRef2(() => {
141
+ });
115
142
  useEffect2(() => {
116
143
  let disposed = false;
117
144
  const reconcile = () => {
@@ -149,10 +176,18 @@ function AuthBoundKoraProvider({
149
176
  setApp(created);
150
177
  }).catch((error) => {
151
178
  if (!disposed && currentGeneration === generation.current) {
152
- setInitError(error instanceof Error ? error : new Error(String(error)));
179
+ void authClient.resolveSyncState?.().then((failedSession) => {
180
+ if (!disposed && currentGeneration === generation.current && failedSession?.state === "authenticated") {
181
+ setInitError({
182
+ error: classifyKoraInitializationError(error),
183
+ session: { userId: failedSession.userId }
184
+ });
185
+ }
186
+ });
153
187
  }
154
188
  });
155
189
  };
190
+ reconcileRef.current = reconcile;
156
191
  reconcile();
157
192
  const unsubscribe = authClient.subscribe?.(reconcile) ?? (() => {
158
193
  });
@@ -167,14 +202,22 @@ function AuthBoundKoraProvider({
167
202
  };
168
203
  }, [authClient]);
169
204
  if (initError) {
205
+ if (renderError) {
206
+ return renderError({
207
+ error: initError.error,
208
+ session: initError.session,
209
+ retry: () => reconcileRef.current()
210
+ });
211
+ }
170
212
  return createElement2(
171
213
  "div",
172
214
  { style: { color: "red", padding: "1rem", fontFamily: "monospace" } },
173
215
  createElement2("strong", null, "Kora initialization error: "),
174
- initError.message
216
+ initError.error.message
175
217
  );
176
218
  }
177
219
  if (state.state === "signed-out" || state.state === "anonymous") return signedOut;
220
+ if (state.state === "authenticated" && state.locked) return locked ?? signedOut;
178
221
  if (!app) return fallback;
179
222
  return createElement2(KoraProvider, { app, fallback }, children);
180
223
  }
@@ -191,17 +234,24 @@ function useApp() {
191
234
  }
192
235
 
193
236
  // src/hooks/use-query.ts
194
- import { assertQueryReady } from "@korajs/store";
195
- 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";
196
239
  var EMPTY_ARRAY = Object.freeze([]);
197
240
  var noopSubscribe = (_onStoreChange) => {
198
241
  return () => {
199
242
  };
200
243
  };
201
- function useQuery(query, options) {
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) {
202
252
  const { queryStoreCache } = useKoraContext();
203
253
  const enabled = options?.enabled !== false;
204
- const descriptorKey = JSON.stringify(query.getDescriptor());
254
+ const descriptorKey = queryKey(query.getDescriptor());
205
255
  const queryRef = useRef3(query);
206
256
  queryRef.current = query;
207
257
  const lastSnapshotRef = useRef3(EMPTY_ARRAY);
@@ -219,122 +269,204 @@ function useQuery(query, options) {
219
269
  queryStoreCache.release(currentQuery);
220
270
  };
221
271
  }, [descriptorKey, enabled, queryStoreCache]);
272
+ const subscribe = enabled && queryStore ? queryStore.subscribe : noopSubscribe;
222
273
  const getSnapshot = useCallback(() => {
223
274
  if (!enabled) {
224
275
  lastSnapshotRef.current = EMPTY_ARRAY;
225
276
  return lastSnapshotRef.current;
226
277
  }
227
- if (!queryStore) {
228
- return lastSnapshotRef.current;
229
- }
230
- if (!queryStore.hasSnapshot()) {
278
+ if (!queryStore || !queryStore.hasSnapshot()) {
231
279
  return lastSnapshotRef.current;
232
280
  }
233
281
  const snapshot = queryStore.getSnapshot();
234
282
  lastSnapshotRef.current = snapshot;
235
283
  return snapshot;
236
284
  }, [enabled, queryStore]);
237
- return useSyncExternalStore(queryStore ? queryStore.subscribe : noopSubscribe, getSnapshot);
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;
300
+ }
301
+ function useQueryState(query, options) {
302
+ const { data, error, ready } = useQuerySubscription(query, options);
303
+ return useMemo2(() => ({ data, error, ready }), [data, error, ready]);
238
304
  }
239
305
 
306
+ // src/hooks/create-kora-hooks.ts
307
+ import { useMemo as useMemo4 } from "react";
308
+
240
309
  // src/hooks/use-mutation.ts
241
310
  import { createMutationController } from "@korajs/core/bindings";
242
- 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";
243
312
 
244
313
  // src/hooks/use-controller.ts
245
314
  import { useEffect as useEffect4, useReducer, useRef as useRef4 } from "react";
315
+ var ORPHAN_REAP_MS = 1e3;
246
316
  function useController(create, destroy, deps) {
247
- const controllerRef = useRef4(null);
317
+ const entryRef = useRef4(null);
248
318
  const createRef = useRef4(create);
249
319
  createRef.current = create;
250
- const [, forceRender] = useReducer((count) => count + 1, 0);
251
- const getController = useRef4(() => {
252
- if (controllerRef.current === null) {
253
- controllerRef.current = createRef.current();
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;
254
346
  }
255
- return controllerRef.current;
256
347
  }).current;
257
348
  useEffect4(() => {
258
- if (controllerRef.current === null) {
259
- controllerRef.current = createRef.current();
260
- forceRender();
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();
261
355
  }
262
356
  return () => {
263
- const controller = controllerRef.current;
264
- controllerRef.current = null;
265
- if (controller !== null) {
266
- destroy(controller);
357
+ const entry = entryRef.current;
358
+ entryRef.current = null;
359
+ if (entry !== null && !entry.reaped) {
360
+ entry.reaped = true;
361
+ destroyRef.current(entry.controller);
267
362
  }
268
363
  };
269
364
  }, deps);
270
- return getController;
365
+ return { get: handle.get, peek: handle.peek, version };
271
366
  }
272
367
 
273
368
  // src/hooks/use-mutation.ts
369
+ var IDLE = Object.freeze({ isLoading: false, error: null });
370
+ var getServerSnapshot2 = () => IDLE;
274
371
  function useMutation(mutationFn, options) {
275
372
  const fnRef = useRef5(mutationFn);
276
373
  fnRef.current = mutationFn;
277
374
  const optionsRef = useRef5(options);
278
375
  optionsRef.current = options;
279
- const getController = useController(
376
+ const controller = useController(
280
377
  () => createMutationController({
281
378
  mutationFn: (...args) => fnRef.current(...args),
282
379
  resolveOptions: () => optionsRef.current
283
380
  }),
284
- (controller) => controller.destroy(),
381
+ (instance) => instance.destroy(),
285
382
  []
286
383
  );
287
- const state = useSyncExternalStore2(
384
+ const getController = controller.get;
385
+ const subscribe = useCallback2(
288
386
  (onStoreChange) => getController().subscribe(onStoreChange),
289
- () => getController().getSnapshot(),
290
- () => getController().getSnapshot()
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]
291
400
  );
292
- return {
293
- mutate: (...args) => getController().mutate(...args),
294
- mutateAsync: (...args) => getController().mutateAsync(...args),
295
- isLoading: state.isLoading,
296
- error: state.error,
297
- reset: () => getController().reset()
298
- };
299
401
  }
300
402
 
301
403
  // src/hooks/use-sync-status.ts
302
- import { createSyncStatusController } from "@korajs/sync";
303
- 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;
304
407
  function useSyncStatus() {
305
408
  const { syncEngine, subscribeSyncStatus, events } = useKoraContext();
306
- const getController = useController(
409
+ const controller = useController(
307
410
  () => createSyncStatusController({
308
411
  syncEngine,
309
412
  subscribeSyncStatus,
310
413
  events: subscribeSyncStatus ? null : events
311
414
  }),
312
- (controller) => controller.destroy(),
415
+ (instance) => instance.destroy(),
313
416
  [syncEngine, subscribeSyncStatus, events]
314
417
  );
315
- return useSyncExternalStore3(
418
+ const getController = controller.get;
419
+ const subscribe = useCallback3(
316
420
  (onStoreChange) => getController().subscribe(onStoreChange),
317
- () => getController().getSnapshot(),
318
- () => getController().getSnapshot()
421
+ [getController, controller.version]
422
+ );
423
+ const peek = controller.peek;
424
+ const getSnapshot = useCallback3(
425
+ () => peek()?.getSnapshot() ?? OFFLINE_SYNC_STATUS,
426
+ [peek]
319
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
+ };
320
452
  }
321
453
 
322
454
  // src/hooks/use-collection.ts
323
- import { useMemo as useMemo2 } from "react";
455
+ import { useMemo as useMemo5 } from "react";
324
456
  function useCollection(name) {
325
457
  const { store } = useKoraContext();
326
- return useMemo2(() => {
458
+ return useMemo5(() => {
327
459
  return store.collection(name);
328
460
  }, [store, name]);
329
461
  }
330
462
 
331
463
  // src/hooks/use-rich-text.ts
332
464
  import { asRichTextSyncEngine, createRichTextController } from "@korajs/store";
333
- import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo3, useSyncExternalStore as useSyncExternalStore4 } from "react";
465
+ import { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo6, useSyncExternalStore as useSyncExternalStore4 } from "react";
334
466
  function useRichText(collectionName, recordId, fieldName, options) {
335
467
  const { store, syncEngine } = useKoraContext();
336
- const collection = useMemo3(() => store.collection(collectionName), [store, collectionName]);
337
- const getController = useController(
468
+ const collection = useMemo6(() => store.collection(collectionName), [store, collectionName]);
469
+ const controller = useController(
338
470
  () => createRichTextController({
339
471
  collection,
340
472
  collectionName,
@@ -345,29 +477,35 @@ function useRichText(collectionName, recordId, fieldName, options) {
345
477
  useDocChannel: options?.useDocChannel,
346
478
  user: options?.user
347
479
  }),
348
- (controller) => controller.destroy(),
480
+ (instance) => instance.destroy(),
349
481
  [collection, collectionName, fieldName, options?.useDocChannel, recordId, store, syncEngine]
350
482
  );
483
+ const getController = controller.get;
351
484
  useEffect5(() => {
352
485
  getController().setUser(options?.user);
353
- }, [getController, options?.user]);
354
- const snapshot = useSyncExternalStore4(
486
+ }, [getController, options?.user, controller.version]);
487
+ const subscribe = useCallback4(
355
488
  (onStoreChange) => getController().subscribe(onStoreChange),
356
- () => getController().getSnapshot(),
357
- () => getController().getSnapshot()
489
+ [getController, controller.version]
358
490
  );
359
- const undo = useCallback2(() => {
491
+ const getSnapshot = useCallback4(() => getController().getSnapshot(), [getController]);
492
+ const snapshot = useSyncExternalStore4(subscribe, getSnapshot, getSnapshot);
493
+ const undo = useCallback4(() => {
360
494
  getController().undo();
361
495
  }, [getController]);
362
- const redo = useCallback2(() => {
496
+ const redo = useCallback4(() => {
363
497
  getController().redo();
364
498
  }, [getController]);
365
- const setCursor = useCallback2(
499
+ const setCursor = useCallback4(
366
500
  (anchor, head) => getController().setCursor(anchor, head),
367
501
  [getController]
368
502
  );
369
- const clearCursor = useCallback2(() => getController().clearCursor(), [getController]);
370
- return buildResult(getController(), snapshot, undo, redo, setCursor, clearCursor);
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
+ );
371
509
  }
372
510
  function buildResult(controller, snapshot, undo, redo, setCursor, clearCursor) {
373
511
  return {
@@ -406,12 +544,13 @@ function usePresence(user) {
406
544
 
407
545
  // src/hooks/use-collaborators.ts
408
546
  import { subscribeRemoteAwarenessStates } from "@korajs/sync";
409
- import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef6, useSyncExternalStore as useSyncExternalStore5 } from "react";
547
+ import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef6, useSyncExternalStore as useSyncExternalStore5 } from "react";
410
548
  var EMPTY_ARRAY2 = [];
549
+ var getServerSnapshot4 = () => EMPTY_ARRAY2;
411
550
  function useCollaborators() {
412
551
  const { syncEngine } = useKoraContext();
413
552
  const snapshotRef = useRef6(EMPTY_ARRAY2);
414
- const subscribe = useCallback3(
553
+ const subscribe = useCallback5(
415
554
  (onStoreChange) => {
416
555
  if (!syncEngine) {
417
556
  snapshotRef.current = EMPTY_ARRAY2;
@@ -419,6 +558,7 @@ function useCollaborators() {
419
558
  };
420
559
  }
421
560
  const awareness = syncEngine.getAwarenessManager();
561
+ snapshotRef.current = EMPTY_ARRAY2;
422
562
  return subscribeRemoteAwarenessStates(awareness, (states) => {
423
563
  snapshotRef.current = states;
424
564
  onStoreChange();
@@ -426,23 +566,26 @@ function useCollaborators() {
426
566
  },
427
567
  [syncEngine]
428
568
  );
429
- const getSnapshot = useCallback3(() => snapshotRef.current, []);
569
+ const getSnapshot = useCallback5(() => snapshotRef.current, []);
430
570
  useEffect7(() => {
431
571
  if (!syncEngine) {
432
572
  snapshotRef.current = EMPTY_ARRAY2;
433
573
  }
434
574
  }, [syncEngine]);
435
- return useSyncExternalStore5(subscribe, getSnapshot);
575
+ return useSyncExternalStore5(subscribe, getSnapshot, getServerSnapshot4);
436
576
  }
437
577
  export {
438
578
  AuthBoundKoraProvider,
439
579
  KoraProvider,
580
+ classifyKoraInitializationError,
581
+ createKoraHooks,
440
582
  useApp,
441
583
  useCollaborators,
442
584
  useCollection,
443
585
  useMutation,
444
586
  usePresence,
445
587
  useQuery,
588
+ useQueryState,
446
589
  useRichText,
447
590
  useSyncStatus
448
591
  };