@onebun/core 0.1.0 → 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.
- package/README.md +277 -3
- package/package.json +13 -2
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +2919 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Redis Provider
|
|
3
|
+
*
|
|
4
|
+
* Singleton provider for sharing Redis connection across multiple consumers
|
|
5
|
+
* (cache, websocket, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Context,
|
|
10
|
+
Effect,
|
|
11
|
+
Layer,
|
|
12
|
+
} from 'effect';
|
|
13
|
+
|
|
14
|
+
import { RedisClient, type RedisClientOptions } from './redis-client';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for shared Redis connection
|
|
18
|
+
*/
|
|
19
|
+
export interface SharedRedisOptions {
|
|
20
|
+
/** Redis connection URL */
|
|
21
|
+
url: string;
|
|
22
|
+
/** Key prefix for all operations */
|
|
23
|
+
keyPrefix?: string;
|
|
24
|
+
/** Enable automatic reconnection */
|
|
25
|
+
reconnect?: boolean;
|
|
26
|
+
/** Enable TLS */
|
|
27
|
+
tls?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Singleton provider for shared Redis connection
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // Configure at app startup
|
|
36
|
+
* SharedRedisProvider.configure({
|
|
37
|
+
* url: 'redis://localhost:6379',
|
|
38
|
+
* keyPrefix: 'myapp:',
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Get shared client
|
|
42
|
+
* const client = await SharedRedisProvider.getClient();
|
|
43
|
+
*
|
|
44
|
+
* // Use in cache
|
|
45
|
+
* const cache = new RedisCache({ useSharedClient: true });
|
|
46
|
+
*
|
|
47
|
+
* // Use in WebSocket storage
|
|
48
|
+
* const wsStorage = new RedisWsStorage(await SharedRedisProvider.getClient());
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export class SharedRedisProvider {
|
|
52
|
+
private static instance: RedisClient | null = null;
|
|
53
|
+
private static options: SharedRedisOptions | null = null;
|
|
54
|
+
private static connecting: Promise<RedisClient> | null = null;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Configure the shared Redis connection
|
|
58
|
+
* Must be called before getClient()
|
|
59
|
+
*/
|
|
60
|
+
static configure(options: SharedRedisOptions): void {
|
|
61
|
+
// Note: If already connected, configuration will apply to new connections only
|
|
62
|
+
SharedRedisProvider.options = options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the shared Redis client (creates connection if needed)
|
|
67
|
+
*/
|
|
68
|
+
static async getClient(): Promise<RedisClient> {
|
|
69
|
+
// Return existing instance
|
|
70
|
+
if (SharedRedisProvider.instance?.isConnected()) {
|
|
71
|
+
return SharedRedisProvider.instance;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Wait for existing connection attempt
|
|
75
|
+
if (SharedRedisProvider.connecting) {
|
|
76
|
+
return await SharedRedisProvider.connecting;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check configuration
|
|
80
|
+
if (!SharedRedisProvider.options) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
'SharedRedisProvider not configured. Call SharedRedisProvider.configure() first.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create new connection
|
|
87
|
+
SharedRedisProvider.connecting = SharedRedisProvider.createConnection();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
SharedRedisProvider.instance = await SharedRedisProvider.connecting;
|
|
91
|
+
|
|
92
|
+
return SharedRedisProvider.instance;
|
|
93
|
+
} finally {
|
|
94
|
+
SharedRedisProvider.connecting = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a new Redis connection
|
|
100
|
+
*/
|
|
101
|
+
private static async createConnection(): Promise<RedisClient> {
|
|
102
|
+
const options = SharedRedisProvider.options!;
|
|
103
|
+
const client = new RedisClient({
|
|
104
|
+
url: options.url,
|
|
105
|
+
keyPrefix: options.keyPrefix,
|
|
106
|
+
reconnect: options.reconnect,
|
|
107
|
+
tls: options.tls,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await client.connect();
|
|
111
|
+
|
|
112
|
+
return client;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Disconnect the shared client
|
|
117
|
+
*/
|
|
118
|
+
static async disconnect(): Promise<void> {
|
|
119
|
+
if (SharedRedisProvider.instance) {
|
|
120
|
+
await SharedRedisProvider.instance.disconnect();
|
|
121
|
+
SharedRedisProvider.instance = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if shared client is connected
|
|
127
|
+
*/
|
|
128
|
+
static isConnected(): boolean {
|
|
129
|
+
return SharedRedisProvider.instance?.isConnected() ?? false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if shared Redis is configured
|
|
134
|
+
*/
|
|
135
|
+
static isConfigured(): boolean {
|
|
136
|
+
return SharedRedisProvider.options !== null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create a standalone Redis client (not shared)
|
|
141
|
+
* Useful for isolated scenarios
|
|
142
|
+
*/
|
|
143
|
+
static createClient(options?: Partial<SharedRedisOptions>): RedisClient {
|
|
144
|
+
const baseOptions: Partial<SharedRedisOptions> = SharedRedisProvider.options || {};
|
|
145
|
+
const finalOptions: RedisClientOptions = {
|
|
146
|
+
url: options?.url || baseOptions.url || '',
|
|
147
|
+
keyPrefix: options?.keyPrefix ?? baseOptions.keyPrefix,
|
|
148
|
+
reconnect: options?.reconnect ?? baseOptions.reconnect ?? true,
|
|
149
|
+
tls: options?.tls ?? baseOptions.tls,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (!finalOptions.url) {
|
|
153
|
+
throw new Error('Redis URL is required');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return new RedisClient(finalOptions);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Reset provider state (mainly for testing)
|
|
161
|
+
*/
|
|
162
|
+
static async reset(): Promise<void> {
|
|
163
|
+
await SharedRedisProvider.disconnect();
|
|
164
|
+
SharedRedisProvider.options = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// Effect.js Integration
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Effect.js Tag for shared Redis service
|
|
174
|
+
*/
|
|
175
|
+
export class SharedRedisService extends Context.Tag('SharedRedisService')<
|
|
176
|
+
SharedRedisService,
|
|
177
|
+
RedisClient
|
|
178
|
+
>() {}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create Effect.js Layer for shared Redis
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* const redisLayer = makeSharedRedisLayer({
|
|
186
|
+
* url: 'redis://localhost:6379',
|
|
187
|
+
* keyPrefix: 'myapp:',
|
|
188
|
+
* });
|
|
189
|
+
*
|
|
190
|
+
* const program = pipe(
|
|
191
|
+
* SharedRedisService,
|
|
192
|
+
* Effect.flatMap(redis => redis.get('key')),
|
|
193
|
+
* Effect.provide(redisLayer),
|
|
194
|
+
* );
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function makeSharedRedisLayer(
|
|
198
|
+
options: SharedRedisOptions,
|
|
199
|
+
): Layer.Layer<SharedRedisService> {
|
|
200
|
+
return Layer.scoped(
|
|
201
|
+
SharedRedisService,
|
|
202
|
+
Effect.gen(function* () {
|
|
203
|
+
// Configure shared provider
|
|
204
|
+
SharedRedisProvider.configure(options);
|
|
205
|
+
|
|
206
|
+
// Get client
|
|
207
|
+
const client = yield* Effect.promise(() => SharedRedisProvider.getClient());
|
|
208
|
+
|
|
209
|
+
// Return client with cleanup
|
|
210
|
+
yield* Effect.addFinalizer(() =>
|
|
211
|
+
Effect.promise(async () => {
|
|
212
|
+
// Note: We don't disconnect shared client on scope close
|
|
213
|
+
// because other consumers might still be using it
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return client;
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get shared Redis client as Effect
|
|
224
|
+
*/
|
|
225
|
+
export const getSharedRedis = Effect.gen(function* () {
|
|
226
|
+
if (!SharedRedisProvider.isConfigured()) {
|
|
227
|
+
return yield* Effect.fail(new Error('SharedRedisProvider not configured'));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return yield* Effect.promise(() => SharedRedisProvider.getClient());
|
|
231
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -266,6 +266,56 @@ export interface ApplicationOptions {
|
|
|
266
266
|
batchTimeout?: number;
|
|
267
267
|
};
|
|
268
268
|
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* WebSocket configuration
|
|
272
|
+
*/
|
|
273
|
+
websocket?: WebSocketApplicationOptions;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Enable graceful shutdown on SIGTERM/SIGINT
|
|
277
|
+
* When enabled, the application will cleanly shutdown on process signals,
|
|
278
|
+
* including closing shared Redis connections.
|
|
279
|
+
* Set to false to disable automatic signal handling.
|
|
280
|
+
* @defaultValue true
|
|
281
|
+
*/
|
|
282
|
+
gracefulShutdown?: boolean;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* WebSocket storage type
|
|
287
|
+
*/
|
|
288
|
+
export type WsStorageType = 'memory' | 'redis';
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* WebSocket storage options
|
|
292
|
+
*/
|
|
293
|
+
export interface WsStorageOptions {
|
|
294
|
+
/** Storage type */
|
|
295
|
+
type: WsStorageType;
|
|
296
|
+
/** Redis-specific options */
|
|
297
|
+
redis?: {
|
|
298
|
+
/** Redis connection URL */
|
|
299
|
+
url: string;
|
|
300
|
+
/** Key prefix for Redis keys */
|
|
301
|
+
prefix?: string;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* WebSocket configuration for OneBunApplication
|
|
307
|
+
*/
|
|
308
|
+
export interface WebSocketApplicationOptions {
|
|
309
|
+
/** Enable/disable WebSocket (default: auto - enabled if gateways exist) */
|
|
310
|
+
enabled?: boolean;
|
|
311
|
+
/** Storage options */
|
|
312
|
+
storage?: WsStorageOptions;
|
|
313
|
+
/** Ping interval in milliseconds for heartbeat (socket.io) */
|
|
314
|
+
pingInterval?: number;
|
|
315
|
+
/** Ping timeout in milliseconds (socket.io) */
|
|
316
|
+
pingTimeout?: number;
|
|
317
|
+
/** Maximum payload size in bytes */
|
|
318
|
+
maxPayload?: number;
|
|
269
319
|
}
|
|
270
320
|
|
|
271
321
|
/**
|