@calimero-network/mero-react 9.0.0 → 9.1.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/README.md CHANGED
@@ -208,6 +208,41 @@ const { contexts, loading, error, refetch } = useContexts(applicationId);
208
208
  // contexts: Array<{ contextId: string; applicationId: string }>
209
209
  ```
210
210
 
211
+ ### Blob hooks
212
+
213
+ `useBlobInfo` / `useBlobUrl` / `useUploadBlob` read and write the node's blob
214
+ store. The reads take an optional `contextId`, and it is what picks the mode:
215
+
216
+ - **without `contextId`** — local-only. The node answers from its own blob
217
+ store immediately, or 404s.
218
+ - **with `contextId`** — network discovery. The node probes that context's
219
+ peers (availability nodes first) for a holder.
220
+
221
+ Discovery is slow: core bounds the search by a **~30s deadline**, and for
222
+ `useBlobUrl` the byte transfer is on top of that. `loading` is real UI state on
223
+ these hooks, not decoration.
224
+
225
+ ```tsx
226
+ // Presence and size, no download — ask this before pulling something large.
227
+ const { info, notFound, loading, error } = useBlobInfo(blobId, { contextId });
228
+ // info: { blobId, size, hash?, mimeType?, source?: 'local' | 'peer' } | null
229
+
230
+ // Bytes as an object URL, revoked for you on unmount and on id/context change.
231
+ const { url } = useBlobUrl(blobId, { contextId, type: 'image/png' });
232
+ return url ? <img src={url} /> : null;
233
+
234
+ // Upload. Pass contextId to announce the blob so the context's peers can
235
+ // discover it later.
236
+ const { uploadBlob, loading, error } = useUploadBlob();
237
+ const result = await uploadBlob({ data: bytes, contextId });
238
+ ```
239
+
240
+ A null/undefined `blobId` fetches nothing, like the other `string | null` read
241
+ hooks. `hash` and `mimeType` are genuinely optional — a peer probe carries only
242
+ presence and size, so a discovery-sourced hit has neither; read `info.source`
243
+ to tell a local answer from a peer one. `notFound` marks the node's legitimate
244
+ "no holder" 404 apart from a transport failure; `error` is set for both.
245
+
211
246
  ### Storage helpers
212
247
 
213
248
  Persist/read node URL, application ID, context ID, and context identity in localStorage.
@@ -360,11 +395,13 @@ useDetachContextFromGroup
360
395
  useNamespaces, useNamespace, useNamespaceGroups, useNamespaceIdentity
361
396
  useNamespacesForApplication, useCreateNamespace, useDeleteNamespace
362
397
  useJoinNamespace, useCreateNamespaceInvitation, useCreateGroupInNamespace
398
+ useBlobInfo, useBlobUrl, useUploadBlob
363
399
 
364
400
  // Types (mero-react)
365
401
  MeroContextValue, MeroProviderConfig, MeroProviderProps
366
402
  CustomConnectionConfig, AppContext, ExecutionResult
367
403
  ApplicationContextRecord, ContextDiscoveryOptions, ContextDiscoveryState
404
+ BlobHookOptions, UseBlobUrlOptions
368
405
 
369
406
  // Storage (mero-react)
370
407
  localStorageTokenStorage
package/dist/index.cjs CHANGED
@@ -1546,11 +1546,13 @@ function useAsyncResource(fetcher, initialValue, deps) {
1546
1546
  if (mountedRef.current && seq === reqRef.current) setLoading(false);
1547
1547
  }
1548
1548
  }, deps);
1549
- react.useEffect(() => {
1549
+ const prevDeps = react.useRef(deps);
1550
+ if (deps.length !== prevDeps.current.length || deps.some((d, i) => !Object.is(d, prevDeps.current[i]))) {
1551
+ prevDeps.current = deps;
1550
1552
  reqRef.current += 1;
1551
1553
  setData(initialRef.current);
1552
1554
  setError(null);
1553
- }, deps);
1555
+ }
1554
1556
  react.useEffect(() => {
1555
1557
  void refetch();
1556
1558
  }, [refetch]);
@@ -2223,6 +2225,13 @@ function useMemberMetadata(groupId, identity) {
2223
2225
  const [metadata, setMetadata] = react.useState(null);
2224
2226
  const [loading, setLoading] = react.useState(false);
2225
2227
  const [error, setError] = react.useState(null);
2228
+ const prevKey = react.useRef(null);
2229
+ const key = `${groupId ?? ""}:${identity ?? ""}`;
2230
+ if (prevKey.current !== null && prevKey.current !== key) {
2231
+ setMetadata(null);
2232
+ setError(null);
2233
+ }
2234
+ prevKey.current = key;
2226
2235
  const run = react.useCallback(
2227
2236
  async (signal) => {
2228
2237
  if (!mero || !groupId || !identity) {
@@ -2730,6 +2739,80 @@ function useMyAuthoredMigration(contextId) {
2730
2739
  refresh
2731
2740
  };
2732
2741
  }
2742
+ function isBlobNotFound(error) {
2743
+ return !!error && error.status === 404;
2744
+ }
2745
+ function useBlobInfo(blobId, options) {
2746
+ const { mero } = useMero();
2747
+ const contextId = options?.contextId;
2748
+ const { data, loading, error, refetch } = useAsyncResource(
2749
+ mero && blobId ? () => mero.admin.getBlobInfo(blobId, contextId ? { contextId } : void 0) : null,
2750
+ null,
2751
+ [mero, blobId, contextId]
2752
+ );
2753
+ return { info: data, notFound: isBlobNotFound(error), loading, error, refetch };
2754
+ }
2755
+ function useBlobUrl(blobId, options) {
2756
+ const { mero } = useMero();
2757
+ const contextId = options?.contextId;
2758
+ const type = options?.type;
2759
+ const mountedRef = useMountedRef();
2760
+ const [url, setUrl] = react.useState(null);
2761
+ const [loading, setLoading] = react.useState(false);
2762
+ const [error, setError] = react.useState(null);
2763
+ const reqRef = react.useRef(0);
2764
+ const urlRef = react.useRef(null);
2765
+ const revoke = react.useCallback(() => {
2766
+ if (urlRef.current) {
2767
+ URL.revokeObjectURL(urlRef.current);
2768
+ urlRef.current = null;
2769
+ }
2770
+ }, []);
2771
+ const refetch = react.useCallback(async () => {
2772
+ const seq = ++reqRef.current;
2773
+ if (!mero || !blobId) return;
2774
+ if (mountedRef.current) {
2775
+ setLoading(true);
2776
+ setError(null);
2777
+ }
2778
+ try {
2779
+ const bytes = await mero.admin.getBlob(blobId, contextId ? { contextId } : void 0);
2780
+ if (!mountedRef.current || seq !== reqRef.current) return;
2781
+ revoke();
2782
+ const objectUrl = URL.createObjectURL(new Blob([bytes], type ? { type } : void 0));
2783
+ urlRef.current = objectUrl;
2784
+ setUrl(objectUrl);
2785
+ } catch (err) {
2786
+ if (mountedRef.current && seq === reqRef.current) setError(toError(err));
2787
+ } finally {
2788
+ if (mountedRef.current && seq === reqRef.current) setLoading(false);
2789
+ }
2790
+ }, [mero, blobId, contextId, type, revoke, mountedRef]);
2791
+ react.useEffect(() => {
2792
+ reqRef.current += 1;
2793
+ revoke();
2794
+ setUrl(null);
2795
+ setError(null);
2796
+ setLoading(false);
2797
+ }, [mero, blobId, contextId, type, revoke]);
2798
+ react.useEffect(() => {
2799
+ void refetch();
2800
+ }, [refetch]);
2801
+ react.useEffect(() => revoke, [revoke]);
2802
+ return { url, notFound: isBlobNotFound(error), loading, error, refetch };
2803
+ }
2804
+ function useUploadBlob() {
2805
+ const { mero } = useMero();
2806
+ const { loading, error, run } = useAsyncMutation();
2807
+ const uploadBlob = react.useCallback(
2808
+ async (request) => {
2809
+ if (!mero) return null;
2810
+ return run(() => mero.admin.uploadBlob(request));
2811
+ },
2812
+ [mero, run]
2813
+ );
2814
+ return { uploadBlob, loading, error };
2815
+ }
2733
2816
  function MigrationPendingBanner({
2734
2817
  contextId,
2735
2818
  className,
@@ -2895,6 +2978,8 @@ exports.themeToCssVars = themeToCssVars;
2895
2978
  exports.useAddGroupMembers = useAddGroupMembers;
2896
2979
  exports.useAppVersion = useAppVersion;
2897
2980
  exports.useApplicationContexts = useApplicationContexts;
2981
+ exports.useBlobInfo = useBlobInfo;
2982
+ exports.useBlobUrl = useBlobUrl;
2898
2983
  exports.useContextDiscovery = useContextDiscovery;
2899
2984
  exports.useContextGroup = useContextGroup;
2900
2985
  exports.useContexts = useContexts;
@@ -2950,6 +3035,7 @@ exports.useSubscription = useSubscription;
2950
3035
  exports.useSyncGroup = useSyncGroup;
2951
3036
  exports.useUpdateMemberRole = useUpdateMemberRole;
2952
3037
  exports.useUpgradeGroup = useUpgradeGroup;
3038
+ exports.useUploadBlob = useUploadBlob;
2953
3039
  Object.keys(meroJs).forEach(function (k) {
2954
3040
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
2955
3041
  enumerable: true,