@fulgurjs/federation 5.4.0 → 5.4.1

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 (44) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.en.md +5 -3
  3. package/README.md +14 -9
  4. package/dist/bridge-app-react.d.cts +1 -1
  5. package/dist/bridge-app-react.d.ts +1 -1
  6. package/dist/bridge-app-react.js +1 -1
  7. package/dist/bridge-app-vue.d.cts +1 -1
  8. package/dist/bridge-app-vue.d.ts +1 -1
  9. package/dist/bridge-app-vue.js +1 -1
  10. package/dist/{bridge-core-BOWvVUxO.d.ts → bridge-core-ejlY9ddN.d.ts} +3 -1
  11. package/dist/bridge-core.d.cts +1 -1
  12. package/dist/bridge-core.d.ts +1 -1
  13. package/dist/bridge-core.js +2 -2
  14. package/dist/bridge-errors.cjs +5 -4
  15. package/dist/bridge-errors.d.cts +1 -1
  16. package/dist/bridge-errors.d.ts +1 -1
  17. package/dist/bridge-errors.js +1 -1
  18. package/dist/{bridge-host-react-CMVd-ym0.d.ts → bridge-host-react-Bua1TCVh.d.ts} +1 -1
  19. package/dist/bridge-host-react.cjs +54 -45
  20. package/dist/bridge-host-react.d.cts +1 -1
  21. package/dist/bridge-host-react.d.ts +1 -1
  22. package/dist/bridge-host-react.js +3 -3
  23. package/dist/{bridge-host-vue-BuCGlLdS.d.ts → bridge-host-vue-Dd1kkwDb.d.ts} +1 -1
  24. package/dist/bridge-host-vue.cjs +54 -45
  25. package/dist/bridge-host-vue.js +3 -3
  26. package/dist/bridge-react.d.ts +4 -4
  27. package/dist/{bridge-router-core-BxBx-reP.d.ts → bridge-router-core-BRZQYDPm.d.cts} +3 -1
  28. package/dist/{bridge-router-core-BxBx-reP.d.cts → bridge-router-core-BRZQYDPm.d.ts} +3 -1
  29. package/dist/bridge-router-react.d.ts +18 -41
  30. package/dist/bridge-router-react.js +213 -36
  31. package/dist/bridge-router-vue.d.ts +11 -36
  32. package/dist/bridge-router-vue.js +188 -44
  33. package/dist/bridge-vue.d.ts +2 -2
  34. package/dist/bridge.d.ts +5 -5
  35. package/dist/{chunk-GF6KOH67.js → chunk-43UX3UZE.js} +1 -1
  36. package/dist/{chunk-WOTR2FUQ.js → chunk-7CQOVLSI.js} +50 -42
  37. package/dist/{chunk-JQF7KYZF.js → chunk-HZGQOW35.js} +5 -4
  38. package/dist/{chunk-OXEAGRPY.js → chunk-KT3YARK5.js} +1 -1
  39. package/dist/{chunk-PRUYHYX2.js → chunk-RXXBTPAY.js} +1 -1
  40. package/dist/{chunk-BTJGYDHT.js → chunk-UFWZPE36.js} +55 -55
  41. package/dist/index.cjs +30 -10
  42. package/dist/index.js +30 -10
  43. package/dist/runtime.js +9 -9
  44. package/package.json +1 -1
@@ -17,7 +17,9 @@ type BridgeNavigationResult = {
17
17
  interface BridgeHostNavigation {
18
18
  getLocation(): BridgeLocation;
19
19
  subscribe(listener: (location: BridgeLocation) => void): () => void;
20
- navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
20
+ navigate(target: BridgeLocation, action: 'push' | 'replace', options?: {
21
+ signal?: AbortSignal;
22
+ }): Promise<BridgeNavigationResult>;
21
23
  go(delta: number): void;
22
24
  }
23
25
  /** 子应用路由通道(桥接宿主按挂载代次创建,随 mount 第三参数交给子应用) */
@@ -28,48 +30,21 @@ interface BridgeChildRoute {
28
30
  go(delta: number): void;
29
31
  }
30
32
 
31
- /**
32
- * Vue Router 4 路由适配(/bridge/router/vue 按需入口;URL 同步任务书 §3.1/§3.3)。
33
- *
34
- * 宿主侧:createVueBridgeNavigation(router) 把 Vue Router 实例包装为宿主导航端口——
35
- * push/replace 落定 NavigationFailure | undefined,failure 即真实取消(守卫拒绝),
36
- * 不以「函数返回」冒充提交成功。
37
- *
38
- * 子应用侧:connectVueBridgeRouter(routing, router) 把子应用自己的 memory router
39
- * 接到通道上——RouterLink/router.push 等真实入口经 afterEach 桥为通道请求(宿主仲裁,
40
- * 取消时回滚最后确认位置);通道广播 replace 应用到本地 router。
41
- * 初始位置已由通道就绪;`ready` 落定前 afterEach 不发起请求(原型实证:install 的
42
- * 初始导航竞态,见任务书 §1 原型结论 1)。
43
- *
44
- * 本入口按需导入 vue-router(peerDependenciesMeta.optional);默认 /bridge、/runtime、
45
- * /react 不加载本模块。
46
- */
33
+ /** Vue Router 4 的宿主导航端口与子应用 memory router 接线(按需入口)。 */
47
34
 
48
- /** Vue Router base(createWebHistory/createWebHashHistory 的 base 参数);逻辑路径 = 实际路径去 base */
49
35
  interface VueBridgeNavigationOptions {
36
+ /** 保留配置兼容;Vue Router fullPath 已剥离 history base,不再二次剥离。 */
50
37
  routerBase?: string;
51
38
  }
52
- /** 宿主侧:把 Vue Router 实例包装为宿主导航端口(宿主 Router 是唯一历史写入方) */
53
- declare function createVueBridgeNavigation(router: Router, options?: VueBridgeNavigationOptions): BridgeHostNavigation;
39
+ declare function createVueBridgeNavigation(router: Router, _options?: VueBridgeNavigationOptions): BridgeHostNavigation;
54
40
  interface VueBridgeRouterConnection {
55
- /** 初始位置就绪(含初始 push 与首次广播应用);ready 前后的清理都必须可用 */
41
+ /** 必须 await ready 后再 app.use(router),初始错误会拒绝此 Promise。 */
56
42
  ready: Promise<void>;
57
- /** 清理本接线(通道订阅与本地监听;通道本身由宿主生命周期销毁) */
58
43
  dispose(): void;
59
44
  }
60
- /**
61
- * 子应用侧:把子应用自己的 memory router 接到通道。
62
- *
63
- * ```ts
64
- * export default defineBridgeApp((props, ctx) => {
65
- * const router = createRouter({ history: createMemoryHistory(), routes })
66
- * const conn = connectVueBridgeRouter(ctx.routing, router)
67
- * const app = createApp(App, props)
68
- * app.use(router)
69
- * return app
70
- * }, { routing: true })
71
- * ```
72
- */
73
- declare function connectVueBridgeRouter(routing: BridgeChildRoute, router: Router): VueBridgeRouterConnection;
45
+ /** 接线限子应用自己的 memory router;dispose 恢复原始导航方法。 */
46
+ declare function connectVueBridgeRouter(routing: BridgeChildRoute, router: Router, options?: {
47
+ signal?: AbortSignal;
48
+ }): VueBridgeRouterConnection;
74
49
 
75
50
  export { type VueBridgeNavigationOptions, type VueBridgeRouterConnection, connectVueBridgeRouter, createVueBridgeNavigation };
@@ -1,24 +1,131 @@
1
- // src/bridge-router-vue.ts
2
- function createVueBridgeNavigation(router, options = {}) {
3
- const routerBase = options.routerBase && options.routerBase !== "/" ? options.routerBase.replace(/\/$/, "") : "";
4
- const toLogic = () => {
5
- const raw = new URL(router.currentRoute.value.fullPath || "/", "http://f.invalid");
6
- const pathname = routerBase && raw.pathname.startsWith(routerBase) ? raw.pathname.slice(routerBase.length) || "/" : raw.pathname;
7
- return { pathname, search: raw.search, hash: raw.hash };
1
+ // src/runtime/errors.ts
2
+ var FgError = class extends Error {
3
+ code;
4
+ details;
5
+ constructor(code, message, details, options) {
6
+ super(`[fulgurjs:${code}] ${message}`, options);
7
+ this.name = "FgError";
8
+ this.code = code;
9
+ this.details = details;
10
+ }
11
+ };
12
+
13
+ // src/bridge-errors.ts
14
+ var BridgeErrorCodes = {
15
+ /** 桥接契约非法(./bridge 默认导出缺 mount/unmount 或不是函数) */
16
+ BRIDGE_INVALID_CONTRACT: "MFU-015",
17
+ /** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
18
+ BRIDGE_LIFECYCLE_FAILED: "MFU-016",
19
+ /** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
20
+ BRIDGE_SESSION_MISMATCH: "MFU-017",
21
+ /** 路由同步配置/前缀冲突(basePath 非法、同页前缀冲突或重叠) */
22
+ ROUTING_CONFIG_INVALID: "MFU-030",
23
+ /** 路由协议缺失/通道失效(子应用未声明 routing 协议,或已销毁通道被再次使用) */
24
+ ROUTING_PROTOCOL_MISSING: "MFU-031",
25
+ /** 非法导航(目标越界前缀、非法 go 参数、失效实例的请求) */
26
+ ROUTING_NAVIGATION_INVALID: "MFU-032",
27
+ /** 路由准备/同步失败(重定向循环、子应用路由准备失败且已阻止错误页面显示) */
28
+ ROUTING_SYNC_FAILED: "MFU-033"
29
+ };
30
+ function routingSyncError(spec, reason, chain, cause) {
31
+ return new FgError(
32
+ BridgeErrorCodes.ROUTING_SYNC_FAILED,
33
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
34
+ \u539F\u56E0\uFF1A${reason}
35
+ \u4FEE\u6CD5\uFF1A\u68C0\u67E5\u8DEF\u7531\u5B88\u536B\u3001\u52A0\u8F7D\u5668\u4E0E\u5BFC\u822A\u5F02\u5E38\uFF1B\u91CD\u5B9A\u5411\u94FE\u4E0D\u5E94\u6210\u73AF\u3002\u76EE\u6807\u94FE\uFF1A${chain.join(" \u2192 ")}\u3002`,
36
+ { spec, chain: [...chain] },
37
+ { cause }
38
+ );
39
+ }
40
+
41
+ // src/bridge-router-core.ts
42
+ function sameLocation(a, b) {
43
+ return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
44
+ }
45
+
46
+ // src/bridge-router-sync.ts
47
+ function connectChildNavigation(routing, apply, onError) {
48
+ let disposed = false;
49
+ let generation = 0;
50
+ let tail = Promise.resolve();
51
+ let confirmed = routing.getLocation();
52
+ let pending;
53
+ const restore = () => apply(routing.getLocation());
54
+ const unsub = routing.subscribe((location) => {
55
+ if (disposed) return;
56
+ const changed = !sameLocation(location, confirmed);
57
+ confirmed = location;
58
+ if (pending && sameLocation(location, pending)) return;
59
+ if (changed) generation++;
60
+ tail = tail.then(() => disposed ? void 0 : restore()).catch(onError);
61
+ });
62
+ return {
63
+ enqueue(prepare, action) {
64
+ const version = generation;
65
+ const cancelled = () => ({ status: "cancelled", location: routing.getLocation() });
66
+ const task = tail.then(async () => {
67
+ if (disposed || version !== generation) return cancelled();
68
+ try {
69
+ const target = await prepare();
70
+ if (disposed) return cancelled();
71
+ if (version !== generation) {
72
+ await restore();
73
+ return cancelled();
74
+ }
75
+ if (!target) return cancelled();
76
+ pending = target;
77
+ const result = await routing.navigate(target, action);
78
+ if (!disposed) await restore();
79
+ return result;
80
+ } catch (cause) {
81
+ if (!disposed) {
82
+ try {
83
+ await restore();
84
+ } catch (restoreError) {
85
+ onError(restoreError);
86
+ }
87
+ }
88
+ throw routingSyncError("child-router", `\u5B50\u5E94\u7528\u8DEF\u7531\u51C6\u5907\u6216\u540C\u6B65\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause);
89
+ } finally {
90
+ pending = void 0;
91
+ }
92
+ });
93
+ tail = task.then(() => {
94
+ }, () => {
95
+ });
96
+ return task;
97
+ },
98
+ dispose() {
99
+ disposed = true;
100
+ generation++;
101
+ unsub();
102
+ }
8
103
  };
104
+ }
105
+
106
+ // src/bridge-router-vue.ts
107
+ var locationFromPath = (path) => {
108
+ const raw = new URL(path || "/", "http://f.invalid");
109
+ return { pathname: raw.pathname, search: raw.search, hash: raw.hash };
110
+ };
111
+ function createVueBridgeNavigation(router, _options = {}) {
112
+ const toLogic = () => locationFromPath(router.currentRoute.value.fullPath);
9
113
  return {
10
114
  getLocation: toLogic,
11
115
  subscribe(listener) {
12
- return router.afterEach(() => listener(toLogic()));
116
+ return router.afterEach((_to, _from, failure) => {
117
+ if (!failure) listener(toLogic());
118
+ });
13
119
  },
14
- async navigate(target, action) {
15
- const to = target.pathname + target.search + target.hash;
120
+ async navigate(target, action, options) {
121
+ if (options?.signal?.aborted) return { status: "cancelled", location: toLogic() };
122
+ const removeGuard = router.beforeResolve(() => options?.signal?.aborted ? false : void 0);
16
123
  try {
124
+ const to = target.pathname + target.search + target.hash;
17
125
  const failure = action === "replace" ? await router.replace(to) : await router.push(to);
18
- if (failure) return { status: "cancelled", location: toLogic() };
19
- return { status: "committed", location: toLogic() };
20
- } catch {
21
- return { status: "cancelled", location: toLogic() };
126
+ return { status: failure ? "cancelled" : "committed", location: toLogic() };
127
+ } finally {
128
+ removeGuard();
22
129
  }
23
130
  },
24
131
  go(delta) {
@@ -26,46 +133,83 @@ function createVueBridgeNavigation(router, options = {}) {
26
133
  }
27
134
  };
28
135
  }
29
- function connectVueBridgeRouter(routing, router) {
30
- let syncing = false;
136
+ function connectVueBridgeRouter(routing, router, options = {}) {
137
+ const push = router.push;
138
+ const replace = router.replace;
139
+ const go = router.go;
140
+ const back = router.back;
141
+ const forward = router.forward;
31
142
  let disposed = false;
32
- let started = false;
33
- const unsub = routing.subscribe((loc) => {
143
+ const report = (error) => console.error("[fulgurjs] \u5B50\u5E94\u7528 Vue \u8DEF\u7531\u540C\u6B65\u5931\u8D25\uFF1A", error);
144
+ const apply = async (loc) => {
145
+ const path = loc.pathname + loc.search + loc.hash;
146
+ if (router.currentRoute.value.fullPath !== path) {
147
+ const failure = await replace.call(router, path);
148
+ if (failure && router.currentRoute.value.fullPath !== path) throw routingSyncError("vue-router", "\u5B50\u5E94\u7528\u5B88\u536B\u62D2\u7EDD\u5E94\u7528\u5BBF\u4E3B\u786E\u8BA4\u7684\u4F4D\u7F6E\uFF1B\u8BF7\u5728\u5BBF\u4E3B\u4FA7\u8BBE\u7F6E\u53D6\u6D88\u5B88\u536B\u3002", []);
149
+ }
150
+ };
151
+ const sync = connectChildNavigation(routing, (loc) => ready.then(() => apply(loc)), report);
152
+ const init = routing.getLocation();
153
+ const ready = push.call(router, init.pathname + init.search + init.hash).then(async (failure) => {
34
154
  if (disposed) return;
35
- const next = loc.pathname + loc.search + loc.hash;
36
- if (router.currentRoute.value.fullPath !== next) void router.replace(next);
155
+ if (routing.getLocation().pathname + routing.getLocation().search + routing.getLocation().hash !== init.pathname + init.search + init.hash) {
156
+ await apply(routing.getLocation());
157
+ return;
158
+ }
159
+ if (failure) throw routingSyncError("vue-router", "\u5B50\u5E94\u7528\u521D\u59CB\u8DEF\u7531\u88AB\u5B88\u536B\u53D6\u6D88\u3002", []);
160
+ const target = locationFromPath(router.currentRoute.value.fullPath);
161
+ if (router.currentRoute.value.fullPath !== init.pathname + init.search + init.hash) {
162
+ await routing.navigate(target, "replace");
163
+ await apply(routing.getLocation());
164
+ }
165
+ }).catch((cause) => {
166
+ throw routingSyncError("vue-router", `\u521D\u59CB\u8DEF\u7531\u51C6\u5907\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause);
37
167
  });
38
- const stopWatch = router.afterEach((to) => {
39
- if (disposed || syncing || !started) return;
40
- const chan = routing.getLocation();
41
- const toFull = to.fullPath;
42
- if (toFull === chan.pathname + chan.search + chan.hash) return;
43
- const raw = new URL(toFull, "http://f.invalid");
44
- syncing = true;
45
- void routing.navigate({ pathname: raw.pathname, search: raw.search, hash: raw.hash }, "push").then((result) => {
46
- syncing = false;
47
- if (result.status === "cancelled" && !disposed) {
48
- const c = result.location;
49
- const cur = c.pathname + c.search + c.hash;
50
- if (router.currentRoute.value.fullPath !== cur) void router.replace(cur);
168
+ const intercept = (native, action) => (to) => {
169
+ const task = (async () => {
170
+ let failure;
171
+ const result = await sync.enqueue(async () => {
172
+ await ready;
173
+ failure = await native.call(router, to);
174
+ return failure ? void 0 : locationFromPath(router.currentRoute.value.fullPath);
175
+ }, typeof to === "object" && to.replace ? "replace" : action);
176
+ if (failure) return failure;
177
+ if (result.status === "cancelled") {
178
+ const remove = router.beforeEach(() => false);
179
+ try {
180
+ return await native.call(router, to);
181
+ } finally {
182
+ remove();
183
+ }
51
184
  }
52
- });
53
- });
54
- const init = routing.getLocation();
55
- const ready = router.push(init.pathname + init.search + init.hash).then(() => {
56
- started = true;
57
- }).catch(() => {
58
- started = true;
59
- });
60
- return {
185
+ })();
186
+ void task.catch(report);
187
+ return task;
188
+ };
189
+ router.push = intercept(push, "push");
190
+ router.replace = intercept(replace, "replace");
191
+ router.go = (delta) => {
192
+ if (!disposed) routing.go(delta);
193
+ };
194
+ router.back = () => router.go(-1);
195
+ router.forward = () => router.go(1);
196
+ const connection = {
61
197
  ready,
62
198
  dispose() {
63
199
  if (disposed) return;
64
200
  disposed = true;
65
- stopWatch();
66
- unsub();
201
+ options.signal?.removeEventListener("abort", connection.dispose);
202
+ sync.dispose();
203
+ router.push = push;
204
+ router.replace = replace;
205
+ router.go = go;
206
+ router.back = back;
207
+ router.forward = forward;
67
208
  }
68
209
  };
210
+ options.signal?.addEventListener("abort", connection.dispose, { once: true });
211
+ if (options.signal?.aborted) connection.dispose();
212
+ return connection;
69
213
  }
70
214
  export {
71
215
  connectVueBridgeRouter,
@@ -1,6 +1,6 @@
1
1
  import * as vue from 'vue';
2
- import { V as VueBridgeAppOptions } from './bridge-host-vue-BuCGlLdS.js';
3
- export { A as AppContext, a as BridgeApp } from './bridge-core-BOWvVUxO.js';
2
+ import { V as VueBridgeAppOptions } from './bridge-host-vue-Dd1kkwDb.js';
3
+ export { A as AppContext, a as BridgeApp } from './bridge-core-ejlY9ddN.js';
4
4
 
5
5
  /** Vue 宿主桥接工厂:返回 `{ appProps: P; sessionKey?: string | null }` 形态的包装组件 */
6
6
  declare const createVueBridgeApp: <P = Record<string, unknown>>(spec: string, options?: VueBridgeAppOptions) => vue.Component;
package/dist/bridge.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as react from 'react';
2
- import { B as BridgeHostRouting } from './bridge-core-BOWvVUxO.js';
3
- export { A as AppContext, a as BridgeApp } from './bridge-core-BOWvVUxO.js';
4
- import { R as ReactBridgeAppOptions } from './bridge-host-react-CMVd-ym0.js';
5
- export { B as BridgeErrorFallback } from './bridge-host-react-CMVd-ym0.js';
2
+ import { B as BridgeHostRouting } from './bridge-core-ejlY9ddN.js';
3
+ export { A as AppContext, a as BridgeApp } from './bridge-core-ejlY9ddN.js';
4
+ import { R as ReactBridgeAppOptions } from './bridge-host-react-Bua1TCVh.js';
5
+ export { B as BridgeErrorFallback } from './bridge-host-react-Bua1TCVh.js';
6
6
  import * as vue from 'vue';
7
- import { V as VueBridgeAppOptions } from './bridge-host-vue-BuCGlLdS.js';
7
+ import { V as VueBridgeAppOptions } from './bridge-host-vue-Dd1kkwDb.js';
8
8
 
9
9
  /** Vue 宿主桥接工厂:返回 `{ appProps: P; sessionKey?: string | null }` 形态的包装组件 */
10
10
  declare const createVueBridgeApp: <P = Record<string, unknown>>(spec: string, options?: VueBridgeAppOptions) => vue.Component;
@@ -10,7 +10,7 @@ import {
10
10
  releaseBridgeSession,
11
11
  resolveBridgeContext,
12
12
  withBridgeTimeout
13
- } from "./chunk-BTJGYDHT.js";
13
+ } from "./chunk-UFWZPE36.js";
14
14
 
15
15
  // src/bridge-host-vue.ts
16
16
  import { defineComponent, h, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, toRaw, watch } from "vue";
@@ -3,7 +3,7 @@ import {
3
3
  routingNavigationError,
4
4
  routingProtocolError,
5
5
  routingSyncError
6
- } from "./chunk-JQF7KYZF.js";
6
+ } from "./chunk-HZGQOW35.js";
7
7
 
8
8
  // src/bridge-router-core.ts
9
9
  var MAX_REDIRECT_PER_REQUEST = 5;
@@ -11,10 +11,11 @@ var MAX_GO_DELTA = 50;
11
11
  function normalizeBasePath(basePath, spec) {
12
12
  const bad = (reason) => routingConfigError(spec, reason);
13
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');
14
+ if (/^\/+$/u.test(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
15
  if (!basePath.startsWith("/")) throw bad(`basePath \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
16
16
  if (/[?#]/.test(basePath) || basePath.includes("://")) throw bad(`basePath \u4E0D\u80FD\u5305\u542B query/hash/\u534F\u8BAE\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
17
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
+ if (/\\|\/\/|%(?:2e|2f|5c)/i.test(basePath) || basePath.split("/").some((part) => part === "." || part === "..")) throw bad("basePath \u4E0D\u80FD\u5305\u542B\u7A7A\u8DEF\u5F84\u6BB5\u3001\u8F6C\u4E49\u5206\u9694\u7B26\u6216\u76F8\u5BF9\u8DEF\u5F84\u6BB5\u3002");
18
19
  return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
19
20
  }
20
21
  function matchesBasePath(hostPathname, basePath) {
@@ -38,7 +39,8 @@ function sameLocation(a, b) {
38
39
  return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
39
40
  }
40
41
  function assertChildTargetInScope(target, basePath, spec) {
41
- if (target.pathname.includes("\0") || target.pathname.split("/").includes("..")) {
42
+ if (!target || typeof target.pathname !== "string" || typeof target.search !== "string" || typeof target.hash !== "string" || target.search !== "" && !target.search.startsWith("?") || target.hash !== "" && !target.hash.startsWith("#")) throw routingNavigationError(spec, "\u5BFC\u822A\u76EE\u6807\u5FC5\u987B\u63D0\u4F9B pathname\u3001search\u3001hash \u4E09\u6BB5\uFF0Csearch/hash \u5E94\u4E3A\u7A7A\u6216\u4EE5 ?/# \u5F00\u5934\u3002");
43
+ if (/[\x00-\x20\\?#]|%(?:2e|2f|5c)/i.test(target.pathname) || target.pathname.includes("//") || target.pathname.split("/").some((part) => part === "." || part === "..")) {
42
44
  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
45
  }
44
46
  const host = toHostLocation(target, basePath, spec);
@@ -80,6 +82,8 @@ var RoutingChannel = class {
80
82
  lastConfirmed;
81
83
  seq = 0;
82
84
  inflight = null;
85
+ queue;
86
+ generation = 0;
83
87
  disposed = false;
84
88
  active = true;
85
89
  releasePrefix;
@@ -94,7 +98,8 @@ var RoutingChannel = class {
94
98
  this.spec = spec;
95
99
  this.basePath = normalizeBasePath(basePath, spec);
96
100
  this.host = host;
97
- this.current = toChildLocation(host.getLocation(), this.basePath);
101
+ const initial = host.getLocation();
102
+ this.current = matchesBasePath(initial.pathname, this.basePath) ? toChildLocation(initial, this.basePath) : { pathname: "/", search: "", hash: "" };
98
103
  this.lastConfirmed = { ...this.current };
99
104
  this.releasePrefix = acquireRoutingPrefix(this.basePath, spec);
100
105
  this.unsubHost = host.subscribe((loc) => this.handleHostLocation(loc));
@@ -112,6 +117,7 @@ var RoutingChannel = class {
112
117
  handleHostLocation(hostLoc) {
113
118
  if (this.disposed) return;
114
119
  if (!matchesBasePath(hostLoc.pathname, this.basePath)) {
120
+ this.cancelPending();
115
121
  this.onEvent?.({ type: "prefix-miss", pathname: hostLoc.pathname });
116
122
  return;
117
123
  }
@@ -121,23 +127,21 @@ var RoutingChannel = class {
121
127
  }
122
128
  const child = toChildLocation(hostLoc, this.basePath);
123
129
  if (this.inflight && sameLocation(child, this.inflight.target)) {
124
- const { seq, resolve } = this.inflight;
125
- this.inflight = null;
130
+ const { seq } = this.inflight;
126
131
  this.lastConfirmed = child;
127
132
  this.emit(child);
128
133
  this.onEvent?.({ type: "request-confirmed", seq });
129
- resolve({ status: "committed", location: { ...child } });
130
134
  return;
131
135
  }
132
136
  if (this.inflight) {
133
- const { seq, resolve } = this.inflight;
134
- this.inflight = null;
137
+ const seq = this.inflight.seq;
135
138
  this.lastConfirmed = child;
139
+ this.cancelPending();
136
140
  this.emit(child);
137
141
  this.onEvent?.({ type: "request-superseded", seq });
138
- resolve({ status: "cancelled", location: { ...child } });
139
142
  return;
140
143
  }
144
+ if (!sameLocation(child, this.lastConfirmed)) this.generation++;
141
145
  this.lastConfirmed = child;
142
146
  this.emit(child);
143
147
  this.onEvent?.({ type: "host-broadcast" });
@@ -154,8 +158,31 @@ var RoutingChannel = class {
154
158
  this.listeners.delete(listener);
155
159
  };
156
160
  }
157
- async navigate(target, action) {
158
- if (this.disposed || !this.active) {
161
+ cancelPending() {
162
+ this.generation++;
163
+ const pending = this.inflight;
164
+ this.inflight = null;
165
+ if (pending) {
166
+ pending.abort.abort();
167
+ pending.resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
168
+ }
169
+ }
170
+ navigate(target, action) {
171
+ const generation = this.generation;
172
+ const snapshot = { ...target };
173
+ const run = () => generation !== this.generation ? Promise.resolve({ status: "cancelled", location: { ...this.lastConfirmed } }) : this.performNavigation(snapshot, action);
174
+ const task = this.queue ? this.queue.then(run) : run();
175
+ const tail = task.then(() => {
176
+ }, () => {
177
+ });
178
+ this.queue = tail;
179
+ void tail.then(() => {
180
+ if (this.queue === tail) this.queue = void 0;
181
+ });
182
+ return task;
183
+ }
184
+ async performNavigation(target, action) {
185
+ if (this.disposed || !this.active || !matchesBasePath(this.host.getLocation().pathname, this.basePath)) {
159
186
  this.onEvent?.({ type: this.disposed ? "rejected-disposed" : "rejected-inactive" });
160
187
  return { status: "cancelled", location: { ...this.lastConfirmed } };
161
188
  }
@@ -178,28 +205,12 @@ var RoutingChannel = class {
178
205
  this.redirectChain = [];
179
206
  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
207
  }
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
208
  const seq = ++this.seq;
199
209
  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) => {
210
+ return new Promise((resolve, reject) => {
211
+ const abort = new AbortController();
212
+ this.inflight = { seq, target: { ...target }, resolve, abort };
213
+ Promise.resolve().then(() => abort.signal.aborted ? { status: "cancelled", location: this.host.getLocation() } : this.host.navigate(toHostLocation(target, this.basePath, this.spec), action, { signal: abort.signal })).then((result) => {
203
214
  if (!this.inflight || this.inflight.seq !== seq) return;
204
215
  if (this.disposed || !this.active) {
205
216
  this.inflight = null;
@@ -207,7 +218,7 @@ var RoutingChannel = class {
207
218
  return;
208
219
  }
209
220
  this.inflight = null;
210
- if (result.status === "committed") {
221
+ if (result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath)) {
211
222
  const child = toChildLocation(result.location, this.basePath);
212
223
  this.lastConfirmed = child;
213
224
  this.emit(child);
@@ -216,21 +227,21 @@ var RoutingChannel = class {
216
227
  }
217
228
  this.onEvent?.({ type: "host-verdict", seq, status: result.status });
218
229
  resolve(
219
- result.status === "committed" ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
230
+ result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath) ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
220
231
  );
221
- }).catch(() => {
232
+ }).catch((cause) => {
222
233
  if (this.inflight?.seq === seq) {
223
234
  this.inflight = null;
224
235
  this.onEvent?.({ type: "host-error", seq });
225
236
  this.emit({ ...this.lastConfirmed });
226
- resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
237
+ reject(routingSyncError(this.spec, `\u5BBF\u4E3B\u5BFC\u822A\u6267\u884C\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause));
227
238
  }
228
239
  });
229
240
  });
230
241
  }
231
242
  /** 委托宿主历史(有限整数);不创建第二条独立历史 */
232
243
  go(delta) {
233
- if (this.disposed || !this.active) return;
244
+ if (this.disposed || !this.active || !matchesBasePath(this.host.getLocation().pathname, this.basePath)) return;
234
245
  if (typeof delta !== "number" || !Number.isInteger(delta) || Math.abs(delta) > MAX_GO_DELTA) {
235
246
  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
247
  }
@@ -239,6 +250,7 @@ var RoutingChannel = class {
239
250
  /** KeepAlive deactivate:暂停广播与写入;通道与订阅保留(activate 后 resync) */
240
251
  setActive(active) {
241
252
  if (this.disposed) return;
253
+ if (!active) this.cancelPending();
242
254
  this.active = active;
243
255
  this.onEvent?.({ type: active ? "resumed" : "paused" });
244
256
  if (active) this.handleHostLocation(this.host.getLocation());
@@ -250,11 +262,7 @@ var RoutingChannel = class {
250
262
  if (this.redirectTimer) clearTimeout(this.redirectTimer);
251
263
  this.unsubHost();
252
264
  this.releasePrefix();
253
- if (this.inflight) {
254
- const { resolve } = this.inflight;
255
- this.inflight = null;
256
- resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
257
- }
265
+ this.cancelPending();
258
266
  this.listeners.clear();
259
267
  this.onEvent?.({ type: "disposed" });
260
268
  }
@@ -101,13 +101,14 @@ function routingNavigationError(spec, reason) {
101
101
  { spec }
102
102
  );
103
103
  }
104
- function routingSyncError(spec, reason, chain) {
104
+ function routingSyncError(spec, reason, chain, cause) {
105
105
  return new FgError(
106
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
107
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
108
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] }
109
+ \u4FEE\u6CD5\uFF1A\u68C0\u67E5\u8DEF\u7531\u5B88\u536B\u3001\u52A0\u8F7D\u5668\u4E0E\u5BFC\u822A\u5F02\u5E38\uFF1B\u91CD\u5B9A\u5411\u94FE\u4E0D\u5E94\u6210\u73AF\u3002\u76EE\u6807\u94FE\uFF1A${chain.join(" \u2192 ")}\u3002`,
110
+ { spec, chain: [...chain] },
111
+ { cause }
111
112
  );
112
113
  }
113
114
 
@@ -10,7 +10,7 @@ import {
10
10
  releaseBridgeSession,
11
11
  resolveBridgeContext,
12
12
  withBridgeTimeout
13
- } from "./chunk-BTJGYDHT.js";
13
+ } from "./chunk-UFWZPE36.js";
14
14
 
15
15
  // src/bridge-host-react.ts
16
16
  import {
@@ -3,7 +3,7 @@ import {
3
3
  bridgeSessionMismatchError,
4
4
  invalidBridgeContractError,
5
5
  invalidSessionKeyError
6
- } from "./chunk-JQF7KYZF.js";
6
+ } from "./chunk-HZGQOW35.js";
7
7
  import {
8
8
  clearAppContext
9
9
  } from "./chunk-TSYLDY6H.js";