@friggframework/core 2.0.0--canary.579.312fe8b.0 → 2.0.0--canary.580.1003d8d.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.
@@ -3,8 +3,6 @@ const { Delegate } = require('../../core');
3
3
  const { FetchError } = require('../../errors');
4
4
  const { get } = require('../../assertions');
5
5
 
6
- const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
7
-
8
6
  class Requester extends Delegate {
9
7
  constructor(params) {
10
8
  super(params);
@@ -15,30 +13,6 @@ class Requester extends Delegate {
15
13
  this.delegateTypes.push(this.DLGT_INVALID_AUTH);
16
14
  this.agent = get(params, 'agent', null);
17
15
 
18
- // Per-attempt HTTP timeout. Without this the framework called fetch()
19
- // with no AbortController and no timeout — a silently-hung TCP
20
- // connection (server accepts but never responds) blocked the calling
21
- // promise forever, cascading into stalled batches, stalled syncs,
22
- // and worker-lambda timeouts.
23
- //
24
- // Configuration precedence:
25
- // 1. Instance param: new Requester({ requestTimeoutMs: 30_000 })
26
- // 2. Class static: static requestTimeoutMs = 30_000
27
- // 3. Default: DEFAULT_REQUEST_TIMEOUT_MS (60s)
28
- //
29
- // Pass 0 (or null) to disable the timeout entirely — reserved for
30
- // test doubles and documented long-running endpoints.
31
- // Intentionally NOT using `get(params, ...)` here — the Frigg
32
- // `get` helper throws RequiredPropertyError if the key is missing
33
- // and no default is provided, which would collide with the fall-
34
- // through to the class-level static override.
35
- const instanceTimeout = params?.requestTimeoutMs;
36
- this.requestTimeoutMs =
37
- instanceTimeout !== undefined && instanceTimeout !== null
38
- ? instanceTimeout
39
- : this.constructor.requestTimeoutMs ??
40
- DEFAULT_REQUEST_TIMEOUT_MS;
41
-
42
16
  // Allow passing in the fetch function
43
17
  // Instance methods can use this.fetch without differentiating
44
18
  this.fetch = get(params, 'fetch', fetch);
@@ -74,134 +48,52 @@ class Requester extends Delegate {
74
48
 
75
49
  if (this.agent) options.agent = this.agent;
76
50
 
77
- // Per-attempt timeout — fresh AbortController per call so the retry
78
- // recursion (with its own backoff sleeps) always gets a clean
79
- // signal. Timer is cleared in the finally block regardless of
80
- // outcome.
81
- const timeoutMs = this.requestTimeoutMs;
82
- const controller = timeoutMs > 0 ? new AbortController() : null;
83
- const timeoutHandle = controller
84
- ? setTimeout(() => controller.abort(), timeoutMs)
85
- : null;
86
- const fetchOptions = controller
87
- ? { ...options, signal: controller.signal }
88
- : options;
89
-
90
- // Timer must stay active through body consumption. node-fetch v2
91
- // resolves the fetch() promise when headers arrive, not when the
92
- // body is fully read — so a server that sends headers and then
93
- // stalls the body would still hang parsedBody() or
94
- // FetchError.create()'s response.text() call. We clear the timer
95
- // only after the body is fully consumed (success path) or
96
- // deliberately before each recursive retry so the new attempt
97
- // starts with its own fresh timer.
98
- let timerCleared = false;
99
- const clearRequestTimer = () => {
100
- if (!timerCleared && timeoutHandle) {
101
- clearTimeout(timeoutHandle);
102
- timerCleared = true;
103
- }
104
- };
105
-
51
+ let response;
106
52
  try {
107
- let response;
108
- try {
109
- response = await this.fetch(encodedUrl, fetchOptions);
110
- } catch (e) {
111
- // AbortController fires AbortError (name) / ETIMEDOUT-shaped
112
- // errors (type on node-fetch) when we hit the timeout. No
113
- // retry on timeout: a slow endpoint is a downstream problem,
114
- // and each retry would wait another `timeoutMs` before giving
115
- // up — amplifying the hang into a per-record multi-minute
116
- // stall at batch scale.
117
- const isTimeout =
118
- e?.name === 'AbortError' || e?.type === 'aborted';
119
- if (e?.code === 'ECONNRESET' && i < this.backOff.length) {
120
- clearRequestTimer();
121
- const delay = this.backOff[i] * 1000;
122
- await new Promise((resolve) =>
123
- setTimeout(resolve, delay)
124
- );
125
- return this._request(url, options, i + 1);
126
- }
127
- const fetchError = await FetchError.create({
128
- resource: encodedUrl,
129
- init: options,
130
- responseBody: isTimeout
131
- ? `Request timed out after ${timeoutMs}ms`
132
- : e,
133
- });
134
- if (isTimeout) {
135
- // Flag + machine-readable fields so callers can
136
- // distinguish a timeout from a generic network error
137
- // without parsing the message (which FetchError
138
- // sanitizes outside of STAGE=dev).
139
- fetchError.isTimeout = true;
140
- fetchError.timeoutMs = timeoutMs;
141
- }
142
- throw fetchError;
143
- }
144
-
145
- const { status } = response;
146
-
147
- // If the status is retriable and there are back off requests left, retry the request
148
- if ((status === 429 || status >= 500) && i < this.backOff.length) {
149
- clearRequestTimer();
53
+ response = await this.fetch(encodedUrl, options);
54
+ } catch (e) {
55
+ if (e.code === 'ECONNRESET' && i < this.backOff.length) {
150
56
  const delay = this.backOff[i] * 1000;
151
57
  await new Promise((resolve) => setTimeout(resolve, delay));
152
58
  return this._request(url, options, i + 1);
153
- } else if (status === 401) {
154
- if (!this.isRefreshable || this.refreshCount > 0) {
155
- await this.notify(this.DLGT_INVALID_AUTH);
156
- } else {
157
- this.refreshCount++;
158
- const refreshSucceeded = await this.refreshAuth();
159
- if (refreshSucceeded) {
160
- clearRequestTimer();
161
- return this._request(url, options, i + 1);
162
- }
163
- }
164
59
  }
165
-
166
- // If the error wasn't retried, throw. FetchError.create reads
167
- // the response body (response.text()) — timer must still be
168
- // alive to catch a stalled body stream.
169
- if (status >= 400) {
170
- const fetchError = await FetchError.create({
171
- resource: encodedUrl,
172
- init: options,
173
- response,
174
- });
175
- throw this._maybeFlagTimeoutDuringBodyRead(
176
- fetchError,
177
- timeoutMs
178
- );
60
+ throw await FetchError.create({
61
+ resource: encodedUrl,
62
+ init: options,
63
+ responseBody: e,
64
+ });
65
+ }
66
+ const { status } = response;
67
+
68
+ // If the status is retriable and there are back off requests left, retry the request
69
+ if ((status === 429 || status >= 500) && i < this.backOff.length) {
70
+ const delay = this.backOff[i] * 1000;
71
+ await new Promise((resolve) => setTimeout(resolve, delay));
72
+ return this._request(url, options, i + 1);
73
+ } else if (status === 401) {
74
+ if (!this.isRefreshable || this.refreshCount > 0) {
75
+ await this.notify(this.DLGT_INVALID_AUTH);
76
+ } else {
77
+ this.refreshCount++;
78
+ const refreshSucceeded = await this.refreshAuth();
79
+ if (refreshSucceeded) {
80
+ return this._request(url, options, i + 1);
81
+ }
179
82
  }
83
+ }
180
84
 
181
- // parsedBody consumes the response body stream. If the server
182
- // stalls mid-stream the timer (still armed) aborts it.
183
- return options.returnFullRes
184
- ? response
185
- : await this.parsedBody(response);
186
- } catch (e) {
187
- // If the abort fired during body consumption, node-fetch emits
188
- // the error as an AbortError on the body stream. Surface the
189
- // same isTimeout flag callers use for header-phase timeouts.
190
- throw this._maybeFlagTimeoutDuringBodyRead(e, timeoutMs);
191
- } finally {
192
- clearRequestTimer();
85
+ // If the error wasn't retried, throw.
86
+ if (status >= 400) {
87
+ throw await FetchError.create({
88
+ resource: encodedUrl,
89
+ init: options,
90
+ response,
91
+ });
193
92
  }
194
- }
195
93
 
196
- _maybeFlagTimeoutDuringBodyRead(err, timeoutMs) {
197
- if (!err || typeof err !== 'object') return err;
198
- if (err.isTimeout) return err;
199
- const isAbort =
200
- err.name === 'AbortError' || err.type === 'aborted';
201
- if (!isAbort) return err;
202
- err.isTimeout = true;
203
- err.timeoutMs = timeoutMs;
204
- return err;
94
+ return options.returnFullRes
95
+ ? response
96
+ : await this.parsedBody(response);
205
97
  }
206
98
 
207
99
  async _get(options) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.579.312fe8b.0",
4
+ "version": "2.0.0--canary.580.1003d8d.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0--canary.579.312fe8b.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.579.312fe8b.0",
43
- "@friggframework/test": "2.0.0--canary.579.312fe8b.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.580.1003d8d.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.580.1003d8d.0",
43
+ "@friggframework/test": "2.0.0--canary.580.1003d8d.0",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "312fe8be9f6afce4d78c1af5f13b03e0a5595cd6"
83
+ "gitHead": "1003d8deff8f587be3cd9210f1c35c5dc92531be"
84
84
  }