cross-tab-worker-databus 0.20.86 → 0.20.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-SDOV3UHG.js → chunk-ZNHJ5OMY.js} +126 -17
- package/dist/{chunk-SDOV3UHG.js.map → chunk-ZNHJ5OMY.js.map} +2 -2
- package/dist/cjs/centrifuge.cjs +125 -16
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/hooks.cjs +2 -2
- package/dist/cjs/hooks.cjs.map +2 -2
- package/dist/cjs/index.cjs +232 -36
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/cjs/vue.cjs +1 -1
- package/dist/cjs/vue.cjs.map +2 -2
- package/dist/core/data-bus.d.ts +28 -5
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -2
- package/dist/hooks.js.map +2 -2
- package/dist/index.js +108 -21
- package/dist/index.js.map +2 -2
- package/dist/vue.d.ts +2 -1
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +1 -1
- package/dist/vue.js.map +2 -2
- package/dist/websocket.d.ts +24 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/api.md +14 -9
- package/docs/architecture.md +6 -2
- package/docs/roadmap.md +8 -1
- package/docs/transports.md +19 -2
- package/docs/zh/api.md +14 -9
- package/docs/zh/architecture.md +6 -2
- package/docs/zh/roadmap.md +8 -1
- package/docs/zh/transports.md +14 -2
- package/package.json +3 -3
package/dist/cjs/hooks.cjs
CHANGED
|
@@ -79,6 +79,7 @@ function useCrossTabStatus(bus) {
|
|
|
79
79
|
}
|
|
80
80
|
function useCrossTabHealth(bus, options) {
|
|
81
81
|
const [health, setHealth] = (0, import_react.useState)(null);
|
|
82
|
+
const intervalMs = options?.intervalMs ?? 1e3;
|
|
82
83
|
(0, import_react.useEffect)(() => {
|
|
83
84
|
if (!bus) {
|
|
84
85
|
setHealth(null);
|
|
@@ -88,14 +89,13 @@ function useCrossTabHealth(bus, options) {
|
|
|
88
89
|
refresh();
|
|
89
90
|
const unsubscribeStatus = bus.onStatus(refresh);
|
|
90
91
|
const unsubscribeError = bus.onError(refresh);
|
|
91
|
-
const intervalMs = options?.intervalMs ?? 1e3;
|
|
92
92
|
const timer = intervalMs > 0 ? setInterval(refresh, intervalMs) : null;
|
|
93
93
|
return () => {
|
|
94
94
|
unsubscribeStatus();
|
|
95
95
|
unsubscribeError();
|
|
96
96
|
if (timer) clearInterval(timer);
|
|
97
97
|
};
|
|
98
|
-
}, [bus]);
|
|
98
|
+
}, [bus, intervalMs]);
|
|
99
99
|
return health;
|
|
100
100
|
}
|
|
101
101
|
//# sourceMappingURL=hooks.cjs.map
|
package/dist/cjs/hooks.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks.ts", "../../src/utils/constants.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * React hooks adapter for cross-tab-worker-databus.\n *\n * A thin, transport-agnostic bridge between the imperative CrossTabDataBus\n * API and React component lifecycles. React is an optional peer dependency \u2014\n * this module is a separate entry point so consumers who don't use React\n * never load it.\n *\n * - `useCrossTabDataBus` owns the bus lifecycle: created on mount, stopped on\n * unmount. It is StrictMode-safe: the double-invoked effect exercises the\n * same stop/recreate path as BFCache suspend/resume.\n * - `useCrossTabSubscription` attaches a message handler with automatic\n * cleanup; the handler is read through a ref, so you can pass inline\n * closures without resubscribing on every render.\n * - `useCrossTabStatus` mirrors `bus.onStatus()` into React state.\n * - `useCrossTabHealth` polls `bus.getHealthSummary()` into React state,\n * with event-driven refreshes on status changes and errors.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport type { DependencyList } from 'react';\nimport type { CrossTabDataBus, DataBusHealthSummary } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\nimport { WORKER_STATUS } from './utils/constants';\n\n/**\n * Create a CrossTabDataBus for the component's lifetime.\n *\n * @param create Factory invoked once per effect run. Return a fresh bus \u2014\n * do not share a bus instance between effects, or StrictMode's\n * mount \u2192 stop \u2192 mount cycle will stop the shared instance out from\n * under the second mount.\n * @param deps Re-create the bus when these change (default: create once).\n * @returns The active bus, or `null` before the first effect has run (SSR\n * and the initial render).\n */\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: DependencyList = []\n): CrossTabDataBus<TConfig, TData> | null {\n const [bus, setBus] = useState<CrossTabDataBus<TConfig, TData> | null>(null);\n const lifecycleGeneration = useRef(0);\n useEffect(() => {\n const generation = ++lifecycleGeneration.current;\n const instance = create();\n if (generation !== lifecycleGeneration.current) return;\n setBus(instance);\n void instance.ready().catch(() => {});\n return () => {\n if (generation === lifecycleGeneration.current) setBus(null);\n void instance.stop();\n };\n // The factory is intentionally not a dependency: callers pass an inline\n // closure and key recreation through `deps` instead.\n }, deps);\n return bus;\n}\n\n/**\n * Subscribe to `topic` for the component's lifetime. The handler is read\n * through a ref on each delivery, so inline closures are safe without\n * unsubscribing/resubscribing on re-renders.\n *\n * When `bus` is null (not yet created) the subscription is queued until the\n * bus appears \u2014 the bus itself queues it until the transport is ready.\n */\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null,\n topic: string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => {\n if (!bus) return;\n return bus.subscribe(topic, message => handlerRef.current(message));\n }, [bus, topic]);\n}\n\n/**\n * Mirror the bus connection status into React state. Reports the live value\n * via `onStatus` and reads the current value synchronously whenever `bus`\n * changes identity.\n */\nexport function useCrossTabStatus<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null\n): WorkerStatus {\n const [status, setStatus] = useState<WorkerStatus>(WORKER_STATUS.CONNECTING);\n useEffect(() => {\n if (!bus) {\n setStatus(WORKER_STATUS.CONNECTING);\n return;\n }\n setStatus(bus.getStatus());\n return bus.onStatus(setStatus);\n }, [bus]);\n return status;\n}\n\n/**\n * Mirror the bus health summary into React state.\n *\n * `getHealthSummary()` is a snapshot, not an event stream, so the hook polls\n * it on an interval (default 1000 ms) and refreshes immediately on status\n * changes and errors. Pass `intervalMs: 0` to rely on event-driven refreshes\n * only. Returns `null` while the bus has not been created yet.\n */\nexport function useCrossTabHealth<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null,\n options?: { intervalMs?: number }\n): DataBusHealthSummary | null {\n const [health, setHealth] = useState<DataBusHealthSummary | null>(null);\n useEffect(() => {\n if (!bus) {\n setHealth(null);\n return;\n }\n const refresh = () => setHealth(bus.getHealthSummary());\n refresh();\n const unsubscribeStatus = bus.onStatus(refresh);\n const unsubscribeError = bus.onError(refresh);\n const intervalMs = options?.intervalMs ?? 1_000;\n const timer = intervalMs > 0 ? setInterval(refresh, intervalMs) : null;\n return () => {\n unsubscribeStatus();\n unsubscribeError();\n if (timer) clearInterval(timer);\n };\n // The options object is intentionally not a dependency: callers pass an\n // inline literal and the interval only affects polling cadence.\n }, [bus]);\n return health;\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"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBA,mBAA4C;;;ACOrC,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;;;ADrDpD,SAAS,mBACd,QACA,OAAuB,CAAC,GACgB;AACxC,QAAM,CAAC,KAAK,MAAM,QAAI,uBAAiD,IAAI;AAC3E,QAAM,0BAAsB,qBAAO,CAAC;AACpC,8BAAU,MAAM;AACd,UAAM,aAAa,EAAE,oBAAoB;AACzC,UAAM,WAAW,OAAO;AACxB,QAAI,eAAe,oBAAoB,QAAS;AAChD,WAAO,QAAQ;AACf,SAAK,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACpC,WAAO,MAAM;AACX,UAAI,eAAe,oBAAoB,QAAS,QAAO,IAAI;AAC3D,WAAK,SAAS,KAAK;AAAA,IACrB;AAAA,EAGF,GAAG,IAAI;AACP,SAAO;AACT;AAUO,SAAS,wBACd,KACA,OACA,SACM;AACN,QAAM,iBAAa,qBAAO,OAAO;AACjC,aAAW,UAAU;AACrB,8BAAU,MAAM;AACd,QAAI,CAAC,IAAK;AACV,WAAO,IAAI,UAAU,OAAO,aAAW,WAAW,QAAQ,OAAO,CAAC;AAAA,EACpE,GAAG,CAAC,KAAK,KAAK,CAAC;AACjB;AAOO,SAAS,kBACd,KACc;AACd,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAuB,cAAc,UAAU;AAC3E,8BAAU,MAAM;AACd,QAAI,CAAC,KAAK;AACR,gBAAU,cAAc,UAAU;AAClC;AAAA,IACF;AACA,cAAU,IAAI,UAAU,CAAC;AACzB,WAAO,IAAI,SAAS,SAAS;AAAA,EAC/B,GAAG,CAAC,GAAG,CAAC;AACR,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["/**\n * React hooks adapter for cross-tab-worker-databus.\n *\n * A thin, transport-agnostic bridge between the imperative CrossTabDataBus\n * API and React component lifecycles. React is an optional peer dependency \u2014\n * this module is a separate entry point so consumers who don't use React\n * never load it.\n *\n * - `useCrossTabDataBus` owns the bus lifecycle: created on mount, stopped on\n * unmount. It is StrictMode-safe: the double-invoked effect exercises the\n * same stop/recreate path as BFCache suspend/resume.\n * - `useCrossTabSubscription` attaches a message handler with automatic\n * cleanup; the handler is read through a ref, so you can pass inline\n * closures without resubscribing on every render.\n * - `useCrossTabStatus` mirrors `bus.onStatus()` into React state.\n * - `useCrossTabHealth` polls `bus.getHealthSummary()` into React state,\n * with event-driven refreshes on status changes and errors.\n */\nimport { useEffect, useRef, useState } from 'react';\nimport type { DependencyList } from 'react';\nimport type { CrossTabDataBus, DataBusHealthSummary } from './core/data-bus';\nimport type { DataBusMessage, WorkerStatus } from './core/types';\nimport { WORKER_STATUS } from './utils/constants';\n\n/**\n * Create a CrossTabDataBus for the component's lifetime.\n *\n * @param create Factory invoked once per effect run. Return a fresh bus \u2014\n * do not share a bus instance between effects, or StrictMode's\n * mount \u2192 stop \u2192 mount cycle will stop the shared instance out from\n * under the second mount.\n * @param deps Re-create the bus when these change (default: create once).\n * @returns The active bus, or `null` before the first effect has run (SSR\n * and the initial render).\n */\nexport function useCrossTabDataBus<TConfig, TData>(\n create: () => CrossTabDataBus<TConfig, TData>,\n deps: DependencyList = []\n): CrossTabDataBus<TConfig, TData> | null {\n const [bus, setBus] = useState<CrossTabDataBus<TConfig, TData> | null>(null);\n const lifecycleGeneration = useRef(0);\n useEffect(() => {\n const generation = ++lifecycleGeneration.current;\n const instance = create();\n if (generation !== lifecycleGeneration.current) return;\n setBus(instance);\n void instance.ready().catch(() => {});\n return () => {\n if (generation === lifecycleGeneration.current) setBus(null);\n void instance.stop();\n };\n // The factory is intentionally not a dependency: callers pass an inline\n // closure and key recreation through `deps` instead.\n }, deps);\n return bus;\n}\n\n/**\n * Subscribe to `topic` for the component's lifetime. The handler is read\n * through a ref on each delivery, so inline closures are safe without\n * unsubscribing/resubscribing on re-renders.\n *\n * When `bus` is null (not yet created) the subscription is queued until the\n * bus appears \u2014 the bus itself queues it until the transport is ready.\n */\nexport function useCrossTabSubscription<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null,\n topic: string,\n handler: (message: DataBusMessage<TData>) => void\n): void {\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n useEffect(() => {\n if (!bus) return;\n return bus.subscribe(topic, message => handlerRef.current(message));\n }, [bus, topic]);\n}\n\n/**\n * Mirror the bus connection status into React state. Reports the live value\n * via `onStatus` and reads the current value synchronously whenever `bus`\n * changes identity.\n */\nexport function useCrossTabStatus<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null\n): WorkerStatus {\n const [status, setStatus] = useState<WorkerStatus>(WORKER_STATUS.CONNECTING);\n useEffect(() => {\n if (!bus) {\n setStatus(WORKER_STATUS.CONNECTING);\n return;\n }\n setStatus(bus.getStatus());\n return bus.onStatus(setStatus);\n }, [bus]);\n return status;\n}\n\n/**\n * Mirror the bus health summary into React state.\n *\n * `getHealthSummary()` is a snapshot, not an event stream, so the hook polls\n * it on an interval (default 1000 ms) and refreshes immediately on status\n * changes and errors. Pass `intervalMs: 0` to rely on event-driven refreshes\n * only; changing the interval replaces the timer without recreating the bus.\n * Returns `null` while the bus has not been created yet.\n */\nexport function useCrossTabHealth<TConfig, TData>(\n bus: CrossTabDataBus<TConfig, TData> | null,\n options?: { intervalMs?: number }\n): DataBusHealthSummary | null {\n const [health, setHealth] = useState<DataBusHealthSummary | null>(null);\n const intervalMs = options?.intervalMs ?? 1_000;\n useEffect(() => {\n if (!bus) {\n setHealth(null);\n return;\n }\n const refresh = () => setHealth(bus.getHealthSummary());\n refresh();\n const unsubscribeStatus = bus.onStatus(refresh);\n const unsubscribeError = bus.onError(refresh);\n const timer = intervalMs > 0 ? setInterval(refresh, intervalMs) : null;\n return () => {\n unsubscribeStatus();\n unsubscribeError();\n if (timer) clearInterval(timer);\n };\n // Depend on the normalized cadence rather than the options object so inline\n // option literals do not restart the effect on every render, while a real\n // intervalMs change still replaces the polling timer.\n }, [bus, intervalMs]);\n return health;\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"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBA,mBAA4C;;;ACOrC,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;;;ADrDpD,SAAS,mBACd,QACA,OAAuB,CAAC,GACgB;AACxC,QAAM,CAAC,KAAK,MAAM,QAAI,uBAAiD,IAAI;AAC3E,QAAM,0BAAsB,qBAAO,CAAC;AACpC,8BAAU,MAAM;AACd,UAAM,aAAa,EAAE,oBAAoB;AACzC,UAAM,WAAW,OAAO;AACxB,QAAI,eAAe,oBAAoB,QAAS;AAChD,WAAO,QAAQ;AACf,SAAK,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACpC,WAAO,MAAM;AACX,UAAI,eAAe,oBAAoB,QAAS,QAAO,IAAI;AAC3D,WAAK,SAAS,KAAK;AAAA,IACrB;AAAA,EAGF,GAAG,IAAI;AACP,SAAO;AACT;AAUO,SAAS,wBACd,KACA,OACA,SACM;AACN,QAAM,iBAAa,qBAAO,OAAO;AACjC,aAAW,UAAU;AACrB,8BAAU,MAAM;AACd,QAAI,CAAC,IAAK;AACV,WAAO,IAAI,UAAU,OAAO,aAAW,WAAW,QAAQ,OAAO,CAAC;AAAA,EACpE,GAAG,CAAC,KAAK,KAAK,CAAC;AACjB;AAOO,SAAS,kBACd,KACc;AACd,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAuB,cAAc,UAAU;AAC3E,8BAAU,MAAM;AACd,QAAI,CAAC,KAAK;AACR,gBAAU,cAAc,UAAU;AAClC;AAAA,IACF;AACA,cAAU,IAAI,UAAU,CAAC;AACzB,WAAO,IAAI,SAAS,SAAS;AAAA,EAC/B,GAAG,CAAC,GAAG,CAAC;AACR,SAAO;AACT;AAWO,SAAS,kBACd,KACA,SAC6B;AAC7B,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAsC,IAAI;AACtE,QAAM,aAAa,SAAS,cAAc;AAC1C,8BAAU,MAAM;AACd,QAAI,CAAC,KAAK;AACR,gBAAU,IAAI;AACd;AAAA,IACF;AACA,UAAM,UAAU,MAAM,UAAU,IAAI,iBAAiB,CAAC;AACtD,YAAQ;AACR,UAAM,oBAAoB,IAAI,SAAS,OAAO;AAC9C,UAAM,mBAAmB,IAAI,QAAQ,OAAO;AAC5C,UAAM,QAAQ,aAAa,IAAI,YAAY,SAAS,UAAU,IAAI;AAClE,WAAO,MAAM;AACX,wBAAkB;AAClB,uBAAiB;AACjB,UAAI,MAAO,eAAc,KAAK;AAAA,IAChC;AAAA,EAIF,GAAG,CAAC,KAAK,UAAU,CAAC;AACpB,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -2329,7 +2329,7 @@ var DedupManager = class {
|
|
|
2329
2329
|
};
|
|
2330
2330
|
|
|
2331
2331
|
// src/core/version.ts
|
|
2332
|
-
var SDK_VERSION = true ? "0.20.
|
|
2332
|
+
var SDK_VERSION = true ? "0.20.87" : "";
|
|
2333
2333
|
|
|
2334
2334
|
// src/core/data-bus.ts
|
|
2335
2335
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2354,6 +2354,11 @@ var CrossTabDataBus = class {
|
|
|
2354
2354
|
started = false;
|
|
2355
2355
|
stopping = false;
|
|
2356
2356
|
transportReady = false;
|
|
2357
|
+
// Whether the installed transport has reported `connected` at least once
|
|
2358
|
+
// since the current open began. A clean `disconnected` after this point is
|
|
2359
|
+
// a lost working connection, not the pre-connect window of a worker-style
|
|
2360
|
+
// backend whose start() resolves before it reports the connection.
|
|
2361
|
+
transportHasConnected = false;
|
|
2357
2362
|
// Last transport failure, retained so ready() can surface it to callers who
|
|
2358
2363
|
// never awaited start() directly. Cleared on the next successful start.
|
|
2359
2364
|
lastError = null;
|
|
@@ -2392,6 +2397,19 @@ var CrossTabDataBus = class {
|
|
|
2392
2397
|
// a transport reopen succeeds so traces can correlate repeated failures.
|
|
2393
2398
|
recoveryAttempt = 0;
|
|
2394
2399
|
recoveryExhausted = false;
|
|
2400
|
+
// Gate that holds transport operations issued after a runtime `error` until
|
|
2401
|
+
// the scheduled recovery attempt has actually run. Without it, a dead
|
|
2402
|
+
// transport still has `transportReady === true` during the cooldown, so
|
|
2403
|
+
// publishes/subscribes would be written to the failed connection and lost.
|
|
2404
|
+
recoveryGate = null;
|
|
2405
|
+
recoveryGateRelease = null;
|
|
2406
|
+
recoveryTimer = null;
|
|
2407
|
+
recoveryTimerToken = 0;
|
|
2408
|
+
// Once an automatic attempt fails, an explicit transport operation may
|
|
2409
|
+
// recover immediately instead of waiting for the next paced attempt. The
|
|
2410
|
+
// gate still stays closed so the operation cannot reach the failed
|
|
2411
|
+
// transport; it is released by the successful on-demand reopen.
|
|
2412
|
+
recoveryDemandAllowed = false;
|
|
2395
2413
|
/** Monotonic generation incremented on every successful transport open.
|
|
2396
2414
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2397
2415
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2610,8 +2628,38 @@ var CrossTabDataBus = class {
|
|
|
2610
2628
|
this.queuedStart = queued;
|
|
2611
2629
|
return queued;
|
|
2612
2630
|
}
|
|
2631
|
+
/** Release every operation waiting on the scheduled recovery attempt. */
|
|
2632
|
+
releaseRecoveryGate() {
|
|
2633
|
+
const release = this.recoveryGateRelease;
|
|
2634
|
+
this.recoveryGate = null;
|
|
2635
|
+
this.recoveryGateRelease = null;
|
|
2636
|
+
this.recoveryDemandAllowed = false;
|
|
2637
|
+
release?.();
|
|
2638
|
+
}
|
|
2639
|
+
/** Cancel a pending automatic retry when an explicit lifecycle transition
|
|
2640
|
+
* supersedes it. The released gate re-enters runTransport(), which then
|
|
2641
|
+
* follows the newest start/stop/suspend intent. */
|
|
2642
|
+
cancelScheduledRecovery() {
|
|
2643
|
+
this.recoveryTimerToken += 1;
|
|
2644
|
+
if (this.recoveryTimer !== null) {
|
|
2645
|
+
clearTimeout(this.recoveryTimer);
|
|
2646
|
+
this.recoveryTimer = null;
|
|
2647
|
+
}
|
|
2648
|
+
this.releaseRecoveryGate();
|
|
2649
|
+
}
|
|
2650
|
+
/** Keep the recovery gate closed after a failed attempt while allowing the
|
|
2651
|
+
* next explicit transport operation to start an immediate on-demand reopen.
|
|
2652
|
+
* If no gate/successor retry remains, release any waiters. */
|
|
2653
|
+
allowDemandRecovery() {
|
|
2654
|
+
if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
|
|
2655
|
+
this.recoveryDemandAllowed = true;
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
this.releaseRecoveryGate();
|
|
2659
|
+
}
|
|
2613
2660
|
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2614
2661
|
resetFailureState() {
|
|
2662
|
+
this.cancelScheduledRecovery();
|
|
2615
2663
|
this.lastError = null;
|
|
2616
2664
|
this.lastErrorAt = null;
|
|
2617
2665
|
this.lastFailure = null;
|
|
@@ -2634,6 +2682,7 @@ var CrossTabDataBus = class {
|
|
|
2634
2682
|
return before.catch(() => void 0).then(() => {
|
|
2635
2683
|
if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
|
|
2636
2684
|
if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
|
|
2685
|
+
this.transportHasConnected = false;
|
|
2637
2686
|
return Promise.resolve(
|
|
2638
2687
|
this.transport.start(config, {
|
|
2639
2688
|
onMessage: (message) => {
|
|
@@ -2655,6 +2704,7 @@ var CrossTabDataBus = class {
|
|
|
2655
2704
|
this.recoveryGeneration += 1;
|
|
2656
2705
|
this.lastSuccessAt = this.now();
|
|
2657
2706
|
this.transportReady = true;
|
|
2707
|
+
this.releaseRecoveryGate();
|
|
2658
2708
|
}
|
|
2659
2709
|
});
|
|
2660
2710
|
}).catch((error) => {
|
|
@@ -2663,11 +2713,9 @@ var CrossTabDataBus = class {
|
|
|
2663
2713
|
if (!this.pendingStop) {
|
|
2664
2714
|
this.pendingStop = this.createStopPromise();
|
|
2665
2715
|
}
|
|
2716
|
+
this.transportReady = false;
|
|
2666
2717
|
this.updateStatus(WORKER_STATUS.ERROR);
|
|
2667
2718
|
this.reportError(error);
|
|
2668
|
-
this.lastError = error;
|
|
2669
|
-
this.lastErrorAt = this.now();
|
|
2670
|
-
this.transportReady = false;
|
|
2671
2719
|
if (stopClusterOnFailure) {
|
|
2672
2720
|
this.stopping = true;
|
|
2673
2721
|
this.cluster.stop();
|
|
@@ -2681,7 +2729,10 @@ var CrossTabDataBus = class {
|
|
|
2681
2729
|
* Returns a rejected promise when the transport has failed and no start is in
|
|
2682
2730
|
* flight — the caller can retry by calling start() or ready() again. While an
|
|
2683
2731
|
* explicit stop() is settling, this rejects unless a restart is queued behind
|
|
2684
|
-
* it; false readiness during teardown is never reported.
|
|
2732
|
+
* it; false readiness during teardown is never reported. While the tab is
|
|
2733
|
+
* BFCache-suspended (pagehide without a following pageshow), this also
|
|
2734
|
+
* rejects: the suspended start promise is the transport-stop gate, not a
|
|
2735
|
+
* readiness signal.
|
|
2685
2736
|
*/
|
|
2686
2737
|
ready() {
|
|
2687
2738
|
if (this.queuedStart) return this.getQueuedStartReady();
|
|
@@ -2690,6 +2741,11 @@ var CrossTabDataBus = class {
|
|
|
2690
2741
|
"CrossTabDataBus is stopping; ready() cannot report readiness until stop() resolves. Wait for stop() to resolve, then call start() before awaiting ready()."
|
|
2691
2742
|
));
|
|
2692
2743
|
}
|
|
2744
|
+
if (this.suspended) {
|
|
2745
|
+
return Promise.reject(new Error(
|
|
2746
|
+
"CrossTabDataBus is suspended; ready() cannot report readiness until pageshow resumes the transport."
|
|
2747
|
+
));
|
|
2748
|
+
}
|
|
2693
2749
|
if (!this.started && !this.hasInitialConfig && this.lastError !== null) {
|
|
2694
2750
|
return Promise.reject(this.lastError);
|
|
2695
2751
|
}
|
|
@@ -2825,11 +2881,15 @@ var CrossTabDataBus = class {
|
|
|
2825
2881
|
getStatus() {
|
|
2826
2882
|
return this.status;
|
|
2827
2883
|
}
|
|
2828
|
-
/** Return the current automatic transport recovery state. `hasError` means a transport error is currently retained. */
|
|
2829
2884
|
/** Return the current automatic transport recovery state plus diagnostics.
|
|
2830
|
-
* `
|
|
2831
|
-
*
|
|
2832
|
-
*
|
|
2885
|
+
* `hasError`/`errorMessage`/`errorAt` describe the most recent retained
|
|
2886
|
+
* *transport* failure — from a transport open or a runtime `onError`. They
|
|
2887
|
+
* share the lifetime of the unified `lastFailure` ledger: a successful
|
|
2888
|
+
* recovery keeps the last failure visible, and only an explicit `start()`
|
|
2889
|
+
* clears it. `generation` increments on every successful transport open
|
|
2890
|
+
* (initial start and every recovery); `lastSuccessAt` is the timestamp of
|
|
2891
|
+
* the most recent successful open, or `null` until the transport reaches
|
|
2892
|
+
* `ready`. */
|
|
2833
2893
|
getRecoveryStats() {
|
|
2834
2894
|
const errorMessage = this.lastError instanceof Error ? this.lastError.message : this.lastError === null ? null : String(this.lastError);
|
|
2835
2895
|
return {
|
|
@@ -2856,7 +2916,7 @@ var CrossTabDataBus = class {
|
|
|
2856
2916
|
* unified failure ledger and recovery context that explains the verdict. */
|
|
2857
2917
|
getHealthSummary() {
|
|
2858
2918
|
const transport = this.transport;
|
|
2859
|
-
const transportDown =
|
|
2919
|
+
const transportDown = this.status !== WORKER_STATUS.CONNECTED;
|
|
2860
2920
|
const state = !this.started ? HEALTH_STATE.STOPPED : this.suspended ? HEALTH_STATE.SUSPENDED : transportDown ? this.recoveryExhausted ? HEALTH_STATE.DEGRADED : this.status === WORKER_STATUS.CONNECTING && this.recoveryAttempt === 0 ? HEALTH_STATE.STARTING : HEALTH_STATE.RECOVERING : HEALTH_STATE.HEALTHY;
|
|
2861
2921
|
return {
|
|
2862
2922
|
healthy: state === HEALTH_STATE.HEALTHY,
|
|
@@ -2947,6 +3007,7 @@ var CrossTabDataBus = class {
|
|
|
2947
3007
|
async performStop() {
|
|
2948
3008
|
this.lifecycleEpoch += 1;
|
|
2949
3009
|
this.stopping = true;
|
|
3010
|
+
this.cancelScheduledRecovery();
|
|
2950
3011
|
this.replayManager.suspend();
|
|
2951
3012
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
2952
3013
|
this.trace.stop();
|
|
@@ -2959,6 +3020,8 @@ var CrossTabDataBus = class {
|
|
|
2959
3020
|
const pendingStop = this.pendingStop;
|
|
2960
3021
|
if (pendingStop) await pendingStop.catch(() => void 0);
|
|
2961
3022
|
else await this.transport.stop();
|
|
3023
|
+
} catch (error) {
|
|
3024
|
+
this.reportError(error);
|
|
2962
3025
|
} finally {
|
|
2963
3026
|
this.transportSubscribedTopics.clear();
|
|
2964
3027
|
this.resetDedup();
|
|
@@ -3024,10 +3087,12 @@ var CrossTabDataBus = class {
|
|
|
3024
3087
|
updateStatus(status) {
|
|
3025
3088
|
const previousStatus = this.status;
|
|
3026
3089
|
this.status = status;
|
|
3090
|
+
if (status === WORKER_STATUS.CONNECTED) this.transportHasConnected = true;
|
|
3027
3091
|
if (previousStatus !== status) this.trace.event({ type: TRACE_EVENT_TYPE.STATUS, status });
|
|
3028
3092
|
this.cluster.setStatus(status);
|
|
3029
3093
|
if (status === WORKER_STATUS.DISCONNECTED || status === WORKER_STATUS.ERROR) this.transportSubscribedTopics.clear();
|
|
3030
3094
|
if (status === WORKER_STATUS.CONNECTED && previousStatus !== WORKER_STATUS.CONNECTED) {
|
|
3095
|
+
if (this.transportReady) this.releaseRecoveryGate();
|
|
3031
3096
|
for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
|
|
3032
3097
|
}
|
|
3033
3098
|
if (status === WORKER_STATUS.ERROR && this.started && !this.stopping) {
|
|
@@ -3040,23 +3105,49 @@ var CrossTabDataBus = class {
|
|
|
3040
3105
|
this.recoveryExhausted = true;
|
|
3041
3106
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: this.recoveryMaxAttempts, outcome: RECOVERY_OUTCOME.EXHAUSTED });
|
|
3042
3107
|
}
|
|
3108
|
+
this.releaseRecoveryGate();
|
|
3043
3109
|
return;
|
|
3044
3110
|
}
|
|
3045
3111
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt, outcome: RECOVERY_OUTCOME.SCHEDULED });
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3112
|
+
if (this.recoveryGate === null) {
|
|
3113
|
+
let release;
|
|
3114
|
+
this.recoveryGate = new Promise((resolve) => {
|
|
3115
|
+
release = resolve;
|
|
3116
|
+
});
|
|
3117
|
+
this.recoveryGateRelease = release;
|
|
3118
|
+
}
|
|
3119
|
+
this.recoveryDemandAllowed = false;
|
|
3120
|
+
const timerToken = ++this.recoveryTimerToken;
|
|
3121
|
+
this.recoveryTimer = setTimeout(() => {
|
|
3122
|
+
if (timerToken !== this.recoveryTimerToken) return;
|
|
3123
|
+
this.recoveryTimer = null;
|
|
3124
|
+
if (this.stopping || !this.started || this.suspended || this.status !== WORKER_STATUS.ERROR) {
|
|
3125
|
+
this.releaseRecoveryGate();
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
this.recoveryDemandAllowed = false;
|
|
3129
|
+
const opening = this.reopenTransport(attempt);
|
|
3130
|
+
void opening.then(
|
|
3131
|
+
() => this.releaseRecoveryGate(),
|
|
3132
|
+
() => this.allowDemandRecovery()
|
|
3133
|
+
);
|
|
3050
3134
|
}, this.recoveryCooldownMs);
|
|
3051
3135
|
}
|
|
3136
|
+
} else if (status === WORKER_STATUS.ERROR) {
|
|
3137
|
+
this.releaseRecoveryGate();
|
|
3052
3138
|
}
|
|
3053
3139
|
this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
|
|
3054
3140
|
}
|
|
3055
3141
|
reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
|
|
3142
|
+
const at = this.now();
|
|
3143
|
+
if (source === FAILURE_SOURCE.TRANSPORT) {
|
|
3144
|
+
this.lastError = error;
|
|
3145
|
+
this.lastErrorAt = at;
|
|
3146
|
+
}
|
|
3056
3147
|
this.lastFailure = {
|
|
3057
3148
|
source,
|
|
3058
3149
|
message: error instanceof Error ? error.message : String(error),
|
|
3059
|
-
at
|
|
3150
|
+
at
|
|
3060
3151
|
};
|
|
3061
3152
|
if (source === FAILURE_SOURCE.PERSISTENCE) {
|
|
3062
3153
|
this.persistenceFailureCount += 1;
|
|
@@ -3139,6 +3230,7 @@ var CrossTabDataBus = class {
|
|
|
3139
3230
|
if (this.stopping) return;
|
|
3140
3231
|
this.lifecycleEpoch += 1;
|
|
3141
3232
|
this.suspended = true;
|
|
3233
|
+
this.cancelScheduledRecovery();
|
|
3142
3234
|
this.transportReady = false;
|
|
3143
3235
|
this.transportSubscribedTopics.clear();
|
|
3144
3236
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
@@ -3208,7 +3300,24 @@ var CrossTabDataBus = class {
|
|
|
3208
3300
|
*/
|
|
3209
3301
|
runTransport(operation) {
|
|
3210
3302
|
if (this.suspended) return;
|
|
3211
|
-
if (this.
|
|
3303
|
+
if (this.recoveryGate && !this.stopping) {
|
|
3304
|
+
if (this.recoveryDemandAllowed && this.status === WORKER_STATUS.ERROR && !this.suspended) {
|
|
3305
|
+
this.recoveryDemandAllowed = false;
|
|
3306
|
+
const opening = this.reopenTransport();
|
|
3307
|
+
void opening.then(
|
|
3308
|
+
() => this.releaseRecoveryGate(),
|
|
3309
|
+
() => this.allowDemandRecovery()
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
const gate = this.recoveryGate;
|
|
3313
|
+
void gate.then(() => {
|
|
3314
|
+
if (this.stopping || this.suspended) return;
|
|
3315
|
+
this.runTransport(operation);
|
|
3316
|
+
});
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
const droppedAfterConnect = this.transportHasConnected && this.status === WORKER_STATUS.DISCONNECTED;
|
|
3320
|
+
if (this.transportReady && this.status !== WORKER_STATUS.ERROR && !droppedAfterConnect && !this.stopping) {
|
|
3212
3321
|
try {
|
|
3213
3322
|
void Promise.resolve(operation()).catch((error) => this.reportError(error));
|
|
3214
3323
|
} catch (error) {
|
|
@@ -3562,6 +3671,7 @@ function parseDataBusPublication(value, fallbackTopic) {
|
|
|
3562
3671
|
}
|
|
3563
3672
|
|
|
3564
3673
|
// src/websocket.ts
|
|
3674
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
3565
3675
|
var WS_OPEN = 1;
|
|
3566
3676
|
var WebSocketTransport = class {
|
|
3567
3677
|
constructor(connection) {
|
|
@@ -3571,12 +3681,27 @@ var WebSocketTransport = class {
|
|
|
3571
3681
|
diagnosticsName = "websocket";
|
|
3572
3682
|
diagnosticsBackend = "native-websocket";
|
|
3573
3683
|
socket = null;
|
|
3684
|
+
socketActive = false;
|
|
3574
3685
|
handlers = null;
|
|
3575
3686
|
subscribedTopics = /* @__PURE__ */ new Set();
|
|
3576
|
-
|
|
3577
|
-
|
|
3687
|
+
// Handshake gate for the current start(). Resolves once the socket opens,
|
|
3688
|
+
// rejects when the attempt fails, so the DataBus start Promise — and every
|
|
3689
|
+
// operation parked behind it — settles at the real connection boundary.
|
|
3690
|
+
connectPromise = null;
|
|
3691
|
+
connectResolve = null;
|
|
3692
|
+
connectReject = null;
|
|
3693
|
+
connectTimer = null;
|
|
3694
|
+
/** Open the WebSocket and wire lifecycle listeners. Resolves once the
|
|
3695
|
+
* handshake completes and rejects when the attempt fails, matching the
|
|
3696
|
+
* `DataBusTransport.start` contract ("resolves on connect or rejects on
|
|
3697
|
+
* failure"). A factory failure is reported through `onStatus('error')` so
|
|
3698
|
+
* the DataBus can recover. */
|
|
3578
3699
|
start(config, handlers) {
|
|
3579
|
-
if (this.socket)
|
|
3700
|
+
if (this.socket && this.socketActive) {
|
|
3701
|
+
return this.connectPromise ?? void 0;
|
|
3702
|
+
}
|
|
3703
|
+
this.socket = null;
|
|
3704
|
+
this.socketActive = false;
|
|
3580
3705
|
this.handlers = handlers;
|
|
3581
3706
|
const factory = config.webSocketFactory ?? this.connection.webSocketFactory ?? defaultWebSocketFactory;
|
|
3582
3707
|
const protocols = config.protocols ?? this.connection.protocols;
|
|
@@ -3588,23 +3713,66 @@ var WebSocketTransport = class {
|
|
|
3588
3713
|
handlers.onError(error);
|
|
3589
3714
|
return;
|
|
3590
3715
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3716
|
+
const opening = new Promise((resolve, reject) => {
|
|
3717
|
+
this.connectResolve = resolve;
|
|
3718
|
+
this.connectReject = reject;
|
|
3719
|
+
let handshakeCompleted = false;
|
|
3720
|
+
let handshakeFailed = false;
|
|
3721
|
+
const timeoutMs = config.connectTimeoutMs ?? this.connection.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
3722
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
3723
|
+
this.connectTimer = setTimeout(() => {
|
|
3724
|
+
if (this.socket !== socket || this.handlers !== handlers || handshakeCompleted) return;
|
|
3725
|
+
handshakeFailed = true;
|
|
3726
|
+
this.connectTimer = null;
|
|
3727
|
+
this.socketActive = false;
|
|
3728
|
+
const error = new Error(`WebSocket did not open within ${timeoutMs}ms.`);
|
|
3729
|
+
handlers.onStatus(WORKER_STATUS.ERROR);
|
|
3730
|
+
handlers.onError(error);
|
|
3731
|
+
this.failConnect(error);
|
|
3732
|
+
socket.close();
|
|
3733
|
+
}, timeoutMs);
|
|
3595
3734
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3735
|
+
socket.onopen = () => {
|
|
3736
|
+
if (this.socket !== socket || this.handlers !== handlers || handshakeFailed) return;
|
|
3737
|
+
this.socketActive = true;
|
|
3738
|
+
this.clearConnectTimer();
|
|
3739
|
+
for (const topic of this.subscribedTopics) {
|
|
3740
|
+
this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });
|
|
3741
|
+
}
|
|
3742
|
+
handlers.onStatus(WORKER_STATUS.CONNECTED);
|
|
3743
|
+
if (!handshakeCompleted) {
|
|
3744
|
+
handshakeCompleted = true;
|
|
3745
|
+
this.settleConnect();
|
|
3746
|
+
}
|
|
3747
|
+
};
|
|
3748
|
+
socket.onclose = () => {
|
|
3749
|
+
if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;
|
|
3750
|
+
this.socketActive = false;
|
|
3751
|
+
handlers.onStatus(WORKER_STATUS.DISCONNECTED);
|
|
3752
|
+
if (!handshakeCompleted) {
|
|
3753
|
+
handshakeFailed = true;
|
|
3754
|
+
this.failConnect(new Error("WebSocket closed before the handshake completed."));
|
|
3755
|
+
}
|
|
3756
|
+
};
|
|
3757
|
+
socket.onerror = () => {
|
|
3758
|
+
if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;
|
|
3759
|
+
this.socketActive = false;
|
|
3760
|
+
handlers.onStatus(WORKER_STATUS.ERROR);
|
|
3761
|
+
if (!handshakeCompleted) {
|
|
3762
|
+
handshakeFailed = true;
|
|
3763
|
+
this.failConnect(new Error("WebSocket failed to open."));
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3766
|
+
socket.onmessage = (event) => {
|
|
3767
|
+
if (this.socket === socket && this.handlers === handlers && this.socketActive) {
|
|
3768
|
+
void this.handleMessage(event.data);
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
this.socket = socket;
|
|
3772
|
+
this.socketActive = true;
|
|
3773
|
+
});
|
|
3774
|
+
this.connectPromise = opening;
|
|
3775
|
+
return opening;
|
|
3608
3776
|
}
|
|
3609
3777
|
/** Idempotent: re-subscribing an active topic re-sends the frame but does
|
|
3610
3778
|
* not duplicate the local tracking entry. */
|
|
@@ -3660,10 +3828,38 @@ var WebSocketTransport = class {
|
|
|
3660
3828
|
/** Close the socket and drop all state. Safe to call multiple times. */
|
|
3661
3829
|
stop() {
|
|
3662
3830
|
const socket = this.socket;
|
|
3831
|
+
const shouldClose = this.socketActive;
|
|
3663
3832
|
this.socket = null;
|
|
3833
|
+
this.socketActive = false;
|
|
3664
3834
|
this.handlers = null;
|
|
3665
3835
|
this.subscribedTopics.clear();
|
|
3666
|
-
|
|
3836
|
+
this.settleConnect();
|
|
3837
|
+
this.connectPromise = null;
|
|
3838
|
+
if (shouldClose) socket?.close();
|
|
3839
|
+
}
|
|
3840
|
+
/** Resolve the in-flight handshake gate. Idempotent: once the socket has
|
|
3841
|
+
* opened (or a newer attempt replaced it) later calls are no-ops. */
|
|
3842
|
+
settleConnect() {
|
|
3843
|
+
this.clearConnectTimer();
|
|
3844
|
+
const resolve = this.connectResolve;
|
|
3845
|
+
this.connectResolve = null;
|
|
3846
|
+
this.connectReject = null;
|
|
3847
|
+
resolve?.();
|
|
3848
|
+
}
|
|
3849
|
+
/** Reject the in-flight handshake gate. Idempotent on the same terms as
|
|
3850
|
+
* {@link settleConnect}. */
|
|
3851
|
+
failConnect(error) {
|
|
3852
|
+
this.clearConnectTimer();
|
|
3853
|
+
const reject = this.connectReject;
|
|
3854
|
+
this.connectResolve = null;
|
|
3855
|
+
this.connectReject = null;
|
|
3856
|
+
reject?.(error);
|
|
3857
|
+
}
|
|
3858
|
+
clearConnectTimer() {
|
|
3859
|
+
if (this.connectTimer !== null) {
|
|
3860
|
+
clearTimeout(this.connectTimer);
|
|
3861
|
+
this.connectTimer = null;
|
|
3862
|
+
}
|
|
3667
3863
|
}
|
|
3668
3864
|
/** Send one JSON frame. Frames are dropped with an `onError` report when
|
|
3669
3865
|
* the socket is not open — subscribe frames are re-sent on open, so the
|