@forgezero/providers 0.1.9 → 0.1.11

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/dist/index.js CHANGED
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
41
+ function scopeCredentials(source, reference) {
42
+ return {
43
+ name: `${source.name}:${reference}`,
44
+ get: (field) => source.get(reference, field)
45
+ };
46
+ }
47
+ function chainScopedCredentials(...sources) {
48
+ return {
49
+ name: sources.map((source) => source.name).join("+"),
50
+ async get(field) {
51
+ let last;
52
+ for (const source of sources) {
53
+ try {
54
+ return await source.get(field);
55
+ } catch (error) {
56
+ last = error;
57
+ }
58
+ }
59
+ throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
60
+ }
61
+ };
62
+ }
63
+ function staticConfig(config) {
64
+ const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
65
+ if (providers.size !== config.providers.length) {
66
+ throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
67
+ }
42
68
  const health = new Map;
43
69
  return {
44
70
  name: "static",
45
- async list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
71
+ async provider(instanceKey) {
72
+ const provider = providers.get(instanceKey);
73
+ return provider ? { ...provider, config: { ...provider.config } } : undefined;
74
+ },
75
+ async list(serviceKey, methodKey) {
76
+ return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
77
+ ...attachment,
78
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
49
79
  }));
50
80
  },
51
- async recordHealth(serviceKey, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
81
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
82
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
53
83
  }
54
84
  };
55
85
  }
86
+ function defineProviderMethod(method) {
87
+ return method;
88
+ }
56
89
  function defineProvider(spec) {
57
90
  return spec;
58
91
  }
92
+ function defineSingleMethodProvider(spec) {
93
+ const { method, invoke, classify, ...identity } = spec;
94
+ return defineProvider({
95
+ ...identity,
96
+ methods: { [method]: defineProviderMethod({ invoke, classify }) }
97
+ });
98
+ }
59
99
  var STRIKES_TO_OFFLINE = 3;
100
+ function serviceMethod() {
101
+ return Object.freeze({});
102
+ }
103
+ function defineService(definition) {
104
+ return definition;
105
+ }
60
106
  function nextHealth(current, kind) {
61
107
  if (kind === "success")
62
108
  return { strikes: 0, status: "ok" };
@@ -71,86 +117,201 @@ function nextHealth(current, kind) {
71
117
  }
72
118
  function createRegistry(options) {
73
119
  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((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
120
+ async function call(serviceKey, methodKey, args, callOptions = {}) {
121
+ const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
76
122
  const attempts = [];
77
- for (const entry of configured) {
123
+ for (const attachment of configured) {
78
124
  if (callOptions.signal?.aborted) {
79
125
  const cancelled = {
80
126
  ok: false,
127
+ fallbackUsed: attempts.length > 0,
81
128
  attempts,
82
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
129
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
83
130
  };
84
- options.after?.(cancelled);
131
+ await options.after?.(cancelled);
85
132
  return cancelled;
86
133
  }
87
- const spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
134
+ const instance = await options.config.provider(attachment.instanceKey);
135
+ if (!instance) {
136
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
90
137
  continue;
91
138
  }
92
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
139
+ const spec = byId.get(instance.providerId);
140
+ const method = spec?.methods[attachment.providerMethod];
141
+ 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" });
94
143
  continue;
95
144
  }
96
- options.before?.({ service: serviceKey, provider: entry.providerId });
145
+ if (!instance.enabled) {
146
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
147
+ continue;
148
+ }
149
+ if (attachment.health?.status === "offline") {
150
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
151
+ continue;
152
+ }
153
+ await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
154
+ const startedAt = performance.now();
97
155
  try {
98
- const result = await spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
156
+ const result = await method.invoke({
157
+ config: instance.config,
158
+ secret: (field) => options.credentials.get(instance.secretRef, field),
101
159
  signal: callOptions.signal
102
160
  }, args);
103
- attempts.push({ providerId: entry.providerId, outcome: "sent" });
104
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
105
- const sent = { ok: true, result, provider: entry.providerId, attempts };
106
- options.after?.(sent);
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"));
163
+ const sent = {
164
+ ok: true,
165
+ result,
166
+ provider: instance.providerId,
167
+ instance: instance.instanceKey,
168
+ method: attachment.providerMethod,
169
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
170
+ fallbackUsed: attempts.length > 1,
171
+ attempts
172
+ };
173
+ await options.after?.(sent);
107
174
  return sent;
108
175
  } catch (error) {
109
176
  if (callOptions.signal?.aborted) {
110
177
  const cancelled = {
111
178
  ok: false,
179
+ fallbackUsed: attempts.length > 0,
112
180
  attempts,
113
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
181
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
114
182
  };
115
- options.after?.(cancelled);
183
+ await options.after?.(cancelled);
116
184
  return cancelled;
117
185
  }
118
- const kind = spec.classify(error);
186
+ const kind = method.classify(error);
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
119
189
  attempts.push({
120
- providerId: entry.providerId,
190
+ providerId: instance.providerId,
191
+ instanceKey: instance.instanceKey,
192
+ providerMethod: attachment.providerMethod,
121
193
  outcome: "failed",
122
194
  kind,
123
- error: error instanceof Error ? error.message : String(error)
195
+ durationMs: performance.now() - startedAt,
196
+ failure: { kind, ...code ? { code } : {}, message },
197
+ error: message
124
198
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
199
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
126
200
  if (kind === "terminal") {
127
201
  const refused = {
128
202
  ok: false,
203
+ fallbackUsed: attempts.length > 1,
129
204
  attempts,
130
205
  error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
131
206
  };
132
- options.after?.(refused);
207
+ await options.after?.(refused);
133
208
  return refused;
134
209
  }
135
210
  }
136
211
  }
137
212
  const failed = {
138
213
  ok: false,
214
+ fallbackUsed: attempts.length > 1,
139
215
  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.`)
216
+ 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
217
  };
142
- options.after?.(failed);
218
+ await options.after?.(failed);
143
219
  return failed;
144
220
  }
145
- return { call };
221
+ function service(definition) {
222
+ return {
223
+ call(method, args, callOptions) {
224
+ return call(definition.key, method, args, callOptions);
225
+ }
226
+ };
227
+ }
228
+ return { call, service };
229
+ }
230
+ function createService(definition, methods, options = {}) {
231
+ const providers = new Map;
232
+ const credentials = new Map;
233
+ const instances = [];
234
+ const serviceMethods = {};
235
+ for (const methodKey of Object.keys(definition.methods)) {
236
+ const attachments = methods[methodKey];
237
+ if (!Array.isArray(attachments)) {
238
+ throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
239
+ }
240
+ const priorities = new Set;
241
+ serviceMethods[methodKey] = attachments.map((attachment, index) => {
242
+ if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
243
+ throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
244
+ }
245
+ if (priorities.has(attachment.priority)) {
246
+ throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
247
+ }
248
+ priorities.add(attachment.priority);
249
+ if (!attachment.provider.methods[attachment.method]) {
250
+ throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
251
+ }
252
+ const existing = providers.get(attachment.provider.id);
253
+ if (existing && existing !== attachment.provider) {
254
+ throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
255
+ }
256
+ providers.set(attachment.provider.id, attachment.provider);
257
+ const internalKey = `${definition.key}:${methodKey}:${index}`;
258
+ credentials.set(internalKey, attachment.credentials);
259
+ instances.push({
260
+ instanceKey: internalKey,
261
+ providerId: attachment.provider.id,
262
+ enabled: attachment.enabled ?? true,
263
+ config: { ...attachment.config ?? {} },
264
+ secretRef: internalKey
265
+ });
266
+ return {
267
+ instanceKey: internalKey,
268
+ providerMethod: attachment.method,
269
+ priority: attachment.priority,
270
+ enabled: attachment.enabled ?? true
271
+ };
272
+ });
273
+ }
274
+ const registry = createRegistry({
275
+ providers: [...providers.values()],
276
+ config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
277
+ credentials: {
278
+ name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
279
+ async get(reference, field) {
280
+ const source = credentials.get(reference);
281
+ if (!source)
282
+ throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
283
+ return source.get(field);
284
+ }
285
+ },
286
+ ...options
287
+ });
288
+ const dynamic = registry.service(definition);
289
+ return {
290
+ async call(method, args, callOptions) {
291
+ const result = await dynamic.call(method, args, callOptions);
292
+ const { instance: _instance, selected, attempts, ...rest } = result;
293
+ return {
294
+ ...rest,
295
+ ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
296
+ attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
297
+ };
298
+ }
299
+ };
146
300
  }
147
- var VERSION = "0.1.9";
301
+ var VERSION = "0.1.11";
148
302
  export {
149
303
  staticConfig,
304
+ serviceMethod,
305
+ scopeCredentials,
150
306
  nextHealth,
151
307
  envCredentials,
308
+ defineSingleMethodProvider,
309
+ defineService,
310
+ defineProviderMethod,
152
311
  defineProvider,
312
+ createService,
153
313
  createRegistry,
314
+ chainScopedCredentials,
154
315
  chainCredentials,
155
316
  VERSION,
156
317
  STRIKES_TO_OFFLINE,
package/dist/pool.js CHANGED
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
41
+ function scopeCredentials(source, reference) {
42
+ return {
43
+ name: `${source.name}:${reference}`,
44
+ get: (field) => source.get(reference, field)
45
+ };
46
+ }
47
+ function chainScopedCredentials(...sources) {
48
+ return {
49
+ name: sources.map((source) => source.name).join("+"),
50
+ async get(field) {
51
+ let last;
52
+ for (const source of sources) {
53
+ try {
54
+ return await source.get(field);
55
+ } catch (error) {
56
+ last = error;
57
+ }
58
+ }
59
+ throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
60
+ }
61
+ };
62
+ }
63
+ function staticConfig(config) {
64
+ const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
65
+ if (providers.size !== config.providers.length) {
66
+ throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
67
+ }
42
68
  const health = new Map;
43
69
  return {
44
70
  name: "static",
45
- async list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
71
+ async provider(instanceKey) {
72
+ const provider = providers.get(instanceKey);
73
+ return provider ? { ...provider, config: { ...provider.config } } : undefined;
74
+ },
75
+ async list(serviceKey, methodKey) {
76
+ return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
77
+ ...attachment,
78
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
49
79
  }));
50
80
  },
51
- async recordHealth(serviceKey, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
81
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
82
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
53
83
  }
54
84
  };
55
85
  }
86
+ function defineProviderMethod(method) {
87
+ return method;
88
+ }
56
89
  function defineProvider(spec) {
57
90
  return spec;
58
91
  }
92
+ function defineSingleMethodProvider(spec) {
93
+ const { method, invoke, classify, ...identity } = spec;
94
+ return defineProvider({
95
+ ...identity,
96
+ methods: { [method]: defineProviderMethod({ invoke, classify }) }
97
+ });
98
+ }
59
99
  var STRIKES_TO_OFFLINE = 3;
100
+ function serviceMethod() {
101
+ return Object.freeze({});
102
+ }
103
+ function defineService(definition) {
104
+ return definition;
105
+ }
60
106
  function nextHealth(current, kind) {
61
107
  if (kind === "success")
62
108
  return { strikes: 0, status: "ok" };
@@ -71,80 +117,188 @@ function nextHealth(current, kind) {
71
117
  }
72
118
  function createRegistry(options) {
73
119
  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((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
120
+ async function call(serviceKey, methodKey, args, callOptions = {}) {
121
+ const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
76
122
  const attempts = [];
77
- for (const entry of configured) {
123
+ for (const attachment of configured) {
78
124
  if (callOptions.signal?.aborted) {
79
125
  const cancelled = {
80
126
  ok: false,
127
+ fallbackUsed: attempts.length > 0,
81
128
  attempts,
82
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
129
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
83
130
  };
84
- options.after?.(cancelled);
131
+ await options.after?.(cancelled);
85
132
  return cancelled;
86
133
  }
87
- const spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
134
+ const instance = await options.config.provider(attachment.instanceKey);
135
+ if (!instance) {
136
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
90
137
  continue;
91
138
  }
92
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
139
+ const spec = byId.get(instance.providerId);
140
+ const method = spec?.methods[attachment.providerMethod];
141
+ 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" });
94
143
  continue;
95
144
  }
96
- options.before?.({ service: serviceKey, provider: entry.providerId });
145
+ if (!instance.enabled) {
146
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
147
+ continue;
148
+ }
149
+ if (attachment.health?.status === "offline") {
150
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
151
+ continue;
152
+ }
153
+ await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
154
+ const startedAt = performance.now();
97
155
  try {
98
- const result = await spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
156
+ const result = await method.invoke({
157
+ config: instance.config,
158
+ secret: (field) => options.credentials.get(instance.secretRef, field),
101
159
  signal: callOptions.signal
102
160
  }, args);
103
- attempts.push({ providerId: entry.providerId, outcome: "sent" });
104
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
105
- const sent = { ok: true, result, provider: entry.providerId, attempts };
106
- options.after?.(sent);
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"));
163
+ const sent = {
164
+ ok: true,
165
+ result,
166
+ provider: instance.providerId,
167
+ instance: instance.instanceKey,
168
+ method: attachment.providerMethod,
169
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
170
+ fallbackUsed: attempts.length > 1,
171
+ attempts
172
+ };
173
+ await options.after?.(sent);
107
174
  return sent;
108
175
  } catch (error) {
109
176
  if (callOptions.signal?.aborted) {
110
177
  const cancelled = {
111
178
  ok: false,
179
+ fallbackUsed: attempts.length > 0,
112
180
  attempts,
113
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
181
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
114
182
  };
115
- options.after?.(cancelled);
183
+ await options.after?.(cancelled);
116
184
  return cancelled;
117
185
  }
118
- const kind = spec.classify(error);
186
+ const kind = method.classify(error);
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
119
189
  attempts.push({
120
- providerId: entry.providerId,
190
+ providerId: instance.providerId,
191
+ instanceKey: instance.instanceKey,
192
+ providerMethod: attachment.providerMethod,
121
193
  outcome: "failed",
122
194
  kind,
123
- error: error instanceof Error ? error.message : String(error)
195
+ durationMs: performance.now() - startedAt,
196
+ failure: { kind, ...code ? { code } : {}, message },
197
+ error: message
124
198
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
199
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
126
200
  if (kind === "terminal") {
127
201
  const refused = {
128
202
  ok: false,
203
+ fallbackUsed: attempts.length > 1,
129
204
  attempts,
130
205
  error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
131
206
  };
132
- options.after?.(refused);
207
+ await options.after?.(refused);
133
208
  return refused;
134
209
  }
135
210
  }
136
211
  }
137
212
  const failed = {
138
213
  ok: false,
214
+ fallbackUsed: attempts.length > 1,
139
215
  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.`)
216
+ 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
217
  };
142
- options.after?.(failed);
218
+ await options.after?.(failed);
143
219
  return failed;
144
220
  }
145
- return { call };
221
+ function service(definition) {
222
+ return {
223
+ call(method, args, callOptions) {
224
+ return call(definition.key, method, args, callOptions);
225
+ }
226
+ };
227
+ }
228
+ return { call, service };
229
+ }
230
+ function createService(definition, methods, options = {}) {
231
+ const providers = new Map;
232
+ const credentials = new Map;
233
+ const instances = [];
234
+ const serviceMethods = {};
235
+ for (const methodKey of Object.keys(definition.methods)) {
236
+ const attachments = methods[methodKey];
237
+ if (!Array.isArray(attachments)) {
238
+ throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
239
+ }
240
+ const priorities = new Set;
241
+ serviceMethods[methodKey] = attachments.map((attachment, index) => {
242
+ if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
243
+ throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
244
+ }
245
+ if (priorities.has(attachment.priority)) {
246
+ throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
247
+ }
248
+ priorities.add(attachment.priority);
249
+ if (!attachment.provider.methods[attachment.method]) {
250
+ throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
251
+ }
252
+ const existing = providers.get(attachment.provider.id);
253
+ if (existing && existing !== attachment.provider) {
254
+ throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
255
+ }
256
+ providers.set(attachment.provider.id, attachment.provider);
257
+ const internalKey = `${definition.key}:${methodKey}:${index}`;
258
+ credentials.set(internalKey, attachment.credentials);
259
+ instances.push({
260
+ instanceKey: internalKey,
261
+ providerId: attachment.provider.id,
262
+ enabled: attachment.enabled ?? true,
263
+ config: { ...attachment.config ?? {} },
264
+ secretRef: internalKey
265
+ });
266
+ return {
267
+ instanceKey: internalKey,
268
+ providerMethod: attachment.method,
269
+ priority: attachment.priority,
270
+ enabled: attachment.enabled ?? true
271
+ };
272
+ });
273
+ }
274
+ const registry = createRegistry({
275
+ providers: [...providers.values()],
276
+ config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
277
+ credentials: {
278
+ name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
279
+ async get(reference, field) {
280
+ const source = credentials.get(reference);
281
+ if (!source)
282
+ throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
283
+ return source.get(field);
284
+ }
285
+ },
286
+ ...options
287
+ });
288
+ const dynamic = registry.service(definition);
289
+ return {
290
+ async call(method, args, callOptions) {
291
+ const result = await dynamic.call(method, args, callOptions);
292
+ const { instance: _instance, selected, attempts, ...rest } = result;
293
+ return {
294
+ ...rest,
295
+ ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
296
+ attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
297
+ };
298
+ }
299
+ };
146
300
  }
147
- var VERSION = "0.1.9";
301
+ var VERSION = "0.1.11";
148
302
 
149
303
  // src/http.ts
150
304
  class BudgetExhausted extends ProviderError {
@@ -227,9 +381,9 @@ function createHttpClient(config) {
227
381
  budget: () => budgetState(config.budget.host)
228
382
  };
229
383
  }
230
- var http = defineProvider({
384
+ var http = defineSingleMethodProvider({
231
385
  id: "http",
232
- service: "http",
386
+ method: "request",
233
387
  label: "HTTP",
234
388
  multiInstance: true,
235
389
  credentials: {
package/dist/storage.d.ts CHANGED
@@ -133,5 +133,5 @@ export type StorageRequest = {
133
133
  expiresInSec?: number;
134
134
  method?: 'GET' | 'PUT';
135
135
  };
136
- export declare const s3: import("./index").ProviderSpec<StorageRequest, unknown>;
137
- export declare const storageProviders: readonly [import("./index").ProviderSpec<StorageRequest, unknown>];
136
+ export declare const s3: import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<StorageRequest, unknown>>>;
137
+ export declare const storageProviders: readonly [import("./index").ProviderDefinition<Record<"request", import("./index").ProviderMethodSpec<StorageRequest, unknown>>>];