@thepassle/app-tools 1.0.0 → 1.1.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/api/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { createLogger } from '../utils/log.js';
2
- const log = createLogger('api');
1
+ import { createLogger } from "../utils/log.js";
2
+ const log = createLogger("api");
3
3
 
4
4
  class StatusError extends Error {
5
5
  constructor(response) {
@@ -10,7 +10,7 @@ class StatusError extends Error {
10
10
 
11
11
  function handleStatus(response) {
12
12
  if (!response.ok) {
13
- log('Response not ok', response);
13
+ log("Response not ok", response);
14
14
  throw new StatusError(response);
15
15
  }
16
16
  return response;
@@ -24,14 +24,14 @@ function handleStatus(response) {
24
24
  /** @typedef {import('./types.js').MetaParams} MetaParams */
25
25
 
26
26
  /**
27
- * @example
27
+ * @example
28
28
  * const api = new Api({
29
29
  * baseURL: 'https://api.foo.com/',
30
30
  * responseType: 'text',
31
31
  * plugins: [
32
32
  * {
33
33
  * beforeFetch: ({url, method, opts, data}) => {},
34
- * afterFetch: (res) => res,
34
+ * afterFetch: ({response}) => response,
35
35
  * }
36
36
  * ]
37
37
  *});
@@ -39,130 +39,168 @@ function handleStatus(response) {
39
39
  export class Api {
40
40
  /** @param {Config} config */
41
41
  constructor(config = {}) {
42
- this.config = {
42
+ this.config = {
43
43
  plugins: [],
44
- responseType: 'json',
45
- ...config
44
+ responseType: "json",
45
+ ...config,
46
46
  };
47
47
  }
48
48
 
49
49
  /**
50
- * @param {string} url
51
- * @param {Method} method
52
- * @param {RequestOptions} [opts]
50
+ * @param {string} url
51
+ * @param {Method} method
52
+ * @param {RequestOptions} [opts]
53
53
  * @param {object} [data]
54
- * @returns
54
+ * @returns
55
55
  */
56
56
  async fetch(url, method, opts, data) {
57
57
  const plugins = [...this.config.plugins, ...(opts?.plugins || [])];
58
58
 
59
59
  let fetchFn = globalThis.fetch;
60
- let baseURL = opts?.baseURL ?? this.config?.baseURL ?? '';
60
+ let baseURL = opts?.baseURL ?? this.config?.baseURL ?? "";
61
61
  let responseType = opts?.responseType ?? this.config.responseType;
62
62
  let headers = new Headers({
63
- 'Content-Type': 'application/json',
64
- ...opts?.headers
63
+ "Content-Type": "application/json",
64
+ ...opts?.headers,
65
65
  });
66
66
 
67
- if(baseURL) {
68
- url = url.replace(/^(?!.*\/\/)\/?/, baseURL + '/');
67
+ if (baseURL) {
68
+ url = url.replace(/^(?!.*\/\/)\/?/, baseURL + "/");
69
69
  }
70
70
 
71
- if(opts?.params) {
72
- url += `${(~url.indexOf('?') ? '&' : '?')}${new URLSearchParams(opts.params)}`;
71
+ if (opts?.params) {
72
+ url += `${~url.indexOf("?") ? "&" : "?"}${new URLSearchParams(opts.params)}`;
73
73
  }
74
74
 
75
- for(const plugin of plugins) {
75
+ for (const plugin of plugins) {
76
76
  try {
77
- const overrides = await plugin?.beforeFetch?.({ responseType, headers, fetchFn, baseURL, url, method, opts, data });
78
- if(overrides) {
79
- ({ responseType, headers, fetchFn, baseURL, url, method, opts, data } = {...overrides});
77
+ const overrides = await plugin?.beforeFetch?.({
78
+ responseType,
79
+ headers,
80
+ fetchFn,
81
+ baseURL,
82
+ url,
83
+ method,
84
+ opts,
85
+ data,
86
+ });
87
+ if (overrides) {
88
+ ({
89
+ responseType,
90
+ headers,
91
+ fetchFn,
92
+ baseURL,
93
+ url,
94
+ method,
95
+ opts,
96
+ data,
97
+ } = { ...overrides });
80
98
  }
81
- } catch(e) {
99
+ } catch (e) {
82
100
  log(`Plugin "${plugin.name}" error on afterFetch hook`);
83
101
  throw e;
84
102
  }
85
103
  }
86
104
 
87
- log(`Fetching ${method} ${url}`, {
88
- responseType,
105
+ log(`Fetching ${method} ${url}`, {
106
+ responseType,
89
107
  // @ts-ignore
90
- headers: Object.fromEntries(headers),
91
- fetchFn,
92
- baseURL,
93
- url,
94
- method,
95
- opts,
96
- data
97
- });
98
- return fetchFn(url, {
108
+ headers: Object.fromEntries(headers),
109
+ fetchFn,
110
+ baseURL,
111
+ url,
99
112
  method,
100
- headers,
101
- ...(data ? { body: JSON.stringify(data) } : {}),
102
- ...(opts?.mode ? { mode: opts.mode } : {}),
103
- ...(opts?.credentials ? { credentials: opts.credentials } : {}),
104
- ...(opts?.cache ? { cache: opts.cache } : {}),
105
- ...(opts?.redirect ? { redirect: opts.redirect } : {}),
106
- ...(opts?.referrer ? { referrer: opts.referrer } : {}),
107
- ...(opts?.referrerPolicy ? { referrerPolicy: opts.referrerPolicy } : {}),
108
- ...(opts?.integrity ? { integrity: opts.integrity } : {}),
109
- ...(opts?.keepalive ? { keepalive: opts.keepalive } : {}),
110
- ...(opts?.signal ? { signal: opts.signal } : {}),
111
- })
112
- /** [PLUGINS - AFTERFETCH] */
113
- .then(async res => {
114
- for(const plugin of plugins) {
115
- try {
116
- const afterFetchResult = await plugin?.afterFetch?.(res) ?? res;
117
- if(afterFetchResult) {
118
- res = afterFetchResult;
119
- }
120
- } catch(e) {
121
- log(`Plugin "${plugin.name}" error on afterFetch hook`)
122
- throw e;
123
- }
124
- }
125
-
126
- return res;
127
- })
128
- /** [STATUS] */
129
- .then(handleStatus)
130
- /** [RESPONSETYPE] */
131
- .then(res => res[responseType]())
132
- .then(async data => {
133
- for(const plugin of plugins) {
134
- try {
135
- data = await plugin?.transform?.(data) ?? data;
136
- } catch(e) {
137
- log(`Plugin "${plugin.name}" error on transform hook`)
138
- throw e;
139
- }
140
- }
141
- log(`Fetch successful ${method} ${url}`, data);
142
- return data;
143
- })
144
- /** [PLUGINS - HANDLEERROR] */
145
- .catch(async e => {
146
- log(`Fetch failed ${method} ${url}`, e);
147
- const shouldThrow = plugins.length === 0 || (await Promise.all(plugins.map(({ handleError }) => handleError?.(e) ?? true))).every(_ => !!_);
148
- if(shouldThrow) throw e;
113
+ opts,
114
+ data,
149
115
  });
116
+ return (
117
+ fetchFn(url, {
118
+ method,
119
+ headers,
120
+ ...(data ? { body: JSON.stringify(data) } : {}),
121
+ ...(opts?.mode ? { mode: opts.mode } : {}),
122
+ ...(opts?.credentials ? { credentials: opts.credentials } : {}),
123
+ ...(opts?.cache ? { cache: opts.cache } : {}),
124
+ ...(opts?.redirect ? { redirect: opts.redirect } : {}),
125
+ ...(opts?.referrer ? { referrer: opts.referrer } : {}),
126
+ ...(opts?.referrerPolicy
127
+ ? { referrerPolicy: opts.referrerPolicy }
128
+ : {}),
129
+ ...(opts?.integrity ? { integrity: opts.integrity } : {}),
130
+ ...(opts?.keepalive ? { keepalive: opts.keepalive } : {}),
131
+ ...(opts?.signal ? { signal: opts.signal } : {}),
132
+ })
133
+ /** [PLUGINS - AFTERFETCH] */
134
+ .then(async (response) => {
135
+ for (const plugin of plugins) {
136
+ try {
137
+ const afterFetchResult = await plugin?.afterFetch?.({
138
+ responseType,
139
+ headers,
140
+ fetchFn,
141
+ baseURL,
142
+ url,
143
+ method,
144
+ opts,
145
+ data,
146
+ response,
147
+ });
148
+ if (afterFetchResult) {
149
+ response = afterFetchResult;
150
+ }
151
+ } catch (e) {
152
+ log(`Plugin "${plugin.name}" error on afterFetch hook`);
153
+ throw e;
154
+ }
155
+ }
156
+
157
+ return response;
158
+ })
159
+ /** [STATUS] */
160
+ .then(handleStatus)
161
+ /** [RESPONSETYPE] */
162
+ .then((response) => response[responseType]())
163
+ .then(async (data) => {
164
+ for (const plugin of plugins) {
165
+ try {
166
+ data = (await plugin?.transform?.(data)) ?? data;
167
+ } catch (e) {
168
+ log(`Plugin "${plugin.name}" error on transform hook`);
169
+ throw e;
170
+ }
171
+ }
172
+ log(`Fetch successful ${method} ${url}`, data);
173
+ return data;
174
+ })
175
+ /** [PLUGINS - HANDLEERROR] */
176
+ .catch(async (e) => {
177
+ log(`Fetch failed ${method} ${url}`, e);
178
+ const shouldThrow =
179
+ plugins.length === 0 ||
180
+ (
181
+ await Promise.all(
182
+ plugins.map(({ handleError }) => handleError?.(e) ?? true),
183
+ )
184
+ ).every((_) => !!_);
185
+ if (shouldThrow) throw e;
186
+ })
187
+ );
150
188
  }
151
189
 
152
190
  /** @type {import('./types.js').BodylessMethod} */
153
- get = (url, opts) => this.fetch(url, 'GET', opts);
191
+ get = (url, opts) => this.fetch(url, "GET", opts);
154
192
  /** @type {import('./types.js').BodylessMethod} */
155
- options = (url, opts) => this.fetch(url, 'OPTIONS', opts);
193
+ options = (url, opts) => this.fetch(url, "OPTIONS", opts);
156
194
  /** @type {import('./types.js').BodylessMethod} */
157
- delete = (url, opts) => this.fetch(url, 'DELETE', opts);
195
+ delete = (url, opts) => this.fetch(url, "DELETE", opts);
158
196
  /** @type {import('./types.js').BodylessMethod} */
159
- head = (url, opts) => this.fetch(url, 'HEAD', opts);
197
+ head = (url, opts) => this.fetch(url, "HEAD", opts);
160
198
  /** @type {import('./types.js').BodyMethod} */
161
- post = (url, data, opts) => this.fetch(url, 'POST', opts, data);
199
+ post = (url, data, opts) => this.fetch(url, "POST", opts, data);
162
200
  /** @type {import('./types.js').BodyMethod} */
163
- put = (url, data, opts) => this.fetch(url, 'PUT', opts, data);
201
+ put = (url, data, opts) => this.fetch(url, "PUT", opts, data);
164
202
  /** @type {import('./types.js').BodyMethod} */
165
- patch = (url, data, opts) => this.fetch(url, 'PATCH', opts, data);
203
+ patch = (url, data, opts) => this.fetch(url, "PATCH", opts, data);
166
204
  }
167
205
 
168
- export const api = new Api();
206
+ export const api = new Api();
@@ -6,12 +6,12 @@ export function abortPlugin() {
6
6
  const requests = new Map();
7
7
 
8
8
  return {
9
- name: 'abort',
9
+ name: "abort",
10
10
  beforeFetch: (meta) => {
11
11
  const { method, url } = meta;
12
12
  requestId = `${method}:${url}`;
13
13
 
14
- if(requests.has(requestId)) {
14
+ if (requests.has(requestId)) {
15
15
  const request = requests.get(requestId);
16
16
  request.abort();
17
17
  }
@@ -21,17 +21,17 @@ export function abortPlugin() {
21
21
  ...meta,
22
22
  opts: {
23
23
  ...meta.opts,
24
- signal: requests.get(requestId).signal
25
- }
24
+ signal: requests.get(requestId).signal,
25
+ },
26
26
  };
27
27
  },
28
- afterFetch: (res) => {
28
+ afterFetch: ({ response }) => {
29
29
  requests.delete(requestId);
30
- return res;
30
+ return response;
31
31
  },
32
32
  // return true if an error should throw, return false if an error should be ignored
33
- handleError: ({name}) => name !== 'AbortError'
34
- }
33
+ handleError: ({ name }) => name !== "AbortError",
34
+ };
35
35
  }
36
36
 
37
- export const abort = abortPlugin();
37
+ export const abort = abortPlugin();
@@ -1,19 +1,38 @@
1
1
  const TEN_MINUTES = 1000 * 60 * 10;
2
+ const DEFAULT_MAX_SIZE = 100;
2
3
 
3
4
  /**
4
- * @param {{maxAge?: number}} options
5
+ * @param {{maxAge?: number, maxSize?: number}} options
5
6
  * @returns {import('../index.js').Plugin}
6
7
  */
7
- export function cachePlugin({ maxAge = TEN_MINUTES } = {}) {
8
- let requestId;
8
+ export function cachePlugin({
9
+ maxAge = TEN_MINUTES,
10
+ maxSize = DEFAULT_MAX_SIZE,
11
+ } = {}) {
9
12
  const cache = new Map();
10
13
 
14
+ function evict() {
15
+ const now = Date.now();
16
+ for (const [key, value] of cache) {
17
+ if (value.updatedAt <= now - maxAge) {
18
+ cache.delete(key);
19
+ }
20
+ }
21
+ // if still over maxSize, evict oldest entries
22
+ if (cache.size > maxSize) {
23
+ const overflow = cache.size - maxSize;
24
+ const keys = cache.keys();
25
+ for (let i = 0; i < overflow; i++) {
26
+ cache.delete(keys.next().value);
27
+ }
28
+ }
29
+ }
30
+
11
31
  return {
12
32
  name: "cache",
13
33
  beforeFetch: (meta) => {
14
34
  const { method, url } = meta;
15
- requestId = `${method}:${url}`;
16
-
35
+ const requestId = `${method}:${url}`;
17
36
  if (cache.has(requestId)) {
18
37
  const cached = cache.get(requestId);
19
38
  if (cached.updatedAt > Date.now() - maxAge) {
@@ -25,16 +44,13 @@ export function cachePlugin({ maxAge = TEN_MINUTES } = {}) {
25
44
  }
26
45
  }
27
46
  },
28
- afterFetch: async (res) => {
29
- const clone = await res.clone();
47
+ afterFetch: async ({ response, method, url }) => {
48
+ const requestId = `${method}:${url}`;
49
+ const clone = response.clone();
30
50
  const data = await clone.json();
31
-
32
- cache.set(requestId, {
33
- updatedAt: Date.now(),
34
- data,
35
- });
36
-
37
- return res;
51
+ cache.set(requestId, { updatedAt: Date.now(), data });
52
+ evict();
53
+ return response;
38
54
  },
39
55
  };
40
56
  }
@@ -5,23 +5,23 @@
5
5
  export function jsonPrefixPlugin(jsonPrefix) {
6
6
  let responseType;
7
7
  return {
8
- name: 'jsonPrefix',
9
- beforeFetch: ({responseType: type}) => {
8
+ name: "jsonPrefix",
9
+ beforeFetch: ({ responseType: type }) => {
10
10
  responseType = type;
11
11
  },
12
- afterFetch: async (res) => {
13
- if(jsonPrefix && responseType === 'json') {
14
- let responseAsText = await res.text();
15
-
16
- if(responseAsText.startsWith(jsonPrefix)) {
12
+ afterFetch: async ({ response }) => {
13
+ if (jsonPrefix && responseType === "json") {
14
+ let responseAsText = await response.text();
15
+
16
+ if (responseAsText.startsWith(jsonPrefix)) {
17
17
  responseAsText = responseAsText.substring(jsonPrefix.length);
18
18
  }
19
19
 
20
- return new Response(responseAsText, res);
20
+ return new Response(responseAsText, response);
21
21
  }
22
- return res;
23
- }
24
- }
22
+ return response;
23
+ },
24
+ };
25
25
  }
26
26
 
27
- export const jsonPrefix = jsonPrefixPlugin(`)]}',\n`);
27
+ export const jsonPrefix = jsonPrefixPlugin(`)]}',\n`);
@@ -1,27 +1,31 @@
1
- /**
1
+ /**
2
2
  * @param {{
3
3
  * collapsed?: boolean
4
4
  * }} options
5
- * @returns {import('../index.js').Plugin}
5
+ * @returns {import('../index.js').Plugin}
6
6
  */
7
- export function loggerPlugin({collapsed = true} = {}) {
7
+ export function loggerPlugin({ collapsed = true } = {}) {
8
8
  let m;
9
9
  let start;
10
- const group = collapsed ? 'groupCollapsed' : 'group';
10
+ const group = collapsed ? "groupCollapsed" : "group";
11
11
  return {
12
- name: 'logger',
12
+ name: "logger",
13
13
  beforeFetch: (meta) => {
14
- console[group](`[START] [${new Date().toLocaleTimeString()}] [${meta.method}] "${meta.url}"`);
14
+ console[group](
15
+ `[START] [${new Date().toLocaleTimeString()}] [${meta.method}] "${meta.url}"`,
16
+ );
15
17
  console.table([meta]);
16
- console.groupEnd()
18
+ console.groupEnd();
17
19
  start = Date.now();
18
20
  m = meta;
19
21
  },
20
- afterFetch: (r) => {
21
- console.log(`[END] [${m.method}] "${m.url}" Request took ${Date.now() - start}ms`);
22
- return r;
23
- }
24
- }
22
+ afterFetch: ({ response }) => {
23
+ console.log(
24
+ `[END] [${m.method}] "${m.url}" Request took ${Date.now() - start}ms`,
25
+ );
26
+ return response;
27
+ },
28
+ };
25
29
  }
26
30
 
27
- export const logger = loggerPlugin();
31
+ export const logger = loggerPlugin();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thepassle/app-tools",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -12,7 +12,7 @@
12
12
  * plugins: [
13
13
  * {
14
14
  * beforeFetch: ({url, method, opts, data}) => {},
15
- * afterFetch: (res) => res,
15
+ * afterFetch: ({response}) => response,
16
16
  * }
17
17
  * ]
18
18
  *});
@@ -1,8 +1,9 @@
1
1
  /**
2
- * @param {{maxAge?: number}} options
2
+ * @param {{maxAge?: number, maxSize?: number}} options
3
3
  * @returns {import('../index.js').Plugin}
4
4
  */
5
- export function cachePlugin({ maxAge }?: {
5
+ export function cachePlugin({ maxAge, maxSize, }?: {
6
6
  maxAge?: number;
7
+ maxSize?: number;
7
8
  }): import('../index.js').Plugin;
8
9
  export const cache: import("../types.js").Plugin;
@@ -9,11 +9,14 @@ export type BodylessMethod = <R>(url: string, opts?: RequestOptions) => Promise<
9
9
  export type Method = 'GET' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH';
10
10
  export interface Plugin {
11
11
  beforeFetch?: (meta: MetaParams) => MetaParams | Promise<MetaParams> | void;
12
- afterFetch?: (res: Response) => void | Promise<void> | Response | Promise<Response>;
12
+ afterFetch?: (meta: AfterFetchParams) => void | Promise<void> | Response | Promise<Response>;
13
13
  transform?: (data: any) => any;
14
14
  name: string;
15
15
  handleError?: (e: Error) => boolean;
16
16
  }
17
+ export interface AfterFetchParams extends MetaParams {
18
+ response: Response;
19
+ }
17
20
  export interface CustomRequestOptions {
18
21
  transform?: (data: object) => object;
19
22
  responseType?: ResponseType;