cross-tab-worker-databus 0.2.0 → 0.3.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core/environment.ts", "../src/core/hash.ts", "../src/core/routing.ts", "../src/core/storage-batch.ts", "../src/core/cluster.ts", "../src/core/trace.ts", "../src/core/data-bus.ts", "../src/worker-mode.ts"],
4
+ "sourcesContent": ["/**\n * Browser environment adapters \u2014 storage, BroadcastChannel, timers, lifecycle.\n *\n * Separates platform-specific APIs from the core coordination logic so the\n * same Runtime can run in a browser, in a test, or in an embedded context\n * with custom adapters injected via `ClusterEnvironment`.\n */\nimport type { TabVisibilityState, WorkerClusterMessage } from './types';\n\n/** Minimal storage interface compatible with both localStorage and MemoryStorage. */\nexport interface StorageLike {\n readonly length: number;\n clear(): void;\n getItem(key: string): string | null;\n key(index: number): string | null;\n removeItem(key: string): void;\n setItem(key: string, value: string): void;\n}\n\n/** Minimal BroadcastChannel interface. The cluster uses it for control messages and event fan-out. */\nexport interface ClusterChannel {\n addEventListener(type: 'message', listener: (event: MessageEvent<WorkerClusterMessage>) => void): void;\n removeEventListener(type: 'message', listener: (event: MessageEvent<WorkerClusterMessage>) => void): void;\n postMessage(message: WorkerClusterMessage): void;\n close(): void;\n}\n\n/**\n * Environment abstraction that lets the cluster operate in Node, SSR, or\n * test environments without touching browser globals directly.\n * Tests inject a fake environment to control timing, storage, and lifecycle.\n */\nexport interface ClusterEnvironment {\n /** localStorage (or null if unavailable). Wrapped by BatchingStorageWriter. */\n storage: StorageLike | null;\n /** sessionStorage (or null if unavailable). Used for stable tab IDs. */\n sessionStorage: StorageLike | null;\n /** Monotonic clock; injected so tests can control time. */\n now: () => number;\n /** Generates a random ID (UUID when crypto is available, else Math.random). */\n randomId: () => string;\n /** Creates a BroadcastChannel by name, or null if unsupported. */\n createChannel: (name: string) => ClusterChannel | null;\n /** Sets an interval; returns a handle for clearInterval. */\n setInterval: (callback: () => void, intervalMs: number) => unknown;\n /** Clears a handle from setInterval. */\n clearInterval: (handle: unknown) => void;\n /** Current tab visibility ('visible' or 'hidden'). */\n getVisibilityState: () => TabVisibilityState;\n /** Register a listener for visibilitychange events. */\n addVisibilityChangeListener: (listener: () => void) => void;\n /** Remove a previously-added visibilitychange listener. */\n removeVisibilityChangeListener: (listener: () => void) => void;\n /** Register a listener for pagehide (BFCache entry). */\n addPageHideListener: (listener: () => void) => void;\n /** Remove a previously-added pagehide listener. */\n removePageHideListener: (listener: () => void) => void;\n /** Register a listener for pageshow (BFCache exit / restore). */\n addPageShowListener: (listener: () => void) => void;\n /** Remove a previously-added pageshow listener. */\n removePageShowListener: (listener: () => void) => void;\n}\n\n/** Resolve a Web Storage interface by name, or null in non-browser contexts.\n * The `typeof window` guard short-circuits in SSR / Node (where `window` is\n * undefined) before touching it; the try/catch covers sandboxed or\n * storage-disabled browsers that throw on property access. */\nfunction getStorage(name: 'localStorage' | 'sessionStorage'): StorageLike | null {\n try {\n return typeof window === 'undefined' ? null : window[name];\n } catch {\n return null;\n }\n}\n\nfunction randomId(): string {\n try {\n return globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);\n } catch {\n return Math.random().toString(36).slice(2);\n }\n}\n\n// A document can create multiple DataBus runtimes (for example market and\n// notice connections). Regenerate a copied opener id only on the first lookup\n// in that document; subsequent runtimes must continue sharing the same tabId.\nlet tabIdentityInitialized = false;\n\n/**\n * Default environment adapter for browser runtimes.\n * Probes for localStorage, BroadcastChannel, document, and window APIs\n * and gracefully returns null / no-ops when they are absent (SSR, Node).\n */\nexport function createBrowserEnvironment(): ClusterEnvironment {\n return {\n storage: getStorage('localStorage'),\n sessionStorage: getStorage('sessionStorage'),\n now: Date.now,\n randomId,\n createChannel: name => {\n try {\n return typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(name);\n } catch {\n return null;\n }\n },\n setInterval: (callback, intervalMs) => globalThis.setInterval(callback, intervalMs),\n clearInterval: handle => globalThis.clearInterval(handle as ReturnType<typeof setInterval>),\n getVisibilityState: () =>\n typeof document !== 'undefined' && document.visibilityState === 'hidden' ? 'hidden' : 'visible',\n addVisibilityChangeListener: listener => {\n if (typeof document !== 'undefined') document.addEventListener('visibilitychange', listener);\n },\n removeVisibilityChangeListener: listener => {\n if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', listener);\n },\n addPageHideListener: listener => {\n if (typeof window !== 'undefined') window.addEventListener('pagehide', listener);\n },\n removePageHideListener: listener => {\n if (typeof window !== 'undefined') window.removeEventListener('pagehide', listener);\n },\n addPageShowListener: listener => {\n if (typeof window !== 'undefined') window.addEventListener('pageshow', listener);\n },\n removePageShowListener: listener => {\n if (typeof window !== 'undefined') window.removeEventListener('pageshow', listener);\n }\n };\n}\n\n/**\n * Probe a storage instance with a write-read-delete round-trip.\n * Returns a type guard so the caller can narrow the type after a successful check.\n * Catches quota errors, disabled-storage (Safari private mode), or opaque\n * exceptions \u2014 any of which means the storage is not usable for coordination\n * and the Runtime must degrade to local mode.\n */\nexport function canUseStorage(storage: StorageLike | null, probeKey: string): storage is StorageLike {\n if (!storage) return false;\n try {\n storage.setItem(probeKey, '1');\n storage.removeItem(probeKey);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Get-or-create a stable tab ID stored in sessionStorage.\n * sessionStorage is scoped to the tab and survives refresh, so the same tab\n * retains its identity across the page lifecycle without coordination overhead.\n * Falls back to a random ID when sessionStorage is unavailable.\n */\nexport function getOrCreateTabId(\n environment: ClusterEnvironment,\n key = 'cross-tab-worker-databus:tab-id'\n): string {\n const storage = environment.sessionStorage;\n try {\n const existing = storage?.getItem(key);\n // `window.open()` may clone the opener's sessionStorage into the new tab.\n // A child page therefore must not blindly reuse the copied value: the\n // value identifies a page/tab instance, not an account or browser window.\n // `noopener` is still recommended by applications, but this guard keeps\n // the SDK safe when an opener is present.\n const hasOpener = typeof window !== 'undefined' && Boolean(window.opener);\n if (existing && (!hasOpener || tabIdentityInitialized)) {\n tabIdentityInitialized = true;\n return existing;\n }\n const created = `tab-${environment.randomId()}`;\n storage?.setItem(key, created);\n tabIdentityInitialized = true;\n return created;\n } catch {\n return `tab-${environment.randomId()}`;\n }\n}\n", "/**\n * Derives a stable 128-bit hex key from a string.\n *\n * This is a non-cryptographic four-way hash (inspired by MurmurHash-style\n * mixing). It exists so connection URLs and topic plaintext never touch\n * localStorage or BroadcastChannel namespaces \u2014 consumers only ever see the\n * opaque key. It trades collision resistance for speed and zero dependencies:\n * use `crypto.subtle.digest` if you need a cryptographic hash.\n */\nexport function createOpaqueKey(value: string): string {\n // Four independent lanes mix the input so a short value still diffuses\n // across all 128 bits rather than only exercising the low bits. Each lane\n // starts from a distinct 32-bit seed XORed with the length so that strings\n // of different lengths diverge from the first mix step.\n let h1 = SEED_H1 ^ value.length;\n let h2 = SEED_H2 ^ value.length;\n let h3 = SEED_H3 ^ value.length;\n let h4 = SEED_H4 ^ value.length;\n\n // Feed every UTF-16 code unit into all four lanes with distinct large primes.\n // Note: this operates on UTF-16 code units, so astral-plane characters (emoji,\n // rare CJK) are hashed as surrogate pairs \u2014 consistent within a process, but\n // not Unicode-normalized. Callers should normalize the topic string beforehand\n // if cross-normalization-form stability is required.\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index);\n h1 = Math.imul(h1 ^ code, PRIME_H1);\n h2 = Math.imul(h2 ^ code, PRIME_H2);\n h3 = Math.imul(h3 ^ code, PRIME_H3);\n h4 = Math.imul(h4 ^ code, PRIME_H4);\n }\n\n // Final avalanche: cross-mix the lanes so nearby inputs produce distant keys,\n // avoiding the clustering a naive sum would exhibit in storage prefixes.\n h1 = avalancheMix(h1, h2);\n h2 = avalancheMix(h2, h3);\n h3 = avalancheMix(h3, h4);\n h4 = avalancheMix(h4, h1);\n\n return [h1, h2, h3, h4].map(hash => (hash >>> 0).toString(16).padStart(8, '0')).join('');\n}\n\n/** Distinct 32-bit seeds for the four hash lanes. */\nconst SEED_H1 = 0xdeadbeef;\nconst SEED_H2 = 0x41c6ce57;\nconst SEED_H3 = 0xc0decafe;\nconst SEED_H4 = 0x9e3779b9;\n\n/** Distinct large primes for the four per-character mix steps. */\nconst PRIME_H1 = 2_654_435_761;\nconst PRIME_H2 = 1_597_334_677;\nconst PRIME_H3 = 2_246_822_519;\nconst PRIME_H4 = 3_266_489_917;\n\n/** Final avalanche constant pair. Each lane is mixed with itself (shifted)\n * and XORed with a neighbor lane (shifted) to cross-diffuse the lanes. */\nconst AVALANCHE_PRIME = 2_246_822_507;\nconst AVALANCHE_CROSS = 3_266_489_909;\n\n/** One step of the final avalanche: mix `self` with a shift and prime, then\n * XOR with a cross-mix of `neighbor` (also shifted and primed) so a change\n * in any lane propagates to the others. The 16/13 shifts spread bits across\n * the 32-bit word before the prime multiply scrambles them further. */\nfunction avalancheMix(self: number, neighbor: number): number {\n return (\n Math.imul(self ^ (self >>> 16), AVALANCHE_PRIME) ^\n Math.imul(neighbor ^ (neighbor >>> 13), AVALANCHE_CROSS)\n );\n}\n", "/**\n * Routing primitives for topic-owner selection and rebalancing.\n *\n * Pure functions that select candidate Workers, compute the least-loaded\n * owner, and decide when to migrate a topic. All side-effect-free, making\n * them straightforward to test and reason about.\n */\nimport type { WorkerRecord, WorkerRoute } from './types';\n\n/** Default cap on the number of Workers that can own topics concurrently.\n * Limits fan-out breadth: only N workers are eligible to be new-route\n * owners, so a cluster of 20 tabs still concentrates ownership on a few. */\nexport const DEFAULT_MAX_ACTIVE_WORKERS = 3;\n\n/**\n * Pick the Worker with the fewest owned topics, optionally preferring a\n * specific sticky owner when it is still in the candidate set.\n * Uses a single reduce pass instead of a full sort \u2014 O(n) \u2014 and breaks\n * load ties by workerId for deterministic routing across tabs (the\n * comparison is code-unit based, not locale-based, for cross-host stability).\n */\nexport function selectLeastLoadedWorker(\n workers: readonly WorkerRecord[],\n preferredWorkerId?: string\n): WorkerRecord | undefined {\n const preferred = workers.find(worker => worker.workerId === preferredWorkerId);\n if (preferred) return preferred;\n return workers.reduce<WorkerRecord | undefined>((least, worker) => {\n if (!least) return worker;\n const byLoad = worker.load - least.load;\n if (byLoad !== 0) return byLoad < 0 ? worker : least;\n // Tie-break by workerId with a locale-independent comparison so routing\n // is deterministic regardless of the host's collation order.\n if (worker.workerId < least.workerId) return worker;\n return least;\n }, undefined);\n}\n\n/**\n * Select the (up to `maxActiveWorkers`) Workers eligible to own topics.\n *\n * Eligibility cascade:\n * 1. Only `connecting` / `connected` workers are candidates.\n * 2. If any candidate is visible, prefer visible tabs (hidden tabs yield as owner).\n * 3. Fall back to all available workers when none is visible, so the cluster\n * does not stall when every tab is in the background.\n * 4. Tie-break by registration time, then workerId, for determinism.\n */\nexport function selectActiveWorkers(\n workers: readonly WorkerRecord[],\n maxActiveWorkers = DEFAULT_MAX_ACTIVE_WORKERS\n): WorkerRecord[] {\n const healthyWorkers = workers.filter(worker => worker.status === 'connecting' || worker.status === 'connected');\n const availableWorkers = healthyWorkers.length > 0 ? healthyWorkers : [...workers];\n const visibleWorkers = availableWorkers.filter(worker => worker.visibilityState === 'visible');\n const candidates = visibleWorkers.length > 0 ? visibleWorkers : availableWorkers;\n return candidates\n .sort(\n (left, right) =>\n left.registeredAt - right.registeredAt ||\n (left.workerId < right.workerId ? -1 : left.workerId > right.workerId ? 1 : 0)\n )\n .slice(0, maxActiveWorkers);\n}\n\n/**\n * Decide whether `currentWorkerId` should hand one topic to a less-loaded peer.\n * Returns the target Worker only when its load gap is significant (more than\n * one topic lighter), so the cluster does not churn over a single-topic\n * imbalance. One topic is migrated per reconciliation round to avoid thrashing.\n *\n * This remains exported as a standalone routing utility for API compatibility.\n * WorkerClusterRuntime intentionally does not use it: established routes are\n * sticky and load balancing applies only when selecting a new owner.\n */\nexport function selectRebalanceTarget(\n workers: readonly WorkerRecord[],\n currentWorkerId: string\n): WorkerRecord | null {\n const currentWorker = workers.find(worker => worker.workerId === currentWorkerId);\n const leastLoadedWorker = selectLeastLoadedWorker(workers);\n if (\n !currentWorker ||\n !leastLoadedWorker ||\n currentWorker.workerId === leastLoadedWorker.workerId ||\n currentWorker.load <= leastLoadedWorker.load + 1\n ) {\n return null;\n }\n return leastLoadedWorker;\n}\n\n/** True when a route's owner is still a live, active Worker in the given set. */\nexport function hasActiveOwner(route: WorkerRoute | null, workers: readonly WorkerRecord[]): boolean {\n return Boolean(route && workers.some(worker => worker.workerId === route.workerId));\n}\n\n/** True when `pattern` is a wildcard topic: `*` (match everything) or a\n * `prefix.*` suffix wildcard (match any remainder, including multiple\n * segments). Any other string is an exact topic. */\nexport function isWildcardTopic(pattern: string): boolean {\n return pattern === '*' || pattern.endsWith('.*');\n}\n\n/** True when a publication on `topic` must be delivered to a subscription\n * made with `pattern`. Exact patterns match only themselves; wildcards use\n * prefix matching so `chat.*` matches `chat.room.1` and `*` matches anything.\n * The empty pattern never matches. */\nexport function topicMatchesPattern(pattern: string, topic: string): boolean {\n if (!pattern || !topic) return false;\n if (pattern === topic) return true;\n if (pattern === '*') return true;\n if (!pattern.endsWith('.*')) return false;\n return topic.startsWith(pattern.slice(0, -1));\n}\n", "/**\n * BatchingStorageWriter \u2014 coalesced, resilient localStorage writes.\n *\n * Decorates a StorageLike with write coalescing: mutations within the same task\n * are merged by key and flushed once via a microtask, with exponential backoff\n * on quota/failure. Keeps the coordination metadata writes off the hot path.\n */\nimport type { StorageLike } from './environment';\n\n/** Initial retry delay for a failed storage write (ms). */\nconst INITIAL_RETRY_DELAY_MS = 50;\n/** Maximum retry delay after exponential backoff (ms). Caps at 1.6 s so a\n * persistently failing key retries roughly every 1-2 s, not every minute. */\nconst MAX_RETRY_DELAY_MS = 1_600;\n// Max retry attempts per key before giving up and dropping the write, so a\n// structurally failing key (e.g. a payload the underlying storage rejects)\n// cannot stall coordination forever. The local transport remains usable.\nconst MAX_RETRY_ATTEMPTS = 5;\n\n/**\n * Coalesces synchronous storage writes and applies them in one pass, with\n * exponential backoff when the underlying storage rejects a write.\n */\n/**\n * Coalesces synchronous storage writes and applies them in one pass, with\n * exponential backoff when the underlying storage rejects a write.\n *\n * Wraps a {@link StorageLike} so callers (WorkerClusterRuntime) see a normal\n * storage interface; reads transparently see pending writes before they flush.\n * The coalescing window is one microtask, so a burst of heartbeat + route +\n * subscriber writes in the same task becomes a single localStorage flush.\n */\nexport class BatchingStorageWriter implements StorageLike {\n /** Coalesced write set. A `null` value represents a pending delete. */\n private readonly pending = new Map<string, string | null>();\n /** Per-key retry counter, reset on a successful write. */\n private readonly retryCount = new Map<string, number>();\n private flushScheduled = false;\n private retryHandle: ReturnType<typeof setTimeout> | null = null;\n private retryDelayMs = INITIAL_RETRY_DELAY_MS;\n\n constructor(private readonly storage: StorageLike) {}\n\n /** Number of writes queued in memory but not yet flushed to storage.\n * Used by tests to assert the coalescing window and by flush() to detect\n * the all-drained state. */\n get pendingSize(): number {\n return this.pending.size;\n }\n\n get length(): number {\n return this.keys().length;\n }\n\n clear(): void {\n this.pending.clear();\n this.flushScheduled = false;\n this.storage.clear();\n this.cancelRetry();\n this.retryCount.clear();\n // Reset backoff so a burst of clear()/flush() cycles does not leave the\n // writer stuck at an elevated retry delay.\n this.retryDelayMs = INITIAL_RETRY_DELAY_MS;\n }\n\n // Reads always see the pending value first (task-local consistency), then\n // fall back to the underlying storage.\n getItem(key: string): string | null {\n if (this.pending.has(key)) return this.pending.get(key) ?? null;\n return this.storage.getItem(key);\n }\n\n key(index: number): string | null {\n return this.keys()[index] ?? null;\n }\n\n removeItem(key: string): void {\n this.pending.set(key, null);\n this.scheduleFlush();\n }\n\n setItem(key: string, value: string): void {\n this.pending.set(key, value);\n this.scheduleFlush();\n }\n\n flush(): void {\n this.flushScheduled = false;\n this.cancelRetry();\n // Apply writes from a snapshot so a concurrent scheduleFlush during the\n // loop cannot re-enter or corrupt the pending map mid-iteration.\n // Array.from is preferred over [...this.pending] here: it avoids the\n // spread's intermediate iterator allocation on a hot path that heartbeats\n // and route writes hit every few seconds.\n for (const [key, value] of Array.from(this.pending)) {\n try {\n if (value === null) this.storage.removeItem(key);\n else this.storage.setItem(key, value);\n this.pending.delete(key);\n this.retryCount.delete(key);\n } catch {\n const attempts = (this.retryCount.get(key) ?? 0) + 1;\n // A persistently failing key (e.g. a payload the storage rejects)\n // is dropped after MAX_RETRY_ATTEMPTS so coordination is not stuck\n // forever. Best-effort: the local transport stays usable without it.\n if (attempts >= MAX_RETRY_ATTEMPTS) {\n this.pending.delete(key);\n this.retryCount.delete(key);\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn('[cross-tab-worker-databus] storage write gave up after retries, dropping key:', key);\n }\n continue;\n }\n this.retryCount.set(key, attempts);\n // Remaining pending writes stay queued for the next attempt, which\n // retries with backoff. Coalescing is preserved by setting the gate\n // so a concurrent scheduleFlush cannot start a second overlapping pass.\n // `break` stops the flush at the first failure so the retry loop can\n // re-attempt this key (and the remaining pending entries) together,\n // rather than continuing to apply later keys while an earlier one is\n // still in a failed-and-retrying state.\n this.scheduleRetry();\n break;\n }\n }\n if (this.pending.size === 0) {\n this.retryDelayMs = INITIAL_RETRY_DELAY_MS;\n this.retryCount.clear();\n }\n }\n\n /** Union of persisted keys and pending writes, minus pending deletes. */\n private keys(): string[] {\n const keys = new Set<string>();\n for (let index = 0; index < this.storage.length; index += 1) {\n const key = this.storage.key(index);\n if (key !== null) keys.add(key);\n }\n for (const [key, value] of this.pending) {\n if (value === null) keys.delete(key);\n else keys.add(key);\n }\n return Array.from(keys);\n }\n\n // Coalesce all synchronous writes within one task into a single microtask\n // flush, avoiding a localStorage write per heartbeat/route/subscriber update.\n // The queueMicrotask fallback to setTimeout handles older runtimes and\n // non-browser environments where queueMicrotask is absent.\n private scheduleFlush(): void {\n if (this.flushScheduled) return;\n this.flushScheduled = true;\n const flush = () => {\n this.flushScheduled = false;\n this.flush();\n };\n if (typeof queueMicrotask === 'function') queueMicrotask(flush);\n else setTimeout(flush, 0);\n }\n\n // Schedule a single retry timer. The guard ensures only one retry is in\n // flight at a time; subsequent scheduleRetry calls during the wait are\n // no-ops because the first retry will re-flush all pending keys together.\n private scheduleRetry(): void {\n if (this.retryHandle !== null) return;\n this.retryHandle = setTimeout(() => {\n this.retryHandle = null;\n this.flush();\n }, this.retryDelayMs);\n // Exponential backoff: 50ms \u2192 100ms \u2192 \u2026 \u2192 capped at 1600ms.\n this.retryDelayMs = Math.min(MAX_RETRY_DELAY_MS, this.retryDelayMs * 2);\n }\n\n private cancelRetry(): void {\n if (this.retryHandle !== null) {\n clearTimeout(this.retryHandle);\n this.retryHandle = null;\n }\n }\n}\n", "/**\n * WorkerClusterRuntime \u2014 cross-tab cluster coordination layer.\n *\n * Manages Worker registration, heartbeat, sticky topic-owner routing,\n * page-lifecycle handoff/resume, and BroadcastChannel-based\n * control messaging. Each DataBus instance owns one Runtime which drives the\n * transport and coordinates with other tabs via localStorage + BroadcastChannel.\n */\nimport { canUseStorage, createBrowserEnvironment, getOrCreateTabId } from './environment';\nimport type { ClusterChannel, ClusterEnvironment, StorageLike } from './environment';\nimport { createOpaqueKey } from './hash';\nimport {\n DEFAULT_MAX_ACTIVE_WORKERS,\n selectActiveWorkers,\n selectLeastLoadedWorker,\n topicMatchesPattern\n} from './routing';\nimport type {\n TopicSubscriberRecord,\n WorkerClusterMessage,\n WorkerControlAction,\n WorkerRecord,\n WorkerRole,\n WorkerRoute,\n WorkerStatus\n} from './types';\nimport { BatchingStorageWriter } from './storage-batch';\n\n/** Callbacks the cluster invokes to drive the transport and lifecycle. */\nexport interface WorkerClusterHandlers {\n /** A SUBSCRIBE/UNSUBSCRIBE/PUBLISH control action was received for this worker. */\n onControl: (action: WorkerControlAction, topic: string, data?: unknown) => void;\n /** A fan-out publication event was received from another Worker. */\n onEvent: (eventType: string, payload: unknown, sourceWorkerId: string) => void;\n /** The cluster suspended (tab hidden / pagehide). */\n onSuspend?: () => void;\n /** The cluster resumed (tab visible / pageshow). */\n onResume?: () => void;\n}\n\nexport interface WorkerClusterOptions {\n /** Namespace for the cluster's storage keys and BroadcastChannel.\n * Two DataBus instances with different clusterKeys operate in isolation. */\n clusterKey: string;\n /** Callbacks the cluster invokes to drive the transport and lifecycle. */\n handlers: WorkerClusterHandlers;\n /** Inject a custom environment (for tests or SSR). Defaults to browser. */\n environment?: ClusterEnvironment;\n /** Override the storage key prefix (default 'cross-tab-worker-databus'). */\n storagePrefix?: string;\n /** Inject a stable tab ID (for tests). Defaults to sessionStorage-derived. */\n tabId?: string;\n /** Inject a worker ID (for tests). Defaults to 'worker-<tabId>-<random>'. */\n workerId?: string;\n /** Cap on concurrently active owners (default 3). See DEFAULT_MAX_ACTIVE_WORKERS. */\n maxActiveWorkers?: number;\n /** Heartbeat + reconcile interval in ms (default 3000). */\n heartbeatIntervalMs?: number;\n /** TTL after which a silent worker is pruned (default 10000). */\n workerTtlMs?: number;\n}\n\n/** Read-only snapshot of the cluster state for diagnostics and tracing. */\nexport interface WorkerClusterSnapshot {\n coordinated: boolean;\n suspended: boolean;\n currentWorker: WorkerRecord;\n workers: WorkerRecord[];\n /** Routes with the plaintext topic injected from the in-memory knownTopics cache. */\n routes: Array<WorkerRoute & { topic: string | null }>;\n subscribedTopics: string[];\n assignedTopics: string[];\n /** Opaque key \u2192 plaintext topic mapping for debugging. */\n knownTopics: Array<{ topicKey: string; topic: string }>;\n}\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 3_000;\nconst DEFAULT_WORKER_TTL_MS = 10_000;\nconst DEFAULT_STORAGE_PREFIX = 'cross-tab-worker-databus';\n// Upper bound on the topicKey \u2192 topic reverse cache. Control messages from\n// other workers can reference arbitrary topics, so cap growth to avoid an\n// unbounded memory leak from a misbehaving or malicious peer.\nconst MAX_KNOWN_TOPICS = 500;\n\n/** Parse a JSON value from storage, returning null on malformed or missing data.\n * Never throws \u2014 a corrupt route/worker record is treated as absent so the\n * reconcile cycle can recreate it. */\nfunction readJson<T>(storage: StorageLike, key: string): T | null {\n try {\n const value = storage.getItem(key);\n return value ? (JSON.parse(value) as T) : null;\n } catch {\n return null;\n }\n}\n\n/** Write a JSON value to storage, swallowing storage errors (coordination is\n * best-effort; a failed write does not break the local transport). The actual\n * write may be coalesced by BatchingStorageWriter \u2014 this just calls setItem. */\nfunction writeJson(storage: StorageLike, key: string, value: unknown): void {\n try {\n storage.setItem(key, JSON.stringify(value));\n } catch {\n // Coordination is best-effort. The local transport remains usable.\n }\n}\n\n/** List all storage keys that start with `prefix`. */\nfunction listKeys(storage: StorageLike, prefix: string): string[] {\n try {\n return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter(\n (key): key is string => Boolean(key?.startsWith(prefix))\n );\n } catch {\n return [];\n }\n}\n\n/** Read and parse every JSON record whose key starts with `prefix`.\n * Duplicates the `listKeys` + `readJson` loop found in readWorkers,\n * cleanupOrphanedRoutes, cleanupOrphanedSubscribers, and getSnapshot \u2014\n * extracted so each call site reads its records in one line. */\nfunction readAllByPrefix<T>(storage: StorageLike, prefix: string): Array<{ key: string; value: T }> {\n return listKeys(storage, prefix)\n .map(key => ({ key, value: readJson<T>(storage, key) }))\n .filter((entry): entry is { key: string; value: T } => entry.value !== null);\n}\n\n/**\n * Cross-tab worker coordination runtime.\n *\n * Manages a cluster of Workers (one per tab) that share topics via localStorage\n * and BroadcastChannel. Each Worker publishes its own record, subscribes to\n * topics, and routes publications through the owning Worker to avoid duplicates.\n *\n * Key responsibilities:\n * - Heartbeat-based failure detection (stale workers pruned after `workerTtlMs`)\n * - Topic-to-Worker routing with load-based rebalancing\n * - Page lifecycle integration (suspend on hide, resume on show)\n * - Storage-backed coordination with BatchingStorageWriter for write coalescing\n */\nexport class WorkerClusterRuntime {\n readonly tabId: string;\n readonly workerId: string;\n\n private readonly environment: ClusterEnvironment;\n private readonly handlers: WorkerClusterHandlers;\n private storage: StorageLike | null;\n private readonly maxActiveWorkers: number;\n private readonly heartbeatIntervalMs: number;\n private readonly workerTtlMs: number;\n private readonly workerPrefix: string;\n private readonly routePrefix: string;\n private readonly subscriberPrefix: string;\n private readonly channelName: string;\n // Topics this tab has subscribed to (local interest, plaintext).\n private readonly subscribedTopics = new Set<string>();\n // Topics assigned to this Worker as owner (topicKey \u2192 topic). Authoritative:\n // membership drives isAssigned() and load. Grows only via CONTROL/SUBSCRIBE\n // (or local self-subscribe), never via the reverse cache.\n private readonly assignedTopics = new Map<string, string>();\n // Reverse mapping: opaque topicKey \u2192 plaintext topic. A bounded cache with\n // FIFO eviction \u2014 NOT authoritative. It can hold a topicKey that is also in\n // assignedTopics (the owned guard prevents evicting those), because it is\n // the only source of plaintext when storage is unavailable. See the\n // rememberTopic() doc for the eviction contract.\n private readonly knownTopics = new Map<string, string>();\n private channel: ClusterChannel | null = null;\n private heartbeatHandle: unknown = null;\n private started = false;\n private suspended = false;\n private lifecycleListening = false;\n private currentRecord: WorkerRecord;\n\n constructor(options: WorkerClusterOptions) {\n this.environment = options.environment ?? createBrowserEnvironment();\n this.handlers = options.handlers;\n this.maxActiveWorkers = options.maxActiveWorkers ?? DEFAULT_MAX_ACTIVE_WORKERS;\n this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n this.workerTtlMs = options.workerTtlMs ?? DEFAULT_WORKER_TTL_MS;\n // Derive storage keys from a hash of the cluster key so that the plaintext\n // cluster identifier never appears in localStorage.\n const clusterHash = createOpaqueKey(options.clusterKey || '__default__');\n const prefix = options.storagePrefix ?? DEFAULT_STORAGE_PREFIX;\n const baseKey = `${prefix}:${clusterHash}`;\n this.workerPrefix = `${baseKey}:worker:`;\n this.routePrefix = `${baseKey}:route:`;\n this.subscriberPrefix = `${baseKey}:subscriber:`;\n this.channelName = `${prefix}:bus:${clusterHash}`;\n // Wrap localStorage in a BatchingStorageWriter to coalesce writes.\n this.storage = canUseStorage(this.environment.storage, `${baseKey}:probe`)\n ? new BatchingStorageWriter(this.environment.storage)\n : null;\n this.tabId = options.tabId ?? getOrCreateTabId(this.environment, `${prefix}:tab-id`);\n this.workerId = options.workerId ?? `worker-${this.tabId}-${this.environment.randomId()}`;\n const now = this.environment.now();\n this.currentRecord = {\n workerId: this.workerId,\n tabId: this.tabId,\n load: 0,\n role: 'standby',\n status: 'connecting',\n visibilityState: this.environment.getVisibilityState(),\n heartbeatAt: now,\n registeredAt: now\n };\n }\n\n /** Start the cluster: register, listen for lifecycle events, and begin heartbeats. */\n start(): void {\n if (this.started) return;\n this.suspended = false;\n this.addLifecycleListeners();\n this.activate();\n }\n\n /**\n * Stop the cluster: pause heartbeats, hand off assigned topics, remove\n * the worker record, and clean up lifecycle listeners. Idempotent.\n * The .clear() calls after pause() are safe no-ops when pause already\n * cleared the maps (the handoff path), but ensure a full teardown in the\n * stop() path where callers expect every Set/Map to be empty afterwards.\n */\n stop(): void {\n if (!this.started && !this.suspended) return;\n this.pause();\n this.flushStorage();\n this.removeLifecycleListeners();\n this.subscribedTopics.clear();\n this.assignedTopics.clear();\n this.knownTopics.clear();\n this.suspended = false;\n }\n\n /**\n * Activate the cluster: open the BroadcastChannel, register the worker record,\n * subscribe to topics, and start the heartbeat interval.\n */\n private activate(): void {\n if (this.started) return;\n this.started = true;\n // Create the BroadcastChannel for cross-tab messaging. If storage is\n // unavailable, we cannot coordinate \u2014 skip the channel. If the channel\n // itself fails to construct (sandboxed iframe, permissions policy),\n // null out storage too: without a channel the storage writes have no\n // peer to observe them, so the BatchingStorageWriter would write for\n // nothing and the degraded code paths must take over.\n this.channel = this.storage ? this.environment.createChannel(this.channelName) : null;\n if (!this.channel) this.storage = null;\n this.channel?.addEventListener('message', this.handleMessage);\n const now = this.environment.now();\n this.currentRecord = {\n ...this.currentRecord,\n heartbeatAt: now,\n registeredAt: now,\n visibilityState: this.environment.getVisibilityState()\n };\n this.refreshRole(this.readWorkers());\n this.writeRecord(true);\n // Re-subscribe any topics that were subscribed before the cluster started.\n // rememberTopic is called once per topic regardless of branch so the reverse\n // cache is populated before either the control message or the subscriber write.\n for (const topic of this.subscribedTopics) {\n const topicKey = this.rememberTopic(topic);\n if (!this.storage) this.sendControl(this.workerId, 'SUBSCRIBE', topic, topicKey);\n else this.writeSubscriber(topicKey);\n }\n this.reconcile();\n // Periodic heartbeat + reconciliation.\n this.heartbeatHandle = this.environment.setInterval(() => {\n this.writeRecord(false);\n this.reconcile();\n }, this.heartbeatIntervalMs);\n }\n\n /**\n * Pause the cluster on pagehide: stop heartbeats, hand off assigned topics\n * to other workers, remove our worker record, and close the channel.\n */\n private pause(): void {\n if (!this.started) return;\n this.started = false;\n this.suspended = true;\n if (this.heartbeatHandle !== null) this.environment.clearInterval(this.heartbeatHandle);\n this.heartbeatHandle = null;\n this.channel?.removeEventListener('message', this.handleMessage);\n // Order matters: release local subscriptions BEFORE handing off assigned\n // topics, then clear the assignment map. Releasing first removes the\n // subscriber records so handoff sees the correct remaining subscribers;\n // clearing after handoff ensures no topic is both handed off and left\n // dangling. Do not reorder without addressing the handoff semantics.\n for (const topic of this.subscribedTopics) this.releaseSubscription(topic, false);\n this.handoffAssignedTopics();\n this.assignedTopics.clear();\n this.removeStorage(this.workerStorageKey(this.workerId));\n // Persist the final routes and worker removal before asking peers to\n // reconcile. A pagehide CONTROL message may be lost; REGISTRY must still\n // let peers observe the completed handoff immediately.\n this.flushStorage();\n this.notifyRegistry();\n this.channel?.close();\n this.channel = null;\n this.handlers.onSuspend?.();\n }\n\n /** Update the worker's connection status and persist the change. */\n setStatus(status: WorkerStatus): void {\n if (this.currentRecord.status === status) return;\n this.currentRecord = { ...this.currentRecord, status };\n if (this.started) this.writeRecord(true);\n }\n\n /**\n * Subscribe to a topic. Returns true if this worker becomes the assigned owner.\n * The topic is recorded locally and the cluster is notified via storage or\n * direct control message.\n */\n subscribe(topic: string): boolean {\n const topicKey = this.rememberTopic(topic);\n this.subscribedTopics.add(topic);\n if (!this.started) return false;\n if (!this.storage) {\n this.sendControl(this.workerId, 'SUBSCRIBE', topic, topicKey);\n return true;\n }\n this.writeSubscriber(topicKey);\n const workers = this.readWorkers();\n const existingRoute = this.readRoute(topicKey);\n if (this.routeOwnerIsLive(existingRoute, workers)) {\n return existingRoute?.workerId === this.workerId;\n }\n\n const activeWorkers = selectActiveWorkers(workers, this.maxActiveWorkers);\n const owner = selectLeastLoadedWorker(activeWorkers) ?? this.currentRecord;\n // A missing live owner cannot participate in a strict handoff. Assign and\n // subscribe immediately; pagehide uses handoffAssignedTopics() while the\n // old owner is still present when release ordering is required.\n this.writeRoute(topicKey, owner, undefined, (existingRoute?.generation ?? 0) + 1);\n this.sendControl(owner.workerId, 'SUBSCRIBE', topic, topicKey);\n this.notifyRegistry();\n return owner.workerId === this.workerId;\n }\n\n /**\n * Remove the local subscription. Cleans up the subscriber record and, if no\n * subscribers remain, deletes the route so the owning Worker can unsubscribe.\n */\n unsubscribe(topic: string): void {\n this.subscribedTopics.delete(topic);\n const topicKey = this.releaseSubscription(topic);\n // Keep the topic in knownTopics if we remain the owner (we may still fan out).\n if (topicKey && !this.assignedTopics.has(topicKey)) this.knownTopics.delete(topicKey);\n }\n\n /** Remove this tab's subscriber record and, when it was the last one, delete\n * the route. Returns the topicKey (so callers like `unsubscribe` can reuse\n * it instead of re-hashing the topic to evict the reverse cache). */\n private releaseSubscription(topic: string, notifyOwner = true): string {\n const topicKey = this.rememberTopic(topic);\n this.removeStorage(this.subscriberStorageKey(topicKey, this.tabId));\n const route = this.readRoute(topicKey);\n if (!route) return topicKey;\n const subscribers = this.readSubscriberTabIds(topicKey, this.readWorkers());\n if (subscribers.length === 0) {\n this.removeStorage(this.routeStorageKey(topicKey));\n if (notifyOwner) this.sendControl(route.workerId, 'UNSUBSCRIBE', topic, topicKey);\n }\n return topicKey;\n }\n\n /** Transfer assigned topics to other active workers so subscribers are not orphaned during pause. */\n private handoffAssignedTopics(): void {\n if (!this.storage || this.assignedTopics.size === 0) return;\n const remainingWorkers = this.readWorkers().filter(worker => worker.workerId !== this.workerId);\n const activeWorkers = selectActiveWorkers(remainingWorkers, this.maxActiveWorkers);\n // `WorkerRecord.load` is a snapshot from before this handoff. Keep a\n // projected load locally so a batch of topics is distributed across the\n // remaining workers instead of every route choosing the same initial\n // minimum.\n const projectedLoads = new Map(activeWorkers.map(worker => [worker.workerId, worker.load]));\n\n for (const [topicKey, topic] of this.assignedTopics) {\n const previous = this.readRoute(topicKey);\n if (previous?.workerId !== this.workerId) continue;\n const subscribers = this.readSubscriberTabIds(topicKey, remainingWorkers);\n if (subscribers.length === 0) {\n this.removeStorage(this.routeStorageKey(topicKey));\n continue;\n }\n const owner = selectLeastLoadedWorker(\n activeWorkers.map(worker => ({ ...worker, load: projectedLoads.get(worker.workerId) ?? worker.load }))\n );\n if (!owner) continue;\n projectedLoads.set(owner.workerId, (projectedLoads.get(owner.workerId) ?? owner.load) + 1);\n const generation = (previous?.generation ?? 0) + 1;\n this.writeRoute(topicKey, owner, previous?.workerId, generation);\n // Make the new route visible before the target confirms it. This also\n // leaves a durable unconfirmed assignment when unload drops CONTROL.\n this.flushStorage();\n // Release the old server subscription before authorizing the new owner.\n // The ACK is sent after the transport operation has been requested.\n this.handlers.onControl('UNSUBSCRIBE', topic);\n this.sendRouteReleased(owner.workerId, topic, topicKey, generation);\n }\n }\n\n /**\n * Publish a message to `topic`, routing through the owning Worker (or self if\n * no owner is found). Returns false when the control message could not be\n * posted to a remote owner, so the caller can surface the failure instead of\n * silently dropping the publication.\n */\n publish(topic: string, data: unknown): boolean {\n const topicKey = this.rememberTopic(topic);\n const workers = this.readWorkers();\n const route = this.readRoute(topicKey);\n const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;\n return this.sendControl(target, 'PUBLISH', topic, topicKey, data);\n }\n\n /** True when `route` exists and its owner worker is among `workers`.\n * Shared by subscribe (skip re-assignment) and publish (route to owner).\n * Intentionally returns a plain boolean (not a type guard) so the caller\n * can still access `route?.generation` in the false branch. */\n private routeOwnerIsLive(route: WorkerRoute | null, workers: readonly WorkerRecord[]): boolean {\n return Boolean(route && workers.some(worker => worker.workerId === route.workerId));\n }\n\n /** Broadcast an event to every tab \u2014 used to fan out transport publications. */\n broadcastEvent(eventType: string, payload: unknown): void {\n this.send({ type: 'EVENT', sourceWorkerId: this.workerId, eventType, payload });\n }\n\n isAssigned(topic: string): boolean {\n // Deliberately recompute the key via createOpaqueKey rather than\n // rememberTopic(): this is a read-only query, not a state change, so it\n // must not populate the knownTopics reverse-cache. Hashing is cheap enough\n // that re-deriving here is preferable to evicting a cached entry that the\n // storage-less readRoute path may need (see rememberTopic eviction guard).\n const topicKey = createOpaqueKey(topic);\n // Prefer the in-memory assignment map: it is updated synchronously on\n // SUBSCRIBE/UNSUBSCRIBE, whereas readRoute() may observe a route that has\n // not yet been flushed through the BatchingStorageWriter, causing a message\n // destined for this worker to be dropped during the write window.\n if (this.assignedTopics.has(topicKey)) return true;\n // Wildcard assignments: this worker owns the transport subscription for a\n // pattern (e.g. \"chat.*\"), so publications arriving under a matching\n // concrete topic (e.g. \"chat.room.1\", as delivered by pattern-aware\n // servers) belong to the same route and must fan out from here too.\n for (const pattern of this.assignedTopics.values()) {\n if (pattern !== topic && topicMatchesPattern(pattern, topic)) return true;\n }\n return this.readRoute(topicKey)?.workerId === this.workerId;\n }\n\n /** True if this worker is among the active set (eligible to own topics). */\n isActiveWorker(): boolean {\n return this.isActiveAmong(this.readWorkers());\n }\n\n /** True when this workerId is in the active subset of `workers`. Shared by\n * isActiveWorker() and refreshRole() so both compute role identically. */\n private isActiveAmong(workers: readonly WorkerRecord[]): boolean {\n return selectActiveWorkers(workers, this.maxActiveWorkers).some(\n worker => worker.workerId === this.workerId\n );\n }\n\n /** True when this tab has a local subscriber registered for `topic` \u2014\n * exactly, or via a wildcard subscription that matches it. */\n hasLocalSubscriber(topic: string): boolean {\n if (this.subscribedTopics.has(topic)) return true;\n for (const pattern of this.subscribedTopics) {\n if (pattern !== topic && topicMatchesPattern(pattern, topic)) return true;\n }\n return false;\n }\n\n /** Read-only snapshot of the cluster state (workers, routes, assignments). */\n getSnapshot(): WorkerClusterSnapshot {\n const routes = this.storage\n ? readAllByPrefix<WorkerRoute>(this.storage, this.routePrefix).map(({ value }) => ({\n ...value,\n topic: this.knownTopics.get(value.topicKey) ?? null\n }))\n : [];\n return {\n coordinated: Boolean(this.storage && this.channel),\n suspended: this.suspended,\n currentWorker: { ...this.currentRecord },\n workers: this.readWorkers().map(worker => ({ ...worker })),\n routes,\n subscribedTopics: Array.from(this.subscribedTopics),\n assignedTopics: Array.from(this.assignedTopics.values()),\n knownTopics: Array.from(this.knownTopics.entries(), ([topicKey, topic]) => ({ topicKey, topic }))\n };\n }\n\n private readonly handlePageHide = () => this.pause();\n\n private readonly handlePageShow = () => {\n if (!this.suspended) return;\n this.suspended = false;\n this.handlers.onResume?.();\n this.activate();\n };\n\n private readonly handleVisibilityChange = () => {\n const visibilityState = this.environment.getVisibilityState();\n if (visibilityState === this.currentRecord.visibilityState) return;\n this.currentRecord = { ...this.currentRecord, visibilityState };\n if (this.started) {\n this.writeRecord(true);\n this.reconcile();\n }\n };\n\n private addLifecycleListeners(): void {\n if (this.lifecycleListening) return;\n this.lifecycleListening = true;\n this.environment.addPageHideListener(this.handlePageHide);\n this.environment.addPageShowListener(this.handlePageShow);\n this.environment.addVisibilityChangeListener(this.handleVisibilityChange);\n }\n\n private removeLifecycleListeners(): void {\n if (!this.lifecycleListening) return;\n this.lifecycleListening = false;\n this.environment.removePageHideListener(this.handlePageHide);\n this.environment.removePageShowListener(this.handlePageShow);\n this.environment.removeVisibilityChangeListener(this.handleVisibilityChange);\n }\n\n /** Handle an incoming cluster message: dispatch by type to the per-type handlers. */\n private readonly handleMessage = (event: MessageEvent<WorkerClusterMessage>) => {\n const message = event.data;\n if (!message || message.sourceWorkerId === this.workerId) return;\n switch (message.type) {\n case 'CONTROL':\n return this.handleControlMessage(message);\n case 'ROUTE_RELEASED':\n return this.handleRouteReleasedMessage(message);\n case 'EVENT':\n this.handlers.onEvent(message.eventType, message.payload, message.sourceWorkerId);\n return;\n case 'REGISTRY':\n default:\n this.reconcile();\n return;\n }\n };\n\n /** Handle a point-to-point CONTROL message (SUBSCRIBE / UNSUBSCRIBE / PUBLISH). */\n private handleControlMessage(\n message: Extract<WorkerClusterMessage, { type: 'CONTROL' }>\n ): void {\n if (message.targetWorkerId !== this.workerId) return;\n this.rememberTopic(message.topic);\n switch (message.action) {\n case 'SUBSCRIBE':\n this.assignedTopics.set(message.topicKey, message.topic);\n this.confirmRoute(message.topicKey);\n break;\n case 'UNSUBSCRIBE':\n // A graceful handoff release short-circuits the generic dispatch.\n if (this.releaseHandoffOnUnsubscribe(message)) return;\n break;\n case 'PUBLISH':\n default:\n break;\n }\n this.handlers.onControl(message.action, message.topic, message.data);\n if (message.action !== 'PUBLISH') this.updateLoad();\n }\n\n /**\n * When this worker is the previous owner in a graceful handoff and the new\n * owner asks us to unsubscribe, release the old transport subscription and\n * ACK the handoff with ROUTE_RELEASED. Returns true when the message was a\n * handoff release (the generic CONTROL dispatch must not run as well).\n */\n private releaseHandoffOnUnsubscribe(\n message: Extract<WorkerClusterMessage, { type: 'CONTROL' }>\n ): boolean {\n this.assignedTopics.delete(message.topicKey);\n const route = this.readRoute(message.topicKey);\n if (route?.handoffFromWorkerId !== this.workerId) return false;\n this.handlers.onControl('UNSUBSCRIBE', message.topic, undefined);\n this.sendRouteReleased(route.workerId, message.topic, message.topicKey, route.generation);\n this.updateLoad();\n return true;\n }\n\n /** Post a ROUTE_RELEASED ACK to the new owner, carrying the current route\n * generation so only the matching new owner may act on it. */\n private sendRouteReleased(\n targetWorkerId: string,\n topic: string,\n topicKey: string,\n generation: number\n ): void {\n this.send({\n type: 'ROUTE_RELEASED',\n sourceWorkerId: this.workerId,\n targetWorkerId,\n topic,\n topicKey,\n generation\n });\n }\n\n /**\n * Accept a graceful handoff only when the route still points to this worker,\n * the release comes from the recorded previous owner, and the generation is\n * at least as new as ours. Any other ROUTE_RELEASED is stale and dropped.\n */\n private handleRouteReleasedMessage(\n message: Extract<WorkerClusterMessage, { type: 'ROUTE_RELEASED' }>\n ): void {\n if (message.targetWorkerId !== this.workerId) return;\n const route = this.readRoute(message.topicKey);\n if (!route || this.isStaleRouteRelease(route, message)) return;\n this.assignedTopics.set(message.topicKey, message.topic);\n this.confirmRoute(message.topicKey);\n this.handlers.onControl('SUBSCRIBE', message.topic, undefined);\n this.updateLoad();\n }\n\n /** A ROUTE_RELEASED is stale (and must be dropped) unless the route still\n * points to us, the release comes from the recorded previous owner, and\n * the release generation is at least as new as ours. */\n private isStaleRouteRelease(\n route: WorkerRoute,\n message: Extract<WorkerClusterMessage, { type: 'ROUTE_RELEASED' }>\n ): boolean {\n return (\n route.workerId !== this.workerId ||\n route.handoffFromWorkerId !== message.sourceWorkerId ||\n route.generation < message.generation\n );\n }\n\n /** Full reconciliation cycle: workers, subscriptions, and assigned topics. */\n private reconcile(): void {\n if (!this.started) return;\n const workers = this.reconcileWorkers();\n const activeWorkers = selectActiveWorkers(workers, this.maxActiveWorkers);\n this.reconcileSubscriptions(workers, activeWorkers);\n this.reconcileAssignedTopics();\n this.updateLoad();\n }\n\n /** Prune stale workers/subscribers/routes and refresh role. Returns the live worker list.\n * Subscribers are cleaned before routes so cleanupOrphanedRoutes sees the\n * updated subscriber set when deciding whether a route is truly orphaned. */\n private reconcileWorkers(): WorkerRecord[] {\n const workers = this.readWorkers();\n this.cleanupOrphanedSubscribers(workers);\n this.cleanupOrphanedRoutes(workers);\n const roleChanged = this.refreshRole(workers);\n if (roleChanged) this.writeRecord(false);\n return workers;\n }\n\n /**\n * Ensure every local subscription has a route and write subscriber records.\n *\n * Existing routes are deliberately sticky while their owner Worker is alive.\n * Load and visibility only influence placement of a new route; they must not\n * move an already-subscribed Topic merely because another Tab joins or becomes\n * visible. Ownership changes only after the owner leaves or its heartbeat\n * expires, which avoids unnecessary transport subscribe/unsubscribe churn.\n */\n private reconcileSubscriptions(\n workers: readonly WorkerRecord[],\n activeWorkers: readonly WorkerRecord[]\n ): void {\n const liveWorkerIds = new Set(workers.map(worker => worker.workerId));\n\n for (const topic of this.subscribedTopics) {\n const topicKey = this.rememberTopic(topic);\n this.writeSubscriber(topicKey);\n const route = this.readRoute(topicKey);\n if (!route || !liveWorkerIds.has(route.workerId)) {\n const owner = selectLeastLoadedWorker(activeWorkers) ?? this.currentRecord;\n // A route invalidated by owner departure or heartbeat expiry is\n // recovered immediately. Graceful pagehide uses the strict ACK path in\n // handoffAssignedTopics(), where the departing owner is still known.\n this.writeRoute(topicKey, owner, undefined, (route?.generation ?? 0) + 1);\n this.sendControl(owner.workerId, 'SUBSCRIBE', topic, topicKey);\n this.notifyRegistry();\n continue;\n }\n if (route.confirmedAt === undefined) {\n // During a handoff, the new owner waits for ROUTE_RELEASED from the\n // previous owner. Retrying SUBSCRIBE here would recreate overlap.\n if (!route.handoffFromWorkerId) {\n this.sendControl(route.workerId, 'SUBSCRIBE', topic, topicKey);\n }\n }\n }\n }\n\n /** Drop assignments where the route no longer points to this worker. */\n private reconcileAssignedTopics(): void {\n for (const [topicKey, topic] of [...this.assignedTopics]) {\n const route = this.readRoute(topicKey);\n if (route?.workerId === this.workerId) continue;\n this.assignedTopics.delete(topicKey);\n this.handlers.onControl('UNSUBSCRIBE', topic, undefined);\n if (route?.handoffFromWorkerId === this.workerId) {\n this.sendRouteReleased(route.workerId, topic, topicKey, route.generation);\n }\n if (!this.subscribedTopics.has(topic)) this.knownTopics.delete(topicKey);\n }\n }\n\n /**\n * Send a control message to `targetWorkerId`, or execute locally when targeting self.\n * Local execution updates the assignment map and route synchronously, bypassing\n * the BroadcastChannel latency.\n */\n private sendControl(\n targetWorkerId: string,\n action: WorkerControlAction,\n topic: string,\n topicKey: string,\n data?: unknown\n ): boolean {\n if (targetWorkerId === this.workerId) {\n switch (action) {\n case 'SUBSCRIBE':\n this.assignedTopics.set(topicKey, topic);\n this.confirmRoute(topicKey);\n break;\n case 'UNSUBSCRIBE':\n this.assignedTopics.delete(topicKey);\n break;\n case 'PUBLISH':\n default:\n break;\n }\n this.handlers.onControl(action, topic, data);\n if (action !== 'PUBLISH') this.updateLoad();\n return true;\n }\n return this.send({\n type: 'CONTROL',\n sourceWorkerId: this.workerId,\n targetWorkerId,\n action,\n topic,\n topicKey,\n ...(data === undefined ? {} : { data })\n });\n }\n\n /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */\n private send(message: WorkerClusterMessage): boolean {\n if (!this.channel) return false;\n try {\n this.channel.postMessage(message);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Read all live worker records from storage, pruning stale entries past the TTL. */\n private readWorkers(): WorkerRecord[] {\n if (!this.storage) return [this.currentRecord];\n const now = this.environment.now();\n const workers: WorkerRecord[] = [];\n for (const { key, value: worker } of readAllByPrefix<WorkerRecord>(this.storage, this.workerPrefix)) {\n if (worker.workerId !== this.workerId && now - worker.heartbeatAt > this.workerTtlMs) {\n this.removeStorage(key);\n continue;\n }\n workers.push(worker);\n }\n if (this.started && !workers.some(worker => worker.workerId === this.workerId)) workers.push(this.currentRecord);\n return workers;\n }\n\n /** Enumerate all tab IDs that have a subscriber record for `topicKey`. */\n private readSubscriberTabIds(topicKey: string, workers: readonly WorkerRecord[]): string[] {\n if (!this.storage) {\n // Degraded mode: only this tab can be a subscriber. Recover the plaintext\n // topic to check local interest \u2014 without it we cannot know if we care.\n const topic = this.knownTopics.get(topicKey);\n return topic && this.subscribedTopics.has(topic) ? [this.tabId] : [];\n }\n const activeTabIds = new Set(workers.map(worker => worker.tabId));\n const subscribers = new Set<string>();\n for (const { key, value: record } of readAllByPrefix<TopicSubscriberRecord>(\n this.storage,\n `${this.subscriberPrefix}${topicKey}:`\n )) {\n if (!activeTabIds.has(record.tabId)) {\n this.removeStorage(key);\n continue;\n }\n subscribers.add(record.tabId);\n }\n return Array.from(subscribers);\n }\n\n /** Read the current route for `topicKey`, returning null when no storage layer exists. */\n private readRoute(topicKey: string): WorkerRoute | null {\n if (!this.storage) return this.buildLocalRoute(topicKey);\n return readJson<WorkerRoute>(this.storage, this.routeStorageKey(topicKey));\n }\n\n /** Synthesize a self-owned route when storage is unavailable (degraded mode).\n * The plaintext topic must be recoverable from the knownTopics cache; a\n * missing entry means we never subscribed to or were assigned the topic,\n * so there is no route to report. */\n private buildLocalRoute(topicKey: string): WorkerRoute | null {\n const topic = this.knownTopics.get(topicKey);\n if (!topic) return null;\n if (!this.subscribedTopics.has(topic) && !this.assignedTopics.has(topicKey)) return null;\n return {\n topicKey,\n workerId: this.workerId,\n tabId: this.tabId,\n updatedAt: this.environment.now(),\n generation: 1\n };\n }\n\n /** Persist a route assignment, mapping `topicKey` to the owning Worker. */\n private writeRoute(\n topicKey: string,\n owner: WorkerRecord,\n handoffFromWorkerId?: string,\n generation = 1\n ): void {\n if (!this.storage) return;\n writeJson(this.storage, this.routeStorageKey(topicKey), this.buildRouteRecord(topicKey, owner, handoffFromWorkerId, generation));\n }\n\n /** Construct a WorkerRoute record from the owner + handoff fields. Extracted\n * so writeRoute and confirmRoute share the same shape; confirmedAt is added\n * by confirmRoute via spread. */\n private buildRouteRecord(\n topicKey: string,\n owner: WorkerRecord,\n handoffFromWorkerId: string | undefined,\n generation: number\n ): WorkerRoute {\n return {\n topicKey,\n workerId: owner.workerId,\n tabId: owner.tabId,\n updatedAt: this.environment.now(),\n generation,\n ...(handoffFromWorkerId ? { handoffFromWorkerId } : {})\n };\n }\n\n /** Stamp a route as confirmed once the owning Worker has acknowledged the assignment. */\n private confirmRoute(topicKey: string): void {\n if (!this.storage) return;\n const route = this.readRoute(topicKey);\n if (!route || route.workerId !== this.workerId || route.confirmedAt !== undefined) return;\n writeJson(this.storage, this.routeStorageKey(topicKey), {\n ...route,\n confirmedAt: this.environment.now()\n } satisfies WorkerRoute);\n }\n\n /** Remove routes whose topic has no subscribers and whose TTL has expired. */\n private cleanupOrphanedRoutes(workers: readonly WorkerRecord[]): void {\n if (!this.storage) return;\n const now = this.environment.now();\n for (const { key, value: route } of readAllByPrefix<WorkerRoute>(this.storage, this.routePrefix)) {\n if (now - route.updatedAt <= this.workerTtlMs) continue;\n if (this.readSubscriberTabIds(route.topicKey, workers).length > 0) continue;\n this.removeStorage(key);\n }\n }\n\n /** Remove subscriber records for tabs that are no longer active. */\n private cleanupOrphanedSubscribers(workers: readonly WorkerRecord[]): void {\n if (!this.storage) return;\n const activeTabIds = new Set(workers.map(worker => worker.tabId));\n for (const { key, value: record } of readAllByPrefix<TopicSubscriberRecord>(this.storage, this.subscriberPrefix)) {\n if (!activeTabIds.has(record.tabId)) this.removeStorage(key);\n }\n }\n\n /** Persist a subscriber record for this tab on `topicKey`. */\n private writeSubscriber(topicKey: string): void {\n if (!this.storage) return;\n writeJson(this.storage, this.subscriberStorageKey(topicKey, this.tabId), {\n tabId: this.tabId,\n updatedAt: this.environment.now()\n } satisfies TopicSubscriberRecord);\n }\n\n /** Persist the current worker record with an updated heartbeat timestamp.\n * @param notify \u2014 when true, broadcast a REGISTRY nudge so peers reconcile\n * immediately instead of waiting for the next heartbeat. False on the\n * periodic heartbeat tick (peers will notice on their own heartbeat) to\n * avoid a REGISTRY storm every 3 s; true on status/role changes that\n * peers should observe promptly. */\n private writeRecord(notify: boolean): void {\n this.currentRecord = { ...this.currentRecord, heartbeatAt: this.environment.now() };\n if (this.storage) writeJson(this.storage, this.workerStorageKey(this.workerId), this.currentRecord);\n if (notify) this.notifyRegistry();\n }\n\n /** Broadcast a REGISTRY message to trigger reconciliation on other tabs. */\n private notifyRegistry(): void {\n this.send({ type: 'REGISTRY', sourceWorkerId: this.workerId });\n }\n\n /** Recompute whether this worker is active (eligible to own topics) or standby. Returns true when changed. */\n private refreshRole(workers: readonly WorkerRecord[]): boolean {\n const role: WorkerRole = this.isActiveAmong(workers) ? 'active' : 'standby';\n if (role === this.currentRecord.role) return false;\n this.currentRecord = { ...this.currentRecord, role };\n return true;\n }\n\n /** Persist the current topic load count (number of assigned topics) for load-balanced routing. */\n private updateLoad(): void {\n const load = this.assignedTopics.size;\n if (load === this.currentRecord.load) return;\n this.currentRecord = { ...this.currentRecord, load };\n if (this.started) this.writeRecord(true);\n }\n\n /**\n * Hash `topic` into its opaque key and populate the reverse-lookup cache.\n *\n * Despite the name, this is NOT a cache lookup \u2014 it unconditionally writes\n * the `topicKey \u2192 topic` pair. Hashing is cheap enough that a caller needing\n * the key should always call this rather than check `knownTopics` first;\n * the cache's FIFO eviction below keeps it bounded. Only `isAssigned`\n * deliberately bypasses this (it must not pollute the cache on a read-only\n * query), so if you add a new call site, prefer `rememberTopic` unless you\n * have the same \"read-only query\" reason.\n */\n private rememberTopic(topic: string): string {\n const topicKey = createOpaqueKey(topic);\n this.knownTopics.set(topicKey, topic);\n // Evict the oldest entry when the cache exceeds its cap. Plain Map iteration\n // order is insertion order, so deleting the first key is FIFO eviction (not\n // true LRU \u2014 reads do not promote recency). Hashing is cheap, so a missed\n // reverse-lookup merely recomputes the key, but the storage-less fallback\n // path (readSubscriberTabIds/readRoute) relies on this cache to recover the\n // plaintext topic. Never evict a key this worker still owns, or those reads\n // would silently return null for an assigned topic.\n if (this.knownTopics.size > MAX_KNOWN_TOPICS) {\n // Evict the oldest non-owned entry (FIFO). Scan from the front so the\n // cap holds as long as at least one tracked topic is not owned. Only\n // when every entry is owned (degenerate) do we let the cap slip \u2014 owned\n // topics must stay resolvable for the storage-less read path.\n // Scan from the front (oldest insertion) for the first non-owned entry.\n // `break` after one eviction: we only need to get back under the cap, and\n // evicting more would unnecessarily drop resolvable topics. If every\n // entry is owned (degenerate), the loop completes without evicting \u2014\n // owned topics must stay resolvable for the storage-less read path.\n for (const candidate of this.knownTopics.keys()) {\n if (candidate === topicKey || this.assignedTopics.has(candidate)) continue;\n this.knownTopics.delete(candidate);\n break;\n }\n }\n return topicKey;\n }\n\n private workerStorageKey(workerId: string): string {\n return `${this.workerPrefix}${workerId}`;\n }\n\n private routeStorageKey(topicKey: string): string {\n return `${this.routePrefix}${topicKey}`;\n }\n\n private subscriberStorageKey(topicKey: string, tabId: string): string {\n return `${this.subscriberPrefix}${topicKey}:${tabId}`;\n }\n\n private removeStorage(key: string): void {\n try {\n this.storage?.removeItem(key);\n } catch {\n // Ignore unavailable storage.\n }\n }\n\n /** Force-flush any pending batched writes (used during shutdown/teardown). */\n private flushStorage(): void {\n if (this.storage instanceof BatchingStorageWriter) this.storage.flush();\n }\n}\n", "/**\n * DataBusTraceReporter \u2014 optional diagnostics, metrics, and delivery latency.\n *\n * Aggregates message throughput and dispatch latency over configurable windows,\n * emitting structured events (lifecycle, status, subscription, coordination,\n * error) and periodic metrics summaries. The sink is decoupled from the hot\n * message path \u2014 errors in the sink are isolated to console.warn.\n */\nimport type { WorkerStatus } from './types';\n\n/** Trace reporting mode: record only events, only metrics, or both. */\n/** Selects which trace categories the reporter emits.\n * - `events` \u2014 lifecycle/status/subscription/coordination/error events only.\n * - `metrics` \u2014 periodic `message_metrics` snapshots only.\n * - `all` \u2014 both event streams and metrics snapshots. */\nexport type DataBusTraceMode = 'events' | 'metrics' | 'all';\n\n/** Emitted when the DataBus starts, stops, suspends, or resumes. */\nexport interface DataBusLifecycleTraceEvent {\n type: 'lifecycle';\n action: 'start' | 'stop' | 'suspend' | 'resume';\n timestamp: number;\n}\n\n/** Emitted when the transport connection status changes. */\nexport interface DataBusStatusTraceEvent {\n type: 'status';\n status: WorkerStatus;\n timestamp: number;\n}\n\n/** Emitted when a topic subscription is added or removed. */\nexport interface DataBusSubscriptionTraceEvent {\n type: 'subscription';\n action: 'subscribe' | 'unsubscribe';\n topic: string;\n activeTopics: number;\n timestamp: number;\n}\n\n/** Emitted on each reconciliation round to record whether the cluster coordinated. */\nexport interface DataBusCoordinationTraceEvent {\n type: 'coordination';\n coordinated: boolean;\n activeWorkers: number;\n workers: string[];\n routes: string[];\n timestamp: number;\n}\n\n/** Emitted when a transport or operation error occurs. */\nexport interface DataBusErrorTraceEvent {\n type: 'error';\n source: 'transport' | 'operation';\n timestamp: number;\n}\n\n/**\n * Periodic metrics snapshot: message throughput, dispatch latency percentiles,\n * and active topic count. Aggregated over the interval and emitted every\n * `metricsIntervalMs`.\n */\nexport interface DataBusMetricsTraceEvent {\n type: 'message_metrics';\n durationMs: number;\n received: number;\n dispatched: number;\n topics: number;\n dispatchSamples: number;\n dispatchAvgMs: number;\n dispatchP50Ms: number;\n dispatchP95Ms: number;\n dispatchMaxMs: number;\n timestamp: number;\n}\n\nexport type DataBusTraceEvent =\n | DataBusLifecycleTraceEvent\n | DataBusStatusTraceEvent\n | DataBusSubscriptionTraceEvent\n | DataBusCoordinationTraceEvent\n | DataBusErrorTraceEvent\n | DataBusMetricsTraceEvent;\n\n/** Distributive-conditional type: given a trace event union, derive the same shape minus `timestamp`. */\ntype DataBusTraceEventInput = DataBusTraceEvent extends infer TEvent\n ? TEvent extends DataBusTraceEvent\n ? Omit<TEvent, 'timestamp'>\n : never\n : never;\n\n/** Configuration for {@link DataBusTraceReporter}. `sink` receives every\n * emitted event (filtered by `mode`); all other fields are optional. */\nexport interface DataBusTraceOptions {\n /** When `false`, the reporter is inert (no events emitted). Default `true`. */\n enabled?: boolean;\n /** Which event categories to emit. Default `all`. */\n mode?: DataBusTraceMode;\n /** Aggregation window for `message_metrics` events. Default 5 s. */\n metricsIntervalMs?: number;\n /** Callback invoked for each emitted trace event. */\n sink: (event: DataBusTraceEvent) => void;\n}\n\n// Default bounds for the metrics aggregation window.\n/** Default metrics aggregation window: 5 s between snapshots. */\nconst DEFAULT_METRICS_INTERVAL_MS = 5_000;\nconst MAX_PENDING_TOPICS = 1_000;\nconst MAX_PENDING_MESSAGES_PER_TOPIC = 256;\n// Latency histogram: 20 buckets, each 50ms wide \u2192 covers 0\u20131000ms.\nconst LATENCY_BUCKET_COUNT = 20;\nconst LATENCY_BUCKET_SIZE_MS = 50;\n\n/**\n * Aggregates DataBus diagnostics \u2014 lifecycle events, status changes, and\n * periodic latency histograms \u2014 and forwards them to a user-supplied sink.\n *\n * Latency is measured in a bucketed histogram (20 buckets \u00D7 50ms) rather than\n * storing every sample, keeping memory bounded even under high throughput.\n */\nexport class DataBusTraceReporter {\n private readonly enabled: boolean;\n private readonly mode: DataBusTraceMode;\n private readonly metricsIntervalMs: number;\n private readonly sink: (event: DataBusTraceEvent) => void;\n private readonly now: () => number;\n private intervalHandle: ReturnType<typeof setInterval> | null = null;\n private intervalStartedAt = 0;\n private received = 0;\n private dispatched = 0;\n private latencySamples = 0;\n private readonly topics = new Set<string>();\n // Per-topic FIFO of received timestamps, used to compute dispatch latency.\n private readonly receivedAt = new Map<string, number[]>();\n // Bucketed histogram: bucket index = floor(delayMs / 50), capped at 19.\n private readonly latencyBuckets = new Array<number>(LATENCY_BUCKET_COUNT).fill(0);\n private latencySumMs = 0;\n\n constructor(options?: DataBusTraceOptions, now: () => number = Date.now) {\n this.enabled = options?.enabled ?? false;\n this.mode = options?.mode ?? 'all';\n this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);\n this.sink = options?.sink ?? (() => undefined);\n this.now = now;\n }\n\n /** Start the periodic metrics flush interval. No-op when mode is 'events'\n * (no metrics to emit), when disabled, or when already running. */\n start(): void {\n if (!this.enabled || this.intervalHandle || this.mode === 'events') return;\n this.intervalStartedAt = this.now();\n this.intervalHandle = setInterval(() => this.flush(), this.metricsIntervalMs);\n }\n\n /** Pause the metrics interval and reset accumulated counters. */\n pause(): void {\n if (this.intervalHandle) clearInterval(this.intervalHandle);\n this.intervalHandle = null;\n this.intervalStartedAt = 0;\n this.resetMetrics();\n }\n\n stop(): void {\n this.pause();\n }\n\n /** Record an instantaneous trace event (lifecycle, status, error, etc.). */\n event(event: DataBusTraceEventInput): void {\n if (!this.enabled || this.mode === 'metrics') return;\n this.emit({ ...event, timestamp: this.now() } as DataBusTraceEvent);\n }\n\n /** Record that a message was received on `topic`; stores its timestamp for latency tracking. */\n recordReceived(topic: string): void {\n if (!this.metricsActive) return;\n this.received += 1;\n this.topics.add(topic);\n const queue = this.receivedAt.get(topic);\n // First receive for this topic creates a new FIFO queue (subject to the\n // topic cap); subsequent receives append to the existing queue (subject to\n // the per-topic cap). Both caps prevent a single misbehaving topic from\n // exhausting memory.\n if (!queue) {\n if (this.receivedAt.size >= MAX_PENDING_TOPICS) return;\n this.receivedAt.set(topic, [this.now()]);\n return;\n }\n if (queue.length >= MAX_PENDING_MESSAGES_PER_TOPIC) return;\n queue.push(this.now());\n }\n\n /**\n * Record that a received message will never be dispatched locally. Pops the\n * matching FIFO slot so a later dispatch on the same topic does not pair\n * with a stale receive timestamp.\n */\n recordDiscarded(topic: string): void {\n if (!this.metricsActive) return;\n const queue = this.receivedAt.get(topic);\n if (!queue) return;\n queue.shift();\n if (queue.length === 0) this.receivedAt.delete(topic);\n }\n\n /**\n * Record that a message was dispatched on `topic`. Pops the oldest receive\n * timestamp (FIFO) and increments the latency histogram. Dispatches without\n * a matching receive (e.g. broadcast fan-out from another tab) still count\n * as dispatched but do not produce a latency sample.\n */\n recordDispatched(topic: string): void {\n if (!this.metricsActive) return;\n this.dispatched += 1;\n this.topics.add(topic);\n const queue = this.receivedAt.get(topic);\n const receivedTimestamp = queue?.shift();\n if (queue && queue.length === 0) this.receivedAt.delete(topic);\n if (receivedTimestamp === undefined) return;\n this.latencySamples += 1;\n const delayMs = Math.max(0, this.now() - receivedTimestamp);\n // Map the raw delay to a 50ms-wide bucket, capped at the last bucket.\n const bucketIndex = Math.min(LATENCY_BUCKET_COUNT - 1, Math.floor(delayMs / LATENCY_BUCKET_SIZE_MS));\n this.latencyBuckets[bucketIndex] = (this.latencyBuckets[bucketIndex] ?? 0) + 1;\n this.latencySumMs += delayMs;\n }\n\n /** True when metrics recording is active: enabled and mode includes metrics.\n * Extracted so the four record / flush methods share one guard expression\n * instead of repeating `!this.enabled || this.mode === 'events'` at each. */\n private get metricsActive(): boolean {\n return this.enabled && this.mode !== 'events';\n }\n\n /** Emit the accumulated metrics snapshot if the interval is active. */\n flush(): void {\n if (!this.metricsActive) return;\n this.flushNow();\n }\n\n private flushNow(): void {\n const timestamp = this.now();\n // Only emit when there was activity in this window \u2014 an all-zero metrics\n // snapshot adds noise without information. The interval still advances\n // intervalStartedAt so the next window's duration is measured correctly.\n if (this.received > 0 || this.dispatched > 0) {\n const samples = this.latencySamples;\n this.emit({\n type: 'message_metrics',\n durationMs: Math.max(0, timestamp - this.intervalStartedAt),\n received: this.received,\n dispatched: this.dispatched,\n topics: this.topics.size,\n dispatchSamples: samples,\n dispatchAvgMs: roundMs(samples === 0 ? 0 : this.latencySumMs / samples),\n // Percentiles are derived from the histogram, not sorted samples.\n dispatchP50Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.5)),\n dispatchP95Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.95)),\n dispatchMaxMs: roundMs(percentileMs(this.latencyBuckets, samples, 1)),\n timestamp\n });\n this.resetMetrics();\n }\n this.intervalStartedAt = timestamp;\n }\n\n private resetMetrics(): void {\n this.received = 0;\n this.dispatched = 0;\n this.latencySamples = 0;\n this.topics.clear();\n this.receivedAt.clear();\n this.latencyBuckets.fill(0);\n this.latencySumMs = 0;\n }\n\n private emit(event: DataBusTraceEvent): void {\n try {\n this.sink(event);\n } catch (error) {\n // Diagnostics must never affect data delivery, but surface a broken sink\n // so instrumentation bugs are not silently hidden.\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn('[cross-tab-worker-databus] trace sink threw:', error);\n }\n }\n }\n}\n\n/** Validate the metrics interval, falling back to the default when omitted.\n * @throws {RangeError} when `value` is not a positive finite number. */\nfunction normalizeInterval(value: number | undefined): number {\n if (value === undefined) return DEFAULT_METRICS_INTERVAL_MS;\n if (!Number.isFinite(value) || value <= 0) {\n throw new RangeError('trace.metricsIntervalMs must be a positive finite number.');\n }\n return value;\n}\n\n/**\n * Approximate a percentile from the bucketed histogram. Walks buckets in\n * order, accumulating counts until the cumulative total reaches the rank\n * (`percentile * sampleCount`), and returns the bucket's midpoint as the\n * estimate. Returns the histogram ceiling when the rank exceeds all counts.\n */\nfunction percentileMs(buckets: readonly number[], sampleCount: number, percentile: number): number {\n if (sampleCount <= 0) return 0;\n const rank = Math.max(1, Math.ceil(percentile * sampleCount));\n let seen = 0;\n for (let index = 0; index < buckets.length; index += 1) {\n seen += buckets[index] ?? 0;\n if (seen >= rank) return (index + 0.5) * LATENCY_BUCKET_SIZE_MS;\n }\n return buckets.length * LATENCY_BUCKET_SIZE_MS;\n}\n\n/** Round to one decimal place for stable, readable metrics output.\n * Avoids floating-point noise like 12.300000000001 in the trace sink. */\nfunction roundMs(value: number): number {\n return Math.round(value * 10) / 10;\n}\n", "/**\n * CrossTabDataBus \u2014 the primary public API for cross-tab data distribution.\n *\n * Wraps WorkerClusterRuntime for cluster coordination and a DataBusTransport\n * for the real connection. Handles local handler reference counting, subscription\n * queuing, message dispatch, and clean lifecycle management (start/stop/BFCache).\n */\nimport { WorkerClusterRuntime } from './cluster';\nimport type { WorkerClusterOptions } from './cluster';\nimport { topicMatchesPattern } from './routing';\nimport type {\n DataBusErrorHandler,\n DataBusMessage,\n DataBusMessageHandler,\n DataBusStatusHandler,\n DataBusTransport,\n WorkerStatus\n} from './types';\nimport { DataBusTraceReporter } from './trace';\nimport type { DataBusTraceOptions } from './trace';\n\n/**\n * Event type used to broadcast topic publications across tabs via the cluster.\n * The cluster's `onEvent` handler filters on this to distinguish databus\n * publications from other control-plane events.\n */\n/** Event type used to broadcast topic publications across tabs via the cluster.\n * The cluster's `onEvent` handler filters on this to distinguish databus\n * publications from other control-plane events. */\nconst PUBLICATION_EVENT = 'DATABUS_PUBLICATION';\n\n/** Constructor options for {@link CrossTabDataBus}. Extends WorkerClusterOptions\n * (cluster coordination config) with the transport, initial connection config,\n * and trace options. */\nexport interface CrossTabDataBusOptions<TConfig, TData>\n extends Omit<WorkerClusterOptions, 'handlers'> {\n transport: DataBusTransport<TConfig, TData>;\n initialConfig?: TConfig;\n autoStart?: boolean;\n trace?: DataBusTraceOptions;\n}\n\n/**\n * High-level cross-tab pub/sub client.\n *\n * Orchestrates a transport (e.g. Centrifuge WebSocket inside a Worker) and a\n * WorkerClusterRuntime for cross-tab coordination. Messages arriving from the\n * transport are fanned out to all tabs in the cluster and dispatched locally\n * to registered handlers.\n */\nexport class CrossTabDataBus<TConfig = unknown, TData = unknown> {\n private readonly transport: DataBusTransport<TConfig, TData>;\n private readonly cluster: WorkerClusterRuntime;\n // Map of topic \u2192 set of local subscribers.\n private readonly topicHandlers = new Map<string, Set<DataBusMessageHandler<TData>>>();\n // Topics for which the transport has been asked to subscribe (used to avoid\n // duplicate subscribe calls during reconnection).\n private readonly transportSubscribedTopics = new Set<string>();\n private readonly statusHandlers = new Set<DataBusStatusHandler>();\n private readonly errorHandlers = new Set<DataBusErrorHandler>();\n private readonly initialConfig: TConfig | undefined;\n private readonly hasInitialConfig: boolean;\n private readonly trace: DataBusTraceReporter;\n private activeConfig: TConfig | undefined;\n private status: WorkerStatus = 'disconnected';\n private started = false;\n private stopping = false;\n private transportReady = false;\n // Last transport failure, retained so ready() can surface it to callers who\n // never awaited start() directly. Cleared on the next successful start.\n private lastError: unknown = null;\n // Gate that serialises start/stop/suspend/resume \u2014 only one lifecycle\n // transition at a time. Resets to null once the operation settles.\n private startPromise: Promise<void> | null = null;\n // Timestamp of the last automatic transport recovery attempt.\n // Used to avoid a tight retry loop when the transport fails repeatedly.\n private lastRecoveryAt = 0;\n // True while the tab is hidden so an in-flight transport start does not mark\n // the transport ready after suspendTransport() has stopped it.\n private suspended = false;\n // Single gate for async transport.stop() cleanup, shared by failed opens and\n // page-hide suspension. Kept separate from startPromise so ready() still\n // surfaces a failure while later opens and automatic recovery wait for the\n // stop to settle.\n private pendingStop: Promise<void> | null = null;\n // Minimum interval in ms between automatic recovery attempts.\n private static readonly RECOVERY_COOLDOWN_MS = 1000;\n\n constructor(options: CrossTabDataBusOptions<TConfig, TData>) {\n const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;\n this.transport = transport;\n this.initialConfig = initialConfig;\n this.hasInitialConfig = 'initialConfig' in options;\n this.trace = new DataBusTraceReporter(trace);\n this.cluster = new WorkerClusterRuntime({\n ...clusterOptions,\n handlers: {\n // The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH\n // control message \u2014 meaning the owning Worker has delegated the action to us.\n onControl: (action, topic, data) => {\n switch (action) {\n case 'SUBSCRIBE':\n if (this.subscribeTransport(topic)) this.traceSubscription('subscribe', topic);\n break;\n case 'UNSUBSCRIBE':\n if (this.unsubscribeTransport(topic)) this.traceSubscription('unsubscribe', topic);\n break;\n case 'PUBLISH':\n this.runTransport(() => this.transport.publish(topic, data));\n break;\n default:\n break;\n }\n },\n // The cluster calls `onEvent` when a publication broadcast arrives from\n // another tab. Dispatch locally if we have subscribers. The payload is\n // typed `unknown` at the cluster boundary (the cluster is transport-\n // agnostic); here we narrow it to DataBusMessage \u2014 the sender is our\n // own broadcastEvent call, which always posts a DataBusMessage.\n onEvent: (eventType, payload) => {\n if (eventType !== PUBLICATION_EVENT) return;\n const message = payload as DataBusMessage<TData>;\n if (this.cluster.hasLocalSubscriber(message.topic)) this.dispatch(message);\n },\n onSuspend: () => {\n // Suppress the suspend trace event during an explicit stop() so\n // the trace log ends on 'stop' rather than 'suspend'\u2192'stop'.\n if (!this.stopping) this.trace.event({ type: 'lifecycle', action: 'suspend' });\n this.trace.pause();\n this.suspendTransport();\n },\n onResume: () => {\n this.trace.event({ type: 'lifecycle', action: 'resume' });\n this.trace.start();\n this.resumeTransport();\n }\n }\n });\n // Auto-start when initialConfig is provided, or when autoStart is explicitly true.\n if (autoStart ?? this.hasInitialConfig) this.ensureStarted();\n }\n\n /**\n * Start the DataBus with the given transport config.\n *\n * The first call starts the cluster and opens the transport. Concurrent calls\n * during an in-flight start return the same promise. Once the operation\n * settles (success or failure) the promise gate is cleared so a subsequent\n * start() or resumeTransport() can open a fresh lifecycle.\n */\n start(config: TConfig): Promise<void> {\n if (this.startPromise) return this.startPromise;\n if (this.started) return Promise.resolve();\n this.started = true;\n this.stopping = false;\n this.suspended = false;\n this.activeConfig = config;\n this.lastError = null;\n this.trace.event({ type: 'lifecycle', action: 'start' });\n this.trace.start();\n this.updateStatus('connecting');\n this.cluster.start();\n // Establish the opening before replaying topicHandlers: cluster.subscribe()\n // can synchronously invoke onControl for self-owned topics, and those\n // callbacks would otherwise see startPromise=null and open a second transport.\n const opening = this.openTransport(config, this.pendingStop ?? Promise.resolve(), true);\n this.startPromise = opening;\n // Replay subscriptions that were registered before start() or that were lost\n // during a previous failure recovery. The cluster.stop() call in the failure\n // path clears subscribedTopics, but topicHandlers retains the intent.\n // Iterating topicHandlers (not transportSubscribedTopics) because the\n // transport hasn't subscribed to anything yet on a fresh start.\n for (const topic of this.topicHandlers.keys()) {\n this.cluster.subscribe(topic);\n }\n const snapshot = this.cluster.getSnapshot();\n this.trace.event({\n type: 'coordination',\n coordinated: snapshot.coordinated,\n activeWorkers: snapshot.workers.filter(worker => worker.role === 'active').length,\n workers: snapshot.workers.map(formatWorkerTrace),\n routes: snapshot.routes.map(formatRouteTrace)\n });\n // Once startup settles (success or failure), clear the pending gate so a\n // later start()/resumeTransport() can open a fresh operation. Guard against\n // clobbering a promise that suspend/resume may have already swapped in.\n void opening.then(\n () => {\n if (this.startPromise === opening) this.startPromise = null;\n },\n () => {\n if (this.startPromise === opening) this.startPromise = null;\n }\n );\n return opening;\n }\n\n /**\n * Open the transport, chained after `before` to ensure lifecycle ordering.\n * When `stopClusterOnFailure` is true (initial start), a transport failure\n * tears down the cluster as well.\n */\n private openTransport(\n config: TConfig,\n before: Promise<unknown>,\n stopClusterOnFailure: boolean\n ): Promise<void> {\n this.transportReady = false;\n const chainedPendingStop = this.pendingStop;\n return before\n .catch(() => undefined)\n .then(() => {\n // stop() or suspendTransport() may have arrived while this opening was\n // queued behind a pending stop. Abandon the open and keep the settled\n // stop gate visible so stop() does not issue a second transport.stop().\n if (this.stopping || this.suspended) return;\n // A stop we actually chained after has settled; this opening now owns\n // the lifecycle. A stop created concurrently (e.g. by suspendTransport)\n // is a different promise and must stay visible to later catch/resume\n // paths so cleanup is not duplicated.\n if (this.pendingStop === chainedPendingStop) this.pendingStop = null;\n return Promise.resolve(\n this.transport.start(config, {\n onMessage: message => this.handleTransportMessage(message),\n onStatus: status => this.updateStatus(status),\n onError: error => this.reportError(error)\n })\n ).then(() => {\n // A transport may report 'error' synchronously during start() (e.g. a\n // Worker that fails to boot) while still returning normally. Treat that\n // as a startup failure instead of marking the transport ready, so a\n // later subscribe/unsubscribe triggers a reopen rather than being\n // silently dropped on a dead transport.\n if (this.status === 'error') {\n throw new Error('Transport failed during startup.');\n }\n if (!this.suspended && !this.stopping) this.transportReady = true;\n });\n })\n .catch(error => {\n // Reset started before reporting so an initial-start failure does not\n // schedule automatic recovery; only the caller can retry a first start.\n if (stopClusterOnFailure) this.started = false;\n // Keep the stop cleanup in a single gate so a subsequent\n // start()/reopenTransport() cannot overlap an asynchronous\n // transport.stop(). If suspendTransport() already chained a stop for\n // the tab hiding mid-open, reuse it instead of stopping twice.\n if (!this.pendingStop) {\n this.pendingStop = this.createStopPromise();\n }\n this.updateStatus('error');\n this.reportError(error);\n this.lastError = error;\n this.transportReady = false;\n if (stopClusterOnFailure) {\n this.stopping = true;\n this.cluster.stop();\n this.stopping = false;\n }\n this.startPromise = null;\n throw error;\n });\n }\n\n /**\n * Await the DataBus to be fully started (lazy init when using initialConfig).\n * Returns a rejected promise when the transport has failed and no start is in\n * flight \u2014 the caller can retry by calling start() or ready() again.\n */\n ready(): Promise<void> {\n try {\n this.ensureStarted();\n } catch (error) {\n return Promise.reject(error);\n }\n if (this.startPromise) return this.startPromise;\n if (this.transportReady) return Promise.resolve();\n // Surface the last failure so callers can distinguish a transient retry\n // from a dead transport. The promise is rejected, not thrown, so the\n // caller can retry by calling ready() or start() again.\n if (this.lastError !== null) return Promise.reject(this.lastError);\n return Promise.reject(\n new Error('Transport is not ready and no start operation is in flight')\n );\n }\n\n /**\n * Register a handler for `topic`. The handler fires on every publication\n * delivered to this tab, regardless of which tab published it. Returns an\n * unsubscribe function for convenience.\n */\n subscribe(topic: string, handler: DataBusMessageHandler<TData>): () => void {\n this.ensureStarted();\n const handlers = this.topicHandlers.get(topic) ?? new Set<DataBusMessageHandler<TData>>();\n const wasUnused = handlers.size === 0;\n handlers.add(handler);\n this.topicHandlers.set(topic, handlers);\n // This 0\u21921 transition is the SINGLE entry point into the cluster\n // subscription. The cluster's subscribedTopics is a Set, so repeated\n // installs of the same topic after a drop cannot double-subscribe:\n // wasUnused is the only gate, and cluster.subscribe() is idempotent by\n // construction. The matching n\u21920 gate is in the unsubscribe path below.\n if (wasUnused) this.cluster.subscribe(topic);\n return () => this.unsubscribe(topic, handler);\n }\n\n /** Remove a specific handler, or all handlers for `topic`.\n * When `handler` is omitted, clears every handler for the topic \u2014 the\n * caller used the `unsubscribe(topic)` form expecting a full teardown.\n * The cluster is only notified on the n\u21920 transition (handlers.size === 0). */\n unsubscribe(topic: string, handler?: DataBusMessageHandler<TData>): void {\n const handlers = this.topicHandlers.get(topic);\n if (!handlers) return;\n if (handler) handlers.delete(handler);\n else handlers.clear();\n if (handlers.size > 0) return;\n this.topicHandlers.delete(topic);\n this.cluster.unsubscribe(topic);\n }\n\n /** Publish a message to `topic`. The owning Worker delivers it to the transport. */\n publish(topic: string, data: unknown): void {\n this.ensureStarted();\n if (!this.cluster.publish(topic, data)) {\n this.reportError(\n new Error('Failed to send the publish control message to the owning worker.')\n );\n }\n }\n\n /** Register a handler that fires on every transport status change. Immediately invoked with the current status. */\n onStatus(handler: DataBusStatusHandler): () => void {\n this.statusHandlers.add(handler);\n try {\n handler(this.status);\n } catch (error) {\n this.reportError(error);\n }\n return () => this.statusHandlers.delete(handler);\n }\n\n /** Register a handler for transport errors. */\n onError(handler: DataBusErrorHandler): () => void {\n this.errorHandlers.add(handler);\n return () => this.errorHandlers.delete(handler);\n }\n\n /** Current transport connection status. */\n getStatus(): WorkerStatus {\n return this.status;\n }\n\n /** Snapshot of the cluster state (workers, routes, assignments).\n * For diagnostics only \u2014 the returned object is a shallow copy but\n * nested arrays are snapshots at call time. */\n getClusterSnapshot() {\n return this.cluster.getSnapshot();\n }\n\n /**\n * Gracefully stop the DataBus: unsubscribe all topics, stop the cluster,\n * and close the transport. Idempotent.\n */\n async stop(): Promise<void> {\n if (!this.started) return;\n this.stopping = true;\n this.trace.event({ type: 'lifecycle', action: 'stop' });\n this.trace.stop();\n this.topicHandlers.clear();\n this.cluster.stop();\n try {\n await this.startPromise?.catch(() => undefined);\n // A failed-open or suspend cleanup already stopped the transport;\n // awaiting it is enough, so stop() is not called a second time.\n const pendingStop = this.pendingStop;\n if (pendingStop) await pendingStop.catch(() => undefined);\n else await this.transport.stop();\n } finally {\n this.transportSubscribedTopics.clear();\n this.started = false;\n this.stopping = false;\n this.suspended = false;\n this.transportReady = false;\n this.startPromise = null;\n this.pendingStop = null;\n this.lastError = null;\n this.activeConfig = undefined;\n this.updateStatus('disconnected');\n }\n }\n\n /**\n * Incoming message from the transport.\n * Records metrics, checks ownership via the cluster, broadcasts to other tabs,\n * and dispatches locally.\n */\n private handleTransportMessage(message: DataBusMessage<TData>): void {\n this.trace.recordReceived(message.topic);\n // Drop messages for topics we do not own \u2014 the owning Worker fans out.\n if (!this.cluster.isAssigned(message.topic)) {\n this.trace.recordDiscarded(message.topic);\n return;\n }\n this.cluster.broadcastEvent(PUBLICATION_EVENT, message);\n if (this.cluster.hasLocalSubscriber(message.topic)) {\n this.dispatch(message);\n return;\n }\n this.trace.recordDiscarded(message.topic);\n }\n\n /** Deliver a message to every local handler registered for its topic,\n * plus every handler registered with a wildcard subscription that matches\n * (e.g. a handler subscribed to \"chat.*\" receives \"chat.room.1\"). */\n private dispatch(message: DataBusMessage<TData>): void {\n this.trace.recordDispatched(message.topic);\n this.invokeHandlers(this.topicHandlers.get(message.topic) ?? [], handler => handler(message));\n for (const [pattern, handlers] of this.topicHandlers) {\n if (pattern !== message.topic && topicMatchesPattern(pattern, message.topic)) {\n this.invokeHandlers(handlers, handler => handler(message));\n }\n }\n }\n\n /**\n * Propagate a status change to the cluster, trace, and all registered\n * status handlers. On reconnect, re-subscribe any topics assigned to us.\n */\n private updateStatus(status: WorkerStatus): void {\n const previousStatus = this.status;\n this.status = status;\n if (previousStatus !== status) this.trace.event({ type: 'status', status });\n this.cluster.setStatus(status);\n // Clear transport subscriptions on disconnect; the transport is gone.\n if (status === 'disconnected' || status === 'error') this.transportSubscribedTopics.clear();\n // Re-subscribe assigned topics when the transport reconnects.\n if (status === 'connected' && previousStatus !== 'connected') {\n for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);\n }\n // Auto-recover from a runtime transport failure (e.g. a crashed Worker)\n // while the bus is still meant to be started. Guarded by a cooldown to\n // avoid a tight retry loop when the transport fails immediately.\n // Uses setTimeout so the recovery does not run re-entrantly inside the\n // callback that produced this status (e.g. openTransport's catch).\n if (status === 'error' && this.started && !this.stopping) {\n const now = Date.now();\n if (now - this.lastRecoveryAt >= CrossTabDataBus.RECOVERY_COOLDOWN_MS) {\n this.lastRecoveryAt = now;\n setTimeout(() => {\n if (this.stopping || !this.started || this.suspended) return;\n // An explicit resume or subscribe already recovered the transport\n // (or is in flight), so this stale timer must not open it again.\n if (this.status !== 'error') return;\n void this.reopenTransport();\n }, CrossTabDataBus.RECOVERY_COOLDOWN_MS);\n }\n }\n this.invokeHandlers(this.statusHandlers, handler => handler(status));\n }\n\n private reportError(error: unknown): void {\n this.trace.event({ type: 'error', source: 'transport' });\n this.invokeHandlers(this.errorHandlers, handler => handler(error), 'error handler');\n }\n\n private traceSubscription(action: 'subscribe' | 'unsubscribe', topic: string): void {\n this.trace.event({\n type: 'subscription',\n action,\n topic,\n activeTopics: this.transportSubscribedTopics.size\n });\n }\n\n /** Ask the transport to subscribe to a topic (idempotent). */\n private subscribeTransport(topic: string): boolean {\n if (this.transportSubscribedTopics.has(topic)) return false;\n this.transportSubscribedTopics.add(topic);\n this.runTransport(() => this.transport.subscribe(topic));\n return true;\n }\n\n private unsubscribeTransport(topic: string): boolean {\n if (!this.transportSubscribedTopics.delete(topic)) return false;\n this.runTransport(() => this.transport.unsubscribe(topic));\n return true;\n }\n\n /** Invoke `callback` for each item in `handlers`, isolating a throwing\n * callback so the remaining ones still run. Dispatch/status handler failures\n * are routed to `reportError` (which surfaces them to error subscribers);\n * error-handler failures are logged to the console to avoid infinite\n * recursion through reportError itself. */\n private invokeHandlers<T>(\n handlers: Iterable<T>,\n callback: (handler: T) => void,\n label: 'dispatch' | 'status' | 'error handler' = 'dispatch'\n ): void {\n for (const handler of handlers) {\n try {\n callback(handler);\n } catch (error) {\n if (label === 'error handler') {\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn('[cross-tab-worker-databus] error handler threw:', error);\n }\n } else {\n this.reportError(error);\n }\n }\n }\n }\n\n /**\n * Suspend the transport when the tab goes hidden. Stops the transport and\n * clears subscription state so it will be re-established on resume.\n */\n private suspendTransport(): void {\n if (this.stopping) return;\n this.suspended = true;\n this.transportReady = false;\n this.transportSubscribedTopics.clear();\n this.updateStatus('disconnected');\n // A failed open already owns a stop cleanup; reuse it so the suspend does\n // not stop an already-stopped transport. Resume/reopen chain after the\n // same pendingStop gate. Without this guard, suspendTransport would issue\n // a second transport.stop() that races with the failed-open cleanup.\n if (this.pendingStop) return;\n // Chain the stop after any in-flight start so an async open settles first.\n // startPromise and pendingStop MUST be the same promise so reopenTransport's\n // `startPromise !== pendingStop` check can distinguish a suspend-stop gate\n // from a resume opening \u2014 do not wrap one without wrapping the other.\n const pending = this.startPromise ?? Promise.resolve();\n const stopping = pending\n .catch(() => undefined)\n .then(() => this.transport.stop())\n .catch(error => this.reportError(error));\n this.startPromise = stopping;\n this.pendingStop = stopping;\n }\n\n /** Create an immediate stop promise (no prior chain). Used by openTransport's\n * failure path where there is no in-flight start to wait for. */\n private createStopPromise(): Promise<void> {\n return Promise.resolve()\n .then(() => this.transport.stop())\n .catch(stopError => this.reportError(stopError));\n }\n\n /**\n * Resume the transport when the tab becomes visible again, or recover from a\n * runtime transport failure. Re-opens the transport with the stored active\n * config, chained after any pending operation so an async transport stop\n * completes before the new start. Returns the opening promise.\n */\n private resumeTransport(): void {\n void this.reopenTransport();\n }\n\n /**\n * Re-open the transport with the previously stored active config. Chains\n * after any in-flight lifecycle operation (e.g. a suspend stop), swallowing\n * its rejection so the reopen is not blocked. Returns the opening promise so\n * callers can queue operations behind it.\n */\n private reopenTransport(): Promise<void> {\n if (this.stopping || this.activeConfig === undefined) return Promise.resolve();\n // A resume/recovery already has an opening in flight. Reuse it so a stale\n // recovery timer or a second caller cannot open a second transport. A\n // page-hide stop gate (startPromise === pendingStop) must not be reused\n // as an opening \u2014 that would make resume return a promise that resolves\n // on stop completion, not on a ready transport. Instead, fall through and\n // chain the new open after that pending stop.\n if (this.startPromise && this.startPromise !== this.pendingStop) return this.startPromise;\n const config = this.activeConfig;\n // A resume/recovery means the bus is meant to keep running, even after an\n // initial start failed and then recovered while hidden. Without this, a\n // later stop() would be a no-op and leave the reopened transport running.\n this.started = true;\n this.suspended = false;\n this.updateStatus('connecting');\n const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();\n const opening = pending\n .catch(() => undefined)\n .then(() => this.openTransport(config, Promise.resolve(), false));\n this.startPromise = opening;\n // Reset the gate on success too, so a later runtime failure can schedule a\n // fresh reopen instead of reusing this settled promise.\n void opening.then(\n () => {\n if (this.startPromise === opening) this.startPromise = null;\n },\n () => undefined\n );\n void opening.catch(() => undefined);\n return opening;\n }\n\n /**\n * Run a transport operation now if the transport is ready, otherwise queue\n * it behind the start promise. This ensures subscribe/unsubscribe calls made\n * during startup are not lost.\n */\n private runTransport(operation: () => void | Promise<void>): void {\n // A hidden tab's transport is intentionally stopped; subscriptions are\n // re-established by the cluster on resume, and publications must not be\n // sent to a stopped transport.\n if (this.suspended) return;\n if (this.transportReady && !this.stopping) {\n try {\n void Promise.resolve(operation()).catch(error => this.reportError(error));\n } catch (error) {\n this.reportError(error);\n }\n return;\n }\n // Transport is down but we are still meant to be started \u2014 reopen so the\n // operation is not silently dropped. This covers the case where a resume\n // or recovery attempt failed, leaving transportReady=false, startPromise=null.\n let ready = this.startPromise;\n if (!ready && this.started && !this.stopping && this.activeConfig !== undefined) {\n ready = this.reopenTransport();\n }\n if (!ready || this.stopping) return;\n void ready\n .then(() => {\n if (!this.started || this.stopping || this.suspended) return;\n return operation();\n })\n .catch(error => this.reportError(error));\n }\n\n /**\n * Ensure the DataBus is started, throwing if no initialConfig was provided.\n * Called automatically by subscribe/publish/ready when autoStart is true.\n */\n private ensureStarted(): void {\n if (this.started) return;\n if (!this.hasInitialConfig) {\n throw new Error(\n 'CrossTabDataBus requires initialConfig for automatic startup, or an explicit start(config) call.'\n );\n }\n const starting = this.start(this.initialConfig as TConfig);\n void starting.catch(() => undefined);\n }\n}\n\n/** Format a WorkerRecord for the coordination trace event. */\nfunction formatWorkerTrace(worker: { workerId: string; status: string; load: number; tabId: string }): string {\n return `${worker.workerId}|${worker.status}|load=${worker.load}|tab=${worker.tabId}`;\n}\n\n/** Format a route for the coordination trace event. */\nfunction formatRouteTrace(route: { topicKey: string; workerId: string; confirmedAt?: number }): string {\n return `${route.topicKey}@${route.workerId}|confirmed=${route.confirmedAt !== undefined}`;\n}\n", "/**\n * Worker-mode types and backend selection.\n *\n * Defines the WorkerMode preference flags and the selectWorkerBackend function\n * that resolves the preference against browser capability to pick the actual\n * backend (dedicated, shared, or local fallback).\n */\n\n/** Preferred Worker mode.\n * - `dedicated` \u2192 try Dedicated Worker first (one WebSocket per tab).\n * - `shared` \u2192 try SharedWorker first (one process, per-port connections).\n * - `auto` \u2192 same as `shared` (alias for forward compatibility). */\nexport type WorkerMode = 'dedicated' | 'shared' | 'auto';\n\n/** Resolved backend that was actually created. `local` means the session\n * runs on the main thread (fallback when no Worker API is available). */\nexport type WorkerBackend = 'dedicated' | 'shared' | 'local';\n\n/** Override Worker availability for testing or environments where feature\n * detection is unreliable (e.g. sandboxed iframes). When a field is omitted,\n * the global `typeof Worker` / `typeof SharedWorker` check is used. */\nexport interface WorkerAvailability {\n /** When provided, overrides `typeof Worker !== 'undefined'`. */\n worker?: boolean;\n /** When provided, overrides `typeof SharedWorker !== 'undefined'`. */\n sharedWorker?: boolean;\n}\n\n/**\n * Resolves the worker backend by feature detection, without touching globals\n * that may be missing in SSR or embedded environments.\n *\n * - `dedicated` prefers Dedicated Worker, then SharedWorker, then local mode.\n * - `shared` and `auto` prefer SharedWorker, then Dedicated Worker, then local.\n * Returns `'local'` when neither Worker API is available (main-thread fallback).\n */\nexport function selectWorkerBackend(\n mode: WorkerMode,\n availability: WorkerAvailability = {}\n): WorkerBackend {\n const hasDedicated = availability.worker ?? typeof Worker !== 'undefined';\n const hasShared = availability.sharedWorker ?? typeof SharedWorker !== 'undefined';\n if (mode === 'shared' || mode === 'auto') {\n return hasShared ? 'shared' : hasDedicated ? 'dedicated' : 'local';\n }\n return hasDedicated ? 'dedicated' : hasShared ? 'shared' : 'local';\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }