@onebun/core 0.1.1 → 0.1.2

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.
@@ -0,0 +1,240 @@
1
+ /**
2
+ * WebSocket Pattern Matcher
3
+ *
4
+ * Provides pattern matching for WebSocket events and room names.
5
+ * Supports wildcards (*) and named parameters ({paramName}).
6
+ *
7
+ * @example
8
+ * // Wildcard patterns
9
+ * matchPattern('chat:*', 'chat:general') // { matched: true, params: {} }
10
+ * matchPattern('user:*:action', 'user:123:action') // { matched: true, params: {} }
11
+ *
12
+ * // Parameter patterns
13
+ * matchPattern('chat:{roomId}', 'chat:general') // { matched: true, params: { roomId: 'general' } }
14
+ * matchPattern('user:{id}:message', 'user:123:message') // { matched: true, params: { id: '123' } }
15
+ *
16
+ * // Combined patterns
17
+ * matchPattern('service:{service}:*', 'service:auth:login') // { matched: true, params: { service: 'auth' } }
18
+ */
19
+
20
+ import type { PatternMatch } from './ws.types';
21
+
22
+ /**
23
+ * Escape special regex characters in a string
24
+ */
25
+ function escapeRegex(str: string): string {
26
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
27
+ }
28
+
29
+ /**
30
+ * Parse a pattern into a regex and extract parameter names
31
+ *
32
+ * @param pattern - Pattern string with wildcards and/or parameters
33
+ * @returns Object with regex and parameter names
34
+ */
35
+ export function parsePattern(pattern: string): {
36
+ regex: RegExp;
37
+ paramNames: string[];
38
+ } {
39
+ const paramNames: string[] = [];
40
+ let regexStr = '^';
41
+ let i = 0;
42
+
43
+ while (i < pattern.length) {
44
+ const char = pattern[i];
45
+
46
+ if (char === '*') {
47
+ // Wildcard: matches any characters except the separator
48
+ regexStr += '[^:]*';
49
+ i++;
50
+ } else if (char === '{') {
51
+ // Named parameter: {paramName}
52
+ const endIndex = pattern.indexOf('}', i);
53
+ if (endIndex === -1) {
54
+ // Invalid pattern, treat as literal
55
+ regexStr += escapeRegex(char);
56
+ i++;
57
+ } else {
58
+ const paramName = pattern.substring(i + 1, endIndex);
59
+ paramNames.push(paramName);
60
+ // Match any characters except the separator
61
+ regexStr += '([^:]+)';
62
+ i = endIndex + 1;
63
+ }
64
+ } else if (char === ':') {
65
+ // Separator
66
+ regexStr += ':';
67
+ i++;
68
+ } else {
69
+ // Literal character
70
+ regexStr += escapeRegex(char);
71
+ i++;
72
+ }
73
+ }
74
+
75
+ regexStr += '$';
76
+
77
+ return {
78
+ regex: new RegExp(regexStr),
79
+ paramNames,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Match a value against a pattern
85
+ *
86
+ * @param pattern - Pattern string with wildcards and/or parameters
87
+ * @param value - Value to match
88
+ * @returns Match result with extracted parameters
89
+ *
90
+ * @example
91
+ * matchPattern('chat:*', 'chat:general')
92
+ * // Returns: { matched: true, params: {} }
93
+ *
94
+ * matchPattern('chat:{roomId}:message', 'chat:general:message')
95
+ * // Returns: { matched: true, params: { roomId: 'general' } }
96
+ *
97
+ * matchPattern('chat:private', 'chat:public')
98
+ * // Returns: { matched: false, params: {} }
99
+ */
100
+ export function matchPattern(pattern: string, value: string): PatternMatch {
101
+ // Exact match fast path
102
+ if (pattern === value) {
103
+ return { matched: true, params: {} };
104
+ }
105
+
106
+ // Check if pattern has any special characters
107
+ if (!pattern.includes('*') && !pattern.includes('{')) {
108
+ // No wildcards or parameters, must be exact match
109
+ return { matched: false, params: {} };
110
+ }
111
+
112
+ const { regex, paramNames } = parsePattern(pattern);
113
+ const match = value.match(regex);
114
+
115
+ if (!match) {
116
+ return { matched: false, params: {} };
117
+ }
118
+
119
+ // Extract parameters
120
+ const params: Record<string, string> = {};
121
+ for (let i = 0; i < paramNames.length; i++) {
122
+ params[paramNames[i]] = match[i + 1];
123
+ }
124
+
125
+ return { matched: true, params };
126
+ }
127
+
128
+ /**
129
+ * Check if a pattern matches a value (without extracting parameters)
130
+ *
131
+ * @param pattern - Pattern string
132
+ * @param value - Value to match
133
+ * @returns Whether the pattern matches
134
+ */
135
+ export function isPatternMatch(pattern: string, value: string): boolean {
136
+ return matchPattern(pattern, value).matched;
137
+ }
138
+
139
+ /**
140
+ * Find all values that match a pattern
141
+ *
142
+ * @param pattern - Pattern string
143
+ * @param values - Array of values to check
144
+ * @returns Array of matching values
145
+ */
146
+ export function findMatchingValues(pattern: string, values: string[]): string[] {
147
+ return values.filter((value) => isPatternMatch(pattern, value));
148
+ }
149
+
150
+ /**
151
+ * Create a pattern matcher function for a specific pattern
152
+ *
153
+ * @param pattern - Pattern string
154
+ * @returns Function that matches values against the pattern
155
+ *
156
+ * @example
157
+ * const matcher = createPatternMatcher('chat:{roomId}:message');
158
+ * matcher('chat:general:message') // { matched: true, params: { roomId: 'general' } }
159
+ * matcher('chat:private:action') // { matched: false, params: {} }
160
+ */
161
+ export function createPatternMatcher(pattern: string): (value: string) => PatternMatch {
162
+ const { regex, paramNames } = parsePattern(pattern);
163
+
164
+ return (value: string): PatternMatch => {
165
+ // Exact match fast path
166
+ if (pattern === value) {
167
+ return { matched: true, params: {} };
168
+ }
169
+
170
+ const match = value.match(regex);
171
+
172
+ if (!match) {
173
+ return { matched: false, params: {} };
174
+ }
175
+
176
+ const params: Record<string, string> = {};
177
+ for (let i = 0; i < paramNames.length; i++) {
178
+ params[paramNames[i]] = match[i + 1];
179
+ }
180
+
181
+ return { matched: true, params };
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Check if a string is a pattern (contains wildcards or parameters)
187
+ *
188
+ * @param value - String to check
189
+ * @returns Whether the string is a pattern
190
+ */
191
+ export function isPattern(value: string): boolean {
192
+ return value.includes('*') || value.includes('{');
193
+ }
194
+
195
+ /**
196
+ * Extract parameter names from a pattern
197
+ *
198
+ * @param pattern - Pattern string
199
+ * @returns Array of parameter names
200
+ *
201
+ * @example
202
+ * getPatternParams('chat:{roomId}:user:{userId}')
203
+ * // Returns: ['roomId', 'userId']
204
+ */
205
+ export function getPatternParams(pattern: string): string[] {
206
+ const params: string[] = [];
207
+ const regex = /\{([^}]+)\}/g;
208
+ let match: RegExpExecArray | null;
209
+
210
+ while ((match = regex.exec(pattern)) !== null) {
211
+ params.push(match[1]);
212
+ }
213
+
214
+ return params;
215
+ }
216
+
217
+ /**
218
+ * Build a value from a pattern and parameters
219
+ *
220
+ * @param pattern - Pattern string
221
+ * @param params - Parameter values
222
+ * @returns Built value string
223
+ *
224
+ * @example
225
+ * buildFromPattern('chat:{roomId}:message', { roomId: 'general' })
226
+ * // Returns: 'chat:general:message'
227
+ */
228
+ export function buildFromPattern(pattern: string, params: Record<string, string>): string {
229
+ let result = pattern;
230
+
231
+ // Replace wildcards with empty string (or you could throw an error)
232
+ result = result.replace(/\*/g, '');
233
+
234
+ // Replace parameters with values
235
+ for (const [key, value] of Object.entries(params)) {
236
+ result = result.replace(`{${key}}`, value);
237
+ }
238
+
239
+ return result;
240
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * WebSocket Service Definition
3
+ *
4
+ * Collects metadata from WebSocket gateways for generating typed clients.
5
+ */
6
+
7
+ import type { WsHandlerType, WsParamMetadata } from './ws.types';
8
+
9
+ import { getModuleMetadata } from './decorators';
10
+ import { getGatewayMetadata, isWebSocketGateway } from './ws-decorators';
11
+
12
+ /**
13
+ * Metadata for a single WebSocket endpoint/event
14
+ */
15
+ export interface WsEndpointMetadata {
16
+ /** Gateway class name */
17
+ gateway: string;
18
+ /** Event pattern */
19
+ event: string;
20
+ /** Handler method name */
21
+ handler: string;
22
+ /** Handler type (message, connect, etc.) */
23
+ type: WsHandlerType;
24
+ /** Parameter metadata */
25
+ params?: WsParamMetadata[];
26
+ }
27
+
28
+ /**
29
+ * Definition of a WebSocket gateway with its events
30
+ */
31
+ export interface WsGatewayDefinition {
32
+ /** Gateway class name */
33
+ name: string;
34
+ /** WebSocket path */
35
+ path: string;
36
+ /** Namespace (if any) */
37
+ namespace?: string;
38
+ /** Map of event patterns to endpoint metadata */
39
+ events: Map<string, WsEndpointMetadata>;
40
+ }
41
+
42
+ /**
43
+ * Service definition containing all WebSocket gateways and events.
44
+ * Used for generating typed WebSocket clients.
45
+ */
46
+ export interface WsServiceDefinition<TModule = unknown> {
47
+ /** Reference to the module class */
48
+ readonly _module: TModule;
49
+ /** Flat list of all endpoints */
50
+ readonly _endpoints: WsEndpointMetadata[];
51
+ /** Gateways grouped by name */
52
+ readonly _gateways: Map<string, WsGatewayDefinition>;
53
+ }
54
+
55
+ /**
56
+ * Module metadata structure
57
+ */
58
+ interface ModuleMetadata {
59
+ imports?: Function[];
60
+ controllers?: Function[];
61
+ providers?: unknown[];
62
+ exports?: unknown[];
63
+ }
64
+
65
+ /**
66
+ * Collect WebSocket endpoints recursively from a module and its imports
67
+ */
68
+ function collectWsEndpointsRecursively(metadata: ModuleMetadata): WsEndpointMetadata[] {
69
+ const endpoints: WsEndpointMetadata[] = [];
70
+
71
+ // Collect from controllers (gateways are in controllers)
72
+ for (const controller of metadata.controllers || []) {
73
+ // Check if it's a WebSocket gateway
74
+ if (!isWebSocketGateway(controller)) {
75
+ continue;
76
+ }
77
+
78
+ const gatewayMeta = getGatewayMetadata(controller);
79
+ if (!gatewayMeta) {
80
+ continue;
81
+ }
82
+
83
+ // Add each handler as an endpoint
84
+ for (const handler of gatewayMeta.handlers) {
85
+ endpoints.push({
86
+ gateway: controller.name,
87
+ event: handler.pattern || handler.type,
88
+ handler: handler.handler,
89
+ type: handler.type,
90
+ params: handler.params,
91
+ });
92
+ }
93
+ }
94
+
95
+ // Recursively collect from imported modules
96
+ for (const importedModule of metadata.imports || []) {
97
+ const importedMeta = getModuleMetadata(importedModule);
98
+ if (importedMeta) {
99
+ endpoints.push(...collectWsEndpointsRecursively(importedMeta));
100
+ }
101
+ }
102
+
103
+ return endpoints;
104
+ }
105
+
106
+ /**
107
+ * Group endpoints by gateway
108
+ */
109
+ function groupByGateway(
110
+ endpoints: WsEndpointMetadata[],
111
+ moduleClass: Function,
112
+ ): Map<string, WsGatewayDefinition> {
113
+ const gateways = new Map<string, WsGatewayDefinition>();
114
+ const metadata = getModuleMetadata(moduleClass);
115
+
116
+ if (!metadata) {
117
+ return gateways;
118
+ }
119
+
120
+ // First, collect gateway metadata
121
+ const gatewayPaths = new Map<string, { path: string; namespace?: string }>();
122
+
123
+ for (const controller of metadata.controllers || []) {
124
+ if (isWebSocketGateway(controller)) {
125
+ const gatewayMeta = getGatewayMetadata(controller);
126
+ if (gatewayMeta) {
127
+ gatewayPaths.set(controller.name, {
128
+ path: gatewayMeta.path,
129
+ namespace: gatewayMeta.namespace,
130
+ });
131
+ }
132
+ }
133
+ }
134
+
135
+ // Group endpoints by gateway
136
+ for (const endpoint of endpoints) {
137
+ if (!gateways.has(endpoint.gateway)) {
138
+ const pathInfo = gatewayPaths.get(endpoint.gateway) || { path: '/' };
139
+ gateways.set(endpoint.gateway, {
140
+ name: endpoint.gateway,
141
+ path: pathInfo.path,
142
+ namespace: pathInfo.namespace,
143
+ events: new Map(),
144
+ });
145
+ }
146
+
147
+ gateways.get(endpoint.gateway)!.events.set(endpoint.event, endpoint);
148
+ }
149
+
150
+ return gateways;
151
+ }
152
+
153
+ /**
154
+ * Create a WebSocket service definition from a module class.
155
+ * Collects all endpoint metadata from gateways and imported modules.
156
+ *
157
+ * @param moduleClass - The root module class decorated with @Module
158
+ * @returns Service definition containing all WebSocket endpoints
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * import { createWsServiceDefinition } from '@onebun/core';
163
+ * import { ChatModule } from './chat.module';
164
+ *
165
+ * export const chatWsDefinition = createWsServiceDefinition(ChatModule);
166
+ *
167
+ * // Use with createWsClient
168
+ * const client = createWsClient(chatWsDefinition, {
169
+ * url: 'ws://localhost:3000',
170
+ * });
171
+ * ```
172
+ */
173
+ export function createWsServiceDefinition<TModule>(
174
+ moduleClass: new (...args: unknown[]) => TModule,
175
+ ): WsServiceDefinition<TModule> {
176
+ const metadata = getModuleMetadata(moduleClass);
177
+ if (!metadata) {
178
+ throw new Error(`${moduleClass.name} is not decorated with @Module`);
179
+ }
180
+
181
+ const endpoints = collectWsEndpointsRecursively(metadata);
182
+ const gateways = groupByGateway(endpoints, moduleClass);
183
+
184
+ return {
185
+ _module: moduleClass as unknown as TModule,
186
+ _endpoints: endpoints,
187
+ _gateways: gateways,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Get gateway names from a service definition
193
+ */
194
+ export function getWsGatewayNames<TDef extends WsServiceDefinition>(
195
+ definition: TDef,
196
+ ): string[] {
197
+ return Array.from(definition._gateways.keys());
198
+ }
199
+
200
+ /**
201
+ * Get event names for a gateway
202
+ */
203
+ export function getWsEventNames<TDef extends WsServiceDefinition>(
204
+ definition: TDef,
205
+ gatewayName: string,
206
+ ): string[] {
207
+ const gateway = definition._gateways.get(gatewayName);
208
+
209
+ return gateway ? Array.from(gateway.events.keys()) : [];
210
+ }
211
+
212
+ /**
213
+ * Get endpoint metadata by gateway and event
214
+ */
215
+ export function getWsEndpoint<TDef extends WsServiceDefinition>(
216
+ definition: TDef,
217
+ gatewayName: string,
218
+ event: string,
219
+ ): WsEndpointMetadata | undefined {
220
+ const gateway = definition._gateways.get(gatewayName);
221
+
222
+ return gateway?.events.get(event);
223
+ }