@bleedingdev/modern-js-i18n-runtime-extensions 3.8.3-ultramodern.6 → 3.9.0-ultramodern.10

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 (56) hide show
  1. package/dist/cjs/index.js +9 -2
  2. package/dist/cjs/localeRedirectPolicy.js +96 -0
  3. package/dist/cjs/localisedUrls/index.js +7 -0
  4. package/dist/cjs/localisedUrls/redirect.js +3 -6
  5. package/dist/cjs/localisedUrls/routeIdentity.js +118 -0
  6. package/dist/cjs/localisedUrls/routes.js +8 -0
  7. package/dist/cjs/paths.js +56 -0
  8. package/dist/cjs/router-navigation/adapter.js +235 -0
  9. package/dist/cjs/router-navigation/index.js +40 -0
  10. package/dist/esm/index.mjs +1 -0
  11. package/dist/esm/localeRedirectPolicy.mjs +46 -0
  12. package/dist/esm/localisedUrls/index.mjs +1 -0
  13. package/dist/esm/localisedUrls/redirect.mjs +3 -6
  14. package/dist/esm/localisedUrls/routeIdentity.mjs +77 -0
  15. package/dist/esm/localisedUrls/routes.mjs +8 -0
  16. package/dist/esm/paths.mjs +12 -0
  17. package/dist/esm/router-navigation/adapter.mjs +197 -0
  18. package/dist/esm/router-navigation/index.mjs +1 -0
  19. package/dist/esm-node/index.mjs +1 -0
  20. package/dist/esm-node/localeRedirectPolicy.mjs +47 -0
  21. package/dist/esm-node/localisedUrls/index.mjs +1 -0
  22. package/dist/esm-node/localisedUrls/redirect.mjs +3 -6
  23. package/dist/esm-node/localisedUrls/routeIdentity.mjs +78 -0
  24. package/dist/esm-node/localisedUrls/routes.mjs +8 -0
  25. package/dist/esm-node/paths.mjs +13 -0
  26. package/dist/esm-node/router-navigation/adapter.mjs +198 -0
  27. package/dist/esm-node/router-navigation/index.mjs +2 -0
  28. package/dist/types/index.d.ts +4 -3
  29. package/dist/types/language-sync/react.d.ts +1 -1
  30. package/dist/types/localeRedirectPolicy.d.ts +22 -0
  31. package/dist/types/localisedUrls/config.d.ts +1 -1
  32. package/dist/types/localisedUrls/index.d.ts +10 -9
  33. package/dist/types/localisedUrls/pathname.d.ts +1 -1
  34. package/dist/types/localisedUrls/redirect.d.ts +8 -0
  35. package/dist/types/localisedUrls/resolve.d.ts +1 -1
  36. package/dist/types/localisedUrls/routeIdentity.d.ts +24 -0
  37. package/dist/types/localisedUrls/routes.d.ts +1 -1
  38. package/dist/types/paths.d.ts +9 -0
  39. package/dist/types/router-navigation/adapter.d.ts +71 -0
  40. package/dist/types/router-navigation/index.d.ts +1 -0
  41. package/package.json +30 -6
  42. package/src/index.ts +1 -0
  43. package/src/localeRedirectPolicy.ts +127 -0
  44. package/src/localisedUrls/index.ts +4 -0
  45. package/src/localisedUrls/redirect.ts +16 -2
  46. package/src/localisedUrls/routeIdentity.ts +107 -0
  47. package/src/localisedUrls/routes.ts +10 -0
  48. package/src/paths.ts +37 -0
  49. package/src/router-navigation/adapter.tsx +536 -0
  50. package/src/router-navigation/index.ts +6 -0
  51. package/tests/languageSyncController.test.ts +0 -77
  52. package/tests/localeRedirectPolicy.test.ts +173 -0
  53. package/tests/localisedUrls.test.ts +0 -141
  54. package/tests/paths.test.ts +24 -0
  55. package/tests/reactLanguageSync.test.tsx +20 -1
  56. package/tests/clientRedirectIntegration.test.tsx +0 -74
@@ -0,0 +1,536 @@
1
+ import { isBrowser, RuntimeContext } from '@modern-js/runtime';
2
+ import {
3
+ InternalRuntimeContext,
4
+ type TInternalRuntimeContext,
5
+ type TRuntimeContext,
6
+ } from '@modern-js/runtime/context';
7
+ import {
8
+ getRouterRuntimeState,
9
+ subscribeRouterRuntimeState,
10
+ } from '@modern-js/runtime-extensions/router-state';
11
+ import type React from 'react';
12
+ import { useCallback, useContext, useSyncExternalStore } from 'react';
13
+
14
+ /**
15
+ * Fork-owned: the selected-router navigation adapter.
16
+ *
17
+ * `useNativeI18nRouterAdapter` only knows react-router's own hooks, so a bare
18
+ * `appTools() + i18nPlugin()` app running the TanStack router would fall back
19
+ * to plain `<a>` links and a non-reactive `window.location`. That breaks
20
+ * language-invariant active state and, worse, leaves the i18n language stuck
21
+ * after a client-side navigation to a localized URL.
22
+ *
23
+ * The subsystem lives in this fork-owned package rather than inside
24
+ * upstream-owned `@modern-js/plugin-i18n`: that package only wires it in
25
+ * through its existing `NavigationProvider` extension point, so the upstream
26
+ * package keeps a seam instead of gaining a fork subsystem. The primitives the
27
+ * adapter composes with - the navigation context provider, the native
28
+ * react-router adapter and the adapter shape - are injected by that seam, so
29
+ * nothing here imports `@modern-js/plugin-i18n` and no dependency cycle is
30
+ * created.
31
+ *
32
+ * The adapter reads the actual selected router out of the fork-owned router
33
+ * runtime-state slot, so it serves both routers without either plugin having
34
+ * to know about i18n.
35
+ */
36
+
37
+ /** The primitives the host plugin injects; see `createI18nRouterNavigation`. */
38
+ export interface I18nRouterNavigationDependencies<
39
+ Adapter extends I18nRouterAdapterShape,
40
+ > {
41
+ I18nNavigationProvider: React.ComponentType<
42
+ React.PropsWithChildren<{ value: Adapter }>
43
+ >;
44
+ useNativeI18nRouterAdapter: () => Adapter;
45
+ }
46
+
47
+ export interface I18nRouterLinkTarget {
48
+ pathname: string;
49
+ href: string;
50
+ search?: Record<string, unknown>;
51
+ hash?: string;
52
+ hashScrollIntoView?: boolean | ScrollIntoViewOptions;
53
+ prefetch?: 'intent' | 'render' | 'viewport' | 'none';
54
+ preload?: unknown;
55
+ }
56
+
57
+ export interface I18nRouterAdapterShape {
58
+ framework?: string;
59
+ hasRouter: boolean;
60
+ location: { pathname: string; search: string; hash: string } | null;
61
+ navigate:
62
+ | ((
63
+ href: string,
64
+ options?: { replace?: boolean; state?: unknown },
65
+ ) => void | Promise<void>)
66
+ | null;
67
+ Link: React.ComponentType<{
68
+ to: string;
69
+ children?: React.ReactNode;
70
+ [key: string]: unknown;
71
+ }> | null;
72
+ params: Record<string, string>;
73
+ createLinkProps?: (target: I18nRouterLinkTarget) => {
74
+ to: string;
75
+ [key: string]: unknown;
76
+ };
77
+ }
78
+
79
+ type I18nRouterFramework = 'react-router' | 'tanstack' | string;
80
+
81
+ interface I18nRouterLocation {
82
+ pathname: string;
83
+ search: string;
84
+ hash: string;
85
+ }
86
+
87
+ interface I18nRouterNavigateOptions {
88
+ replace?: boolean;
89
+ state?: unknown;
90
+ }
91
+
92
+ type I18nRouterNavigate = (
93
+ href: string,
94
+ options?: I18nRouterNavigateOptions,
95
+ ) => void | Promise<void>;
96
+
97
+ type I18nRouterLink = React.ComponentType<{
98
+ to: string;
99
+ children?: React.ReactNode;
100
+ [key: string]: unknown;
101
+ }>;
102
+
103
+ type RuntimeContextWithRouter = TRuntimeContext & {
104
+ router?: {
105
+ useInRouterContext?: () => boolean;
106
+ useRouter?: (options?: { warn?: boolean }) => unknown;
107
+ useLocation?: () => unknown;
108
+ useNavigate?: () => I18nRouterNavigate;
109
+ useParams?: () => Record<string, string>;
110
+ useHref?: () => unknown;
111
+ Link?: I18nRouterLink;
112
+ };
113
+ };
114
+
115
+ type InternalRuntimeContextWithRouter = TInternalRuntimeContext & {
116
+ router?: RuntimeContextWithRouter['router'];
117
+ };
118
+
119
+ type RouterInstance = {
120
+ navigate?: (...args: any[]) => unknown;
121
+ state?: {
122
+ location?: unknown;
123
+ matches?: Array<{ params?: Record<string, string> }>;
124
+ };
125
+ stores?: {
126
+ location?: {
127
+ get?: () => unknown;
128
+ subscribe?: (listener: () => void) => () => void;
129
+ };
130
+ matches?: {
131
+ get?: () => Array<{ params?: Record<string, string> }>;
132
+ };
133
+ };
134
+ subscribe?:
135
+ | ((listener: () => void) => () => void)
136
+ | ((eventType: string, listener: () => void) => () => void);
137
+ };
138
+
139
+ const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
140
+ if (typeof value !== 'string' || !value) {
141
+ return '';
142
+ }
143
+ return value.startsWith(prefix) ? value : `${prefix}${value}`;
144
+ };
145
+
146
+ const normalizeLocation = (location: unknown): I18nRouterLocation | null => {
147
+ if (!location || typeof location !== 'object') {
148
+ return null;
149
+ }
150
+
151
+ const locationValue = location as {
152
+ pathname?: unknown;
153
+ search?: unknown;
154
+ searchStr?: unknown;
155
+ hash?: unknown;
156
+ };
157
+
158
+ if (typeof locationValue.pathname !== 'string') {
159
+ return null;
160
+ }
161
+
162
+ return {
163
+ pathname: locationValue.pathname,
164
+ search: normalizeUrlPart(
165
+ typeof locationValue.search === 'string'
166
+ ? locationValue.search
167
+ : locationValue.searchStr,
168
+ '?',
169
+ ),
170
+ hash: normalizeUrlPart(locationValue.hash, '#'),
171
+ };
172
+ };
173
+
174
+ const getWindowLocation = (): I18nRouterLocation | null => {
175
+ if (!isBrowser()) {
176
+ return null;
177
+ }
178
+
179
+ return {
180
+ pathname: window.location.pathname,
181
+ search: window.location.search,
182
+ hash: window.location.hash,
183
+ };
184
+ };
185
+
186
+ const getRouterFramework = (
187
+ runtimeContext: RuntimeContextWithRouter,
188
+ internalContext: InternalRuntimeContextWithRouter,
189
+ inReactRouter: boolean,
190
+ ): I18nRouterFramework | undefined => {
191
+ const framework =
192
+ getRouterRuntimeState(internalContext)?.framework ||
193
+ getRouterRuntimeState(runtimeContext)?.framework;
194
+
195
+ if (framework) {
196
+ return framework;
197
+ }
198
+
199
+ if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) {
200
+ return 'tanstack';
201
+ }
202
+
203
+ if (
204
+ internalContext.router?.useLocation ||
205
+ internalContext.router?.useHref ||
206
+ runtimeContext.router?.useLocation ||
207
+ runtimeContext.router?.useHref
208
+ ) {
209
+ return 'react-router';
210
+ }
211
+
212
+ if (inReactRouter) {
213
+ return 'react-router';
214
+ }
215
+
216
+ return undefined;
217
+ };
218
+
219
+ const getRouterInstance = (
220
+ internalContext: InternalRuntimeContextWithRouter,
221
+ contextRouter?: RouterInstance | null,
222
+ ): RouterInstance | null => {
223
+ if (contextRouter) {
224
+ return contextRouter;
225
+ }
226
+
227
+ const router = getRouterRuntimeState(internalContext)?.instance;
228
+ if (!router || typeof router !== 'object') {
229
+ return null;
230
+ }
231
+ return router as RouterInstance;
232
+ };
233
+
234
+ const getRouterStateLocation = (
235
+ internalContext: InternalRuntimeContextWithRouter,
236
+ contextRouter?: RouterInstance | null,
237
+ ): I18nRouterLocation | null => {
238
+ const router = getRouterInstance(internalContext, contextRouter);
239
+ return (
240
+ normalizeLocation(router?.stores?.location?.get?.()) ||
241
+ normalizeLocation(router?.state?.location)
242
+ );
243
+ };
244
+
245
+ const getRouterParams = (
246
+ internalContext: InternalRuntimeContextWithRouter,
247
+ contextRouter?: RouterInstance | null,
248
+ ): Record<string, string> => {
249
+ const router = getRouterInstance(internalContext, contextRouter);
250
+ const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
251
+ if (!Array.isArray(matches)) {
252
+ return {};
253
+ }
254
+
255
+ return matches.reduce<Record<string, string>>((params, match) => {
256
+ if (match?.params) {
257
+ Object.assign(params, match.params);
258
+ }
259
+ return params;
260
+ }, {});
261
+ };
262
+
263
+ const getRouterSnapshot = (
264
+ internalContext: InternalRuntimeContextWithRouter,
265
+ contextRouter?: RouterInstance | null,
266
+ ) => {
267
+ const location = getRouterStateLocation(internalContext, contextRouter);
268
+ const params = getRouterParams(internalContext, contextRouter);
269
+ return JSON.stringify([
270
+ location?.pathname ?? '',
271
+ location?.search ?? '',
272
+ location?.hash ?? '',
273
+ Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
274
+ ]);
275
+ };
276
+
277
+ /**
278
+ * Build the selected-router navigation hook and provider from the host
279
+ * plugin's own primitives. Injecting them is what keeps this fork-owned
280
+ * subsystem free of any import edge back into the plugin that installs it.
281
+ */
282
+ export const createI18nRouterNavigation = <
283
+ Adapter extends I18nRouterAdapterShape,
284
+ >({
285
+ I18nNavigationProvider,
286
+ useNativeI18nRouterAdapter,
287
+ }: I18nRouterNavigationDependencies<Adapter>) => {
288
+ const useIntegratedRouterAdapter = (): Adapter => {
289
+ const runtimeContext = useContext(
290
+ RuntimeContext,
291
+ ) as RuntimeContextWithRouter;
292
+ const internalContext = useContext(
293
+ InternalRuntimeContext,
294
+ ) as InternalRuntimeContextWithRouter;
295
+ const routerApi = internalContext.router || runtimeContext.router;
296
+ const native = useNativeI18nRouterAdapter();
297
+ const inReactRouter = native.hasRouter;
298
+ const reactRouterNavigate = native.navigate;
299
+ const reactRouterLocation = inReactRouter ? native.location : null;
300
+ const reactRouterParams = native.params;
301
+ const framework = getRouterFramework(
302
+ runtimeContext,
303
+ internalContext,
304
+ inReactRouter,
305
+ );
306
+ const contextUseRouter =
307
+ !inReactRouter && framework === 'tanstack'
308
+ ? internalContext.router?.useRouter || runtimeContext.router?.useRouter
309
+ : undefined;
310
+ const contextRouter = contextUseRouter
311
+ ? (contextUseRouter({ warn: false }) as RouterInstance | null)
312
+ : null;
313
+ // `hasRouter` means "this adapter can actually drive a router", not "a
314
+ // router framework was named". Two things can drive one: a live react-router
315
+ // context we are rendering inside, or an instance published in the
316
+ // runtime-state slot. A framework name alone is neither.
317
+ //
318
+ // This matters because router providers publish their name before their
319
+ // instance. `plugin-tanstack` publishes its hooks in `onBeforeRender` and
320
+ // only installs the instance when `RouterWrapper` renders - below anything
321
+ // wrapping the app, this provider included - so on a CSR boot the first
322
+ // evaluation here sees a name and no instance. Claiming a router there hands
323
+ // `changeLanguage()` a `navigate` that can only throw, instead of letting it
324
+ // fall back to a full-page load.
325
+ const slotRouter = getRouterInstance(internalContext, contextRouter);
326
+ // A slot instance counts only when `navigate` below can drive it: a named
327
+ // framework this adapter implements, with a navigate function. An unknown
328
+ // provider's instance would otherwise hand `changeLanguage()` a navigate
329
+ // that goes nowhere instead of the routerless full-page fallback.
330
+ const drivableSlotRouter =
331
+ (framework === 'tanstack' || framework === 'react-router') &&
332
+ typeof slotRouter?.navigate === 'function';
333
+ const hasRouter = Boolean(reactRouterNavigate) || drivableSlotRouter;
334
+
335
+ const subscribeToRouter = useCallback(
336
+ (update: () => void) => {
337
+ const unsubscribers: Array<() => void> = [];
338
+
339
+ // The instance can arrive after this provider has already rendered, so
340
+ // watch the slot itself: without this the store subscription below would
341
+ // be a permanent no-op on a CSR boot and the adapter would never notice
342
+ // the router it is meant to drive.
343
+ const unsubscribeFromSlot = subscribeRouterRuntimeState(
344
+ internalContext,
345
+ update,
346
+ );
347
+ if (typeof unsubscribeFromSlot === 'function') {
348
+ unsubscribers.push(unsubscribeFromSlot);
349
+ }
350
+
351
+ const router = getRouterInstance(internalContext, contextRouter);
352
+ if (!router) {
353
+ return () => {
354
+ for (const unsubscribe of unsubscribers) {
355
+ unsubscribe();
356
+ }
357
+ };
358
+ }
359
+
360
+ if (
361
+ framework === 'react-router' &&
362
+ !inReactRouter &&
363
+ typeof router.subscribe === 'function'
364
+ ) {
365
+ const subscribe = router.subscribe as (
366
+ this: RouterInstance,
367
+ listener: () => void,
368
+ ) => () => void;
369
+ const unsubscribe = subscribe.call(router, update);
370
+ if (typeof unsubscribe === 'function') {
371
+ unsubscribers.push(unsubscribe);
372
+ }
373
+ }
374
+
375
+ if (
376
+ framework === 'tanstack' &&
377
+ typeof router.stores?.location?.subscribe === 'function'
378
+ ) {
379
+ const unsubscribe = router.stores.location.subscribe(update);
380
+ if (typeof unsubscribe === 'function') {
381
+ unsubscribers.push(unsubscribe);
382
+ }
383
+ }
384
+
385
+ if (
386
+ framework === 'tanstack' &&
387
+ typeof router.subscribe === 'function'
388
+ ) {
389
+ const subscribe = router.subscribe as (
390
+ this: RouterInstance,
391
+ eventType: string,
392
+ listener: () => void,
393
+ ) => () => void;
394
+ for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
395
+ const unsubscribe = subscribe.call(router, eventType, update);
396
+ if (typeof unsubscribe === 'function') {
397
+ unsubscribers.push(unsubscribe);
398
+ }
399
+ }
400
+ }
401
+
402
+ return () => {
403
+ for (const unsubscribe of unsubscribers) {
404
+ unsubscribe();
405
+ }
406
+ };
407
+ },
408
+ [contextRouter, framework, inReactRouter, internalContext],
409
+ );
410
+ const getSnapshot = useCallback(
411
+ () => getRouterSnapshot(internalContext, contextRouter),
412
+ [contextRouter, internalContext],
413
+ );
414
+ useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
415
+
416
+ const navigate = useCallback<I18nRouterNavigate>(
417
+ (href, options) => {
418
+ const router = getRouterInstance(internalContext, contextRouter);
419
+ const activeFramework = getRouterFramework(
420
+ runtimeContext,
421
+ internalContext,
422
+ inReactRouter,
423
+ );
424
+
425
+ if (activeFramework === 'tanstack') {
426
+ if (typeof router?.navigate === 'function') {
427
+ return router.navigate({
428
+ to: href,
429
+ replace: options?.replace,
430
+ ...(options?.state === undefined ? {} : { state: options.state }),
431
+ }) as void | Promise<void>;
432
+ }
433
+ throw new Error('TanStack router instance is not available.');
434
+ }
435
+
436
+ if (reactRouterNavigate) {
437
+ return reactRouterNavigate(href, options);
438
+ }
439
+
440
+ if (activeFramework === 'react-router') {
441
+ if (typeof router?.navigate === 'function') {
442
+ return router.navigate(href, options) as void | Promise<void>;
443
+ }
444
+ throw new Error('React Router instance is not available.');
445
+ }
446
+ },
447
+ [
448
+ contextRouter,
449
+ internalContext,
450
+ inReactRouter,
451
+ reactRouterNavigate,
452
+ runtimeContext,
453
+ ],
454
+ );
455
+
456
+ const location =
457
+ (reactRouterLocation
458
+ ? normalizeLocation(reactRouterLocation)
459
+ : getRouterStateLocation(internalContext, contextRouter)) ||
460
+ getWindowLocation();
461
+ const params = inReactRouter
462
+ ? (reactRouterParams as Record<string, string>)
463
+ : getRouterParams(internalContext, contextRouter);
464
+ const Link =
465
+ framework === 'tanstack'
466
+ ? internalContext.router?.Link || runtimeContext.router?.Link || null
467
+ : framework === 'react-router' || inReactRouter
468
+ ? (routerApi?.Link ?? null)
469
+ : null;
470
+
471
+ return {
472
+ framework,
473
+ hasRouter,
474
+ location,
475
+ navigate: hasRouter ? navigate : null,
476
+ Link,
477
+ params,
478
+ ...(framework === 'tanstack'
479
+ ? { createLinkProps: createTanstackLinkProps }
480
+ : {}),
481
+ } as Adapter;
482
+ };
483
+
484
+ const createTanstackLinkProps = (target: I18nRouterLinkTarget) => {
485
+ const preload =
486
+ target.preload !== undefined
487
+ ? target.preload
488
+ : target.prefetch === 'none'
489
+ ? false
490
+ : target.prefetch;
491
+ return {
492
+ to: target.pathname,
493
+ ...(target.search ? { search: target.search } : {}),
494
+ ...(target.hash ? { hash: target.hash } : {}),
495
+ ...(target.hashScrollIntoView === undefined
496
+ ? {}
497
+ : { hashScrollIntoView: target.hashScrollIntoView }),
498
+ ...(preload === undefined ? {} : { preload }),
499
+ };
500
+ };
501
+
502
+ /**
503
+ * Supplies the actual selected router; native links still render its Link.
504
+ *
505
+ * This provider sits above the app, so it sits above the router element too.
506
+ * That is fine for a router this adapter reaches through the fork-owned
507
+ * runtime-state slot - TanStack hands out its instance, not a React context -
508
+ * but react-router publishes its location and navigate through context that
509
+ * only exists *below* the router. Reading it from up here yields an inert
510
+ * adapter, and publishing that would shadow the native one a consumer
511
+ * resolves at its own position, where the context is live. So only take over
512
+ * for the router reached through the slot, and only once its instance is
513
+ * actually installed; until then `useI18nRouterAdapter` falls back to the
514
+ * native adapter and `changeLanguage()` keeps its full-page behaviour.
515
+ */
516
+ const I18nRouterNavigationProvider = ({
517
+ children,
518
+ }: React.PropsWithChildren) => {
519
+ const value = useIntegratedRouterAdapter();
520
+ const native = useNativeI18nRouterAdapter();
521
+ // Publish only what a consumer could not resolve better itself. Inside a
522
+ // live react-router context the native adapter - evaluated at the
523
+ // consumer's own position, where that context exists - is already the
524
+ // right answer, and shadowing it with one read from above the router
525
+ // yields an inert adapter. With no router to drive at all, staying absent
526
+ // is what preserves the full-page `changeLanguage()` fallback.
527
+ if (!value.hasRouter || native.hasRouter) {
528
+ return <>{children}</>;
529
+ }
530
+ return (
531
+ <I18nNavigationProvider value={value}>{children}</I18nNavigationProvider>
532
+ );
533
+ };
534
+
535
+ return { useIntegratedRouterAdapter, I18nRouterNavigationProvider };
536
+ };
@@ -0,0 +1,6 @@
1
+ export {
2
+ createI18nRouterNavigation,
3
+ type I18nRouterAdapterShape,
4
+ type I18nRouterLinkTarget,
5
+ type I18nRouterNavigationDependencies,
6
+ } from './adapter';
@@ -80,7 +80,6 @@ describe('latest language synchronization coordinator', () => {
80
80
 
81
81
  binding.request('cs');
82
82
  binding.request('de');
83
- expect(pending.map(change => change.language)).toEqual(['cs', 'de']);
84
83
 
85
84
  pending[1].resolve();
86
85
  await pending[1].promise;
@@ -91,7 +90,6 @@ describe('latest language synchronization coordinator', () => {
91
90
  await pending[0].promise;
92
91
  await flush();
93
92
  expect(target.language).toBe('cs');
94
- expect(pending.map(change => change.language)).toEqual(['cs', 'de', 'de']);
95
93
 
96
94
  pending[2].resolve();
97
95
  await pending[2].promise;
@@ -100,81 +98,6 @@ describe('latest language synchronization coordinator', () => {
100
98
  expect(commits.at(-1)).toBe('de');
101
99
  });
102
100
 
103
- test('repairs a shared singleton after the starting mount leaves', async () => {
104
- const target = { language: 'en' };
105
- const { changeLanguage, pending } = deferredChanger(target);
106
- const commits: string[] = [];
107
- const first = createLatestLanguageSyncBinding<Target>();
108
- const second = createLatestLanguageSyncBinding<Target>();
109
- for (const binding of [first, second]) {
110
- binding.updateCallbacks({
111
- changeLanguage,
112
- commitLanguage: (_target, language) => commits.push(language),
113
- readLanguage: current => current.language,
114
- });
115
- }
116
-
117
- first.activate(target);
118
- first.request('cs');
119
- first.deactivate();
120
- second.activate(target);
121
- second.request('de');
122
-
123
- pending[1].resolve();
124
- await pending[1].promise;
125
- await flush();
126
- pending[0].resolve();
127
- await pending[0].promise;
128
- await flush();
129
-
130
- expect(pending.map(change => change.language)).toEqual(['cs', 'de', 'de']);
131
- pending[2].resolve();
132
- await pending[2].promise;
133
- await flush();
134
- expect(target.language).toBe('de');
135
- expect(commits.at(-1)).toBe('de');
136
- });
137
-
138
- test('supersedes a hung same-language attempt and repairs it after a later route', async () => {
139
- rstest.useFakeTimers();
140
- const target = { language: 'en' };
141
- const { changeLanguage, pending } = deferredChanger(target);
142
- const commits: string[] = [];
143
- const binding = createLatestLanguageSyncBinding<Target>({
144
- attemptTimeoutMs: 100,
145
- retryDelayMs: () => 10,
146
- });
147
- binding.updateCallbacks({
148
- changeLanguage,
149
- commitLanguage: (_target, language) => commits.push(language),
150
- readLanguage: current => current.language,
151
- });
152
- binding.activate(target);
153
- binding.request('cs');
154
-
155
- await rstest.advanceTimersByTimeAsync(110);
156
- expect(pending.map(change => change.language)).toEqual(['cs', 'cs']);
157
- pending[1].resolve();
158
- await pending[1].promise;
159
- await flush();
160
- expect(commits.at(-1)).toBe('cs');
161
-
162
- binding.request('de');
163
- pending[2].resolve();
164
- await pending[2].promise;
165
- await flush();
166
- expect(target.language).toBe('de');
167
-
168
- pending[0].resolve();
169
- await pending[0].promise;
170
- await flush();
171
- expect(pending.at(-1)?.language).toBe('de');
172
- pending.at(-1)?.resolve();
173
- await pending.at(-1)?.promise;
174
- await flush();
175
- expect(target.language).toBe('de');
176
- });
177
-
178
101
  test('bounds permanent failures and reports the final consequence once', async () => {
179
102
  rstest.useFakeTimers();
180
103
  const target = { language: 'en' };