@calimero-network/mero-react 5.0.0 → 6.0.0

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.cjs CHANGED
@@ -1266,6 +1266,185 @@ function base58ToHex(input) {
1266
1266
  }
1267
1267
 
1268
1268
  // src/hooks/index.ts
1269
+ var NO_EPHEMERAL_SURFACE_MESSAGE = "useEphemeral: this client has no `mero.ephemeral` surface. Ephemeral presence requires @calimero-network/mero-js >= 9.1.0; upgrade the installed @calimero-network/mero-js (and any lockfile pin) to use it.";
1270
+ var COULD_NOT_RESOLVE_IDENTITY_MESSAGE = "useEphemeral: could not resolve a local context identity, so your own presence cannot be filtered out. Pass includeSelf: true to accept this.";
1271
+ var REPLAY_RECONCILE_MS = 50;
1272
+ function useEphemeral(contextId, options = {}) {
1273
+ const { includeSelf = false } = options;
1274
+ const { mero } = useMero();
1275
+ const ephemeral = mero ? mero.ephemeral ?? null : null;
1276
+ const [peers, setPeers] = react.useState(/* @__PURE__ */ new Map());
1277
+ const [latchedError, setLatchedError] = react.useState(null);
1278
+ const contextKeyRef = react.useRef(contextId);
1279
+ contextKeyRef.current = contextId;
1280
+ const setSourceError = react.useCallback(
1281
+ (source, err, key) => {
1282
+ if (key !== contextKeyRef.current) return;
1283
+ setLatchedError({ contextKey: key, source, error: err });
1284
+ },
1285
+ []
1286
+ );
1287
+ const clearSourceError = react.useCallback((source, key) => {
1288
+ setLatchedError(
1289
+ (prev) => prev && prev.source === source && prev.contextKey === key ? null : prev
1290
+ );
1291
+ }, []);
1292
+ const surfaceError = react.useMemo(
1293
+ () => mero && !ephemeral ? new Error(NO_EPHEMERAL_SURFACE_MESSAGE) : null,
1294
+ [mero, ephemeral]
1295
+ );
1296
+ const error = surfaceError ?? (latchedError && latchedError.contextKey === contextId && !(latchedError.source === "identity" && includeSelf) ? latchedError.error : null);
1297
+ const receivedAtRef = react.useRef(/* @__PURE__ */ new Map());
1298
+ const codecRef = react.useRef(options.codec);
1299
+ codecRef.current = options.codec;
1300
+ const initialRef = react.useRef(options.initial);
1301
+ initialRef.current = options.initial;
1302
+ const selfRef = react.useRef(null);
1303
+ const lastLocalRef = react.useRef(options.initial);
1304
+ const pendingRef = react.useRef(void 0);
1305
+ const timerRef = react.useRef(null);
1306
+ const lastSentAtRef = react.useRef(Date.now());
1307
+ const replaySeenRef = react.useRef(null);
1308
+ const replayTimerRef = react.useRef(null);
1309
+ const ageOf = react.useCallback((author) => {
1310
+ const at = receivedAtRef.current.get(author);
1311
+ return at === void 0 ? void 0 : Date.now() - at;
1312
+ }, []);
1313
+ react.useEffect(() => {
1314
+ setPeers(/* @__PURE__ */ new Map());
1315
+ receivedAtRef.current = /* @__PURE__ */ new Map();
1316
+ if (timerRef.current) {
1317
+ clearTimeout(timerRef.current);
1318
+ timerRef.current = null;
1319
+ }
1320
+ if (replayTimerRef.current) {
1321
+ clearTimeout(replayTimerRef.current);
1322
+ replayTimerRef.current = null;
1323
+ }
1324
+ replaySeenRef.current = null;
1325
+ pendingRef.current = void 0;
1326
+ lastLocalRef.current = initialRef.current;
1327
+ selfRef.current = null;
1328
+ setLatchedError(null);
1329
+ }, [contextId]);
1330
+ react.useEffect(() => {
1331
+ if (!mero || !contextId) return;
1332
+ const key = contextId;
1333
+ let cancelled = false;
1334
+ mero.admin.getContextIdentitiesOwned(contextId).then((res) => {
1335
+ if (cancelled) return;
1336
+ const self = res.identities?.[0] ?? null;
1337
+ selfRef.current = self;
1338
+ if (!self) {
1339
+ setSourceError("identity", new Error(COULD_NOT_RESOLVE_IDENTITY_MESSAGE), key);
1340
+ return;
1341
+ }
1342
+ clearSourceError("identity", key);
1343
+ if (!includeSelf) {
1344
+ receivedAtRef.current.delete(self);
1345
+ setPeers((prev) => {
1346
+ if (!prev.has(self)) return prev;
1347
+ const next = new Map(prev);
1348
+ next.delete(self);
1349
+ return next;
1350
+ });
1351
+ }
1352
+ }).catch((err) => {
1353
+ if (!cancelled) {
1354
+ setSourceError("identity", toError(err), key);
1355
+ }
1356
+ });
1357
+ return () => {
1358
+ cancelled = true;
1359
+ };
1360
+ }, [mero, contextId, includeSelf, setSourceError, clearSourceError]);
1361
+ react.useEffect(() => {
1362
+ if (!ephemeral || !contextId) return;
1363
+ const closeReplayWindow = () => {
1364
+ replayTimerRef.current = null;
1365
+ const seen = replaySeenRef.current;
1366
+ replaySeenRef.current = null;
1367
+ if (!seen) return;
1368
+ setPeers((prev) => {
1369
+ let next = null;
1370
+ for (const author of prev.keys()) {
1371
+ if (seen.has(author)) continue;
1372
+ if (!next) next = new Map(prev);
1373
+ next.delete(author);
1374
+ receivedAtRef.current.delete(author);
1375
+ }
1376
+ return next ?? prev;
1377
+ });
1378
+ };
1379
+ const unsubscribe = ephemeral.subscribe(
1380
+ contextId,
1381
+ (entry) => {
1382
+ const { author, state } = entry;
1383
+ if (entry.ageMs !== void 0) {
1384
+ if (!replaySeenRef.current) replaySeenRef.current = /* @__PURE__ */ new Set();
1385
+ if (replayTimerRef.current) clearTimeout(replayTimerRef.current);
1386
+ replayTimerRef.current = setTimeout(closeReplayWindow, REPLAY_RECONCILE_MS);
1387
+ }
1388
+ if (entry.removed || state === void 0) {
1389
+ replaySeenRef.current?.delete(author);
1390
+ receivedAtRef.current.delete(author);
1391
+ setPeers((prev) => {
1392
+ if (!prev.has(author)) return prev;
1393
+ const next = new Map(prev);
1394
+ next.delete(author);
1395
+ return next;
1396
+ });
1397
+ return;
1398
+ }
1399
+ if (!includeSelf && author === selfRef.current) return;
1400
+ replaySeenRef.current?.add(author);
1401
+ receivedAtRef.current.set(author, Date.now() - (entry.ageMs ?? 0));
1402
+ setPeers((prev) => new Map(prev).set(author, state));
1403
+ },
1404
+ codecRef.current
1405
+ );
1406
+ return () => {
1407
+ if (replayTimerRef.current) {
1408
+ clearTimeout(replayTimerRef.current);
1409
+ replayTimerRef.current = null;
1410
+ }
1411
+ replaySeenRef.current = null;
1412
+ unsubscribe();
1413
+ };
1414
+ }, [ephemeral, contextId, includeSelf]);
1415
+ const publish = react.useCallback((value) => {
1416
+ if (!ephemeral || !contextId) return;
1417
+ const key = contextId;
1418
+ lastSentAtRef.current = Date.now();
1419
+ void ephemeral.set(key, value, codecRef.current).then(
1420
+ () => clearSourceError("publish", key),
1421
+ (err) => setSourceError("publish", toError(err), key)
1422
+ );
1423
+ }, [ephemeral, contextId, setSourceError, clearSourceError]);
1424
+ const setPresence = react.useCallback((partial) => {
1425
+ const next = { ...lastLocalRef.current ?? {}, ...partial };
1426
+ lastLocalRef.current = next;
1427
+ const throttleMs = options.throttleMs ?? 30;
1428
+ if (throttleMs <= 0) {
1429
+ publish(next);
1430
+ return;
1431
+ }
1432
+ pendingRef.current = next;
1433
+ if (timerRef.current) return;
1434
+ const elapsed = Date.now() - lastSentAtRef.current;
1435
+ const delay = Math.max(throttleMs - elapsed, 0);
1436
+ timerRef.current = setTimeout(() => {
1437
+ timerRef.current = null;
1438
+ const queued = pendingRef.current;
1439
+ pendingRef.current = void 0;
1440
+ if (queued !== void 0) publish(queued);
1441
+ }, delay);
1442
+ }, [publish, options.throttleMs]);
1443
+ react.useEffect(() => () => {
1444
+ if (timerRef.current) clearTimeout(timerRef.current);
1445
+ }, []);
1446
+ return { peers, setPresence, ageOf, error };
1447
+ }
1269
1448
  function toError(err) {
1270
1449
  return err instanceof Error ? err : new Error(String(err));
1271
1450
  }
@@ -1811,6 +1990,15 @@ function useNamespace(namespaceId) {
1811
1990
  );
1812
1991
  return { namespace: data, loading, error, refetch };
1813
1992
  }
1993
+ function useNodeIdentity() {
1994
+ const { mero } = useMero();
1995
+ const { data, loading, error, refetch } = useAsyncResource(
1996
+ mero ? () => mero.admin.getNodeIdentity() : null,
1997
+ null,
1998
+ [mero]
1999
+ );
2000
+ return { identity: data, loading, error, refetch };
2001
+ }
1814
2002
  function useNamespaceIdentity(namespaceId) {
1815
2003
  const { mero } = useMero();
1816
2004
  const { data, loading, error, refetch } = useAsyncResource(
@@ -2180,6 +2368,35 @@ function useDetachContextFromGroup() {
2180
2368
  );
2181
2369
  return { detachContextFromGroup, loading, error };
2182
2370
  }
2371
+ var DEFAULT_MIGRATION_POLL_INTERVAL_MS = 5e3;
2372
+ function useMigrationEvents(namespaceId, onEvent) {
2373
+ const { mero } = useMero();
2374
+ const mountedRef = useMountedRef();
2375
+ const [live, setLive] = react.useState(false);
2376
+ const handlerRef = react.useRef(onEvent);
2377
+ handlerRef.current = onEvent;
2378
+ react.useEffect(() => {
2379
+ if (!mero || !namespaceId) return;
2380
+ const sse = mero.events;
2381
+ const unsubscribe = sse.onMigrationEvent((event) => handlerRef.current(event));
2382
+ let cancelled = false;
2383
+ void (async () => {
2384
+ try {
2385
+ await sse.connect();
2386
+ await sse.subscribe({ groupIds: [namespaceId] });
2387
+ if (!cancelled && mountedRef.current) setLive(true);
2388
+ } catch {
2389
+ if (!cancelled && mountedRef.current) setLive(false);
2390
+ }
2391
+ })();
2392
+ return () => {
2393
+ cancelled = true;
2394
+ if (mountedRef.current) setLive(false);
2395
+ unsubscribe();
2396
+ };
2397
+ }, [mero, namespaceId, mountedRef]);
2398
+ return live;
2399
+ }
2183
2400
  function useMigrationStatus(namespaceId, options) {
2184
2401
  const { mero } = useMero();
2185
2402
  const [status, setStatus] = react.useState(null);
@@ -2224,7 +2441,8 @@ function useMigrationStatus(namespaceId, options) {
2224
2441
  react.useEffect(() => {
2225
2442
  void refetch();
2226
2443
  }, [refetch]);
2227
- const pollIntervalMs = options?.pollIntervalMs;
2444
+ const sseLive = useMigrationEvents(namespaceId, () => void refetch());
2445
+ const pollIntervalMs = options?.pollIntervalMs ?? (sseLive ? void 0 : DEFAULT_MIGRATION_POLL_INTERVAL_MS);
2228
2446
  react.useEffect(() => {
2229
2447
  if (!pollIntervalMs || !mero || !namespaceId) return;
2230
2448
  const handle = setInterval(() => void refetch(), pollIntervalMs);
@@ -2237,6 +2455,12 @@ function useMigrationStatus(namespaceId, options) {
2237
2455
  rollup,
2238
2456
  members,
2239
2457
  allMigrated: rollup?.allMigrated ?? false,
2458
+ /**
2459
+ * When this node watched the fleet converge, or `null`. Durable, unlike
2460
+ * `allMigrated`, which is recomputed from in-TTL heartbeats and lapses back
2461
+ * to false when a member simply goes quiet.
2462
+ */
2463
+ fleetCompletedAt: status?.fleetCompletedAt ?? null,
2240
2464
  membersPendingSignature: rollup?.membersPendingSignature ?? 0,
2241
2465
  /** Members whose migrate aborted (migration-check failed or apply errored). */
2242
2466
  failed: rollup?.failed ?? 0,
@@ -2356,7 +2580,6 @@ var EMPTY_GROUP_APP_VERSION = {
2356
2580
  version: null,
2357
2581
  applicationId: null,
2358
2582
  pendingApply: false,
2359
- upgradePolicy: null,
2360
2583
  activeUpgrade: null
2361
2584
  };
2362
2585
  function isZeroAppKey(appKey) {
@@ -2388,17 +2611,14 @@ function useGroupAppVersion(groupId) {
2388
2611
  const namespace = namespaces.find((entry) => entry.namespaceId === groupId);
2389
2612
  let appKey;
2390
2613
  let applicationId;
2391
- let upgradePolicy;
2392
2614
  let activeUpgrade = null;
2393
2615
  if (namespace) {
2394
2616
  appKey = namespace.appKey;
2395
2617
  applicationId = namespace.targetApplicationId;
2396
- upgradePolicy = namespace.upgradePolicy ?? null;
2397
2618
  } else {
2398
2619
  const info = await mero.admin.getGroupInfo(groupId);
2399
2620
  appKey = info.appKey;
2400
2621
  applicationId = info.targetApplicationId;
2401
- upgradePolicy = info.upgradePolicy ?? null;
2402
2622
  activeUpgrade = info.activeUpgrade ?? null;
2403
2623
  }
2404
2624
  let version = null;
@@ -2412,7 +2632,7 @@ function useGroupAppVersion(groupId) {
2412
2632
  }
2413
2633
  }
2414
2634
  if (mountedRef.current && seq === reqRef.current) {
2415
- setData({ version, applicationId, pendingApply, upgradePolicy, activeUpgrade });
2635
+ setData({ version, applicationId, pendingApply, activeUpgrade });
2416
2636
  }
2417
2637
  } catch (err) {
2418
2638
  const errorValue = toError(err);
@@ -2623,6 +2843,7 @@ exports.AppMode = AppMode;
2623
2843
  exports.CalimeroLogo = CalimeroLogo;
2624
2844
  exports.ConnectButton = ConnectButton;
2625
2845
  exports.ConnectionType = ConnectionType;
2846
+ exports.DEFAULT_MIGRATION_POLL_INTERVAL_MS = DEFAULT_MIGRATION_POLL_INTERVAL_MS;
2626
2847
  exports.LoginModal = LoginModal;
2627
2848
  exports.MERO_CSS_VARS = MERO_CSS_VARS;
2628
2849
  exports.MeroContext = MeroContext;
@@ -2665,6 +2886,7 @@ exports.useDeleteContext = useDeleteContext;
2665
2886
  exports.useDeleteGroup = useDeleteGroup;
2666
2887
  exports.useDeleteNamespace = useDeleteNamespace;
2667
2888
  exports.useDetachContextFromGroup = useDetachContextFromGroup;
2889
+ exports.useEphemeral = useEphemeral;
2668
2890
  exports.useExecute = useExecute;
2669
2891
  exports.useGroupAppVersion = useGroupAppVersion;
2670
2892
  exports.useGroupCapabilities = useGroupCapabilities;
@@ -2682,6 +2904,7 @@ exports.useJoinSubgroupInheritance = useJoinSubgroupInheritance;
2682
2904
  exports.useLatestVersion = useLatestVersion;
2683
2905
  exports.useMemberMetadata = useMemberMetadata;
2684
2906
  exports.useMero = useMero;
2907
+ exports.useMigrationEvents = useMigrationEvents;
2685
2908
  exports.useMigrationStatus = useMigrationStatus;
2686
2909
  exports.useMyAuthoredMigration = useMyAuthoredMigration;
2687
2910
  exports.useNamespace = useNamespace;
@@ -2689,6 +2912,7 @@ exports.useNamespaceGroups = useNamespaceGroups;
2689
2912
  exports.useNamespaceIdentity = useNamespaceIdentity;
2690
2913
  exports.useNamespaces = useNamespaces;
2691
2914
  exports.useNamespacesForApplication = useNamespacesForApplication;
2915
+ exports.useNodeIdentity = useNodeIdentity;
2692
2916
  exports.useRemoveGroupMembers = useRemoveGroupMembers;
2693
2917
  exports.useReparentGroup = useReparentGroup;
2694
2918
  exports.useResyncContext = useResyncContext;