@zudojs/adapters 1.0.0 → 1.1.0
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 +23 -1
- package/dist/adapter/adapter.health.d.ts +31 -0
- package/dist/adapter/adapter.health.js +83 -0
- package/dist/adapter/adapter.registry.d.ts +26 -0
- package/dist/adapter/adapter.registry.js +52 -4
- package/dist/adapter/index.d.ts +2 -0
- package/dist/adapter/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/testing/adapterTesting.helper.d.ts +7 -3
- package/dist/testing/adapterTesting.helper.js +6 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Boundary layer between Zudojs and external platforms with adapter contracts, registry, capabilities, and transport abstractions.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-adapters](https://zudojs.oyinlola.site/docs/packages-adapters) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-adapters.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -88,7 +94,23 @@ const health = createDegradedHealth("replica lag above threshold");
|
|
|
88
94
|
```
|
|
89
95
|
|
|
90
96
|
A `LifecycleAdapter` adds optional `configure(options)` and `health()` to the
|
|
91
|
-
base contract.
|
|
97
|
+
base contract. The registry calls them:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { AdapterRegistry, createHealthyHealth, createMockAdapter } from "@zudojs/adapters";
|
|
101
|
+
|
|
102
|
+
const registry = new AdapterRegistry();
|
|
103
|
+
registry.register(
|
|
104
|
+
createMockAdapter({ name: "db", health: () => createHealthyHealth(), configure: () => {} }),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await registry.configure("db", { poolSize: 10 }); // throws if the adapter has no configure()
|
|
108
|
+
const report = await registry.healthAll({ timeout: 2_000 });
|
|
109
|
+
report.status; // worst of the per-adapter statuses: "healthy" | "degraded" | "unhealthy"
|
|
110
|
+
report.adapters.db; // AdapterHealth; throwing, timed-out or aborted checks are "unhealthy"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Adapters without `health()` are left out of `report.adapters`.
|
|
92
114
|
|
|
93
115
|
## Transport contracts
|
|
94
116
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/adapters/adapter
|
|
3
|
+
*
|
|
4
|
+
* Health aggregation and configuration for registered adapters.
|
|
5
|
+
*
|
|
6
|
+
* `LifecycleAdapter.health()` and `configure()` were part of the contract
|
|
7
|
+
* with nothing in the package that called them. `AdapterRegistry.healthAll()`
|
|
8
|
+
* and `AdapterRegistry.configure()` delegate here.
|
|
9
|
+
*/
|
|
10
|
+
import type { Adapter } from "./adapter.type.js";
|
|
11
|
+
import type { AdapterHealth, AdapterHealthStatus, AdapterOperationOptions } from "../lifecycle/lifecycle.type.js";
|
|
12
|
+
/** Aggregated health of every adapter that implements `health()`. */
|
|
13
|
+
export interface AdapterHealthReport {
|
|
14
|
+
/** The worst status across `adapters`; `"healthy"` when none report. */
|
|
15
|
+
readonly status: AdapterHealthStatus;
|
|
16
|
+
/** Per-adapter health, keyed by normalized adapter name. */
|
|
17
|
+
readonly adapters: Readonly<Record<string, AdapterHealth>>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Collects health from every adapter that implements `health()`. A check that
|
|
21
|
+
* throws, rejects, times out or is aborted is reported as `"unhealthy"`
|
|
22
|
+
* instead of failing the whole report.
|
|
23
|
+
*/
|
|
24
|
+
export declare function collectAdapterHealth(entries: ReadonlyArray<readonly [string, Adapter]>, options?: AdapterOperationOptions): Promise<AdapterHealthReport>;
|
|
25
|
+
/**
|
|
26
|
+
* Passes options to an adapter's `configure()` hook.
|
|
27
|
+
*
|
|
28
|
+
* @throws {AdapterConfigurationError} If the adapter has no `configure()` hook.
|
|
29
|
+
*/
|
|
30
|
+
export declare function configureAdapter(name: string, adapter: Adapter, options: unknown): Promise<void>;
|
|
31
|
+
//# sourceMappingURL=adapter.health.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/adapters/adapter
|
|
3
|
+
*
|
|
4
|
+
* Health aggregation and configuration for registered adapters.
|
|
5
|
+
*
|
|
6
|
+
* `LifecycleAdapter.health()` and `configure()` were part of the contract
|
|
7
|
+
* with nothing in the package that called them. `AdapterRegistry.healthAll()`
|
|
8
|
+
* and `AdapterRegistry.configure()` delegate here.
|
|
9
|
+
*/
|
|
10
|
+
import { AdapterConfigurationError } from "@zudojs/errors";
|
|
11
|
+
const RANK = {
|
|
12
|
+
healthy: 0,
|
|
13
|
+
degraded: 1,
|
|
14
|
+
unhealthy: 2,
|
|
15
|
+
};
|
|
16
|
+
function unhealthy(message) {
|
|
17
|
+
return { status: "unhealthy", message, timestamp: Date.now() };
|
|
18
|
+
}
|
|
19
|
+
/** Runs one health check, bounded by `timeout` and `signal`. */
|
|
20
|
+
async function checkOne(adapter, options) {
|
|
21
|
+
if (options.signal?.aborted)
|
|
22
|
+
return unhealthy("Health check aborted.");
|
|
23
|
+
let timer;
|
|
24
|
+
let onAbort;
|
|
25
|
+
const guards = [];
|
|
26
|
+
if (options.timeout !== undefined) {
|
|
27
|
+
guards.push(new Promise((resolve) => {
|
|
28
|
+
timer = setTimeout(() => resolve(unhealthy(`Health check timed out after ${options.timeout} ms.`)), options.timeout);
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
if (options.signal) {
|
|
32
|
+
const signal = options.signal;
|
|
33
|
+
guards.push(new Promise((resolve) => {
|
|
34
|
+
onAbort = () => resolve(unhealthy("Health check aborted."));
|
|
35
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const check = Promise.resolve()
|
|
40
|
+
.then(() => adapter.health?.())
|
|
41
|
+
.catch((error) => unhealthy(error instanceof Error ? error.message : String(error)));
|
|
42
|
+
return await Promise.race([check, ...guards]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (timer !== undefined)
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
if (onAbort)
|
|
48
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Collects health from every adapter that implements `health()`. A check that
|
|
53
|
+
* throws, rejects, times out or is aborted is reported as `"unhealthy"`
|
|
54
|
+
* instead of failing the whole report.
|
|
55
|
+
*/
|
|
56
|
+
export async function collectAdapterHealth(entries, options = {}) {
|
|
57
|
+
const adapters = {};
|
|
58
|
+
await Promise.all(entries
|
|
59
|
+
.filter(([, adapter]) => typeof adapter.health === "function")
|
|
60
|
+
.map(async ([name, adapter]) => {
|
|
61
|
+
adapters[name] = await checkOne(adapter, options);
|
|
62
|
+
}));
|
|
63
|
+
let status = "healthy";
|
|
64
|
+
for (const health of Object.values(adapters)) {
|
|
65
|
+
const rank = RANK[health.status] ?? RANK.unhealthy;
|
|
66
|
+
if (rank > RANK[status])
|
|
67
|
+
status = rank === RANK.unhealthy ? "unhealthy" : health.status;
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({ status, adapters: Object.freeze(adapters) });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Passes options to an adapter's `configure()` hook.
|
|
73
|
+
*
|
|
74
|
+
* @throws {AdapterConfigurationError} If the adapter has no `configure()` hook.
|
|
75
|
+
*/
|
|
76
|
+
export async function configureAdapter(name, adapter, options) {
|
|
77
|
+
const configurable = adapter;
|
|
78
|
+
if (typeof configurable.configure !== "function") {
|
|
79
|
+
throw new AdapterConfigurationError(name, new Error(`Adapter "${name}" does not implement configure().`));
|
|
80
|
+
}
|
|
81
|
+
await configurable.configure(options);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=adapter.health.js.map
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { Adapter } from "./adapter.type.js";
|
|
15
15
|
import type { AdapterCapabilities } from "../capabilities/capabilities.type.js";
|
|
16
|
+
import type { AdapterHealthReport } from "./adapter.health.js";
|
|
17
|
+
import type { AdapterOperationOptions } from "../lifecycle/lifecycle.type.js";
|
|
16
18
|
/** A capability an adapter can declare. */
|
|
17
19
|
export type AdapterCapabilityName = keyof AdapterCapabilities;
|
|
18
20
|
/**
|
|
@@ -113,6 +115,19 @@ export declare class AdapterRegistry {
|
|
|
113
115
|
* @throws {AggregateError} After attempting all adapters, if any failed.
|
|
114
116
|
*/
|
|
115
117
|
stopAll(): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Checks the health of every adapter that implements `health()`. Failures,
|
|
120
|
+
* timeouts (`options.timeout`) and aborts (`options.signal`) are reported
|
|
121
|
+
* as `"unhealthy"` entries rather than thrown.
|
|
122
|
+
*/
|
|
123
|
+
healthAll(options?: AdapterOperationOptions): Promise<AdapterHealthReport>;
|
|
124
|
+
/**
|
|
125
|
+
* Passes options to one adapter's `configure()` hook.
|
|
126
|
+
*
|
|
127
|
+
* @throws {AdapterNotFoundError} If no adapter with the name is registered.
|
|
128
|
+
* @throws {AdapterConfigurationError} If the adapter has no `configure()`.
|
|
129
|
+
*/
|
|
130
|
+
configure(name: string, options: unknown): Promise<void>;
|
|
116
131
|
/**
|
|
117
132
|
* Returns the number of registered adapters.
|
|
118
133
|
*/
|
|
@@ -130,6 +145,17 @@ export declare class AdapterRegistry {
|
|
|
130
145
|
* @throws {AggregateError} After attempting all adapters, if any failed.
|
|
131
146
|
*/
|
|
132
147
|
disposeAll(): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* Stops, then disposes, one adapter.
|
|
150
|
+
*
|
|
151
|
+
* `dispose()` runs even when `stop()` throws: the adapter has already
|
|
152
|
+
* left the registry by the time this is called, so skipping disposal
|
|
153
|
+
* would orphan its connections and timers with nothing left holding a
|
|
154
|
+
* reference to release them. A `stop()` failure is still reported — on
|
|
155
|
+
* its own when disposal succeeds, alongside the disposal error when
|
|
156
|
+
* both fail.
|
|
157
|
+
*/
|
|
158
|
+
private teardown;
|
|
133
159
|
/**
|
|
134
160
|
* Runs an operation against every adapter, collecting failures rather than
|
|
135
161
|
* stopping at the first one — a half-applied lifecycle transition leaves
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `removeAndDispose()` / `disposeAll()` to also release adapter resources.
|
|
13
13
|
*/
|
|
14
14
|
import { AdapterAlreadyRegisteredError, AdapterCapabilityMissingError, AdapterConfigurationError, AdapterNotFoundError, } from "@zudojs/errors";
|
|
15
|
+
import { collectAdapterHealth, configureAdapter } from "./adapter.health.js";
|
|
15
16
|
/**
|
|
16
17
|
* Registry for Zudojs adapters.
|
|
17
18
|
*
|
|
@@ -85,8 +86,7 @@ export class AdapterRegistry {
|
|
|
85
86
|
return false;
|
|
86
87
|
}
|
|
87
88
|
this.adapters.delete(key);
|
|
88
|
-
await
|
|
89
|
-
await adapter.dispose?.();
|
|
89
|
+
await this.teardown(adapter);
|
|
90
90
|
return true;
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
@@ -161,6 +161,23 @@ export class AdapterRegistry {
|
|
|
161
161
|
async stopAll() {
|
|
162
162
|
await this.forEachAdapter((adapter) => adapter.stop?.(), "One or more adapters failed to stop.");
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Checks the health of every adapter that implements `health()`. Failures,
|
|
166
|
+
* timeouts (`options.timeout`) and aborts (`options.signal`) are reported
|
|
167
|
+
* as `"unhealthy"` entries rather than thrown.
|
|
168
|
+
*/
|
|
169
|
+
async healthAll(options) {
|
|
170
|
+
return collectAdapterHealth([...this.adapters.entries()], options);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Passes options to one adapter's `configure()` hook.
|
|
174
|
+
*
|
|
175
|
+
* @throws {AdapterNotFoundError} If no adapter with the name is registered.
|
|
176
|
+
* @throws {AdapterConfigurationError} If the adapter has no `configure()`.
|
|
177
|
+
*/
|
|
178
|
+
async configure(name, options) {
|
|
179
|
+
await configureAdapter(this.normalizeName(name), this.require(name), options);
|
|
180
|
+
}
|
|
164
181
|
/**
|
|
165
182
|
* Returns the number of registered adapters.
|
|
166
183
|
*/
|
|
@@ -187,8 +204,7 @@ export class AdapterRegistry {
|
|
|
187
204
|
const failures = [];
|
|
188
205
|
for (const adapter of adapters) {
|
|
189
206
|
try {
|
|
190
|
-
await
|
|
191
|
-
await adapter.dispose?.();
|
|
207
|
+
await this.teardown(adapter);
|
|
192
208
|
}
|
|
193
209
|
catch (error) {
|
|
194
210
|
failures.push(error);
|
|
@@ -198,6 +214,38 @@ export class AdapterRegistry {
|
|
|
198
214
|
throw new AggregateError(failures, "One or more adapters failed to dispose.");
|
|
199
215
|
}
|
|
200
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Stops, then disposes, one adapter.
|
|
219
|
+
*
|
|
220
|
+
* `dispose()` runs even when `stop()` throws: the adapter has already
|
|
221
|
+
* left the registry by the time this is called, so skipping disposal
|
|
222
|
+
* would orphan its connections and timers with nothing left holding a
|
|
223
|
+
* reference to release them. A `stop()` failure is still reported — on
|
|
224
|
+
* its own when disposal succeeds, alongside the disposal error when
|
|
225
|
+
* both fail.
|
|
226
|
+
*/
|
|
227
|
+
async teardown(adapter) {
|
|
228
|
+
let stopError;
|
|
229
|
+
let stopFailed = false;
|
|
230
|
+
try {
|
|
231
|
+
await adapter.stop?.();
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
stopFailed = true;
|
|
235
|
+
stopError = error;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
await adapter.dispose?.();
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (stopFailed) {
|
|
242
|
+
throw new AggregateError([stopError, error], `Adapter "${adapter.name}" failed to stop and to dispose.`);
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
if (stopFailed)
|
|
247
|
+
throw stopError;
|
|
248
|
+
}
|
|
201
249
|
/**
|
|
202
250
|
* Runs an operation against every adapter, collecting failures rather than
|
|
203
251
|
* stopping at the first one — a half-applied lifecycle transition leaves
|
package/dist/adapter/index.d.ts
CHANGED
|
@@ -6,4 +6,6 @@
|
|
|
6
6
|
export type { Adapter } from "./adapter.type.js";
|
|
7
7
|
export { AdapterRegistry } from "./adapter.registry.js";
|
|
8
8
|
export type { AdapterCapabilityName } from "./adapter.registry.js";
|
|
9
|
+
export { collectAdapterHealth, configureAdapter } from "./adapter.health.js";
|
|
10
|
+
export type { AdapterHealthReport } from "./adapter.health.js";
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/adapter/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
export type { Adapter } from "./adapter/index.js";
|
|
19
19
|
export { AdapterRegistry } from "./adapter/index.js";
|
|
20
20
|
export type { AdapterCapabilityName } from "./adapter/index.js";
|
|
21
|
+
export type { AdapterHealthReport } from "./adapter/index.js";
|
|
21
22
|
export type { AdapterCapabilities } from "./capabilities/index.js";
|
|
22
23
|
export type { AdapterMetadata } from "./metadata/index.js";
|
|
23
24
|
export type { AdapterHealthStatus, AdapterHealth, AdapterOperationOptions, LifecycleAdapter, } from "./lifecycle/index.js";
|
|
@@ -5,13 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Adapter } from "../adapter/adapter.type.js";
|
|
7
7
|
import { AdapterRegistry } from "../adapter/adapter.registry.js";
|
|
8
|
-
import type { AdapterHealth } from "../lifecycle/lifecycle.type.js";
|
|
8
|
+
import type { AdapterHealth, LifecycleAdapter } from "../lifecycle/lifecycle.type.js";
|
|
9
9
|
/**
|
|
10
10
|
* Creates a minimal mock adapter for testing.
|
|
11
|
+
*
|
|
12
|
+
* Accepts the `LifecycleAdapter` hooks too: `health` and `configure` are
|
|
13
|
+
* forwarded when given, so the result can be exercised with
|
|
14
|
+
* `AdapterRegistry.healthAll()` and `configure()`.
|
|
11
15
|
*/
|
|
12
|
-
export declare function createMockAdapter(overrides?: Partial<
|
|
16
|
+
export declare function createMockAdapter(overrides?: Partial<LifecycleAdapter> & {
|
|
13
17
|
name: string;
|
|
14
|
-
}):
|
|
18
|
+
}): LifecycleAdapter;
|
|
15
19
|
/**
|
|
16
20
|
* Creates a mock adapter registry pre-populated with adapters.
|
|
17
21
|
*/
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { AdapterRegistry } from "../adapter/adapter.registry.js";
|
|
7
7
|
/**
|
|
8
8
|
* Creates a minimal mock adapter for testing.
|
|
9
|
+
*
|
|
10
|
+
* Accepts the `LifecycleAdapter` hooks too: `health` and `configure` are
|
|
11
|
+
* forwarded when given, so the result can be exercised with
|
|
12
|
+
* `AdapterRegistry.healthAll()` and `configure()`.
|
|
9
13
|
*/
|
|
10
14
|
export function createMockAdapter(overrides = { name: "mock" }) {
|
|
11
15
|
const capabilities = {
|
|
@@ -31,6 +35,8 @@ export function createMockAdapter(overrides = { name: "mock" }) {
|
|
|
31
35
|
start: overrides.start,
|
|
32
36
|
stop: overrides.stop,
|
|
33
37
|
dispose: overrides.dispose,
|
|
38
|
+
...(overrides.health ? { health: overrides.health } : {}),
|
|
39
|
+
...(overrides.configure ? { configure: overrides.configure } : {}),
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
/**
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/adapters",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Boundary layer between Zudojs and external platforms — adapter contracts, registry, capabilities, and transport abstractions.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -23,10 +27,7 @@
|
|
|
23
27
|
"node": ">=24.0.0"
|
|
24
28
|
},
|
|
25
29
|
"dependencies": {
|
|
26
|
-
"@zudojs/errors": "1.
|
|
27
|
-
"@zudojs/constants": "1.0.0",
|
|
28
|
-
"@zudojs/types": "1.0.0",
|
|
29
|
-
"@zudojs/lifecycle": "1.0.0"
|
|
30
|
+
"@zudojs/errors": "1.1.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"typescript": "7.0.2",
|