@git-stunts/alfred 0.2.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/src/compose.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @fileoverview Composition utilities for combining resilience policies.
3
+ *
4
+ * Provides functions to compose, fallback, and race policies, enabling
5
+ * complex resilience strategies through simple primitives.
6
+ *
7
+ * @module @git-stunts/alfred/compose
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} Policy
12
+ * @property {<T>(fn: () => Promise<T>) => Promise<T>} execute - Executes the function with the policy applied
13
+ */
14
+
15
+ /**
16
+ * Composes multiple policies into a single policy.
17
+ *
18
+ * Policies are applied from outermost to innermost. The first policy wraps
19
+ * the second, which wraps the third, and so on. Each policy's execute method
20
+ * receives a function that invokes the next policy in the chain.
21
+ *
22
+ * @param {...Policy} policies - Policies to compose (outer to inner order)
23
+ * @returns {Policy} - Combined policy
24
+ *
25
+ * @example
26
+ * const resilient = compose(
27
+ * { execute: (fn) => timeout(5000, fn) },
28
+ * { execute: (fn) => retry(fn, { retries: 3 }) }
29
+ * );
30
+ * // timeout wraps retry: timeout(5000, () => retry(fn, { retries: 3 }))
31
+ * await resilient.execute(() => fetch(url));
32
+ */
33
+ export function compose(...policies) {
34
+ if (policies.length === 0) {
35
+ return {
36
+ execute: (fn) => fn()
37
+ };
38
+ }
39
+
40
+ return {
41
+ /**
42
+ * @template T
43
+ * @param {() => Promise<T>} fn - Function to execute
44
+ * @returns {Promise<T>}
45
+ */
46
+ execute(fn) {
47
+ // Build the chain from innermost to outermost
48
+ // policies[0] is outermost, policies[n-1] is innermost
49
+ let chain = fn;
50
+
51
+ for (let i = policies.length - 1; i >= 0; i--) {
52
+ const policy = policies[i];
53
+ const next = chain;
54
+ chain = () => policy.execute(next);
55
+ }
56
+
57
+ return chain();
58
+ }
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Creates a fallback policy that tries a secondary policy if the primary fails.
64
+ *
65
+ * @param {Policy} primary - Primary policy to attempt first
66
+ * @param {Policy} secondary - Secondary policy to attempt on failure
67
+ * @returns {Policy} - Fallback policy
68
+ *
69
+ * @example
70
+ * const withFallback = fallback(
71
+ * { execute: (fn) => fastCache.get(key) ?? fn() },
72
+ * { execute: (fn) => slowDatabase.get(key) ?? fn() }
73
+ * );
74
+ * await withFallback.execute(() => computeExpensiveValue());
75
+ */
76
+ export function fallback(primary, secondary) {
77
+ return {
78
+ /**
79
+ * @template T
80
+ * @param {() => Promise<T>} fn - Function to execute
81
+ * @returns {Promise<T>}
82
+ */
83
+ async execute(fn) {
84
+ try {
85
+ return await primary.execute(fn);
86
+ } catch {
87
+ return await secondary.execute(fn);
88
+ }
89
+ }
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Creates a racing policy that runs two policies concurrently.
95
+ *
96
+ * Returns the result of whichever policy succeeds first.
97
+ * If both fail, throws the error from the first policy.
98
+ *
99
+ * @param {Policy} policyA - First policy to race
100
+ * @param {Policy} policyB - Second policy to race
101
+ * @returns {Policy} - Racing policy
102
+ *
103
+ * @example
104
+ * const fastest = race(
105
+ * { execute: (fn) => primaryServer.fetch(url) },
106
+ * { execute: (fn) => backupServer.fetch(url) }
107
+ * );
108
+ * await fastest.execute(() => defaultFetch(url));
109
+ */
110
+ export function race(policyA, policyB) {
111
+ return {
112
+ /**
113
+ * @template T
114
+ * @param {() => Promise<T>} fn - Function to execute
115
+ * @returns {Promise<T>}
116
+ */
117
+ execute(fn) {
118
+ return new Promise((resolve, reject) => {
119
+ let settled = false;
120
+ /** @type {Error | null} */
121
+ let firstError = null;
122
+ let failureCount = 0;
123
+
124
+ const handleSuccess = (result) => {
125
+ if (!settled) {
126
+ settled = true;
127
+ resolve(result);
128
+ }
129
+ };
130
+
131
+ const handleFailure = (error, isFirst) => {
132
+ if (settled) {
133
+ return;
134
+ }
135
+
136
+ if (isFirst) {
137
+ firstError = error;
138
+ }
139
+ failureCount++;
140
+
141
+ // If both have failed, reject with first error
142
+ if (failureCount === 2) {
143
+ settled = true;
144
+ reject(firstError);
145
+ }
146
+ };
147
+
148
+ policyA.execute(fn).then(handleSuccess, (e) => handleFailure(e, true));
149
+ policyB.execute(fn).then(handleSuccess, (e) => handleFailure(e, false));
150
+ });
151
+ }
152
+ };
153
+ }
package/src/errors.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Error thrown when all retry attempts are exhausted.
3
+ */
4
+ export class RetryExhaustedError extends Error {
5
+ /**
6
+ * @param {number} attempts - Total attempts made
7
+ * @param {Error} cause - The last error that caused the failure
8
+ */
9
+ constructor(attempts, cause) {
10
+ super(`Retry exhausted after ${attempts} attempts: ${cause.message}`);
11
+ this.name = 'RetryExhaustedError';
12
+ this.attempts = attempts;
13
+ this.cause = cause;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Error thrown when circuit breaker is open.
19
+ */
20
+ export class CircuitOpenError extends Error {
21
+ /**
22
+ * @param {Date} openedAt - When the circuit opened
23
+ * @param {number} failureCount - Number of failures that triggered opening
24
+ */
25
+ constructor(openedAt, failureCount) {
26
+ super(`Circuit breaker is open (since ${openedAt.toISOString()}, ${failureCount} failures)`);
27
+ this.name = 'CircuitOpenError';
28
+ this.openedAt = openedAt;
29
+ this.failureCount = failureCount;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Error thrown when an operation times out.
35
+ */
36
+ export class TimeoutError extends Error {
37
+ /**
38
+ * @param {number} timeout - Configured timeout in ms
39
+ * @param {number} elapsed - Actual elapsed time in ms
40
+ */
41
+ constructor(timeout, elapsed) {
42
+ super(`Operation timed out after ${elapsed}ms (limit: ${timeout}ms)`);
43
+ this.name = 'TimeoutError';
44
+ this.timeout = timeout;
45
+ this.elapsed = elapsed;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Error thrown when bulkhead queue is full.
51
+ */
52
+ export class BulkheadRejectedError extends Error {
53
+ /**
54
+ * @param {number} limit - Max concurrent executions
55
+ * @param {number} queueLimit - Max pending requests
56
+ */
57
+ constructor(limit, queueLimit) {
58
+ super(`Bulkhead rejected: queue full (limit: ${limit}, queue: ${queueLimit})`);
59
+ this.name = 'BulkheadRejectedError';
60
+ this.limit = limit;
61
+ this.queueLimit = queueLimit;
62
+ }
63
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,136 @@
1
+ export interface RetryOptions {
2
+ retries?: number;
3
+ delay?: number;
4
+ maxDelay?: number;
5
+ backoff?: 'constant' | 'linear' | 'exponential';
6
+ jitter?: 'none' | 'full' | 'equal' | 'decorrelated';
7
+ shouldRetry?: (error: Error) => boolean;
8
+ onRetry?: (error: Error, attempt: number, delay: number) => void;
9
+ telemetry?: TelemetrySink;
10
+ clock?: any;
11
+ }
12
+
13
+ export interface CircuitBreakerOptions {
14
+ threshold: number;
15
+ duration: number;
16
+ successThreshold?: number;
17
+ shouldTrip?: (error: Error) => boolean;
18
+ onOpen?: () => void;
19
+ onClose?: () => void;
20
+ onHalfOpen?: () => void;
21
+ telemetry?: TelemetrySink;
22
+ clock?: any;
23
+ }
24
+
25
+ export interface TimeoutOptions {
26
+ onTimeout?: (elapsed: number) => void;
27
+ telemetry?: TelemetrySink;
28
+ }
29
+
30
+ export interface BulkheadOptions {
31
+ limit: number;
32
+ queueLimit?: number;
33
+ telemetry?: TelemetrySink;
34
+ clock?: any;
35
+ }
36
+
37
+ export interface TelemetryEvent {
38
+ type: string;
39
+ timestamp: number;
40
+ [key: string]: any;
41
+ }
42
+
43
+ export interface TelemetrySink {
44
+ emit(event: TelemetryEvent): void;
45
+ }
46
+
47
+ export class InMemorySink implements TelemetrySink {
48
+ events: TelemetryEvent[];
49
+ emit(event: TelemetryEvent): void;
50
+ clear(): void;
51
+ }
52
+
53
+ export class ConsoleSink implements TelemetrySink {
54
+ emit(event: TelemetryEvent): void;
55
+ }
56
+
57
+ export class NoopSink implements TelemetrySink {
58
+ emit(event: TelemetryEvent): void;
59
+ }
60
+
61
+ export class MultiSink implements TelemetrySink {
62
+ constructor(sinks: TelemetrySink[]);
63
+ emit(event: TelemetryEvent): void;
64
+ }
65
+
66
+ export class RetryExhaustedError extends Error {
67
+ attempts: number;
68
+ cause: Error;
69
+ constructor(attempts: number, cause: Error);
70
+ }
71
+
72
+ export class CircuitOpenError extends Error {
73
+ openedAt: Date;
74
+ failureCount: number;
75
+ constructor(openedAt: Date, failureCount: number);
76
+ }
77
+
78
+ export class TimeoutError extends Error {
79
+ timeout: number;
80
+ elapsed: number;
81
+ constructor(timeout: number, elapsed: number);
82
+ }
83
+
84
+ export class BulkheadRejectedError extends Error {
85
+ limit: number;
86
+ queueLimit: number;
87
+ constructor(limit: number, queueLimit: number);
88
+ }
89
+
90
+ export function retry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
91
+
92
+ export interface CircuitBreaker {
93
+ execute<T>(fn: () => Promise<T>): Promise<T>;
94
+ readonly state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
95
+ }
96
+
97
+ export function circuitBreaker(options: CircuitBreakerOptions): CircuitBreaker;
98
+
99
+ export function timeout<T>(ms: number, fn: ((signal: AbortSignal) => Promise<T>) | (() => Promise<T>), options?: TimeoutOptions): Promise<T>;
100
+
101
+ export interface Bulkhead {
102
+ execute<T>(fn: () => Promise<T>): Promise<T>;
103
+ readonly stats: { active: number; pending: number; available: number };
104
+ }
105
+
106
+ export function bulkhead(options: BulkheadOptions): Bulkhead;
107
+
108
+ export function compose(...policies: any[]): { execute<T>(fn: () => Promise<T>): Promise<T> };
109
+ export function fallback(primary: any, secondary: any): { execute<T>(fn: () => Promise<T>): Promise<T> };
110
+ export function race(primary: any, secondary: any): { execute<T>(fn: () => Promise<T>): Promise<T> };
111
+
112
+ export class Policy {
113
+ constructor(executor: (fn: () => Promise<any>) => Promise<any>);
114
+ static retry(options?: RetryOptions): Policy;
115
+ static circuitBreaker(options: CircuitBreakerOptions): Policy;
116
+ static timeout(ms: number, options?: TimeoutOptions): Policy;
117
+ static bulkhead(options: BulkheadOptions): Policy;
118
+ static noop(): Policy;
119
+
120
+ wrap(otherPolicy: Policy): Policy;
121
+ or(otherPolicy: Policy): Policy;
122
+ race(otherPolicy: Policy): Policy;
123
+ execute<T>(fn: () => Promise<T>): Promise<T>;
124
+ }
125
+
126
+ export class SystemClock {
127
+ now(): number;
128
+ sleep(ms: number): Promise<void>;
129
+ }
130
+
131
+ export class TestClock {
132
+ now(): number;
133
+ sleep(ms: number): Promise<void>;
134
+ tick(ms?: number): Promise<void>;
135
+ advance(ms: number): Promise<void>;
136
+ }
package/src/index.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @fileoverview Main entry point for @git-stunts/alfred resilience library.
3
+ * Exports all public APIs for building resilient applications.
4
+ */
5
+
6
+ // @ts-self-types="./index.d.ts"
7
+
8
+ // Error types
9
+ export {
10
+ RetryExhaustedError,
11
+ CircuitOpenError,
12
+ TimeoutError,
13
+ BulkheadRejectedError
14
+ } from './errors.js';
15
+
16
+ // Resilience policies
17
+ export { retry } from './policies/retry.js';
18
+ export { circuitBreaker } from './policies/circuit-breaker.js';
19
+ export { timeout } from './policies/timeout.js';
20
+ export { bulkhead } from './policies/bulkhead.js';
21
+
22
+ // Composition utilities
23
+ export { compose, fallback, race } from './compose.js';
24
+
25
+ // Base policy class
26
+ export { Policy, Policy as default } from './policy.js';
27
+
28
+ // Clock utilities
29
+ export { SystemClock, TestClock } from './utils/clock.js';
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @fileoverview Bulkhead policy for concurrency limiting.
3
+ *
4
+ * Limits the number of concurrent executions of an operation,
5
+ * optionally queuing excess requests up to a limit.
6
+ *
7
+ * @module @git-stunts/alfred/policies/bulkhead
8
+ */
9
+
10
+ import { BulkheadRejectedError } from '../errors.js';
11
+ import { SystemClock } from '../utils/clock.js';
12
+ import { NoopSink } from '../telemetry.js';
13
+
14
+ /**
15
+ * @typedef {Object} BulkheadOptions
16
+ * @property {number} limit - Maximum concurrent executions
17
+ * @property {number} [queueLimit=0] - Maximum pending requests in queue
18
+ * @property {import('../telemetry.js').TelemetrySink} [telemetry] - Telemetry sink
19
+ * @property {{ now(): number }} [clock] - Clock for timestamps
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} BulkheadStats
24
+ * @property {number} active - Currently executing requests
25
+ * @property {number} pending - Requests waiting in queue
26
+ * @property {number} available - Remaining execution slots
27
+ */
28
+
29
+ class BulkheadPolicy {
30
+ constructor(options) {
31
+ const {
32
+ limit,
33
+ queueLimit = 0,
34
+ telemetry = new NoopSink(),
35
+ clock = new SystemClock()
36
+ } = options;
37
+
38
+ if (limit <= 0) {
39
+ throw new Error('Bulkhead limit must be greater than 0');
40
+ }
41
+
42
+ this.limit = limit;
43
+ this.queueLimit = queueLimit;
44
+ this.telemetry = telemetry;
45
+ this.clock = clock;
46
+
47
+ this.active = 0;
48
+ this.queue = [];
49
+ }
50
+
51
+ processQueue() {
52
+ if (this.active < this.limit && this.queue.length > 0) {
53
+ const { fn, resolve, reject } = this.queue.shift();
54
+ this.active++;
55
+
56
+ this.emitEvent('bulkhead.execute', {
57
+ active: this.active,
58
+ pending: this.queue.length
59
+ });
60
+
61
+ Promise.resolve()
62
+ .then(() => fn())
63
+ .then(resolve, reject)
64
+ .finally(() => {
65
+ this.active--;
66
+ this.emitEvent('bulkhead.complete', {
67
+ active: this.active,
68
+ pending: this.queue.length
69
+ });
70
+ this.processQueue();
71
+ });
72
+ }
73
+ }
74
+
75
+ emitEvent(type, data) {
76
+ this.telemetry.emit({
77
+ type,
78
+ timestamp: this.clock.now(),
79
+ ...data
80
+ });
81
+ }
82
+
83
+ async execute(fn) {
84
+ if (this.active < this.limit) {
85
+ this.active++;
86
+ this.emitEvent('bulkhead.execute', {
87
+ active: this.active,
88
+ pending: this.queue.length
89
+ });
90
+
91
+ try {
92
+ return await fn();
93
+ } finally {
94
+ this.active--;
95
+ this.emitEvent('bulkhead.complete', {
96
+ active: this.active,
97
+ pending: this.queue.length
98
+ });
99
+ this.processQueue();
100
+ }
101
+ }
102
+
103
+ if (this.queue.length < this.queueLimit) {
104
+ this.emitEvent('bulkhead.queued', {
105
+ active: this.active,
106
+ pending: this.queue.length + 1
107
+ });
108
+
109
+ return new Promise((resolve, reject) => {
110
+ this.queue.push({ fn, resolve, reject });
111
+ });
112
+ }
113
+
114
+ this.emitEvent('bulkhead.reject', {
115
+ active: this.active,
116
+ pending: this.queue.length
117
+ });
118
+ throw new BulkheadRejectedError(this.limit, this.queueLimit);
119
+ }
120
+
121
+ get stats() {
122
+ return {
123
+ active: this.active,
124
+ pending: this.queue.length,
125
+ available: Math.max(0, this.limit - this.active)
126
+ };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Creates a bulkhead policy.
132
+ *
133
+ * @param {BulkheadOptions} options - Bulkhead configuration
134
+ * @returns {{ execute: <T>(fn: () => Promise<T>) => Promise<T>, stats: BulkheadStats }}
135
+ */
136
+ export function bulkhead(options) {
137
+ const policy = new BulkheadPolicy(options);
138
+
139
+ return {
140
+ execute: (fn) => policy.execute(fn),
141
+ get stats() {
142
+ return policy.stats;
143
+ }
144
+ };
145
+ }
@@ -0,0 +1,189 @@
1
+ import { CircuitOpenError } from '../errors.js';
2
+ import { SystemClock } from '../utils/clock.js';
3
+ import { NoopSink } from '../telemetry.js';
4
+
5
+ /**
6
+ * Circuit breaker states.
7
+ * @readonly
8
+ * @enum {string}
9
+ */
10
+ const State = {
11
+ CLOSED: 'CLOSED',
12
+ OPEN: 'OPEN',
13
+ HALF_OPEN: 'HALF_OPEN'
14
+ };
15
+
16
+ /**
17
+ * @typedef {Object} CircuitBreakerOptions
18
+ * @property {number} threshold - Number of failures before opening circuit (required)
19
+ * @property {number} duration - Milliseconds to stay open before transitioning to half-open (required)
20
+ * @property {number} [successThreshold=1] - Consecutive successes in half-open to close circuit
21
+ * @property {(error: Error) => boolean} [shouldTrip] - Predicate to determine if error should count as failure
22
+ * @property {() => void} [onOpen] - Callback when circuit opens
23
+ * @property {() => void} [onClose] - Callback when circuit closes
24
+ * @property {() => void} [onHalfOpen] - Callback when circuit transitions to half-open
25
+ * @property {{ now(): number }} [clock] - Clock implementation for testing
26
+ * @property {import('../telemetry.js').TelemetrySink} [telemetry] - Telemetry sink
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} CircuitBreaker
31
+ * @property {<T>(fn: () => Promise<T>) => Promise<T>} execute - Executes function with circuit breaker protection
32
+ * @property {string} state - Current circuit state (CLOSED, OPEN, HALF_OPEN)
33
+ */
34
+
35
+ class CircuitBreakerPolicy {
36
+ constructor(options) {
37
+ const {
38
+ threshold,
39
+ duration,
40
+ successThreshold = 1,
41
+ shouldTrip = () => true,
42
+ onOpen,
43
+ onClose,
44
+ onHalfOpen,
45
+ clock = new SystemClock(),
46
+ telemetry = new NoopSink()
47
+ } = options;
48
+
49
+ if (threshold === undefined || threshold === null) {
50
+ throw new Error('threshold is required');
51
+ }
52
+ if (duration === undefined || duration === null) {
53
+ throw new Error('duration is required');
54
+ }
55
+
56
+ this.options = {
57
+ threshold,
58
+ duration,
59
+ successThreshold,
60
+ shouldTrip,
61
+ onOpen,
62
+ onClose,
63
+ onHalfOpen,
64
+ clock,
65
+ telemetry
66
+ };
67
+
68
+ this._state = State.CLOSED;
69
+ this.failureCount = 0;
70
+ this.successCount = 0;
71
+ this.openedAt = null;
72
+ }
73
+
74
+ get state() {
75
+ return this._state;
76
+ }
77
+
78
+ emitEvent(type, data) {
79
+ this.options.telemetry.emit({
80
+ type,
81
+ timestamp: this.options.clock.now(),
82
+ ...data
83
+ });
84
+ }
85
+
86
+ open() {
87
+ this._state = State.OPEN;
88
+ this.openedAt = new Date(this.options.clock.now());
89
+ this.options.onOpen?.();
90
+ this.emitEvent('circuit.open', { failureCount: this.failureCount });
91
+ }
92
+
93
+ close() {
94
+ this._state = State.CLOSED;
95
+ this.failureCount = 0;
96
+ this.successCount = 0;
97
+ this.openedAt = null;
98
+ this.options.onClose?.();
99
+ this.emitEvent('circuit.close');
100
+ }
101
+
102
+ halfOpen() {
103
+ this._state = State.HALF_OPEN;
104
+ this.successCount = 0;
105
+ this.options.onHalfOpen?.();
106
+ this.emitEvent('circuit.half-open');
107
+ }
108
+
109
+ shouldAttemptReset() {
110
+ if (this._state !== State.OPEN || !this.openedAt) {
111
+ return false;
112
+ }
113
+ const elapsed = this.options.clock.now() - this.openedAt.getTime();
114
+ return elapsed >= this.options.duration;
115
+ }
116
+
117
+ recordSuccess() {
118
+ this.emitEvent('circuit.success', { state: this._state });
119
+
120
+ if (this._state === State.HALF_OPEN) {
121
+ this.successCount++;
122
+ if (this.successCount >= this.options.successThreshold) {
123
+ this.close();
124
+ }
125
+ } else if (this._state === State.CLOSED) {
126
+ this.failureCount = 0;
127
+ }
128
+ }
129
+
130
+ recordFailure(error) {
131
+ if (!this.options.shouldTrip(error)) {
132
+ return;
133
+ }
134
+
135
+ this.emitEvent('circuit.failure', {
136
+ error,
137
+ state: this._state
138
+ });
139
+
140
+ if (this._state === State.HALF_OPEN) {
141
+ this.open();
142
+ } else if (this._state === State.CLOSED) {
143
+ this.failureCount++;
144
+ if (this.failureCount >= this.options.threshold) {
145
+ this.open();
146
+ }
147
+ }
148
+ }
149
+
150
+ async execute(fn) {
151
+ if (this.shouldAttemptReset()) {
152
+ this.halfOpen();
153
+ }
154
+
155
+ if (this._state === State.OPEN) {
156
+ this.emitEvent('circuit.reject', {
157
+ openedAt: this.openedAt,
158
+ failureCount: this.failureCount
159
+ });
160
+ throw new CircuitOpenError(this.openedAt, this.failureCount);
161
+ }
162
+
163
+ try {
164
+ const result = await fn();
165
+ this.recordSuccess();
166
+ return result;
167
+ } catch (error) {
168
+ this.recordFailure(error);
169
+ throw error;
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Creates a circuit breaker that prevents cascading failures by failing fast
176
+ * when a dependency is unhealthy.
177
+ *
178
+ * @param {CircuitBreakerOptions} options - Configuration options
179
+ * @returns {CircuitBreaker} Circuit breaker instance
180
+ */
181
+ export function circuitBreaker(options) {
182
+ const policy = new CircuitBreakerPolicy(options);
183
+ return {
184
+ execute: (fn) => policy.execute(fn),
185
+ get state() {
186
+ return policy.state;
187
+ }
188
+ };
189
+ }