@onebun/core 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/{application.test.ts → application/application.test.ts} +6 -5
- package/src/{application.ts → application/application.ts} +131 -12
- package/src/application/index.ts +9 -0
- package/src/{multi-service-application.test.ts → application/multi-service-application.test.ts} +2 -1
- package/src/{multi-service-application.ts → application/multi-service-application.ts} +2 -1
- package/src/{multi-service.types.ts → application/multi-service.types.ts} +1 -1
- package/src/{decorators.test.ts → decorators/decorators.test.ts} +2 -1
- package/src/{decorators.ts → decorators/decorators.ts} +3 -2
- package/src/decorators/index.ts +15 -0
- package/src/index.ts +47 -134
- package/src/module/index.ts +12 -0
- package/src/{module.test.ts → module/module.test.ts} +3 -2
- package/src/{module.ts → module/module.ts} +6 -5
- package/src/queue/adapters/index.ts +8 -0
- package/src/queue/adapters/memory.adapter.test.ts +405 -0
- package/src/queue/adapters/memory.adapter.ts +509 -0
- package/src/queue/adapters/redis.adapter.ts +673 -0
- package/src/queue/cron-expression.test.ts +145 -0
- package/src/queue/cron-expression.ts +115 -0
- package/src/queue/cron-parser.test.ts +185 -0
- package/src/queue/cron-parser.ts +287 -0
- package/src/queue/decorators.test.ts +292 -0
- package/src/queue/decorators.ts +493 -0
- package/src/queue/docs-examples.test.ts +449 -0
- package/src/queue/guards.test.ts +309 -0
- package/src/queue/guards.ts +307 -0
- package/src/queue/index.ts +118 -0
- package/src/queue/pattern-matcher.test.ts +191 -0
- package/src/queue/pattern-matcher.ts +252 -0
- package/src/queue/queue.service.ts +421 -0
- package/src/queue/scheduler.test.ts +235 -0
- package/src/queue/scheduler.ts +379 -0
- package/src/queue/types.ts +502 -0
- package/src/redis/index.ts +8 -0
- package/src/{env-resolver.ts → service-client/env-resolver.ts} +1 -1
- package/src/service-client/index.ts +10 -0
- package/src/{service-client.test.ts → service-client/service-client.test.ts} +3 -2
- package/src/{service-client.ts → service-client/service-client.ts} +1 -1
- package/src/{service-definition.test.ts → service-client/service-definition.test.ts} +3 -2
- package/src/{service-definition.ts → service-client/service-definition.ts} +2 -2
- package/src/testing/index.ts +7 -0
- package/src/types.ts +34 -5
- package/src/websocket/index.ts +50 -0
- package/src/{ws-decorators.ts → websocket/ws-decorators.ts} +2 -1
- package/src/{ws-integration.test.ts → websocket/ws-integration.test.ts} +3 -2
- package/src/{ws-service-definition.ts → websocket/ws-service-definition.ts} +2 -1
- package/src/{ws-storage-redis.ts → websocket/ws-storage-redis.ts} +1 -1
- /package/src/{metadata.test.ts → decorators/metadata.test.ts} +0 -0
- /package/src/{metadata.ts → decorators/metadata.ts} +0 -0
- /package/src/{config.service.test.ts → module/config.service.test.ts} +0 -0
- /package/src/{config.service.ts → module/config.service.ts} +0 -0
- /package/src/{controller.test.ts → module/controller.test.ts} +0 -0
- /package/src/{controller.ts → module/controller.ts} +0 -0
- /package/src/{service.test.ts → module/service.test.ts} +0 -0
- /package/src/{service.ts → module/service.ts} +0 -0
- /package/src/{redis-client.ts → redis/redis-client.ts} +0 -0
- /package/src/{shared-redis.ts → redis/shared-redis.ts} +0 -0
- /package/src/{env-resolver.test.ts → service-client/env-resolver.test.ts} +0 -0
- /package/src/{service-client.types.ts → service-client/service-client.types.ts} +0 -0
- /package/src/{test-utils.test.ts → testing/test-utils.test.ts} +0 -0
- /package/src/{test-utils.ts → testing/test-utils.ts} +0 -0
- /package/src/{ws-base-gateway.test.ts → websocket/ws-base-gateway.test.ts} +0 -0
- /package/src/{ws-base-gateway.ts → websocket/ws-base-gateway.ts} +0 -0
- /package/src/{ws-client.test.ts → websocket/ws-client.test.ts} +0 -0
- /package/src/{ws-client.ts → websocket/ws-client.ts} +0 -0
- /package/src/{ws-client.types.ts → websocket/ws-client.types.ts} +0 -0
- /package/src/{ws-decorators.test.ts → websocket/ws-decorators.test.ts} +0 -0
- /package/src/{ws-guards.test.ts → websocket/ws-guards.test.ts} +0 -0
- /package/src/{ws-guards.ts → websocket/ws-guards.ts} +0 -0
- /package/src/{ws-handler.ts → websocket/ws-handler.ts} +0 -0
- /package/src/{ws-pattern-matcher.test.ts → websocket/ws-pattern-matcher.test.ts} +0 -0
- /package/src/{ws-pattern-matcher.ts → websocket/ws-pattern-matcher.ts} +0 -0
- /package/src/{ws-socketio-protocol.test.ts → websocket/ws-socketio-protocol.test.ts} +0 -0
- /package/src/{ws-socketio-protocol.ts → websocket/ws-socketio-protocol.ts} +0 -0
- /package/src/{ws-storage-memory.test.ts → websocket/ws-storage-memory.test.ts} +0 -0
- /package/src/{ws-storage-memory.ts → websocket/ws-storage-memory.ts} +0 -0
- /package/src/{ws-storage.ts → websocket/ws-storage.ts} +0 -0
- /package/src/{ws.types.ts → websocket/ws.types.ts} +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Module
|
|
3
|
+
*
|
|
4
|
+
* Unified queue system for OneBun.
|
|
5
|
+
* Supports multiple backends: memory, redis, nats, jetstream.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type {
|
|
10
|
+
AckMode,
|
|
11
|
+
MessageMetadata,
|
|
12
|
+
Message,
|
|
13
|
+
PublishOptions,
|
|
14
|
+
RetryOptions,
|
|
15
|
+
DeadLetterOptions,
|
|
16
|
+
SubscribeOptions,
|
|
17
|
+
QueueEvents,
|
|
18
|
+
Subscription,
|
|
19
|
+
OverlapStrategy,
|
|
20
|
+
ScheduledJobOptions,
|
|
21
|
+
ScheduledJobInfo,
|
|
22
|
+
QueueFeature,
|
|
23
|
+
QueueAdapterType,
|
|
24
|
+
MessageHandler,
|
|
25
|
+
QueueAdapter,
|
|
26
|
+
BuiltInAdapterType,
|
|
27
|
+
QueueAdapterConstructor,
|
|
28
|
+
QueueConfig,
|
|
29
|
+
SubscribeDecoratorOptions,
|
|
30
|
+
CronDecoratorOptions,
|
|
31
|
+
IntervalDecoratorOptions,
|
|
32
|
+
TimeoutDecoratorOptions,
|
|
33
|
+
MessageExecutionContext,
|
|
34
|
+
MessageGuard,
|
|
35
|
+
MessageGuardConstructor,
|
|
36
|
+
} from './types';
|
|
37
|
+
|
|
38
|
+
// Cron Parser
|
|
39
|
+
export {
|
|
40
|
+
parseCronExpression,
|
|
41
|
+
getNextRun,
|
|
42
|
+
getNextRuns,
|
|
43
|
+
getMillisecondsUntilNextRun,
|
|
44
|
+
isValidCronExpression,
|
|
45
|
+
type CronSchedule,
|
|
46
|
+
} from './cron-parser';
|
|
47
|
+
|
|
48
|
+
// Cron Expression
|
|
49
|
+
export { CronExpression, type CronExpressionValue } from './cron-expression';
|
|
50
|
+
|
|
51
|
+
// Pattern Matcher
|
|
52
|
+
export {
|
|
53
|
+
parseQueuePattern,
|
|
54
|
+
matchQueuePattern,
|
|
55
|
+
isQueuePatternMatch,
|
|
56
|
+
createQueuePatternMatcher,
|
|
57
|
+
isQueuePattern,
|
|
58
|
+
getQueuePatternParams,
|
|
59
|
+
buildFromQueuePattern,
|
|
60
|
+
findMatchingTopics,
|
|
61
|
+
type QueuePatternMatch,
|
|
62
|
+
} from './pattern-matcher';
|
|
63
|
+
|
|
64
|
+
// Guards
|
|
65
|
+
export {
|
|
66
|
+
MessageExecutionContextImpl,
|
|
67
|
+
MessageAuthGuard,
|
|
68
|
+
MessageServiceGuard,
|
|
69
|
+
MessageHeaderGuard,
|
|
70
|
+
MessageTraceGuard,
|
|
71
|
+
MessageAllGuards,
|
|
72
|
+
MessageAnyGuard,
|
|
73
|
+
executeMessageGuards,
|
|
74
|
+
createMessageGuard,
|
|
75
|
+
} from './guards';
|
|
76
|
+
|
|
77
|
+
// Decorators
|
|
78
|
+
export {
|
|
79
|
+
Subscribe,
|
|
80
|
+
Cron,
|
|
81
|
+
Interval,
|
|
82
|
+
Timeout,
|
|
83
|
+
UseMessageGuards,
|
|
84
|
+
OnQueueReady,
|
|
85
|
+
OnQueueError,
|
|
86
|
+
OnMessageFailed,
|
|
87
|
+
OnMessageReceived,
|
|
88
|
+
OnMessageProcessed,
|
|
89
|
+
// Metadata helpers
|
|
90
|
+
getSubscribeMetadata,
|
|
91
|
+
getCronMetadata,
|
|
92
|
+
getIntervalMetadata,
|
|
93
|
+
getTimeoutMetadata,
|
|
94
|
+
getMessageGuards,
|
|
95
|
+
getLifecycleHandlers,
|
|
96
|
+
hasQueueDecorators,
|
|
97
|
+
QUEUE_METADATA,
|
|
98
|
+
type SubscribeMetadata,
|
|
99
|
+
type CronMetadata,
|
|
100
|
+
type IntervalMetadata,
|
|
101
|
+
type TimeoutMetadata,
|
|
102
|
+
type LifecycleMetadata,
|
|
103
|
+
} from './decorators';
|
|
104
|
+
|
|
105
|
+
// Scheduler
|
|
106
|
+
export { QueueScheduler, createQueueScheduler } from './scheduler';
|
|
107
|
+
|
|
108
|
+
// Queue Service
|
|
109
|
+
export {
|
|
110
|
+
QueueService,
|
|
111
|
+
QueueServiceTag,
|
|
112
|
+
makeQueueLayer,
|
|
113
|
+
createQueueService,
|
|
114
|
+
resolveAdapterType,
|
|
115
|
+
} from './queue.service';
|
|
116
|
+
|
|
117
|
+
// Adapters (re-export from adapters folder)
|
|
118
|
+
export * from './adapters';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Pattern Matcher Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
} from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
matchQueuePattern,
|
|
13
|
+
isQueuePatternMatch,
|
|
14
|
+
createQueuePatternMatcher,
|
|
15
|
+
isQueuePattern,
|
|
16
|
+
getQueuePatternParams,
|
|
17
|
+
buildFromQueuePattern,
|
|
18
|
+
findMatchingTopics,
|
|
19
|
+
} from './pattern-matcher';
|
|
20
|
+
|
|
21
|
+
describe('queue-pattern-matcher', () => {
|
|
22
|
+
describe('matchQueuePattern', () => {
|
|
23
|
+
it('should match exact strings', () => {
|
|
24
|
+
const result = matchQueuePattern('orders.created', 'orders.created');
|
|
25
|
+
expect(result.matched).toBe(true);
|
|
26
|
+
expect(result.params).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should not match different strings', () => {
|
|
30
|
+
const result = matchQueuePattern('orders.created', 'orders.updated');
|
|
31
|
+
expect(result.matched).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should match single-level wildcard (*)', () => {
|
|
35
|
+
const result = matchQueuePattern('orders.*', 'orders.created');
|
|
36
|
+
expect(result.matched).toBe(true);
|
|
37
|
+
expect(result.params).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should match single-level wildcard in the middle', () => {
|
|
41
|
+
const result = matchQueuePattern('orders.*.status', 'orders.123.status');
|
|
42
|
+
expect(result.matched).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should not match single-level wildcard with extra segments', () => {
|
|
46
|
+
const result = matchQueuePattern('orders.*', 'orders.123.status');
|
|
47
|
+
expect(result.matched).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should match multi-level wildcard (#)', () => {
|
|
51
|
+
const result = matchQueuePattern('events.#', 'events.user.created');
|
|
52
|
+
expect(result.matched).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should match multi-level wildcard with many segments', () => {
|
|
56
|
+
const result = matchQueuePattern('events.#', 'events.user.order.payment.completed');
|
|
57
|
+
expect(result.matched).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should match multi-level wildcard at end (empty)', () => {
|
|
61
|
+
// events.# should match events.anything, not just 'events'
|
|
62
|
+
const result = matchQueuePattern('events.#', 'events.');
|
|
63
|
+
expect(result.matched).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should extract named parameters', () => {
|
|
67
|
+
const result = matchQueuePattern('orders.{orderId}', 'orders.123');
|
|
68
|
+
expect(result.matched).toBe(true);
|
|
69
|
+
expect(result.params).toEqual({ orderId: '123' });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should extract multiple parameters', () => {
|
|
73
|
+
const result = matchQueuePattern('users.{userId}.orders.{orderId}', 'users.456.orders.123');
|
|
74
|
+
expect(result.matched).toBe(true);
|
|
75
|
+
expect(result.params).toEqual({ userId: '456', orderId: '123' });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle combined wildcard and parameters', () => {
|
|
79
|
+
const result = matchQueuePattern('service.{name}.*', 'service.auth.login');
|
|
80
|
+
expect(result.matched).toBe(true);
|
|
81
|
+
expect(result.params).toEqual({ name: 'auth' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should not match when parameter value contains separator', () => {
|
|
85
|
+
const result = matchQueuePattern('orders.{id}', 'orders.123.extra');
|
|
86
|
+
expect(result.matched).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('isQueuePatternMatch', () => {
|
|
91
|
+
it('should return true for matching patterns', () => {
|
|
92
|
+
expect(isQueuePatternMatch('orders.*', 'orders.created')).toBe(true);
|
|
93
|
+
expect(isQueuePatternMatch('events.#', 'events.user.created')).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return false for non-matching patterns', () => {
|
|
97
|
+
expect(isQueuePatternMatch('orders.*', 'users.created')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('createQueuePatternMatcher', () => {
|
|
102
|
+
it('should create reusable matcher function', () => {
|
|
103
|
+
const matcher = createQueuePatternMatcher('orders.{orderId}.status');
|
|
104
|
+
|
|
105
|
+
const result1 = matcher('orders.123.status');
|
|
106
|
+
expect(result1.matched).toBe(true);
|
|
107
|
+
expect(result1.params).toEqual({ orderId: '123' });
|
|
108
|
+
|
|
109
|
+
const result2 = matcher('orders.456.status');
|
|
110
|
+
expect(result2.matched).toBe(true);
|
|
111
|
+
expect(result2.params).toEqual({ orderId: '456' });
|
|
112
|
+
|
|
113
|
+
const result3 = matcher('orders.123.other');
|
|
114
|
+
expect(result3.matched).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should optimize exact match patterns', () => {
|
|
118
|
+
const matcher = createQueuePatternMatcher('orders.created');
|
|
119
|
+
|
|
120
|
+
expect(matcher('orders.created').matched).toBe(true);
|
|
121
|
+
expect(matcher('orders.updated').matched).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('isQueuePattern', () => {
|
|
126
|
+
it('should return true for single-level wildcard patterns', () => {
|
|
127
|
+
expect(isQueuePattern('orders.*')).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return true for multi-level wildcard patterns', () => {
|
|
131
|
+
expect(isQueuePattern('events.#')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return true for parameter patterns', () => {
|
|
135
|
+
expect(isQueuePattern('orders.{id}')).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return false for literal strings', () => {
|
|
139
|
+
expect(isQueuePattern('orders.created')).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('getQueuePatternParams', () => {
|
|
144
|
+
it('should extract parameter names', () => {
|
|
145
|
+
const params = getQueuePatternParams('users.{userId}.orders.{orderId}');
|
|
146
|
+
expect(params).toEqual(['userId', 'orderId']);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return empty array for patterns without parameters', () => {
|
|
150
|
+
const params = getQueuePatternParams('orders.*');
|
|
151
|
+
expect(params).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('buildFromQueuePattern', () => {
|
|
156
|
+
it('should build topic from pattern and params', () => {
|
|
157
|
+
const result = buildFromQueuePattern('orders.{id}.status', { id: '123' });
|
|
158
|
+
expect(result).toBe('orders.123.status');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle multiple parameters', () => {
|
|
162
|
+
const result = buildFromQueuePattern('users.{uid}.orders.{oid}', { uid: '456', oid: '123' });
|
|
163
|
+
expect(result).toBe('users.456.orders.123');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should remove wildcards', () => {
|
|
167
|
+
const result = buildFromQueuePattern('orders.*.status', {});
|
|
168
|
+
expect(result).toBe('orders..status');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('findMatchingTopics', () => {
|
|
173
|
+
it('should find all matching topics', () => {
|
|
174
|
+
const topics = ['orders.created', 'orders.updated', 'users.created', 'orders.deleted'];
|
|
175
|
+
const matching = findMatchingTopics('orders.*', topics);
|
|
176
|
+
expect(matching).toEqual(['orders.created', 'orders.updated', 'orders.deleted']);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should find topics with multi-level wildcard', () => {
|
|
180
|
+
const topics = ['events.user.created', 'events.order.created', 'logs.info', 'events.deep.nested.event'];
|
|
181
|
+
const matching = findMatchingTopics('events.#', topics);
|
|
182
|
+
expect(matching).toEqual(['events.user.created', 'events.order.created', 'events.deep.nested.event']);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should return empty array when nothing matches', () => {
|
|
186
|
+
const topics = ['users.created', 'users.updated'];
|
|
187
|
+
const matching = findMatchingTopics('orders.*', topics);
|
|
188
|
+
expect(matching).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Pattern Matcher
|
|
3
|
+
*
|
|
4
|
+
* Pattern matching for queue subscriptions.
|
|
5
|
+
* Supports both AMQP-style and MQTT-style wildcards.
|
|
6
|
+
*
|
|
7
|
+
* Supported patterns:
|
|
8
|
+
* - Exact match: 'orders.created' matches only 'orders.created'
|
|
9
|
+
* - Single-level wildcard (*): 'orders.*' matches 'orders.created', 'orders.updated'
|
|
10
|
+
* - Multi-level wildcard (#): 'events.#' matches 'events.user.created', 'events.order.shipped'
|
|
11
|
+
* - Named parameters: 'orders.{orderId}' extracts orderId from matching messages
|
|
12
|
+
*
|
|
13
|
+
* Separator: '.' (dot) is used as the separator (like AMQP/MQTT)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Single-level wildcard
|
|
18
|
+
* matchQueuePattern('orders.*', 'orders.created') // true
|
|
19
|
+
* matchQueuePattern('orders.*', 'orders.created.new') // false
|
|
20
|
+
*
|
|
21
|
+
* // Multi-level wildcard
|
|
22
|
+
* matchQueuePattern('events.#', 'events.user.created') // true
|
|
23
|
+
* matchQueuePattern('events.#', 'events') // true
|
|
24
|
+
*
|
|
25
|
+
* // Named parameters
|
|
26
|
+
* matchQueuePattern('orders.{id}.status', 'orders.123.status')
|
|
27
|
+
* // { matched: true, params: { id: '123' } }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pattern match result
|
|
33
|
+
*/
|
|
34
|
+
export interface QueuePatternMatch {
|
|
35
|
+
matched: boolean;
|
|
36
|
+
params: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Escape special regex characters
|
|
41
|
+
*/
|
|
42
|
+
function escapeRegex(str: string): string {
|
|
43
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a queue pattern into a regex and extract parameter names
|
|
48
|
+
*
|
|
49
|
+
* @param pattern - Pattern string with wildcards and/or parameters
|
|
50
|
+
* @returns Object with regex and parameter names
|
|
51
|
+
*/
|
|
52
|
+
export function parseQueuePattern(pattern: string): {
|
|
53
|
+
regex: RegExp;
|
|
54
|
+
paramNames: string[];
|
|
55
|
+
} {
|
|
56
|
+
const paramNames: string[] = [];
|
|
57
|
+
let regexStr = '^';
|
|
58
|
+
let i = 0;
|
|
59
|
+
|
|
60
|
+
while (i < pattern.length) {
|
|
61
|
+
const char = pattern[i];
|
|
62
|
+
|
|
63
|
+
if (char === '#') {
|
|
64
|
+
// Multi-level wildcard: matches any number of segments (including zero)
|
|
65
|
+
// # must be at the end or followed by separator
|
|
66
|
+
if (i === pattern.length - 1) {
|
|
67
|
+
regexStr += '.*';
|
|
68
|
+
} else if (pattern[i + 1] === '.') {
|
|
69
|
+
regexStr += '(?:[^.]+\\.)*';
|
|
70
|
+
i++; // Skip the dot
|
|
71
|
+
} else {
|
|
72
|
+
// Invalid # position, treat as literal
|
|
73
|
+
regexStr += escapeRegex(char);
|
|
74
|
+
}
|
|
75
|
+
i++;
|
|
76
|
+
} else if (char === '*') {
|
|
77
|
+
// Single-level wildcard: matches exactly one segment
|
|
78
|
+
regexStr += '[^.]+';
|
|
79
|
+
i++;
|
|
80
|
+
} else if (char === '{') {
|
|
81
|
+
// Named parameter: {paramName}
|
|
82
|
+
const endIndex = pattern.indexOf('}', i);
|
|
83
|
+
if (endIndex === -1) {
|
|
84
|
+
// Invalid pattern, treat as literal
|
|
85
|
+
regexStr += escapeRegex(char);
|
|
86
|
+
i++;
|
|
87
|
+
} else {
|
|
88
|
+
const paramName = pattern.substring(i + 1, endIndex);
|
|
89
|
+
paramNames.push(paramName);
|
|
90
|
+
// Match one segment
|
|
91
|
+
regexStr += '([^.]+)';
|
|
92
|
+
i = endIndex + 1;
|
|
93
|
+
}
|
|
94
|
+
} else if (char === '.') {
|
|
95
|
+
// Separator
|
|
96
|
+
regexStr += '\\.';
|
|
97
|
+
i++;
|
|
98
|
+
} else {
|
|
99
|
+
// Literal character
|
|
100
|
+
regexStr += escapeRegex(char);
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
regexStr += '$';
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
regex: new RegExp(regexStr),
|
|
109
|
+
paramNames,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Match a topic against a pattern
|
|
115
|
+
*
|
|
116
|
+
* @param pattern - Pattern string with wildcards and/or parameters
|
|
117
|
+
* @param topic - Topic to match
|
|
118
|
+
* @returns Match result with extracted parameters
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* matchQueuePattern('orders.*', 'orders.created')
|
|
123
|
+
* // { matched: true, params: {} }
|
|
124
|
+
*
|
|
125
|
+
* matchQueuePattern('orders.{orderId}', 'orders.123')
|
|
126
|
+
* // { matched: true, params: { orderId: '123' } }
|
|
127
|
+
*
|
|
128
|
+
* matchQueuePattern('events.#', 'events.user.created')
|
|
129
|
+
* // { matched: true, params: {} }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function matchQueuePattern(pattern: string, topic: string): QueuePatternMatch {
|
|
133
|
+
// Exact match fast path
|
|
134
|
+
if (pattern === topic) {
|
|
135
|
+
return { matched: true, params: {} };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if pattern has any special characters
|
|
139
|
+
if (!pattern.includes('*') && !pattern.includes('#') && !pattern.includes('{')) {
|
|
140
|
+
// No wildcards or parameters, must be exact match
|
|
141
|
+
return { matched: false, params: {} };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { regex, paramNames } = parseQueuePattern(pattern);
|
|
145
|
+
const match = topic.match(regex);
|
|
146
|
+
|
|
147
|
+
if (!match) {
|
|
148
|
+
return { matched: false, params: {} };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Extract parameters
|
|
152
|
+
const params: Record<string, string> = {};
|
|
153
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
154
|
+
params[paramNames[i]] = match[i + 1];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { matched: true, params };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if a pattern matches a topic (without extracting parameters)
|
|
162
|
+
*/
|
|
163
|
+
export function isQueuePatternMatch(pattern: string, topic: string): boolean {
|
|
164
|
+
return matchQueuePattern(pattern, topic).matched;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a pattern matcher function for a specific pattern
|
|
169
|
+
* (pre-compiles the regex for better performance)
|
|
170
|
+
*
|
|
171
|
+
* @param pattern - Pattern string
|
|
172
|
+
* @returns Function that matches topics against the pattern
|
|
173
|
+
*/
|
|
174
|
+
export function createQueuePatternMatcher(pattern: string): (topic: string) => QueuePatternMatch {
|
|
175
|
+
// Exact match fast path
|
|
176
|
+
if (!pattern.includes('*') && !pattern.includes('#') && !pattern.includes('{')) {
|
|
177
|
+
return (topic: string) => ({
|
|
178
|
+
matched: pattern === topic,
|
|
179
|
+
params: {},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const { regex, paramNames } = parseQueuePattern(pattern);
|
|
184
|
+
|
|
185
|
+
return (topic: string): QueuePatternMatch => {
|
|
186
|
+
// Exact match fast path
|
|
187
|
+
if (pattern === topic) {
|
|
188
|
+
return { matched: true, params: {} };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const match = topic.match(regex);
|
|
192
|
+
|
|
193
|
+
if (!match) {
|
|
194
|
+
return { matched: false, params: {} };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const params: Record<string, string> = {};
|
|
198
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
199
|
+
params[paramNames[i]] = match[i + 1];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { matched: true, params };
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if a string is a pattern (contains wildcards or parameters)
|
|
208
|
+
*/
|
|
209
|
+
export function isQueuePattern(value: string): boolean {
|
|
210
|
+
return value.includes('*') || value.includes('#') || value.includes('{');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extract parameter names from a pattern
|
|
215
|
+
*/
|
|
216
|
+
export function getQueuePatternParams(pattern: string): string[] {
|
|
217
|
+
const params: string[] = [];
|
|
218
|
+
const regex = /\{([^}]+)\}/g;
|
|
219
|
+
let match: RegExpExecArray | null;
|
|
220
|
+
|
|
221
|
+
while ((match = regex.exec(pattern)) !== null) {
|
|
222
|
+
params.push(match[1]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return params;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build a topic from a pattern and parameters
|
|
230
|
+
*/
|
|
231
|
+
export function buildFromQueuePattern(pattern: string, params: Record<string, string>): string {
|
|
232
|
+
let result = pattern;
|
|
233
|
+
|
|
234
|
+
// Replace parameters with values
|
|
235
|
+
for (const [key, value] of Object.entries(params)) {
|
|
236
|
+
result = result.replace(`{${key}}`, value);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Remove wildcards (they shouldn't be in built topics)
|
|
240
|
+
result = result.replace(/[*#]/g, '');
|
|
241
|
+
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Find all topics that match a pattern
|
|
247
|
+
*/
|
|
248
|
+
export function findMatchingTopics(pattern: string, topics: string[]): string[] {
|
|
249
|
+
const matcher = createQueuePatternMatcher(pattern);
|
|
250
|
+
|
|
251
|
+
return topics.filter((topic) => matcher(topic).matched);
|
|
252
|
+
}
|