cross-tab-worker-databus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +185 -0
  4. package/README.zh.md +98 -0
  5. package/dist/centrifuge-protocol.d.ts +77 -0
  6. package/dist/centrifuge-protocol.d.ts.map +1 -0
  7. package/dist/centrifuge-session.d.ts +39 -0
  8. package/dist/centrifuge-session.d.ts.map +1 -0
  9. package/dist/centrifuge.d.ts +135 -0
  10. package/dist/centrifuge.d.ts.map +1 -0
  11. package/dist/centrifuge.js +407 -0
  12. package/dist/centrifuge.js.map +7 -0
  13. package/dist/centrifuge.shared.worker.js +5220 -0
  14. package/dist/centrifuge.shared.worker.js.map +7 -0
  15. package/dist/centrifuge.worker.js +5109 -0
  16. package/dist/centrifuge.worker.js.map +7 -0
  17. package/dist/chunk-GABYBK7I.js +1527 -0
  18. package/dist/chunk-GABYBK7I.js.map +7 -0
  19. package/dist/core/cluster.d.ts +219 -0
  20. package/dist/core/cluster.d.ts.map +1 -0
  21. package/dist/core/data-bus.d.ts +133 -0
  22. package/dist/core/data-bus.d.ts.map +1 -0
  23. package/dist/core/environment.d.ts +67 -0
  24. package/dist/core/environment.d.ts.map +1 -0
  25. package/dist/core/hash.d.ts +11 -0
  26. package/dist/core/hash.d.ts.map +1 -0
  27. package/dist/core/routing.d.ts +42 -0
  28. package/dist/core/routing.d.ts.map +1 -0
  29. package/dist/core/storage-batch.d.ts +35 -0
  30. package/dist/core/storage-batch.d.ts.map +1 -0
  31. package/dist/core/trace.d.ts +126 -0
  32. package/dist/core/trace.d.ts.map +1 -0
  33. package/dist/core/types.d.ts +112 -0
  34. package/dist/core/types.d.ts.map +1 -0
  35. package/dist/index.d.ts +23 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +27 -0
  38. package/dist/index.js.map +7 -0
  39. package/dist/worker-mode.d.ts +25 -0
  40. package/dist/worker-mode.d.ts.map +1 -0
  41. package/dist/workers/centrifuge.shared.worker.d.ts +2 -0
  42. package/dist/workers/centrifuge.shared.worker.d.ts.map +1 -0
  43. package/dist/workers/centrifuge.worker.d.ts +2 -0
  44. package/dist/workers/centrifuge.worker.d.ts.map +1 -0
  45. package/dist/workers/port-reaper.d.ts +52 -0
  46. package/dist/workers/port-reaper.d.ts.map +1 -0
  47. package/docs/README.md +21 -0
  48. package/docs/api.md +261 -0
  49. package/docs/architecture.md +514 -0
  50. package/docs/capabilities.md +41 -0
  51. package/docs/configuration.md +211 -0
  52. package/docs/getting-started.md +161 -0
  53. package/docs/zh/README.md +23 -0
  54. package/docs/zh/api.md +261 -0
  55. package/docs/zh/architecture.md +515 -0
  56. package/docs/zh/capabilities.md +41 -0
  57. package/docs/zh/configuration.md +211 -0
  58. package/docs/zh/getting-started.md +161 -0
  59. package/package.json +71 -0
@@ -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). */\n storage: StorageLike | null;\n /** sessionStorage (or null if unavailable). Used for stable tab IDs. */\n sessionStorage: StorageLike | null;\n now: () => number;\n randomId: () => string;\n createChannel: (name: string) => ClusterChannel | null;\n setInterval: (callback: () => void, intervalMs: number) => unknown;\n clearInterval: (handle: unknown) => void;\n getVisibilityState: () => TabVisibilityState;\n addVisibilityChangeListener: (listener: () => void) => void;\n removeVisibilityChangeListener: (listener: () => void) => void;\n addPageHideListener: (listener: () => void) => void;\n removePageHideListener: (listener: () => void) => void;\n addPageShowListener: (listener: () => void) => void;\n removePageShowListener: (listener: () => void) => void;\n}\n\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, or opaque exceptions.\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.\n let h1 = 0xdeadbeef ^ value.length;\n let h2 = 0x41c6ce57 ^ value.length;\n let h3 = 0xc0decafe ^ value.length;\n let h4 = 0x9e3779b9 ^ value.length;\n\n // Feed every code unit into all four lanes with distinct large primes.\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index);\n h1 = Math.imul(h1 ^ code, 2_654_435_761);\n h2 = Math.imul(h2 ^ code, 1_597_334_677);\n h3 = Math.imul(h3 ^ code, 2_246_822_519);\n h4 = Math.imul(h4 ^ code, 3_266_489_917);\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 = Math.imul(h1 ^ (h1 >>> 16), 2_246_822_507) ^ Math.imul(h2 ^ (h2 >>> 13), 3_266_489_909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2_246_822_507) ^ Math.imul(h3 ^ (h3 >>> 13), 3_266_489_909);\n h3 = Math.imul(h3 ^ (h3 >>> 16), 2_246_822_507) ^ Math.imul(h4 ^ (h4 >>> 13), 3_266_489_909);\n h4 = Math.imul(h4 ^ (h4 >>> 16), 2_246_822_507) ^ Math.imul(h1 ^ (h1 >>> 13), 3_266_489_909);\n\n return [h1, h2, h3, h4].map(hash => (hash >>> 0).toString(16).padStart(8, '0')).join('');\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. */\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.\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 return worker.workerId.localeCompare(least.workerId) < 0 ? worker : 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 || left.workerId.localeCompare(right.workerId)\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 * 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\nconst INITIAL_RETRY_DELAY_MS = 50;\nconst MAX_RETRY_DELAY_MS = 1_600;\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 */\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 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 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 // 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 for (const [key, value] of [...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 } catch {\n // A failed write (e.g. quota exceeded) retries with backoff; remaining\n // pending writes stay queued for the next attempt.\n this.scheduleRetry();\n break;\n }\n }\n if (this.pending.size === 0) this.retryDelayMs = INITIAL_RETRY_DELAY_MS;\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 [...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 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 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} from './routing';\nimport type {\n TopicSubscriberRecord,\n WorkerClusterMessage,\n WorkerControlAction,\n WorkerRecord,\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 clusterKey: string;\n handlers: WorkerClusterHandlers;\n environment?: ClusterEnvironment;\n storagePrefix?: string;\n tabId?: string;\n workerId?: string;\n maxActiveWorkers?: number;\n heartbeatIntervalMs?: number;\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. */\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 best-effort). */\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/**\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 */\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.\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 if (!this.storage) {\n for (const topic of this.subscribedTopics) {\n this.sendControl(this.workerId, 'SUBSCRIBE', topic, this.rememberTopic(topic));\n }\n } else {\n for (const topic of this.subscribedTopics) this.writeSubscriber(this.rememberTopic(topic));\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 (existingRoute && workers.some(worker => worker.workerId === existingRoute.workerId)) {\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 const topicKey = this.rememberTopic(topic);\n this.subscribedTopics.delete(topic);\n this.releaseSubscription(topic);\n // Keep the topic in knownTopics if we remain the owner (we may still fan out).\n if (!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 the route. */\n private releaseSubscription(topic: string, notifyOwner = true): void {\n const topicKey = this.rememberTopic(topic);\n this.removeStorage(this.subscriberStorageKey(topicKey, this.tabId));\n const route = this.readRoute(topicKey);\n if (!route) return;\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 }\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 if (this.readRoute(topicKey)?.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 previous = this.readRoute(topicKey);\n this.writeRoute(topicKey, owner, previous?.workerId, (previous?.generation ?? 0) + 1);\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 const generation = (previous?.generation ?? 0) + 1;\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.send({\n type: 'ROUTE_RELEASED',\n sourceWorkerId: this.workerId,\n targetWorkerId: owner.workerId,\n topic,\n topicKey,\n generation\n });\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 = route && workers.some(worker => worker.workerId === route.workerId)\n ? route.workerId\n : this.workerId;\n return this.sendControl(target ?? this.workerId, 'PUBLISH', topic, topicKey, data);\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 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 selectActiveWorkers(this.readWorkers(), this.maxActiveWorkers).some(\n worker => worker.workerId === this.workerId\n );\n }\n\n /** True when this tab has a local subscriber registered for `topic`. */\n hasLocalSubscriber(topic: string): boolean {\n return this.subscribedTopics.has(topic);\n }\n\n /** Read-only snapshot of the cluster state (workers, routes, assignments). */\n getSnapshot(): WorkerClusterSnapshot {\n const routes = listKeysSafe(this.storage, this.routePrefix)\n .map(key => (this.storage ? readJson<WorkerRoute>(this.storage, key) : null))\n .filter((route): route is WorkerRoute => Boolean(route))\n .map(route => ({\n ...route,\n topic: this.knownTopics.get(route.topicKey) ?? null\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: [...this.subscribedTopics],\n assignedTopics: [...this.assignedTopics.values()],\n knownTopics: [...this.knownTopics.entries()].map(([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 if (message.type === 'CONTROL') return this.handleControlMessage(message);\n if (message.type === 'ROUTE_RELEASED') return this.handleRouteReleasedMessage(message);\n if (message.type === 'EVENT') {\n this.handlers.onEvent(message.eventType, message.payload, message.sourceWorkerId);\n return;\n }\n this.reconcile();\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 if (message.action === 'SUBSCRIBE') {\n this.assignedTopics.set(message.topicKey, message.topic);\n this.confirmRoute(message.topicKey);\n }\n if (message.action === 'UNSUBSCRIBE') {\n if (this.releaseHandoffOnUnsubscribe(message)) return;\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.send({\n type: 'ROUTE_RELEASED',\n sourceWorkerId: this.workerId,\n targetWorkerId: route.workerId,\n topic: message.topic,\n topicKey: message.topicKey,\n generation: route.generation\n });\n this.updateLoad();\n return true;\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 (\n !route ||\n route.workerId !== this.workerId ||\n route.handoffFromWorkerId !== message.sourceWorkerId ||\n route.generation < message.generation\n ) 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 /** 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 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 if (this.readRoute(topicKey)?.workerId === this.workerId) continue;\n this.assignedTopics.delete(topicKey);\n this.handlers.onControl('UNSUBSCRIBE', topic, undefined);\n const route = this.readRoute(topicKey);\n if (route?.handoffFromWorkerId === this.workerId) {\n this.send({\n type: 'ROUTE_RELEASED',\n sourceWorkerId: this.workerId,\n targetWorkerId: route.workerId,\n topic,\n topicKey,\n generation: route.generation\n });\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 if (action === 'SUBSCRIBE') {\n this.assignedTopics.set(topicKey, topic);\n this.confirmRoute(topicKey);\n }\n if (action === 'UNSUBSCRIBE') this.assignedTopics.delete(topicKey);\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 of listKeys(this.storage, this.workerPrefix)) {\n const worker = readJson<WorkerRecord>(this.storage, key);\n if (!worker) continue;\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) return this.subscribedTopics.has(this.knownTopics.get(topicKey) ?? '') ? [this.tabId] : [];\n const activeTabIds = new Set(workers.map(worker => worker.tabId));\n const subscribers = new Set<string>();\n for (const key of listKeys(this.storage, `${this.subscriberPrefix}${topicKey}:`)) {\n const record = readJson<TopicSubscriberRecord>(this.storage, key);\n if (!record || !activeTabIds.has(record.tabId)) {\n this.removeStorage(key);\n continue;\n }\n subscribers.add(record.tabId);\n }\n return [...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) {\n const topic = this.knownTopics.get(topicKey);\n return topic && (this.subscribedTopics.has(topic) || this.assignedTopics.has(topicKey))\n ? {\n topicKey,\n workerId: this.workerId,\n tabId: this.tabId,\n updatedAt: this.environment.now(),\n generation: 1\n }\n : null;\n }\n return readJson<WorkerRoute>(this.storage, this.routeStorageKey(topicKey));\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), {\n topicKey,\n workerId: owner.workerId,\n tabId: owner.tabId,\n updatedAt: this.environment.now(),\n generation,\n ...(handoffFromWorkerId ? { handoffFromWorkerId } : {})\n } satisfies WorkerRoute);\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 of listKeys(this.storage, this.routePrefix)) {\n const route = readJson<WorkerRoute>(this.storage, key);\n if (!route || 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 of listKeys(this.storage, this.subscriberPrefix)) {\n const record = readJson<TopicSubscriberRecord>(this.storage, key);\n if (!record || !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 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 = selectActiveWorkers(workers, this.maxActiveWorkers).some(worker => worker.workerId === this.workerId)\n ? 'active'\n : '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 const oldest = this.knownTopics.keys().next().value;\n if (oldest !== undefined && oldest !== topicKey && !this.assignedTopics.has(oldest)) {\n this.knownTopics.delete(oldest);\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\nfunction listKeysSafe(storage: StorageLike | null, prefix: string): string[] {\n return storage ? listKeys(storage, prefix) : [];\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. */\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\nexport interface DataBusTraceOptions {\n enabled?: boolean;\n mode?: DataBusTraceMode;\n metricsIntervalMs?: number;\n sink: (event: DataBusTraceEvent) => void;\n}\n\n// Default bounds for the metrics aggregation window.\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 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.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.enabled || this.mode === 'events') return;\n this.received += 1;\n this.topics.add(topic);\n const queue = this.receivedAt.get(topic);\n if (!queue) {\n // Cap the number of tracked topics to avoid unbounded memory growth.\n if (this.receivedAt.size >= MAX_PENDING_TOPICS) return;\n this.receivedAt.set(topic, [this.now()]);\n return;\n }\n // Cap per-topic queue length so a single busy topic cannot starve others.\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.enabled || this.mode === 'events') 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.enabled || this.mode === 'events') 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 /** Emit the accumulated metrics snapshot if the interval is active. */\n flush(): void {\n if (!this.enabled || this.mode === 'events') return;\n this.flushNow();\n }\n\n private flushNow(): void {\n const timestamp = this.now();\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. */\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. */\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 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 */\nconst PUBLICATION_EVENT = 'DATABUS_PUBLICATION';\n\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 if (action === 'SUBSCRIBE') {\n if (this.subscribeTransport(topic)) this.traceSubscription('subscribe', topic);\n }\n if (action === 'UNSUBSCRIBE') {\n if (this.unsubscribeTransport(topic)) this.traceSubscription('unsubscribe', topic);\n }\n if (action === 'PUBLISH') this.runTransport(() => this.transport.publish(topic, data));\n },\n // The cluster calls `onEvent` when a publication broadcast arrives from\n // another tab. Dispatch locally if we have subscribers.\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 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 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(w => `${w.workerId}|${w.status}|load=${w.load}|tab=${w.tabId}`),\n routes: snapshot.routes.map(r => `${r.topicKey}@${r.workerId}|confirmed=${r.confirmedAt !== undefined}`)\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 = Promise.resolve()\n .then(() => this.transport.stop())\n .catch(stopError => this.reportError(stopError));\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 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 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 private dispatch(message: DataBusMessage<TData>): void {\n this.trace.recordDispatched(message.topic);\n for (const handler of this.topicHandlers.get(message.topic) ?? []) {\n try {\n handler(message);\n } catch (error) {\n this.reportError(error);\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 for (const handler of this.statusHandlers) {\n try {\n handler(status);\n } catch (error) {\n this.reportError(error);\n }\n }\n }\n\n private reportError(error: unknown): void {\n this.trace.event({ type: 'error', source: 'transport' });\n for (const handler of this.errorHandlers) {\n try {\n handler(error);\n } catch (handlerError) {\n // A failing error handler must not break the others or the call stack.\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn('[cross-tab-worker-databus] error handler threw:', handlerError);\n }\n }\n }\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 /**\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.\n if (this.pendingStop) return;\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 /**\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 must not be reused as an opening, so the reopen\n // still chains after that stop below.\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 * 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: 'dedicated' \u2192 try Dedicated Worker first, 'shared'/'auto' \u2192 try SharedWorker first. */\nexport type WorkerMode = 'dedicated' | 'shared' | 'auto';\n\n/** Resolved backend that was actually created. */\nexport type WorkerBackend = 'dedicated' | 'shared' | 'local';\n\n/** Override Worker availability for testing or environments where feature detection is unreliable. */\nexport interface WorkerAvailability {\n worker?: boolean;\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 */\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
+ }
@@ -0,0 +1,219 @@
1
+ import type { ClusterEnvironment } from './environment';
2
+ import type { WorkerControlAction, WorkerRecord, WorkerRoute, WorkerStatus } from './types';
3
+ /** Callbacks the cluster invokes to drive the transport and lifecycle. */
4
+ export interface WorkerClusterHandlers {
5
+ /** A SUBSCRIBE/UNSUBSCRIBE/PUBLISH control action was received for this worker. */
6
+ onControl: (action: WorkerControlAction, topic: string, data?: unknown) => void;
7
+ /** A fan-out publication event was received from another Worker. */
8
+ onEvent: (eventType: string, payload: unknown, sourceWorkerId: string) => void;
9
+ /** The cluster suspended (tab hidden / pagehide). */
10
+ onSuspend?: () => void;
11
+ /** The cluster resumed (tab visible / pageshow). */
12
+ onResume?: () => void;
13
+ }
14
+ export interface WorkerClusterOptions {
15
+ /** Namespace for the cluster's storage keys and BroadcastChannel. */
16
+ clusterKey: string;
17
+ handlers: WorkerClusterHandlers;
18
+ environment?: ClusterEnvironment;
19
+ storagePrefix?: string;
20
+ tabId?: string;
21
+ workerId?: string;
22
+ maxActiveWorkers?: number;
23
+ heartbeatIntervalMs?: number;
24
+ workerTtlMs?: number;
25
+ }
26
+ /** Read-only snapshot of the cluster state for diagnostics and tracing. */
27
+ export interface WorkerClusterSnapshot {
28
+ coordinated: boolean;
29
+ suspended: boolean;
30
+ currentWorker: WorkerRecord;
31
+ workers: WorkerRecord[];
32
+ /** Routes with the plaintext topic injected from the in-memory knownTopics cache. */
33
+ routes: Array<WorkerRoute & {
34
+ topic: string | null;
35
+ }>;
36
+ subscribedTopics: string[];
37
+ assignedTopics: string[];
38
+ /** Opaque key → plaintext topic mapping for debugging. */
39
+ knownTopics: Array<{
40
+ topicKey: string;
41
+ topic: string;
42
+ }>;
43
+ }
44
+ /**
45
+ * Cross-tab worker coordination runtime.
46
+ *
47
+ * Manages a cluster of Workers (one per tab) that share topics via localStorage
48
+ * and BroadcastChannel. Each Worker publishes its own record, subscribes to
49
+ * topics, and routes publications through the owning Worker to avoid duplicates.
50
+ *
51
+ * Key responsibilities:
52
+ * - Heartbeat-based failure detection (stale workers pruned after `workerTtlMs`)
53
+ * - Topic-to-Worker routing with load-based rebalancing
54
+ * - Page lifecycle integration (suspend on hide, resume on show)
55
+ * - Storage-backed coordination with BatchingStorageWriter for write coalescing
56
+ */
57
+ export declare class WorkerClusterRuntime {
58
+ readonly tabId: string;
59
+ readonly workerId: string;
60
+ private readonly environment;
61
+ private readonly handlers;
62
+ private storage;
63
+ private readonly maxActiveWorkers;
64
+ private readonly heartbeatIntervalMs;
65
+ private readonly workerTtlMs;
66
+ private readonly workerPrefix;
67
+ private readonly routePrefix;
68
+ private readonly subscriberPrefix;
69
+ private readonly channelName;
70
+ private readonly subscribedTopics;
71
+ private readonly assignedTopics;
72
+ private readonly knownTopics;
73
+ private channel;
74
+ private heartbeatHandle;
75
+ private started;
76
+ private suspended;
77
+ private lifecycleListening;
78
+ private currentRecord;
79
+ constructor(options: WorkerClusterOptions);
80
+ /** Start the cluster: register, listen for lifecycle events, and begin heartbeats. */
81
+ start(): void;
82
+ /**
83
+ * Stop the cluster: pause heartbeats, hand off assigned topics, remove
84
+ * the worker record, and clean up lifecycle listeners. Idempotent.
85
+ */
86
+ stop(): void;
87
+ /**
88
+ * Activate the cluster: open the BroadcastChannel, register the worker record,
89
+ * subscribe to topics, and start the heartbeat interval.
90
+ */
91
+ private activate;
92
+ /**
93
+ * Pause the cluster on pagehide: stop heartbeats, hand off assigned topics
94
+ * to other workers, remove our worker record, and close the channel.
95
+ */
96
+ private pause;
97
+ /** Update the worker's connection status and persist the change. */
98
+ setStatus(status: WorkerStatus): void;
99
+ /**
100
+ * Subscribe to a topic. Returns true if this worker becomes the assigned owner.
101
+ * The topic is recorded locally and the cluster is notified via storage or
102
+ * direct control message.
103
+ */
104
+ subscribe(topic: string): boolean;
105
+ /**
106
+ * Remove the local subscription. Cleans up the subscriber record and, if no
107
+ * subscribers remain, deletes the route so the owning Worker can unsubscribe.
108
+ */
109
+ unsubscribe(topic: string): void;
110
+ /** Remove this tab's subscriber record and, when it was the last one, delete the route. */
111
+ private releaseSubscription;
112
+ /** Transfer assigned topics to other active workers so subscribers are not orphaned during pause. */
113
+ private handoffAssignedTopics;
114
+ /**
115
+ * Publish a message to `topic`, routing through the owning Worker (or self if
116
+ * no owner is found). Returns false when the control message could not be
117
+ * posted to a remote owner, so the caller can surface the failure instead of
118
+ * silently dropping the publication.
119
+ */
120
+ publish(topic: string, data: unknown): boolean;
121
+ /** Broadcast an event to every tab — used to fan out transport publications. */
122
+ broadcastEvent(eventType: string, payload: unknown): void;
123
+ isAssigned(topic: string): boolean;
124
+ /** True if this worker is among the active set (eligible to own topics). */
125
+ isActiveWorker(): boolean;
126
+ /** True when this tab has a local subscriber registered for `topic`. */
127
+ hasLocalSubscriber(topic: string): boolean;
128
+ /** Read-only snapshot of the cluster state (workers, routes, assignments). */
129
+ getSnapshot(): WorkerClusterSnapshot;
130
+ private readonly handlePageHide;
131
+ private readonly handlePageShow;
132
+ private readonly handleVisibilityChange;
133
+ private addLifecycleListeners;
134
+ private removeLifecycleListeners;
135
+ /** Handle an incoming cluster message: dispatch by type to the per-type handlers. */
136
+ private readonly handleMessage;
137
+ /** Handle a point-to-point CONTROL message (SUBSCRIBE / UNSUBSCRIBE / PUBLISH). */
138
+ private handleControlMessage;
139
+ /**
140
+ * When this worker is the previous owner in a graceful handoff and the new
141
+ * owner asks us to unsubscribe, release the old transport subscription and
142
+ * ACK the handoff with ROUTE_RELEASED. Returns true when the message was a
143
+ * handoff release (the generic CONTROL dispatch must not run as well).
144
+ */
145
+ private releaseHandoffOnUnsubscribe;
146
+ /**
147
+ * Accept a graceful handoff only when the route still points to this worker,
148
+ * the release comes from the recorded previous owner, and the generation is
149
+ * at least as new as ours. Any other ROUTE_RELEASED is stale and dropped.
150
+ */
151
+ private handleRouteReleasedMessage;
152
+ /** Full reconciliation cycle: workers, subscriptions, and assigned topics. */
153
+ private reconcile;
154
+ /** Prune stale workers/subscribers/routes and refresh role. Returns the live worker list. */
155
+ private reconcileWorkers;
156
+ /**
157
+ * Ensure every local subscription has a route and write subscriber records.
158
+ *
159
+ * Existing routes are deliberately sticky while their owner Worker is alive.
160
+ * Load and visibility only influence placement of a new route; they must not
161
+ * move an already-subscribed Topic merely because another Tab joins or becomes
162
+ * visible. Ownership changes only after the owner leaves or its heartbeat
163
+ * expires, which avoids unnecessary transport subscribe/unsubscribe churn.
164
+ */
165
+ private reconcileSubscriptions;
166
+ /** Drop assignments where the route no longer points to this worker. */
167
+ private reconcileAssignedTopics;
168
+ /**
169
+ * Send a control message to `targetWorkerId`, or execute locally when targeting self.
170
+ * Local execution updates the assignment map and route synchronously, bypassing
171
+ * the BroadcastChannel latency.
172
+ */
173
+ private sendControl;
174
+ /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
175
+ private send;
176
+ /** Read all live worker records from storage, pruning stale entries past the TTL. */
177
+ private readWorkers;
178
+ /** Enumerate all tab IDs that have a subscriber record for `topicKey`. */
179
+ private readSubscriberTabIds;
180
+ /** Read the current route for `topicKey`, returning null when no storage layer exists. */
181
+ private readRoute;
182
+ /** Persist a route assignment, mapping `topicKey` to the owning Worker. */
183
+ private writeRoute;
184
+ /** Stamp a route as confirmed once the owning Worker has acknowledged the assignment. */
185
+ private confirmRoute;
186
+ /** Remove routes whose topic has no subscribers and whose TTL has expired. */
187
+ private cleanupOrphanedRoutes;
188
+ /** Remove subscriber records for tabs that are no longer active. */
189
+ private cleanupOrphanedSubscribers;
190
+ /** Persist a subscriber record for this tab on `topicKey`. */
191
+ private writeSubscriber;
192
+ /** Persist the current worker record with an updated heartbeat timestamp. */
193
+ private writeRecord;
194
+ /** Broadcast a REGISTRY message to trigger reconciliation on other tabs. */
195
+ private notifyRegistry;
196
+ /** Recompute whether this worker is active (eligible to own topics) or standby. Returns true when changed. */
197
+ private refreshRole;
198
+ /** Persist the current topic load count (number of assigned topics) for load-balanced routing. */
199
+ private updateLoad;
200
+ /**
201
+ * Hash `topic` into its opaque key and populate the reverse-lookup cache.
202
+ *
203
+ * Despite the name, this is NOT a cache lookup — it unconditionally writes
204
+ * the `topicKey → topic` pair. Hashing is cheap enough that a caller needing
205
+ * the key should always call this rather than check `knownTopics` first;
206
+ * the cache's FIFO eviction below keeps it bounded. Only `isAssigned`
207
+ * deliberately bypasses this (it must not pollute the cache on a read-only
208
+ * query), so if you add a new call site, prefer `rememberTopic` unless you
209
+ * have the same "read-only query" reason.
210
+ */
211
+ private rememberTopic;
212
+ private workerStorageKey;
213
+ private routeStorageKey;
214
+ private subscriberStorageKey;
215
+ private removeStorage;
216
+ /** Force-flush any pending batched writes (used during shutdown/teardown). */
217
+ private flushStorage;
218
+ }
219
+ //# sourceMappingURL=cluster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/core/cluster.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAkB,kBAAkB,EAAe,MAAM,eAAe,CAAC;AAOrF,OAAO,KAAK,EAGV,mBAAmB,EACnB,YAAY,EACZ,WAAW,EACX,YAAY,EACb,MAAM,SAAS,CAAC;AAGjB,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAChF,oEAAoE;IACpE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/E,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,qBAAqB,CAAC;IAChC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,2EAA2E;AAC3E,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,YAAY,CAAC;IAC5B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,qFAAqF;IACrF,MAAM,EAAE,KAAK,CAAC,WAAW,GAAG;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACtD,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,WAAW,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzD;AAwCD;;;;;;;;;;;;GAYG;AACH,qBAAa,oBAAoB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IAItD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAM5D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,aAAa,CAAe;gBAExB,OAAO,EAAE,oBAAoB;IAkCzC,sFAAsF;IACtF,KAAK,IAAI,IAAI;IAOb;;;OAGG;IACH,IAAI,IAAI,IAAI;IAWZ;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAiChB;;;OAGG;IACH,OAAO,CAAC,KAAK;IA0Bb,oEAAoE;IACpE,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAMrC;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IA0BjC;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAQhC,2FAA2F;IAC3F,OAAO,CAAC,mBAAmB;IAY3B,qGAAqG;IACrG,OAAO,CAAC,qBAAqB;IA0C7B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO;IAU9C,gFAAgF;IAChF,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAIzD,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAelC,4EAA4E;IAC5E,cAAc,IAAI,OAAO;IAMzB,wEAAwE;IACxE,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI1C,8EAA8E;IAC9E,WAAW,IAAI,qBAAqB;IAoBpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IAErD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAK7B;IAEF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAQrC;IAEF,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,wBAAwB;IAQhC,qFAAqF;IACrF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAU5B;IAEF,mFAAmF;IACnF,OAAO,CAAC,oBAAoB;IAgB5B;;;;;OAKG;IACH,OAAO,CAAC,2BAA2B;IAmBnC;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;IAiBlC,8EAA8E;IAC9E,OAAO,CAAC,SAAS;IASjB,6FAA6F;IAC7F,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IA8B9B,wEAAwE;IACxE,OAAO,CAAC,uBAAuB;IAoB/B;;;;OAIG;IACH,OAAO,CAAC,WAAW;IA4BnB,oFAAoF;IACpF,OAAO,CAAC,IAAI;IAUZ,qFAAqF;IACrF,OAAO,CAAC,WAAW;IAiBnB,0EAA0E;IAC1E,OAAO,CAAC,oBAAoB;IAe5B,0FAA0F;IAC1F,OAAO,CAAC,SAAS;IAgBjB,2EAA2E;IAC3E,OAAO,CAAC,UAAU;IAiBlB,yFAAyF;IACzF,OAAO,CAAC,YAAY;IAUpB,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB;IAW7B,oEAAoE;IACpE,OAAO,CAAC,0BAA0B;IASlC,8DAA8D;IAC9D,OAAO,CAAC,eAAe;IAQvB,6EAA6E;IAC7E,OAAO,CAAC,WAAW;IAMnB,4EAA4E;IAC5E,OAAO,CAAC,cAAc;IAItB,8GAA8G;IAC9G,OAAO,CAAC,WAAW;IASnB,kGAAkG;IAClG,OAAO,CAAC,UAAU;IAOlB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,aAAa;IAQrB,8EAA8E;IAC9E,OAAO,CAAC,YAAY;CAGrB"}