@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
package/src/index.ts
CHANGED
|
@@ -63,3 +63,99 @@ export type { ServiceClientOptions, ControllerClient } from './service-client.ty
|
|
|
63
63
|
|
|
64
64
|
// ENV resolver
|
|
65
65
|
export { resolveEnvOverrides, resolveEnvOverridesSync } from './env-resolver';
|
|
66
|
+
|
|
67
|
+
// WebSocket Gateway
|
|
68
|
+
export { BaseWebSocketGateway } from './ws-base-gateway';
|
|
69
|
+
export {
|
|
70
|
+
WebSocketGateway,
|
|
71
|
+
OnConnect,
|
|
72
|
+
OnDisconnect,
|
|
73
|
+
OnJoinRoom,
|
|
74
|
+
OnLeaveRoom,
|
|
75
|
+
OnMessage,
|
|
76
|
+
Client,
|
|
77
|
+
Socket,
|
|
78
|
+
MessageData,
|
|
79
|
+
RoomName,
|
|
80
|
+
PatternParams,
|
|
81
|
+
WsServer,
|
|
82
|
+
UseWsGuards,
|
|
83
|
+
getGatewayMetadata,
|
|
84
|
+
isWebSocketGateway,
|
|
85
|
+
} from './ws-decorators';
|
|
86
|
+
export {
|
|
87
|
+
WsAuthGuard,
|
|
88
|
+
WsPermissionGuard,
|
|
89
|
+
WsRoomGuard,
|
|
90
|
+
WsAnyPermissionGuard,
|
|
91
|
+
WsServiceGuard,
|
|
92
|
+
WsAllGuards,
|
|
93
|
+
WsAnyGuard,
|
|
94
|
+
WsExecutionContextImpl,
|
|
95
|
+
executeGuards,
|
|
96
|
+
createGuard,
|
|
97
|
+
} from './ws-guards';
|
|
98
|
+
export type {
|
|
99
|
+
WsClientData,
|
|
100
|
+
WsAuthData,
|
|
101
|
+
WsRoom,
|
|
102
|
+
WsHandlerType,
|
|
103
|
+
WsParamType,
|
|
104
|
+
GatewayMetadata,
|
|
105
|
+
WsHandlerMetadata,
|
|
106
|
+
WsParamMetadata,
|
|
107
|
+
WebSocketGatewayOptions,
|
|
108
|
+
WsStorageOptions,
|
|
109
|
+
WebSocketApplicationOptions,
|
|
110
|
+
WsMessage,
|
|
111
|
+
WsHandlerResponse,
|
|
112
|
+
PatternMatch,
|
|
113
|
+
WsExecutionContext,
|
|
114
|
+
WsGuard,
|
|
115
|
+
WsServer as WsServerType,
|
|
116
|
+
} from './ws.types';
|
|
117
|
+
export {
|
|
118
|
+
WsHandlerType as WsHandlerTypeEnum,
|
|
119
|
+
WsParamType as WsParamTypeEnum,
|
|
120
|
+
isWsMessage,
|
|
121
|
+
isWsHandlerResponse,
|
|
122
|
+
isWsClientData,
|
|
123
|
+
isWsRoom,
|
|
124
|
+
} from './ws.types';
|
|
125
|
+
export {
|
|
126
|
+
matchPattern,
|
|
127
|
+
isPatternMatch,
|
|
128
|
+
createPatternMatcher,
|
|
129
|
+
isPattern,
|
|
130
|
+
getPatternParams,
|
|
131
|
+
buildFromPattern,
|
|
132
|
+
} from './ws-pattern-matcher';
|
|
133
|
+
export type { WsStorageAdapter, WsPubSubStorageAdapter, WsStorageEventPayload } from './ws-storage';
|
|
134
|
+
export { WsStorageEvent, isPubSubAdapter } from './ws-storage';
|
|
135
|
+
export { InMemoryWsStorage, createInMemoryWsStorage } from './ws-storage-memory';
|
|
136
|
+
export { RedisWsStorage, createRedisWsStorage } from './ws-storage-redis';
|
|
137
|
+
|
|
138
|
+
// Redis Client (shared)
|
|
139
|
+
export { RedisClient, createRedisClient } from './redis-client';
|
|
140
|
+
export type { RedisClientOptions } from './redis-client';
|
|
141
|
+
export {
|
|
142
|
+
SharedRedisProvider, SharedRedisService, makeSharedRedisLayer, getSharedRedis,
|
|
143
|
+
} from './shared-redis';
|
|
144
|
+
export type { SharedRedisOptions } from './shared-redis';
|
|
145
|
+
|
|
146
|
+
// WebSocket Service Definition and Client
|
|
147
|
+
export {
|
|
148
|
+
createWsServiceDefinition, getWsGatewayNames, getWsEventNames, getWsEndpoint,
|
|
149
|
+
} from './ws-service-definition';
|
|
150
|
+
export type { WsServiceDefinition, WsGatewayDefinition, WsEndpointMetadata } from './ws-service-definition';
|
|
151
|
+
export { createWsClient } from './ws-client';
|
|
152
|
+
export type {
|
|
153
|
+
WsClientOptions,
|
|
154
|
+
WsClient,
|
|
155
|
+
WsGatewayClient,
|
|
156
|
+
WsEventListener,
|
|
157
|
+
WsClientEvent,
|
|
158
|
+
WsClientEventListeners,
|
|
159
|
+
TypedWsClient,
|
|
160
|
+
} from './ws-client.types';
|
|
161
|
+
export { WsConnectionState } from './ws-client.types';
|
package/src/module.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
registerControllerDependencies,
|
|
24
24
|
} from './decorators';
|
|
25
25
|
import { getServiceMetadata, getServiceTag } from './service';
|
|
26
|
+
import { isWebSocketGateway } from './ws-decorators';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* OneBun Module implementation
|
|
@@ -331,14 +332,19 @@ export class OneBunModule implements Module {
|
|
|
331
332
|
const controllerConstructor = controllerClass as new (...args: unknown[]) => Controller;
|
|
332
333
|
const controller = new controllerConstructor(...dependencies);
|
|
333
334
|
|
|
334
|
-
// Initialize controller with logger and config
|
|
335
|
-
controller.initializeController
|
|
335
|
+
// Initialize controller with logger and config (skip for WebSocket gateways)
|
|
336
|
+
if (!isWebSocketGateway(controllerClass) && typeof controller.initializeController === 'function') {
|
|
337
|
+
controller.initializeController(this.logger, this.config);
|
|
338
|
+
}
|
|
336
339
|
|
|
337
340
|
this.controllerInstances.set(controllerClass, controller);
|
|
338
341
|
|
|
339
342
|
// Inject all services into controller (for legacy compatibility)
|
|
340
|
-
for
|
|
341
|
-
|
|
343
|
+
// Skip for WebSocket gateways which don't have setService
|
|
344
|
+
if (!isWebSocketGateway(controllerClass) && typeof controller.setService === 'function') {
|
|
345
|
+
for (const [tag, serviceInstance] of this.serviceInstances.entries()) {
|
|
346
|
+
controller.setService(tag, serviceInstance);
|
|
347
|
+
}
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
if (paramTypes && paramTypes.length > 0) {
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Unified Redis client for use across OneBun packages (cache, websocket, etc.)
|
|
5
|
+
* Uses Bun's built-in RedisClient (Bun v1.2.9+).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Type for Bun's RedisClient - will be available at runtime
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
type BunRedisClient = any;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for Redis client
|
|
14
|
+
*/
|
|
15
|
+
export interface RedisClientOptions {
|
|
16
|
+
/** Redis connection URL (redis://host:port or rediss://host:port for TLS) */
|
|
17
|
+
url: string;
|
|
18
|
+
/** Key prefix for all operations */
|
|
19
|
+
keyPrefix?: string;
|
|
20
|
+
/** Enable automatic reconnection */
|
|
21
|
+
reconnect?: boolean;
|
|
22
|
+
/** Enable TLS */
|
|
23
|
+
tls?: boolean;
|
|
24
|
+
/** Connection timeout in milliseconds */
|
|
25
|
+
connectTimeout?: number;
|
|
26
|
+
/** Command timeout in milliseconds */
|
|
27
|
+
commandTimeout?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Subscription handler type
|
|
32
|
+
*/
|
|
33
|
+
type SubscriptionHandler = (message: string, channel: string) => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Redis client wrapper with unified API
|
|
37
|
+
*/
|
|
38
|
+
export class RedisClient {
|
|
39
|
+
private client: BunRedisClient | null = null;
|
|
40
|
+
private subscriptions = new Map<string, SubscriptionHandler[]>();
|
|
41
|
+
private subscriberClient: BunRedisClient | null = null;
|
|
42
|
+
private readonly options: RedisClientOptions;
|
|
43
|
+
private connected = false;
|
|
44
|
+
|
|
45
|
+
constructor(options: RedisClientOptions) {
|
|
46
|
+
this.options = {
|
|
47
|
+
keyPrefix: '',
|
|
48
|
+
reconnect: true,
|
|
49
|
+
...options,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Connect to Redis
|
|
55
|
+
*/
|
|
56
|
+
async connect(): Promise<void> {
|
|
57
|
+
if (this.connected && this.client) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Get Bun's RedisClient constructor
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
|
|
64
|
+
const BunGlobal = (globalThis as any).Bun;
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
|
|
66
|
+
const BunDirect = typeof Bun !== 'undefined' ? (Bun as any) : null;
|
|
67
|
+
|
|
68
|
+
// Try different access methods for RedisClient constructor
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
70
|
+
const BunRedisClient = BunGlobal?.RedisClient
|
|
71
|
+
|| BunDirect?.RedisClient
|
|
72
|
+
|| BunGlobal?.Redis;
|
|
73
|
+
|
|
74
|
+
if (!BunRedisClient) {
|
|
75
|
+
throw new Error('Bun.RedisClient is not available. Make sure you are using Bun runtime v1.2.9+');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create client with URL and options
|
|
79
|
+
this.client = new BunRedisClient(this.options.url, {
|
|
80
|
+
autoReconnect: this.options.reconnect ?? true,
|
|
81
|
+
connectionTimeout: this.options.connectTimeout,
|
|
82
|
+
enableAutoPipelining: true,
|
|
83
|
+
tls: this.options.tls,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Connect to Redis server
|
|
87
|
+
if (this.client.connect) {
|
|
88
|
+
await this.client.connect();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.connected = true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.connected = false;
|
|
94
|
+
throw new Error(`Failed to connect to Redis: ${error}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Disconnect from Redis
|
|
100
|
+
*/
|
|
101
|
+
async disconnect(): Promise<void> {
|
|
102
|
+
if (this.subscriberClient) {
|
|
103
|
+
// Unsubscribe from all channels
|
|
104
|
+
for (const channel of this.subscriptions.keys()) {
|
|
105
|
+
try {
|
|
106
|
+
await this.subscriberClient.unsubscribe(channel);
|
|
107
|
+
} catch {
|
|
108
|
+
// Ignore errors during cleanup
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.subscriptions.clear();
|
|
112
|
+
this.subscriberClient = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.client) {
|
|
116
|
+
try {
|
|
117
|
+
await this.client.quit();
|
|
118
|
+
} catch {
|
|
119
|
+
// Ignore errors during cleanup
|
|
120
|
+
}
|
|
121
|
+
this.client = null;
|
|
122
|
+
this.connected = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if connected
|
|
128
|
+
*/
|
|
129
|
+
isConnected(): boolean {
|
|
130
|
+
return this.connected && this.client !== null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get prefixed key
|
|
135
|
+
*/
|
|
136
|
+
private prefixKey(key: string): string {
|
|
137
|
+
return this.options.keyPrefix ? `${this.options.keyPrefix}${key}` : key;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Ensure client is connected
|
|
142
|
+
*/
|
|
143
|
+
private ensureConnected(): BunRedisClient {
|
|
144
|
+
if (!this.client || !this.connected) {
|
|
145
|
+
throw new Error('Redis client not connected. Call connect() first.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return this.client;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Basic Operations
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get a value by key
|
|
157
|
+
*/
|
|
158
|
+
async get(key: string): Promise<string | null> {
|
|
159
|
+
const client = this.ensureConnected();
|
|
160
|
+
const result = await client.get(this.prefixKey(key));
|
|
161
|
+
|
|
162
|
+
return result ?? null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set a value with optional TTL
|
|
167
|
+
*/
|
|
168
|
+
async set(key: string, value: string, ttlMs?: number): Promise<void> {
|
|
169
|
+
const client = this.ensureConnected();
|
|
170
|
+
const prefixedKey = this.prefixKey(key);
|
|
171
|
+
|
|
172
|
+
if (ttlMs !== undefined && ttlMs > 0) {
|
|
173
|
+
// Use raw SET command with PX option for atomic set with TTL
|
|
174
|
+
await client.send('SET', [prefixedKey, value, 'PX', String(ttlMs)]);
|
|
175
|
+
} else {
|
|
176
|
+
await client.set(prefixedKey, value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Delete a key
|
|
182
|
+
*/
|
|
183
|
+
async del(key: string): Promise<void> {
|
|
184
|
+
const client = this.ensureConnected();
|
|
185
|
+
await client.del(this.prefixKey(key));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if key exists
|
|
190
|
+
*/
|
|
191
|
+
async exists(key: string): Promise<boolean> {
|
|
192
|
+
const client = this.ensureConnected();
|
|
193
|
+
const result = await client.exists(this.prefixKey(key));
|
|
194
|
+
|
|
195
|
+
return result > 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get keys matching pattern
|
|
200
|
+
*/
|
|
201
|
+
async keys(pattern: string): Promise<string[]> {
|
|
202
|
+
const client = this.ensureConnected();
|
|
203
|
+
const prefixedPattern = this.prefixKey(pattern);
|
|
204
|
+
const result = await client.keys(prefixedPattern);
|
|
205
|
+
|
|
206
|
+
// Remove prefix from results
|
|
207
|
+
const prefix = this.options.keyPrefix || '';
|
|
208
|
+
|
|
209
|
+
return result.map((k: string) => k.startsWith(prefix) ? k.substring(prefix.length) : k);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Set TTL on existing key
|
|
214
|
+
*/
|
|
215
|
+
async expire(key: string, ttlMs: number): Promise<boolean> {
|
|
216
|
+
const client = this.ensureConnected();
|
|
217
|
+
const result = await client.pexpire(this.prefixKey(key), ttlMs);
|
|
218
|
+
|
|
219
|
+
return result === 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get TTL of a key in milliseconds
|
|
224
|
+
*/
|
|
225
|
+
async ttl(key: string): Promise<number> {
|
|
226
|
+
const client = this.ensureConnected();
|
|
227
|
+
const result = await client.pttl(this.prefixKey(key));
|
|
228
|
+
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Batch Operations
|
|
234
|
+
// ============================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get multiple values
|
|
238
|
+
*/
|
|
239
|
+
async mget(keys: string[]): Promise<(string | null)[]> {
|
|
240
|
+
if (keys.length === 0) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const client = this.ensureConnected();
|
|
245
|
+
const prefixedKeys = keys.map((k) => this.prefixKey(k));
|
|
246
|
+
const results = await client.mget(...prefixedKeys);
|
|
247
|
+
|
|
248
|
+
return results.map((r: string | null) => r ?? null);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Set multiple values with optional TTL
|
|
253
|
+
*/
|
|
254
|
+
async mset(entries: Array<{ key: string; value: string; ttlMs?: number }>): Promise<void> {
|
|
255
|
+
if (entries.length === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const client = this.ensureConnected();
|
|
260
|
+
|
|
261
|
+
// Group entries by TTL
|
|
262
|
+
const noTtl: [string, string][] = [];
|
|
263
|
+
const withTtl: Array<{ key: string; value: string; ttlMs: number }> = [];
|
|
264
|
+
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
if (entry.ttlMs !== undefined && entry.ttlMs > 0) {
|
|
267
|
+
withTtl.push({ key: this.prefixKey(entry.key), value: entry.value, ttlMs: entry.ttlMs });
|
|
268
|
+
} else {
|
|
269
|
+
noTtl.push([this.prefixKey(entry.key), entry.value]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Set entries without TTL using MSET
|
|
274
|
+
if (noTtl.length > 0) {
|
|
275
|
+
const flat = noTtl.flat();
|
|
276
|
+
await client.mset(...flat);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Set entries with TTL individually using raw SET command
|
|
280
|
+
for (const entry of withTtl) {
|
|
281
|
+
await client.send('SET', [entry.key, entry.value, 'PX', String(entry.ttlMs)]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// Hash Operations
|
|
287
|
+
// ============================================================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Set a hash field
|
|
291
|
+
*/
|
|
292
|
+
async hset(key: string, field: string, value: string): Promise<void> {
|
|
293
|
+
const client = this.ensureConnected();
|
|
294
|
+
await client.hset(this.prefixKey(key), field, value);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get a hash field
|
|
299
|
+
*/
|
|
300
|
+
async hget(key: string, field: string): Promise<string | null> {
|
|
301
|
+
const client = this.ensureConnected();
|
|
302
|
+
const result = await client.hget(this.prefixKey(key), field);
|
|
303
|
+
|
|
304
|
+
return result ?? null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get all hash fields
|
|
309
|
+
*/
|
|
310
|
+
async hgetall(key: string): Promise<Record<string, string>> {
|
|
311
|
+
const client = this.ensureConnected();
|
|
312
|
+
const result = await client.hgetall(this.prefixKey(key));
|
|
313
|
+
|
|
314
|
+
return result || {};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Delete a hash field
|
|
319
|
+
*/
|
|
320
|
+
async hdel(key: string, field: string): Promise<void> {
|
|
321
|
+
const client = this.ensureConnected();
|
|
322
|
+
await client.hdel(this.prefixKey(key), field);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Set multiple hash fields
|
|
327
|
+
*/
|
|
328
|
+
async hmset(key: string, data: Record<string, string>): Promise<void> {
|
|
329
|
+
const client = this.ensureConnected();
|
|
330
|
+
const entries = Object.entries(data).flat();
|
|
331
|
+
if (entries.length > 0) {
|
|
332
|
+
await client.hset(this.prefixKey(key), ...entries);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Set Operations
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Add members to a set
|
|
342
|
+
*/
|
|
343
|
+
async sadd(key: string, ...members: string[]): Promise<void> {
|
|
344
|
+
if (members.length === 0) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const client = this.ensureConnected();
|
|
349
|
+
await client.sadd(this.prefixKey(key), ...members);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Remove members from a set
|
|
354
|
+
*/
|
|
355
|
+
async srem(key: string, ...members: string[]): Promise<void> {
|
|
356
|
+
if (members.length === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const client = this.ensureConnected();
|
|
361
|
+
await client.srem(this.prefixKey(key), ...members);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get all members of a set
|
|
366
|
+
*/
|
|
367
|
+
async smembers(key: string): Promise<string[]> {
|
|
368
|
+
const client = this.ensureConnected();
|
|
369
|
+
const result = await client.smembers(this.prefixKey(key));
|
|
370
|
+
|
|
371
|
+
return result || [];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if member exists in set
|
|
376
|
+
*/
|
|
377
|
+
async sismember(key: string, member: string): Promise<boolean> {
|
|
378
|
+
const client = this.ensureConnected();
|
|
379
|
+
const result = await client.sismember(this.prefixKey(key), member);
|
|
380
|
+
|
|
381
|
+
return result === 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get set size
|
|
386
|
+
*/
|
|
387
|
+
async scard(key: string): Promise<number> {
|
|
388
|
+
const client = this.ensureConnected();
|
|
389
|
+
const result = await client.scard(this.prefixKey(key));
|
|
390
|
+
|
|
391
|
+
return result || 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// Pub/Sub Operations
|
|
396
|
+
// ============================================================================
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Publish a message to a channel
|
|
400
|
+
*/
|
|
401
|
+
async publish(channel: string, message: string): Promise<void> {
|
|
402
|
+
const client = this.ensureConnected();
|
|
403
|
+
await client.publish(this.prefixKey(channel), message);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Subscribe to a channel
|
|
408
|
+
*/
|
|
409
|
+
async subscribe(channel: string, handler: SubscriptionHandler): Promise<void> {
|
|
410
|
+
const prefixedChannel = this.prefixKey(channel);
|
|
411
|
+
|
|
412
|
+
// Track handler
|
|
413
|
+
const handlers = this.subscriptions.get(prefixedChannel) || [];
|
|
414
|
+
handlers.push(handler);
|
|
415
|
+
this.subscriptions.set(prefixedChannel, handlers);
|
|
416
|
+
|
|
417
|
+
// Create subscriber client if needed
|
|
418
|
+
if (!this.subscriberClient) {
|
|
419
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
|
|
420
|
+
const BunGlobal = (globalThis as any).Bun;
|
|
421
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
|
|
422
|
+
const BunDirect = typeof Bun !== 'undefined' ? (Bun as any) : null;
|
|
423
|
+
|
|
424
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
425
|
+
const BunRedisClient = BunGlobal?.RedisClient
|
|
426
|
+
|| BunDirect?.RedisClient
|
|
427
|
+
|| BunGlobal?.Redis;
|
|
428
|
+
|
|
429
|
+
if (!BunRedisClient) {
|
|
430
|
+
throw new Error('Bun.RedisClient is not available');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.subscriberClient = new BunRedisClient(this.options.url, {
|
|
434
|
+
autoReconnect: this.options.reconnect ?? true,
|
|
435
|
+
connectionTimeout: this.options.connectTimeout,
|
|
436
|
+
tls: this.options.tls,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Connect subscriber client
|
|
440
|
+
if (this.subscriberClient.connect) {
|
|
441
|
+
await this.subscriberClient.connect();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Subscribe using Bun's subscribe method
|
|
446
|
+
await this.subscriberClient.subscribe(prefixedChannel, (message: string) => {
|
|
447
|
+
const channelHandlers = this.subscriptions.get(prefixedChannel);
|
|
448
|
+
if (channelHandlers) {
|
|
449
|
+
for (const h of channelHandlers) {
|
|
450
|
+
try {
|
|
451
|
+
h(message, channel);
|
|
452
|
+
} catch {
|
|
453
|
+
// Ignore handler errors
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Unsubscribe from a channel
|
|
462
|
+
*/
|
|
463
|
+
async unsubscribe(channel: string): Promise<void> {
|
|
464
|
+
const prefixedChannel = this.prefixKey(channel);
|
|
465
|
+
this.subscriptions.delete(prefixedChannel);
|
|
466
|
+
|
|
467
|
+
if (this.subscriberClient) {
|
|
468
|
+
try {
|
|
469
|
+
await this.subscriberClient.unsubscribe(prefixedChannel);
|
|
470
|
+
} catch {
|
|
471
|
+
// Ignore errors
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ============================================================================
|
|
477
|
+
// Raw Client Access
|
|
478
|
+
// ============================================================================
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get the underlying Bun Redis client
|
|
482
|
+
*/
|
|
483
|
+
getClient(): BunRedisClient | null {
|
|
484
|
+
return this.client;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Execute a raw command
|
|
489
|
+
*/
|
|
490
|
+
async raw<T = unknown>(command: string, ...args: string[]): Promise<T> {
|
|
491
|
+
const client = this.ensureConnected();
|
|
492
|
+
|
|
493
|
+
return await client[command](...args);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create a new Redis client
|
|
499
|
+
*/
|
|
500
|
+
export function createRedisClient(options: RedisClientOptions): RedisClient {
|
|
501
|
+
return new RedisClient(options);
|
|
502
|
+
}
|