@directive-run/knowledge 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/LICENSE +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- package/package.json +65 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Core Patterns
|
|
2
|
+
|
|
3
|
+
How to think about building with Directive: modules, systems, and the constraint-resolver pattern.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "Where does this logic go?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
User wants to...
|
|
9
|
+
├── Store state → schema.facts + init()
|
|
10
|
+
├── Compute derived values → schema.derivations + derive
|
|
11
|
+
├── React to state changes → effects
|
|
12
|
+
├── Trigger side effects when conditions are met → constraints + resolvers
|
|
13
|
+
├── Handle user actions → schema.events + events handlers
|
|
14
|
+
└── Coordinate multiple modules → createSystem({ modules: {} })
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Module Shape (Canonical Object Syntax)
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// CORRECT — full module definition
|
|
21
|
+
import { createModule, t } from "@directive-run/core";
|
|
22
|
+
|
|
23
|
+
const myModule = createModule("name", {
|
|
24
|
+
schema: {
|
|
25
|
+
facts: {
|
|
26
|
+
count: t.number(),
|
|
27
|
+
phase: t.string<"idle" | "loading" | "done">(),
|
|
28
|
+
user: t.object<{ id: string; name: string } | null>(),
|
|
29
|
+
},
|
|
30
|
+
derivations: {
|
|
31
|
+
isLoading: t.boolean(),
|
|
32
|
+
displayName: t.string(),
|
|
33
|
+
},
|
|
34
|
+
events: {
|
|
35
|
+
increment: {},
|
|
36
|
+
setUser: { user: t.object<{ id: string; name: string }>() },
|
|
37
|
+
},
|
|
38
|
+
requirements: {
|
|
39
|
+
FETCH_USER: { userId: t.string() },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
init: (facts) => {
|
|
44
|
+
facts.count = 0;
|
|
45
|
+
facts.phase = "idle";
|
|
46
|
+
facts.user = null;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
derive: {
|
|
50
|
+
isLoading: (facts) => facts.phase === "loading",
|
|
51
|
+
displayName: (facts) => {
|
|
52
|
+
if (!facts.user) {
|
|
53
|
+
return "Guest";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return facts.user.name;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
effects: {
|
|
61
|
+
logPhase: {
|
|
62
|
+
run: (facts, prev) => {
|
|
63
|
+
if (prev?.phase !== facts.phase) {
|
|
64
|
+
console.log(`Phase: ${facts.phase}`);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
constraints: {
|
|
71
|
+
fetchWhenReady: {
|
|
72
|
+
when: (facts) => facts.phase === "idle" && facts.count > 0,
|
|
73
|
+
require: (facts) => ({ type: "FETCH_USER", userId: "user-1" }),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
resolvers: {
|
|
78
|
+
fetchUser: {
|
|
79
|
+
requirement: "FETCH_USER",
|
|
80
|
+
resolve: async (req, context) => {
|
|
81
|
+
context.facts.phase = "loading";
|
|
82
|
+
const res = await fetch(`/api/users/${req.userId}`);
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
context.facts.user = data;
|
|
85
|
+
context.facts.phase = "done";
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
events: {
|
|
91
|
+
increment: (facts) => {
|
|
92
|
+
facts.count += 1;
|
|
93
|
+
},
|
|
94
|
+
setUser: (facts, payload) => {
|
|
95
|
+
facts.user = payload.user;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## System Creation
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { createSystem } from "@directive-run/core";
|
|
105
|
+
import { loggingPlugin, devtoolsPlugin } from "@directive-run/core/plugins";
|
|
106
|
+
|
|
107
|
+
// Single module — direct access: system.facts.count
|
|
108
|
+
const system = createSystem({
|
|
109
|
+
module: myModule,
|
|
110
|
+
plugins: [loggingPlugin(), devtoolsPlugin()],
|
|
111
|
+
debug: { timeTravel: true, maxSnapshots: 100 },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Multi-module — namespaced access: system.facts.auth.token
|
|
115
|
+
const system = createSystem({
|
|
116
|
+
modules: { auth: authModule, cart: cartModule },
|
|
117
|
+
plugins: [devtoolsPlugin()],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Lifecycle
|
|
121
|
+
system.start();
|
|
122
|
+
await system.settle(); // Wait for all resolvers to complete
|
|
123
|
+
// ... use the system ...
|
|
124
|
+
system.stop();
|
|
125
|
+
system.destroy();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Decision Tree: "User says 'fetch data when authenticated'"
|
|
129
|
+
|
|
130
|
+
WRONG thinking: "I'll put the fetch call in a resolver that checks auth."
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// WRONG — resolver doing condition checking + data fetching
|
|
134
|
+
resolvers: {
|
|
135
|
+
fetchData: {
|
|
136
|
+
requirement: "FETCH_DATA",
|
|
137
|
+
resolve: async (req, context) => {
|
|
138
|
+
if (!context.facts.isAuthenticated) {
|
|
139
|
+
return; // Resolver should not check conditions
|
|
140
|
+
}
|
|
141
|
+
const data = await fetch("/api/data");
|
|
142
|
+
context.facts.data = await data.json();
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
CORRECT thinking: "Constraint declares WHEN, resolver declares HOW."
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// CORRECT — constraint declares the need, resolver fulfills it
|
|
152
|
+
constraints: {
|
|
153
|
+
fetchWhenAuthenticated: {
|
|
154
|
+
when: (facts) => facts.isAuthenticated && !facts.data,
|
|
155
|
+
require: { type: "FETCH_DATA" },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
resolvers: {
|
|
160
|
+
fetchData: {
|
|
161
|
+
requirement: "FETCH_DATA",
|
|
162
|
+
resolve: async (req, context) => {
|
|
163
|
+
const data = await fetch("/api/data");
|
|
164
|
+
context.facts.data = await data.json();
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Reading System State
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Facts — mutable state
|
|
174
|
+
system.facts.count = 5;
|
|
175
|
+
const val = system.facts.count;
|
|
176
|
+
|
|
177
|
+
// Derivations — read-only computed values
|
|
178
|
+
const loading = system.derive.isLoading;
|
|
179
|
+
|
|
180
|
+
// Events — dispatch user actions
|
|
181
|
+
system.events.increment();
|
|
182
|
+
system.events.setUser({ user: { id: "1", name: "Alice" } });
|
|
183
|
+
|
|
184
|
+
// Subscribe to changes
|
|
185
|
+
const unsub = system.subscribe(["count", "isLoading"], () => {
|
|
186
|
+
console.log(system.facts.count, system.derive.isLoading);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Watch individual values
|
|
190
|
+
system.watch("count", (newVal, oldVal) => {
|
|
191
|
+
console.log(`Count: ${oldVal} -> ${newVal}`);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Wait for a condition
|
|
195
|
+
await system.when((facts) => facts.phase === "done");
|
|
196
|
+
await system.when((facts) => facts.phase === "done", { timeout: 5000 });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Schema Patterns
|
|
200
|
+
|
|
201
|
+
Only `facts` is required in the schema. Other sections are optional:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Minimal module — facts only
|
|
205
|
+
const minimal = createModule("minimal", {
|
|
206
|
+
schema: {
|
|
207
|
+
facts: { count: t.number() },
|
|
208
|
+
},
|
|
209
|
+
init: (facts) => {
|
|
210
|
+
facts.count = 0;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Type assertion pattern (alternative to t.* builders)
|
|
215
|
+
const typed = createModule("typed", {
|
|
216
|
+
schema: {
|
|
217
|
+
facts: {} as { count: number; name: string },
|
|
218
|
+
derivations: {} as { doubled: number },
|
|
219
|
+
},
|
|
220
|
+
init: (facts) => {
|
|
221
|
+
facts.count = 0;
|
|
222
|
+
facts.name = "";
|
|
223
|
+
},
|
|
224
|
+
derive: {
|
|
225
|
+
doubled: (facts) => facts.count * 2,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
```
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Error Boundaries
|
|
2
|
+
|
|
3
|
+
How to handle errors in Directive: recovery strategies, error boundaries, lifecycle hooks, and the circuit breaker pattern.
|
|
4
|
+
|
|
5
|
+
## Decision Tree: "How should errors be handled?"
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Where did the error occur?
|
|
9
|
+
├── Resolver (API call, async work)
|
|
10
|
+
│ ├── Transient failure (network, timeout) → retry / retry-later
|
|
11
|
+
│ ├── Permanent failure (404, auth) → skip or throw
|
|
12
|
+
│ └── Unknown → retry-later with maxRetries
|
|
13
|
+
│
|
|
14
|
+
├── Constraint (evaluation error)
|
|
15
|
+
│ ├── Bug in when() logic → throw (fix the code)
|
|
16
|
+
│ └── Unexpected data shape → skip (disable constraint)
|
|
17
|
+
│
|
|
18
|
+
├── Effect (side effect failed)
|
|
19
|
+
│ ├── Non-critical (logging, analytics) → skip
|
|
20
|
+
│ └── Critical (sync to external system) → retry-later
|
|
21
|
+
│
|
|
22
|
+
├── Derivation (computation error)
|
|
23
|
+
│ └── Usually a bug → throw (fix the code)
|
|
24
|
+
│
|
|
25
|
+
└── External service (repeated failures)
|
|
26
|
+
└── Circuit breaker pattern
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Recovery Strategies
|
|
30
|
+
|
|
31
|
+
Directive supports five recovery strategies:
|
|
32
|
+
|
|
33
|
+
| Strategy | Behavior |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `"skip"` | Swallow the error, continue processing |
|
|
36
|
+
| `"retry"` | Retry immediately (respects resolver retry policy) |
|
|
37
|
+
| `"retry-later"` | Retry after a delay with exponential backoff |
|
|
38
|
+
| `"disable"` | Disable the failing constraint/effect permanently |
|
|
39
|
+
| `"throw"` | Re-throw the error, halting the system |
|
|
40
|
+
|
|
41
|
+
## System-Level Error Boundary
|
|
42
|
+
|
|
43
|
+
Configure error handling for the entire system:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const system = createSystem({
|
|
47
|
+
module: myModule,
|
|
48
|
+
|
|
49
|
+
errorBoundary: {
|
|
50
|
+
// Per-subsystem strategies (string or function)
|
|
51
|
+
onConstraintError: "skip",
|
|
52
|
+
onResolverError: "retry-later",
|
|
53
|
+
onEffectError: "skip",
|
|
54
|
+
onDerivationError: "throw",
|
|
55
|
+
|
|
56
|
+
// Global error callback — fires for all errors
|
|
57
|
+
onError: (error) => {
|
|
58
|
+
// error is a DirectiveError with source tracking
|
|
59
|
+
console.error(`[${error.source}] ${error.sourceId}: ${error.message}`);
|
|
60
|
+
console.error("Recoverable:", error.recoverable);
|
|
61
|
+
console.error("Context:", error.context);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Configuration for retry-later strategy
|
|
65
|
+
retryLater: {
|
|
66
|
+
delayMs: 1000, // Initial delay (default: 1000)
|
|
67
|
+
maxRetries: 3, // Max retry attempts (default: 3)
|
|
68
|
+
backoffMultiplier: 2, // Multiply delay each retry (default: 2)
|
|
69
|
+
maxDelayMs: 30000, // Cap on delay growth (default: 30000)
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Dynamic Error Handling with Functions
|
|
76
|
+
|
|
77
|
+
Use functions instead of strings for conditional recovery:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
errorBoundary: {
|
|
81
|
+
onResolverError: (error, resolverId) => {
|
|
82
|
+
// Network errors — retry later
|
|
83
|
+
if (error.message.includes("NetworkError")) {
|
|
84
|
+
return "retry-later";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Auth errors — skip, don't retry
|
|
88
|
+
if (error.message.includes("401")) {
|
|
89
|
+
return "skip";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Everything else — throw
|
|
93
|
+
return "throw";
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
onConstraintError: (error, constraintId) => {
|
|
97
|
+
// Disable constraints that repeatedly fail
|
|
98
|
+
if (constraintId === "experimentalFeature") {
|
|
99
|
+
return "disable";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return "skip";
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
onEffectError: (error, effectId) => {
|
|
106
|
+
// Analytics can fail silently
|
|
107
|
+
if (effectId.startsWith("analytics")) {
|
|
108
|
+
return "skip";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return "throw";
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## DirectiveError
|
|
117
|
+
|
|
118
|
+
All errors passed to error boundary callbacks are `DirectiveError` instances with source tracking:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { DirectiveError } from "@directive-run/core";
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await system.settle();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err instanceof DirectiveError) {
|
|
127
|
+
err.source; // "constraint" | "resolver" | "effect" | "derivation" | "system"
|
|
128
|
+
err.sourceId; // e.g., "fetchUser" — the specific item that failed
|
|
129
|
+
err.recoverable; // boolean — whether recovery strategies apply
|
|
130
|
+
err.context; // arbitrary debug data (e.g., the requirement object)
|
|
131
|
+
err.message; // human-readable description
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Resolver-Level Error Handling
|
|
137
|
+
|
|
138
|
+
Resolvers have their own retry policy independent of the error boundary:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
resolvers: {
|
|
142
|
+
fetchData: {
|
|
143
|
+
requirement: "FETCH_DATA",
|
|
144
|
+
|
|
145
|
+
// Resolver-level retry policy
|
|
146
|
+
retry: {
|
|
147
|
+
attempts: 3,
|
|
148
|
+
backoff: "exponential",
|
|
149
|
+
initialDelay: 200,
|
|
150
|
+
maxDelay: 5000,
|
|
151
|
+
shouldRetry: (error, attempt) => {
|
|
152
|
+
// Only retry server errors, not client errors
|
|
153
|
+
if (error.message.includes("4")) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
resolve: async (req, context) => {
|
|
162
|
+
const res = await fetch("/api/data");
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
throw new Error(`HTTP ${res.status}`);
|
|
165
|
+
}
|
|
166
|
+
context.facts.data = await res.json();
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
When both resolver retry and error boundary are configured, the resolver retries first. If all retries fail, the error boundary strategy is applied.
|
|
173
|
+
|
|
174
|
+
## Lifecycle Hooks
|
|
175
|
+
|
|
176
|
+
Module-level hooks for lifecycle events:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const myModule = createModule("app", {
|
|
180
|
+
schema: { facts: { status: t.string() } },
|
|
181
|
+
|
|
182
|
+
hooks: {
|
|
183
|
+
onInit: (system) => {
|
|
184
|
+
console.log("Module initialized");
|
|
185
|
+
},
|
|
186
|
+
onStart: (system) => {
|
|
187
|
+
console.log("System started");
|
|
188
|
+
},
|
|
189
|
+
onStop: (system) => {
|
|
190
|
+
console.log("System stopped");
|
|
191
|
+
},
|
|
192
|
+
onError: (error, hookContext) => {
|
|
193
|
+
console.error("Module error:", error.message);
|
|
194
|
+
// error is a DirectiveError
|
|
195
|
+
// hookContext provides additional details
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
init: (facts) => {
|
|
200
|
+
facts.status = "ready";
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Circuit Breaker
|
|
206
|
+
|
|
207
|
+
For protecting against cascading failures from external services. The circuit breaker tracks failure rates and short-circuits requests when a threshold is exceeded.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { createCircuitBreaker } from "@directive-run/core/plugins";
|
|
211
|
+
|
|
212
|
+
const apiBreaker = createCircuitBreaker({
|
|
213
|
+
name: "external-api",
|
|
214
|
+
failureThreshold: 5, // Open after 5 failures (default: 5)
|
|
215
|
+
recoveryTimeMs: 30000, // Wait 30s before trying again (default: 30000)
|
|
216
|
+
halfOpenMaxRequests: 3, // Allow 3 trial requests in half-open (default: 3)
|
|
217
|
+
failureWindowMs: 60000, // Count failures within 60s window (default: 60000)
|
|
218
|
+
|
|
219
|
+
// Optional: classify which errors count as failures
|
|
220
|
+
isFailure: (error) => {
|
|
221
|
+
// Don't count 404s as circuit-breaking failures
|
|
222
|
+
if (error.message.includes("404")) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return true;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Optional: react to state changes
|
|
230
|
+
onStateChange: (from, to) => {
|
|
231
|
+
console.log(`Circuit: ${from} -> ${to}`);
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Using Circuit Breaker in Resolvers
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
resolvers: {
|
|
240
|
+
fetchData: {
|
|
241
|
+
requirement: "FETCH_DATA",
|
|
242
|
+
resolve: async (req, context) => {
|
|
243
|
+
const data = await apiBreaker.execute(async () => {
|
|
244
|
+
const res = await fetch("/api/data");
|
|
245
|
+
|
|
246
|
+
return res.json();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
context.facts.data = data;
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Circuit Breaker + Constraints
|
|
256
|
+
|
|
257
|
+
Wire the circuit breaker state into constraints for automatic fallback:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
constraints: {
|
|
261
|
+
apiDown: {
|
|
262
|
+
when: () => apiBreaker.getState() === "OPEN",
|
|
263
|
+
require: { type: "USE_FALLBACK" },
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
fetchNormally: {
|
|
267
|
+
when: (facts) => apiBreaker.getState() !== "OPEN" && !facts.data,
|
|
268
|
+
require: { type: "FETCH_DATA" },
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Circuit Breaker States
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
CLOSED → Normal operation, all requests pass through
|
|
277
|
+
↓ (failures >= threshold)
|
|
278
|
+
OPEN → All requests rejected immediately
|
|
279
|
+
↓ (after recoveryTimeMs)
|
|
280
|
+
HALF_OPEN → Limited trial requests allowed
|
|
281
|
+
↓ (trial succeeds) → CLOSED
|
|
282
|
+
↓ (trial fails) → OPEN
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Circuit Breaker API
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
apiBreaker.getState(); // "CLOSED" | "OPEN" | "HALF_OPEN"
|
|
289
|
+
apiBreaker.isAllowed(); // boolean — would a request be allowed?
|
|
290
|
+
apiBreaker.getStats(); // { totalRequests, totalFailures, recentFailures, ... }
|
|
291
|
+
apiBreaker.forceState("CLOSED"); // Force state (useful in tests)
|
|
292
|
+
apiBreaker.reset(); // Reset to CLOSED with cleared stats
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### CircuitBreakerOpenError
|
|
296
|
+
|
|
297
|
+
When the circuit is open, `execute()` throws a `CircuitBreakerOpenError`:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { CircuitBreakerOpenError } from "@directive-run/core/plugins";
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await apiBreaker.execute(() => fetch("/api"));
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error instanceof CircuitBreakerOpenError) {
|
|
306
|
+
error.code; // "CIRCUIT_OPEN"
|
|
307
|
+
error.retryAfterMs; // ms until circuit transitions to HALF_OPEN
|
|
308
|
+
error.state; // "OPEN" | "HALF_OPEN"
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Error Handling Checklist
|
|
314
|
+
|
|
315
|
+
1. Set system-level `errorBoundary` with strategies for each subsystem
|
|
316
|
+
2. Add `retry` policy on resolvers that call external services
|
|
317
|
+
3. Use `shouldRetry` to avoid retrying permanent failures (4xx errors)
|
|
318
|
+
4. Use circuit breaker for services with known reliability issues
|
|
319
|
+
5. Wire circuit breaker state into constraints for automatic fallback
|
|
320
|
+
6. Add `onError` callback for logging/monitoring
|
|
321
|
+
7. Use `"throw"` for derivation errors (they indicate bugs)
|
|
322
|
+
8. Use `"skip"` for non-critical effects (logging, analytics)
|