@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.
Files changed (90) hide show
  1. package/README.md +194 -188
  2. package/dist/DependencyInjection.d.ts +45 -27
  3. package/dist/collections/LinkedList.d.ts +9 -8
  4. package/dist/collections/index.js +1 -1
  5. package/dist/collections/index.js.map +3 -3
  6. package/dist/collections/index.mjs +1 -1
  7. package/dist/collections/index.mjs.map +3 -3
  8. package/dist/di/index.js +1 -1
  9. package/dist/di/index.js.map +3 -3
  10. package/dist/di/index.mjs +1 -1
  11. package/dist/di/index.mjs.map +3 -3
  12. package/dist/elements/index.js +1 -1
  13. package/dist/elements/index.js.map +1 -1
  14. package/dist/errors.d.ts +20 -0
  15. package/dist/forms/FormValidator.d.ts +3 -22
  16. package/dist/forms/ValidationRules.d.ts +4 -6
  17. package/dist/forms/index.js +1 -1
  18. package/dist/forms/index.js.map +4 -4
  19. package/dist/forms/index.mjs +1 -1
  20. package/dist/forms/index.mjs.map +4 -4
  21. package/dist/forms/setFormData.d.ts +39 -1
  22. package/dist/html/TableRenderer.d.ts +1 -0
  23. package/dist/html/index.js +1 -1
  24. package/dist/html/index.js.map +3 -3
  25. package/dist/html/index.mjs +1 -1
  26. package/dist/html/index.mjs.map +3 -3
  27. package/dist/html/template.d.ts +4 -0
  28. package/dist/http/ServerSentEvents.d.ts +1 -1
  29. package/dist/http/SimpleWebSocket.d.ts +1 -1
  30. package/dist/http/http.d.ts +1 -0
  31. package/dist/http/index.js +1 -1
  32. package/dist/http/index.js.map +3 -3
  33. package/dist/http/index.mjs +1 -1
  34. package/dist/http/index.mjs.map +3 -3
  35. package/dist/i18n/icu.d.ts +1 -1
  36. package/dist/i18n/index.js +1 -1
  37. package/dist/i18n/index.js.map +2 -2
  38. package/dist/i18n/index.mjs +1 -1
  39. package/dist/i18n/index.mjs.map +2 -2
  40. package/dist/index.js +3 -3
  41. package/dist/index.js.map +3 -3
  42. package/dist/index.mjs +3 -3
  43. package/dist/index.mjs.map +3 -3
  44. package/dist/routing/NavigateRouteEvent.d.ts +4 -4
  45. package/dist/routing/index.js +3 -3
  46. package/dist/routing/index.js.map +3 -3
  47. package/dist/routing/index.mjs +3 -3
  48. package/dist/routing/index.mjs.map +3 -3
  49. package/dist/routing/navigation.d.ts +1 -1
  50. package/dist/routing/routeTargetRegistry.d.ts +1 -0
  51. package/dist/routing/types.d.ts +2 -1
  52. package/dist/templates/NodeTemplate.d.ts +3 -1
  53. package/dist/utils/index.d.ts +1 -1
  54. package/dist/utils/index.js +1 -1
  55. package/dist/utils/index.js.map +3 -3
  56. package/dist/utils/index.mjs +1 -1
  57. package/dist/utils/index.mjs.map +3 -3
  58. package/docs/Architecture.md +333 -333
  59. package/docs/DependencyInjection.md +277 -237
  60. package/docs/Errors.md +87 -87
  61. package/docs/GettingStarted.md +238 -231
  62. package/docs/Pipes.md +5 -5
  63. package/docs/Translations.md +167 -312
  64. package/docs/WhyRelaxjs.md +336 -336
  65. package/docs/api.json +93193 -0
  66. package/docs/elements/dom.md +102 -102
  67. package/docs/forms/creating-form-components.md +924 -924
  68. package/docs/forms/form-api.md +94 -94
  69. package/docs/forms/forms.md +99 -99
  70. package/docs/forms/patterns.md +311 -311
  71. package/docs/forms/reading-writing.md +465 -365
  72. package/docs/forms/validation.md +351 -351
  73. package/docs/html/TableRenderer.md +291 -291
  74. package/docs/html/html.md +175 -175
  75. package/docs/html/index.md +54 -54
  76. package/docs/html/template.md +422 -422
  77. package/docs/http/HttpClient.md +459 -459
  78. package/docs/http/ServerSentEvents.md +184 -184
  79. package/docs/http/index.md +109 -109
  80. package/docs/i18n/i18n.md +49 -4
  81. package/docs/i18n/intl-standard.md +178 -178
  82. package/docs/routing/RouteLink.md +98 -98
  83. package/docs/routing/Routing.md +332 -332
  84. package/docs/routing/layouts.md +207 -207
  85. package/docs/setup/bootstrapping.md +154 -0
  86. package/docs/setup/build-and-deploy.md +183 -0
  87. package/docs/setup/project-structure.md +170 -0
  88. package/docs/setup/vite.md +175 -0
  89. package/docs/utilities.md +143 -143
  90. package/package.json +4 -2
@@ -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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/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 'relaxjs/http';
450
-
451
- // WebSocket
452
- import {
453
- WebSocketClient,
454
- WebSocketOptions,
455
- WebSocketCodec,
456
- WebSocketAbstraction,
457
- WebSocketFactory
458
- } from 'relaxjs/http';
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
+ ```