@aiassesstech/nole 0.4.15 → 0.5.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/dist/resilience/circuit-breaker.d.ts +46 -0
- package/dist/resilience/circuit-breaker.d.ts.map +1 -0
- package/dist/resilience/circuit-breaker.js +102 -0
- package/dist/resilience/circuit-breaker.js.map +1 -0
- package/dist/resilience/index.d.ts +8 -0
- package/dist/resilience/index.d.ts.map +1 -0
- package/dist/resilience/index.js +8 -0
- package/dist/resilience/index.js.map +1 -0
- package/dist/resilience/post-queue.d.ts +41 -0
- package/dist/resilience/post-queue.d.ts.map +1 -0
- package/dist/resilience/post-queue.js +106 -0
- package/dist/resilience/post-queue.js.map +1 -0
- package/dist/resilience/queue-drain.d.ts +35 -0
- package/dist/resilience/queue-drain.d.ts.map +1 -0
- package/dist/resilience/queue-drain.js +92 -0
- package/dist/resilience/queue-drain.js.map +1 -0
- package/dist/resilience/resilient-client.d.ts +54 -0
- package/dist/resilience/resilient-client.d.ts.map +1 -0
- package/dist/resilience/resilient-client.js +120 -0
- package/dist/resilience/resilient-client.js.map +1 -0
- package/dist/resilience/retry.d.ts +22 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +68 -0
- package/dist/resilience/retry.js.map +1 -0
- package/dist/resilience/service-configs.d.ts +14 -0
- package/dist/resilience/service-configs.d.ts.map +1 -0
- package/dist/resilience/service-configs.js +45 -0
- package/dist/resilience/service-configs.js.map +1 -0
- package/dist/resilience/service-registry.d.ts +39 -0
- package/dist/resilience/service-registry.d.ts.map +1 -0
- package/dist/resilience/service-registry.js +82 -0
- package/dist/resilience/service-registry.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker — Three-state pattern for external service protection.
|
|
3
|
+
*
|
|
4
|
+
* CLOSED → requests pass through, failures counted
|
|
5
|
+
* OPEN → requests rejected immediately, cooldown timer running
|
|
6
|
+
* HALF-OPEN → single probe request allowed, success closes, failure reopens
|
|
7
|
+
*
|
|
8
|
+
* @see SPEC-nole-operational-resilience.md §3
|
|
9
|
+
*/
|
|
10
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
11
|
+
export interface CircuitBreakerConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
failureThreshold: number;
|
|
14
|
+
failureWindowMs: number;
|
|
15
|
+
cooldownMs: number;
|
|
16
|
+
maxCooldownMs: number;
|
|
17
|
+
cooldownMultiplier: number;
|
|
18
|
+
failureStatusCodes: number[];
|
|
19
|
+
onStateChange?: (from: CircuitState, to: CircuitState, service: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare class CircuitBreaker {
|
|
22
|
+
private config;
|
|
23
|
+
private state;
|
|
24
|
+
private failures;
|
|
25
|
+
private lastStateChange;
|
|
26
|
+
private currentCooldownMs;
|
|
27
|
+
private consecutiveOpenings;
|
|
28
|
+
private probeAllowed;
|
|
29
|
+
constructor(config: CircuitBreakerConfig);
|
|
30
|
+
canExecute(): boolean;
|
|
31
|
+
recordSuccess(): void;
|
|
32
|
+
recordFailure(error: string): void;
|
|
33
|
+
isFailureStatus(status: number): boolean;
|
|
34
|
+
getState(): CircuitState;
|
|
35
|
+
getStatus(): {
|
|
36
|
+
state: CircuitState;
|
|
37
|
+
failureCount: number;
|
|
38
|
+
consecutiveOpenings: number;
|
|
39
|
+
currentCooldownMs: number;
|
|
40
|
+
lastStateChange: number;
|
|
41
|
+
recentFailures: string[];
|
|
42
|
+
};
|
|
43
|
+
private transition;
|
|
44
|
+
private pruneOldFailures;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=circuit-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/resilience/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAE3D,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACjF;AAOD,qBAAa,cAAc;IAQb,OAAO,CAAC,MAAM;IAP1B,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,eAAe,CAAsB;IAC7C,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,mBAAmB,CAAa;IACxC,OAAO,CAAC,YAAY,CAAS;gBAET,MAAM,EAAE,oBAAoB;IAIhD,UAAU,IAAI,OAAO;IAsBrB,aAAa,IAAI,IAAI;IASrB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IA6BlC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIxC,QAAQ,IAAI,YAAY;IAUxB,SAAS;;;;;;;;IAYT,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,gBAAgB;CAIzB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit Breaker — Three-state pattern for external service protection.
|
|
3
|
+
*
|
|
4
|
+
* CLOSED → requests pass through, failures counted
|
|
5
|
+
* OPEN → requests rejected immediately, cooldown timer running
|
|
6
|
+
* HALF-OPEN → single probe request allowed, success closes, failure reopens
|
|
7
|
+
*
|
|
8
|
+
* @see SPEC-nole-operational-resilience.md §3
|
|
9
|
+
*/
|
|
10
|
+
export class CircuitBreaker {
|
|
11
|
+
config;
|
|
12
|
+
state = 'closed';
|
|
13
|
+
failures = [];
|
|
14
|
+
lastStateChange = Date.now();
|
|
15
|
+
currentCooldownMs;
|
|
16
|
+
consecutiveOpenings = 0;
|
|
17
|
+
probeAllowed = false;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.currentCooldownMs = config.cooldownMs;
|
|
21
|
+
}
|
|
22
|
+
canExecute() {
|
|
23
|
+
this.pruneOldFailures();
|
|
24
|
+
switch (this.state) {
|
|
25
|
+
case 'closed':
|
|
26
|
+
return true;
|
|
27
|
+
case 'open': {
|
|
28
|
+
const elapsed = Date.now() - this.lastStateChange;
|
|
29
|
+
if (elapsed >= this.currentCooldownMs) {
|
|
30
|
+
this.transition('half-open');
|
|
31
|
+
this.probeAllowed = false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
case 'half-open':
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
recordSuccess() {
|
|
41
|
+
if (this.state === 'half-open') {
|
|
42
|
+
this.consecutiveOpenings = 0;
|
|
43
|
+
this.currentCooldownMs = this.config.cooldownMs;
|
|
44
|
+
this.transition('closed');
|
|
45
|
+
this.failures = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
recordFailure(error) {
|
|
49
|
+
const ts = Date.now();
|
|
50
|
+
switch (this.state) {
|
|
51
|
+
case 'closed':
|
|
52
|
+
this.failures.push({ timestamp: ts, error });
|
|
53
|
+
this.pruneOldFailures();
|
|
54
|
+
if (this.failures.length >= this.config.failureThreshold) {
|
|
55
|
+
this.consecutiveOpenings++;
|
|
56
|
+
this.currentCooldownMs = Math.min(this.config.cooldownMs * Math.pow(this.config.cooldownMultiplier, this.consecutiveOpenings - 1), this.config.maxCooldownMs);
|
|
57
|
+
this.transition('open');
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case 'half-open':
|
|
61
|
+
this.consecutiveOpenings++;
|
|
62
|
+
this.currentCooldownMs = Math.min(this.currentCooldownMs * this.config.cooldownMultiplier, this.config.maxCooldownMs);
|
|
63
|
+
this.transition('open');
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
isFailureStatus(status) {
|
|
68
|
+
return this.config.failureStatusCodes.includes(status);
|
|
69
|
+
}
|
|
70
|
+
getState() {
|
|
71
|
+
if (this.state === 'open') {
|
|
72
|
+
const elapsed = Date.now() - this.lastStateChange;
|
|
73
|
+
if (elapsed >= this.currentCooldownMs) {
|
|
74
|
+
this.transition('half-open');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return this.state;
|
|
78
|
+
}
|
|
79
|
+
getStatus() {
|
|
80
|
+
this.pruneOldFailures();
|
|
81
|
+
return {
|
|
82
|
+
state: this.getState(),
|
|
83
|
+
failureCount: this.failures.length,
|
|
84
|
+
consecutiveOpenings: this.consecutiveOpenings,
|
|
85
|
+
currentCooldownMs: this.currentCooldownMs,
|
|
86
|
+
lastStateChange: this.lastStateChange,
|
|
87
|
+
recentFailures: this.failures.slice(-3).map(f => f.error),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
transition(to) {
|
|
91
|
+
const from = this.state;
|
|
92
|
+
this.state = to;
|
|
93
|
+
this.lastStateChange = Date.now();
|
|
94
|
+
this.probeAllowed = false;
|
|
95
|
+
this.config.onStateChange?.(from, to, this.config.name);
|
|
96
|
+
}
|
|
97
|
+
pruneOldFailures() {
|
|
98
|
+
const cutoff = Date.now() - this.config.failureWindowMs;
|
|
99
|
+
this.failures = this.failures.filter(f => f.timestamp > cutoff);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../src/resilience/circuit-breaker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CircuitBreaker, type CircuitBreakerConfig, type CircuitState } from './circuit-breaker.js';
|
|
2
|
+
export { type RetryConfig, DEFAULT_RETRY_CONFIG, calculateDelay, isRetryable, extractRetryAfter, sleep, } from './retry.js';
|
|
3
|
+
export { ResilientClient, CircuitOpenError, type ResilientClientConfig, type ResilientResponse, } from './resilient-client.js';
|
|
4
|
+
export { ServiceRegistry, type ServiceHealth, type ServiceStatus, type RegistrySnapshot, } from './service-registry.js';
|
|
5
|
+
export { PostQueue, type QueuedPost, type PostPlatform, type PostQueueConfig, DEFAULT_QUEUE_CONFIG, } from './post-queue.js';
|
|
6
|
+
export { QueueDrain, type PostSender, type QueueDrainConfig, DEFAULT_DRAIN_CONFIG, } from './queue-drain.js';
|
|
7
|
+
export { PLATFORM_API_BREAKER, X_API_BREAKER, MOLTBOOK_API_BREAKER, BLOCKCHAIN_BREAKER, } from './service-configs.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resilience/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpG,OAAO,EACL,KAAK,WAAW,EAChB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,KAAK,GACN,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,GACvB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,SAAS,EACT,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,UAAU,EACV,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CircuitBreaker } from './circuit-breaker.js';
|
|
2
|
+
export { DEFAULT_RETRY_CONFIG, calculateDelay, isRetryable, extractRetryAfter, sleep, } from './retry.js';
|
|
3
|
+
export { ResilientClient, CircuitOpenError, } from './resilient-client.js';
|
|
4
|
+
export { ServiceRegistry, } from './service-registry.js';
|
|
5
|
+
export { PostQueue, DEFAULT_QUEUE_CONFIG, } from './post-queue.js';
|
|
6
|
+
export { QueueDrain, DEFAULT_DRAIN_CONFIG, } from './queue-drain.js';
|
|
7
|
+
export { PLATFORM_API_BREAKER, X_API_BREAKER, MOLTBOOK_API_BREAKER, BLOCKCHAIN_BREAKER, } from './service-configs.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resilience/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAgD,MAAM,sBAAsB,CAAC;AAEpG,OAAO,EAEL,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,KAAK,GACN,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,gBAAgB,GAGjB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,eAAe,GAIhB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,SAAS,EAIT,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,UAAU,EAGV,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostQueue — Durable queue for social media posts during outages.
|
|
3
|
+
*
|
|
4
|
+
* When X/Twitter or MoltBook circuit is open, posts are queued to
|
|
5
|
+
* a JSON file and drained when the service recovers.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §7
|
|
8
|
+
*/
|
|
9
|
+
export type PostPlatform = 'x-twitter' | 'moltbook';
|
|
10
|
+
export interface QueuedPost {
|
|
11
|
+
id: string;
|
|
12
|
+
platform: PostPlatform;
|
|
13
|
+
content: string;
|
|
14
|
+
metadata: Record<string, unknown>;
|
|
15
|
+
queuedAt: number;
|
|
16
|
+
retries: number;
|
|
17
|
+
lastRetryAt: number | null;
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface PostQueueConfig {
|
|
21
|
+
dataDir: string;
|
|
22
|
+
maxQueueSize: number;
|
|
23
|
+
maxRetries: number;
|
|
24
|
+
maxAgeMs: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_QUEUE_CONFIG: PostQueueConfig;
|
|
27
|
+
export declare class PostQueue {
|
|
28
|
+
private config;
|
|
29
|
+
private queue;
|
|
30
|
+
private filePath;
|
|
31
|
+
constructor(config: PostQueueConfig);
|
|
32
|
+
enqueue(platform: PostPlatform, content: string, metadata?: Record<string, unknown>): QueuedPost;
|
|
33
|
+
dequeue(platform: PostPlatform): QueuedPost | null;
|
|
34
|
+
requeue(post: QueuedPost, error: string): void;
|
|
35
|
+
peek(platform: PostPlatform): QueuedPost[];
|
|
36
|
+
size(platform?: PostPlatform): number;
|
|
37
|
+
private pruneExpired;
|
|
38
|
+
private load;
|
|
39
|
+
private save;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=post-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-queue.d.ts","sourceRoot":"","sources":["../../src/resilience/post-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,UAAU,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,oBAAoB,EAAE,eAKlC,CAAC;AAEF,qBAAa,SAAS;IAIR,OAAO,CAAC,MAAM;IAH1B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,QAAQ,CAAS;gBAEL,MAAM,EAAE,eAAe;IAK3C,OAAO,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,UAAU;IAuBpG,OAAO,CAAC,QAAQ,EAAE,YAAY,GAAG,UAAU,GAAG,IAAI;IAclD,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAY9C,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,UAAU,EAAE;IAK1C,IAAI,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,MAAM;IAQrC,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,IAAI;CAWb"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostQueue — Durable queue for social media posts during outages.
|
|
3
|
+
*
|
|
4
|
+
* When X/Twitter or MoltBook circuit is open, posts are queued to
|
|
5
|
+
* a JSON file and drained when the service recovers.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §7
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
export const DEFAULT_QUEUE_CONFIG = {
|
|
12
|
+
dataDir: '',
|
|
13
|
+
maxQueueSize: 100,
|
|
14
|
+
maxRetries: 5,
|
|
15
|
+
maxAgeMs: 24 * 60 * 60 * 1000, // 24h
|
|
16
|
+
};
|
|
17
|
+
export class PostQueue {
|
|
18
|
+
config;
|
|
19
|
+
queue = [];
|
|
20
|
+
filePath;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.filePath = path.join(config.dataDir, 'post-queue.json');
|
|
24
|
+
this.load();
|
|
25
|
+
}
|
|
26
|
+
enqueue(platform, content, metadata = {}) {
|
|
27
|
+
this.pruneExpired();
|
|
28
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
29
|
+
throw new Error(`Post queue full (${this.config.maxQueueSize} items) — cannot enqueue`);
|
|
30
|
+
}
|
|
31
|
+
const post = {
|
|
32
|
+
id: `post_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
33
|
+
platform,
|
|
34
|
+
content,
|
|
35
|
+
metadata,
|
|
36
|
+
queuedAt: Date.now(),
|
|
37
|
+
retries: 0,
|
|
38
|
+
lastRetryAt: null,
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
this.queue.push(post);
|
|
42
|
+
this.save();
|
|
43
|
+
return post;
|
|
44
|
+
}
|
|
45
|
+
dequeue(platform) {
|
|
46
|
+
this.pruneExpired();
|
|
47
|
+
const idx = this.queue.findIndex(p => p.platform === platform && p.retries < this.config.maxRetries);
|
|
48
|
+
if (idx === -1)
|
|
49
|
+
return null;
|
|
50
|
+
const [post] = this.queue.splice(idx, 1);
|
|
51
|
+
this.save();
|
|
52
|
+
return post;
|
|
53
|
+
}
|
|
54
|
+
requeue(post, error) {
|
|
55
|
+
post.retries++;
|
|
56
|
+
post.lastRetryAt = Date.now();
|
|
57
|
+
post.error = error;
|
|
58
|
+
if (post.retries < this.config.maxRetries) {
|
|
59
|
+
this.queue.push(post);
|
|
60
|
+
}
|
|
61
|
+
this.save();
|
|
62
|
+
}
|
|
63
|
+
peek(platform) {
|
|
64
|
+
this.pruneExpired();
|
|
65
|
+
return this.queue.filter(p => p.platform === platform);
|
|
66
|
+
}
|
|
67
|
+
size(platform) {
|
|
68
|
+
this.pruneExpired();
|
|
69
|
+
if (platform) {
|
|
70
|
+
return this.queue.filter(p => p.platform === platform).length;
|
|
71
|
+
}
|
|
72
|
+
return this.queue.length;
|
|
73
|
+
}
|
|
74
|
+
pruneExpired() {
|
|
75
|
+
const cutoff = Date.now() - this.config.maxAgeMs;
|
|
76
|
+
const before = this.queue.length;
|
|
77
|
+
this.queue = this.queue.filter(p => p.queuedAt > cutoff && p.retries < this.config.maxRetries);
|
|
78
|
+
if (this.queue.length !== before) {
|
|
79
|
+
this.save();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
load() {
|
|
83
|
+
try {
|
|
84
|
+
if (fs.existsSync(this.filePath)) {
|
|
85
|
+
const data = fs.readFileSync(this.filePath, 'utf-8');
|
|
86
|
+
this.queue = JSON.parse(data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
this.queue = [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
save() {
|
|
94
|
+
try {
|
|
95
|
+
const dir = path.dirname(this.filePath);
|
|
96
|
+
if (!fs.existsSync(dir)) {
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.queue, null, 2), 'utf-8');
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Non-fatal — queue is best-effort durable
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=post-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-queue.js","sourceRoot":"","sources":["../../src/resilience/post-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueDrain — Periodic processor that drains the PostQueue when
|
|
3
|
+
* service circuits recover to closed or half-open.
|
|
4
|
+
*
|
|
5
|
+
* @see SPEC-nole-operational-resilience.md §7.2
|
|
6
|
+
*/
|
|
7
|
+
import type { PostQueue, PostPlatform } from './post-queue.js';
|
|
8
|
+
import type { ServiceRegistry } from './service-registry.js';
|
|
9
|
+
export type PostSender = (platform: PostPlatform, content: string, metadata: Record<string, unknown>) => Promise<void>;
|
|
10
|
+
export interface QueueDrainConfig {
|
|
11
|
+
intervalMs: number;
|
|
12
|
+
batchSize: number;
|
|
13
|
+
interPostDelayMs: number;
|
|
14
|
+
logger?: {
|
|
15
|
+
debug: (msg: string, meta?: Record<string, unknown>) => void;
|
|
16
|
+
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
17
|
+
error: (msg: string, meta?: Record<string, unknown>) => void;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare const DEFAULT_DRAIN_CONFIG: QueueDrainConfig;
|
|
21
|
+
export declare class QueueDrain {
|
|
22
|
+
private queue;
|
|
23
|
+
private registry;
|
|
24
|
+
private sender;
|
|
25
|
+
private config;
|
|
26
|
+
private timer;
|
|
27
|
+
private running;
|
|
28
|
+
private logger;
|
|
29
|
+
constructor(queue: PostQueue, registry: ServiceRegistry, sender: PostSender, config?: QueueDrainConfig);
|
|
30
|
+
start(): void;
|
|
31
|
+
stop(): void;
|
|
32
|
+
drain(): Promise<number>;
|
|
33
|
+
isRunning(): boolean;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=queue-drain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-drain.d.ts","sourceRoot":"","sources":["../../src/resilience/queue-drain.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE;QACP,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAC5D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAC9D,CAAC;CACH;AAED,eAAO,MAAM,oBAAoB,EAAE,gBAIlC,CAAC;AAEF,qBAAa,UAAU;IAMnB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM;IARhB,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAA0C;gBAG9C,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,gBAAuC;IASzD,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAQN,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAgD9B,SAAS,IAAI,OAAO;CAGrB"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueDrain — Periodic processor that drains the PostQueue when
|
|
3
|
+
* service circuits recover to closed or half-open.
|
|
4
|
+
*
|
|
5
|
+
* @see SPEC-nole-operational-resilience.md §7.2
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_DRAIN_CONFIG = {
|
|
8
|
+
intervalMs: 60_000,
|
|
9
|
+
batchSize: 5,
|
|
10
|
+
interPostDelayMs: 5_000,
|
|
11
|
+
};
|
|
12
|
+
export class QueueDrain {
|
|
13
|
+
queue;
|
|
14
|
+
registry;
|
|
15
|
+
sender;
|
|
16
|
+
config;
|
|
17
|
+
timer = null;
|
|
18
|
+
running = false;
|
|
19
|
+
logger;
|
|
20
|
+
constructor(queue, registry, sender, config = DEFAULT_DRAIN_CONFIG) {
|
|
21
|
+
this.queue = queue;
|
|
22
|
+
this.registry = registry;
|
|
23
|
+
this.sender = sender;
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.logger = config.logger ?? {
|
|
26
|
+
debug: () => { },
|
|
27
|
+
warn: () => { },
|
|
28
|
+
error: () => { },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
start() {
|
|
32
|
+
if (this.timer)
|
|
33
|
+
return;
|
|
34
|
+
this.timer = setInterval(() => void this.drain(), this.config.intervalMs);
|
|
35
|
+
this.logger.debug('Queue drain started', { intervalMs: this.config.intervalMs });
|
|
36
|
+
}
|
|
37
|
+
stop() {
|
|
38
|
+
if (this.timer) {
|
|
39
|
+
clearInterval(this.timer);
|
|
40
|
+
this.timer = null;
|
|
41
|
+
}
|
|
42
|
+
this.logger.debug('Queue drain stopped');
|
|
43
|
+
}
|
|
44
|
+
async drain() {
|
|
45
|
+
if (this.running)
|
|
46
|
+
return 0;
|
|
47
|
+
this.running = true;
|
|
48
|
+
let sent = 0;
|
|
49
|
+
try {
|
|
50
|
+
const platforms = ['x-twitter', 'moltbook'];
|
|
51
|
+
for (const platform of platforms) {
|
|
52
|
+
if (!this.registry.isAvailable(platform)) {
|
|
53
|
+
this.logger.debug(`${platform}: circuit still open, skipping drain`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const queued = this.queue.size(platform);
|
|
57
|
+
if (queued === 0)
|
|
58
|
+
continue;
|
|
59
|
+
this.logger.debug(`${platform}: draining ${queued} queued posts`);
|
|
60
|
+
for (let i = 0; i < this.config.batchSize; i++) {
|
|
61
|
+
const post = this.queue.dequeue(platform);
|
|
62
|
+
if (!post)
|
|
63
|
+
break;
|
|
64
|
+
try {
|
|
65
|
+
await this.sender(platform, post.content, post.metadata);
|
|
66
|
+
sent++;
|
|
67
|
+
this.registry.recordSuccess(platform);
|
|
68
|
+
this.logger.debug(`${platform}: sent queued post ${post.id}`);
|
|
69
|
+
if (i < this.config.batchSize - 1) {
|
|
70
|
+
await new Promise(r => setTimeout(r, this.config.interPostDelayMs));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
75
|
+
this.queue.requeue(post, error);
|
|
76
|
+
this.registry.recordFailure(platform);
|
|
77
|
+
this.logger.warn(`${platform}: failed to send ${post.id}, requeued`, { error });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.running = false;
|
|
85
|
+
}
|
|
86
|
+
return sent;
|
|
87
|
+
}
|
|
88
|
+
isRunning() {
|
|
89
|
+
return this.timer !== null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=queue-drain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-drain.js","sourceRoot":"","sources":["../../src/resilience/queue-drain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResilientClient — Unified wrapper combining circuit breaker + retry.
|
|
3
|
+
*
|
|
4
|
+
* Replaces raw fetch() calls in Nole's service clients with
|
|
5
|
+
* protection against transient failures and cascading outages.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §5
|
|
8
|
+
*/
|
|
9
|
+
import { type CircuitBreakerConfig, type CircuitState } from './circuit-breaker.js';
|
|
10
|
+
import { type RetryConfig } from './retry.js';
|
|
11
|
+
export declare class CircuitOpenError extends Error {
|
|
12
|
+
readonly service: string;
|
|
13
|
+
readonly state: CircuitState;
|
|
14
|
+
readonly cooldownRemainingMs: number;
|
|
15
|
+
constructor(service: string, cooldownRemainingMs: number);
|
|
16
|
+
}
|
|
17
|
+
export interface ResilientClientConfig {
|
|
18
|
+
circuitBreaker: CircuitBreakerConfig;
|
|
19
|
+
retry?: RetryConfig;
|
|
20
|
+
timeoutMs: number;
|
|
21
|
+
logger?: {
|
|
22
|
+
debug: (msg: string, meta?: Record<string, unknown>) => void;
|
|
23
|
+
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
24
|
+
error: (msg: string, meta?: Record<string, unknown>) => void;
|
|
25
|
+
};
|
|
26
|
+
fetchFn?: typeof globalThis.fetch;
|
|
27
|
+
}
|
|
28
|
+
export interface ResilientResponse {
|
|
29
|
+
status: number;
|
|
30
|
+
headers: Headers;
|
|
31
|
+
body: string;
|
|
32
|
+
attempts: number;
|
|
33
|
+
totalDurationMs: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class ResilientClient {
|
|
36
|
+
private config;
|
|
37
|
+
private breaker;
|
|
38
|
+
private retryConfig;
|
|
39
|
+
private timeoutMs;
|
|
40
|
+
private logger;
|
|
41
|
+
private fetchFn;
|
|
42
|
+
constructor(config: ResilientClientConfig);
|
|
43
|
+
execute(url: string, init?: RequestInit): Promise<ResilientResponse>;
|
|
44
|
+
getCircuitStatus(): {
|
|
45
|
+
state: CircuitState;
|
|
46
|
+
failureCount: number;
|
|
47
|
+
consecutiveOpenings: number;
|
|
48
|
+
currentCooldownMs: number;
|
|
49
|
+
lastStateChange: number;
|
|
50
|
+
recentFailures: string[];
|
|
51
|
+
};
|
|
52
|
+
get serviceName(): string;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=resilient-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilient-client.d.ts","sourceRoot":"","sources":["../../src/resilience/resilient-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAkB,KAAK,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpG,OAAO,EACL,KAAK,WAAW,EAMjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;gBAEzB,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM;CAOzD;AAED,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,oBAAoB,CAAC;IACrC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QACP,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAC5D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAC9D,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,eAAe;IAOd,OAAO,CAAC,MAAM;IAN1B,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAA+C;IAC7D,OAAO,CAAC,OAAO,CAA0B;gBAErB,MAAM,EAAE,qBAAqB;IAY3C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAyF1E,gBAAgB;;;;;;;;IAIhB,IAAI,WAAW,IAAI,MAAM,CAExB;CACF"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResilientClient — Unified wrapper combining circuit breaker + retry.
|
|
3
|
+
*
|
|
4
|
+
* Replaces raw fetch() calls in Nole's service clients with
|
|
5
|
+
* protection against transient failures and cascading outages.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §5
|
|
8
|
+
*/
|
|
9
|
+
import { CircuitBreaker } from './circuit-breaker.js';
|
|
10
|
+
import { DEFAULT_RETRY_CONFIG, calculateDelay, isRetryable, extractRetryAfter, sleep, } from './retry.js';
|
|
11
|
+
export class CircuitOpenError extends Error {
|
|
12
|
+
service;
|
|
13
|
+
state;
|
|
14
|
+
cooldownRemainingMs;
|
|
15
|
+
constructor(service, cooldownRemainingMs) {
|
|
16
|
+
super(`Circuit breaker OPEN for ${service} — next probe in ${Math.ceil(cooldownRemainingMs / 1000)}s`);
|
|
17
|
+
this.name = 'CircuitOpenError';
|
|
18
|
+
this.service = service;
|
|
19
|
+
this.state = 'open';
|
|
20
|
+
this.cooldownRemainingMs = cooldownRemainingMs;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class ResilientClient {
|
|
24
|
+
config;
|
|
25
|
+
breaker;
|
|
26
|
+
retryConfig;
|
|
27
|
+
timeoutMs;
|
|
28
|
+
logger;
|
|
29
|
+
fetchFn;
|
|
30
|
+
constructor(config) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.breaker = new CircuitBreaker(config.circuitBreaker);
|
|
33
|
+
this.retryConfig = config.retry ?? DEFAULT_RETRY_CONFIG;
|
|
34
|
+
this.timeoutMs = config.timeoutMs;
|
|
35
|
+
this.fetchFn = config.fetchFn ?? globalThis.fetch;
|
|
36
|
+
this.logger = config.logger ?? {
|
|
37
|
+
debug: () => { },
|
|
38
|
+
warn: () => { },
|
|
39
|
+
error: () => { },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async execute(url, init) {
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
const service = this.config.circuitBreaker.name;
|
|
45
|
+
if (!this.breaker.canExecute()) {
|
|
46
|
+
const status = this.breaker.getStatus();
|
|
47
|
+
const remaining = status.currentCooldownMs - (Date.now() - status.lastStateChange);
|
|
48
|
+
throw new CircuitOpenError(service, Math.max(0, remaining));
|
|
49
|
+
}
|
|
50
|
+
let lastError = null;
|
|
51
|
+
const maxAttempts = 1 + this.retryConfig.maxRetries;
|
|
52
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
53
|
+
try {
|
|
54
|
+
this.logger.debug(`${service}: attempt ${attempt + 1}/${maxAttempts}`, { url });
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
57
|
+
const resp = await this.fetchFn(url, {
|
|
58
|
+
...init,
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
if (this.breaker.isFailureStatus(resp.status)) {
|
|
63
|
+
const body = await resp.text().catch(() => '');
|
|
64
|
+
this.breaker.recordFailure(`HTTP ${resp.status}: ${body.slice(0, 200)}`);
|
|
65
|
+
if (isRetryable(resp.status, this.retryConfig) && attempt < maxAttempts - 1) {
|
|
66
|
+
const delay = resp.status === 429
|
|
67
|
+
? (extractRetryAfter(resp.headers) ?? calculateDelay(attempt, this.retryConfig))
|
|
68
|
+
: calculateDelay(attempt, this.retryConfig);
|
|
69
|
+
this.logger.warn(`${service}: HTTP ${resp.status}, retrying in ${delay}ms`, {
|
|
70
|
+
attempt: attempt + 1, url,
|
|
71
|
+
});
|
|
72
|
+
await sleep(delay);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`${service}: HTTP ${resp.status} after ${attempt + 1} attempts`);
|
|
76
|
+
}
|
|
77
|
+
if (resp.status >= 200 && resp.status < 500) {
|
|
78
|
+
this.breaker.recordSuccess();
|
|
79
|
+
}
|
|
80
|
+
if (resp.status === 429 && attempt < maxAttempts - 1) {
|
|
81
|
+
const delay = extractRetryAfter(resp.headers) ?? calculateDelay(attempt, this.retryConfig);
|
|
82
|
+
this.logger.warn(`${service}: rate limited (429), waiting ${delay}ms`, { url });
|
|
83
|
+
await sleep(delay);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const body = await resp.text();
|
|
87
|
+
return {
|
|
88
|
+
status: resp.status,
|
|
89
|
+
headers: resp.headers,
|
|
90
|
+
body,
|
|
91
|
+
attempts: attempt + 1,
|
|
92
|
+
totalDurationMs: Date.now() - startTime,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (err instanceof CircuitOpenError)
|
|
97
|
+
throw err;
|
|
98
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
99
|
+
lastError = error;
|
|
100
|
+
this.breaker.recordFailure(error.message.slice(0, 200));
|
|
101
|
+
if (isRetryable(error, this.retryConfig) && attempt < maxAttempts - 1) {
|
|
102
|
+
const delay = calculateDelay(attempt, this.retryConfig);
|
|
103
|
+
this.logger.warn(`${service}: network error, retrying in ${delay}ms`, {
|
|
104
|
+
attempt: attempt + 1, error: error.message.slice(0, 100),
|
|
105
|
+
});
|
|
106
|
+
await sleep(delay);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw lastError ?? new Error(`${service}: all ${maxAttempts} attempts failed`);
|
|
112
|
+
}
|
|
113
|
+
getCircuitStatus() {
|
|
114
|
+
return this.breaker.getStatus();
|
|
115
|
+
}
|
|
116
|
+
get serviceName() {
|
|
117
|
+
return this.config.circuitBreaker.name;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=resilient-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilient-client.js","sourceRoot":"","sources":["../../src/resilience/resilient-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAgD,MAAM,sBAAsB,CAAC;AACpG,OAAO,EAEL,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,KAAK,GACN,MAAM,YAAY,CAAC;AAEpB,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAChC,OAAO,CAAS;IAChB,KAAK,CAAe;IACpB,mBAAmB,CAAS;IAErC,YAAY,OAAe,EAAE,mBAA2B;QACtD,KAAK,CAAC,4BAA4B,OAAO,oBAAoB,IAAI,CAAC,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACvG,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;QACpB,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;IACjD,CAAC;CACF;AAsBD,MAAM,OAAO,eAAe;IAON;IANZ,OAAO,CAAiB;IACxB,WAAW,CAAc;IACzB,SAAS,CAAS;IAClB,MAAM,CAA+C;IACrD,OAAO,CAA0B;IAEzC,YAAoB,MAA6B;QAA7B,WAAM,GAAN,MAAM,CAAuB;QAC/C,IAAI,CAAC,OAAO,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QACzD,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,KAAK,IAAI,oBAAoB,CAAC;QACxD,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC;QAClD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI;YAC7B,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;YACf,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;YACd,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;SAChB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW,EAAE,IAAkB;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC;QAEhD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;YAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;YACxC,MAAM,SAAS,GAAG,MAAM,CAAC,iBAAiB,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;YACnF,MAAM,IAAI,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;QAC9D,CAAC;QAED,IAAI,SAAS,GAAiB,IAAI,CAAC;QACnC,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC;QAEpD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;YACvD,IAAI,CAAC;gBACH,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,aAAa,OAAO,GAAG,CAAC,IAAI,WAAW,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;gBAEhF,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;gBACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;gBAErE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;oBACnC,GAAG,IAAI;oBACP,MAAM,EAAE,UAAU,CAAC,MAAM;iBAC1B,CAAC,CAAC;gBAEH,YAAY,CAAC,OAAO,CAAC,CAAC;gBAEtB,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC9C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;oBAC/C,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,QAAQ,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;oBAEzE,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,OAAO,GAAG,WAAW,GAAG,CAAC,EAAE,CAAC;wBAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,KAAK,GAAG;4BAC/B,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;4BAChF,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;wBAE9C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,UAAU,IAAI,CAAC,MAAM,iBAAiB,KAAK,IAAI,EAAE;4BAC1E,OAAO,EAAE,OAAO,GAAG,CAAC,EAAE,GAAG;yBAC1B,CAAC,CAAC;wBAEH,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;wBACnB,SAAS;oBACX,CAAC;oBAED,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,UAAU,IAAI,CAAC,MAAM,UAAU,OAAO,GAAG,CAAC,WAAW,CAAC,CAAC;gBACnF,CAAC;gBAED,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;oBAC5C,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;gBAC/B,CAAC;gBAED,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,OAAO,GAAG,WAAW,GAAG,CAAC,EAAE,CAAC;oBACrD,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;oBAC3F,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,iCAAiC,KAAK,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;oBAChF,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;oBACnB,SAAS;gBACX,CAAC;gBAED,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;gBAC/B,OAAO;oBACL,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,IAAI;oBACJ,QAAQ,EAAE,OAAO,GAAG,CAAC;oBACrB,eAAe,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;iBACxC,CAAC;YAEJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,gBAAgB;oBAAE,MAAM,GAAG,CAAC;gBAE/C,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBAClE,SAAS,GAAG,KAAK,CAAC;gBAElB,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;gBAExD,IAAI,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,OAAO,GAAG,WAAW,GAAG,CAAC,EAAE,CAAC;oBACtE,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;oBACxD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,gCAAgC,KAAK,IAAI,EAAE;wBACpE,OAAO,EAAE,OAAO,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;qBACzD,CAAC,CAAC;oBACH,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;oBACnB,SAAS;gBACX,CAAC;YACH,CAAC;QACH,CAAC;QAED,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,GAAG,OAAO,SAAS,WAAW,kBAAkB,CAAC,CAAC;IACjF,CAAC;IAED,gBAAgB;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;IAClC,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC;IACzC,CAAC;CACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry logic — exponential backoff with jitter, Retry-After support.
|
|
3
|
+
*
|
|
4
|
+
* Retry is separate from the circuit breaker: the breaker decides whether
|
|
5
|
+
* to attempt at all, retry decides how many times to reattempt on failure.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §4
|
|
8
|
+
*/
|
|
9
|
+
export interface RetryConfig {
|
|
10
|
+
maxRetries: number;
|
|
11
|
+
baseDelayMs: number;
|
|
12
|
+
maxDelayMs: number;
|
|
13
|
+
jitterFactor: number;
|
|
14
|
+
retryableStatusCodes: number[];
|
|
15
|
+
retryOnNetworkError: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
|
|
18
|
+
export declare function calculateDelay(attempt: number, config: RetryConfig): number;
|
|
19
|
+
export declare function isRetryable(statusOrError: number | Error, config: RetryConfig): boolean;
|
|
20
|
+
export declare function extractRetryAfter(headers: Headers | Record<string, string>): number | null;
|
|
21
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/resilience/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,eAAO,MAAM,oBAAoB,EAAE,WAOlC,CAAC;AAEF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAK3E;AAED,wBAAgB,WAAW,CACzB,aAAa,EAAE,MAAM,GAAG,KAAK,EAC7B,MAAM,EAAE,WAAW,GAClB,OAAO,CAkBT;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAkB1F;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAYrE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry logic — exponential backoff with jitter, Retry-After support.
|
|
3
|
+
*
|
|
4
|
+
* Retry is separate from the circuit breaker: the breaker decides whether
|
|
5
|
+
* to attempt at all, retry decides how many times to reattempt on failure.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §4
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_RETRY_CONFIG = {
|
|
10
|
+
maxRetries: 3,
|
|
11
|
+
baseDelayMs: 1_000,
|
|
12
|
+
maxDelayMs: 30_000,
|
|
13
|
+
jitterFactor: 0.25,
|
|
14
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
15
|
+
retryOnNetworkError: true,
|
|
16
|
+
};
|
|
17
|
+
export function calculateDelay(attempt, config) {
|
|
18
|
+
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
|
|
19
|
+
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
20
|
+
const jitter = cappedDelay * config.jitterFactor * (Math.random() * 2 - 1);
|
|
21
|
+
return Math.max(0, Math.round(cappedDelay + jitter));
|
|
22
|
+
}
|
|
23
|
+
export function isRetryable(statusOrError, config) {
|
|
24
|
+
if (typeof statusOrError === 'number') {
|
|
25
|
+
return config.retryableStatusCodes.includes(statusOrError);
|
|
26
|
+
}
|
|
27
|
+
if (!config.retryOnNetworkError)
|
|
28
|
+
return false;
|
|
29
|
+
const msg = statusOrError.message.toLowerCase();
|
|
30
|
+
return (msg.includes('econnrefused') ||
|
|
31
|
+
msg.includes('econnreset') ||
|
|
32
|
+
msg.includes('etimedout') ||
|
|
33
|
+
msg.includes('enotfound') ||
|
|
34
|
+
msg.includes('epipe') ||
|
|
35
|
+
msg.includes('socket hang up') ||
|
|
36
|
+
msg.includes('fetch failed') ||
|
|
37
|
+
msg.includes('abort'));
|
|
38
|
+
}
|
|
39
|
+
export function extractRetryAfter(headers) {
|
|
40
|
+
const value = headers instanceof Headers
|
|
41
|
+
? headers.get('retry-after')
|
|
42
|
+
: headers['retry-after'];
|
|
43
|
+
if (!value)
|
|
44
|
+
return null;
|
|
45
|
+
const seconds = parseInt(value, 10);
|
|
46
|
+
if (!isNaN(seconds)) {
|
|
47
|
+
return seconds * 1000;
|
|
48
|
+
}
|
|
49
|
+
const date = new Date(value);
|
|
50
|
+
if (!isNaN(date.getTime())) {
|
|
51
|
+
return Math.max(0, date.getTime() - Date.now());
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
export function sleep(ms, signal) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
if (signal?.aborted) {
|
|
58
|
+
reject(new Error('Aborted'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const timer = setTimeout(resolve, ms);
|
|
62
|
+
signal?.addEventListener('abort', () => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
reject(new Error('Aborted'));
|
|
65
|
+
}, { once: true });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/resilience/retry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-service circuit breaker configurations.
|
|
3
|
+
*
|
|
4
|
+
* Each service has different failure characteristics — these configs
|
|
5
|
+
* are tuned to match expected behavior and recovery patterns.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §3.3
|
|
8
|
+
*/
|
|
9
|
+
import type { CircuitBreakerConfig } from './circuit-breaker.js';
|
|
10
|
+
export declare const PLATFORM_API_BREAKER: Omit<CircuitBreakerConfig, 'onStateChange'>;
|
|
11
|
+
export declare const X_API_BREAKER: Omit<CircuitBreakerConfig, 'onStateChange'>;
|
|
12
|
+
export declare const MOLTBOOK_API_BREAKER: Omit<CircuitBreakerConfig, 'onStateChange'>;
|
|
13
|
+
export declare const BLOCKCHAIN_BREAKER: Omit<CircuitBreakerConfig, 'onStateChange'>;
|
|
14
|
+
//# sourceMappingURL=service-configs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-configs.d.ts","sourceRoot":"","sources":["../../src/resilience/service-configs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,eAAO,MAAM,oBAAoB,EAAE,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAQ5E,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAQrE,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAQ5E,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAQ1E,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-service circuit breaker configurations.
|
|
3
|
+
*
|
|
4
|
+
* Each service has different failure characteristics — these configs
|
|
5
|
+
* are tuned to match expected behavior and recovery patterns.
|
|
6
|
+
*
|
|
7
|
+
* @see SPEC-nole-operational-resilience.md §3.3
|
|
8
|
+
*/
|
|
9
|
+
export const PLATFORM_API_BREAKER = {
|
|
10
|
+
name: 'platform-api',
|
|
11
|
+
failureThreshold: 3,
|
|
12
|
+
failureWindowMs: 60_000,
|
|
13
|
+
cooldownMs: 30_000,
|
|
14
|
+
maxCooldownMs: 5 * 60_000,
|
|
15
|
+
cooldownMultiplier: 2,
|
|
16
|
+
failureStatusCodes: [500, 502, 503, 504],
|
|
17
|
+
};
|
|
18
|
+
export const X_API_BREAKER = {
|
|
19
|
+
name: 'x-twitter',
|
|
20
|
+
failureThreshold: 5,
|
|
21
|
+
failureWindowMs: 5 * 60_000,
|
|
22
|
+
cooldownMs: 2 * 60_000,
|
|
23
|
+
maxCooldownMs: 30 * 60_000,
|
|
24
|
+
cooldownMultiplier: 2,
|
|
25
|
+
failureStatusCodes: [500, 502, 503, 504],
|
|
26
|
+
};
|
|
27
|
+
export const MOLTBOOK_API_BREAKER = {
|
|
28
|
+
name: 'moltbook',
|
|
29
|
+
failureThreshold: 3,
|
|
30
|
+
failureWindowMs: 2 * 60_000,
|
|
31
|
+
cooldownMs: 60_000,
|
|
32
|
+
maxCooldownMs: 10 * 60_000,
|
|
33
|
+
cooldownMultiplier: 2,
|
|
34
|
+
failureStatusCodes: [500, 502, 503, 504],
|
|
35
|
+
};
|
|
36
|
+
export const BLOCKCHAIN_BREAKER = {
|
|
37
|
+
name: 'blockchain',
|
|
38
|
+
failureThreshold: 5,
|
|
39
|
+
failureWindowMs: 10 * 60_000,
|
|
40
|
+
cooldownMs: 5 * 60_000,
|
|
41
|
+
maxCooldownMs: 60 * 60_000,
|
|
42
|
+
cooldownMultiplier: 2,
|
|
43
|
+
failureStatusCodes: [500, 502, 503, 504],
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=service-configs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-configs.js","sourceRoot":"","sources":["../../src/resilience/service-configs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,CAAC,MAAM,oBAAoB,GAAgD;IAC/E,IAAI,EAAE,cAAc;IACpB,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,MAAM;IACvB,UAAU,EAAE,MAAM;IAClB,aAAa,EAAE,CAAC,GAAG,MAAM;IACzB,kBAAkB,EAAE,CAAC;IACrB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;CACzC,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAgD;IACxE,IAAI,EAAE,WAAW;IACjB,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,CAAC,GAAG,MAAM;IAC3B,UAAU,EAAE,CAAC,GAAG,MAAM;IACtB,aAAa,EAAE,EAAE,GAAG,MAAM;IAC1B,kBAAkB,EAAE,CAAC;IACrB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;CACzC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAgD;IAC/E,IAAI,EAAE,UAAU;IAChB,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,CAAC,GAAG,MAAM;IAC3B,UAAU,EAAE,MAAM;IAClB,aAAa,EAAE,EAAE,GAAG,MAAM;IAC1B,kBAAkB,EAAE,CAAC;IACrB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;CACzC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAgD;IAC7E,IAAI,EAAE,YAAY;IAClB,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,EAAE,GAAG,MAAM;IAC5B,UAAU,EAAE,CAAC,GAAG,MAAM;IACtB,aAAa,EAAE,EAAE,GAAG,MAAM;IAC1B,kBAAkB,EAAE,CAAC;IACrB,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;CACzC,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Registry — Degraded-mode management.
|
|
3
|
+
*
|
|
4
|
+
* Tracks which external services are healthy, degraded, or down.
|
|
5
|
+
* Nole's business logic queries this to adapt behavior.
|
|
6
|
+
* Writes a status file that Mighty Mark can read (read-only contract).
|
|
7
|
+
*
|
|
8
|
+
* @see SPEC-nole-operational-resilience.md §6
|
|
9
|
+
*/
|
|
10
|
+
import type { ResilientClient } from './resilient-client.js';
|
|
11
|
+
export type ServiceHealth = 'healthy' | 'degraded' | 'down';
|
|
12
|
+
export interface ServiceStatus {
|
|
13
|
+
name: string;
|
|
14
|
+
health: ServiceHealth;
|
|
15
|
+
circuitState: string;
|
|
16
|
+
failureCount: number;
|
|
17
|
+
lastSuccessfulCall: number | null;
|
|
18
|
+
lastFailedCall: number | null;
|
|
19
|
+
recentErrors: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface RegistrySnapshot {
|
|
22
|
+
timestamp: number;
|
|
23
|
+
overallHealth: ServiceHealth;
|
|
24
|
+
services: Record<string, ServiceStatus>;
|
|
25
|
+
}
|
|
26
|
+
export declare class ServiceRegistry {
|
|
27
|
+
private clients;
|
|
28
|
+
private lastSuccess;
|
|
29
|
+
private lastFailure;
|
|
30
|
+
register(client: ResilientClient): void;
|
|
31
|
+
recordSuccess(serviceName: string): void;
|
|
32
|
+
recordFailure(serviceName: string): void;
|
|
33
|
+
getHealth(serviceName: string): ServiceHealth;
|
|
34
|
+
getOverallHealth(): ServiceHealth;
|
|
35
|
+
getSnapshot(): RegistrySnapshot;
|
|
36
|
+
writeStatusFile(dataDir: string): Promise<void>;
|
|
37
|
+
isAvailable(serviceName: string): boolean;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=service-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-registry.d.ts","sourceRoot":"","sources":["../../src/resilience/service-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACzC;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,WAAW,CAA6B;IAEhD,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;IAIvC,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAIxC,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAIxC,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa;IAc7C,gBAAgB,IAAI,aAAa;IAUjC,WAAW,IAAI,gBAAgB;IAuBzB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAerD,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;CAG1C"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Registry — Degraded-mode management.
|
|
3
|
+
*
|
|
4
|
+
* Tracks which external services are healthy, degraded, or down.
|
|
5
|
+
* Nole's business logic queries this to adapt behavior.
|
|
6
|
+
* Writes a status file that Mighty Mark can read (read-only contract).
|
|
7
|
+
*
|
|
8
|
+
* @see SPEC-nole-operational-resilience.md §6
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
export class ServiceRegistry {
|
|
13
|
+
clients = new Map();
|
|
14
|
+
lastSuccess = new Map();
|
|
15
|
+
lastFailure = new Map();
|
|
16
|
+
register(client) {
|
|
17
|
+
this.clients.set(client.serviceName, client);
|
|
18
|
+
}
|
|
19
|
+
recordSuccess(serviceName) {
|
|
20
|
+
this.lastSuccess.set(serviceName, Date.now());
|
|
21
|
+
}
|
|
22
|
+
recordFailure(serviceName) {
|
|
23
|
+
this.lastFailure.set(serviceName, Date.now());
|
|
24
|
+
}
|
|
25
|
+
getHealth(serviceName) {
|
|
26
|
+
const client = this.clients.get(serviceName);
|
|
27
|
+
if (!client)
|
|
28
|
+
return 'down';
|
|
29
|
+
const status = client.getCircuitStatus();
|
|
30
|
+
switch (status.state) {
|
|
31
|
+
case 'open': return 'down';
|
|
32
|
+
case 'half-open': return 'degraded';
|
|
33
|
+
case 'closed':
|
|
34
|
+
return status.failureCount > 0 ? 'degraded' : 'healthy';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
getOverallHealth() {
|
|
38
|
+
let worst = 'healthy';
|
|
39
|
+
for (const [name] of this.clients) {
|
|
40
|
+
const health = this.getHealth(name);
|
|
41
|
+
if (health === 'down')
|
|
42
|
+
return 'down';
|
|
43
|
+
if (health === 'degraded')
|
|
44
|
+
worst = 'degraded';
|
|
45
|
+
}
|
|
46
|
+
return worst;
|
|
47
|
+
}
|
|
48
|
+
getSnapshot() {
|
|
49
|
+
const services = {};
|
|
50
|
+
for (const [name, client] of this.clients) {
|
|
51
|
+
const circuit = client.getCircuitStatus();
|
|
52
|
+
services[name] = {
|
|
53
|
+
name,
|
|
54
|
+
health: this.getHealth(name),
|
|
55
|
+
circuitState: circuit.state,
|
|
56
|
+
failureCount: circuit.failureCount,
|
|
57
|
+
lastSuccessfulCall: this.lastSuccess.get(name) ?? null,
|
|
58
|
+
lastFailedCall: this.lastFailure.get(name) ?? null,
|
|
59
|
+
recentErrors: circuit.recentFailures,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
overallHealth: this.getOverallHealth(),
|
|
65
|
+
services,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async writeStatusFile(dataDir) {
|
|
69
|
+
const snapshot = this.getSnapshot();
|
|
70
|
+
const statusPath = path.join(dataDir, 'service-health.json');
|
|
71
|
+
try {
|
|
72
|
+
await fs.promises.writeFile(statusPath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Non-fatal — status file is informational
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
isAvailable(serviceName) {
|
|
79
|
+
return this.getHealth(serviceName) !== 'down';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=service-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-registry.js","sourceRoot":"","sources":["../../src/resilience/service-registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiassesstech/nole",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Nole — Autonomous Trust Evangelist & Intelligence Operative for AI. Economic agency, social presence, and autonomous decision-making within a governed hierarchy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|