@omnixal/openclaw-nats-plugin 0.1.18 → 0.2.1

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.
Files changed (37) hide show
  1. package/openclaw.plugin.json +1 -0
  2. package/package.json +4 -2
  3. package/plugins/nats-context-engine/http-handler.ts +1 -1
  4. package/plugins/nats-context-engine/index.ts +153 -0
  5. package/sidecar/bun.lock +8 -6
  6. package/sidecar/package.json +4 -4
  7. package/sidecar/src/app.module.ts +11 -2
  8. package/sidecar/src/consumer/consumer.controller.ts +29 -30
  9. package/sidecar/src/consumer/consumer.module.ts +3 -1
  10. package/sidecar/src/db/migrations/0003_wet_deathbird.sql +12 -0
  11. package/sidecar/src/db/migrations/0004_familiar_zaladane.sql +17 -0
  12. package/sidecar/src/db/migrations/meta/0003_snapshot.json +194 -0
  13. package/sidecar/src/db/migrations/meta/0004_snapshot.json +306 -0
  14. package/sidecar/src/db/migrations/meta/_journal.json +14 -0
  15. package/sidecar/src/db/schema.ts +35 -0
  16. package/sidecar/src/health/health.service.ts +1 -1
  17. package/sidecar/src/index.ts +6 -5
  18. package/sidecar/src/metrics/metrics.controller.ts +16 -0
  19. package/sidecar/src/metrics/metrics.module.ts +10 -0
  20. package/sidecar/src/metrics/metrics.service.ts +64 -0
  21. package/sidecar/src/publisher/publisher.controller.ts +2 -2
  22. package/sidecar/src/publisher/publisher.module.ts +2 -0
  23. package/sidecar/src/publisher/publisher.service.ts +6 -1
  24. package/sidecar/src/router/router.controller.ts +76 -0
  25. package/sidecar/src/router/router.module.ts +11 -0
  26. package/sidecar/src/router/router.repository.ts +78 -0
  27. package/sidecar/src/router/router.service.ts +73 -0
  28. package/sidecar/src/scheduler/scheduler.controller.ts +68 -0
  29. package/sidecar/src/scheduler/scheduler.module.ts +13 -0
  30. package/sidecar/src/scheduler/scheduler.repository.ts +64 -0
  31. package/sidecar/src/scheduler/scheduler.service.ts +138 -0
  32. package/sidecar/src/validation/schemas.ts +18 -0
  33. package/skills/nats-events/SKILL.md +106 -0
  34. package/skills/nats-events/scripts/nats-cron-trigger.sh +29 -0
  35. package/dashboard/dist/assets/index--UFIkwvP.js +0 -2
  36. package/dashboard/dist/assets/index-CafgidIc.css +0 -2
  37. package/dashboard/dist/index.html +0 -13
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-nats-plugin",
3
+ "skills": ["./skills"],
3
4
  "configSchema": {
4
5
  "type": "object",
5
6
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnixal/openclaw-nats-plugin",
3
- "version": "0.1.18",
3
+ "version": "0.2.1",
4
4
  "description": "NATS JetStream event-driven plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "scripts": {
12
12
  "build:dashboard": "cd dashboard && bun run build",
13
- "prepublishOnly": "bun run build:dashboard"
13
+ "prepublishOnly": "bun run build:dashboard",
14
+ "nats": "docker run -d --name nats -p 4222:4222 nats:2.10-alpine -js"
14
15
  },
15
16
  "files": [
16
17
  "index.ts",
@@ -19,6 +20,7 @@
19
20
  "hooks/",
20
21
  "plugins/",
21
22
  "sidecar/",
23
+ "skills/",
22
24
  "docker/",
23
25
  "dashboard/dist/",
24
26
  "openclaw.plugin.json",
@@ -55,7 +55,7 @@ async function proxyToSidecar(
55
55
  }
56
56
 
57
57
  let body: string | undefined;
58
- if (req.method === 'POST' || req.method === 'PUT') {
58
+ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
59
59
  body = await readBody(req);
60
60
  }
61
61
 
@@ -78,6 +78,159 @@ export default function (api: any) {
78
78
  }, { priority: 8 });
79
79
  }, { priority: 99 });
80
80
 
81
+ // ── Agent Tools ─────────────────────────────────────────────────
82
+
83
+ const SIDECAR_URL = process.env.NATS_SIDECAR_URL || 'http://127.0.0.1:3104';
84
+ const SIDECAR_KEY = process.env.NATS_PLUGIN_API_KEY || 'dev-nats-plugin-key';
85
+
86
+ const sidecarFetch = async (path: string, options: RequestInit = {}) => {
87
+ const res = await fetch(`${SIDECAR_URL}${path}`, {
88
+ ...options,
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'Authorization': `Bearer ${SIDECAR_KEY}`,
92
+ ...options.headers,
93
+ },
94
+ signal: AbortSignal.timeout(5000),
95
+ });
96
+ return res.json();
97
+ };
98
+
99
+ api.registerTool({
100
+ name: 'nats_publish',
101
+ description: 'Publish an event to the NATS event bus. Use for cron triggers, custom events, task notifications.',
102
+ parameters: {
103
+ type: 'object',
104
+ properties: {
105
+ subject: { type: 'string', description: 'Event subject (must start with agent.events.)' },
106
+ payload: { type: 'object', description: 'Event payload data' },
107
+ },
108
+ required: ['subject', 'payload'],
109
+ },
110
+ async execute(_id: string, params: any) {
111
+ const result = await sidecarFetch('/api/publish', {
112
+ method: 'POST',
113
+ body: JSON.stringify({ subject: params.subject, payload: params.payload }),
114
+ });
115
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
116
+ },
117
+ });
118
+
119
+ api.registerTool({
120
+ name: 'nats_subscribe',
121
+ description: 'Subscribe to events matching a pattern. Matched events will be delivered to the target session as messages.',
122
+ parameters: {
123
+ type: 'object',
124
+ properties: {
125
+ pattern: { type: 'string', description: 'Subject pattern (exact, or wildcard with * for one level, > for all descendants)' },
126
+ target: { type: 'string', description: 'Session key to deliver to (default: main)' },
127
+ },
128
+ required: ['pattern'],
129
+ },
130
+ async execute(_id: string, params: any) {
131
+ const result = await sidecarFetch('/api/routes', {
132
+ method: 'POST',
133
+ body: JSON.stringify({ pattern: params.pattern, target: params.target ?? 'main' }),
134
+ });
135
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
136
+ },
137
+ });
138
+
139
+ api.registerTool({
140
+ name: 'nats_unsubscribe',
141
+ description: 'Remove an event subscription by its ID.',
142
+ parameters: {
143
+ type: 'object',
144
+ properties: {
145
+ id: { type: 'string', description: 'Route ID to delete (from nats_subscriptions)' },
146
+ },
147
+ required: ['id'],
148
+ },
149
+ async execute(_id: string, params: any) {
150
+ const result = await sidecarFetch(`/api/routes/${params.id}`, { method: 'DELETE' });
151
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
152
+ },
153
+ });
154
+
155
+ api.registerTool({
156
+ name: 'nats_subscriptions',
157
+ description: 'List event subscriptions. Optionally filter by pattern or target session.',
158
+ parameters: {
159
+ type: 'object',
160
+ properties: {
161
+ pattern: { type: 'string', description: 'Filter: show routes matching this pattern' },
162
+ target: { type: 'string', description: 'Filter: show routes delivering to this session' },
163
+ },
164
+ },
165
+ async execute(_id: string, params: any) {
166
+ const qs = new URLSearchParams();
167
+ if (params?.pattern) qs.set('pattern', params.pattern);
168
+ if (params?.target) qs.set('target', params.target);
169
+ const path = qs.toString() ? `/api/routes?${qs}` : '/api/routes';
170
+ const result = await sidecarFetch(path);
171
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
172
+ },
173
+ });
174
+
175
+ // ── Cron Scheduler Tools ────────────────────────────────────────────
176
+
177
+ api.registerTool({
178
+ name: 'nats_cron_add',
179
+ description: 'Create or update a scheduled cron job that publishes a NATS event on a schedule. No LLM wake — fires directly.',
180
+ parameters: {
181
+ type: 'object',
182
+ properties: {
183
+ name: { type: 'string', description: 'Unique job name (e.g., daily-report, hourly-check)' },
184
+ cron: { type: 'string', description: 'Cron expression (e.g., "0 9 * * *" for daily at 9am)' },
185
+ subject: { type: 'string', description: 'NATS subject to publish (must start with agent.events.)' },
186
+ payload: { type: 'object', description: 'Event payload data' },
187
+ timezone: { type: 'string', description: 'Timezone (default: UTC). e.g., Europe/Moscow' },
188
+ },
189
+ required: ['name', 'cron', 'subject'],
190
+ },
191
+ async execute(_id: string, params: any) {
192
+ const result = await sidecarFetch('/api/cron', {
193
+ method: 'POST',
194
+ body: JSON.stringify({
195
+ name: params.name,
196
+ cron: params.cron,
197
+ subject: params.subject,
198
+ payload: params.payload ?? {},
199
+ timezone: params.timezone,
200
+ }),
201
+ });
202
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
203
+ },
204
+ });
205
+
206
+ api.registerTool({
207
+ name: 'nats_cron_remove',
208
+ description: 'Remove a scheduled cron job by name.',
209
+ parameters: {
210
+ type: 'object',
211
+ properties: {
212
+ name: { type: 'string', description: 'Job name to remove' },
213
+ },
214
+ required: ['name'],
215
+ },
216
+ async execute(_id: string, params: any) {
217
+ const result = await sidecarFetch(`/api/cron/${encodeURIComponent(params.name)}`, {
218
+ method: 'DELETE',
219
+ });
220
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
221
+ },
222
+ });
223
+
224
+ api.registerTool({
225
+ name: 'nats_cron_list',
226
+ description: 'List all scheduled cron jobs with their next run time and status.',
227
+ parameters: { type: 'object', properties: {} },
228
+ async execute() {
229
+ const result = await sidecarFetch('/api/cron');
230
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
231
+ },
232
+ });
233
+
81
234
  // ── Dashboard UI ─────────────────────────────────────────────────
82
235
 
83
236
  api.registerHttpRoute({
package/sidecar/bun.lock CHANGED
@@ -5,11 +5,11 @@
5
5
  "": {
6
6
  "name": "@ai-entrepreneur/nats-sidecar",
7
7
  "dependencies": {
8
- "@onebun/core": "^0.2.12",
8
+ "@onebun/core": "^0.2.14",
9
9
  "@onebun/drizzle": "^0.2.4",
10
- "@onebun/envs": "^0.2.1",
10
+ "@onebun/envs": "^0.2.2",
11
11
  "@onebun/logger": "^0.2.1",
12
- "@onebun/nats": "^0.2.5",
12
+ "@onebun/nats": "^0.2.6",
13
13
  "arktype": "^2.2.0",
14
14
  "ulid": "^2.3.0",
15
15
  },
@@ -104,17 +104,17 @@
104
104
 
105
105
  "@nats-io/transport-node": ["@nats-io/transport-node@3.3.1", "", { "dependencies": { "@nats-io/nats-core": "3.3.1", "@nats-io/nkeys": "2.0.3", "@nats-io/nuid": "2.0.3" } }, "sha512-GBvY0VcvyQEILgy5bjpqU1GpDYmSF06bW59I7cewZuNGS9u3AoV/gf+a+3ep45T/Z+UC661atq/b7x+QV12w+Q=="],
106
106
 
107
- "@onebun/core": ["@onebun/core@0.2.12", "", { "dependencies": { "@onebun/envs": "^0.2.1", "@onebun/logger": "^0.2.1", "@onebun/metrics": "^0.2.2", "@onebun/requests": "^0.2.1", "@onebun/trace": "^0.2.1", "arktype": "^2.0.0", "effect": "^3.13.10" }, "peerDependencies": { "testcontainers": ">=10.0.0" }, "optionalPeers": ["testcontainers"] }, "sha512-Vy3A0pmp/eZjwgEgdZApMKHPJ697LvQETmqEWyUqUr01zcfy0HYI9KcRBRIHlGoajAYghwH+D9w4N3Ip6pGCcA=="],
107
+ "@onebun/core": ["@onebun/core@0.2.14", "", { "dependencies": { "@onebun/envs": "^0.2.2", "@onebun/logger": "^0.2.1", "@onebun/metrics": "^0.2.2", "@onebun/requests": "^0.2.1", "@onebun/trace": "^0.2.1", "arktype": "^2.0.0", "effect": "^3.13.10" }, "peerDependencies": { "testcontainers": ">=10.0.0" }, "optionalPeers": ["testcontainers"] }, "sha512-uClu4Oez18y6BldubdE6R/I02Brrk5eXCoxxPatgRJ9qYRM+jKSTNmeZVoqqo7ai/rMYM9lJ1WuF9d7p6/RtDA=="],
108
108
 
109
109
  "@onebun/drizzle": ["@onebun/drizzle@0.2.4", "", { "dependencies": { "@onebun/envs": "^0.2.1", "@onebun/logger": "^0.2.1", "arktype": "^2.0.0", "drizzle-arktype": "^0.1.3", "drizzle-kit": "^0.31.6", "drizzle-orm": "^0.44.7", "effect": "^3.13.10" }, "peerDependencies": { "@onebun/core": ">=0.2.0" }, "bin": { "onebun-drizzle": "bin/drizzle-kit.ts" } }, "sha512-LbkW2hU9pTKZU/rlrHNdwhI4jYoMl+v22c3G2zc0L0aW77nW7ZCfp5YqOJYufWJbfOTSWEnNOVZQXMueYhBxsA=="],
110
110
 
111
- "@onebun/envs": ["@onebun/envs@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-kiXJcA4ct194+aNJK8zkrVuaAgPPVpTkcW8tJU9XN9KOh8003lENOOuUZUcieMCxdMWUgo08lp9UgiwawLan+Q=="],
111
+ "@onebun/envs": ["@onebun/envs@0.2.2", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-WIjc1LpGnecYArSWsZhheyUSYJlo+iz9SA7ZfIXQnt1vkLd7ILCmVCtODBvqG9Mh86CMmromf1lCyRkjNZyLoA=="],
112
112
 
113
113
  "@onebun/logger": ["@onebun/logger@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-u/zirsUSGBfbjVv274qqIHG5jzPBWY3vl8HzM6hjzsMCCpExgstkQiP1eP9rF1isIzhetwmyfBpYYc9eYsbrrw=="],
114
114
 
115
115
  "@onebun/metrics": ["@onebun/metrics@0.2.2", "", { "dependencies": { "@onebun/requests": "^0.2.1", "effect": "^3.13.10", "prom-client": "^15.1.3" } }, "sha512-8oN74MZeaWyyPHi5H3pZyY0V3cM8ORupHe2fR0gGYsZqHyM4S9UJStV29rNQlvhyesHzL7p5x3Ux6n/SRYBszw=="],
116
116
 
117
- "@onebun/nats": ["@onebun/nats@0.2.5", "", { "dependencies": { "@nats-io/jetstream": "^3.0.0-31", "@nats-io/transport-node": "^3.0.0-31", "effect": "^3.13.10" }, "peerDependencies": { "@onebun/core": ">=0.2.0" } }, "sha512-J+p9UPJGqBLeeBY6n50E6at1lyA+xmg+wzqw1HwN516t+XHzctmnrB8S5PmX/3sQI18fktFcJgAyWJfQA1piDA=="],
117
+ "@onebun/nats": ["@onebun/nats@0.2.6", "", { "dependencies": { "@nats-io/jetstream": "^3.0.0-31", "@nats-io/transport-node": "^3.0.0-31", "effect": "^3.13.10" }, "peerDependencies": { "@onebun/core": ">=0.2.0" } }, "sha512-b7b0PUu0eGDLPbstOVjaCW0GCKsBzQb8qyYQmjKPu6RCsm02qvUU8HWDytoy01mYnQxWpI93qVogGiQKd6Ps4A=="],
118
118
 
119
119
  "@onebun/requests": ["@onebun/requests@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-Wit+o3zRiuZOM7O0nAJ0rpFVLgkypJ1UR5uuHi0IZuiCvGmxv+Vus2+QqHoCL141L7SPO1Xlywt8dVqqu4NP7w=="],
120
120
 
@@ -446,6 +446,8 @@
446
446
 
447
447
  "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
448
448
 
449
+ "@onebun/drizzle/@onebun/envs": ["@onebun/envs@0.2.1", "", { "dependencies": { "effect": "^3.13.10" } }, "sha512-kiXJcA4ct194+aNJK8zkrVuaAgPPVpTkcW8tJU9XN9KOh8003lENOOuUZUcieMCxdMWUgo08lp9UgiwawLan+Q=="],
450
+
449
451
  "bcrypt-pbkdf/tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
450
452
 
451
453
  "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -11,14 +11,14 @@
11
11
  "typecheck": "bunx tsc --noEmit",
12
12
  "db:generate": "bunx onebun-drizzle generate",
13
13
  "db:push": "bunx onebun-drizzle push",
14
- "db:studio": "bunx onebun-drizzle studio"
14
+ "db:studio": "bunx onebun-drizzle studio",
15
15
  },
16
16
  "dependencies": {
17
- "@onebun/core": "^0.2.12",
17
+ "@onebun/core": "^0.2.14",
18
18
  "@onebun/drizzle": "^0.2.4",
19
- "@onebun/envs": "^0.2.1",
19
+ "@onebun/envs": "^0.2.2",
20
20
  "@onebun/logger": "^0.2.1",
21
- "@onebun/nats": "^0.2.5",
21
+ "@onebun/nats": "^0.2.6",
22
22
  "arktype": "^2.2.0",
23
23
  "ulid": "^2.3.0"
24
24
  },
@@ -1,5 +1,6 @@
1
- import { Module } from '@onebun/core';
1
+ import { getConfig, Module } from '@onebun/core';
2
2
  import { DrizzleModule, DatabaseType } from '@onebun/drizzle';
3
+ import { envSchema, type AppConfig } from './config';
3
4
  import { DedupModule } from './dedup/dedup.module';
4
5
  import { PublisherModule } from './publisher/publisher.module';
5
6
  import { PreHandlersModule } from './pre-handlers/pre-handlers.module';
@@ -7,6 +8,11 @@ import { GatewayClientModule } from './gateway/gateway-client.module';
7
8
  import { ConsumerModule } from './consumer/consumer.module';
8
9
  import { PendingModule } from './pending/pending.module';
9
10
  import { HealthModule } from './health/health.module';
11
+ import { RouterModule } from './router/router.module';
12
+ import { SchedulerModule } from './scheduler/scheduler.module';
13
+ import { MetricsModule } from './metrics/metrics.module';
14
+
15
+ const config = getConfig<AppConfig>(envSchema);
10
16
 
11
17
  @Module({
12
18
  imports: [
@@ -14,7 +20,7 @@ import { HealthModule } from './health/health.module';
14
20
  connection: {
15
21
  type: DatabaseType.SQLITE,
16
22
  options: {
17
- url: process.env.DB_PATH ?? './data/nats-sidecar.db',
23
+ url: config.get('database.url'),
18
24
  },
19
25
  },
20
26
  migrationsFolder: './src/db/migrations',
@@ -26,6 +32,9 @@ import { HealthModule } from './health/health.module';
26
32
  ConsumerModule,
27
33
  PendingModule,
28
34
  HealthModule,
35
+ RouterModule,
36
+ SchedulerModule,
37
+ MetricsModule,
29
38
  ],
30
39
  })
31
40
  export class AppModule {}
@@ -2,6 +2,8 @@ import { Controller, BaseController, Subscribe, OnQueueReady, type Message } fro
2
2
  import { PipelineService } from '../pre-handlers/pipeline.service';
3
3
  import { GatewayClientService } from '../gateway/gateway-client.service';
4
4
  import { PendingService } from '../pending/pending.service';
5
+ import { RouterService } from '../router/router.service';
6
+ import { MetricsService } from '../metrics/metrics.service';
5
7
  import type { NatsEventEnvelope } from '../publisher/envelope';
6
8
 
7
9
  @Controller('/consumer')
@@ -10,6 +12,8 @@ export class ConsumerController extends BaseController {
10
12
  private pipeline: PipelineService,
11
13
  private gatewayClient: GatewayClientService,
12
14
  private pendingService: PendingService,
15
+ private routerService: RouterService,
16
+ private metrics: MetricsService,
13
17
  ) {
14
18
  super();
15
19
  }
@@ -20,7 +24,7 @@ export class ConsumerController extends BaseController {
20
24
  this.logger.info(`Queue connected, consuming as ${consumerName}`);
21
25
  }
22
26
 
23
- @Subscribe('agent.inbound.>', {
27
+ @Subscribe('agent.events.>', {
24
28
  ackMode: 'manual',
25
29
  group: 'openclaw-main',
26
30
  })
@@ -29,29 +33,39 @@ export class ConsumerController extends BaseController {
29
33
  const envelope = this.extractEnvelope(message);
30
34
 
31
35
  const { result, ctx } = await this.pipeline.process(envelope);
32
-
33
36
  if (result === 'drop') {
34
37
  await message.ack();
35
38
  return;
36
39
  }
37
40
 
38
- // Deliver to Gateway
41
+ // Check routing rules
42
+ const routes = await this.routerService.findMatchingRoutes(envelope.subject);
43
+ if (routes.length === 0) {
44
+ // No route — just ack (event is in JetStream for audit)
45
+ await message.ack();
46
+ return;
47
+ }
48
+
49
+ // Deliver to each matching target
39
50
  if (this.gatewayClient.isAlive()) {
40
- await this.gatewayClient.inject({
41
- target: envelope.agentTarget ?? 'main',
42
- message: this.formatMessage(envelope),
43
- metadata: {
44
- source: 'nats',
45
- eventId: envelope.id,
46
- subject: envelope.subject,
47
- priority: (ctx.enrichments['priority'] as number) ?? envelope.meta?.priority ?? 5,
48
- },
49
- });
51
+ for (const route of routes) {
52
+ await this.gatewayClient.inject({
53
+ target: route.target,
54
+ message: this.formatMessage(envelope),
55
+ metadata: {
56
+ source: 'nats',
57
+ eventId: envelope.id,
58
+ subject: envelope.subject,
59
+ priority: (ctx.enrichments['priority'] as number) ?? envelope.meta?.priority ?? 5,
60
+ },
61
+ });
62
+ await this.routerService.recordDelivery(route.id, envelope.subject);
63
+ this.metrics.recordConsume(envelope.subject);
64
+ }
50
65
  await message.ack();
51
66
  } else {
52
- // Gateway not available — store as pending for ContextEngine pickup
53
67
  await this.pendingService.addPending(envelope);
54
- await message.ack(); // ack because we stored it locally
68
+ await message.ack();
55
69
  this.logger.warn(`Gateway unavailable, stored pending event ${envelope.id}`);
56
70
  }
57
71
  } catch (err) {
@@ -60,29 +74,14 @@ export class ConsumerController extends BaseController {
60
74
  }
61
75
  }
62
76
 
63
- /**
64
- * Extract the NatsEventEnvelope from the adapter message.
65
- *
66
- * The JetStreamQueueAdapter wraps messages in its own envelope:
67
- * { id, pattern, data, timestamp, metadata }
68
- *
69
- * Our NatsEventEnvelope is inside `data` when published via PublisherService,
70
- * or the raw data itself when published externally.
71
- */
72
77
  private extractEnvelope(message: Message<unknown>): NatsEventEnvelope {
73
78
  const data = message.data as any;
74
-
75
- // If the data already looks like a NatsEventEnvelope (has id, subject, payload),
76
- // use it directly.
77
79
  if (data && typeof data === 'object' && 'subject' in data && 'payload' in data) {
78
80
  return data as NatsEventEnvelope;
79
81
  }
80
-
81
- // Otherwise, treat it as a raw payload string that needs parsing
82
82
  if (typeof data === 'string') {
83
83
  return JSON.parse(data) as NatsEventEnvelope;
84
84
  }
85
-
86
85
  throw new Error('Unable to extract envelope from message');
87
86
  }
88
87
 
@@ -2,9 +2,11 @@ import { Module } from '@onebun/core';
2
2
  import { ConsumerController } from './consumer.controller';
3
3
  import { PreHandlersModule } from '../pre-handlers/pre-handlers.module';
4
4
  import { PendingModule } from '../pending/pending.module';
5
+ import { RouterModule } from '../router/router.module';
6
+ import { MetricsModule } from '../metrics/metrics.module';
5
7
 
6
8
  @Module({
7
- imports: [PreHandlersModule, PendingModule],
9
+ imports: [PreHandlersModule, PendingModule, RouterModule, MetricsModule],
8
10
  controllers: [ConsumerController],
9
11
  })
10
12
  export class ConsumerModule {}
@@ -0,0 +1,12 @@
1
+ CREATE TABLE `event_routes` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `pattern` text NOT NULL,
4
+ `target` text DEFAULT 'main' NOT NULL,
5
+ `enabled` integer DEFAULT true NOT NULL,
6
+ `priority` integer DEFAULT 5 NOT NULL,
7
+ `created_at` integer NOT NULL
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE UNIQUE INDEX `event_routes_pattern_unique` ON `event_routes` (`pattern`);--> statement-breakpoint
11
+ CREATE INDEX `event_routes_pattern_idx` ON `event_routes` (`pattern`);--> statement-breakpoint
12
+ CREATE INDEX `event_routes_target_idx` ON `event_routes` (`target`);
@@ -0,0 +1,17 @@
1
+ CREATE TABLE `cron_jobs` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `name` text NOT NULL,
4
+ `expr` text NOT NULL,
5
+ `subject` text NOT NULL,
6
+ `payload` text,
7
+ `timezone` text DEFAULT 'UTC' NOT NULL,
8
+ `enabled` integer DEFAULT true NOT NULL,
9
+ `last_run_at` integer,
10
+ `created_at` integer NOT NULL
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE UNIQUE INDEX `cron_jobs_name_unique` ON `cron_jobs` (`name`);--> statement-breakpoint
14
+ CREATE INDEX `cron_jobs_name_idx` ON `cron_jobs` (`name`);--> statement-breakpoint
15
+ ALTER TABLE `event_routes` ADD `last_delivered_at` integer;--> statement-breakpoint
16
+ ALTER TABLE `event_routes` ADD `last_event_subject` text;--> statement-breakpoint
17
+ ALTER TABLE `event_routes` ADD `delivery_count` integer DEFAULT 0 NOT NULL;
@@ -0,0 +1,194 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "cb92737c-c257-4b58-bbf0-798ab1494961",
5
+ "prevId": "3836f3c7-1186-453d-bb01-c2f5e58a1f4f",
6
+ "tables": {
7
+ "dedup_events": {
8
+ "name": "dedup_events",
9
+ "columns": {
10
+ "event_id": {
11
+ "name": "event_id",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "subject": {
18
+ "name": "subject",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "seen_at": {
25
+ "name": "seen_at",
26
+ "type": "integer",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ }
31
+ },
32
+ "indexes": {
33
+ "dedup_events_seen_at_idx": {
34
+ "name": "dedup_events_seen_at_idx",
35
+ "columns": [
36
+ "seen_at"
37
+ ],
38
+ "isUnique": false
39
+ }
40
+ },
41
+ "foreignKeys": {},
42
+ "compositePrimaryKeys": {},
43
+ "uniqueConstraints": {},
44
+ "checkConstraints": {}
45
+ },
46
+ "event_routes": {
47
+ "name": "event_routes",
48
+ "columns": {
49
+ "id": {
50
+ "name": "id",
51
+ "type": "text",
52
+ "primaryKey": true,
53
+ "notNull": true,
54
+ "autoincrement": false
55
+ },
56
+ "pattern": {
57
+ "name": "pattern",
58
+ "type": "text",
59
+ "primaryKey": false,
60
+ "notNull": true,
61
+ "autoincrement": false
62
+ },
63
+ "target": {
64
+ "name": "target",
65
+ "type": "text",
66
+ "primaryKey": false,
67
+ "notNull": true,
68
+ "autoincrement": false,
69
+ "default": "'main'"
70
+ },
71
+ "enabled": {
72
+ "name": "enabled",
73
+ "type": "integer",
74
+ "primaryKey": false,
75
+ "notNull": true,
76
+ "autoincrement": false,
77
+ "default": true
78
+ },
79
+ "priority": {
80
+ "name": "priority",
81
+ "type": "integer",
82
+ "primaryKey": false,
83
+ "notNull": true,
84
+ "autoincrement": false,
85
+ "default": 5
86
+ },
87
+ "created_at": {
88
+ "name": "created_at",
89
+ "type": "integer",
90
+ "primaryKey": false,
91
+ "notNull": true,
92
+ "autoincrement": false
93
+ }
94
+ },
95
+ "indexes": {
96
+ "event_routes_pattern_unique": {
97
+ "name": "event_routes_pattern_unique",
98
+ "columns": [
99
+ "pattern"
100
+ ],
101
+ "isUnique": true
102
+ },
103
+ "event_routes_pattern_idx": {
104
+ "name": "event_routes_pattern_idx",
105
+ "columns": [
106
+ "pattern"
107
+ ],
108
+ "isUnique": false
109
+ },
110
+ "event_routes_target_idx": {
111
+ "name": "event_routes_target_idx",
112
+ "columns": [
113
+ "target"
114
+ ],
115
+ "isUnique": false
116
+ }
117
+ },
118
+ "foreignKeys": {},
119
+ "compositePrimaryKeys": {},
120
+ "uniqueConstraints": {},
121
+ "checkConstraints": {}
122
+ },
123
+ "pending_events": {
124
+ "name": "pending_events",
125
+ "columns": {
126
+ "id": {
127
+ "name": "id",
128
+ "type": "text",
129
+ "primaryKey": true,
130
+ "notNull": true,
131
+ "autoincrement": false
132
+ },
133
+ "session_key": {
134
+ "name": "session_key",
135
+ "type": "text",
136
+ "primaryKey": false,
137
+ "notNull": true,
138
+ "autoincrement": false
139
+ },
140
+ "subject": {
141
+ "name": "subject",
142
+ "type": "text",
143
+ "primaryKey": false,
144
+ "notNull": true,
145
+ "autoincrement": false
146
+ },
147
+ "payload": {
148
+ "name": "payload",
149
+ "type": "text",
150
+ "primaryKey": false,
151
+ "notNull": false,
152
+ "autoincrement": false
153
+ },
154
+ "priority": {
155
+ "name": "priority",
156
+ "type": "integer",
157
+ "primaryKey": false,
158
+ "notNull": true,
159
+ "autoincrement": false,
160
+ "default": 5
161
+ },
162
+ "created_at": {
163
+ "name": "created_at",
164
+ "type": "integer",
165
+ "primaryKey": false,
166
+ "notNull": true,
167
+ "autoincrement": false
168
+ },
169
+ "delivered_at": {
170
+ "name": "delivered_at",
171
+ "type": "integer",
172
+ "primaryKey": false,
173
+ "notNull": false,
174
+ "autoincrement": false
175
+ }
176
+ },
177
+ "indexes": {},
178
+ "foreignKeys": {},
179
+ "compositePrimaryKeys": {},
180
+ "uniqueConstraints": {},
181
+ "checkConstraints": {}
182
+ }
183
+ },
184
+ "views": {},
185
+ "enums": {},
186
+ "_meta": {
187
+ "schemas": {},
188
+ "tables": {},
189
+ "columns": {}
190
+ },
191
+ "internal": {
192
+ "indexes": {}
193
+ }
194
+ }