@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/dist/database.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/database.ts
|
|
304
342
|
var pools = new Map;
|
package/dist/email.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/email.ts
|
|
304
342
|
var recipients = (to) => Array.isArray(to) ? [...to] : [to];
|
|
@@ -332,6 +370,8 @@ var jetemailConfig = {
|
|
|
332
370
|
}
|
|
333
371
|
};
|
|
334
372
|
var jetemailSend = defineProviderMethod({
|
|
373
|
+
version: "v1",
|
|
374
|
+
lifecycle: "current",
|
|
335
375
|
async invoke(context, message) {
|
|
336
376
|
assertMessage(message);
|
|
337
377
|
const config = context.config;
|
|
@@ -382,6 +422,8 @@ function assertBatch(batch) {
|
|
|
382
422
|
assertMessage(message);
|
|
383
423
|
}
|
|
384
424
|
var jetemailSendBatch = defineProviderMethod({
|
|
425
|
+
version: "v1",
|
|
426
|
+
lifecycle: "current",
|
|
385
427
|
async invoke(context, batch) {
|
|
386
428
|
assertBatch(batch);
|
|
387
429
|
const config = context.config;
|
|
@@ -446,6 +488,8 @@ var jetemail = defineProvider({
|
|
|
446
488
|
}
|
|
447
489
|
});
|
|
448
490
|
var smtpSend = defineProviderMethod({
|
|
491
|
+
version: "rfc5321",
|
|
492
|
+
lifecycle: "current",
|
|
449
493
|
async invoke(context, message) {
|
|
450
494
|
assertMessage(message);
|
|
451
495
|
const config = context.config;
|
package/dist/http.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/index.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface ServiceMethodAttachment {
|
|
|
80
80
|
instanceKey: string;
|
|
81
81
|
/** Explicit provider capability; it is never inferred from the service name. */
|
|
82
82
|
providerMethod: string;
|
|
83
|
+
/** Exact provider API/contract branch. Omit only to select the provider's declared current branch. */
|
|
84
|
+
providerMethodVersion?: string;
|
|
83
85
|
priority: number;
|
|
84
86
|
enabled: boolean;
|
|
85
87
|
health?: ProviderHealth;
|
|
@@ -90,7 +92,7 @@ export interface ConfigSource {
|
|
|
90
92
|
provider(instanceKey: string): Promise<ProviderInstanceConfig | undefined>;
|
|
91
93
|
list(serviceKey: string, methodKey: string): Promise<readonly ServiceMethodAttachment[]>;
|
|
92
94
|
/** Persisted, because strikes that reset on restart retry a dead provider forever. */
|
|
93
|
-
recordHealth(serviceKey: string, methodKey: string, instanceKey: string, providerMethod: string, health: ProviderHealth): Promise<void>;
|
|
95
|
+
recordHealth(serviceKey: string, methodKey: string, instanceKey: string, providerMethod: string, providerMethodVersion: string, health: ProviderHealth): Promise<void>;
|
|
94
96
|
}
|
|
95
97
|
export interface StaticRegistryConfig {
|
|
96
98
|
providers: readonly ProviderInstanceConfig[];
|
|
@@ -116,12 +118,21 @@ export interface InvokeContext {
|
|
|
116
118
|
signal?: AbortSignal;
|
|
117
119
|
}
|
|
118
120
|
export interface ProviderMethodSpec<Args = never, Result = never> {
|
|
121
|
+
/** Provider-owned contract/API branch, such as `v1` or `2026-08-01`. */
|
|
122
|
+
version: string;
|
|
123
|
+
lifecycle: 'current' | 'legacy' | 'deprecated' | 'retired';
|
|
119
124
|
invoke(context: InvokeContext, args: Args): Promise<Result>;
|
|
120
125
|
classify(error: unknown): FailureKind;
|
|
121
126
|
}
|
|
122
127
|
export declare function defineProviderMethod<Args, Result>(method: ProviderMethodSpec<Args, Result>): ProviderMethodSpec<Args, Result>;
|
|
123
128
|
export type AnyProviderMethod = ProviderMethodSpec<any, any>;
|
|
124
|
-
export
|
|
129
|
+
export interface ProviderMethodBranches<Args = any, Result = any> {
|
|
130
|
+
currentVersion: string;
|
|
131
|
+
versions: Readonly<Record<string, ProviderMethodSpec<Args, Result>>>;
|
|
132
|
+
}
|
|
133
|
+
export declare function defineProviderMethodBranches<Args, Result>(branches: ProviderMethodBranches<Args, Result>): ProviderMethodBranches<Args, Result>;
|
|
134
|
+
export type ProviderMethodEntry = AnyProviderMethod | ProviderMethodBranches;
|
|
135
|
+
export type ProviderMethods = Record<string, ProviderMethodEntry>;
|
|
125
136
|
/** A vendor/provider identity with named capabilities, independent of services. */
|
|
126
137
|
export interface ProviderDefinition<Methods extends ProviderMethods = ProviderMethods> {
|
|
127
138
|
id: string;
|
|
@@ -136,6 +147,8 @@ export declare function defineProvider<Methods extends ProviderMethods>(spec: Pr
|
|
|
136
147
|
/** Convenience for a provider that exposes exactly one named method. */
|
|
137
148
|
export declare function defineSingleMethodProvider<Method extends string, Args, Result>(spec: Omit<ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>, 'methods'> & {
|
|
138
149
|
method: Method;
|
|
150
|
+
version?: string;
|
|
151
|
+
lifecycle?: ProviderMethodSpec['lifecycle'];
|
|
139
152
|
invoke(context: InvokeContext, args: Args): Promise<Result>;
|
|
140
153
|
classify(error: unknown): FailureKind;
|
|
141
154
|
}): ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>;
|
|
@@ -144,6 +157,7 @@ export interface Attempt {
|
|
|
144
157
|
providerId: string;
|
|
145
158
|
instanceKey: string;
|
|
146
159
|
providerMethod: string;
|
|
160
|
+
providerMethodVersion: string;
|
|
147
161
|
outcome: 'sent' | 'skipped' | 'failed';
|
|
148
162
|
kind?: FailureKind;
|
|
149
163
|
durationMs?: number;
|
|
@@ -165,6 +179,7 @@ export interface CallResult<Result> {
|
|
|
165
179
|
providerId: string;
|
|
166
180
|
instanceKey: string;
|
|
167
181
|
providerMethod: string;
|
|
182
|
+
providerMethodVersion: string;
|
|
168
183
|
};
|
|
169
184
|
fallbackUsed: boolean;
|
|
170
185
|
attempts: readonly Attempt[];
|
|
@@ -196,6 +211,8 @@ type ServiceResult<T> = T extends ServiceMethodContract<unknown, infer Result> ?
|
|
|
196
211
|
export interface DirectServiceAttachment {
|
|
197
212
|
provider: ProviderDefinition;
|
|
198
213
|
method: string;
|
|
214
|
+
/** Select an exact legacy/current branch. Omit to use the provider's declared currentVersion. */
|
|
215
|
+
version?: string;
|
|
199
216
|
credentials: ScopedCredentialSource;
|
|
200
217
|
config?: Record<string, unknown>;
|
|
201
218
|
priority: number;
|
|
@@ -213,6 +230,7 @@ export type DirectCallResult<Result> = Omit<CallResult<Result>, 'instance' | 'se
|
|
|
213
230
|
selected?: {
|
|
214
231
|
providerId: string;
|
|
215
232
|
providerMethod: string;
|
|
233
|
+
providerMethodVersion: string;
|
|
216
234
|
};
|
|
217
235
|
attempts: readonly DirectAttempt[];
|
|
218
236
|
};
|
|
@@ -285,5 +303,5 @@ export interface EmailBatchResult {
|
|
|
285
303
|
};
|
|
286
304
|
results: readonly EmailBatchItemResult[];
|
|
287
305
|
}
|
|
288
|
-
export declare const VERSION = "0.1.
|
|
306
|
+
export declare const VERSION = "0.1.20";
|
|
289
307
|
export {};
|