@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { Middleware, compose, createPipeline } from '../../src/middleware';
|
|
3
|
+
import { Context } from '../../src/context';
|
|
4
|
+
|
|
5
|
+
describe('Middleware Pipeline', () => {
|
|
6
|
+
let mockRequest: Request;
|
|
7
|
+
let context: Context;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockRequest = new Request('http://localhost:3000/test', { method: 'GET' });
|
|
11
|
+
context = new Context(mockRequest, {});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('compose', () => {
|
|
15
|
+
test('should compose empty middleware array', async () => {
|
|
16
|
+
const pipeline = compose([]);
|
|
17
|
+
const handler = () => new Response('OK');
|
|
18
|
+
const response = await pipeline(context, handler);
|
|
19
|
+
|
|
20
|
+
expect(response.status).toBe(200);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should compose single middleware', async () => {
|
|
24
|
+
const middleware: Middleware = async (ctx, next) => {
|
|
25
|
+
ctx.set('executed', true);
|
|
26
|
+
return await next();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const pipeline = compose([middleware]);
|
|
30
|
+
const handler = () => new Response('OK');
|
|
31
|
+
const response = await pipeline(context, handler);
|
|
32
|
+
|
|
33
|
+
expect(context.get('executed')).toBe(true);
|
|
34
|
+
expect(response.status).toBe(200);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should compose multiple middleware in order', async () => {
|
|
38
|
+
const order: number[] = [];
|
|
39
|
+
|
|
40
|
+
const middleware1: Middleware = async (ctx, next) => {
|
|
41
|
+
order.push(1);
|
|
42
|
+
await next();
|
|
43
|
+
order.push(4);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const middleware2: Middleware = async (ctx, next) => {
|
|
47
|
+
order.push(2);
|
|
48
|
+
await next();
|
|
49
|
+
order.push(3);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const pipeline = compose([middleware1, middleware2]);
|
|
53
|
+
const handler = () => {
|
|
54
|
+
order.push(5);
|
|
55
|
+
return new Response('OK');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
await pipeline(context, handler);
|
|
59
|
+
|
|
60
|
+
expect(order).toEqual([1, 2, 5, 3, 4]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should pass context through middleware', async () => {
|
|
64
|
+
const middleware: Middleware = async (ctx, next) => {
|
|
65
|
+
ctx.set('before', 'value1');
|
|
66
|
+
const response = await next();
|
|
67
|
+
ctx.set('after', 'value2');
|
|
68
|
+
return response;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const pipeline = compose([middleware]);
|
|
72
|
+
await pipeline(context, () => new Response('OK'));
|
|
73
|
+
|
|
74
|
+
expect(context.get('before')).toBe('value1');
|
|
75
|
+
expect(context.get('after')).toBe('value2');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should allow early termination', async () => {
|
|
79
|
+
const middleware: Middleware = async (ctx, next) => {
|
|
80
|
+
return new Response('Unauthorized', { status: 401 });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const pipeline = compose([middleware]);
|
|
84
|
+
const handler = () => {
|
|
85
|
+
throw new Error('Should not be called');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const response = await pipeline(context, handler);
|
|
89
|
+
expect(response.status).toBe(401);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('should handle errors in middleware', async () => {
|
|
93
|
+
const middleware: Middleware = async (ctx, next) => {
|
|
94
|
+
try {
|
|
95
|
+
return await next();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const pipeline = compose([middleware]);
|
|
102
|
+
const handler = () => {
|
|
103
|
+
throw new Error('Handler error');
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const response = await pipeline(context, handler);
|
|
107
|
+
expect(response.status).toBe(500);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('createPipeline', () => {
|
|
112
|
+
test('should create reusable pipeline', async () => {
|
|
113
|
+
const pipeline = createPipeline();
|
|
114
|
+
|
|
115
|
+
pipeline.use(async (ctx, next) => {
|
|
116
|
+
ctx.set('step1', true);
|
|
117
|
+
return await next();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
pipeline.use(async (ctx, next) => {
|
|
121
|
+
ctx.set('step2', true);
|
|
122
|
+
return await next();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const response = await pipeline.execute(context, () => new Response('OK'));
|
|
126
|
+
|
|
127
|
+
expect(context.get('step1')).toBe(true);
|
|
128
|
+
expect(context.get('step2')).toBe(true);
|
|
129
|
+
expect(response.status).toBe(200);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('should allow adding middleware after creation', async () => {
|
|
133
|
+
const pipeline = createPipeline();
|
|
134
|
+
|
|
135
|
+
pipeline.use(async (ctx, next) => {
|
|
136
|
+
ctx.set('count', 1);
|
|
137
|
+
return await next();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let response = await pipeline.execute(context, () => new Response('OK'));
|
|
141
|
+
expect(context.get('count')).toBe(1);
|
|
142
|
+
|
|
143
|
+
// Add more middleware
|
|
144
|
+
pipeline.use(async (ctx, next) => {
|
|
145
|
+
ctx.set('count', (ctx.get('count') as number) + 1);
|
|
146
|
+
return await next();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const ctx2 = new Context(mockRequest, {});
|
|
150
|
+
response = await pipeline.execute(ctx2, () => new Response('OK'));
|
|
151
|
+
expect(ctx2.get('count')).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('Built-in Middleware', () => {
|
|
156
|
+
test('logger middleware should work', async () => {
|
|
157
|
+
const { logger } = await import('../../src/middleware/built-in');
|
|
158
|
+
|
|
159
|
+
const pipeline = compose([logger()]);
|
|
160
|
+
const response = await pipeline(context, () => new Response('OK'));
|
|
161
|
+
|
|
162
|
+
expect(response.status).toBe(200);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('CORS middleware should add headers', async () => {
|
|
166
|
+
const { cors } = await import('../../src/middleware/built-in');
|
|
167
|
+
|
|
168
|
+
const pipeline = compose([cors()]);
|
|
169
|
+
const response = await pipeline(context, () => new Response('OK'));
|
|
170
|
+
|
|
171
|
+
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('requestId middleware should set request ID', async () => {
|
|
175
|
+
const { requestId } = await import('../../src/middleware/built-in');
|
|
176
|
+
|
|
177
|
+
const pipeline = compose([requestId()]);
|
|
178
|
+
const response = await pipeline(context, () => new Response('OK'));
|
|
179
|
+
|
|
180
|
+
expect(context.get('requestId')).toBeDefined();
|
|
181
|
+
expect(response.headers.get('X-Request-Id')).toBeDefined();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { Module, Controller, Injectable, createApp, AppModule } from '../../src/modules';
|
|
3
|
+
import { Container, Token } from '../../src/container';
|
|
4
|
+
|
|
5
|
+
// Helper to check metadata (since we use WeakMap storage)
|
|
6
|
+
function hasInjectable(target: abstract new (...args: unknown[]) => unknown): boolean {
|
|
7
|
+
// Check if class has Injectable metadata by trying to instantiate
|
|
8
|
+
// In a real scenario, the metadata is stored internally
|
|
9
|
+
return true; // Simplified for test
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Test fixtures
|
|
13
|
+
interface IUserService {
|
|
14
|
+
getUser(id: string): { id: string; name: string };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface IUserRepository {
|
|
18
|
+
findById(id: string): { id: string; name: string } | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const USER_SERVICE_TOKEN = Token<IUserService>('IUserService');
|
|
22
|
+
const USER_REPOSITORY_TOKEN = Token<IUserRepository>('IUserRepository');
|
|
23
|
+
|
|
24
|
+
class UserRepository implements IUserRepository {
|
|
25
|
+
private users = new Map([
|
|
26
|
+
['1', { id: '1', name: 'John' }],
|
|
27
|
+
['2', { id: '2', name: 'Jane' }],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
findById(id: string): { id: string; name: string } | null {
|
|
31
|
+
return this.users.get(id) ?? null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class UserService implements IUserService {
|
|
36
|
+
constructor(private repo: IUserRepository) {}
|
|
37
|
+
|
|
38
|
+
getUser(id: string): { id: string; name: string } {
|
|
39
|
+
const user = this.repo.findById(id);
|
|
40
|
+
return user ?? { id, name: 'Unknown' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class UsersController {
|
|
45
|
+
constructor(private userService: IUserService) {}
|
|
46
|
+
|
|
47
|
+
findAll() {
|
|
48
|
+
return [{ id: '1', name: 'John' }, { id: '2', name: 'Jane' }];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
findOne(id: string) {
|
|
52
|
+
return this.userService.getUser(id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Module({
|
|
57
|
+
providers: [
|
|
58
|
+
{ token: USER_REPOSITORY_TOKEN, useClass: UserRepository },
|
|
59
|
+
{ token: USER_SERVICE_TOKEN, useClass: UserService, inject: [USER_REPOSITORY_TOKEN] },
|
|
60
|
+
],
|
|
61
|
+
controllers: [UsersController],
|
|
62
|
+
exports: [USER_SERVICE_TOKEN],
|
|
63
|
+
})
|
|
64
|
+
class UsersModule {}
|
|
65
|
+
|
|
66
|
+
@Module({
|
|
67
|
+
imports: [UsersModule],
|
|
68
|
+
})
|
|
69
|
+
class TestAppModule {}
|
|
70
|
+
|
|
71
|
+
describe('Module System', () => {
|
|
72
|
+
describe('@Injectable Decorator', () => {
|
|
73
|
+
test('should mark class as injectable', () => {
|
|
74
|
+
@Injectable()
|
|
75
|
+
class TestService {}
|
|
76
|
+
|
|
77
|
+
// Class should be returned unchanged
|
|
78
|
+
expect(TestService).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('@Controller Decorator', () => {
|
|
83
|
+
test('should mark class as controller with path', () => {
|
|
84
|
+
@Controller('/api')
|
|
85
|
+
class TestController {}
|
|
86
|
+
|
|
87
|
+
// Class should be returned unchanged
|
|
88
|
+
expect(TestController).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('@Module Decorator', () => {
|
|
93
|
+
test('should define module metadata', () => {
|
|
94
|
+
@Module({
|
|
95
|
+
providers: [],
|
|
96
|
+
controllers: [],
|
|
97
|
+
})
|
|
98
|
+
class TestModule {}
|
|
99
|
+
|
|
100
|
+
// Module should be defined
|
|
101
|
+
expect(TestModule).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('AppModule', () => {
|
|
106
|
+
test('should create module from class', () => {
|
|
107
|
+
const appModule = new AppModule(TestAppModule);
|
|
108
|
+
expect(appModule).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should collect providers from module tree', () => {
|
|
112
|
+
const appModule = new AppModule(TestAppModule);
|
|
113
|
+
const providers = appModule.getProviders();
|
|
114
|
+
|
|
115
|
+
expect(providers.length).toBeGreaterThan(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should collect controllers from module tree', () => {
|
|
119
|
+
const appModule = new AppModule(TestAppModule);
|
|
120
|
+
const controllers = appModule.getControllers();
|
|
121
|
+
|
|
122
|
+
expect(controllers.length).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('createApp', () => {
|
|
127
|
+
test('should create application with module', () => {
|
|
128
|
+
const app = createApp(TestAppModule);
|
|
129
|
+
expect(app).toBeDefined();
|
|
130
|
+
expect(app.container).toBeInstanceOf(Container);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should resolve imported module providers', async () => {
|
|
134
|
+
const app = createApp(TestAppModule);
|
|
135
|
+
await app.init();
|
|
136
|
+
const userService = app.container.resolve(USER_SERVICE_TOKEN);
|
|
137
|
+
|
|
138
|
+
expect(userService).toBeDefined();
|
|
139
|
+
expect(userService.getUser('1')).toEqual({ id: '1', name: 'John' });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { PubSub, createPubSub, createMemoryPubSub } from '../../src/websocket';
|
|
3
|
+
|
|
4
|
+
describe('PubSub (In-Memory)', () => {
|
|
5
|
+
let pubsub: PubSub;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
pubsub = createMemoryPubSub();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
pubsub.destroy();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Basic Operations', () => {
|
|
16
|
+
test('should publish and receive messages', async () => {
|
|
17
|
+
const messages: unknown[] = [];
|
|
18
|
+
|
|
19
|
+
await pubsub.subscribe('test-channel', (message) => {
|
|
20
|
+
messages.push(message.data);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await pubsub.publish('test-channel', 'hello world');
|
|
24
|
+
await pubsub.publish('test-channel', { foo: 'bar' });
|
|
25
|
+
|
|
26
|
+
expect(messages.length).toBe(2);
|
|
27
|
+
expect(messages[0]).toBe('hello world');
|
|
28
|
+
expect(messages[1]).toEqual({ foo: 'bar' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should support multiple subscribers on same channel', async () => {
|
|
32
|
+
const messages1: unknown[] = [];
|
|
33
|
+
const messages2: unknown[] = [];
|
|
34
|
+
|
|
35
|
+
await pubsub.subscribe('shared-channel', (msg) => { messages1.push(msg.data); });
|
|
36
|
+
await pubsub.subscribe('shared-channel', (msg) => { messages2.push(msg.data); });
|
|
37
|
+
|
|
38
|
+
const delivered = await pubsub.publish('shared-channel', 'broadcast');
|
|
39
|
+
|
|
40
|
+
expect(delivered).toBe(2);
|
|
41
|
+
expect(messages1).toEqual(['broadcast']);
|
|
42
|
+
expect(messages2).toEqual(['broadcast']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should return unsubscribe function', async () => {
|
|
46
|
+
const messages: unknown[] = [];
|
|
47
|
+
|
|
48
|
+
const unsubscribe = await pubsub.subscribe('temp-channel', (msg) => {
|
|
49
|
+
messages.push(msg.data);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await pubsub.publish('temp-channel', 'message 1');
|
|
53
|
+
expect(messages.length).toBe(1);
|
|
54
|
+
|
|
55
|
+
// Unsubscribe
|
|
56
|
+
await unsubscribe();
|
|
57
|
+
|
|
58
|
+
await pubsub.publish('temp-channel', 'message 2');
|
|
59
|
+
expect(messages.length).toBe(1); // Should still be 1
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should handle async callbacks', async () => {
|
|
63
|
+
const messages: string[] = [];
|
|
64
|
+
|
|
65
|
+
await pubsub.subscribe('async-channel', async (msg) => {
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
67
|
+
messages.push(`processed: ${msg.data}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await pubsub.publish('async-channel', 'test');
|
|
71
|
+
|
|
72
|
+
// Wait for async callback
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
74
|
+
|
|
75
|
+
expect(messages).toEqual(['processed: test']);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('Pattern Subscriptions', () => {
|
|
80
|
+
test('should match pattern with * wildcard', async () => {
|
|
81
|
+
const messages: string[] = [];
|
|
82
|
+
|
|
83
|
+
await pubsub.psubscribe('user:*', (msg) => {
|
|
84
|
+
messages.push(msg.channel);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await pubsub.publish('user:123', 'data');
|
|
88
|
+
await pubsub.publish('user:456', 'data');
|
|
89
|
+
await pubsub.publish('post:789', 'data');
|
|
90
|
+
|
|
91
|
+
expect(messages).toEqual(['user:123', 'user:456']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should match pattern with ? single char', async () => {
|
|
95
|
+
const messages: string[] = [];
|
|
96
|
+
|
|
97
|
+
await pubsub.psubscribe('user:?', (msg) => {
|
|
98
|
+
messages.push(msg.channel);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await pubsub.publish('user:1', 'data');
|
|
102
|
+
await pubsub.publish('user:a', 'data');
|
|
103
|
+
await pubsub.publish('user:12', 'data'); // Should not match (2 chars)
|
|
104
|
+
|
|
105
|
+
expect(messages).toEqual(['user:1', 'user:a']);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('should match complex patterns', async () => {
|
|
109
|
+
const messages: string[] = [];
|
|
110
|
+
|
|
111
|
+
await pubsub.psubscribe('app:*:event', (msg) => {
|
|
112
|
+
messages.push(msg.channel);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await pubsub.publish('app:user:event', 'data');
|
|
116
|
+
await pubsub.publish('app:order:event', 'data');
|
|
117
|
+
await pubsub.publish('app:user:other', 'data');
|
|
118
|
+
|
|
119
|
+
expect(messages).toEqual(['app:user:event', 'app:order:event']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('should include pattern in message', async () => {
|
|
123
|
+
let receivedPattern: string | undefined;
|
|
124
|
+
|
|
125
|
+
await pubsub.psubscribe('test:*', (msg) => {
|
|
126
|
+
receivedPattern = msg.pattern;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await pubsub.publish('test:channel', 'data');
|
|
130
|
+
|
|
131
|
+
expect(receivedPattern).toBe('test:*');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('Subscriber Management', () => {
|
|
136
|
+
test('should count channel subscribers', async () => {
|
|
137
|
+
expect(pubsub.getChannelSubscribers('my-channel')).toBe(0);
|
|
138
|
+
|
|
139
|
+
await pubsub.subscribe('my-channel', () => {});
|
|
140
|
+
expect(pubsub.getChannelSubscribers('my-channel')).toBe(1);
|
|
141
|
+
|
|
142
|
+
await pubsub.subscribe('my-channel', () => {});
|
|
143
|
+
expect(pubsub.getChannelSubscribers('my-channel')).toBe(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should count pattern subscribers', async () => {
|
|
147
|
+
expect(pubsub.getPatternSubscribers('test:*')).toBe(0);
|
|
148
|
+
|
|
149
|
+
await pubsub.psubscribe('test:*', () => {});
|
|
150
|
+
expect(pubsub.getPatternSubscribers('test:*')).toBe(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should count total subscribers', async () => {
|
|
154
|
+
await pubsub.subscribe('channel1', () => {});
|
|
155
|
+
await pubsub.subscribe('channel2', () => {});
|
|
156
|
+
await pubsub.psubscribe('pattern:*', () => {});
|
|
157
|
+
|
|
158
|
+
expect(pubsub.getTotalSubscribers()).toBe(3);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('should unsubscribe all from channel', async () => {
|
|
162
|
+
await pubsub.subscribe('multi-channel', () => {});
|
|
163
|
+
await pubsub.subscribe('multi-channel', () => {});
|
|
164
|
+
|
|
165
|
+
expect(pubsub.getChannelSubscribers('multi-channel')).toBe(2);
|
|
166
|
+
|
|
167
|
+
await pubsub.unsubscribe('multi-channel');
|
|
168
|
+
|
|
169
|
+
expect(pubsub.getChannelSubscribers('multi-channel')).toBe(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should punsubscribe all from pattern', async () => {
|
|
173
|
+
await pubsub.psubscribe('test:*', () => {});
|
|
174
|
+
await pubsub.psubscribe('test:*', () => {});
|
|
175
|
+
|
|
176
|
+
expect(pubsub.getPatternSubscribers('test:*')).toBe(2);
|
|
177
|
+
|
|
178
|
+
await pubsub.punsubscribe('test:*');
|
|
179
|
+
|
|
180
|
+
expect(pubsub.getPatternSubscribers('test:*')).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Clear Operations', () => {
|
|
185
|
+
test('should clear all subscriptions', async () => {
|
|
186
|
+
await pubsub.subscribe('channel1', () => {});
|
|
187
|
+
await pubsub.subscribe('channel2', () => {});
|
|
188
|
+
await pubsub.psubscribe('pattern:*', () => {});
|
|
189
|
+
|
|
190
|
+
await pubsub.clear();
|
|
191
|
+
|
|
192
|
+
expect(pubsub.getTotalSubscribers()).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Message Structure', () => {
|
|
197
|
+
test('should include timestamp in message', async () => {
|
|
198
|
+
let receivedTimestamp: number | undefined;
|
|
199
|
+
const beforeTime = Date.now();
|
|
200
|
+
|
|
201
|
+
await pubsub.subscribe('timestamp-channel', (msg) => {
|
|
202
|
+
receivedTimestamp = msg.timestamp;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await pubsub.publish('timestamp-channel', 'test');
|
|
206
|
+
|
|
207
|
+
expect(receivedTimestamp).toBeDefined();
|
|
208
|
+
expect(receivedTimestamp!).toBeGreaterThanOrEqual(beforeTime);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('should include channel in message', async () => {
|
|
212
|
+
let receivedChannel: string | undefined;
|
|
213
|
+
|
|
214
|
+
await pubsub.subscribe('my-channel', (msg) => {
|
|
215
|
+
receivedChannel = msg.channel;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await pubsub.publish('my-channel', 'test');
|
|
219
|
+
|
|
220
|
+
expect(receivedChannel).toBe('my-channel');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('PubSub Factory Functions', () => {
|
|
226
|
+
test('createPubSub should create memory pubsub by default', () => {
|
|
227
|
+
const pubsub = createPubSub();
|
|
228
|
+
expect(pubsub.getDriverType()).toBe('memory');
|
|
229
|
+
pubsub.destroy();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('createPubSub with driver option', () => {
|
|
233
|
+
const memPubsub = createPubSub({ driver: 'memory' });
|
|
234
|
+
expect(memPubsub.getDriverType()).toBe('memory');
|
|
235
|
+
memPubsub.destroy();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('createMemoryPubSub should create memory pubsub', () => {
|
|
239
|
+
const pubsub = createMemoryPubSub();
|
|
240
|
+
expect(pubsub.getDriverType()).toBe('memory');
|
|
241
|
+
pubsub.destroy();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('PubSub Connection State', () => {
|
|
246
|
+
test('should be connected after initialization for memory', () => {
|
|
247
|
+
const pubsub = createMemoryPubSub();
|
|
248
|
+
expect(pubsub.isConnected).toBe(true);
|
|
249
|
+
pubsub.destroy();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('should not be connected after destroy', () => {
|
|
253
|
+
const pubsub = createMemoryPubSub();
|
|
254
|
+
pubsub.destroy();
|
|
255
|
+
expect(pubsub.isConnected).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|