@directive-run/knowledge 0.2.0 → 0.4.2
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 +3 -3
- package/ai/ai-adapters.md +7 -7
- package/ai/ai-agents-streaming.md +8 -8
- package/ai/ai-budget-resilience.md +5 -5
- package/ai/ai-communication.md +1 -1
- package/ai/ai-guardrails-memory.md +7 -7
- package/ai/ai-mcp-rag.md +5 -5
- package/ai/ai-multi-agent.md +14 -14
- package/ai/ai-orchestrator.md +8 -8
- package/ai/ai-security.md +2 -2
- package/ai/ai-tasks.md +9 -9
- package/ai/ai-testing-evals.md +2 -2
- package/core/anti-patterns.md +39 -39
- package/core/constraints.md +15 -15
- package/core/core-patterns.md +9 -9
- package/core/error-boundaries.md +7 -7
- package/core/multi-module.md +16 -16
- package/core/naming.md +21 -21
- package/core/plugins.md +14 -14
- package/core/react-adapter.md +13 -13
- package/core/resolvers.md +14 -14
- package/core/schema-types.md +22 -22
- package/core/system-api.md +16 -16
- package/core/testing.md +5 -5
- package/core/time-travel.md +20 -20
- package/dist/index.cjs +6 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -97
- package/dist/index.js.map +1 -1
- package/examples/ab-testing.ts +18 -90
- package/examples/ai-checkpoint.ts +68 -87
- package/examples/ai-guardrails.ts +20 -70
- package/examples/auth-flow.ts +2 -2
- package/examples/batch-resolver.ts +19 -59
- package/examples/contact-form.ts +220 -69
- package/examples/counter.ts +77 -95
- package/examples/dashboard-loader.ts +37 -55
- package/examples/debounce-constraints.ts +0 -2
- package/examples/dynamic-modules.ts +17 -20
- package/examples/error-boundaries.ts +30 -81
- package/examples/newsletter.ts +22 -49
- package/examples/notifications.ts +24 -23
- package/examples/optimistic-updates.ts +36 -41
- package/examples/pagination.ts +2 -2
- package/examples/permissions.ts +22 -32
- package/examples/provider-routing.ts +26 -83
- package/examples/shopping-cart.ts +8 -8
- package/examples/sudoku.ts +55 -62
- package/examples/theme-locale.ts +4 -7
- package/examples/time-machine.ts +12 -90
- package/examples/topic-guard.ts +30 -38
- package/examples/url-sync.ts +8 -8
- package/examples/websocket.ts +5 -5
- package/package.json +3 -3
|
@@ -70,7 +70,7 @@ export const dashboardLoaderSchema = {
|
|
|
70
70
|
preferencesFailRate: t.number(),
|
|
71
71
|
permissionsFailRate: t.number(),
|
|
72
72
|
loadRequested: t.boolean(),
|
|
73
|
-
eventLog: t.
|
|
73
|
+
eventLog: t.array<EventLogEntry>(),
|
|
74
74
|
},
|
|
75
75
|
derivations: {
|
|
76
76
|
loadedCount: t.number(),
|
|
@@ -106,7 +106,7 @@ function addLogEntry(
|
|
|
106
106
|
resource: string,
|
|
107
107
|
detail: string,
|
|
108
108
|
): void {
|
|
109
|
-
const log = [...
|
|
109
|
+
const log = [...facts.eventLog];
|
|
110
110
|
log.push({ timestamp: Date.now(), event, resource, detail });
|
|
111
111
|
facts.eventLog = log;
|
|
112
112
|
}
|
|
@@ -181,9 +181,9 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
181
181
|
},
|
|
182
182
|
|
|
183
183
|
combinedStatus: (facts, derive) => {
|
|
184
|
-
const loaded = derive.loadedCount
|
|
185
|
-
const anyErr = derive.anyError
|
|
186
|
-
const anyLoad = derive.anyLoading
|
|
184
|
+
const loaded = derive.loadedCount;
|
|
185
|
+
const anyErr = derive.anyError;
|
|
186
|
+
const anyLoad = derive.anyLoading;
|
|
187
187
|
const allIdle = [
|
|
188
188
|
facts.profile,
|
|
189
189
|
facts.preferences,
|
|
@@ -216,7 +216,7 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
216
216
|
},
|
|
217
217
|
|
|
218
218
|
canStart: (facts) => {
|
|
219
|
-
const id =
|
|
219
|
+
const id = facts.userId.trim();
|
|
220
220
|
const allIdle = [
|
|
221
221
|
facts.profile,
|
|
222
222
|
facts.preferences,
|
|
@@ -237,7 +237,7 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
237
237
|
},
|
|
238
238
|
|
|
239
239
|
start: (facts) => {
|
|
240
|
-
const id =
|
|
240
|
+
const id = facts.userId.trim();
|
|
241
241
|
if (id.length === 0) {
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
@@ -293,54 +293,42 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
293
293
|
needsProfile: {
|
|
294
294
|
priority: 100,
|
|
295
295
|
when: (facts) => {
|
|
296
|
-
const id =
|
|
297
|
-
const profile = facts.profile
|
|
296
|
+
const id = facts.userId.trim();
|
|
297
|
+
const profile = facts.profile;
|
|
298
298
|
|
|
299
|
-
return
|
|
300
|
-
(facts.loadRequested as boolean) &&
|
|
301
|
-
id !== "" &&
|
|
302
|
-
profile.status === "idle"
|
|
303
|
-
);
|
|
299
|
+
return facts.loadRequested && id !== "" && profile.status === "idle";
|
|
304
300
|
},
|
|
305
301
|
require: (facts) => ({
|
|
306
302
|
type: "FETCH_PROFILE",
|
|
307
|
-
userId:
|
|
303
|
+
userId: facts.userId.trim(),
|
|
308
304
|
}),
|
|
309
305
|
},
|
|
310
306
|
|
|
311
307
|
needsPreferences: {
|
|
312
308
|
priority: 90,
|
|
313
309
|
when: (facts) => {
|
|
314
|
-
const id =
|
|
315
|
-
const prefs = facts.preferences
|
|
310
|
+
const id = facts.userId.trim();
|
|
311
|
+
const prefs = facts.preferences;
|
|
316
312
|
|
|
317
|
-
return
|
|
318
|
-
(facts.loadRequested as boolean) &&
|
|
319
|
-
id !== "" &&
|
|
320
|
-
prefs.status === "idle"
|
|
321
|
-
);
|
|
313
|
+
return facts.loadRequested && id !== "" && prefs.status === "idle";
|
|
322
314
|
},
|
|
323
315
|
require: (facts) => ({
|
|
324
316
|
type: "FETCH_PREFERENCES",
|
|
325
|
-
userId:
|
|
317
|
+
userId: facts.userId.trim(),
|
|
326
318
|
}),
|
|
327
319
|
},
|
|
328
320
|
|
|
329
321
|
needsPermissions: {
|
|
330
322
|
priority: 80,
|
|
331
323
|
when: (facts) => {
|
|
332
|
-
const id =
|
|
333
|
-
const perms = facts.permissions
|
|
324
|
+
const id = facts.userId.trim();
|
|
325
|
+
const perms = facts.permissions;
|
|
334
326
|
|
|
335
|
-
return
|
|
336
|
-
(facts.loadRequested as boolean) &&
|
|
337
|
-
id !== "" &&
|
|
338
|
-
perms.status === "idle"
|
|
339
|
-
);
|
|
327
|
+
return facts.loadRequested && id !== "" && perms.status === "idle";
|
|
340
328
|
},
|
|
341
329
|
require: (facts) => ({
|
|
342
330
|
type: "FETCH_PERMISSIONS",
|
|
343
|
-
userId:
|
|
331
|
+
userId: facts.userId.trim(),
|
|
344
332
|
}),
|
|
345
333
|
},
|
|
346
334
|
},
|
|
@@ -355,7 +343,7 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
355
343
|
retry: { attempts: 3, backoff: "exponential" },
|
|
356
344
|
timeout: 10000,
|
|
357
345
|
resolve: async (req, context) => {
|
|
358
|
-
const prev = context.facts.profile
|
|
346
|
+
const prev = context.facts.profile;
|
|
359
347
|
context.facts.profile = {
|
|
360
348
|
...prev,
|
|
361
349
|
status: "loading",
|
|
@@ -372,24 +360,22 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
372
360
|
try {
|
|
373
361
|
const data = await fetchMockProfile(
|
|
374
362
|
req.userId,
|
|
375
|
-
context.facts.profileDelay
|
|
376
|
-
context.facts.profileFailRate
|
|
363
|
+
context.facts.profileDelay,
|
|
364
|
+
context.facts.profileFailRate,
|
|
377
365
|
);
|
|
378
366
|
context.facts.profile = {
|
|
379
367
|
data,
|
|
380
368
|
status: "success",
|
|
381
369
|
error: null,
|
|
382
|
-
attempts:
|
|
383
|
-
|
|
384
|
-
startedAt: (context.facts.profile as ResourceState<Profile>)
|
|
385
|
-
.startedAt,
|
|
370
|
+
attempts: context.facts.profile.attempts,
|
|
371
|
+
startedAt: context.facts.profile.startedAt,
|
|
386
372
|
completedAt: Date.now(),
|
|
387
373
|
};
|
|
388
374
|
addLogEntry(context.facts, "success", "profile", data.name);
|
|
389
375
|
} catch (err) {
|
|
390
376
|
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
391
377
|
context.facts.profile = {
|
|
392
|
-
...
|
|
378
|
+
...context.facts.profile,
|
|
393
379
|
status: "error",
|
|
394
380
|
error: msg,
|
|
395
381
|
completedAt: Date.now(),
|
|
@@ -404,7 +390,7 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
404
390
|
requirement: "FETCH_PREFERENCES",
|
|
405
391
|
retry: { attempts: 2, backoff: "exponential" },
|
|
406
392
|
resolve: async (req, context) => {
|
|
407
|
-
const prev = context.facts.preferences
|
|
393
|
+
const prev = context.facts.preferences;
|
|
408
394
|
context.facts.preferences = {
|
|
409
395
|
...prev,
|
|
410
396
|
status: "loading",
|
|
@@ -421,17 +407,15 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
421
407
|
try {
|
|
422
408
|
const data = await fetchMockPreferences(
|
|
423
409
|
req.userId,
|
|
424
|
-
context.facts.preferencesDelay
|
|
425
|
-
context.facts.preferencesFailRate
|
|
410
|
+
context.facts.preferencesDelay,
|
|
411
|
+
context.facts.preferencesFailRate,
|
|
426
412
|
);
|
|
427
413
|
context.facts.preferences = {
|
|
428
414
|
data,
|
|
429
415
|
status: "success",
|
|
430
416
|
error: null,
|
|
431
|
-
attempts:
|
|
432
|
-
|
|
433
|
-
startedAt: (context.facts.preferences as ResourceState<Preferences>)
|
|
434
|
-
.startedAt,
|
|
417
|
+
attempts: context.facts.preferences.attempts,
|
|
418
|
+
startedAt: context.facts.preferences.startedAt,
|
|
435
419
|
completedAt: Date.now(),
|
|
436
420
|
};
|
|
437
421
|
addLogEntry(
|
|
@@ -443,7 +427,7 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
443
427
|
} catch (err) {
|
|
444
428
|
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
445
429
|
context.facts.preferences = {
|
|
446
|
-
...
|
|
430
|
+
...context.facts.preferences,
|
|
447
431
|
status: "error",
|
|
448
432
|
error: msg,
|
|
449
433
|
completedAt: Date.now(),
|
|
@@ -459,7 +443,7 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
459
443
|
retry: { attempts: 3, backoff: "exponential" },
|
|
460
444
|
timeout: 15000,
|
|
461
445
|
resolve: async (req, context) => {
|
|
462
|
-
const prev = context.facts.permissions
|
|
446
|
+
const prev = context.facts.permissions;
|
|
463
447
|
context.facts.permissions = {
|
|
464
448
|
...prev,
|
|
465
449
|
status: "loading",
|
|
@@ -476,17 +460,15 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
476
460
|
try {
|
|
477
461
|
const data = await fetchMockPermissions(
|
|
478
462
|
req.userId,
|
|
479
|
-
context.facts.permissionsDelay
|
|
480
|
-
context.facts.permissionsFailRate
|
|
463
|
+
context.facts.permissionsDelay,
|
|
464
|
+
context.facts.permissionsFailRate,
|
|
481
465
|
);
|
|
482
466
|
context.facts.permissions = {
|
|
483
467
|
data,
|
|
484
468
|
status: "success",
|
|
485
469
|
error: null,
|
|
486
|
-
attempts:
|
|
487
|
-
|
|
488
|
-
startedAt: (context.facts.permissions as ResourceState<Permissions>)
|
|
489
|
-
.startedAt,
|
|
470
|
+
attempts: context.facts.permissions.attempts,
|
|
471
|
+
startedAt: context.facts.permissions.startedAt,
|
|
490
472
|
completedAt: Date.now(),
|
|
491
473
|
};
|
|
492
474
|
addLogEntry(
|
|
@@ -498,7 +480,7 @@ export const dashboardLoaderModule = createModule("dashboard-loader", {
|
|
|
498
480
|
} catch (err) {
|
|
499
481
|
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
500
482
|
context.facts.permissions = {
|
|
501
|
-
...
|
|
483
|
+
...context.facts.permissions,
|
|
502
484
|
status: "error",
|
|
503
485
|
error: msg,
|
|
504
486
|
completedAt: Date.now(),
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
import { createSystem } from "@directive-run/core";
|
|
15
15
|
import { devtoolsPlugin } from "@directive-run/core/plugins";
|
|
16
16
|
import {
|
|
17
|
-
type EventLogEntry,
|
|
18
|
-
type SearchResult,
|
|
19
17
|
debounceSearchModule,
|
|
20
18
|
debounceSearchSchema,
|
|
21
19
|
} from "./debounce-search.js";
|
|
@@ -28,7 +28,7 @@ export interface EventLogEntry {
|
|
|
28
28
|
// ============================================================================
|
|
29
29
|
|
|
30
30
|
function addLogEntry(facts: any, event: string, detail: string): void {
|
|
31
|
-
const log = [...
|
|
31
|
+
const log = [...facts.eventLog];
|
|
32
32
|
log.push({ timestamp: Date.now(), event, detail });
|
|
33
33
|
if (log.length > 50) {
|
|
34
34
|
log.splice(0, log.length - 50);
|
|
@@ -42,8 +42,8 @@ function addLogEntry(facts: any, event: string, detail: string): void {
|
|
|
42
42
|
|
|
43
43
|
export const dashboardSchema = {
|
|
44
44
|
facts: {
|
|
45
|
-
loadedModules: t.
|
|
46
|
-
eventLog: t.
|
|
45
|
+
loadedModules: t.array<string>(),
|
|
46
|
+
eventLog: t.array<EventLogEntry>(),
|
|
47
47
|
},
|
|
48
48
|
derivations: {
|
|
49
49
|
loadedCount: t.number(),
|
|
@@ -63,12 +63,12 @@ export const dashboardModule = createModule("dashboard", {
|
|
|
63
63
|
},
|
|
64
64
|
|
|
65
65
|
derive: {
|
|
66
|
-
loadedCount: (facts) =>
|
|
66
|
+
loadedCount: (facts) => facts.loadedModules.length,
|
|
67
67
|
},
|
|
68
68
|
|
|
69
69
|
events: {
|
|
70
70
|
moduleLoaded: (facts, { name }) => {
|
|
71
|
-
facts.loadedModules = [...
|
|
71
|
+
facts.loadedModules = [...facts.loadedModules, name];
|
|
72
72
|
addLogEntry(facts, "loaded", `Loaded "${name}" module`);
|
|
73
73
|
},
|
|
74
74
|
},
|
|
@@ -105,18 +105,15 @@ export const counterModule = createModule("counter", {
|
|
|
105
105
|
},
|
|
106
106
|
|
|
107
107
|
derive: {
|
|
108
|
-
isNearMax: (facts) =>
|
|
108
|
+
isNearMax: (facts) => facts.count >= 90,
|
|
109
109
|
},
|
|
110
110
|
|
|
111
111
|
events: {
|
|
112
112
|
increment: (facts) => {
|
|
113
|
-
facts.count =
|
|
113
|
+
facts.count = facts.count + facts.step;
|
|
114
114
|
},
|
|
115
115
|
decrement: (facts) => {
|
|
116
|
-
facts.count = Math.max(
|
|
117
|
-
0,
|
|
118
|
-
(facts.count as number) - (facts.step as number),
|
|
119
|
-
);
|
|
116
|
+
facts.count = Math.max(0, facts.count - facts.step);
|
|
120
117
|
},
|
|
121
118
|
setStep: (facts, { value }) => {
|
|
122
119
|
facts.step = value;
|
|
@@ -126,7 +123,7 @@ export const counterModule = createModule("counter", {
|
|
|
126
123
|
constraints: {
|
|
127
124
|
overflow: {
|
|
128
125
|
priority: 100,
|
|
129
|
-
when: (facts) =>
|
|
126
|
+
when: (facts) => facts.count >= 100,
|
|
130
127
|
require: () => ({ type: "COUNTER_RESET" }),
|
|
131
128
|
},
|
|
132
129
|
},
|
|
@@ -183,13 +180,13 @@ export const weatherModule = createModule("weather", {
|
|
|
183
180
|
|
|
184
181
|
derive: {
|
|
185
182
|
summary: (facts) => {
|
|
186
|
-
if (
|
|
183
|
+
if (facts.city === "") {
|
|
187
184
|
return "";
|
|
188
185
|
}
|
|
189
186
|
|
|
190
187
|
return `${facts.temperature}\u00B0F, ${facts.condition}`;
|
|
191
188
|
},
|
|
192
|
-
hasFetched: (facts) =>
|
|
189
|
+
hasFetched: (facts) => facts.lastFetchedCity !== "",
|
|
193
190
|
},
|
|
194
191
|
|
|
195
192
|
events: {
|
|
@@ -205,12 +202,12 @@ export const weatherModule = createModule("weather", {
|
|
|
205
202
|
needsFetch: {
|
|
206
203
|
priority: 100,
|
|
207
204
|
when: (facts) =>
|
|
208
|
-
|
|
205
|
+
facts.city.length >= 2 &&
|
|
209
206
|
facts.city !== facts.lastFetchedCity &&
|
|
210
|
-
!
|
|
207
|
+
!facts.isLoading,
|
|
211
208
|
require: (facts) => ({
|
|
212
209
|
type: "FETCH_WEATHER",
|
|
213
|
-
city: facts.city
|
|
210
|
+
city: facts.city,
|
|
214
211
|
}),
|
|
215
212
|
},
|
|
216
213
|
},
|
|
@@ -226,7 +223,7 @@ export const weatherModule = createModule("weather", {
|
|
|
226
223
|
const data = await mockFetchWeather(req.city, 800);
|
|
227
224
|
|
|
228
225
|
// Stale check: only apply if city still matches
|
|
229
|
-
if (
|
|
226
|
+
if (context.facts.city === req.city) {
|
|
230
227
|
context.facts.temperature = data.temperature;
|
|
231
228
|
context.facts.condition = data.condition;
|
|
232
229
|
context.facts.humidity = data.humidity;
|
|
@@ -269,7 +266,7 @@ export const diceModule = createModule("dice", {
|
|
|
269
266
|
},
|
|
270
267
|
|
|
271
268
|
derive: {
|
|
272
|
-
total: (facts) =>
|
|
269
|
+
total: (facts) => facts.die1 + facts.die2,
|
|
273
270
|
isDoubles: (facts) => facts.die1 === facts.die2,
|
|
274
271
|
},
|
|
275
272
|
|
|
@@ -277,7 +274,7 @@ export const diceModule = createModule("dice", {
|
|
|
277
274
|
roll: (facts) => {
|
|
278
275
|
facts.die1 = Math.floor(Math.random() * 6) + 1;
|
|
279
276
|
facts.die2 = Math.floor(Math.random() * 6) + 1;
|
|
280
|
-
facts.rollCount =
|
|
277
|
+
facts.rollCount = facts.rollCount + 1;
|
|
281
278
|
},
|
|
282
279
|
},
|
|
283
280
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Example: error-boundaries
|
|
2
|
-
// Source: examples/error-boundaries/src/
|
|
3
|
-
//
|
|
2
|
+
// Source: examples/error-boundaries/src/module.ts
|
|
3
|
+
// Pure module file — no DOM wiring
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Resilient API Dashboard —
|
|
6
|
+
* Resilient API Dashboard — Module Definition
|
|
7
7
|
*
|
|
8
8
|
* 3 simulated API services with configurable failure rates. Users inject errors
|
|
9
9
|
* and watch recovery strategies, circuit breaker state transitions, retry-later
|
|
@@ -17,17 +17,18 @@ import {
|
|
|
17
17
|
createSystem,
|
|
18
18
|
t,
|
|
19
19
|
} from "@directive-run/core";
|
|
20
|
-
import { devtoolsPlugin, performancePlugin } from "@directive-run/core/plugins";
|
|
21
20
|
import {
|
|
22
21
|
type CircuitState,
|
|
23
22
|
createCircuitBreaker,
|
|
23
|
+
devtoolsPlugin,
|
|
24
|
+
performancePlugin,
|
|
24
25
|
} from "@directive-run/core/plugins";
|
|
25
26
|
|
|
26
27
|
// ============================================================================
|
|
27
28
|
// Types
|
|
28
29
|
// ============================================================================
|
|
29
30
|
|
|
30
|
-
interface ServiceState {
|
|
31
|
+
export interface ServiceState {
|
|
31
32
|
name: string;
|
|
32
33
|
status: "idle" | "loading" | "success" | "error";
|
|
33
34
|
lastResult: string;
|
|
@@ -36,7 +37,7 @@ interface ServiceState {
|
|
|
36
37
|
lastError: string;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
interface TimelineEntry {
|
|
40
|
+
export interface TimelineEntry {
|
|
40
41
|
time: number;
|
|
41
42
|
event: string;
|
|
42
43
|
detail: string;
|
|
@@ -44,12 +45,12 @@ interface TimelineEntry {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// ============================================================================
|
|
47
|
-
//
|
|
48
|
+
// Timeline
|
|
48
49
|
// ============================================================================
|
|
49
50
|
|
|
50
|
-
const timeline: TimelineEntry[] = [];
|
|
51
|
+
export const timeline: TimelineEntry[] = [];
|
|
51
52
|
|
|
52
|
-
function addTimeline(
|
|
53
|
+
export function addTimeline(
|
|
53
54
|
event: string,
|
|
54
55
|
detail: string,
|
|
55
56
|
type: TimelineEntry["type"],
|
|
@@ -60,13 +61,18 @@ function addTimeline(
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Circuit Breakers (one per service)
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
export const circuitBreakers = {
|
|
64
69
|
users: createCircuitBreaker({
|
|
65
70
|
name: "users-api",
|
|
66
71
|
failureThreshold: 3,
|
|
67
72
|
recoveryTimeMs: 5000,
|
|
68
73
|
halfOpenMaxRequests: 2,
|
|
69
74
|
onStateChange: (from, to) => {
|
|
75
|
+
addTimeline("circuit", `users: ${from} → ${to}`, "circuit");
|
|
70
76
|
},
|
|
71
77
|
}),
|
|
72
78
|
orders: createCircuitBreaker({
|
|
@@ -75,6 +81,7 @@ const circuitBreakers = {
|
|
|
75
81
|
recoveryTimeMs: 5000,
|
|
76
82
|
halfOpenMaxRequests: 2,
|
|
77
83
|
onStateChange: (from, to) => {
|
|
84
|
+
addTimeline("circuit", `orders: ${from} → ${to}`, "circuit");
|
|
78
85
|
},
|
|
79
86
|
}),
|
|
80
87
|
analytics: createCircuitBreaker({
|
|
@@ -83,6 +90,7 @@ const circuitBreakers = {
|
|
|
83
90
|
recoveryTimeMs: 5000,
|
|
84
91
|
halfOpenMaxRequests: 2,
|
|
85
92
|
onStateChange: (from, to) => {
|
|
93
|
+
addTimeline("circuit", `analytics: ${from} → ${to}`, "circuit");
|
|
86
94
|
},
|
|
87
95
|
}),
|
|
88
96
|
};
|
|
@@ -91,7 +99,7 @@ const circuitBreakers = {
|
|
|
91
99
|
// Schema
|
|
92
100
|
// ============================================================================
|
|
93
101
|
|
|
94
|
-
const schema = {
|
|
102
|
+
export const schema = {
|
|
95
103
|
facts: {
|
|
96
104
|
usersService: t.object<ServiceState>(),
|
|
97
105
|
ordersService: t.object<ServiceState>(),
|
|
@@ -290,15 +298,16 @@ const dashboardModule = createModule("dashboard", {
|
|
|
290
298
|
});
|
|
291
299
|
|
|
292
300
|
// Success
|
|
293
|
-
const current = context.facts[serviceKey]
|
|
301
|
+
const current = context.facts[serviceKey];
|
|
294
302
|
context.facts[serviceKey] = {
|
|
295
303
|
...current,
|
|
296
304
|
status: "success",
|
|
297
305
|
lastResult: `Loaded at ${new Date().toLocaleTimeString()}`,
|
|
298
306
|
successCount: current.successCount + 1,
|
|
299
307
|
};
|
|
308
|
+
addTimeline("success", `${service} fetched`, "success");
|
|
300
309
|
} catch (error) {
|
|
301
|
-
const current = context.facts[serviceKey]
|
|
310
|
+
const current = context.facts[serviceKey];
|
|
302
311
|
const msg = error instanceof Error ? error.message : String(error);
|
|
303
312
|
context.facts[serviceKey] = {
|
|
304
313
|
...current,
|
|
@@ -307,6 +316,7 @@ const dashboardModule = createModule("dashboard", {
|
|
|
307
316
|
errorCount: current.errorCount + 1,
|
|
308
317
|
};
|
|
309
318
|
context.facts.totalErrors = context.facts.totalErrors + 1;
|
|
319
|
+
addTimeline("error", `${service}: ${msg.slice(0, 60)}`, "error");
|
|
310
320
|
|
|
311
321
|
// Re-throw so the error boundary handles recovery
|
|
312
322
|
throw error;
|
|
@@ -320,8 +330,9 @@ const dashboardModule = createModule("dashboard", {
|
|
|
320
330
|
// Performance Plugin
|
|
321
331
|
// ============================================================================
|
|
322
332
|
|
|
323
|
-
const perf = performancePlugin({
|
|
333
|
+
export const perf = performancePlugin({
|
|
324
334
|
onSlowResolver: (id, ms) => {
|
|
335
|
+
addTimeline("perf", `slow resolver: ${id} (${Math.round(ms)}ms)`, "info");
|
|
325
336
|
},
|
|
326
337
|
});
|
|
327
338
|
|
|
@@ -331,12 +342,13 @@ const perf = performancePlugin({
|
|
|
331
342
|
|
|
332
343
|
let currentStrategy: RecoveryStrategy = "retry-later";
|
|
333
344
|
|
|
334
|
-
const system = createSystem({
|
|
345
|
+
export const system = createSystem({
|
|
335
346
|
module: dashboardModule,
|
|
336
347
|
debug: { runHistory: true },
|
|
337
348
|
plugins: [perf, devtoolsPlugin({ name: "error-boundaries" })],
|
|
338
349
|
errorBoundary: {
|
|
339
350
|
onResolverError: (_error, resolver) => {
|
|
351
|
+
addTimeline(
|
|
340
352
|
"recovery",
|
|
341
353
|
`${resolver}: strategy=${currentStrategy}`,
|
|
342
354
|
"recovery",
|
|
@@ -352,79 +364,16 @@ const system = createSystem({
|
|
|
352
364
|
backoffMultiplier: 2,
|
|
353
365
|
},
|
|
354
366
|
onError: (error) => {
|
|
367
|
+
addTimeline("error", `boundary: ${error.message.slice(0, 60)}`, "error");
|
|
355
368
|
},
|
|
356
369
|
},
|
|
357
370
|
});
|
|
358
|
-
system.start();
|
|
359
371
|
|
|
360
372
|
// Track strategy changes to update error boundary (via re-dispatch)
|
|
361
373
|
system.subscribe(["strategy"], () => {
|
|
362
|
-
const newStrategy = system.facts.strategy
|
|
374
|
+
const newStrategy = system.facts.strategy;
|
|
363
375
|
if (newStrategy !== currentStrategy) {
|
|
364
376
|
currentStrategy = newStrategy;
|
|
377
|
+
addTimeline("recovery", `strategy → ${newStrategy}`, "recovery");
|
|
365
378
|
}
|
|
366
379
|
});
|
|
367
|
-
|
|
368
|
-
// ============================================================================
|
|
369
|
-
// DOM References
|
|
370
|
-
// ============================================================================
|
|
371
|
-
|
|
372
|
-
// Service cards
|
|
373
|
-
|
|
374
|
-
// Sliders
|
|
375
|
-
"eb-users-failrate",
|
|
376
|
-
"eb-orders-failrate",
|
|
377
|
-
"eb-analytics-failrate",
|
|
378
|
-
|
|
379
|
-
// Strategy dropdown
|
|
380
|
-
"eb-strategy",
|
|
381
|
-
|
|
382
|
-
// Timeline
|
|
383
|
-
|
|
384
|
-
// ============================================================================
|
|
385
|
-
// Render
|
|
386
|
-
// ============================================================================
|
|
387
|
-
|
|
388
|
-
function escapeHtml(text: string): string {
|
|
389
|
-
|
|
390
|
-
return div.innerHTML;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
service: ServiceState,
|
|
394
|
-
): void {
|
|
395
|
-
if (service.lastError) {
|
|
396
|
-
} else {
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
// ============================================================================
|
|
402
|
-
// Subscribe
|
|
403
|
-
// ============================================================================
|
|
404
|
-
|
|
405
|
-
const allKeys = [
|
|
406
|
-
...Object.keys(schema.facts),
|
|
407
|
-
...Object.keys(schema.derivations),
|
|
408
|
-
];
|
|
409
|
-
system.subscribe(allKeys, render);
|
|
410
|
-
|
|
411
|
-
// Periodic refresh for circuit breaker state transitions + retry queue
|
|
412
|
-
setInterval(() => {
|
|
413
|
-
render();
|
|
414
|
-
}, 1000);
|
|
415
|
-
|
|
416
|
-
// ============================================================================
|
|
417
|
-
// Controls
|
|
418
|
-
// ============================================================================
|
|
419
|
-
|
|
420
|
-
// Fetch buttons
|
|
421
|
-
|
|
422
|
-
// Strategy selector
|
|
423
|
-
|
|
424
|
-
// Sliders
|
|
425
|
-
|
|
426
|
-
// ============================================================================
|
|
427
|
-
// Initial Render
|
|
428
|
-
// ============================================================================
|
|
429
|
-
|
|
430
|
-
render();
|