pi-harness-runtime 1.1.335 → 1.1.337
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/index.ts +21 -12
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/dist/index.d.ts +11 -0
- package/packages/connection-runtime/dist/index.js +11 -0
- package/packages/connection-runtime/dist/registry.d.ts +69 -0
- package/packages/connection-runtime/dist/registry.js +117 -0
- package/packages/connection-runtime/dist/supervisor.d.ts +89 -0
- package/packages/connection-runtime/dist/supervisor.js +169 -0
- package/packages/connection-runtime/dist/types.d.ts +70 -0
- package/packages/connection-runtime/dist/types.js +16 -0
- package/packages/connection-runtime/package.json +24 -0
- package/packages/connection-runtime/src/index.ts +12 -0
- package/packages/connection-runtime/src/registry.ts +142 -0
- package/packages/connection-runtime/src/supervisor.ts +224 -0
- package/packages/connection-runtime/src/types.ts +88 -0
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/approvals.d.ts +273 -0
- package/packages/contracts/dist/approvals.js +93 -0
- package/packages/contracts/dist/base.d.ts +78 -0
- package/packages/contracts/dist/base.js +82 -0
- package/packages/contracts/dist/events.d.ts +99 -0
- package/packages/contracts/dist/events.js +65 -0
- package/packages/contracts/dist/index.d.ts +20 -0
- package/packages/contracts/dist/index.js +20 -0
- package/packages/contracts/dist/jobs.d.ts +234 -0
- package/packages/contracts/dist/jobs.js +79 -0
- package/packages/contracts/dist/notifications.d.ts +317 -0
- package/packages/contracts/dist/notifications.js +85 -0
- package/packages/contracts/dist/projects.d.ts +171 -0
- package/packages/contracts/dist/projects.js +62 -0
- package/packages/contracts/dist/sessions.d.ts +226 -0
- package/packages/contracts/dist/sessions.js +82 -0
- package/packages/contracts/package.json +24 -0
- package/packages/contracts/src/approvals.ts +115 -0
- package/packages/contracts/src/base.ts +126 -0
- package/packages/contracts/src/events.ts +78 -0
- package/packages/contracts/src/index.ts +21 -0
- package/packages/contracts/src/jobs.ts +101 -0
- package/packages/contracts/src/notifications.ts +104 -0
- package/packages/contracts/src/projects.ts +81 -0
- package/packages/contracts/src/sessions.ts +104 -0
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/dist/index.d.ts +12 -0
- package/packages/event-sink/dist/index.js +12 -0
- package/packages/event-sink/dist/outbox.d.ts +51 -0
- package/packages/event-sink/dist/outbox.js +88 -0
- package/packages/event-sink/dist/projection.d.ts +41 -0
- package/packages/event-sink/dist/projection.js +62 -0
- package/packages/event-sink/dist/sink.d.ts +81 -0
- package/packages/event-sink/dist/sink.js +106 -0
- package/packages/event-sink/package.json +28 -0
- package/packages/event-sink/src/index.ts +15 -0
- package/packages/event-sink/src/outbox.ts +119 -0
- package/packages/event-sink/src/projection.ts +84 -0
- package/packages/event-sink/src/sink.ts +167 -0
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/dist/checker.d.ts +66 -0
- package/packages/permission-mode/dist/checker.js +64 -0
- package/packages/permission-mode/dist/index.d.ts +12 -0
- package/packages/permission-mode/dist/index.js +11 -0
- package/packages/permission-mode/dist/manager.d.ts +65 -0
- package/packages/permission-mode/dist/manager.js +91 -0
- package/packages/permission-mode/dist/mode.d.ts +31 -0
- package/packages/permission-mode/dist/mode.js +47 -0
- package/packages/permission-mode/package.json +24 -0
- package/packages/permission-mode/src/checker.ts +103 -0
- package/packages/permission-mode/src/index.ts +13 -0
- package/packages/permission-mode/src/manager.ts +154 -0
- package/packages/permission-mode/src/mode.ts +73 -0
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/dist/client.d.ts +53 -0
- package/packages/relay/dist/client.js +95 -0
- package/packages/relay/dist/index.d.ts +9 -0
- package/packages/relay/dist/index.js +8 -0
- package/packages/relay/dist/server.d.ts +58 -0
- package/packages/relay/dist/server.js +93 -0
- package/packages/relay/dist/types.d.ts +35 -0
- package/packages/relay/dist/types.js +4 -0
- package/packages/relay/package.json +24 -0
- package/packages/relay/src/client.ts +119 -0
- package/packages/relay/src/index.ts +10 -0
- package/packages/relay/src/server.ts +120 -0
- package/packages/relay/src/types.ts +38 -0
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection Store
|
|
3
|
+
*
|
|
4
|
+
* Maintains materialized views of events for efficient queries.
|
|
5
|
+
* Projections are updated atomically with events.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Projection Store
|
|
9
|
+
*/
|
|
10
|
+
export class ProjectionStore {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
projections = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Register a projection
|
|
15
|
+
*/
|
|
16
|
+
register(projection) {
|
|
17
|
+
this.projections.set(projection.name, {
|
|
18
|
+
name: projection.name,
|
|
19
|
+
state: projection.initialState,
|
|
20
|
+
initialState: projection.initialState,
|
|
21
|
+
reduce: projection.reduce,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Apply events to all projections
|
|
26
|
+
*/
|
|
27
|
+
apply(events) {
|
|
28
|
+
for (const event of events) {
|
|
29
|
+
for (const [name, entry] of this.projections.entries()) {
|
|
30
|
+
try {
|
|
31
|
+
entry.state = entry.reduce(entry.state, event);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Silently ignore projection errors - don't crash the event loop
|
|
35
|
+
// callers can check projection state after apply()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get projection state
|
|
42
|
+
*/
|
|
43
|
+
get(name) {
|
|
44
|
+
const entry = this.projections.get(name);
|
|
45
|
+
return entry ? entry.state : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reset projection to initial state
|
|
49
|
+
*/
|
|
50
|
+
reset(name) {
|
|
51
|
+
const entry = this.projections.get(name);
|
|
52
|
+
if (entry) {
|
|
53
|
+
entry.state = entry.initialState;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* List all projection names
|
|
58
|
+
*/
|
|
59
|
+
list() {
|
|
60
|
+
return Array.from(this.projections.keys());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Sink - Atomic Event Sourcing
|
|
3
|
+
*
|
|
4
|
+
* Inspired by T3Code's orchestrator:
|
|
5
|
+
* - Events + projections + outbox effects committed atomically
|
|
6
|
+
* - Command acknowledgement = intent committed, not work finished
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Event with metadata
|
|
10
|
+
*/
|
|
11
|
+
export interface StoredEvent {
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
workspaceId?: string;
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
jobId?: string;
|
|
18
|
+
payload?: Record<string, unknown>;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
commandId?: string;
|
|
21
|
+
sequence: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Outbox entry for side effects
|
|
25
|
+
*/
|
|
26
|
+
export interface OutboxEntry {
|
|
27
|
+
id: string;
|
|
28
|
+
eventId: string;
|
|
29
|
+
effect: string;
|
|
30
|
+
payload: unknown;
|
|
31
|
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
32
|
+
attempts: number;
|
|
33
|
+
lastError?: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
processedAt?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Event Sink options
|
|
39
|
+
*/
|
|
40
|
+
export interface EventSinkOptions {
|
|
41
|
+
/** Database file path */
|
|
42
|
+
dbPath: string;
|
|
43
|
+
/** Called when events are committed */
|
|
44
|
+
onEventsCommitted?: (events: StoredEvent[]) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Event Sink - Atomic event sourcing
|
|
48
|
+
*/
|
|
49
|
+
export declare class EventSink {
|
|
50
|
+
private db;
|
|
51
|
+
private sequence;
|
|
52
|
+
private onEventsCommitted?;
|
|
53
|
+
constructor(options: EventSinkOptions);
|
|
54
|
+
/**
|
|
55
|
+
* Commit events atomically with projections and outbox
|
|
56
|
+
*/
|
|
57
|
+
commit<T extends StoredEvent>(events: T[], outboxEffects?: Array<{
|
|
58
|
+
effect: string;
|
|
59
|
+
payload: unknown;
|
|
60
|
+
}>): Promise<{
|
|
61
|
+
events: StoredEvent[];
|
|
62
|
+
outbox: OutboxEntry[];
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Get events by filter
|
|
66
|
+
*/
|
|
67
|
+
getEvents(filter?: {
|
|
68
|
+
types?: string[];
|
|
69
|
+
since?: number;
|
|
70
|
+
until?: number;
|
|
71
|
+
limit?: number;
|
|
72
|
+
}): Promise<StoredEvent[]>;
|
|
73
|
+
/**
|
|
74
|
+
* Get pending outbox entries
|
|
75
|
+
*/
|
|
76
|
+
getPendingOutbox(limit?: number): Promise<OutboxEntry[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Update outbox entry status
|
|
79
|
+
*/
|
|
80
|
+
updateOutboxStatus(id: string, status: OutboxEntry['status'], error?: string): Promise<void>;
|
|
81
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Sink - Atomic Event Sourcing
|
|
3
|
+
*
|
|
4
|
+
* Inspired by T3Code's orchestrator:
|
|
5
|
+
* - Events + projections + outbox effects committed atomically
|
|
6
|
+
* - Command acknowledgement = intent committed, not work finished
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Event Sink - Atomic event sourcing
|
|
10
|
+
*/
|
|
11
|
+
export class EventSink {
|
|
12
|
+
db = new Map();
|
|
13
|
+
sequence = 0;
|
|
14
|
+
onEventsCommitted;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.onEventsCommitted = options.onEventsCommitted;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Commit events atomically with projections and outbox
|
|
20
|
+
*/
|
|
21
|
+
async commit(events, outboxEffects = []) {
|
|
22
|
+
const committedEvents = [];
|
|
23
|
+
const outbox = [];
|
|
24
|
+
// Assign sequence numbers atomically
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
this.sequence++;
|
|
27
|
+
const storedEvent = {
|
|
28
|
+
...event,
|
|
29
|
+
sequence: this.sequence,
|
|
30
|
+
};
|
|
31
|
+
committedEvents.push(storedEvent);
|
|
32
|
+
// Store event
|
|
33
|
+
this.db.set(`event:${storedEvent.id}`, storedEvent);
|
|
34
|
+
}
|
|
35
|
+
// Create outbox entries
|
|
36
|
+
for (const effect of outboxEffects) {
|
|
37
|
+
const entry = {
|
|
38
|
+
id: crypto.randomUUID(),
|
|
39
|
+
eventId: committedEvents[0]?.id || '',
|
|
40
|
+
effect: effect.effect,
|
|
41
|
+
payload: effect.payload,
|
|
42
|
+
status: 'pending',
|
|
43
|
+
attempts: 0,
|
|
44
|
+
createdAt: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
outbox.push(entry);
|
|
47
|
+
this.db.set(`outbox:${entry.id}`, entry);
|
|
48
|
+
}
|
|
49
|
+
// Notify listeners
|
|
50
|
+
this.onEventsCommitted?.(committedEvents);
|
|
51
|
+
return { events: committedEvents, outbox };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get events by filter
|
|
55
|
+
*/
|
|
56
|
+
async getEvents(filter) {
|
|
57
|
+
const events = [];
|
|
58
|
+
const limit = filter?.limit || 100;
|
|
59
|
+
for (const [key, value] of this.db.entries()) {
|
|
60
|
+
if (key.startsWith('event:') && typeof value === 'object') {
|
|
61
|
+
const event = value;
|
|
62
|
+
if (filter?.types && !filter.types.includes(event.type))
|
|
63
|
+
continue;
|
|
64
|
+
if (filter?.since && event.sequence <= filter.since)
|
|
65
|
+
continue;
|
|
66
|
+
if (filter?.until && event.sequence >= filter.until)
|
|
67
|
+
continue;
|
|
68
|
+
events.push(event);
|
|
69
|
+
if (events.length >= limit)
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return events.sort((a, b) => a.sequence - b.sequence);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get pending outbox entries
|
|
77
|
+
*/
|
|
78
|
+
async getPendingOutbox(limit = 10) {
|
|
79
|
+
const entries = [];
|
|
80
|
+
for (const [key, value] of this.db.entries()) {
|
|
81
|
+
if (key.startsWith('outbox:') && typeof value === 'object') {
|
|
82
|
+
const entry = value;
|
|
83
|
+
if (entry.status === 'pending' || entry.status === 'failed') {
|
|
84
|
+
entries.push(entry);
|
|
85
|
+
if (entries.length >= limit)
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Update outbox entry status
|
|
94
|
+
*/
|
|
95
|
+
async updateOutboxStatus(id, status, error) {
|
|
96
|
+
const entry = this.db.get(`outbox:${id}`);
|
|
97
|
+
if (entry) {
|
|
98
|
+
entry.status = status;
|
|
99
|
+
entry.lastError = error;
|
|
100
|
+
entry.attempts++;
|
|
101
|
+
if (status === 'completed') {
|
|
102
|
+
entry.processedAt = new Date().toISOString();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-harness/event-sink",
|
|
3
|
+
"version": "1.1.337",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Atomic event sourcing with SQLite transactions for pi-harness",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "bun test"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@pi-harness/contracts": "workspace:*",
|
|
20
|
+
"better-sqlite3": "^11.7.0",
|
|
21
|
+
"uuid": "^11.0.3"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.7.2",
|
|
25
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
26
|
+
"@types/uuid": "^10.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Sink - Atomic Event Sourcing
|
|
3
|
+
*
|
|
4
|
+
* Inspired by T3Code's orchestrator event sourcing:
|
|
5
|
+
* - Commands serialize to event log
|
|
6
|
+
* - EventSink commits events + projections + outbox effects atomically
|
|
7
|
+
* - EffectWorker performs side effects AFTER intent commits
|
|
8
|
+
* - Command acknowledgement = intent committed, not work finished
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { EventCreate } from '@pi-harness/contracts';
|
|
12
|
+
|
|
13
|
+
export { EventSink } from './sink.js';
|
|
14
|
+
export { ProjectionStore } from './projection.js';
|
|
15
|
+
export { OutboxWorker } from './outbox.js';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbox Worker
|
|
3
|
+
*
|
|
4
|
+
* Processes outbox entries after events are committed.
|
|
5
|
+
* Side effects are performed asynchronously.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OutboxEntry } from './sink.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Effect handler function
|
|
12
|
+
*/
|
|
13
|
+
export type EffectHandler = (payload: unknown) => Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Outbox Worker options
|
|
17
|
+
*/
|
|
18
|
+
export interface OutboxWorkerOptions {
|
|
19
|
+
/** Maximum retry attempts */
|
|
20
|
+
maxRetries?: number;
|
|
21
|
+
/** Retry delay in ms */
|
|
22
|
+
retryDelay?: number;
|
|
23
|
+
/** Effect handlers */
|
|
24
|
+
handlers?: Record<string, EffectHandler>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Outbox Worker
|
|
29
|
+
*
|
|
30
|
+
* Processes pending outbox entries and executes their effects.
|
|
31
|
+
*/
|
|
32
|
+
export class OutboxWorker {
|
|
33
|
+
private maxRetries: number;
|
|
34
|
+
private retryDelay: number;
|
|
35
|
+
private handlers: Map<string, EffectHandler> = new Map();
|
|
36
|
+
private running = false;
|
|
37
|
+
private interval?: ReturnType<typeof setInterval>;
|
|
38
|
+
|
|
39
|
+
constructor(options: OutboxWorkerOptions = {}) {
|
|
40
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
41
|
+
this.retryDelay = options.retryDelay ?? 1000;
|
|
42
|
+
|
|
43
|
+
if (options.handlers) {
|
|
44
|
+
for (const [name, handler] of Object.entries(options.handlers)) {
|
|
45
|
+
this.handlers.set(name, handler);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register an effect handler
|
|
52
|
+
*/
|
|
53
|
+
register(effect: string, handler: EffectHandler): void {
|
|
54
|
+
this.handlers.set(effect, handler);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start processing outbox
|
|
59
|
+
*/
|
|
60
|
+
start(
|
|
61
|
+
getPending: () => Promise<OutboxEntry[]>,
|
|
62
|
+
updateStatus: (id: string, status: OutboxEntry['status'], error?: string) => Promise<void>,
|
|
63
|
+
pollInterval = 1000,
|
|
64
|
+
): void {
|
|
65
|
+
if (this.running) return;
|
|
66
|
+
this.running = true;
|
|
67
|
+
|
|
68
|
+
this.interval = setInterval(async () => {
|
|
69
|
+
if (!this.running) return;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const entries = await getPending();
|
|
73
|
+
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!this.running) break;
|
|
76
|
+
|
|
77
|
+
if (entry.attempts >= this.maxRetries) {
|
|
78
|
+
await updateStatus(entry.id, 'failed', 'Max retries exceeded');
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handler = this.handlers.get(entry.effect);
|
|
83
|
+
if (!handler) {
|
|
84
|
+
await updateStatus(entry.id, 'failed', `No handler for effect: ${entry.effect}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await updateStatus(entry.id, 'processing');
|
|
90
|
+
await handler(entry.payload);
|
|
91
|
+
await updateStatus(entry.id, 'completed');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
await updateStatus(entry.id, 'failed', String(error));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('[OutboxWorker] Error processing outbox:', error);
|
|
98
|
+
}
|
|
99
|
+
}, pollInterval);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stop processing
|
|
104
|
+
*/
|
|
105
|
+
stop(): void {
|
|
106
|
+
this.running = false;
|
|
107
|
+
if (this.interval) {
|
|
108
|
+
clearInterval(this.interval);
|
|
109
|
+
this.interval = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if running
|
|
115
|
+
*/
|
|
116
|
+
isRunning(): boolean {
|
|
117
|
+
return this.running;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection Store
|
|
3
|
+
*
|
|
4
|
+
* Maintains materialized views of events for efficient queries.
|
|
5
|
+
* Projections are updated atomically with events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { StoredEvent } from './sink.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Projection function type
|
|
12
|
+
*/
|
|
13
|
+
export type ProjectionFn<T> = (state: T | null, event: StoredEvent) => T;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal projection entry with state
|
|
17
|
+
*/
|
|
18
|
+
interface ProjectionEntry<T> {
|
|
19
|
+
name: string;
|
|
20
|
+
state: T;
|
|
21
|
+
initialState: T;
|
|
22
|
+
reduce: ProjectionFn<T>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Projection Store
|
|
27
|
+
*/
|
|
28
|
+
export class ProjectionStore {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
private projections = new Map<string, ProjectionEntry<any>>();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a projection
|
|
34
|
+
*/
|
|
35
|
+
register<T>(projection: { name: string; initialState: T; reduce: ProjectionFn<T> }): void {
|
|
36
|
+
this.projections.set(projection.name, {
|
|
37
|
+
name: projection.name,
|
|
38
|
+
state: projection.initialState,
|
|
39
|
+
initialState: projection.initialState,
|
|
40
|
+
reduce: projection.reduce,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Apply events to all projections
|
|
46
|
+
*/
|
|
47
|
+
apply(events: StoredEvent[]): void {
|
|
48
|
+
for (const event of events) {
|
|
49
|
+
for (const [name, entry] of this.projections.entries()) {
|
|
50
|
+
try {
|
|
51
|
+
entry.state = entry.reduce(entry.state, event);
|
|
52
|
+
} catch {
|
|
53
|
+
// Silently ignore projection errors - don't crash the event loop
|
|
54
|
+
// callers can check projection state after apply()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get projection state
|
|
62
|
+
*/
|
|
63
|
+
get<T>(name: string): T | null {
|
|
64
|
+
const entry = this.projections.get(name);
|
|
65
|
+
return entry ? entry.state : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reset projection to initial state
|
|
70
|
+
*/
|
|
71
|
+
reset(name: string): void {
|
|
72
|
+
const entry = this.projections.get(name);
|
|
73
|
+
if (entry) {
|
|
74
|
+
entry.state = entry.initialState;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* List all projection names
|
|
80
|
+
*/
|
|
81
|
+
list(): string[] {
|
|
82
|
+
return Array.from(this.projections.keys());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Sink - Atomic Event Sourcing
|
|
3
|
+
*
|
|
4
|
+
* Inspired by T3Code's orchestrator:
|
|
5
|
+
* - Events + projections + outbox effects committed atomically
|
|
6
|
+
* - Command acknowledgement = intent committed, not work finished
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Event with metadata
|
|
11
|
+
*/
|
|
12
|
+
export interface StoredEvent {
|
|
13
|
+
id: string;
|
|
14
|
+
type: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
workspaceId?: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
jobId?: string;
|
|
19
|
+
payload?: Record<string, unknown>;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
commandId?: string;
|
|
22
|
+
sequence: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Outbox entry for side effects
|
|
27
|
+
*/
|
|
28
|
+
export interface OutboxEntry {
|
|
29
|
+
id: string;
|
|
30
|
+
eventId: string;
|
|
31
|
+
effect: string;
|
|
32
|
+
payload: unknown;
|
|
33
|
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
34
|
+
attempts: number;
|
|
35
|
+
lastError?: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
processedAt?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Event Sink options
|
|
42
|
+
*/
|
|
43
|
+
export interface EventSinkOptions {
|
|
44
|
+
/** Database file path */
|
|
45
|
+
dbPath: string;
|
|
46
|
+
/** Called when events are committed */
|
|
47
|
+
onEventsCommitted?: (events: StoredEvent[]) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Event Sink - Atomic event sourcing
|
|
52
|
+
*/
|
|
53
|
+
export class EventSink {
|
|
54
|
+
private db: Map<string, unknown> = new Map();
|
|
55
|
+
private sequence = 0;
|
|
56
|
+
private onEventsCommitted?: (events: StoredEvent[]) => void;
|
|
57
|
+
|
|
58
|
+
constructor(options: EventSinkOptions) {
|
|
59
|
+
this.onEventsCommitted = options.onEventsCommitted;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Commit events atomically with projections and outbox
|
|
64
|
+
*/
|
|
65
|
+
async commit<T extends StoredEvent>(
|
|
66
|
+
events: T[],
|
|
67
|
+
outboxEffects: Array<{ effect: string; payload: unknown }> = [],
|
|
68
|
+
): Promise<{ events: StoredEvent[]; outbox: OutboxEntry[] }> {
|
|
69
|
+
const committedEvents: StoredEvent[] = [];
|
|
70
|
+
const outbox: OutboxEntry[] = [];
|
|
71
|
+
|
|
72
|
+
// Assign sequence numbers atomically
|
|
73
|
+
for (const event of events) {
|
|
74
|
+
this.sequence++;
|
|
75
|
+
const storedEvent: StoredEvent = {
|
|
76
|
+
...event,
|
|
77
|
+
sequence: this.sequence,
|
|
78
|
+
};
|
|
79
|
+
committedEvents.push(storedEvent);
|
|
80
|
+
|
|
81
|
+
// Store event
|
|
82
|
+
this.db.set(`event:${storedEvent.id}`, storedEvent);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create outbox entries
|
|
86
|
+
for (const effect of outboxEffects) {
|
|
87
|
+
const entry: OutboxEntry = {
|
|
88
|
+
id: crypto.randomUUID(),
|
|
89
|
+
eventId: committedEvents[0]?.id || '',
|
|
90
|
+
effect: effect.effect,
|
|
91
|
+
payload: effect.payload,
|
|
92
|
+
status: 'pending',
|
|
93
|
+
attempts: 0,
|
|
94
|
+
createdAt: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
outbox.push(entry);
|
|
97
|
+
this.db.set(`outbox:${entry.id}`, entry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Notify listeners
|
|
101
|
+
this.onEventsCommitted?.(committedEvents);
|
|
102
|
+
|
|
103
|
+
return { events: committedEvents, outbox };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get events by filter
|
|
108
|
+
*/
|
|
109
|
+
async getEvents(filter?: {
|
|
110
|
+
types?: string[];
|
|
111
|
+
since?: number;
|
|
112
|
+
until?: number;
|
|
113
|
+
limit?: number;
|
|
114
|
+
}): Promise<StoredEvent[]> {
|
|
115
|
+
const events: StoredEvent[] = [];
|
|
116
|
+
const limit = filter?.limit || 100;
|
|
117
|
+
|
|
118
|
+
for (const [key, value] of this.db.entries()) {
|
|
119
|
+
if (key.startsWith('event:') && typeof value === 'object') {
|
|
120
|
+
const event = value as StoredEvent;
|
|
121
|
+
|
|
122
|
+
if (filter?.types && !filter.types.includes(event.type)) continue;
|
|
123
|
+
if (filter?.since && event.sequence <= filter.since) continue;
|
|
124
|
+
if (filter?.until && event.sequence >= filter.until) continue;
|
|
125
|
+
|
|
126
|
+
events.push(event);
|
|
127
|
+
if (events.length >= limit) break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return events.sort((a, b) => a.sequence - b.sequence);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get pending outbox entries
|
|
136
|
+
*/
|
|
137
|
+
async getPendingOutbox(limit = 10): Promise<OutboxEntry[]> {
|
|
138
|
+
const entries: OutboxEntry[] = [];
|
|
139
|
+
|
|
140
|
+
for (const [key, value] of this.db.entries()) {
|
|
141
|
+
if (key.startsWith('outbox:') && typeof value === 'object') {
|
|
142
|
+
const entry = value as OutboxEntry;
|
|
143
|
+
if (entry.status === 'pending' || entry.status === 'failed') {
|
|
144
|
+
entries.push(entry);
|
|
145
|
+
if (entries.length >= limit) break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return entries;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Update outbox entry status
|
|
155
|
+
*/
|
|
156
|
+
async updateOutboxStatus(id: string, status: OutboxEntry['status'], error?: string): Promise<void> {
|
|
157
|
+
const entry = this.db.get(`outbox:${id}`) as OutboxEntry | undefined;
|
|
158
|
+
if (entry) {
|
|
159
|
+
entry.status = status;
|
|
160
|
+
entry.lastError = error;
|
|
161
|
+
entry.attempts++;
|
|
162
|
+
if (status === 'completed') {
|
|
163
|
+
entry.processedAt = new Date().toISOString();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|