@fulgurjs/federation 5.3.3 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/DESIGN.md +4 -0
  3. package/README.en.md +23 -3
  4. package/README.md +83 -1
  5. package/dist/bridge-app-react.cjs +14 -4
  6. package/dist/bridge-app-react.d.cts +16 -4
  7. package/dist/bridge-app-react.d.ts +16 -4
  8. package/dist/bridge-app-react.js +6 -4
  9. package/dist/bridge-app-vue.cjs +55 -9
  10. package/dist/bridge-app-vue.d.cts +25 -4
  11. package/dist/bridge-app-vue.d.ts +25 -4
  12. package/dist/bridge-app-vue.js +47 -9
  13. package/dist/bridge-core-BOWvVUxO.d.ts +91 -0
  14. package/dist/bridge-core.cjs +9 -1
  15. package/dist/bridge-core.d.cts +8 -1
  16. package/dist/bridge-core.d.ts +8 -1
  17. package/dist/bridge-core.js +2 -2
  18. package/dist/bridge-errors.cjs +55 -3
  19. package/dist/bridge-errors.d.cts +18 -2
  20. package/dist/bridge-errors.d.ts +18 -2
  21. package/dist/bridge-errors.js +11 -3
  22. package/dist/{bridge-host-react-C55RmGll.d.ts → bridge-host-react-CMVd-ym0.d.ts} +1 -1
  23. package/dist/bridge-host-react.cjs +324 -3
  24. package/dist/bridge-host-react.d.cts +2 -0
  25. package/dist/bridge-host-react.d.ts +2 -0
  26. package/dist/bridge-host-react.js +30 -4
  27. package/dist/{bridge-host-vue-BAxDKqGH.d.ts → bridge-host-vue-BuCGlLdS.d.ts} +1 -1
  28. package/dist/bridge-host-vue.cjs +335 -3
  29. package/dist/bridge-host-vue.js +42 -5
  30. package/dist/bridge-react.d.ts +5 -3
  31. package/dist/bridge-router-core-BxBx-reP.d.cts +44 -0
  32. package/dist/bridge-router-core-BxBx-reP.d.ts +44 -0
  33. package/dist/bridge-router-react.d.ts +96 -0
  34. package/dist/bridge-router-react.js +70 -0
  35. package/dist/bridge-router-vue.d.ts +75 -0
  36. package/dist/bridge-router-vue.js +73 -0
  37. package/dist/bridge-vue.d.ts +2 -2
  38. package/dist/bridge.d.ts +6 -4
  39. package/dist/{chunk-WD2ON7SN.js → chunk-BTJGYDHT.js} +303 -2
  40. package/dist/{chunk-GWJKABE7.js → chunk-GF6KOH67.js} +39 -4
  41. package/dist/{chunk-DH2QMM4Y.js → chunk-JQF7KYZF.js} +50 -2
  42. package/dist/{chunk-ZTRJLCOF.js → chunk-OXEAGRPY.js} +27 -3
  43. package/dist/{chunk-LE2G23E7.js → chunk-PRUYHYX2.js} +1 -1
  44. package/dist/chunk-WOTR2FUQ.js +266 -0
  45. package/dist/index.cjs +2 -2
  46. package/dist/index.js +2 -2
  47. package/dist/react.d.ts +51 -4
  48. package/dist/runtime-entry.d.ts +60 -4
  49. package/dist/runtime.js +1 -1
  50. package/examples/bridge/react-host/package-lock.json +105 -104
  51. package/examples/bridge/react-remote/package-lock.json +105 -104
  52. package/examples/bridge/vue-host/package-lock.json +105 -104
  53. package/examples/bridge/vue-remote/package-lock.json +105 -104
  54. package/examples/react/host/package-lock.json +1 -1
  55. package/examples/react/remote/package-lock.json +1 -1
  56. package/examples/vue/host/package-lock.json +1 -1
  57. package/examples/vue/remote/package-lock.json +1 -1
  58. package/package.json +28 -4
  59. package/dist/bridge-core-6uLJiMVa.d.ts +0 -42
@@ -1,17 +1,24 @@
1
1
  import {
2
+ RoutingChannel,
2
3
  acquireBridgeSession,
3
4
  assertBridgeContract,
4
5
  assertBridgeOptions,
6
+ assertBridgeRoutingProtocol,
5
7
  assertControlledSessionKey,
6
8
  bridgeLifecycleError,
7
9
  provideAppContext,
8
10
  releaseBridgeSession,
9
11
  resolveBridgeContext,
10
12
  withBridgeTimeout
11
- } from "./chunk-WD2ON7SN.js";
13
+ } from "./chunk-BTJGYDHT.js";
12
14
 
13
15
  // src/bridge-host-vue.ts
14
- import { defineComponent, h, onBeforeUnmount, onMounted, ref, shallowRef, toRaw, watch } from "vue";
16
+ import { defineComponent, h, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, toRaw, watch } from "vue";
17
+ var assertBridgeRoutingProtocolSpec = (spec, contract) => assertBridgeRoutingProtocol(spec, contract);
18
+ function routingKey(routing) {
19
+ if (!routing) return void 0;
20
+ return routing.basePath + "|" + String(routing.navigation && typeof routing.navigation.navigate === "function");
21
+ }
15
22
  var ERROR_STYLE = {
16
23
  padding: "16px",
17
24
  border: "1px solid #fde2e2",
@@ -89,7 +96,9 @@ function createVueBridgeAppWithLoader(loadRemote) {
89
96
  props: {
90
97
  appProps: { type: Object, required: false, default: void 0 },
91
98
  // 桥接控制参数:undefined(不启用受控会话)/ null(登出态)/ 非空字符串(登录代次)
92
- sessionKey: { type: null, required: false, default: void 0 }
99
+ sessionKey: { type: null, required: false, default: void 0 },
100
+ // URL 同步控制通道(独立于 appProps/Context;省略 = 原 memory 模式)
101
+ routing: { type: Object, required: false, default: void 0 }
93
102
  },
94
103
  setup(props) {
95
104
  const status = ref("idle");
@@ -99,6 +108,8 @@ function createVueBridgeAppWithLoader(loadRemote) {
99
108
  let contract;
100
109
  let containerBlocked = false;
101
110
  let acquiredSession;
111
+ let channel;
112
+ let genAbort;
102
113
  const releaseAcquired = () => {
103
114
  if (acquiredSession !== void 0) {
104
115
  releaseBridgeSession(acquiredSession);
@@ -110,6 +121,10 @@ function createVueBridgeAppWithLoader(loadRemote) {
110
121
  generation++;
111
122
  const el = container.value;
112
123
  releaseAcquired();
124
+ genAbort?.abort();
125
+ genAbort = void 0;
126
+ channel?.dispose();
127
+ channel = void 0;
113
128
  if (contract && el) {
114
129
  const c = contract;
115
130
  contract = void 0;
@@ -151,7 +166,18 @@ function createVueBridgeAppWithLoader(loadRemote) {
151
166
  if (myGeneration !== generation) return;
152
167
  contract = assertBridgeContract(spec, mod);
153
168
  const appPropsSource = props.appProps ? toRaw(props.appProps) : void 0;
154
- await contract.mount(el, { ...appPropsSource ?? {} });
169
+ genAbort = new AbortController();
170
+ let mountOptions = { signal: genAbort.signal };
171
+ if (props.routing) {
172
+ assertBridgeRoutingProtocolSpec(spec, contract);
173
+ const navigation = props.routing.navigation;
174
+ const latestLoc = navigation.getLocation();
175
+ if (myGeneration !== generation) return;
176
+ channel = new RoutingChannel(`bridge-vue:${spec}:${myGeneration}`, props.routing.basePath, navigation, spec);
177
+ mountOptions = { signal: genAbort.signal, routing: channel };
178
+ void latestLoc;
179
+ }
180
+ await contract.mount(el, { ...appPropsSource ?? {} }, mountOptions);
155
181
  if (myGeneration !== generation) return;
156
182
  status.value = "ready";
157
183
  } catch (e) {
@@ -176,6 +202,15 @@ function createVueBridgeAppWithLoader(loadRemote) {
176
202
  watch(() => props.sessionKey, () => {
177
203
  start();
178
204
  });
205
+ watch(() => routingKey(props.routing), (after, before) => {
206
+ if (after !== before) start();
207
+ });
208
+ onDeactivated(() => {
209
+ channel?.setActive(false);
210
+ });
211
+ onActivated(() => {
212
+ channel?.setActive(true);
213
+ });
179
214
  onBeforeUnmount(() => {
180
215
  invalidate();
181
216
  });
@@ -9,7 +9,15 @@ var BridgeErrorCodes = {
9
9
  /** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
10
10
  BRIDGE_LIFECYCLE_FAILED: "MFU-016",
11
11
  /** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
12
- BRIDGE_SESSION_MISMATCH: "MFU-017"
12
+ BRIDGE_SESSION_MISMATCH: "MFU-017",
13
+ /** 路由同步配置/前缀冲突(basePath 非法、同页前缀冲突或重叠) */
14
+ ROUTING_CONFIG_INVALID: "MFU-030",
15
+ /** 路由协议缺失/通道失效(子应用未声明 routing 协议,或已销毁通道被再次使用) */
16
+ ROUTING_PROTOCOL_MISSING: "MFU-031",
17
+ /** 非法导航(目标越界前缀、非法 go 参数、失效实例的请求) */
18
+ ROUTING_NAVIGATION_INVALID: "MFU-032",
19
+ /** 路由准备/同步失败(重定向循环、子应用路由准备失败且已阻止错误页面显示) */
20
+ ROUTING_SYNC_FAILED: "MFU-033"
13
21
  };
14
22
  function invalidBridgeContractError(spec, mod) {
15
23
  const actual = mod === null ? "null" : typeof mod;
@@ -66,11 +74,51 @@ function invalidSessionKeyError(spec, value) {
66
74
  { spec, value: String(value) }
67
75
  );
68
76
  }
77
+ function routingConfigError(spec, reason) {
78
+ return new FgError(
79
+ BridgeErrorCodes.ROUTING_CONFIG_INVALID,
80
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u914D\u7F6E\u65E0\u6CD5\u542F\u7528\u3002
81
+ \u539F\u56E0\uFF1A${reason}
82
+ \u4FEE\u6CD5\uFF1A\u4FEE\u6B63 routing.basePath \u4E3A\u5BBF\u4E3B\u8DEF\u7531\u89C6\u89D2\u7684\u9759\u6001\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u5982 "/approval"\uFF09\uFF0C\u5E76\u4FDD\u8BC1\u540C\u9875\u5404\u540C\u6B65\u5B9E\u4F8B\u524D\u7F00\u4E92\u4E0D\u91CD\u53E0\uFF1B\u5BBF\u4E3B\u8DEF\u7531\u9700\u58F0\u660E\u5BF9\u5E94\u7684\u540E\u7F00\u5339\u914D\uFF08Vue "/approval/:pathMatch(.*)*"\u3001React "/approval/*"\uFF09\u3002`,
83
+ { spec }
84
+ );
85
+ }
86
+ function routingProtocolError(spec, reason) {
87
+ return new FgError(
88
+ BridgeErrorCodes.ROUTING_PROTOCOL_MISSING,
89
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u901A\u9053\u4E0D\u53EF\u7528\u3002
90
+ \u539F\u56E0\uFF1A${reason}
91
+ \u4FEE\u6CD5\uFF1A\u5B50\u5E94\u7528\u4EE5 defineBridgeApp(\u5DE5\u5382, { routing: true }) \u58F0\u660E\u8DEF\u7531\u534F\u8BAE\u5E76\u5728\u5DE5\u5382\u7B2C\u4E8C\u53C2\u6570\u63A5\u6536 { signal, routing } \u63A5\u7EBF\u53D7\u63A7\u8DEF\u7531\uFF1B\u901A\u9053\u968F\u6302\u8F7D\u751F\u547D\u5468\u671F\u521B\u5EFA\uFF0C\u9500\u6BC1\u540E\u4E0D\u5F97\u590D\u7528\uFF08\u91CD\u6302\u4F1A\u5F97\u5230\u65B0\u901A\u9053\uFF09\u3002`,
92
+ { spec }
93
+ );
94
+ }
95
+ function routingNavigationError(spec, reason) {
96
+ return new FgError(
97
+ BridgeErrorCodes.ROUTING_NAVIGATION_INVALID,
98
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684\u4E00\u6B21\u5BFC\u822A\u8BF7\u6C42\u88AB\u62D2\u7EDD\u3002
99
+ \u539F\u56E0\uFF1A${reason}
100
+ \u4FEE\u6CD5\uFF1A\u5B50\u5E94\u7528\u5BFC\u822A\u76EE\u6807\u5FC5\u987B\u4F4D\u4E8E\u81EA\u8EAB basePath \u524D\u7F00\u5185\uFF08\u8DE8\u524D\u7F00\u8BF7\u7528\u5BBF\u4E3B\u83DC\u5355/\u5BBF\u4E3B\u80FD\u529B\uFF09\uFF1Bgo \u53C2\u6570\u4E3A\u6709\u9650\u6574\u6570\u4E14 |delta| \u2264 50\u3002`,
101
+ { spec }
102
+ );
103
+ }
104
+ function routingSyncError(spec, reason, chain) {
105
+ return new FgError(
106
+ BridgeErrorCodes.ROUTING_SYNC_FAILED,
107
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5728\u91CD\u5B9A\u5411\u94FE\u4E0A\u8D85\u51FA\u4E0A\u9650\uFF08${chain.length} \u6B21\uFF09\u3002
108
+ \u539F\u56E0\uFF1A${reason}
109
+ \u4FEE\u6CD5\uFF1A\u68C0\u67E5\u5B50\u5E94\u7528\u8DEF\u7531\u7684\u91CD\u5B9A\u5411\u89C4\u5219\u662F\u5426\u4E92\u76F8\u6210\u73AF\uFF1B\u540C\u6B65\u91CD\u5B9A\u5411\u4F7F\u7528 replace \u4E14\u4E0D\u5F97\u5BF9\u540C\u4E00\u4F4D\u7F6E\u91CD\u590D\u53D1\u8D77\u3002\u76EE\u6807\u94FE\uFF1A${chain.join(" \u2192 ")}\u3002`,
110
+ { spec, chain: [...chain] }
111
+ );
112
+ }
69
113
 
70
114
  export {
71
115
  BridgeErrorCodes,
72
116
  invalidBridgeContractError,
73
117
  bridgeLifecycleError,
74
118
  bridgeSessionMismatchError,
75
- invalidSessionKeyError
119
+ invalidSessionKeyError,
120
+ routingConfigError,
121
+ routingProtocolError,
122
+ routingNavigationError,
123
+ routingSyncError
76
124
  };
@@ -1,14 +1,16 @@
1
1
  import {
2
+ RoutingChannel,
2
3
  acquireBridgeSession,
3
4
  assertBridgeContract,
4
5
  assertBridgeOptions,
6
+ assertBridgeRoutingProtocol,
5
7
  assertControlledSessionKey,
6
8
  bridgeLifecycleError,
7
9
  provideAppContext,
8
10
  releaseBridgeSession,
9
11
  resolveBridgeContext,
10
12
  withBridgeTimeout
11
- } from "./chunk-WD2ON7SN.js";
13
+ } from "./chunk-BTJGYDHT.js";
12
14
 
13
15
  // src/bridge-host-react.ts
14
16
  import {
@@ -18,6 +20,10 @@ import {
18
20
  useRef,
19
21
  useState
20
22
  } from "react";
23
+ function routingKey(routing) {
24
+ if (!routing) return void 0;
25
+ return routing.basePath + "|" + String(routing.navigation && typeof routing.navigation.navigate === "function");
26
+ }
21
27
  var ERROR_STYLE = {
22
28
  padding: "16px",
23
29
  border: "1px solid #fde2e2",
@@ -88,6 +94,8 @@ function createReactBridgeAppWithLoader(loadRemote) {
88
94
  const contractRef = useRef(void 0);
89
95
  const acquiredRef = useRef(void 0);
90
96
  const blockedRef = useRef(false);
97
+ const channelRef = useRef(void 0);
98
+ const abortRef = useRef(void 0);
91
99
  const latest = useRef(props);
92
100
  latest.current = props;
93
101
  useEffect(() => {
@@ -100,6 +108,12 @@ function createReactBridgeAppWithLoader(loadRemote) {
100
108
  acquiredRef.current = void 0;
101
109
  }
102
110
  };
111
+ const disposeChannel = () => {
112
+ channelRef.current?.dispose();
113
+ channelRef.current = void 0;
114
+ abortRef.current?.abort();
115
+ abortRef.current = void 0;
116
+ };
103
117
  const run = async () => {
104
118
  if (!el) return;
105
119
  const sk = latest.current.sessionKey;
@@ -124,7 +138,16 @@ function createReactBridgeAppWithLoader(loadRemote) {
124
138
  if (generationRef.current !== myGen) return;
125
139
  const contract = assertBridgeContract(spec, mod);
126
140
  contractRef.current = contract;
127
- await contract.mount(el, { ...latest.current.appProps ?? {} });
141
+ abortRef.current = new AbortController();
142
+ let mountOptions = { signal: abortRef.current.signal };
143
+ const routing = latest.current.routing;
144
+ if (routing) {
145
+ assertBridgeRoutingProtocol(spec, contract);
146
+ if (generationRef.current !== myGen) return;
147
+ channelRef.current = new RoutingChannel(`bridge-react:${spec}:${myGen}`, routing.basePath, routing.navigation, spec);
148
+ mountOptions = { signal: abortRef.current.signal, routing: channelRef.current };
149
+ }
150
+ await contract.mount(el, { ...latest.current.appProps ?? {} }, mountOptions);
128
151
  if (generationRef.current !== myGen) return;
129
152
  setStatus("ready");
130
153
  } catch (e) {
@@ -137,6 +160,7 @@ function createReactBridgeAppWithLoader(loadRemote) {
137
160
  return () => {
138
161
  generationRef.current++;
139
162
  release();
163
+ disposeChannel();
140
164
  const c = contractRef.current;
141
165
  contractRef.current = void 0;
142
166
  if (c && el) {
@@ -151,7 +175,7 @@ function createReactBridgeAppWithLoader(loadRemote) {
151
175
  }
152
176
  }
153
177
  };
154
- }, [props.sessionKey, attempt]);
178
+ }, [props.sessionKey, attempt, routingKey(latest.current.routing)]);
155
179
  const retry = () => setAttempt((a) => a + 1);
156
180
  const children = [];
157
181
  if (status === "pending" && options.fallback !== void 0) {
@@ -3,7 +3,7 @@ import {
3
3
  bridgeSessionMismatchError,
4
4
  invalidBridgeContractError,
5
5
  invalidSessionKeyError
6
- } from "./chunk-DH2QMM4Y.js";
6
+ } from "./chunk-JQF7KYZF.js";
7
7
  import {
8
8
  clearAppContext
9
9
  } from "./chunk-TSYLDY6H.js";
@@ -0,0 +1,266 @@
1
+ import {
2
+ routingConfigError,
3
+ routingNavigationError,
4
+ routingProtocolError,
5
+ routingSyncError
6
+ } from "./chunk-JQF7KYZF.js";
7
+
8
+ // src/bridge-router-core.ts
9
+ var MAX_REDIRECT_PER_REQUEST = 5;
10
+ var MAX_GO_DELTA = 50;
11
+ function normalizeBasePath(basePath, spec) {
12
+ const bad = (reason) => routingConfigError(spec, reason);
13
+ if (typeof basePath !== "string" || basePath === "") throw bad("basePath \u4E0D\u80FD\u4E3A\u7A7A\u3002");
14
+ if (basePath === "/") throw bad('basePath \u4E0D\u80FD\u662F\u6839 "/"\u2014\u2014\u6839\u8DEF\u5F84\u5C5E\u4E8E\u5BBF\u4E3B\u81EA\u8EAB\u9875\u9762\uFF0C\u8BF7\u4E3A\u540C\u6B65\u5B9E\u4F8B\u5206\u914D\u72EC\u7ACB\u524D\u7F00\u3002');
15
+ if (!basePath.startsWith("/")) throw bad(`basePath \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
16
+ if (/[?#]/.test(basePath) || basePath.includes("://")) throw bad(`basePath \u4E0D\u80FD\u5305\u542B query/hash/\u534F\u8BAE\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
17
+ if (basePath.includes("*") || basePath.includes(":")) throw bad(`basePath \u4E0D\u63A5\u53D7\u901A\u914D\u7B26\u6216\u53C2\u6570\u6A21\u677F\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002\u8BF7\u4F7F\u7528\u9759\u6001\u7EDD\u5BF9\u8DEF\u5F84\u3002`);
18
+ return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
19
+ }
20
+ function matchesBasePath(hostPathname, basePath) {
21
+ return hostPathname === basePath || hostPathname.startsWith(basePath + "/");
22
+ }
23
+ function overlapsBasePath(a, b) {
24
+ return a === b || a.startsWith(b + "/") || b.startsWith(a + "/");
25
+ }
26
+ function toChildLocation(host, basePath) {
27
+ const rest = host.pathname === basePath ? "/" : host.pathname.slice(basePath.length);
28
+ return { pathname: rest || "/", search: host.search, hash: host.hash };
29
+ }
30
+ function toHostLocation(child, basePath, spec) {
31
+ const childPath = child.pathname === "/" ? "" : child.pathname;
32
+ if (childPath !== "" && !childPath.startsWith("/")) {
33
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807 pathname \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${child.pathname}"\u3002`);
34
+ }
35
+ return { pathname: basePath + childPath, search: child.search, hash: child.hash };
36
+ }
37
+ function sameLocation(a, b) {
38
+ return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
39
+ }
40
+ function assertChildTargetInScope(target, basePath, spec) {
41
+ if (target.pathname.includes("\0") || target.pathname.split("/").includes("..")) {
42
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807\u8BD5\u56FE\u9003\u9038\u81EA\u8EAB\u524D\u7F00\uFF1Apathname "${target.pathname}"\u3002\u8DE8\u524D\u7F00\u5BFC\u822A\u8BF7\u901A\u8FC7\u5BBF\u4E3B\u83DC\u5355\u7B49\u5BBF\u4E3B\u80FD\u529B\u5B8C\u6210\u3002`);
43
+ }
44
+ const host = toHostLocation(target, basePath, spec);
45
+ if (!matchesBasePath(host.pathname, basePath)) {
46
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807 ${host.pathname} \u4E0D\u5728\u524D\u7F00 ${basePath} \u4E0B\u3002`);
47
+ }
48
+ }
49
+ var activeRoutingPrefixes = /* @__PURE__ */ new Map();
50
+ function acquireRoutingPrefix(basePath, spec) {
51
+ for (const [active] of activeRoutingPrefixes) {
52
+ if ((activeRoutingPrefixes.get(active) ?? 0) > 0 && overlapsBasePath(active, basePath)) {
53
+ throw routingConfigError(spec, `\u540C\u9875\u5DF2\u6709\u5360\u7528\u524D\u7F00 "${active}" \u7684\u540C\u6B65\u5B9E\u4F8B\uFF0C\u65B0\u5B9E\u4F8B "${spec}" \u7684 basePath "${basePath}" \u4E0E\u5176\u91CD\u53E0\u3002`);
54
+ }
55
+ }
56
+ activeRoutingPrefixes.set(basePath, (activeRoutingPrefixes.get(basePath) ?? 0) + 1);
57
+ let released = false;
58
+ return () => {
59
+ if (released) return;
60
+ released = true;
61
+ activeRoutingPrefixes.set(basePath, Math.max(0, (activeRoutingPrefixes.get(basePath) ?? 1) - 1));
62
+ };
63
+ }
64
+ function assertBridgeRoutingProtocol(spec, contract) {
65
+ const r = contract?.routing;
66
+ if (!r || r.protocol !== 1) {
67
+ throw routingProtocolError(
68
+ spec,
69
+ `\u8FDC\u7A0B\u5951\u7EA6\u672A\u58F0\u660E\u8DEF\u7531\u534F\u8BAE\uFF08\u671F\u671B routing: { protocol: 1 }\uFF0C\u5B9E\u9645 ${r === void 0 ? "\u7F3A\u5931" : `protocol=${String(r?.protocol)}`}\uFF09\u3002\u5F00\u542F URL \u540C\u6B65\u4E0D\u80FD\u9759\u9ED8\u9000\u56DE memory \u6A21\u5F0F\u3002`
70
+ );
71
+ }
72
+ }
73
+ var RoutingChannel = class {
74
+ id;
75
+ basePath;
76
+ spec;
77
+ host;
78
+ listeners = /* @__PURE__ */ new Set();
79
+ current;
80
+ lastConfirmed;
81
+ seq = 0;
82
+ inflight = null;
83
+ disposed = false;
84
+ active = true;
85
+ releasePrefix;
86
+ unsubHost;
87
+ /** 连续内部 replace 计数(有界重定向检测) */
88
+ redirectChain = [];
89
+ redirectTimer;
90
+ /** 诊断观测钩子(测试断言/诊断用) */
91
+ onEvent;
92
+ constructor(id, basePath, host, spec) {
93
+ this.id = id;
94
+ this.spec = spec;
95
+ this.basePath = normalizeBasePath(basePath, spec);
96
+ this.host = host;
97
+ this.current = toChildLocation(host.getLocation(), this.basePath);
98
+ this.lastConfirmed = { ...this.current };
99
+ this.releasePrefix = acquireRoutingPrefix(this.basePath, spec);
100
+ this.unsubHost = host.subscribe((loc) => this.handleHostLocation(loc));
101
+ }
102
+ emit(loc) {
103
+ this.current = { ...loc };
104
+ for (const l of [...this.listeners]) {
105
+ try {
106
+ l(this.current);
107
+ } catch (e) {
108
+ console.error("[fulgurjs] \u5B50\u5E94\u7528\u8DEF\u7531\u8BA2\u9605\u8005\u5F02\u5E38\uFF08\u5DF2\u9694\u79BB\uFF0C\u4E0D\u5F71\u54CD\u5BBF\u4E3B\uFF09\uFF1A", e);
109
+ }
110
+ }
111
+ }
112
+ handleHostLocation(hostLoc) {
113
+ if (this.disposed) return;
114
+ if (!matchesBasePath(hostLoc.pathname, this.basePath)) {
115
+ this.onEvent?.({ type: "prefix-miss", pathname: hostLoc.pathname });
116
+ return;
117
+ }
118
+ if (!this.active) {
119
+ this.onEvent?.({ type: "paused-host-loc" });
120
+ return;
121
+ }
122
+ const child = toChildLocation(hostLoc, this.basePath);
123
+ if (this.inflight && sameLocation(child, this.inflight.target)) {
124
+ const { seq, resolve } = this.inflight;
125
+ this.inflight = null;
126
+ this.lastConfirmed = child;
127
+ this.emit(child);
128
+ this.onEvent?.({ type: "request-confirmed", seq });
129
+ resolve({ status: "committed", location: { ...child } });
130
+ return;
131
+ }
132
+ if (this.inflight) {
133
+ const { seq, resolve } = this.inflight;
134
+ this.inflight = null;
135
+ this.lastConfirmed = child;
136
+ this.emit(child);
137
+ this.onEvent?.({ type: "request-superseded", seq });
138
+ resolve({ status: "cancelled", location: { ...child } });
139
+ return;
140
+ }
141
+ this.lastConfirmed = child;
142
+ this.emit(child);
143
+ this.onEvent?.({ type: "host-broadcast" });
144
+ }
145
+ getLocation() {
146
+ return { ...this.current };
147
+ }
148
+ subscribe(listener) {
149
+ if (this.disposed) {
150
+ throw routingProtocolError(this.spec, `\u901A\u9053 ${this.id} \u5DF2\u9500\u6BC1\uFF0C\u4E0D\u80FD\u518D\u8BA2\u9605\uFF08\u65E7\u5B9E\u4F8B\u7684\u8FDF\u5230\u8BA2\u9605\u5FC5\u987B\u5931\u6548\uFF1B\u91CD\u6302\u4F1A\u5F97\u5230\u65B0\u901A\u9053\uFF09\u3002`);
151
+ }
152
+ this.listeners.add(listener);
153
+ return () => {
154
+ this.listeners.delete(listener);
155
+ };
156
+ }
157
+ async navigate(target, action) {
158
+ if (this.disposed || !this.active) {
159
+ this.onEvent?.({ type: this.disposed ? "rejected-disposed" : "rejected-inactive" });
160
+ return { status: "cancelled", location: { ...this.lastConfirmed } };
161
+ }
162
+ assertChildTargetInScope(target, this.basePath, this.spec);
163
+ if (sameLocation(target, this.lastConfirmed)) {
164
+ this.onEvent?.({ type: "noop" });
165
+ return { status: "committed", location: { ...this.lastConfirmed } };
166
+ }
167
+ if (action === "replace") {
168
+ this.redirectChain.push(target.pathname + target.search + target.hash);
169
+ } else {
170
+ this.redirectChain = [];
171
+ }
172
+ if (this.redirectTimer) clearTimeout(this.redirectTimer);
173
+ this.redirectTimer = setTimeout(() => {
174
+ this.redirectChain = [];
175
+ }, 1e3);
176
+ if (this.redirectChain.length > MAX_REDIRECT_PER_REQUEST) {
177
+ const chain = [...this.redirectChain];
178
+ this.redirectChain = [];
179
+ throw routingSyncError(this.spec, "\u540C\u4E00\u901A\u9053\u7684\u8FDE\u7EED replace \u91CD\u5B9A\u5411\u8D85\u8FC7\u4E0A\u9650\uFF08\u7591\u4F3C\u5B50\u5E94\u7528\u8DEF\u7531\u4E92\u76F8\u6210\u73AF\uFF09\u3002", chain);
180
+ }
181
+ if (this.inflight) {
182
+ const prev = this.inflight;
183
+ await new Promise((resolve) => {
184
+ const timer = setInterval(() => {
185
+ if (this.inflight !== prev) {
186
+ clearInterval(timer);
187
+ resolve();
188
+ }
189
+ }, 0);
190
+ });
191
+ if (this.disposed || !this.active) {
192
+ return { status: "cancelled", location: { ...this.lastConfirmed } };
193
+ }
194
+ if (sameLocation(target, this.lastConfirmed)) {
195
+ return { status: "committed", location: { ...this.lastConfirmed } };
196
+ }
197
+ }
198
+ const seq = ++this.seq;
199
+ this.onEvent?.({ type: "request-start", seq, target: { ...target }, action });
200
+ return new Promise((resolve) => {
201
+ this.inflight = { seq, target: { ...target }, resolve };
202
+ this.host.navigate(toHostLocation(target, this.basePath, this.spec), action).then((result) => {
203
+ if (!this.inflight || this.inflight.seq !== seq) return;
204
+ if (this.disposed || !this.active) {
205
+ this.inflight = null;
206
+ resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
207
+ return;
208
+ }
209
+ this.inflight = null;
210
+ if (result.status === "committed") {
211
+ const child = toChildLocation(result.location, this.basePath);
212
+ this.lastConfirmed = child;
213
+ this.emit(child);
214
+ } else {
215
+ this.emit({ ...this.lastConfirmed });
216
+ }
217
+ this.onEvent?.({ type: "host-verdict", seq, status: result.status });
218
+ resolve(
219
+ result.status === "committed" ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
220
+ );
221
+ }).catch(() => {
222
+ if (this.inflight?.seq === seq) {
223
+ this.inflight = null;
224
+ this.onEvent?.({ type: "host-error", seq });
225
+ this.emit({ ...this.lastConfirmed });
226
+ resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
227
+ }
228
+ });
229
+ });
230
+ }
231
+ /** 委托宿主历史(有限整数);不创建第二条独立历史 */
232
+ go(delta) {
233
+ if (this.disposed || !this.active) return;
234
+ if (typeof delta !== "number" || !Number.isInteger(delta) || Math.abs(delta) > MAX_GO_DELTA) {
235
+ throw routingNavigationError(this.spec, `go \u53C2\u6570\u5FC5\u987B\u662F\u6709\u9650\u6574\u6570\uFF08|delta| \u2264 ${MAX_GO_DELTA}\uFF09\uFF1A\u5F53\u524D\u4E3A ${String(delta)}\u3002`);
236
+ }
237
+ this.host.go(delta);
238
+ }
239
+ /** KeepAlive deactivate:暂停广播与写入;通道与订阅保留(activate 后 resync) */
240
+ setActive(active) {
241
+ if (this.disposed) return;
242
+ this.active = active;
243
+ this.onEvent?.({ type: active ? "resumed" : "paused" });
244
+ if (active) this.handleHostLocation(this.host.getLocation());
245
+ }
246
+ /** 挂载生命周期结束:清理订阅/登记/在飞请求;此后一切写入失效 */
247
+ dispose() {
248
+ if (this.disposed) return;
249
+ this.disposed = true;
250
+ if (this.redirectTimer) clearTimeout(this.redirectTimer);
251
+ this.unsubHost();
252
+ this.releasePrefix();
253
+ if (this.inflight) {
254
+ const { resolve } = this.inflight;
255
+ this.inflight = null;
256
+ resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
257
+ }
258
+ this.listeners.clear();
259
+ this.onEvent?.({ type: "disposed" });
260
+ }
261
+ };
262
+
263
+ export {
264
+ assertBridgeRoutingProtocol,
265
+ RoutingChannel
266
+ };