@elysiumjs/core 0.1.0

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/src/wamp.ts ADDED
@@ -0,0 +1,388 @@
1
+ // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import type { Class } from 'type-fest';
16
+
17
+ import { Elysia } from 'elysia';
18
+ import { omit } from 'radash';
19
+ // @ts-expect-error The Wampy type definitions are not up to date
20
+ import Wampy from 'wampy';
21
+
22
+ import { Event } from './event';
23
+ import { Service } from './service';
24
+ import { nextTick, Symbols } from './utils';
25
+
26
+ // FIXME: Temporary solution for Bun to properly fill the protocol field. Remove this once oven-sh/bun#18744 is fixed.
27
+ class WampWebsocket extends WebSocket {
28
+ public constructor(
29
+ url: string,
30
+ private readonly protocols?: string | string[]
31
+ ) {
32
+ super(url, protocols);
33
+ }
34
+
35
+ public get protocol(): string {
36
+ return this.protocols?.[0] ?? 'wamp.2.json';
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Options used to register a WAMP RPC for invocation.
42
+ * @author Axel Nana <axel.nana@workbud.com>
43
+ */
44
+ export type WampRegistrationOptions = {
45
+ /**
46
+ * The match policy to use for the registration.
47
+ * @default 'exact'
48
+ */
49
+ match?: 'prefix' | 'exact' | 'wildcard';
50
+
51
+ /**
52
+ * The invocation policy to use for the registration.
53
+ * @default 'single'
54
+ */
55
+ invoke?: 'single' | 'first' | 'last' | 'roundrobin' | 'random';
56
+ };
57
+
58
+ /**
59
+ * Options used to subscribe to a WAMP topic.
60
+ * @author Axel Nana <axel.nana@workbud.com>
61
+ */
62
+ export type WampSubscriptionOptions = Omit<WampRegistrationOptions, 'invoke'>;
63
+
64
+ /**
65
+ * Properties required when declaring a WAMP controller using the `@wamp()` decorator.
66
+ * @author Axel Nana <axel.nana@workbud.com>
67
+ */
68
+ export type WampProps = {
69
+ /**
70
+ * The URL to the WAMP server.
71
+ */
72
+ url: string;
73
+
74
+ /**
75
+ * The name of the realm to connect to.
76
+ */
77
+ realm: string;
78
+
79
+ /**
80
+ * Whether to automatically reconnect to the WAMP server on connection loss.
81
+ */
82
+ autoReconnect?: boolean;
83
+
84
+ /**
85
+ * The maximum number of times to retry connecting to the WAMP server.
86
+ */
87
+ maxRetries?: number;
88
+
89
+ /**
90
+ * The interval to wait between retries when connecting to the WAMP server.
91
+ */
92
+ retryInterval?: number;
93
+ };
94
+
95
+ /**
96
+ * Data sent to a WAMP RPC.
97
+ * @author Axel Nana <axel.nana@workbud.com>
98
+ */
99
+ export type WampRegistrationHandlerArgs = {
100
+ /**
101
+ * The arguments passed to the RPC or subscription.
102
+ */
103
+ argsList: any[];
104
+
105
+ /**
106
+ * The keyword arguments passed to the RPC or subscription.
107
+ */
108
+ argsDict: any;
109
+
110
+ /**
111
+ * The WAMP details object.
112
+ */
113
+ details: {
114
+ receive_progress?: boolean;
115
+ trustlevel?: number;
116
+ };
117
+
118
+ /**
119
+ * The result handler for the RPC. Use this to send progressive results.
120
+ * @param value
121
+ */
122
+ result_handler: (value: any) => void;
123
+
124
+ /**
125
+ * The error handler for the RPC when working with progressive results.
126
+ * @param error The thrown error.
127
+ */
128
+ error_handler: (error: any) => void;
129
+ };
130
+
131
+ /**
132
+ * Data sent to a WAMP subscription.
133
+ * @author Axel Nana <axel.nana@workbud.com>
134
+ */
135
+ export type WampSubscriptionHandlerArgs = Omit<
136
+ WampRegistrationHandlerArgs,
137
+ 'result_handler' | 'error_handler'
138
+ >;
139
+
140
+ /**
141
+ * The WAMP RPC handler function.
142
+ * @author Axel Nana <axel.nana@workbud.com>
143
+ * @param data The data sent to the handler.
144
+ */
145
+ export type WampRegistrationHandler = <T = any>(data: WampRegistrationHandlerArgs) => T;
146
+
147
+ /**
148
+ * The WAMP subscription handler function.
149
+ * @author Axel Nana <axel.nana@workbud.com>
150
+ * @param data The data sent to the handler.
151
+ */
152
+ export type WampSubscriptionHandler = <T = any>(data: WampSubscriptionHandlerArgs) => T;
153
+
154
+ /**
155
+ * Metadata for a WAMP RPC.
156
+ * @author Axel Nana <axel.nana@workbud.com>
157
+ */
158
+ type WampRegistration = {
159
+ /**
160
+ * The topic to register the RPC for.
161
+ */
162
+ topic: string;
163
+
164
+ /**
165
+ * The registration options for the RPC.
166
+ */
167
+ options?: WampRegistrationOptions;
168
+
169
+ /**
170
+ * The RPC handler function.
171
+ */
172
+ handler: WampRegistrationHandler;
173
+ };
174
+
175
+ /**
176
+ * Metadata for a WAMP subscription.
177
+ * @author Axel Nana <axel.nana@workbud.com>
178
+ */
179
+ type WampSubscription = {
180
+ /**
181
+ * The topic to subscribe to.
182
+ */
183
+ topic: string;
184
+
185
+ /**
186
+ * The subscription options for the subscription.
187
+ */
188
+ options?: WampSubscriptionOptions;
189
+
190
+ /**
191
+ * The subscription handler function.
192
+ */
193
+ handler: WampSubscriptionHandler;
194
+ };
195
+
196
+ export namespace Wamp {
197
+ /**
198
+ * Marks a class as a WAMP controller.
199
+ * @author Axel Nana <axel.nana@workbud.com>
200
+ * @param options The decorator options.
201
+ */
202
+ export const controller = (options: WampProps) => {
203
+ return function (target: Class<any>) {
204
+ async function handleWamp(): Promise<Elysia> {
205
+ // TODO: Use the logger service here
206
+ console.log(`Registering Wamp route for ${options.url} using ${target.name}`);
207
+ await nextTick();
208
+
209
+ const controller = Service.make(target);
210
+
211
+ const metadata = Reflect.getMetadata(Symbols.wamp, target) ?? {};
212
+
213
+ const w = new Wampy(options.url, {
214
+ ws: WampWebsocket,
215
+ ...omit(options, ['url']),
216
+ onClose: metadata.close?.bind(controller),
217
+ onError() {
218
+ metadata.error?.call(controller);
219
+ // TODO: Add more details like controller name and url
220
+ Event.emit('elysium:error', new Error('Wamp connection error'));
221
+ },
222
+ onReconnect: metadata.reconnect?.bind(controller),
223
+ onReconnectSuccess: metadata.reconnectSuccess?.bind(controller)
224
+ });
225
+
226
+ const registrations: WampRegistration[] = metadata.registrations ?? [];
227
+ const subscriptions: WampSubscription[] = metadata.subscriptions ?? [];
228
+
229
+ for (const registration of registrations) {
230
+ w.register(
231
+ registration.topic,
232
+ registration.handler.bind(controller),
233
+ registration.options
234
+ ).then((r: { topic: string; requestId: string; registrationId: string }) => {
235
+ // TODO: Use the logger service here
236
+ console.log(`Registered ${registration.topic} with id ${r.registrationId}`);
237
+ });
238
+ }
239
+
240
+ for (const subscription of subscriptions) {
241
+ w.subscribe(
242
+ subscription.topic,
243
+ subscription.handler.bind(controller),
244
+ subscription.options
245
+ ).then(
246
+ (s: {
247
+ topic: string;
248
+ requestId: string;
249
+ subscriptionId: string;
250
+ subscriptionKey: string;
251
+ }) => {
252
+ // TODO: Use the logger service here
253
+ console.log(`Subscribed ${subscription.topic} with id ${s.subscriptionId}`);
254
+ }
255
+ );
256
+ }
257
+
258
+ const app = new Elysia();
259
+
260
+ app.onStart(async (_) => {
261
+ await w.connect();
262
+ metadata.open?.call(controller);
263
+ });
264
+
265
+ return app;
266
+ }
267
+
268
+ Reflect.defineMetadata(Symbols.elysiaPlugin, handleWamp, target);
269
+ };
270
+ };
271
+
272
+ /**
273
+ * Registers a method as a WAMP RPC.
274
+ * @author Axel Nana <axel.nana@workbud.com>
275
+ * @param topic The RPC topic to register.
276
+ * @param options The registration options.
277
+ */
278
+ export const register = (topic: string, options?: WampRegistrationOptions): MethodDecorator => {
279
+ return function (target, _propertyKey, descriptor) {
280
+ const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
281
+
282
+ metadata.registrations ??= [];
283
+ metadata.registrations.push({
284
+ topic,
285
+ options,
286
+ handler: descriptor.value as WampRegistrationHandler
287
+ });
288
+
289
+ Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
290
+ };
291
+ };
292
+
293
+ /**
294
+ * Subscribes to a WAMP topic.
295
+ * @author Axel Nana <axel.nana@workbud.com>
296
+ * @param topic The topic to subscribe to.
297
+ * @param options The subscription options.
298
+ */
299
+ export const subscribe = (topic: string, options?: WampSubscriptionOptions): MethodDecorator => {
300
+ return function (target, _propertyKey, descriptor) {
301
+ const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
302
+
303
+ metadata.subscriptions ??= [];
304
+ metadata.subscriptions.push({
305
+ topic,
306
+ options,
307
+ handler: descriptor.value as WampRegistrationHandler
308
+ });
309
+
310
+ Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
311
+ };
312
+ };
313
+
314
+ /**
315
+ * Marks a method as the WAMP "open" event handler.
316
+ * @author Axel Nana <axel.nana@workbud.com>
317
+ *
318
+ * This decorator should be used on a WAMP controller method. Only one "open" event handler
319
+ * can be defined per WAMP controller.
320
+ */
321
+ export const onOpen = (): MethodDecorator => {
322
+ return function (target, _propertyKey, descriptor) {
323
+ const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
324
+ metadata.open = descriptor.value;
325
+ Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
326
+ };
327
+ };
328
+
329
+ /**
330
+ * Marks a method as the WAMP "close" event handler.
331
+ * @author Axel Nana <axel.nana@workbud.com>
332
+ *
333
+ * This decorator should be used on a WAMP controller method. Only one "close" event handler
334
+ * can be defined per WAMP controller.
335
+ */
336
+ export const onClose = (): MethodDecorator => {
337
+ return function (target, _propertyKey, descriptor) {
338
+ const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
339
+ metadata.close = descriptor.value;
340
+ Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
341
+ };
342
+ };
343
+
344
+ /**
345
+ * Marks a method as the WAMP "error" event handler.
346
+ * @author Axel Nana <axel.nana@workbud.com>
347
+ *
348
+ * This decorator should be used on a WAMP controller method. Only one "error" event handler
349
+ * can be defined per WAMP controller.
350
+ */
351
+ export const onError = (): MethodDecorator => {
352
+ return function (target, _propertyKey, descriptor) {
353
+ const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
354
+ metadata.error = descriptor.value;
355
+ Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
356
+ };
357
+ };
358
+
359
+ /**
360
+ * Marks a method as the WAMP "reconnect" event handler.
361
+ * @author Axel Nana <axel.nana@workbud.com>
362
+ *
363
+ * This decorator should be used on a WAMP controller method. Only one "reconnect" event handler
364
+ * can be defined per WAMP controller.
365
+ */
366
+ export const onReconnect = (): MethodDecorator => {
367
+ return function (target, _propertyKey, descriptor) {
368
+ const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
369
+ metadata.reconnect = descriptor.value;
370
+ Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
371
+ };
372
+ };
373
+
374
+ /**
375
+ * Marks a method as the WAMP "reconnectSuccess" event handler.
376
+ * @author Axel Nana <axel.nana@workbud.com>
377
+ *
378
+ * This decorator should be used on a WAMP controller method. Only one "reconnectSuccess" event handler
379
+ * can be defined per WAMP controller.
380
+ */
381
+ export const onReconnectSuccess = (): MethodDecorator => {
382
+ return function (target, _propertyKey, descriptor) {
383
+ const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
384
+ metadata.reconnectSuccess = descriptor.value;
385
+ Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
386
+ };
387
+ };
388
+ }
@@ -0,0 +1,186 @@
1
+ // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import type { ServerWebSocket } from 'bun';
16
+ import type { Context, ErrorContext, TSchema } from 'elysia';
17
+ import type { Class, ConditionalPick, JsonObject, Primitive } from 'type-fest';
18
+ import type { Route } from './http';
19
+
20
+ import { Elysia } from 'elysia';
21
+
22
+ import { Event } from './event';
23
+ import { Service } from './service';
24
+ import { nextTick, Symbols } from './utils';
25
+
26
+ /**
27
+ * Properties required when declaring a websocket controller using the `@websocket()` decorator.
28
+ * @author Axel Nana <axel.nana@workbud.com>
29
+ */
30
+ export type WebsocketProps = {
31
+ /**
32
+ * The path of the websocket route.
33
+ */
34
+ path: Route;
35
+
36
+ /**
37
+ * The options for the websocket server.
38
+ */
39
+ options?: ConditionalPick<Bun.WebSocketHandler, Primitive | JsonObject>;
40
+ };
41
+
42
+ /**
43
+ * The websocket connection instance.
44
+ * @author Axel Nana <axel.nana@workbud.com>
45
+ * @template TData Additional data stored in the websocket connection.
46
+ */
47
+ export type WS<TData = unknown> = ServerWebSocket<
48
+ {
49
+ /**
50
+ * Unique identifier for the websocket connection.
51
+ */
52
+ id: string;
53
+ } & Context &
54
+ TData
55
+ >;
56
+
57
+ /**
58
+ * The data generated when an error occurs in a websocket connection.
59
+ * @author Axel Nana <axel.nana@workbud.com>
60
+ */
61
+ export type WSError = ErrorContext & { error: Readonly<Error> };
62
+
63
+ export namespace Websocket {
64
+ /**
65
+ * Marks a class as a websocket controller.
66
+ * @author Axel Nana <axel.nana@workbud.com>
67
+ * @param props The websocket route properties.
68
+ */
69
+ export const controller = (props: WebsocketProps) => {
70
+ return function (target: Class<any>) {
71
+ async function handleWebsocket(): Promise<Elysia> {
72
+ // TODO: Use the logger service here
73
+ console.log(`Registering Websocket route for ${props.path} using ${target.name}`);
74
+ await nextTick();
75
+
76
+ const controller = Service.make(target);
77
+
78
+ const metadata = Reflect.getMetadata(Symbols.websocket, target) ?? {};
79
+ const open = metadata.open?.bind(controller);
80
+ const close = metadata.close?.bind(controller);
81
+ const message = metadata.message?.bind(controller);
82
+ const drain = metadata.drain?.bind(controller);
83
+ const error =
84
+ metadata.error?.bind(controller) ??
85
+ ((e: WSError) => Event.emit('elysium:error', e.error));
86
+
87
+ const app = new Elysia();
88
+
89
+ // TODO: Add middlewares here
90
+ app.ws(props.path, {
91
+ // TODO: beforeHandle
92
+ // TODO: afterHandle
93
+ open,
94
+ close,
95
+ message,
96
+ drain,
97
+ error,
98
+ body: metadata.body,
99
+ ...props.options
100
+ });
101
+
102
+ return app;
103
+ }
104
+
105
+ Reflect.defineMetadata(Symbols.elysiaPlugin, handleWebsocket, target);
106
+ };
107
+ };
108
+
109
+ /**
110
+ * Marks a method as the websocket "open" event handler.
111
+ * @author Axel Nana <axel.nana@workbud.com>
112
+ *
113
+ * This decorator should be used on a websocket controller method. Only one "open" event handler
114
+ * can be defined per websocket controller.
115
+ */
116
+ export const onOpen = (): MethodDecorator => {
117
+ return function (target, propertyKey, descriptor) {
118
+ const metadata = Reflect.getMetadata(Symbols.websocket, target.constructor) ?? {};
119
+ metadata.open = descriptor.value;
120
+ Reflect.defineMetadata(Symbols.websocket, metadata, target.constructor);
121
+ };
122
+ };
123
+
124
+ /**
125
+ * Marks a method as the websocket "close" event handler.
126
+ * @author Axel Nana <axel.nana@workbud.com>
127
+ *
128
+ * This decorator should be used on a websocket controller method. Only one "close" event handler
129
+ * can be defined per websocket controller.
130
+ */
131
+ export const onClose = (): MethodDecorator => {
132
+ return function (target, propertyKey, descriptor) {
133
+ const metadata = Reflect.getMetadata(Symbols.websocket, target.constructor) ?? {};
134
+ metadata.close = descriptor.value;
135
+ Reflect.defineMetadata(Symbols.websocket, metadata, target.constructor);
136
+ };
137
+ };
138
+
139
+ /**
140
+ * Marks a method as the websocket "message" event handler.
141
+ * @author Axel Nana <axel.nana@workbud.com>
142
+ *
143
+ * This decorator should be used on a websocket controller method. Only one "message" event handler
144
+ * can be defined per websocket controller.
145
+ *
146
+ * @param schema The schema of the message body.
147
+ */
148
+ export const onMessage = (schema: TSchema): MethodDecorator => {
149
+ return function (target, propertyKey, descriptor) {
150
+ const metadata = Reflect.getMetadata(Symbols.websocket, target.constructor) ?? {};
151
+ metadata.message = descriptor.value;
152
+ metadata.body = schema;
153
+ Reflect.defineMetadata(Symbols.websocket, metadata, target.constructor);
154
+ };
155
+ };
156
+
157
+ /**
158
+ * Marks a method as the websocket "drain" event handler.
159
+ * @author Axel Nana <axel.nana@workbud.com>
160
+ *
161
+ * This decorator should be used on a websocket controller method. Only one "drain" event handler
162
+ * can be defined per websocket controller.
163
+ */
164
+ export const onDrain = (): MethodDecorator => {
165
+ return function (target, propertyKey, descriptor) {
166
+ const metadata = Reflect.getMetadata(Symbols.websocket, target.constructor) ?? {};
167
+ metadata.drain = descriptor.value;
168
+ Reflect.defineMetadata(Symbols.websocket, metadata, target.constructor);
169
+ };
170
+ };
171
+
172
+ /**
173
+ * Marks a method as the websocket "error" event handler.
174
+ * @author Axel Nana <axel.nana@workbud.com>
175
+ *
176
+ * This decorator should be used on a websocket controller method. Only one "error" event handler
177
+ * can be defined per websocket controller.
178
+ */
179
+ export const onError = (): MethodDecorator => {
180
+ return function (target, propertyKey, descriptor) {
181
+ const metadata = Reflect.getMetadata(Symbols.websocket, target.constructor) ?? {};
182
+ metadata.error = descriptor.value;
183
+ Reflect.defineMetadata(Symbols.websocket, metadata, target.constructor);
184
+ };
185
+ };
186
+ }