@fivem-nui/svelte 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.
- package/README.md +284 -2
- package/dist/actions/clickOutside/clickOutside.d.ts +24 -0
- package/dist/actions/clickOutside/clickOutside.d.ts.map +1 -0
- package/dist/actions/clickOutside/clickOutside.js +62 -0
- package/dist/actions/clickOutside/clickOutside.js.map +1 -0
- package/dist/actions/clickOutside/handler.d.ts +11 -0
- package/dist/actions/clickOutside/handler.d.ts.map +1 -0
- package/dist/actions/clickOutside/handler.js +45 -0
- package/dist/actions/clickOutside/handler.js.map +1 -0
- package/dist/actions/clickOutside/index.d.ts +4 -0
- package/dist/actions/clickOutside/index.d.ts.map +1 -0
- package/dist/actions/clickOutside/index.js +4 -0
- package/dist/actions/clickOutside/index.js.map +1 -0
- package/dist/actions/clickOutside/types.d.ts +36 -0
- package/dist/actions/clickOutside/types.d.ts.map +1 -0
- package/dist/actions/clickOutside/types.js +6 -0
- package/dist/actions/clickOutside/types.js.map +1 -0
- package/dist/actions/clickOutside.d.ts +63 -0
- package/dist/actions/clickOutside.d.ts.map +1 -0
- package/dist/actions/clickOutside.js +106 -0
- package/dist/actions/clickOutside.js.map +1 -0
- package/dist/actions/escape/escape.d.ts +26 -0
- package/dist/actions/escape/escape.d.ts.map +1 -0
- package/dist/actions/escape/escape.js +74 -0
- package/dist/actions/escape/escape.js.map +1 -0
- package/dist/actions/escape/handler.d.ts +11 -0
- package/dist/actions/escape/handler.d.ts.map +1 -0
- package/dist/actions/escape/handler.js +50 -0
- package/dist/actions/escape/handler.js.map +1 -0
- package/dist/actions/escape/index.d.ts +4 -0
- package/dist/actions/escape/index.d.ts.map +1 -0
- package/dist/actions/escape/index.js +4 -0
- package/dist/actions/escape/index.js.map +1 -0
- package/dist/actions/escape/types.d.ts +70 -0
- package/dist/actions/escape/types.d.ts.map +1 -0
- package/dist/actions/escape/types.js +12 -0
- package/dist/actions/escape/types.js.map +1 -0
- package/dist/actions/escape.d.ts +99 -0
- package/dist/actions/escape.d.ts.map +1 -0
- package/dist/actions/escape.js +128 -0
- package/dist/actions/escape.js.map +1 -0
- package/dist/actions/focusTrap/focusTrap.d.ts +19 -0
- package/dist/actions/focusTrap/focusTrap.d.ts.map +1 -0
- package/dist/actions/focusTrap/focusTrap.js +198 -0
- package/dist/actions/focusTrap/focusTrap.js.map +1 -0
- package/dist/actions/focusTrap/focusables.d.ts +39 -0
- package/dist/actions/focusTrap/focusables.d.ts.map +1 -0
- package/dist/actions/focusTrap/focusables.js +142 -0
- package/dist/actions/focusTrap/focusables.js.map +1 -0
- package/dist/actions/focusTrap/index.d.ts +5 -0
- package/dist/actions/focusTrap/index.d.ts.map +1 -0
- package/dist/actions/focusTrap/index.js +5 -0
- package/dist/actions/focusTrap/index.js.map +1 -0
- package/dist/actions/focusTrap/trapStack.d.ts +33 -0
- package/dist/actions/focusTrap/trapStack.d.ts.map +1 -0
- package/dist/actions/focusTrap/trapStack.js +84 -0
- package/dist/actions/focusTrap/trapStack.js.map +1 -0
- package/dist/actions/focusTrap/types.d.ts +50 -0
- package/dist/actions/focusTrap/types.d.ts.map +1 -0
- package/dist/actions/focusTrap/types.js +10 -0
- package/dist/actions/focusTrap/types.js.map +1 -0
- package/dist/actions/focusTrap.d.ts +24 -0
- package/dist/actions/focusTrap.d.ts.map +1 -0
- package/dist/actions/focusTrap.js +201 -0
- package/dist/actions/focusTrap.js.map +1 -0
- package/dist/actions/focusTrap.types.d.ts +50 -0
- package/dist/actions/focusTrap.types.d.ts.map +1 -0
- package/dist/actions/focusTrap.types.js +10 -0
- package/dist/actions/focusTrap.types.js.map +1 -0
- package/dist/actions/focusables.d.ts +39 -0
- package/dist/actions/focusables.d.ts.map +1 -0
- package/dist/actions/focusables.js +142 -0
- package/dist/actions/focusables.js.map +1 -0
- package/dist/actions/index.d.ts +5 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +5 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/layer/index.d.ts +4 -0
- package/dist/actions/layer/index.d.ts.map +1 -0
- package/dist/actions/layer/index.js +4 -0
- package/dist/actions/layer/index.js.map +1 -0
- package/dist/actions/layer/layer.d.ts +28 -0
- package/dist/actions/layer/layer.d.ts.map +1 -0
- package/dist/actions/layer/layer.js +118 -0
- package/dist/actions/layer/layer.js.map +1 -0
- package/dist/actions/layer/layerStack.d.ts +10 -0
- package/dist/actions/layer/layerStack.d.ts.map +1 -0
- package/dist/actions/layer/layerStack.js +153 -0
- package/dist/actions/layer/layerStack.js.map +1 -0
- package/dist/actions/layer/types.d.ts +105 -0
- package/dist/actions/layer/types.d.ts.map +1 -0
- package/dist/actions/layer/types.js +14 -0
- package/dist/actions/layer/types.js.map +1 -0
- package/dist/actions/layer.d.ts +133 -0
- package/dist/actions/layer.d.ts.map +1 -0
- package/dist/actions/layer.js +269 -0
- package/dist/actions/layer.js.map +1 -0
- package/dist/actions/trapStack.d.ts +33 -0
- package/dist/actions/trapStack.d.ts.map +1 -0
- package/dist/actions/trapStack.js +84 -0
- package/dist/actions/trapStack.js.map +1 -0
- package/dist/client.d.ts +179 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +176 -0
- package/dist/client.js.map +1 -0
- package/dist/context/index.d.ts +68 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +112 -0
- package/dist/context/index.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/mock/index.d.ts +27 -0
- package/dist/mock/index.d.ts.map +1 -0
- package/dist/mock/index.js +137 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/mock/interceptor.d.ts +19 -0
- package/dist/mock/interceptor.d.ts.map +1 -0
- package/dist/mock/interceptor.js +128 -0
- package/dist/mock/interceptor.js.map +1 -0
- package/dist/mock/types.d.ts +100 -0
- package/dist/mock/types.d.ts.map +1 -0
- package/dist/mock/types.js +27 -0
- package/dist/mock/types.js.map +1 -0
- package/dist/mock/url.d.ts +14 -0
- package/dist/mock/url.d.ts.map +1 -0
- package/dist/mock/url.js +38 -0
- package/dist/mock/url.js.map +1 -0
- package/dist/runes/clickOutside.svelte.d.ts +29 -0
- package/dist/runes/clickOutside.svelte.d.ts.map +1 -0
- package/dist/runes/clickOutside.svelte.js +42 -0
- package/dist/runes/clickOutside.svelte.js.map +1 -0
- package/dist/runes/escape.svelte.d.ts +34 -0
- package/dist/runes/escape.svelte.d.ts.map +1 -0
- package/dist/runes/escape.svelte.js +44 -0
- package/dist/runes/escape.svelte.js.map +1 -0
- package/dist/runes/focusTrap.svelte.d.ts +21 -0
- package/dist/runes/focusTrap.svelte.d.ts.map +1 -0
- package/dist/runes/focusTrap.svelte.js +37 -0
- package/dist/runes/focusTrap.svelte.js.map +1 -0
- package/dist/runes/index.svelte.d.ts +8 -0
- package/dist/runes/index.svelte.d.ts.map +1 -0
- package/dist/runes/index.svelte.js +8 -0
- package/dist/runes/index.svelte.js.map +1 -0
- package/dist/runes/layer.svelte.d.ts +36 -0
- package/dist/runes/layer.svelte.d.ts.map +1 -0
- package/dist/runes/layer.svelte.js +48 -0
- package/dist/runes/layer.svelte.js.map +1 -0
- package/dist/runes/query.svelte.d.ts +104 -0
- package/dist/runes/query.svelte.d.ts.map +1 -0
- package/dist/runes/query.svelte.js +179 -0
- package/dist/runes/query.svelte.js.map +1 -0
- package/dist/runes/state.svelte.d.ts +72 -0
- package/dist/runes/state.svelte.d.ts.map +1 -0
- package/dist/runes/state.svelte.js +98 -0
- package/dist/runes/state.svelte.js.map +1 -0
- package/dist/runes/window.svelte.d.ts +120 -0
- package/dist/runes/window.svelte.d.ts.map +1 -0
- package/dist/runes/window.svelte.js +196 -0
- package/dist/runes/window.svelte.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/nui.d.ts +34 -0
- package/dist/types/nui.d.ts.map +1 -0
- package/dist/types/nui.js +17 -0
- package/dist/types/nui.js.map +1 -0
- package/dist/utils/env.d.ts +21 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +36 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +30 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/keyboard.d.ts +127 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +152 -0
- package/dist/utils/keyboard.js.map +1 -0
- package/package.json +76 -4
- package/src/actions/clickOutside/clickOutside.ts +78 -0
- package/src/actions/clickOutside/handler.ts +49 -0
- package/src/actions/clickOutside/index.ts +11 -0
- package/src/actions/clickOutside/types.ts +54 -0
- package/src/actions/escape/escape.ts +90 -0
- package/src/actions/escape/handler.ts +52 -0
- package/src/actions/escape/index.ts +11 -0
- package/src/actions/escape/types.ts +88 -0
- package/src/actions/focusTrap/focusTrap.ts +228 -0
- package/src/actions/focusTrap/focusables.ts +155 -0
- package/src/actions/focusTrap/index.ts +22 -0
- package/src/actions/focusTrap/trapStack.ts +109 -0
- package/src/actions/focusTrap/types.ts +64 -0
- package/src/actions/index.ts +35 -0
- package/src/actions/layer/index.ts +13 -0
- package/src/actions/layer/layer.ts +140 -0
- package/src/actions/layer/layerStack.ts +169 -0
- package/src/actions/layer/types.ts +132 -0
- package/src/client.ts +323 -0
- package/src/context/index.ts +127 -0
- package/src/index.ts +53 -0
- package/src/mock/index.ts +176 -0
- package/src/mock/interceptor.ts +177 -0
- package/src/mock/types.ts +125 -0
- package/src/mock/url.ts +37 -0
- package/src/runes/clickOutside.svelte.ts +53 -0
- package/src/runes/escape.svelte.ts +71 -0
- package/src/runes/focusTrap.svelte.ts +56 -0
- package/src/runes/index.svelte.ts +38 -0
- package/src/runes/layer.svelte.ts +74 -0
- package/src/runes/query.svelte.ts +313 -0
- package/src/runes/state.svelte.ts +175 -0
- package/src/runes/window.svelte.ts +308 -0
- package/src/utils/env.ts +36 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/keyboard.ts +244 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
import type { NuiEventKey, NuiEventRequest, NuiEventResponse } from '@fivem-nui/core';
|
|
3
|
+
import {
|
|
4
|
+
globalErrorHandler,
|
|
5
|
+
NuiErrorCategory,
|
|
6
|
+
isAbortError,
|
|
7
|
+
createAbortError,
|
|
8
|
+
type RetryOptions
|
|
9
|
+
} from '@fivem-nui/error';
|
|
10
|
+
import type { SvelteNuiClient } from '../client';
|
|
11
|
+
import { useNuiClient } from '../context';
|
|
12
|
+
|
|
13
|
+
export { type RetryOptions };
|
|
14
|
+
|
|
15
|
+
export enum NuiQueryStatus {
|
|
16
|
+
Idle = 'idle',
|
|
17
|
+
Loading = 'loading',
|
|
18
|
+
Success = 'success',
|
|
19
|
+
Error = 'error'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NuiQueryOptions<TRequest = unknown, TResponse = unknown> {
|
|
23
|
+
/** Optional client override (defaults to context or global client). */
|
|
24
|
+
client?: SvelteNuiClient;
|
|
25
|
+
|
|
26
|
+
/** Request timeout in ms (0 for no timeout). */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Automatic retry configuration using exponential backoff.
|
|
31
|
+
* - `true`: enables default retry (3 attempts for timeout and network errors).
|
|
32
|
+
* - `RetryOptions`: custom retry configuration (maxRetries, delays, filter).
|
|
33
|
+
*/
|
|
34
|
+
retry?: boolean | RetryOptions;
|
|
35
|
+
|
|
36
|
+
/** Execute automatically on mount. */
|
|
37
|
+
immediate?: boolean;
|
|
38
|
+
|
|
39
|
+
/** Initial payload used for immediate fetch or fallback refetch. */
|
|
40
|
+
initialRequest?: TRequest;
|
|
41
|
+
|
|
42
|
+
/** Called on successful response. */
|
|
43
|
+
onSuccess?: (data: TResponse) => void;
|
|
44
|
+
|
|
45
|
+
/** Called on request failure (except when aborted). */
|
|
46
|
+
onError?: (error: Error) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Controller for an asynchronous NUI query.
|
|
51
|
+
*/
|
|
52
|
+
export interface NuiQuery<TRequest, TResponse> {
|
|
53
|
+
readonly status: NuiQueryStatus;
|
|
54
|
+
readonly data: TResponse | null;
|
|
55
|
+
readonly loading: boolean;
|
|
56
|
+
readonly error: Error | null;
|
|
57
|
+
readonly called: boolean;
|
|
58
|
+
readonly lastUpdated: number | null;
|
|
59
|
+
|
|
60
|
+
/** Sends the request to C#. Cancels any pending request for this query. */
|
|
61
|
+
execute(request?: TRequest, overrideTimeout?: number): Promise<TResponse>;
|
|
62
|
+
|
|
63
|
+
/** Retries the last request using the same payload. */
|
|
64
|
+
refetch(overrideTimeout?: number): Promise<TResponse>;
|
|
65
|
+
|
|
66
|
+
/** Cancels the active request and stops loading. */
|
|
67
|
+
abort(): void;
|
|
68
|
+
|
|
69
|
+
/** Aborts any pending request and resets data, error, and status to idle. */
|
|
70
|
+
reset(): void;
|
|
71
|
+
|
|
72
|
+
/** Cleans up the query and aborts pending requests. */
|
|
73
|
+
destroy(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reusable controller managing reactive state and network requests for a NUI query.
|
|
78
|
+
*/
|
|
79
|
+
export class NuiQueryController<TRequest = unknown, TResponse = unknown> implements NuiQuery<TRequest, TResponse> {
|
|
80
|
+
data = $state<TResponse | null>(null);
|
|
81
|
+
loading = $state(false);
|
|
82
|
+
error = $state<Error | null>(null);
|
|
83
|
+
called = $state(false);
|
|
84
|
+
lastUpdated = $state<number | null>(null);
|
|
85
|
+
|
|
86
|
+
private isSuccess = $state(false);
|
|
87
|
+
private lastRequest?: TRequest;
|
|
88
|
+
private hasLastRequest = false;
|
|
89
|
+
private activeRequestId = 0;
|
|
90
|
+
private abortController: AbortController | null = null;
|
|
91
|
+
private isDisposed = false;
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
private readonly eventName: string,
|
|
95
|
+
private readonly options: NuiQueryOptions<TRequest, TResponse> = {},
|
|
96
|
+
private readonly client: SvelteNuiClient
|
|
97
|
+
) {
|
|
98
|
+
this.lastRequest = options.initialRequest;
|
|
99
|
+
this.hasLastRequest = 'initialRequest' in options;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get status(): NuiQueryStatus {
|
|
103
|
+
if (this.loading) return NuiQueryStatus.Loading;
|
|
104
|
+
if (this.error !== null) return NuiQueryStatus.Error;
|
|
105
|
+
if (this.isSuccess) return NuiQueryStatus.Success;
|
|
106
|
+
return NuiQueryStatus.Idle;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async sendRequest(payload: TRequest | undefined, overrideTimeout?: number): Promise<TResponse> {
|
|
110
|
+
if (this.isDisposed) {
|
|
111
|
+
throw new Error('[useNuiQuery] Cannot execute request on a destroyed query controller.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.abortController) {
|
|
115
|
+
this.abortController.abort();
|
|
116
|
+
}
|
|
117
|
+
const currentController = new AbortController();
|
|
118
|
+
this.abortController = currentController;
|
|
119
|
+
const currentRequestId = ++this.activeRequestId;
|
|
120
|
+
|
|
121
|
+
this.loading = true;
|
|
122
|
+
this.error = null;
|
|
123
|
+
this.called = true;
|
|
124
|
+
|
|
125
|
+
const requestFn = () =>
|
|
126
|
+
this.client.request<TRequest, TResponse>(
|
|
127
|
+
this.eventName,
|
|
128
|
+
payload,
|
|
129
|
+
overrideTimeout ?? this.options.timeout,
|
|
130
|
+
currentController.signal
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
let result: TResponse;
|
|
135
|
+
if (this.options.retry) {
|
|
136
|
+
const retryConfig: RetryOptions =
|
|
137
|
+
typeof this.options.retry === 'object' ? { ...this.options.retry } : {};
|
|
138
|
+
|
|
139
|
+
const userShouldRetry = retryConfig.shouldRetry;
|
|
140
|
+
retryConfig.shouldRetry = (errorInfo) => {
|
|
141
|
+
if (
|
|
142
|
+
currentRequestId !== this.activeRequestId ||
|
|
143
|
+
currentController.signal.aborted ||
|
|
144
|
+
isAbortError(errorInfo.error)
|
|
145
|
+
) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (userShouldRetry) {
|
|
149
|
+
return userShouldRetry(errorInfo);
|
|
150
|
+
}
|
|
151
|
+
return (
|
|
152
|
+
(retryConfig.retryOnTimeout !== false && errorInfo.category === NuiErrorCategory.TIMEOUT) ||
|
|
153
|
+
(retryConfig.retryOnNetworkError !== false && errorInfo.category === NuiErrorCategory.NETWORK)
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
result = await globalErrorHandler.retry(requestFn, retryConfig);
|
|
158
|
+
} else {
|
|
159
|
+
result = await requestFn();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (currentRequestId !== this.activeRequestId || currentController.signal.aborted) {
|
|
163
|
+
throw createAbortError();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.data = result;
|
|
167
|
+
this.isSuccess = true;
|
|
168
|
+
this.lastUpdated = Date.now();
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
this.options.onSuccess?.(result);
|
|
172
|
+
} catch (callbackErr) {
|
|
173
|
+
void globalErrorHandler
|
|
174
|
+
.handle(
|
|
175
|
+
callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr)),
|
|
176
|
+
{ event: this.eventName },
|
|
177
|
+
'useNuiQuery:onSuccess'
|
|
178
|
+
)
|
|
179
|
+
.catch(() => {});
|
|
180
|
+
console.error(callbackErr);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
} catch (err: unknown) {
|
|
185
|
+
if (currentRequestId !== this.activeRequestId || currentController.signal.aborted) {
|
|
186
|
+
throw isAbortError(err) ? err : createAbortError();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (isAbortError(err)) {
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
194
|
+
this.error = e;
|
|
195
|
+
this.isSuccess = false;
|
|
196
|
+
|
|
197
|
+
void globalErrorHandler
|
|
198
|
+
.handle(e, { event: this.eventName, payload }, 'useNuiQuery:request')
|
|
199
|
+
.catch(() => {});
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
this.options.onError?.(e);
|
|
203
|
+
} catch (callbackErr) {
|
|
204
|
+
void globalErrorHandler
|
|
205
|
+
.handle(
|
|
206
|
+
callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr)),
|
|
207
|
+
{ event: this.eventName },
|
|
208
|
+
'useNuiQuery:onError'
|
|
209
|
+
)
|
|
210
|
+
.catch(() => {});
|
|
211
|
+
console.error(callbackErr);
|
|
212
|
+
}
|
|
213
|
+
throw e;
|
|
214
|
+
} finally {
|
|
215
|
+
if (currentRequestId === this.activeRequestId) {
|
|
216
|
+
this.loading = false;
|
|
217
|
+
this.abortController = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
execute = async (...args: [request?: TRequest, overrideTimeout?: number]): Promise<TResponse> => {
|
|
223
|
+
const hasExplicitRequest = args.length > 0;
|
|
224
|
+
if (hasExplicitRequest) {
|
|
225
|
+
this.lastRequest = args[0];
|
|
226
|
+
this.hasLastRequest = true;
|
|
227
|
+
}
|
|
228
|
+
return this.sendRequest(this.lastRequest, args[1]);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
refetch = (overrideTimeout?: number): Promise<TResponse> => {
|
|
232
|
+
return this.sendRequest(this.lastRequest, overrideTimeout);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
abort = (): void => {
|
|
236
|
+
if (this.abortController) {
|
|
237
|
+
this.abortController.abort();
|
|
238
|
+
this.abortController = null;
|
|
239
|
+
}
|
|
240
|
+
this.activeRequestId++;
|
|
241
|
+
this.loading = false;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
reset = (): void => {
|
|
245
|
+
this.abort();
|
|
246
|
+
this.data = null;
|
|
247
|
+
this.error = null;
|
|
248
|
+
this.isSuccess = false;
|
|
249
|
+
this.called = false;
|
|
250
|
+
this.lastUpdated = null;
|
|
251
|
+
this.lastRequest = this.options.initialRequest;
|
|
252
|
+
this.hasLastRequest = 'initialRequest' in this.options;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
destroy = (): void => {
|
|
256
|
+
if (this.isDisposed) return;
|
|
257
|
+
this.isDisposed = true;
|
|
258
|
+
this.abort();
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Reactive query for communicating with C# (`nui.request`).
|
|
264
|
+
* Manages reactive `data`, `loading`, `error`, and `status`.
|
|
265
|
+
*
|
|
266
|
+
* Note: Access properties directly (e.g. `query.data`). Do not destructure, or you'll lose reactivity.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```svelte
|
|
270
|
+
* <script>
|
|
271
|
+
* import { useNuiQuery } from '@fivem-nui/svelte';
|
|
272
|
+
*
|
|
273
|
+
* const buy = useNuiQuery('shop:buy');
|
|
274
|
+
* </script>
|
|
275
|
+
*
|
|
276
|
+
* <button onclick={() => buy.execute({ id: 'bread', qty: 1 })} disabled={buy.loading}>
|
|
277
|
+
* {buy.loading ? 'Buying...' : 'Buy'}
|
|
278
|
+
* </button>
|
|
279
|
+
*
|
|
280
|
+
* {#if buy.error}
|
|
281
|
+
* <p class="error">{buy.error.message}</p>
|
|
282
|
+
* {/if}
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
export function useNuiQuery<K extends NuiEventKey>(
|
|
286
|
+
eventName: K,
|
|
287
|
+
options?: NuiQueryOptions<NuiEventRequest<K>, NuiEventResponse<K>>
|
|
288
|
+
): NuiQuery<NuiEventRequest<K>, NuiEventResponse<K>>;
|
|
289
|
+
|
|
290
|
+
export function useNuiQuery<TRequest = unknown, TResponse = unknown>(
|
|
291
|
+
eventName: string,
|
|
292
|
+
options?: NuiQueryOptions<TRequest, TResponse>
|
|
293
|
+
): NuiQuery<TRequest, TResponse>;
|
|
294
|
+
|
|
295
|
+
export function useNuiQuery<TRequest = unknown, TResponse = unknown>(
|
|
296
|
+
eventName: string,
|
|
297
|
+
options: NuiQueryOptions<TRequest, TResponse> = {}
|
|
298
|
+
): NuiQuery<TRequest, TResponse> {
|
|
299
|
+
const client = options.client ?? useNuiClient();
|
|
300
|
+
const query = new NuiQueryController(eventName, options, client);
|
|
301
|
+
|
|
302
|
+
$effect(() => () => query.destroy());
|
|
303
|
+
|
|
304
|
+
$effect(() => {
|
|
305
|
+
untrack(() => {
|
|
306
|
+
if (options.immediate) {
|
|
307
|
+
void query.execute(options.initialRequest).catch(() => {});
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return query;
|
|
313
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { onDestroy } from 'svelte';
|
|
2
|
+
import type { NuiEventKey, NuiEventPayload } from '@fivem-nui/core';
|
|
3
|
+
import { globalErrorHandler } from '@fivem-nui/error';
|
|
4
|
+
import type { SvelteNuiClient } from '../client';
|
|
5
|
+
import { useNuiClient } from '../context';
|
|
6
|
+
|
|
7
|
+
export interface NuiStateOptions<T> {
|
|
8
|
+
/** Optional client override (defaults to context or global client). */
|
|
9
|
+
client?: SvelteNuiClient;
|
|
10
|
+
|
|
11
|
+
/** Transform or validate raw incoming payload before updating state. */
|
|
12
|
+
transform?: (raw: unknown) => T;
|
|
13
|
+
|
|
14
|
+
/** Called when payload handling or transform fails. */
|
|
15
|
+
onError?: (error: Error) => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Automatic cleanup on component unmount.
|
|
19
|
+
* - `false`: disables lifecycle hook (useful for global stores / singletons).
|
|
20
|
+
* - `true`: strictly requires component context (throws if called outside).
|
|
21
|
+
* - `undefined`: automatically hooks into component lifecycle if available.
|
|
22
|
+
*/
|
|
23
|
+
autoDestroy?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Controller for reactive state synchronized with a NUI event.
|
|
28
|
+
*/
|
|
29
|
+
export interface NuiState<T> {
|
|
30
|
+
/** Current value. */
|
|
31
|
+
value: T;
|
|
32
|
+
|
|
33
|
+
/** Timestamp (epoch ms) of the last received event, or null. */
|
|
34
|
+
readonly updatedAt: number | null;
|
|
35
|
+
|
|
36
|
+
/** Number of received events. */
|
|
37
|
+
readonly count: number;
|
|
38
|
+
|
|
39
|
+
/** Last processing or transform error, or null. */
|
|
40
|
+
readonly error: Error | null;
|
|
41
|
+
|
|
42
|
+
/** Sets the value manually without incrementing count or updatedAt. */
|
|
43
|
+
set(value: T): void;
|
|
44
|
+
|
|
45
|
+
/** Updates the value using an updater function. */
|
|
46
|
+
update(fn: (prev: T) => T): void;
|
|
47
|
+
|
|
48
|
+
/** Resets state back to the initial value and clears errors. */
|
|
49
|
+
reset(): void;
|
|
50
|
+
|
|
51
|
+
/** Unsubscribes from the event. */
|
|
52
|
+
destroy(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Binds a FiveM NUI event to reactive state.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```svelte
|
|
60
|
+
* <script>
|
|
61
|
+
* import { useNuiState } from '@fivem-nui/svelte';
|
|
62
|
+
*
|
|
63
|
+
* const player = useNuiState('player:update', { health: 100, armor: 0 });
|
|
64
|
+
* </script>
|
|
65
|
+
*
|
|
66
|
+
* <div>Health: {player.value.health}</div>
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* Controller managing reactive state synchronized with a NUI event.
|
|
71
|
+
*/
|
|
72
|
+
export class NuiStateController<T> implements NuiState<T> {
|
|
73
|
+
value = $state<T>(undefined as unknown as T);
|
|
74
|
+
updatedAt = $state<number | null>(null);
|
|
75
|
+
count = $state(0);
|
|
76
|
+
error = $state<Error | null>(null);
|
|
77
|
+
|
|
78
|
+
private readonly initialValue: T;
|
|
79
|
+
private isDestroyed = false;
|
|
80
|
+
private readonly unsubscribe: () => void;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
eventName: string,
|
|
84
|
+
initialValue: T,
|
|
85
|
+
options: NuiStateOptions<T> = {},
|
|
86
|
+
client: SvelteNuiClient
|
|
87
|
+
) {
|
|
88
|
+
this.initialValue = initialValue;
|
|
89
|
+
this.value = initialValue;
|
|
90
|
+
|
|
91
|
+
this.unsubscribe = client.on(eventName, (data: unknown) => {
|
|
92
|
+
try {
|
|
93
|
+
this.value = options.transform ? options.transform(data) : (data as T);
|
|
94
|
+
this.updatedAt = Date.now();
|
|
95
|
+
this.count += 1;
|
|
96
|
+
this.error = null;
|
|
97
|
+
} catch (err: unknown) {
|
|
98
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
99
|
+
this.error = e;
|
|
100
|
+
void globalErrorHandler
|
|
101
|
+
.handle(e, { event: eventName, payload: data }, 'useNuiState:transform')
|
|
102
|
+
.catch(() => {});
|
|
103
|
+
try {
|
|
104
|
+
options.onError?.(e);
|
|
105
|
+
} catch (callbackErr) {
|
|
106
|
+
void globalErrorHandler
|
|
107
|
+
.handle(
|
|
108
|
+
callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr)),
|
|
109
|
+
{ event: eventName },
|
|
110
|
+
'useNuiState:onError'
|
|
111
|
+
)
|
|
112
|
+
.catch(() => {});
|
|
113
|
+
console.error(callbackErr);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
set = (v: T): void => {
|
|
120
|
+
this.value = v;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
update = (fn: (prev: T) => T): void => {
|
|
124
|
+
this.value = fn(this.value);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
reset = (): void => {
|
|
128
|
+
this.value = this.initialValue;
|
|
129
|
+
this.updatedAt = null;
|
|
130
|
+
this.count = 0;
|
|
131
|
+
this.error = null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
destroy = (): void => {
|
|
135
|
+
if (this.isDestroyed) return;
|
|
136
|
+
this.isDestroyed = true;
|
|
137
|
+
this.unsubscribe();
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function useNuiState<K extends NuiEventKey>(
|
|
142
|
+
eventName: K,
|
|
143
|
+
initialValue: NuiEventPayload<K>,
|
|
144
|
+
options?: NuiStateOptions<NuiEventPayload<K>>
|
|
145
|
+
): NuiState<NuiEventPayload<K>>;
|
|
146
|
+
export function useNuiState<T>(
|
|
147
|
+
eventName: string,
|
|
148
|
+
initialValue: T,
|
|
149
|
+
options?: NuiStateOptions<T>
|
|
150
|
+
): NuiState<T>;
|
|
151
|
+
export function useNuiState<T>(
|
|
152
|
+
eventName: string,
|
|
153
|
+
initialValue: T,
|
|
154
|
+
options: NuiStateOptions<T> = {}
|
|
155
|
+
): NuiState<T> {
|
|
156
|
+
const client = options.client ?? useNuiClient();
|
|
157
|
+
const state = new NuiStateController(eventName, initialValue, options, client);
|
|
158
|
+
|
|
159
|
+
if (options.autoDestroy === true) {
|
|
160
|
+
try {
|
|
161
|
+
onDestroy(state.destroy);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
state.destroy();
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
} else if (options.autoDestroy !== false) {
|
|
167
|
+
try {
|
|
168
|
+
onDestroy(state.destroy);
|
|
169
|
+
} catch {
|
|
170
|
+
// Called outside a component lifecycle; cleanup must be managed manually via destroy().
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return state;
|
|
175
|
+
}
|