service-creator 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
+
2
+ [![NPM Version](https://img.shields.io/npm/v/service-creator.svg)](https://www.npmjs.com/package/service-creator)
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![CI](https://github.com/royriojas/create-service/actions/workflows/ci.yml/badge.svg)](https://github.com/royriojas/create-service/actions/workflows/ci.yml)
5
+
6
+
1
7
  # service-creator
2
8
 
3
- A simple abstraction to create "services", plain objects that can be used to perform fetch calls in a convention over configuration fashion.
9
+ A simple abstraction to create "services" plain objects with typed async methods that perform fetch calls, using convention over configuration.
4
10
 
5
11
  ## Installation
6
12
 
@@ -10,9 +16,15 @@ npm install service-creator
10
16
 
11
17
  ## Usage
12
18
 
13
- ```js
14
- import { createService } from 'service-creator';
15
- import { v4 as uuid } from 'uuid';
19
+ Define your service endpoints with `createEndpoint<TResponse, TArgs?>()` for full type safety:
20
+
21
+ ```ts
22
+ import { createService, createEndpoint } from 'service-creator';
23
+
24
+ interface User {
25
+ id: string;
26
+ name: string;
27
+ }
16
28
 
17
29
  const fetcher = {
18
30
  fetch: async (url, opts) => {
@@ -21,47 +33,109 @@ const fetcher = {
21
33
  },
22
34
  };
23
35
 
24
- // defining the interface of the service is helpful to have good typings
25
- // service methods are async functions that return a promise that resolves to the response
26
- // expected to be received from the fetch/xhr calls
27
- export interface PPService {
28
- getSomeData: (prompt: string) => Promise<SomeData[]>;
29
- getDataById: (id: string) => Promise<SomeData>;
36
+ const userService = createService({
37
+ endpoints: {
38
+ // No args just one generic for the response type
39
+ listUsers: createEndpoint<User[]>({
40
+ url: '/v1/users',
41
+ }),
42
+
43
+ // With args — response type first, then args type
44
+ getUser: createEndpoint<User, { id: string }>({
45
+ url: ({ id }) => `/v1/users/${id}`,
46
+ }),
47
+
48
+ // POST with body
49
+ createUser: createEndpoint<User, { name: string }>({
50
+ url: '/v1/users',
51
+ method: 'POST',
52
+ body: (args) => args, // args is typed as { name: string }
53
+ }),
54
+
55
+ // Response transformation
56
+ getUserName: createEndpoint<string, { id: string }>({
57
+ url: ({ id }) => `/v1/users/${id}`,
58
+ transform: (data) => data.name, // raw response → typed return value
59
+ }),
60
+ },
61
+ basePath: 'https://api.example.com',
62
+ fetcher,
63
+ });
64
+
65
+ // All types are fully inferred:
66
+ const users = await userService.listUsers(); // User[]
67
+ const user = await userService.getUser({ id: '123' }); // User
68
+ const created = await userService.createUser({ name: 'Ali' });// User
69
+ const name = await userService.getUserName({ id: '123' }); // string
70
+ ```
71
+
72
+ ### `createEndpoint<TResponse, TArgs?, TError?>`
73
+
74
+ | Generic | Description | Default |
75
+ |---|---|---|
76
+ | `TResponse` | Return type (`Promise<TResponse>`) | `any` |
77
+ | `TArgs` | Input parameter type. Omit for no-arg endpoints. | `void` |
78
+ | `TError` | Error type (available via `InferError`) | `Error` |
79
+
80
+ ### Descriptor options
81
+
82
+ | Option | Type | Description |
83
+ |---|---|---|
84
+ | `url` | `string \| (args: TArgs) => string` | Endpoint URL — static or dynamic |
85
+ | `method` | `HttpMethod` | HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.) |
86
+ | `body` | `object \| (args: TArgs) => any` | Request body — static or computed from args |
87
+ | `headers` | `object \| (args: TArgs) => any` | Request headers — static or computed from args |
88
+ | `params` | `object \| (args: TArgs) => any` | Query string params — static or computed from args |
89
+ | `fetchOpts` | `FetchOptions` | Additional fetch options (credentials, mode, etc.) |
90
+ | `transform` | `(data: any) => TResponse` | Transform the raw response before returning |
91
+
92
+ ### Custom error types
93
+
94
+ ```ts
95
+ interface ApiError {
96
+ code: number;
97
+ message: string;
30
98
  }
31
99
 
32
- const commonHeadersFn = () => {
33
- const appCtx = getCtx();
100
+ const service = createService({
101
+ endpoints: {
102
+ riskyCall: createEndpoint<Data, { id: string }, ApiError>({
103
+ url: ({ id }) => `/v1/data/${id}`,
104
+ }),
105
+ },
106
+ fetcher,
107
+ });
108
+ ```
34
109
 
35
- return {
36
- 'x-req-id': uuid(),
37
- };
38
- };
110
+ ## Alternative styles
111
+
112
+ Plain descriptors (types inferred from function signatures):
39
113
 
114
+ ```ts
40
115
  const service = createService({
41
116
  endpoints: {
42
- getSomeData: {
43
- method: 'GET',
44
- url: '/v1/get-some-data',
45
- body: ({ prompt }) => ({ prompt }),
46
- headers: commonHeadersFn,
47
- },
48
- getDataById: {
49
- method: 'GET',
50
- headers: commonHeadersFn,
51
- url: ({ id }) => `/v1/get-some-data-by-id/${id}`,
117
+ getUser: {
118
+ url: (id: string) => `/v1/users/${id}`,
119
+ transform: (data: any): User => data,
52
120
  },
53
121
  },
54
- basePath: api,
55
122
  fetcher,
56
123
  });
124
+ ```
57
125
 
58
- const data = await service.getSomeData({ prompt: 'hello world' });
59
-
60
- console.log(data); // expected an array of SomeData
126
+ Legacy explicit interface:
61
127
 
62
- const dataById = await service.getDataById({ id: '1' });
128
+ ```ts
129
+ interface MyService {
130
+ getUser: (id: string) => Promise<User>;
131
+ }
63
132
 
64
- console.log(dataById); // expected a single SomeData
133
+ const service = createService<MyService>({
134
+ endpoints: {
135
+ getUser: { url: (id: string) => `/v1/users/${id}` },
136
+ },
137
+ fetcher,
138
+ });
65
139
  ```
66
140
 
67
141
  ## License
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { Fetcher, FetchOptions } from './fetch-types';
2
+
2
3
  export interface Fn<T extends Array<any>> {
3
4
  (...args: T): string;
4
5
  }
@@ -7,18 +8,102 @@ export type StringOrFn<T extends ServiceFn> = string | Fn<Parameters<T>>;
7
8
  export type ObjectOrFn<T extends ServiceFn> = object | Fn<Parameters<T>>;
8
9
  export interface Descriptor<K extends ServiceFn> {
9
10
  url: StringOrFn<K>;
10
- method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'TRACE' | 'CONNECT';
11
+ method?: HttpMethod;
11
12
  body?: ObjectOrFn<K>;
12
13
  headers?: ObjectOrFn<K>;
13
14
  params?: ObjectOrFn<K>;
14
15
  fetchOpts?: FetchOptions;
16
+ transform?: (data: any) => any;
15
17
  }
16
18
  export type ServiceDescriptor<T> = {
17
19
  [K in keyof T]: Descriptor<T[K] extends ServiceFn ? T[K] : never>;
18
20
  };
19
21
  export interface CreateServiceArgs<T> {
20
22
  endpoints: ServiceDescriptor<T>;
21
- basePath: string | undefined;
23
+ basePath?: string;
22
24
  fetcher: Fetcher;
23
25
  }
24
- export declare const createService: <T>({ endpoints, basePath, fetcher }: CreateServiceArgs<T>) => T;
26
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'TRACE' | 'CONNECT';
27
+ export interface DescriptorBase {
28
+ url: string | ((...args: any[]) => string);
29
+ method?: HttpMethod;
30
+ body?: object | ((...args: any[]) => any);
31
+ headers?: object | ((...args: any[]) => any);
32
+ params?: object | ((...args: any[]) => any);
33
+ fetchOpts?: FetchOptions;
34
+ transform?: (data: any) => any;
35
+ }
36
+ /** A descriptor branded with explicit TResponse, TArgs, TError types */
37
+ export interface TypedEndpoint<TResponse = any, TArgs = void, TError = Error> extends DescriptorBase {
38
+ readonly [ENDPOINT_TYPES]: {
39
+ args: TArgs;
40
+ response: TResponse;
41
+ error: TError;
42
+ };
43
+ }
44
+ /** Configuration accepted by createEndpoint */
45
+ export interface EndpointConfig<TResponse, TArgs> {
46
+ url: string | ((args: TArgs) => string);
47
+ method?: HttpMethod;
48
+ body?: object | ((args: TArgs) => any);
49
+ headers?: object | ((args: TArgs) => any);
50
+ params?: object | ((args: TArgs) => any);
51
+ fetchOpts?: FetchOptions;
52
+ transform?: (data: any) => TResponse;
53
+ }
54
+ /**
55
+ * Define a typed endpoint with explicit response/input/error types.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * // No args:
60
+ * createEndpoint<User[]>({ url: '/users' })
61
+ * // service method: () => Promise<User[]>
62
+ *
63
+ * // With args:
64
+ * createEndpoint<User, { id: string }>({
65
+ * url: ({ id }) => `/users/${id}`,
66
+ * })
67
+ * // service method: (args: { id: string }) => Promise<User>
68
+ * ```
69
+ */
70
+ export declare function createEndpoint<TResponse = any, TArgs = void, TError = Error>(config: EndpointConfig<TResponse, TArgs>): TypedEndpoint<TResponse, TArgs, TError>;
71
+ /** Infer the service method args from a descriptor */
72
+ export type InferArgs<D> = D extends {
73
+ readonly [ENDPOINT_TYPES]: {
74
+ args: infer A;
75
+ };
76
+ } ? [A] extends [void] ? [] : [args: A] : D extends {
77
+ url: (...args: infer P) => any;
78
+ } ? P : D extends {
79
+ body: (...args: infer P) => any;
80
+ } ? P : D extends {
81
+ params: (...args: infer P) => any;
82
+ } ? P : D extends {
83
+ headers: (...args: infer P) => any;
84
+ } ? P : any[];
85
+ /** Infer the service method return type from a descriptor */
86
+ export type InferReturn<D> = D extends {
87
+ readonly [ENDPOINT_TYPES]: {
88
+ response: infer R;
89
+ };
90
+ } ? R : D extends {
91
+ transform: (data: any) => infer R;
92
+ } ? R : any;
93
+ /** Infer the error type from a branded descriptor */
94
+ export type InferError<D> = D extends {
95
+ readonly [ENDPOINT_TYPES]: {
96
+ error: infer E;
97
+ };
98
+ } ? E : Error;
99
+ export type InferService<T extends Record<string, DescriptorBase>> = {
100
+ [K in keyof T]: (...args: InferArgs<T[K]>) => Promise<InferReturn<T[K]>>;
101
+ };
102
+ /** Create a service with types inferred from the endpoint descriptors */
103
+ export declare function createService<const T extends Record<string, DescriptorBase>>(args: {
104
+ endpoints: T;
105
+ basePath?: string;
106
+ fetcher: Fetcher;
107
+ }): InferService<T>;
108
+ /** @deprecated Use the inferred API (call without explicit generic) */
109
+ export declare function createService<T>(args: CreateServiceArgs<T>): T;
@@ -1,4 +1,5 @@
1
1
  import { FetchFn } from './fetchJSON';
2
+
2
3
  export type FetchJSONArgs = Parameters<typeof fetch>;
3
4
  export type FetchURL = FetchJSONArgs[0];
4
5
  export type FetchOptions = Exclude<FetchJSONArgs[1], null | undefined>;
@@ -1,4 +1,5 @@
1
1
  import { FetchURL, SerializeBodyProps } from './fetch-types';
2
+
2
3
  export interface ErrorData {
3
4
  error: string;
4
5
  }
package/dist/index.js CHANGED
@@ -1,110 +1,131 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
-
7
- var _createService = require("./create-service");
8
-
9
- Object.keys(_createService).forEach(function (key) {
10
- if (key === "default" || key === "__esModule") return;
11
- if (key in exports && exports[key] === _createService[key]) return;
12
- Object.defineProperty(exports, key, {
13
- enumerable: true,
14
- get: function get() {
15
- return _createService[key];
16
- }
17
- });
18
- });
19
-
20
- var _fetchTypes = require("./fetch-types");
21
-
22
- Object.keys(_fetchTypes).forEach(function (key) {
23
- if (key === "default" || key === "__esModule") return;
24
- if (key in exports && exports[key] === _fetchTypes[key]) return;
25
- Object.defineProperty(exports, key, {
26
- enumerable: true,
27
- get: function get() {
28
- return _fetchTypes[key];
29
- }
1
+ var b = Object.defineProperty;
2
+ var R = (e, t, r) => t in e ? b(e, t, { enumerable: !0, configurable: !0, writable: !0, value: r }) : e[t] = r;
3
+ var y = (e, t, r) => (R(e, typeof t != "symbol" ? t + "" : t, r), r);
4
+ const P = (e, t) => (e = e.replace(/\/$/, ""), t = t.replace(/^\//, ""), `${e}/${t}`), O = (e, t) => {
5
+ const r = t ?? new URLSearchParams();
6
+ return Object.keys(e).forEach((a) => {
7
+ const n = e[a];
8
+ typeof n < "u" && (Array.isArray(n) ? n.forEach((s) => {
9
+ r.append(a, s);
10
+ }) : r.set(a, `${n}`));
11
+ }), r;
12
+ }, U = (e, t) => {
13
+ const [r, c] = e.split(/\?/), a = new URLSearchParams(c), n = O(t, a), s = [r], o = n.toString();
14
+ return o.length > 0 && s.push(`?${o}`), s.join("");
15
+ }, L = (e, t) => {
16
+ const [r, c] = e.split(/\?/), a = new URLSearchParams(c);
17
+ t.forEach((o) => {
18
+ a.delete(o);
30
19
  });
31
- });
32
-
33
- var _fetchJSON = require("./fetchJSON");
34
-
35
- Object.keys(_fetchJSON).forEach(function (key) {
36
- if (key === "default" || key === "__esModule") return;
37
- if (key in exports && exports[key] === _fetchJSON[key]) return;
38
- Object.defineProperty(exports, key, {
39
- enumerable: true,
40
- get: function get() {
41
- return _fetchJSON[key];
20
+ const n = [r], s = a.toString();
21
+ return s.length > 0 && n.push(`?${s}`), n.join("");
22
+ }, v = (e = []) => e.length > 0 ? e[0] : void 0, d = async (e, ...t) => {
23
+ try {
24
+ return await e(...t);
25
+ } catch {
26
+ const c = e ? e.name : "anonymous";
27
+ console.warn(`[CreateService Error] ${c} failed with args:`, t);
28
+ return;
29
+ }
30
+ };
31
+ function C(e) {
32
+ return e;
33
+ }
34
+ function D({ endpoints: e, basePath: t, fetcher: r }) {
35
+ return Object.keys(e).reduce((c, a) => (c[a] = async (...n) => {
36
+ const { url: s, body: o, headers: i, method: u, params: f, fetchOpts: l, transform: w } = e[a];
37
+ let h = typeof s == "function" ? await d(s, ...n) : s;
38
+ if (typeof h != "string")
39
+ throw console.error(
40
+ "[Create Service Error]: URL must be a string or a function returning a string",
41
+ h
42
+ ), new Error(
43
+ `URL must be a string or a function returning a string for method ${a}.`
44
+ );
45
+ t && (h = P(t, h));
46
+ const S = typeof o == "function" ? await d(o, ...n) : u !== "GET" ? v(n) : void 0, g = typeof i == "function" ? await d(i, ...n) : i, m = typeof f == "function" ? await d(f, ...n) : f, E = { ...l || {}, body: S, headers: g, method: u };
47
+ m && (h = U(h, m));
48
+ const p = await r.fetch(h, E);
49
+ return w ? w(p) : p;
50
+ }, c), {});
51
+ }
52
+ class I extends Error {
53
+ constructor() {
54
+ super(...arguments);
55
+ y(this, "response");
56
+ y(this, "data");
57
+ }
58
+ }
59
+ async function J(e, t) {
60
+ const { serializeBody: r = !0, method: c, ...a } = t || {}, n = c ?? "GET", s = {
61
+ ...a,
62
+ method: n,
63
+ headers: {
64
+ "Content-type": "application/json",
65
+ ...a.headers
66
+ },
67
+ body: n === "GET" ? void 0 : r ? JSON.stringify(t == null ? void 0 : t.body) : t == null ? void 0 : t.body
68
+ };
69
+ try {
70
+ const o = await fetch(e, s);
71
+ if (o.status < 200 || o.status > 300) {
72
+ let u;
73
+ try {
74
+ u = await o.json();
75
+ } catch {
76
+ u = { error: "JSON_ERROR_NOT_RECEIVED" };
77
+ }
78
+ const f = new I(u.error);
79
+ throw f.response = o, f.data = u, f;
42
80
  }
43
- });
44
- });
45
-
46
- var _withRetries = require("./withRetries");
47
-
48
- Object.keys(_withRetries).forEach(function (key) {
49
- if (key === "default" || key === "__esModule") return;
50
- if (key in exports && exports[key] === _withRetries[key]) return;
51
- Object.defineProperty(exports, key, {
52
- enumerable: true,
53
- get: function get() {
54
- return _withRetries[key];
81
+ return await o.json();
82
+ } catch (o) {
83
+ throw console.error(">>> err", o), o;
84
+ }
85
+ }
86
+ const N = (e, { retries: t = 2, onTryError: r, onFail: c } = {}) => async (...n) => {
87
+ let s = 0;
88
+ for (; s < t; )
89
+ try {
90
+ return await e(...n);
91
+ } catch (o) {
92
+ if (s < t)
93
+ try {
94
+ await (r == null ? void 0 : r(o, n, s));
95
+ } catch (i) {
96
+ throw await (c == null ? void 0 : c(i, n)), i;
97
+ }
98
+ if (s += 1, s >= t)
99
+ throw await (c == null ? void 0 : c(o, n)), o;
55
100
  }
56
- });
57
- });
58
-
59
- var _combinePathWithBase = require("./combinePathWithBase");
60
-
61
- Object.keys(_combinePathWithBase).forEach(function (key) {
62
- if (key === "default" || key === "__esModule") return;
63
- if (key in exports && exports[key] === _combinePathWithBase[key]) return;
64
- Object.defineProperty(exports, key, {
65
- enumerable: true,
66
- get: function get() {
67
- return _combinePathWithBase[key];
68
- }
69
- });
70
- });
71
-
72
- var _urlHelpers = require("./url-helpers");
73
-
74
- Object.keys(_urlHelpers).forEach(function (key) {
75
- if (key === "default" || key === "__esModule") return;
76
- if (key in exports && exports[key] === _urlHelpers[key]) return;
77
- Object.defineProperty(exports, key, {
78
- enumerable: true,
79
- get: function get() {
80
- return _urlHelpers[key];
81
- }
82
- });
83
- });
84
-
85
- var _tryParse = require("./try-parse");
86
-
87
- Object.keys(_tryParse).forEach(function (key) {
88
- if (key === "default" || key === "__esModule") return;
89
- if (key in exports && exports[key] === _tryParse[key]) return;
90
- Object.defineProperty(exports, key, {
91
- enumerable: true,
92
- get: function get() {
93
- return _tryParse[key];
94
- }
95
- });
96
- });
97
-
98
- var _ls = require("./ls");
99
-
100
- Object.keys(_ls).forEach(function (key) {
101
- if (key === "default" || key === "__esModule") return;
102
- if (key in exports && exports[key] === _ls[key]) return;
103
- Object.defineProperty(exports, key, {
104
- enumerable: true,
105
- get: function get() {
106
- return _ls[key];
107
- }
108
- });
109
- });
110
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFDQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFDQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFDQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFDQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFDQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFDQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTs7QUFDQTs7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQSIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCAqIGZyb20gJy4vY3JlYXRlLXNlcnZpY2UnO1xuZXhwb3J0ICogZnJvbSAnLi9mZXRjaC10eXBlcyc7XG5leHBvcnQgKiBmcm9tICcuL2ZldGNoSlNPTic7XG5leHBvcnQgKiBmcm9tICcuL3dpdGhSZXRyaWVzJztcbmV4cG9ydCAqIGZyb20gJy4vY29tYmluZVBhdGhXaXRoQmFzZSc7XG5leHBvcnQgKiBmcm9tICcuL3VybC1oZWxwZXJzJztcbmV4cG9ydCAqIGZyb20gJy4vdHJ5LXBhcnNlJztcbmV4cG9ydCAqIGZyb20gJy4vbHMnO1xuIl19
101
+ }, $ = (e, t) => {
102
+ try {
103
+ return JSON.parse(e);
104
+ } catch {
105
+ return t;
106
+ }
107
+ }, A = (e, t) => {
108
+ const r = localStorage.getItem(e);
109
+ return $(r) ?? t;
110
+ }, x = (e, t) => {
111
+ const r = JSON.stringify(t);
112
+ localStorage.setItem(e, r);
113
+ }, G = (e) => {
114
+ localStorage.removeItem(e);
115
+ };
116
+ export {
117
+ I as ResponseError,
118
+ P as combinePathWithBase,
119
+ C as createEndpoint,
120
+ D as createService,
121
+ J as fetchJSON,
122
+ A as getItem,
123
+ O as paramsToURLSearchParams,
124
+ G as removeItem,
125
+ L as removeQueryParam,
126
+ x as setItem,
127
+ U as setQueryParams,
128
+ $ as tryParse,
129
+ N as withRetries
130
+ };
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/combinePathWithBase.ts","../src/url-helpers.ts","../src/create-service.ts","../src/fetchJSON.ts","../src/withRetries.ts","../src/try-parse.ts","../src/ls.ts"],"sourcesContent":["export const combinePathWithBase = (host: string, path: string) => {\n host = host.replace(/\\/$/, '');\n path = path.replace(/^\\//, '');\n return `${host}/${path}`;\n};\n","export const paramsToURLSearchParams = <T extends object>(\n params: T,\n urlParams?: URLSearchParams,\n) => {\n const urlSearchParams = urlParams ?? new URLSearchParams();\n\n const keys = Object.keys(params);\n keys.forEach((key) => {\n const val = params[key as keyof T];\n if (typeof val !== 'undefined') {\n if (Array.isArray(val)) {\n val.forEach((v) => {\n urlSearchParams.append(key, v);\n });\n } else {\n urlSearchParams.set(key, `${val}`);\n }\n }\n });\n\n return urlSearchParams;\n};\n\nexport const setQueryParams = <T extends object>(url: string, params: T) => {\n const [baseString, searchString] = url.split(/\\?/);\n\n const urlParams = new URLSearchParams(searchString);\n const mergeParams = paramsToURLSearchParams(params, urlParams);\n\n const parts = [baseString];\n\n const paramsToAdd = mergeParams.toString();\n\n if (paramsToAdd.length > 0) {\n parts.push(`?${paramsToAdd}`);\n }\n\n return parts.join('');\n};\n\nexport const removeQueryParam = (url: string, params: string[]) => {\n const [baseString, searchString] = url.split(/\\?/);\n\n const urlParams = new URLSearchParams(searchString);\n\n params.forEach((param) => {\n urlParams.delete(param);\n });\n\n const parts = [baseString];\n\n const paramsToAdd = urlParams.toString();\n\n if (paramsToAdd.length > 0) {\n parts.push(`?${paramsToAdd}`);\n }\n\n return parts.join('');\n};\n","import { combinePathWithBase } from './combinePathWithBase';\nimport { Fetcher, FetchOptions } from './fetch-types';\nimport { setQueryParams } from './url-helpers';\n\nconst getDefaultData = (args: any[] = []) => (args.length > 0 ? args[0] : undefined);\n\nconst tryCall = async (fn: Function, ...args: any[]) => {\n try {\n return await fn(...args);\n } catch (err) {\n const fnName = fn ? fn.name : 'anonymous';\n console.warn(`[CreateService Error] ${fnName} failed with args:`, args);\n return undefined;\n }\n};\n\n// ─── Legacy types (kept for backward compat with createService<T>) ───\n\nexport interface Fn<T extends Array<any>> {\n (...args: T): string;\n}\n\nexport type ServiceFn = (...args: any[]) => Promise<any>;\n\nexport type StringOrFn<T extends ServiceFn> = string | Fn<Parameters<T>>;\nexport type ObjectOrFn<T extends ServiceFn> = object | Fn<Parameters<T>>;\n\nexport interface Descriptor<K extends ServiceFn> {\n url: StringOrFn<K>;\n method?: HttpMethod;\n body?: ObjectOrFn<K>;\n headers?: ObjectOrFn<K>;\n params?: ObjectOrFn<K>;\n fetchOpts?: FetchOptions;\n transform?: (data: any) => any;\n}\n\nexport type ServiceDescriptor<T> = {\n [K in keyof T]: Descriptor<T[K] extends ServiceFn ? T[K] : never>;\n};\n\nexport interface CreateServiceArgs<T> {\n endpoints: ServiceDescriptor<T>;\n basePath?: string;\n fetcher: Fetcher;\n}\n\n// ─── New inferred API types ───\n\nexport type HttpMethod =\n | 'GET'\n | 'POST'\n | 'PUT'\n | 'DELETE'\n | 'PATCH'\n | 'HEAD'\n | 'OPTIONS'\n | 'TRACE'\n | 'CONNECT';\n\nexport interface DescriptorBase {\n url: string | ((...args: any[]) => string);\n method?: HttpMethod;\n body?: object | ((...args: any[]) => any);\n headers?: object | ((...args: any[]) => any);\n params?: object | ((...args: any[]) => any);\n fetchOpts?: FetchOptions;\n transform?: (data: any) => any;\n}\n\n// ─── createEndpoint helper ───\n\n/** @internal Type brand symbol — not present at runtime */\ndeclare const ENDPOINT_TYPES: unique symbol;\n\n/** A descriptor branded with explicit TResponse, TArgs, TError types */\nexport interface TypedEndpoint<\n TResponse = any,\n TArgs = void,\n TError = Error,\n> extends DescriptorBase {\n readonly [ENDPOINT_TYPES]: { args: TArgs; response: TResponse; error: TError };\n}\n\n/** Configuration accepted by createEndpoint */\nexport interface EndpointConfig<TResponse, TArgs> {\n url: string | ((args: TArgs) => string);\n method?: HttpMethod;\n body?: object | ((args: TArgs) => any);\n headers?: object | ((args: TArgs) => any);\n params?: object | ((args: TArgs) => any);\n fetchOpts?: FetchOptions;\n transform?: (data: any) => TResponse;\n}\n\n/**\n * Define a typed endpoint with explicit response/input/error types.\n *\n * @example\n * ```ts\n * // No args:\n * createEndpoint<User[]>({ url: '/users' })\n * // service method: () => Promise<User[]>\n *\n * // With args:\n * createEndpoint<User, { id: string }>({\n * url: ({ id }) => `/users/${id}`,\n * })\n * // service method: (args: { id: string }) => Promise<User>\n * ```\n */\nexport function createEndpoint<TResponse = any, TArgs = void, TError = Error>(\n config: EndpointConfig<TResponse, TArgs>,\n): TypedEndpoint<TResponse, TArgs, TError> {\n return config as any;\n}\n\n// ─── Type inference ───\n\n/** Infer the service method args from a descriptor */\nexport type InferArgs<D> =\n // 1. Branded endpoint — use the explicit TArgs\n D extends { readonly [ENDPOINT_TYPES]: { args: infer A } }\n ? [A] extends [void]\n ? []\n : [args: A]\n : // 2. Infer from descriptor functions (url → body → params → headers)\n D extends { url: (...args: infer P) => any }\n ? P\n : D extends { body: (...args: infer P) => any }\n ? P\n : D extends { params: (...args: infer P) => any }\n ? P\n : D extends { headers: (...args: infer P) => any }\n ? P\n : any[];\n\n/** Infer the service method return type from a descriptor */\nexport type InferReturn<D> =\n // 1. Branded endpoint — use the explicit TResponse\n D extends { readonly [ENDPOINT_TYPES]: { response: infer R } }\n ? R\n : // 2. Infer from transform return type\n D extends { transform: (data: any) => infer R }\n ? R\n : any;\n\n/** Infer the error type from a branded descriptor */\nexport type InferError<D> = D extends {\n readonly [ENDPOINT_TYPES]: { error: infer E };\n}\n ? E\n : Error;\n\nexport type InferService<T extends Record<string, DescriptorBase>> = {\n [K in keyof T]: (...args: InferArgs<T[K]>) => Promise<InferReturn<T[K]>>;\n};\n\n// ─── Overloads ───\n\n/** Create a service with types inferred from the endpoint descriptors */\nexport function createService<const T extends Record<string, DescriptorBase>>(args: {\n endpoints: T;\n basePath?: string;\n fetcher: Fetcher;\n}): InferService<T>;\n\n/** @deprecated Use the inferred API (call without explicit generic) */\nexport function createService<T>(args: CreateServiceArgs<T>): T;\n\n// ─── Implementation ───\n\nexport function createService({ endpoints, basePath, fetcher }: any): any {\n return Object.keys(endpoints).reduce((service: any, serviceName: string) => {\n service[serviceName] = async (...args: any[]) => {\n const { url, body, headers, method, params, fetchOpts, transform } = endpoints[serviceName];\n\n let urlToUse: string = typeof url === 'function' ? await tryCall(url, ...args) : url;\n if (typeof urlToUse !== 'string') {\n console.error(\n '[Create Service Error]: URL must be a string or a function returning a string',\n urlToUse,\n );\n throw new Error(\n `URL must be a string or a function returning a string for method ${serviceName}.`,\n );\n }\n\n // TODO: detect absolute urlToUse and prevent adding the base path\n if (basePath) {\n urlToUse = combinePathWithBase(basePath, urlToUse);\n }\n\n const getDefault = () => (method !== 'GET' ? getDefaultData(args) : undefined);\n\n // if data is a function we pass all the arguments to the function so it creates\n // the payload with all the provided arguments, expecting it to return a single object\n // if the data is not a function then we get the first argument passed to the function as the\n // data payload. If this is an object it will be serialized using json stringify before sending it\n const bodyToUse = typeof body === 'function' ? await tryCall(body, ...args) : getDefault();\n const headersToUse =\n typeof headers === 'function' ? await tryCall(headers, ...args) : headers;\n\n const paramsToUse = typeof params === 'function' ? await tryCall(params, ...args) : params;\n const opts = { ...(fetchOpts || {}), body: bodyToUse, headers: headersToUse, method };\n\n if (paramsToUse) {\n urlToUse = setQueryParams(urlToUse, paramsToUse);\n }\n\n const result = await fetcher.fetch(urlToUse, opts);\n return transform ? transform(result) : result;\n };\n\n return service;\n }, {} as any);\n}\n","import { FetchOptions, FetchURL, SerializeBodyProps } from './fetch-types';\n\nexport interface ErrorData {\n error: string;\n}\nexport class ResponseError extends Error {\n response?: Response;\n\n data?: ErrorData;\n}\n\nexport async function fetchJSON<T, K>(url: FetchURL, params?: SerializeBodyProps<K>): Promise<T> {\n const { serializeBody = true, method, ...rest } = params || {};\n const fetchMethod = method ?? 'GET';\n\n const props: FetchOptions = {\n ...rest,\n method: fetchMethod,\n headers: {\n 'Content-type': 'application/json',\n ...rest.headers,\n },\n body:\n fetchMethod === 'GET'\n ? undefined\n : ((serializeBody ? JSON.stringify(params?.body) : params?.body) as FetchOptions['body']),\n };\n\n try {\n const resp = await fetch(url, props);\n if (resp.status < 200 || resp.status > 300) {\n let data: ErrorData;\n try {\n data = await resp.json();\n } catch (parseError) {\n data = { error: 'JSON_ERROR_NOT_RECEIVED' };\n }\n const error = new ResponseError(data.error);\n error.response = resp;\n error.data = data;\n\n throw error;\n }\n const data = await resp.json();\n return data;\n } catch (err) {\n console.error('>>> err', err);\n throw err;\n }\n}\n\nexport type FetchFn = typeof fetchJSON;\n","export interface WithRetriesOpts<K> {\n retries?: number;\n onFail?: (error: Error, args: K) => Promise<void> | void;\n onTryError?: (error: Error, args: K, attemptIndex: number) => Promise<void> | void;\n}\n\nexport const withRetries = <K extends any[], T>(\n fn: (...args: K) => Promise<T>,\n { retries = 2, onTryError, onFail }: WithRetriesOpts<K> = {},\n) => {\n const retFn = async (...args: K) => {\n let attemptIndex = 0;\n\n while (attemptIndex < retries) {\n try {\n const ret = await fn(...args); // eslint-disable-line no-await-in-loop\n\n return ret as T;\n } catch (err) {\n if (attemptIndex < retries) {\n try {\n await onTryError?.(err as Error, args, attemptIndex); // eslint-disable-line no-await-in-loop\n } catch (error) {\n await onFail?.(error as Error, args); // eslint-disable-line no-await-in-loop\n throw error;\n }\n }\n attemptIndex += 1;\n if (attemptIndex >= retries) {\n await onFail?.(err as Error, args); // eslint-disable-line no-await-in-loop\n throw err;\n }\n }\n }\n };\n\n return retFn as (...args: K) => Promise<T>;\n};\n","export const tryParse = <T>(val: string | null, def?: T) => {\n try {\n return JSON.parse(val!) as T;\n } catch (err) {\n return def;\n }\n};\n","import { tryParse } from './try-parse';\n\nexport const getItem = <T>(key: string, defaultValue: T) => {\n const entity = localStorage.getItem(key);\n\n return tryParse(entity) ?? defaultValue;\n};\n\nexport const setItem = <T>(key: string, entity: T) => {\n const str = JSON.stringify(entity);\n\n localStorage.setItem(key, str);\n};\n\nexport const removeItem = (key: string) => {\n localStorage.removeItem(key);\n};\n"],"names":["combinePathWithBase","host","path","paramsToURLSearchParams","params","urlParams","urlSearchParams","key","val","v","setQueryParams","url","baseString","searchString","mergeParams","parts","paramsToAdd","removeQueryParam","param","getDefaultData","args","tryCall","fn","fnName","createEndpoint","config","createService","endpoints","basePath","fetcher","service","serviceName","body","headers","method","fetchOpts","transform","urlToUse","bodyToUse","headersToUse","paramsToUse","opts","result","ResponseError","__publicField","fetchJSON","serializeBody","rest","fetchMethod","props","resp","data","error","err","withRetries","retries","onTryError","onFail","attemptIndex","tryParse","def","getItem","defaultValue","entity","setItem","str","removeItem"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ (function(r,u){typeof exports=="object"&&typeof module<"u"?u(exports):typeof define=="function"&&define.amd?define(["exports"],u):(r=typeof globalThis<"u"?globalThis:r||self,u(r.serviceCreator={}))})(this,function(r){"use strict";var A=Object.defineProperty;var Q=(r,u,f)=>u in r?A(r,u,{enumerable:!0,configurable:!0,writable:!0,value:f}):r[u]=f;var w=(r,u,f)=>(Q(r,typeof u!="symbol"?u+"":u,f),f);const u=(t,e)=>(t=t.replace(/\/$/,""),e=e.replace(/^\//,""),`${t}/${e}`),f=(t,e)=>{const o=e??new URLSearchParams;return Object.keys(t).forEach(i=>{const n=t[i];typeof n<"u"&&(Array.isArray(n)?n.forEach(s=>{o.append(i,s)}):o.set(i,`${n}`))}),o},S=(t,e)=>{const[o,c]=t.split(/\?/),i=new URLSearchParams(c),n=f(e,i),s=[o],a=n.toString();return a.length>0&&s.push(`?${a}`),s.join("")},v=(t,e)=>{const[o,c]=t.split(/\?/),i=new URLSearchParams(c);e.forEach(a=>{i.delete(a)});const n=[o],s=i.toString();return s.length>0&&n.push(`?${s}`),n.join("")},O=(t=[])=>t.length>0?t[0]:void 0,l=async(t,...e)=>{try{return await t(...e)}catch{const c=t?t.name:"anonymous";console.warn(`[CreateService Error] ${c} failed with args:`,e);return}};function I(t){return t}function T({endpoints:t,basePath:e,fetcher:o}){return Object.keys(t).reduce((c,i)=>(c[i]=async(...n)=>{const{url:s,body:a,headers:d,method:h,params:y,fetchOpts:P,transform:b}=t[i];let m=typeof s=="function"?await l(s,...n):s;if(typeof m!="string")throw console.error("[Create Service Error]: URL must be a string or a function returning a string",m),new Error(`URL must be a string or a function returning a string for method ${i}.`);e&&(m=u(e,m));const J=typeof a=="function"?await l(a,...n):h!=="GET"?O(n):void 0,N=typeof d=="function"?await l(d,...n):d,E=typeof y=="function"?await l(y,...n):y,D={...P||{},body:J,headers:N,method:h};E&&(m=S(m,E));const R=await o.fetch(m,D);return b?b(R):R},c),{})}class g extends Error{constructor(){super(...arguments);w(this,"response");w(this,"data")}}async function U(t,e){const{serializeBody:o=!0,method:c,...i}=e||{},n=c??"GET",s={...i,method:n,headers:{"Content-type":"application/json",...i.headers},body:n==="GET"?void 0:o?JSON.stringify(e==null?void 0:e.body):e==null?void 0:e.body};try{const a=await fetch(t,s);if(a.status<200||a.status>300){let h;try{h=await a.json()}catch{h={error:"JSON_ERROR_NOT_RECEIVED"}}const y=new g(h.error);throw y.response=a,y.data=h,y}return await a.json()}catch(a){throw console.error(">>> err",a),a}}const j=(t,{retries:e=2,onTryError:o,onFail:c}={})=>async(...n)=>{let s=0;for(;s<e;)try{return await t(...n)}catch(a){if(s<e)try{await(o==null?void 0:o(a,n,s))}catch(d){throw await(c==null?void 0:c(d,n)),d}if(s+=1,s>=e)throw await(c==null?void 0:c(a,n)),a}},p=(t,e)=>{try{return JSON.parse(t)}catch{return e}},$=(t,e)=>{const o=localStorage.getItem(t);return p(o)??e},L=(t,e)=>{const o=JSON.stringify(e);localStorage.setItem(t,o)},C=t=>{localStorage.removeItem(t)};r.ResponseError=g,r.combinePathWithBase=u,r.createEndpoint=I,r.createService=T,r.fetchJSON=U,r.getItem=$,r.paramsToURLSearchParams=f,r.removeItem=C,r.removeQueryParam=v,r.setItem=L,r.setQueryParams=S,r.tryParse=p,r.withRetries=j,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});
2
+ //# sourceMappingURL=index.umd.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.cjs","sources":["../src/combinePathWithBase.ts","../src/url-helpers.ts","../src/create-service.ts","../src/fetchJSON.ts","../src/withRetries.ts","../src/try-parse.ts","../src/ls.ts"],"sourcesContent":["export const combinePathWithBase = (host: string, path: string) => {\n host = host.replace(/\\/$/, '');\n path = path.replace(/^\\//, '');\n return `${host}/${path}`;\n};\n","export const paramsToURLSearchParams = <T extends object>(\n params: T,\n urlParams?: URLSearchParams,\n) => {\n const urlSearchParams = urlParams ?? new URLSearchParams();\n\n const keys = Object.keys(params);\n keys.forEach((key) => {\n const val = params[key as keyof T];\n if (typeof val !== 'undefined') {\n if (Array.isArray(val)) {\n val.forEach((v) => {\n urlSearchParams.append(key, v);\n });\n } else {\n urlSearchParams.set(key, `${val}`);\n }\n }\n });\n\n return urlSearchParams;\n};\n\nexport const setQueryParams = <T extends object>(url: string, params: T) => {\n const [baseString, searchString] = url.split(/\\?/);\n\n const urlParams = new URLSearchParams(searchString);\n const mergeParams = paramsToURLSearchParams(params, urlParams);\n\n const parts = [baseString];\n\n const paramsToAdd = mergeParams.toString();\n\n if (paramsToAdd.length > 0) {\n parts.push(`?${paramsToAdd}`);\n }\n\n return parts.join('');\n};\n\nexport const removeQueryParam = (url: string, params: string[]) => {\n const [baseString, searchString] = url.split(/\\?/);\n\n const urlParams = new URLSearchParams(searchString);\n\n params.forEach((param) => {\n urlParams.delete(param);\n });\n\n const parts = [baseString];\n\n const paramsToAdd = urlParams.toString();\n\n if (paramsToAdd.length > 0) {\n parts.push(`?${paramsToAdd}`);\n }\n\n return parts.join('');\n};\n","import { combinePathWithBase } from './combinePathWithBase';\nimport { Fetcher, FetchOptions } from './fetch-types';\nimport { setQueryParams } from './url-helpers';\n\nconst getDefaultData = (args: any[] = []) => (args.length > 0 ? args[0] : undefined);\n\nconst tryCall = async (fn: Function, ...args: any[]) => {\n try {\n return await fn(...args);\n } catch (err) {\n const fnName = fn ? fn.name : 'anonymous';\n console.warn(`[CreateService Error] ${fnName} failed with args:`, args);\n return undefined;\n }\n};\n\n// ─── Legacy types (kept for backward compat with createService<T>) ───\n\nexport interface Fn<T extends Array<any>> {\n (...args: T): string;\n}\n\nexport type ServiceFn = (...args: any[]) => Promise<any>;\n\nexport type StringOrFn<T extends ServiceFn> = string | Fn<Parameters<T>>;\nexport type ObjectOrFn<T extends ServiceFn> = object | Fn<Parameters<T>>;\n\nexport interface Descriptor<K extends ServiceFn> {\n url: StringOrFn<K>;\n method?: HttpMethod;\n body?: ObjectOrFn<K>;\n headers?: ObjectOrFn<K>;\n params?: ObjectOrFn<K>;\n fetchOpts?: FetchOptions;\n transform?: (data: any) => any;\n}\n\nexport type ServiceDescriptor<T> = {\n [K in keyof T]: Descriptor<T[K] extends ServiceFn ? T[K] : never>;\n};\n\nexport interface CreateServiceArgs<T> {\n endpoints: ServiceDescriptor<T>;\n basePath?: string;\n fetcher: Fetcher;\n}\n\n// ─── New inferred API types ───\n\nexport type HttpMethod =\n | 'GET'\n | 'POST'\n | 'PUT'\n | 'DELETE'\n | 'PATCH'\n | 'HEAD'\n | 'OPTIONS'\n | 'TRACE'\n | 'CONNECT';\n\nexport interface DescriptorBase {\n url: string | ((...args: any[]) => string);\n method?: HttpMethod;\n body?: object | ((...args: any[]) => any);\n headers?: object | ((...args: any[]) => any);\n params?: object | ((...args: any[]) => any);\n fetchOpts?: FetchOptions;\n transform?: (data: any) => any;\n}\n\n// ─── createEndpoint helper ───\n\n/** @internal Type brand symbol — not present at runtime */\ndeclare const ENDPOINT_TYPES: unique symbol;\n\n/** A descriptor branded with explicit TResponse, TArgs, TError types */\nexport interface TypedEndpoint<\n TResponse = any,\n TArgs = void,\n TError = Error,\n> extends DescriptorBase {\n readonly [ENDPOINT_TYPES]: { args: TArgs; response: TResponse; error: TError };\n}\n\n/** Configuration accepted by createEndpoint */\nexport interface EndpointConfig<TResponse, TArgs> {\n url: string | ((args: TArgs) => string);\n method?: HttpMethod;\n body?: object | ((args: TArgs) => any);\n headers?: object | ((args: TArgs) => any);\n params?: object | ((args: TArgs) => any);\n fetchOpts?: FetchOptions;\n transform?: (data: any) => TResponse;\n}\n\n/**\n * Define a typed endpoint with explicit response/input/error types.\n *\n * @example\n * ```ts\n * // No args:\n * createEndpoint<User[]>({ url: '/users' })\n * // service method: () => Promise<User[]>\n *\n * // With args:\n * createEndpoint<User, { id: string }>({\n * url: ({ id }) => `/users/${id}`,\n * })\n * // service method: (args: { id: string }) => Promise<User>\n * ```\n */\nexport function createEndpoint<TResponse = any, TArgs = void, TError = Error>(\n config: EndpointConfig<TResponse, TArgs>,\n): TypedEndpoint<TResponse, TArgs, TError> {\n return config as any;\n}\n\n// ─── Type inference ───\n\n/** Infer the service method args from a descriptor */\nexport type InferArgs<D> =\n // 1. Branded endpoint — use the explicit TArgs\n D extends { readonly [ENDPOINT_TYPES]: { args: infer A } }\n ? [A] extends [void]\n ? []\n : [args: A]\n : // 2. Infer from descriptor functions (url → body → params → headers)\n D extends { url: (...args: infer P) => any }\n ? P\n : D extends { body: (...args: infer P) => any }\n ? P\n : D extends { params: (...args: infer P) => any }\n ? P\n : D extends { headers: (...args: infer P) => any }\n ? P\n : any[];\n\n/** Infer the service method return type from a descriptor */\nexport type InferReturn<D> =\n // 1. Branded endpoint — use the explicit TResponse\n D extends { readonly [ENDPOINT_TYPES]: { response: infer R } }\n ? R\n : // 2. Infer from transform return type\n D extends { transform: (data: any) => infer R }\n ? R\n : any;\n\n/** Infer the error type from a branded descriptor */\nexport type InferError<D> = D extends {\n readonly [ENDPOINT_TYPES]: { error: infer E };\n}\n ? E\n : Error;\n\nexport type InferService<T extends Record<string, DescriptorBase>> = {\n [K in keyof T]: (...args: InferArgs<T[K]>) => Promise<InferReturn<T[K]>>;\n};\n\n// ─── Overloads ───\n\n/** Create a service with types inferred from the endpoint descriptors */\nexport function createService<const T extends Record<string, DescriptorBase>>(args: {\n endpoints: T;\n basePath?: string;\n fetcher: Fetcher;\n}): InferService<T>;\n\n/** @deprecated Use the inferred API (call without explicit generic) */\nexport function createService<T>(args: CreateServiceArgs<T>): T;\n\n// ─── Implementation ───\n\nexport function createService({ endpoints, basePath, fetcher }: any): any {\n return Object.keys(endpoints).reduce((service: any, serviceName: string) => {\n service[serviceName] = async (...args: any[]) => {\n const { url, body, headers, method, params, fetchOpts, transform } = endpoints[serviceName];\n\n let urlToUse: string = typeof url === 'function' ? await tryCall(url, ...args) : url;\n if (typeof urlToUse !== 'string') {\n console.error(\n '[Create Service Error]: URL must be a string or a function returning a string',\n urlToUse,\n );\n throw new Error(\n `URL must be a string or a function returning a string for method ${serviceName}.`,\n );\n }\n\n // TODO: detect absolute urlToUse and prevent adding the base path\n if (basePath) {\n urlToUse = combinePathWithBase(basePath, urlToUse);\n }\n\n const getDefault = () => (method !== 'GET' ? getDefaultData(args) : undefined);\n\n // if data is a function we pass all the arguments to the function so it creates\n // the payload with all the provided arguments, expecting it to return a single object\n // if the data is not a function then we get the first argument passed to the function as the\n // data payload. If this is an object it will be serialized using json stringify before sending it\n const bodyToUse = typeof body === 'function' ? await tryCall(body, ...args) : getDefault();\n const headersToUse =\n typeof headers === 'function' ? await tryCall(headers, ...args) : headers;\n\n const paramsToUse = typeof params === 'function' ? await tryCall(params, ...args) : params;\n const opts = { ...(fetchOpts || {}), body: bodyToUse, headers: headersToUse, method };\n\n if (paramsToUse) {\n urlToUse = setQueryParams(urlToUse, paramsToUse);\n }\n\n const result = await fetcher.fetch(urlToUse, opts);\n return transform ? transform(result) : result;\n };\n\n return service;\n }, {} as any);\n}\n","import { FetchOptions, FetchURL, SerializeBodyProps } from './fetch-types';\n\nexport interface ErrorData {\n error: string;\n}\nexport class ResponseError extends Error {\n response?: Response;\n\n data?: ErrorData;\n}\n\nexport async function fetchJSON<T, K>(url: FetchURL, params?: SerializeBodyProps<K>): Promise<T> {\n const { serializeBody = true, method, ...rest } = params || {};\n const fetchMethod = method ?? 'GET';\n\n const props: FetchOptions = {\n ...rest,\n method: fetchMethod,\n headers: {\n 'Content-type': 'application/json',\n ...rest.headers,\n },\n body:\n fetchMethod === 'GET'\n ? undefined\n : ((serializeBody ? JSON.stringify(params?.body) : params?.body) as FetchOptions['body']),\n };\n\n try {\n const resp = await fetch(url, props);\n if (resp.status < 200 || resp.status > 300) {\n let data: ErrorData;\n try {\n data = await resp.json();\n } catch (parseError) {\n data = { error: 'JSON_ERROR_NOT_RECEIVED' };\n }\n const error = new ResponseError(data.error);\n error.response = resp;\n error.data = data;\n\n throw error;\n }\n const data = await resp.json();\n return data;\n } catch (err) {\n console.error('>>> err', err);\n throw err;\n }\n}\n\nexport type FetchFn = typeof fetchJSON;\n","export interface WithRetriesOpts<K> {\n retries?: number;\n onFail?: (error: Error, args: K) => Promise<void> | void;\n onTryError?: (error: Error, args: K, attemptIndex: number) => Promise<void> | void;\n}\n\nexport const withRetries = <K extends any[], T>(\n fn: (...args: K) => Promise<T>,\n { retries = 2, onTryError, onFail }: WithRetriesOpts<K> = {},\n) => {\n const retFn = async (...args: K) => {\n let attemptIndex = 0;\n\n while (attemptIndex < retries) {\n try {\n const ret = await fn(...args); // eslint-disable-line no-await-in-loop\n\n return ret as T;\n } catch (err) {\n if (attemptIndex < retries) {\n try {\n await onTryError?.(err as Error, args, attemptIndex); // eslint-disable-line no-await-in-loop\n } catch (error) {\n await onFail?.(error as Error, args); // eslint-disable-line no-await-in-loop\n throw error;\n }\n }\n attemptIndex += 1;\n if (attemptIndex >= retries) {\n await onFail?.(err as Error, args); // eslint-disable-line no-await-in-loop\n throw err;\n }\n }\n }\n };\n\n return retFn as (...args: K) => Promise<T>;\n};\n","export const tryParse = <T>(val: string | null, def?: T) => {\n try {\n return JSON.parse(val!) as T;\n } catch (err) {\n return def;\n }\n};\n","import { tryParse } from './try-parse';\n\nexport const getItem = <T>(key: string, defaultValue: T) => {\n const entity = localStorage.getItem(key);\n\n return tryParse(entity) ?? defaultValue;\n};\n\nexport const setItem = <T>(key: string, entity: T) => {\n const str = JSON.stringify(entity);\n\n localStorage.setItem(key, str);\n};\n\nexport const removeItem = (key: string) => {\n localStorage.removeItem(key);\n};\n"],"names":["combinePathWithBase","host","path","paramsToURLSearchParams","params","urlParams","urlSearchParams","key","val","v","setQueryParams","url","baseString","searchString","mergeParams","parts","paramsToAdd","removeQueryParam","param","getDefaultData","args","tryCall","fn","fnName","createEndpoint","config","createService","endpoints","basePath","fetcher","service","serviceName","body","headers","method","fetchOpts","transform","urlToUse","bodyToUse","headersToUse","paramsToUse","opts","result","ResponseError","__publicField","fetchJSON","serializeBody","rest","fetchMethod","props","resp","data","error","err","withRetries","retries","onTryError","onFail","attemptIndex","tryParse","def","getItem","defaultValue","entity","setItem","str","removeItem"],"mappings":"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"}