@rpcbase/client 0.435.0 → 0.436.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.js CHANGED
@@ -2303,6 +2303,17 @@ const runNotificationDigest = async ({
2303
2303
  skippedReason: result.skippedReason
2304
2304
  };
2305
2305
  };
2306
+ const NEW_NOTIFICATION_START_TOLERANCE_MS = 5e3;
2307
+ const NATIVE_NOTIFICATION_TAG_PREFIX = "rb-notification";
2308
+ const ACTIVE_TAB_HEARTBEAT_MS = 5e3;
2309
+ const ACTIVE_TAB_TTL_MS = 15e3;
2310
+ const DELIVERY_TTL_MS = 10 * 60 * 1e3;
2311
+ const FALLBACK_DELIVERY_LOCK_TTL_MS = 2e3;
2312
+ const TAB_STATE_KEY_PREFIX = "rb:notifications:tab:";
2313
+ const DELIVERY_KEY_PREFIX = "rb:notifications:delivery:";
2314
+ const DELIVERY_LOCK_PREFIX = "rb-notifications-delivery:";
2315
+ const FALLBACK_DELIVERY_LOCK_KEY_PREFIX = "rb:notifications:delivery-lock:";
2316
+ const NOTIFICATION_TAB_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2306
2317
  const NotificationsRealtimeContext = createContext(null);
2307
2318
  const toIso = (value) => {
2308
2319
  if (value instanceof Date) return value.toISOString();
@@ -2337,8 +2348,267 @@ const buildDisabledTopics = (settings) => {
2337
2348
  return pref.inApp === false ? topic : null;
2338
2349
  }).filter((topic) => Boolean(topic));
2339
2350
  };
2351
+ const getWebpushLink = (item) => {
2352
+ const platformWebpush = item.platform?.webpush;
2353
+ if (!platformWebpush || typeof platformWebpush !== "object" || Array.isArray(platformWebpush)) return "";
2354
+ const fcmOptions = platformWebpush.fcmOptions;
2355
+ if (!fcmOptions || typeof fcmOptions !== "object" || Array.isArray(fcmOptions)) return "";
2356
+ return typeof fcmOptions.link === "string" ? fcmOptions.link : "";
2357
+ };
2358
+ const getScopedUserKey = (userId) => encodeURIComponent(userId);
2359
+ const getTabStateKey = (userId) => `${TAB_STATE_KEY_PREFIX}${getScopedUserKey(userId)}:${NOTIFICATION_TAB_ID}`;
2360
+ const getTabStateKeyPrefix = (userId) => `${TAB_STATE_KEY_PREFIX}${getScopedUserKey(userId)}:`;
2361
+ const getDeliveryKey = (userId, notificationId) => `${DELIVERY_KEY_PREFIX}${getScopedUserKey(userId)}:${encodeURIComponent(notificationId)}`;
2362
+ const getDeliveryLockName = (userId) => `${DELIVERY_LOCK_PREFIX}${getScopedUserKey(userId)}`;
2363
+ const getFallbackDeliveryLockKey = (userId) => `${FALLBACK_DELIVERY_LOCK_KEY_PREFIX}${getScopedUserKey(userId)}`;
2364
+ const getLocalStorage = () => {
2365
+ if (typeof window === "undefined") return null;
2366
+ try {
2367
+ return window.localStorage;
2368
+ } catch {
2369
+ return null;
2370
+ }
2371
+ };
2372
+ const parseJsonRecord = (value) => {
2373
+ if (!value) return null;
2374
+ try {
2375
+ const parsed = JSON.parse(value);
2376
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2377
+ return parsed;
2378
+ } catch {
2379
+ return null;
2380
+ }
2381
+ };
2382
+ const getLockManager = () => {
2383
+ if (typeof navigator === "undefined") return null;
2384
+ const locks = navigator.locks;
2385
+ return locks && typeof locks.request === "function" ? locks : null;
2386
+ };
2387
+ const isDocumentActive = () => {
2388
+ if (typeof document === "undefined") return false;
2389
+ return document.visibilityState === "visible" && document.hasFocus();
2390
+ };
2391
+ const writeNotificationTabState = (userId) => {
2392
+ const storage = getLocalStorage();
2393
+ if (!storage) return;
2394
+ const state = {
2395
+ tabId: NOTIFICATION_TAB_ID,
2396
+ active: isDocumentActive(),
2397
+ updatedAt: Date.now()
2398
+ };
2399
+ try {
2400
+ storage.setItem(getTabStateKey(userId), JSON.stringify(state));
2401
+ } catch {
2402
+ return;
2403
+ }
2404
+ };
2405
+ const removeNotificationTabState = (userId) => {
2406
+ const storage = getLocalStorage();
2407
+ if (!storage) return;
2408
+ try {
2409
+ storage.removeItem(getTabStateKey(userId));
2410
+ } catch {
2411
+ return;
2412
+ }
2413
+ };
2414
+ const hasActiveNotificationTab = (userId) => {
2415
+ const storage = getLocalStorage();
2416
+ if (!storage) return false;
2417
+ const prefix = getTabStateKeyPrefix(userId);
2418
+ const now = Date.now();
2419
+ try {
2420
+ for (let index = storage.length - 1; index >= 0; index -= 1) {
2421
+ const key = storage.key(index);
2422
+ if (!key?.startsWith(prefix)) continue;
2423
+ const record = parseJsonRecord(storage.getItem(key));
2424
+ const updatedAt = typeof record?.updatedAt === "number" ? record.updatedAt : 0;
2425
+ if (!updatedAt || now - updatedAt > ACTIVE_TAB_TTL_MS) {
2426
+ storage.removeItem(key);
2427
+ continue;
2428
+ }
2429
+ if (record?.active === true) return true;
2430
+ }
2431
+ } catch {
2432
+ return false;
2433
+ }
2434
+ return false;
2435
+ };
2436
+ const claimNotificationDeliveriesFromStorage = (items, userId, mode) => {
2437
+ const storage = getLocalStorage();
2438
+ if (!storage) return items;
2439
+ const now = Date.now();
2440
+ const claimed = [];
2441
+ for (const item of items) {
2442
+ const key = getDeliveryKey(userId, item.id);
2443
+ const current = parseJsonRecord(storage.getItem(key));
2444
+ const deliveredAt = typeof current?.deliveredAt === "number" ? current.deliveredAt : 0;
2445
+ if (deliveredAt && now - deliveredAt < DELIVERY_TTL_MS) continue;
2446
+ const marker = {
2447
+ tabId: NOTIFICATION_TAB_ID,
2448
+ mode,
2449
+ deliveredAt: now
2450
+ };
2451
+ try {
2452
+ storage.setItem(key, JSON.stringify(marker));
2453
+ const stored = parseJsonRecord(storage.getItem(key));
2454
+ if (stored?.tabId === NOTIFICATION_TAB_ID && stored.deliveredAt === now) {
2455
+ claimed.push(item);
2456
+ }
2457
+ } catch {
2458
+ claimed.push(item);
2459
+ }
2460
+ }
2461
+ return claimed;
2462
+ };
2463
+ const acquireFallbackDeliveryLock = (userId) => {
2464
+ const storage = getLocalStorage();
2465
+ if (!storage) {
2466
+ return {
2467
+ token: `${NOTIFICATION_TAB_ID}:memory`,
2468
+ expiresAt: Date.now() + FALLBACK_DELIVERY_LOCK_TTL_MS
2469
+ };
2470
+ }
2471
+ const key = getFallbackDeliveryLockKey(userId);
2472
+ const now = Date.now();
2473
+ try {
2474
+ const current = parseJsonRecord(storage.getItem(key));
2475
+ const currentExpiresAt = typeof current?.expiresAt === "number" ? current.expiresAt : 0;
2476
+ if (currentExpiresAt > now && current?.token !== void 0) return null;
2477
+ const marker = {
2478
+ token: `${NOTIFICATION_TAB_ID}:${now}:${Math.random().toString(36).slice(2)}`,
2479
+ expiresAt: now + FALLBACK_DELIVERY_LOCK_TTL_MS
2480
+ };
2481
+ storage.setItem(key, JSON.stringify(marker));
2482
+ const stored = parseJsonRecord(storage.getItem(key));
2483
+ return stored?.token === marker.token ? marker : null;
2484
+ } catch {
2485
+ return {
2486
+ token: `${NOTIFICATION_TAB_ID}:memory`,
2487
+ expiresAt: now + FALLBACK_DELIVERY_LOCK_TTL_MS
2488
+ };
2489
+ }
2490
+ };
2491
+ const releaseFallbackDeliveryLock = (userId, marker) => {
2492
+ const storage = getLocalStorage();
2493
+ if (!storage) return;
2494
+ const key = getFallbackDeliveryLockKey(userId);
2495
+ try {
2496
+ const stored = parseJsonRecord(storage.getItem(key));
2497
+ if (stored?.token === marker.token) {
2498
+ storage.removeItem(key);
2499
+ }
2500
+ } catch {
2501
+ return;
2502
+ }
2503
+ };
2504
+ const withFallbackDeliveryLock = (userId, callback, fallback) => {
2505
+ const marker = acquireFallbackDeliveryLock(userId);
2506
+ if (!marker) return fallback;
2507
+ try {
2508
+ return callback();
2509
+ } finally {
2510
+ releaseFallbackDeliveryLock(userId, marker);
2511
+ }
2512
+ };
2513
+ const claimNotificationDeliveries = async (items, userId, mode) => {
2514
+ if (items.length === 0) return [];
2515
+ const claim = () => {
2516
+ if (mode === "native" && hasActiveNotificationTab(userId)) return [];
2517
+ return claimNotificationDeliveriesFromStorage(items, userId, mode);
2518
+ };
2519
+ const locks = getLockManager();
2520
+ if (!locks) return withFallbackDeliveryLock(userId, claim, []);
2521
+ try {
2522
+ const claimed = await locks.request(getDeliveryLockName(userId), {
2523
+ mode: "exclusive",
2524
+ ifAvailable: true
2525
+ }, (lock) => {
2526
+ if (!lock) return [];
2527
+ return claim();
2528
+ });
2529
+ return Array.isArray(claimed) ? claimed : [];
2530
+ } catch {
2531
+ return claim();
2532
+ }
2533
+ };
2534
+ const showInAppNotifications = (items) => {
2535
+ if (items.length > 3) {
2536
+ toast(`${items.length} new notifications`, {
2537
+ description: "Open the notifications drawer to view them."
2538
+ });
2539
+ return;
2540
+ }
2541
+ for (const item of items) {
2542
+ toast(item.title, {
2543
+ description: item.body
2544
+ });
2545
+ }
2546
+ };
2547
+ const canShowNativeNotification = () => {
2548
+ if (typeof window === "undefined") return false;
2549
+ if (!("Notification" in window)) return false;
2550
+ return window.Notification.permission === "granted";
2551
+ };
2552
+ const focusNotificationLink = (link) => {
2553
+ window.focus();
2554
+ if (link) {
2555
+ window.location.href = link;
2556
+ }
2557
+ };
2558
+ const showNativeNotification = (item) => {
2559
+ if (!canShowNativeNotification()) return;
2560
+ try {
2561
+ const link = getWebpushLink(item);
2562
+ const browserNotification = new window.Notification(item.title, {
2563
+ body: item.body,
2564
+ icon: item.image,
2565
+ tag: `${NATIVE_NOTIFICATION_TAG_PREFIX}:${item.id}`,
2566
+ data: {
2567
+ ...item.data,
2568
+ notificationId: item.id,
2569
+ link
2570
+ }
2571
+ });
2572
+ browserNotification.onclick = () => focusNotificationLink(link);
2573
+ } catch {
2574
+ return;
2575
+ }
2576
+ };
2577
+ const showNativeNotifications = (items) => {
2578
+ if (!canShowNativeNotification()) return;
2579
+ if (items.length > 3) {
2580
+ try {
2581
+ const browserNotification = new window.Notification(`${items.length} new notifications`, {
2582
+ body: "Open the notifications drawer to view them.",
2583
+ tag: `${NATIVE_NOTIFICATION_TAG_PREFIX}:summary`
2584
+ });
2585
+ browserNotification.onclick = () => focusNotificationLink("");
2586
+ } catch {
2587
+ return;
2588
+ }
2589
+ return;
2590
+ }
2591
+ for (const item of items) {
2592
+ showNativeNotification(item);
2593
+ }
2594
+ };
2595
+ const showNotificationsForCurrentFocusState = async (items, userId) => {
2596
+ writeNotificationTabState(userId);
2597
+ if (isDocumentActive()) {
2598
+ const claimed2 = await claimNotificationDeliveries(items, userId, "in-app");
2599
+ if (claimed2.length > 0) {
2600
+ showInAppNotifications(claimed2);
2601
+ }
2602
+ return;
2603
+ }
2604
+ if (!canShowNativeNotification()) return;
2605
+ const claimed = await claimNotificationDeliveries(items, userId, "native");
2606
+ if (claimed.length > 0) {
2607
+ showNativeNotifications(claimed);
2608
+ }
2609
+ };
2340
2610
  function NotificationsRealtimeProvider(t0) {
2341
- const $ = c(47);
2611
+ const $ = c(52);
2342
2612
  const {
2343
2613
  userId,
2344
2614
  limit: t1,
@@ -2547,8 +2817,47 @@ function NotificationsRealtimeProvider(t0) {
2547
2817
  useEffect(t15, t16);
2548
2818
  let t17;
2549
2819
  let t18;
2550
- if ($[32] !== canUseRts || $[33] !== notifications || $[34] !== notificationsQuery.loading || $[35] !== toastOnNew) {
2820
+ if ($[32] !== canUseRts || $[33] !== trimmedUserId) {
2551
2821
  t17 = () => {
2822
+ if (!canUseRts) {
2823
+ return;
2824
+ }
2825
+ if (typeof window === "undefined" || typeof document === "undefined") {
2826
+ return;
2827
+ }
2828
+ const writeState = () => writeNotificationTabState(trimmedUserId);
2829
+ const removeState = () => removeNotificationTabState(trimmedUserId);
2830
+ writeState();
2831
+ const intervalId = window.setInterval(writeState, ACTIVE_TAB_HEARTBEAT_MS);
2832
+ window.addEventListener("focus", writeState);
2833
+ window.addEventListener("blur", writeState);
2834
+ window.addEventListener("pageshow", writeState);
2835
+ window.addEventListener("pagehide", removeState);
2836
+ document.addEventListener("visibilitychange", writeState);
2837
+ return () => {
2838
+ window.clearInterval(intervalId);
2839
+ window.removeEventListener("focus", writeState);
2840
+ window.removeEventListener("blur", writeState);
2841
+ window.removeEventListener("pageshow", writeState);
2842
+ window.removeEventListener("pagehide", removeState);
2843
+ document.removeEventListener("visibilitychange", writeState);
2844
+ removeState();
2845
+ };
2846
+ };
2847
+ t18 = [canUseRts, trimmedUserId];
2848
+ $[32] = canUseRts;
2849
+ $[33] = trimmedUserId;
2850
+ $[34] = t17;
2851
+ $[35] = t18;
2852
+ } else {
2853
+ t17 = $[34];
2854
+ t18 = $[35];
2855
+ }
2856
+ useEffect(t17, t18);
2857
+ let t19;
2858
+ let t20;
2859
+ if ($[36] !== canUseRts || $[37] !== notifications || $[38] !== notificationsQuery.loading || $[39] !== toastOnNew || $[40] !== trimmedUserId) {
2860
+ t19 = () => {
2552
2861
  if (!toastOnNew) {
2553
2862
  return;
2554
2863
  }
@@ -2561,83 +2870,80 @@ function NotificationsRealtimeProvider(t0) {
2561
2870
  if (!hasSeededToastIds.current) {
2562
2871
  hasSeededToastIds.current = true;
2563
2872
  lastToastIds.current = new Set(notifications.map(_temp5));
2873
+ const eligible = notifications.filter((n_3) => {
2874
+ const createdAtMs = Date.parse(n_3.createdAt);
2875
+ if (!Number.isFinite(createdAtMs)) {
2876
+ return false;
2877
+ }
2878
+ return createdAtMs >= toastStartMs.current - NEW_NOTIFICATION_START_TOLERANCE_MS;
2879
+ });
2880
+ if (eligible.length) {
2881
+ showNotificationsForCurrentFocusState(eligible, trimmedUserId);
2882
+ }
2564
2883
  return;
2565
2884
  }
2566
- const nextNew = notifications.filter((n_3) => !lastToastIds.current.has(n_3.id));
2885
+ const nextNew = notifications.filter((n_4) => !lastToastIds.current.has(n_4.id));
2567
2886
  if (!nextNew.length) {
2568
2887
  return;
2569
2888
  }
2570
- for (const n_4 of nextNew) {
2571
- lastToastIds.current.add(n_4.id);
2889
+ for (const n_5 of nextNew) {
2890
+ lastToastIds.current.add(n_5.id);
2572
2891
  }
2573
- const isTabActive = typeof document !== "undefined" && document.visibilityState === "visible";
2574
- if (!isTabActive) {
2575
- return;
2576
- }
2577
- const eligible = nextNew.filter((n_5) => {
2578
- const createdAtMs = Date.parse(n_5.createdAt);
2579
- if (!Number.isFinite(createdAtMs)) {
2892
+ const eligible_0 = nextNew.filter((n_6) => {
2893
+ const createdAtMs_0 = Date.parse(n_6.createdAt);
2894
+ if (!Number.isFinite(createdAtMs_0)) {
2580
2895
  return true;
2581
2896
  }
2582
- return createdAtMs >= toastStartMs.current;
2897
+ return createdAtMs_0 >= toastStartMs.current;
2583
2898
  });
2584
- if (!eligible.length) {
2585
- return;
2586
- }
2587
- if (eligible.length > 3) {
2588
- toast(`${eligible.length} new notifications`, {
2589
- description: "Open the notifications drawer to view them."
2590
- });
2899
+ if (!eligible_0.length) {
2591
2900
  return;
2592
2901
  }
2593
- for (const n_6 of eligible) {
2594
- toast(n_6.title, {
2595
- description: n_6.body
2596
- });
2597
- }
2902
+ showNotificationsForCurrentFocusState(eligible_0, trimmedUserId);
2598
2903
  };
2599
- t18 = [canUseRts, notifications, notificationsQuery.loading, toastOnNew];
2600
- $[32] = canUseRts;
2601
- $[33] = notifications;
2602
- $[34] = notificationsQuery.loading;
2603
- $[35] = toastOnNew;
2604
- $[36] = t17;
2605
- $[37] = t18;
2904
+ t20 = [canUseRts, notifications, notificationsQuery.loading, toastOnNew, trimmedUserId];
2905
+ $[36] = canUseRts;
2906
+ $[37] = notifications;
2907
+ $[38] = notificationsQuery.loading;
2908
+ $[39] = toastOnNew;
2909
+ $[40] = trimmedUserId;
2910
+ $[41] = t19;
2911
+ $[42] = t20;
2606
2912
  } else {
2607
- t17 = $[36];
2608
- t18 = $[37];
2913
+ t19 = $[41];
2914
+ t20 = $[42];
2609
2915
  }
2610
- useEffect(t17, t18);
2611
- let t19;
2612
- if ($[38] !== notifications || $[39] !== notificationsQuery.error || $[40] !== notificationsQuery.loading || $[41] !== unreadCount || $[42] !== unseenCount) {
2613
- t19 = {
2916
+ useEffect(t19, t20);
2917
+ let t21;
2918
+ if ($[43] !== notifications || $[44] !== notificationsQuery.error || $[45] !== notificationsQuery.loading || $[46] !== unreadCount || $[47] !== unseenCount) {
2919
+ t21 = {
2614
2920
  notifications,
2615
2921
  unreadCount,
2616
2922
  unseenCount,
2617
2923
  loading: notificationsQuery.loading,
2618
2924
  error: notificationsQuery.error
2619
2925
  };
2620
- $[38] = notifications;
2621
- $[39] = notificationsQuery.error;
2622
- $[40] = notificationsQuery.loading;
2623
- $[41] = unreadCount;
2624
- $[42] = unseenCount;
2625
- $[43] = t19;
2926
+ $[43] = notifications;
2927
+ $[44] = notificationsQuery.error;
2928
+ $[45] = notificationsQuery.loading;
2929
+ $[46] = unreadCount;
2930
+ $[47] = unseenCount;
2931
+ $[48] = t21;
2626
2932
  } else {
2627
- t19 = $[43];
2628
- }
2629
- const value = t19;
2630
- const t20 = canUseRts ? value : null;
2631
- let t21;
2632
- if ($[44] !== children || $[45] !== t20) {
2633
- t21 = /* @__PURE__ */ jsx(NotificationsRealtimeContext.Provider, { value: t20, children });
2634
- $[44] = children;
2635
- $[45] = t20;
2636
- $[46] = t21;
2933
+ t21 = $[48];
2934
+ }
2935
+ const value = t21;
2936
+ const t22 = canUseRts ? value : null;
2937
+ let t23;
2938
+ if ($[49] !== children || $[50] !== t22) {
2939
+ t23 = /* @__PURE__ */ jsx(NotificationsRealtimeContext.Provider, { value: t22, children });
2940
+ $[49] = children;
2941
+ $[50] = t22;
2942
+ $[51] = t23;
2637
2943
  } else {
2638
- t21 = $[46];
2944
+ t23 = $[51];
2639
2945
  }
2640
- return t21;
2946
+ return t23;
2641
2947
  }
2642
2948
  function _temp5(n_2) {
2643
2949
  return n_2.id;