@ribbon-studios/js-utils 1.1.1 → 1.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/CONTRIBUTING.md CHANGED
@@ -5,7 +5,7 @@ Then this is the perfect place for you!
5
5
 
6
6
  ## Prerequisites
7
7
 
8
- - NodeJS 18
8
+ - NodeJS 22
9
9
 
10
10
  ## Setting Up Locally
11
11
 
package/README.md CHANGED
@@ -19,6 +19,14 @@ Collection of generic javascript utilities curated by the Rainbow Cafe~
19
19
  - [`assert`](#assert)
20
20
  - [`assert.defined`](#assertdefined)
21
21
  - [`never`](#never)
22
+ - [`retry`](#retry)
23
+ - [Fetch](#fetch)
24
+ - [`rfetch`](#rfetch)
25
+ - [`rfetch.get`](#rfetchget)
26
+ - [`rfetch.put`](#rfetchput)
27
+ - [`rfetch.post`](#rfetchpost)
28
+ - [`rfetch.patch`](#rfetchpatch)
29
+ - [`rfetch.remove`](#rfetchremove)
22
30
 
23
31
  ## Promises
24
32
 
@@ -96,6 +104,106 @@ const promise = never(); // Returns a promise that never resolves
96
104
  const promise = never(Promise.resolve('hello')); // Returns a promise that never resolves
97
105
  ```
98
106
 
107
+ ### `retry`
108
+
109
+ Retries a function `n` times until it resolves successfully.
110
+ This can be useful for requests that tend to be flaky.
111
+
112
+ ```tsx
113
+ import { retry } from '@ribbon-studios/js-utils';
114
+
115
+ // Returns a promise that resolves when the request is successful or fails after its exceeded that maximum attempts.
116
+ const promise = retry(() => getMaps(), 5);
117
+ ```
118
+
119
+ ## Fetch
120
+
121
+ ### `rfetch`
122
+
123
+ Lightweight wrapper around fetch that automatically handles:
124
+
125
+ - Query Params
126
+ - Form Data & JSON bodies
127
+ - JSON Responses (fallsback to text)
128
+ - Type Casting
129
+ - Errors
130
+
131
+ ```tsx
132
+ import { rfetch, type RibbonFetchError } from '@ribbon-studios/js-utils';
133
+
134
+ try {
135
+ const response = await rfetch<MyExpectedResponse>('https://ribbonstudios.com', {
136
+ params: {
137
+ hello: 'world',
138
+ },
139
+ body: {
140
+ hallo: 'welt',
141
+ },
142
+ });
143
+
144
+ console.log(response);
145
+ // => MyExpectedResponse
146
+ } catch (error: RibbonFetchError<MyExpectedErrorResponse>) {
147
+ console.error(error);
148
+ // => { status: number; content: MyExpectedErrorResponse; }
149
+ }
150
+ ```
151
+
152
+ ### `rfetch.get`
153
+
154
+ Shorthand for GET requests.
155
+
156
+ ```tsx
157
+ import { rfetch, type RibbonFetchError } from '@ribbon-studios/js-utils';
158
+
159
+ // Shorthand for GET requests.
160
+ await rfetch.get<MyExpectedResponse>('https://ribbonstudios.com');
161
+ ```
162
+
163
+ ### `rfetch.put`
164
+
165
+ Shorthand for PUT requests.
166
+
167
+ ```tsx
168
+ import { rfetch, type RibbonFetchError } from '@ribbon-studios/js-utils';
169
+
170
+ // Shorthand for PUT requests.
171
+ await rfetch.put<MyExpectedResponse>('https://ribbonstudios.com');
172
+ ```
173
+
174
+ ### `rfetch.post`
175
+
176
+ Shorthand for POST requests.
177
+
178
+ ```tsx
179
+ import { rfetch, type RibbonFetchError } from '@ribbon-studios/js-utils';
180
+
181
+ // Shorthand for POST requests.
182
+ await rfetch.post<MyExpectedResponse>('https://ribbonstudios.com');
183
+ ```
184
+
185
+ ### `rfetch.patch`
186
+
187
+ Shorthand for PATCH requests.
188
+
189
+ ```tsx
190
+ import { rfetch, type RibbonFetchError } from '@ribbon-studios/js-utils';
191
+
192
+ // Shorthand for PATCH requests.
193
+ await rfetch.patch<MyExpectedResponse>('https://ribbonstudios.com');
194
+ ```
195
+
196
+ ### `rfetch.remove`
197
+
198
+ Shorthand for DELETE requests.
199
+
200
+ ```tsx
201
+ import { rfetch, type RibbonFetchError } from '@ribbon-studios/js-utils';
202
+
203
+ // Shorthand for DELETE requests.
204
+ await rfetch.remove<MyExpectedResponse>('https://ribbonstudios.com');
205
+ ```
206
+
99
207
  [_**Want to Contribute?**_](/CONTRIBUTING.md)
100
208
 
101
209
  [npm-version-image]: https://img.shields.io/npm/v/@ribbon-studios/js-utils.svg
@@ -107,7 +215,7 @@ const promise = never(Promise.resolve('hello')); // Returns a promise that never
107
215
  [coveralls-url]: https://coveralls.io/github/ribbon-studios/js-utils?branch=main
108
216
  [code-style-image]: https://img.shields.io/badge/code%20style-prettier-ff69b4.svg
109
217
  [code-style-url]: https://prettier.io
110
- [maintainability-image]: https://img.shields.io/codeclimate/maintainability/ribbon-studios/refreshly
111
- [maintainability-url]: https://codeclimate.com/github/ribbon-studios/refreshly/maintainability
218
+ [maintainability-image]: https://img.shields.io/codeclimate/maintainability/ribbon-studios/js-utils
219
+ [maintainability-url]: https://codeclimate.com/github/ribbon-studios/js-utils/maintainability
112
220
  [semantic-release-url]: https://github.com/semantic-release/semantic-release
113
221
  [semantic-release-image]: https://img.shields.io/badge/%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.cjs CHANGED
@@ -2,8 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  async function assert(p, predicate, message) {
4
4
  const value = await p;
5
- if (predicate(value))
6
- return value;
5
+ if (predicate(value)) return value;
7
6
  throw new Error(message ?? "Value does not satisfy predicate");
8
7
  }
9
8
  ((assert2) => {
@@ -27,11 +26,100 @@ async function delay(promise, ms) {
27
26
  delay2.fallback = fallback;
28
27
  })(delay || (delay = {}));
29
28
  async function never(p) {
30
- if (p)
31
- console.warn(`Promise is being called via "never", please ensure this doesn't get deployed!`, p);
29
+ if (p) console.warn(`Promise is being called via "never", please ensure this doesn't get deployed!`, p);
32
30
  return new Promise(() => {
33
31
  });
34
32
  }
33
+ async function retry(fn, n) {
34
+ let attempts = 0;
35
+ while (true) {
36
+ try {
37
+ return await fn();
38
+ } catch (error) {
39
+ if (++attempts < n) continue;
40
+ throw error;
41
+ }
42
+ }
43
+ }
44
+ async function rfetch(url, options) {
45
+ var _a, _b;
46
+ const { params, headers, body, ...internalOptions } = {
47
+ method: "GET",
48
+ headers: {},
49
+ ...options
50
+ };
51
+ const internalURL = url instanceof URL ? url : new URL(url, url.startsWith("/") ? location.origin : void 0);
52
+ if (params) {
53
+ for (const [key, values] of Object.entries(params)) {
54
+ if (Array.isArray(values)) {
55
+ for (const value of values) {
56
+ internalURL.searchParams.append(key, value.toString());
57
+ }
58
+ } else {
59
+ internalURL.searchParams.append(key, values.toString());
60
+ }
61
+ }
62
+ }
63
+ let internalBody = void 0;
64
+ if (body) {
65
+ internalBody = body instanceof FormData ? body : JSON.stringify(body);
66
+ }
67
+ const response = await fetch(internalURL, {
68
+ ...internalOptions,
69
+ headers: {
70
+ "Content-Type": internalBody instanceof FormData ? "application/x-www-form-urlencoded" : "application/json",
71
+ ...headers
72
+ },
73
+ body: internalBody
74
+ });
75
+ const content = ((_b = (_a = response.headers.get("Content-Type")) == null ? void 0 : _a.toLowerCase()) == null ? void 0 : _b.includes("json")) ? await response.json() : await response.text();
76
+ if (response.ok) {
77
+ return content;
78
+ }
79
+ return Promise.reject({
80
+ status: response.status,
81
+ content
82
+ });
83
+ }
84
+ ((rfetch2) => {
85
+ async function get(url, options) {
86
+ return rfetch2(url, {
87
+ ...options,
88
+ method: "GET"
89
+ });
90
+ }
91
+ rfetch2.get = get;
92
+ async function put(url, options) {
93
+ return rfetch2(url, {
94
+ ...options,
95
+ method: "PUT"
96
+ });
97
+ }
98
+ rfetch2.put = put;
99
+ async function post(url, options) {
100
+ return rfetch2(url, {
101
+ ...options,
102
+ method: "POST"
103
+ });
104
+ }
105
+ rfetch2.post = post;
106
+ async function patch(url, options) {
107
+ return rfetch2(url, {
108
+ ...options,
109
+ method: "PATCH"
110
+ });
111
+ }
112
+ rfetch2.patch = patch;
113
+ async function remove(url, options) {
114
+ return rfetch2(url, {
115
+ ...options,
116
+ method: "DELETE"
117
+ });
118
+ }
119
+ rfetch2.remove = remove;
120
+ })(rfetch || (rfetch = {}));
35
121
  exports.assert = assert;
36
122
  exports.delay = delay;
37
123
  exports.never = never;
124
+ exports.retry = retry;
125
+ exports.rfetch = rfetch;
package/dist/index.d.cts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './promises';
2
+ export * from './rfetch';
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './promises';
2
+ export * from './rfetch';
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  async function assert(p, predicate, message) {
2
2
  const value = await p;
3
- if (predicate(value))
4
- return value;
3
+ if (predicate(value)) return value;
5
4
  throw new Error(message ?? "Value does not satisfy predicate");
6
5
  }
7
6
  ((assert2) => {
@@ -25,13 +24,102 @@ async function delay(promise, ms) {
25
24
  delay2.fallback = fallback;
26
25
  })(delay || (delay = {}));
27
26
  async function never(p) {
28
- if (p)
29
- console.warn(`Promise is being called via "never", please ensure this doesn't get deployed!`, p);
27
+ if (p) console.warn(`Promise is being called via "never", please ensure this doesn't get deployed!`, p);
30
28
  return new Promise(() => {
31
29
  });
32
30
  }
31
+ async function retry(fn, n) {
32
+ let attempts = 0;
33
+ while (true) {
34
+ try {
35
+ return await fn();
36
+ } catch (error) {
37
+ if (++attempts < n) continue;
38
+ throw error;
39
+ }
40
+ }
41
+ }
42
+ async function rfetch(url, options) {
43
+ var _a, _b;
44
+ const { params, headers, body, ...internalOptions } = {
45
+ method: "GET",
46
+ headers: {},
47
+ ...options
48
+ };
49
+ const internalURL = url instanceof URL ? url : new URL(url, url.startsWith("/") ? location.origin : void 0);
50
+ if (params) {
51
+ for (const [key, values] of Object.entries(params)) {
52
+ if (Array.isArray(values)) {
53
+ for (const value of values) {
54
+ internalURL.searchParams.append(key, value.toString());
55
+ }
56
+ } else {
57
+ internalURL.searchParams.append(key, values.toString());
58
+ }
59
+ }
60
+ }
61
+ let internalBody = void 0;
62
+ if (body) {
63
+ internalBody = body instanceof FormData ? body : JSON.stringify(body);
64
+ }
65
+ const response = await fetch(internalURL, {
66
+ ...internalOptions,
67
+ headers: {
68
+ "Content-Type": internalBody instanceof FormData ? "application/x-www-form-urlencoded" : "application/json",
69
+ ...headers
70
+ },
71
+ body: internalBody
72
+ });
73
+ const content = ((_b = (_a = response.headers.get("Content-Type")) == null ? void 0 : _a.toLowerCase()) == null ? void 0 : _b.includes("json")) ? await response.json() : await response.text();
74
+ if (response.ok) {
75
+ return content;
76
+ }
77
+ return Promise.reject({
78
+ status: response.status,
79
+ content
80
+ });
81
+ }
82
+ ((rfetch2) => {
83
+ async function get(url, options) {
84
+ return rfetch2(url, {
85
+ ...options,
86
+ method: "GET"
87
+ });
88
+ }
89
+ rfetch2.get = get;
90
+ async function put(url, options) {
91
+ return rfetch2(url, {
92
+ ...options,
93
+ method: "PUT"
94
+ });
95
+ }
96
+ rfetch2.put = put;
97
+ async function post(url, options) {
98
+ return rfetch2(url, {
99
+ ...options,
100
+ method: "POST"
101
+ });
102
+ }
103
+ rfetch2.post = post;
104
+ async function patch(url, options) {
105
+ return rfetch2(url, {
106
+ ...options,
107
+ method: "PATCH"
108
+ });
109
+ }
110
+ rfetch2.patch = patch;
111
+ async function remove(url, options) {
112
+ return rfetch2(url, {
113
+ ...options,
114
+ method: "DELETE"
115
+ });
116
+ }
117
+ rfetch2.remove = remove;
118
+ })(rfetch || (rfetch = {}));
33
119
  export {
34
120
  assert,
35
121
  delay,
36
- never
122
+ never,
123
+ retry,
124
+ rfetch
37
125
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
1
  export * from './assert';
2
2
  export * from './delay';
3
3
  export * from './never';
4
+ export * from './retry';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Attempts a request {@link n} times until it resolves.
3
+ *
4
+ * @param fn The function to invoke.
5
+ * @param n The maximum number of attempts
6
+ * @returns A resolved promise if it succeeds or the rejected promise if it exceeds {@link n}
7
+ */
8
+ export declare function retry<T>(fn: () => Promise<T>, n: number): Promise<T>;
@@ -0,0 +1,65 @@
1
+ type RibbonFetchParamType = string | number | boolean;
2
+ export type RibbonFetchOptions = {
3
+ method?: 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE';
4
+ params?: Record<string, RibbonFetchParamType | RibbonFetchParamType[]>;
5
+ body?: any;
6
+ headers?: HeadersInit;
7
+ } & Omit<RequestInit, 'body' | 'headers' | 'method'>;
8
+ export type RibbonFetchBasicOptions = Omit<RibbonFetchOptions, 'method' | 'body'>;
9
+ export type RibbonFetchBodyOptions = Omit<RibbonFetchOptions, 'method'>;
10
+ export type RibbonFetchError<R> = {
11
+ status: number;
12
+ content: R;
13
+ };
14
+ /**
15
+ * A lightweight wrapper around fetch to simplify its usage.
16
+ *
17
+ * @param url The url you wish to fetch.
18
+ * @param options The request options.
19
+ * @returns The typed response or an error containing the `status` and the `content`
20
+ */
21
+ export declare function rfetch<T = any>(url: string | URL, options?: RibbonFetchOptions): Promise<T>;
22
+ export declare namespace rfetch {
23
+ /**
24
+ * Shorthand method for a GET request
25
+ *
26
+ * @param url The url you wish to fetch.
27
+ * @param options The request options.
28
+ * @returns The typed response or an error containing the `status` and the `content`
29
+ */
30
+ function get<T>(url: string | URL, options?: RibbonFetchBasicOptions): Promise<T>;
31
+ /**
32
+ * Shorthand method for a PUT request
33
+ *
34
+ * @param url The url you wish to fetch.
35
+ * @param options The request options.
36
+ * @returns The typed response or an error containing the `status` and the `content`
37
+ */
38
+ function put<T>(url: string | URL, options?: RibbonFetchBodyOptions): Promise<T>;
39
+ /**
40
+ * Shorthand method for a POST request
41
+ *
42
+ * @param url The url you wish to fetch.
43
+ * @param options The request options.
44
+ * @returns The typed response or an error containing the `status` and the `content`
45
+ */
46
+ function post<T>(url: string | URL, options?: RibbonFetchBodyOptions): Promise<T>;
47
+ /**
48
+ * Shorthand method for a PATCH request
49
+ *
50
+ * @param url The url you wish to fetch.
51
+ * @param options The request options.
52
+ * @returns The typed response or an error containing the `status` and the `content`
53
+ */
54
+ function patch<T>(url: string | URL, options?: RibbonFetchBodyOptions): Promise<T>;
55
+ /**
56
+ * Shorthand method for a DELETE request
57
+ *
58
+ * @param url The url you wish to fetch.
59
+ * @param options The request options.
60
+ * @returns The typed response or an error containing the `status` and the `content`
61
+ * @note This is named `remove` purely because `delete` is a reserved key
62
+ */
63
+ function remove<T>(url: string | URL, options?: RibbonFetchBodyOptions): Promise<T>;
64
+ }
65
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ribbon-studios/js-utils",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Collection of generic javascript utilities curated by the Rainbow Cafe~",
5
5
  "type": "module",
6
6
  "source": "src/*.ts",
@@ -28,20 +28,23 @@
28
28
  "build": "rm -rf dist && vite build"
29
29
  },
30
30
  "devDependencies": {
31
- "@types/node": "^20.11.20",
32
- "@typescript-eslint/eslint-plugin": "^7.0.2",
33
- "@typescript-eslint/parser": "^7.0.2",
34
- "@vitest/coverage-v8": "^1.3.1",
35
- "chance": "^1.1.11",
36
- "eslint": "^8.57.0",
37
- "eslint-config-prettier": "^9.1.0",
38
- "eslint-plugin-unused-imports": "^3.1.0",
39
- "happy-dom": "^13.5.0",
40
- "typescript": "^5.3.3",
41
- "vite": "^5.1.4",
42
- "vite-plugin-dts": "^3.7.3",
43
- "vite-plugin-lib-types": "^3.0.9",
44
- "vitest": "^1.3.1",
31
+ "@eslint/js": "^9.22.0",
32
+ "@types/chance": "^1.1.6",
33
+ "@types/node": "^22.13.10",
34
+ "@typescript-eslint/eslint-plugin": "^8.26.0",
35
+ "@typescript-eslint/parser": "^8.26.0",
36
+ "@vitest/coverage-v8": "^3.0.8",
37
+ "ajv": "^8.17.1",
38
+ "chance": "^1.1.12",
39
+ "eslint-plugin-unused-imports": "^4.1.4",
40
+ "happy-dom": "^17.4.1",
41
+ "jiti": "^2.4.2",
42
+ "typescript": "^5.8.2",
43
+ "typescript-eslint": "^8.26.0",
44
+ "vite": "^6.2.1",
45
+ "vite-plugin-dts": "^4.5.3",
46
+ "vite-plugin-lib-types": "^3.1.2",
47
+ "vitest": "^3.0.8",
45
48
  "vitest-dom": "^0.1.1"
46
49
  },
47
50
  "publishConfig": {