cross-tab-worker-databus 0.21.8 → 0.21.9

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 (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/centrifuge-protocol.d.ts +7 -0
  3. package/dist/centrifuge-protocol.d.ts.map +1 -1
  4. package/dist/centrifuge.d.ts.map +1 -1
  5. package/dist/centrifuge.js +5 -2
  6. package/dist/centrifuge.js.map +2 -2
  7. package/dist/centrifuge.shared.worker.js +8 -1
  8. package/dist/centrifuge.shared.worker.js.map +2 -2
  9. package/dist/centrifuge.worker.js +2 -1
  10. package/dist/centrifuge.worker.js.map +2 -2
  11. package/dist/{chunk-TZ7ZP7YD.js → chunk-2TTJ7LGZ.js} +3 -2
  12. package/dist/{chunk-TZ7ZP7YD.js.map → chunk-2TTJ7LGZ.js.map} +2 -2
  13. package/dist/{chunk-DBKAWZBG.js → chunk-75ZDU7VG.js} +3 -3
  14. package/dist/cjs/centrifuge.cjs +6 -2
  15. package/dist/cjs/centrifuge.cjs.map +2 -2
  16. package/dist/cjs/hooks.cjs.map +1 -1
  17. package/dist/cjs/index.cjs +1 -1
  18. package/dist/cjs/index.cjs.map +1 -1
  19. package/dist/cjs/vue.cjs.map +1 -1
  20. package/dist/hooks.js +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/utils/constants.d.ts +1 -0
  23. package/dist/utils/constants.d.ts.map +1 -1
  24. package/dist/vue.js +1 -1
  25. package/dist/workers/port-reaper.d.ts +4 -1
  26. package/dist/workers/port-reaper.d.ts.map +1 -1
  27. package/docs/architecture.md +1 -1
  28. package/docs/benchmarks.md +7 -7
  29. package/docs/configuration.md +2 -1
  30. package/docs/roadmap.md +19 -1
  31. package/docs/transports.md +6 -1
  32. package/docs/zh/architecture.md +1 -1
  33. package/docs/zh/benchmarks.md +7 -7
  34. package/docs/zh/configuration.md +2 -1
  35. package/docs/zh/roadmap.md +13 -1
  36. package/docs/zh/transports.md +4 -1
  37. package/package.json +1 -1
  38. /package/dist/{chunk-DBKAWZBG.js.map → chunk-75ZDU7VG.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/vue.ts", "../../src/utils/constants.ts"],
4
- "sourcesContent": ["/** Vue 3 composables adapter for cross-tab-worker-databus.\n * Vue is an optional peer dependency; this module is a separate entry point.\n */\nimport { onBeforeUnmount, onMounted, ref, shallowRef, watch, type Ref } from 'vue';\nimport type { CrossTabDataBus, DataBusHealthSummary } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\nimport { WORKER_STATUS } from './utils/constants';\n\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: ReadonlyArray<Ref<unknown> | (() => unknown)> = []\n): Ref<CrossTabDataBus<TConfig, TData> | null> {\n const bus = shallowRef<CrossTabDataBus<TConfig, TData> | null>(null);\n let instance: CrossTabDataBus<TConfig, TData> | null = null;\n let lifecycleGeneration = 0;\n const stop = async () => { const current = instance; instance = null; bus.value = null; if (current) await current.stop(); };\n const start = () => {\n const generation = ++lifecycleGeneration;\n void stop().then(() => {\n if (generation !== lifecycleGeneration) return;\n const next = create();\n instance = next;\n bus.value = next;\n void next.ready().catch(() => {});\n });\n };\n onMounted(start);\n onBeforeUnmount(() => {\n // Bump the generation so a start() still awaiting its stop() sees itself\n // superseded. Without this the pending continuation would run create()\n // after the component is gone, leaving a live bus with no owner to stop\n // it (the React adapter has no such window: its create() is synchronous\n // inside useEffect).\n lifecycleGeneration += 1;\n void stop();\n });\n if (deps.length > 0) watch(deps, start);\n return bus as Ref<CrossTabDataBus<TConfig, TData> | null>;\n}\n\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>, topic: Ref<string> | string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n let currentBus: CrossTabDataBus<TConfig, TData> | null = null;\n let currentTopic: string | null = null;\n let cleanup: (() => void) | undefined;\n const stop = () => { cleanup?.(); cleanup = undefined; currentBus = null; currentTopic = null; };\n const sync = () => {\n const nextBus = bus.value;\n const nextTopic = typeof topic === 'string' ? topic : topic.value;\n if (nextBus === currentBus && currentTopic === nextTopic && cleanup) return;\n stop();\n if (!nextBus) return;\n currentBus = nextBus;\n currentTopic = nextTopic;\n cleanup = nextBus.subscribe(nextTopic, handler);\n };\n watch(bus, sync, { immediate: true });\n if (typeof topic !== 'string') watch(topic, sync);\n onBeforeUnmount(stop);\n}\n\nexport function useCrossTabStatus<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>\n): Ref<WorkerStatus> {\n const status = ref<WorkerStatus>(WORKER_STATUS.CONNECTING);\n let cleanup: (() => void) | undefined;\n watch(bus, next => {\n cleanup?.(); cleanup = undefined;\n status.value = next?.getStatus() ?? WORKER_STATUS.CONNECTING;\n if (next) cleanup = next.onStatus(value => { status.value = value; });\n }, { immediate: true });\n onBeforeUnmount(() => cleanup?.());\n return status;\n}\n\n/** Options for {@link useCrossTabHealth}. */\nexport interface UseCrossTabHealthOptions {\n /** Polling cadence in ms for the health snapshot. Default 1000; `0` disables\n * polling and relies on status/error events only. */\n intervalMs?: number;\n}\n\n/**\n * Mirror the bus health summary into a Vue ref. `getHealthSummary()` is a\n * snapshot, so the composable polls it on an interval (default 1000 ms) and\n * refreshes on status changes and errors. A reactive interval change replaces\n * the timer without rebuilding the bus. Returns `null` until the bus exists.\n */\nexport function useCrossTabHealth<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>,\n options?: UseCrossTabHealthOptions\n): Ref<DataBusHealthSummary | null> {\n const health = ref<DataBusHealthSummary | null>(null);\n let timer: ReturnType<typeof setInterval> | null = null;\n let cleanups: Array<() => void> = [];\n const teardown = () => {\n if (timer) clearInterval(timer);\n timer = null;\n for (const cleanup of cleanups) cleanup();\n cleanups = [];\n };\n watch([bus, () => options?.intervalMs], ([next]) => {\n teardown();\n if (!next) {\n health.value = null;\n return;\n }\n const refresh = () => { health.value = next.getHealthSummary(); };\n refresh();\n cleanups.push(next.onStatus(refresh));\n cleanups.push(next.onError(refresh));\n const intervalMs = options?.intervalMs ?? 1_000;\n if (intervalMs > 0) timer = setInterval(refresh, intervalMs);\n }, { immediate: true });\n onBeforeUnmount(teardown);\n return health as Ref<DataBusHealthSummary | null>;\n}\n", "/**\n * \u516C\u5171\u5B57\u7B26\u4E32\u5E38\u91CF \u2014\u2014 \u751F\u547D\u5468\u671F\u72B6\u6001\u3001\u89D2\u8272\u3001\u63A7\u5236\u52A8\u4F5C\u3001\u534F\u8BAE\u6D88\u606F\u7C7B\u578B\u3001\u679A\u4E3E\u4E0E\n * \u547D\u540D\u7A7A\u95F4\u524D\u7F00\u3002\n *\n * \u6240\u6709\u8FD0\u884C\u65F6\u4F7F\u7528\u7684\u5B57\u7B26\u4E32\u5B57\u9762\u91CF\u96C6\u4E2D\u5728\u6B64\u4E00\u4EFD\u503C\uFF0C\u7C7B\u578B\u5B9A\u4E49\uFF08src/core/types.ts\n * \u7B49\uFF09\u901A\u8FC7 `(typeof X)[keyof typeof X]` \u4ECE\u8FD9\u4E9B\u5E38\u91CF\u6D3E\u751F\uFF0C\u6BD4\u8F83/switch \u5904\u5F15\u7528\n * \u540C\u4E00\u5BF9\u8C61\u6210\u5458\u3002\u8FD9\u6837\u65E2\u80FD\u6D88\u9664\u6563\u843D\u5728\u5404\u6587\u4EF6\u91CC\u7684\u91CD\u590D\u5B57\u7B26\u4E32\uFF08\u907F\u514D\u5927\u5C0F\u5199\u4E0D\u7EDF\u4E00\n * \u6216\u7B14\u8BEF\uFF09\uFF0C\u53C8\u4FDD\u8BC1\u7C7B\u578B\u4E0E\u503C\u6C38\u4E0D\u9519\u4F4D\u3002\n *\n * ## \u4F7F\u7528\u7EA6\u5B9A\n *\n * - **\u65B0\u589E\u6216\u4FEE\u6539\u5B57\u7B26\u4E32\u5B57\u9762\u91CF\u65F6\uFF0C\u5148\u5230\u8FD9\u91CC\u67E5\u627E/\u589E\u8865\uFF0C\u4E0D\u8981\u5728\u4E1A\u52A1\u6587\u4EF6\u91CC\u76F4\u63A5\u5199\n * \u9B54\u6CD5\u5B57\u7B26\u4E32\u3002** \u626B\u63CF\u6B8B\u7559\u5B57\u9762\u91CF\uFF1A`rg \"'(['a-z]+)'\" src/`\u3002\n * - **\u53EA\u7528\u4E8E\u7C7B\u578B\u4F4D\u7F6E\u7684\u5E38\u91CF**\uFF08\u5982 `PERSISTENCE_OPERATION`\uFF09\u5728\u8C03\u7528\u65B9\u7528\n * `import type`\uFF1B**\u5728\u8FD0\u884C\u65F6\u6BD4\u8F83/\u6784\u9020\u4E2D\u4F7F\u7528\u7684**\uFF08\u5982 `WORKER_STATUS.ERROR`\u3001\n * `TRACE_EVENT_TYPE.LIFECYCLE`\uFF09\u5FC5\u987B\u7528\u503C\u5BFC\u5165\uFF0C\u5426\u5219\u4F1A\u62A5 \"cannot be used as\n * a value because it was imported using 'import type'\"\u3002\n * - **\u503C\u6D3E\u751F\u7C7B\u578B**\uFF1A`export type WorkerStatus = (typeof WORKER_STATUS)[keyof\n * typeof WORKER_STATUS]`\u3002\u8FD9\u6837\u65B0\u589E\u679A\u4E3E\u5206\u652F\u65F6\u7C7B\u578B\u81EA\u52A8\u6536\u7A84\uFF0C\u7F16\u8BD1\u5668\u4F1A\u6307\u51FA\n * \u6BCF\u4E2A\u9057\u6F0F\u7684 switch \u5206\u652F\u3002\n * - \u5206\u7EC4\u7684\u987A\u5E8F\uFF08\u72B6\u6001 / \u89D2\u8272 / \u540E\u7AEF / \u6D88\u606F\u7C7B\u578B / \u679A\u4E3E / \u524D\u7F00\uFF09\u4FDD\u6301\u7A33\u5B9A\uFF0C\u65B9\u4FBF\n * \u7EF4\u62A4\u8005\u4E00\u773C\u5B9A\u4F4D\u3002\n */\n\n// ---- \u751F\u547D\u5468\u671F\u72B6\u6001 / \u89D2\u8272 / \u53EF\u89C1\u6027 ----\nexport const WORKER_STATUS = {\n CONNECTING: 'connecting',\n CONNECTED: 'connected',\n DISCONNECTED: 'disconnected',\n ERROR: 'error',\n} as const;\n\nexport const WORKER_ROLE = {\n ACTIVE: 'active',\n STANDBY: 'standby',\n} as const;\n\nexport const TAB_VISIBILITY = {\n VISIBLE: 'visible',\n HIDDEN: 'hidden',\n} as const;\n\n// ---- Worker \u540E\u7AEF\u6A21\u5F0F\u4E0E\u89E3\u6790\u7ED3\u679C\uFF08WorkerMode / WorkerBackend\uFF09----\nexport const WORKER_MODE = {\n DEDICATED: 'dedicated',\n SHARED: 'shared',\n AUTO: 'auto',\n} as const;\n\nexport const WORKER_BACKEND = {\n DEDICATED: 'dedicated',\n SHARED: 'shared',\n LOCAL: 'local',\n} as const;\n\n// ---- \u5B58\u50A8\u4E8B\u4EF6\u901A\u9053\u56DE\u9000 ----\nexport const CHANNEL_FALLBACK = {\n NONE: 'none',\n STORAGE_EVENT: 'storage-event',\n} as const;\n\n// ---- Worker / MessagePort / BroadcastChannel \u7684 EventTarget \u4E8B\u4EF6\u540D\u5224\u522B ----\n// MessageEvent.type\uFF08'message' / 'messageerror'\uFF09\u4E0E Worker \u4E0A\u7684 'error' \u4E8B\u4EF6\u540D\uFF0C\n// \u7528\u4E8E\u73AF\u5883\u9002\u914D\u5668\u63A5\u53E3\u7B7E\u540D\u4E0E\u6D4B\u8BD5\u66FF\u8EAB\u7684 addEventListener \u771F\u5047\u5206\u652F\u3002\nexport const EVENT_TYPE = {\n MESSAGE: 'message',\n MESSAGEERROR: 'messageerror',\n ERROR: 'error',\n} as const;\n\n// ---- \u63A7\u5236\u5E73\u9762\u52A8\u4F5C\uFF08WorkerControlAction\uFF09----\nexport const CONTROL_ACTION = {\n SUBSCRIBE: 'SUBSCRIBE',\n UNSUBSCRIBE: 'UNSUBSCRIBE',\n PUBLISH: 'PUBLISH',\n} as const;\n\n// ---- BroadcastChannel \u96C6\u7FA4\u6D88\u606F\u7C7B\u578B ----\nexport const CLUSTER_MESSAGE_TYPE = {\n CONTROL: 'CONTROL',\n EVENT: 'EVENT',\n REGISTRY: 'REGISTRY',\n ROUTE_RELEASED: 'ROUTE_RELEASED',\n} as const;\n\n// ---- \u547D\u540D\u7A7A\u95F4\u524D\u7F00\u4E0E\u4E8B\u4EF6\u540D ----\nexport const DEFAULT_STORAGE_PREFIX = 'cross-tab-worker-databus';\nexport const STORAGE_CHANNEL_PREFIX = `${DEFAULT_STORAGE_PREFIX}:channel:`;\nexport const TAB_ID_STORAGE_KEY = `${DEFAULT_STORAGE_PREFIX}:tab-id`;\nexport const PUBLICATION_EVENT = 'DATABUS_PUBLICATION';\n\n// ---- \u89C4\u8303 JSON \u4FE1\u5C01\u64CD\u4F5C\u7801 ----\nexport const PUBLICATION_ENVELOPE_OP = 'publication';\n\n// ---- trace \u4E8B\u4EF6\u7C7B\u578B ----\nexport const TRACE_EVENT_TYPE = {\n LIFECYCLE: 'lifecycle',\n STATUS: 'status',\n SUBSCRIPTION: 'subscription',\n COORDINATION: 'coordination',\n ERROR: 'error',\n RELIABILITY: 'reliability',\n MESSAGE_METRICS: 'message_metrics',\n} as const;\n\n// ---- trace \u751F\u547D\u5468\u671F\u52A8\u4F5C ----\nexport const TRACE_LIFECYCLE_ACTION = {\n START: 'start',\n STOP: 'stop',\n SUSPEND: 'suspend',\n RESUME: 'resume',\n} as const;\n\n// ---- \u5185\u90E8 handler \u8C03\u7528\u6807\u7B7E\uFF08\u9694\u79BB\u5F02\u5E38\u65F6\u7684\u5206\u7C7B\u6765\u6E90\uFF09----\nexport const INVOKE_LABEL = {\n DISPATCH: 'dispatch',\n STATUS: 'status',\n ERROR_HANDLER: 'error handler',\n} as const;\n\n// ---- trace \u6A21\u5F0F\uFF08DataBusTraceMode\uFF09----\nexport const TRACE_MODE = {\n EVENTS: 'events',\n METRICS: 'metrics',\n ALL: 'all',\n} as const;\n\n// ---- trace \u9519\u8BEF\u6765\u6E90 ----\nexport const TRACE_ERROR_SOURCE = {\n TRANSPORT: 'transport',\n OPERATION: 'operation',\n} as const;\n\n// ---- reliability \u8BCA\u65AD\u64CD\u4F5C ----\nexport const RELIABILITY_OPERATION = {\n TRANSPORT_RECOVERY: 'transport_recovery',\n ROUTE_ACK: 'route_ack',\n ROUTE_MIGRATION: 'route_migration',\n // A re-election that recovered a stranded unconfirmed handoff (previous\n // owner gone, ACK never arrived). Distinct from ROUTE_MIGRATION so trace\n // consumers can tell recoveries apart from routine graceful handoffs.\n ROUTE_MIGRATION_RECOVERY: 'route_migration_recovery',\n PERSISTENCE_CLEANUP: 'persistence_cleanup',\n PERSISTENCE_RETRY: 'persistence_retry',\n DEDUP_SUPPRESSED: 'dedup_suppressed',\n} as const;\n\n// ---- \u81EA\u52A8\u6062\u590D\u5C1D\u8BD5\u7ED3\u679C ----\nexport const RECOVERY_OUTCOME = {\n SCHEDULED: 'scheduled',\n SUCCEEDED: 'succeeded',\n FAILED: 'failed',\n EXHAUSTED: 'exhausted',\n} as const;\n\n// ---- \u8BA2\u9605\u52A8\u4F5C ----\nexport const SUBSCRIPTION_ACTION = {\n SUBSCRIBE: 'subscribe',\n UNSUBSCRIBE: 'unsubscribe',\n} as const;\n\n// ---- \u56DE\u653E\u4FEE\u526A\u7B56\u7565 ----\nexport const PRUNE_STRATEGY = {\n COUNT: 'count',\n AGE: 'age',\n BOTH: 'both',\n} as const;\n\n// ---- \u6301\u4E45\u5316\u64CD\u4F5C ----\nexport const PERSISTENCE_OPERATION = {\n LOAD: 'load',\n APPEND: 'append',\n CLEAR: 'clear',\n CLEAR_TOPIC: 'clearTopic',\n CLEAR_BEFORE: 'clearBefore',\n} as const;\n\n// ---- \u6545\u969C\u6765\u6E90\uFF08DataBusFailureSource\uFF09----\nexport const FAILURE_SOURCE = {\n TRANSPORT: 'transport',\n PERSISTENCE: 'persistence',\n DISPATCH: 'dispatch',\n} as const;\n\n// ---- \u5065\u5EB7\u5224\u5B9A\u72B6\u6001\uFF08DataBusHealthSummary['state']\uFF09----\nexport const HEALTH_STATE = {\n STOPPED: 'stopped',\n STARTING: 'starting',\n HEALTHY: 'healthy',\n RECOVERING: 'recovering',\n SUSPENDED: 'suspended',\n DEGRADED: 'degraded',\n} as const;\n\n// ---- WebSocket wire \u534F\u8BAE\u64CD\u4F5C\u7801 ----\nexport const WS_OP = {\n SUBSCRIBE: 'subscribe',\n UNSUBSCRIBE: 'unsubscribe',\n PUBLISH: 'publish',\n PUBLISH_BATCH: 'publishBatch',\n} as const;\n\n// ---- Centrifuge Worker \u8F93\u5165\u6D88\u606F\u7C7B\u578B ----\nexport const CENTRIFUGE_INPUT_TYPE = {\n INIT: 'INIT',\n SUBSCRIBE: 'SUBSCRIBE',\n UNSUBSCRIBE: 'UNSUBSCRIBE',\n PUBLISH: 'PUBLISH',\n PUBLISH_BIN: 'PUBLISH_BIN',\n PING: 'PING',\n STOP: 'STOP',\n TOKEN_RESPONSE: 'TOKEN_RESPONSE',\n TOKEN_ERROR: 'TOKEN_ERROR',\n} as const;\n\n// ---- Centrifuge Worker \u8F93\u51FA\u6D88\u606F\u7C7B\u578B ----\nexport const CENTRIFUGE_OUTPUT_TYPE = {\n STATUS: 'STATUS',\n MESSAGE: 'MESSAGE',\n MESSAGE_BIN: 'MESSAGE_BIN',\n ERROR: 'ERROR',\n TOKEN_REQUEST: 'TOKEN_REQUEST',\n} as const;\n"],
4
+ "sourcesContent": ["/** Vue 3 composables adapter for cross-tab-worker-databus.\n * Vue is an optional peer dependency; this module is a separate entry point.\n */\nimport { onBeforeUnmount, onMounted, ref, shallowRef, watch, type Ref } from 'vue';\nimport type { CrossTabDataBus, DataBusHealthSummary } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\nimport { WORKER_STATUS } from './utils/constants';\n\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: ReadonlyArray<Ref<unknown> | (() => unknown)> = []\n): Ref<CrossTabDataBus<TConfig, TData> | null> {\n const bus = shallowRef<CrossTabDataBus<TConfig, TData> | null>(null);\n let instance: CrossTabDataBus<TConfig, TData> | null = null;\n let lifecycleGeneration = 0;\n const stop = async () => { const current = instance; instance = null; bus.value = null; if (current) await current.stop(); };\n const start = () => {\n const generation = ++lifecycleGeneration;\n void stop().then(() => {\n if (generation !== lifecycleGeneration) return;\n const next = create();\n instance = next;\n bus.value = next;\n void next.ready().catch(() => {});\n });\n };\n onMounted(start);\n onBeforeUnmount(() => {\n // Bump the generation so a start() still awaiting its stop() sees itself\n // superseded. Without this the pending continuation would run create()\n // after the component is gone, leaving a live bus with no owner to stop\n // it (the React adapter has no such window: its create() is synchronous\n // inside useEffect).\n lifecycleGeneration += 1;\n void stop();\n });\n if (deps.length > 0) watch(deps, start);\n return bus as Ref<CrossTabDataBus<TConfig, TData> | null>;\n}\n\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>, topic: Ref<string> | string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n let currentBus: CrossTabDataBus<TConfig, TData> | null = null;\n let currentTopic: string | null = null;\n let cleanup: (() => void) | undefined;\n const stop = () => { cleanup?.(); cleanup = undefined; currentBus = null; currentTopic = null; };\n const sync = () => {\n const nextBus = bus.value;\n const nextTopic = typeof topic === 'string' ? topic : topic.value;\n if (nextBus === currentBus && currentTopic === nextTopic && cleanup) return;\n stop();\n if (!nextBus) return;\n currentBus = nextBus;\n currentTopic = nextTopic;\n cleanup = nextBus.subscribe(nextTopic, handler);\n };\n watch(bus, sync, { immediate: true });\n if (typeof topic !== 'string') watch(topic, sync);\n onBeforeUnmount(stop);\n}\n\nexport function useCrossTabStatus<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>\n): Ref<WorkerStatus> {\n const status = ref<WorkerStatus>(WORKER_STATUS.CONNECTING);\n let cleanup: (() => void) | undefined;\n watch(bus, next => {\n cleanup?.(); cleanup = undefined;\n status.value = next?.getStatus() ?? WORKER_STATUS.CONNECTING;\n if (next) cleanup = next.onStatus(value => { status.value = value; });\n }, { immediate: true });\n onBeforeUnmount(() => cleanup?.());\n return status;\n}\n\n/** Options for {@link useCrossTabHealth}. */\nexport interface UseCrossTabHealthOptions {\n /** Polling cadence in ms for the health snapshot. Default 1000; `0` disables\n * polling and relies on status/error events only. */\n intervalMs?: number;\n}\n\n/**\n * Mirror the bus health summary into a Vue ref. `getHealthSummary()` is a\n * snapshot, so the composable polls it on an interval (default 1000 ms) and\n * refreshes on status changes and errors. A reactive interval change replaces\n * the timer without rebuilding the bus. Returns `null` until the bus exists.\n */\nexport function useCrossTabHealth<TConfig, TData>(\n bus: Ref<CrossTabDataBus<TConfig, TData> | null>,\n options?: UseCrossTabHealthOptions\n): Ref<DataBusHealthSummary | null> {\n const health = ref<DataBusHealthSummary | null>(null);\n let timer: ReturnType<typeof setInterval> | null = null;\n let cleanups: Array<() => void> = [];\n const teardown = () => {\n if (timer) clearInterval(timer);\n timer = null;\n for (const cleanup of cleanups) cleanup();\n cleanups = [];\n };\n watch([bus, () => options?.intervalMs], ([next]) => {\n teardown();\n if (!next) {\n health.value = null;\n return;\n }\n const refresh = () => { health.value = next.getHealthSummary(); };\n refresh();\n cleanups.push(next.onStatus(refresh));\n cleanups.push(next.onError(refresh));\n const intervalMs = options?.intervalMs ?? 1_000;\n if (intervalMs > 0) timer = setInterval(refresh, intervalMs);\n }, { immediate: true });\n onBeforeUnmount(teardown);\n return health as Ref<DataBusHealthSummary | null>;\n}\n", "/**\n * \u516C\u5171\u5B57\u7B26\u4E32\u5E38\u91CF \u2014\u2014 \u751F\u547D\u5468\u671F\u72B6\u6001\u3001\u89D2\u8272\u3001\u63A7\u5236\u52A8\u4F5C\u3001\u534F\u8BAE\u6D88\u606F\u7C7B\u578B\u3001\u679A\u4E3E\u4E0E\n * \u547D\u540D\u7A7A\u95F4\u524D\u7F00\u3002\n *\n * \u6240\u6709\u8FD0\u884C\u65F6\u4F7F\u7528\u7684\u5B57\u7B26\u4E32\u5B57\u9762\u91CF\u96C6\u4E2D\u5728\u6B64\u4E00\u4EFD\u503C\uFF0C\u7C7B\u578B\u5B9A\u4E49\uFF08src/core/types.ts\n * \u7B49\uFF09\u901A\u8FC7 `(typeof X)[keyof typeof X]` \u4ECE\u8FD9\u4E9B\u5E38\u91CF\u6D3E\u751F\uFF0C\u6BD4\u8F83/switch \u5904\u5F15\u7528\n * \u540C\u4E00\u5BF9\u8C61\u6210\u5458\u3002\u8FD9\u6837\u65E2\u80FD\u6D88\u9664\u6563\u843D\u5728\u5404\u6587\u4EF6\u91CC\u7684\u91CD\u590D\u5B57\u7B26\u4E32\uFF08\u907F\u514D\u5927\u5C0F\u5199\u4E0D\u7EDF\u4E00\n * \u6216\u7B14\u8BEF\uFF09\uFF0C\u53C8\u4FDD\u8BC1\u7C7B\u578B\u4E0E\u503C\u6C38\u4E0D\u9519\u4F4D\u3002\n *\n * ## \u4F7F\u7528\u7EA6\u5B9A\n *\n * - **\u65B0\u589E\u6216\u4FEE\u6539\u5B57\u7B26\u4E32\u5B57\u9762\u91CF\u65F6\uFF0C\u5148\u5230\u8FD9\u91CC\u67E5\u627E/\u589E\u8865\uFF0C\u4E0D\u8981\u5728\u4E1A\u52A1\u6587\u4EF6\u91CC\u76F4\u63A5\u5199\n * \u9B54\u6CD5\u5B57\u7B26\u4E32\u3002** \u626B\u63CF\u6B8B\u7559\u5B57\u9762\u91CF\uFF1A`rg \"'(['a-z]+)'\" src/`\u3002\n * - **\u53EA\u7528\u4E8E\u7C7B\u578B\u4F4D\u7F6E\u7684\u5E38\u91CF**\uFF08\u5982 `PERSISTENCE_OPERATION`\uFF09\u5728\u8C03\u7528\u65B9\u7528\n * `import type`\uFF1B**\u5728\u8FD0\u884C\u65F6\u6BD4\u8F83/\u6784\u9020\u4E2D\u4F7F\u7528\u7684**\uFF08\u5982 `WORKER_STATUS.ERROR`\u3001\n * `TRACE_EVENT_TYPE.LIFECYCLE`\uFF09\u5FC5\u987B\u7528\u503C\u5BFC\u5165\uFF0C\u5426\u5219\u4F1A\u62A5 \"cannot be used as\n * a value because it was imported using 'import type'\"\u3002\n * - **\u503C\u6D3E\u751F\u7C7B\u578B**\uFF1A`export type WorkerStatus = (typeof WORKER_STATUS)[keyof\n * typeof WORKER_STATUS]`\u3002\u8FD9\u6837\u65B0\u589E\u679A\u4E3E\u5206\u652F\u65F6\u7C7B\u578B\u81EA\u52A8\u6536\u7A84\uFF0C\u7F16\u8BD1\u5668\u4F1A\u6307\u51FA\n * \u6BCF\u4E2A\u9057\u6F0F\u7684 switch \u5206\u652F\u3002\n * - \u5206\u7EC4\u7684\u987A\u5E8F\uFF08\u72B6\u6001 / \u89D2\u8272 / \u540E\u7AEF / \u6D88\u606F\u7C7B\u578B / \u679A\u4E3E / \u524D\u7F00\uFF09\u4FDD\u6301\u7A33\u5B9A\uFF0C\u65B9\u4FBF\n * \u7EF4\u62A4\u8005\u4E00\u773C\u5B9A\u4F4D\u3002\n */\n\n// ---- \u751F\u547D\u5468\u671F\u72B6\u6001 / \u89D2\u8272 / \u53EF\u89C1\u6027 ----\nexport const WORKER_STATUS = {\n CONNECTING: 'connecting',\n CONNECTED: 'connected',\n DISCONNECTED: 'disconnected',\n ERROR: 'error',\n} as const;\n\nexport const WORKER_ROLE = {\n ACTIVE: 'active',\n STANDBY: 'standby',\n} as const;\n\nexport const TAB_VISIBILITY = {\n VISIBLE: 'visible',\n HIDDEN: 'hidden',\n} as const;\n\n// ---- Worker \u540E\u7AEF\u6A21\u5F0F\u4E0E\u89E3\u6790\u7ED3\u679C\uFF08WorkerMode / WorkerBackend\uFF09----\nexport const WORKER_MODE = {\n DEDICATED: 'dedicated',\n SHARED: 'shared',\n AUTO: 'auto',\n} as const;\n\nexport const WORKER_BACKEND = {\n DEDICATED: 'dedicated',\n SHARED: 'shared',\n LOCAL: 'local',\n} as const;\n\n// ---- \u5B58\u50A8\u4E8B\u4EF6\u901A\u9053\u56DE\u9000 ----\nexport const CHANNEL_FALLBACK = {\n NONE: 'none',\n STORAGE_EVENT: 'storage-event',\n} as const;\n\n// ---- Worker / MessagePort / BroadcastChannel \u7684 EventTarget \u4E8B\u4EF6\u540D\u5224\u522B ----\n// MessageEvent.type\uFF08'message' / 'messageerror'\uFF09\u4E0E Worker \u4E0A\u7684 'error' \u4E8B\u4EF6\u540D\uFF0C\n// \u7528\u4E8E\u73AF\u5883\u9002\u914D\u5668\u63A5\u53E3\u7B7E\u540D\u4E0E\u6D4B\u8BD5\u66FF\u8EAB\u7684 addEventListener \u771F\u5047\u5206\u652F\u3002\nexport const EVENT_TYPE = {\n MESSAGE: 'message',\n MESSAGEERROR: 'messageerror',\n ERROR: 'error',\n} as const;\n\n// ---- \u63A7\u5236\u5E73\u9762\u52A8\u4F5C\uFF08WorkerControlAction\uFF09----\nexport const CONTROL_ACTION = {\n SUBSCRIBE: 'SUBSCRIBE',\n UNSUBSCRIBE: 'UNSUBSCRIBE',\n PUBLISH: 'PUBLISH',\n} as const;\n\n// ---- BroadcastChannel \u96C6\u7FA4\u6D88\u606F\u7C7B\u578B ----\nexport const CLUSTER_MESSAGE_TYPE = {\n CONTROL: 'CONTROL',\n EVENT: 'EVENT',\n REGISTRY: 'REGISTRY',\n ROUTE_RELEASED: 'ROUTE_RELEASED',\n} as const;\n\n// ---- \u547D\u540D\u7A7A\u95F4\u524D\u7F00\u4E0E\u4E8B\u4EF6\u540D ----\nexport const DEFAULT_STORAGE_PREFIX = 'cross-tab-worker-databus';\nexport const STORAGE_CHANNEL_PREFIX = `${DEFAULT_STORAGE_PREFIX}:channel:`;\nexport const TAB_ID_STORAGE_KEY = `${DEFAULT_STORAGE_PREFIX}:tab-id`;\nexport const PUBLICATION_EVENT = 'DATABUS_PUBLICATION';\n\n// ---- \u89C4\u8303 JSON \u4FE1\u5C01\u64CD\u4F5C\u7801 ----\nexport const PUBLICATION_ENVELOPE_OP = 'publication';\n\n// ---- trace \u4E8B\u4EF6\u7C7B\u578B ----\nexport const TRACE_EVENT_TYPE = {\n LIFECYCLE: 'lifecycle',\n STATUS: 'status',\n SUBSCRIPTION: 'subscription',\n COORDINATION: 'coordination',\n ERROR: 'error',\n RELIABILITY: 'reliability',\n MESSAGE_METRICS: 'message_metrics',\n} as const;\n\n// ---- trace \u751F\u547D\u5468\u671F\u52A8\u4F5C ----\nexport const TRACE_LIFECYCLE_ACTION = {\n START: 'start',\n STOP: 'stop',\n SUSPEND: 'suspend',\n RESUME: 'resume',\n} as const;\n\n// ---- \u5185\u90E8 handler \u8C03\u7528\u6807\u7B7E\uFF08\u9694\u79BB\u5F02\u5E38\u65F6\u7684\u5206\u7C7B\u6765\u6E90\uFF09----\nexport const INVOKE_LABEL = {\n DISPATCH: 'dispatch',\n STATUS: 'status',\n ERROR_HANDLER: 'error handler',\n} as const;\n\n// ---- trace \u6A21\u5F0F\uFF08DataBusTraceMode\uFF09----\nexport const TRACE_MODE = {\n EVENTS: 'events',\n METRICS: 'metrics',\n ALL: 'all',\n} as const;\n\n// ---- trace \u9519\u8BEF\u6765\u6E90 ----\nexport const TRACE_ERROR_SOURCE = {\n TRANSPORT: 'transport',\n OPERATION: 'operation',\n} as const;\n\n// ---- reliability \u8BCA\u65AD\u64CD\u4F5C ----\nexport const RELIABILITY_OPERATION = {\n TRANSPORT_RECOVERY: 'transport_recovery',\n ROUTE_ACK: 'route_ack',\n ROUTE_MIGRATION: 'route_migration',\n // A re-election that recovered a stranded unconfirmed handoff (previous\n // owner gone, ACK never arrived). Distinct from ROUTE_MIGRATION so trace\n // consumers can tell recoveries apart from routine graceful handoffs.\n ROUTE_MIGRATION_RECOVERY: 'route_migration_recovery',\n PERSISTENCE_CLEANUP: 'persistence_cleanup',\n PERSISTENCE_RETRY: 'persistence_retry',\n DEDUP_SUPPRESSED: 'dedup_suppressed',\n} as const;\n\n// ---- \u81EA\u52A8\u6062\u590D\u5C1D\u8BD5\u7ED3\u679C ----\nexport const RECOVERY_OUTCOME = {\n SCHEDULED: 'scheduled',\n SUCCEEDED: 'succeeded',\n FAILED: 'failed',\n EXHAUSTED: 'exhausted',\n} as const;\n\n// ---- \u8BA2\u9605\u52A8\u4F5C ----\nexport const SUBSCRIPTION_ACTION = {\n SUBSCRIBE: 'subscribe',\n UNSUBSCRIBE: 'unsubscribe',\n} as const;\n\n// ---- \u56DE\u653E\u4FEE\u526A\u7B56\u7565 ----\nexport const PRUNE_STRATEGY = {\n COUNT: 'count',\n AGE: 'age',\n BOTH: 'both',\n} as const;\n\n// ---- \u6301\u4E45\u5316\u64CD\u4F5C ----\nexport const PERSISTENCE_OPERATION = {\n LOAD: 'load',\n APPEND: 'append',\n CLEAR: 'clear',\n CLEAR_TOPIC: 'clearTopic',\n CLEAR_BEFORE: 'clearBefore',\n} as const;\n\n// ---- \u6545\u969C\u6765\u6E90\uFF08DataBusFailureSource\uFF09----\nexport const FAILURE_SOURCE = {\n TRANSPORT: 'transport',\n PERSISTENCE: 'persistence',\n DISPATCH: 'dispatch',\n} as const;\n\n// ---- \u5065\u5EB7\u5224\u5B9A\u72B6\u6001\uFF08DataBusHealthSummary['state']\uFF09----\nexport const HEALTH_STATE = {\n STOPPED: 'stopped',\n STARTING: 'starting',\n HEALTHY: 'healthy',\n RECOVERING: 'recovering',\n SUSPENDED: 'suspended',\n DEGRADED: 'degraded',\n} as const;\n\n// ---- WebSocket wire \u534F\u8BAE\u64CD\u4F5C\u7801 ----\nexport const WS_OP = {\n SUBSCRIBE: 'subscribe',\n UNSUBSCRIBE: 'unsubscribe',\n PUBLISH: 'publish',\n PUBLISH_BATCH: 'publishBatch',\n} as const;\n\n// ---- Centrifuge Worker \u8F93\u5165\u6D88\u606F\u7C7B\u578B ----\nexport const CENTRIFUGE_INPUT_TYPE = {\n INIT: 'INIT',\n SUBSCRIBE: 'SUBSCRIBE',\n UNSUBSCRIBE: 'UNSUBSCRIBE',\n PUBLISH: 'PUBLISH',\n PUBLISH_BIN: 'PUBLISH_BIN',\n PING: 'PING',\n STOP: 'STOP',\n TOKEN_RESPONSE: 'TOKEN_RESPONSE',\n TOKEN_ERROR: 'TOKEN_ERROR',\n} as const;\n\n// ---- Centrifuge Worker \u8F93\u51FA\u6D88\u606F\u7C7B\u578B ----\nexport const CENTRIFUGE_OUTPUT_TYPE = {\n STATUS: 'STATUS',\n MESSAGE: 'MESSAGE',\n MESSAGE_BIN: 'MESSAGE_BIN',\n ERROR: 'ERROR',\n TOKEN_REQUEST: 'TOKEN_REQUEST',\n SESSION_REAPED: 'SESSION_REAPED',\n} as const;\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,iBAA6E;;;ACsBtE,IAAM,gBAAgB;AAAA,EAC3B,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,cAAc;AAAA,EACd,OAAO;AACT;AAwDO,IAAM,yBAAyB;AAC/B,IAAM,yBAAyB,GAAG,sBAAsB;AACxD,IAAM,qBAAqB,GAAG,sBAAsB;;;ADhFpD,SAAS,mBACd,QACA,OAAsD,CAAC,GACV;AAC7C,QAAM,UAAM,uBAAmD,IAAI;AACnE,MAAI,WAAmD;AACvD,MAAI,sBAAsB;AAC1B,QAAM,OAAO,YAAY;AAAE,UAAM,UAAU;AAAU,eAAW;AAAM,QAAI,QAAQ;AAAM,QAAI,QAAS,OAAM,QAAQ,KAAK;AAAA,EAAG;AAC3H,QAAM,QAAQ,MAAM;AAClB,UAAM,aAAa,EAAE;AACrB,SAAK,KAAK,EAAE,KAAK,MAAM;AACrB,UAAI,eAAe,oBAAqB;AACxC,YAAM,OAAO,OAAO;AACpB,iBAAW;AACX,UAAI,QAAQ;AACZ,WAAK,KAAK,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAClC,CAAC;AAAA,EACH;AACA,4BAAU,KAAK;AACf,kCAAgB,MAAM;AAMpB,2BAAuB;AACvB,SAAK,KAAK;AAAA,EACZ,CAAC;AACD,MAAI,KAAK,SAAS,EAAG,uBAAM,MAAM,KAAK;AACtC,SAAO;AACT;AAEO,SAAS,wBACd,KAAkD,OAClD,SACM;AACN,MAAI,aAAqD;AACzD,MAAI,eAA8B;AAClC,MAAI;AACJ,QAAM,OAAO,MAAM;AAAE,cAAU;AAAG,cAAU;AAAW,iBAAa;AAAM,mBAAe;AAAA,EAAM;AAC/F,QAAM,OAAO,MAAM;AACjB,UAAM,UAAU,IAAI;AACpB,UAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC5D,QAAI,YAAY,cAAc,iBAAiB,aAAa,QAAS;AACrE,SAAK;AACL,QAAI,CAAC,QAAS;AACd,iBAAa;AACb,mBAAe;AACf,cAAU,QAAQ,UAAU,WAAW,OAAO;AAAA,EAChD;AACA,wBAAM,KAAK,MAAM,EAAE,WAAW,KAAK,CAAC;AACpC,MAAI,OAAO,UAAU,SAAU,uBAAM,OAAO,IAAI;AAChD,kCAAgB,IAAI;AACtB;AAEO,SAAS,kBACd,KACmB;AACnB,QAAM,aAAS,gBAAkB,cAAc,UAAU;AACzD,MAAI;AACJ,wBAAM,KAAK,UAAQ;AACjB,cAAU;AAAG,cAAU;AACvB,WAAO,QAAQ,MAAM,UAAU,KAAK,cAAc;AAClD,QAAI,KAAM,WAAU,KAAK,SAAS,WAAS;AAAE,aAAO,QAAQ;AAAA,IAAO,CAAC;AAAA,EACtE,GAAG,EAAE,WAAW,KAAK,CAAC;AACtB,kCAAgB,MAAM,UAAU,CAAC;AACjC,SAAO;AACT;AAeO,SAAS,kBACd,KACA,SACkC;AAClC,QAAM,aAAS,gBAAiC,IAAI;AACpD,MAAI,QAA+C;AACnD,MAAI,WAA8B,CAAC;AACnC,QAAM,WAAW,MAAM;AACrB,QAAI,MAAO,eAAc,KAAK;AAC9B,YAAQ;AACR,eAAW,WAAW,SAAU,SAAQ;AACxC,eAAW,CAAC;AAAA,EACd;AACA,wBAAM,CAAC,KAAK,MAAM,SAAS,UAAU,GAAG,CAAC,CAAC,IAAI,MAAM;AAClD,aAAS;AACT,QAAI,CAAC,MAAM;AACT,aAAO,QAAQ;AACf;AAAA,IACF;AACA,UAAM,UAAU,MAAM;AAAE,aAAO,QAAQ,KAAK,iBAAiB;AAAA,IAAG;AAChE,YAAQ;AACR,aAAS,KAAK,KAAK,SAAS,OAAO,CAAC;AACpC,aAAS,KAAK,KAAK,QAAQ,OAAO,CAAC;AACnC,UAAM,aAAa,SAAS,cAAc;AAC1C,QAAI,aAAa,EAAG,SAAQ,YAAY,SAAS,UAAU;AAAA,EAC7D,GAAG,EAAE,WAAW,KAAK,CAAC;AACtB,kCAAgB,QAAQ;AACxB,SAAO;AACT;",
6
6
  "names": []
7
7
  }
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WORKER_STATUS
3
- } from "./chunk-TZ7ZP7YD.js";
3
+ } from "./chunk-2TTJ7LGZ.js";
4
4
 
5
5
  // src/hooks.ts
6
6
  import { useEffect, useRef, useState } from "react";
package/dist/index.js CHANGED
@@ -20,13 +20,13 @@ import {
20
20
  selectRebalanceTarget,
21
21
  selectWorkerBackend,
22
22
  topicMatchesPattern
23
- } from "./chunk-DBKAWZBG.js";
23
+ } from "./chunk-75ZDU7VG.js";
24
24
  import {
25
25
  DEFAULT_STORAGE_PREFIX,
26
26
  PRUNE_STRATEGY,
27
27
  WORKER_STATUS,
28
28
  WS_OP
29
- } from "./chunk-TZ7ZP7YD.js";
29
+ } from "./chunk-2TTJ7LGZ.js";
30
30
 
31
31
  // src/core/replay-persistence.ts
32
32
  function createIndexedDbReplayPersistence(options) {
@@ -166,5 +166,6 @@ export declare const CENTRIFUGE_OUTPUT_TYPE: {
166
166
  readonly MESSAGE_BIN: "MESSAGE_BIN";
167
167
  readonly ERROR: "ERROR";
168
168
  readonly TOKEN_REQUEST: "TOKEN_REQUEST";
169
+ readonly SESSION_REAPED: "SESSION_REAPED";
169
170
  };
170
171
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/utils/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,eAAO,MAAM,WAAW;;;CAGd,CAAC;AAEX,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AAGX,eAAO,MAAM,WAAW;;;;CAId,CAAC;AAEX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,gBAAgB;;;CAGnB,CAAC;AAKX,eAAO,MAAM,UAAU;;;;CAIb,CAAC;AAGX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,oBAAoB;;;;;CAKvB,CAAC;AAGX,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AACjE,eAAO,MAAM,sBAAsB,sCAAuC,CAAC;AAC3E,eAAO,MAAM,kBAAkB,oCAAqC,CAAC;AACrE,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAGvD,eAAO,MAAM,uBAAuB,gBAAgB,CAAC;AAGrD,eAAO,MAAM,gBAAgB;;;;;;;;CAQnB,CAAC;AAGX,eAAO,MAAM,sBAAsB;;;;;CAKzB,CAAC;AAGX,eAAO,MAAM,YAAY;;;;CAIf,CAAC;AAGX,eAAO,MAAM,UAAU;;;;CAIb,CAAC;AAGX,eAAO,MAAM,kBAAkB;;;CAGrB,CAAC;AAGX,eAAO,MAAM,qBAAqB;;;;;;;;CAWxB,CAAC;AAGX,eAAO,MAAM,gBAAgB;;;;;CAKnB,CAAC;AAGX,eAAO,MAAM,mBAAmB;;;CAGtB,CAAC;AAGX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,qBAAqB;;;;;;CAMxB,CAAC;AAGX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,YAAY;;;;;;;CAOf,CAAC;AAGX,eAAO,MAAM,KAAK;;;;;CAKR,CAAC;AAGX,eAAO,MAAM,qBAAqB;;;;;;;;;;CAUxB,CAAC;AAGX,eAAO,MAAM,sBAAsB;;;;;;CAMzB,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/utils/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC;AAEX,eAAO,MAAM,WAAW;;;CAGd,CAAC;AAEX,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AAGX,eAAO,MAAM,WAAW;;;;CAId,CAAC;AAEX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,gBAAgB;;;CAGnB,CAAC;AAKX,eAAO,MAAM,UAAU;;;;CAIb,CAAC;AAGX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,oBAAoB;;;;;CAKvB,CAAC;AAGX,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AACjE,eAAO,MAAM,sBAAsB,sCAAuC,CAAC;AAC3E,eAAO,MAAM,kBAAkB,oCAAqC,CAAC;AACrE,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAGvD,eAAO,MAAM,uBAAuB,gBAAgB,CAAC;AAGrD,eAAO,MAAM,gBAAgB;;;;;;;;CAQnB,CAAC;AAGX,eAAO,MAAM,sBAAsB;;;;;CAKzB,CAAC;AAGX,eAAO,MAAM,YAAY;;;;CAIf,CAAC;AAGX,eAAO,MAAM,UAAU;;;;CAIb,CAAC;AAGX,eAAO,MAAM,kBAAkB;;;CAGrB,CAAC;AAGX,eAAO,MAAM,qBAAqB;;;;;;;;CAWxB,CAAC;AAGX,eAAO,MAAM,gBAAgB;;;;;CAKnB,CAAC;AAGX,eAAO,MAAM,mBAAmB;;;CAGtB,CAAC;AAGX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,qBAAqB;;;;;;CAMxB,CAAC;AAGX,eAAO,MAAM,cAAc;;;;CAIjB,CAAC;AAGX,eAAO,MAAM,YAAY;;;;;;;CAOf,CAAC;AAGX,eAAO,MAAM,KAAK;;;;;CAKR,CAAC;AAGX,eAAO,MAAM,qBAAqB;;;;;;;;;;CAUxB,CAAC;AAGX,eAAO,MAAM,sBAAsB;;;;;;;CAOzB,CAAC"}
package/dist/vue.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WORKER_STATUS
3
- } from "./chunk-TZ7ZP7YD.js";
3
+ } from "./chunk-2TTJ7LGZ.js";
4
4
 
5
5
  // src/vue.ts
6
6
  import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
@@ -14,8 +14,11 @@
14
14
  * The timers and clock are injected so the reaper can be unit-tested without a
15
15
  * real worker or DOM.
16
16
  */
17
- /** A reaped port: close it (stop message delivery) and stop its session. */
17
+ /** A reaped port: announce the loss, close it (stop message delivery), then stop
18
+ * its session. `notify` runs first and on its own because a port that is about to
19
+ * be closed is the only chance its owner gets to learn why. */
18
20
  export interface ReapTarget {
21
+ notify(): void;
19
22
  close(): void;
20
23
  stop(): void;
21
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"port-reaper.d.ts","sourceRoot":"","sources":["../../src/workers/port-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;CACd;AAED,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;AAC7D,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;IACnE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAiC;gBAGhC,GAAG,GAAE,MAAM,MAAiB,EAC7C,QAAQ,GAAE,QAA2E,EACrF,UAAU,GAAE,UAA4C;IAM1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC;;;oEAGgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAOrD;;qDAEiD;IACjD,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK9B;;;gFAG4E;IAC5E,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI;IAShE;;4EAEwE;IACxE,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAQ/B;2EACuE;IACvE,OAAO,IAAI,IAAI;IAgBf;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ;IAahB;;8DAE0D;IAC1D,OAAO,CAAC,mBAAmB;IAQ3B;;qEAEiE;IACjE,OAAO,CAAC,IAAI;CAmCb"}
1
+ {"version":3,"file":"port-reaper.d.ts","sourceRoot":"","sources":["../../src/workers/port-reaper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH;;+DAE+D;AAC/D,MAAM,WAAW,UAAU;IACzB,MAAM,IAAI,IAAI,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;CACd;AAED,KAAK,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;AAC7D,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;AAE3C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkC;IACnE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAiC;gBAGhC,GAAG,GAAE,MAAM,MAAiB,EAC7C,QAAQ,GAAE,QAA2E,EACrF,UAAU,GAAE,UAA4C;IAM1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IAExC;;;oEAGgE;IAChE,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IAOrD;;qDAEiD;IACjD,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAK9B;;;gFAG4E;IAC5E,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,GAAG,IAAI;IAShE;;4EAEwE;IACxE,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAQ/B;2EACuE;IACvE,OAAO,IAAI,IAAI;IAgBf;;;yEAGqE;IACrE,OAAO,CAAC,QAAQ;IAahB;;8DAE0D;IAC1D,OAAO,CAAC,mBAAmB;IAQ3B;;qEAEiE;IACjE,OAAO,CAAC,IAAI;CAoDb"}
@@ -41,7 +41,7 @@ graph TB
41
41
 
42
42
  By default, when `workerMode: 'dedicated'`, each Tab has its own dedicated transport Worker. When configured as `shared` or `auto` and the browser supports SharedWorker, same-origin tabs share the same SharedWorker; each connection port within the SharedWorker creates its own independent `CentrifugeSession`, so one Tab refreshing or stopping does not affect other Tabs. The `auto` mode degrades in order of **SharedWorker → Dedicated Worker → Local mode**, while the `dedicated` mode degrades in order of **Dedicated Worker → SharedWorker → Local mode**. `BroadcastChannel` is only responsible for control messages and real-time publication forwarding; localStorage is only responsible for eventually-consistent coordination metadata.
43
43
 
44
- Because `MessagePort` has no `close` event, a tab that crashes before sending `STOP` would otherwise leak its session and WebSocket. The main thread therefore sends a `PING` every 10 seconds, and the SharedWorker reaps any port that stays silent for more than 30 seconds, releasing the session and its subscriptions.
44
+ Because `MessagePort` has no `close` event, a tab that crashes before sending `STOP` would otherwise leak its session and WebSocket. The main thread therefore sends a `PING` every 10 seconds, and the SharedWorker reaps any port that stays silent for more than 30 seconds, releasing the session and its subscriptions. Silence is not the same as absence, though, so a reclaimed port is announced before it is closed: a tab whose heartbeat was merely starved (a long synchronous task, the throttling a backgrounded tab gets) learns its backend is gone, reports the failure, and rebuilds the session on a fresh port.
45
45
 
46
46
  ## Layers
47
47
 
@@ -2,19 +2,19 @@
2
2
 
3
3
  # Browser Benchmark Trend
4
4
 
5
- > Data through 2026-09-22, from the 56 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
5
+ > Data through 2026-09-22, from the 69 archived `bench-results/browser-*.json` reports (run `pnpm bench:browser` to add one; regenerate this doc with `node scripts/bench-trend.mjs`).
6
6
 
7
7
  The comparison baseline for release gating is `pnpm bench:compare --fail-above-pct 50`, which compares the newest report against the median of the same metric in the preceding reports (50% ceiling absorbs shared-runner noise; a single previous report was a fragile baseline because the in-page hot-path metrics alternate between a fast and a slow mode on identical code). This doc records the long-run picture: values are per-metric latencies where lower is better, and the last column is the best run inside the most recent 5 reports — not an all-time record, because the in-page matrix changed measurement semantics in early September 2026 and older reports are not comparable.
8
8
 
9
9
  <!-- BENCH-TREND:BEGIN (machine-generated table) -->
10
10
  | Metric | Previous (ms) | Latest (ms) | Δ | Best of last 5 runs (ms) |
11
11
  |---|---|---|---|---|
12
- | publish per-message (ms, lower is better) — dedicated | 51.9353 | 51.5536 | -0.38 | 40.0685 |
13
- | publish per-message (ms, lower is better) — shared | 40.9535 | 35.4994 | -5.45 | 33.7695 |
14
- | wildcard dispatch ×1000 (ms, lower is better) | 6.1 | 6.6 | +0.50 | 5.6 |
15
- | publishBatch ×1000 (ms, lower is better) | 4.3 | 3.8 | -0.50 | 3.5 |
16
- | dedup ×1000 (ms, lower is better) | 12.4 | 12.1 | -0.30 | 10.9 |
17
- | trace + publish ×1000 (ms, lower is better) | 4.8 | 5.4 | +0.60 | 4.7 |
12
+ | publish per-message (ms, lower is better) — dedicated | 80.1971 | 63.1886 | -17.01 | 62.3503 |
13
+ | publish per-message (ms, lower is better) — shared | 49.8516 | 54.1494 | +4.30 | 39.6914 |
14
+ | wildcard dispatch ×1000 (ms, lower is better) | 7.9 | 6.6 | -1.30 | 6.5 |
15
+ | publishBatch ×1000 (ms, lower is better) | 4.4 | 5 | +0.60 | 4.4 |
16
+ | dedup ×1000 (ms, lower is better) | 14 | 15.6 | +1.60 | 14 |
17
+ | trace + publish ×1000 (ms, lower is better) | 5.3 | 5.9 | +0.60 | 5.2 |
18
18
  | first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
19
19
  <!-- BENCH-TREND:END -->
20
20
 
@@ -213,11 +213,12 @@ A `MessagePort` has no `close` event, so the SharedWorker cannot know when a tab
213
213
  - **Heartbeat interval**: `heartbeatIntervalMs` (default `10000` ms). The main thread sends a `PING` on this cadence. Pass `Infinity` to disable heartbeats entirely — use this only when the SharedWorker reaper is not needed (e.g. the SharedWorker is guaranteed to be torn down with the tab).
214
214
  - **Session timeout**: `3 × heartbeatIntervalMs` (default `30000` ms). A port silent for longer than its timeout is reaped: its session is stopped and its WebSocket closed. This is distinct from the Core cluster heartbeat (default `3000` ms) which tracks worker liveness via localStorage — see the note below.
215
215
  - **Adaptive cadence**: the reaper runs at the smallest configured heartbeat interval across active ports, so a port with a short heartbeat is reaped promptly. When the last port disconnects, the reaper interval is cleared so a long-lived SharedWorker does not run a perpetual no-op interval between connection bursts.
216
+ - **The tab is told before the port goes**: a port that is being reclaimed receives one final `SESSION_REAPED` message, then is closed, then its session is stopped. The order is the point — a `MessagePort` has no close event, and posting into a port the other side closed succeeds while delivering nothing, so this is the only thing a starved tab can ever learn from. Without it a tab that was merely stalled rather than gone (a long synchronous task, or the throttling a backgrounded tab gets) kept its cluster role and its topic routes while every publication it sent vanished into the dead port, and `getHealthSummary()` went on reporting `healthy` / `connected`. On receiving the message the transport discards the backend and reports the failure through `onError` and an `error` status, which is what lets the normal recovery path rebuild the session on a fresh port. Measured in a real browser: after a 34 s stall the tab re-established its subscription on its own — no user gesture, no lifecycle event — and the replacement connection was under a second old when it was observed.
216
217
  - **Port closed before session stop**: when a port is reaped, the port is closed first and the session stopped after. Closing the port discards the session's `disconnected` status post (so it never reaches a live-but-slow main thread) and guarantees a closed port can never deliver a later message that would resurrect the session outside the reaper's tracking.
217
218
  - **Failure isolation**: both reaping and `dispose()` wrap `target.close()`/`target.stop()` in try-catch, so a single failing port cannot abort the reaping pass or leave subsequent dead tabs uncollected.
218
219
  - **Shutdown cleanup**: when the SharedWorker shuts down, `PortReaper.dispose()` stops the timer and closes/stops **every** still-tracked session, so no `CentrifugeSession` or WebSocket outlives the reaper. This complements per-port reaping, which only covers ports that went silent while the reaper was running.
219
220
 
220
- This is the mechanism that recovers sessions for tabs that crash without sending `STOP`. Lower `heartbeatIntervalMs` to reap dead sessions faster, at the cost of more frequent PING messages on the port.
221
+ This is the mechanism that recovers sessions for tabs that crash without sending `STOP`, and the announcement it makes is what lets a tab that did *not* crash notice and reconnect. Lower `heartbeatIntervalMs` to reap dead sessions faster, at the cost of more frequent PING messages on the port — and note that a value below roughly a third of the longest task your page runs will start reaping live tabs, each of which recovers by rebuilding its session.
221
222
 
222
223
  `heartbeatIntervalMs` must be a positive number or `Infinity`; a value of `0`, a negative number, or `NaN` causes the transport constructor to throw a `TypeError` immediately (such a value would otherwise degenerate `setInterval` into a 0ms busy loop).
223
224
 
package/docs/roadmap.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Roadmap
2
2
 
3
- 0.21.8 was released on September 23, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
3
+ 0.21.9 was released on September 23, 2026. The project is intentionally continuing through reliability-focused releases before a 1.0.0 stability freeze.
4
+
5
+ ## 0.21.9 delivered scope
6
+
7
+ - A SharedWorker tab whose main thread was starved — a long synchronous task, or the timer
8
+ throttling a backgrounded tab gets — used to lose its transport and never come back. The
9
+ SharedWorker's reaper reclaimed the silent port, and because a `MessagePort` has no close
10
+ event and a closed port accepts every post while delivering nothing, the tab went on owning
11
+ its routes and reporting `healthy` / `connected` while its publications vanished. The reaper
12
+ now announces the reclaim on the port before closing it, which the transport turns into an
13
+ ordinary backend loss so the existing recovery rebuilds the session. Measured in a real
14
+ browser both ways (desktop Chrome; on headless Chromium the reaper cannot fire while the
15
+ page is blocked, so the precondition does not occur there): rebuilt automatically in 36.7 s with the
16
+ announcement, and never within 90 s without it.
17
+ - Shared-mode owner migration had no browser coverage at all. Three shared tabs now pin that
18
+ closing the owner moves the single server-side subscription onto a connection that was
19
+ already open, and that the handover keeps delivering in both directions.
20
+ - No wire-format, storage-layout or cluster-protocol change: the added message travels between
21
+ a page and its own Worker, and a main thread that does not know the type ignores it.
4
22
 
5
23
  ## 0.21.8 delivered scope
6
24
 
@@ -96,7 +96,12 @@ reference shape, including:
96
96
  error handlers check it so late errors from a superseded Worker cannot
97
97
  corrupt the fresh session.
98
98
  - **SharedWorker heartbeat**: if you use a SharedWorker, send periodic PINGs
99
- so a `PortReaper` can reclaim dead-tab sessions.
99
+ so a `PortReaper` can reclaim dead-tab sessions, and handle the
100
+ `SESSION_REAPED` message the reaper posts before it closes a port. That
101
+ message is the only signal a starved-but-still-alive tab can receive (a
102
+ `MessagePort` has no close event, and posting into a closed one delivers
103
+ nothing), and this transport turns it into a reported backend failure so the
104
+ recovery path rebuilds the session.
100
105
 
101
106
  ### 4. Expose as a subpath
102
107
 
@@ -41,7 +41,7 @@ graph TB
41
41
 
42
42
  默认 `workerMode: 'dedicated'` 时,每个 Tab 使用独立的 transport Worker。配置为 `shared` 或 `auto` 且浏览器支持 SharedWorker 时,同源 Tab 复用同一个 SharedWorker;SharedWorker 内每个连接 port 各自维护独立的 `CentrifugeSession`,一个 Tab 刷新或停止不会影响其他 Tab。`auto` 模式按 **SharedWorker → Dedicated Worker → 主线程 WebSocket** 降级,`dedicated` 模式按 **Dedicated Worker → SharedWorker → 主线程 WebSocket** 降级。`BroadcastChannel` 只负责控制消息和实时 publication 转发;localStorage 只负责最终一致的协调元数据。
43
43
 
44
- 由于 `MessagePort` 没有 `close` 事件,Tab 崩溃且未发送 `STOP` 时会遗留 session 和 WebSocket。主线程因此每 10 秒发送一次 `PING`,SharedWorker 对超过 30 秒无消息的 port 执行回收,释放对应 session 及其订阅。
44
+ 由于 `MessagePort` 没有 `close` 事件,Tab 崩溃且未发送 `STOP` 时会遗留 session 和 WebSocket。主线程因此每 10 秒发送一次 `PING`,SharedWorker 对超过 30 秒无消息的 port 执行回收,释放对应 session 及其订阅。但“沉默”并不等于“不存在”,因此被回收的端口会在关闭前先收到一条通知:心跳只是被饿死(长同步任务、后台 Tab 受到的定时器节流)的 Tab 由此得知 backend 已失效,会上报该失败并在新端口上重建会话。
45
45
 
46
46
  ## 分层
47
47
 
@@ -2,19 +2,19 @@
2
2
 
3
3
  # 浏览器基准趋势
4
4
 
5
- > 数据截至 2026-09-22,基于 56 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
5
+ > 数据截至 2026-09-22,基于 69 份归档的 `bench-results/browser-*.json` 报告(运行 `pnpm bench:browser` 追加一份;用 `node scripts/bench-trend.mjs` 重新生成本文档)。
6
6
 
7
7
  发布门禁的对比基线是 `pnpm bench:compare --fail-above-pct 50`:将最新报告与其之前若干报告中同一指标的中位数比较(50% 上限用于吸收共享 runner 的噪声;此前只看"上一份报告"并不稳定,因为页内热路径指标在同一份代码上会在快/慢两档之间来回跳)。本文记录长期趋势:数值为逐指标延迟,越低越好;最后一列是最近 5 份报告内的最优值,而不是历史纪录——页内基准矩阵在 2026 年 9 月初变更过测量语义,更早的报告不可比。
8
8
 
9
9
  <!-- BENCH-TREND:BEGIN (machine-generated table) -->
10
10
  | 指标 | 上次 (ms) | 本次 (ms) | Δ | 近 5 次最优 (ms) |
11
11
  |---|---|---|---|---|
12
- | publish per-message (ms, lower is better) — dedicated | 51.9353 | 51.5536 | -0.38 | 40.0685 |
13
- | publish per-message (ms, lower is better) — shared | 40.9535 | 35.4994 | -5.45 | 33.7695 |
14
- | wildcard dispatch ×1000 (ms, lower is better) | 6.1 | 6.6 | +0.50 | 5.6 |
15
- | publishBatch ×1000 (ms, lower is better) | 4.3 | 3.8 | -0.50 | 3.5 |
16
- | dedup ×1000 (ms, lower is better) | 12.4 | 12.1 | -0.30 | 10.9 |
17
- | trace + publish ×1000 (ms, lower is better) | 4.8 | 5.4 | +0.60 | 4.7 |
12
+ | publish per-message (ms, lower is better) — dedicated | 80.1971 | 63.1886 | -17.01 | 62.3503 |
13
+ | publish per-message (ms, lower is better) — shared | 49.8516 | 54.1494 | +4.30 | 39.6914 |
14
+ | wildcard dispatch ×1000 (ms, lower is better) | 7.9 | 6.6 | -1.30 | 6.5 |
15
+ | publishBatch ×1000 (ms, lower is better) | 4.4 | 5 | +0.60 | 4.4 |
16
+ | dedup ×1000 (ms, lower is better) | 14 | 15.6 | +1.60 | 14 |
17
+ | trace + publish ×1000 (ms, lower is better) | 5.3 | 5.9 | +0.60 | 5.2 |
18
18
  | first-packet cold dispatch (ms, lower is better) | 0 | 0 | +0.00 | 0 |
19
19
  <!-- BENCH-TREND:END -->
20
20
 
@@ -206,11 +206,12 @@ storage 事件只在其他 document 中派发。因此同一 document 内共享
206
206
  - **心跳间隔**:`heartbeatIntervalMs`(默认 `10000` ms)。主线程按此间隔发送 `PING`。传 `Infinity` 完全禁用心跳——仅在确保 SharedWorker 会随 Tab 一起销毁时使用。
207
207
  - **会话超时**:`3 × heartbeatIntervalMs`(默认 `30000` ms)。超过超时未收到消息的端口会被回收:其会话停止,WebSocket 关闭。这与 Core 集群心跳(默认 `3000` ms,通过 localStorage 跟踪 worker 存活)相互独立——见下方说明。
208
208
  - **自适应频率**:回收器以所有活动端口中最小的心跳间隔运行,使短心跳端口的会话能被及时回收。当最后一个端口断开时,回收器定时器清除,避免长时间存在的 SharedWorker 在连接爆发间隙运行永久的空循环。
209
+ - **关闭端口前先通知 Tab**:被回收的端口会先收到最后一条 `SESSION_REAPED` 消息,然后才被关闭、其会话才被停止。顺序正是关键所在——`MessagePort` 没有 close 事件,而向一个对端已关闭的端口发送消息会“成功”却什么都不送达,这是被饿死的 Tab 唯一能得知真相的途径。此前,一个只是被长时间阻塞、并没有消失的 Tab(一个很长的同步任务,或后台 Tab 受到的定时器节流)会保留自己的集群角色与 topic 路由,而它发出的每条发布都消失在已关闭的端口里,`getHealthSummary()` 仍在报告 `healthy` / `connected`。收到该消息后,transport 会丢弃这个 backend,并通过 `onError` 与 `error` 状态报告失败,正常恢复路径随后在新端口上重建会话。真实浏览器实测:34 秒阻塞之后,Tab 自行重建了订阅——无需用户操作、无需生命周期事件——观察时这条替代连接建立尚不到一秒。
209
210
  - **先关闭端口再停止会话**:回收端口时,先关闭端口,再停止会话。关闭端口会丢弃会话的 `disconnected` 状态通知(使其不会到达可能仍在运行但缓慢的主线程),并保证已关闭的端口永远无法传递后续消息,从而在回收器追踪之外复活僵尸会话。
210
211
  - **失败隔离**:回收与 `dispose()` 都用 try-catch 包裹 `target.close()`/`target.stop()`,单个异常端口不会中断本轮回收,也不会让后续死 Tab 无人回收。
211
212
  - **关闭清理**:SharedWorker 关闭时,`PortReaper.dispose()` 停止定时器并关闭/停止**所有**仍被追踪的会话,确保没有任何 `CentrifugeSession` 或 WebSocket 比 reaper 活得更久。这补充了按端口回收——后者只覆盖 reaper 运行期间静默的端口。
212
213
 
213
- 这是从崩溃(未发送 `STOP`)的 Tab 中恢复会话的机制。降低 `heartbeatIntervalMs` 可更快回收死会话,代价是端口上更频繁的 PING 消息。
214
+ 这是从崩溃(未发送 `STOP`)的 Tab 中恢复会话的机制;而它发出的那条通知,正是没有崩溃、只是被饿死的 Tab 能够察觉并重连的原因。降低 `heartbeatIntervalMs` 可更快回收死会话,代价是端口上更频繁的 PING 消息——另外请注意:如果它的值低于页面最长任务耗时的约三分之一,活着的 Tab 也会被开始回收,而每一次回收都要靠重建会话来恢复。
214
215
 
215
216
  `heartbeatIntervalMs` 必须为正数或 `Infinity`;`0`、负数或 `NaN` 会导致 transport 构造函数立即抛出 `TypeError`(否则 `setInterval` 会降级为 0ms 忙循环)。
216
217
 
@@ -1,6 +1,18 @@
1
1
  # 路线图
2
2
 
3
- 0.21.8 已于 2026 年 9 月 23 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
3
+ 0.21.9 已于 2026 年 9 月 23 日发布。项目会先持续完成可靠性发布,再进入 1.0.0 稳定性冻结。
4
+
5
+ ## 0.21.9 已完成范围
6
+
7
+ - 被饿死的 SharedWorker Tab 曾经会静默丢失 transport 并且永不恢复:一个长同步任务,或后台
8
+ Tab 受到的定时器节流,都会让共享线程里的回收器收回该端口;而 `MessagePort` 没有 close
9
+ 事件、向已关闭端口发送消息会“成功”却什么都不送达,于是这个 Tab 仍在持有自己的路由、仍在
10
+ 报告 `healthy` / `connected`,而它发出的发布全部消失。现在回收器会在关闭端口前先在该端口上发出这次回收的通知,transport 把它当作一次普通的 backend 失效,于是既有的恢复流程会重建会话。
11
+ 真实浏览器两个方向都验证过:有通知时 36.7 秒自动重建,去掉通知后 90 秒内始终不恢复。
12
+ - 共享模式的所有权迁移此前完全没有浏览器级覆盖。现在三个共享 Tab 会固定住:关闭 owner 时,
13
+ 那条唯一的服务端订阅必须落到一条本来就早已打开的连接上,并且交接之后两个方向都仍能投递。
14
+ - 没有改动任何线格式、存储布局或集群协议版本:新增的消息只在页面与它自己的 Worker 之间流动,
15
+ 不认识该类型的主线程会直接忽略。
4
16
 
5
17
  ## 0.21.8 已完成范围
6
18
 
@@ -85,7 +85,10 @@ transport 选择后端(SharedWorker / Dedicated Worker / 本地),向它发
85
85
  - **generation 守卫**:创建后端时递增单调计数器;错误处理检查它,使被取代的
86
86
  Worker 的迟到错误不会污染新 session。
87
87
  - **SharedWorker 心跳**:若用 SharedWorker,定期发 PING,让 `PortReaper` 能
88
- 回收死 tab 的 session。
88
+ 回收死 tab 的 session,并处理回收器在关闭端口前发出的 `SESSION_REAPED`
89
+ 消息。对被饿死但仍存活的 tab 来说,那是唯一能收到的信号(`MessagePort` 没有
90
+ close 事件,向已关闭的端口发送消息什么都不会送达),本 transport 会把它转成
91
+ 一次上报的 backend 失败,让恢复流程得以重建 session。
89
92
 
90
93
  ### 4. 作为 subpath 暴露
91
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-tab-worker-databus",
3
- "version": "0.21.8",
3
+ "version": "0.21.9",
4
4
  "description": "Framework-agnostic cross-tab data bus with Dedicated/Shared Worker clustering and Centrifuge support.",
5
5
  "type": "module",
6
6
  "license": "MIT",