@basictech/react 0.11.0 → 0.12.0-beta.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.mjs CHANGED
@@ -1,3 +1,10 @@
1
+ // src/create-basic.tsx
2
+ import { useContext as useContext2 } from "react";
3
+
4
+ // src/context.ts
5
+ import { createContext } from "react";
6
+ var BasicClientContext = createContext(null);
7
+
1
8
  // src/create-client.ts
2
9
  import {
3
10
  MemoryKeyValueStorage,
@@ -6,6 +13,10 @@ import {
6
13
  } from "@basictech/core";
7
14
 
8
15
  // src/adapters/broadcast-channel.ts
16
+ function supportsAccessTokenAdoption(store) {
17
+ const candidate = store;
18
+ return typeof candidate?.accessToken === "function" && typeof candidate?.adoptAccessToken === "function";
19
+ }
9
20
  function createBrowserMessageChannel(name) {
10
21
  const channel = new BroadcastChannel(name);
11
22
  const adapter = {
@@ -16,12 +27,11 @@ function createBrowserMessageChannel(name) {
16
27
  channel.onmessage = (event) => adapter.onmessage?.({ data: event.data });
17
28
  return adapter;
18
29
  }
19
- function createBrowserAuthChannelFactory(clientId, tokenStore) {
20
- const prefix = `basic_auth:${clientId}:`;
21
- return (name) => {
30
+ function createBrowserAuthChannelFactory(tokenStore) {
31
+ return (name, context) => {
22
32
  const channel = createBrowserMessageChannel(name);
23
- const profileId = name.startsWith(prefix) ? name.slice(prefix.length) : "";
24
- const tokenKey = `u:${profileId}:basic_tokens`;
33
+ if (!context) return channel;
34
+ const { tokenKey } = context;
25
35
  const adapter = {
26
36
  postMessage: (message) => {
27
37
  const outgoing = message;
@@ -568,32 +578,27 @@ function browserReconcileTrigger(listener) {
568
578
  function createBasicClient(config) {
569
579
  const local = browserStorage("localStorage") ?? new MemoryKeyValueStorage();
570
580
  const session = browserStorage("sessionStorage") ?? new MemoryKeyValueStorage();
571
- const defaultTokenStore = config.tokenStore ? null : new BrowserTokenStore(local);
581
+ const tokenStore = config.tokenStore ?? new BrowserTokenStore(local);
572
582
  const canBroadcast = typeof BroadcastChannel !== "undefined";
573
583
  const canPersistReplica = typeof indexedDB !== "undefined";
584
+ const messageChannel = !canBroadcast ? void 0 : supportsAccessTokenAdoption(tokenStore) ? createBrowserAuthChannelFactory(tokenStore) : createBrowserMessageChannel;
574
585
  return createCoreClient({
575
586
  ...config,
576
587
  kv: config.kv ?? local,
577
- tokenStore: config.tokenStore ?? defaultTokenStore,
588
+ tokenStore,
578
589
  sessionStorage: config.sessionStorage ?? session,
579
590
  replicaStore: config.replicaStore ?? (canPersistReplica ? new PersistenceStore(config.clientId) : new MemoryReplicaStoreFactory()),
580
591
  navigate: config.navigate ?? browserNavigate,
581
592
  currentUrl: config.currentUrl ?? browserCurrentUrl,
582
593
  replaceUrl: config.replaceUrl ?? browserReplaceUrl,
583
594
  reconcileTrigger: config.reconcileTrigger ?? browserReconcileTrigger,
584
- createMessageChannel: config.createMessageChannel ?? (canBroadcast && defaultTokenStore ? createBrowserAuthChannelFactory(config.clientId, defaultTokenStore) : void 0),
595
+ createMessageChannel: config.createMessageChannel ?? messageChannel,
585
596
  uploadTransport: config.uploadTransport ?? (typeof XMLHttpRequest === "undefined" ? void 0 : browserUploadTransport)
586
597
  });
587
598
  }
588
599
 
589
600
  // src/provider.tsx
590
601
  import { useEffect, useRef, useState, useSyncExternalStore } from "react";
591
-
592
- // src/context.ts
593
- import { createContext } from "react";
594
- var BasicClientContext = createContext(null);
595
-
596
- // src/provider.tsx
597
602
  import { jsx } from "react/jsx-runtime";
598
603
  var clientLifecycles = /* @__PURE__ */ new WeakMap();
599
604
  function retainClient(client) {
@@ -662,10 +667,10 @@ function BasicProvider(props) {
662
667
  }
663
668
 
664
669
  // src/hooks/accounts.ts
665
- import { useCallback, useMemo } from "react";
670
+ import { useCallback, useMemo as useMemo2 } from "react";
666
671
 
667
672
  // src/hooks/client.ts
668
- import { useContext, useEffect as useEffect2, useRef as useRef2, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
673
+ import { useContext, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
669
674
  import {
670
675
  BasicError as BasicError4
671
676
  } from "@basictech/core";
@@ -677,6 +682,10 @@ function useRequiredClient() {
677
682
  function useClientSnapshot(client) {
678
683
  return useSyncExternalStore2(client.subscribe, client.getSnapshot, client.getSnapshot);
679
684
  }
685
+ function useSignInGate(snapshot) {
686
+ const code = snapshot.isReady && !snapshot.isSignedIn ? snapshot.readOnlyReason ?? "AUTHORIZATION_REQUIRED" : null;
687
+ return useMemo(() => code === null ? null : new BasicError4(code, code === "AUTH_EXPIRED" ? "Session expired. Sign in again to load this list." : "Sign in to load this list."), [code]);
688
+ }
680
689
  function toBasicError(error, fallback = "UNKNOWN_ERROR") {
681
690
  if (error instanceof BasicError4) return error;
682
691
  return new BasicError4(fallback, error instanceof Error ? error.message : String(error));
@@ -727,36 +736,33 @@ function useAccountsFor(client) {
727
736
  const switchAccount = useCallback((id) => client.switchAccount(id), [client]);
728
737
  const addAccount = useCallback(() => client.addAccount(), [client]);
729
738
  const removeAccount = useCallback((id) => client.removeAccount(id), [client]);
730
- return useMemo(() => ({
739
+ return useMemo2(() => ({
731
740
  accounts: snapshot.accounts,
732
- active: snapshot.activeProfile,
741
+ activeAccount: snapshot.activeAccount,
733
742
  switchAccount,
734
743
  addAccount,
735
744
  removeAccount
736
- }), [snapshot.accounts, snapshot.activeProfile, switchAccount, addAccount, removeAccount]);
745
+ }), [snapshot.accounts, snapshot.activeAccount, switchAccount, addAccount, removeAccount]);
737
746
  }
738
747
  function useAccounts() {
739
748
  return useAccountsFor(useRequiredClient());
740
749
  }
741
750
 
742
751
  // src/hooks/auth.ts
743
- import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
744
- import {
745
- BasicError as BasicError5
746
- } from "@basictech/core";
752
+ import { useCallback as useCallback2, useMemo as useMemo3 } from "react";
747
753
  function useAuthFor(client) {
748
754
  const snapshot = useClientSnapshot(client);
749
755
  const signIn = useCallback2((input) => client.signIn(input), [client]);
750
756
  const signOut = useCallback2(() => client.signOut(), [client]);
751
757
  const getToken = useCallback2(() => client.getToken(), [client]);
752
- return useMemo2(() => ({
758
+ return useMemo3(() => ({
753
759
  isReady: snapshot.isReady,
754
760
  isSignedIn: snapshot.isSignedIn,
755
761
  isAnonymous: snapshot.isAnonymous,
756
762
  canWrite: snapshot.canWrite,
757
763
  readOnlyReason: snapshot.readOnlyReason,
758
764
  status: snapshot.authStatus,
759
- error: snapshot.authErrorCode ? new BasicError5(snapshot.authErrorCode) : null,
765
+ error: snapshot.authError,
760
766
  user: snapshot.user,
761
767
  did: snapshot.did,
762
768
  handle: snapshot.handle,
@@ -769,6 +775,9 @@ function useAuth() {
769
775
  return useAuthFor(useRequiredClient());
770
776
  }
771
777
 
778
+ // src/hooks/basic.ts
779
+ import { useMemo as useMemo6 } from "react";
780
+
772
781
  // src/hooks/db.ts
773
782
  function useDbFor(client, source = "default") {
774
783
  return client.db(source);
@@ -784,13 +793,13 @@ function useCollection(name, options = {}) {
784
793
  }
785
794
 
786
795
  // src/hooks/repos.ts
787
- import { useCallback as useCallback3, useMemo as useMemo3 } from "react";
796
+ import { useCallback as useCallback3, useMemo as useMemo4 } from "react";
788
797
  function useReposFor(client) {
789
798
  const snapshot = useClientSnapshot(client);
790
799
  const refresh = useCallback3(() => client.refreshRepos(), [client]);
791
800
  const create = useCallback3((input) => client.createRepo(input), [client]);
792
801
  const archive = useCallback3((repoId) => client.archiveRepo(repoId), [client]);
793
- return useMemo3(() => ({
802
+ return useMemo4(() => ({
794
803
  repos: snapshot.repos,
795
804
  defaultRepoId: snapshot.defaultRepoId,
796
805
  refresh,
@@ -803,7 +812,7 @@ function useRepos() {
803
812
  }
804
813
 
805
814
  // src/hooks/status.ts
806
- import { useCallback as useCallback4, useMemo as useMemo4 } from "react";
815
+ import { useCallback as useCallback4, useMemo as useMemo5 } from "react";
807
816
  function useSyncStatusFor(client, source) {
808
817
  const snapshot = useClientSnapshot(client);
809
818
  const sourceKey = JSON.stringify(source);
@@ -818,7 +827,7 @@ function useSyncStatusFor(client, source) {
818
827
  // eslint-disable-next-line react-hooks/exhaustive-deps
819
828
  [client, sourceKey]
820
829
  );
821
- return useMemo4(() => ({
830
+ return useMemo5(() => ({
822
831
  status: source === void 0 ? snapshot.syncStatus : client.status(source),
823
832
  pendingCount: client.pending(source).length,
824
833
  rejected: client.rejected(source),
@@ -845,51 +854,41 @@ function useBasicFor(client) {
845
854
  const auth = useAuthFor(client);
846
855
  const accounts = useAccountsFor(client);
847
856
  const sync = useSyncStatusFor(client);
848
- const repos = useReposFor(client);
849
- return {
857
+ const repos = useReposFor(client).repos;
858
+ const db = useDbFor(client);
859
+ return useMemo6(() => ({
850
860
  ...auth,
851
861
  client,
852
- db: useDbFor(client),
862
+ db,
853
863
  accounts,
854
864
  sync,
855
- repos: repos.repos
856
- };
865
+ repos
866
+ }), [auth, client, db, accounts, sync, repos]);
857
867
  }
858
868
  function useBasic() {
859
869
  return useBasicFor(useRequiredClient());
860
870
  }
861
871
 
862
872
  // src/hooks/files.ts
863
- import { useMemo as useMemo5, useSyncExternalStore as useSyncExternalStore3 } from "react";
873
+ import { useMemo as useMemo7, useSyncExternalStore as useSyncExternalStore3 } from "react";
864
874
  var zero = () => 0;
865
875
  function useFilesFor(client, query = {}, options = {}) {
866
876
  const source = options.source ?? "default";
867
877
  const sourceKey = JSON.stringify(source);
868
878
  const queryKey = JSON.stringify(query);
869
879
  const snapshot = useClientSnapshot(client);
870
- const subscription = useMemo5(
880
+ const subscription = useMemo7(
871
881
  () => client.queryStore.collection(source, "_files"),
872
882
  // eslint-disable-next-line react-hooks/exhaustive-deps
873
883
  [client, sourceKey]
874
884
  );
875
885
  const version = useSyncExternalStore3(subscription.subscribe, subscription.getSnapshot, zero);
876
886
  return useAsyncResult(
877
- async () => {
878
- const files = client.db(source).files;
879
- if (source !== "default" && "mountId" in source) {
880
- const page = await files.list({
881
- ...query.prefix !== void 0 ? { prefix: query.prefix } : {},
882
- ...query.limit !== void 0 ? { limit: query.limit } : {},
883
- ...query.cursor !== void 0 ? { cursor: query.cursor } : {}
884
- });
885
- return page.data;
886
- }
887
- return (await files.list(query)).data;
888
- },
887
+ async () => (await client.db(source).files.list(query)).data,
889
888
  [],
890
889
  snapshot.isReady,
891
890
  snapshot.isReady,
892
- [sourceKey, queryKey, version, snapshot.activeProfile?.id]
891
+ [sourceKey, queryKey, version, snapshot.activeAccount?.id]
893
892
  );
894
893
  }
895
894
  function useFiles(query = {}, options = {}) {
@@ -897,13 +896,15 @@ function useFiles(query = {}, options = {}) {
897
896
  }
898
897
  function useStorageInfoFor(client) {
899
898
  const snapshot = useClientSnapshot(client);
900
- return useAsyncResult(
899
+ const gate = useSignInGate(snapshot);
900
+ const result = useAsyncResult(
901
901
  () => client.storageInfo(client.resolveRepoId()),
902
902
  null,
903
903
  snapshot.isReady && snapshot.isSignedIn,
904
904
  snapshot.isReady,
905
- [snapshot.activeProfile?.id]
905
+ [snapshot.activeAccount?.id]
906
906
  );
907
+ return { ...result, error: result.error ?? gate };
907
908
  }
908
909
  function useStorageInfo() {
909
910
  return useStorageInfoFor(useRequiredClient());
@@ -913,33 +914,34 @@ function useStorageInfo() {
913
914
  import { useCallback as useCallback5 } from "react";
914
915
  function useMountsFor(client, query = {}) {
915
916
  const snapshot = useClientSnapshot(client);
917
+ const gate = useSignInGate(snapshot);
916
918
  const queryKey = JSON.stringify(query);
917
919
  const result = useAsyncResult(
918
920
  () => client.shares.mounts(query),
919
921
  [],
920
922
  snapshot.isReady && snapshot.isSignedIn,
921
923
  snapshot.isReady,
922
- [queryKey, snapshot.activeProfile?.id]
924
+ [queryKey, snapshot.activeAccount?.id]
923
925
  );
924
926
  const open = useCallback5(
925
927
  (mountId) => client.shares.mount(mountId),
926
928
  [client]
927
929
  );
928
930
  const manageUrl = useCallback5(() => client.shares.manageUrl(), [client]);
929
- const { data: mounts, ...state } = result;
930
- return { data: mounts, mounts, ...state, open, manageUrl };
931
+ return { ...result, error: result.error ?? gate, open, manageUrl };
931
932
  }
932
933
  function useMounts(query = {}) {
933
934
  return useMountsFor(useRequiredClient(), query);
934
935
  }
935
936
  function useOutgoingSharesFor(client) {
936
937
  const snapshot = useClientSnapshot(client);
938
+ const gate = useSignInGate(snapshot);
937
939
  const result = useAsyncResult(
938
940
  () => client.shares.listOutgoing(),
939
941
  [],
940
942
  snapshot.isReady && snapshot.isSignedIn,
941
943
  snapshot.isReady,
942
- [snapshot.activeProfile?.id]
944
+ [snapshot.activeAccount?.id]
943
945
  );
944
946
  const create = useCallback5((input) => client.shares.createOutgoing(input), [client]);
945
947
  const get = useCallback5((id) => client.shares.getOutgoing(id), [client]);
@@ -947,11 +949,9 @@ function useOutgoingSharesFor(client) {
947
949
  const revoke = useCallback5((id) => client.shares.revoke(id), [client]);
948
950
  const getContactHandle = useCallback5((did) => client.shares.getContactHandle(did), [client]);
949
951
  const manageUrl = useCallback5(() => client.shares.manageUrl(), [client]);
950
- const { data: outgoingShares, ...state } = result;
951
952
  return {
952
- data: outgoingShares,
953
- outgoingShares,
954
- ...state,
953
+ ...result,
954
+ error: result.error ?? gate,
955
955
  create,
956
956
  get,
957
957
  cancel,
@@ -965,58 +965,35 @@ function useOutgoingShares() {
965
965
  }
966
966
 
967
967
  // src/hooks/query.ts
968
- import { useEffect as useEffect3, useMemo as useMemo6, useState as useState3, useSyncExternalStore as useSyncExternalStore4 } from "react";
969
- var subscribeNever = () => () => {
970
- };
971
- var zero2 = () => 0;
968
+ import { useEffect as useEffect3, useState as useState3 } from "react";
972
969
  function useQueryValueFor(client, collection, query = {}, options = {}) {
973
970
  const source = options.source ?? "default";
974
971
  const sourceKey = JSON.stringify(source);
975
972
  const queryKey = JSON.stringify(query);
976
973
  const snapshot = useClientSnapshot(client);
977
- const subscription = useMemo6(
978
- () => client.queryStore.collection(source, collection),
979
- // sourceKey captures semantic source identity rather than object identity.
980
- // eslint-disable-next-line react-hooks/exhaustive-deps
981
- [client, sourceKey, collection]
982
- );
983
- const version = useSyncExternalStore4(
984
- subscription?.subscribe ?? subscribeNever,
985
- subscription?.getSnapshot ?? zero2,
986
- zero2
987
- );
988
974
  const [result, setResult] = useState3({
989
975
  data: [],
990
976
  isLoading: true,
991
977
  error: null
992
978
  });
993
979
  useEffect3(() => {
994
- let active = true;
995
- if (!snapshot.isReady) {
996
- setResult((current) => ({ ...current, isLoading: true, error: null }));
997
- return () => {
998
- active = false;
999
- };
1000
- }
1001
980
  setResult((current) => ({ ...current, isLoading: true, error: null }));
1002
- const collectionApi = client.db(source).collection(collection);
1003
- const list = collectionApi.list;
1004
- void list.call(collectionApi, query).then(
1005
- (page) => {
1006
- if (active) setResult({ data: page.data, isLoading: false, error: null });
1007
- },
1008
- (error) => {
1009
- if (active) setResult((current) => ({
981
+ if (!snapshot.isReady) return;
982
+ try {
983
+ return client.db(source).collection(collection).watch(
984
+ query,
985
+ (page) => setResult({ data: page.data, isLoading: false, error: null }),
986
+ (error) => setResult((current) => ({
1010
987
  ...current,
1011
988
  isLoading: false,
1012
989
  error: toBasicError(error, "QUERY_FAILED")
1013
- }));
1014
- }
1015
- );
1016
- return () => {
1017
- active = false;
1018
- };
1019
- }, [client, collection, sourceKey, queryKey, version, snapshot.isReady]);
990
+ }))
991
+ );
992
+ } catch (error) {
993
+ setResult((current) => ({ ...current, isLoading: false, error: toBasicError(error, "QUERY_FAILED") }));
994
+ return;
995
+ }
996
+ }, [client, collection, sourceKey, queryKey, snapshot.isReady]);
1020
997
  return result;
1021
998
  }
1022
999
  function useQueryFor(client, collection, query = {}, options = {}) {
@@ -1033,25 +1010,31 @@ function createBasic(config) {
1033
1010
  function Provider({ children, renderWhileLoading }) {
1034
1011
  return /* @__PURE__ */ jsx2(BasicProvider, { client, renderWhileLoading, children });
1035
1012
  }
1013
+ function useBound() {
1014
+ if (useContext2(BasicClientContext) !== client) {
1015
+ throw new Error("Bound Basic hooks must be used within their own <basic.Provider>");
1016
+ }
1017
+ return client;
1018
+ }
1036
1019
  function useBoundDb(source = "default") {
1037
- return client.db(source);
1020
+ return useBound().db(source);
1038
1021
  }
1039
1022
  return {
1040
1023
  client,
1041
1024
  Provider,
1042
- useBasic: () => useBasicFor(client),
1043
- useAuth: () => useAuthFor(client),
1044
- useAccounts: () => useAccountsFor(client),
1025
+ useBasic: () => useBasicFor(useBound()),
1026
+ useAuth: () => useAuthFor(useBound()),
1027
+ useAccounts: () => useAccountsFor(useBound()),
1045
1028
  useDb: useBoundDb,
1046
- useCollection: (name, options = {}) => useCollectionFor(client, name, options),
1047
- useQuery: (collection, query = {}, options = {}) => useQueryFor(client, collection, query, options),
1048
- useSyncStatus: (source) => useSyncStatusFor(client, source),
1049
- useSchemaStatus: (source = "default") => useSchemaStatusFor(client, source),
1050
- useRepos: () => useReposFor(client),
1051
- useFiles: (query = {}, options = {}) => useFilesFor(client, query, options),
1052
- useStorageInfo: () => useStorageInfoFor(client),
1053
- useMounts: (query = {}) => useMountsFor(client, query),
1054
- useOutgoingShares: () => useOutgoingSharesFor(client)
1029
+ useCollection: (name, options = {}) => useCollectionFor(useBound(), name, options),
1030
+ useQuery: (collection, query = {}, options = {}) => useQueryFor(useBound(), collection, query, options),
1031
+ useSyncStatus: (source) => useSyncStatusFor(useBound(), source),
1032
+ useSchemaStatus: (source = "default") => useSchemaStatusFor(useBound(), source),
1033
+ useRepos: () => useReposFor(useBound()),
1034
+ useFiles: (query = {}, options = {}) => useFilesFor(useBound(), query, options),
1035
+ useStorageInfo: () => useStorageInfoFor(useBound()),
1036
+ useMounts: (query = {}) => useMountsFor(useBound(), query),
1037
+ useOutgoingShares: () => useOutgoingSharesFor(useBound())
1055
1038
  };
1056
1039
  }
1057
1040
  export {