@assebc/ng-signal-http 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.
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Signal, InjectionToken, EnvironmentProviders } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global configuration passed to `provideSignalHttp()`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* provideSignalHttp({ baseUrl: 'https://api.example.com', timeout: 10_000 });
|
|
9
|
+
*/
|
|
10
|
+
interface SignalHttpConfig {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
interceptors?: HttpInterceptor[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Hooks that intercept every request, response, or error in the pipeline.
|
|
18
|
+
* All three hooks are optional and may return a `Promise`.
|
|
19
|
+
* Interceptors run in registration order.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const authInterceptor: HttpInterceptor = {
|
|
23
|
+
* request: async (config) => ({
|
|
24
|
+
* ...config,
|
|
25
|
+
* headers: { ...config.headers, Authorization: `Bearer ${getToken()}` },
|
|
26
|
+
* }),
|
|
27
|
+
* };
|
|
28
|
+
*/
|
|
29
|
+
interface HttpInterceptor {
|
|
30
|
+
request?: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;
|
|
31
|
+
response?: (response: Response) => Response | Promise<Response>;
|
|
32
|
+
error?: (error: Error) => Error | Promise<never>;
|
|
33
|
+
}
|
|
34
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
35
|
+
/**
|
|
36
|
+
* Per-request options. Passed to `SignalHttpClient.executeRequest()` or
|
|
37
|
+
* returned by a `UrlFactory` to provide method and body alongside the URL.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const config: RequestConfig = {
|
|
41
|
+
* url: '/users/1',
|
|
42
|
+
* method: 'GET',
|
|
43
|
+
* params: { expand: 'address' },
|
|
44
|
+
* };
|
|
45
|
+
*/
|
|
46
|
+
interface RequestConfig {
|
|
47
|
+
url: string;
|
|
48
|
+
method: HttpMethod;
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
body?: unknown;
|
|
51
|
+
params?: Record<string, string | number | boolean>;
|
|
52
|
+
/** Overrides the global timeout for this request. */
|
|
53
|
+
timeout?: number;
|
|
54
|
+
/** Merged with the internal `AbortController` signal. */
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fine-grained retry configuration for `querySignal`.
|
|
59
|
+
* `AbortError` is never retried regardless of this config.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const retry: RetryConfig = {
|
|
63
|
+
* count: 3,
|
|
64
|
+
* delay: (attempt) => 1000 * 2 ** (attempt - 1),
|
|
65
|
+
* shouldRetry: (err) => !(err instanceof HttpError && err.isClientError),
|
|
66
|
+
* };
|
|
67
|
+
*/
|
|
68
|
+
interface RetryConfig {
|
|
69
|
+
count: number;
|
|
70
|
+
delay?: number | ((attempt: number) => number);
|
|
71
|
+
shouldRetry?: (error: Error, attempt: number) => boolean;
|
|
72
|
+
}
|
|
73
|
+
type HttpClientStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
74
|
+
/**
|
|
75
|
+
* Options for `querySignal()`.
|
|
76
|
+
*
|
|
77
|
+
* @template T - The expected response data type.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* querySignal('/users', { retry: 3, refetchOnFocus: true, staleTime: 60_000 });
|
|
81
|
+
*/
|
|
82
|
+
interface HttpClientOptions<T> {
|
|
83
|
+
initialValue?: T;
|
|
84
|
+
lazy?: boolean;
|
|
85
|
+
retry?: number | RetryConfig;
|
|
86
|
+
staleTime?: number;
|
|
87
|
+
refetchInterval?: number;
|
|
88
|
+
refetchOnFocus?: boolean;
|
|
89
|
+
refetchOnReconnect?: boolean;
|
|
90
|
+
onSuccess?: (data: T) => void;
|
|
91
|
+
onError?: (error: Error) => void;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* State and control handle returned by `querySignal()`.
|
|
95
|
+
*
|
|
96
|
+
* @template T - The response data type.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* const result = querySignal<User>('/users/1');
|
|
100
|
+
* effect(() => console.log(result.status(), result.data()));
|
|
101
|
+
*/
|
|
102
|
+
interface HttpClientResult<T> {
|
|
103
|
+
readonly data: Signal<T | null>;
|
|
104
|
+
readonly loading: Signal<boolean>;
|
|
105
|
+
readonly error: Signal<Error | null>;
|
|
106
|
+
readonly status: Signal<HttpClientStatus>;
|
|
107
|
+
readonly isStale: Signal<boolean>;
|
|
108
|
+
refetch: () => Promise<void>;
|
|
109
|
+
invalidate: () => void;
|
|
110
|
+
reset: () => void;
|
|
111
|
+
}
|
|
112
|
+
type QueryStatus = HttpClientStatus;
|
|
113
|
+
type QueryResult<T> = HttpClientResult<T>;
|
|
114
|
+
type RequestOptions<T> = HttpClientOptions<T>;
|
|
115
|
+
/**
|
|
116
|
+
* Lifecycle callbacks for `mutationSignal()`.
|
|
117
|
+
*
|
|
118
|
+
* @template TInput - The input type passed to `mutate()`.
|
|
119
|
+
* @template TOutput - The response data type.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* mutationSignal(factory, { onSuccess: (data) => toast('Saved!') });
|
|
123
|
+
*/
|
|
124
|
+
interface MutationOptions<TInput, TOutput> {
|
|
125
|
+
onSuccess?: (data: TOutput, input: TInput) => void;
|
|
126
|
+
onError?: (error: Error, input: TInput) => void;
|
|
127
|
+
onSettled?: (data: TOutput | null, error: Error | null, input: TInput) => void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* State and control handle returned by `mutationSignal()`.
|
|
131
|
+
*
|
|
132
|
+
* @template TInput - The input type passed to `mutate()`.
|
|
133
|
+
* @template TOutput - The response data type.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const create = mutationSignal<NewUser, User>(factory);
|
|
137
|
+
* await create.mutate({ name: 'Alice' });
|
|
138
|
+
*/
|
|
139
|
+
interface MutationResult<TInput, TOutput> {
|
|
140
|
+
readonly isPending: Signal<boolean>;
|
|
141
|
+
readonly error: Signal<Error | null>;
|
|
142
|
+
readonly data: Signal<TOutput | null>;
|
|
143
|
+
mutate: (input: TInput) => Promise<TOutput>;
|
|
144
|
+
reset: () => void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Low-level HTTP service that wraps the native Fetch API.
|
|
149
|
+
* Prefer `querySignal` and `mutationSignal` for component use;
|
|
150
|
+
* use this directly in guards, resolvers, and async effects.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* const http = inject(SignalHttpClient);
|
|
154
|
+
* const user = await http.executeRequest<User>({ url: '/users/1', method: 'GET' });
|
|
155
|
+
*/
|
|
156
|
+
declare class SignalHttpClient {
|
|
157
|
+
private readonly config;
|
|
158
|
+
/**
|
|
159
|
+
* Fires a GET request and returns a read-only signal updated when the response arrives.
|
|
160
|
+
* Must be called from an injection context. The request is aborted on host destroy.
|
|
161
|
+
*
|
|
162
|
+
* @param url - Absolute or base-relative URL.
|
|
163
|
+
* @param options - Per-request overrides (headers, params, timeout, signal).
|
|
164
|
+
* @returns A read-only `Signal<T | null>` — `null` until the response arrives.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* readonly user = inject(SignalHttpClient).get<User>('/users/1');
|
|
168
|
+
*/
|
|
169
|
+
get<T>(url: string, options?: Partial<RequestConfig>): Signal<T | null>;
|
|
170
|
+
/**
|
|
171
|
+
* Fires a POST request and returns a read-only signal updated when the response arrives.
|
|
172
|
+
* Must be called from an injection context. The request is aborted on host destroy.
|
|
173
|
+
*
|
|
174
|
+
* @param url - Absolute or base-relative URL.
|
|
175
|
+
* @param body - Request body; serialised to JSON.
|
|
176
|
+
* @param options - Per-request overrides.
|
|
177
|
+
* @returns A read-only `Signal<T | null>` — `null` until the response arrives.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* readonly result = inject(SignalHttpClient).post<Token>('/auth/login', credentials);
|
|
181
|
+
*/
|
|
182
|
+
post<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>;
|
|
183
|
+
/**
|
|
184
|
+
* Fires a PUT request and returns a read-only signal updated when the response arrives.
|
|
185
|
+
* Must be called from an injection context. The request is aborted on host destroy.
|
|
186
|
+
*
|
|
187
|
+
* @param url - Absolute or base-relative URL.
|
|
188
|
+
* @param body - Request body; serialised to JSON.
|
|
189
|
+
* @param options - Per-request overrides.
|
|
190
|
+
* @returns A read-only `Signal<T | null>` — `null` until the response arrives.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* readonly updated = inject(SignalHttpClient).put<User>('/users/1', { name: 'Alice' });
|
|
194
|
+
*/
|
|
195
|
+
put<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>;
|
|
196
|
+
/**
|
|
197
|
+
* Fires a PATCH request and returns a read-only signal updated when the response arrives.
|
|
198
|
+
* Must be called from an injection context. The request is aborted on host destroy.
|
|
199
|
+
*
|
|
200
|
+
* @param url - Absolute or base-relative URL.
|
|
201
|
+
* @param body - Partial request body; serialised to JSON.
|
|
202
|
+
* @param options - Per-request overrides.
|
|
203
|
+
* @returns A read-only `Signal<T | null>` — `null` until the response arrives.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* readonly patched = inject(SignalHttpClient).patch<User>('/users/1', { email: 'new@example.com' });
|
|
207
|
+
*/
|
|
208
|
+
patch<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>;
|
|
209
|
+
/**
|
|
210
|
+
* Fires a DELETE request and returns a read-only signal updated when the response arrives.
|
|
211
|
+
* Must be called from an injection context. The request is aborted on host destroy.
|
|
212
|
+
*
|
|
213
|
+
* @param url - Absolute or base-relative URL.
|
|
214
|
+
* @param options - Per-request overrides.
|
|
215
|
+
* @returns A read-only `Signal<T | null>` — `null` until the response arrives.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* readonly deleted = inject(SignalHttpClient).delete<void>('/users/1');
|
|
219
|
+
*/
|
|
220
|
+
delete<T>(url: string, options?: Partial<RequestConfig>): Signal<T | null>;
|
|
221
|
+
/**
|
|
222
|
+
* Executes a raw HTTP request and returns a `Promise`.
|
|
223
|
+
* Use this in effects, event handlers, guards, and resolvers where async/await is preferred.
|
|
224
|
+
* Runs request → response → error interceptors in registration order.
|
|
225
|
+
* `AbortError` is re-thrown without passing through error interceptors.
|
|
226
|
+
*
|
|
227
|
+
* @param config - Full request configuration including method and URL.
|
|
228
|
+
* @returns A `Promise` that resolves with the parsed response body.
|
|
229
|
+
* @throws `HttpError` on non-2xx responses; `AbortError` on cancellation.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* const token = await http.executeRequest<Token>({ url: '/auth/login', method: 'POST', body: creds });
|
|
233
|
+
*/
|
|
234
|
+
executeRequest<T>(config: RequestConfig): Promise<T>;
|
|
235
|
+
private createSignal;
|
|
236
|
+
private buildUrl;
|
|
237
|
+
private buildHeaders;
|
|
238
|
+
private parseResponse;
|
|
239
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SignalHttpClient, never>;
|
|
240
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<SignalHttpClient>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
declare const SIGNAL_HTTP_CONFIG: InjectionToken<SignalHttpConfig>;
|
|
244
|
+
/**
|
|
245
|
+
* Registers `ng-signal-http` in an Angular application.
|
|
246
|
+
* Call once in `app.config.ts` inside `ApplicationConfig.providers`.
|
|
247
|
+
*
|
|
248
|
+
* @param config - Global HTTP configuration applied to all requests.
|
|
249
|
+
* @returns Angular `EnvironmentProviders` to add to your app config.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* export const appConfig: ApplicationConfig = {
|
|
253
|
+
* providers: [provideSignalHttp({ baseUrl: 'https://api.example.com' })],
|
|
254
|
+
* };
|
|
255
|
+
*/
|
|
256
|
+
declare function provideSignalHttp(config?: SignalHttpConfig): EnvironmentProviders;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Error thrown when an HTTP request receives a non-2xx response.
|
|
260
|
+
* Extends the native `Error` class and adds HTTP-specific helpers.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* effect(() => {
|
|
264
|
+
* const err = query.error();
|
|
265
|
+
* if (err instanceof HttpError && err.isUnauthorized) router.navigate(['/login']);
|
|
266
|
+
* });
|
|
267
|
+
*/
|
|
268
|
+
declare class HttpError extends Error {
|
|
269
|
+
readonly status: number;
|
|
270
|
+
readonly response?: Response | undefined;
|
|
271
|
+
/**
|
|
272
|
+
* @param message - Human-readable error description.
|
|
273
|
+
* @param status - HTTP status code (e.g. 404, 500).
|
|
274
|
+
* @param response - The raw `Response` object, if available.
|
|
275
|
+
*/
|
|
276
|
+
constructor(message: string, status: number, response?: Response | undefined);
|
|
277
|
+
get isClientError(): boolean;
|
|
278
|
+
get isServerError(): boolean;
|
|
279
|
+
get isTimeout(): boolean;
|
|
280
|
+
get isNotFound(): boolean;
|
|
281
|
+
get isUnauthorized(): boolean;
|
|
282
|
+
get isForbidden(): boolean;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
type UrlFactory = () => string | RequestConfig;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Creates a reactive GET query bound to the current Angular injection context.
|
|
289
|
+
*
|
|
290
|
+
* Fetches immediately (unless `lazy: true`) and re-fetches automatically whenever
|
|
291
|
+
* any signal read inside the `url` factory changes. The in-flight request is
|
|
292
|
+
* aborted when the host component or service is destroyed.
|
|
293
|
+
*
|
|
294
|
+
* @template T - The expected response data type.
|
|
295
|
+
* @param url - A static URL string or a factory that returns a URL or `RequestConfig`.
|
|
296
|
+
* Signal reads inside the factory are tracked — changing them triggers a re-fetch.
|
|
297
|
+
* @param options - Query behaviour: lazy loading, retry, stale time, polling, callbacks, etc.
|
|
298
|
+
* @returns An `HttpClientResult<T>` with reactive signals and control methods.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* // Static URL — fetch once on init
|
|
302
|
+
* const posts = querySignal<Post[]>('/posts');
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* // Reactive factory — refetches when postId() changes
|
|
306
|
+
* const postId = signal(1);
|
|
307
|
+
* const post = querySignal<Post>(() => `/posts/${postId()}`);
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* // With options
|
|
311
|
+
* const data = querySignal('/feed', {
|
|
312
|
+
* retry: 3,
|
|
313
|
+
* refetchOnFocus: true,
|
|
314
|
+
* staleTime: 60_000,
|
|
315
|
+
* onError: (err) => console.error(err),
|
|
316
|
+
* });
|
|
317
|
+
*/
|
|
318
|
+
declare function querySignal<T>(url: string | UrlFactory, options?: HttpClientOptions<T>): HttpClientResult<T>;
|
|
319
|
+
|
|
320
|
+
type MutationFactory<TInput> = (input: TInput) => RequestConfig;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Creates an imperative mutation bound to the current Angular injection context.
|
|
324
|
+
*
|
|
325
|
+
* Does nothing until `mutate(input)` is called. Calling `mutate()` while a previous
|
|
326
|
+
* request is in flight cancels the previous request automatically.
|
|
327
|
+
* The in-flight request is aborted when the host component or service is destroyed.
|
|
328
|
+
*
|
|
329
|
+
* @template TInput - The input type passed to `mutate()`.
|
|
330
|
+
* @template TOutput - The response data type returned by the server.
|
|
331
|
+
* @param requestFactory - Receives the mutation input and returns a `RequestConfig`.
|
|
332
|
+
* @param options - Optional lifecycle callbacks (`onSuccess`, `onError`, `onSettled`).
|
|
333
|
+
* @returns A `MutationResult<TInput, TOutput>` with reactive signals and a `mutate` trigger.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* const createPost = mutationSignal<NewPost, Post>(
|
|
337
|
+
* (input) => ({ url: '/posts', method: 'POST', body: input }),
|
|
338
|
+
* { onSuccess: (post) => console.log('Created:', post.id) },
|
|
339
|
+
* );
|
|
340
|
+
*
|
|
341
|
+
* // Trigger from an event handler:
|
|
342
|
+
* await createPost.mutate({ title: 'Hello', body: 'World', userId: 1 });
|
|
343
|
+
*/
|
|
344
|
+
declare function mutationSignal<TInput, TOutput>(requestFactory: MutationFactory<TInput>, options?: MutationOptions<TInput, TOutput>): MutationResult<TInput, TOutput>;
|
|
345
|
+
|
|
346
|
+
export { HttpError, SIGNAL_HTTP_CONFIG, SignalHttpClient, mutationSignal, provideSignalHttp, querySignal };
|
|
347
|
+
export type { HttpInterceptor, HttpMethod, MutationFactory, MutationOptions, MutationResult, QueryResult, QueryStatus, RequestConfig, RequestOptions, RetryConfig, SignalHttpConfig, UrlFactory };
|