@basictech/react 0.11.0-beta.1 → 0.11.0-beta.2

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 CHANGED
@@ -151,8 +151,8 @@ client.
151
151
  | --- | --- | --- | --- |
152
152
  | `clientId` | `string` | Required | The application's client DID. Its Basic app metadata must register the redirect URI. |
153
153
  | `schema` | `S extends BasicSchema` | Required by `createBasic` | Pass the `defineSchema()` result used by this application. It is optional only in the lower-level `createBasicClient`. |
154
- | `identityOrigin` | `string` | `'https://basic.id'` | Use another Identity v1 origin for handle resolution and the shares management URL. |
155
- | `defaultPds` | `string` | `'https://pds.basic.id'` | Change the PDS discovered by no-argument `signIn()`. `signIn(handleOrDid)` discovers that identity's PDS instead. |
154
+ | `identityOrigin` | `string` | `'https://basic.id'` | Use another Basic ID web origin for the shares management URL. |
155
+ | `defaultPds` | `string` | `'https://pds.basic.id'` | Change the PDS used for handle resolution and no-argument `signIn()`. `signIn(handleOrDid)` discovers that identity's PDS after resolving handles here. |
156
156
  | `redirectUri` | `string` | `location.origin + location.pathname` | Set an explicit OAuth callback route. Outside a browser location, it is required. The URI must appear in the client metadata for `clientId`. |
157
157
  | `mode` | `'sync' \| 'rest'` | `'sync'` | Choose direct REST when offline replicas, anonymous mode, multi-account state, and `watch()` are not needed. |
158
158
  | `anonymous` | `boolean` | `true` in sync mode | Set `false` to require sign-in. REST mode always disables anonymous workspaces. |
@@ -170,6 +170,7 @@ client.
170
170
  | `currentUrl` | `() => string` | `location.href`, or `''` without `location` | Tell auth bootstrap where to read OAuth `code` and `state`. |
171
171
  | `replaceUrl` | `(url: string) => void` | `history.replaceState({}, '', url)` | Integrate removal of consumed OAuth query parameters with custom navigation. |
172
172
  | `createMessageChannel` | `(name: string) => AuthMessageChannel` | `BroadcastChannel` when available | Replace or disable cross-context auth notifications. The default token adapter also transfers access tokens transiently between tabs. |
173
+ | `reconcileTrigger` | `(listener: () => void) => () => void` | `focus`, `online`, and `visibilitychange` listeners | Subscribe the client to host lifecycle signals so a suspended or backgrounded application reconciles credentials with its owner and mounted Sync sessions on resume. Return a cleanup function; the client calls it on `stop()`. |
173
174
  | `ownerCredential` | `OwnerCredentialAdapter` | None | First-party Basic ID/dev tooling can inject owner credentials for `client.drive()`, account-wide `client.storageInfo()`, and `client.shares.leave()`. Normal app credentials should use existing repos and `shares.manageUrl()`. |
174
175
  | `uploadTransport` | `UploadTransportAdapter` | Browser XHR, then the core fetch fallback | Supply a multipart transport. XHR is used by default because fetch has no upload-progress events. |
175
176
  | `warn` | `(message: string) => void` | `console.warn` for schema checks | Route schema-drift warnings into application logging. |
@@ -192,6 +193,10 @@ The defaults are selected from browser capabilities at client creation:
192
193
  - **Auth coordination:** token rotation and sign-out notifications use `BroadcastChannel` when
193
194
  available. Access tokens can cross that channel transiently but are not written by the default
194
195
  token store.
196
+ - **Lifecycle recovery:** when a tab regains focus, comes back online, or becomes visible again,
197
+ the client reconciles fresh credentials with the owner and mounted Sync sessions. A browser that
198
+ suspended the page recovers without a reload, and transient refresh failures retry rather than
199
+ signing the profile out. Override `reconcileTrigger` to supply that signal in another host.
195
200
  - **Uploads:** browser multipart uploads use XHR for real `onProgress(loaded, total)` events. The
196
201
  fetch fallback reports only start and completion.
197
202
 
@@ -745,6 +750,7 @@ Shares v2 is capability-gated and requires sign-in. `useOutgoingShares()` return
745
750
  get(id: string): Promise<OutgoingShareInfo>
746
751
  cancel(id: string): Promise<OutgoingShareInfo>
747
752
  revoke(id: string): Promise<OutgoingShareInfo>
753
+ getContactHandle(did: string): Promise<string | null>
748
754
  manageUrl(): string
749
755
  }
750
756
  ```
@@ -754,7 +760,7 @@ Shares v2 is capability-gated and requires sign-in. `useOutgoingShares()` return
754
760
  | Field | Type | Meaning |
755
761
  | --- | --- | --- |
756
762
  | `repo` | `'default' \| { repoId: string }` | Origin repo |
757
- | recipient | Exactly one of `recipientHandle: string` or `recipientDid: string` | A handle is trimmed, lowercased, and resolved through `identityOrigin`; a canonical DID skips handle resolution. |
763
+ | recipient | Exactly one of `recipientHandle: string` or `recipientDid: string` | A handle is trimmed, lowercased, and resolved through `defaultPds`; a canonical DID skips handle resolution. |
758
764
  | `role` | `'viewer' \| 'editor'` | Collaborator role |
759
765
  | `scope` | `Array<{ table: string } \| { table: string; recordIds: string[] }>` | Whole-table clauses or static record sets |
760
766
  | `acceptBy` | `string` | Optional acceptance deadline passed to Shares v2 |
@@ -807,6 +813,10 @@ export function OutgoingShares() {
807
813
  `deliveryState`, and created, accepted, and ended timestamps/reasons. Revocation prevents future
808
814
  access but cannot retract data a recipient already downloaded.
809
815
 
816
+ After a handle-based share is created successfully, the SDK stores a profile-local contact entry
817
+ containing `{ did, handle }` in browser local storage. Use `getContactHandle(recipientDid)` to show
818
+ the remembered handle; it returns `null` for DIDs that were shared directly or are not cached.
819
+
810
820
  `manageUrl()` returns the Basic ID `/shares` handoff. Invite acceptance and ordinary mount
811
821
  management belong there. `client.shares.leave()` is intentionally owner-only and throws
812
822
  `OWNER_CREDENTIAL_REQUIRED` without an explicit owner adapter.
@@ -1093,8 +1103,8 @@ for the lower-level model.
1093
1103
  Client Components should import `@basictech/react` directly.
1094
1104
  - Migrating from 0.10 requires deliberate call-site changes; there is no compatibility layer. Read
1095
1105
  [Migrating to Basic SDK 0.11](https://github.com/basicdb/basic-server/blob/dev/docs/MIGRATING_TO_0.11.md).
1096
- - `0.11.0-beta.0` is a beta API. Check the installed version rather than assuming the repository
1097
- workspace has already been published.
1106
+ - 0.11 is a beta API, published to npm under the `beta` dist-tag. Check the installed version
1107
+ rather than assuming it matches the repository workspace.
1098
1108
 
1099
1109
  ## License
1100
1110
 
package/dist/index.d.mts CHANGED
@@ -87,6 +87,7 @@ interface UseOutgoingSharesResult {
87
87
  get(id: string): Promise<OutgoingShareInfo>;
88
88
  cancel(id: string): Promise<OutgoingShareInfo>;
89
89
  revoke(id: string): Promise<OutgoingShareInfo>;
90
+ getContactHandle(did: string): Promise<string | null>;
90
91
  manageUrl(): string;
91
92
  }
92
93
  declare function useOutgoingShares(): UseOutgoingSharesResult;
package/dist/index.d.ts CHANGED
@@ -87,6 +87,7 @@ interface UseOutgoingSharesResult {
87
87
  get(id: string): Promise<OutgoingShareInfo>;
88
88
  cancel(id: string): Promise<OutgoingShareInfo>;
89
89
  revoke(id: string): Promise<OutgoingShareInfo>;
90
+ getContactHandle(did: string): Promise<string | null>;
90
91
  manageUrl(): string;
91
92
  }
92
93
  declare function useOutgoingShares(): UseOutgoingSharesResult;
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ __export(index_exports, {
60
60
  module.exports = __toCommonJS(index_exports);
61
61
 
62
62
  // src/create-client.ts
63
- var import_core5 = require("@basictech/core");
63
+ var import_core4 = require("@basictech/core");
64
64
 
65
65
  // src/adapters/broadcast-channel.ts
66
66
  function createBrowserMessageChannel(name) {
@@ -444,7 +444,6 @@ function browserStorage(name) {
444
444
  }
445
445
 
446
446
  // src/adapters/token-store.ts
447
- var import_core3 = require("@basictech/core");
448
447
  var ACCESS_TOKEN_ADOPTION_TIMEOUT_MS = 100;
449
448
  function parseTokens(value) {
450
449
  if (!value) return null;
@@ -521,7 +520,7 @@ var BrowserTokenStore = class {
521
520
  else this.accessWaiters.delete(key);
522
521
  }
523
522
  waitForAccessToken(key, refreshToken) {
524
- return new Promise((resolve, reject) => {
523
+ return new Promise((resolve) => {
525
524
  let timer;
526
525
  const waiter = {
527
526
  refreshToken,
@@ -537,17 +536,14 @@ var BrowserTokenStore = class {
537
536
  const remaining = (this.accessWaiters.get(key) ?? []).filter((candidate) => candidate !== waiter);
538
537
  if (remaining.length) this.accessWaiters.set(key, remaining);
539
538
  else this.accessWaiters.delete(key);
540
- reject(new import_core3.BasicError(
541
- "ACCESS_TOKEN_ADOPTION_TIMEOUT",
542
- "timed out waiting for the access token matching persisted refresh material"
543
- ));
539
+ resolve(null);
544
540
  }, ACCESS_TOKEN_ADOPTION_TIMEOUT_MS);
545
541
  });
546
542
  }
547
543
  };
548
544
 
549
545
  // src/adapters/upload.ts
550
- var import_core4 = require("@basictech/core");
546
+ var import_core3 = require("@basictech/core");
551
547
  function responseHeaders(xhr) {
552
548
  const headers = new Headers();
553
549
  for (const line of xhr.getAllResponseHeaders().trim().split(/[\r\n]+/)) {
@@ -560,7 +556,7 @@ function responseHeaders(xhr) {
560
556
  var browserUploadTransport = {
561
557
  upload(request) {
562
558
  if (typeof XMLHttpRequest === "undefined") {
563
- return Promise.reject(new import_core4.BasicError("XHR_UNAVAILABLE"));
559
+ return Promise.reject(new import_core3.BasicError("XHR_UNAVAILABLE"));
564
560
  }
565
561
  return new Promise((resolve, reject) => {
566
562
  const xhr = new XMLHttpRequest();
@@ -592,9 +588,9 @@ var browserUploadTransport = {
592
588
  statusText: xhr.statusText,
593
589
  headers: responseHeaders(xhr)
594
590
  })));
595
- xhr.onerror = () => finish(() => reject(new import_core4.BasicError("NETWORK_ERROR", "the upload could not reach the server")));
591
+ xhr.onerror = () => finish(() => reject(new import_core3.BasicError("NETWORK_ERROR", "the upload could not reach the server")));
596
592
  xhr.onabort = () => finish(() => reject(request.signal?.reason ?? new DOMException("Aborted", "AbortError")));
597
- xhr.ontimeout = () => finish(() => reject(new import_core4.BasicError("NETWORK_ERROR", "the upload timed out")));
593
+ xhr.ontimeout = () => finish(() => reject(new import_core3.BasicError("NETWORK_ERROR", "the upload timed out")));
598
594
  const form = new FormData();
599
595
  form.append("path", request.path);
600
596
  if (request.name !== void 0) form.append("name", request.name);
@@ -606,21 +602,37 @@ var browserUploadTransport = {
606
602
  };
607
603
 
608
604
  // src/create-client.ts
605
+ function browserReconcileTrigger(listener) {
606
+ if (typeof window === "undefined" || typeof document === "undefined") return () => {
607
+ };
608
+ const onVisible = () => {
609
+ if (document.visibilityState === "visible") listener();
610
+ };
611
+ window.addEventListener("focus", listener);
612
+ window.addEventListener("online", listener);
613
+ document.addEventListener("visibilitychange", onVisible);
614
+ return () => {
615
+ window.removeEventListener("focus", listener);
616
+ window.removeEventListener("online", listener);
617
+ document.removeEventListener("visibilitychange", onVisible);
618
+ };
619
+ }
609
620
  function createBasicClient(config) {
610
- const local = browserStorage("localStorage") ?? new import_core5.MemoryKeyValueStorage();
611
- const session = browserStorage("sessionStorage") ?? new import_core5.MemoryKeyValueStorage();
621
+ const local = browserStorage("localStorage") ?? new import_core4.MemoryKeyValueStorage();
622
+ const session = browserStorage("sessionStorage") ?? new import_core4.MemoryKeyValueStorage();
612
623
  const defaultTokenStore = config.tokenStore ? null : new BrowserTokenStore(local);
613
624
  const canBroadcast = typeof BroadcastChannel !== "undefined";
614
625
  const canPersistReplica = typeof indexedDB !== "undefined";
615
- return (0, import_core5.createBasicClient)({
626
+ return (0, import_core4.createBasicClient)({
616
627
  ...config,
617
628
  kv: config.kv ?? local,
618
629
  tokenStore: config.tokenStore ?? defaultTokenStore,
619
630
  sessionStorage: config.sessionStorage ?? session,
620
- replicaStore: config.replicaStore ?? (canPersistReplica ? new PersistenceStore(config.clientId) : new import_core5.MemoryReplicaStoreFactory()),
631
+ replicaStore: config.replicaStore ?? (canPersistReplica ? new PersistenceStore(config.clientId) : new import_core4.MemoryReplicaStoreFactory()),
621
632
  navigate: config.navigate ?? browserNavigate,
622
633
  currentUrl: config.currentUrl ?? browserCurrentUrl,
623
634
  replaceUrl: config.replaceUrl ?? browserReplaceUrl,
635
+ reconcileTrigger: config.reconcileTrigger ?? browserReconcileTrigger,
624
636
  createMessageChannel: config.createMessageChannel ?? (canBroadcast && defaultTokenStore ? createBrowserAuthChannelFactory(config.clientId, defaultTokenStore) : void 0),
625
637
  uploadTransport: config.uploadTransport ?? (typeof XMLHttpRequest === "undefined" ? void 0 : browserUploadTransport)
626
638
  });
@@ -706,7 +718,7 @@ var import_react4 = require("react");
706
718
 
707
719
  // src/hooks/client.ts
708
720
  var import_react3 = require("react");
709
- var import_core6 = require("@basictech/core");
721
+ var import_core5 = require("@basictech/core");
710
722
  function useRequiredClient() {
711
723
  const client = (0, import_react3.useContext)(BasicClientContext);
712
724
  if (!client) throw new Error("Basic hooks must be used within a <BasicProvider>");
@@ -716,8 +728,8 @@ function useClientSnapshot(client) {
716
728
  return (0, import_react3.useSyncExternalStore)(client.subscribe, client.getSnapshot, client.getSnapshot);
717
729
  }
718
730
  function toBasicError(error, fallback = "UNKNOWN_ERROR") {
719
- if (error instanceof import_core6.BasicError) return error;
720
- return new import_core6.BasicError(fallback, error instanceof Error ? error.message : String(error));
731
+ if (error instanceof import_core5.BasicError) return error;
732
+ return new import_core5.BasicError(fallback, error instanceof Error ? error.message : String(error));
721
733
  }
722
734
  function useAsyncResult(load, initialData, enabled, settled, dependencies) {
723
735
  const loadRef = (0, import_react3.useRef)(load);
@@ -779,7 +791,7 @@ function useAccounts() {
779
791
 
780
792
  // src/hooks/auth.ts
781
793
  var import_react5 = require("react");
782
- var import_core7 = require("@basictech/core");
794
+ var import_core6 = require("@basictech/core");
783
795
  function useAuthFor(client) {
784
796
  const snapshot = useClientSnapshot(client);
785
797
  const signIn = (0, import_react5.useCallback)((input) => client.signIn(input), [client]);
@@ -790,7 +802,7 @@ function useAuthFor(client) {
790
802
  isSignedIn: snapshot.isSignedIn,
791
803
  isAnonymous: snapshot.isAnonymous,
792
804
  status: snapshot.authStatus,
793
- error: snapshot.authErrorCode ? new import_core7.BasicError(snapshot.authErrorCode) : null,
805
+ error: snapshot.authErrorCode ? new import_core6.BasicError(snapshot.authErrorCode) : null,
794
806
  user: snapshot.user,
795
807
  did: snapshot.did,
796
808
  handle: snapshot.handle,
@@ -979,6 +991,7 @@ function useOutgoingSharesFor(client) {
979
991
  const get = (0, import_react9.useCallback)((id) => client.shares.getOutgoing(id), [client]);
980
992
  const cancel = (0, import_react9.useCallback)((id) => client.shares.cancel(id), [client]);
981
993
  const revoke = (0, import_react9.useCallback)((id) => client.shares.revoke(id), [client]);
994
+ const getContactHandle = (0, import_react9.useCallback)((did) => client.shares.getContactHandle(did), [client]);
982
995
  const manageUrl = (0, import_react9.useCallback)(() => client.shares.manageUrl(), [client]);
983
996
  const { data: outgoingShares, ...state } = result;
984
997
  return {
@@ -989,6 +1002,7 @@ function useOutgoingSharesFor(client) {
989
1002
  get,
990
1003
  cancel,
991
1004
  revoke,
1005
+ getContactHandle,
992
1006
  manageUrl
993
1007
  };
994
1008
  }