@byscott-io/where-is-waldo 0.1.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/LICENSE +21 -0
- package/README.md +466 -0
- package/dist/index.esm.js +895 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +919 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/src/cable/index.js +120 -0
- package/src/context/PresenceContext.jsx +71 -0
- package/src/core/presenceReporter.js +163 -0
- package/src/core/rosterStore.js +117 -0
- package/src/hooks/usePresence.js +122 -0
- package/src/hooks/useRoster.js +138 -0
- package/src/hooks/useWaldoEvent.js +63 -0
- package/src/index.js +41 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/cable/index.js","../src/core/presenceReporter.js","../src/hooks/usePresence.js","../src/context/PresenceContext.jsx","../src/hooks/useWaldoEvent.js","../src/core/rosterStore.js","../src/hooks/useRoster.js"],"sourcesContent":["import { createConsumer } from '@rails/actioncable';\n\nlet consumer = null;\nlet config = {\n url: '/cable',\n getToken: null,\n};\n\n// Subscribe-by-event-type registry: message type -> Set of listener fns.\n// Components subscribe to a raw event type (see subscribeToEvent /\n// useWaldoEvent) and decide for themselves whether they care about a given\n// payload. handleMessage dispatches every incoming message to the listeners\n// registered for its type.\nconst listeners = new Map();\n\n/**\n * Subscribe to a raw cable event type. Returns an unsubscribe function.\n * Multiple subscribers per type are supported; each receives (data, message).\n * @param {string} type - The message type to listen for\n * @param {Function} callback - Called with (data, message) on each matching event\n * @returns {Function} unsubscribe\n */\nexport function subscribeToEvent(type, callback) {\n if (!type || typeof callback !== 'function') return () => {};\n\n let set = listeners.get(type);\n if (!set) {\n set = new Set();\n listeners.set(type, set);\n }\n set.add(callback);\n\n return function unsubscribe() {\n const current = listeners.get(type);\n if (!current) return;\n current.delete(callback);\n if (current.size === 0) listeners.delete(type);\n };\n}\n\n/**\n * Configure the ActionCable connection\n * @param {Object} options\n * @param {string} options.url - WebSocket URL (default: '/cable')\n * @param {Function} options.getToken - Function that returns auth token\n */\nexport function configureCable(options = {}) {\n config = { ...config, ...options };\n // Reset consumer when config changes\n if (consumer) {\n consumer.disconnect();\n consumer = null;\n }\n}\n\n/**\n * Get or create the ActionCable consumer\n * @returns {Consumer}\n */\nexport function getConsumer() {\n if (!consumer) {\n let url = config.url;\n\n if (config.getToken) {\n const token = config.getToken();\n if (token) {\n const separator = url.includes('?') ? '&' : '?';\n url = `${url}${separator}token=${encodeURIComponent(token)}`;\n }\n }\n\n consumer = createConsumer(url);\n }\n\n return consumer;\n}\n\n/**\n * Disconnect and reset the consumer\n */\nexport function disconnectCable() {\n if (consumer) {\n consumer.disconnect();\n consumer = null;\n }\n}\n\n/**\n * Get current cable configuration\n * @returns {Object}\n */\nexport function getCableConfig() {\n return { ...config };\n}\n\n/**\n * Handle an incoming message by dispatching to every listener subscribed to\n * its type.\n * @param {Object} message - The message object with type and data\n * @returns {boolean} - Whether at least one listener was invoked\n */\nexport function handleMessage(message) {\n const { type, data } = message;\n\n const set = listeners.get(type);\n if (!set || set.size === 0) return false;\n\n // Iterate a snapshot so a listener that unsubscribes during dispatch is safe.\n Array.from(set).forEach((cb) => {\n try {\n cb(data, message);\n } catch (err) {\n // One bad listener must not block the others.\n // eslint-disable-next-line no-console\n console.error(`[where-is-waldo] listener for \"${type}\" threw:`, err);\n }\n });\n\n return true;\n}\n","// Framework-agnostic presence reporter: the session-state -> heartbeat state\n// machine, with NO DOM and NO React Native APIs. It owns the ActionCable\n// subscription, the activity/visibility state, and the heartbeat cadence\n// (interval + immediate beats on transitions). Platform layers feed it sensor\n// inputs and it reports state back — so the web hook (DOM listeners) and a\n// React Native reporter (AppState + touch) drive the exact same logic.\n//\n// const reporter = createPresenceReporter({ metadata: { platform: 'mobile' } });\n// reporter.start();\n// // sensors:\n// reporter.setVisible(isForeground); // tab hidden / app backgrounded => false\n// reporter.reportActivity(); // a user interaction happened\n// // teardown:\n// reporter.stop();\n//\n// The cable deps (consumer, cable config, message bus) are injectable so the\n// core is unit-testable without a real socket; they default to the shared cable\n// module, which works on React Native with the built-in WebSocket.\n\nimport { getConsumer, getCableConfig, handleMessage } from '../cable';\n\nconst DEFAULTS = {\n channelName: 'WhereIsWaldo::PresenceChannel',\n heartbeatInterval: 30000, // ms\n activityTimeout: 30000, // ms of no activity -> subject_active=false\n metadata: {},\n debug: false,\n};\n\nexport function createPresenceReporter(options = {}) {\n const config = { ...DEFAULTS, ...options };\n const now = config.now || (() => Date.now());\n const resolveConsumer = config.getConsumer || getConsumer;\n const resolveCableConfig = config.getCableConfig || getCableConfig;\n const dispatch = config.handleMessage || handleMessage;\n const log = (...args) => {\n // eslint-disable-next-line no-console\n if (config.debug) console.log('[PresenceReporter]', ...args);\n };\n\n let subscription = null;\n let heartbeatTimer = null;\n let activityTimer = null;\n let lastActivityAt = 0;\n\n // visible = tab/app is foreground & visible; active = recent user activity.\n const state = { connected: false, visible: true, active: true, sessionId: null };\n\n const emitChange = () => {\n if (config.onChange) config.onChange({ ...state });\n };\n\n const buildPayload = (overrides = {}) => {\n const payload = {\n tab_visible: overrides.tab_visible ?? state.visible,\n subject_active: overrides.subject_active ?? state.active,\n metadata: config.metadata || {},\n };\n if (lastActivityAt) payload.last_activity_at = lastActivityAt;\n return payload;\n };\n\n const sendHeartbeat = (overrides = {}) => {\n if (!subscription) return;\n log('heartbeat', overrides);\n subscription.perform('heartbeat', buildPayload(overrides));\n };\n\n // (Re)arm the inactivity timer: after activityTimeout with no activity, flip\n // to inactive and send an immediate heartbeat announcing it.\n const scheduleInactivity = () => {\n if (activityTimer) clearTimeout(activityTimer);\n activityTimer = setTimeout(() => {\n if (!state.active) return;\n state.active = false;\n emitChange();\n sendHeartbeat({ subject_active: false });\n }, config.activityTimeout);\n };\n\n const reportActivity = () => {\n lastActivityAt = now();\n if (!state.active) {\n state.active = true;\n emitChange();\n sendHeartbeat({ subject_active: true }); // immediate: inactive -> active\n }\n scheduleInactivity();\n };\n\n const setVisible = (visible) => {\n if (visible === state.visible) return;\n state.visible = visible;\n emitChange();\n sendHeartbeat(); // immediate: visibility change\n if (visible) reportActivity();\n };\n\n const startHeartbeatLoop = () => {\n if (heartbeatTimer) clearInterval(heartbeatTimer);\n heartbeatTimer = setInterval(() => sendHeartbeat(), config.heartbeatInterval);\n };\n\n const start = () => {\n if (subscription) return subscription;\n\n subscription = resolveConsumer().subscriptions.create(\n { channel: config.channelName, metadata: config.metadata || {} },\n {\n connected() {\n state.connected = true;\n const cable = resolveCableConfig();\n if (cable && cable.sessionId) state.sessionId = cable.sessionId;\n emitChange();\n if (config.onConnected) config.onConnected();\n startHeartbeatLoop();\n },\n disconnected() {\n state.connected = false;\n emitChange();\n if (config.onDisconnected) config.onDisconnected();\n },\n received(message) {\n // Honor targeted (per-session) messages, then route to the event bus.\n if (message && message._target_session && message._target_session !== state.sessionId) {\n return;\n }\n dispatch(message);\n },\n }\n );\n\n scheduleInactivity();\n return subscription;\n };\n\n const stop = () => {\n if (heartbeatTimer) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n if (activityTimer) {\n clearTimeout(activityTimer);\n activityTimer = null;\n }\n if (subscription) {\n subscription.unsubscribe();\n subscription = null;\n }\n state.connected = false;\n };\n\n return {\n start,\n stop,\n setVisible,\n reportActivity,\n sendHeartbeat,\n getState: () => ({ ...state }),\n };\n}\n\nexport default createPresenceReporter;\n","import { useState, useEffect, useCallback, useRef } from 'react';\nimport { createPresenceReporter } from '../core/presenceReporter';\n\nconst DEFAULT_OPTIONS = {\n channelName: 'WhereIsWaldo::PresenceChannel',\n heartbeatInterval: 30000,\n activityTimeout: 30000,\n trackActivity: true,\n trackVisibility: true,\n debug: false,\n};\n\nconst initialVisible = () =>\n typeof document === 'undefined' ? true : document.visibilityState !== 'hidden';\nconst initialFocused = () =>\n typeof document === 'undefined' ? true : document.hasFocus();\n\n/**\n * usePresence - browser presence tracking. This is now a thin DOM-sensor\n * wrapper around the framework-agnostic core (../core/presenceReporter): it\n * binds `document`/`window` events to the reporter's `setVisible` /\n * `reportActivity` inputs and mirrors the reporter's state into React state.\n * The heartbeat cadence, activity/visibility state machine, and cable\n * subscription all live in the core, which a React Native reporter reuses.\n *\n * @param {Object} options - see DEFAULT_OPTIONS\n * @returns {{ connected, sessionId, tabVisible, windowFocused, subjectActive, sendHeartbeat }}\n */\nexport function usePresence(options = {}) {\n const config = { ...DEFAULT_OPTIONS, ...options };\n\n const [connected, setConnected] = useState(false);\n const [sessionId, setSessionId] = useState(null);\n const [subjectActive, setSubjectActive] = useState(true);\n const [tabVisible, setTabVisible] = useState(initialVisible);\n const [windowFocused, setWindowFocused] = useState(initialFocused);\n\n const reporterRef = useRef(null);\n // Latest sensor readings, read by the DOM handlers to compute foreground.\n const sensorRef = useRef({ tab: initialVisible(), focus: initialFocused() });\n\n // Create + start the reporter once (re-created only if the channel changes).\n useEffect(() => {\n const reporter = createPresenceReporter({\n channelName: config.channelName,\n heartbeatInterval: config.heartbeatInterval,\n activityTimeout: config.activityTimeout,\n metadata: config.metadata,\n debug: config.debug,\n onConnected: config.onConnected,\n onDisconnected: config.onDisconnected,\n onChange: (state) => {\n setConnected(state.connected);\n setSubjectActive(state.active);\n if (state.sessionId) setSessionId(state.sessionId);\n },\n });\n reporterRef.current = reporter;\n reporter.start();\n // Seed foreground state from the current sensors.\n reporter.setVisible(sensorRef.current.tab && sensorRef.current.focus);\n\n return () => {\n reporter.stop();\n reporterRef.current = null;\n };\n }, [config.channelName]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Activity sensors -> reporter.reportActivity()\n useEffect(() => {\n if (!config.trackActivity) return undefined;\n\n const onActivity = () => reporterRef.current?.reportActivity();\n const events = ['mousemove', 'keydown', 'scroll', 'touchstart', 'click'];\n events.forEach((event) => window.addEventListener(event, onActivity, { passive: true }));\n\n return () => events.forEach((event) => window.removeEventListener(event, onActivity));\n }, [config.trackActivity]);\n\n // Visibility + focus sensors -> reporter.setVisible(foreground)\n useEffect(() => {\n if (!config.trackVisibility) return undefined;\n\n const pushForeground = () => {\n reporterRef.current?.setVisible(sensorRef.current.tab && sensorRef.current.focus);\n };\n const onVisibility = () => {\n const visible = document.visibilityState !== 'hidden';\n sensorRef.current.tab = visible;\n setTabVisible(visible);\n pushForeground();\n };\n const onFocus = () => {\n sensorRef.current.focus = true;\n setWindowFocused(true);\n pushForeground();\n };\n const onBlur = () => {\n sensorRef.current.focus = false;\n setWindowFocused(false);\n pushForeground();\n };\n\n document.addEventListener('visibilitychange', onVisibility);\n window.addEventListener('focus', onFocus);\n window.addEventListener('blur', onBlur);\n\n return () => {\n document.removeEventListener('visibilitychange', onVisibility);\n window.removeEventListener('focus', onFocus);\n window.removeEventListener('blur', onBlur);\n };\n }, [config.trackVisibility]);\n\n const sendHeartbeat = useCallback((overrides) => {\n reporterRef.current?.sendHeartbeat(overrides);\n }, []);\n\n return { connected, sessionId, tabVisible, windowFocused, subjectActive, sendHeartbeat };\n}\n\nexport default usePresence;\n","import React, { createContext, useContext, useMemo } from 'react';\nimport { usePresence } from '../hooks/usePresence';\nimport { getCableConfig } from '../cable';\n\nconst PresenceContext = createContext(null);\n\nconst DEFAULT_CONFIG = {\n channelName: 'WhereIsWaldo::PresenceChannel',\n heartbeatInterval: 30000,\n activityTimeout: 30000,\n trackActivity: true,\n trackVisibility: true,\n debug: false,\n};\n\n/**\n * Get merged config from defaults, cable config, and overrides\n */\nfunction getMergedConfig(configOverrides) {\n const cableConfig = getCableConfig();\n const presenceConfig = cableConfig.presence || {};\n return { ...DEFAULT_CONFIG, ...presenceConfig, ...configOverrides };\n}\n\n/**\n * PresenceProvider - Provides real-time presence tracking and message handling\n *\n * @param {Object} props\n * @param {React.ReactNode} props.children\n * @param {Object} props.metadata - Optional metadata to attach to presence\n * @param {Object} props.config - Optional configuration overrides\n * @param {Function} props.onConnected - Callback when connected\n * @param {Function} props.onDisconnected - Callback when disconnected\n */\nexport function PresenceProvider({\n children,\n metadata = {},\n config: configOverrides = {},\n onConnected,\n onDisconnected,\n}) {\n const config = useMemo(() => getMergedConfig(configOverrides), [configOverrides]);\n\n // Use the shared usePresence hook for all presence tracking\n const presence = usePresence({\n ...config,\n metadata,\n onConnected,\n onDisconnected,\n });\n\n return (\n <PresenceContext.Provider value={presence}>\n {children}\n </PresenceContext.Provider>\n );\n}\n\n/**\n * Hook to access presence context\n * @returns {Object} Presence context value\n */\nexport function usePresenceContext() {\n const context = useContext(PresenceContext);\n if (!context) {\n throw new Error('usePresenceContext must be used within a PresenceProvider');\n }\n return context;\n}\n\nexport default PresenceContext;\n","import { useEffect, useRef } from 'react';\nimport { subscribeToEvent } from '../cable';\n\n/**\n * useWaldoEvent - subscribe a component to one or more raw real-time event types.\n *\n * The component subscribes to a message `type` (or an array of types) and\n * receives the payload on every matching cable event; it decides for itself\n * whether it cares about a given record (e.g. `if (data.project_id === myId)\n * ...`). The subscription is automatically torn down when the component\n * unmounts (\"out of view\"), exactly like a useEffect cleanup.\n *\n * The handler is held in a ref so it always sees the latest props/state without\n * re-subscribing on every render — callers do NOT need to memoize it. The\n * subscription is re-created only when the type(s) / `enabled` change.\n *\n * This is a pure subscription primitive — it has no timing/debounce concern.\n * To coalesce a burst of events, compose with a debounce at the call site, e.g.\n * corebyscott's `useDebouncedCallback`:\n *\n * const refetch = useDebouncedCallback(loadAll, 250);\n * useWaldoEvent(['issue_update', 'project_update'], refetch);\n *\n * @param {string|string[]} type - Cable message type(s), e.g. 'issue_update'\n * @param {Function} handler - Called with (data, message) on each event\n * @param {Object} [options]\n * @param {boolean} [options.enabled=true] - When false, no subscription is made\n *\n * @example\n * useWaldoEvent('issue_update', (data) => {\n * if (data.id === issueId) setIssue((prev) => ({ ...prev, ...data }));\n * });\n */\nexport function useWaldoEvent(type, handler, options = {}) {\n const { enabled = true } = options;\n const handlerRef = useRef(handler);\n\n // Keep the ref pointing at the latest handler every render.\n useEffect(() => {\n handlerRef.current = handler;\n });\n\n // Stable, order-insensitive key so an inline array literal doesn't\n // re-subscribe on every render.\n const types = Array.isArray(type) ? type : [type];\n const typesKey = types.filter(Boolean).join(' ');\n\n useEffect(() => {\n if (!enabled || !typesKey) return undefined;\n const typeList = typesKey.split(' ');\n\n const onEvent = (data, message) => {\n if (handlerRef.current) handlerRef.current(data, message);\n };\n\n const unsubscribes = typeList.map((t) => subscribeToEvent(t, onEvent));\n return () => {\n unsubscribes.forEach((unsub) => unsub());\n };\n }, [typesKey, enabled]);\n}\n\nexport default useWaldoEvent;\n","// Framework-agnostic roster store: pure JS, no DOM and no React Native APIs, so\n// it is safe to import from the web package AND the mobile (React Native)\n// package. It owns the \"materialize full state from a snapshot + transition\n// deltas\" logic and the derivations over it (per-device roll-up, sorting,\n// labels, colors) — everything except the sensors and the pixels, which are\n// necessarily platform-specific.\n\n// Cable message types exchanged on the roster stream.\nexport const ROSTER_SNAPSHOT = 'roster_snapshot';\nexport const ROSTER_DELTA = 'roster_delta';\n// Content-free \"re-poll now\" trigger (:nudge mode). Carries no data.\nexport const ROSTER_NUDGE = 'roster_nudge';\n\n// Activity ranking, most-present first. Shared by sorting and by the \"highest\n// across devices\" roll-up so web and mobile agree.\nexport const STATUS_RANK = { active: 3, idle: 2, background: 1, offline: 0 };\n\n// Status -> presence dot color. Platform UIs may theme these, but sharing a\n// default keeps web and mobile visually consistent out of the box.\nexport const STATUS_COLORS = {\n active: '#22c55e', // visible + working\n idle: '#f59e0b', // visible but not actively using\n background: '#9ca3af', // only backgrounded/hidden sessions\n offline: '#d1d5db', // no live sessions\n};\n\nexport function presenceColor(status) {\n return STATUS_COLORS[status] || STATUS_COLORS.offline;\n}\n\n/**\n * Apply an incoming cable message to a members Map (keyed by subject id),\n * returning a NEW Map (immutable update) so callers can trigger re-renders by\n * identity. Unknown message types return the same Map unchanged.\n *\n * - snapshot: replaces the whole roster.\n * - delta: upserts one member. Offline members are KEPT (status \"offline\") so\n * the roster stays the full team with live dots rather than vanishing rows.\n *\n * @param {Map<any, Object>} map - current members map\n * @param {Object} message - { type, members? , member? }\n * @returns {Map<any, Object>}\n */\nexport function applyRosterMessage(map, message) {\n if (!message || !message.type) return map;\n\n if (message.type === ROSTER_SNAPSHOT) {\n const next = new Map();\n (message.members || []).forEach((m) => next.set(m.id, m));\n return next;\n }\n\n if (message.type === ROSTER_DELTA && message.member) {\n const next = new Map(map);\n if (message.member._removed) {\n // Member left the viewer's visible scope (a visibility change) — drop it.\n // Distinct from going offline, which arrives as status \"offline\".\n next.delete(message.member.id);\n } else {\n next.set(message.member.id, message.member);\n }\n return next;\n }\n\n return map;\n}\n\n/**\n * Modes that require the client to poll the server for deltas (vs. passively\n * receiving pushed broadcasts). The server declares the mode in the snapshot.\n */\nexport function isPollMode(mode) {\n return mode === 'poll' || mode === 'nudge';\n}\n\n/**\n * Members Map -> array, sorted most-present first then by display label.\n */\nexport function sortedMembers(map) {\n const list = Array.from(map.values());\n list.sort((a, b) => {\n const rank = (STATUS_RANK[b.status] ?? 0) - (STATUS_RANK[a.status] ?? 0);\n if (rank !== 0) return rank;\n return String(memberLabel(a)).localeCompare(String(memberLabel(b)));\n });\n return list;\n}\n\n/**\n * Only members with a live session (status !== \"offline\").\n */\nexport function onlineMembers(members) {\n return members.filter((m) => m.status && m.status !== 'offline');\n}\n\n/**\n * That member's status on a specific device/platform, e.g.\n * deviceStatus(member, 'mobile') // => \"idle\"\n * Answers \"is the user active on mobile?\" vs the overall member.status.\n */\nexport function deviceStatus(member, platform) {\n return (member && member.devices && member.devices[platform]) || 'offline';\n}\n\n/**\n * Best-effort display label from a member's subject data.\n */\nexport function memberLabel(member) {\n if (!member) return '';\n return (\n member.name ||\n [member.first_name, member.last_name].filter(Boolean).join(' ') ||\n member.email ||\n member.username ||\n `#${member.id}`\n );\n}\n","import { useState, useEffect, useRef, useCallback } from 'react';\nimport { getConsumer } from '../cable';\nimport {\n applyRosterMessage,\n sortedMembers,\n onlineMembers,\n isPollMode,\n ROSTER_SNAPSHOT,\n ROSTER_NUDGE,\n} from '../core/rosterStore';\n\nconst DEFAULT_CHANNEL = 'WhereIsWaldo::RosterChannel';\nconst DEFAULT_POLL_INTERVAL = 15; // seconds; overridden by the server's snapshot\nconst DEFAULT_NUDGE_JITTER = 0.5; // seconds; overridden by the server's snapshot\n\n/**\n * usePresenceRoster - live \"who's around\" roster as data (bring your own UI).\n *\n * The server picks the delivery mode per account and declares it in the initial\n * snapshot. This hook adapts transparently — no client mode config:\n * - broadcast/fanout : passively receive pushed deltas.\n * - pull/nudge : poll the server (`perform('poll')`) on the interval the\n * server specifies; the server replies with a filtered\n * snapshot (resync) or diff.\n * Either way the shared reducer (../core/rosterStore) keeps `members` as the\n * full, live set (snapshot seeded, deltas patched, removals dropped).\n *\n * Each member: { id, status, devices: { web, mobile, ... }, ...subjectData }.\n *\n * @param {Object} [options]\n * @param {string} [options.channelName] - override the roster channel name\n * @param {boolean} [options.enabled=true] - when false, no subscription is made\n * @param {Function} [options.filter] - (member) => boolean. Client-side filter\n * applied to the returned members. COSMETIC ONLY — intended for `:broadcast`\n * mode, where the full account roster reaches the client and you want to hide\n * some rows in the UI. It is NOT an access-control boundary (the unfiltered\n * data is already on the client). For enforced visibility use a server-side\n * mode (`:poll`/`:nudge`/`:fanout`).\n * @returns {{ members: Array, online: Array, onlineCount: number,\n * byId: Object, connected: boolean, mode: string|null }}\n */\nexport function usePresenceRoster(options = {}) {\n const { channelName = DEFAULT_CHANNEL, enabled = true, filter } = options;\n\n const [members, setMembers] = useState([]);\n const [connected, setConnected] = useState(false);\n const [mode, setMode] = useState(null);\n\n const mapRef = useRef(new Map());\n const pollTimerRef = useRef(null);\n const nudgeTimerRef = useRef(null);\n const nudgeJitterRef = useRef(DEFAULT_NUDGE_JITTER);\n\n const flush = useCallback(() => {\n setMembers(sortedMembers(mapRef.current));\n }, []);\n\n useEffect(() => {\n if (!enabled) return undefined;\n\n const consumer = getConsumer();\n\n const startPolling = (subscription, intervalSeconds) => {\n if (pollTimerRef.current) return; // already polling\n const ms = (intervalSeconds || DEFAULT_POLL_INTERVAL) * 1000;\n // Small random offset so clients don't poll in lockstep (herd control).\n const jitter = () => 0.85 + Math.random() * 0.3;\n const tick = () => {\n subscription.perform('poll');\n pollTimerRef.current = setTimeout(tick, ms * jitter());\n };\n pollTimerRef.current = setTimeout(tick, ms * jitter());\n };\n\n // :nudge — on a content-free trigger, re-poll off-cycle after a random\n // jitter (herd control), debounced to one pending re-poll at a time.\n const scheduleNudgePoll = (subscription) => {\n if (nudgeTimerRef.current) return;\n const delay = Math.random() * nudgeJitterRef.current * 1000;\n nudgeTimerRef.current = setTimeout(() => {\n nudgeTimerRef.current = null;\n subscription.perform('poll');\n }, delay);\n };\n\n const subscription = consumer.subscriptions.create(\n { channel: channelName },\n {\n connected() {\n setConnected(true);\n },\n disconnected() {\n setConnected(false);\n },\n received(message) {\n if (message && message.type === ROSTER_SNAPSHOT) {\n setMode(message.mode || null);\n if (message.nudge_jitter != null) nudgeJitterRef.current = message.nudge_jitter;\n if (isPollMode(message.mode)) startPolling(subscription, message.poll_interval);\n }\n if (message && message.type === ROSTER_NUDGE) {\n scheduleNudgePoll(subscription);\n return; // content-free trigger; nothing to reduce\n }\n const next = applyRosterMessage(mapRef.current, message);\n if (next !== mapRef.current) {\n mapRef.current = next;\n flush();\n }\n },\n }\n );\n\n return () => {\n if (pollTimerRef.current) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n if (nudgeTimerRef.current) {\n clearTimeout(nudgeTimerRef.current);\n nudgeTimerRef.current = null;\n }\n subscription.unsubscribe();\n };\n }, [channelName, enabled, flush]);\n\n // Client-side (cosmetic) filter — see options.filter.\n const visibleMembers = filter ? members.filter(filter) : members;\n const online = onlineMembers(visibleMembers);\n const byId = {};\n visibleMembers.forEach((m) => {\n byId[m.id] = m;\n });\n\n return { members: visibleMembers, online, onlineCount: online.length, byId, connected, mode };\n}\n\nexport default usePresenceRoster;\n"],"names":["consumer","config","url","getToken","listeners","Map","subscribeToEvent","type","callback","set","get","Set","add","unsubscribe","current","size","configureCable","options","arguments","length","undefined","_objectSpread","disconnect","getConsumer","token","separator","includes","concat","encodeURIComponent","createConsumer","disconnectCable","getCableConfig","handleMessage","message","data","Array","from","forEach","cb","err","console","error","DEFAULTS","channelName","heartbeatInterval","activityTimeout","metadata","debug","createPresenceReporter","now","Date","resolveConsumer","resolveCableConfig","dispatch","log","_console","_len","args","_key","apply","subscription","heartbeatTimer","activityTimer","lastActivityAt","state","connected","visible","active","sessionId","emitChange","onChange","buildPayload","_overrides$tab_visibl","_overrides$subject_ac","overrides","payload","tab_visible","subject_active","last_activity_at","sendHeartbeat","perform","scheduleInactivity","clearTimeout","setTimeout","reportActivity","setVisible","startHeartbeatLoop","clearInterval","setInterval","start","subscriptions","create","channel","cable","onConnected","disconnected","onDisconnected","received","_target_session","stop","getState","DEFAULT_OPTIONS","trackActivity","trackVisibility","initialVisible","document","visibilityState","initialFocused","hasFocus","usePresence","_useState","useState","_useState2","_slicedToArray","setConnected","_useState3","_useState4","setSessionId","_useState5","_useState6","subjectActive","setSubjectActive","_useState7","_useState8","tabVisible","setTabVisible","_useState9","_useState0","windowFocused","setWindowFocused","reporterRef","useRef","sensorRef","tab","focus","useEffect","reporter","onActivity","_reporterRef$current","events","event","window","addEventListener","passive","removeEventListener","pushForeground","_reporterRef$current2","onVisibility","onFocus","onBlur","useCallback","_reporterRef$current3","PresenceContext","createContext","DEFAULT_CONFIG","getMergedConfig","configOverrides","cableConfig","presenceConfig","presence","PresenceProvider","_ref","children","_ref$metadata","_ref$config","useMemo","React","createElement","Provider","value","usePresenceContext","context","useContext","Error","useWaldoEvent","handler","_options$enabled","enabled","handlerRef","types","isArray","typesKey","filter","Boolean","join","typeList","split","onEvent","unsubscribes","map","t","unsub","ROSTER_SNAPSHOT","ROSTER_DELTA","ROSTER_NUDGE","STATUS_RANK","idle","background","offline","STATUS_COLORS","presenceColor","status","applyRosterMessage","next","members","m","id","member","_removed","isPollMode","mode","sortedMembers","list","values","sort","a","b","_STATUS_RANK$b$status","_STATUS_RANK$a$status","rank","String","memberLabel","localeCompare","onlineMembers","deviceStatus","platform","devices","name","first_name","last_name","email","username","DEFAULT_CHANNEL","DEFAULT_POLL_INTERVAL","DEFAULT_NUDGE_JITTER","usePresenceRoster","_options$channelName","setMembers","setMode","mapRef","pollTimerRef","nudgeTimerRef","nudgeJitterRef","flush","startPolling","intervalSeconds","ms","jitter","Math","random","tick","scheduleNudgePoll","delay","nudge_jitter","poll_interval","visibleMembers","online","byId","onlineCount"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,IAAIA,QAAQ,GAAG,IAAI;AACnB,IAAIC,MAAM,GAAG;AACXC,EAAAA,GAAG,EAAE,QAAQ;AACbC,EAAAA,QAAQ,EAAE;AACZ,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA,IAAMC,SAAS,GAAG,IAAIC,GAAG,EAAE;;AAE3B;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,gBAAgBA,CAACC,IAAI,EAAEC,QAAQ,EAAE;EAC/C,IAAI,CAACD,IAAI,IAAI,OAAOC,QAAQ,KAAK,UAAU,EAAE,OAAO,YAAM,CAAC,CAAC;AAE5D,EAAA,IAAIC,GAAG,GAAGL,SAAS,CAACM,GAAG,CAACH,IAAI,CAAC;EAC7B,IAAI,CAACE,GAAG,EAAE;AACRA,IAAAA,GAAG,GAAG,IAAIE,GAAG,EAAE;AACfP,IAAAA,SAAS,CAACK,GAAG,CAACF,IAAI,EAAEE,GAAG,CAAC;AAC1B,EAAA;AACAA,EAAAA,GAAG,CAACG,GAAG,CAACJ,QAAQ,CAAC;EAEjB,OAAO,SAASK,WAAWA,GAAG;AAC5B,IAAA,IAAMC,OAAO,GAAGV,SAAS,CAACM,GAAG,CAACH,IAAI,CAAC;IACnC,IAAI,CAACO,OAAO,EAAE;IACdA,OAAO,CAAA,QAAA,CAAO,CAACN,QAAQ,CAAC;IACxB,IAAIM,OAAO,CAACC,IAAI,KAAK,CAAC,EAAEX,SAAS,CAAA,QAAA,CAAO,CAACG,IAAI,CAAC;EAChD,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACO,SAASS,cAAcA,GAAe;AAAA,EAAA,IAAdC,OAAO,GAAAC,SAAA,CAAAC,MAAA,GAAA,CAAA,IAAAD,SAAA,CAAA,CAAA,CAAA,KAAAE,SAAA,GAAAF,SAAA,CAAA,CAAA,CAAA,GAAG,EAAE;EACzCjB,MAAM,GAAAoB,cAAA,CAAAA,cAAA,KAAQpB,MAAM,CAAA,EAAKgB,OAAO,CAAE;AAClC;AACA,EAAA,IAAIjB,QAAQ,EAAE;IACZA,QAAQ,CAACsB,UAAU,EAAE;AACrBtB,IAAAA,QAAQ,GAAG,IAAI;AACjB,EAAA;AACF;;AAEA;AACA;AACA;AACA;AACO,SAASuB,WAAWA,GAAG;EAC5B,IAAI,CAACvB,QAAQ,EAAE;AACb,IAAA,IAAIE,GAAG,GAAGD,MAAM,CAACC,GAAG;IAEpB,IAAID,MAAM,CAACE,QAAQ,EAAE;AACnB,MAAA,IAAMqB,KAAK,GAAGvB,MAAM,CAACE,QAAQ,EAAE;AAC/B,MAAA,IAAIqB,KAAK,EAAE;QACT,IAAMC,SAAS,GAAGvB,GAAG,CAACwB,QAAQ,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG;AAC/CxB,QAAAA,GAAG,GAAA,EAAA,CAAAyB,MAAA,CAAMzB,GAAG,EAAAyB,MAAA,CAAGF,SAAS,EAAA,QAAA,CAAA,CAAAE,MAAA,CAASC,kBAAkB,CAACJ,KAAK,CAAC,CAAE;AAC9D,MAAA;AACF,IAAA;AAEAxB,IAAAA,QAAQ,GAAG6B,0BAAc,CAAC3B,GAAG,CAAC;AAChC,EAAA;AAEA,EAAA,OAAOF,QAAQ;AACjB;;AAEA;AACA;AACA;AACO,SAAS8B,eAAeA,GAAG;AAChC,EAAA,IAAI9B,QAAQ,EAAE;IACZA,QAAQ,CAACsB,UAAU,EAAE;AACrBtB,IAAAA,QAAQ,GAAG,IAAI;AACjB,EAAA;AACF;;AAEA;AACA;AACA;AACA;AACO,SAAS+B,cAAcA,GAAG;EAC/B,OAAAV,cAAA,KAAYpB,MAAM,CAAA;AACpB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS+B,aAAaA,CAACC,OAAO,EAAE;AACrC,EAAA,IAAQ1B,IAAI,GAAW0B,OAAO,CAAtB1B,IAAI;IAAE2B,IAAI,GAAKD,OAAO,CAAhBC,IAAI;AAElB,EAAA,IAAMzB,GAAG,GAAGL,SAAS,CAACM,GAAG,CAACH,IAAI,CAAC;EAC/B,IAAI,CAACE,GAAG,IAAIA,GAAG,CAACM,IAAI,KAAK,CAAC,EAAE,OAAO,KAAK;;AAExC;EACAoB,KAAK,CAACC,IAAI,CAAC3B,GAAG,CAAC,CAAC4B,OAAO,CAAC,UAACC,EAAE,EAAK;IAC9B,IAAI;AACFA,MAAAA,EAAE,CAACJ,IAAI,EAAED,OAAO,CAAC;IACnB,CAAC,CAAC,OAAOM,GAAG,EAAE;AACZ;AACA;MACAC,OAAO,CAACC,KAAK,CAAA,kCAAA,CAAAd,MAAA,CAAmCpB,IAAI,EAAA,WAAA,CAAA,EAAYgC,GAAG,CAAC;AACtE,IAAA;AACF,EAAA,CAAC,CAAC;AAEF,EAAA,OAAO,IAAI;AACb;;AClGA,IAAMG,QAAQ,GAAG;AACfC,EAAAA,WAAW,EAAE,+BAA+B;AAC5CC,EAAAA,iBAAiB,EAAE,KAAK;AAAE;AAC1BC,EAAAA,eAAe,EAAE,KAAK;AAAE;EACxBC,QAAQ,EAAE,EAAE;AACZC,EAAAA,KAAK,EAAE;AACT,CAAC;AAEM,SAASC,sBAAsBA,GAAe;AAAA,EAAA,IAAd/B,OAAO,GAAAC,SAAA,CAAAC,MAAA,GAAA,CAAA,IAAAD,SAAA,CAAA,CAAA,CAAA,KAAAE,SAAA,GAAAF,SAAA,CAAA,CAAA,CAAA,GAAG,EAAE;EACjD,IAAMjB,MAAM,GAAAoB,cAAA,CAAAA,cAAA,CAAA,EAAA,EAAQqB,QAAQ,CAAA,EAAKzB,OAAO,CAAE;AAC1C,EAAA,IAAMgC,GAAG,GAAGhD,MAAM,CAACgD,GAAG,IAAK,YAAA;AAAA,IAAA,OAAMC,IAAI,CAACD,GAAG,EAAE;EAAA,CAAC;AAC5C,EAAA,IAAME,eAAe,GAAGlD,MAAM,CAACsB,WAAW,IAAIA,WAAW;AACzD,EAAA,IAAM6B,kBAAkB,GAAGnD,MAAM,CAAC8B,cAAc,IAAIA,cAAc;AAClE,EAAA,IAAMsB,QAAQ,GAAGpD,MAAM,CAAC+B,aAAa,IAAIA,aAAa;AACtD,EAAA,IAAMsB,GAAG,GAAG,SAANA,GAAGA,GAAgB;AAAA,IAAA,IAAAC,QAAA;AAAA,IAAA,KAAA,IAAAC,IAAA,GAAAtC,SAAA,CAAAC,MAAA,EAATsC,IAAI,GAAA,IAAAtB,KAAA,CAAAqB,IAAA,GAAAE,IAAA,GAAA,CAAA,EAAAA,IAAA,GAAAF,IAAA,EAAAE,IAAA,EAAA,EAAA;AAAJD,MAAAA,IAAI,CAAAC,IAAA,CAAA,GAAAxC,SAAA,CAAAwC,IAAA,CAAA;AAAA,IAAA;AAClB;IACA,IAAIzD,MAAM,CAAC8C,KAAK,EAAE,CAAAQ,QAAA,GAAAf,OAAO,EAACc,GAAG,CAAAK,KAAA,CAAAJ,QAAA,EAAA,CAAC,oBAAoB,EAAA5B,MAAA,CAAK8B,IAAI,CAAA,CAAC;EAC9D,CAAC;EAED,IAAIG,YAAY,GAAG,IAAI;EACvB,IAAIC,cAAc,GAAG,IAAI;EACzB,IAAIC,aAAa,GAAG,IAAI;EACxB,IAAIC,cAAc,GAAG,CAAC;;AAEtB;AACA,EAAA,IAAMC,KAAK,GAAG;AAAEC,IAAAA,SAAS,EAAE,KAAK;AAAEC,IAAAA,OAAO,EAAE,IAAI;AAAEC,IAAAA,MAAM,EAAE,IAAI;AAAEC,IAAAA,SAAS,EAAE;GAAM;AAEhF,EAAA,IAAMC,UAAU,GAAG,SAAbA,UAAUA,GAAS;AACvB,IAAA,IAAIpE,MAAM,CAACqE,QAAQ,EAAErE,MAAM,CAACqE,QAAQ,CAAAjD,cAAA,CAAA,EAAA,EAAM2C,KAAK,CAAE,CAAC;EACpD,CAAC;AAED,EAAA,IAAMO,YAAY,GAAG,SAAfA,YAAYA,GAAuB;IAAA,IAAAC,qBAAA,EAAAC,qBAAA;AAAA,IAAA,IAAnBC,SAAS,GAAAxD,SAAA,CAAAC,MAAA,GAAA,CAAA,IAAAD,SAAA,CAAA,CAAA,CAAA,KAAAE,SAAA,GAAAF,SAAA,CAAA,CAAA,CAAA,GAAG,EAAE;AAClC,IAAA,IAAMyD,OAAO,GAAG;AACdC,MAAAA,WAAW,EAAA,CAAAJ,qBAAA,GAAEE,SAAS,CAACE,WAAW,MAAA,IAAA,IAAAJ,qBAAA,KAAA,MAAA,GAAAA,qBAAA,GAAIR,KAAK,CAACE,OAAO;AACnDW,MAAAA,cAAc,EAAA,CAAAJ,qBAAA,GAAEC,SAAS,CAACG,cAAc,MAAA,IAAA,IAAAJ,qBAAA,KAAA,MAAA,GAAAA,qBAAA,GAAIT,KAAK,CAACG,MAAM;AACxDrB,MAAAA,QAAQ,EAAE7C,MAAM,CAAC6C,QAAQ,IAAI;KAC9B;AACD,IAAA,IAAIiB,cAAc,EAAEY,OAAO,CAACG,gBAAgB,GAAGf,cAAc;AAC7D,IAAA,OAAOY,OAAO;EAChB,CAAC;AAED,EAAA,IAAMI,aAAa,GAAG,SAAhBA,aAAaA,GAAuB;AAAA,IAAA,IAAnBL,SAAS,GAAAxD,SAAA,CAAAC,MAAA,GAAA,CAAA,IAAAD,SAAA,CAAA,CAAA,CAAA,KAAAE,SAAA,GAAAF,SAAA,CAAA,CAAA,CAAA,GAAG,EAAE;IACnC,IAAI,CAAC0C,YAAY,EAAE;AACnBN,IAAAA,GAAG,CAAC,WAAW,EAAEoB,SAAS,CAAC;IAC3Bd,YAAY,CAACoB,OAAO,CAAC,WAAW,EAAET,YAAY,CAACG,SAAS,CAAC,CAAC;EAC5D,CAAC;;AAED;AACA;AACA,EAAA,IAAMO,kBAAkB,GAAG,SAArBA,kBAAkBA,GAAS;AAC/B,IAAA,IAAInB,aAAa,EAAEoB,YAAY,CAACpB,aAAa,CAAC;IAC9CA,aAAa,GAAGqB,UAAU,CAAC,YAAM;AAC/B,MAAA,IAAI,CAACnB,KAAK,CAACG,MAAM,EAAE;MACnBH,KAAK,CAACG,MAAM,GAAG,KAAK;AACpBE,MAAAA,UAAU,EAAE;AACZU,MAAAA,aAAa,CAAC;AAAEF,QAAAA,cAAc,EAAE;AAAM,OAAC,CAAC;AAC1C,IAAA,CAAC,EAAE5E,MAAM,CAAC4C,eAAe,CAAC;EAC5B,CAAC;AAED,EAAA,IAAMuC,cAAc,GAAG,SAAjBA,cAAcA,GAAS;IAC3BrB,cAAc,GAAGd,GAAG,EAAE;AACtB,IAAA,IAAI,CAACe,KAAK,CAACG,MAAM,EAAE;MACjBH,KAAK,CAACG,MAAM,GAAG,IAAI;AACnBE,MAAAA,UAAU,EAAE;AACZU,MAAAA,aAAa,CAAC;AAAEF,QAAAA,cAAc,EAAE;OAAM,CAAC,CAAC;AAC1C,IAAA;AACAI,IAAAA,kBAAkB,EAAE;EACtB,CAAC;AAED,EAAA,IAAMI,UAAU,GAAG,SAAbA,UAAUA,CAAInB,OAAO,EAAK;AAC9B,IAAA,IAAIA,OAAO,KAAKF,KAAK,CAACE,OAAO,EAAE;IAC/BF,KAAK,CAACE,OAAO,GAAGA,OAAO;AACvBG,IAAAA,UAAU,EAAE;IACZU,aAAa,EAAE,CAAC;AAChB,IAAA,IAAIb,OAAO,EAAEkB,cAAc,EAAE;EAC/B,CAAC;AAED,EAAA,IAAME,kBAAkB,GAAG,SAArBA,kBAAkBA,GAAS;AAC/B,IAAA,IAAIzB,cAAc,EAAE0B,aAAa,CAAC1B,cAAc,CAAC;IACjDA,cAAc,GAAG2B,WAAW,CAAC,YAAA;MAAA,OAAMT,aAAa,EAAE;IAAA,CAAA,EAAE9E,MAAM,CAAC2C,iBAAiB,CAAC;EAC/E,CAAC;AAED,EAAA,IAAM6C,KAAK,GAAG,SAARA,KAAKA,GAAS;IAClB,IAAI7B,YAAY,EAAE,OAAOA,YAAY;IAErCA,YAAY,GAAGT,eAAe,EAAE,CAACuC,aAAa,CAACC,MAAM,CACnD;MAAEC,OAAO,EAAE3F,MAAM,CAAC0C,WAAW;AAAEG,MAAAA,QAAQ,EAAE7C,MAAM,CAAC6C,QAAQ,IAAI;AAAG,KAAC,EAChE;MACEmB,SAAS,EAAA,SAATA,SAASA,GAAG;QACVD,KAAK,CAACC,SAAS,GAAG,IAAI;AACtB,QAAA,IAAM4B,KAAK,GAAGzC,kBAAkB,EAAE;AAClC,QAAA,IAAIyC,KAAK,IAAIA,KAAK,CAACzB,SAAS,EAAEJ,KAAK,CAACI,SAAS,GAAGyB,KAAK,CAACzB,SAAS;AAC/DC,QAAAA,UAAU,EAAE;QACZ,IAAIpE,MAAM,CAAC6F,WAAW,EAAE7F,MAAM,CAAC6F,WAAW,EAAE;AAC5CR,QAAAA,kBAAkB,EAAE;MACtB,CAAC;MACDS,YAAY,EAAA,SAAZA,YAAYA,GAAG;QACb/B,KAAK,CAACC,SAAS,GAAG,KAAK;AACvBI,QAAAA,UAAU,EAAE;QACZ,IAAIpE,MAAM,CAAC+F,cAAc,EAAE/F,MAAM,CAAC+F,cAAc,EAAE;MACpD,CAAC;AACDC,MAAAA,QAAQ,EAAA,SAARA,QAAQA,CAAChE,OAAO,EAAE;AAChB;AACA,QAAA,IAAIA,OAAO,IAAIA,OAAO,CAACiE,eAAe,IAAIjE,OAAO,CAACiE,eAAe,KAAKlC,KAAK,CAACI,SAAS,EAAE;AACrF,UAAA;AACF,QAAA;QACAf,QAAQ,CAACpB,OAAO,CAAC;AACnB,MAAA;AACF,KACF,CAAC;AAEDgD,IAAAA,kBAAkB,EAAE;AACpB,IAAA,OAAOrB,YAAY;EACrB,CAAC;AAED,EAAA,IAAMuC,IAAI,GAAG,SAAPA,IAAIA,GAAS;AACjB,IAAA,IAAItC,cAAc,EAAE;MAClB0B,aAAa,CAAC1B,cAAc,CAAC;AAC7BA,MAAAA,cAAc,GAAG,IAAI;AACvB,IAAA;AACA,IAAA,IAAIC,aAAa,EAAE;MACjBoB,YAAY,CAACpB,aAAa,CAAC;AAC3BA,MAAAA,aAAa,GAAG,IAAI;AACtB,IAAA;AACA,IAAA,IAAIF,YAAY,EAAE;MAChBA,YAAY,CAAC/C,WAAW,EAAE;AAC1B+C,MAAAA,YAAY,GAAG,IAAI;AACrB,IAAA;IACAI,KAAK,CAACC,SAAS,GAAG,KAAK;EACzB,CAAC;EAED,OAAO;AACLwB,IAAAA,KAAK,EAALA,KAAK;AACLU,IAAAA,IAAI,EAAJA,IAAI;AACJd,IAAAA,UAAU,EAAVA,UAAU;AACVD,IAAAA,cAAc,EAAdA,cAAc;AACdL,IAAAA,aAAa,EAAbA,aAAa;IACbqB,QAAQ,EAAE,SAAVA,QAAQA,GAAA;MAAA,OAAA/E,cAAA,KAAc2C,KAAK,CAAA;AAAA,IAAA;GAC5B;AACH;;AC7JA,IAAMqC,eAAe,GAAG;AACtB1D,EAAAA,WAAW,EAAE,+BAA+B;AAC5CC,EAAAA,iBAAiB,EAAE,KAAK;AACxBC,EAAAA,eAAe,EAAE,KAAK;AACtByD,EAAAA,aAAa,EAAE,IAAI;AACnBC,EAAAA,eAAe,EAAE,IAAI;AACrBxD,EAAAA,KAAK,EAAE;AACT,CAAC;AAED,IAAMyD,cAAc,GAAG,SAAjBA,cAAcA,GAAA;EAAA,OAClB,OAAOC,QAAQ,KAAK,WAAW,GAAG,IAAI,GAAGA,QAAQ,CAACC,eAAe,KAAK,QAAQ;AAAA,CAAA;AAChF,IAAMC,cAAc,GAAG,SAAjBA,cAAcA,GAAA;EAAA,OAClB,OAAOF,QAAQ,KAAK,WAAW,GAAG,IAAI,GAAGA,QAAQ,CAACG,QAAQ,EAAE;AAAA,CAAA;;AAE9D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,WAAWA,GAAe;AAAA,EAAA,IAAd5F,OAAO,GAAAC,SAAA,CAAAC,MAAA,GAAA,CAAA,IAAAD,SAAA,CAAA,CAAA,CAAA,KAAAE,SAAA,GAAAF,SAAA,CAAA,CAAA,CAAA,GAAG,EAAE;EACtC,IAAMjB,MAAM,GAAAoB,cAAA,CAAAA,cAAA,CAAA,EAAA,EAAQgF,eAAe,CAAA,EAAKpF,OAAO,CAAE;AAEjD,EAAA,IAAA6F,SAAA,GAAkCC,cAAQ,CAAC,KAAK,CAAC;IAAAC,UAAA,GAAAC,cAAA,CAAAH,SAAA,EAAA,CAAA,CAAA;AAA1C7C,IAAAA,SAAS,GAAA+C,UAAA,CAAA,CAAA,CAAA;AAAEE,IAAAA,YAAY,GAAAF,UAAA,CAAA,CAAA,CAAA;AAC9B,EAAA,IAAAG,UAAA,GAAkCJ,cAAQ,CAAC,IAAI,CAAC;IAAAK,UAAA,GAAAH,cAAA,CAAAE,UAAA,EAAA,CAAA,CAAA;AAAzC/C,IAAAA,SAAS,GAAAgD,UAAA,CAAA,CAAA,CAAA;AAAEC,IAAAA,YAAY,GAAAD,UAAA,CAAA,CAAA,CAAA;AAC9B,EAAA,IAAAE,UAAA,GAA0CP,cAAQ,CAAC,IAAI,CAAC;IAAAQ,UAAA,GAAAN,cAAA,CAAAK,UAAA,EAAA,CAAA,CAAA;AAAjDE,IAAAA,aAAa,GAAAD,UAAA,CAAA,CAAA,CAAA;AAAEE,IAAAA,gBAAgB,GAAAF,UAAA,CAAA,CAAA,CAAA;AACtC,EAAA,IAAAG,UAAA,GAAoCX,cAAQ,CAACP,cAAc,CAAC;IAAAmB,UAAA,GAAAV,cAAA,CAAAS,UAAA,EAAA,CAAA,CAAA;AAArDE,IAAAA,UAAU,GAAAD,UAAA,CAAA,CAAA,CAAA;AAAEE,IAAAA,aAAa,GAAAF,UAAA,CAAA,CAAA,CAAA;AAChC,EAAA,IAAAG,UAAA,GAA0Cf,cAAQ,CAACJ,cAAc,CAAC;IAAAoB,UAAA,GAAAd,cAAA,CAAAa,UAAA,EAAA,CAAA,CAAA;AAA3DE,IAAAA,aAAa,GAAAD,UAAA,CAAA,CAAA,CAAA;AAAEE,IAAAA,gBAAgB,GAAAF,UAAA,CAAA,CAAA,CAAA;AAEtC,EAAA,IAAMG,WAAW,GAAGC,YAAM,CAAC,IAAI,CAAC;AAChC;EACA,IAAMC,SAAS,GAAGD,YAAM,CAAC;IAAEE,GAAG,EAAE7B,cAAc,EAAE;IAAE8B,KAAK,EAAE3B,cAAc;AAAG,GAAC,CAAC;;AAE5E;AACA4B,EAAAA,eAAS,CAAC,YAAM;IACd,IAAMC,QAAQ,GAAGxF,sBAAsB,CAAC;MACtCL,WAAW,EAAE1C,MAAM,CAAC0C,WAAW;MAC/BC,iBAAiB,EAAE3C,MAAM,CAAC2C,iBAAiB;MAC3CC,eAAe,EAAE5C,MAAM,CAAC4C,eAAe;MACvCC,QAAQ,EAAE7C,MAAM,CAAC6C,QAAQ;MACzBC,KAAK,EAAE9C,MAAM,CAAC8C,KAAK;MACnB+C,WAAW,EAAE7F,MAAM,CAAC6F,WAAW;MAC/BE,cAAc,EAAE/F,MAAM,CAAC+F,cAAc;AACrC1B,MAAAA,QAAQ,EAAE,SAAVA,QAAQA,CAAGN,KAAK,EAAK;AACnBkD,QAAAA,YAAY,CAAClD,KAAK,CAACC,SAAS,CAAC;AAC7BwD,QAAAA,gBAAgB,CAACzD,KAAK,CAACG,MAAM,CAAC;QAC9B,IAAIH,KAAK,CAACI,SAAS,EAAEiD,YAAY,CAACrD,KAAK,CAACI,SAAS,CAAC;AACpD,MAAA;AACF,KAAC,CAAC;IACF8D,WAAW,CAACpH,OAAO,GAAG0H,QAAQ;IAC9BA,QAAQ,CAAC/C,KAAK,EAAE;AAChB;AACA+C,IAAAA,QAAQ,CAACnD,UAAU,CAAC+C,SAAS,CAACtH,OAAO,CAACuH,GAAG,IAAID,SAAS,CAACtH,OAAO,CAACwH,KAAK,CAAC;AAErE,IAAA,OAAO,YAAM;MACXE,QAAQ,CAACrC,IAAI,EAAE;MACf+B,WAAW,CAACpH,OAAO,GAAG,IAAI;IAC5B,CAAC;EACH,CAAC,EAAE,CAACb,MAAM,CAAC0C,WAAW,CAAC,CAAC,CAAC;;AAEzB;AACA4F,EAAAA,eAAS,CAAC,YAAM;AACd,IAAA,IAAI,CAACtI,MAAM,CAACqG,aAAa,EAAE,OAAOlF,SAAS;AAE3C,IAAA,IAAMqH,UAAU,GAAG,SAAbA,UAAUA,GAAA;AAAA,MAAA,IAAAC,oBAAA;AAAA,MAAA,OAAA,CAAAA,oBAAA,GAASR,WAAW,CAACpH,OAAO,MAAA,IAAA,IAAA4H,oBAAA,KAAA,MAAA,GAAA,MAAA,GAAnBA,oBAAA,CAAqBtD,cAAc,EAAE;AAAA,IAAA,CAAA;AAC9D,IAAA,IAAMuD,MAAM,GAAG,CAAC,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC;AACxEA,IAAAA,MAAM,CAACtG,OAAO,CAAC,UAACuG,KAAK,EAAA;AAAA,MAAA,OAAKC,MAAM,CAACC,gBAAgB,CAACF,KAAK,EAAEH,UAAU,EAAE;AAAEM,QAAAA,OAAO,EAAE;AAAK,OAAC,CAAC;IAAA,CAAA,CAAC;IAExF,OAAO,YAAA;AAAA,MAAA,OAAMJ,MAAM,CAACtG,OAAO,CAAC,UAACuG,KAAK,EAAA;AAAA,QAAA,OAAKC,MAAM,CAACG,mBAAmB,CAACJ,KAAK,EAAEH,UAAU,CAAC;MAAA,CAAA,CAAC;AAAA,IAAA,CAAA;AACvF,EAAA,CAAC,EAAE,CAACxI,MAAM,CAACqG,aAAa,CAAC,CAAC;;AAE1B;AACAiC,EAAAA,eAAS,CAAC,YAAM;AACd,IAAA,IAAI,CAACtI,MAAM,CAACsG,eAAe,EAAE,OAAOnF,SAAS;AAE7C,IAAA,IAAM6H,cAAc,GAAG,SAAjBA,cAAcA,GAAS;AAAA,MAAA,IAAAC,qBAAA;MAC3B,CAAAA,qBAAA,GAAAhB,WAAW,CAACpH,OAAO,MAAA,IAAA,IAAAoI,qBAAA,KAAA,MAAA,IAAnBA,qBAAA,CAAqB7D,UAAU,CAAC+C,SAAS,CAACtH,OAAO,CAACuH,GAAG,IAAID,SAAS,CAACtH,OAAO,CAACwH,KAAK,CAAC;IACnF,CAAC;AACD,IAAA,IAAMa,YAAY,GAAG,SAAfA,YAAYA,GAAS;AACzB,MAAA,IAAMjF,OAAO,GAAGuC,QAAQ,CAACC,eAAe,KAAK,QAAQ;AACrD0B,MAAAA,SAAS,CAACtH,OAAO,CAACuH,GAAG,GAAGnE,OAAO;MAC/B2D,aAAa,CAAC3D,OAAO,CAAC;AACtB+E,MAAAA,cAAc,EAAE;IAClB,CAAC;AACD,IAAA,IAAMG,OAAO,GAAG,SAAVA,OAAOA,GAAS;AACpBhB,MAAAA,SAAS,CAACtH,OAAO,CAACwH,KAAK,GAAG,IAAI;MAC9BL,gBAAgB,CAAC,IAAI,CAAC;AACtBgB,MAAAA,cAAc,EAAE;IAClB,CAAC;AACD,IAAA,IAAMI,MAAM,GAAG,SAATA,MAAMA,GAAS;AACnBjB,MAAAA,SAAS,CAACtH,OAAO,CAACwH,KAAK,GAAG,KAAK;MAC/BL,gBAAgB,CAAC,KAAK,CAAC;AACvBgB,MAAAA,cAAc,EAAE;IAClB,CAAC;AAEDxC,IAAAA,QAAQ,CAACqC,gBAAgB,CAAC,kBAAkB,EAAEK,YAAY,CAAC;AAC3DN,IAAAA,MAAM,CAACC,gBAAgB,CAAC,OAAO,EAAEM,OAAO,CAAC;AACzCP,IAAAA,MAAM,CAACC,gBAAgB,CAAC,MAAM,EAAEO,MAAM,CAAC;AAEvC,IAAA,OAAO,YAAM;AACX5C,MAAAA,QAAQ,CAACuC,mBAAmB,CAAC,kBAAkB,EAAEG,YAAY,CAAC;AAC9DN,MAAAA,MAAM,CAACG,mBAAmB,CAAC,OAAO,EAAEI,OAAO,CAAC;AAC5CP,MAAAA,MAAM,CAACG,mBAAmB,CAAC,MAAM,EAAEK,MAAM,CAAC;IAC5C,CAAC;AACH,EAAA,CAAC,EAAE,CAACpJ,MAAM,CAACsG,eAAe,CAAC,CAAC;AAE5B,EAAA,IAAMxB,aAAa,GAAGuE,iBAAW,CAAC,UAAC5E,SAAS,EAAK;AAAA,IAAA,IAAA6E,qBAAA;AAC/C,IAAA,CAAAA,qBAAA,GAAArB,WAAW,CAACpH,OAAO,MAAA,IAAA,IAAAyI,qBAAA,KAAA,MAAA,IAAnBA,qBAAA,CAAqBxE,aAAa,CAACL,SAAS,CAAC;EAC/C,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO;AAAET,IAAAA,SAAS,EAATA,SAAS;AAAEG,IAAAA,SAAS,EAATA,SAAS;AAAEwD,IAAAA,UAAU,EAAVA,UAAU;AAAEI,IAAAA,aAAa,EAAbA,aAAa;AAAER,IAAAA,aAAa,EAAbA,aAAa;AAAEzC,IAAAA,aAAa,EAAbA;GAAe;AAC1F;;ACnHA,IAAMyE,eAAe,gBAAGC,mBAAa,CAAC,IAAI;AAE1C,IAAMC,cAAc,GAAG;AACrB/G,EAAAA,WAAW,EAAE,+BAA+B;AAC5CC,EAAAA,iBAAiB,EAAE,KAAK;AACxBC,EAAAA,eAAe,EAAE,KAAK;AACtByD,EAAAA,aAAa,EAAE,IAAI;AACnBC,EAAAA,eAAe,EAAE,IAAI;AACrBxD,EAAAA,KAAK,EAAE;AACT,CAAC;;AAED;AACA;AACA;AACA,SAAS4G,eAAeA,CAACC,eAAe,EAAE;AACxC,EAAA,IAAMC,WAAW,GAAG9H,cAAc,EAAE;AACpC,EAAA,IAAM+H,cAAc,GAAGD,WAAW,CAACE,QAAQ,IAAI,EAAE;EACjD,OAAA1I,cAAA,CAAAA,cAAA,CAAAA,cAAA,KAAYqI,cAAc,CAAA,EAAKI,cAAc,CAAA,EAAKF,eAAe,CAAA;AACnE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASI,gBAAgBA,CAAAC,IAAA,EAM7B;AAAA,EAAA,IALDC,QAAQ,GAAAD,IAAA,CAARC,QAAQ;IAAAC,aAAA,GAAAF,IAAA,CACRnH,QAAQ;AAARA,IAAAA,QAAQ,GAAAqH,aAAA,KAAA,MAAA,GAAG,EAAE,GAAAA,aAAA;IAAAC,WAAA,GAAAH,IAAA,CACbhK,MAAM;AAAE2J,IAAAA,eAAe,GAAAQ,WAAA,KAAA,MAAA,GAAG,EAAE,GAAAA,WAAA;IAC5BtE,WAAW,GAAAmE,IAAA,CAAXnE,WAAW;IACXE,cAAc,GAAAiE,IAAA,CAAdjE,cAAc;EAEd,IAAM/F,MAAM,GAAGoK,aAAO,CAAC,YAAA;IAAA,OAAMV,eAAe,CAACC,eAAe,CAAC;EAAA,CAAA,EAAE,CAACA,eAAe,CAAC,CAAC;;AAEjF;EACA,IAAMG,QAAQ,GAAGlD,WAAW,CAAAxF,cAAA,CAAAA,cAAA,KACvBpB,MAAM,CAAA,EAAA,EAAA,EAAA;AACT6C,IAAAA,QAAQ,EAARA,QAAQ;AACRgD,IAAAA,WAAW,EAAXA,WAAW;AACXE,IAAAA,cAAc,EAAdA;AAAc,GAAA,CACf,CAAC;AAEF,EAAA,oBACEsE,KAAA,CAAAC,aAAA,CAACf,eAAe,CAACgB,QAAQ,EAAA;AAACC,IAAAA,KAAK,EAAEV;AAAS,GAAA,EACvCG,QACuB,CAAC;AAE/B;;AAEA;AACA;AACA;AACA;AACO,SAASQ,kBAAkBA,GAAG;AACnC,EAAA,IAAMC,OAAO,GAAGC,gBAAU,CAACpB,eAAe,CAAC;EAC3C,IAAI,CAACmB,OAAO,EAAE;AACZ,IAAA,MAAM,IAAIE,KAAK,CAAC,2DAA2D,CAAC;AAC9E,EAAA;AACA,EAAA,OAAOF,OAAO;AAChB;;ACjEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASG,aAAaA,CAACvK,IAAI,EAAEwK,OAAO,EAAgB;AAAA,EAAA,IAAd9J,OAAO,GAAAC,SAAA,CAAAC,MAAA,GAAA,CAAA,IAAAD,SAAA,CAAA,CAAA,CAAA,KAAAE,SAAA,GAAAF,SAAA,CAAA,CAAA,CAAA,GAAG,EAAE;AACvD,EAAA,IAAA8J,gBAAA,GAA2B/J,OAAO,CAA1BgK,OAAO;AAAPA,IAAAA,OAAO,GAAAD,gBAAA,KAAA,MAAA,GAAG,IAAI,GAAAA,gBAAA;AACtB,EAAA,IAAME,UAAU,GAAG/C,YAAM,CAAC4C,OAAO,CAAC;;AAElC;AACAxC,EAAAA,eAAS,CAAC,YAAM;IACd2C,UAAU,CAACpK,OAAO,GAAGiK,OAAO;AAC9B,EAAA,CAAC,CAAC;;AAEF;AACA;AACA,EAAA,IAAMI,KAAK,GAAGhJ,KAAK,CAACiJ,OAAO,CAAC7K,IAAI,CAAC,GAAGA,IAAI,GAAG,CAACA,IAAI,CAAC;AACjD,EAAA,IAAM8K,QAAQ,GAAGF,KAAK,CAACG,MAAM,CAACC,OAAO,CAAC,CAACC,IAAI,CAAC,GAAG,CAAC;AAEhDjD,EAAAA,eAAS,CAAC,YAAM;AACd,IAAA,IAAI,CAAC0C,OAAO,IAAI,CAACI,QAAQ,EAAE,OAAOjK,SAAS;AAC3C,IAAA,IAAMqK,QAAQ,GAAGJ,QAAQ,CAACK,KAAK,CAAC,GAAG,CAAC;IAEpC,IAAMC,OAAO,GAAG,SAAVA,OAAOA,CAAIzJ,IAAI,EAAED,OAAO,EAAK;MACjC,IAAIiJ,UAAU,CAACpK,OAAO,EAAEoK,UAAU,CAACpK,OAAO,CAACoB,IAAI,EAAED,OAAO,CAAC;IAC3D,CAAC;AAED,IAAA,IAAM2J,YAAY,GAAGH,QAAQ,CAACI,GAAG,CAAC,UAACC,CAAC,EAAA;AAAA,MAAA,OAAKxL,gBAAgB,CAACwL,CAAC,EAAEH,OAAO,CAAC;IAAA,CAAA,CAAC;AACtE,IAAA,OAAO,YAAM;AACXC,MAAAA,YAAY,CAACvJ,OAAO,CAAC,UAAC0J,KAAK,EAAA;QAAA,OAAKA,KAAK,EAAE;MAAA,CAAA,CAAC;IAC1C,CAAC;AACH,EAAA,CAAC,EAAE,CAACV,QAAQ,EAAEJ,OAAO,CAAC,CAAC;AACzB;;AC5DA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACO,IAAMe,eAAe,GAAG;AACxB,IAAMC,YAAY,GAAG;AAC5B;AACO,IAAMC,YAAY,GAAG,cAAc;;AAE1C;AACA;AACO,IAAMC,WAAW,GAAG;AAAEhI,EAAAA,MAAM,EAAE,CAAC;AAAEiI,EAAAA,IAAI,EAAE,CAAC;AAAEC,EAAAA,UAAU,EAAE,CAAC;AAAEC,EAAAA,OAAO,EAAE;AAAE;;AAE3E;AACA;AACO,IAAMC,aAAa,GAAG;AAC3BpI,EAAAA,MAAM,EAAE,SAAS;AAAE;AACnBiI,EAAAA,IAAI,EAAE,SAAS;AAAE;AACjBC,EAAAA,UAAU,EAAE,SAAS;AAAE;EACvBC,OAAO,EAAE,SAAS;AACpB;AAEO,SAASE,aAAaA,CAACC,MAAM,EAAE;AACpC,EAAA,OAAOF,aAAa,CAACE,MAAM,CAAC,IAAIF,aAAa,CAACD,OAAO;AACvD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASI,kBAAkBA,CAACb,GAAG,EAAE5J,OAAO,EAAE;EAC/C,IAAI,CAACA,OAAO,IAAI,CAACA,OAAO,CAAC1B,IAAI,EAAE,OAAOsL,GAAG;AAEzC,EAAA,IAAI5J,OAAO,CAAC1B,IAAI,KAAKyL,eAAe,EAAE;AACpC,IAAA,IAAMW,IAAI,GAAG,IAAItM,GAAG,EAAE;IACtB,CAAC4B,OAAO,CAAC2K,OAAO,IAAI,EAAE,EAAEvK,OAAO,CAAC,UAACwK,CAAC,EAAA;MAAA,OAAKF,IAAI,CAAClM,GAAG,CAACoM,CAAC,CAACC,EAAE,EAAED,CAAC,CAAC;IAAA,CAAA,CAAC;AACzD,IAAA,OAAOF,IAAI;AACb,EAAA;EAEA,IAAI1K,OAAO,CAAC1B,IAAI,KAAK0L,YAAY,IAAIhK,OAAO,CAAC8K,MAAM,EAAE;AACnD,IAAA,IAAMJ,KAAI,GAAG,IAAItM,GAAG,CAACwL,GAAG,CAAC;AACzB,IAAA,IAAI5J,OAAO,CAAC8K,MAAM,CAACC,QAAQ,EAAE;AAC3B;AACA;AACAL,MAAAA,KAAI,UAAO,CAAC1K,OAAO,CAAC8K,MAAM,CAACD,EAAE,CAAC;AAChC,IAAA,CAAC,MAAM;AACLH,MAAAA,KAAI,CAAClM,GAAG,CAACwB,OAAO,CAAC8K,MAAM,CAACD,EAAE,EAAE7K,OAAO,CAAC8K,MAAM,CAAC;AAC7C,IAAA;AACA,IAAA,OAAOJ,KAAI;AACb,EAAA;AAEA,EAAA,OAAOd,GAAG;AACZ;;AAEA;AACA;AACA;AACA;AACO,SAASoB,UAAUA,CAACC,IAAI,EAAE;AAC/B,EAAA,OAAOA,IAAI,KAAK,MAAM,IAAIA,IAAI,KAAK,OAAO;AAC5C;;AAEA;AACA;AACA;AACO,SAASC,aAAaA,CAACtB,GAAG,EAAE;EACjC,IAAMuB,IAAI,GAAGjL,KAAK,CAACC,IAAI,CAACyJ,GAAG,CAACwB,MAAM,EAAE,CAAC;AACrCD,EAAAA,IAAI,CAACE,IAAI,CAAC,UAACC,CAAC,EAAEC,CAAC,EAAK;IAAA,IAAAC,qBAAA,EAAAC,qBAAA;AAClB,IAAA,IAAMC,IAAI,GAAG,CAAA,CAAAF,qBAAA,GAACtB,WAAW,CAACqB,CAAC,CAACf,MAAM,CAAC,MAAA,IAAA,IAAAgB,qBAAA,cAAAA,qBAAA,GAAI,CAAC,KAAA,CAAAC,qBAAA,GAAKvB,WAAW,CAACoB,CAAC,CAACd,MAAM,CAAC,MAAA,IAAA,IAAAiB,qBAAA,KAAA,MAAA,GAAAA,qBAAA,GAAI,CAAC,CAAC;AACxE,IAAA,IAAIC,IAAI,KAAK,CAAC,EAAE,OAAOA,IAAI;AAC3B,IAAA,OAAOC,MAAM,CAACC,WAAW,CAACN,CAAC,CAAC,CAAC,CAACO,aAAa,CAACF,MAAM,CAACC,WAAW,CAACL,CAAC,CAAC,CAAC,CAAC;AACrE,EAAA,CAAC,CAAC;AACF,EAAA,OAAOJ,IAAI;AACb;;AAEA;AACA;AACA;AACO,SAASW,aAAaA,CAACnB,OAAO,EAAE;AACrC,EAAA,OAAOA,OAAO,CAACtB,MAAM,CAAC,UAACuB,CAAC,EAAA;IAAA,OAAKA,CAAC,CAACJ,MAAM,IAAII,CAAC,CAACJ,MAAM,KAAK,SAAS;EAAA,CAAA,CAAC;AAClE;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASuB,YAAYA,CAACjB,MAAM,EAAEkB,QAAQ,EAAE;AAC7C,EAAA,OAAQlB,MAAM,IAAIA,MAAM,CAACmB,OAAO,IAAInB,MAAM,CAACmB,OAAO,CAACD,QAAQ,CAAC,IAAK,SAAS;AAC5E;;AAEA;AACA;AACA;AACO,SAASJ,WAAWA,CAACd,MAAM,EAAE;AAClC,EAAA,IAAI,CAACA,MAAM,EAAE,OAAO,EAAE;AACtB,EAAA,OACEA,MAAM,CAACoB,IAAI,IACX,CAACpB,MAAM,CAACqB,UAAU,EAAErB,MAAM,CAACsB,SAAS,CAAC,CAAC/C,MAAM,CAACC,OAAO,CAAC,CAACC,IAAI,CAAC,GAAG,CAAC,IAC/DuB,MAAM,CAACuB,KAAK,IACZvB,MAAM,CAACwB,QAAQ,IAAA,GAAA,CAAA5M,MAAA,CACXoL,MAAM,CAACD,EAAE,CAAE;AAEnB;;ACzGA,IAAM0B,eAAe,GAAG,6BAA6B;AACrD,IAAMC,qBAAqB,GAAG,EAAE,CAAC;AACjC,IAAMC,oBAAoB,GAAG,GAAG,CAAC;;AAEjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,iBAAiBA,GAAe;AAAA,EAAA,IAAd1N,OAAO,GAAAC,SAAA,CAAAC,MAAA,GAAA,CAAA,IAAAD,SAAA,CAAA,CAAA,CAAA,KAAAE,SAAA,GAAAF,SAAA,CAAA,CAAA,CAAA,GAAG,EAAE;AAC5C,EAAA,IAAA0N,oBAAA,GAAkE3N,OAAO,CAAjE0B,WAAW;AAAXA,IAAAA,WAAW,GAAAiM,oBAAA,KAAA,MAAA,GAAGJ,eAAe,GAAAI,oBAAA;IAAA5D,gBAAA,GAA6B/J,OAAO,CAAlCgK,OAAO;AAAPA,IAAAA,OAAO,GAAAD,gBAAA,KAAA,MAAA,GAAG,IAAI,GAAAA,gBAAA;IAAEM,MAAM,GAAKrK,OAAO,CAAlBqK,MAAM;AAE7D,EAAA,IAAAxE,SAAA,GAA8BC,cAAQ,CAAC,EAAE,CAAC;IAAAC,UAAA,GAAAC,cAAA,CAAAH,SAAA,EAAA,CAAA,CAAA;AAAnC8F,IAAAA,OAAO,GAAA5F,UAAA,CAAA,CAAA,CAAA;AAAE6H,IAAAA,UAAU,GAAA7H,UAAA,CAAA,CAAA,CAAA;AAC1B,EAAA,IAAAG,UAAA,GAAkCJ,cAAQ,CAAC,KAAK,CAAC;IAAAK,UAAA,GAAAH,cAAA,CAAAE,UAAA,EAAA,CAAA,CAAA;AAA1ClD,IAAAA,SAAS,GAAAmD,UAAA,CAAA,CAAA,CAAA;AAAEF,IAAAA,YAAY,GAAAE,UAAA,CAAA,CAAA,CAAA;AAC9B,EAAA,IAAAE,UAAA,GAAwBP,cAAQ,CAAC,IAAI,CAAC;IAAAQ,UAAA,GAAAN,cAAA,CAAAK,UAAA,EAAA,CAAA,CAAA;AAA/B4F,IAAAA,IAAI,GAAA3F,UAAA,CAAA,CAAA,CAAA;AAAEuH,IAAAA,OAAO,GAAAvH,UAAA,CAAA,CAAA,CAAA;EAEpB,IAAMwH,MAAM,GAAG5G,YAAM,CAAC,IAAI9H,GAAG,EAAE,CAAC;AAChC,EAAA,IAAM2O,YAAY,GAAG7G,YAAM,CAAC,IAAI,CAAC;AACjC,EAAA,IAAM8G,aAAa,GAAG9G,YAAM,CAAC,IAAI,CAAC;AAClC,EAAA,IAAM+G,cAAc,GAAG/G,YAAM,CAACuG,oBAAoB,CAAC;AAEnD,EAAA,IAAMS,KAAK,GAAG7F,iBAAW,CAAC,YAAM;AAC9BuF,IAAAA,UAAU,CAAC1B,aAAa,CAAC4B,MAAM,CAACjO,OAAO,CAAC,CAAC;EAC3C,CAAC,EAAE,EAAE,CAAC;AAENyH,EAAAA,eAAS,CAAC,YAAM;AACd,IAAA,IAAI,CAAC0C,OAAO,EAAE,OAAO7J,SAAS;AAE9B,IAAA,IAAMpB,QAAQ,GAAGuB,WAAW,EAAE;IAE9B,IAAM6N,YAAY,GAAG,SAAfA,YAAYA,CAAIxL,YAAY,EAAEyL,eAAe,EAAK;AACtD,MAAA,IAAIL,YAAY,CAAClO,OAAO,EAAE,OAAO;AACjC,MAAA,IAAMwO,EAAE,GAAG,CAACD,eAAe,IAAIZ,qBAAqB,IAAI,IAAI;AAC5D;AACA,MAAA,IAAMc,MAAM,GAAG,SAATA,MAAMA,GAAA;QAAA,OAAS,IAAI,GAAGC,IAAI,CAACC,MAAM,EAAE,GAAG,GAAG;AAAA,MAAA,CAAA;AAC/C,MAAA,IAAMC,KAAI,GAAG,SAAPA,IAAIA,GAAS;AACjB9L,QAAAA,YAAY,CAACoB,OAAO,CAAC,MAAM,CAAC;AAC5BgK,QAAAA,YAAY,CAAClO,OAAO,GAAGqE,UAAU,CAACuK,KAAI,EAAEJ,EAAE,GAAGC,MAAM,EAAE,CAAC;MACxD,CAAC;AACDP,MAAAA,YAAY,CAAClO,OAAO,GAAGqE,UAAU,CAACuK,KAAI,EAAEJ,EAAE,GAAGC,MAAM,EAAE,CAAC;IACxD,CAAC;;AAED;AACA;AACA,IAAA,IAAMI,iBAAiB,GAAG,SAApBA,iBAAiBA,CAAI/L,YAAY,EAAK;MAC1C,IAAIqL,aAAa,CAACnO,OAAO,EAAE;AAC3B,MAAA,IAAM8O,KAAK,GAAGJ,IAAI,CAACC,MAAM,EAAE,GAAGP,cAAc,CAACpO,OAAO,GAAG,IAAI;AAC3DmO,MAAAA,aAAa,CAACnO,OAAO,GAAGqE,UAAU,CAAC,YAAM;QACvC8J,aAAa,CAACnO,OAAO,GAAG,IAAI;AAC5B8C,QAAAA,YAAY,CAACoB,OAAO,CAAC,MAAM,CAAC;MAC9B,CAAC,EAAE4K,KAAK,CAAC;IACX,CAAC;AAED,IAAA,IAAMhM,YAAY,GAAG5D,QAAQ,CAAC0F,aAAa,CAACC,MAAM,CAChD;AAAEC,MAAAA,OAAO,EAAEjD;AAAY,KAAC,EACxB;MACEsB,SAAS,EAAA,SAATA,SAASA,GAAG;QACViD,YAAY,CAAC,IAAI,CAAC;MACpB,CAAC;MACDnB,YAAY,EAAA,SAAZA,YAAYA,GAAG;QACbmB,YAAY,CAAC,KAAK,CAAC;MACrB,CAAC;AACDjB,MAAAA,QAAQ,EAAA,SAARA,QAAQA,CAAChE,OAAO,EAAE;AAChB,QAAA,IAAIA,OAAO,IAAIA,OAAO,CAAC1B,IAAI,KAAKyL,eAAe,EAAE;AAC/C8C,UAAAA,OAAO,CAAC7M,OAAO,CAACiL,IAAI,IAAI,IAAI,CAAC;AAC7B,UAAA,IAAIjL,OAAO,CAAC4N,YAAY,IAAI,IAAI,EAAEX,cAAc,CAACpO,OAAO,GAAGmB,OAAO,CAAC4N,YAAY;AAC/E,UAAA,IAAI5C,UAAU,CAAChL,OAAO,CAACiL,IAAI,CAAC,EAAEkC,YAAY,CAACxL,YAAY,EAAE3B,OAAO,CAAC6N,aAAa,CAAC;AACjF,QAAA;AACA,QAAA,IAAI7N,OAAO,IAAIA,OAAO,CAAC1B,IAAI,KAAK2L,YAAY,EAAE;UAC5CyD,iBAAiB,CAAC/L,YAAY,CAAC;AAC/B,UAAA,OAAO;AACT,QAAA;QACA,IAAM+I,IAAI,GAAGD,kBAAkB,CAACqC,MAAM,CAACjO,OAAO,EAAEmB,OAAO,CAAC;AACxD,QAAA,IAAI0K,IAAI,KAAKoC,MAAM,CAACjO,OAAO,EAAE;UAC3BiO,MAAM,CAACjO,OAAO,GAAG6L,IAAI;AACrBwC,UAAAA,KAAK,EAAE;AACT,QAAA;AACF,MAAA;AACF,KACF,CAAC;AAED,IAAA,OAAO,YAAM;MACX,IAAIH,YAAY,CAAClO,OAAO,EAAE;AACxBoE,QAAAA,YAAY,CAAC8J,YAAY,CAAClO,OAAO,CAAC;QAClCkO,YAAY,CAAClO,OAAO,GAAG,IAAI;AAC7B,MAAA;MACA,IAAImO,aAAa,CAACnO,OAAO,EAAE;AACzBoE,QAAAA,YAAY,CAAC+J,aAAa,CAACnO,OAAO,CAAC;QACnCmO,aAAa,CAACnO,OAAO,GAAG,IAAI;AAC9B,MAAA;MACA8C,YAAY,CAAC/C,WAAW,EAAE;IAC5B,CAAC;EACH,CAAC,EAAE,CAAC8B,WAAW,EAAEsI,OAAO,EAAEkE,KAAK,CAAC,CAAC;;AAEjC;EACA,IAAMY,cAAc,GAAGzE,MAAM,GAAGsB,OAAO,CAACtB,MAAM,CAACA,MAAM,CAAC,GAAGsB,OAAO;AAChE,EAAA,IAAMoD,MAAM,GAAGjC,aAAa,CAACgC,cAAc,CAAC;EAC5C,IAAME,IAAI,GAAG,EAAE;AACfF,EAAAA,cAAc,CAAC1N,OAAO,CAAC,UAACwK,CAAC,EAAK;AAC5BoD,IAAAA,IAAI,CAACpD,CAAC,CAACC,EAAE,CAAC,GAAGD,CAAC;AAChB,EAAA,CAAC,CAAC;EAEF,OAAO;AAAED,IAAAA,OAAO,EAAEmD,cAAc;AAAEC,IAAAA,MAAM,EAANA,MAAM;IAAEE,WAAW,EAAEF,MAAM,CAAC7O,MAAM;AAAE8O,IAAAA,IAAI,EAAJA,IAAI;AAAEhM,IAAAA,SAAS,EAATA,SAAS;AAAEiJ,IAAAA,IAAI,EAAJA;GAAM;AAC/F;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@byscott-io/where-is-waldo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Real-time presence tracking for ActionCable with React hooks",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src",
|
|
10
|
+
"!**/*.test.js",
|
|
11
|
+
"!**/*.test.jsx"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rollup -c",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"lint": "eslint src --ext .js,.jsx",
|
|
18
|
+
"lint:fix": "eslint src --ext .js,.jsx --fix",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"cypress:open": "cypress open",
|
|
21
|
+
"cypress:run": "cypress run",
|
|
22
|
+
"server": "cd spec/dummy && RAILS_ENV=development bundle exec puma -p 3001",
|
|
23
|
+
"server:setup": "cd spec/dummy && RAILS_ENV=development bundle exec rake db:create db:migrate",
|
|
24
|
+
"test:e2e": "start-server-and-test server http://localhost:3001 cypress:run",
|
|
25
|
+
"test:e2e:open": "start-server-and-test server http://localhost:3001 cypress:open"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@rails/actioncable": "^7.0.0 || ^8.0.0",
|
|
29
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@babel/core": "^7.29.7",
|
|
33
|
+
"@babel/preset-env": "^7.29.7",
|
|
34
|
+
"@babel/preset-react": "^7.29.7",
|
|
35
|
+
"@rollup/plugin-babel": "^7.1.0",
|
|
36
|
+
"@rollup/plugin-commonjs": "^29.0.3",
|
|
37
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
38
|
+
"@rails/actioncable": "^8.1.300",
|
|
39
|
+
"@testing-library/dom": "^10.4.1",
|
|
40
|
+
"@testing-library/react": "^16.3.2",
|
|
41
|
+
"cypress": "^15.18.1",
|
|
42
|
+
"eslint": "^8.50.0",
|
|
43
|
+
"eslint-plugin-react": "^7.33.0",
|
|
44
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
45
|
+
"jsdom": "^29.1.1",
|
|
46
|
+
"react": "^19.2.7",
|
|
47
|
+
"react-dom": "^19.2.7",
|
|
48
|
+
"rollup": "^4.62.2",
|
|
49
|
+
"start-server-and-test": "^3.0.11",
|
|
50
|
+
"vitest": "^4.1.10"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/byscott-io/where_is_waldo.git"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"actioncable",
|
|
58
|
+
"presence",
|
|
59
|
+
"websocket",
|
|
60
|
+
"react",
|
|
61
|
+
"rails",
|
|
62
|
+
"real-time"
|
|
63
|
+
],
|
|
64
|
+
"author": "Scott Gibson",
|
|
65
|
+
"license": "MIT",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/byscott-io/where_is_waldo/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/byscott-io/where_is_waldo#readme",
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"registry": "https://registry.npmjs.org",
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"resolutions": {
|
|
75
|
+
"qs": "^6.15.2"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createConsumer } from '@rails/actioncable';
|
|
2
|
+
|
|
3
|
+
let consumer = null;
|
|
4
|
+
let config = {
|
|
5
|
+
url: '/cable',
|
|
6
|
+
getToken: null,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Subscribe-by-event-type registry: message type -> Set of listener fns.
|
|
10
|
+
// Components subscribe to a raw event type (see subscribeToEvent /
|
|
11
|
+
// useWaldoEvent) and decide for themselves whether they care about a given
|
|
12
|
+
// payload. handleMessage dispatches every incoming message to the listeners
|
|
13
|
+
// registered for its type.
|
|
14
|
+
const listeners = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Subscribe to a raw cable event type. Returns an unsubscribe function.
|
|
18
|
+
* Multiple subscribers per type are supported; each receives (data, message).
|
|
19
|
+
* @param {string} type - The message type to listen for
|
|
20
|
+
* @param {Function} callback - Called with (data, message) on each matching event
|
|
21
|
+
* @returns {Function} unsubscribe
|
|
22
|
+
*/
|
|
23
|
+
export function subscribeToEvent(type, callback) {
|
|
24
|
+
if (!type || typeof callback !== 'function') return () => {};
|
|
25
|
+
|
|
26
|
+
let set = listeners.get(type);
|
|
27
|
+
if (!set) {
|
|
28
|
+
set = new Set();
|
|
29
|
+
listeners.set(type, set);
|
|
30
|
+
}
|
|
31
|
+
set.add(callback);
|
|
32
|
+
|
|
33
|
+
return function unsubscribe() {
|
|
34
|
+
const current = listeners.get(type);
|
|
35
|
+
if (!current) return;
|
|
36
|
+
current.delete(callback);
|
|
37
|
+
if (current.size === 0) listeners.delete(type);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Configure the ActionCable connection
|
|
43
|
+
* @param {Object} options
|
|
44
|
+
* @param {string} options.url - WebSocket URL (default: '/cable')
|
|
45
|
+
* @param {Function} options.getToken - Function that returns auth token
|
|
46
|
+
*/
|
|
47
|
+
export function configureCable(options = {}) {
|
|
48
|
+
config = { ...config, ...options };
|
|
49
|
+
// Reset consumer when config changes
|
|
50
|
+
if (consumer) {
|
|
51
|
+
consumer.disconnect();
|
|
52
|
+
consumer = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get or create the ActionCable consumer
|
|
58
|
+
* @returns {Consumer}
|
|
59
|
+
*/
|
|
60
|
+
export function getConsumer() {
|
|
61
|
+
if (!consumer) {
|
|
62
|
+
let url = config.url;
|
|
63
|
+
|
|
64
|
+
if (config.getToken) {
|
|
65
|
+
const token = config.getToken();
|
|
66
|
+
if (token) {
|
|
67
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
68
|
+
url = `${url}${separator}token=${encodeURIComponent(token)}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
consumer = createConsumer(url);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return consumer;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Disconnect and reset the consumer
|
|
80
|
+
*/
|
|
81
|
+
export function disconnectCable() {
|
|
82
|
+
if (consumer) {
|
|
83
|
+
consumer.disconnect();
|
|
84
|
+
consumer = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get current cable configuration
|
|
90
|
+
* @returns {Object}
|
|
91
|
+
*/
|
|
92
|
+
export function getCableConfig() {
|
|
93
|
+
return { ...config };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handle an incoming message by dispatching to every listener subscribed to
|
|
98
|
+
* its type.
|
|
99
|
+
* @param {Object} message - The message object with type and data
|
|
100
|
+
* @returns {boolean} - Whether at least one listener was invoked
|
|
101
|
+
*/
|
|
102
|
+
export function handleMessage(message) {
|
|
103
|
+
const { type, data } = message;
|
|
104
|
+
|
|
105
|
+
const set = listeners.get(type);
|
|
106
|
+
if (!set || set.size === 0) return false;
|
|
107
|
+
|
|
108
|
+
// Iterate a snapshot so a listener that unsubscribes during dispatch is safe.
|
|
109
|
+
Array.from(set).forEach((cb) => {
|
|
110
|
+
try {
|
|
111
|
+
cb(data, message);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// One bad listener must not block the others.
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.error(`[where-is-waldo] listener for "${type}" threw:`, err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { usePresence } from '../hooks/usePresence';
|
|
3
|
+
import { getCableConfig } from '../cable';
|
|
4
|
+
|
|
5
|
+
const PresenceContext = createContext(null);
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
channelName: 'WhereIsWaldo::PresenceChannel',
|
|
9
|
+
heartbeatInterval: 30000,
|
|
10
|
+
activityTimeout: 30000,
|
|
11
|
+
trackActivity: true,
|
|
12
|
+
trackVisibility: true,
|
|
13
|
+
debug: false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get merged config from defaults, cable config, and overrides
|
|
18
|
+
*/
|
|
19
|
+
function getMergedConfig(configOverrides) {
|
|
20
|
+
const cableConfig = getCableConfig();
|
|
21
|
+
const presenceConfig = cableConfig.presence || {};
|
|
22
|
+
return { ...DEFAULT_CONFIG, ...presenceConfig, ...configOverrides };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* PresenceProvider - Provides real-time presence tracking and message handling
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} props
|
|
29
|
+
* @param {React.ReactNode} props.children
|
|
30
|
+
* @param {Object} props.metadata - Optional metadata to attach to presence
|
|
31
|
+
* @param {Object} props.config - Optional configuration overrides
|
|
32
|
+
* @param {Function} props.onConnected - Callback when connected
|
|
33
|
+
* @param {Function} props.onDisconnected - Callback when disconnected
|
|
34
|
+
*/
|
|
35
|
+
export function PresenceProvider({
|
|
36
|
+
children,
|
|
37
|
+
metadata = {},
|
|
38
|
+
config: configOverrides = {},
|
|
39
|
+
onConnected,
|
|
40
|
+
onDisconnected,
|
|
41
|
+
}) {
|
|
42
|
+
const config = useMemo(() => getMergedConfig(configOverrides), [configOverrides]);
|
|
43
|
+
|
|
44
|
+
// Use the shared usePresence hook for all presence tracking
|
|
45
|
+
const presence = usePresence({
|
|
46
|
+
...config,
|
|
47
|
+
metadata,
|
|
48
|
+
onConnected,
|
|
49
|
+
onDisconnected,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<PresenceContext.Provider value={presence}>
|
|
54
|
+
{children}
|
|
55
|
+
</PresenceContext.Provider>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Hook to access presence context
|
|
61
|
+
* @returns {Object} Presence context value
|
|
62
|
+
*/
|
|
63
|
+
export function usePresenceContext() {
|
|
64
|
+
const context = useContext(PresenceContext);
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error('usePresenceContext must be used within a PresenceProvider');
|
|
67
|
+
}
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default PresenceContext;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Framework-agnostic presence reporter: the session-state -> heartbeat state
|
|
2
|
+
// machine, with NO DOM and NO React Native APIs. It owns the ActionCable
|
|
3
|
+
// subscription, the activity/visibility state, and the heartbeat cadence
|
|
4
|
+
// (interval + immediate beats on transitions). Platform layers feed it sensor
|
|
5
|
+
// inputs and it reports state back — so the web hook (DOM listeners) and a
|
|
6
|
+
// React Native reporter (AppState + touch) drive the exact same logic.
|
|
7
|
+
//
|
|
8
|
+
// const reporter = createPresenceReporter({ metadata: { platform: 'mobile' } });
|
|
9
|
+
// reporter.start();
|
|
10
|
+
// // sensors:
|
|
11
|
+
// reporter.setVisible(isForeground); // tab hidden / app backgrounded => false
|
|
12
|
+
// reporter.reportActivity(); // a user interaction happened
|
|
13
|
+
// // teardown:
|
|
14
|
+
// reporter.stop();
|
|
15
|
+
//
|
|
16
|
+
// The cable deps (consumer, cable config, message bus) are injectable so the
|
|
17
|
+
// core is unit-testable without a real socket; they default to the shared cable
|
|
18
|
+
// module, which works on React Native with the built-in WebSocket.
|
|
19
|
+
|
|
20
|
+
import { getConsumer, getCableConfig, handleMessage } from '../cable';
|
|
21
|
+
|
|
22
|
+
const DEFAULTS = {
|
|
23
|
+
channelName: 'WhereIsWaldo::PresenceChannel',
|
|
24
|
+
heartbeatInterval: 30000, // ms
|
|
25
|
+
activityTimeout: 30000, // ms of no activity -> subject_active=false
|
|
26
|
+
metadata: {},
|
|
27
|
+
debug: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createPresenceReporter(options = {}) {
|
|
31
|
+
const config = { ...DEFAULTS, ...options };
|
|
32
|
+
const now = config.now || (() => Date.now());
|
|
33
|
+
const resolveConsumer = config.getConsumer || getConsumer;
|
|
34
|
+
const resolveCableConfig = config.getCableConfig || getCableConfig;
|
|
35
|
+
const dispatch = config.handleMessage || handleMessage;
|
|
36
|
+
const log = (...args) => {
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
if (config.debug) console.log('[PresenceReporter]', ...args);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let subscription = null;
|
|
42
|
+
let heartbeatTimer = null;
|
|
43
|
+
let activityTimer = null;
|
|
44
|
+
let lastActivityAt = 0;
|
|
45
|
+
|
|
46
|
+
// visible = tab/app is foreground & visible; active = recent user activity.
|
|
47
|
+
const state = { connected: false, visible: true, active: true, sessionId: null };
|
|
48
|
+
|
|
49
|
+
const emitChange = () => {
|
|
50
|
+
if (config.onChange) config.onChange({ ...state });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const buildPayload = (overrides = {}) => {
|
|
54
|
+
const payload = {
|
|
55
|
+
tab_visible: overrides.tab_visible ?? state.visible,
|
|
56
|
+
subject_active: overrides.subject_active ?? state.active,
|
|
57
|
+
metadata: config.metadata || {},
|
|
58
|
+
};
|
|
59
|
+
if (lastActivityAt) payload.last_activity_at = lastActivityAt;
|
|
60
|
+
return payload;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sendHeartbeat = (overrides = {}) => {
|
|
64
|
+
if (!subscription) return;
|
|
65
|
+
log('heartbeat', overrides);
|
|
66
|
+
subscription.perform('heartbeat', buildPayload(overrides));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// (Re)arm the inactivity timer: after activityTimeout with no activity, flip
|
|
70
|
+
// to inactive and send an immediate heartbeat announcing it.
|
|
71
|
+
const scheduleInactivity = () => {
|
|
72
|
+
if (activityTimer) clearTimeout(activityTimer);
|
|
73
|
+
activityTimer = setTimeout(() => {
|
|
74
|
+
if (!state.active) return;
|
|
75
|
+
state.active = false;
|
|
76
|
+
emitChange();
|
|
77
|
+
sendHeartbeat({ subject_active: false });
|
|
78
|
+
}, config.activityTimeout);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const reportActivity = () => {
|
|
82
|
+
lastActivityAt = now();
|
|
83
|
+
if (!state.active) {
|
|
84
|
+
state.active = true;
|
|
85
|
+
emitChange();
|
|
86
|
+
sendHeartbeat({ subject_active: true }); // immediate: inactive -> active
|
|
87
|
+
}
|
|
88
|
+
scheduleInactivity();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const setVisible = (visible) => {
|
|
92
|
+
if (visible === state.visible) return;
|
|
93
|
+
state.visible = visible;
|
|
94
|
+
emitChange();
|
|
95
|
+
sendHeartbeat(); // immediate: visibility change
|
|
96
|
+
if (visible) reportActivity();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const startHeartbeatLoop = () => {
|
|
100
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
101
|
+
heartbeatTimer = setInterval(() => sendHeartbeat(), config.heartbeatInterval);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const start = () => {
|
|
105
|
+
if (subscription) return subscription;
|
|
106
|
+
|
|
107
|
+
subscription = resolveConsumer().subscriptions.create(
|
|
108
|
+
{ channel: config.channelName, metadata: config.metadata || {} },
|
|
109
|
+
{
|
|
110
|
+
connected() {
|
|
111
|
+
state.connected = true;
|
|
112
|
+
const cable = resolveCableConfig();
|
|
113
|
+
if (cable && cable.sessionId) state.sessionId = cable.sessionId;
|
|
114
|
+
emitChange();
|
|
115
|
+
if (config.onConnected) config.onConnected();
|
|
116
|
+
startHeartbeatLoop();
|
|
117
|
+
},
|
|
118
|
+
disconnected() {
|
|
119
|
+
state.connected = false;
|
|
120
|
+
emitChange();
|
|
121
|
+
if (config.onDisconnected) config.onDisconnected();
|
|
122
|
+
},
|
|
123
|
+
received(message) {
|
|
124
|
+
// Honor targeted (per-session) messages, then route to the event bus.
|
|
125
|
+
if (message && message._target_session && message._target_session !== state.sessionId) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
dispatch(message);
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
scheduleInactivity();
|
|
134
|
+
return subscription;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const stop = () => {
|
|
138
|
+
if (heartbeatTimer) {
|
|
139
|
+
clearInterval(heartbeatTimer);
|
|
140
|
+
heartbeatTimer = null;
|
|
141
|
+
}
|
|
142
|
+
if (activityTimer) {
|
|
143
|
+
clearTimeout(activityTimer);
|
|
144
|
+
activityTimer = null;
|
|
145
|
+
}
|
|
146
|
+
if (subscription) {
|
|
147
|
+
subscription.unsubscribe();
|
|
148
|
+
subscription = null;
|
|
149
|
+
}
|
|
150
|
+
state.connected = false;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
start,
|
|
155
|
+
stop,
|
|
156
|
+
setVisible,
|
|
157
|
+
reportActivity,
|
|
158
|
+
sendHeartbeat,
|
|
159
|
+
getState: () => ({ ...state }),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default createPresenceReporter;
|