@relax.js/core 1.0.3 → 1.0.5
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 +194 -188
- package/dist/DependencyInjection.d.ts +45 -27
- package/dist/collections/LinkedList.d.ts +9 -8
- package/dist/collections/index.js +1 -1
- package/dist/collections/index.js.map +3 -3
- package/dist/collections/index.mjs +1 -1
- package/dist/collections/index.mjs.map +3 -3
- package/dist/di/index.js +1 -1
- package/dist/di/index.js.map +3 -3
- package/dist/di/index.mjs +1 -1
- package/dist/di/index.mjs.map +3 -3
- package/dist/elements/index.js +1 -1
- package/dist/elements/index.js.map +1 -1
- package/dist/errors.d.ts +20 -0
- package/dist/forms/FormValidator.d.ts +3 -22
- package/dist/forms/ValidationRules.d.ts +4 -6
- package/dist/forms/index.js +1 -1
- package/dist/forms/index.js.map +4 -4
- package/dist/forms/index.mjs +1 -1
- package/dist/forms/index.mjs.map +4 -4
- package/dist/forms/setFormData.d.ts +39 -1
- package/dist/html/TableRenderer.d.ts +1 -0
- package/dist/html/index.js +1 -1
- package/dist/html/index.js.map +3 -3
- package/dist/html/index.mjs +1 -1
- package/dist/html/index.mjs.map +3 -3
- package/dist/html/template.d.ts +4 -0
- package/dist/http/ServerSentEvents.d.ts +1 -1
- package/dist/http/SimpleWebSocket.d.ts +1 -1
- package/dist/http/http.d.ts +1 -0
- package/dist/http/index.js +1 -1
- package/dist/http/index.js.map +3 -3
- package/dist/http/index.mjs +1 -1
- package/dist/http/index.mjs.map +3 -3
- package/dist/i18n/icu.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/index.js.map +2 -2
- package/dist/i18n/index.mjs +1 -1
- package/dist/i18n/index.mjs.map +2 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +3 -3
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +3 -3
- package/dist/routing/NavigateRouteEvent.d.ts +4 -4
- package/dist/routing/index.js +3 -3
- package/dist/routing/index.js.map +3 -3
- package/dist/routing/index.mjs +3 -3
- package/dist/routing/index.mjs.map +3 -3
- package/dist/routing/navigation.d.ts +1 -1
- package/dist/routing/routeTargetRegistry.d.ts +1 -0
- package/dist/routing/types.d.ts +2 -1
- package/dist/templates/NodeTemplate.d.ts +3 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +3 -3
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +3 -3
- package/docs/Architecture.md +333 -333
- package/docs/DependencyInjection.md +277 -237
- package/docs/Errors.md +87 -87
- package/docs/GettingStarted.md +238 -231
- package/docs/Pipes.md +5 -5
- package/docs/Translations.md +167 -312
- package/docs/WhyRelaxjs.md +336 -336
- package/docs/api.json +93193 -0
- package/docs/elements/dom.md +102 -102
- package/docs/forms/creating-form-components.md +924 -924
- package/docs/forms/form-api.md +94 -94
- package/docs/forms/forms.md +99 -99
- package/docs/forms/patterns.md +311 -311
- package/docs/forms/reading-writing.md +465 -365
- package/docs/forms/validation.md +351 -351
- package/docs/html/TableRenderer.md +291 -291
- package/docs/html/html.md +175 -175
- package/docs/html/index.md +54 -54
- package/docs/html/template.md +422 -422
- package/docs/http/HttpClient.md +459 -459
- package/docs/http/ServerSentEvents.md +184 -184
- package/docs/http/index.md +109 -109
- package/docs/i18n/i18n.md +49 -4
- package/docs/i18n/intl-standard.md +178 -178
- package/docs/routing/RouteLink.md +98 -98
- package/docs/routing/Routing.md +332 -332
- package/docs/routing/layouts.md +207 -207
- package/docs/setup/bootstrapping.md +154 -0
- package/docs/setup/build-and-deploy.md +183 -0
- package/docs/setup/project-structure.md +170 -0
- package/docs/setup/vite.md +175 -0
- package/docs/utilities.md +143 -143
- package/package.json +4 -2
package/docs/http/HttpClient.md
CHANGED
|
@@ -1,459 +1,459 @@
|
|
|
1
|
-
# HTTP Client
|
|
2
|
-
|
|
3
|
-
Type-safe HTTP module built on fetch() with automatic JWT handling.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { configure, get, post } from '
|
|
9
|
-
|
|
10
|
-
configure({ baseUrl: '/api/v1' });
|
|
11
|
-
|
|
12
|
-
// GET request
|
|
13
|
-
const response = await get('/users');
|
|
14
|
-
const users = response.as<User[]>();
|
|
15
|
-
|
|
16
|
-
// POST request
|
|
17
|
-
const result = await post('/users', JSON.stringify({ name: 'John' }));
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Configuration
|
|
21
|
-
|
|
22
|
-
Call `configure()` once at app startup to set defaults for all requests:
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
import { configure } from '
|
|
26
|
-
|
|
27
|
-
configure({
|
|
28
|
-
baseUrl: '/api/v1',
|
|
29
|
-
contentType: 'application/json',
|
|
30
|
-
bearerTokenName: 'authToken'
|
|
31
|
-
});
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
interface HttpOptions {
|
|
36
|
-
baseUrl?: string; // Base URL prepended to all requests
|
|
37
|
-
contentType?: string; // Default content type (default: 'application/json')
|
|
38
|
-
bearerTokenName?: string; // JWT token key in localStorage (default: 'jwt', null to disable)
|
|
39
|
-
timeout?: number; // Default request timeout in milliseconds
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Request Timeouts
|
|
44
|
-
|
|
45
|
-
Set a default timeout for all requests:
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
configure({
|
|
49
|
-
baseUrl: '/api',
|
|
50
|
-
timeout: 10000 // 10 seconds
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Requests that exceed the timeout are automatically aborted. Per-request signals override the default timeout:
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
// Custom timeout for a slow endpoint
|
|
58
|
-
await get('/reports/generate', null, {
|
|
59
|
-
signal: AbortSignal.timeout(60000)
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Manual abort control
|
|
63
|
-
const controller = new AbortController();
|
|
64
|
-
await get('/users', null, { signal: controller.signal });
|
|
65
|
-
controller.abort();
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## HTTP Methods
|
|
69
|
-
|
|
70
|
-
All methods are standalone functions. They return `Promise<HttpResponse>`.
|
|
71
|
-
|
|
72
|
-
### GET
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
import { get } from '
|
|
76
|
-
|
|
77
|
-
// Simple GET
|
|
78
|
-
const response = await get('/users');
|
|
79
|
-
|
|
80
|
-
// With query parameters
|
|
81
|
-
const filtered = await get('/users', {
|
|
82
|
-
status: 'active',
|
|
83
|
-
role: 'admin'
|
|
84
|
-
});
|
|
85
|
-
// Results in: /api/v1/users?status=active&role=admin
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### POST
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { post } from '
|
|
92
|
-
|
|
93
|
-
const user = { name: 'John', email: 'john@example.com' };
|
|
94
|
-
const response = await post('/users', JSON.stringify(user));
|
|
95
|
-
|
|
96
|
-
if (response.success) {
|
|
97
|
-
const created = response.as<User>();
|
|
98
|
-
console.log('Created user:', created.id);
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### PUT
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
import { put } from '
|
|
106
|
-
|
|
107
|
-
const updates = { name: 'John Updated' };
|
|
108
|
-
const response = await put('/users/123', JSON.stringify(updates));
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### DELETE
|
|
112
|
-
|
|
113
|
-
The function is named `del` (not `delete`, which is a reserved word):
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
import { del } from '
|
|
117
|
-
|
|
118
|
-
const response = await del('/users/123');
|
|
119
|
-
if (response.success) {
|
|
120
|
-
console.log('User deleted');
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Generic Request
|
|
125
|
-
|
|
126
|
-
Use `request()` for full control over the request:
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
import { request } from '
|
|
130
|
-
|
|
131
|
-
const response = await request('/users', {
|
|
132
|
-
method: 'POST',
|
|
133
|
-
headers: {
|
|
134
|
-
'Content-Type': 'application/json',
|
|
135
|
-
'X-Custom-Header': 'value'
|
|
136
|
-
},
|
|
137
|
-
body: JSON.stringify(data),
|
|
138
|
-
credentials: 'include'
|
|
139
|
-
});
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Response Handling
|
|
143
|
-
|
|
144
|
-
All methods return an `HttpResponse`:
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
interface HttpResponse {
|
|
148
|
-
success: boolean; // true for 2xx responses
|
|
149
|
-
statusCode: number; // HTTP status code
|
|
150
|
-
statusReason: string; // HTTP status text
|
|
151
|
-
contentType: string | null; // Response content type
|
|
152
|
-
body: unknown; // Parsed JSON body (success) or raw text (error)
|
|
153
|
-
charset: string | null; // Response charset
|
|
154
|
-
as<T>(): T; // Type-cast body (throws on error responses)
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Type-Safe Responses
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
interface User {
|
|
162
|
-
id: number;
|
|
163
|
-
name: string;
|
|
164
|
-
email: string;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const response = await get('/users/123');
|
|
168
|
-
|
|
169
|
-
if (response.success) {
|
|
170
|
-
const user = response.as<User>();
|
|
171
|
-
displayUser(user);
|
|
172
|
-
} else {
|
|
173
|
-
console.error(`Error ${response.statusCode}: ${response.body}`);
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### 204 No Content
|
|
178
|
-
|
|
179
|
-
Responses with status 204 return `null` as the body (no JSON parsing attempted).
|
|
180
|
-
|
|
181
|
-
## Authentication
|
|
182
|
-
|
|
183
|
-
JWT tokens are automatically read from localStorage and added as `Authorization: Bearer <token>`:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
// Login and store token
|
|
187
|
-
const loginResponse = await post('/auth/login', JSON.stringify({
|
|
188
|
-
username: 'user',
|
|
189
|
-
password: 'pass'
|
|
190
|
-
}));
|
|
191
|
-
|
|
192
|
-
if (loginResponse.success) {
|
|
193
|
-
const { token } = loginResponse.as<{ token: string }>();
|
|
194
|
-
localStorage.setItem('jwt', token);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// All subsequent requests include the Authorization header automatically
|
|
198
|
-
const protectedData = await get('/protected/resource');
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Disabling Auto-Auth
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
configure({
|
|
205
|
-
baseUrl: '/api/public',
|
|
206
|
-
bearerTokenName: null // Disable JWT handling
|
|
207
|
-
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Custom Token Name
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
configure({
|
|
214
|
-
bearerTokenName: 'auth_token' // Reads from localStorage.getItem('auth_token')
|
|
215
|
-
});
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Error Handling
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
import { get, HttpError } from '
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
const response = await get('/users/999');
|
|
225
|
-
|
|
226
|
-
if (!response.success) {
|
|
227
|
-
throw new HttpError(response);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return response.as<User>();
|
|
231
|
-
} catch (error) {
|
|
232
|
-
if (error instanceof HttpError) {
|
|
233
|
-
console.error(`HTTP ${error.response.statusCode}: ${error.message}`);
|
|
234
|
-
} else {
|
|
235
|
-
console.error('Network error:', error);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Testing
|
|
241
|
-
|
|
242
|
-
Replace the global fetch implementation for unit tests:
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
import { setFetch, get, configure } from '
|
|
246
|
-
|
|
247
|
-
// Mock fetch for tests
|
|
248
|
-
setFetch(async (url, options) => {
|
|
249
|
-
return new Response(JSON.stringify({ id: 1, name: 'Test User' }), {
|
|
250
|
-
status: 200,
|
|
251
|
-
headers: { 'content-type': 'application/json' }
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
configure({ baseUrl: '/api' });
|
|
256
|
-
const response = await get('/users/1');
|
|
257
|
-
const user = response.as<User>();
|
|
258
|
-
// user === { id: 1, name: 'Test User' }
|
|
259
|
-
|
|
260
|
-
// Restore real fetch
|
|
261
|
-
setFetch();
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
## WebSocket Client
|
|
265
|
-
|
|
266
|
-
Type-safe WebSocket client with automatic reconnection and message queuing.
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
import { WebSocketClient } from '
|
|
270
|
-
|
|
271
|
-
interface ChatMessage {
|
|
272
|
-
user: string;
|
|
273
|
-
text: string;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const ws = new WebSocketClient<ChatMessage>('wss://chat.example.com', {
|
|
277
|
-
autoReconnect: true,
|
|
278
|
-
reconnectDelay: 1000,
|
|
279
|
-
maxReconnectDelay: 30000,
|
|
280
|
-
onConnect: (socket) => console.log('Connected'),
|
|
281
|
-
onClose: (socket) => console.log('Disconnected')
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
ws.connect();
|
|
285
|
-
|
|
286
|
-
// Send messages (queued automatically if disconnected)
|
|
287
|
-
ws.send({ user: 'John', text: 'Hello!' });
|
|
288
|
-
|
|
289
|
-
// Receive messages
|
|
290
|
-
while (ws.connected) {
|
|
291
|
-
try {
|
|
292
|
-
const message = await ws.receive();
|
|
293
|
-
console.log(`${message.user}: ${message.text}`);
|
|
294
|
-
} catch (error) {
|
|
295
|
-
console.log('Connection closed');
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
ws.disconnect();
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Features
|
|
304
|
-
|
|
305
|
-
- **Auto-reconnect**: Automatically reconnects with exponential backoff (enabled by default)
|
|
306
|
-
- **Message queuing**: Messages sent while disconnected are queued and sent on reconnect
|
|
307
|
-
- **Type-safe**: Generic type parameter for message types
|
|
308
|
-
- **JSON by default**: Automatically serializes/deserializes JSON messages
|
|
309
|
-
- **Connection state**: Check `connected` property for current state
|
|
310
|
-
- **Graceful disconnect**: Call `disconnect()` to close without auto-reconnect
|
|
311
|
-
|
|
312
|
-
### WebSocket Options
|
|
313
|
-
|
|
314
|
-
```typescript
|
|
315
|
-
interface WebSocketOptions<TMessage> {
|
|
316
|
-
codec?: WebSocketCodec<TMessage>; // Custom message encoding
|
|
317
|
-
autoReconnect?: boolean; // Auto-reconnect (default: true)
|
|
318
|
-
reconnectDelay?: number; // Initial reconnect delay in ms (default: 1000)
|
|
319
|
-
maxReconnectDelay?: number; // Max reconnect delay in ms (default: 30000)
|
|
320
|
-
onConnect?: (socket: WebSocketClient<TMessage>) => void;
|
|
321
|
-
onClose?: (socket: WebSocketClient<TMessage>) => void;
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### Exponential Backoff
|
|
326
|
-
|
|
327
|
-
When auto-reconnect is enabled, the client uses exponential backoff:
|
|
328
|
-
|
|
329
|
-
| Attempt | Delay (with defaults) |
|
|
330
|
-
|---------|----------------------|
|
|
331
|
-
| 1 | 1s |
|
|
332
|
-
| 2 | 2s |
|
|
333
|
-
| 3 | 4s |
|
|
334
|
-
| 4 | 8s |
|
|
335
|
-
| 5 | 16s |
|
|
336
|
-
| 6+ | 30s (max) |
|
|
337
|
-
|
|
338
|
-
The delay resets to `reconnectDelay` after a successful connection.
|
|
339
|
-
|
|
340
|
-
### Custom Message Codec
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
interface WebSocketCodec<TMessage> {
|
|
344
|
-
encode(data: TMessage): string | ArrayBufferLike | Blob | ArrayBufferView;
|
|
345
|
-
decode(data: string | ArrayBufferLike | Blob | ArrayBufferView): TMessage;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const ws = new WebSocketClient<Message>('wss://api.example.com', {
|
|
349
|
-
codec: {
|
|
350
|
-
encode(msg: Message): string {
|
|
351
|
-
return msgpack.encode(msg);
|
|
352
|
-
},
|
|
353
|
-
decode(data: string): Message {
|
|
354
|
-
return msgpack.decode(data);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Receive Behavior
|
|
361
|
-
|
|
362
|
-
`receive()` returns a Promise that resolves when a message arrives. Only one `receive()` call can be active at a time:
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
// Correct: sequential receives
|
|
366
|
-
const msg1 = await ws.receive();
|
|
367
|
-
const msg2 = await ws.receive();
|
|
368
|
-
|
|
369
|
-
// Error: concurrent receives throw
|
|
370
|
-
const [msg1, msg2] = await Promise.all([ws.receive(), ws.receive()]); // Throws!
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
The promise rejects if the connection closes while waiting:
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
try {
|
|
377
|
-
const message = await ws.receive();
|
|
378
|
-
handleMessage(message);
|
|
379
|
-
} catch (error) {
|
|
380
|
-
// error.message === 'WebSocket connection closed'
|
|
381
|
-
console.log('Connection lost');
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### Testing WebSocket
|
|
386
|
-
|
|
387
|
-
Pass a factory function instead of a URL:
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
import { WebSocketClient, WebSocketAbstraction, WebSocketFactory } from '
|
|
391
|
-
|
|
392
|
-
const mockSocket: WebSocketAbstraction = {
|
|
393
|
-
onopen: null,
|
|
394
|
-
onerror: null,
|
|
395
|
-
onclose: null,
|
|
396
|
-
onmessage: null,
|
|
397
|
-
send: vi.fn(),
|
|
398
|
-
close: vi.fn()
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const factory: WebSocketFactory = () => mockSocket;
|
|
402
|
-
const ws = new WebSocketClient<Message>(factory);
|
|
403
|
-
ws.connect();
|
|
404
|
-
|
|
405
|
-
// Simulate server message
|
|
406
|
-
mockSocket.onmessage?.({ data: '{"text": "hello"}' });
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
## API Reference
|
|
410
|
-
|
|
411
|
-
### Functions
|
|
412
|
-
|
|
413
|
-
| Function | Description |
|
|
414
|
-
|----------|-------------|
|
|
415
|
-
| `configure(options)` | Set module-wide defaults (base URL, content type, JWT) |
|
|
416
|
-
| `get(url, queryString?, options?)` | GET request with optional query parameters |
|
|
417
|
-
| `post(url, body, options?)` | POST request with body |
|
|
418
|
-
| `put(url, body, options?)` | PUT request with body |
|
|
419
|
-
| `del(url, options?)` | DELETE request |
|
|
420
|
-
| `request(url, options?)` | Generic request with full RequestInit options |
|
|
421
|
-
| `setFetch(fn?)` | Replace fetch implementation for testing |
|
|
422
|
-
|
|
423
|
-
### WebSocketClient
|
|
424
|
-
|
|
425
|
-
| Method/Property | Description |
|
|
426
|
-
|-----------------|-------------|
|
|
427
|
-
| `connect()` | Establish WebSocket connection |
|
|
428
|
-
| `disconnect()` | Close connection without auto-reconnect |
|
|
429
|
-
| `send(data)` | Send message (queued if disconnected) |
|
|
430
|
-
| `receive()` | Receive next message (rejects on close) |
|
|
431
|
-
| `connected` | `boolean` - Current connection state |
|
|
432
|
-
|
|
433
|
-
### Exports
|
|
434
|
-
|
|
435
|
-
```typescript
|
|
436
|
-
// HTTP
|
|
437
|
-
import {
|
|
438
|
-
configure,
|
|
439
|
-
get,
|
|
440
|
-
post,
|
|
441
|
-
put,
|
|
442
|
-
del,
|
|
443
|
-
request,
|
|
444
|
-
setFetch,
|
|
445
|
-
HttpOptions,
|
|
446
|
-
HttpResponse,
|
|
447
|
-
HttpError,
|
|
448
|
-
RequestOptions
|
|
449
|
-
} from '
|
|
450
|
-
|
|
451
|
-
// WebSocket
|
|
452
|
-
import {
|
|
453
|
-
WebSocketClient,
|
|
454
|
-
WebSocketOptions,
|
|
455
|
-
WebSocketCodec,
|
|
456
|
-
WebSocketAbstraction,
|
|
457
|
-
WebSocketFactory
|
|
458
|
-
} from '
|
|
459
|
-
```
|
|
1
|
+
# HTTP Client
|
|
2
|
+
|
|
3
|
+
Type-safe HTTP module built on fetch() with automatic JWT handling.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { configure, get, post } from '@relax.js/core/http';
|
|
9
|
+
|
|
10
|
+
configure({ baseUrl: '/api/v1' });
|
|
11
|
+
|
|
12
|
+
// GET request
|
|
13
|
+
const response = await get('/users');
|
|
14
|
+
const users = response.as<User[]>();
|
|
15
|
+
|
|
16
|
+
// POST request
|
|
17
|
+
const result = await post('/users', JSON.stringify({ name: 'John' }));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
Call `configure()` once at app startup to set defaults for all requests:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { configure } from '@relax.js/core/http';
|
|
26
|
+
|
|
27
|
+
configure({
|
|
28
|
+
baseUrl: '/api/v1',
|
|
29
|
+
contentType: 'application/json',
|
|
30
|
+
bearerTokenName: 'authToken'
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
interface HttpOptions {
|
|
36
|
+
baseUrl?: string; // Base URL prepended to all requests
|
|
37
|
+
contentType?: string; // Default content type (default: 'application/json')
|
|
38
|
+
bearerTokenName?: string; // JWT token key in localStorage (default: 'jwt', null to disable)
|
|
39
|
+
timeout?: number; // Default request timeout in milliseconds
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Request Timeouts
|
|
44
|
+
|
|
45
|
+
Set a default timeout for all requests:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
configure({
|
|
49
|
+
baseUrl: '/api',
|
|
50
|
+
timeout: 10000 // 10 seconds
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requests that exceed the timeout are automatically aborted. Per-request signals override the default timeout:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Custom timeout for a slow endpoint
|
|
58
|
+
await get('/reports/generate', null, {
|
|
59
|
+
signal: AbortSignal.timeout(60000)
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Manual abort control
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
await get('/users', null, { signal: controller.signal });
|
|
65
|
+
controller.abort();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## HTTP Methods
|
|
69
|
+
|
|
70
|
+
All methods are standalone functions. They return `Promise<HttpResponse>`.
|
|
71
|
+
|
|
72
|
+
### GET
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { get } from '@relax.js/core/http';
|
|
76
|
+
|
|
77
|
+
// Simple GET
|
|
78
|
+
const response = await get('/users');
|
|
79
|
+
|
|
80
|
+
// With query parameters
|
|
81
|
+
const filtered = await get('/users', {
|
|
82
|
+
status: 'active',
|
|
83
|
+
role: 'admin'
|
|
84
|
+
});
|
|
85
|
+
// Results in: /api/v1/users?status=active&role=admin
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### POST
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { post } from '@relax.js/core/http';
|
|
92
|
+
|
|
93
|
+
const user = { name: 'John', email: 'john@example.com' };
|
|
94
|
+
const response = await post('/users', JSON.stringify(user));
|
|
95
|
+
|
|
96
|
+
if (response.success) {
|
|
97
|
+
const created = response.as<User>();
|
|
98
|
+
console.log('Created user:', created.id);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### PUT
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { put } from '@relax.js/core/http';
|
|
106
|
+
|
|
107
|
+
const updates = { name: 'John Updated' };
|
|
108
|
+
const response = await put('/users/123', JSON.stringify(updates));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### DELETE
|
|
112
|
+
|
|
113
|
+
The function is named `del` (not `delete`, which is a reserved word):
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { del } from '@relax.js/core/http';
|
|
117
|
+
|
|
118
|
+
const response = await del('/users/123');
|
|
119
|
+
if (response.success) {
|
|
120
|
+
console.log('User deleted');
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Generic Request
|
|
125
|
+
|
|
126
|
+
Use `request()` for full control over the request:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { request } from '@relax.js/core/http';
|
|
130
|
+
|
|
131
|
+
const response = await request('/users', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'X-Custom-Header': 'value'
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify(data),
|
|
138
|
+
credentials: 'include'
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Response Handling
|
|
143
|
+
|
|
144
|
+
All methods return an `HttpResponse`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface HttpResponse {
|
|
148
|
+
success: boolean; // true for 2xx responses
|
|
149
|
+
statusCode: number; // HTTP status code
|
|
150
|
+
statusReason: string; // HTTP status text
|
|
151
|
+
contentType: string | null; // Response content type
|
|
152
|
+
body: unknown; // Parsed JSON body (success) or raw text (error)
|
|
153
|
+
charset: string | null; // Response charset
|
|
154
|
+
as<T>(): T; // Type-cast body (throws on error responses)
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Type-Safe Responses
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface User {
|
|
162
|
+
id: number;
|
|
163
|
+
name: string;
|
|
164
|
+
email: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const response = await get('/users/123');
|
|
168
|
+
|
|
169
|
+
if (response.success) {
|
|
170
|
+
const user = response.as<User>();
|
|
171
|
+
displayUser(user);
|
|
172
|
+
} else {
|
|
173
|
+
console.error(`Error ${response.statusCode}: ${response.body}`);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 204 No Content
|
|
178
|
+
|
|
179
|
+
Responses with status 204 return `null` as the body (no JSON parsing attempted).
|
|
180
|
+
|
|
181
|
+
## Authentication
|
|
182
|
+
|
|
183
|
+
JWT tokens are automatically read from localStorage and added as `Authorization: Bearer <token>`:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// Login and store token
|
|
187
|
+
const loginResponse = await post('/auth/login', JSON.stringify({
|
|
188
|
+
username: 'user',
|
|
189
|
+
password: 'pass'
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
if (loginResponse.success) {
|
|
193
|
+
const { token } = loginResponse.as<{ token: string }>();
|
|
194
|
+
localStorage.setItem('jwt', token);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// All subsequent requests include the Authorization header automatically
|
|
198
|
+
const protectedData = await get('/protected/resource');
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Disabling Auto-Auth
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
configure({
|
|
205
|
+
baseUrl: '/api/public',
|
|
206
|
+
bearerTokenName: null // Disable JWT handling
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Custom Token Name
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
configure({
|
|
214
|
+
bearerTokenName: 'auth_token' // Reads from localStorage.getItem('auth_token')
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Error Handling
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { get, HttpError } from '@relax.js/core/http';
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const response = await get('/users/999');
|
|
225
|
+
|
|
226
|
+
if (!response.success) {
|
|
227
|
+
throw new HttpError(response);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return response.as<User>();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof HttpError) {
|
|
233
|
+
console.error(`HTTP ${error.response.statusCode}: ${error.message}`);
|
|
234
|
+
} else {
|
|
235
|
+
console.error('Network error:', error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Testing
|
|
241
|
+
|
|
242
|
+
Replace the global fetch implementation for unit tests:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { setFetch, get, configure } from '@relax.js/core/http';
|
|
246
|
+
|
|
247
|
+
// Mock fetch for tests
|
|
248
|
+
setFetch(async (url, options) => {
|
|
249
|
+
return new Response(JSON.stringify({ id: 1, name: 'Test User' }), {
|
|
250
|
+
status: 200,
|
|
251
|
+
headers: { 'content-type': 'application/json' }
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
configure({ baseUrl: '/api' });
|
|
256
|
+
const response = await get('/users/1');
|
|
257
|
+
const user = response.as<User>();
|
|
258
|
+
// user === { id: 1, name: 'Test User' }
|
|
259
|
+
|
|
260
|
+
// Restore real fetch
|
|
261
|
+
setFetch();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## WebSocket Client
|
|
265
|
+
|
|
266
|
+
Type-safe WebSocket client with automatic reconnection and message queuing.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { WebSocketClient } from '@relax.js/core/http';
|
|
270
|
+
|
|
271
|
+
interface ChatMessage {
|
|
272
|
+
user: string;
|
|
273
|
+
text: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const ws = new WebSocketClient<ChatMessage>('wss://chat.example.com', {
|
|
277
|
+
autoReconnect: true,
|
|
278
|
+
reconnectDelay: 1000,
|
|
279
|
+
maxReconnectDelay: 30000,
|
|
280
|
+
onConnect: (socket) => console.log('Connected'),
|
|
281
|
+
onClose: (socket) => console.log('Disconnected')
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
ws.connect();
|
|
285
|
+
|
|
286
|
+
// Send messages (queued automatically if disconnected)
|
|
287
|
+
ws.send({ user: 'John', text: 'Hello!' });
|
|
288
|
+
|
|
289
|
+
// Receive messages
|
|
290
|
+
while (ws.connected) {
|
|
291
|
+
try {
|
|
292
|
+
const message = await ws.receive();
|
|
293
|
+
console.log(`${message.user}: ${message.text}`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.log('Connection closed');
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
ws.disconnect();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Features
|
|
304
|
+
|
|
305
|
+
- **Auto-reconnect**: Automatically reconnects with exponential backoff (enabled by default)
|
|
306
|
+
- **Message queuing**: Messages sent while disconnected are queued and sent on reconnect
|
|
307
|
+
- **Type-safe**: Generic type parameter for message types
|
|
308
|
+
- **JSON by default**: Automatically serializes/deserializes JSON messages
|
|
309
|
+
- **Connection state**: Check `connected` property for current state
|
|
310
|
+
- **Graceful disconnect**: Call `disconnect()` to close without auto-reconnect
|
|
311
|
+
|
|
312
|
+
### WebSocket Options
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
interface WebSocketOptions<TMessage> {
|
|
316
|
+
codec?: WebSocketCodec<TMessage>; // Custom message encoding
|
|
317
|
+
autoReconnect?: boolean; // Auto-reconnect (default: true)
|
|
318
|
+
reconnectDelay?: number; // Initial reconnect delay in ms (default: 1000)
|
|
319
|
+
maxReconnectDelay?: number; // Max reconnect delay in ms (default: 30000)
|
|
320
|
+
onConnect?: (socket: WebSocketClient<TMessage>) => void;
|
|
321
|
+
onClose?: (socket: WebSocketClient<TMessage>) => void;
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Exponential Backoff
|
|
326
|
+
|
|
327
|
+
When auto-reconnect is enabled, the client uses exponential backoff:
|
|
328
|
+
|
|
329
|
+
| Attempt | Delay (with defaults) |
|
|
330
|
+
|---------|----------------------|
|
|
331
|
+
| 1 | 1s |
|
|
332
|
+
| 2 | 2s |
|
|
333
|
+
| 3 | 4s |
|
|
334
|
+
| 4 | 8s |
|
|
335
|
+
| 5 | 16s |
|
|
336
|
+
| 6+ | 30s (max) |
|
|
337
|
+
|
|
338
|
+
The delay resets to `reconnectDelay` after a successful connection.
|
|
339
|
+
|
|
340
|
+
### Custom Message Codec
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
interface WebSocketCodec<TMessage> {
|
|
344
|
+
encode(data: TMessage): string | ArrayBufferLike | Blob | ArrayBufferView;
|
|
345
|
+
decode(data: string | ArrayBufferLike | Blob | ArrayBufferView): TMessage;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const ws = new WebSocketClient<Message>('wss://api.example.com', {
|
|
349
|
+
codec: {
|
|
350
|
+
encode(msg: Message): string {
|
|
351
|
+
return msgpack.encode(msg);
|
|
352
|
+
},
|
|
353
|
+
decode(data: string): Message {
|
|
354
|
+
return msgpack.decode(data);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Receive Behavior
|
|
361
|
+
|
|
362
|
+
`receive()` returns a Promise that resolves when a message arrives. Only one `receive()` call can be active at a time:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Correct: sequential receives
|
|
366
|
+
const msg1 = await ws.receive();
|
|
367
|
+
const msg2 = await ws.receive();
|
|
368
|
+
|
|
369
|
+
// Error: concurrent receives throw
|
|
370
|
+
const [msg1, msg2] = await Promise.all([ws.receive(), ws.receive()]); // Throws!
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The promise rejects if the connection closes while waiting:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
try {
|
|
377
|
+
const message = await ws.receive();
|
|
378
|
+
handleMessage(message);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
// error.message === 'WebSocket connection closed'
|
|
381
|
+
console.log('Connection lost');
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Testing WebSocket
|
|
386
|
+
|
|
387
|
+
Pass a factory function instead of a URL:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { WebSocketClient, WebSocketAbstraction, WebSocketFactory } from '@relax.js/core/http';
|
|
391
|
+
|
|
392
|
+
const mockSocket: WebSocketAbstraction = {
|
|
393
|
+
onopen: null,
|
|
394
|
+
onerror: null,
|
|
395
|
+
onclose: null,
|
|
396
|
+
onmessage: null,
|
|
397
|
+
send: vi.fn(),
|
|
398
|
+
close: vi.fn()
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const factory: WebSocketFactory = () => mockSocket;
|
|
402
|
+
const ws = new WebSocketClient<Message>(factory);
|
|
403
|
+
ws.connect();
|
|
404
|
+
|
|
405
|
+
// Simulate server message
|
|
406
|
+
mockSocket.onmessage?.({ data: '{"text": "hello"}' });
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## API Reference
|
|
410
|
+
|
|
411
|
+
### Functions
|
|
412
|
+
|
|
413
|
+
| Function | Description |
|
|
414
|
+
|----------|-------------|
|
|
415
|
+
| `configure(options)` | Set module-wide defaults (base URL, content type, JWT) |
|
|
416
|
+
| `get(url, queryString?, options?)` | GET request with optional query parameters |
|
|
417
|
+
| `post(url, body, options?)` | POST request with body |
|
|
418
|
+
| `put(url, body, options?)` | PUT request with body |
|
|
419
|
+
| `del(url, options?)` | DELETE request |
|
|
420
|
+
| `request(url, options?)` | Generic request with full RequestInit options |
|
|
421
|
+
| `setFetch(fn?)` | Replace fetch implementation for testing |
|
|
422
|
+
|
|
423
|
+
### WebSocketClient
|
|
424
|
+
|
|
425
|
+
| Method/Property | Description |
|
|
426
|
+
|-----------------|-------------|
|
|
427
|
+
| `connect()` | Establish WebSocket connection |
|
|
428
|
+
| `disconnect()` | Close connection without auto-reconnect |
|
|
429
|
+
| `send(data)` | Send message (queued if disconnected) |
|
|
430
|
+
| `receive()` | Receive next message (rejects on close) |
|
|
431
|
+
| `connected` | `boolean` - Current connection state |
|
|
432
|
+
|
|
433
|
+
### Exports
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// HTTP
|
|
437
|
+
import {
|
|
438
|
+
configure,
|
|
439
|
+
get,
|
|
440
|
+
post,
|
|
441
|
+
put,
|
|
442
|
+
del,
|
|
443
|
+
request,
|
|
444
|
+
setFetch,
|
|
445
|
+
HttpOptions,
|
|
446
|
+
HttpResponse,
|
|
447
|
+
HttpError,
|
|
448
|
+
RequestOptions
|
|
449
|
+
} from '@relax.js/core/http';
|
|
450
|
+
|
|
451
|
+
// WebSocket
|
|
452
|
+
import {
|
|
453
|
+
WebSocketClient,
|
|
454
|
+
WebSocketOptions,
|
|
455
|
+
WebSocketCodec,
|
|
456
|
+
WebSocketAbstraction,
|
|
457
|
+
WebSocketFactory
|
|
458
|
+
} from '@relax.js/core/http';
|
|
459
|
+
```
|