alepha 0.19.3 → 0.19.4

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 (215) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +99 -43
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +257 -40
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +5 -5
  14. package/dist/api/notifications/index.browser.js +0 -1
  15. package/dist/api/notifications/index.browser.js.map +1 -1
  16. package/dist/api/notifications/index.d.ts +3 -3
  17. package/dist/api/notifications/index.d.ts.map +1 -1
  18. package/dist/api/notifications/index.js +0 -1
  19. package/dist/api/notifications/index.js.map +1 -1
  20. package/dist/api/parameters/index.browser.js +112 -1
  21. package/dist/api/parameters/index.browser.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +90 -3
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +79 -12
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/{billing → api/payments}/index.d.ts +67 -49
  27. package/dist/api/payments/index.d.ts.map +1 -0
  28. package/dist/{billing → api/payments}/index.js +108 -74
  29. package/dist/api/payments/index.js.map +1 -0
  30. package/dist/api/subscriptions/index.d.ts +1692 -0
  31. package/dist/api/subscriptions/index.d.ts.map +1 -0
  32. package/dist/api/subscriptions/index.js +1870 -0
  33. package/dist/api/subscriptions/index.js.map +1 -0
  34. package/dist/api/users/index.d.ts +18 -2
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +167 -34
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.d.ts +13 -13
  39. package/dist/api/workflows/index.browser.js +246 -0
  40. package/dist/api/workflows/index.browser.js.map +1 -0
  41. package/dist/api/workflows/index.d.ts +1618 -0
  42. package/dist/api/workflows/index.d.ts.map +1 -0
  43. package/dist/api/workflows/index.js +1504 -0
  44. package/dist/api/workflows/index.js.map +1 -0
  45. package/dist/cli/core/index.d.ts +44 -28
  46. package/dist/cli/core/index.d.ts.map +1 -1
  47. package/dist/cli/core/index.js +16 -61
  48. package/dist/cli/core/index.js.map +1 -1
  49. package/dist/cli/vendor/index.d.ts +31 -8
  50. package/dist/cli/vendor/index.d.ts.map +1 -1
  51. package/dist/cli/vendor/index.js +79 -24
  52. package/dist/cli/vendor/index.js.map +1 -1
  53. package/dist/core/index.browser.js +21 -2
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +33 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +21 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/index.native.js +21 -2
  60. package/dist/core/index.native.js.map +1 -1
  61. package/dist/core/index.workerd.js +21 -2
  62. package/dist/core/index.workerd.js.map +1 -1
  63. package/dist/email/smtp/index.js +24 -8
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/orm/core/index.browser.js +0 -18
  66. package/dist/orm/core/index.browser.js.map +1 -1
  67. package/dist/orm/core/index.bun.js +0 -17
  68. package/dist/orm/core/index.bun.js.map +1 -1
  69. package/dist/orm/core/index.d.ts +1 -13
  70. package/dist/orm/core/index.d.ts.map +1 -1
  71. package/dist/orm/core/index.js +0 -17
  72. package/dist/orm/core/index.js.map +1 -1
  73. package/dist/orm/postgres/index.bun.js +3 -3
  74. package/dist/orm/postgres/index.bun.js.map +1 -1
  75. package/dist/orm/postgres/index.d.ts.map +1 -1
  76. package/dist/orm/postgres/index.js +3 -3
  77. package/dist/orm/postgres/index.js.map +1 -1
  78. package/dist/react/router/index.browser.js +25 -3
  79. package/dist/react/router/index.browser.js.map +1 -1
  80. package/dist/react/router/index.d.ts +16 -1
  81. package/dist/react/router/index.d.ts.map +1 -1
  82. package/dist/react/router/index.js +25 -3
  83. package/dist/react/router/index.js.map +1 -1
  84. package/dist/security/index.d.ts +28 -0
  85. package/dist/security/index.d.ts.map +1 -1
  86. package/dist/security/index.js +28 -0
  87. package/dist/security/index.js.map +1 -1
  88. package/package.json +37 -20
  89. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  90. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  91. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  92. package/src/api/invitations/entities/invitations.ts +33 -0
  93. package/src/api/invitations/index.ts +65 -0
  94. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  95. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  96. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  97. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  98. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  99. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  100. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  101. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  102. package/src/api/invitations/services/InvitationService.ts +556 -0
  103. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  104. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  105. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  106. package/src/api/jobs/index.ts +0 -3
  107. package/src/api/jobs/primitives/$job.ts +22 -11
  108. package/src/api/jobs/providers/JobProvider.ts +229 -19
  109. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  110. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  111. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  112. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  113. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  114. package/src/api/jobs/services/JobService.ts +51 -12
  115. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  116. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  117. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  118. package/src/api/parameters/index.browser.ts +12 -0
  119. package/src/api/parameters/primitives/$parameter.ts +20 -3
  120. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  121. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  122. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  123. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  124. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  125. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  126. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  127. package/src/{billing → api/payments}/index.ts +31 -25
  128. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  129. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  130. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  131. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  132. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  133. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  134. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  135. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  136. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  137. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  138. package/src/api/subscriptions/index.ts +144 -0
  139. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  140. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  141. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  142. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  143. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  144. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  145. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  146. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  147. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  148. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  149. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  150. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  151. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  152. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  153. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  154. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  155. package/src/api/subscriptions/services/BillingService.ts +437 -0
  156. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  157. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  158. package/src/api/subscriptions/services/UsageService.ts +118 -0
  159. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  160. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  161. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  162. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  163. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  164. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  165. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  166. package/src/api/users/controllers/UserController.ts +3 -8
  167. package/src/api/users/notifications/UserNotifications.ts +23 -0
  168. package/src/api/users/schemas/loginSchema.ts +1 -1
  169. package/src/api/users/services/CredentialService.ts +51 -4
  170. package/src/api/users/services/RegistrationService.ts +38 -9
  171. package/src/api/users/services/SessionService.ts +62 -9
  172. package/src/api/users/services/UserService.ts +21 -12
  173. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  174. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  175. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  176. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  177. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  178. package/src/api/workflows/index.browser.ts +22 -0
  179. package/src/api/workflows/index.ts +124 -0
  180. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  181. package/src/api/workflows/primitives/$workflow.ts +202 -0
  182. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  183. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  184. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  185. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  186. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  187. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  188. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  189. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  190. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  191. package/src/api/workflows/services/WorkflowService.ts +382 -0
  192. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  193. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  194. package/src/cli/vendor/services/VendorService.ts +126 -27
  195. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  196. package/src/core/providers/SchemaValidator.ts +1 -1
  197. package/src/core/providers/TypeProvider.ts +46 -3
  198. package/src/orm/__tests__/enums.spec.ts +22 -29
  199. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  200. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  202. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  203. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  204. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  205. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  206. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  207. package/src/security/primitives/$secure.ts +28 -0
  208. package/dist/billing/index.d.ts.map +0 -1
  209. package/dist/billing/index.js.map +0 -1
  210. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  211. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  212. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  213. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  214. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  215. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -882,4 +882,161 @@ describe("$page browser tests", () => {
882
882
  });
883
883
  });
884
884
  });
885
+
886
+ describe("transition supersession", () => {
887
+ it("should not commit a stale slow transition when a newer navigation already won", async () => {
888
+ let resolvePageA: ((value: { data: string }) => void) | undefined;
889
+ const pageARendered = vi.fn();
890
+ const pageBRendered = vi.fn();
891
+
892
+ class App {
893
+ pageA = $page({
894
+ path: "/page-a",
895
+ loader: () =>
896
+ new Promise<{ data: string }>((resolve) => {
897
+ resolvePageA = resolve;
898
+ }),
899
+ component: ({ data }: { data: string }) => {
900
+ pageARendered();
901
+ return <div data-testid="page-a">A: {data}</div>;
902
+ },
903
+ });
904
+
905
+ pageB = $page({
906
+ path: "/page-b",
907
+ component: () => {
908
+ pageBRendered();
909
+ return <div data-testid="page-b">B</div>;
910
+ },
911
+ });
912
+ }
913
+
914
+ alepha = Alepha.create().with(AlephaReact).with(App);
915
+ await alepha.start();
916
+
917
+ const router = alepha.inject(ReactRouter);
918
+
919
+ // Start navigating to /page-a — its loader hangs, push() will not
920
+ // resolve until we manually resolve the loader below.
921
+ const pushAPromise = router.push("/page-a");
922
+
923
+ // Yield so the in-flight transition for /page-a actually starts and
924
+ // reaches the awaited loader before pushB enters the race.
925
+ await new Promise((r) => setTimeout(r, 0));
926
+
927
+ // Navigate to /page-b before /page-a finishes loading. This should
928
+ // supersede the in-flight /page-a transition.
929
+ await act(async () => {
930
+ await router.push("/page-b");
931
+ });
932
+
933
+ // /page-b should now be the committed router state.
934
+ expect(router.state.name).toBe("pageB");
935
+ expect(router.state.url.pathname).toBe("/page-b");
936
+
937
+ // Now resolve /page-a's loader: this is the race window where the
938
+ // stale /page-a transition could overwrite /page-b.
939
+ resolvePageA?.({ data: "loaded" });
940
+ await act(async () => {
941
+ await pushAPromise;
942
+ });
943
+
944
+ // Stale /page-a must NOT have committed.
945
+ expect(router.state.name).toBe("pageB");
946
+ expect(router.state.url.pathname).toBe("/page-b");
947
+ // /page-a's component must never have been instantiated.
948
+ expect(pageARendered).not.toHaveBeenCalled();
949
+ });
950
+
951
+ it("should not fire onEnter for a stale superseded page", async () => {
952
+ let resolvePageA: (() => void) | undefined;
953
+ const pageAOnEnter = vi.fn();
954
+ const pageBOnEnter = vi.fn();
955
+
956
+ class App {
957
+ pageA = $page({
958
+ path: "/page-a",
959
+ loader: () =>
960
+ new Promise<void>((resolve) => {
961
+ resolvePageA = resolve;
962
+ }),
963
+ onEnter: pageAOnEnter,
964
+ component: () => <div data-testid="page-a">A</div>,
965
+ });
966
+
967
+ pageB = $page({
968
+ path: "/page-b",
969
+ onEnter: pageBOnEnter,
970
+ component: () => <div data-testid="page-b">B</div>,
971
+ });
972
+ }
973
+
974
+ alepha = Alepha.create().with(AlephaReact).with(App);
975
+ await alepha.start();
976
+
977
+ const router = alepha.inject(ReactRouter);
978
+
979
+ const pushAPromise = router.push("/page-a");
980
+ await new Promise((r) => setTimeout(r, 0));
981
+
982
+ await act(async () => {
983
+ await router.push("/page-b");
984
+ });
985
+
986
+ expect(router.state.name).toBe("pageB");
987
+ expect(pageBOnEnter).toHaveBeenCalledTimes(1);
988
+ expect(pageAOnEnter).not.toHaveBeenCalled();
989
+
990
+ // Resolve the stale loader: it must remain a no-op.
991
+ resolvePageA?.();
992
+ await act(async () => {
993
+ await pushAPromise;
994
+ });
995
+
996
+ expect(router.state.name).toBe("pageB");
997
+ expect(pageAOnEnter).not.toHaveBeenCalled();
998
+ expect(pageBOnEnter).toHaveBeenCalledTimes(1);
999
+ });
1000
+
1001
+ it("should not call pushState for a stale superseded transition", async () => {
1002
+ let resolvePageA: (() => void) | undefined;
1003
+
1004
+ class App {
1005
+ pageA = $page({
1006
+ path: "/page-a",
1007
+ loader: () =>
1008
+ new Promise<void>((resolve) => {
1009
+ resolvePageA = resolve;
1010
+ }),
1011
+ component: () => <div data-testid="page-a">A</div>,
1012
+ });
1013
+
1014
+ pageB = $page({
1015
+ path: "/page-b",
1016
+ component: () => <div data-testid="page-b">B</div>,
1017
+ });
1018
+ }
1019
+
1020
+ alepha = Alepha.create().with(AlephaReact).with(App);
1021
+ await alepha.start();
1022
+
1023
+ const router = alepha.inject(ReactRouter);
1024
+
1025
+ const pushAPromise = router.push("/page-a");
1026
+ await new Promise((r) => setTimeout(r, 0));
1027
+
1028
+ await act(async () => {
1029
+ await router.push("/page-b");
1030
+ });
1031
+ expect(window.location.pathname).toBe("/page-b");
1032
+
1033
+ resolvePageA?.();
1034
+ await act(async () => {
1035
+ await pushAPromise;
1036
+ });
1037
+
1038
+ // The stale /page-a transition must not have rewritten the URL bar.
1039
+ expect(window.location.pathname).toBe("/page-b");
1040
+ });
1041
+ });
885
1042
  });
@@ -79,6 +79,17 @@ export class ReactBrowserProvider {
79
79
  from?: string;
80
80
  };
81
81
 
82
+ /**
83
+ * Monotonic counter used to detect stale (superseded) transitions.
84
+ *
85
+ * Each call to `render()` captures `++this.transitionId` and any
86
+ * subsequent `render()` invalidates older in-flight transitions.
87
+ * This prevents a slow page from racing past a newer navigation
88
+ * (e.g. user clicks /pageA which has a 2s loader, then clicks /pageB
89
+ * — pageB must remain the committed page).
90
+ */
91
+ protected transitionId = 0;
92
+
82
93
  public get state(): ReactRouterState {
83
94
  return this.alepha.store.get("alepha.react.router.state")!;
84
95
  }
@@ -167,12 +178,21 @@ export class ReactBrowserProvider {
167
178
  options,
168
179
  });
169
180
 
181
+ const myTransitionId = ++this.transitionId;
182
+
170
183
  await this.render({
171
184
  url,
172
185
  previous: options.force ? [] : this.state.layers,
173
186
  meta: options.meta,
187
+ transitionId: myTransitionId,
174
188
  });
175
189
 
190
+ // A newer navigation has superseded us — bail out without touching
191
+ // history, otherwise we'd push a duplicate/stale entry.
192
+ if (myTransitionId !== this.transitionId) {
193
+ return;
194
+ }
195
+
176
196
  // when redirecting in browser
177
197
  if (this.state.url.pathname + this.state.url.search !== url) {
178
198
  this.pushState(this.state.url.pathname + this.state.url.search);
@@ -183,6 +203,7 @@ export class ReactBrowserProvider {
183
203
  }
184
204
 
185
205
  protected async render(options: RouterRenderOptions = {}): Promise<void> {
206
+ const myTransitionId = options.transitionId ?? ++this.transitionId;
186
207
  const previous = options.previous ?? this.state.layers;
187
208
  const url = options.url ?? this.url;
188
209
  const start = this.dateTimeProvider.now();
@@ -196,12 +217,25 @@ export class ReactBrowserProvider {
196
217
  to: url,
197
218
  });
198
219
 
220
+ const isStale = () => this.transitionId !== myTransitionId;
221
+
199
222
  const redirect = await this.router.transition(
200
223
  new URL(`http://localhost${url}`),
201
224
  previous,
202
225
  options.meta,
226
+ isStale,
203
227
  );
204
228
 
229
+ // A newer navigation has superseded us between the time we awaited
230
+ // transition() and now. Drop everything: don't follow redirects, don't
231
+ // log success, don't clear `transitioning` (the newer render owns it).
232
+ if (isStale()) {
233
+ this.log.debug("Transition superseded — discarding stale result", {
234
+ to: url,
235
+ });
236
+ return;
237
+ }
238
+
205
239
  if (redirect) {
206
240
  this.log.info("Redirecting to", {
207
241
  redirect,
@@ -307,4 +341,9 @@ export interface RouterRenderOptions {
307
341
  url?: string;
308
342
  previous?: PreviousLayerData[];
309
343
  meta?: Record<string, any>;
344
+ /**
345
+ * Transition id used to detect supersession by a newer navigation.
346
+ * When omitted, render() allocates a fresh id internally.
347
+ */
348
+ transitionId?: number;
310
349
  }
@@ -51,6 +51,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
51
51
  url: URL,
52
52
  previous: PreviousLayerData[] = [],
53
53
  meta = {},
54
+ isStale: () => boolean = () => false,
54
55
  ): Promise<string | void> {
55
56
  const { pathname, search } = url;
56
57
 
@@ -101,6 +102,13 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
101
102
  state,
102
103
  previous,
103
104
  );
105
+ // A newer navigation already won — bail before committing or
106
+ // emitting any further events. The caller (ReactBrowserProvider)
107
+ // also re-checks staleness, but stopping here avoids running
108
+ // success hooks for a transition the user no longer wants.
109
+ if (isStale()) {
110
+ return;
111
+ }
104
112
  if (redirect) {
105
113
  return redirect;
106
114
  }
@@ -120,6 +128,13 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
120
128
  });
121
129
  await this.alepha.events.emit("react:transition:success", { state });
122
130
  } catch (e) {
131
+ // If we were superseded mid-flight, swallow the error: the user has
132
+ // already moved on, and an error UI for an abandoned page would
133
+ // overwrite the newer page they actually want.
134
+ if (isStale()) {
135
+ return;
136
+ }
137
+
123
138
  this.log.error("Transition has failed", e);
124
139
 
125
140
  let element: ReactNode | undefined;
@@ -151,6 +166,13 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
151
166
  });
152
167
  }
153
168
 
169
+ // Final supersession check before any side effects (onLeave/onEnter,
170
+ // store mutation, head rewrite). Stale transitions must be a complete
171
+ // no-op from this point on.
172
+ if (isStale()) {
173
+ return;
174
+ }
175
+
154
176
  // [feature]: local hook for leaving a page
155
177
  if (previous) {
156
178
  for (let i = 0; i < previous.length; i++) {