fable 3.1.70 → 3.1.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fable",
3
- "version": "3.1.70",
3
+ "version": "3.1.71",
4
4
  "description": "A service dependency injection, configuration and logging library.",
5
5
  "main": "source/Fable.js",
6
6
  "scripts": {
@@ -52,7 +52,7 @@
52
52
  "homepage": "https://github.com/stevenvelozo/fable",
53
53
  "devDependencies": {
54
54
  "pict-docuserve": "^0.1.5",
55
- "quackage": "^1.1.0"
55
+ "quackage": "^1.1.2"
56
56
  },
57
57
  "dependencies": {
58
58
  "async.eachlimit": "^0.5.2",
@@ -27,29 +27,63 @@ class FableServiceRestClient extends libFableServiceBase
27
27
  // of the request options before they are passed to the request library.
28
28
  this.prepareRequestOptions = (pOptions) => { return pOptions; };
29
29
 
30
- // Check for keep-alive configuration (from options or fable settings)
31
- // Useful in environments where connections are slow to establish
32
- // (e.g. inside poorly configured customer networks / VPNs)
33
- let tmpKeepAlive = this.options.KeepAlive || this.fable.settings.RestClientKeepAlive;
30
+ // Default per-request timeout (ms). Applied in preRequest when a caller
31
+ // does not supply their own. Node 20+ installs a ~5s socket timeout on
32
+ // http.globalAgent that aborts legitimately long-running requests; any
33
+ // explicit `timeout` on the request options takes that default out of
34
+ // play. See the "Request Timeout" test suite for the behaviors covered.
35
+ if (typeof this.options.RequestTimeout === 'number')
36
+ {
37
+ this.defaultRequestTimeout = this.options.RequestTimeout;
38
+ }
39
+ else if (typeof this.fable.settings.RestClientRequestTimeout === 'number')
40
+ {
41
+ this.defaultRequestTimeout = this.fable.settings.RestClientRequestTimeout;
42
+ }
43
+ else
44
+ {
45
+ this.defaultRequestTimeout = 60000;
46
+ }
47
+
48
+ // Always install our own http/https agents so every request bypasses
49
+ // http.globalAgent (and its Node 20+ mystery socket timeout). The
50
+ // KeepAlive flag only controls whether keepAlive is enabled on our own
51
+ // agents, not whether we have agents at all. Additional tuning
52
+ // (maxSockets, agent timeout, etc.) flows through KeepAliveAgentOptions.
53
+ let tmpKeepAlive = Boolean(this.options.KeepAlive || this.fable.settings.RestClientKeepAlive);
54
+ let tmpAgentOptions = Object.assign({}, this.options.KeepAliveAgentOptions);
34
55
  if (tmpKeepAlive)
35
56
  {
36
- this.initializeKeepAliveAgent(this.options.KeepAliveAgentOptions);
57
+ tmpAgentOptions.keepAlive = true;
37
58
  }
59
+ this._installHttpAgents(tmpAgentOptions);
38
60
  }
39
61
 
40
62
  /**
41
63
  * Initialize HTTP keep-alive agents and wire them into prepareRequestOptions.
42
- * Creates both an HTTP and HTTPS agent so the correct one is selected per-request
43
- * based on the URL protocol.
64
+ * Back-compat entry point: always forces keepAlive on. Prefer configuring
65
+ * the RestClient via the KeepAlive / KeepAliveAgentOptions constructor
66
+ * options instead of calling this method directly.
44
67
  *
45
68
  * @param {Object} [pAgentOptions] - Additional options passed to the Http/Https Agent constructors (e.g. timeout).
46
69
  */
47
70
  initializeKeepAliveAgent(pAgentOptions)
48
71
  {
49
72
  let tmpAgentOptions = Object.assign({ keepAlive: true }, pAgentOptions);
73
+ this._installHttpAgents(tmpAgentOptions);
74
+ }
50
75
 
51
- this.httpAgent = new libHttp.Agent(tmpAgentOptions);
52
- this.httpsAgent = new libHttps.Agent(tmpAgentOptions);
76
+ /**
77
+ * Construct http/https Agents from the given options and wire them into
78
+ * prepareRequestOptions so every request carries an explicit agent.
79
+ *
80
+ * @param {Object} pAgentOptions - Options passed directly to the Http/Https Agent constructors.
81
+ * @private
82
+ */
83
+ _installHttpAgents(pAgentOptions)
84
+ {
85
+ this.httpAgent = new libHttp.Agent(pAgentOptions);
86
+ this.httpsAgent = new libHttps.Agent(pAgentOptions);
53
87
 
54
88
  // Capture any previously set prepareRequestOptions so we can chain
55
89
  let tmpPreviousPrepareRequestOptions = this.prepareRequestOptions;
@@ -103,6 +137,14 @@ class FableServiceRestClient extends libFableServiceBase
103
137
  tmpOptions.url = this.fable.settings.RestClientURLPrefix + tmpOptions.url;
104
138
  }
105
139
 
140
+ // Apply the default request timeout when the caller hasn't supplied
141
+ // one. Setting any numeric value (including 0) suppresses the Node 20+
142
+ // http.globalAgent ~5s socket timeout.
143
+ if (typeof tmpOptions.timeout !== 'number')
144
+ {
145
+ tmpOptions.timeout = this.defaultRequestTimeout;
146
+ }
147
+
106
148
  return this.prepareRequestOptions(tmpOptions);
107
149
  }
108
150
 
@@ -148,73 +148,84 @@ suite
148
148
 
149
149
  suite
150
150
  (
151
- 'KeepAlive Agent',
151
+ 'HTTP Agent',
152
152
  function ()
153
153
  {
154
154
  test
155
155
  (
156
- 'Initialize keep-alive agents via options.',
156
+ 'Always installs http/https agents, even when KeepAlive is not set.',
157
157
  function ()
158
158
  {
159
+ // Without an explicit agent, requests would fall through to
160
+ // http.globalAgent and hit the Node 20+ ~5s socket timeout.
159
161
  let testFable = new libFable();
160
- let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-Options');
162
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-DefaultAgent');
161
163
 
162
164
  Expect(tmpRestClient.httpAgent).to.be.an('object');
163
165
  Expect(tmpRestClient.httpsAgent).to.be.an('object');
166
+ Expect(Boolean(tmpRestClient.httpAgent.keepAlive)).to.equal(false);
167
+ Expect(Boolean(tmpRestClient.httpsAgent.keepAlive)).to.equal(false);
168
+ }
169
+ );
170
+ test
171
+ (
172
+ 'Enables keepAlive on agents when KeepAlive option is set.',
173
+ function ()
174
+ {
175
+ let testFable = new libFable();
176
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-Options');
177
+
164
178
  Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
165
179
  Expect(tmpRestClient.httpsAgent.keepAlive).to.equal(true);
166
180
  }
167
181
  );
168
182
  test
169
183
  (
170
- 'Initialize keep-alive agents via fable settings.',
184
+ 'Enables keepAlive on agents via fable settings.',
171
185
  function ()
172
186
  {
173
187
  let testFable = new libFable({ RestClientKeepAlive: true });
174
188
  let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-KeepAlive-Settings');
175
189
 
176
- Expect(tmpRestClient.httpAgent).to.be.an('object');
177
- Expect(tmpRestClient.httpsAgent).to.be.an('object');
178
190
  Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
179
191
  Expect(tmpRestClient.httpsAgent.keepAlive).to.equal(true);
180
192
  }
181
193
  );
182
194
  test
183
195
  (
184
- 'Pass additional agent options via KeepAliveAgentOptions.',
196
+ 'Passes additional agent options through KeepAliveAgentOptions.',
185
197
  function ()
186
198
  {
187
199
  let testFable = new libFable();
188
200
  let tmpRestClient = testFable.instantiateServiceProvider('RestClient',
189
201
  {
190
202
  KeepAlive: true,
191
- KeepAliveAgentOptions: { timeout: 300000 }
203
+ KeepAliveAgentOptions: { timeout: 300000, maxSockets: 32 }
192
204
  }, 'RestClient-KeepAlive-AgentOpts');
193
205
 
194
- Expect(tmpRestClient.httpAgent).to.be.an('object');
195
- Expect(tmpRestClient.httpsAgent).to.be.an('object');
196
206
  Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
197
- Expect(tmpRestClient.httpsAgent.keepAlive).to.equal(true);
198
- // Verify the custom timeout was passed through
199
207
  Expect(tmpRestClient.httpAgent.options.timeout).to.equal(300000);
208
+ Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(32);
200
209
  Expect(tmpRestClient.httpsAgent.options.timeout).to.equal(300000);
201
210
  }
202
211
  );
203
- test
212
+ test
204
213
  (
205
- 'Do not create agents when KeepAlive is not set.',
214
+ 'KeepAliveAgentOptions apply even when KeepAlive is not enabled.',
206
215
  function ()
207
216
  {
217
+ // Tuning options still flow through when keepAlive is off.
208
218
  let testFable = new libFable();
209
- let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-NoKeepAlive');
219
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient',
220
+ { KeepAliveAgentOptions: { maxSockets: 8 } }, 'RestClient-AgentOpts-NoKeepAlive');
210
221
 
211
- Expect(tmpRestClient.httpAgent).to.equal(undefined);
212
- Expect(tmpRestClient.httpsAgent).to.equal(undefined);
222
+ Expect(Boolean(tmpRestClient.httpAgent.keepAlive)).to.equal(false);
223
+ Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(8);
213
224
  }
214
225
  );
215
226
  test
216
227
  (
217
- 'Inject HTTP agent into request options for http URLs.',
228
+ 'Injects the http agent on http:// URLs.',
218
229
  function (fTestComplete)
219
230
  {
220
231
  var tmpServer = libHTTP.createServer(function (pReq, pRes)
@@ -227,9 +238,8 @@ suite
227
238
  {
228
239
  var tmpPort = tmpServer.address().port;
229
240
  var testFable = new libFable();
230
- var tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-HTTP');
241
+ var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-AgentInject-HTTP');
231
242
 
232
- // Wrap prepareRequestOptions to capture the final options
233
243
  var tmpOriginalPrepare = tmpRestClient.prepareRequestOptions;
234
244
  var tmpCapturedAgent = null;
235
245
  tmpRestClient.prepareRequestOptions = function (pOptions)
@@ -243,7 +253,6 @@ suite
243
253
  function (pError, pResponse, pBody)
244
254
  {
245
255
  Expect(tmpCapturedAgent).to.equal(tmpRestClient.httpAgent);
246
- Expect(pBody).to.be.an('object');
247
256
  Expect(pBody.OK).to.equal(true);
248
257
  tmpServer.close();
249
258
  fTestComplete();
@@ -253,7 +262,25 @@ suite
253
262
  );
254
263
  test
255
264
  (
256
- 'Chain with previously set prepareRequestOptions.',
265
+ 'Selects the https agent for https:// URLs.',
266
+ function ()
267
+ {
268
+ // We don't need a live HTTPS server to verify the selection
269
+ // logic — invoke prepareRequestOptions directly and inspect
270
+ // which agent was chosen.
271
+ let testFable = new libFable();
272
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-AgentInject-HTTPS');
273
+
274
+ let tmpHttps = tmpRestClient.prepareRequestOptions({ url: 'https://example.com/x' });
275
+ let tmpHttp = tmpRestClient.prepareRequestOptions({ url: 'http://example.com/x' });
276
+
277
+ Expect(tmpHttps.agent).to.equal(tmpRestClient.httpsAgent);
278
+ Expect(tmpHttp.agent).to.equal(tmpRestClient.httpAgent);
279
+ }
280
+ );
281
+ test
282
+ (
283
+ 'Chains with previously set prepareRequestOptions.',
257
284
  function (fTestComplete)
258
285
  {
259
286
  var tmpServer = libHTTP.createServer(function (pReq, pRes)
@@ -268,13 +295,10 @@ suite
268
295
  var testFable = new libFable();
269
296
  var tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-Chain');
270
297
 
271
- // Set a custom prepareRequestOptions AFTER keep-alive init to verify chaining
272
- var tmpKeepAlivePrepare = tmpRestClient.prepareRequestOptions;
298
+ var tmpInstalledPrepare = tmpRestClient.prepareRequestOptions;
273
299
  tmpRestClient.prepareRequestOptions = function (pOptions)
274
300
  {
275
- // Apply keep-alive first
276
- let tmpResult = tmpKeepAlivePrepare(pOptions);
277
- // Then add custom header
301
+ let tmpResult = tmpInstalledPrepare(pOptions);
278
302
  if (!tmpResult.headers)
279
303
  {
280
304
  tmpResult.headers = {};
@@ -286,7 +310,6 @@ suite
286
310
  tmpRestClient.getJSON('http://localhost:' + tmpPort + '/test',
287
311
  function (pError, pResponse, pBody)
288
312
  {
289
- Expect(pBody).to.be.an('object');
290
313
  Expect(pBody.CustomHeader).to.equal('test-value');
291
314
  tmpServer.close();
292
315
  fTestComplete();
@@ -294,6 +317,165 @@ suite
294
317
  });
295
318
  }
296
319
  );
320
+ test
321
+ (
322
+ 'initializeKeepAliveAgent remains a back-compat entry point.',
323
+ function ()
324
+ {
325
+ let testFable = new libFable();
326
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-BackCompat');
327
+
328
+ // Default constructor: no keepAlive
329
+ Expect(Boolean(tmpRestClient.httpAgent.keepAlive)).to.equal(false);
330
+
331
+ // Calling the legacy method flips keepAlive on
332
+ tmpRestClient.initializeKeepAliveAgent({ maxSockets: 4 });
333
+ Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
334
+ Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(4);
335
+ }
336
+ );
337
+ }
338
+ );
339
+
340
+ suite
341
+ (
342
+ 'Request Timeout',
343
+ function ()
344
+ {
345
+ // Helper: spin up a local HTTP server that never responds so we can
346
+ // verify that the simple-get 'Request timed out' path fires on our
347
+ // configured timeout rather than any ambient http.globalAgent default.
348
+ var createHangingServer = function (fOnListen)
349
+ {
350
+ var tmpServer = libHTTP.createServer(function (pReq, pRes)
351
+ {
352
+ // Intentionally never write or end the response
353
+ });
354
+ tmpServer.listen(0, function ()
355
+ {
356
+ fOnListen(tmpServer, tmpServer.address().port);
357
+ });
358
+ return tmpServer;
359
+ };
360
+
361
+ test
362
+ (
363
+ 'Applies the library default timeout (60000ms) when none is supplied.',
364
+ function ()
365
+ {
366
+ let testFable = new libFable();
367
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-DefaultTimeout');
368
+
369
+ Expect(tmpRestClient.defaultRequestTimeout).to.equal(60000);
370
+
371
+ let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET' });
372
+ Expect(tmpPrepared.timeout).to.equal(60000);
373
+ }
374
+ );
375
+ test
376
+ (
377
+ 'RequestTimeout constructor option overrides the library default.',
378
+ function ()
379
+ {
380
+ let testFable = new libFable();
381
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 60000 }, 'RestClient-OptTimeout');
382
+
383
+ Expect(tmpRestClient.defaultRequestTimeout).to.equal(60000);
384
+
385
+ let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET' });
386
+ Expect(tmpPrepared.timeout).to.equal(60000);
387
+ }
388
+ );
389
+ test
390
+ (
391
+ 'RestClientRequestTimeout fable setting overrides the library default.',
392
+ function ()
393
+ {
394
+ let testFable = new libFable({ RestClientRequestTimeout: 45000 });
395
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-SettingTimeout');
396
+
397
+ Expect(tmpRestClient.defaultRequestTimeout).to.equal(45000);
398
+ }
399
+ );
400
+ test
401
+ (
402
+ 'Constructor option takes precedence over fable setting.',
403
+ function ()
404
+ {
405
+ let testFable = new libFable({ RestClientRequestTimeout: 45000 });
406
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 15000 }, 'RestClient-TimeoutPrecedence');
407
+
408
+ Expect(tmpRestClient.defaultRequestTimeout).to.equal(15000);
409
+ }
410
+ );
411
+ test
412
+ (
413
+ 'Caller-supplied timeout on the request options is preserved.',
414
+ function ()
415
+ {
416
+ let testFable = new libFable();
417
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-CallerTimeout');
418
+
419
+ let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET', timeout: 1234 });
420
+ Expect(tmpPrepared.timeout).to.equal(1234);
421
+ }
422
+ );
423
+ test
424
+ (
425
+ 'Explicit timeout of 0 on the request options is preserved.',
426
+ function ()
427
+ {
428
+ // 0 is a valid numeric value — it signals "override the mystery
429
+ // default with no timeout" and must not be replaced.
430
+ let testFable = new libFable();
431
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-ZeroTimeout');
432
+
433
+ let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET', timeout: 0 });
434
+ Expect(tmpPrepared.timeout).to.equal(0);
435
+ }
436
+ );
437
+ test
438
+ (
439
+ 'RequestTimeout value of 0 is honored.',
440
+ function ()
441
+ {
442
+ let testFable = new libFable();
443
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 0 }, 'RestClient-ZeroOptTimeout');
444
+
445
+ Expect(tmpRestClient.defaultRequestTimeout).to.equal(0);
446
+
447
+ let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET' });
448
+ Expect(tmpPrepared.timeout).to.equal(0);
449
+ }
450
+ );
451
+ test
452
+ (
453
+ 'A short RequestTimeout actually aborts a hanging request.',
454
+ function (fTestComplete)
455
+ {
456
+ this.timeout(5000);
457
+ var tmpServer;
458
+ tmpServer = createHangingServer(function (pServer, pPort)
459
+ {
460
+ var testFable = new libFable();
461
+ var tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 300 }, 'RestClient-LiveTimeout');
462
+
463
+ var tmpStart = Date.now();
464
+ tmpRestClient.getJSON('http://localhost:' + pPort + '/hangs',
465
+ function (pError, pResponse, pBody)
466
+ {
467
+ var tmpElapsed = Date.now() - tmpStart;
468
+ Expect(pError).to.be.an.instanceof(Error);
469
+ Expect(pError.message).to.contain('timed out');
470
+ // Guard against the Node 20+ ~5s globalAgent timeout
471
+ // silently beating our 300ms setting.
472
+ Expect(tmpElapsed).to.be.lessThan(2500);
473
+ pServer.close();
474
+ fTestComplete();
475
+ });
476
+ });
477
+ }
478
+ );
297
479
  }
298
480
  );
299
481