aspi 1.1.0-beta.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/dist/index.js ADDED
@@ -0,0 +1,1210 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/http.ts
8
+ var httpErrors = {
9
+ BAD_REQUEST: 400,
10
+ UNAUTHORIZED: 401,
11
+ FORBIDDEN: 403,
12
+ NOT_FOUND: 404,
13
+ METHOD_NOT_ALLOWED: 405,
14
+ CONFLICT: 409,
15
+ PAYLOAD_TOO_LARGE: 413,
16
+ UNSUPPORTED_MEDIA_TYPE: 415,
17
+ UNPROCESSABLE_ENTITY: 422,
18
+ TOO_MANY_REQUESTS: 429,
19
+ INTERNAL_SERVER_ERROR: 500,
20
+ NOT_IMPLEMENTED: 501,
21
+ BAD_GATEWAY: 502,
22
+ SERVICE_UNAVAILABLE: 503,
23
+ GATEWAY_TIMEOUT: 504
24
+ };
25
+ var getHttpErrorStatus = (status) => {
26
+ return Object.keys(httpErrors).find(
27
+ (key) => httpErrors[key] === status
28
+ );
29
+ };
30
+
31
+ // src/error.ts
32
+ var AspiError = class extends Error {
33
+ tag = "aspiError";
34
+ request;
35
+ response;
36
+ /**
37
+ * Creates an instance of AspiError
38
+ * @param {string} message - The error message
39
+ * @param {AspiRe} request - The request configuration
40
+ * @param {AspiResponse} response - The error response
41
+ */
42
+ constructor(message, request, response) {
43
+ super(message);
44
+ this.request = request;
45
+ this.response = response;
46
+ }
47
+ /**
48
+ * Conditionally executes callback if status matches
49
+ * @template T
50
+ * @param {HttpErrorStatus} status - The status to match against
51
+ * @param {(args: { request: AspiRequest; response: AspiResponse }) => T} cb - Callback to execute on match
52
+ * @returns {T | undefined} Result of callback if status matches, undefined otherwise
53
+ */
54
+ ifMatch(status, cb) {
55
+ if (this.response.statusText === status) {
56
+ return cb({
57
+ request: this.request,
58
+ response: this.response
59
+ });
60
+ }
61
+ }
62
+ };
63
+ var CustomError = class extends Error {
64
+ tag;
65
+ data;
66
+ /**
67
+ * Creates an instance of CustomError
68
+ * @param {Tag} tag - Tag identifying the error type
69
+ * @param {A} data - Additional error data
70
+ */
71
+ constructor(tag, data) {
72
+ super(tag);
73
+ this.tag = tag;
74
+ this.data = data;
75
+ }
76
+ };
77
+
78
+ // src/result.ts
79
+ var result_exports = {};
80
+ __export(result_exports, {
81
+ catchAllErrors: () => catchAllErrors,
82
+ catchError: () => catchError,
83
+ catchErrors: () => catchErrors,
84
+ err: () => err,
85
+ getErrorOrNull: () => getErrorOrNull,
86
+ getOrElse: () => getOrElse,
87
+ getOrNull: () => getOrNull,
88
+ getOrThrow: () => getOrThrow,
89
+ getOrThrowWith: () => getOrThrowWith,
90
+ isErr: () => isErr,
91
+ isOk: () => isOk,
92
+ map: () => map,
93
+ mapErr: () => mapErr,
94
+ match: () => match,
95
+ ok: () => ok,
96
+ pipe: () => pipe
97
+ });
98
+ var ok = (value) => ({ __tag: "ok", value });
99
+ var err = (error) => ({ __tag: "err", error });
100
+ var isOk = (result) => result.__tag === "ok";
101
+ var isErr = (result) => !isOk(result);
102
+ function map(selfOrFn, fn) {
103
+ if (fn) {
104
+ const self = selfOrFn;
105
+ return isOk(self) ? ok(fn(self.value)) : self;
106
+ } else {
107
+ const mappingFn = selfOrFn;
108
+ return (self) => isOk(self) ? ok(mappingFn(self.value)) : self;
109
+ }
110
+ }
111
+ function mapErr(selfOrFn, fn) {
112
+ if (fn) {
113
+ const self = selfOrFn;
114
+ return isErr(self) ? err(fn(self.error)) : self;
115
+ } else {
116
+ const mappingFn = selfOrFn;
117
+ return (self) => isErr(self) ? err(mappingFn(self.error)) : self;
118
+ }
119
+ }
120
+ function match(selfOrHandlers, handlers) {
121
+ if (handlers) {
122
+ const self = selfOrHandlers;
123
+ return isOk(self) ? handlers.onOk(self.value) : handlers.onErr(self.error);
124
+ } else {
125
+ const handlersObj = selfOrHandlers;
126
+ return (self) => isOk(self) ? handlersObj.onOk(self.value) : handlersObj.onErr(self.error);
127
+ }
128
+ }
129
+ var getOrNull = (result) => {
130
+ if (isOk(result)) {
131
+ return result.value;
132
+ } else {
133
+ return null;
134
+ }
135
+ };
136
+ var getErrorOrNull = (result) => {
137
+ if (isErr(result)) {
138
+ return result.error;
139
+ } else {
140
+ return null;
141
+ }
142
+ };
143
+ var getOrElse = (result, fallback) => isOk(result) ? result.value : fallback;
144
+ var getOrThrow = (result) => {
145
+ if (isOk(result)) {
146
+ return result.value;
147
+ } else {
148
+ throw result.error;
149
+ }
150
+ };
151
+ function getOrThrowWith(selfOrFn, fn) {
152
+ if (fn) {
153
+ const self = selfOrFn;
154
+ if (isOk(self)) {
155
+ return self.value;
156
+ } else {
157
+ throw fn(self.error);
158
+ }
159
+ } else {
160
+ const throwFn = selfOrFn;
161
+ return (self) => {
162
+ if (isOk(self)) {
163
+ return self.value;
164
+ } else {
165
+ throw throwFn(self.error);
166
+ }
167
+ };
168
+ }
169
+ }
170
+ function catchError(resultOrErrorOrTag, tagOrFn, fnOrUndefined) {
171
+ if (typeof resultOrErrorOrTag === "string") {
172
+ const tag = resultOrErrorOrTag;
173
+ const fn = tagOrFn;
174
+ return (resultOrError) => {
175
+ if ("__tag" in resultOrError) {
176
+ const result = resultOrError;
177
+ if (isErr(result) && result.error.tag === tag) {
178
+ fn(result.error);
179
+ return ok(null);
180
+ }
181
+ return result;
182
+ }
183
+ const error = resultOrError;
184
+ if (error.tag === tag) {
185
+ fn(error);
186
+ }
187
+ };
188
+ } else {
189
+ const resultOrError = resultOrErrorOrTag;
190
+ const tag = tagOrFn;
191
+ const fn = fnOrUndefined;
192
+ if ("__tag" in resultOrError) {
193
+ const result = resultOrError;
194
+ if (isErr(result) && result.error.tag === tag) {
195
+ fn(result.error);
196
+ return ok(null);
197
+ }
198
+ return result;
199
+ }
200
+ const error = resultOrError;
201
+ if (error.tag === tag) {
202
+ fn(error);
203
+ }
204
+ }
205
+ }
206
+ function catchAllErrors(resultOrFn, fnOrUndefined) {
207
+ if ("__tag" in resultOrFn) {
208
+ const result = resultOrFn;
209
+ if (isErr(result)) {
210
+ fnOrUndefined(result.error);
211
+ }
212
+ return result;
213
+ }
214
+ return (result) => {
215
+ if (isErr(result)) {
216
+ fnOrUndefined(result.error);
217
+ }
218
+ return result;
219
+ };
220
+ }
221
+ function catchErrors(resultOrHandlers, handlersOrUndefined) {
222
+ if ("__tag" in resultOrHandlers) {
223
+ const result = resultOrHandlers;
224
+ if (isErr(result)) {
225
+ const handler = handlersOrUndefined[result.error.tag];
226
+ if (handler) {
227
+ handler(result.error);
228
+ }
229
+ }
230
+ return result;
231
+ } else {
232
+ return (result) => {
233
+ if (isErr(result)) {
234
+ const handler = handlersOrUndefined[result.error.tag];
235
+ if (handler) {
236
+ handler(result.error);
237
+ }
238
+ }
239
+ return result;
240
+ };
241
+ }
242
+ }
243
+ var pipe = (result, ab) => {
244
+ function run(a) {
245
+ return ab(a);
246
+ }
247
+ run.pipe = (bc) => pipe(result, (a) => bc(ab(a)));
248
+ run.execute = () => run(result);
249
+ return run;
250
+ };
251
+
252
+ // src/request.ts
253
+ var Request = class {
254
+ #path;
255
+ #localRequestInit;
256
+ #customErrorCbs = {};
257
+ #queryParams;
258
+ #middlewares;
259
+ #schema = null;
260
+ #bodySchema = null;
261
+ #retryConfig;
262
+ #shouldBeResult = false;
263
+ #bodySchemaIssues = [];
264
+ constructor(method, path, {
265
+ requestConfig,
266
+ retryConfig,
267
+ middlewares,
268
+ errorCbs
269
+ }) {
270
+ this.#path = path;
271
+ this.#middlewares = middlewares || [];
272
+ this.#localRequestInit = { ...requestConfig, method };
273
+ this.#retryConfig = retryConfig;
274
+ this.#customErrorCbs = errorCbs || {};
275
+ }
276
+ /**
277
+ * Sets the base URL for the request.
278
+ * @param url The base URL to set
279
+ * @returns The request instance for chaining
280
+ * @example
281
+ * const request = new Request('/users', config);
282
+ * request.setBaseUrl('https://api.example.com');
283
+ */
284
+ setBaseUrl(url) {
285
+ this.#localRequestInit.baseUrl = url;
286
+ return this;
287
+ }
288
+ /**
289
+ * Sets the retry configuration for the request.
290
+ * @param retry The retry configuration object
291
+ * @returns The request instance for chaining
292
+ * @example
293
+ * const request = new Request('/users', config);
294
+ * request.setRetry({
295
+ * retries: 3, // Number of retry attempts
296
+ * retryDelay: 1000, // Delay between retries in ms
297
+ * retryOn: [500, 502, 503], // Status codes to retry on
298
+ * retryWhile: (request, response) => response.status >= 500 // Custom retry condition
299
+ * });
300
+ */
301
+ setRetry(retry) {
302
+ this.#retryConfig = {
303
+ ...this.#retryConfig,
304
+ ...retry
305
+ };
306
+ return this;
307
+ }
308
+ /**
309
+ * Sets multiple headers for the request.
310
+ * @param headers An object containing header key-value pairs
311
+ * @returns The request instance for chaining
312
+ * @example
313
+ * const request = new Request('/users', config);
314
+ * request.setHeaders({
315
+ * 'Content-Type': 'application/json',
316
+ * 'Accept': 'application/json'
317
+ * });
318
+ */
319
+ setHeaders(headers) {
320
+ this.#localRequestInit.headers = headers;
321
+ return this;
322
+ }
323
+ /**
324
+ * Sets a single header for the request.
325
+ * @param key The header key
326
+ * @param value The header value
327
+ * @returns The request instance for chaining
328
+ * @example
329
+ * const request = new Request('/users', config);
330
+ * request.setHeader('Content-Type', 'application/json');
331
+ */
332
+ setHeader(key, value) {
333
+ this.#localRequestInit.headers = {
334
+ ...this.#localRequestInit.headers,
335
+ [key]: value
336
+ };
337
+ return this;
338
+ }
339
+ /**
340
+ * Sets the Authorization header with a bearer token.
341
+ * @param token The bearer token to set
342
+ * @returns The request instance for chaining
343
+ * @example
344
+ * const request = new Request('/users', config);
345
+ * request.setBearer('my-auth-token');
346
+ */
347
+ setBearer(token) {
348
+ return this.setHeader("Authorization", `Bearer ${token}`);
349
+ }
350
+ /**
351
+ * Sets a validation schema for the request body using a StandardSchemaV1 schema.
352
+ * @template TSchema Type parameter extending StandardSchemaV1
353
+ * @param schema The schema to validate the body against
354
+ * @returns The request instance for chaining with updated error type
355
+ * @example
356
+ * const userSchema = z.object({
357
+ * name: z.string(),
358
+ * email: z.string().email()
359
+ * });
360
+ *
361
+ * const request = new Request('/users', config);
362
+ * request
363
+ * .bodySchema(userSchema)
364
+ * .bodyJson({
365
+ * name: 'John',
366
+ * email: 'john@example.com'
367
+ * });
368
+ */
369
+ bodySchema(schema) {
370
+ this.#bodySchema = schema;
371
+ return this;
372
+ }
373
+ /**
374
+ * Sets the request body as JSON by automatically stringifying the provided object.
375
+ * @param body The object to be stringified and sent as the request body
376
+ * @returns The request instance for chaining
377
+ * @example
378
+ * const request = new Request('/users', config);
379
+ * request.bodyJson({
380
+ * name: 'John Doe',
381
+ * email: 'john@example.com',
382
+ * age: 30
383
+ * });
384
+ */
385
+ bodyJson(body) {
386
+ if (this.#bodySchema) {
387
+ const data = this.#bodySchema["~standard"].validate(body);
388
+ if (data instanceof Promise) {
389
+ throw new Error("Schema validation should not return a promise");
390
+ }
391
+ if (data.issues) {
392
+ this.#bodySchemaIssues = data.issues;
393
+ } else {
394
+ this.#localRequestInit.body = JSON.stringify(body);
395
+ }
396
+ }
397
+ return this;
398
+ }
399
+ /**
400
+ * Sets the raw request body (unsafe, use with caution).
401
+ * @param body The body content to send with the request
402
+ * @returns The request instance for chaining
403
+ * @example
404
+ * const request = new Request('/users', config);
405
+ * request.unsafeBody(new FormData());
406
+ *
407
+ * // or with raw text
408
+ * request.unsafeBody('Hello World');
409
+ *
410
+ * // or with URLSearchParams
411
+ * request.unsafeBody(new URLSearchParams({ key: 'value' }));
412
+ */
413
+ unsafeBody(body) {
414
+ this.#localRequestInit.body = body;
415
+ return this;
416
+ }
417
+ /**
418
+ * Handles 404 Not Found errors with a custom callback.
419
+ * @param cb The callback function to handle the error
420
+ * @returns The request instance for chaining
421
+ * @example
422
+ * const request = new Request('/users', config);
423
+ * request.notFound((error) => {
424
+ * console.log('Resource not found:', error);
425
+ * return { message: 'Custom not found message' };
426
+ * });
427
+ */
428
+ notFound(cb) {
429
+ return this.error("notFoundError", "NOT_FOUND", cb);
430
+ }
431
+ /**
432
+ * Handles 429 Too Many Requests errors with a custom callback.
433
+ * @param cb The callback function to handle the error
434
+ * @returns The request instance for chaining
435
+ * @example
436
+ * const request = new Request('/users', config);
437
+ * request.tooManyRequests((error) => {
438
+ * console.log('Rate limited:', error);
439
+ * return { message: 'Please try again later' };
440
+ * });
441
+ */
442
+ tooManyRequests(cb) {
443
+ return this.error("tooManyRequestsError", "TOO_MANY_REQUESTS", cb);
444
+ }
445
+ /**
446
+ * Handles 400 Bad Request errors with a custom callback.
447
+ * @param cb The callback function to handle the error
448
+ * @returns The request instance for chaining
449
+ * @example
450
+ * const request = new Request('/users', config);
451
+ * request.badRequest((error) => {
452
+ * console.log('Bad request error:', error);
453
+ * return { message: 'Invalid request parameters' };
454
+ * });
455
+ */
456
+ badRequest(cb) {
457
+ return this.error("badRequestError", "BAD_REQUEST", cb);
458
+ }
459
+ /**
460
+ * Handles 409 Conflict errors with a custom callback.
461
+ * @param cb The callback function to handle the error
462
+ * @returns The request instance for chaining
463
+ * @example
464
+ * const request = new Request('/users', config);
465
+ * request.conflict((error) => {
466
+ * console.log('Conflict error:', error);
467
+ * return { message: 'Resource conflict detected' };
468
+ * });
469
+ */
470
+ conflict(cb) {
471
+ return this.error("conflictError", "CONFLICT", cb);
472
+ }
473
+ /**
474
+ * Handles 401 Unauthorized errors with a custom callback.
475
+ * @param cb The callback function to handle the error
476
+ * @returns The request instance for chaining
477
+ * @example
478
+ * const request = new Request('/users', config);
479
+ * request.unauthorised((error) => {
480
+ * console.log('Unauthorized access:', error);
481
+ * return { message: 'Please login first' };
482
+ * });
483
+ */
484
+ unauthorised(cb) {
485
+ return this.error("unauthorisedError", "UNAUTHORIZED", cb);
486
+ }
487
+ /**
488
+ * Handles 403 Forbidden errors with a custom callback.
489
+ * @param cb The callback function to handle the error
490
+ * @returns The request instance for chaining
491
+ * @example
492
+ * const request = new Request('/users', config);
493
+ * request.forbidden((error) => {
494
+ * console.log('Access forbidden:', error);
495
+ * return { message: 'You do not have permission' };
496
+ * });
497
+ */
498
+ forbidden(cb) {
499
+ return this.error("forbiddenError", "FORBIDDEN", cb);
500
+ }
501
+ /**
502
+ * Handles 501 Not Implemented errors with a custom callback.
503
+ * @param cb The callback function to handle the error
504
+ * @returns The request instance for chaining
505
+ * @example
506
+ * const request = new Request('/users', config);
507
+ * request.notImplemented((error) => {
508
+ * console.log('Not implemented:', error);
509
+ * return { message: 'This feature is not implemented yet' };
510
+ * });
511
+ */
512
+ notImplemented(cb) {
513
+ return this.error("notImplementedError", "NOT_IMPLEMENTED", cb);
514
+ }
515
+ /**
516
+ * Handles 500 Internal Server Error errors with a custom callback.
517
+ * @param cb The callback function to handle the error
518
+ * @returns The request instance for chaining
519
+ * @example
520
+ * const request = new Request('/users', config);
521
+ * request.internalServerError((error) => {
522
+ * console.log('Server error:', error);
523
+ * return { message: 'An unexpected error occurred' };
524
+ * });
525
+ */
526
+ internalServerError(cb) {
527
+ return this.error("internalServerError", "INTERNAL_SERVER_ERROR", cb);
528
+ }
529
+ /**
530
+ * Sets a custom error handler for a specific HTTP status code.
531
+ * @param tag A string identifier for the error type
532
+ * @param status The HTTP error status to handle
533
+ * @param cb The callback function to handle the error
534
+ * @returns The request instance for chaining
535
+ * @example
536
+ * const request = new Request('/users', config);
537
+ * request
538
+ .withResult()
539
+ .error('customError', 'BAD_REQUEST', (error) => {
540
+ * console.log('Bad request error:', error);
541
+ * return {
542
+ * message: 'Invalid input',
543
+ * details: error.response.responseData
544
+ * };
545
+ * });
546
+ *
547
+ * // Later when making the request:
548
+ * const result = await request.json();
549
+ * if (Result.isErr(result)) {
550
+ * if(result.tag === 'customError') {
551
+ * console.log(result.error.data.message); // 'Invalid input'
552
+ * }
553
+ */
554
+ error(tag, status, cb) {
555
+ this.#customErrorCbs[httpErrors[status]] = {
556
+ cb,
557
+ tag
558
+ };
559
+ return this;
560
+ }
561
+ /**
562
+ * Sets query parameters for the request URL.
563
+ * @param params An object containing query parameter key-value pairs
564
+ * @returns The request instance for chaining
565
+ * @example
566
+ * const request = new Request('/users', config);
567
+ * request.setQueryParams({
568
+ * page: '1',
569
+ * limit: '10',
570
+ * sort: 'desc'
571
+ * });
572
+ */
573
+ setQueryParams(params) {
574
+ this.#queryParams = new URLSearchParams(params);
575
+ return this;
576
+ }
577
+ /**
578
+ * Sets the output schema for validating the response data using Zod.
579
+ * @param schema The Zod schema to validate the response
580
+ * @returns The request instance for chaining
581
+ * @example
582
+ * import { z } from 'zod';
583
+ *
584
+ * const userSchema = z.object({
585
+ * id: z.number(),
586
+ * name: z.string(),
587
+ * email: z.string().email()
588
+ * });
589
+ *
590
+ * const request = new Request('/users', config);
591
+ * const result = await request
592
+ * .withResult()
593
+ * .output(userSchema)
594
+ * .json();
595
+ *
596
+ * if (Result.isOk(result)) {
597
+ * const user = result.value; // Typed and validated user data
598
+ * }
599
+ */
600
+ schema(schema) {
601
+ this.#schema = schema;
602
+ return this;
603
+ }
604
+ /**
605
+ * Executes the request and returns the JSON response.
606
+ * @returns A Promise containing the Result type with either successful data or error information
607
+ * @example
608
+ * const request = new Request('/users', config);
609
+ * const result = await request
610
+ * .setQueryParams({ id: '123' })
611
+ * .withResult()
612
+ * .notFound((error) => ({ message: 'User not found' }))
613
+ * .json<User>();
614
+ *
615
+ * if (Result.isOk(result)) {
616
+ * const user = result.value; // User data
617
+ * } else {
618
+ * console.error(result.error); // Error handling
619
+ * }
620
+ */
621
+ async json() {
622
+ const output = await this.#makeRequest(
623
+ async (response) => response.json().catch(
624
+ (e) => new CustomError("jsonParseError", {
625
+ message: e instanceof Error ? e.message : "Failed to parse JSON"
626
+ })
627
+ ),
628
+ true
629
+ );
630
+ return this.#mapResponse(output);
631
+ }
632
+ /**
633
+ * Executes the request and returns the response as plain text.
634
+ * @returns A Promise containing the Result type with either successful text data or error information
635
+ * @example
636
+ * const request = new Request('/data.txt', config);
637
+ * const result = await request
638
+ * .setQueryParams({ version: '1' })
639
+ * .withResult()
640
+ * .notFound((error) => ({ message: 'Text file not found' }))
641
+ * .text();
642
+ *
643
+ * if (Result.isOk(result)) {
644
+ * const text = result.value; // Plain text content
645
+ * } else {
646
+ * console.error(result.error); // Error handling
647
+ * }
648
+ */
649
+ async text() {
650
+ const output = await this.#makeRequest(
651
+ (response) => response.text()
652
+ );
653
+ return this.#mapResponse(output);
654
+ }
655
+ /**
656
+ * Executes the request and returns the response as a Blob.
657
+ * @returns A Promise containing the Result type with either successful Blob data or error information
658
+ * @example
659
+ * const request = new Request('/image.jpg', config);
660
+ * const result = await request
661
+ * .setQueryParams({ size: 'large' })
662
+ * .withResult()
663
+ * .notFound((error) => ({ message: 'Image not found' }))
664
+ * .blob();
665
+ *
666
+ * if (Result.isOk(result)) {
667
+ * const imageBlob = result.value; // Blob data
668
+ * } else {
669
+ * console.error(result.error); // Error handling
670
+ * }
671
+ */
672
+ async blob() {
673
+ const output = await this.#makeRequest((response) => response.blob());
674
+ return this.#mapResponse(output);
675
+ }
676
+ #url() {
677
+ const baseUrl = this.#localRequestInit.baseUrl?.replace(/\/+$/, "") ?? "";
678
+ const path = this.#path.replace(/^\/+/, "/");
679
+ const queryString = this.#queryParams ? `?${this.#queryParams.toString()}` : "";
680
+ const url = [baseUrl, path, queryString].filter(Boolean).join("");
681
+ return url;
682
+ }
683
+ /**
684
+ * Returns the complete URL for the request including base URL, path, and query parameters.
685
+ * @returns The complete URL string
686
+ * @example
687
+ * const request = new Request('/users', config);
688
+ * request.setBaseUrl('https://api.example.com');
689
+ * request.setQueryParams({ id: '123' });
690
+ * console.log(request.url()); // 'https://api.example.com/users?id=123'
691
+ */
692
+ url() {
693
+ return this.#url();
694
+ }
695
+ /**
696
+ * Configures the request to return a Result type instead of a tuple.
697
+ * @returns The request instance for chaining with Result type return value
698
+ * @example
699
+ * const request = new Request('/users', config);
700
+ * const result = await request
701
+ * .withResult()
702
+ * .json<User>();
703
+ *
704
+ * // Returns Result type instead of tuple
705
+ * if (Result.isOk(result)) {
706
+ * const user = result.value;
707
+ * }
708
+ */
709
+ withResult() {
710
+ this.#shouldBeResult = true;
711
+ return this;
712
+ }
713
+ #mapResponse(value) {
714
+ if (this.#shouldBeResult) {
715
+ return value;
716
+ }
717
+ if (isOk(value)) {
718
+ return [getOrNull(value), null];
719
+ } else {
720
+ return [null, getErrorOrNull(value)];
721
+ }
722
+ }
723
+ async #makeRequest(responseParser, isJson = false) {
724
+ if (this.#bodySchemaIssues.length) {
725
+ return err(new CustomError("parseError", this.#bodySchemaIssues));
726
+ }
727
+ const request = this.#request();
728
+ const { retries, retryDelay, retryOn, retryWhile, onRetry } = this.#sanitisedRetryConfig();
729
+ try {
730
+ const requestInit = request.requestInit;
731
+ const url = this.#url();
732
+ let attempts = 1;
733
+ let response;
734
+ let responseData;
735
+ while (attempts <= retries) {
736
+ try {
737
+ response = await fetch(url, requestInit);
738
+ responseData = await responseParser(response);
739
+ if (responseData instanceof CustomError) {
740
+ return err(responseData);
741
+ }
742
+ const retryWhileCondition = retryWhile ? await retryWhile(
743
+ request,
744
+ this.#makeResponse(response, responseData)
745
+ ) : false;
746
+ if (response.ok || !retryOn.includes(response.status) && !retryWhileCondition) {
747
+ break;
748
+ }
749
+ if (response.status in this.#customErrorCbs && attempts === retries) {
750
+ const result = this.#customErrorCbs[response.status].cb({
751
+ request,
752
+ response: this.#makeResponse(response, responseData)
753
+ });
754
+ return err(
755
+ new CustomError(
756
+ // @ts-ignore
757
+ this.#customErrorCbs[response.status].tag,
758
+ result
759
+ )
760
+ );
761
+ }
762
+ if (attempts < retries) {
763
+ const delay = typeof retryDelay === "function" ? await retryDelay(
764
+ retries - attempts - 1,
765
+ retries,
766
+ request,
767
+ this.#makeResponse(response, responseData)
768
+ ) : retryDelay;
769
+ await new Promise((resolve) => setTimeout(resolve, delay));
770
+ }
771
+ } catch (e) {
772
+ if (attempts === retries) throw e;
773
+ const delay = typeof retryDelay === "function" ? await retryDelay(
774
+ retries - attempts - 1,
775
+ retries,
776
+ request,
777
+ this.#makeResponse(response, responseData)
778
+ ) : retryDelay;
779
+ await new Promise((resolve) => setTimeout(resolve, delay));
780
+ }
781
+ if (onRetry) {
782
+ onRetry(request, this.#makeResponse(response, responseData));
783
+ }
784
+ attempts++;
785
+ }
786
+ if (!response.ok) {
787
+ if (response.status in this.#customErrorCbs) {
788
+ const result = this.#customErrorCbs[response.status].cb({
789
+ request,
790
+ response: this.#makeResponse(response, responseData)
791
+ });
792
+ return err(
793
+ new CustomError(
794
+ // @ts-ignore
795
+ this.#customErrorCbs[response.status].tag,
796
+ result
797
+ )
798
+ );
799
+ }
800
+ return err(
801
+ new AspiError(
802
+ response.statusText,
803
+ this.#request(),
804
+ this.#makeResponse(response, responseData)
805
+ )
806
+ );
807
+ }
808
+ if (isJson && this.#schema) {
809
+ const data = this.#schema["~standard"].validate(responseData);
810
+ if (data instanceof Promise) {
811
+ throw new Error("Schema validation should not return a promise");
812
+ }
813
+ if (data.issues) {
814
+ return err(new CustomError("parseError", data.issues));
815
+ }
816
+ return ok({
817
+ data: data.value,
818
+ request,
819
+ response: this.#makeResponse(response, responseData)
820
+ });
821
+ }
822
+ return ok({
823
+ data: responseData,
824
+ request,
825
+ response: this.#makeResponse(response, responseData)
826
+ });
827
+ } catch (error) {
828
+ if (500 in this.#customErrorCbs) {
829
+ const result = this.#customErrorCbs[500].cb({
830
+ request,
831
+ response: {
832
+ status: 500,
833
+ statusText: "INTERNAL_SERVER_ERROR"
834
+ }
835
+ });
836
+ return err(
837
+ new CustomError(
838
+ // @ts-ignore
839
+ this.#customErrorCbs[500].tag,
840
+ result
841
+ )
842
+ );
843
+ }
844
+ return err(
845
+ new AspiError(
846
+ error instanceof Error ? error.message : "Something went wrong",
847
+ request,
848
+ {
849
+ status: 500,
850
+ statusText: "INTERNAL_SERVER_ERROR"
851
+ }
852
+ )
853
+ );
854
+ }
855
+ }
856
+ #request() {
857
+ let requestInit = this.#localRequestInit;
858
+ for (const middleware of this.#middlewares) {
859
+ requestInit = middleware(this.#localRequestInit);
860
+ }
861
+ return {
862
+ requestInit,
863
+ baseUrl: this.#localRequestInit.baseUrl,
864
+ path: this.#path,
865
+ queryParams: this.#queryParams || null,
866
+ retryConfig: this.#sanitisedRetryConfig()
867
+ };
868
+ }
869
+ #sanitisedRetryConfig() {
870
+ const retries = this.#retryConfig?.retries || 1;
871
+ const retryDelay = this.#retryConfig?.retryDelay || 0;
872
+ const retryOn = this.#retryConfig?.retryOn || [];
873
+ const retryWhile = this.#retryConfig?.retryWhile;
874
+ const onRetry = this.#retryConfig?.onRetry;
875
+ return {
876
+ retries,
877
+ retryDelay,
878
+ retryOn,
879
+ retryWhile,
880
+ onRetry
881
+ };
882
+ }
883
+ #makeResponse(response, responseData) {
884
+ return {
885
+ response,
886
+ status: response.status,
887
+ statusText: getHttpErrorStatus(response.status),
888
+ responseData
889
+ };
890
+ }
891
+ };
892
+
893
+ // src/aspi.ts
894
+ var Aspi = class {
895
+ #globalRequestInit;
896
+ #middlewares = [];
897
+ #retryConfig;
898
+ #customErrorCbs = {};
899
+ constructor(config) {
900
+ const { retryConfig, ...requestConfig } = config;
901
+ this.#globalRequestInit = requestConfig;
902
+ this.#retryConfig = retryConfig;
903
+ }
904
+ /**
905
+ * Sets the base URL for all API requests
906
+ * @param {string} url - The base URL to be set
907
+ * @returns {Aspi} The Aspi instance for chaining
908
+ * @example
909
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
910
+ * api.setBaseUrl('https://api.newdomain.com');
911
+ */
912
+ setBaseUrl(url) {
913
+ this.#globalRequestInit.baseUrl = url;
914
+ return this;
915
+ }
916
+ /**
917
+ * Sets the retry configuration for failed requests
918
+ * @param {AspiRetryConfig<TRequest>} retry - The retry configuration object
919
+ * @returns {Aspi} The Aspi instance for chaining
920
+ * @example
921
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
922
+ * api.setRetry({
923
+ * retries: 3,
924
+ * retryDelay: 1000,
925
+ * retryOn: [500, 502],
926
+ * retryWhile: (req, res) => res.status === 500
927
+ * });
928
+ */
929
+ setRetry(retry) {
930
+ this.#retryConfig = {
931
+ ...this.#retryConfig,
932
+ ...retry
933
+ };
934
+ return this;
935
+ }
936
+ #createRequest(method, path, body) {
937
+ return new Request(method, path, {
938
+ requestConfig: {
939
+ ...this.#globalRequestInit,
940
+ method,
941
+ body
942
+ },
943
+ retryConfig: this.#retryConfig,
944
+ middlewares: this.#middlewares,
945
+ errorCbs: this.#customErrorCbs
946
+ });
947
+ }
948
+ /**
949
+ * Makes a GET request to the specified path
950
+ * @param {string} path - The path to make the request to
951
+ * @returns {Request} A Request instance for chaining
952
+ * @example
953
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
954
+ * const response = await api.get('/users').json();
955
+ */
956
+ get(path) {
957
+ return this.#createRequest("GET", path);
958
+ }
959
+ /**
960
+ * Makes a POST request to the specified path with an optional body
961
+ * @param {string} path - The path to make the request to
962
+ * @param {BodyInit} [body] - The body of the request
963
+ * @returns {Request} A Request instance for chaining
964
+ * @example
965
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
966
+ * const response = await api.post('/users', { name: 'John' }).json();
967
+ */
968
+ post(path, body) {
969
+ return this.#createRequest("POST", path, body);
970
+ }
971
+ /**
972
+ * Makes a PUT request to the specified path with an optional body
973
+ * @param {string} path - The path to make the request to
974
+ * @param {BodyInit} [body] - The body of the request
975
+ * @returns {Request} A Request instance for chaining
976
+ * @example
977
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
978
+ * const response = await api.put('/users/1', { name: 'John' }).json();
979
+ */
980
+ put(path, body) {
981
+ return this.#createRequest("PUT", path, body);
982
+ }
983
+ /**
984
+ * Makes a PATCH request to the specified path with an optional body
985
+ * @param {string} path - The path to make the request to
986
+ * @param {BodyInit} [body] - The body of the request
987
+ * @returns {Request} A Request instance for chaining
988
+ * @example
989
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
990
+ * const response = await api.patch('/users/1', { name: 'John' }).json();
991
+ */
992
+ patch(path, body) {
993
+ return this.#createRequest("PATCH", path, body);
994
+ }
995
+ /**
996
+ * Makes a DELETE request to the specified path
997
+ * @param {string} path - The path to make the request to
998
+ * @returns {Request} A Request instance for chaining
999
+ * @example
1000
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1001
+ * const response = await api.delete('/users/1').json();
1002
+ */
1003
+ delete(path) {
1004
+ return this.#createRequest("DELETE", path);
1005
+ }
1006
+ /**
1007
+ * Makes a HEAD request to the specified path
1008
+ * @param {string} path - The path to make the request to
1009
+ * @returns {Request} A Request instance for chaining
1010
+ * @example
1011
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1012
+ * const response = await api.head('/users').json();
1013
+ */
1014
+ head(path) {
1015
+ return this.#createRequest("HEAD", path);
1016
+ }
1017
+ /**
1018
+ * Makes an OPTIONS request to the specified path
1019
+ * @param {string} path - The path to make the request to
1020
+ * @returns {Request} A Request instance for chaining
1021
+ * @example
1022
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1023
+ * const response = await api.options('/users').json();
1024
+ */
1025
+ options(path) {
1026
+ return this.#createRequest("OPTIONS", path);
1027
+ }
1028
+ /**
1029
+ * Sets multiple headers for all API requests
1030
+ * @param {Record<string, string>} headers - An object containing header key-value pairs
1031
+ * @returns {Aspi} The Aspi instance for chaining
1032
+ * @example
1033
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1034
+ * api.setHeaders({
1035
+ * 'Content-Type': 'application/json',
1036
+ * 'Accept': 'application/json'
1037
+ * });
1038
+ */
1039
+ setHeaders(headers) {
1040
+ this.#globalRequestInit.headers = headers;
1041
+ return this;
1042
+ }
1043
+ /**
1044
+ * Sets a single header for all API requests
1045
+ * @param {string} key - The header key
1046
+ * @param {string} value - The header value
1047
+ * @returns {Aspi} The Aspi instance for chaining
1048
+ * @example
1049
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1050
+ * api.setHeader('Content-Type', 'application/json');
1051
+ */
1052
+ setHeader(key, value) {
1053
+ this.#globalRequestInit.headers = {
1054
+ ...this.#globalRequestInit.headers,
1055
+ [key]: value
1056
+ };
1057
+ return this;
1058
+ }
1059
+ /**
1060
+ * Sets the Authorization header with a Bearer token
1061
+ * @param {string} token - The Bearer token
1062
+ * @returns {Aspi} The Aspi instance for chaining
1063
+ * @example
1064
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1065
+ * api.setBearer('myAuthToken123');
1066
+ */
1067
+ setBearer(token) {
1068
+ return this.setHeader("Authorization", `Bearer ${token}`);
1069
+ }
1070
+ /**
1071
+ * Use a middleware function to transform requests
1072
+ * @param {Middleware<T, U>} fn - The middleware function to apply
1073
+ * @returns {Aspi<U>} A new Aspi instance with the applied middleware
1074
+ * @example
1075
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1076
+ * api.use((req) => {
1077
+ * // Add custom headers
1078
+ * req.headers = {
1079
+ * ...req.headers,
1080
+ * 'x-custom-header': 'custom-value'
1081
+ * };
1082
+ * return req;
1083
+ * });
1084
+ */
1085
+ use(fn) {
1086
+ this.#middlewares = [...this.#middlewares, fn];
1087
+ return this;
1088
+ }
1089
+ /**
1090
+ * Registers a custom error handler for a specific HTTP status
1091
+ * @param {string} tag - The error tag identifier
1092
+ * @param {HttpErrorStatus} status - The HTTP status code to handle
1093
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1094
+ * @returns {Aspi} The Aspi instance for chaining
1095
+ * @example
1096
+ * const api = new Aspi({ baseUrl: 'https://api.example.com' });
1097
+ * api.error('customError', 'BAD_REQUEST', (req, res) => {
1098
+ * console.log('Bad request error occurred');
1099
+ * return { message: 'Invalid input' };
1100
+ * });
1101
+ */
1102
+ error(tag, status, cb) {
1103
+ this.#customErrorCbs[httpErrors[status]] = {
1104
+ cb,
1105
+ tag
1106
+ };
1107
+ return this;
1108
+ }
1109
+ /**
1110
+ * Handles 404 Not Found errors with a custom callback.
1111
+ * @param cb The callback function to handle the error
1112
+ * @returns The request instance for chaining
1113
+ * @example
1114
+ * const request = new Request('/users', config);
1115
+ * request.notFound((error) => {
1116
+ * console.log('Not found:', error);
1117
+ * return { message: 'Resource not found' };
1118
+ * });
1119
+ */
1120
+ notFound(cb) {
1121
+ return this.error("notFoundError", "NOT_FOUND", cb);
1122
+ }
1123
+ /**
1124
+ * Handles 429 Too Many Requests errors with a custom callback.
1125
+ * @param cb The callback function to handle the error
1126
+ * @returns The request instance for chaining
1127
+ * @example
1128
+ * const request = new Request('/users', config);
1129
+ * request.tooManyRequests((error) => {
1130
+ * console.log('Rate limited:', error);
1131
+ * return { message: 'Please try again later' };
1132
+ * });
1133
+ */
1134
+ tooManyRequests(cb) {
1135
+ return this.error("tooManyRequestsError", "TOO_MANY_REQUESTS", cb);
1136
+ }
1137
+ /**
1138
+ * Handles 409 Conflict errors with a custom callback.
1139
+ * @param cb The callback function to handle the error
1140
+ * @returns The request instance for chaining
1141
+ * @example
1142
+ * const request = new Request('/users', config);
1143
+ * request.conflict((error) => {
1144
+ * console.log('Conflict error:', error);
1145
+ * return { message: 'Resource conflict detected' };
1146
+ * });
1147
+ */
1148
+ conflict(cb) {
1149
+ return this.error("conflictError", "CONFLICT", cb);
1150
+ }
1151
+ /**
1152
+ * Registers a handler for 400 Bad Request errors
1153
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1154
+ * @returns {Aspi} The Aspi instance for chaining
1155
+ * @example
1156
+ * api.badRequest((req, res) => ({ message: 'Invalid request parameters' }));
1157
+ */
1158
+ badRequest(cb) {
1159
+ return this.error("badRequestError", "BAD_REQUEST", cb);
1160
+ }
1161
+ /**
1162
+ * Registers a handler for 401 Unauthorized errors
1163
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1164
+ * @returns {Aspi} The Aspi instance for chaining
1165
+ * @example
1166
+ * api.unauthorized((req, res) => ({ message: 'Authentication required' }));
1167
+ */
1168
+ unauthorized(cb) {
1169
+ return this.error("unauthorizedError", "UNAUTHORIZED", cb);
1170
+ }
1171
+ /**
1172
+ * Registers a handler for 403 Forbidden errors
1173
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1174
+ * @returns {Aspi} The Aspi instance for chaining
1175
+ * @example
1176
+ * api.forbidden((req, res) => ({ message: 'Access denied' }));
1177
+ */
1178
+ forbidden(cb) {
1179
+ return this.error("forbiddenError", "FORBIDDEN", cb);
1180
+ }
1181
+ /**
1182
+ * Registers a handler for 501 Not Implemented errors
1183
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1184
+ * @returns {Aspi} The Aspi instance for chaining
1185
+ * @example
1186
+ * api.notImplemented((req, res) => ({ message: 'Feature not implemented' }));
1187
+ */
1188
+ notImplemented(cb) {
1189
+ return this.error("notImplementedError", "NOT_IMPLEMENTED", cb);
1190
+ }
1191
+ /**
1192
+ * Registers a handler for 500 Internal Server errors
1193
+ * @param {CustomErrorCb<TRequest, A>} cb - The callback function to handle the error
1194
+ * @returns {Aspi} The Aspi instance for chaining
1195
+ * @example
1196
+ * api.internalServerError((req, res) => ({ message: 'Server error occurred' }));
1197
+ */
1198
+ internalServerError(cb) {
1199
+ return this.error("internalServerErrorError", "INTERNAL_SERVER_ERROR", cb);
1200
+ }
1201
+ };
1202
+ export {
1203
+ Aspi,
1204
+ AspiError,
1205
+ CustomError,
1206
+ Request,
1207
+ result_exports as Result,
1208
+ getHttpErrorStatus,
1209
+ httpErrors
1210
+ };