@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/dist/index.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?.methods[attachment.providerMethod];
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
- if (!attachment.provider.methods[attachment.method]) {
250
- throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
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.18";
339
+ var VERSION = "0.1.20";
302
340
  export {
303
341
  staticConfig,
304
342
  serviceMethod,
@@ -307,6 +345,7 @@ export {
307
345
  envCredentials,
308
346
  defineSingleMethodProvider,
309
347
  defineService,
348
+ defineProviderMethodBranches,
310
349
  defineProviderMethod,
311
350
  defineProvider,
312
351
  createService,
package/dist/pool.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?.methods[attachment.providerMethod];
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
- if (!attachment.provider.methods[attachment.method]) {
250
- throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
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.18";
339
+ var VERSION = "0.1.20";
302
340
 
303
341
  // src/http.ts
304
342
  class BudgetExhausted extends ProviderError {
package/dist/storage.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?.methods[attachment.providerMethod];
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
- if (!attachment.provider.methods[attachment.method]) {
250
- throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
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.18";
339
+ var VERSION = "0.1.20";
302
340
 
303
341
  // src/storage.ts
304
342
  var encoder = new TextEncoder;