@foundatiofx/fetchclient 0.47.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.
Files changed (106) hide show
  1. package/esm/mod.js +5 -0
  2. package/esm/package.json +3 -0
  3. package/esm/src/Counter.js +36 -0
  4. package/esm/src/DefaultHelpers.js +132 -0
  5. package/esm/src/FetchClient.js +543 -0
  6. package/esm/src/FetchClientCache.js +88 -0
  7. package/esm/src/FetchClientContext.js +1 -0
  8. package/esm/src/FetchClientMiddleware.js +1 -0
  9. package/esm/src/FetchClientOptions.js +1 -0
  10. package/esm/src/FetchClientProvider.js +200 -0
  11. package/esm/src/FetchClientResponse.js +1 -0
  12. package/esm/src/LinkHeader.js +70 -0
  13. package/esm/src/ObjectEvent.js +15 -0
  14. package/esm/src/ProblemDetails.js +47 -0
  15. package/esm/src/RateLimitMiddleware.js +115 -0
  16. package/esm/src/RateLimiter.js +347 -0
  17. package/esm/src/RequestOptions.js +1 -0
  18. package/license +20 -0
  19. package/package.json +50 -0
  20. package/readme.md +303 -0
  21. package/script/mod.js +27 -0
  22. package/script/package.json +3 -0
  23. package/script/src/Counter.js +40 -0
  24. package/script/src/DefaultHelpers.js +149 -0
  25. package/script/src/FetchClient.js +547 -0
  26. package/script/src/FetchClientCache.js +92 -0
  27. package/script/src/FetchClientContext.js +2 -0
  28. package/script/src/FetchClientMiddleware.js +2 -0
  29. package/script/src/FetchClientOptions.js +2 -0
  30. package/script/src/FetchClientProvider.js +204 -0
  31. package/script/src/FetchClientResponse.js +2 -0
  32. package/script/src/LinkHeader.js +72 -0
  33. package/script/src/ObjectEvent.js +19 -0
  34. package/script/src/ProblemDetails.js +51 -0
  35. package/script/src/RateLimitMiddleware.js +120 -0
  36. package/script/src/RateLimiter.js +356 -0
  37. package/script/src/RequestOptions.js +2 -0
  38. package/types/_dnt.test_shims.d.ts.map +1 -0
  39. package/types/deps/jsr.io/@std/assert/1.0.14/almost_equals.d.ts.map +1 -0
  40. package/types/deps/jsr.io/@std/assert/1.0.14/array_includes.d.ts.map +1 -0
  41. package/types/deps/jsr.io/@std/assert/1.0.14/assert.d.ts.map +1 -0
  42. package/types/deps/jsr.io/@std/assert/1.0.14/assertion_error.d.ts.map +1 -0
  43. package/types/deps/jsr.io/@std/assert/1.0.14/equal.d.ts.map +1 -0
  44. package/types/deps/jsr.io/@std/assert/1.0.14/equals.d.ts.map +1 -0
  45. package/types/deps/jsr.io/@std/assert/1.0.14/exists.d.ts.map +1 -0
  46. package/types/deps/jsr.io/@std/assert/1.0.14/fail.d.ts.map +1 -0
  47. package/types/deps/jsr.io/@std/assert/1.0.14/false.d.ts.map +1 -0
  48. package/types/deps/jsr.io/@std/assert/1.0.14/greater.d.ts.map +1 -0
  49. package/types/deps/jsr.io/@std/assert/1.0.14/greater_or_equal.d.ts.map +1 -0
  50. package/types/deps/jsr.io/@std/assert/1.0.14/instance_of.d.ts.map +1 -0
  51. package/types/deps/jsr.io/@std/assert/1.0.14/is_error.d.ts.map +1 -0
  52. package/types/deps/jsr.io/@std/assert/1.0.14/less.d.ts.map +1 -0
  53. package/types/deps/jsr.io/@std/assert/1.0.14/less_or_equal.d.ts.map +1 -0
  54. package/types/deps/jsr.io/@std/assert/1.0.14/match.d.ts.map +1 -0
  55. package/types/deps/jsr.io/@std/assert/1.0.14/mod.d.ts.map +1 -0
  56. package/types/deps/jsr.io/@std/assert/1.0.14/not_equals.d.ts.map +1 -0
  57. package/types/deps/jsr.io/@std/assert/1.0.14/not_instance_of.d.ts.map +1 -0
  58. package/types/deps/jsr.io/@std/assert/1.0.14/not_match.d.ts.map +1 -0
  59. package/types/deps/jsr.io/@std/assert/1.0.14/not_strict_equals.d.ts.map +1 -0
  60. package/types/deps/jsr.io/@std/assert/1.0.14/object_match.d.ts.map +1 -0
  61. package/types/deps/jsr.io/@std/assert/1.0.14/rejects.d.ts.map +1 -0
  62. package/types/deps/jsr.io/@std/assert/1.0.14/strict_equals.d.ts.map +1 -0
  63. package/types/deps/jsr.io/@std/assert/1.0.14/string_includes.d.ts.map +1 -0
  64. package/types/deps/jsr.io/@std/assert/1.0.14/throws.d.ts.map +1 -0
  65. package/types/deps/jsr.io/@std/assert/1.0.14/unimplemented.d.ts.map +1 -0
  66. package/types/deps/jsr.io/@std/assert/1.0.14/unreachable.d.ts.map +1 -0
  67. package/types/deps/jsr.io/@std/internal/1.0.10/build_message.d.ts.map +1 -0
  68. package/types/deps/jsr.io/@std/internal/1.0.10/diff.d.ts.map +1 -0
  69. package/types/deps/jsr.io/@std/internal/1.0.10/diff_str.d.ts.map +1 -0
  70. package/types/deps/jsr.io/@std/internal/1.0.10/format.d.ts.map +1 -0
  71. package/types/deps/jsr.io/@std/internal/1.0.10/styles.d.ts.map +1 -0
  72. package/types/deps/jsr.io/@std/internal/1.0.10/types.d.ts.map +1 -0
  73. package/types/mod.d.ts +11 -0
  74. package/types/mod.d.ts.map +1 -0
  75. package/types/src/Counter.d.ts +27 -0
  76. package/types/src/Counter.d.ts.map +1 -0
  77. package/types/src/DefaultHelpers.d.ts +106 -0
  78. package/types/src/DefaultHelpers.d.ts.map +1 -0
  79. package/types/src/FetchClient.d.ts +156 -0
  80. package/types/src/FetchClient.d.ts.map +1 -0
  81. package/types/src/FetchClient.test.d.ts.map +1 -0
  82. package/types/src/FetchClientCache.d.ts +62 -0
  83. package/types/src/FetchClientCache.d.ts.map +1 -0
  84. package/types/src/FetchClientContext.d.ts +8 -0
  85. package/types/src/FetchClientContext.d.ts.map +1 -0
  86. package/types/src/FetchClientMiddleware.d.ts +9 -0
  87. package/types/src/FetchClientMiddleware.d.ts.map +1 -0
  88. package/types/src/FetchClientOptions.d.ts +53 -0
  89. package/types/src/FetchClientOptions.d.ts.map +1 -0
  90. package/types/src/FetchClientProvider.d.ts +109 -0
  91. package/types/src/FetchClientProvider.d.ts.map +1 -0
  92. package/types/src/FetchClientResponse.d.ts +29 -0
  93. package/types/src/FetchClientResponse.d.ts.map +1 -0
  94. package/types/src/LinkHeader.d.ts +15 -0
  95. package/types/src/LinkHeader.d.ts.map +1 -0
  96. package/types/src/ObjectEvent.d.ts +20 -0
  97. package/types/src/ObjectEvent.d.ts.map +1 -0
  98. package/types/src/ProblemDetails.d.ts +43 -0
  99. package/types/src/ProblemDetails.d.ts.map +1 -0
  100. package/types/src/RateLimit.test.d.ts.map +1 -0
  101. package/types/src/RateLimitMiddleware.d.ts +50 -0
  102. package/types/src/RateLimitMiddleware.d.ts.map +1 -0
  103. package/types/src/RateLimiter.d.ts +179 -0
  104. package/types/src/RateLimiter.d.ts.map +1 -0
  105. package/types/src/RequestOptions.d.ts +64 -0
  106. package/types/src/RequestOptions.d.ts.map +1 -0
@@ -0,0 +1,347 @@
1
+ /**
2
+ * A rate limiter that tracks requests per time window.
3
+ */
4
+ export class RateLimiter {
5
+ options;
6
+ buckets = new Map();
7
+ groupOptions = new Map();
8
+ constructor(options) {
9
+ this.options = {
10
+ getGroupFunc: () => "global",
11
+ onRateLimitExceeded: () => { },
12
+ groups: {},
13
+ ...options,
14
+ };
15
+ // Initialize group options if provided
16
+ if (options.groups) {
17
+ for (const [groupKey, groupOptions] of Object.entries(options.groups)) {
18
+ this.groupOptions.set(groupKey, groupOptions);
19
+ }
20
+ }
21
+ }
22
+ /**
23
+ * Checks if a request is allowed and updates the rate limit state.
24
+ * @param url - The request URL
25
+ * @returns True if the request is allowed, false if rate limit is exceeded
26
+ */
27
+ isAllowed(url) {
28
+ const key = this.options.getGroupFunc(url);
29
+ const groupOptions = this.getGroupOptions(key);
30
+ const now = Date.now();
31
+ // Use group-specific options if available, otherwise fall back to global options
32
+ const maxRequests = groupOptions.maxRequests ?? 0;
33
+ const windowSeconds = groupOptions.windowSeconds ?? 0;
34
+ const onRateLimitExceeded = groupOptions.onRateLimitExceeded ??
35
+ this.options.onRateLimitExceeded;
36
+ let bucket = this.buckets.get(key);
37
+ if (!bucket) {
38
+ bucket = {
39
+ requests: [],
40
+ resetTime: now + (windowSeconds * 1000),
41
+ };
42
+ this.buckets.set(key, bucket);
43
+ }
44
+ // Clean up old requests outside the time window
45
+ const windowStart = now - (windowSeconds * 1000);
46
+ bucket.requests = bucket.requests.filter((time) => time > windowStart);
47
+ // Update reset time if all requests have expired
48
+ if (bucket.requests.length === 0) {
49
+ bucket.resetTime = now + (windowSeconds * 1000);
50
+ }
51
+ // Check if we're within the rate limit
52
+ if (bucket.requests.length >= maxRequests) {
53
+ onRateLimitExceeded(bucket.resetTime);
54
+ return false;
55
+ }
56
+ // Add the current request
57
+ bucket.requests.push(now);
58
+ return true;
59
+ }
60
+ /**
61
+ * Gets the current request count for a specific key.
62
+ * @param url - The request URL
63
+ * @returns The current number of requests in the time window
64
+ */
65
+ getRequestCount(url) {
66
+ const key = this.options.getGroupFunc(url);
67
+ const groupOptions = this.getGroupOptions(key);
68
+ const bucket = this.buckets.get(key);
69
+ if (!bucket) {
70
+ return 0;
71
+ }
72
+ const now = Date.now();
73
+ const windowSeconds = groupOptions.windowSeconds ?? 0;
74
+ const windowStart = now - (windowSeconds * 1000);
75
+ return bucket.requests.filter((time) => time > windowStart).length;
76
+ }
77
+ /**
78
+ * Gets the remaining requests allowed for a specific key.
79
+ * @param url - The request URL
80
+ * @returns The number of remaining requests allowed
81
+ */
82
+ getRemainingRequests(url) {
83
+ const key = this.options.getGroupFunc(url);
84
+ const groupOptions = this.getGroupOptions(key);
85
+ const maxRequests = groupOptions.maxRequests ?? 0;
86
+ return Math.max(0, maxRequests - this.getRequestCount(url));
87
+ }
88
+ /**
89
+ * Gets the time when the rate limit will reset for a specific key.
90
+ * @param url - The request URL
91
+ * @returns The reset time in milliseconds since epoch, or null if no bucket exists
92
+ */
93
+ getResetTime(url) {
94
+ const key = this.options.getGroupFunc(url);
95
+ const bucket = this.buckets.get(key);
96
+ return bucket?.resetTime ?? null;
97
+ }
98
+ /**
99
+ * Clears the rate limit state for a specific key.
100
+ * @param url - The request URL
101
+ */
102
+ clearBucket(url) {
103
+ const key = this.options.getGroupFunc(url);
104
+ this.buckets.delete(key);
105
+ }
106
+ /**
107
+ * Gets the group key for a URL.
108
+ * @param url - The request URL
109
+ * @returns The group key
110
+ */
111
+ getGroup(url) {
112
+ return this.options.getGroupFunc(url);
113
+ }
114
+ /**
115
+ * Gets the options for a specific group. Falls back to global options if not set.
116
+ * @param group - The group key
117
+ * @returns The options for the group
118
+ */
119
+ getGroupOptions(group) {
120
+ const options = this.groupOptions.get(group);
121
+ if (!options) {
122
+ return {
123
+ maxRequests: this.options.maxRequests,
124
+ windowSeconds: this.options.windowSeconds,
125
+ };
126
+ }
127
+ return options;
128
+ }
129
+ /**
130
+ * Checks if a group has specific options set.
131
+ * @param group - The group key
132
+ * @returns True if the group has options, false otherwise
133
+ */
134
+ hasGroupOptions(group) {
135
+ return this.groupOptions.has(group);
136
+ }
137
+ /**
138
+ * Sets options for a specific group.
139
+ * @param group - The group key
140
+ * @param options - The options to set
141
+ */
142
+ setGroupOptions(group, options) {
143
+ this.groupOptions.set(group, options);
144
+ }
145
+ /**
146
+ * Sets rate limit options for a request.
147
+ * @param url - The request URL
148
+ * @param options - The options to set for this group
149
+ */
150
+ setOptionsForRequest(url, options) {
151
+ const group = this.getGroup(url);
152
+ this.setGroupOptions(group, options);
153
+ }
154
+ /**
155
+ * Updates rate limit options for a request based on standard rate limit headers.
156
+ * @param url - The request URL
157
+ * @param method - The HTTP method
158
+ * @param headers - The response headers containing rate limit information
159
+ */
160
+ updateFromHeadersForRequest(url, headers) {
161
+ const group = this.getGroup(url);
162
+ this.updateFromHeaders(group, headers);
163
+ }
164
+ /**
165
+ * Updates rate limit options based on standard rate limit headers.
166
+ * @param group - The group key
167
+ * @param headers - The response headers containing rate limit information
168
+ */
169
+ updateFromHeaders(group, headers) {
170
+ // Get existing group-specific options (not global fallback)
171
+ const currentOptions = this.hasGroupOptions(group)
172
+ ? this.groupOptions.get(group)
173
+ : {};
174
+ const newOptions = { ...currentOptions };
175
+ // Parse IETF standard rate limit headers first, then fall back to x-ratelimit headers
176
+ let limit = null;
177
+ let window = null;
178
+ let reset = null;
179
+ // Try IETF standard headers first
180
+ const rateLimitPolicyHeader = headers.get("ratelimit-policy");
181
+ if (rateLimitPolicyHeader) {
182
+ const parsed = parseRateLimitPolicyHeader(rateLimitPolicyHeader);
183
+ if (parsed?.limit) {
184
+ limit = parsed.limit.toString();
185
+ }
186
+ if (parsed?.windowSeconds) {
187
+ window = parsed.windowSeconds.toString();
188
+ }
189
+ }
190
+ const rateLimitHeader = headers.get("ratelimit");
191
+ if (rateLimitHeader) {
192
+ const parsed = parseRateLimitHeader(rateLimitHeader);
193
+ if (parsed?.resetSeconds) {
194
+ reset = parsed.resetSeconds.toString();
195
+ }
196
+ }
197
+ // Fall back to x-ratelimit headers if IETF headers not found
198
+ if (!limit) {
199
+ limit = headers.get("x-ratelimit-limit") ||
200
+ headers.get("x-rate-limit-limit");
201
+ }
202
+ if (!window) {
203
+ window = headers.get("x-ratelimit-window") ||
204
+ headers.get("x-rate-limit-window");
205
+ }
206
+ if (!reset) {
207
+ reset = headers.get("x-ratelimit-reset") ||
208
+ headers.get("x-rate-limit-reset");
209
+ }
210
+ let hasChanges = false;
211
+ // Apply the parsed values
212
+ if (limit) {
213
+ const maxRequests = parseInt(limit, 10);
214
+ if (!isNaN(maxRequests)) {
215
+ newOptions.maxRequests = maxRequests;
216
+ hasChanges = true;
217
+ }
218
+ }
219
+ if (window) {
220
+ const windowSeconds = parseInt(window, 10);
221
+ if (!isNaN(windowSeconds)) {
222
+ newOptions.windowSeconds = windowSeconds;
223
+ hasChanges = true;
224
+ }
225
+ }
226
+ else if (reset) {
227
+ // If no window header, try to calculate from reset time
228
+ const resetTime = parseInt(reset, 10);
229
+ if (!isNaN(resetTime)) {
230
+ const now = Math.floor(Date.now() / 1000);
231
+ const windowSeconds = Math.max(1, resetTime - now);
232
+ newOptions.windowSeconds = windowSeconds;
233
+ hasChanges = true;
234
+ }
235
+ }
236
+ // Update the group options if we found valid headers
237
+ if (hasChanges) {
238
+ this.setGroupOptions(group, newOptions);
239
+ }
240
+ }
241
+ /**
242
+ * Clears all rate limit state.
243
+ */
244
+ clearAll() {
245
+ this.buckets.clear();
246
+ }
247
+ }
248
+ /**
249
+ * Creates a group generator function that groups requests by domain only (no protocol).
250
+ * @param url - The request URL
251
+ * @returns A string representing the domain without protocol
252
+ */
253
+ export function groupByDomain(url) {
254
+ try {
255
+ const urlObj = new URL(url);
256
+ return urlObj.hostname;
257
+ }
258
+ catch {
259
+ return url;
260
+ }
261
+ }
262
+ /**
263
+ * Creates an IETF standard RateLimit header value.
264
+ * @param info - The rate limit information
265
+ * @returns The formatted RateLimit header value
266
+ */
267
+ export function buildRateLimitHeader(info) {
268
+ let headerValue = `"${info.policy}";r=${info.remaining}`;
269
+ if (info.resetSeconds > 0) {
270
+ headerValue += `;t=${info.resetSeconds}`;
271
+ }
272
+ return headerValue;
273
+ }
274
+ /**
275
+ * Creates an IETF standard RateLimit-Policy header value.
276
+ * @param info - The rate limit information
277
+ * @returns The formatted RateLimit-Policy header value
278
+ */
279
+ export function buildRateLimitPolicyHeader(info) {
280
+ let headerValue = `"${info.policy}";q=${info.limit}`;
281
+ if (info.windowSeconds && info.windowSeconds > 0) {
282
+ headerValue += `;w=${info.windowSeconds}`;
283
+ }
284
+ return headerValue;
285
+ }
286
+ /**
287
+ * Parses an IETF standard RateLimit header value.
288
+ * @param headerValue - The RateLimit header value to parse
289
+ * @returns The parsed rate limit information or null if invalid
290
+ */
291
+ export function parseRateLimitHeader(headerValue) {
292
+ if (!headerValue)
293
+ return null;
294
+ try {
295
+ const result = {};
296
+ // Extract policy name (quoted string at the beginning)
297
+ const policyMatch = headerValue.match(/^"([^"]+)"/);
298
+ if (policyMatch) {
299
+ result.policy = policyMatch[1];
300
+ }
301
+ // Extract remaining (r parameter)
302
+ const remainingMatch = headerValue.match(/r=(\d+)/);
303
+ if (remainingMatch) {
304
+ result.remaining = parseInt(remainingMatch[1], 10);
305
+ }
306
+ // Extract reset time (t parameter)
307
+ const resetMatch = headerValue.match(/t=(\d+)/);
308
+ if (resetMatch) {
309
+ result.resetSeconds = parseInt(resetMatch[1], 10);
310
+ }
311
+ return result;
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ }
317
+ /**
318
+ * Parses an IETF standard RateLimit-Policy header value.
319
+ * @param headerValue - The RateLimit-Policy header value to parse
320
+ * @returns The parsed rate limit policy information or null if invalid
321
+ */
322
+ export function parseRateLimitPolicyHeader(headerValue) {
323
+ if (!headerValue)
324
+ return null;
325
+ try {
326
+ const result = {};
327
+ // Extract policy name (quoted string at the beginning)
328
+ const policyMatch = headerValue.match(/^"([^"]+)"/);
329
+ if (policyMatch) {
330
+ result.policy = policyMatch[1];
331
+ }
332
+ // Extract quota/limit (q parameter)
333
+ const quotaMatch = headerValue.match(/q=(\d+)/);
334
+ if (quotaMatch) {
335
+ result.limit = parseInt(quotaMatch[1], 10);
336
+ }
337
+ // Extract window (w parameter)
338
+ const windowMatch = headerValue.match(/w=(\d+)/);
339
+ if (windowMatch) {
340
+ result.windowSeconds = parseInt(windowMatch[1], 10);
341
+ }
342
+ return result;
343
+ }
344
+ catch {
345
+ return null;
346
+ }
347
+ }
@@ -0,0 +1 @@
1
+ export {};
package/license ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Foundatio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@foundatiofx/fetchclient",
3
+ "version": "0.47.0",
4
+ "description": "A typed JSON fetch client with middleware support for Deno, Node and the browser.",
5
+ "keywords": [
6
+ "Fetch",
7
+ "Middleware",
8
+ "ProblemDetails",
9
+ "Problem"
10
+ ],
11
+ "author": {
12
+ "name": "Eric J. Smith",
13
+ "email": "eric@exceptionless.com",
14
+ "url": "https://exceptionless.com"
15
+ },
16
+ "homepage": "https://github.com/FoundatioFx/FetchClient#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/FoundatioFx/FetchClient.git"
20
+ },
21
+ "license": "Apache-2.0",
22
+ "bugs": {
23
+ "url": "https://github.com/FoundatioFx/FetchClient/issues"
24
+ },
25
+ "main": "./script/mod.js",
26
+ "module": "./esm/mod.js",
27
+ "types": "./types/mod.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "import": {
31
+ "types": "./types/mod.d.ts",
32
+ "default": "./esm/mod.js"
33
+ },
34
+ "require": {
35
+ "types": "./types/mod.d.ts",
36
+ "default": "./script/mod.js"
37
+ }
38
+ }
39
+ },
40
+ "scripts": {
41
+ "test": "node test_runner.js"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.9.0",
45
+ "picocolors": "^1.0.0",
46
+ "zod": "^4.1.4",
47
+ "@deno/shim-deno": "~0.18.0"
48
+ },
49
+ "_generatedBy": "dnt@dev"
50
+ }