@fivem-nui/core 0.0.0-stage → 1.0.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.
Files changed (78) hide show
  1. package/README.md +230 -2
  2. package/dist/CallbackRegistry.d.ts +30 -0
  3. package/dist/CallbackRegistry.d.ts.map +1 -0
  4. package/dist/CallbackRegistry.js +37 -0
  5. package/dist/CallbackRegistry.js.map +1 -0
  6. package/dist/EventBus.d.ts +44 -0
  7. package/dist/EventBus.d.ts.map +1 -0
  8. package/dist/EventBus.js +98 -0
  9. package/dist/EventBus.js.map +1 -0
  10. package/dist/MessageTransport.d.ts +32 -0
  11. package/dist/MessageTransport.d.ts.map +1 -0
  12. package/dist/MessageTransport.js +70 -0
  13. package/dist/MessageTransport.js.map +1 -0
  14. package/dist/NuiClient.d.ts +155 -0
  15. package/dist/NuiClient.d.ts.map +1 -0
  16. package/dist/NuiClient.js +425 -0
  17. package/dist/NuiClient.js.map +1 -0
  18. package/dist/NuiClientOptions.d.ts +48 -0
  19. package/dist/NuiClientOptions.d.ts.map +1 -0
  20. package/dist/NuiClientOptions.js +14 -0
  21. package/dist/NuiClientOptions.js.map +1 -0
  22. package/dist/RequestCoordinator.d.ts +46 -0
  23. package/dist/RequestCoordinator.d.ts.map +1 -0
  24. package/dist/RequestCoordinator.js +77 -0
  25. package/dist/RequestCoordinator.js.map +1 -0
  26. package/dist/diagnostics/NuiDiagnostics.d.ts +29 -0
  27. package/dist/diagnostics/NuiDiagnostics.d.ts.map +1 -0
  28. package/dist/diagnostics/NuiDiagnostics.js +2 -0
  29. package/dist/diagnostics/NuiDiagnostics.js.map +1 -0
  30. package/dist/diagnostics/index.d.ts +11 -0
  31. package/dist/diagnostics/index.d.ts.map +1 -0
  32. package/dist/diagnostics/index.js +2 -0
  33. package/dist/diagnostics/index.js.map +1 -0
  34. package/dist/errors.d.ts +41 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +50 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/index.d.ts +17 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +10 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/logger.d.ts +22 -0
  43. package/dist/logger.d.ts.map +1 -0
  44. package/dist/logger.js +42 -0
  45. package/dist/logger.js.map +1 -0
  46. package/dist/testing/fake-fetch.d.ts +30 -0
  47. package/dist/testing/fake-fetch.d.ts.map +1 -0
  48. package/dist/testing/fake-fetch.js +34 -0
  49. package/dist/testing/fake-fetch.js.map +1 -0
  50. package/dist/testing/index.d.ts +39 -0
  51. package/dist/testing/index.d.ts.map +1 -0
  52. package/dist/testing/index.js +37 -0
  53. package/dist/testing/index.js.map +1 -0
  54. package/dist/testing/make-client.d.ts +77 -0
  55. package/dist/testing/make-client.d.ts.map +1 -0
  56. package/dist/testing/make-client.js +117 -0
  57. package/dist/testing/make-client.js.map +1 -0
  58. package/dist/types.d.ts +128 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +60 -3
  63. package/src/CallbackRegistry.ts +46 -0
  64. package/src/EventBus.ts +119 -0
  65. package/src/MessageTransport.ts +88 -0
  66. package/src/NuiClient.ts +617 -0
  67. package/src/NuiClientOptions.ts +57 -0
  68. package/src/RequestCoordinator.ts +105 -0
  69. package/src/diagnostics/NuiDiagnostics.ts +28 -0
  70. package/src/diagnostics/index.ts +10 -0
  71. package/src/errors.ts +58 -0
  72. package/src/fivem.d.ts +5 -0
  73. package/src/index.ts +33 -0
  74. package/src/logger.ts +53 -0
  75. package/src/testing/fake-fetch.ts +64 -0
  76. package/src/testing/index.ts +38 -0
  77. package/src/testing/make-client.ts +215 -0
  78. package/src/types.ts +151 -0
package/README.md CHANGED
@@ -1,3 +1,231 @@
1
- # Temporary Holding Version
1
+ # @fivem-nui/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Strongly typed TypeScript bridge between FiveM NUI panel and C#/.NET client script.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/rian-be/FiveM.Nui/blob/main/LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22.12.0-brightgreen.svg)](#requirements)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-7.x-blue.svg)](#requirements)
8
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-success.svg)](#requirements)
9
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing)
10
+
11
+ `@fivem-nui/core` is the TypeScript half of [FiveM.Nui](../../../README.md): typed request/response, C#-callable callbacks, events, and window notifications, with no framework and no runtime dependencies. Use it with Svelte, React, Vue, or plain TypeScript.
12
+
13
+ ## Highlights
14
+
15
+ - **Typed request/response**: correlation IDs, deadlines, and cancellation.
16
+ - **Callbacks**: handlers C# can invoke, with clean unregistration.
17
+ - **Events**: one-way C# → UI subscriptions, isolated per handler.
18
+ - **Window notifications**: `open` / `close` / `focus` / `unfocus`.
19
+ - **Error codes, not text**: branch on `NuiRequestError.code`.
20
+ - **Diagnostics**: message counters for a dev panel.
21
+ - **Zero runtime dependencies**, no framework lock-in.
22
+
23
+ ## Requirements
24
+
25
+ - Node.js `>= 22.12.0`
26
+ - TypeScript `7.x` (the package ships ESM output and `.d.ts` declarations)
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @fivem-nui/core
32
+ ```
33
+
34
+ Or add it to your `package.json`:
35
+
36
+ ```json
37
+ {
38
+ "dependencies": {
39
+ "@fivem-nui/core": "^1.0.0"
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Entry points
45
+
46
+ One package, three import paths:
47
+
48
+ | Import path | Use for |
49
+ |-------------------------------|----------------------------------|
50
+ | `@fivem-nui/core` | Production UI |
51
+ | `@fivem-nui/core/testing` | Tests, no FiveM needed |
52
+ | `@fivem-nui/core/diagnostics` | Dev panels, counters |
53
+
54
+ ```ts
55
+ import { makeClient } from '@fivem-nui/core/testing';
56
+ import type { NuiDiagnostics } from '@fivem-nui/core/diagnostics';
57
+ ```
58
+
59
+ See [Package Entry Points](../../../docs/api/typescript.md#11-package-entry-points) for the full map, and
60
+ [Testing Guide](../../../docs/developers/testing.md) for testing recipes.
61
+
62
+ ## Quick start
63
+
64
+ ```ts
65
+ import { nui } from '@fivem-nui/core';
66
+
67
+ // Once, on page start. Idempotent.
68
+ nui.listen();
69
+
70
+ // C# -> UI: handle a request and answer it.
71
+ nui.callback<void, GetPlayerDataResponse>('player:get', async () => ({
72
+ success: true,
73
+ value: { name: 'NuiPlayer', health: 100 },
74
+ }));
75
+
76
+ // C# -> UI: react to an event. Keep the unsubscribe for teardown.
77
+ const unsubscribe = nui.on<InventoryUpdated>('inventory:update', render);
78
+
79
+ // UI -> C#: fire and forget.
80
+ nui.notify('demo:action', { action: 'send' });
81
+
82
+ // UI -> C#: call a callback registered in C# and await the response.
83
+ const result = await nui.request<UseItemRequest, UseItemResponse>('inventory:use', { itemId });
84
+ ```
85
+
86
+ `GetPlayerDataResponse`, `InventoryUpdated`, `UseItemRequest`, and `UseItemResponse` are application defined types. The C# side must expose the matching callbacks and handlers.
87
+
88
+ ## Generated contracts (optional)
89
+
90
+ The event name strings above are unchecked: a typo compiles and fails at runtime. To make every call statically checked against the C# contract, generate a TypeScript map with the [`FiveM.Nui.Contracts`](../../../tools/FiveM.Nui.Contracts) tool from C# types annotated with `[NuiContract]` and `[NuiEvent]`:
91
+
92
+ ```bash
93
+ fivem-nui-contracts -a path/to/MyProject.dll -o src/contracts.ts
94
+ ```
95
+
96
+ The generated file ends with a module augmentation:
97
+
98
+ ```ts
99
+ declare module '@fivem-nui/core' {
100
+ interface NuiEventMap {
101
+ 'inventory:use': { request: UseItemRequest; response: UseItemResponse };
102
+ 'player:healthChanged': { payload: HealthChanged };
103
+ }
104
+ }
105
+ ```
106
+
107
+ Import it once for its side effect so the map is part of the program:
108
+
109
+ ```ts
110
+ import './contracts';
111
+ import { nui } from '@fivem-nui/core';
112
+
113
+ // request/response are inferred from the contract.
114
+ const response = await nui.request('inventory:use', { itemId });
115
+ response.ok; // typed as boolean
116
+
117
+ // A name that is not in the map is a compile error.
118
+ await nui.request('inventory:usee', { itemId }); // Error: not assignable to 'never'
119
+ ```
120
+
121
+ While no map is generated `NuiEventMap` stays empty and the client keeps its permissive, string-based signatures, so this is opt-in. When the map is augmented, `request`, `send`, `sendAndWait`, `callback`, `on`, and `once` check the name, the request payload, and the response type; explicit type arguments remain available as an escape hatch. Use `--core-module <module>` if the package is imported under a different specifier.
122
+
123
+ ## API
124
+
125
+ | Method | Direction | Purpose |
126
+ |--------------------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------|
127
+ | `listen(target?)` | - | Start receiving messages from C#. Idempotent; returns a function that stops listening. |
128
+ | `on<T>(event, handler)` | C# → UI | Subscribe to an event. Returns `unsubscribe`. |
129
+ | `once<T>(event, handler)` | C# → UI | Subscribe to the next matching event only. Returns `unsubscribe`. |
130
+ | `off(event, handler?)` | C# → UI | Remove one handler, or all handlers for the event. |
131
+ | `callback<TReq, TRes>(event, handler)` | C# → UI | Handle C# request the handler returns a `NuiResult`. A thrown handler is answered with `CALLBACK_ERROR`. |
132
+ | `unregisterCallback(event)` | C# → UI | Remove handler, so C# gets `UNKNOWN_CALLBACK` again. Returns `boolean`. |
133
+ | `request<TReq, TRes>(event, payload?, timeout?, signal?)` | UI → C# | Send request and await the response; rejects with `TIMEOUT` after `timeout` (default `30000` ms), or `ABORTED` when `signal` aborts. |
134
+ | `send(event, payload?)` | UI → C# | Fire and forget the HTTP reply is not read. |
135
+ | `sendAndWait(event, payload?)` | UI → C# | Resolve once the POST lands, reject on `POST_TIMEOUT`/`TRANSPORT`. Delivery confirmation only not C# acknowledgement. |
136
+ | `notify(callback, payload?)` | UI → C# | POST to a named resource callback; fire and forget. |
137
+ | `notifyAndWait(callback, payload?)` | UI → C# | Same, but resolve once the resource answers. |
138
+ | `onWindow(handler)` | C# → UI | Receive `(action, window)` for `open` / `close` / `focus` / `unfocus`. |
139
+ | `getDiagnostics()` | - | Counter snapshot (`sent`, `received`, `pending`, `listeners`, …). |
140
+ | `dispose()` | - | Drop listeners, callbacks and pending requests; reject the latter with `DISPOSED`. |
141
+
142
+ ## Errors
143
+
144
+ Failed operations reject with a `NuiRequestError`. Read `error.code`; `error.message` is for logs and may change. `isNuiRequestError(error)` narrows it inside `catch`.
145
+
146
+ | Code | Meaning |
147
+ |--------------------|-----------------------------------------------------------------------------------|
148
+ | `UNKNOWN_CALLBACK` | No handler is registered for the event on the side that had to answer. |
149
+ | `CALLBACK_ERROR` | A callback handler threw. |
150
+ | `TIMEOUT` | `request()` exceeded its deadline. |
151
+ | `ABORTED` | The caller aborted the request through its `AbortSignal`. |
152
+ | `POST_TIMEOUT` | A single POST was aborted after `postTimeoutMs`. |
153
+ | `TRANSPORT` | The POST failed for another reason (for example, the resource stopped answering). |
154
+ | `DISPOSED` | `dispose()` was called while the operation was attempted or pending. |
155
+ | `UNKNOWN_ERROR` | A failure was reported without a code. |
156
+
157
+ ```ts
158
+ import { nui, NuiErrorCodes, isNuiRequestError } from '@fivem-nui/core';
159
+
160
+ try {
161
+ await nui.request('inventory:use', { itemId });
162
+ } catch (error) {
163
+ if (!isNuiRequestError(error)) throw error;
164
+
165
+ if (error.code === NuiErrorCodes.Timeout) {
166
+ showToast('The server did not respond');
167
+ } else {
168
+ showToast('The request could not be completed');
169
+ }
170
+ }
171
+ ```
172
+
173
+ A `TIMEOUT` rejects the promise but does **not** cancel or roll back the operation: the POST may already have reached C#, and late response is dropped. If the operation has side effects, handle retries and duplicates in your application.
174
+
175
+ Pass an `AbortSignal` as the fourth argument to `request()` to stop waiting for one request without disposing the client. It rejects with `ABORTED`, and a response that arrives later is dropped.
176
+
177
+ ## Configuration
178
+
179
+ ```ts
180
+ new NuiClient({
181
+ resource: 'panel_orders', // default: GetParentResourceName(), else 'unknown_resource'
182
+ callback: 'nui', // resource callback endpoint (default: 'nui')
183
+ window: 'panel_orders', // filter for onWindow (default: every window)
184
+ postTimeoutMs: 10_000, // per POST abort deadline (default: 10000)
185
+ requestTimeoutMs: 30_000, // default request() deadline (default: 30000)
186
+ logger: nullNuiLogger, // consoleNuiLogger (default) or your own NuiLogger
187
+ fetch: myFetch, // injection seam; default: globalThis.fetch
188
+ });
189
+ ```
190
+
191
+ The default export `nui` is bound to the ambient resource. Create an instance when you need different options or your own lifecycle.
192
+
193
+ ## Design notes
194
+
195
+ - **`notify` never awaits its reply.** The browser reads nothing from the HTTP response, so awaiting would only stall the page for the whole POST timeout. Use `notifyAndWait` when delivery matters.
196
+ - **`notifyAndWait` exists for the `ready` handshake.** A silent failure there leaves the panel dead for the rest of the session.
197
+ - **`request` is for C# callbacks.** If the UI owns the handler for an event, ask C# with `request`, not `notify`: `notify` discards the reply.
198
+ - **Dispose on teardown.** Call `dispose()` when the page unmounts (remount, HMR) so a stale client cannot keep answering C# on behalf of dead page. After `dispose()` the client is inert; create a new instance to continue.
199
+ - **Handlers are isolated.** A throwing event handler is logged and skipped; its siblings still receive the event. A throwing `callback` handler is answered with `CALLBACK_ERROR` rather than leaving C# waiting.
200
+
201
+ ## Architecture
202
+
203
+ `NuiClient` is a thin facade over four collaborators, mirroring the C# side of the bridge:
204
+
205
+ | File | Class | Responsibility |
206
+ |-------------------------|------------------------------------------|------------------------------------------------------------------------------|
207
+ | `MessageTransport.ts` | `MessageTransport` | POSTs to `https://<resource>/<callback>` with an `AbortController` deadline. |
208
+ | `RequestCoordinator.ts` | `RequestCoordinator` | In flight requests, correlation IDs, deadlines, settlement. |
209
+ | `CallbackRegistry.ts` | `CallbackRegistry` | C# request handler lookup. |
210
+ | `EventBus.ts` | `EventBus` | C# → UI subscription and dispatch, with error isolation. |
211
+ | `NuiClientOptions.ts` | `NuiClientOptions`, `readResourceName()` | Configuration and resource-name detection. |
212
+ | `NuiClient.ts` | `NuiClient` | Orchestration: message dispatch, counters, lifecycle. |
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ npm install
218
+ npm run build # tsc -> dist/
219
+ npm run check # tsc --noEmit, plus the type-level contract tests
220
+ npm test # vitest run
221
+ npm run coverage # vitest run --coverage
222
+ npm run watch # rebuild on change
223
+ ```
224
+
225
+ ## Contributing
226
+
227
+ Issues and pull requests are welcome at [github.com/rian-be/FiveM.Nui](https://github.com/rian-be/FiveM.Nui). Keep changes focused, add or update tests for new behavior, and update the docs when a public API or its behavior changes.
228
+
229
+ ## License
230
+
231
+ [MIT](../../../LICENSE) © 2026 Rian.be
@@ -0,0 +1,30 @@
1
+ import type { NuiMessage } from './types';
2
+ /** Internal storage shape for a registered callback handler. */
3
+ export type CallbackHandler = (request: unknown) => Promise<NuiMessage>;
4
+ /**
5
+ * Registry for C# request handlers.
6
+ *
7
+ * Stores handlers keyed by event name, providing typ safe registration
8
+ * and O(1) lookup by name. A handler that throws is caught by the caller
9
+ * (via {@link NuiClient.handleRequest}) and reported back to C# as
10
+ * `CALLBACK_ERROR` instead of being left unanswered.
11
+ *
12
+ * Mirrors C# `NuiCallbackRegistry`.
13
+ */
14
+ export declare class CallbackRegistry {
15
+ private callbacks;
16
+ /** Registers (or replaces) a handler for `eventName`. */
17
+ register(eventName: string, handler: CallbackHandler): void;
18
+ /**
19
+ * Removes the handler for `eventName`.
20
+ * @returns `true` if a handler was removed.
21
+ */
22
+ unregister(eventName: string): boolean;
23
+ /** Retrieves the handler for `eventName`, or `undefined` if none. */
24
+ get(eventName: string): CallbackHandler | undefined;
25
+ /** Number of registered callbacks. */
26
+ get count(): number;
27
+ /** Removes every registered callback. Called during `dispose()`. */
28
+ clear(): void;
29
+ }
30
+ //# sourceMappingURL=CallbackRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallbackRegistry.d.ts","sourceRoot":"","sources":["../src/CallbackRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,gEAAgE;AAChE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAsC;IAEvD,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAE1D;IAED;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAErC;IAED,qEAAqE;IACrE,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAElD;IAED,sCAAsC;IACtC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,oEAAoE;IACpE,KAAK,IAAI,IAAI,CAEZ;CACF"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Registry for C# request handlers.
3
+ *
4
+ * Stores handlers keyed by event name, providing typ safe registration
5
+ * and O(1) lookup by name. A handler that throws is caught by the caller
6
+ * (via {@link NuiClient.handleRequest}) and reported back to C# as
7
+ * `CALLBACK_ERROR` instead of being left unanswered.
8
+ *
9
+ * Mirrors C# `NuiCallbackRegistry`.
10
+ */
11
+ export class CallbackRegistry {
12
+ callbacks = new Map();
13
+ /** Registers (or replaces) a handler for `eventName`. */
14
+ register(eventName, handler) {
15
+ this.callbacks.set(eventName, handler);
16
+ }
17
+ /**
18
+ * Removes the handler for `eventName`.
19
+ * @returns `true` if a handler was removed.
20
+ */
21
+ unregister(eventName) {
22
+ return this.callbacks.delete(eventName);
23
+ }
24
+ /** Retrieves the handler for `eventName`, or `undefined` if none. */
25
+ get(eventName) {
26
+ return this.callbacks.get(eventName);
27
+ }
28
+ /** Number of registered callbacks. */
29
+ get count() {
30
+ return this.callbacks.size;
31
+ }
32
+ /** Removes every registered callback. Called during `dispose()`. */
33
+ clear() {
34
+ this.callbacks.clear();
35
+ }
36
+ }
37
+ //# sourceMappingURL=CallbackRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallbackRegistry.js","sourceRoot":"","sources":["../src/CallbackRegistry.ts"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AACH,MAAM,OAAO,gBAAgB;IACnB,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;IAEvD,yDAAyD;IACzD,QAAQ,CAAC,SAAiB,EAAE,OAAwB;QAClD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,UAAU,CAAC,SAAiB;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC1C,CAAC;IAED,qEAAqE;IACrE,GAAG,CAAC,SAAiB;QACnB,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvC,CAAC;IAED,sCAAsC;IACtC,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC7B,CAAC;IAED,oEAAoE;IACpE,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,44 @@
1
+ import type { NuiLogger } from './logger';
2
+ /** Internal listener type: accepts any payload, callers cast as needed. */
3
+ export type EventHandler = (data: unknown) => void;
4
+ /**
5
+ * Type safe event subscription and dispatch.
6
+ *
7
+ * Maintains multiple listeners per event name, supports one-shot
8
+ * subscriptions (`once`), and isolates handler errors so a throwing
9
+ * listener cannot prevent its siblings from receiving the payload.
10
+ *
11
+ * Mirrors C# `EventManager`.
12
+ */
13
+ export declare class EventBus {
14
+ private readonly listeners;
15
+ private readonly logger;
16
+ constructor(logger: NuiLogger);
17
+ /**
18
+ * Subscribes to `eventName`.
19
+ * @returns an unsubscribe function.
20
+ */
21
+ on<T>(eventName: string, handler: (data: T) => void): () => void;
22
+ /**
23
+ * Subscribes to the next occurrence of `eventName` only.
24
+ * @returns an unsubscribe function (also valid after the handler fired).
25
+ */
26
+ once<T>(eventName: string, handler: (data: T) => void): () => void;
27
+ /**
28
+ * Unsubscribes from `eventName`.
29
+ * If `handler` is omitted, removes every listener for that event.
30
+ */
31
+ off(eventName: string, handler?: EventHandler): void;
32
+ /**
33
+ * Dispatches `payload` to every listener of `eventName`.
34
+ *
35
+ * Copies the listener set first so a handler that unsubscribes itself
36
+ * (or removes a sibling) cannot skip the next listener.
37
+ */
38
+ emit(eventName: string, payload: unknown): void;
39
+ /** Total listeners across all events. */
40
+ get listenerCount(): number;
41
+ /** Removes every listener. Called during `dispose()`. */
42
+ clear(): void;
43
+ }
44
+ //# sourceMappingURL=EventBus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventBus.d.ts","sourceRoot":"","sources":["../src/EventBus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;AAEnD;;;;;;;;GAQG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IAEnC,YAAY,MAAM,EAAE,SAAS,EAE5B;IAED;;;OAGG;IACH,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAkB/D;IAED;;;OAGG;IACH,IAAI,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAOjE;IAED;;;OAGG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAgBnD;IAED;;;;;OAKG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAc9C;IAED,yCAAyC;IACzC,IAAI,aAAa,IAAI,MAAM,CAQ1B;IAED,yDAAyD;IACzD,KAAK,IAAI,IAAI,CAEZ;CACF"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Type safe event subscription and dispatch.
3
+ *
4
+ * Maintains multiple listeners per event name, supports one-shot
5
+ * subscriptions (`once`), and isolates handler errors so a throwing
6
+ * listener cannot prevent its siblings from receiving the payload.
7
+ *
8
+ * Mirrors C# `EventManager`.
9
+ */
10
+ export class EventBus {
11
+ listeners = new Map();
12
+ logger;
13
+ constructor(logger) {
14
+ this.logger = logger;
15
+ }
16
+ /**
17
+ * Subscribes to `eventName`.
18
+ * @returns an unsubscribe function.
19
+ */
20
+ on(eventName, handler) {
21
+ let set = this.listeners.get(eventName);
22
+ if (!set) {
23
+ set = new Set();
24
+ this.listeners.set(eventName, set);
25
+ }
26
+ const listener = handler;
27
+ set.add(listener);
28
+ return () => {
29
+ set.delete(listener);
30
+ if (set.size === 0) {
31
+ this.listeners.delete(eventName);
32
+ }
33
+ };
34
+ }
35
+ /**
36
+ * Subscribes to the next occurrence of `eventName` only.
37
+ * @returns an unsubscribe function (also valid after the handler fired).
38
+ */
39
+ once(eventName, handler) {
40
+ const unsubscribe = this.on(eventName, data => {
41
+ unsubscribe();
42
+ handler(data);
43
+ });
44
+ return unsubscribe;
45
+ }
46
+ /**
47
+ * Unsubscribes from `eventName`.
48
+ * If `handler` is omitted, removes every listener for that event.
49
+ */
50
+ off(eventName, handler) {
51
+ const set = this.listeners.get(eventName);
52
+ if (!set) {
53
+ return;
54
+ }
55
+ if (handler) {
56
+ set.delete(handler);
57
+ if (set.size === 0) {
58
+ this.listeners.delete(eventName);
59
+ }
60
+ }
61
+ else {
62
+ this.listeners.delete(eventName);
63
+ }
64
+ }
65
+ /**
66
+ * Dispatches `payload` to every listener of `eventName`.
67
+ *
68
+ * Copies the listener set first so a handler that unsubscribes itself
69
+ * (or removes a sibling) cannot skip the next listener.
70
+ */
71
+ emit(eventName, payload) {
72
+ const set = this.listeners.get(eventName);
73
+ if (!set) {
74
+ return;
75
+ }
76
+ for (const listener of [...set]) {
77
+ try {
78
+ listener(payload);
79
+ }
80
+ catch (error) {
81
+ this.logger.error(`Error in event handler for: ${eventName}`, error);
82
+ }
83
+ }
84
+ }
85
+ /** Total listeners across all events. */
86
+ get listenerCount() {
87
+ let total = 0;
88
+ for (const set of this.listeners.values()) {
89
+ total += set.size;
90
+ }
91
+ return total;
92
+ }
93
+ /** Removes every listener. Called during `dispose()`. */
94
+ clear() {
95
+ this.listeners.clear();
96
+ }
97
+ }
98
+ //# sourceMappingURL=EventBus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventBus.js","sourceRoot":"","sources":["../src/EventBus.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,MAAM,OAAO,QAAQ;IACF,SAAS,GAAG,IAAI,GAAG,EAA6B,CAAC;IACjD,MAAM,CAAY;IAEnC,YAAY,MAAiB;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;;OAGG;IACH,EAAE,CAAI,SAAiB,EAAE,OAA0B;QACjD,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAExC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACrC,CAAC;QAED,MAAM,QAAQ,GAAG,OAAuB,CAAC;QACzC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAElB,OAAO,GAAG,EAAE;YACV,GAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAEtB,IAAI,GAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACpB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACnC,CAAC;QACH,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,IAAI,CAAI,SAAiB,EAAE,OAA0B;QACnD,MAAM,WAAW,GAAG,IAAI,CAAC,EAAE,CAAI,SAAS,EAAE,IAAI,CAAC,EAAE;YAC/C,WAAW,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QAEH,OAAO,WAAW,CAAC;IACrB,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,SAAiB,EAAE,OAAsB;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAE1C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO;QACT,CAAC;QAED,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAEpB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACnB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,SAAiB,EAAE,OAAgB;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAE1C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO;QACT,CAAC;QAED,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC;gBACH,QAAQ,CAAC,OAAO,CAAC,CAAC;YACpB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,SAAS,EAAE,EAAE,KAAK,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;IACH,CAAC;IAED,yCAAyC;IACzC,IAAI,aAAa;QACf,IAAI,KAAK,GAAG,CAAC,CAAC;QAEd,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,KAAK,IAAI,GAAG,CAAC,IAAI,CAAC;QACpB,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED,yDAAyD;IACzD,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ import type { NuiLogger } from './logger';
2
+ export type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ /**
4
+ * HTTP transport for FiveM NUI.
5
+ *
6
+ * Posts envelopes to `https://<resource>/<callback>` using `fetch`.
7
+ * Each POST is bounded by an `AbortController` deadline so a resource
8
+ * that stops answering (restart, shutdown) does not hang the page forever.
9
+ *
10
+ * Mirrors C# `FiveMNuiTransport`.
11
+ */
12
+ export declare class MessageTransport {
13
+ private readonly resource;
14
+ private readonly callbackName;
15
+ private readonly postTimeoutMs;
16
+ private readonly logger;
17
+ private readonly fetchImpl;
18
+ constructor(resource: string, callbackName: string, postTimeoutMs: number, logger: NuiLogger, fetchImpl: FetchImpl);
19
+ /** URL of the main resource callback. */
20
+ get mainCallbackUrl(): string;
21
+ /** Builds the URL for an arbitrary callback on the same resource. */
22
+ callbackUrl(callbackName: string): string;
23
+ /**
24
+ * Raw POST to `url` with the configured timeout deadline.
25
+ *
26
+ * @throws {NuiRequestError} with code `POST_TIMEOUT` when the resource
27
+ * stops answering within `postTimeoutMs`.
28
+ * @throws {NuiRequestError} with code `TRANSPORT` for any other failure.
29
+ */
30
+ post(url: string, body: unknown): Promise<void>;
31
+ }
32
+ //# sourceMappingURL=MessageTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageTransport.d.ts","sourceRoot":"","sources":["../src/MessageTransport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1C,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE5F;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IAEtC,YACE,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,SAAS,EAOrB;IAED,yCAAyC;IACzC,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,qEAAqE;IACrE,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAExC;IAED;;;;;;OAMG;IACG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCpD;CACF"}
@@ -0,0 +1,70 @@
1
+ import { NuiErrorCodes, NuiRequestError } from './errors';
2
+ /**
3
+ * HTTP transport for FiveM NUI.
4
+ *
5
+ * Posts envelopes to `https://<resource>/<callback>` using `fetch`.
6
+ * Each POST is bounded by an `AbortController` deadline so a resource
7
+ * that stops answering (restart, shutdown) does not hang the page forever.
8
+ *
9
+ * Mirrors C# `FiveMNuiTransport`.
10
+ */
11
+ export class MessageTransport {
12
+ resource;
13
+ callbackName;
14
+ postTimeoutMs;
15
+ logger;
16
+ fetchImpl;
17
+ constructor(resource, callbackName, postTimeoutMs, logger, fetchImpl) {
18
+ this.resource = resource;
19
+ this.callbackName = callbackName;
20
+ this.postTimeoutMs = postTimeoutMs;
21
+ this.logger = logger;
22
+ this.fetchImpl = fetchImpl;
23
+ }
24
+ /** URL of the main resource callback. */
25
+ get mainCallbackUrl() {
26
+ return `https://${this.resource}/${encodeURIComponent(this.callbackName)}`;
27
+ }
28
+ /** Builds the URL for an arbitrary callback on the same resource. */
29
+ callbackUrl(callbackName) {
30
+ return `https://${this.resource}/${encodeURIComponent(callbackName)}`;
31
+ }
32
+ /**
33
+ * Raw POST to `url` with the configured timeout deadline.
34
+ *
35
+ * @throws {NuiRequestError} with code `POST_TIMEOUT` when the resource
36
+ * stops answering within `postTimeoutMs`.
37
+ * @throws {NuiRequestError} with code `TRANSPORT` for any other failure.
38
+ */
39
+ async post(url, body) {
40
+ // Without an abort, a resource that stops answering leaves this promise
41
+ // pending forever and the caller's await never returns.
42
+ const controller = new AbortController();
43
+ const deadline = setTimeout(() => controller.abort(), this.postTimeoutMs);
44
+ try {
45
+ await this.fetchImpl(url, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json; charset=UTF-8' },
48
+ body: JSON.stringify(body),
49
+ signal: controller.signal,
50
+ });
51
+ }
52
+ catch (error) {
53
+ // A timeout abort surfaces as an error whose `name` is 'AbortError'.
54
+ // In browsers that is a DOMException, but some fetch polyfills throw a
55
+ // plain Error, so match on the name instead of the constructor. Checking
56
+ // only `controller.signal.aborted` is unsafe: a race between the deadline
57
+ // firing and an unrelated network error would misclassify the network
58
+ // error as POST_TIMEOUT.
59
+ const isAbortError = error?.name === 'AbortError';
60
+ if (isAbortError) {
61
+ throw new NuiRequestError(NuiErrorCodes.PostTimeout, `POST ${url} timed out after ${this.postTimeoutMs}ms`);
62
+ }
63
+ throw new NuiRequestError(NuiErrorCodes.Transport, String(error));
64
+ }
65
+ finally {
66
+ clearTimeout(deadline);
67
+ }
68
+ }
69
+ }
70
+ //# sourceMappingURL=MessageTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageTransport.js","sourceRoot":"","sources":["../src/MessageTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAM1D;;;;;;;;GAQG;AACH,MAAM,OAAO,gBAAgB;IACV,QAAQ,CAAS;IACjB,YAAY,CAAS;IACrB,aAAa,CAAS;IACtB,MAAM,CAAY;IAClB,SAAS,CAAY;IAEtC,YACE,QAAgB,EAChB,YAAoB,EACpB,aAAqB,EACrB,MAAiB,EACjB,SAAoB;QAEpB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,yCAAyC;IACzC,IAAI,eAAe;QACjB,OAAO,WAAW,IAAI,CAAC,QAAQ,IAAI,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;IAC7E,CAAC;IAED,qEAAqE;IACrE,WAAW,CAAC,YAAoB;QAC9B,OAAO,WAAW,IAAI,CAAC,QAAQ,IAAI,kBAAkB,CAAC,YAAY,CAAC,EAAE,CAAC;IACxE,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,IAAa;QACnC,wEAAwE;QACxE,wDAAwD;QACxD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAE1E,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE;gBACxB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,iCAAiC,EAAE;gBAC9D,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,qEAAqE;YACrE,uEAAuE;YACvE,yEAAyE;YACzE,0EAA0E;YAC1E,sEAAsE;YACtE,yBAAyB;YACzB,MAAM,YAAY,GAAI,KAAkC,EAAE,IAAI,KAAK,YAAY,CAAC;YAEhF,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,eAAe,CACvB,aAAa,CAAC,WAAW,EACzB,QAAQ,GAAG,oBAAoB,IAAI,CAAC,aAAa,IAAI,CACtD,CAAC;YACJ,CAAC;YAED,MAAM,IAAI,eAAe,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpE,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;CACF"}