@fulgurjs/federation 5.4.0 → 5.4.2

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 (60) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.en.md +6 -4
  3. package/README.md +15 -10
  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 +25 -42
  30. package/dist/bridge-router-react.js +213 -36
  31. package/dist/bridge-router-vue.d.ts +18 -37
  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 +42 -10
  42. package/dist/index.js +42 -10
  43. package/dist/runtime.js +9 -9
  44. package/examples/bridge/react-host/package-lock.json +13 -5
  45. package/examples/bridge/react-host/package.json +1 -1
  46. package/examples/bridge/react-remote/package-lock.json +13 -5
  47. package/examples/bridge/react-remote/package.json +1 -1
  48. package/examples/bridge/vue-host/package-lock.json +13 -5
  49. package/examples/bridge/vue-host/package.json +1 -1
  50. package/examples/bridge/vue-remote/package-lock.json +13 -5
  51. package/examples/bridge/vue-remote/package.json +1 -1
  52. package/examples/react/host/package-lock.json +13 -4
  53. package/examples/react/host/package.json +1 -1
  54. package/examples/react/remote/package-lock.json +13 -4
  55. package/examples/react/remote/package.json +1 -1
  56. package/examples/vue/host/package-lock.json +13 -4
  57. package/examples/vue/host/package.json +1 -1
  58. package/examples/vue/remote/package-lock.json +13 -4
  59. package/examples/vue/remote/package.json +1 -1
  60. package/package.json +1 -1
@@ -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";
@@ -627,16 +627,7 @@ function createRuntime() {
627
627
  return selectShareEntry(name, opts, true)?.value;
628
628
  }
629
629
  function pinLoadedShare2(name, opts, localVersion, instance) {
630
- if (instance === void 0) return;
631
- let entry;
632
- try {
633
- entry = selectShareEntry(name, opts);
634
- } catch {
635
- return;
636
- }
637
- if (!entry || entry.value !== void 0 || entry.version !== localVersion) return;
638
- entry.value = instance;
639
- entry.loaded = true;
630
+ registerFallbackInstance(name, { ...opts, localVersion }, instance);
640
631
  }
641
632
  function sanitizeUrl(u) {
642
633
  try {
@@ -1230,13 +1221,14 @@ function routingNavigationError(spec, reason) {
1230
1221
  { spec }
1231
1222
  );
1232
1223
  }
1233
- function routingSyncError(spec, reason, chain) {
1224
+ function routingSyncError(spec, reason, chain, cause) {
1234
1225
  return new FgError(
1235
1226
  BridgeErrorCodes.ROUTING_SYNC_FAILED,
1236
- `\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
1227
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
1237
1228
  \u539F\u56E0\uFF1A${reason}
1238
- \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`,
1239
- { spec, chain: [...chain] }
1229
+ \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`,
1230
+ { spec, chain: [...chain] },
1231
+ { cause }
1240
1232
  );
1241
1233
  }
1242
1234
 
@@ -1349,10 +1341,11 @@ var MAX_GO_DELTA = 50;
1349
1341
  function normalizeBasePath(basePath, spec) {
1350
1342
  const bad = (reason) => routingConfigError(spec, reason);
1351
1343
  if (typeof basePath !== "string" || basePath === "") throw bad("basePath \u4E0D\u80FD\u4E3A\u7A7A\u3002");
1352
- 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');
1344
+ 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');
1353
1345
  if (!basePath.startsWith("/")) throw bad(`basePath \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
1354
1346
  if (/[?#]/.test(basePath) || basePath.includes("://")) throw bad(`basePath \u4E0D\u80FD\u5305\u542B query/hash/\u534F\u8BAE\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
1355
1347
  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`);
1348
+ 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");
1356
1349
  return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
1357
1350
  }
1358
1351
  function matchesBasePath(hostPathname, basePath) {
@@ -1376,7 +1369,8 @@ function sameLocation(a, b) {
1376
1369
  return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
1377
1370
  }
1378
1371
  function assertChildTargetInScope(target, basePath, spec) {
1379
- if (target.pathname.includes("\0") || target.pathname.split("/").includes("..")) {
1372
+ 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");
1373
+ if (/[\x00-\x20\\?#]|%(?:2e|2f|5c)/i.test(target.pathname) || target.pathname.includes("//") || target.pathname.split("/").some((part) => part === "." || part === "..")) {
1380
1374
  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`);
1381
1375
  }
1382
1376
  const host = toHostLocation(target, basePath, spec);
@@ -1418,6 +1412,8 @@ var RoutingChannel = class {
1418
1412
  lastConfirmed;
1419
1413
  seq = 0;
1420
1414
  inflight = null;
1415
+ queue;
1416
+ generation = 0;
1421
1417
  disposed = false;
1422
1418
  active = true;
1423
1419
  releasePrefix;
@@ -1432,7 +1428,8 @@ var RoutingChannel = class {
1432
1428
  this.spec = spec;
1433
1429
  this.basePath = normalizeBasePath(basePath, spec);
1434
1430
  this.host = host;
1435
- this.current = toChildLocation(host.getLocation(), this.basePath);
1431
+ const initial = host.getLocation();
1432
+ this.current = matchesBasePath(initial.pathname, this.basePath) ? toChildLocation(initial, this.basePath) : { pathname: "/", search: "", hash: "" };
1436
1433
  this.lastConfirmed = { ...this.current };
1437
1434
  this.releasePrefix = acquireRoutingPrefix(this.basePath, spec);
1438
1435
  this.unsubHost = host.subscribe((loc) => this.handleHostLocation(loc));
@@ -1450,6 +1447,7 @@ var RoutingChannel = class {
1450
1447
  handleHostLocation(hostLoc) {
1451
1448
  if (this.disposed) return;
1452
1449
  if (!matchesBasePath(hostLoc.pathname, this.basePath)) {
1450
+ this.cancelPending();
1453
1451
  this.onEvent?.({ type: "prefix-miss", pathname: hostLoc.pathname });
1454
1452
  return;
1455
1453
  }
@@ -1459,23 +1457,21 @@ var RoutingChannel = class {
1459
1457
  }
1460
1458
  const child = toChildLocation(hostLoc, this.basePath);
1461
1459
  if (this.inflight && sameLocation(child, this.inflight.target)) {
1462
- const { seq, resolve } = this.inflight;
1463
- this.inflight = null;
1460
+ const { seq } = this.inflight;
1464
1461
  this.lastConfirmed = child;
1465
1462
  this.emit(child);
1466
1463
  this.onEvent?.({ type: "request-confirmed", seq });
1467
- resolve({ status: "committed", location: { ...child } });
1468
1464
  return;
1469
1465
  }
1470
1466
  if (this.inflight) {
1471
- const { seq, resolve } = this.inflight;
1472
- this.inflight = null;
1467
+ const seq = this.inflight.seq;
1473
1468
  this.lastConfirmed = child;
1469
+ this.cancelPending();
1474
1470
  this.emit(child);
1475
1471
  this.onEvent?.({ type: "request-superseded", seq });
1476
- resolve({ status: "cancelled", location: { ...child } });
1477
1472
  return;
1478
1473
  }
1474
+ if (!sameLocation(child, this.lastConfirmed)) this.generation++;
1479
1475
  this.lastConfirmed = child;
1480
1476
  this.emit(child);
1481
1477
  this.onEvent?.({ type: "host-broadcast" });
@@ -1492,8 +1488,31 @@ var RoutingChannel = class {
1492
1488
  this.listeners.delete(listener);
1493
1489
  };
1494
1490
  }
1495
- async navigate(target, action) {
1496
- if (this.disposed || !this.active) {
1491
+ cancelPending() {
1492
+ this.generation++;
1493
+ const pending = this.inflight;
1494
+ this.inflight = null;
1495
+ if (pending) {
1496
+ pending.abort.abort();
1497
+ pending.resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
1498
+ }
1499
+ }
1500
+ navigate(target, action) {
1501
+ const generation = this.generation;
1502
+ const snapshot = { ...target };
1503
+ const run = () => generation !== this.generation ? Promise.resolve({ status: "cancelled", location: { ...this.lastConfirmed } }) : this.performNavigation(snapshot, action);
1504
+ const task = this.queue ? this.queue.then(run) : run();
1505
+ const tail = task.then(() => {
1506
+ }, () => {
1507
+ });
1508
+ this.queue = tail;
1509
+ void tail.then(() => {
1510
+ if (this.queue === tail) this.queue = void 0;
1511
+ });
1512
+ return task;
1513
+ }
1514
+ async performNavigation(target, action) {
1515
+ if (this.disposed || !this.active || !matchesBasePath(this.host.getLocation().pathname, this.basePath)) {
1497
1516
  this.onEvent?.({ type: this.disposed ? "rejected-disposed" : "rejected-inactive" });
1498
1517
  return { status: "cancelled", location: { ...this.lastConfirmed } };
1499
1518
  }
@@ -1516,28 +1535,12 @@ var RoutingChannel = class {
1516
1535
  this.redirectChain = [];
1517
1536
  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);
1518
1537
  }
1519
- if (this.inflight) {
1520
- const prev = this.inflight;
1521
- await new Promise((resolve) => {
1522
- const timer = setInterval(() => {
1523
- if (this.inflight !== prev) {
1524
- clearInterval(timer);
1525
- resolve();
1526
- }
1527
- }, 0);
1528
- });
1529
- if (this.disposed || !this.active) {
1530
- return { status: "cancelled", location: { ...this.lastConfirmed } };
1531
- }
1532
- if (sameLocation(target, this.lastConfirmed)) {
1533
- return { status: "committed", location: { ...this.lastConfirmed } };
1534
- }
1535
- }
1536
1538
  const seq = ++this.seq;
1537
1539
  this.onEvent?.({ type: "request-start", seq, target: { ...target }, action });
1538
- return new Promise((resolve) => {
1539
- this.inflight = { seq, target: { ...target }, resolve };
1540
- this.host.navigate(toHostLocation(target, this.basePath, this.spec), action).then((result) => {
1540
+ return new Promise((resolve, reject) => {
1541
+ const abort = new AbortController();
1542
+ this.inflight = { seq, target: { ...target }, resolve, abort };
1543
+ 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) => {
1541
1544
  if (!this.inflight || this.inflight.seq !== seq) return;
1542
1545
  if (this.disposed || !this.active) {
1543
1546
  this.inflight = null;
@@ -1545,7 +1548,7 @@ var RoutingChannel = class {
1545
1548
  return;
1546
1549
  }
1547
1550
  this.inflight = null;
1548
- if (result.status === "committed") {
1551
+ if (result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath)) {
1549
1552
  const child = toChildLocation(result.location, this.basePath);
1550
1553
  this.lastConfirmed = child;
1551
1554
  this.emit(child);
@@ -1554,21 +1557,21 @@ var RoutingChannel = class {
1554
1557
  }
1555
1558
  this.onEvent?.({ type: "host-verdict", seq, status: result.status });
1556
1559
  resolve(
1557
- result.status === "committed" ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
1560
+ result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath) ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
1558
1561
  );
1559
- }).catch(() => {
1562
+ }).catch((cause) => {
1560
1563
  if (this.inflight?.seq === seq) {
1561
1564
  this.inflight = null;
1562
1565
  this.onEvent?.({ type: "host-error", seq });
1563
1566
  this.emit({ ...this.lastConfirmed });
1564
- resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
1567
+ reject(routingSyncError(this.spec, `\u5BBF\u4E3B\u5BFC\u822A\u6267\u884C\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause));
1565
1568
  }
1566
1569
  });
1567
1570
  });
1568
1571
  }
1569
1572
  /** 委托宿主历史(有限整数);不创建第二条独立历史 */
1570
1573
  go(delta) {
1571
- if (this.disposed || !this.active) return;
1574
+ if (this.disposed || !this.active || !matchesBasePath(this.host.getLocation().pathname, this.basePath)) return;
1572
1575
  if (typeof delta !== "number" || !Number.isInteger(delta) || Math.abs(delta) > MAX_GO_DELTA) {
1573
1576
  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`);
1574
1577
  }
@@ -1577,6 +1580,7 @@ var RoutingChannel = class {
1577
1580
  /** KeepAlive deactivate:暂停广播与写入;通道与订阅保留(activate 后 resync) */
1578
1581
  setActive(active) {
1579
1582
  if (this.disposed) return;
1583
+ if (!active) this.cancelPending();
1580
1584
  this.active = active;
1581
1585
  this.onEvent?.({ type: active ? "resumed" : "paused" });
1582
1586
  if (active) this.handleHostLocation(this.host.getLocation());
@@ -1588,11 +1592,7 @@ var RoutingChannel = class {
1588
1592
  if (this.redirectTimer) clearTimeout(this.redirectTimer);
1589
1593
  this.unsubHost();
1590
1594
  this.releasePrefix();
1591
- if (this.inflight) {
1592
- const { resolve } = this.inflight;
1593
- this.inflight = null;
1594
- resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
1595
- }
1595
+ this.cancelPending();
1596
1596
  this.listeners.clear();
1597
1597
  this.onEvent?.({ type: "disposed" });
1598
1598
  }