@forgezero/providers 0.1.9 → 0.1.10
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/README.md +160 -52
- package/dist/binance.js +95 -38
- package/dist/chain.d.ts +1 -1
- package/dist/chain.js +95 -38
- package/dist/database.d.ts +2 -2
- package/dist/database.js +95 -38
- package/dist/email.d.ts +26 -44
- package/dist/email.js +160 -127
- package/dist/http.d.ts +2 -2
- package/dist/http.js +95 -38
- package/dist/index.d.ts +80 -17
- package/dist/index.js +97 -36
- package/dist/pool.js +95 -38
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +95 -38
- package/dist/translation.d.ts +6 -4
- package/dist/translation.js +95 -38
- package/package.json +5 -5
package/dist/email.js
CHANGED
|
@@ -38,25 +38,45 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
function staticConfig(
|
|
41
|
+
function staticConfig(config) {
|
|
42
42
|
const health = new Map;
|
|
43
43
|
return {
|
|
44
44
|
name: "static",
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
async provider(instanceKey) {
|
|
46
|
+
const provider = config.providers[instanceKey];
|
|
47
|
+
return provider ? { instanceKey, ...provider, config: { ...provider.config } } : undefined;
|
|
48
|
+
},
|
|
49
|
+
async list(serviceKey, methodKey) {
|
|
50
|
+
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
51
|
+
...attachment,
|
|
52
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
49
53
|
}));
|
|
50
54
|
},
|
|
51
|
-
async recordHealth(serviceKey,
|
|
52
|
-
health.set(`${serviceKey}:${
|
|
55
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
56
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
53
57
|
}
|
|
54
58
|
};
|
|
55
59
|
}
|
|
60
|
+
function defineProviderMethod(method) {
|
|
61
|
+
return method;
|
|
62
|
+
}
|
|
56
63
|
function defineProvider(spec) {
|
|
57
64
|
return spec;
|
|
58
65
|
}
|
|
66
|
+
function defineSingleMethodProvider(spec) {
|
|
67
|
+
const { method, invoke, classify, ...identity } = spec;
|
|
68
|
+
return defineProvider({
|
|
69
|
+
...identity,
|
|
70
|
+
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
59
73
|
var STRIKES_TO_OFFLINE = 3;
|
|
74
|
+
function serviceMethod() {
|
|
75
|
+
return Object.freeze({});
|
|
76
|
+
}
|
|
77
|
+
function defineService(definition) {
|
|
78
|
+
return definition;
|
|
79
|
+
}
|
|
60
80
|
function nextHealth(current, kind) {
|
|
61
81
|
if (kind === "success")
|
|
62
82
|
return { strikes: 0, status: "ok" };
|
|
@@ -71,80 +91,117 @@ function nextHealth(current, kind) {
|
|
|
71
91
|
}
|
|
72
92
|
function createRegistry(options) {
|
|
73
93
|
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
74
|
-
async function call(serviceKey, args, callOptions = {}) {
|
|
75
|
-
const configured = [...await options.config.list(serviceKey)].filter((
|
|
94
|
+
async function call(serviceKey, methodKey, args, callOptions = {}) {
|
|
95
|
+
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
76
96
|
const attempts = [];
|
|
77
|
-
for (const
|
|
97
|
+
for (const attachment of configured) {
|
|
78
98
|
if (callOptions.signal?.aborted) {
|
|
79
99
|
const cancelled = {
|
|
80
100
|
ok: false,
|
|
101
|
+
fallbackUsed: attempts.length > 0,
|
|
81
102
|
attempts,
|
|
82
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
103
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
83
104
|
};
|
|
84
|
-
options.after?.(cancelled);
|
|
105
|
+
await options.after?.(cancelled);
|
|
85
106
|
return cancelled;
|
|
86
107
|
}
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
attempts.push({ providerId:
|
|
108
|
+
const instance = await options.config.provider(attachment.instanceKey);
|
|
109
|
+
if (!instance) {
|
|
110
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const spec = byId.get(instance.providerId);
|
|
114
|
+
const method = spec?.methods[attachment.providerMethod];
|
|
115
|
+
if (!spec || !method) {
|
|
116
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
90
117
|
continue;
|
|
91
118
|
}
|
|
92
|
-
if (
|
|
93
|
-
attempts.push({ providerId:
|
|
119
|
+
if (!instance.enabled) {
|
|
120
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
94
121
|
continue;
|
|
95
122
|
}
|
|
96
|
-
|
|
123
|
+
if (attachment.health?.status === "offline") {
|
|
124
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
128
|
+
const startedAt = performance.now();
|
|
97
129
|
try {
|
|
98
|
-
const result = await
|
|
99
|
-
config:
|
|
100
|
-
secret: (field) => options.credentials.get(
|
|
130
|
+
const result = await method.invoke({
|
|
131
|
+
config: instance.config,
|
|
132
|
+
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
101
133
|
signal: callOptions.signal
|
|
102
134
|
}, args);
|
|
103
|
-
attempts.push({ providerId:
|
|
104
|
-
await options.config.recordHealth(serviceKey,
|
|
105
|
-
const sent = {
|
|
106
|
-
|
|
135
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
136
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
137
|
+
const sent = {
|
|
138
|
+
ok: true,
|
|
139
|
+
result,
|
|
140
|
+
provider: instance.providerId,
|
|
141
|
+
instance: instance.instanceKey,
|
|
142
|
+
method: attachment.providerMethod,
|
|
143
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
144
|
+
fallbackUsed: attempts.length > 1,
|
|
145
|
+
attempts
|
|
146
|
+
};
|
|
147
|
+
await options.after?.(sent);
|
|
107
148
|
return sent;
|
|
108
149
|
} catch (error) {
|
|
109
150
|
if (callOptions.signal?.aborted) {
|
|
110
151
|
const cancelled = {
|
|
111
152
|
ok: false,
|
|
153
|
+
fallbackUsed: attempts.length > 0,
|
|
112
154
|
attempts,
|
|
113
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
155
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
114
156
|
};
|
|
115
|
-
options.after?.(cancelled);
|
|
157
|
+
await options.after?.(cancelled);
|
|
116
158
|
return cancelled;
|
|
117
159
|
}
|
|
118
|
-
const kind =
|
|
160
|
+
const kind = method.classify(error);
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
|
|
119
163
|
attempts.push({
|
|
120
|
-
providerId:
|
|
164
|
+
providerId: instance.providerId,
|
|
165
|
+
instanceKey: instance.instanceKey,
|
|
166
|
+
providerMethod: attachment.providerMethod,
|
|
121
167
|
outcome: "failed",
|
|
122
168
|
kind,
|
|
123
|
-
|
|
169
|
+
durationMs: performance.now() - startedAt,
|
|
170
|
+
failure: { kind, ...code ? { code } : {}, message },
|
|
171
|
+
error: message
|
|
124
172
|
});
|
|
125
|
-
await options.config.recordHealth(serviceKey,
|
|
173
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
126
174
|
if (kind === "terminal") {
|
|
127
175
|
const refused = {
|
|
128
176
|
ok: false,
|
|
177
|
+
fallbackUsed: attempts.length > 1,
|
|
129
178
|
attempts,
|
|
130
179
|
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
131
180
|
};
|
|
132
|
-
options.after?.(refused);
|
|
181
|
+
await options.after?.(refused);
|
|
133
182
|
return refused;
|
|
134
183
|
}
|
|
135
184
|
}
|
|
136
185
|
}
|
|
137
186
|
const failed = {
|
|
138
187
|
ok: false,
|
|
188
|
+
fallbackUsed: attempts.length > 1,
|
|
139
189
|
attempts,
|
|
140
|
-
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
|
|
190
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
|
|
141
191
|
};
|
|
142
|
-
options.after?.(failed);
|
|
192
|
+
await options.after?.(failed);
|
|
143
193
|
return failed;
|
|
144
194
|
}
|
|
145
|
-
|
|
195
|
+
function service(definition) {
|
|
196
|
+
return {
|
|
197
|
+
call(method, args, callOptions) {
|
|
198
|
+
return call(definition.key, method, args, callOptions);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return { call, service };
|
|
146
203
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
204
|
+
var VERSION = "0.1.10";
|
|
148
205
|
|
|
149
206
|
// src/email.ts
|
|
150
207
|
var recipients = (to) => Array.isArray(to) ? [...to] : [to];
|
|
@@ -156,31 +213,28 @@ function assertMessage(message) {
|
|
|
156
213
|
throw new ProviderError("EMAIL_NO_BODY", "An email needs html, text, or both.");
|
|
157
214
|
}
|
|
158
215
|
}
|
|
159
|
-
var
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
type: "string",
|
|
170
|
-
title: "API key",
|
|
171
|
-
writeOnly: true,
|
|
172
|
-
description: "Transactional key from the JetEmail dashboard."
|
|
173
|
-
}
|
|
216
|
+
var jetemailCredentials = {
|
|
217
|
+
type: "object",
|
|
218
|
+
additionalProperties: false,
|
|
219
|
+
required: ["apiKey"],
|
|
220
|
+
properties: {
|
|
221
|
+
apiKey: {
|
|
222
|
+
type: "string",
|
|
223
|
+
title: "API key",
|
|
224
|
+
writeOnly: true,
|
|
225
|
+
description: "Transactional key from the JetEmail dashboard."
|
|
174
226
|
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var jetemailConfig = {
|
|
230
|
+
type: "object",
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
properties: {
|
|
233
|
+
eu: { type: "boolean", default: false, title: "EU residency" },
|
|
234
|
+
from: { type: "string", format: "email", title: "Default from address" }
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var jetemailSend = defineProviderMethod({
|
|
184
238
|
async invoke(context, message) {
|
|
185
239
|
assertMessage(message);
|
|
186
240
|
const config = context.config;
|
|
@@ -230,12 +284,7 @@ function assertBatch(batch) {
|
|
|
230
284
|
for (const message of batch.emails)
|
|
231
285
|
assertMessage(message);
|
|
232
286
|
}
|
|
233
|
-
var
|
|
234
|
-
id: "jetemail",
|
|
235
|
-
service: "email.batch",
|
|
236
|
-
label: "JetEmail",
|
|
237
|
-
credentials: jetemail.credentials,
|
|
238
|
-
config: jetemail.config,
|
|
287
|
+
var jetemailSendBatch = defineProviderMethod({
|
|
239
288
|
async invoke(context, batch) {
|
|
240
289
|
assertBatch(batch);
|
|
241
290
|
const config = context.config;
|
|
@@ -287,37 +336,19 @@ var jetemailBatch = defineProvider({
|
|
|
287
336
|
});
|
|
288
337
|
return { summary, results };
|
|
289
338
|
},
|
|
290
|
-
classify:
|
|
339
|
+
classify: jetemailSend.classify
|
|
291
340
|
});
|
|
292
|
-
var
|
|
293
|
-
id: "
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
config: {
|
|
306
|
-
type: "object",
|
|
307
|
-
additionalProperties: false,
|
|
308
|
-
required: ["host", "from"],
|
|
309
|
-
properties: {
|
|
310
|
-
host: { type: "string", title: "Host" },
|
|
311
|
-
port: { type: "integer", default: 587, minimum: 1, maximum: 65535 },
|
|
312
|
-
secure: {
|
|
313
|
-
type: "boolean",
|
|
314
|
-
default: false,
|
|
315
|
-
title: "Implicit TLS",
|
|
316
|
-
description: "True for port 465. Port 587 upgrades with STARTTLS and should stay false."
|
|
317
|
-
},
|
|
318
|
-
from: { type: "string", format: "email", title: "From address" }
|
|
319
|
-
}
|
|
320
|
-
},
|
|
341
|
+
var jetemail = defineProvider({
|
|
342
|
+
id: "jetemail",
|
|
343
|
+
label: "JetEmail",
|
|
344
|
+
credentials: jetemailCredentials,
|
|
345
|
+
config: jetemailConfig,
|
|
346
|
+
methods: {
|
|
347
|
+
send: jetemailSend,
|
|
348
|
+
sendBatch: jetemailSendBatch
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
var smtpSend = defineProviderMethod({
|
|
321
352
|
async invoke(context, message) {
|
|
322
353
|
assertMessage(message);
|
|
323
354
|
const config = context.config;
|
|
@@ -356,45 +387,47 @@ var smtp = defineProvider({
|
|
|
356
387
|
return "retryable";
|
|
357
388
|
}
|
|
358
389
|
});
|
|
359
|
-
var
|
|
390
|
+
var smtp = defineProvider({
|
|
360
391
|
id: "smtp",
|
|
361
|
-
service: "email.batch",
|
|
362
392
|
label: "SMTP",
|
|
363
393
|
multiInstance: true,
|
|
364
|
-
credentials:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const rows = await Promise.all(batch.emails.slice(offset, offset + 5).map(async (message) => {
|
|
371
|
-
try {
|
|
372
|
-
const sent = await smtp.invoke(context, message);
|
|
373
|
-
return { status: "success", id: sent.id };
|
|
374
|
-
} catch (cause) {
|
|
375
|
-
return {
|
|
376
|
-
status: "failed",
|
|
377
|
-
error: cause instanceof Error ? cause.message.slice(0, 500) : "SMTP send failed"
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
}));
|
|
381
|
-
results.push(...rows);
|
|
394
|
+
credentials: {
|
|
395
|
+
type: "object",
|
|
396
|
+
additionalProperties: false,
|
|
397
|
+
properties: {
|
|
398
|
+
user: { type: "string", title: "Username" },
|
|
399
|
+
password: { type: "string", title: "Password", writeOnly: true }
|
|
382
400
|
}
|
|
383
|
-
const successful = results.filter((result) => result.status === "success").length;
|
|
384
|
-
return {
|
|
385
|
-
summary: { total: results.length, successful, failed: results.length - successful },
|
|
386
|
-
results
|
|
387
|
-
};
|
|
388
401
|
},
|
|
389
|
-
|
|
402
|
+
config: {
|
|
403
|
+
type: "object",
|
|
404
|
+
additionalProperties: false,
|
|
405
|
+
required: ["host", "from"],
|
|
406
|
+
properties: {
|
|
407
|
+
host: { type: "string", title: "Host" },
|
|
408
|
+
port: { type: "integer", default: 587, minimum: 1, maximum: 65535 },
|
|
409
|
+
secure: {
|
|
410
|
+
type: "boolean",
|
|
411
|
+
default: false,
|
|
412
|
+
title: "Implicit TLS",
|
|
413
|
+
description: "True for port 465. Port 587 upgrades with STARTTLS and should stay false."
|
|
414
|
+
},
|
|
415
|
+
from: { type: "string", format: "email", title: "From address" }
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
methods: { send: smtpSend }
|
|
390
419
|
});
|
|
391
420
|
var emailProviders = [jetemail, smtp];
|
|
392
|
-
var
|
|
421
|
+
var emailService = defineService({
|
|
422
|
+
key: "email",
|
|
423
|
+
methods: {
|
|
424
|
+
send: serviceMethod(),
|
|
425
|
+
sendBatch: serviceMethod()
|
|
426
|
+
}
|
|
427
|
+
});
|
|
393
428
|
export {
|
|
394
|
-
smtpBatch,
|
|
395
429
|
smtp,
|
|
396
|
-
jetemailBatch,
|
|
397
430
|
jetemail,
|
|
398
|
-
|
|
399
|
-
|
|
431
|
+
emailService,
|
|
432
|
+
emailProviders
|
|
400
433
|
};
|
package/dist/http.d.ts
CHANGED
|
@@ -93,8 +93,8 @@ export type HttpClient = ReturnType<typeof createHttpClient>;
|
|
|
93
93
|
* `classify` is where the venue-specific knowledge lives, and it is the part
|
|
94
94
|
* that decides whether a retry helps or makes things worse.
|
|
95
95
|
*/
|
|
96
|
-
export declare const http: import("./index").
|
|
96
|
+
export declare const http: import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<HttpRequest, HttpResponse<unknown>>>>;
|
|
97
97
|
/** Binance reports a running total for the window, not a per-call cost. */
|
|
98
98
|
export declare const binanceWeight: (response: Response) => number | undefined;
|
|
99
|
-
export declare const httpProviders: readonly [import("./index").
|
|
99
|
+
export declare const httpProviders: readonly [import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<HttpRequest, HttpResponse<unknown>>>>];
|
|
100
100
|
export {};
|
package/dist/http.js
CHANGED
|
@@ -38,25 +38,45 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
function staticConfig(
|
|
41
|
+
function staticConfig(config) {
|
|
42
42
|
const health = new Map;
|
|
43
43
|
return {
|
|
44
44
|
name: "static",
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
async provider(instanceKey) {
|
|
46
|
+
const provider = config.providers[instanceKey];
|
|
47
|
+
return provider ? { instanceKey, ...provider, config: { ...provider.config } } : undefined;
|
|
48
|
+
},
|
|
49
|
+
async list(serviceKey, methodKey) {
|
|
50
|
+
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
51
|
+
...attachment,
|
|
52
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
49
53
|
}));
|
|
50
54
|
},
|
|
51
|
-
async recordHealth(serviceKey,
|
|
52
|
-
health.set(`${serviceKey}:${
|
|
55
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
56
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
53
57
|
}
|
|
54
58
|
};
|
|
55
59
|
}
|
|
60
|
+
function defineProviderMethod(method) {
|
|
61
|
+
return method;
|
|
62
|
+
}
|
|
56
63
|
function defineProvider(spec) {
|
|
57
64
|
return spec;
|
|
58
65
|
}
|
|
66
|
+
function defineSingleMethodProvider(spec) {
|
|
67
|
+
const { method, invoke, classify, ...identity } = spec;
|
|
68
|
+
return defineProvider({
|
|
69
|
+
...identity,
|
|
70
|
+
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
59
73
|
var STRIKES_TO_OFFLINE = 3;
|
|
74
|
+
function serviceMethod() {
|
|
75
|
+
return Object.freeze({});
|
|
76
|
+
}
|
|
77
|
+
function defineService(definition) {
|
|
78
|
+
return definition;
|
|
79
|
+
}
|
|
60
80
|
function nextHealth(current, kind) {
|
|
61
81
|
if (kind === "success")
|
|
62
82
|
return { strikes: 0, status: "ok" };
|
|
@@ -71,80 +91,117 @@ function nextHealth(current, kind) {
|
|
|
71
91
|
}
|
|
72
92
|
function createRegistry(options) {
|
|
73
93
|
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
74
|
-
async function call(serviceKey, args, callOptions = {}) {
|
|
75
|
-
const configured = [...await options.config.list(serviceKey)].filter((
|
|
94
|
+
async function call(serviceKey, methodKey, args, callOptions = {}) {
|
|
95
|
+
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
76
96
|
const attempts = [];
|
|
77
|
-
for (const
|
|
97
|
+
for (const attachment of configured) {
|
|
78
98
|
if (callOptions.signal?.aborted) {
|
|
79
99
|
const cancelled = {
|
|
80
100
|
ok: false,
|
|
101
|
+
fallbackUsed: attempts.length > 0,
|
|
81
102
|
attempts,
|
|
82
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
103
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
83
104
|
};
|
|
84
|
-
options.after?.(cancelled);
|
|
105
|
+
await options.after?.(cancelled);
|
|
85
106
|
return cancelled;
|
|
86
107
|
}
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
attempts.push({ providerId:
|
|
108
|
+
const instance = await options.config.provider(attachment.instanceKey);
|
|
109
|
+
if (!instance) {
|
|
110
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const spec = byId.get(instance.providerId);
|
|
114
|
+
const method = spec?.methods[attachment.providerMethod];
|
|
115
|
+
if (!spec || !method) {
|
|
116
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
90
117
|
continue;
|
|
91
118
|
}
|
|
92
|
-
if (
|
|
93
|
-
attempts.push({ providerId:
|
|
119
|
+
if (!instance.enabled) {
|
|
120
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
94
121
|
continue;
|
|
95
122
|
}
|
|
96
|
-
|
|
123
|
+
if (attachment.health?.status === "offline") {
|
|
124
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
128
|
+
const startedAt = performance.now();
|
|
97
129
|
try {
|
|
98
|
-
const result = await
|
|
99
|
-
config:
|
|
100
|
-
secret: (field) => options.credentials.get(
|
|
130
|
+
const result = await method.invoke({
|
|
131
|
+
config: instance.config,
|
|
132
|
+
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
101
133
|
signal: callOptions.signal
|
|
102
134
|
}, args);
|
|
103
|
-
attempts.push({ providerId:
|
|
104
|
-
await options.config.recordHealth(serviceKey,
|
|
105
|
-
const sent = {
|
|
106
|
-
|
|
135
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
136
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
137
|
+
const sent = {
|
|
138
|
+
ok: true,
|
|
139
|
+
result,
|
|
140
|
+
provider: instance.providerId,
|
|
141
|
+
instance: instance.instanceKey,
|
|
142
|
+
method: attachment.providerMethod,
|
|
143
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
144
|
+
fallbackUsed: attempts.length > 1,
|
|
145
|
+
attempts
|
|
146
|
+
};
|
|
147
|
+
await options.after?.(sent);
|
|
107
148
|
return sent;
|
|
108
149
|
} catch (error) {
|
|
109
150
|
if (callOptions.signal?.aborted) {
|
|
110
151
|
const cancelled = {
|
|
111
152
|
ok: false,
|
|
153
|
+
fallbackUsed: attempts.length > 0,
|
|
112
154
|
attempts,
|
|
113
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
155
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
114
156
|
};
|
|
115
|
-
options.after?.(cancelled);
|
|
157
|
+
await options.after?.(cancelled);
|
|
116
158
|
return cancelled;
|
|
117
159
|
}
|
|
118
|
-
const kind =
|
|
160
|
+
const kind = method.classify(error);
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
|
|
119
163
|
attempts.push({
|
|
120
|
-
providerId:
|
|
164
|
+
providerId: instance.providerId,
|
|
165
|
+
instanceKey: instance.instanceKey,
|
|
166
|
+
providerMethod: attachment.providerMethod,
|
|
121
167
|
outcome: "failed",
|
|
122
168
|
kind,
|
|
123
|
-
|
|
169
|
+
durationMs: performance.now() - startedAt,
|
|
170
|
+
failure: { kind, ...code ? { code } : {}, message },
|
|
171
|
+
error: message
|
|
124
172
|
});
|
|
125
|
-
await options.config.recordHealth(serviceKey,
|
|
173
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
126
174
|
if (kind === "terminal") {
|
|
127
175
|
const refused = {
|
|
128
176
|
ok: false,
|
|
177
|
+
fallbackUsed: attempts.length > 1,
|
|
129
178
|
attempts,
|
|
130
179
|
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
131
180
|
};
|
|
132
|
-
options.after?.(refused);
|
|
181
|
+
await options.after?.(refused);
|
|
133
182
|
return refused;
|
|
134
183
|
}
|
|
135
184
|
}
|
|
136
185
|
}
|
|
137
186
|
const failed = {
|
|
138
187
|
ok: false,
|
|
188
|
+
fallbackUsed: attempts.length > 1,
|
|
139
189
|
attempts,
|
|
140
|
-
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
|
|
190
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
|
|
141
191
|
};
|
|
142
|
-
options.after?.(failed);
|
|
192
|
+
await options.after?.(failed);
|
|
143
193
|
return failed;
|
|
144
194
|
}
|
|
145
|
-
|
|
195
|
+
function service(definition) {
|
|
196
|
+
return {
|
|
197
|
+
call(method, args, callOptions) {
|
|
198
|
+
return call(definition.key, method, args, callOptions);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return { call, service };
|
|
146
203
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
204
|
+
var VERSION = "0.1.10";
|
|
148
205
|
|
|
149
206
|
// src/http.ts
|
|
150
207
|
class BudgetExhausted extends ProviderError {
|
|
@@ -227,9 +284,9 @@ function createHttpClient(config) {
|
|
|
227
284
|
budget: () => budgetState(config.budget.host)
|
|
228
285
|
};
|
|
229
286
|
}
|
|
230
|
-
var http =
|
|
287
|
+
var http = defineSingleMethodProvider({
|
|
231
288
|
id: "http",
|
|
232
|
-
|
|
289
|
+
method: "request",
|
|
233
290
|
label: "HTTP",
|
|
234
291
|
multiInstance: true,
|
|
235
292
|
credentials: {
|