@gkoos/caracal 0.1.0 → 0.1.1
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/CHANGELOG.md +16 -5
- package/README.md +10 -0
- package/dist/{circuit-breaker-BSkcV0W_.d.ts → circuit-breaker-DHEz0YLV.d.ts} +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +51 -36
- package/dist/index.js.map +1 -1
- package/dist/redis.d.ts +1 -1
- package/dist/redis.js +11 -0
- package/dist/redis.js.map +1 -1
- package/package.json +9 -5
- package/src/coordination/redis/scripts.ts +20 -3
- package/src/core/bulkhead.ts +4 -1
- package/src/core/circuit-breaker.ts +81 -36
- package/test/harness/adapter-contract.ts +170 -0
- package/test/harness/index.ts +15 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { operation } from "../../src/core/operation.js"
|
|
2
|
+
import type {
|
|
3
|
+
Adapter,
|
|
4
|
+
Classification,
|
|
5
|
+
OperationCapabilities,
|
|
6
|
+
OperationEvent,
|
|
7
|
+
Outcome,
|
|
8
|
+
} from "../../src/core/types.js"
|
|
9
|
+
|
|
10
|
+
export interface AdapterContractSuccess<Args, Result> {
|
|
11
|
+
readonly args: Args
|
|
12
|
+
readonly assertResult?: (result: Result) => void | Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AdapterContractCapabilityCase<Args> {
|
|
16
|
+
readonly args: Args
|
|
17
|
+
readonly expected: OperationCapabilities
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AdapterContractClassificationCase<Result> {
|
|
21
|
+
readonly outcome: Outcome<Result>
|
|
22
|
+
readonly expected: Classification
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AdapterContractAbortCase<Args> {
|
|
26
|
+
readonly args: Args
|
|
27
|
+
readonly verify: (controls: {
|
|
28
|
+
readonly controller: AbortController
|
|
29
|
+
readonly execute: () => Promise<unknown>
|
|
30
|
+
}) => void | Promise<void>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AdapterContractOptions<Args, Result> {
|
|
34
|
+
readonly name: string
|
|
35
|
+
readonly adapter: Adapter<Args, Result>
|
|
36
|
+
readonly success: AdapterContractSuccess<Args, Result>
|
|
37
|
+
readonly capabilities: readonly AdapterContractCapabilityCase<Args>[]
|
|
38
|
+
readonly classifications?: readonly AdapterContractClassificationCase<Result>[]
|
|
39
|
+
readonly abort?: AdapterContractAbortCase<Args>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AdapterContractCheck {
|
|
43
|
+
readonly name: string
|
|
44
|
+
run(): Promise<void>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AdapterContractSuite {
|
|
48
|
+
readonly name: string
|
|
49
|
+
readonly checks: readonly AdapterContractCheck[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertEqual<T>(actual: T, expected: T, message: string): void {
|
|
53
|
+
if (!Object.is(actual, expected)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${message}: expected ${String(expected)}, received ${String(actual)}`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertLifecycle(events: readonly OperationEvent[]): void {
|
|
61
|
+
const types = events.map((event) => event.type)
|
|
62
|
+
const expected = [
|
|
63
|
+
"execution.started",
|
|
64
|
+
"attempt.started",
|
|
65
|
+
"attempt.settled",
|
|
66
|
+
"execution.settled",
|
|
67
|
+
]
|
|
68
|
+
if (
|
|
69
|
+
types.length !== expected.length ||
|
|
70
|
+
types.some((type, index) => type !== expected[index])
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`expected operation lifecycle ${expected.join(" -> ")}; received ${types.join(" -> ")}`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns runner-agnostic checks for a third-party adapter. Register each
|
|
80
|
+
* check with the application's test runner; this module imports no test runner.
|
|
81
|
+
*/
|
|
82
|
+
export function defineAdapterContractSuite<Args, Result>(
|
|
83
|
+
options: AdapterContractOptions<Args, Result>,
|
|
84
|
+
): AdapterContractSuite {
|
|
85
|
+
const checks: AdapterContractCheck[] = options.capabilities.map(
|
|
86
|
+
(capabilityCase, index) => ({
|
|
87
|
+
name: `${options.name}: capabilities ${index + 1}`,
|
|
88
|
+
async run(): Promise<void> {
|
|
89
|
+
const actual = options.adapter.capabilities(capabilityCase.args)
|
|
90
|
+
assertEqual(
|
|
91
|
+
actual.abort,
|
|
92
|
+
capabilityCase.expected.abort,
|
|
93
|
+
"abort capability",
|
|
94
|
+
)
|
|
95
|
+
assertEqual(
|
|
96
|
+
actual.replay,
|
|
97
|
+
capabilityCase.expected.replay,
|
|
98
|
+
"replay capability",
|
|
99
|
+
)
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
checks.push({
|
|
105
|
+
name: `${options.name}: successful operation lifecycle`,
|
|
106
|
+
async run(): Promise<void> {
|
|
107
|
+
const events: OperationEvent[] = []
|
|
108
|
+
const subject = operation({
|
|
109
|
+
name: `adapter-contract:${options.name}`,
|
|
110
|
+
adapter: options.adapter,
|
|
111
|
+
events: { emit: (event) => events.push(event) },
|
|
112
|
+
})
|
|
113
|
+
const result = await subject.execute(options.success.args, {
|
|
114
|
+
executionId: "adapter-contract",
|
|
115
|
+
})
|
|
116
|
+
await options.success.assertResult?.(result)
|
|
117
|
+
assertLifecycle(events)
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
for (const [index, classificationCase] of (
|
|
122
|
+
options.classifications ?? []
|
|
123
|
+
).entries()) {
|
|
124
|
+
checks.push({
|
|
125
|
+
name: `${options.name}: classification ${index + 1}`,
|
|
126
|
+
async run(): Promise<void> {
|
|
127
|
+
const actual =
|
|
128
|
+
options.adapter.classify?.(classificationCase.outcome) ??
|
|
129
|
+
(classificationCase.outcome.status === "success"
|
|
130
|
+
? "success"
|
|
131
|
+
: "failure")
|
|
132
|
+
assertEqual(
|
|
133
|
+
actual,
|
|
134
|
+
classificationCase.expected,
|
|
135
|
+
"outcome classification",
|
|
136
|
+
)
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.abort !== undefined) {
|
|
142
|
+
checks.push({
|
|
143
|
+
name: `${options.name}: abort behavior`,
|
|
144
|
+
async run(): Promise<void> {
|
|
145
|
+
const controller = new AbortController()
|
|
146
|
+
const subject = operation({
|
|
147
|
+
name: `adapter-contract:${options.name}`,
|
|
148
|
+
adapter: options.adapter,
|
|
149
|
+
})
|
|
150
|
+
await options.abort?.verify({
|
|
151
|
+
controller,
|
|
152
|
+
execute: () =>
|
|
153
|
+
subject.execute(options.abort?.args as Args, {
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
}),
|
|
156
|
+
})
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return Object.freeze({ name: options.name, checks: Object.freeze(checks) })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function runAdapterContractSuite(
|
|
165
|
+
suite: AdapterContractSuite,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
for (const check of suite.checks) {
|
|
168
|
+
await check.run()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Runner-agnostic adapter contract checks; source remains outside src/. */
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
AdapterContractAbortCase,
|
|
5
|
+
AdapterContractCapabilityCase,
|
|
6
|
+
AdapterContractCheck,
|
|
7
|
+
AdapterContractClassificationCase,
|
|
8
|
+
AdapterContractOptions,
|
|
9
|
+
AdapterContractSuccess,
|
|
10
|
+
AdapterContractSuite,
|
|
11
|
+
} from "./adapter-contract.js"
|
|
12
|
+
export {
|
|
13
|
+
defineAdapterContractSuite,
|
|
14
|
+
runAdapterContractSuite,
|
|
15
|
+
} from "./adapter-contract.js"
|