fable 3.1.69 → 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.69",
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,27 +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();
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.
67
+ *
68
+ * @param {Object} [pAgentOptions] - Additional options passed to the Http/Https Agent constructors (e.g. timeout).
44
69
  */
45
- initializeKeepAliveAgent()
70
+ initializeKeepAliveAgent(pAgentOptions)
46
71
  {
47
- let tmpAgentOptions = { keepAlive: true };
72
+ let tmpAgentOptions = Object.assign({ keepAlive: true }, pAgentOptions);
73
+ this._installHttpAgents(tmpAgentOptions);
74
+ }
48
75
 
49
- this.httpAgent = new libHttp.Agent(tmpAgentOptions);
50
- 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);
51
87
 
52
88
  // Capture any previously set prepareRequestOptions so we can chain
53
89
  let tmpPreviousPrepareRequestOptions = this.prepareRequestOptions;
@@ -101,6 +137,14 @@ class FableServiceRestClient extends libFableServiceBase
101
137
  tmpOptions.url = this.fable.settings.RestClientURLPrefix + tmpOptions.url;
102
138
  }
103
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
+
104
148
  return this.prepareRequestOptions(tmpOptions);
105
149
  }
106
150
 
@@ -148,52 +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
- 'Do not create agents when KeepAlive is not set.',
196
+ 'Passes additional agent options through KeepAliveAgentOptions.',
185
197
  function ()
186
198
  {
187
199
  let testFable = new libFable();
188
- let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-NoKeepAlive');
200
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient',
201
+ {
202
+ KeepAlive: true,
203
+ KeepAliveAgentOptions: { timeout: 300000, maxSockets: 32 }
204
+ }, 'RestClient-KeepAlive-AgentOpts');
189
205
 
190
- Expect(tmpRestClient.httpAgent).to.equal(undefined);
191
- Expect(tmpRestClient.httpsAgent).to.equal(undefined);
206
+ Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
207
+ Expect(tmpRestClient.httpAgent.options.timeout).to.equal(300000);
208
+ Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(32);
209
+ Expect(tmpRestClient.httpsAgent.options.timeout).to.equal(300000);
192
210
  }
193
211
  );
194
212
  test
195
213
  (
196
- 'Inject HTTP agent into request options for http URLs.',
214
+ 'KeepAliveAgentOptions apply even when KeepAlive is not enabled.',
215
+ function ()
216
+ {
217
+ // Tuning options still flow through when keepAlive is off.
218
+ let testFable = new libFable();
219
+ let tmpRestClient = testFable.instantiateServiceProvider('RestClient',
220
+ { KeepAliveAgentOptions: { maxSockets: 8 } }, 'RestClient-AgentOpts-NoKeepAlive');
221
+
222
+ Expect(Boolean(tmpRestClient.httpAgent.keepAlive)).to.equal(false);
223
+ Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(8);
224
+ }
225
+ );
226
+ test
227
+ (
228
+ 'Injects the http agent on http:// URLs.',
197
229
  function (fTestComplete)
198
230
  {
199
231
  var tmpServer = libHTTP.createServer(function (pReq, pRes)
@@ -206,9 +238,8 @@ suite
206
238
  {
207
239
  var tmpPort = tmpServer.address().port;
208
240
  var testFable = new libFable();
209
- var tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-HTTP');
241
+ var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-AgentInject-HTTP');
210
242
 
211
- // Wrap prepareRequestOptions to capture the final options
212
243
  var tmpOriginalPrepare = tmpRestClient.prepareRequestOptions;
213
244
  var tmpCapturedAgent = null;
214
245
  tmpRestClient.prepareRequestOptions = function (pOptions)
@@ -222,7 +253,6 @@ suite
222
253
  function (pError, pResponse, pBody)
223
254
  {
224
255
  Expect(tmpCapturedAgent).to.equal(tmpRestClient.httpAgent);
225
- Expect(pBody).to.be.an('object');
226
256
  Expect(pBody.OK).to.equal(true);
227
257
  tmpServer.close();
228
258
  fTestComplete();
@@ -232,7 +262,25 @@ suite
232
262
  );
233
263
  test
234
264
  (
235
- '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.',
236
284
  function (fTestComplete)
237
285
  {
238
286
  var tmpServer = libHTTP.createServer(function (pReq, pRes)
@@ -247,13 +295,10 @@ suite
247
295
  var testFable = new libFable();
248
296
  var tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-Chain');
249
297
 
250
- // Set a custom prepareRequestOptions AFTER keep-alive init to verify chaining
251
- var tmpKeepAlivePrepare = tmpRestClient.prepareRequestOptions;
298
+ var tmpInstalledPrepare = tmpRestClient.prepareRequestOptions;
252
299
  tmpRestClient.prepareRequestOptions = function (pOptions)
253
300
  {
254
- // Apply keep-alive first
255
- let tmpResult = tmpKeepAlivePrepare(pOptions);
256
- // Then add custom header
301
+ let tmpResult = tmpInstalledPrepare(pOptions);
257
302
  if (!tmpResult.headers)
258
303
  {
259
304
  tmpResult.headers = {};
@@ -265,7 +310,6 @@ suite
265
310
  tmpRestClient.getJSON('http://localhost:' + tmpPort + '/test',
266
311
  function (pError, pResponse, pBody)
267
312
  {
268
- Expect(pBody).to.be.an('object');
269
313
  Expect(pBody.CustomHeader).to.equal('test-value');
270
314
  tmpServer.close();
271
315
  fTestComplete();
@@ -273,6 +317,165 @@ suite
273
317
  });
274
318
  }
275
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
+ );
276
479
  }
277
480
  );
278
481