@forgezero/providers 0.1.18 → 0.1.20
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 +95 -38
- package/dist/binance.js +56 -18
- package/dist/chain.js +56 -18
- package/dist/database.js +56 -18
- package/dist/email.js +62 -18
- package/dist/http.js +56 -18
- package/dist/index.d.ts +21 -3
- package/dist/index.js +57 -18
- package/dist/pool.js +56 -18
- package/dist/storage.js +56 -18
- package/dist/translation.js +56 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ bun test
|
|
|
59
59
|
<a id="forgezero-providers"></a>
|
|
60
60
|
## @forgezero/providers
|
|
61
61
|
|
|
62
|
-
The registry: priority, health, and a terminal-versus-retryable verdict per failure. This entry exposes
|
|
62
|
+
The registry: priority, health, and a terminal-versus-retryable verdict per failure. This entry exposes 17 named value exports and 31 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
|
|
63
63
|
|
|
64
64
|
```text
|
|
65
65
|
import {
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
createService,
|
|
73
73
|
defineProvider,
|
|
74
74
|
defineProviderMethod,
|
|
75
|
+
defineProviderMethodBranches,
|
|
75
76
|
defineService,
|
|
76
77
|
defineSingleMethodProvider,
|
|
77
78
|
envCredentials,
|
|
@@ -102,6 +103,8 @@ import type {
|
|
|
102
103
|
ProviderDefinition,
|
|
103
104
|
ProviderHealth,
|
|
104
105
|
ProviderInstanceConfig,
|
|
106
|
+
ProviderMethodBranches,
|
|
107
|
+
ProviderMethodEntry,
|
|
105
108
|
ProviderMethodSpec,
|
|
106
109
|
ProviderMethods,
|
|
107
110
|
RegistryOptions,
|
|
@@ -204,7 +207,7 @@ import type {
|
|
|
204
207
|
|
|
205
208
|
## @forgezero/providers/email — Email with ordered provider fallback
|
|
206
209
|
|
|
207
|
-
The service owns send/sendBatch.
|
|
210
|
+
The service owns send/sendBatch. Ordered entries select exact provider method versions; SMTP is never presented as batch-capable.
|
|
208
211
|
|
|
209
212
|
```text
|
|
210
213
|
import {
|
|
@@ -518,100 +521,154 @@ export const selectedCapability = RealtimeProviderError;
|
|
|
518
521
|
|
|
519
522
|
## Built-in provider capabilities
|
|
520
523
|
|
|
521
|
-
A provider owns capabilities; it does not own a service. A service method attaches
|
|
524
|
+
A provider owns versioned capabilities; it does not own a service. A service method attaches an exact provider, method and version in an ordered array. Current, legacy and deprecated branches can coexist; retired branches never run.
|
|
522
525
|
|
|
523
526
|
```text
|
|
524
|
-
evm-rpc
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
527
|
+
evm-rpc
|
|
528
|
+
- request
|
|
529
|
+
jetemail
|
|
530
|
+
- send
|
|
531
|
+
- sendBatch
|
|
532
|
+
smtp
|
|
533
|
+
- send
|
|
534
|
+
arangodb
|
|
535
|
+
- query
|
|
536
|
+
http
|
|
537
|
+
- request
|
|
538
|
+
s3
|
|
539
|
+
- request
|
|
540
|
+
google-ai-studio
|
|
541
|
+
- translate
|
|
531
542
|
```
|
|
532
543
|
|
|
533
544
|
## EVM JSON-RPC node (evm-rpc)
|
|
534
545
|
|
|
535
|
-
Import: @forgezero/providers/chain.
|
|
546
|
+
Import: @forgezero/providers/chain. Each method below is a provider capability that a service attaches by exact version. Several endpoints per chain is the normal case, not a luxury: public RPC is rate-limited and paid gateways have outages, and a scanner that stops when one is down stops crediting deposits while the money sits on the chain. "Already known" and "nonce too low" are TERMINAL — the transaction is already in flight, so rebroadcasting through another node is how a sweep gets sent twice.
|
|
536
547
|
|
|
537
548
|
```text
|
|
538
549
|
importPath: '@forgezero/providers/chain'
|
|
539
550
|
providerId: 'evm-rpc'
|
|
540
|
-
methods:
|
|
541
|
-
|
|
542
|
-
|
|
551
|
+
methods:
|
|
552
|
+
- request
|
|
553
|
+
credentials:
|
|
554
|
+
- url
|
|
555
|
+
- bearer
|
|
556
|
+
config:
|
|
557
|
+
- chainId
|
|
558
|
+
- maxLogRange
|
|
559
|
+
- timeoutMs
|
|
543
560
|
```
|
|
544
561
|
|
|
545
562
|
## JetEmail (jetemail)
|
|
546
563
|
|
|
547
|
-
Import: @forgezero/providers/email.
|
|
564
|
+
Import: @forgezero/providers/email. Each method below is a provider capability that a service attaches by exact version. A 401 is RETRYABLE at the loop level — that key is bad, the next provider's may not be. Treating any 4xx as fatal is what loses the fallback.
|
|
548
565
|
|
|
549
566
|
```text
|
|
550
567
|
importPath: '@forgezero/providers/email'
|
|
551
568
|
providerId: 'jetemail'
|
|
552
|
-
methods:
|
|
553
|
-
|
|
554
|
-
|
|
569
|
+
methods:
|
|
570
|
+
- send
|
|
571
|
+
- sendBatch
|
|
572
|
+
credentials:
|
|
573
|
+
- apiKey
|
|
574
|
+
config:
|
|
575
|
+
- eu
|
|
576
|
+
- from
|
|
555
577
|
```
|
|
556
578
|
|
|
557
579
|
## SMTP (smtp)
|
|
558
580
|
|
|
559
|
-
Import: @forgezero/providers/email.
|
|
581
|
+
Import: @forgezero/providers/email. Each method below is a provider capability that a service attaches by exact version. Any number of named relays, each with its own priority slot. Also the bootstrap path: the vault opens after a ceremony, and the ceremony needs mail.
|
|
560
582
|
|
|
561
583
|
```text
|
|
562
584
|
importPath: '@forgezero/providers/email'
|
|
563
585
|
providerId: 'smtp'
|
|
564
|
-
methods:
|
|
565
|
-
|
|
566
|
-
|
|
586
|
+
methods:
|
|
587
|
+
- send
|
|
588
|
+
credentials:
|
|
589
|
+
- user
|
|
590
|
+
- password
|
|
591
|
+
config:
|
|
592
|
+
- host
|
|
593
|
+
- port
|
|
594
|
+
- secure
|
|
595
|
+
- from
|
|
596
|
+
- transport
|
|
567
597
|
```
|
|
568
598
|
|
|
569
599
|
## ArangoDB (arangodb)
|
|
570
600
|
|
|
571
|
-
Import: @forgezero/providers/database.
|
|
601
|
+
Import: @forgezero/providers/database. Each method below is a provider capability that a service attaches by exact version. Failover is OFF by default. Writing to a different database because the first was slow is data loss with extra steps; this is here for credential rotation and health.
|
|
572
602
|
|
|
573
603
|
```text
|
|
574
604
|
importPath: '@forgezero/providers/database'
|
|
575
605
|
providerId: 'arangodb'
|
|
576
|
-
methods:
|
|
577
|
-
|
|
578
|
-
|
|
606
|
+
methods:
|
|
607
|
+
- query
|
|
608
|
+
credentials:
|
|
609
|
+
- password
|
|
610
|
+
config:
|
|
611
|
+
- url
|
|
612
|
+
- urls
|
|
613
|
+
- readPreferredUrls
|
|
614
|
+
- readPreferredFallback
|
|
615
|
+
- clusterId
|
|
616
|
+
- database
|
|
617
|
+
- username
|
|
579
618
|
```
|
|
580
619
|
|
|
581
620
|
## HTTP (http)
|
|
582
621
|
|
|
583
|
-
Import: @forgezero/providers/http.
|
|
622
|
+
Import: @forgezero/providers/http. Each method below is a provider capability that a service attaches by exact version. 418 and 429 back off rather than falling through. 418 is a venue saying "you ignored a 429 and are now banned", and hammering makes the ban longer.
|
|
584
623
|
|
|
585
624
|
```text
|
|
586
625
|
importPath: '@forgezero/providers/http'
|
|
587
626
|
providerId: 'http'
|
|
588
|
-
methods:
|
|
589
|
-
|
|
590
|
-
|
|
627
|
+
methods:
|
|
628
|
+
- request
|
|
629
|
+
credentials:
|
|
630
|
+
- apiKey
|
|
631
|
+
- apiSecret
|
|
632
|
+
config:
|
|
633
|
+
- baseUrl
|
|
634
|
+
- limit
|
|
635
|
+
- windowMs
|
|
636
|
+
- headroom
|
|
591
637
|
```
|
|
592
638
|
|
|
593
639
|
## S3-compatible storage (s3)
|
|
594
640
|
|
|
595
|
-
Import: @forgezero/providers/storage.
|
|
641
|
+
Import: @forgezero/providers/storage. Each method below is a provider capability that a service attaches by exact version. A 403 is retryable because on S3 it usually means this key lacks a permission, not that the key is invalid. Works against AWS, R2, MinIO, Garage and Backblaze — path-style addressing by default, because that is what everything except AWS expects.
|
|
596
642
|
|
|
597
643
|
```text
|
|
598
644
|
importPath: '@forgezero/providers/storage'
|
|
599
645
|
providerId: 's3'
|
|
600
|
-
methods:
|
|
601
|
-
|
|
602
|
-
|
|
646
|
+
methods:
|
|
647
|
+
- request
|
|
648
|
+
credentials:
|
|
649
|
+
- accessKeyId
|
|
650
|
+
- secretAccessKey
|
|
651
|
+
- sessionToken
|
|
652
|
+
config:
|
|
653
|
+
- endpoint
|
|
654
|
+
- region
|
|
655
|
+
- bucket
|
|
656
|
+
- addressing
|
|
603
657
|
```
|
|
604
658
|
|
|
605
659
|
## Google AI Studio (google-ai-studio)
|
|
606
660
|
|
|
607
|
-
Import: @forgezero/providers/translation.
|
|
661
|
+
Import: @forgezero/providers/translation. Each method below is a provider capability that a service attaches by exact version. Machine translation for a tenant that wants it — ForgeZero translates nothing at runtime. Chosen as the first adapter because the free tier needs no billing account, so the feature can be tried without a procurement conversation. A 429 is a BACKOFF rather than a failure: on the free tier it is expected traffic, and treating it as terminal abandons a catalogue most of the way through. A short answer is terminal, because one translation missing from a batch shifts every later string onto the wrong source and nothing about the result looks broken afterwards.
|
|
608
662
|
|
|
609
663
|
```text
|
|
610
664
|
importPath: '@forgezero/providers/translation'
|
|
611
665
|
providerId: 'google-ai-studio'
|
|
612
|
-
methods:
|
|
613
|
-
|
|
614
|
-
|
|
666
|
+
methods:
|
|
667
|
+
- translate
|
|
668
|
+
credentials:
|
|
669
|
+
- apiKey
|
|
670
|
+
config:
|
|
671
|
+
- model
|
|
615
672
|
```
|
|
616
673
|
|
|
617
674
|
Full rendered documentation: https://www.forgezero.net/docs/providers
|
package/dist/binance.js
CHANGED
|
@@ -75,25 +75,55 @@ function staticConfig(config) {
|
|
|
75
75
|
async list(serviceKey, methodKey) {
|
|
76
76
|
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
77
77
|
...attachment,
|
|
78
|
-
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
78
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:${attachment.providerMethodVersion ?? "current"}`) ?? (attachment.providerMethodVersion === undefined ? [...health.entries()].find(([key]) => key.startsWith(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:`))?.[1] : undefined) ?? attachment.health
|
|
79
79
|
}));
|
|
80
80
|
},
|
|
81
|
-
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
82
|
-
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
81
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, providerMethodVersion, next) {
|
|
82
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}:${providerMethodVersion}`, next);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
function defineProviderMethod(method) {
|
|
87
87
|
return method;
|
|
88
88
|
}
|
|
89
|
+
function defineProviderMethodBranches(branches) {
|
|
90
|
+
const entries = Object.entries(branches.versions);
|
|
91
|
+
if (entries.length === 0 || !branches.versions[branches.currentVersion]) {
|
|
92
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", "A versioned provider method needs a current branch.");
|
|
93
|
+
}
|
|
94
|
+
for (const [version, method] of entries) {
|
|
95
|
+
if (method.version !== version) {
|
|
96
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method branch "${version}" declares version "${method.version}".`);
|
|
97
|
+
}
|
|
98
|
+
if (version === branches.currentVersion && method.lifecycle !== "current") {
|
|
99
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Current provider method branch "${version}" must have lifecycle current.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return branches;
|
|
103
|
+
}
|
|
104
|
+
var methodBranch = (provider, methodName, version) => {
|
|
105
|
+
const entry = provider?.methods[methodName];
|
|
106
|
+
if (!entry)
|
|
107
|
+
return;
|
|
108
|
+
if ("versions" in entry)
|
|
109
|
+
return entry.versions[version ?? entry.currentVersion];
|
|
110
|
+
return version === undefined || version === entry.version ? entry : undefined;
|
|
111
|
+
};
|
|
89
112
|
function defineProvider(spec) {
|
|
113
|
+
for (const [name, entry] of Object.entries(spec.methods)) {
|
|
114
|
+
if ("versions" in entry)
|
|
115
|
+
defineProviderMethodBranches(entry);
|
|
116
|
+
else if (!entry.version || !["current", "legacy", "deprecated", "retired"].includes(entry.lifecycle)) {
|
|
117
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method "${spec.id}.${name}" needs a version and lifecycle.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
90
120
|
return spec;
|
|
91
121
|
}
|
|
92
122
|
function defineSingleMethodProvider(spec) {
|
|
93
|
-
const { method, invoke, classify, ...identity } = spec;
|
|
123
|
+
const { method, version = "v1", lifecycle = "current", invoke, classify, ...identity } = spec;
|
|
94
124
|
return defineProvider({
|
|
95
125
|
...identity,
|
|
96
|
-
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
126
|
+
methods: { [method]: defineProviderMethod({ version, lifecycle, invoke, classify }) }
|
|
97
127
|
});
|
|
98
128
|
}
|
|
99
129
|
var STRIKES_TO_OFFLINE = 3;
|
|
@@ -121,6 +151,7 @@ function createRegistry(options) {
|
|
|
121
151
|
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
122
152
|
const attempts = [];
|
|
123
153
|
for (const attachment of configured) {
|
|
154
|
+
const requestedVersion = attachment.providerMethodVersion;
|
|
124
155
|
if (callOptions.signal?.aborted) {
|
|
125
156
|
const cancelled = {
|
|
126
157
|
ok: false,
|
|
@@ -133,21 +164,25 @@ function createRegistry(options) {
|
|
|
133
164
|
}
|
|
134
165
|
const instance = await options.config.provider(attachment.instanceKey);
|
|
135
166
|
if (!instance) {
|
|
136
|
-
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
167
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: "instance not registered" });
|
|
137
168
|
continue;
|
|
138
169
|
}
|
|
139
170
|
const spec = byId.get(instance.providerId);
|
|
140
|
-
const method = spec
|
|
171
|
+
const method = methodBranch(spec, attachment.providerMethod, requestedVersion);
|
|
141
172
|
if (!spec || !method) {
|
|
142
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
173
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: !spec ? "provider not registered" : "method version not supported" });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (method.lifecycle === "retired") {
|
|
177
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "method version retired" });
|
|
143
178
|
continue;
|
|
144
179
|
}
|
|
145
180
|
if (!instance.enabled) {
|
|
146
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
181
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "instance disabled" });
|
|
147
182
|
continue;
|
|
148
183
|
}
|
|
149
184
|
if (attachment.health?.status === "offline") {
|
|
150
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
185
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "offline" });
|
|
151
186
|
continue;
|
|
152
187
|
}
|
|
153
188
|
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
@@ -158,15 +193,15 @@ function createRegistry(options) {
|
|
|
158
193
|
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
159
194
|
signal: callOptions.signal
|
|
160
195
|
}, args);
|
|
161
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
162
|
-
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
196
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
197
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, "success"));
|
|
163
198
|
const sent = {
|
|
164
199
|
ok: true,
|
|
165
200
|
result,
|
|
166
201
|
provider: instance.providerId,
|
|
167
202
|
instance: instance.instanceKey,
|
|
168
203
|
method: attachment.providerMethod,
|
|
169
|
-
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
204
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version },
|
|
170
205
|
fallbackUsed: attempts.length > 1,
|
|
171
206
|
attempts
|
|
172
207
|
};
|
|
@@ -190,13 +225,14 @@ function createRegistry(options) {
|
|
|
190
225
|
providerId: instance.providerId,
|
|
191
226
|
instanceKey: instance.instanceKey,
|
|
192
227
|
providerMethod: attachment.providerMethod,
|
|
228
|
+
providerMethodVersion: method.version,
|
|
193
229
|
outcome: "failed",
|
|
194
230
|
kind,
|
|
195
231
|
durationMs: performance.now() - startedAt,
|
|
196
232
|
failure: { kind, ...code ? { code } : {}, message },
|
|
197
233
|
error: message
|
|
198
234
|
});
|
|
199
|
-
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
235
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, kind));
|
|
200
236
|
if (kind === "terminal") {
|
|
201
237
|
const refused = {
|
|
202
238
|
ok: false,
|
|
@@ -246,8 +282,9 @@ function createService(definition, methods, options = {}) {
|
|
|
246
282
|
throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
|
|
247
283
|
}
|
|
248
284
|
priorities.add(attachment.priority);
|
|
249
|
-
|
|
250
|
-
|
|
285
|
+
const branch = methodBranch(attachment.provider, attachment.method, attachment.version);
|
|
286
|
+
if (!branch || branch.lifecycle === "retired") {
|
|
287
|
+
throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support active method "${attachment.method}@${attachment.version ?? "current"}".`);
|
|
251
288
|
}
|
|
252
289
|
const existing = providers.get(attachment.provider.id);
|
|
253
290
|
if (existing && existing !== attachment.provider) {
|
|
@@ -266,6 +303,7 @@ function createService(definition, methods, options = {}) {
|
|
|
266
303
|
return {
|
|
267
304
|
instanceKey: internalKey,
|
|
268
305
|
providerMethod: attachment.method,
|
|
306
|
+
providerMethodVersion: branch.version,
|
|
269
307
|
priority: attachment.priority,
|
|
270
308
|
enabled: attachment.enabled ?? true
|
|
271
309
|
};
|
|
@@ -292,13 +330,13 @@ function createService(definition, methods, options = {}) {
|
|
|
292
330
|
const { instance: _instance, selected, attempts, ...rest } = result;
|
|
293
331
|
return {
|
|
294
332
|
...rest,
|
|
295
|
-
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
|
|
333
|
+
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod, providerMethodVersion: selected.providerMethodVersion } } : {},
|
|
296
334
|
attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
|
|
297
335
|
};
|
|
298
336
|
}
|
|
299
337
|
};
|
|
300
338
|
}
|
|
301
|
-
var VERSION = "0.1.
|
|
339
|
+
var VERSION = "0.1.20";
|
|
302
340
|
|
|
303
341
|
// src/http.ts
|
|
304
342
|
class BudgetExhausted extends ProviderError {
|
package/dist/chain.js
CHANGED
|
@@ -75,25 +75,55 @@ function staticConfig(config) {
|
|
|
75
75
|
async list(serviceKey, methodKey) {
|
|
76
76
|
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
77
77
|
...attachment,
|
|
78
|
-
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
78
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:${attachment.providerMethodVersion ?? "current"}`) ?? (attachment.providerMethodVersion === undefined ? [...health.entries()].find(([key]) => key.startsWith(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:`))?.[1] : undefined) ?? attachment.health
|
|
79
79
|
}));
|
|
80
80
|
},
|
|
81
|
-
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
82
|
-
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
81
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, providerMethodVersion, next) {
|
|
82
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}:${providerMethodVersion}`, next);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
function defineProviderMethod(method) {
|
|
87
87
|
return method;
|
|
88
88
|
}
|
|
89
|
+
function defineProviderMethodBranches(branches) {
|
|
90
|
+
const entries = Object.entries(branches.versions);
|
|
91
|
+
if (entries.length === 0 || !branches.versions[branches.currentVersion]) {
|
|
92
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", "A versioned provider method needs a current branch.");
|
|
93
|
+
}
|
|
94
|
+
for (const [version, method] of entries) {
|
|
95
|
+
if (method.version !== version) {
|
|
96
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method branch "${version}" declares version "${method.version}".`);
|
|
97
|
+
}
|
|
98
|
+
if (version === branches.currentVersion && method.lifecycle !== "current") {
|
|
99
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Current provider method branch "${version}" must have lifecycle current.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return branches;
|
|
103
|
+
}
|
|
104
|
+
var methodBranch = (provider, methodName, version) => {
|
|
105
|
+
const entry = provider?.methods[methodName];
|
|
106
|
+
if (!entry)
|
|
107
|
+
return;
|
|
108
|
+
if ("versions" in entry)
|
|
109
|
+
return entry.versions[version ?? entry.currentVersion];
|
|
110
|
+
return version === undefined || version === entry.version ? entry : undefined;
|
|
111
|
+
};
|
|
89
112
|
function defineProvider(spec) {
|
|
113
|
+
for (const [name, entry] of Object.entries(spec.methods)) {
|
|
114
|
+
if ("versions" in entry)
|
|
115
|
+
defineProviderMethodBranches(entry);
|
|
116
|
+
else if (!entry.version || !["current", "legacy", "deprecated", "retired"].includes(entry.lifecycle)) {
|
|
117
|
+
throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method "${spec.id}.${name}" needs a version and lifecycle.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
90
120
|
return spec;
|
|
91
121
|
}
|
|
92
122
|
function defineSingleMethodProvider(spec) {
|
|
93
|
-
const { method, invoke, classify, ...identity } = spec;
|
|
123
|
+
const { method, version = "v1", lifecycle = "current", invoke, classify, ...identity } = spec;
|
|
94
124
|
return defineProvider({
|
|
95
125
|
...identity,
|
|
96
|
-
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
126
|
+
methods: { [method]: defineProviderMethod({ version, lifecycle, invoke, classify }) }
|
|
97
127
|
});
|
|
98
128
|
}
|
|
99
129
|
var STRIKES_TO_OFFLINE = 3;
|
|
@@ -121,6 +151,7 @@ function createRegistry(options) {
|
|
|
121
151
|
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
122
152
|
const attempts = [];
|
|
123
153
|
for (const attachment of configured) {
|
|
154
|
+
const requestedVersion = attachment.providerMethodVersion;
|
|
124
155
|
if (callOptions.signal?.aborted) {
|
|
125
156
|
const cancelled = {
|
|
126
157
|
ok: false,
|
|
@@ -133,21 +164,25 @@ function createRegistry(options) {
|
|
|
133
164
|
}
|
|
134
165
|
const instance = await options.config.provider(attachment.instanceKey);
|
|
135
166
|
if (!instance) {
|
|
136
|
-
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
167
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: "instance not registered" });
|
|
137
168
|
continue;
|
|
138
169
|
}
|
|
139
170
|
const spec = byId.get(instance.providerId);
|
|
140
|
-
const method = spec
|
|
171
|
+
const method = methodBranch(spec, attachment.providerMethod, requestedVersion);
|
|
141
172
|
if (!spec || !method) {
|
|
142
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
173
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: !spec ? "provider not registered" : "method version not supported" });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (method.lifecycle === "retired") {
|
|
177
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "method version retired" });
|
|
143
178
|
continue;
|
|
144
179
|
}
|
|
145
180
|
if (!instance.enabled) {
|
|
146
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
181
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "instance disabled" });
|
|
147
182
|
continue;
|
|
148
183
|
}
|
|
149
184
|
if (attachment.health?.status === "offline") {
|
|
150
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
185
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "offline" });
|
|
151
186
|
continue;
|
|
152
187
|
}
|
|
153
188
|
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
@@ -158,15 +193,15 @@ function createRegistry(options) {
|
|
|
158
193
|
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
159
194
|
signal: callOptions.signal
|
|
160
195
|
}, args);
|
|
161
|
-
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
162
|
-
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
196
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
197
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, "success"));
|
|
163
198
|
const sent = {
|
|
164
199
|
ok: true,
|
|
165
200
|
result,
|
|
166
201
|
provider: instance.providerId,
|
|
167
202
|
instance: instance.instanceKey,
|
|
168
203
|
method: attachment.providerMethod,
|
|
169
|
-
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
204
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version },
|
|
170
205
|
fallbackUsed: attempts.length > 1,
|
|
171
206
|
attempts
|
|
172
207
|
};
|
|
@@ -190,13 +225,14 @@ function createRegistry(options) {
|
|
|
190
225
|
providerId: instance.providerId,
|
|
191
226
|
instanceKey: instance.instanceKey,
|
|
192
227
|
providerMethod: attachment.providerMethod,
|
|
228
|
+
providerMethodVersion: method.version,
|
|
193
229
|
outcome: "failed",
|
|
194
230
|
kind,
|
|
195
231
|
durationMs: performance.now() - startedAt,
|
|
196
232
|
failure: { kind, ...code ? { code } : {}, message },
|
|
197
233
|
error: message
|
|
198
234
|
});
|
|
199
|
-
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
235
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, kind));
|
|
200
236
|
if (kind === "terminal") {
|
|
201
237
|
const refused = {
|
|
202
238
|
ok: false,
|
|
@@ -246,8 +282,9 @@ function createService(definition, methods, options = {}) {
|
|
|
246
282
|
throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
|
|
247
283
|
}
|
|
248
284
|
priorities.add(attachment.priority);
|
|
249
|
-
|
|
250
|
-
|
|
285
|
+
const branch = methodBranch(attachment.provider, attachment.method, attachment.version);
|
|
286
|
+
if (!branch || branch.lifecycle === "retired") {
|
|
287
|
+
throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support active method "${attachment.method}@${attachment.version ?? "current"}".`);
|
|
251
288
|
}
|
|
252
289
|
const existing = providers.get(attachment.provider.id);
|
|
253
290
|
if (existing && existing !== attachment.provider) {
|
|
@@ -266,6 +303,7 @@ function createService(definition, methods, options = {}) {
|
|
|
266
303
|
return {
|
|
267
304
|
instanceKey: internalKey,
|
|
268
305
|
providerMethod: attachment.method,
|
|
306
|
+
providerMethodVersion: branch.version,
|
|
269
307
|
priority: attachment.priority,
|
|
270
308
|
enabled: attachment.enabled ?? true
|
|
271
309
|
};
|
|
@@ -292,13 +330,13 @@ function createService(definition, methods, options = {}) {
|
|
|
292
330
|
const { instance: _instance, selected, attempts, ...rest } = result;
|
|
293
331
|
return {
|
|
294
332
|
...rest,
|
|
295
|
-
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
|
|
333
|
+
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod, providerMethodVersion: selected.providerMethodVersion } } : {},
|
|
296
334
|
attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
|
|
297
335
|
};
|
|
298
336
|
}
|
|
299
337
|
};
|
|
300
338
|
}
|
|
301
|
-
var VERSION = "0.1.
|
|
339
|
+
var VERSION = "0.1.20";
|
|
302
340
|
|
|
303
341
|
// src/chain.ts
|
|
304
342
|
var hexToNumber = (value) => Number(BigInt(value));
|