@xndrjs/i18n-react 0.8.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,357 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createI18nHandle } from "@xndrjs/i18n";
3
+ import { IcuTranslationProviderMulti } from "@xndrjs/i18n";
4
+ import { createLoadCoordinator } from "./create-load-coordinator.js";
5
+
6
+ type MultiSchema = {
7
+ default: {
8
+ greeting: { en: string; it: string };
9
+ };
10
+ billing: {
11
+ invoice: { en: string; it: string };
12
+ };
13
+ };
14
+
15
+ type TestMultiParams = {
16
+ default: {
17
+ greeting: never;
18
+ };
19
+ billing: {
20
+ invoice: never;
21
+ };
22
+ };
23
+
24
+ const defaultEn = {
25
+ greeting: { en: "Hello" },
26
+ };
27
+
28
+ const defaultIt = {
29
+ greeting: { it: "Ciao" },
30
+ };
31
+
32
+ function deferred<T>() {
33
+ let resolve!: (value: T) => void;
34
+ let reject!: (reason?: unknown) => void;
35
+ const promise = new Promise<T>((res, rej) => {
36
+ resolve = res;
37
+ reject = rej;
38
+ });
39
+ return { promise, resolve, reject };
40
+ }
41
+
42
+ describe("createLoadCoordinator", () => {
43
+ it("dedupes in-flight loads for the same key", async () => {
44
+ const coordinator = createLoadCoordinator<string>();
45
+ const load = vi.fn(() => Promise.resolve("loaded"));
46
+ const engine = {};
47
+ const key = { engineRef: engine, partition: "en", namespaces: ["default"] as const };
48
+
49
+ coordinator.request({ ...key, load });
50
+ coordinator.request({ ...key, load });
51
+
52
+ expect(load).toHaveBeenCalledTimes(1);
53
+ expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
54
+ await expect(coordinator.getPromise()).resolves.toBe("loaded");
55
+ expect(coordinator.revision).toBe(1);
56
+ expect(coordinator.getResolvedScope()).toBe("loaded");
57
+ expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "loaded" });
58
+ });
59
+
60
+ it("skips reload when the same key is already resolved", async () => {
61
+ const coordinator = createLoadCoordinator<number>();
62
+ const load = vi.fn(() => Promise.resolve(42));
63
+ const engine = {};
64
+ const key = {
65
+ engineRef: engine,
66
+ partition: undefined,
67
+ namespaces: ["default"] as const,
68
+ };
69
+
70
+ coordinator.request({ ...key, load });
71
+ await coordinator.getPromise();
72
+
73
+ coordinator.request({ ...key, load });
74
+
75
+ expect(load).toHaveBeenCalledTimes(1);
76
+ expect(coordinator.revision).toBe(1);
77
+ await expect(coordinator.getPromise()).resolves.toBe(42);
78
+ expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: 42 });
79
+ });
80
+
81
+ it("resolves synchronously when tryResolveSync returns a scope", () => {
82
+ const coordinator = createLoadCoordinator<string>();
83
+ const load = vi.fn(() => Promise.resolve("async"));
84
+ const tryResolveSync = vi.fn(() => "sync");
85
+ const engine = {};
86
+ const key = {
87
+ engineRef: engine,
88
+ partition: "en",
89
+ namespaces: ["default"] as const,
90
+ };
91
+
92
+ coordinator.request({ ...key, load, tryResolveSync });
93
+
94
+ expect(load).not.toHaveBeenCalled();
95
+ expect(tryResolveSync).toHaveBeenCalledTimes(1);
96
+ expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "sync" });
97
+ expect(coordinator.getResolvedScope()).toBe("sync");
98
+ });
99
+
100
+ it("falls back to async load when tryResolveSync returns null", async () => {
101
+ const coordinator = createLoadCoordinator<string>();
102
+ const load = vi.fn(() => Promise.resolve("async"));
103
+ const tryResolveSync = vi.fn(() => null);
104
+ const engine = {};
105
+ const key = {
106
+ engineRef: engine,
107
+ partition: "en",
108
+ namespaces: ["default"] as const,
109
+ };
110
+
111
+ coordinator.request({ ...key, load, tryResolveSync });
112
+
113
+ expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
114
+ await expect(coordinator.getPromise()).resolves.toBe("async");
115
+ expect(load).toHaveBeenCalledTimes(1);
116
+ expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "async" });
117
+ });
118
+
119
+ it("keeps concurrent namespace loads independent", async () => {
120
+ const coordinator = createLoadCoordinator<string>();
121
+ const billing = deferred<string>();
122
+ const user = deferred<string>();
123
+ const engine = {};
124
+ const billingKey = {
125
+ engineRef: engine,
126
+ partition: "en",
127
+ namespaces: ["billing"] as const,
128
+ };
129
+ const userKey = {
130
+ engineRef: engine,
131
+ partition: "en",
132
+ namespaces: ["user"] as const,
133
+ };
134
+
135
+ coordinator.request({ ...billingKey, load: () => billing.promise });
136
+ const billingPromise = coordinator.getPromise();
137
+
138
+ coordinator.request({ ...userKey, load: () => user.promise });
139
+ const userPromise = coordinator.getPromise();
140
+
141
+ expect(coordinator.getEntry(billingKey)).toEqual({ status: "pending" });
142
+ expect(coordinator.getEntry(userKey)).toEqual({ status: "pending" });
143
+
144
+ billing.resolve("billing-scope");
145
+ user.resolve("user-scope");
146
+
147
+ await expect(billingPromise).resolves.toBe("billing-scope");
148
+ await expect(userPromise).resolves.toBe("user-scope");
149
+ expect(coordinator.revision).toBe(2);
150
+ expect(coordinator.getEntry(billingKey)).toEqual({
151
+ status: "resolved",
152
+ scope: "billing-scope",
153
+ });
154
+ expect(coordinator.getEntry(userKey)).toEqual({
155
+ status: "resolved",
156
+ scope: "user-scope",
157
+ });
158
+ });
159
+
160
+ it("caches distinct partitions without cancelling the earlier one", async () => {
161
+ const coordinator = createLoadCoordinator<string>();
162
+ const first = deferred<string>();
163
+ const second = deferred<string>();
164
+ const engine = {};
165
+
166
+ coordinator.request({
167
+ engineRef: engine,
168
+ partition: "en",
169
+ namespaces: ["default"],
170
+ load: () => first.promise,
171
+ });
172
+ const enPromise = coordinator.getPromise();
173
+
174
+ coordinator.request({
175
+ engineRef: engine,
176
+ partition: "it",
177
+ namespaces: ["default"],
178
+ load: () => second.promise,
179
+ });
180
+ const itPromise = coordinator.getPromise();
181
+
182
+ first.resolve("en-scope");
183
+ second.resolve("it-scope");
184
+
185
+ await expect(enPromise).resolves.toBe("en-scope");
186
+ await expect(itPromise).resolves.toBe("it-scope");
187
+ expect(coordinator.revision).toBe(2);
188
+ });
189
+
190
+ it("notifies subscribers when a load resolves", async () => {
191
+ const coordinator = createLoadCoordinator<string>();
192
+ const pending = deferred<string>();
193
+ const engine = {};
194
+ const key = {
195
+ engineRef: engine,
196
+ partition: "en",
197
+ namespaces: ["default"] as const,
198
+ };
199
+ const listener = vi.fn();
200
+
201
+ const unsubscribe = coordinator.subscribe(listener);
202
+ coordinator.request({ ...key, load: () => pending.promise });
203
+
204
+ expect(listener).not.toHaveBeenCalled();
205
+ expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
206
+
207
+ pending.resolve("ready");
208
+ await expect(coordinator.getPromise()).resolves.toBe("ready");
209
+
210
+ expect(listener).toHaveBeenCalledTimes(1);
211
+ expect(coordinator.revision).toBe(1);
212
+ expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "ready" });
213
+
214
+ unsubscribe();
215
+ const other = deferred<string>();
216
+ coordinator.request({
217
+ engineRef: engine,
218
+ partition: "it",
219
+ namespaces: ["default"],
220
+ load: () => other.promise,
221
+ });
222
+ other.resolve("other");
223
+ await vi.waitFor(() => expect(coordinator.revision).toBe(2));
224
+ expect(listener).toHaveBeenCalledTimes(1);
225
+ });
226
+
227
+ it("keeps an error entry and notifies subscribers on reject", async () => {
228
+ const coordinator = createLoadCoordinator<string>();
229
+ const pending = deferred<string>();
230
+ const engine = {};
231
+ const key = {
232
+ engineRef: engine,
233
+ partition: "en",
234
+ namespaces: ["default"] as const,
235
+ };
236
+ const load = vi.fn(() => pending.promise);
237
+ const listener = vi.fn();
238
+ const failure = new Error("load failed");
239
+
240
+ coordinator.subscribe(listener);
241
+ coordinator.request({ ...key, load });
242
+
243
+ expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
244
+
245
+ pending.reject(failure);
246
+ await expect(coordinator.getPromise()).rejects.toThrow("load failed");
247
+
248
+ expect(listener).toHaveBeenCalledTimes(1);
249
+ expect(coordinator.revision).toBe(1);
250
+ expect(coordinator.getEntry(key)).toEqual({ status: "error", error: failure });
251
+ expect(coordinator.getResolvedScope()).toBeNull();
252
+
253
+ // Deduped: settled error entry is not retried until retry().
254
+ coordinator.request({ ...key, load });
255
+ expect(load).toHaveBeenCalledTimes(1);
256
+ expect(coordinator.getEntry(key)).toEqual({ status: "error", error: failure });
257
+ });
258
+
259
+ it("retry() evicts an error entry so the next ensure reloads", async () => {
260
+ const coordinator = createLoadCoordinator<string>();
261
+ const engine = {};
262
+ const key = {
263
+ engineRef: engine,
264
+ partition: "en",
265
+ namespaces: ["default"] as const,
266
+ };
267
+ const load = vi
268
+ .fn()
269
+ .mockImplementationOnce(() => Promise.reject(new Error("fail")))
270
+ .mockImplementationOnce(() => Promise.resolve("ok"));
271
+
272
+ coordinator.request({ ...key, load });
273
+ await expect(coordinator.getPromise()).rejects.toThrow("fail");
274
+ expect(coordinator.getEntry(key).status).toBe("error");
275
+
276
+ coordinator.retry(key);
277
+ expect(coordinator.getEntry(key)).toEqual({ status: "pending" });
278
+
279
+ coordinator.request({ ...key, load });
280
+ await expect(coordinator.getPromise()).resolves.toBe("ok");
281
+ expect(load).toHaveBeenCalledTimes(2);
282
+ expect(coordinator.getEntry(key)).toEqual({ status: "resolved", scope: "ok" });
283
+ });
284
+
285
+ it("distinguishes different engineRef objects via WeakMap ids", async () => {
286
+ const coordinator = createLoadCoordinator<string>();
287
+ const engineA = {};
288
+ const engineB = {};
289
+ const loadA = vi.fn(() => Promise.resolve("a"));
290
+ const loadB = vi.fn(() => Promise.resolve("b"));
291
+
292
+ coordinator.request({
293
+ engineRef: engineA,
294
+ partition: "en",
295
+ namespaces: ["default"],
296
+ load: loadA,
297
+ });
298
+ const promiseA = coordinator.getPromise();
299
+ coordinator.request({
300
+ engineRef: engineB,
301
+ partition: "en",
302
+ namespaces: ["default"],
303
+ load: loadB,
304
+ });
305
+ const promiseB = coordinator.getPromise();
306
+
307
+ expect(loadA).toHaveBeenCalledTimes(1);
308
+ expect(loadB).toHaveBeenCalledTimes(1);
309
+ await expect(promiseA).resolves.toBe("a");
310
+ await expect(promiseB).resolves.toBe("b");
311
+ expect(
312
+ coordinator.getEntry({ engineRef: engineA, partition: "en", namespaces: ["default"] })
313
+ ).toEqual({ status: "resolved", scope: "a" });
314
+ expect(
315
+ coordinator.getEntry({ engineRef: engineB, partition: "en", namespaces: ["default"] })
316
+ ).toEqual({ status: "resolved", scope: "b" });
317
+ });
318
+
319
+ it("returns pending for unknown keys without using React use()", () => {
320
+ const coordinator = createLoadCoordinator<string>();
321
+ expect(
322
+ coordinator.getEntry({
323
+ engineRef: {},
324
+ partition: "en",
325
+ namespaces: ["missing"],
326
+ })
327
+ ).toEqual({ status: "pending" });
328
+ expect(coordinator.revision).toBe(0);
329
+ });
330
+
331
+ it("integrates with handle split-by-locale loads", async () => {
332
+ const defaultLoader = vi.fn(async (locale: string) =>
333
+ locale === "it" ? defaultIt : defaultEn
334
+ );
335
+ const engine = new IcuTranslationProviderMulti<MultiSchema, TestMultiParams>({});
336
+ const handle = createI18nHandle(engine, {
337
+ namespaceLoaders: {
338
+ default: defaultLoader,
339
+ },
340
+ });
341
+ const load = () => handle.load({ namespaces: ["default"], locale: "it" });
342
+ const coordinator = createLoadCoordinator<Awaited<ReturnType<typeof load>>>();
343
+ const key = {
344
+ engineRef: engine,
345
+ partition: "it",
346
+ namespaces: ["default"] as const,
347
+ };
348
+
349
+ coordinator.request({ ...key, load });
350
+
351
+ const scope = await coordinator.getPromise();
352
+ expect(defaultLoader).toHaveBeenCalledWith("it", { locale: "it" });
353
+ expect(scope.t("default", "greeting")).toBe("Ciao");
354
+ expect(coordinator.revision).toBe(1);
355
+ expect(coordinator.getEntry(key).status).toBe("resolved");
356
+ });
357
+ });
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Load deduplication for lazy i18n namespace gates / HOCs.
3
+ *
4
+ * Supports multiple concurrent keys so parallel per-namespace gates can share
5
+ * one coordinator. Each `(engine, partition, namespaces)` entry keeps its own
6
+ * promise and status. Display state (keep-previous / pendingLocale / stable `t`)
7
+ * lives here so gate components stay pure observers of `useSyncExternalStore`.
8
+ */
9
+ import { createScopedT } from "./create-scoped-t.js";
10
+ import type {
11
+ GetDisplayEntryOptions,
12
+ LoadCoordinator,
13
+ LoadCoordinatorEntry,
14
+ LoadCoordinatorKey,
15
+ LoadCoordinatorRequest,
16
+ LoadDisplayEntry,
17
+ ScopedScopeLike,
18
+ ScopedTranslateFn,
19
+ } from "./types.js";
20
+
21
+ const engineIds = new WeakMap<object, number>();
22
+ let nextEngineId = 1;
23
+
24
+ function engineId(ref: unknown): number {
25
+ if (ref === null || (typeof ref !== "object" && typeof ref !== "function")) {
26
+ return 0;
27
+ }
28
+ const obj = ref as object;
29
+ let id = engineIds.get(obj);
30
+ if (id === undefined) {
31
+ id = nextEngineId++;
32
+ engineIds.set(obj, id);
33
+ }
34
+ return id;
35
+ }
36
+
37
+ function cacheKey(
38
+ engineRef: unknown,
39
+ partition: string | undefined,
40
+ namespaces: readonly string[]
41
+ ): string {
42
+ return `${engineId(engineRef)}\0${partition ?? ""}\0${namespaces.join("\0")}`;
43
+ }
44
+
45
+ /** Gate identity for keep-previous (namespaces only — partition/locale can change). */
46
+ function gateId(engineRef: unknown, namespaces: readonly string[]): string {
47
+ return `${engineId(engineRef)}\0${namespaces.join("\0")}`;
48
+ }
49
+
50
+ interface CacheEntry<Scope> {
51
+ status: "pending" | "resolved" | "error";
52
+ promise: Promise<Scope>;
53
+ resolvedScope: Scope | null;
54
+ error: unknown;
55
+ requestId: number;
56
+ /** Stable LoadCoordinatorEntry snapshot for getEntry(). */
57
+ entrySnapshot: LoadCoordinatorEntry<Scope>;
58
+ }
59
+
60
+ interface DisplayCache {
61
+ lastReady: { scope: ScopedScopeLike; locale: string } | null;
62
+ /** Stable display entry object until inputs change. */
63
+ displaySnapshot: LoadDisplayEntry<ScopedScopeLike> | null;
64
+ /** Identity key for the last displaySnapshot. */
65
+ displayKey: string | null;
66
+ boundT: ScopedTranslateFn | null;
67
+ boundTKey: string | null;
68
+ }
69
+
70
+ /**
71
+ * Dedupes in-flight loads by `(engineRef, partition, namespaces)` and exposes
72
+ * display snapshots + retry for manual pending/resolved/error rendering.
73
+ */
74
+ export function createLoadCoordinator<Scope = ScopedScopeLike>(): LoadCoordinator<Scope> {
75
+ let revision = 0;
76
+ let nextRequestId = 0;
77
+ const entries = new Map<string, CacheEntry<Scope>>();
78
+ const displayByGate = new Map<string, DisplayCache>();
79
+ const listeners = new Set<() => void>();
80
+ let lastKey: string | null = null;
81
+
82
+ function notify(): void {
83
+ for (const listener of listeners) {
84
+ listener();
85
+ }
86
+ }
87
+
88
+ function bumpAndNotify(): void {
89
+ revision += 1;
90
+ // Invalidate display snapshots that depend on entry status.
91
+ for (const display of displayByGate.values()) {
92
+ display.displaySnapshot = null;
93
+ display.displayKey = null;
94
+ }
95
+ notify();
96
+ }
97
+
98
+ function ensure(input: LoadCoordinatorRequest<Scope>): void {
99
+ const key = cacheKey(input.engineRef, input.partition, input.namespaces);
100
+ lastKey = key;
101
+
102
+ const existing = entries.get(key);
103
+ if (existing !== undefined) {
104
+ return;
105
+ }
106
+
107
+ if (input.tryResolveSync !== undefined) {
108
+ const syncScope = input.tryResolveSync();
109
+ if (syncScope !== null) {
110
+ const entrySnapshot: LoadCoordinatorEntry<Scope> = {
111
+ status: "resolved",
112
+ scope: syncScope,
113
+ };
114
+ entries.set(key, {
115
+ status: "resolved",
116
+ promise: Promise.resolve(syncScope),
117
+ resolvedScope: syncScope,
118
+ error: null,
119
+ requestId: ++nextRequestId,
120
+ entrySnapshot,
121
+ });
122
+ return;
123
+ }
124
+ }
125
+
126
+ const requestId = ++nextRequestId;
127
+ const promise = input.load().then(
128
+ (scope) => {
129
+ const entry = entries.get(key);
130
+ if (entry === undefined || entry.requestId !== requestId) {
131
+ return scope;
132
+ }
133
+ entry.status = "resolved";
134
+ entry.resolvedScope = scope;
135
+ entry.error = null;
136
+ entry.entrySnapshot = { status: "resolved", scope };
137
+ bumpAndNotify();
138
+ return scope;
139
+ },
140
+ (error: unknown) => {
141
+ const entry = entries.get(key);
142
+ if (entry !== undefined && entry.requestId === requestId) {
143
+ entry.status = "error";
144
+ entry.error = error;
145
+ entry.resolvedScope = null;
146
+ entry.entrySnapshot = { status: "error", error };
147
+ bumpAndNotify();
148
+ }
149
+ throw error;
150
+ }
151
+ );
152
+ // Mark rejection handled when only getEntry consumers attach (no getPromise).
153
+ void promise.catch(() => void null);
154
+
155
+ entries.set(key, {
156
+ status: "pending",
157
+ promise,
158
+ resolvedScope: null,
159
+ error: null,
160
+ requestId,
161
+ entrySnapshot: { status: "pending" },
162
+ });
163
+ }
164
+
165
+ function getEntry(keyInput: LoadCoordinatorKey): LoadCoordinatorEntry<Scope> {
166
+ const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);
167
+ const entry = entries.get(key);
168
+ if (entry === undefined) {
169
+ return { status: "pending" };
170
+ }
171
+ return entry.entrySnapshot;
172
+ }
173
+
174
+ function getOrCreateDisplay(engineRef: unknown, namespaces: readonly string[]): DisplayCache {
175
+ const id = gateId(engineRef, namespaces);
176
+ let display = displayByGate.get(id);
177
+ if (display === undefined) {
178
+ display = {
179
+ lastReady: null,
180
+ displaySnapshot: null,
181
+ displayKey: null,
182
+ boundT: null,
183
+ boundTKey: null,
184
+ };
185
+ displayByGate.set(id, display);
186
+ }
187
+ return display;
188
+ }
189
+
190
+ function bindT(
191
+ display: DisplayCache,
192
+ scope: ScopedScopeLike,
193
+ locale: string,
194
+ namespaces: readonly string[],
195
+ scopeToken: string
196
+ ): ScopedTranslateFn {
197
+ const tKey = `${scopeToken}\0${locale}\0${namespaces.join("\0")}`;
198
+ if (display.boundT !== null && display.boundTKey === tKey) {
199
+ return display.boundT;
200
+ }
201
+ const t = createScopedT(scope, { namespaces, locale });
202
+ display.boundT = t;
203
+ display.boundTKey = tKey;
204
+ return t;
205
+ }
206
+
207
+ function getDisplayEntry(
208
+ keyInput: LoadCoordinatorKey,
209
+ options: GetDisplayEntryOptions
210
+ ): LoadDisplayEntry<Scope> {
211
+ const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);
212
+ const entry = entries.get(key);
213
+ const display = getOrCreateDisplay(keyInput.engineRef, options.namespaces);
214
+ const keepPrevious = options.keepPrevious !== false;
215
+
216
+ const retry = () => {
217
+ retryKey(keyInput);
218
+ };
219
+
220
+ let result: LoadDisplayEntry<Scope>;
221
+
222
+ if (entry === undefined || entry.status === "pending") {
223
+ const kept = keepPrevious ? display.lastReady : null;
224
+ result = {
225
+ status: "pending",
226
+ display: kept as { scope: Scope & ScopedScopeLike; locale: string } | null,
227
+ pendingLocale: kept ? options.locale : undefined,
228
+ t: kept
229
+ ? bindT(display, kept.scope, kept.locale, options.namespaces, `kept:${kept.locale}`)
230
+ : null,
231
+ } as LoadDisplayEntry<Scope>;
232
+ } else if (entry.status === "resolved") {
233
+ const scope = entry.resolvedScope as Scope & ScopedScopeLike;
234
+ display.lastReady = { scope, locale: options.locale };
235
+ result = {
236
+ status: "ready",
237
+ scope,
238
+ locale: options.locale,
239
+ t: bindT(display, scope, options.locale, options.namespaces, `ready:${key}`),
240
+ } as LoadDisplayEntry<Scope>;
241
+ } else {
242
+ const kept = keepPrevious ? display.lastReady : null;
243
+ result = {
244
+ status: "error",
245
+ error: entry.error,
246
+ display: kept as { scope: Scope & ScopedScopeLike; locale: string } | null,
247
+ pendingLocale: kept ? options.locale : undefined,
248
+ t: kept
249
+ ? bindT(display, kept.scope, kept.locale, options.namespaces, `kept:${kept.locale}`)
250
+ : null,
251
+ retry,
252
+ } as LoadDisplayEntry<Scope>;
253
+ }
254
+
255
+ const displayKey = `${key}\0${result.status}\0${options.locale}\0${String(!!result.t)}`;
256
+ if (display.displaySnapshot !== null && display.displayKey === displayKey) {
257
+ return display.displaySnapshot as LoadDisplayEntry<Scope>;
258
+ }
259
+ display.displaySnapshot = result as LoadDisplayEntry<ScopedScopeLike>;
260
+ display.displayKey = displayKey;
261
+ return result;
262
+ }
263
+
264
+ function retryKey(keyInput: LoadCoordinatorKey): void {
265
+ const key = cacheKey(keyInput.engineRef, keyInput.partition, keyInput.namespaces);
266
+ if (!entries.has(key)) {
267
+ return;
268
+ }
269
+ entries.delete(key);
270
+ bumpAndNotify();
271
+ }
272
+
273
+ function subscribe(listener: () => void): () => void {
274
+ listeners.add(listener);
275
+ return () => {
276
+ listeners.delete(listener);
277
+ };
278
+ }
279
+
280
+ function getPromise(): Promise<Scope> {
281
+ if (lastKey === null) {
282
+ throw new Error("createLoadCoordinator: call ensure()/request() before getPromise()");
283
+ }
284
+ const entry = entries.get(lastKey);
285
+ if (entry === undefined) {
286
+ throw new Error("createLoadCoordinator: call ensure()/request() before getPromise()");
287
+ }
288
+ return entry.promise;
289
+ }
290
+
291
+ function getResolvedScope(): Scope | null {
292
+ if (lastKey === null) {
293
+ return null;
294
+ }
295
+ const entry = entries.get(lastKey);
296
+ return entry?.status === "resolved" ? entry.resolvedScope : null;
297
+ }
298
+
299
+ return {
300
+ get revision() {
301
+ return revision;
302
+ },
303
+ request: ensure,
304
+ ensure,
305
+ getEntry,
306
+ getDisplayEntry,
307
+ retry: retryKey,
308
+ subscribe,
309
+ getPromise,
310
+ getResolvedScope,
311
+ };
312
+ }