@fivem-nui/core 1.0.0 → 1.1.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.
- package/dist/EventBus.js +2 -2
- package/dist/EventBus.js.map +1 -1
- package/dist/MessageTransport.d.ts.map +1 -1
- package/dist/MessageTransport.js +6 -1
- package/dist/MessageTransport.js.map +1 -1
- package/dist/NuiClient.d.ts +3 -3
- package/dist/NuiClient.d.ts.map +1 -1
- package/dist/NuiClient.js +14 -9
- package/dist/NuiClient.js.map +1 -1
- package/package.json +1 -1
- package/src/CallbackRegistry.ts +46 -46
- package/src/EventBus.ts +119 -119
- package/src/MessageTransport.ts +95 -88
- package/src/NuiClient.ts +24 -20
- package/src/NuiClientOptions.ts +57 -57
- package/src/RequestCoordinator.ts +105 -105
- package/src/diagnostics/NuiDiagnostics.ts +28 -28
- package/src/types.ts +151 -151
package/src/MessageTransport.ts
CHANGED
|
@@ -1,88 +1,95 @@
|
|
|
1
|
-
import { NuiErrorCodes, NuiRequestError } from './errors';
|
|
2
|
-
import type { NuiLogger } from './logger';
|
|
3
|
-
import type { NuiMessage } from './types';
|
|
4
|
-
|
|
5
|
-
export type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* HTTP transport for FiveM NUI.
|
|
9
|
-
*
|
|
10
|
-
* Posts envelopes to `https://<resource>/<callback>` using `fetch`.
|
|
11
|
-
* Each POST is bounded by an `AbortController` deadline so a resource
|
|
12
|
-
* that stops answering (restart, shutdown) does not hang the page forever.
|
|
13
|
-
*
|
|
14
|
-
* Mirrors C# `FiveMNuiTransport`.
|
|
15
|
-
*/
|
|
16
|
-
export class MessageTransport {
|
|
17
|
-
private readonly resource: string;
|
|
18
|
-
private readonly callbackName: string;
|
|
19
|
-
private readonly postTimeoutMs: number;
|
|
20
|
-
private readonly logger: NuiLogger;
|
|
21
|
-
private readonly fetchImpl: FetchImpl;
|
|
22
|
-
|
|
23
|
-
constructor(
|
|
24
|
-
resource: string,
|
|
25
|
-
callbackName: string,
|
|
26
|
-
postTimeoutMs: number,
|
|
27
|
-
logger: NuiLogger,
|
|
28
|
-
fetchImpl: FetchImpl,
|
|
29
|
-
) {
|
|
30
|
-
this.resource = resource;
|
|
31
|
-
this.callbackName = callbackName;
|
|
32
|
-
this.postTimeoutMs = postTimeoutMs;
|
|
33
|
-
this.logger = logger;
|
|
34
|
-
this.fetchImpl = fetchImpl;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** URL of the main resource callback. */
|
|
38
|
-
get mainCallbackUrl(): string {
|
|
39
|
-
return `https://${this.resource}/${encodeURIComponent(this.callbackName)}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Builds the URL for an arbitrary callback on the same resource. */
|
|
43
|
-
callbackUrl(callbackName: string): string {
|
|
44
|
-
return `https://${this.resource}/${encodeURIComponent(callbackName)}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Raw POST to `url` with the configured timeout deadline.
|
|
49
|
-
*
|
|
50
|
-
* @throws {NuiRequestError} with code `POST_TIMEOUT` when the resource
|
|
51
|
-
* stops answering within `postTimeoutMs`.
|
|
52
|
-
* @throws {NuiRequestError} with code `TRANSPORT` for any other failure.
|
|
53
|
-
*/
|
|
54
|
-
async post(url: string, body: unknown): Promise<void> {
|
|
55
|
-
// Without an abort, a resource that stops answering leaves this promise
|
|
56
|
-
// pending forever and the caller's await never returns.
|
|
57
|
-
const controller = new AbortController();
|
|
58
|
-
const deadline = setTimeout(() => controller.abort(), this.postTimeoutMs);
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
await this.fetchImpl(url, {
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
|
64
|
-
body: JSON.stringify(body),
|
|
65
|
-
signal: controller.signal,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1
|
+
import { NuiErrorCodes, NuiRequestError } from './errors';
|
|
2
|
+
import type { NuiLogger } from './logger';
|
|
3
|
+
import type { NuiMessage } from './types';
|
|
4
|
+
|
|
5
|
+
export type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HTTP transport for FiveM NUI.
|
|
9
|
+
*
|
|
10
|
+
* Posts envelopes to `https://<resource>/<callback>` using `fetch`.
|
|
11
|
+
* Each POST is bounded by an `AbortController` deadline so a resource
|
|
12
|
+
* that stops answering (restart, shutdown) does not hang the page forever.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors C# `FiveMNuiTransport`.
|
|
15
|
+
*/
|
|
16
|
+
export class MessageTransport {
|
|
17
|
+
private readonly resource: string;
|
|
18
|
+
private readonly callbackName: string;
|
|
19
|
+
private readonly postTimeoutMs: number;
|
|
20
|
+
private readonly logger: NuiLogger;
|
|
21
|
+
private readonly fetchImpl: FetchImpl;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
resource: string,
|
|
25
|
+
callbackName: string,
|
|
26
|
+
postTimeoutMs: number,
|
|
27
|
+
logger: NuiLogger,
|
|
28
|
+
fetchImpl: FetchImpl,
|
|
29
|
+
) {
|
|
30
|
+
this.resource = resource;
|
|
31
|
+
this.callbackName = callbackName;
|
|
32
|
+
this.postTimeoutMs = postTimeoutMs;
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
this.fetchImpl = fetchImpl;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** URL of the main resource callback. */
|
|
38
|
+
get mainCallbackUrl(): string {
|
|
39
|
+
return `https://${this.resource}/${encodeURIComponent(this.callbackName)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Builds the URL for an arbitrary callback on the same resource. */
|
|
43
|
+
callbackUrl(callbackName: string): string {
|
|
44
|
+
return `https://${this.resource}/${encodeURIComponent(callbackName)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Raw POST to `url` with the configured timeout deadline.
|
|
49
|
+
*
|
|
50
|
+
* @throws {NuiRequestError} with code `POST_TIMEOUT` when the resource
|
|
51
|
+
* stops answering within `postTimeoutMs`.
|
|
52
|
+
* @throws {NuiRequestError} with code `TRANSPORT` for any other failure.
|
|
53
|
+
*/
|
|
54
|
+
async post(url: string, body: unknown): Promise<void> {
|
|
55
|
+
// Without an abort, a resource that stops answering leaves this promise
|
|
56
|
+
// pending forever and the caller's await never returns.
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const deadline = setTimeout(() => controller.abort(), this.postTimeoutMs);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.fetchImpl(url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
signal: controller.signal,
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new NuiRequestError(
|
|
69
|
+
NuiErrorCodes.Transport,
|
|
70
|
+
`POST ${url} failed with HTTP ${response.status}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// A timeout abort surfaces as an error whose `name` is 'AbortError'.
|
|
75
|
+
// In browsers that is a DOMException, but some fetch polyfills throw a
|
|
76
|
+
// plain Error, so match on the name instead of the constructor. Checking
|
|
77
|
+
// only `controller.signal.aborted` is unsafe: a race between the deadline
|
|
78
|
+
// firing and an unrelated network error would misclassify the network
|
|
79
|
+
// error as POST_TIMEOUT.
|
|
80
|
+
const isAbortError = (error as { name?: string } | null)?.name === 'AbortError';
|
|
81
|
+
|
|
82
|
+
if (isAbortError) {
|
|
83
|
+
throw new NuiRequestError(
|
|
84
|
+
NuiErrorCodes.PostTimeout,
|
|
85
|
+
`POST ${url} timed out after ${this.postTimeoutMs}ms`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (error instanceof NuiRequestError) throw error;
|
|
90
|
+
throw new NuiRequestError(NuiErrorCodes.Transport, String(error));
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(deadline);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/NuiClient.ts
CHANGED
|
@@ -35,15 +35,15 @@ const WINDOW_ACTIONS: ReadonlySet<string> = new Set<NuiWindowAction>([
|
|
|
35
35
|
type WindowHandler = (action: NuiWindowAction, window: string | undefined) => void;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* `NuiClient` is **thin
|
|
38
|
+
* `NuiClient` is **thin façade** it composes four collaborators and
|
|
39
39
|
* orchestrates their interaction:
|
|
40
40
|
*
|
|
41
41
|
* | Collaborator | Responsibility |
|
|
42
42
|
* |------------------------|----------------------------------------|
|
|
43
43
|
* | `MessageTransport` | Raw HTTP POST to `https://<res>/<cb>` |
|
|
44
|
-
* | `RequestCoordinator` | Correlation ID
|
|
44
|
+
* | `RequestCoordinator` | Correlation ID ↔ Promise resolution |
|
|
45
45
|
* | `CallbackRegistry` | C#-request handler lookup |
|
|
46
|
-
* | `EventBus` | C
|
|
46
|
+
* | `EventBus` | C#→UI event subscription / dispatch |
|
|
47
47
|
*
|
|
48
48
|
* Window lifecycle listeners (`onWindow`) and the browser `message`
|
|
49
49
|
* listener (`listen`) stay here because they are browser-integration glue.
|
|
@@ -94,7 +94,7 @@ export class NuiClient {
|
|
|
94
94
|
|
|
95
95
|
this.logger.debug('NuiClient initialized', { resource, callbackName, postTimeoutMs });
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
/**
|
|
99
99
|
* Sends a one way message to C#.
|
|
100
100
|
*
|
|
@@ -277,7 +277,7 @@ export class NuiClient {
|
|
|
277
277
|
throw error;
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
// Callback server (C# → UI requests)
|
|
282
282
|
/**
|
|
283
283
|
* Registers a callback handler for C# requests.
|
|
@@ -339,7 +339,7 @@ export class NuiClient {
|
|
|
339
339
|
unregisterCallback(eventName: string): boolean {
|
|
340
340
|
return this.callbackRegistry.unregister(eventName);
|
|
341
341
|
}
|
|
342
|
-
|
|
342
|
+
|
|
343
343
|
/**
|
|
344
344
|
* Subscribes to C# events.
|
|
345
345
|
*
|
|
@@ -349,7 +349,7 @@ export class NuiClient {
|
|
|
349
349
|
on<T>(eventName: keyof NuiEventMap extends never ? string : never, handler: (data: T) => void): () => void;
|
|
350
350
|
on(eventName: string, handler: (data: unknown) => void): () => void {
|
|
351
351
|
if (this.disposed) {
|
|
352
|
-
return () => {};
|
|
352
|
+
return () => { };
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
return this.eventBus.on(eventName, handler);
|
|
@@ -362,7 +362,7 @@ export class NuiClient {
|
|
|
362
362
|
once<T>(eventName: keyof NuiEventMap extends never ? string : never, handler: (data: T) => void): () => void;
|
|
363
363
|
once(eventName: string, handler: (data: unknown) => void): () => void {
|
|
364
364
|
if (this.disposed) {
|
|
365
|
-
return () => {};
|
|
365
|
+
return () => { };
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
return this.eventBus.once(eventName, handler);
|
|
@@ -374,7 +374,7 @@ export class NuiClient {
|
|
|
374
374
|
off(eventName: string, handler?: EventHandler): void {
|
|
375
375
|
this.eventBus.off(eventName, handler);
|
|
376
376
|
}
|
|
377
|
-
|
|
377
|
+
|
|
378
378
|
/**
|
|
379
379
|
* Subscribes to window lifecycle envelopes from C#.
|
|
380
380
|
*
|
|
@@ -383,7 +383,7 @@ export class NuiClient {
|
|
|
383
383
|
*/
|
|
384
384
|
onWindow(handler: WindowHandler): () => void {
|
|
385
385
|
if (this.disposed) {
|
|
386
|
-
return () => {};
|
|
386
|
+
return () => { };
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
this.windowListeners.add(handler);
|
|
@@ -392,7 +392,7 @@ export class NuiClient {
|
|
|
392
392
|
this.windowListeners.delete(handler);
|
|
393
393
|
};
|
|
394
394
|
}
|
|
395
|
-
|
|
395
|
+
|
|
396
396
|
/**
|
|
397
397
|
* Routes an incoming message from C#. Pass `MessageEvent.data` here;
|
|
398
398
|
* `listen()` does that for you.
|
|
@@ -430,7 +430,7 @@ export class NuiClient {
|
|
|
430
430
|
*/
|
|
431
431
|
listen(target: Window = window): () => void {
|
|
432
432
|
if (this.listening) {
|
|
433
|
-
return () => {};
|
|
433
|
+
return () => { };
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
this.listening = true;
|
|
@@ -451,15 +451,19 @@ export class NuiClient {
|
|
|
451
451
|
|
|
452
452
|
target.addEventListener('message', listener as EventListener);
|
|
453
453
|
|
|
454
|
-
|
|
454
|
+
let stopped = false;
|
|
455
|
+
const stop = () => {
|
|
456
|
+
if (stopped) return;
|
|
457
|
+
stopped = true;
|
|
455
458
|
target.removeEventListener('message', listener as EventListener);
|
|
456
|
-
this.
|
|
457
|
-
|
|
459
|
+
if (this.stopListening === stop) {
|
|
460
|
+
this.listening = false;
|
|
461
|
+
this.stopListening = null;
|
|
462
|
+
}
|
|
458
463
|
};
|
|
464
|
+
this.stopListening = stop;
|
|
459
465
|
|
|
460
|
-
return
|
|
461
|
-
this.stopListening?.();
|
|
462
|
-
};
|
|
466
|
+
return stop;
|
|
463
467
|
}
|
|
464
468
|
|
|
465
469
|
/** Handles incoming requests from C#. */
|
|
@@ -556,7 +560,7 @@ export class NuiClient {
|
|
|
556
560
|
}
|
|
557
561
|
}
|
|
558
562
|
}
|
|
559
|
-
|
|
563
|
+
|
|
560
564
|
/**
|
|
561
565
|
* POSTs an envelope to the main callback, incrementing the `sent` counter.
|
|
562
566
|
* Reused by `send`, `sendAndWait` and `request`.
|
|
@@ -575,7 +579,7 @@ export class NuiClient {
|
|
|
575
579
|
this.logger.warn('Could not deliver response', error);
|
|
576
580
|
}
|
|
577
581
|
}
|
|
578
|
-
|
|
582
|
+
|
|
579
583
|
/** Current counters. Cheap enough to call from a diagnostics panel. */
|
|
580
584
|
getDiagnostics(): NuiDiagnostics {
|
|
581
585
|
return {
|
package/src/NuiClientOptions.ts
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import type { NuiLogger } from './logger';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Options for the NUI bridge.
|
|
5
|
-
*/
|
|
6
|
-
export interface NuiClientOptions {
|
|
7
|
-
/**
|
|
8
|
-
* Resource name the NUI page is served from. Defaults to `GetParentResourceName()`.
|
|
9
|
-
*/
|
|
10
|
-
resource?: string;
|
|
11
|
-
/**
|
|
12
|
-
* Name of the resource callback C# registered via `RegisterRawNuiCallback`.
|
|
13
|
-
* Every outbound message is POSTed to `https://<resource>/<callback>`.
|
|
14
|
-
*/
|
|
15
|
-
callback?: string;
|
|
16
|
-
/**
|
|
17
|
-
* Window name that `open`/`close` envelopes refer to. Passed straight
|
|
18
|
-
* through to `onWindow`, so the UI can ignore other windows.
|
|
19
|
-
*/
|
|
20
|
-
window?: string;
|
|
21
|
-
/**
|
|
22
|
-
* How long a single POST may take before it is aborted, in milliseconds.
|
|
23
|
-
* The resource can stop answering at any time (restart, shutdown) and a raw
|
|
24
|
-
* fetch without a deadline would leave the caller awaiting forever.
|
|
25
|
-
*/
|
|
26
|
-
postTimeoutMs?: number;
|
|
27
|
-
/**
|
|
28
|
-
* Default deadline for {@link NuiClient.request}, in milliseconds.
|
|
29
|
-
* Matches the C# `NuiOptions.DefaultTimeout` default of 30s.
|
|
30
|
-
*/
|
|
31
|
-
requestTimeoutMs?: number;
|
|
32
|
-
/**
|
|
33
|
-
* Where diagnostics go. Defaults to the console; pass `nullNuiLogger` to
|
|
34
|
-
* silence it entirely.
|
|
35
|
-
*/
|
|
36
|
-
logger?: NuiLogger;
|
|
37
|
-
/**
|
|
38
|
-
* Injection seam for tests. Defaults to the global `fetch`.
|
|
39
|
-
*/
|
|
40
|
-
fetch?: typeof fetch;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Resolves the owning FiveM resource name from the ambient FiveM runtime.
|
|
45
|
-
*
|
|
46
|
-
* Falls back to `'unknown_resource'` when running outside a game context
|
|
47
|
-
* (SSR, unit tests, build pipeline) so importing the package never throws.
|
|
48
|
-
*/
|
|
49
|
-
export function readResourceName(): string {
|
|
50
|
-
const runtime = globalThis as { GetParentResourceName?: () => string };
|
|
51
|
-
|
|
52
|
-
if (typeof runtime.GetParentResourceName === 'function') {
|
|
53
|
-
return runtime.GetParentResourceName();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return 'unknown_resource';
|
|
57
|
-
}
|
|
1
|
+
import type { NuiLogger } from './logger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for the NUI bridge.
|
|
5
|
+
*/
|
|
6
|
+
export interface NuiClientOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Resource name the NUI page is served from. Defaults to `GetParentResourceName()`.
|
|
9
|
+
*/
|
|
10
|
+
resource?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Name of the resource callback C# registered via `RegisterRawNuiCallback`.
|
|
13
|
+
* Every outbound message is POSTed to `https://<resource>/<callback>`.
|
|
14
|
+
*/
|
|
15
|
+
callback?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Window name that `open`/`close` envelopes refer to. Passed straight
|
|
18
|
+
* through to `onWindow`, so the UI can ignore other windows.
|
|
19
|
+
*/
|
|
20
|
+
window?: string;
|
|
21
|
+
/**
|
|
22
|
+
* How long a single POST may take before it is aborted, in milliseconds.
|
|
23
|
+
* The resource can stop answering at any time (restart, shutdown) and a raw
|
|
24
|
+
* fetch without a deadline would leave the caller awaiting forever.
|
|
25
|
+
*/
|
|
26
|
+
postTimeoutMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Default deadline for {@link NuiClient.request}, in milliseconds.
|
|
29
|
+
* Matches the C# `NuiOptions.DefaultTimeout` default of 30s.
|
|
30
|
+
*/
|
|
31
|
+
requestTimeoutMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Where diagnostics go. Defaults to the console; pass `nullNuiLogger` to
|
|
34
|
+
* silence it entirely.
|
|
35
|
+
*/
|
|
36
|
+
logger?: NuiLogger;
|
|
37
|
+
/**
|
|
38
|
+
* Injection seam for tests. Defaults to the global `fetch`.
|
|
39
|
+
*/
|
|
40
|
+
fetch?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the owning FiveM resource name from the ambient FiveM runtime.
|
|
45
|
+
*
|
|
46
|
+
* Falls back to `'unknown_resource'` when running outside a game context
|
|
47
|
+
* (SSR, unit tests, build pipeline) so importing the package never throws.
|
|
48
|
+
*/
|
|
49
|
+
export function readResourceName(): string {
|
|
50
|
+
const runtime = globalThis as { GetParentResourceName?: () => string };
|
|
51
|
+
|
|
52
|
+
if (typeof runtime.GetParentResourceName === 'function') {
|
|
53
|
+
return runtime.GetParentResourceName();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return 'unknown_resource';
|
|
57
|
+
}
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A pending request awaiting a correlated response.
|
|
3
|
-
*/
|
|
4
|
-
interface PendingRequest {
|
|
5
|
-
resolve: (value: unknown) => void;
|
|
6
|
-
reject: (error: Error) => void;
|
|
7
|
-
timeout: ReturnType<typeof setTimeout>,
|
|
8
|
-
/** Released whenever the request settles or is removed (e.g. abort listener). */
|
|
9
|
-
cleanup?: () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Tracks in-flight requests by correlation ID, matching responses
|
|
14
|
-
* to their Promise resolvers.
|
|
15
|
-
*
|
|
16
|
-
* Mirrors C# `ConcurrentRequestManager` same idea, same responsibilities:
|
|
17
|
-
* register, resolve, reject, unregister, and reject all on dispose.
|
|
18
|
-
*/
|
|
19
|
-
export class RequestCoordinator {
|
|
20
|
-
private pending = new Map<string, PendingRequest>();
|
|
21
|
-
|
|
22
|
-
/** Store a pending request so a later response can resolve or reject it. */
|
|
23
|
-
register(
|
|
24
|
-
id: string,
|
|
25
|
-
resolve: PendingRequest['resolve'],
|
|
26
|
-
reject: PendingRequest['reject'],
|
|
27
|
-
timeout: ReturnType<typeof setTimeout>,
|
|
28
|
-
cleanup?: () => void,
|
|
29
|
-
): void {
|
|
30
|
-
this.pending.set(id, { resolve, reject, timeout, cleanup });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Resolve the request whose correlation ID matches.
|
|
35
|
-
* @returns `true` if a matching request was found.
|
|
36
|
-
*/
|
|
37
|
-
resolve(id: string, payload: unknown): boolean {
|
|
38
|
-
const entry = this.pending.get(id);
|
|
39
|
-
if (!entry) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
clearTimeout(entry.timeout);
|
|
44
|
-
entry.cleanup?.();
|
|
45
|
-
this.pending.delete(id);
|
|
46
|
-
entry.resolve(payload);
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Reject the request whose correlation ID matches.
|
|
52
|
-
* @returns `true` if a matching request was found.
|
|
53
|
-
*/
|
|
54
|
-
reject(id: string, error: Error): boolean {
|
|
55
|
-
const entry = this.pending.get(id);
|
|
56
|
-
if (!entry) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
clearTimeout(entry.timeout);
|
|
61
|
-
entry.cleanup?.();
|
|
62
|
-
this.pending.delete(id);
|
|
63
|
-
entry.reject(error);
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Removes a request without resolving or rejecting it (error-path cleanup). */
|
|
68
|
-
unregister(id: string): void {
|
|
69
|
-
const entry = this.pending.get(id);
|
|
70
|
-
if (entry) {
|
|
71
|
-
clearTimeout(entry.timeout);
|
|
72
|
-
entry.cleanup?.();
|
|
73
|
-
this.pending.delete(id);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Number of requests still awaiting a response. */
|
|
78
|
-
get pendingCount(): number {
|
|
79
|
-
return this.pending.size;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Rejects every outstanding request with the supplied error.
|
|
84
|
-
* Called during `dispose()` so callers don't hang forever.
|
|
85
|
-
* @returns the number of requests that were rejected.
|
|
86
|
-
*/
|
|
87
|
-
rejectAll(error: Error): number {
|
|
88
|
-
const entries = Array.from(this.pending.values());
|
|
89
|
-
|
|
90
|
-
this.pending.clear();
|
|
91
|
-
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
clearTimeout(entry.timeout);
|
|
94
|
-
entry.cleanup?.();
|
|
95
|
-
entry.reject(error);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return entries.length;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Generates a unique correlation ID for a new request. */
|
|
102
|
-
static generateCorrelationId(): string {
|
|
103
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* A pending request awaiting a correlated response.
|
|
3
|
+
*/
|
|
4
|
+
interface PendingRequest {
|
|
5
|
+
resolve: (value: unknown) => void;
|
|
6
|
+
reject: (error: Error) => void;
|
|
7
|
+
timeout: ReturnType<typeof setTimeout>,
|
|
8
|
+
/** Released whenever the request settles or is removed (e.g. abort listener). */
|
|
9
|
+
cleanup?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tracks in-flight requests by correlation ID, matching responses
|
|
14
|
+
* to their Promise resolvers.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors C# `ConcurrentRequestManager` same idea, same responsibilities:
|
|
17
|
+
* register, resolve, reject, unregister, and reject all on dispose.
|
|
18
|
+
*/
|
|
19
|
+
export class RequestCoordinator {
|
|
20
|
+
private pending = new Map<string, PendingRequest>();
|
|
21
|
+
|
|
22
|
+
/** Store a pending request so a later response can resolve or reject it. */
|
|
23
|
+
register(
|
|
24
|
+
id: string,
|
|
25
|
+
resolve: PendingRequest['resolve'],
|
|
26
|
+
reject: PendingRequest['reject'],
|
|
27
|
+
timeout: ReturnType<typeof setTimeout>,
|
|
28
|
+
cleanup?: () => void,
|
|
29
|
+
): void {
|
|
30
|
+
this.pending.set(id, { resolve, reject, timeout, cleanup });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the request whose correlation ID matches.
|
|
35
|
+
* @returns `true` if a matching request was found.
|
|
36
|
+
*/
|
|
37
|
+
resolve(id: string, payload: unknown): boolean {
|
|
38
|
+
const entry = this.pending.get(id);
|
|
39
|
+
if (!entry) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clearTimeout(entry.timeout);
|
|
44
|
+
entry.cleanup?.();
|
|
45
|
+
this.pending.delete(id);
|
|
46
|
+
entry.resolve(payload);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reject the request whose correlation ID matches.
|
|
52
|
+
* @returns `true` if a matching request was found.
|
|
53
|
+
*/
|
|
54
|
+
reject(id: string, error: Error): boolean {
|
|
55
|
+
const entry = this.pending.get(id);
|
|
56
|
+
if (!entry) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clearTimeout(entry.timeout);
|
|
61
|
+
entry.cleanup?.();
|
|
62
|
+
this.pending.delete(id);
|
|
63
|
+
entry.reject(error);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Removes a request without resolving or rejecting it (error-path cleanup). */
|
|
68
|
+
unregister(id: string): void {
|
|
69
|
+
const entry = this.pending.get(id);
|
|
70
|
+
if (entry) {
|
|
71
|
+
clearTimeout(entry.timeout);
|
|
72
|
+
entry.cleanup?.();
|
|
73
|
+
this.pending.delete(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Number of requests still awaiting a response. */
|
|
78
|
+
get pendingCount(): number {
|
|
79
|
+
return this.pending.size;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Rejects every outstanding request with the supplied error.
|
|
84
|
+
* Called during `dispose()` so callers don't hang forever.
|
|
85
|
+
* @returns the number of requests that were rejected.
|
|
86
|
+
*/
|
|
87
|
+
rejectAll(error: Error): number {
|
|
88
|
+
const entries = Array.from(this.pending.values());
|
|
89
|
+
|
|
90
|
+
this.pending.clear();
|
|
91
|
+
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
clearTimeout(entry.timeout);
|
|
94
|
+
entry.cleanup?.();
|
|
95
|
+
entry.reject(error);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return entries.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Generates a unique correlation ID for a new request. */
|
|
102
|
+
static generateCorrelationId(): string {
|
|
103
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
104
|
+
}
|
|
105
|
+
}
|