@forgezero/providers 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -52
- package/dist/binance.js +95 -38
- package/dist/chain.d.ts +1 -1
- package/dist/chain.js +95 -38
- package/dist/database.d.ts +2 -2
- package/dist/database.js +95 -38
- package/dist/email.d.ts +26 -44
- package/dist/email.js +160 -127
- package/dist/http.d.ts +2 -2
- package/dist/http.js +95 -38
- package/dist/index.d.ts +80 -17
- package/dist/index.js +97 -36
- package/dist/pool.js +95 -38
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +95 -38
- package/dist/translation.d.ts +6 -4
- package/dist/translation.js +95 -38
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -55,25 +55,39 @@ export interface ProviderHealth {
|
|
|
55
55
|
status: 'ok' | 'degraded' | 'offline';
|
|
56
56
|
lastFailureAtTs?: number;
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
/** One configured provider identity. It is independent of every service. */
|
|
59
|
+
export interface ProviderInstanceConfig {
|
|
60
|
+
/** Stable key used by service-method attachments. */
|
|
61
|
+
instanceKey: string;
|
|
59
62
|
providerId: string;
|
|
60
|
-
/**
|
|
61
|
-
instanceKey?: string;
|
|
62
|
-
priority: number;
|
|
63
|
+
/** Disables every method on this instance without deleting its configuration. */
|
|
63
64
|
enabled: boolean;
|
|
64
65
|
config: Record<string, unknown>;
|
|
65
66
|
/** Names a vault entry. NEVER the secret itself — a dump yields metadata. */
|
|
66
67
|
secretRef: string;
|
|
68
|
+
}
|
|
69
|
+
/** One service method's attachment to one provider instance method. */
|
|
70
|
+
export interface ServiceMethodAttachment {
|
|
71
|
+
instanceKey: string;
|
|
72
|
+
/** Explicit provider capability; it is never inferred from the service name. */
|
|
73
|
+
providerMethod: string;
|
|
74
|
+
priority: number;
|
|
75
|
+
enabled: boolean;
|
|
67
76
|
health?: ProviderHealth;
|
|
68
77
|
}
|
|
69
|
-
/**
|
|
78
|
+
/** Provider identities and service routing policy may live in any store. */
|
|
70
79
|
export interface ConfigSource {
|
|
71
80
|
readonly name: string;
|
|
72
|
-
|
|
81
|
+
provider(instanceKey: string): Promise<ProviderInstanceConfig | undefined>;
|
|
82
|
+
list(serviceKey: string, methodKey: string): Promise<readonly ServiceMethodAttachment[]>;
|
|
73
83
|
/** Persisted, because strikes that reset on restart retry a dead provider forever. */
|
|
74
|
-
recordHealth(serviceKey: string,
|
|
84
|
+
recordHealth(serviceKey: string, methodKey: string, instanceKey: string, providerMethod: string, health: ProviderHealth): Promise<void>;
|
|
75
85
|
}
|
|
76
|
-
export
|
|
86
|
+
export interface StaticRegistryConfig {
|
|
87
|
+
providers: Record<string, Omit<ProviderInstanceConfig, 'instanceKey'>>;
|
|
88
|
+
services: Record<string, Record<string, readonly ServiceMethodAttachment[]>>;
|
|
89
|
+
}
|
|
90
|
+
export declare function staticConfig(config: StaticRegistryConfig): ConfigSource;
|
|
77
91
|
/**
|
|
78
92
|
* How a failure should be treated.
|
|
79
93
|
*
|
|
@@ -92,29 +106,58 @@ export interface InvokeContext {
|
|
|
92
106
|
secret(field: string): Promise<string>;
|
|
93
107
|
signal?: AbortSignal;
|
|
94
108
|
}
|
|
95
|
-
export interface
|
|
109
|
+
export interface ProviderMethodSpec<Args = never, Result = never> {
|
|
110
|
+
invoke(context: InvokeContext, args: Args): Promise<Result>;
|
|
111
|
+
classify(error: unknown): FailureKind;
|
|
112
|
+
}
|
|
113
|
+
export declare function defineProviderMethod<Args, Result>(method: ProviderMethodSpec<Args, Result>): ProviderMethodSpec<Args, Result>;
|
|
114
|
+
export type AnyProviderMethod = ProviderMethodSpec<any, any>;
|
|
115
|
+
export type ProviderMethods = Record<string, AnyProviderMethod>;
|
|
116
|
+
/** A vendor/provider identity with named capabilities, independent of services. */
|
|
117
|
+
export interface ProviderDefinition<Methods extends ProviderMethods = ProviderMethods> {
|
|
96
118
|
id: string;
|
|
97
|
-
service: string;
|
|
98
119
|
label: string;
|
|
99
120
|
multiInstance?: boolean;
|
|
100
121
|
/** JSON Schema. `writeOnly` fields route to secret storage. */
|
|
101
122
|
credentials?: Record<string, unknown>;
|
|
102
123
|
config?: Record<string, unknown>;
|
|
124
|
+
methods: Methods;
|
|
125
|
+
}
|
|
126
|
+
export declare function defineProvider<Methods extends ProviderMethods>(spec: ProviderDefinition<Methods>): ProviderDefinition<Methods>;
|
|
127
|
+
/** Convenience for a provider that exposes exactly one named method. */
|
|
128
|
+
export declare function defineSingleMethodProvider<Method extends string, Args, Result>(spec: Omit<ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>, 'methods'> & {
|
|
129
|
+
method: Method;
|
|
103
130
|
invoke(context: InvokeContext, args: Args): Promise<Result>;
|
|
104
131
|
classify(error: unknown): FailureKind;
|
|
105
|
-
}
|
|
106
|
-
export declare function defineProvider<Args, Result>(spec: ProviderSpec<Args, Result>): ProviderSpec<Args, Result>;
|
|
132
|
+
}): ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>;
|
|
107
133
|
export declare const STRIKES_TO_OFFLINE = 3;
|
|
108
134
|
export interface Attempt {
|
|
109
135
|
providerId: string;
|
|
136
|
+
instanceKey: string;
|
|
137
|
+
providerMethod: string;
|
|
110
138
|
outcome: 'sent' | 'skipped' | 'failed';
|
|
111
139
|
kind?: FailureKind;
|
|
140
|
+
durationMs?: number;
|
|
141
|
+
failure?: {
|
|
142
|
+
kind: FailureKind;
|
|
143
|
+
code?: string;
|
|
144
|
+
message: string;
|
|
145
|
+
};
|
|
146
|
+
/** @deprecated Read `failure.message`; retained for 0.1 compatibility. */
|
|
112
147
|
error?: string;
|
|
113
148
|
}
|
|
114
149
|
export interface CallResult<Result> {
|
|
115
150
|
ok: boolean;
|
|
116
151
|
result?: Result;
|
|
117
152
|
provider?: string;
|
|
153
|
+
instance?: string;
|
|
154
|
+
method?: string;
|
|
155
|
+
selected?: {
|
|
156
|
+
providerId: string;
|
|
157
|
+
instanceKey: string;
|
|
158
|
+
providerMethod: string;
|
|
159
|
+
};
|
|
160
|
+
fallbackUsed: boolean;
|
|
118
161
|
attempts: readonly Attempt[];
|
|
119
162
|
error?: ProviderError;
|
|
120
163
|
}
|
|
@@ -122,6 +165,20 @@ export interface CallOptions {
|
|
|
122
165
|
/** Reaches provider fetch/socket code; cancellation never falls through to another vendor. */
|
|
123
166
|
signal?: AbortSignal;
|
|
124
167
|
}
|
|
168
|
+
/** Type-only input/output contract for one service method. */
|
|
169
|
+
export interface ServiceMethodContract<Args, Result> {
|
|
170
|
+
readonly __args?: (args: Args) => void;
|
|
171
|
+
readonly __result?: Result;
|
|
172
|
+
}
|
|
173
|
+
export declare function serviceMethod<Args, Result>(): ServiceMethodContract<Args, Result>;
|
|
174
|
+
export type ServiceMethods = Record<string, ServiceMethodContract<any, any>>;
|
|
175
|
+
export interface ServiceDefinition<Key extends string, Methods extends ServiceMethods> {
|
|
176
|
+
key: Key;
|
|
177
|
+
methods: Methods;
|
|
178
|
+
}
|
|
179
|
+
export declare function defineService<Key extends string, Methods extends ServiceMethods>(definition: ServiceDefinition<Key, Methods>): ServiceDefinition<Key, Methods>;
|
|
180
|
+
type ServiceArgs<T> = T extends ServiceMethodContract<infer Args, unknown> ? Args : never;
|
|
181
|
+
type ServiceResult<T> = T extends ServiceMethodContract<unknown, infer Result> ? Result : never;
|
|
125
182
|
export interface RegistryOptions {
|
|
126
183
|
credentials: CredentialSource;
|
|
127
184
|
config: ConfigSource;
|
|
@@ -136,16 +193,21 @@ export interface RegistryOptions {
|
|
|
136
193
|
* keep at the DEFINITION site, where `defineProvider` still checks that
|
|
137
194
|
* `invoke` matches what the provider claims to take and return.
|
|
138
195
|
*/
|
|
139
|
-
providers: readonly
|
|
196
|
+
providers: readonly ProviderDefinition[];
|
|
140
197
|
before?: (context: {
|
|
141
198
|
service: string;
|
|
199
|
+
method: string;
|
|
142
200
|
provider: string;
|
|
143
|
-
|
|
144
|
-
|
|
201
|
+
instance: string;
|
|
202
|
+
}) => void | Promise<void>;
|
|
203
|
+
after?: (result: CallResult<unknown>) => void | Promise<void>;
|
|
145
204
|
}
|
|
146
205
|
export declare function nextHealth(current: ProviderHealth | undefined, kind: FailureKind | 'success'): ProviderHealth;
|
|
147
206
|
export declare function createRegistry(options: RegistryOptions): {
|
|
148
|
-
call: <Result>(serviceKey: string, args: unknown, callOptions?: CallOptions) => Promise<CallResult<Result>>;
|
|
207
|
+
call: <Result>(serviceKey: string, methodKey: string, args: unknown, callOptions?: CallOptions) => Promise<CallResult<Result>>;
|
|
208
|
+
service: <Key extends string, Methods extends ServiceMethods>(definition: ServiceDefinition<Key, Methods>) => {
|
|
209
|
+
call<Method extends Extract<keyof Methods, string>>(method: Method, args: ServiceArgs<Methods[Method]>, callOptions?: CallOptions): Promise<CallResult<ServiceResult<Methods[Method]>>>;
|
|
210
|
+
};
|
|
149
211
|
};
|
|
150
212
|
/** Passed to `registry.call('email', message)`. */
|
|
151
213
|
export interface EmailMessage {
|
|
@@ -176,4 +238,5 @@ export interface EmailBatchResult {
|
|
|
176
238
|
};
|
|
177
239
|
results: readonly EmailBatchItemResult[];
|
|
178
240
|
}
|
|
179
|
-
export declare const VERSION = "0.1.
|
|
241
|
+
export declare const VERSION = "0.1.10";
|
|
242
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -38,25 +38,45 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
function staticConfig(
|
|
41
|
+
function staticConfig(config) {
|
|
42
42
|
const health = new Map;
|
|
43
43
|
return {
|
|
44
44
|
name: "static",
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
async provider(instanceKey) {
|
|
46
|
+
const provider = config.providers[instanceKey];
|
|
47
|
+
return provider ? { instanceKey, ...provider, config: { ...provider.config } } : undefined;
|
|
48
|
+
},
|
|
49
|
+
async list(serviceKey, methodKey) {
|
|
50
|
+
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
51
|
+
...attachment,
|
|
52
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
49
53
|
}));
|
|
50
54
|
},
|
|
51
|
-
async recordHealth(serviceKey,
|
|
52
|
-
health.set(`${serviceKey}:${
|
|
55
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
56
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
53
57
|
}
|
|
54
58
|
};
|
|
55
59
|
}
|
|
60
|
+
function defineProviderMethod(method) {
|
|
61
|
+
return method;
|
|
62
|
+
}
|
|
56
63
|
function defineProvider(spec) {
|
|
57
64
|
return spec;
|
|
58
65
|
}
|
|
66
|
+
function defineSingleMethodProvider(spec) {
|
|
67
|
+
const { method, invoke, classify, ...identity } = spec;
|
|
68
|
+
return defineProvider({
|
|
69
|
+
...identity,
|
|
70
|
+
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
59
73
|
var STRIKES_TO_OFFLINE = 3;
|
|
74
|
+
function serviceMethod() {
|
|
75
|
+
return Object.freeze({});
|
|
76
|
+
}
|
|
77
|
+
function defineService(definition) {
|
|
78
|
+
return definition;
|
|
79
|
+
}
|
|
60
80
|
function nextHealth(current, kind) {
|
|
61
81
|
if (kind === "success")
|
|
62
82
|
return { strikes: 0, status: "ok" };
|
|
@@ -71,84 +91,125 @@ function nextHealth(current, kind) {
|
|
|
71
91
|
}
|
|
72
92
|
function createRegistry(options) {
|
|
73
93
|
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
74
|
-
async function call(serviceKey, args, callOptions = {}) {
|
|
75
|
-
const configured = [...await options.config.list(serviceKey)].filter((
|
|
94
|
+
async function call(serviceKey, methodKey, args, callOptions = {}) {
|
|
95
|
+
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
76
96
|
const attempts = [];
|
|
77
|
-
for (const
|
|
97
|
+
for (const attachment of configured) {
|
|
78
98
|
if (callOptions.signal?.aborted) {
|
|
79
99
|
const cancelled = {
|
|
80
100
|
ok: false,
|
|
101
|
+
fallbackUsed: attempts.length > 0,
|
|
81
102
|
attempts,
|
|
82
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
103
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
83
104
|
};
|
|
84
|
-
options.after?.(cancelled);
|
|
105
|
+
await options.after?.(cancelled);
|
|
85
106
|
return cancelled;
|
|
86
107
|
}
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
attempts.push({ providerId:
|
|
108
|
+
const instance = await options.config.provider(attachment.instanceKey);
|
|
109
|
+
if (!instance) {
|
|
110
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const spec = byId.get(instance.providerId);
|
|
114
|
+
const method = spec?.methods[attachment.providerMethod];
|
|
115
|
+
if (!spec || !method) {
|
|
116
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
90
117
|
continue;
|
|
91
118
|
}
|
|
92
|
-
if (
|
|
93
|
-
attempts.push({ providerId:
|
|
119
|
+
if (!instance.enabled) {
|
|
120
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
94
121
|
continue;
|
|
95
122
|
}
|
|
96
|
-
|
|
123
|
+
if (attachment.health?.status === "offline") {
|
|
124
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
128
|
+
const startedAt = performance.now();
|
|
97
129
|
try {
|
|
98
|
-
const result = await
|
|
99
|
-
config:
|
|
100
|
-
secret: (field) => options.credentials.get(
|
|
130
|
+
const result = await method.invoke({
|
|
131
|
+
config: instance.config,
|
|
132
|
+
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
101
133
|
signal: callOptions.signal
|
|
102
134
|
}, args);
|
|
103
|
-
attempts.push({ providerId:
|
|
104
|
-
await options.config.recordHealth(serviceKey,
|
|
105
|
-
const sent = {
|
|
106
|
-
|
|
135
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
136
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
137
|
+
const sent = {
|
|
138
|
+
ok: true,
|
|
139
|
+
result,
|
|
140
|
+
provider: instance.providerId,
|
|
141
|
+
instance: instance.instanceKey,
|
|
142
|
+
method: attachment.providerMethod,
|
|
143
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
144
|
+
fallbackUsed: attempts.length > 1,
|
|
145
|
+
attempts
|
|
146
|
+
};
|
|
147
|
+
await options.after?.(sent);
|
|
107
148
|
return sent;
|
|
108
149
|
} catch (error) {
|
|
109
150
|
if (callOptions.signal?.aborted) {
|
|
110
151
|
const cancelled = {
|
|
111
152
|
ok: false,
|
|
153
|
+
fallbackUsed: attempts.length > 0,
|
|
112
154
|
attempts,
|
|
113
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
155
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
114
156
|
};
|
|
115
|
-
options.after?.(cancelled);
|
|
157
|
+
await options.after?.(cancelled);
|
|
116
158
|
return cancelled;
|
|
117
159
|
}
|
|
118
|
-
const kind =
|
|
160
|
+
const kind = method.classify(error);
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
|
|
119
163
|
attempts.push({
|
|
120
|
-
providerId:
|
|
164
|
+
providerId: instance.providerId,
|
|
165
|
+
instanceKey: instance.instanceKey,
|
|
166
|
+
providerMethod: attachment.providerMethod,
|
|
121
167
|
outcome: "failed",
|
|
122
168
|
kind,
|
|
123
|
-
|
|
169
|
+
durationMs: performance.now() - startedAt,
|
|
170
|
+
failure: { kind, ...code ? { code } : {}, message },
|
|
171
|
+
error: message
|
|
124
172
|
});
|
|
125
|
-
await options.config.recordHealth(serviceKey,
|
|
173
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
126
174
|
if (kind === "terminal") {
|
|
127
175
|
const refused = {
|
|
128
176
|
ok: false,
|
|
177
|
+
fallbackUsed: attempts.length > 1,
|
|
129
178
|
attempts,
|
|
130
179
|
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
131
180
|
};
|
|
132
|
-
options.after?.(refused);
|
|
181
|
+
await options.after?.(refused);
|
|
133
182
|
return refused;
|
|
134
183
|
}
|
|
135
184
|
}
|
|
136
185
|
}
|
|
137
186
|
const failed = {
|
|
138
187
|
ok: false,
|
|
188
|
+
fallbackUsed: attempts.length > 1,
|
|
139
189
|
attempts,
|
|
140
|
-
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
|
|
190
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
|
|
141
191
|
};
|
|
142
|
-
options.after?.(failed);
|
|
192
|
+
await options.after?.(failed);
|
|
143
193
|
return failed;
|
|
144
194
|
}
|
|
145
|
-
|
|
195
|
+
function service(definition) {
|
|
196
|
+
return {
|
|
197
|
+
call(method, args, callOptions) {
|
|
198
|
+
return call(definition.key, method, args, callOptions);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return { call, service };
|
|
146
203
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
204
|
+
var VERSION = "0.1.10";
|
|
148
205
|
export {
|
|
149
206
|
staticConfig,
|
|
207
|
+
serviceMethod,
|
|
150
208
|
nextHealth,
|
|
151
209
|
envCredentials,
|
|
210
|
+
defineSingleMethodProvider,
|
|
211
|
+
defineService,
|
|
212
|
+
defineProviderMethod,
|
|
152
213
|
defineProvider,
|
|
153
214
|
createRegistry,
|
|
154
215
|
chainCredentials,
|
package/dist/pool.js
CHANGED
|
@@ -38,25 +38,45 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
function staticConfig(
|
|
41
|
+
function staticConfig(config) {
|
|
42
42
|
const health = new Map;
|
|
43
43
|
return {
|
|
44
44
|
name: "static",
|
|
45
|
-
async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
async provider(instanceKey) {
|
|
46
|
+
const provider = config.providers[instanceKey];
|
|
47
|
+
return provider ? { instanceKey, ...provider, config: { ...provider.config } } : undefined;
|
|
48
|
+
},
|
|
49
|
+
async list(serviceKey, methodKey) {
|
|
50
|
+
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
51
|
+
...attachment,
|
|
52
|
+
health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
|
|
49
53
|
}));
|
|
50
54
|
},
|
|
51
|
-
async recordHealth(serviceKey,
|
|
52
|
-
health.set(`${serviceKey}:${
|
|
55
|
+
async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
|
|
56
|
+
health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
|
|
53
57
|
}
|
|
54
58
|
};
|
|
55
59
|
}
|
|
60
|
+
function defineProviderMethod(method) {
|
|
61
|
+
return method;
|
|
62
|
+
}
|
|
56
63
|
function defineProvider(spec) {
|
|
57
64
|
return spec;
|
|
58
65
|
}
|
|
66
|
+
function defineSingleMethodProvider(spec) {
|
|
67
|
+
const { method, invoke, classify, ...identity } = spec;
|
|
68
|
+
return defineProvider({
|
|
69
|
+
...identity,
|
|
70
|
+
methods: { [method]: defineProviderMethod({ invoke, classify }) }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
59
73
|
var STRIKES_TO_OFFLINE = 3;
|
|
74
|
+
function serviceMethod() {
|
|
75
|
+
return Object.freeze({});
|
|
76
|
+
}
|
|
77
|
+
function defineService(definition) {
|
|
78
|
+
return definition;
|
|
79
|
+
}
|
|
60
80
|
function nextHealth(current, kind) {
|
|
61
81
|
if (kind === "success")
|
|
62
82
|
return { strikes: 0, status: "ok" };
|
|
@@ -71,80 +91,117 @@ function nextHealth(current, kind) {
|
|
|
71
91
|
}
|
|
72
92
|
function createRegistry(options) {
|
|
73
93
|
const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
74
|
-
async function call(serviceKey, args, callOptions = {}) {
|
|
75
|
-
const configured = [...await options.config.list(serviceKey)].filter((
|
|
94
|
+
async function call(serviceKey, methodKey, args, callOptions = {}) {
|
|
95
|
+
const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
|
|
76
96
|
const attempts = [];
|
|
77
|
-
for (const
|
|
97
|
+
for (const attachment of configured) {
|
|
78
98
|
if (callOptions.signal?.aborted) {
|
|
79
99
|
const cancelled = {
|
|
80
100
|
ok: false,
|
|
101
|
+
fallbackUsed: attempts.length > 0,
|
|
81
102
|
attempts,
|
|
82
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
103
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
83
104
|
};
|
|
84
|
-
options.after?.(cancelled);
|
|
105
|
+
await options.after?.(cancelled);
|
|
85
106
|
return cancelled;
|
|
86
107
|
}
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
attempts.push({ providerId:
|
|
108
|
+
const instance = await options.config.provider(attachment.instanceKey);
|
|
109
|
+
if (!instance) {
|
|
110
|
+
attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const spec = byId.get(instance.providerId);
|
|
114
|
+
const method = spec?.methods[attachment.providerMethod];
|
|
115
|
+
if (!spec || !method) {
|
|
116
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
|
|
90
117
|
continue;
|
|
91
118
|
}
|
|
92
|
-
if (
|
|
93
|
-
attempts.push({ providerId:
|
|
119
|
+
if (!instance.enabled) {
|
|
120
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
|
|
94
121
|
continue;
|
|
95
122
|
}
|
|
96
|
-
|
|
123
|
+
if (attachment.health?.status === "offline") {
|
|
124
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
|
|
128
|
+
const startedAt = performance.now();
|
|
97
129
|
try {
|
|
98
|
-
const result = await
|
|
99
|
-
config:
|
|
100
|
-
secret: (field) => options.credentials.get(
|
|
130
|
+
const result = await method.invoke({
|
|
131
|
+
config: instance.config,
|
|
132
|
+
secret: (field) => options.credentials.get(instance.secretRef, field),
|
|
101
133
|
signal: callOptions.signal
|
|
102
134
|
}, args);
|
|
103
|
-
attempts.push({ providerId:
|
|
104
|
-
await options.config.recordHealth(serviceKey,
|
|
105
|
-
const sent = {
|
|
106
|
-
|
|
135
|
+
attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
|
|
136
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
|
|
137
|
+
const sent = {
|
|
138
|
+
ok: true,
|
|
139
|
+
result,
|
|
140
|
+
provider: instance.providerId,
|
|
141
|
+
instance: instance.instanceKey,
|
|
142
|
+
method: attachment.providerMethod,
|
|
143
|
+
selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
|
|
144
|
+
fallbackUsed: attempts.length > 1,
|
|
145
|
+
attempts
|
|
146
|
+
};
|
|
147
|
+
await options.after?.(sent);
|
|
107
148
|
return sent;
|
|
108
149
|
} catch (error) {
|
|
109
150
|
if (callOptions.signal?.aborted) {
|
|
110
151
|
const cancelled = {
|
|
111
152
|
ok: false,
|
|
153
|
+
fallbackUsed: attempts.length > 0,
|
|
112
154
|
attempts,
|
|
113
|
-
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
|
|
155
|
+
error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
|
|
114
156
|
};
|
|
115
|
-
options.after?.(cancelled);
|
|
157
|
+
await options.after?.(cancelled);
|
|
116
158
|
return cancelled;
|
|
117
159
|
}
|
|
118
|
-
const kind =
|
|
160
|
+
const kind = method.classify(error);
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
|
|
119
163
|
attempts.push({
|
|
120
|
-
providerId:
|
|
164
|
+
providerId: instance.providerId,
|
|
165
|
+
instanceKey: instance.instanceKey,
|
|
166
|
+
providerMethod: attachment.providerMethod,
|
|
121
167
|
outcome: "failed",
|
|
122
168
|
kind,
|
|
123
|
-
|
|
169
|
+
durationMs: performance.now() - startedAt,
|
|
170
|
+
failure: { kind, ...code ? { code } : {}, message },
|
|
171
|
+
error: message
|
|
124
172
|
});
|
|
125
|
-
await options.config.recordHealth(serviceKey,
|
|
173
|
+
await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
|
|
126
174
|
if (kind === "terminal") {
|
|
127
175
|
const refused = {
|
|
128
176
|
ok: false,
|
|
177
|
+
fallbackUsed: attempts.length > 1,
|
|
129
178
|
attempts,
|
|
130
179
|
error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
|
|
131
180
|
};
|
|
132
|
-
options.after?.(refused);
|
|
181
|
+
await options.after?.(refused);
|
|
133
182
|
return refused;
|
|
134
183
|
}
|
|
135
184
|
}
|
|
136
185
|
}
|
|
137
186
|
const failed = {
|
|
138
187
|
ok: false,
|
|
188
|
+
fallbackUsed: attempts.length > 1,
|
|
139
189
|
attempts,
|
|
140
|
-
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
|
|
190
|
+
error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
|
|
141
191
|
};
|
|
142
|
-
options.after?.(failed);
|
|
192
|
+
await options.after?.(failed);
|
|
143
193
|
return failed;
|
|
144
194
|
}
|
|
145
|
-
|
|
195
|
+
function service(definition) {
|
|
196
|
+
return {
|
|
197
|
+
call(method, args, callOptions) {
|
|
198
|
+
return call(definition.key, method, args, callOptions);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return { call, service };
|
|
146
203
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
204
|
+
var VERSION = "0.1.10";
|
|
148
205
|
|
|
149
206
|
// src/http.ts
|
|
150
207
|
class BudgetExhausted extends ProviderError {
|
|
@@ -227,9 +284,9 @@ function createHttpClient(config) {
|
|
|
227
284
|
budget: () => budgetState(config.budget.host)
|
|
228
285
|
};
|
|
229
286
|
}
|
|
230
|
-
var http =
|
|
287
|
+
var http = defineSingleMethodProvider({
|
|
231
288
|
id: "http",
|
|
232
|
-
|
|
289
|
+
method: "request",
|
|
233
290
|
label: "HTTP",
|
|
234
291
|
multiInstance: true,
|
|
235
292
|
credentials: {
|
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").
|
|
137
|
-
export declare const storageProviders: readonly [import("./index").
|
|
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>>>];
|