@ouroboros/body 1.2.2 → 1.3.1

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
@@ -70,7 +70,13 @@ function MyApp() {
70
70
  'my_service',
71
71
  'my_request',
72
72
  { _id: 'someid' }
73
- ).then(setData, setError);
73
+ ).then(res => {
74
+ if(res.error) {
75
+ setError(res.error);
76
+ } else {
77
+ setData(res.data);
78
+ }
79
+ });
74
80
  }
75
81
 
76
82
  return <>
@@ -136,7 +142,13 @@ the [example](#body) could have also been written as
136
142
  'my_service',
137
143
  'my_request',
138
144
  { _id: 'someid' }
139
- ).then(setData, setError);
145
+ ).then(res => {
146
+ if(res.error) {
147
+ setError(res.error);
148
+ } else {
149
+ setData(res.data);
150
+ }
151
+ });
140
152
  ```
141
153
 
142
154
  [ [top](#ouroborosbody), [contents](#contents), [body](#body) ]
@@ -289,17 +301,17 @@ body.read(
289
301
  'my_service',
290
302
  'my/request',
291
303
  { _id: 'someid' }
292
- ).then(res => {},
293
- error => {
294
- if(error.code === errors.DATA_FIELDS) {
304
+ ).then(res => {
305
+ if(res.error) {
306
+ if(res.error.code === errors.DATA_FIELDS) {
295
307
  // Bad data sent to request
296
- } else if(error.code === errors.DB_NO_RECORD) {
308
+ } else if(res.error.code === errors.DB_NO_RECORD) {
297
309
  // Bad ID, no such record
298
310
  } else {
299
311
  // Unknown error code
300
312
  }
301
313
  }
302
- )
314
+ })
303
315
  ```
304
316
 
305
317
  [ [top](#ouroborosbody), [contents](#contents) ]
package/lib/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export type onRequestingStruct = {
36
36
  data: any;
37
37
  url: string;
38
38
  };
39
- export type onWarning = (warning: any, info: onRequestedStruct) => void;
39
+ export type onWarning = (warning: any, info: onRequestedStruct) => void | false;
40
40
  export type responseStruct = {
41
41
  data?: any;
42
42
  error?: responseErrorStruct;
@@ -47,7 +47,7 @@ export type responseErrorStruct = {
47
47
  msg?: any;
48
48
  };
49
49
  export type responseResolve = (res: responseStruct) => void;
50
- export type responseReject = (error: responseErrorStruct) => boolean;
50
+ export type responseReject = (error: responseErrorStruct) => void;
51
51
  /**
52
52
  * Body
53
53
  *
package/lib/index.js CHANGED
@@ -21,6 +21,8 @@ const METHODS = {
21
21
  read: 'GET',
22
22
  update: 'PUT'
23
23
  };
24
+ // 30 second timeout
25
+ const REQUEST_TIMEOUT = 30000;
24
26
  /**
25
27
  * Body
26
28
  *
@@ -80,6 +82,11 @@ class Body {
80
82
  * @param data The data associated with the request
81
83
  */
82
84
  request(action, service, noun, data) {
85
+ // If no error handler was assigned
86
+ if (!this.error) {
87
+ throw new Error('Must assign an error handler via body.on(\'error\', handler)' +
88
+ ' or body.onError(handler)');
89
+ }
83
90
  // Generate the URL for the request
84
91
  let url = `https://${this._domain}/${service}/${noun}`;
85
92
  // Init the response and json variables
@@ -98,24 +105,31 @@ class Body {
98
105
  }
99
106
  // Set this
100
107
  const _ = this;
101
- // Handles an error based on whether an error handler is set
102
- function handleError(message) {
103
- if (_.error) {
104
- _.error(message, { action, data, url });
105
- }
106
- else {
107
- throw new Error(message);
108
- }
109
- }
110
108
  // Create a new Promise and return it
111
109
  return new Promise((resolve, reject) => {
110
+ // Handle errors the same way every time
111
+ function handleError(reason) {
112
+ // Generate the error message
113
+ const err = `${METHODS[action]} ${url} ${reason}`;
114
+ // Call the error handler
115
+ _.error(err, { action, data, url });
116
+ // Return reject path
117
+ return reject({ code: 0, msg: err });
118
+ }
119
+ // Create an abort controller for timeouts
120
+ const controller = new AbortController();
112
121
  // Init the fetch init
113
122
  const fetchInit = {
114
- method: METHODS[action],
115
123
  headers: {
116
124
  'Content-Type': 'application/json; charset=utf-8'
117
- }
125
+ },
126
+ method: METHODS[action],
127
+ signal: controller.signal
118
128
  };
129
+ // Start a timer so we timeout if the request doesn't return
130
+ const timer = setTimeout(() => {
131
+ controller.abort();
132
+ }, REQUEST_TIMEOUT);
119
133
  // If we have a session token, add it as the Authorization header
120
134
  if (_.token) {
121
135
  fetchInit.headers.Authorization = _.token;
@@ -135,54 +149,63 @@ class Body {
135
149
  // If the Content-Type is missing or invalid
136
150
  const ct = response.headers.get('Content-Type');
137
151
  if (!ct || ct !== 'application/json; charset=utf-8') {
138
- handleError(`${METHODS[action]} ${url} returned invalid Content-Type: ${ct}`);
152
+ return handleError(`returned invalid Content-Type: "${ct}"`);
139
153
  }
140
154
  // Return the JSON
141
- return response.json();
155
+ return response.json().then(result => {
156
+ // If there's no result, return an error + reject
157
+ if (!result) {
158
+ return handleError('returned: empty JSON');
159
+ }
160
+ // Set res
161
+ res = result;
162
+ // Init the structure to pass to resolve
163
+ const oResult = {};
164
+ // If we got an error
165
+ if ('error' in result && result.error) {
166
+ // If we don't have an onErrorCode callback, or we
167
+ // do and calling it returns false
168
+ if (!_.errorCode ||
169
+ _.errorCode(result.error, { action, data, res, url }) === false) {
170
+ // If it wasn't handled, add it to the result
171
+ oResult.error = structuredClone(result.error);
172
+ }
173
+ }
174
+ // If we got a warning
175
+ if ('warning' in result && result.warning) {
176
+ // If we don't have an onWarning callback, or we do
177
+ // and calling it returns false
178
+ if (!_.warning ||
179
+ _.warning(result.warning, { action, data, res, url }) === false) {
180
+ // If it wasn't handled, add it to the result
181
+ oResult.warning = structuredClone(result.warning);
182
+ }
183
+ }
184
+ // If we got data
185
+ if ('data' in result) {
186
+ // Add it
187
+ oResult.data = structuredClone(result.data);
188
+ }
189
+ // Resolve
190
+ return resolve(oResult);
191
+ });
142
192
  }
143
193
  // If it's 401
144
- if (response.status === 401) {
194
+ else if (response.status === 401) {
145
195
  // If we have a no session callback
146
196
  if (_.noSession) {
147
197
  _.noSession();
148
- return reject({ code: 0, msg: '401 Not Authorized' });
149
- }
150
- else {
151
- throw new Error(`${METHODS[action]} ${url} return 401 NOT AUTHORIZED`);
152
198
  }
199
+ // Reject the request
200
+ return handleError('returned: 401 NOT AUTHORIZED');
153
201
  }
154
202
  // Else, invalid status
155
- else {
156
- handleError(`${METHODS[action]} ${url} returned invalid status: ${response.status}`);
157
- }
158
- }).then(result => {
159
- // If there's no result, do nothing
160
- if (!result) {
161
- return;
162
- }
163
- // Set res
164
- res = result;
165
- // If we got an error
166
- if ('error' in result && result.error) {
167
- // If we don't have an onErrorCode callback, or we do and
168
- // calling it returns false
169
- if (!_.errorCode ||
170
- _.errorCode(result.error, { action, data, res, url }) === false) {
171
- return reject(result.error);
172
- }
173
- }
174
- // If we got a warning and we have an onWarning callback
175
- if ('warning' in result && result.warning && _.warning) {
176
- _.warning(result.warning, { action, data, res, url });
177
- }
178
- // If we got data
179
- if ('data' in result) {
180
- // Resolve it
181
- return resolve(result.data);
182
- }
203
+ return handleError(`returned: invalid status ${response.status}`);
183
204
  }).catch(reason => {
184
- handleError(`${METHODS[action]} ${url} failed because of "${reason}"`);
205
+ return handleError(`failed with: "${String(reason)}"`);
185
206
  }).finally(() => {
207
+ // Clear the timeout timer
208
+ clearTimeout(timer);
186
209
  // If we have a requested callback
187
210
  if (this.requested) {
188
211
  this.requested({ action, data, res, url });
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouroboros/body",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Classes to communicate with Ouroboros Coding Inc. body services",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -26,6 +26,6 @@
26
26
  "devDependencies": {
27
27
  "tslint": "^6.1.3",
28
28
  "tslint-config-prettier": "^1.18.0",
29
- "typescript": "^5.9.2"
29
+ "typescript": "^5.9.3"
30
30
  }
31
- }
31
+ }
package/releases.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @ouroboros/body releases
2
2
 
3
+ ## 1.3.1
4
+ - Fixed issues with requests potentially not resolving promises on errors.
5
+ - Added 30 second timeout on requests so they don't hang forever.
6
+
7
+ ## 1.3.0
8
+ - Stopped splitting data and error into resolve and reject as this made it harder to identify and solve real communication problems. From now on all requests that are returned successfully, regardless of the result, are returned in full to resolve. This means all current `.then()` calls on requests must handle the full responseStruct result instead of just the `.data`. Similarly, any code currently running in the reject based on completed, but error filled, requests must be moved into the first argument of `.then()`.
9
+
3
10
  ## 1.2.2
4
11
  - Fixed issue where `finally` not being called on Promises if 401 Not Authorized is returned.
5
12
 
package/src/index.ts CHANGED
@@ -42,7 +42,7 @@ export type onRequestingStruct = {
42
42
  data: any,
43
43
  url: string
44
44
  };
45
- export type onWarning = (warning: any, info: onRequestedStruct) => void;
45
+ export type onWarning = (warning: any, info: onRequestedStruct) => void | false;
46
46
  export type responseStruct = {
47
47
  data?: any,
48
48
  error?: responseErrorStruct,
@@ -53,7 +53,7 @@ export type responseErrorStruct = {
53
53
  msg?: any
54
54
  }
55
55
  export type responseResolve = (res: responseStruct) => void;
56
- export type responseReject = (error: responseErrorStruct) => boolean;
56
+ export type responseReject = (error: responseErrorStruct) => void;
57
57
 
58
58
  // Actions to methods
59
59
  const METHODS = {
@@ -63,6 +63,9 @@ const METHODS = {
63
63
  update: 'PUT'
64
64
  }
65
65
 
66
+ // 30 second timeout
67
+ const REQUEST_TIMEOUT = 30000;
68
+
66
69
  /**
67
70
  * Body
68
71
  *
@@ -140,6 +143,14 @@ class Body {
140
143
  data: any
141
144
  ): Promise<any> {
142
145
 
146
+ // If no error handler was assigned
147
+ if(!this.error) {
148
+ throw new Error(
149
+ 'Must assign an error handler via body.on(\'error\', handler)' +
150
+ ' or body.onError(handler)'
151
+ )
152
+ }
153
+
143
154
  // Generate the URL for the request
144
155
  let url = `https://${this._domain}/${service}/${noun}`;
145
156
 
@@ -162,26 +173,39 @@ class Body {
162
173
  // Set this
163
174
  const _ = this;
164
175
 
165
- // Handles an error based on whether an error handler is set
166
- function handleError(message: string): void {
167
- if(_.error) {
168
- _.error(message, { action, data, url });
169
- } else {
170
- throw new Error(message);
171
- }
172
- }
173
-
174
176
  // Create a new Promise and return it
175
177
  return new Promise((resolve: responseResolve, reject) => {
176
178
 
179
+ // Handle errors the same way every time
180
+ function handleError(reason: string): void {
181
+
182
+ // Generate the error message
183
+ const err: string = `${METHODS[action]} ${url} ${reason}`;
184
+
185
+ // Call the error handler
186
+ (_.error as onError)(err, { action, data, url });
187
+
188
+ // Return reject path
189
+ return reject({ code: 0, msg: err });
190
+ }
191
+
192
+ // Create an abort controller for timeouts
193
+ const controller = new AbortController()
194
+
177
195
  // Init the fetch init
178
196
  const fetchInit: any = {
179
- method: METHODS[action],
180
197
  headers: {
181
198
  'Content-Type': 'application/json; charset=utf-8'
182
- }
199
+ },
200
+ method: METHODS[action],
201
+ signal: controller.signal
183
202
  }
184
203
 
204
+ // Start a timer so we timeout if the request doesn't return
205
+ const timer = setTimeout(() => {
206
+ controller.abort();
207
+ }, REQUEST_TIMEOUT);
208
+
185
209
  // If we have a session token, add it as the Authorization header
186
210
  if(_.token) {
187
211
  fetchInit.headers.Authorization = _.token;
@@ -206,81 +230,91 @@ class Body {
206
230
  // If the Content-Type is missing or invalid
207
231
  const ct = response.headers.get('Content-Type');
208
232
  if(!ct || ct !== 'application/json; charset=utf-8') {
209
- handleError(
210
- `${METHODS[action]} ${url} returned invalid Content-Type: ${ct}`
211
- );
233
+ return handleError(`returned invalid Content-Type: "${ct}"`);
212
234
  }
213
235
 
214
236
  // Return the JSON
215
- return response.json();
237
+ return response.json().then(result => {
238
+
239
+ // If there's no result, return an error + reject
240
+ if(!result) {
241
+ return handleError('returned: empty JSON');
242
+ }
243
+
244
+ // Set res
245
+ res = result;
246
+
247
+ // Init the structure to pass to resolve
248
+ const oResult: responseStruct = { };
249
+
250
+ // If we got an error
251
+ if('error' in result && result.error) {
252
+
253
+ // If we don't have an onErrorCode callback, or we
254
+ // do and calling it returns false
255
+ if(!_.errorCode ||
256
+ _.errorCode(
257
+ result.error as responseErrorStruct,
258
+ { action, data, res, url }
259
+ ) === false
260
+ ) {
261
+ // If it wasn't handled, add it to the result
262
+ oResult.error = structuredClone(result.error);
263
+ }
264
+ }
265
+
266
+ // If we got a warning
267
+ if('warning' in result && result.warning) {
268
+
269
+ // If we don't have an onWarning callback, or we do
270
+ // and calling it returns false
271
+ if(!_.warning ||
272
+ _.warning(
273
+ result.warning,
274
+ { action, data, res, url }
275
+ ) === false
276
+ ) {
277
+ // If it wasn't handled, add it to the result
278
+ oResult.warning = structuredClone(
279
+ result.warning
280
+ );
281
+ }
282
+ }
283
+
284
+ // If we got data
285
+ if('data' in result) {
286
+
287
+ // Add it
288
+ oResult.data = structuredClone(result.data);
289
+ }
290
+
291
+ // Resolve
292
+ return resolve(oResult);
293
+ });
216
294
  }
217
295
 
218
296
  // If it's 401
219
- if(response.status === 401) {
297
+ else if(response.status === 401) {
220
298
 
221
299
  // If we have a no session callback
222
300
  if(_.noSession) {
223
301
  _.noSession();
224
- return reject({ code: 0, msg: '401 Not Authorized' });
225
- } else {
226
- throw new Error(
227
- `${METHODS[action]} ${url} return 401 NOT AUTHORIZED`
228
- );
229
302
  }
230
- }
231
- // Else, invalid status
232
- else {
233
- handleError(
234
- `${METHODS[action]} ${url} returned invalid status: ${response.status}`
235
- );
236
- }
237
303
 
238
- }).then(result => {
239
-
240
- // If there's no result, do nothing
241
- if(!result) {
242
- return;
243
- }
244
-
245
- // Set res
246
- res = result;
247
-
248
- // If we got an error
249
- if('error' in result && result.error) {
250
-
251
- // If we don't have an onErrorCode callback, or we do and
252
- // calling it returns false
253
- if(!_.errorCode ||
254
- _.errorCode(
255
- result.error as responseErrorStruct,
256
- { action, data, res, url }
257
- ) === false
258
- ) {
259
- return reject(result.error);
260
- }
304
+ // Reject the request
305
+ return handleError('returned: 401 NOT AUTHORIZED');
261
306
  }
262
307
 
263
- // If we got a warning and we have an onWarning callback
264
- if('warning' in result && result.warning && _.warning) {
265
- _.warning(
266
- result.warning, { action, data, res, url }
267
- );
268
- }
269
-
270
- // If we got data
271
- if('data' in result) {
272
-
273
- // Resolve it
274
- return resolve(result.data);
275
- }
308
+ // Else, invalid status
309
+ return handleError(`returned: invalid status ${response.status}`);
276
310
 
277
311
  }).catch(reason => {
278
- handleError(
279
- `${METHODS[action]} ${url} failed because of "${reason}"`
280
- )
281
-
312
+ return handleError(`failed with: "${String(reason)}"`);
282
313
  }).finally(() => {
283
314
 
315
+ // Clear the timeout timer
316
+ clearTimeout(timer);
317
+
284
318
  // If we have a requested callback
285
319
  if(this.requested) {
286
320
  this.requested({ action, data, res, url });
@@ -0,0 +1,19 @@
1
+ 6 ESLint rules behave differently from their TSLint counterparts:
2
+ * no-invalid-this:
3
+ - Functions in methods will no longer be ignored.
4
+ * @typescript-eslint/no-unused-expressions:
5
+ - The TSLint optional config "allow-new" is the default ESLint behavior and will no longer be ignored.
6
+ * prefer-arrow/prefer-arrow-functions:
7
+ - ESLint (eslint-plugin-prefer-arrow plugin) does not support allowing named functions defined with the function keyword.
8
+ * eqeqeq:
9
+ - Option "smart" allows for comparing two literal values, evaluating the value of typeof and null comparisons.
10
+ * no-underscore-dangle:
11
+ - Leading and trailing underscores (_) on identifiers will now be ignored.
12
+ * padded-blocks:
13
+ - ESLint's padded-blocks rule also bans a blank line before a closing brace.
14
+
15
+ Error: multiple output react/jsx-curly-spacing ESLint rule options were generated, but tslint-to-eslint-config doesn't have "merger" logic to deal with this.
16
+ Please file an issue at https://github.com/typescript-eslint/tslint-to-eslint-config/issues/new?template=missing_merger.md. Thanks!
17
+
18
+ * import-destructuring-spacing
19
+