@igniter-js/caller 0.1.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.
- package/AGENTS.md +124 -0
- package/CHANGELOG.md +14 -0
- package/README.md +233 -0
- package/dist/index.d.mts +738 -0
- package/dist/index.d.ts +738 -0
- package/dist/index.js +1343 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1332 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
import { IgniterError } from '@igniter-js/core';
|
|
2
|
+
|
|
3
|
+
// src/builder/igniter-caller.builder.ts
|
|
4
|
+
var IgniterCallerBuilder = class _IgniterCallerBuilder {
|
|
5
|
+
constructor(state, factory) {
|
|
6
|
+
this.state = state;
|
|
7
|
+
this.factory = factory;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new builder instance.
|
|
11
|
+
*/
|
|
12
|
+
static create(factory) {
|
|
13
|
+
return new _IgniterCallerBuilder({}, factory);
|
|
14
|
+
}
|
|
15
|
+
/** Sets the base URL for all requests. */
|
|
16
|
+
withBaseUrl(baseURL) {
|
|
17
|
+
return new _IgniterCallerBuilder({ ...this.state, baseURL }, this.factory);
|
|
18
|
+
}
|
|
19
|
+
/** Merges default headers for all requests. */
|
|
20
|
+
withHeaders(headers) {
|
|
21
|
+
return new _IgniterCallerBuilder({ ...this.state, headers }, this.factory);
|
|
22
|
+
}
|
|
23
|
+
/** Sets default cookies (sent as the `Cookie` header). */
|
|
24
|
+
withCookies(cookies) {
|
|
25
|
+
return new _IgniterCallerBuilder({ ...this.state, cookies }, this.factory);
|
|
26
|
+
}
|
|
27
|
+
/** Attaches a logger instance. */
|
|
28
|
+
withLogger(logger) {
|
|
29
|
+
return new _IgniterCallerBuilder({ ...this.state, logger }, this.factory);
|
|
30
|
+
}
|
|
31
|
+
/** Adds a request interceptor that runs before each request. */
|
|
32
|
+
withRequestInterceptor(interceptor) {
|
|
33
|
+
const requestInterceptors = [
|
|
34
|
+
...this.state.requestInterceptors || [],
|
|
35
|
+
interceptor
|
|
36
|
+
];
|
|
37
|
+
return new _IgniterCallerBuilder(
|
|
38
|
+
{ ...this.state, requestInterceptors },
|
|
39
|
+
this.factory
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
/** Adds a response interceptor that runs after each request. */
|
|
43
|
+
withResponseInterceptor(interceptor) {
|
|
44
|
+
const responseInterceptors = [
|
|
45
|
+
...this.state.responseInterceptors || [],
|
|
46
|
+
interceptor
|
|
47
|
+
];
|
|
48
|
+
return new _IgniterCallerBuilder(
|
|
49
|
+
{ ...this.state, responseInterceptors },
|
|
50
|
+
this.factory
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Configures a persistent store adapter for caching.
|
|
55
|
+
*
|
|
56
|
+
* When configured, cache operations will use the store (e.g., Redis)
|
|
57
|
+
* instead of in-memory cache, enabling persistent cache across deployments.
|
|
58
|
+
*/
|
|
59
|
+
withStore(store, options) {
|
|
60
|
+
return new _IgniterCallerBuilder(
|
|
61
|
+
{ ...this.state, store, storeOptions: options },
|
|
62
|
+
this.factory
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Configures schema-based type safety and validation.
|
|
67
|
+
*
|
|
68
|
+
* Enables automatic type inference for requests/responses based on
|
|
69
|
+
* route and method, with optional runtime validation via Zod.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const api = IgniterCaller.create()
|
|
74
|
+
* .withSchemas({
|
|
75
|
+
* '/users': {
|
|
76
|
+
* GET: {
|
|
77
|
+
* responses: {
|
|
78
|
+
* 200: z.array(UserSchema),
|
|
79
|
+
* 401: ErrorSchema,
|
|
80
|
+
* },
|
|
81
|
+
* },
|
|
82
|
+
* POST: {
|
|
83
|
+
* request: CreateUserSchema,
|
|
84
|
+
* responses: {
|
|
85
|
+
* 201: UserSchema,
|
|
86
|
+
* 400: ValidationErrorSchema,
|
|
87
|
+
* },
|
|
88
|
+
* },
|
|
89
|
+
* },
|
|
90
|
+
* })
|
|
91
|
+
* .build()
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
withSchemas(schemas, validation) {
|
|
95
|
+
return new _IgniterCallerBuilder(
|
|
96
|
+
{ ...this.state, schemas, schemaValidation: validation },
|
|
97
|
+
this.factory
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Builds the `IgniterCaller` instance.
|
|
102
|
+
*/
|
|
103
|
+
build() {
|
|
104
|
+
return this.factory(this.state);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var IgniterCallerError = class _IgniterCallerError extends IgniterError {
|
|
108
|
+
constructor(payload) {
|
|
109
|
+
const metadata = {
|
|
110
|
+
...payload.metadata,
|
|
111
|
+
operation: payload.operation,
|
|
112
|
+
statusText: payload.statusText
|
|
113
|
+
};
|
|
114
|
+
const details = payload.details ?? (payload.cause !== void 0 ? { cause: payload.cause } : void 0);
|
|
115
|
+
super({
|
|
116
|
+
code: payload.code,
|
|
117
|
+
message: payload.message,
|
|
118
|
+
statusCode: payload.statusCode,
|
|
119
|
+
causer: "@igniter-js/caller",
|
|
120
|
+
details,
|
|
121
|
+
metadata,
|
|
122
|
+
logger: payload.logger
|
|
123
|
+
});
|
|
124
|
+
this.name = "IgniterCallerError";
|
|
125
|
+
this.operation = payload.operation;
|
|
126
|
+
this.statusText = payload.statusText;
|
|
127
|
+
this.cause = payload.cause;
|
|
128
|
+
}
|
|
129
|
+
static is(error) {
|
|
130
|
+
return error instanceof _IgniterCallerError;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/utils/body.ts
|
|
135
|
+
var IgniterCallerBodyUtils = class {
|
|
136
|
+
/**
|
|
137
|
+
* Returns true when the request body should be passed to `fetch` as-is.
|
|
138
|
+
*/
|
|
139
|
+
static isRawBody(body) {
|
|
140
|
+
if (!body) return false;
|
|
141
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return true;
|
|
142
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) return true;
|
|
143
|
+
if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer)
|
|
144
|
+
return true;
|
|
145
|
+
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams)
|
|
146
|
+
return true;
|
|
147
|
+
if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream)
|
|
148
|
+
return true;
|
|
149
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) return true;
|
|
150
|
+
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body))
|
|
151
|
+
return true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Removes Content-Type for FormData so fetch can set boundaries automatically.
|
|
156
|
+
*/
|
|
157
|
+
static normalizeHeadersForBody(headers, body) {
|
|
158
|
+
if (!headers) return headers;
|
|
159
|
+
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
160
|
+
if (!isFormData) return headers;
|
|
161
|
+
if (!("Content-Type" in headers)) return headers;
|
|
162
|
+
const { "Content-Type": _contentType, ...rest } = headers;
|
|
163
|
+
return rest;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/utils/cache.ts
|
|
168
|
+
var _IgniterCallerCacheUtils = class _IgniterCallerCacheUtils {
|
|
169
|
+
/**
|
|
170
|
+
* Configures a persistent store adapter for caching.
|
|
171
|
+
*
|
|
172
|
+
* When configured, cache operations will use the store (e.g., Redis)
|
|
173
|
+
* instead of in-memory cache, enabling persistent cache across deployments.
|
|
174
|
+
*/
|
|
175
|
+
static setStore(store, options) {
|
|
176
|
+
_IgniterCallerCacheUtils.store = store;
|
|
177
|
+
if (options) {
|
|
178
|
+
_IgniterCallerCacheUtils.storeOptions = {
|
|
179
|
+
..._IgniterCallerCacheUtils.storeOptions,
|
|
180
|
+
...options
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Gets the configured store adapter.
|
|
186
|
+
*/
|
|
187
|
+
static getStore() {
|
|
188
|
+
return _IgniterCallerCacheUtils.store;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Gets cached data if it exists and is not stale.
|
|
192
|
+
*/
|
|
193
|
+
static async get(key, staleTime) {
|
|
194
|
+
const prefixedKey = _IgniterCallerCacheUtils.getPrefixedKey(key);
|
|
195
|
+
if (_IgniterCallerCacheUtils.store) {
|
|
196
|
+
try {
|
|
197
|
+
const data = await _IgniterCallerCacheUtils.store.get(prefixedKey);
|
|
198
|
+
if (data !== null) {
|
|
199
|
+
return data;
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error("IgniterCaller: Store get failed", error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const entry = _IgniterCallerCacheUtils.cache.get(prefixedKey);
|
|
206
|
+
if (!entry) return void 0;
|
|
207
|
+
if (staleTime && Date.now() - entry.timestamp > staleTime) {
|
|
208
|
+
_IgniterCallerCacheUtils.cache.delete(prefixedKey);
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
return entry.data;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Stores data in cache with current timestamp.
|
|
215
|
+
*/
|
|
216
|
+
static async set(key, data, ttl) {
|
|
217
|
+
const prefixedKey = _IgniterCallerCacheUtils.getPrefixedKey(key);
|
|
218
|
+
const effectiveTtl = ttl || _IgniterCallerCacheUtils.storeOptions.ttl || 3600;
|
|
219
|
+
if (_IgniterCallerCacheUtils.store) {
|
|
220
|
+
try {
|
|
221
|
+
await _IgniterCallerCacheUtils.store.set(prefixedKey, data, {
|
|
222
|
+
ttl: effectiveTtl
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error("IgniterCaller: Store set failed", error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
_IgniterCallerCacheUtils.cache.set(prefixedKey, {
|
|
230
|
+
data,
|
|
231
|
+
timestamp: Date.now()
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Clears a specific cache entry.
|
|
236
|
+
*/
|
|
237
|
+
static async clear(key) {
|
|
238
|
+
const prefixedKey = _IgniterCallerCacheUtils.getPrefixedKey(key);
|
|
239
|
+
if (_IgniterCallerCacheUtils.store) {
|
|
240
|
+
try {
|
|
241
|
+
await _IgniterCallerCacheUtils.store.delete(prefixedKey);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error("IgniterCaller: Store delete failed", error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
_IgniterCallerCacheUtils.cache.delete(prefixedKey);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Clears all cache entries matching a pattern.
|
|
250
|
+
*
|
|
251
|
+
* @param pattern Glob pattern (e.g., '/users/*') or exact key
|
|
252
|
+
*/
|
|
253
|
+
static async clearPattern(pattern) {
|
|
254
|
+
const prefixedPattern = _IgniterCallerCacheUtils.getPrefixedKey(pattern);
|
|
255
|
+
const regex = _IgniterCallerCacheUtils.globToRegex(prefixedPattern);
|
|
256
|
+
for (const key of _IgniterCallerCacheUtils.cache.keys()) {
|
|
257
|
+
if (regex.test(key)) {
|
|
258
|
+
_IgniterCallerCacheUtils.cache.delete(key);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (_IgniterCallerCacheUtils.store) {
|
|
262
|
+
console.warn(
|
|
263
|
+
"IgniterCaller: Pattern-based cache invalidation in store requires manual implementation"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Clears all cache entries.
|
|
269
|
+
*/
|
|
270
|
+
static async clearAll() {
|
|
271
|
+
_IgniterCallerCacheUtils.cache.clear();
|
|
272
|
+
if (_IgniterCallerCacheUtils.store) {
|
|
273
|
+
console.warn(
|
|
274
|
+
"IgniterCaller: clearAll() only clears in-memory cache. Store cache requires manual cleanup."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Adds the configured prefix to a key.
|
|
280
|
+
*/
|
|
281
|
+
static getPrefixedKey(key) {
|
|
282
|
+
const prefix = _IgniterCallerCacheUtils.storeOptions.keyPrefix || "";
|
|
283
|
+
return `${prefix}${key}`;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Converts a glob pattern to a RegExp.
|
|
287
|
+
*/
|
|
288
|
+
static globToRegex(pattern) {
|
|
289
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
290
|
+
return new RegExp(`^${escaped}$`);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
_IgniterCallerCacheUtils.cache = /* @__PURE__ */ new Map();
|
|
294
|
+
_IgniterCallerCacheUtils.store = null;
|
|
295
|
+
_IgniterCallerCacheUtils.storeOptions = {
|
|
296
|
+
ttl: 3600,
|
|
297
|
+
keyPrefix: "igniter:caller:",
|
|
298
|
+
fallbackToFetch: true
|
|
299
|
+
};
|
|
300
|
+
var IgniterCallerCacheUtils = _IgniterCallerCacheUtils;
|
|
301
|
+
|
|
302
|
+
// src/utils/schema.ts
|
|
303
|
+
var IgniterCallerSchemaUtils = class _IgniterCallerSchemaUtils {
|
|
304
|
+
/**
|
|
305
|
+
* Matches a URL path against schema map paths (supports path parameters).
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```ts
|
|
309
|
+
* matchPath('/users/123', '/users/:id') // { matched: true, params: { id: '123' } }
|
|
310
|
+
* matchPath('/users', '/users/:id') // { matched: false }
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
static matchPath(actualPath, schemaPath) {
|
|
314
|
+
if (actualPath === schemaPath) {
|
|
315
|
+
return { matched: true, params: {} };
|
|
316
|
+
}
|
|
317
|
+
const paramNames = [];
|
|
318
|
+
const regexPattern = schemaPath.replace(/:[^/]+/g, (match2) => {
|
|
319
|
+
paramNames.push(match2.slice(1));
|
|
320
|
+
return "([^/]+)";
|
|
321
|
+
}).replace(/\//g, "\\/");
|
|
322
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
323
|
+
const match = actualPath.match(regex);
|
|
324
|
+
if (!match) {
|
|
325
|
+
return { matched: false };
|
|
326
|
+
}
|
|
327
|
+
const params = {};
|
|
328
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
329
|
+
params[paramNames[i]] = match[i + 1];
|
|
330
|
+
}
|
|
331
|
+
return { matched: true, params };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Finds the schema for a given path and method from the schema map.
|
|
335
|
+
*/
|
|
336
|
+
static findSchema(schemaMap, path, method) {
|
|
337
|
+
if (!schemaMap) {
|
|
338
|
+
return { schema: void 0, params: {} };
|
|
339
|
+
}
|
|
340
|
+
const exactMatch = schemaMap[path]?.[method];
|
|
341
|
+
if (exactMatch) {
|
|
342
|
+
return { schema: exactMatch, params: {} };
|
|
343
|
+
}
|
|
344
|
+
for (const [schemaPath, methods] of Object.entries(schemaMap)) {
|
|
345
|
+
const matchResult = _IgniterCallerSchemaUtils.matchPath(path, schemaPath);
|
|
346
|
+
if (matchResult.matched) {
|
|
347
|
+
const schema = methods?.[method];
|
|
348
|
+
if (schema) {
|
|
349
|
+
return { schema, params: matchResult.params || {} };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { schema: void 0, params: {} };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Validates input using StandardSchemaV1.
|
|
357
|
+
*
|
|
358
|
+
* If the schema provides `~standard.validate`, it will be used.
|
|
359
|
+
* Otherwise, returns the input as-is.
|
|
360
|
+
*/
|
|
361
|
+
static async validateWithStandardSchema(schema, input) {
|
|
362
|
+
const standard = schema?.["~standard"];
|
|
363
|
+
if (!standard?.validate) {
|
|
364
|
+
return input;
|
|
365
|
+
}
|
|
366
|
+
const result = await standard.validate(input);
|
|
367
|
+
if (result?.issues?.length) {
|
|
368
|
+
throw {
|
|
369
|
+
issues: result.issues,
|
|
370
|
+
_standardSchemaValidationError: true
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return result?.value ?? input;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Validates request body against schema.
|
|
377
|
+
*
|
|
378
|
+
* @returns Validated data or throws/logs error based on validation mode
|
|
379
|
+
*/
|
|
380
|
+
static async validateRequest(data, schema, options, context, logger) {
|
|
381
|
+
if (!schema || options?.mode === "off") {
|
|
382
|
+
return data;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const validated = await _IgniterCallerSchemaUtils.validateWithStandardSchema(schema, data);
|
|
386
|
+
return validated;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (options?.onValidationError && error?._standardSchemaValidationError) {
|
|
389
|
+
options.onValidationError(error, { ...context, statusCode: 0 });
|
|
390
|
+
}
|
|
391
|
+
if (options?.mode === "soft") {
|
|
392
|
+
logger?.warn("Request validation failed (soft mode)", {
|
|
393
|
+
url: context.url,
|
|
394
|
+
method: context.method,
|
|
395
|
+
errors: error?.issues
|
|
396
|
+
});
|
|
397
|
+
return data;
|
|
398
|
+
}
|
|
399
|
+
throw new IgniterCallerError({
|
|
400
|
+
code: "IGNITER_CALLER_REQUEST_VALIDATION_FAILED",
|
|
401
|
+
message: `Request validation failed for ${context.method} ${context.url}`,
|
|
402
|
+
statusCode: 400,
|
|
403
|
+
details: error?.issues,
|
|
404
|
+
operation: "validateRequest"
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Validates response data against schema.
|
|
410
|
+
*
|
|
411
|
+
* @returns Validated data or throws/logs error based on validation mode
|
|
412
|
+
*/
|
|
413
|
+
static async validateResponse(data, schema, statusCode, options, context, logger) {
|
|
414
|
+
if (!schema || options?.mode === "off") {
|
|
415
|
+
return data;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const validated = await _IgniterCallerSchemaUtils.validateWithStandardSchema(schema, data);
|
|
419
|
+
return validated;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (options?.onValidationError && error?._standardSchemaValidationError) {
|
|
422
|
+
options.onValidationError(error, { ...context, statusCode });
|
|
423
|
+
}
|
|
424
|
+
if (options?.mode === "soft") {
|
|
425
|
+
logger?.warn("Response validation failed (soft mode)", {
|
|
426
|
+
url: context.url,
|
|
427
|
+
method: context.method,
|
|
428
|
+
statusCode,
|
|
429
|
+
errors: error?.issues
|
|
430
|
+
});
|
|
431
|
+
return data;
|
|
432
|
+
}
|
|
433
|
+
throw new IgniterCallerError({
|
|
434
|
+
code: "IGNITER_CALLER_RESPONSE_VALIDATION_FAILED",
|
|
435
|
+
message: `Response validation failed for ${context.method} ${context.url} (${statusCode})`,
|
|
436
|
+
statusCode,
|
|
437
|
+
details: error?.issues,
|
|
438
|
+
operation: "validateResponse"
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/utils/url.ts
|
|
445
|
+
var IgniterCallerUrlUtils = class {
|
|
446
|
+
static buildUrl(params) {
|
|
447
|
+
const { url, baseURL, query } = params;
|
|
448
|
+
let fullUrl = url;
|
|
449
|
+
if (baseURL && !/^https?:\/\//i.test(url)) {
|
|
450
|
+
fullUrl = baseURL + url;
|
|
451
|
+
}
|
|
452
|
+
if (query && Object.keys(query).length > 0) {
|
|
453
|
+
const queryParams = new URLSearchParams(
|
|
454
|
+
Object.entries(query).map(([key, value]) => [key, String(value)])
|
|
455
|
+
).toString();
|
|
456
|
+
fullUrl += (fullUrl.includes("?") ? "&" : "?") + queryParams;
|
|
457
|
+
}
|
|
458
|
+
return fullUrl;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// src/builder/igniter-caller-request.builder.ts
|
|
463
|
+
var IgniterCallerRequestBuilder = class {
|
|
464
|
+
constructor(params) {
|
|
465
|
+
this.options = {
|
|
466
|
+
method: "GET",
|
|
467
|
+
url: "",
|
|
468
|
+
headers: {
|
|
469
|
+
"Content-Type": "application/json"
|
|
470
|
+
},
|
|
471
|
+
timeout: 3e4,
|
|
472
|
+
cache: "default"
|
|
473
|
+
};
|
|
474
|
+
if (params.baseURL) this.options.baseURL = params.baseURL;
|
|
475
|
+
if (params.defaultHeaders) {
|
|
476
|
+
this.options.headers = {
|
|
477
|
+
...this.options.headers,
|
|
478
|
+
...params.defaultHeaders
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (params.defaultCookies) {
|
|
482
|
+
const cookieStr = Object.entries(params.defaultCookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
483
|
+
this.options.headers = { ...this.options.headers, Cookie: cookieStr };
|
|
484
|
+
}
|
|
485
|
+
this.logger = params.logger;
|
|
486
|
+
this.requestInterceptors = params.requestInterceptors;
|
|
487
|
+
this.responseInterceptors = params.responseInterceptors;
|
|
488
|
+
this.eventEmitter = params.eventEmitter;
|
|
489
|
+
this.schemas = params.schemas;
|
|
490
|
+
this.schemaValidation = params.schemaValidation;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Overrides the logger for this request chain.
|
|
494
|
+
*/
|
|
495
|
+
withLogger(logger) {
|
|
496
|
+
this.logger = logger;
|
|
497
|
+
return this;
|
|
498
|
+
}
|
|
499
|
+
method(method) {
|
|
500
|
+
this.options.method = method;
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
url(url) {
|
|
504
|
+
this.options.url = url;
|
|
505
|
+
return this;
|
|
506
|
+
}
|
|
507
|
+
body(body) {
|
|
508
|
+
this.options.body = body;
|
|
509
|
+
return this;
|
|
510
|
+
}
|
|
511
|
+
params(params) {
|
|
512
|
+
this.options.params = params;
|
|
513
|
+
return this;
|
|
514
|
+
}
|
|
515
|
+
headers(headers) {
|
|
516
|
+
this.options.headers = { ...this.options.headers, ...headers };
|
|
517
|
+
return this;
|
|
518
|
+
}
|
|
519
|
+
timeout(timeout) {
|
|
520
|
+
this.options.timeout = timeout;
|
|
521
|
+
return this;
|
|
522
|
+
}
|
|
523
|
+
cache(cache, key) {
|
|
524
|
+
this.options.cache = cache;
|
|
525
|
+
this.cacheKey = key;
|
|
526
|
+
return this;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Configures retry behavior for failed requests.
|
|
530
|
+
*/
|
|
531
|
+
retry(maxAttempts, options) {
|
|
532
|
+
this.retryOptions = { maxAttempts, ...options };
|
|
533
|
+
return this;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Provides a fallback value if the request fails.
|
|
537
|
+
*/
|
|
538
|
+
fallback(fn) {
|
|
539
|
+
this.fallbackFn = fn;
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Sets cache stale time in milliseconds.
|
|
544
|
+
*/
|
|
545
|
+
stale(milliseconds) {
|
|
546
|
+
this.staleTime = milliseconds;
|
|
547
|
+
return this;
|
|
548
|
+
}
|
|
549
|
+
responseType(schema) {
|
|
550
|
+
this.options.responseSchema = schema;
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Downloads a file via GET request.
|
|
555
|
+
*/
|
|
556
|
+
getFile(url) {
|
|
557
|
+
this.options.method = "GET";
|
|
558
|
+
this.options.url = url;
|
|
559
|
+
return {
|
|
560
|
+
execute: async () => {
|
|
561
|
+
const {
|
|
562
|
+
url: finalUrl,
|
|
563
|
+
requestInit,
|
|
564
|
+
controller,
|
|
565
|
+
timeoutId
|
|
566
|
+
} = this.buildRequest();
|
|
567
|
+
this.logger?.debug("IgniterCaller.getFile started", { url: finalUrl });
|
|
568
|
+
try {
|
|
569
|
+
const response = await fetch(finalUrl, {
|
|
570
|
+
...requestInit,
|
|
571
|
+
signal: controller.signal
|
|
572
|
+
});
|
|
573
|
+
clearTimeout(timeoutId);
|
|
574
|
+
if (!response.ok) {
|
|
575
|
+
const errorText = await response.text().catch(() => "");
|
|
576
|
+
return {
|
|
577
|
+
file: null,
|
|
578
|
+
error: new IgniterCallerError({
|
|
579
|
+
code: "IGNITER_CALLER_HTTP_ERROR",
|
|
580
|
+
operation: "download",
|
|
581
|
+
message: errorText || `HTTP error! status: ${response.status}`,
|
|
582
|
+
statusText: response.statusText,
|
|
583
|
+
statusCode: response.status,
|
|
584
|
+
logger: this.logger,
|
|
585
|
+
metadata: { url: finalUrl }
|
|
586
|
+
})
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const data = await response.blob();
|
|
590
|
+
const contentType = response.headers.get("content-type");
|
|
591
|
+
const extension = contentType?.split("/")[1];
|
|
592
|
+
const filenameHeader = response.headers.get("content-disposition");
|
|
593
|
+
const filename = filenameHeader?.split("filename=")[1];
|
|
594
|
+
const filenameExtension = filename?.split(".").pop();
|
|
595
|
+
if (typeof File === "undefined") {
|
|
596
|
+
return {
|
|
597
|
+
file: null,
|
|
598
|
+
error: new IgniterCallerError({
|
|
599
|
+
code: "IGNITER_CALLER_UNKNOWN_ERROR",
|
|
600
|
+
operation: "download",
|
|
601
|
+
message: "`File` is not available in this runtime. Consider using `response.blob()` directly.",
|
|
602
|
+
logger: this.logger,
|
|
603
|
+
metadata: { url: finalUrl }
|
|
604
|
+
})
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const file = new File(
|
|
608
|
+
[data],
|
|
609
|
+
filename || `file.${extension || filenameExtension || "unknown"}`
|
|
610
|
+
);
|
|
611
|
+
this.logger?.info("IgniterCaller.getFile success", {
|
|
612
|
+
url: finalUrl,
|
|
613
|
+
size: file.size,
|
|
614
|
+
type: file.type
|
|
615
|
+
});
|
|
616
|
+
return { file, error: null };
|
|
617
|
+
} catch (error) {
|
|
618
|
+
clearTimeout(timeoutId);
|
|
619
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
620
|
+
return {
|
|
621
|
+
file: null,
|
|
622
|
+
error: new IgniterCallerError({
|
|
623
|
+
code: "IGNITER_CALLER_TIMEOUT",
|
|
624
|
+
operation: "download",
|
|
625
|
+
message: `Request timeout after ${this.options.timeout || 3e4}ms`,
|
|
626
|
+
statusCode: 408,
|
|
627
|
+
logger: this.logger,
|
|
628
|
+
metadata: { url: finalUrl },
|
|
629
|
+
cause: error
|
|
630
|
+
})
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
file: null,
|
|
635
|
+
error: new IgniterCallerError({
|
|
636
|
+
code: "IGNITER_CALLER_UNKNOWN_ERROR",
|
|
637
|
+
operation: "download",
|
|
638
|
+
message: error?.message || "Failed to download file",
|
|
639
|
+
logger: this.logger,
|
|
640
|
+
metadata: { url: finalUrl },
|
|
641
|
+
cause: error
|
|
642
|
+
})
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async execute() {
|
|
649
|
+
const effectiveCacheKey = this.cacheKey || this.options.url;
|
|
650
|
+
if (effectiveCacheKey && this.staleTime) {
|
|
651
|
+
const cached = await IgniterCallerCacheUtils.get(
|
|
652
|
+
effectiveCacheKey,
|
|
653
|
+
this.staleTime
|
|
654
|
+
);
|
|
655
|
+
if (cached !== void 0) {
|
|
656
|
+
this.logger?.debug("IgniterCaller.execute cache hit", {
|
|
657
|
+
key: effectiveCacheKey
|
|
658
|
+
});
|
|
659
|
+
const cachedResult = { data: cached, error: void 0 };
|
|
660
|
+
await this.emitEvent(cachedResult);
|
|
661
|
+
return cachedResult;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const result = await this.executeWithRetry();
|
|
665
|
+
if (result.error && this.fallbackFn) {
|
|
666
|
+
this.logger?.debug("IgniterCaller.execute applying fallback", {
|
|
667
|
+
error: result.error
|
|
668
|
+
});
|
|
669
|
+
const fallbackResult = { data: this.fallbackFn(), error: void 0 };
|
|
670
|
+
await this.emitEvent(fallbackResult);
|
|
671
|
+
return fallbackResult;
|
|
672
|
+
}
|
|
673
|
+
if (!result.error && result.data !== void 0 && effectiveCacheKey) {
|
|
674
|
+
await IgniterCallerCacheUtils.set(
|
|
675
|
+
effectiveCacheKey,
|
|
676
|
+
result.data,
|
|
677
|
+
this.staleTime
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
await this.emitEvent(result);
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
async executeWithRetry() {
|
|
684
|
+
const maxAttempts = this.retryOptions?.maxAttempts || 1;
|
|
685
|
+
const baseDelay = this.retryOptions?.baseDelay || 1e3;
|
|
686
|
+
const backoff = this.retryOptions?.backoff || "linear";
|
|
687
|
+
const retryOnStatus = this.retryOptions?.retryOnStatus || [
|
|
688
|
+
408,
|
|
689
|
+
429,
|
|
690
|
+
500,
|
|
691
|
+
502,
|
|
692
|
+
503,
|
|
693
|
+
504
|
|
694
|
+
];
|
|
695
|
+
let lastError;
|
|
696
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
697
|
+
if (attempt > 0) {
|
|
698
|
+
const delay = backoff === "exponential" ? baseDelay * 2 ** (attempt - 1) : baseDelay * attempt;
|
|
699
|
+
this.logger?.debug("IgniterCaller.execute retrying", {
|
|
700
|
+
attempt,
|
|
701
|
+
delay
|
|
702
|
+
});
|
|
703
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
704
|
+
}
|
|
705
|
+
const result = await this.executeSingleRequest();
|
|
706
|
+
if (!result.error) {
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
const shouldRetry = result.error instanceof IgniterCallerError && result.error.statusCode && retryOnStatus.includes(result.error.statusCode);
|
|
710
|
+
if (!shouldRetry || attempt === maxAttempts - 1) {
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
713
|
+
lastError = result.error;
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
data: void 0,
|
|
717
|
+
error: lastError
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async executeSingleRequest() {
|
|
721
|
+
const { responseSchema } = this.options;
|
|
722
|
+
let { url, requestInit, controller, timeoutId } = this.buildRequest();
|
|
723
|
+
this.logger?.debug("IgniterCaller.execute started", {
|
|
724
|
+
method: this.options.method,
|
|
725
|
+
url
|
|
726
|
+
});
|
|
727
|
+
if (this.requestInterceptors && this.requestInterceptors.length > 0) {
|
|
728
|
+
let modifiedOptions = { ...this.options, url };
|
|
729
|
+
for (const interceptor of this.requestInterceptors) {
|
|
730
|
+
modifiedOptions = await interceptor(modifiedOptions);
|
|
731
|
+
}
|
|
732
|
+
this.options = modifiedOptions;
|
|
733
|
+
const rebuilt = this.buildRequest();
|
|
734
|
+
url = rebuilt.url;
|
|
735
|
+
requestInit = rebuilt.requestInit;
|
|
736
|
+
controller = rebuilt.controller;
|
|
737
|
+
timeoutId = rebuilt.timeoutId;
|
|
738
|
+
}
|
|
739
|
+
if (this.schemas && this.options.body) {
|
|
740
|
+
const { schema: endpointSchema } = IgniterCallerSchemaUtils.findSchema(
|
|
741
|
+
this.schemas,
|
|
742
|
+
url,
|
|
743
|
+
this.options.method
|
|
744
|
+
);
|
|
745
|
+
if (endpointSchema?.request) {
|
|
746
|
+
try {
|
|
747
|
+
await IgniterCallerSchemaUtils.validateRequest(
|
|
748
|
+
this.options.body,
|
|
749
|
+
endpointSchema.request,
|
|
750
|
+
this.schemaValidation,
|
|
751
|
+
{ url, method: this.options.method },
|
|
752
|
+
this.logger
|
|
753
|
+
);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
return {
|
|
756
|
+
data: void 0,
|
|
757
|
+
error
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
const httpResponse = await fetch(url, {
|
|
764
|
+
...requestInit,
|
|
765
|
+
signal: controller.signal
|
|
766
|
+
});
|
|
767
|
+
clearTimeout(timeoutId);
|
|
768
|
+
if (!httpResponse.ok) {
|
|
769
|
+
const errorText = await httpResponse.text().catch(() => "");
|
|
770
|
+
return {
|
|
771
|
+
data: void 0,
|
|
772
|
+
error: new IgniterCallerError({
|
|
773
|
+
code: "IGNITER_CALLER_HTTP_ERROR",
|
|
774
|
+
operation: "execute",
|
|
775
|
+
message: errorText || `HTTP error! status: ${httpResponse.status}`,
|
|
776
|
+
statusText: httpResponse.statusText,
|
|
777
|
+
statusCode: httpResponse.status,
|
|
778
|
+
logger: this.logger,
|
|
779
|
+
metadata: {
|
|
780
|
+
method: this.options.method,
|
|
781
|
+
url
|
|
782
|
+
}
|
|
783
|
+
})
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
const contentType = httpResponse.headers.get("content-type");
|
|
787
|
+
const data = contentType?.includes("application/json") ? await httpResponse.json() : await httpResponse.text();
|
|
788
|
+
let validatedData = data;
|
|
789
|
+
if (this.schemas) {
|
|
790
|
+
const { schema: endpointSchema } = IgniterCallerSchemaUtils.findSchema(
|
|
791
|
+
this.schemas,
|
|
792
|
+
url,
|
|
793
|
+
this.options.method
|
|
794
|
+
);
|
|
795
|
+
const responseSchema2 = endpointSchema?.responses?.[httpResponse.status];
|
|
796
|
+
if (responseSchema2) {
|
|
797
|
+
try {
|
|
798
|
+
validatedData = await IgniterCallerSchemaUtils.validateResponse(
|
|
799
|
+
data,
|
|
800
|
+
responseSchema2,
|
|
801
|
+
httpResponse.status,
|
|
802
|
+
this.schemaValidation,
|
|
803
|
+
{ url, method: this.options.method },
|
|
804
|
+
this.logger
|
|
805
|
+
);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
return {
|
|
808
|
+
data: void 0,
|
|
809
|
+
error
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (responseSchema) {
|
|
815
|
+
const result = responseSchema.safeParse(data);
|
|
816
|
+
if (!result.success) {
|
|
817
|
+
return {
|
|
818
|
+
data: void 0,
|
|
819
|
+
error: new IgniterCallerError({
|
|
820
|
+
code: "IGNITER_CALLER_RESPONSE_VALIDATION_FAILED",
|
|
821
|
+
operation: "parseResponse",
|
|
822
|
+
message: `Response validation failed: ${result.error.message}`,
|
|
823
|
+
logger: this.logger,
|
|
824
|
+
statusCode: 500,
|
|
825
|
+
metadata: {
|
|
826
|
+
method: this.options.method,
|
|
827
|
+
url
|
|
828
|
+
},
|
|
829
|
+
cause: result.error
|
|
830
|
+
})
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
let response2 = {
|
|
834
|
+
data: result.data,
|
|
835
|
+
error: void 0
|
|
836
|
+
};
|
|
837
|
+
if (this.responseInterceptors && this.responseInterceptors.length > 0) {
|
|
838
|
+
for (const interceptor of this.responseInterceptors) {
|
|
839
|
+
response2 = await interceptor(response2);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return response2;
|
|
843
|
+
}
|
|
844
|
+
let response = {
|
|
845
|
+
data: validatedData,
|
|
846
|
+
error: void 0
|
|
847
|
+
};
|
|
848
|
+
if (this.responseInterceptors && this.responseInterceptors.length > 0) {
|
|
849
|
+
for (const interceptor of this.responseInterceptors) {
|
|
850
|
+
response = await interceptor(response);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return response;
|
|
854
|
+
} catch (error) {
|
|
855
|
+
clearTimeout(timeoutId);
|
|
856
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
857
|
+
return {
|
|
858
|
+
data: void 0,
|
|
859
|
+
error: new IgniterCallerError({
|
|
860
|
+
code: "IGNITER_CALLER_TIMEOUT",
|
|
861
|
+
operation: "execute",
|
|
862
|
+
message: `Request timeout after ${this.options.timeout || 3e4}ms`,
|
|
863
|
+
statusCode: 408,
|
|
864
|
+
logger: this.logger,
|
|
865
|
+
metadata: {
|
|
866
|
+
method: this.options.method,
|
|
867
|
+
url
|
|
868
|
+
},
|
|
869
|
+
cause: error
|
|
870
|
+
})
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
data: void 0,
|
|
875
|
+
error: new IgniterCallerError({
|
|
876
|
+
code: "IGNITER_CALLER_UNKNOWN_ERROR",
|
|
877
|
+
operation: "execute",
|
|
878
|
+
message: error?.message || "Unknown error occurred during request",
|
|
879
|
+
statusCode: 500,
|
|
880
|
+
logger: this.logger,
|
|
881
|
+
metadata: {
|
|
882
|
+
method: this.options.method,
|
|
883
|
+
url
|
|
884
|
+
},
|
|
885
|
+
cause: error
|
|
886
|
+
})
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
buildRequest() {
|
|
891
|
+
const { method, url, body, params, headers, timeout, baseURL, cache } = this.options;
|
|
892
|
+
const fullUrl = IgniterCallerUrlUtils.buildUrl({
|
|
893
|
+
url,
|
|
894
|
+
baseURL,
|
|
895
|
+
query: params
|
|
896
|
+
});
|
|
897
|
+
const rawBody = IgniterCallerBodyUtils.isRawBody(body);
|
|
898
|
+
const finalHeaders = IgniterCallerBodyUtils.normalizeHeadersForBody(
|
|
899
|
+
headers,
|
|
900
|
+
body
|
|
901
|
+
);
|
|
902
|
+
const requestInit = {
|
|
903
|
+
method,
|
|
904
|
+
headers: finalHeaders,
|
|
905
|
+
cache,
|
|
906
|
+
...body && method !== "GET" && method !== "HEAD" ? { body: rawBody ? body : JSON.stringify(body) } : {}
|
|
907
|
+
};
|
|
908
|
+
const controller = new AbortController();
|
|
909
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout || 3e4);
|
|
910
|
+
return {
|
|
911
|
+
url: fullUrl,
|
|
912
|
+
requestInit,
|
|
913
|
+
controller,
|
|
914
|
+
timeoutId
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Emits event for this response using injected emitter.
|
|
919
|
+
*/
|
|
920
|
+
async emitEvent(result) {
|
|
921
|
+
if (this.eventEmitter) {
|
|
922
|
+
try {
|
|
923
|
+
await this.eventEmitter(this.options.url, this.options.method, result);
|
|
924
|
+
} catch (error) {
|
|
925
|
+
this.logger?.debug("Failed to emit event", { error });
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
// src/core/igniter-caller-events.ts
|
|
932
|
+
var IgniterCallerEvents = class {
|
|
933
|
+
constructor() {
|
|
934
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
935
|
+
this.patternListeners = /* @__PURE__ */ new Map();
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Registers a listener for a specific URL or pattern.
|
|
939
|
+
*
|
|
940
|
+
* @param pattern URL string (exact match) or RegExp pattern
|
|
941
|
+
* @param callback Function to execute when a response matches
|
|
942
|
+
* @returns Cleanup function to remove the listener
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* ```ts
|
|
946
|
+
* // Listen to specific endpoint
|
|
947
|
+
* const cleanup = api.on('/users', (result) => {
|
|
948
|
+
* console.log('Users fetched:', result.data)
|
|
949
|
+
* })
|
|
950
|
+
*
|
|
951
|
+
* // Listen to pattern
|
|
952
|
+
* api.on(/^\/users\/\d+$/, (result) => {
|
|
953
|
+
* console.log('User detail fetched')
|
|
954
|
+
* })
|
|
955
|
+
*
|
|
956
|
+
* // Cleanup when done
|
|
957
|
+
* cleanup()
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
960
|
+
on(pattern, callback) {
|
|
961
|
+
if (typeof pattern === "string") {
|
|
962
|
+
if (!this.listeners.has(pattern)) {
|
|
963
|
+
this.listeners.set(pattern, /* @__PURE__ */ new Set());
|
|
964
|
+
}
|
|
965
|
+
const callbacks2 = this.listeners.get(pattern);
|
|
966
|
+
if (callbacks2) {
|
|
967
|
+
callbacks2.add(callback);
|
|
968
|
+
}
|
|
969
|
+
return () => {
|
|
970
|
+
const callbacks3 = this.listeners.get(pattern);
|
|
971
|
+
if (callbacks3) {
|
|
972
|
+
callbacks3.delete(callback);
|
|
973
|
+
if (callbacks3.size === 0) {
|
|
974
|
+
this.listeners.delete(pattern);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
if (!this.patternListeners.has(pattern)) {
|
|
980
|
+
this.patternListeners.set(pattern, /* @__PURE__ */ new Set());
|
|
981
|
+
}
|
|
982
|
+
const callbacks = this.patternListeners.get(pattern);
|
|
983
|
+
if (callbacks) {
|
|
984
|
+
callbacks.add(callback);
|
|
985
|
+
}
|
|
986
|
+
return () => {
|
|
987
|
+
const callbacks2 = this.patternListeners.get(pattern);
|
|
988
|
+
if (callbacks2) {
|
|
989
|
+
callbacks2.delete(callback);
|
|
990
|
+
if (callbacks2.size === 0) {
|
|
991
|
+
this.patternListeners.delete(pattern);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Removes a specific listener or all listeners for a pattern.
|
|
998
|
+
*/
|
|
999
|
+
off(pattern, callback) {
|
|
1000
|
+
if (typeof pattern === "string") {
|
|
1001
|
+
if (callback !== void 0) {
|
|
1002
|
+
this.listeners.get(pattern)?.delete(callback);
|
|
1003
|
+
} else {
|
|
1004
|
+
this.listeners.delete(pattern);
|
|
1005
|
+
}
|
|
1006
|
+
} else {
|
|
1007
|
+
if (callback !== void 0) {
|
|
1008
|
+
this.patternListeners.get(pattern)?.delete(callback);
|
|
1009
|
+
} else {
|
|
1010
|
+
this.patternListeners.delete(pattern);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Emits an event to all matching listeners.
|
|
1016
|
+
*
|
|
1017
|
+
* @internal
|
|
1018
|
+
*/
|
|
1019
|
+
async emit(url, method, result) {
|
|
1020
|
+
const context = {
|
|
1021
|
+
url,
|
|
1022
|
+
method,
|
|
1023
|
+
timestamp: Date.now()
|
|
1024
|
+
};
|
|
1025
|
+
const exactListeners = this.listeners.get(url);
|
|
1026
|
+
if (exactListeners) {
|
|
1027
|
+
for (const callback of exactListeners) {
|
|
1028
|
+
try {
|
|
1029
|
+
await callback(result, context);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
console.error("Error in IgniterCaller event listener:", error);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
for (const [pattern, callbacks] of this.patternListeners.entries()) {
|
|
1036
|
+
if (pattern.test(url)) {
|
|
1037
|
+
for (const callback of callbacks) {
|
|
1038
|
+
try {
|
|
1039
|
+
await callback(result, context);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
console.error("Error in IgniterCaller event listener:", error);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Removes all listeners.
|
|
1049
|
+
*/
|
|
1050
|
+
clear() {
|
|
1051
|
+
this.listeners.clear();
|
|
1052
|
+
this.patternListeners.clear();
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
// src/core/igniter-caller.ts
|
|
1057
|
+
var _IgniterCaller = class _IgniterCaller {
|
|
1058
|
+
constructor(baseURL, opts) {
|
|
1059
|
+
this.baseURL = baseURL;
|
|
1060
|
+
this.headers = opts?.headers;
|
|
1061
|
+
this.cookies = opts?.cookies;
|
|
1062
|
+
this.logger = opts?.logger;
|
|
1063
|
+
this.requestInterceptors = opts?.requestInterceptors;
|
|
1064
|
+
this.responseInterceptors = opts?.responseInterceptors;
|
|
1065
|
+
this.schemas = opts?.schemas;
|
|
1066
|
+
this.schemaValidation = opts?.schemaValidation;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Canonical initialization entrypoint.
|
|
1070
|
+
*
|
|
1071
|
+
* This is designed to remain stable when extracted to `@igniter-js/caller`.
|
|
1072
|
+
*/
|
|
1073
|
+
static create() {
|
|
1074
|
+
return IgniterCallerBuilder.create((state) => {
|
|
1075
|
+
if (state.store) {
|
|
1076
|
+
IgniterCallerCacheUtils.setStore(state.store, state.storeOptions);
|
|
1077
|
+
}
|
|
1078
|
+
return new _IgniterCaller(state.baseURL, {
|
|
1079
|
+
headers: state.headers,
|
|
1080
|
+
cookies: state.cookies,
|
|
1081
|
+
logger: state.logger,
|
|
1082
|
+
requestInterceptors: state.requestInterceptors,
|
|
1083
|
+
responseInterceptors: state.responseInterceptors,
|
|
1084
|
+
schemas: state.schemas,
|
|
1085
|
+
schemaValidation: state.schemaValidation
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Returns a new client with the same config and a new logger.
|
|
1091
|
+
*/
|
|
1092
|
+
withLogger(logger) {
|
|
1093
|
+
return new _IgniterCaller(this.baseURL, {
|
|
1094
|
+
headers: this.headers,
|
|
1095
|
+
cookies: this.cookies,
|
|
1096
|
+
logger,
|
|
1097
|
+
requestInterceptors: this.requestInterceptors,
|
|
1098
|
+
responseInterceptors: this.responseInterceptors,
|
|
1099
|
+
schemas: this.schemas,
|
|
1100
|
+
schemaValidation: this.schemaValidation
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
setBaseURL(baseURL) {
|
|
1104
|
+
this.baseURL = baseURL;
|
|
1105
|
+
return this;
|
|
1106
|
+
}
|
|
1107
|
+
setHeaders(headers) {
|
|
1108
|
+
this.headers = headers;
|
|
1109
|
+
return this;
|
|
1110
|
+
}
|
|
1111
|
+
setCookies(cookies) {
|
|
1112
|
+
this.cookies = cookies;
|
|
1113
|
+
return this;
|
|
1114
|
+
}
|
|
1115
|
+
get() {
|
|
1116
|
+
return new IgniterCallerRequestBuilder({
|
|
1117
|
+
baseURL: this.baseURL,
|
|
1118
|
+
defaultHeaders: this.headers,
|
|
1119
|
+
defaultCookies: this.cookies,
|
|
1120
|
+
logger: this.logger,
|
|
1121
|
+
requestInterceptors: this.requestInterceptors,
|
|
1122
|
+
responseInterceptors: this.responseInterceptors,
|
|
1123
|
+
eventEmitter: async (url, method, result) => {
|
|
1124
|
+
await _IgniterCaller.emitEvent(url, method, result);
|
|
1125
|
+
},
|
|
1126
|
+
schemas: this.schemas,
|
|
1127
|
+
schemaValidation: this.schemaValidation
|
|
1128
|
+
}).method("GET");
|
|
1129
|
+
}
|
|
1130
|
+
post() {
|
|
1131
|
+
return new IgniterCallerRequestBuilder({
|
|
1132
|
+
baseURL: this.baseURL,
|
|
1133
|
+
defaultHeaders: this.headers,
|
|
1134
|
+
defaultCookies: this.cookies,
|
|
1135
|
+
logger: this.logger,
|
|
1136
|
+
requestInterceptors: this.requestInterceptors,
|
|
1137
|
+
responseInterceptors: this.responseInterceptors,
|
|
1138
|
+
eventEmitter: async (url, method, result) => {
|
|
1139
|
+
await _IgniterCaller.emitEvent(url, method, result);
|
|
1140
|
+
},
|
|
1141
|
+
schemas: this.schemas,
|
|
1142
|
+
schemaValidation: this.schemaValidation
|
|
1143
|
+
}).method("POST");
|
|
1144
|
+
}
|
|
1145
|
+
put() {
|
|
1146
|
+
return new IgniterCallerRequestBuilder({
|
|
1147
|
+
baseURL: this.baseURL,
|
|
1148
|
+
defaultHeaders: this.headers,
|
|
1149
|
+
defaultCookies: this.cookies,
|
|
1150
|
+
logger: this.logger,
|
|
1151
|
+
requestInterceptors: this.requestInterceptors,
|
|
1152
|
+
responseInterceptors: this.responseInterceptors,
|
|
1153
|
+
eventEmitter: async (url, method, result) => {
|
|
1154
|
+
await _IgniterCaller.emitEvent(url, method, result);
|
|
1155
|
+
},
|
|
1156
|
+
schemas: this.schemas,
|
|
1157
|
+
schemaValidation: this.schemaValidation
|
|
1158
|
+
}).method("PUT");
|
|
1159
|
+
}
|
|
1160
|
+
patch() {
|
|
1161
|
+
return new IgniterCallerRequestBuilder({
|
|
1162
|
+
baseURL: this.baseURL,
|
|
1163
|
+
defaultHeaders: this.headers,
|
|
1164
|
+
defaultCookies: this.cookies,
|
|
1165
|
+
logger: this.logger,
|
|
1166
|
+
requestInterceptors: this.requestInterceptors,
|
|
1167
|
+
responseInterceptors: this.responseInterceptors,
|
|
1168
|
+
eventEmitter: async (url, method, result) => {
|
|
1169
|
+
await _IgniterCaller.emitEvent(url, method, result);
|
|
1170
|
+
},
|
|
1171
|
+
schemas: this.schemas,
|
|
1172
|
+
schemaValidation: this.schemaValidation
|
|
1173
|
+
}).method("PATCH");
|
|
1174
|
+
}
|
|
1175
|
+
delete() {
|
|
1176
|
+
return new IgniterCallerRequestBuilder({
|
|
1177
|
+
baseURL: this.baseURL,
|
|
1178
|
+
defaultHeaders: this.headers,
|
|
1179
|
+
defaultCookies: this.cookies,
|
|
1180
|
+
logger: this.logger,
|
|
1181
|
+
requestInterceptors: this.requestInterceptors,
|
|
1182
|
+
responseInterceptors: this.responseInterceptors,
|
|
1183
|
+
eventEmitter: async (url, method, result) => {
|
|
1184
|
+
await _IgniterCaller.emitEvent(url, method, result);
|
|
1185
|
+
},
|
|
1186
|
+
schemas: this.schemas,
|
|
1187
|
+
schemaValidation: this.schemaValidation
|
|
1188
|
+
}).method("DELETE");
|
|
1189
|
+
}
|
|
1190
|
+
request() {
|
|
1191
|
+
return new IgniterCallerRequestBuilder({
|
|
1192
|
+
baseURL: this.baseURL,
|
|
1193
|
+
defaultHeaders: this.headers,
|
|
1194
|
+
defaultCookies: this.cookies,
|
|
1195
|
+
logger: this.logger,
|
|
1196
|
+
requestInterceptors: this.requestInterceptors,
|
|
1197
|
+
responseInterceptors: this.responseInterceptors,
|
|
1198
|
+
eventEmitter: async (url, method, result) => {
|
|
1199
|
+
await _IgniterCaller.emitEvent(url, method, result);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Executes multiple requests in parallel and returns results as an array.
|
|
1205
|
+
*
|
|
1206
|
+
* This is useful for batching independent API calls.
|
|
1207
|
+
*/
|
|
1208
|
+
static async batch(requests) {
|
|
1209
|
+
return Promise.all(requests);
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Registers a global event listener for HTTP responses.
|
|
1213
|
+
*
|
|
1214
|
+
* This allows observing API responses across the application for:
|
|
1215
|
+
* - Debugging and logging
|
|
1216
|
+
* - Real-time monitoring
|
|
1217
|
+
* - Cache invalidation triggers
|
|
1218
|
+
* - Analytics and telemetry
|
|
1219
|
+
*
|
|
1220
|
+
* @param pattern URL string (exact match) or RegExp pattern
|
|
1221
|
+
* @param callback Function to execute when a response matches
|
|
1222
|
+
* @returns Cleanup function to remove the listener
|
|
1223
|
+
*
|
|
1224
|
+
* @example
|
|
1225
|
+
* ```ts
|
|
1226
|
+
* // Listen to all user endpoints
|
|
1227
|
+
* const cleanup = IgniterCaller.on(/^\/users/, (result, context) => {
|
|
1228
|
+
* console.log(`${context.method} ${context.url}`, result)
|
|
1229
|
+
* })
|
|
1230
|
+
*
|
|
1231
|
+
* // Cleanup when done
|
|
1232
|
+
* cleanup()
|
|
1233
|
+
* ```
|
|
1234
|
+
*/
|
|
1235
|
+
static on(pattern, callback) {
|
|
1236
|
+
return _IgniterCaller.events.on(pattern, callback);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Removes event listeners for a pattern.
|
|
1240
|
+
*/
|
|
1241
|
+
static off(pattern, callback) {
|
|
1242
|
+
_IgniterCaller.events.off(pattern, callback);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Invalidates a specific cache entry.
|
|
1246
|
+
*
|
|
1247
|
+
* This is useful after mutations to ensure fresh data on next fetch.
|
|
1248
|
+
*
|
|
1249
|
+
* @example
|
|
1250
|
+
* ```ts
|
|
1251
|
+
* // After creating a user
|
|
1252
|
+
* await api.post().url('/users').body(newUser).execute()
|
|
1253
|
+
* await IgniterCaller.invalidate('/users') // Clear users list cache
|
|
1254
|
+
* ```
|
|
1255
|
+
*/
|
|
1256
|
+
static async invalidate(key) {
|
|
1257
|
+
await IgniterCallerCacheUtils.clear(key);
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Invalidates all cache entries matching a pattern.
|
|
1261
|
+
*
|
|
1262
|
+
* @param pattern Glob pattern (e.g., '/users/*') or exact key
|
|
1263
|
+
*
|
|
1264
|
+
* @example
|
|
1265
|
+
* ```ts
|
|
1266
|
+
* // Invalidate all user-related caches
|
|
1267
|
+
* await IgniterCaller.invalidatePattern('/users/*')
|
|
1268
|
+
* ```
|
|
1269
|
+
*/
|
|
1270
|
+
static async invalidatePattern(pattern) {
|
|
1271
|
+
await IgniterCallerCacheUtils.clearPattern(pattern);
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Emits an event to all registered listeners.
|
|
1275
|
+
*
|
|
1276
|
+
* @internal
|
|
1277
|
+
*/
|
|
1278
|
+
static async emitEvent(url, method, result) {
|
|
1279
|
+
await _IgniterCaller.events.emit(url, method, result);
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
/** Global event emitter for observing HTTP responses */
|
|
1283
|
+
_IgniterCaller.events = new IgniterCallerEvents();
|
|
1284
|
+
var IgniterCaller = _IgniterCaller;
|
|
1285
|
+
|
|
1286
|
+
// src/utils/testing.ts
|
|
1287
|
+
var IgniterCallerMock = class {
|
|
1288
|
+
/**
|
|
1289
|
+
* Creates a successful mock response.
|
|
1290
|
+
*/
|
|
1291
|
+
static mockResponse(data) {
|
|
1292
|
+
return { data, error: void 0 };
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Creates an error mock response.
|
|
1296
|
+
*/
|
|
1297
|
+
static mockError(code, message = "Mock error") {
|
|
1298
|
+
return {
|
|
1299
|
+
data: void 0,
|
|
1300
|
+
error: new IgniterCallerError({
|
|
1301
|
+
code,
|
|
1302
|
+
operation: "execute",
|
|
1303
|
+
message
|
|
1304
|
+
})
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Creates a successful file download mock.
|
|
1309
|
+
*/
|
|
1310
|
+
static mockFile(filename, content) {
|
|
1311
|
+
const blob = typeof content === "string" ? new Blob([content]) : content;
|
|
1312
|
+
const file = new File([blob], filename);
|
|
1313
|
+
return { file, error: null };
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Creates a failed file download mock.
|
|
1317
|
+
*/
|
|
1318
|
+
static mockFileError(message = "Mock file error") {
|
|
1319
|
+
return {
|
|
1320
|
+
file: null,
|
|
1321
|
+
error: new IgniterCallerError({
|
|
1322
|
+
code: "IGNITER_CALLER_UNKNOWN_ERROR",
|
|
1323
|
+
operation: "download",
|
|
1324
|
+
message
|
|
1325
|
+
})
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
export { IgniterCaller, IgniterCallerBodyUtils, IgniterCallerBuilder, IgniterCallerCacheUtils, IgniterCallerError, IgniterCallerEvents, IgniterCallerMock, IgniterCallerRequestBuilder, IgniterCallerSchemaUtils, IgniterCallerUrlUtils };
|
|
1331
|
+
//# sourceMappingURL=index.mjs.map
|
|
1332
|
+
//# sourceMappingURL=index.mjs.map
|