@omnixal/openclaw-nats-plugin 0.2.15 → 0.2.17

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 (27) hide show
  1. package/dashboard/src/lib/LogsPanel.svelte +50 -1
  2. package/dashboard/src/lib/RoutesPanel.svelte +120 -11
  3. package/dashboard/src/lib/api.ts +20 -0
  4. package/package.json +1 -1
  5. package/plugins/nats-context-engine/http-handler.ts +12 -67
  6. package/plugins/nats-context-engine/index.ts +17 -3
  7. package/sidecar/src/config.ts +4 -0
  8. package/sidecar/src/consumer/consumer.controller.ts +41 -4
  9. package/sidecar/src/consumer/consumer.module.ts +2 -1
  10. package/sidecar/src/db/migrations/0008_fluffy_pestilence.sql +1 -0
  11. package/sidecar/src/db/migrations/0009_sour_romulus.sql +6 -0
  12. package/sidecar/src/db/migrations/meta/0008_snapshot.json +492 -0
  13. package/sidecar/src/db/migrations/meta/0009_snapshot.json +514 -0
  14. package/sidecar/src/db/migrations/meta/_journal.json +14 -0
  15. package/sidecar/src/db/schema.ts +8 -2
  16. package/sidecar/src/pending/pending-flush.service.ts +70 -0
  17. package/sidecar/src/pending/pending.module.ts +5 -1
  18. package/sidecar/src/pending/pending.repository.ts +6 -2
  19. package/sidecar/src/pending/pending.service.ts +2 -2
  20. package/sidecar/src/route-filter/filter-expression.ts +10 -0
  21. package/sidecar/src/route-filter/route-filter.module.ts +8 -0
  22. package/sidecar/src/route-filter/route-filter.service.ts +61 -0
  23. package/sidecar/src/router/router.controller.ts +14 -1
  24. package/sidecar/src/router/router.repository.ts +15 -4
  25. package/sidecar/src/router/router.service.ts +14 -3
  26. package/sidecar/src/scheduler/scheduler.controller.ts +2 -2
  27. package/sidecar/src/validation/schemas.ts +16 -0
@@ -0,0 +1,8 @@
1
+ import { Module } from '@onebun/core';
2
+ import { RouteFilterService } from './route-filter.service';
3
+
4
+ @Module({
5
+ services: [RouteFilterService],
6
+ exports: [RouteFilterService],
7
+ })
8
+ export class RouteFilterModule {}
@@ -0,0 +1,61 @@
1
+ import { Service, BaseService } from '@onebun/core';
2
+ import type { FilterExpression, FilterCondition } from './filter-expression';
3
+
4
+ @Service()
5
+ export class RouteFilterService extends BaseService {
6
+ evaluate(payload: unknown, filter: FilterExpression | null): boolean {
7
+ if (!filter || !filter.conditions || filter.conditions.length === 0) return true;
8
+ const results = filter.conditions.map(c => this.evalCondition(payload, c));
9
+ return filter.logic === 'or' ? results.some(Boolean) : results.every(Boolean);
10
+ }
11
+
12
+ private resolveField(obj: unknown, path: string): { found: boolean; value: unknown } {
13
+ const parts = path.split('.');
14
+ let current: unknown = obj;
15
+ for (const part of parts) {
16
+ if (current === null || current === undefined) return { found: false, value: undefined };
17
+ if (typeof current === 'object') {
18
+ if (!(part in (current as Record<string, unknown>))) return { found: false, value: undefined };
19
+ current = (current as Record<string, unknown>)[part];
20
+ } else {
21
+ return { found: false, value: undefined };
22
+ }
23
+ }
24
+ return { found: true, value: current };
25
+ }
26
+
27
+ private evalCondition(payload: unknown, cond: FilterCondition): boolean {
28
+ const { found, value } = this.resolveField(payload, cond.field);
29
+
30
+ if (cond.op === 'exists') {
31
+ return cond.value ? found : !found;
32
+ }
33
+
34
+ if (!found) return false;
35
+
36
+ switch (cond.op) {
37
+ case 'eq':
38
+ return value === cond.value;
39
+ case 'neq':
40
+ return value !== cond.value;
41
+ case 'gt':
42
+ return typeof value === 'number' && typeof cond.value === 'number' && value > cond.value;
43
+ case 'gte':
44
+ return typeof value === 'number' && typeof cond.value === 'number' && value >= cond.value;
45
+ case 'lt':
46
+ return typeof value === 'number' && typeof cond.value === 'number' && value < cond.value;
47
+ case 'lte':
48
+ return typeof value === 'number' && typeof cond.value === 'number' && value <= cond.value;
49
+ case 'in':
50
+ return Array.isArray(cond.value) && cond.value.includes(value);
51
+ case 'nin':
52
+ return Array.isArray(cond.value) && !cond.value.includes(value);
53
+ case 'contains':
54
+ if (typeof value === 'string' && typeof cond.value === 'string') return value.includes(cond.value);
55
+ if (Array.isArray(value)) return value.includes(cond.value);
56
+ return false;
57
+ default:
58
+ return false;
59
+ }
60
+ }
61
+ }
@@ -37,10 +37,14 @@ export class RouterController extends BaseController {
37
37
  const routes = await this.routerService.listRoutes();
38
38
  const result = routes.map(r => ({
39
39
  id: r.id,
40
+ name: r.name,
40
41
  pattern: r.pattern,
41
42
  target: r.target,
42
43
  priority: r.priority,
43
44
  enabled: r.enabled,
45
+ filter: r.filter ?? null,
46
+ filterDropCount: r.filterDropCount ?? 0,
47
+ customPayload: r.customPayload ?? null,
44
48
  lastDeliveredAt: r.lastDeliveredAt?.toISOString() ?? null,
45
49
  lastEventSubject: r.lastEventSubject ?? null,
46
50
  deliveryCount: r.deliveryCount ?? 0,
@@ -66,10 +70,14 @@ export class RouterController extends BaseController {
66
70
  if (!isValidAgentSubject(body.pattern)) {
67
71
  return this.error('pattern must start with "agent.events." followed by at least one token and must not end with "."', 400, 400);
68
72
  }
73
+ const name = body.name ?? body.pattern;
69
74
  const { route, created } = await this.routerService.subscribe(
75
+ name,
70
76
  body.pattern,
71
77
  body.target ?? 'main',
72
78
  body.priority ?? 5,
79
+ body.filter ?? null,
80
+ body.payload,
73
81
  );
74
82
  return this.success({ ...route, created });
75
83
  }
@@ -79,7 +87,12 @@ export class RouterController extends BaseController {
79
87
  @Param('id') id: string,
80
88
  @Body(updateRouteBodySchema) body: UpdateRouteBody,
81
89
  ): Promise<OneBunResponse> {
82
- const updated = await this.routerService.updateById(id, body);
90
+ const { payload: customPayload, filter, ...rest } = body;
91
+ const updated = await this.routerService.updateById(id, {
92
+ ...rest,
93
+ ...(customPayload !== undefined ? { customPayload } : {}),
94
+ ...(filter !== undefined ? { filter: filter as any } : {}),
95
+ });
83
96
  if (!updated) {
84
97
  return this.error('Route not found', 404, 404);
85
98
  }
@@ -36,11 +36,14 @@ export class RouterRepository extends BaseService {
36
36
  .insert(eventRoutes)
37
37
  .values(route)
38
38
  .onConflictDoUpdate({
39
- target: eventRoutes.pattern,
39
+ target: eventRoutes.name,
40
40
  set: {
41
+ pattern: sql`excluded.pattern`,
41
42
  target: sql`excluded.target`,
42
43
  priority: sql`excluded.priority`,
43
44
  enabled: sql`excluded.enabled`,
45
+ filter: sql`excluded.filter`,
46
+ customPayload: sql`excluded.custom_payload`,
44
47
  },
45
48
  })
46
49
  .returning();
@@ -49,7 +52,7 @@ export class RouterRepository extends BaseService {
49
52
  return { route: result, created };
50
53
  }
51
54
 
52
- async updateById(id: string, fields: Partial<Pick<DbEventRoute, 'target' | 'priority' | 'enabled'>>): Promise<DbEventRoute | null> {
55
+ async updateById(id: string, fields: Partial<Pick<DbEventRoute, 'target' | 'priority' | 'enabled' | 'customPayload' | 'filter'>>): Promise<DbEventRoute | null> {
53
56
  const [result] = await this.db.update(eventRoutes)
54
57
  .set(fields)
55
58
  .where(eq(eventRoutes.id, id))
@@ -68,13 +71,21 @@ export class RouterRepository extends BaseService {
68
71
  .where(eq(eventRoutes.id, routeId));
69
72
  }
70
73
 
74
+ async incrementFilterDropCount(routeId: string): Promise<void> {
75
+ await this.db.update(eventRoutes)
76
+ .set({
77
+ filterDropCount: sql`${eventRoutes.filterDropCount} + 1`,
78
+ })
79
+ .where(eq(eventRoutes.id, routeId));
80
+ }
81
+
71
82
  async deleteById(id: string): Promise<boolean> {
72
83
  const result = await this.db.delete(eventRoutes).where(eq(eventRoutes.id, id)).returning();
73
84
  return result.length > 0;
74
85
  }
75
86
 
76
- async deleteByPattern(pattern: string): Promise<boolean> {
77
- const result = await this.db.delete(eventRoutes).where(eq(eventRoutes.pattern, pattern)).returning();
87
+ async deleteByName(name: string): Promise<boolean> {
88
+ const result = await this.db.delete(eventRoutes).where(eq(eventRoutes.name, name)).returning();
78
89
  return result.length > 0;
79
90
  }
80
91
 
@@ -1,6 +1,7 @@
1
1
  import { Service, BaseService } from '@onebun/core';
2
2
  import { RouterRepository } from './router.repository';
3
3
  import type { DbEventRoute } from '../db/schema';
4
+ import type { FilterExpression } from '../route-filter/filter-expression';
4
5
  import { ulid } from 'ulid';
5
6
 
6
7
  @Service()
@@ -40,21 +41,27 @@ export class RouterService extends BaseService {
40
41
  }
41
42
 
42
43
  async subscribe(
44
+ name: string,
43
45
  pattern: string,
44
46
  target: string = 'main',
45
47
  priority: number = 5,
48
+ filter?: FilterExpression | null,
49
+ customPayload?: unknown,
46
50
  ): Promise<{ route: DbEventRoute; created: boolean }> {
47
51
  return this.repo.upsert({
48
52
  id: ulid(),
53
+ name,
49
54
  pattern,
50
55
  target,
51
56
  enabled: true,
52
57
  priority,
58
+ filter: filter ?? null,
59
+ customPayload: customPayload ?? null,
53
60
  createdAt: new Date(),
54
61
  });
55
62
  }
56
63
 
57
- async updateById(id: string, fields: { target?: string; priority?: number; enabled?: boolean }): Promise<DbEventRoute | null> {
64
+ async updateById(id: string, fields: { target?: string; priority?: number; enabled?: boolean; customPayload?: unknown; filter?: FilterExpression | null }): Promise<DbEventRoute | null> {
58
65
  return this.repo.updateById(id, fields);
59
66
  }
60
67
 
@@ -62,8 +69,12 @@ export class RouterService extends BaseService {
62
69
  await this.repo.recordDelivery(routeId, subject, lagMs);
63
70
  }
64
71
 
65
- async unsubscribe(pattern: string): Promise<boolean> {
66
- return this.repo.deleteByPattern(pattern);
72
+ async incrementFilterDropCount(routeId: string): Promise<void> {
73
+ await this.repo.incrementFilterDropCount(routeId);
74
+ }
75
+
76
+ async unsubscribeByName(name: string): Promise<boolean> {
77
+ return this.repo.deleteByName(name);
67
78
  }
68
79
 
69
80
  async deleteById(id: string): Promise<boolean> {
@@ -124,7 +124,7 @@ export class SchedulerController extends BaseController {
124
124
 
125
125
  // ── Queue Handlers ────────────────────────────────────────────────
126
126
 
127
- @Subscribe('scheduler.fire.*', { group: 'scheduler-handler' })
127
+ @Subscribe('scheduler.fire.*', { group: 'scheduler-cron-handler' })
128
128
  async handleFire(message: Message<unknown>): Promise<void> {
129
129
  const jobName = message.pattern?.split('.').pop();
130
130
  if (jobName) {
@@ -132,7 +132,7 @@ export class SchedulerController extends BaseController {
132
132
  }
133
133
  }
134
134
 
135
- @Subscribe('scheduler.timer.*', { group: 'scheduler-handler' })
135
+ @Subscribe('scheduler.timer.*', { group: 'scheduler-timer-handler' })
136
136
  async handleTimerFire(message: Message<unknown>): Promise<void> {
137
137
  const timerName = message.pattern?.split('.').pop();
138
138
  if (timerName) {
@@ -18,10 +18,24 @@ export const markDeliveredBodySchema = type({
18
18
 
19
19
  export type MarkDeliveredBody = typeof markDeliveredBodySchema.infer;
20
20
 
21
+ export const filterConditionSchema = type({
22
+ field: 'string',
23
+ op: "'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'exists'",
24
+ value: 'unknown',
25
+ });
26
+
27
+ export const filterExpressionSchema = type({
28
+ logic: "'and' | 'or'",
29
+ conditions: filterConditionSchema.array(),
30
+ });
31
+
21
32
  export const createRouteBodySchema = type({
22
33
  pattern: 'string',
34
+ 'name?': 'string',
23
35
  'target?': 'string',
24
36
  'priority?': 'number',
37
+ 'payload?': 'unknown',
38
+ 'filter?': filterExpressionSchema,
25
39
  });
26
40
 
27
41
  export type CreateRouteBody = typeof createRouteBodySchema.infer;
@@ -40,6 +54,8 @@ export const updateRouteBodySchema = type({
40
54
  'target?': 'string',
41
55
  'priority?': 'number',
42
56
  'enabled?': 'boolean',
57
+ 'payload?': 'unknown',
58
+ 'filter?': 'unknown',
43
59
  });
44
60
 
45
61
  export type UpdateRouteBody = typeof updateRouteBodySchema.infer;