alepha 0.14.0 → 0.14.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.
Files changed (149) hide show
  1. package/README.md +3 -3
  2. package/dist/api/audits/index.d.ts +80 -1
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +80 -1
  6. package/dist/api/files/index.d.ts.map +1 -1
  7. package/dist/api/files/index.js.map +1 -1
  8. package/dist/api/jobs/index.d.ts +236 -157
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js.map +1 -1
  11. package/dist/api/notifications/index.d.ts +21 -1
  12. package/dist/api/notifications/index.d.ts.map +1 -1
  13. package/dist/api/parameters/index.d.ts +451 -4
  14. package/dist/api/parameters/index.d.ts.map +1 -1
  15. package/dist/api/parameters/index.js.map +1 -1
  16. package/dist/api/users/index.d.ts +252 -249
  17. package/dist/api/users/index.d.ts.map +1 -1
  18. package/dist/api/users/index.js +4 -0
  19. package/dist/api/users/index.js.map +1 -1
  20. package/dist/api/verifications/index.d.ts +128 -128
  21. package/dist/api/verifications/index.d.ts.map +1 -1
  22. package/dist/batch/index.js.map +1 -1
  23. package/dist/cache/core/index.js.map +1 -1
  24. package/dist/cli/index.d.ts +304 -115
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/index.js +650 -531
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/command/index.d.ts +210 -13
  29. package/dist/command/index.d.ts.map +1 -1
  30. package/dist/command/index.js +306 -69
  31. package/dist/command/index.js.map +1 -1
  32. package/dist/core/index.browser.js.map +1 -1
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/core/index.d.ts.map +1 -1
  35. package/dist/core/index.js +7 -6
  36. package/dist/core/index.js.map +1 -1
  37. package/dist/core/index.native.js +7 -6
  38. package/dist/core/index.native.js.map +1 -1
  39. package/dist/datetime/index.js.map +1 -1
  40. package/dist/fake/index.js.map +1 -1
  41. package/dist/file/index.d.ts.map +1 -1
  42. package/dist/file/index.js.map +1 -1
  43. package/dist/lock/redis/index.js.map +1 -1
  44. package/dist/logger/index.js.map +1 -1
  45. package/dist/mcp/index.js.map +1 -1
  46. package/dist/orm/index.browser.js +26 -5
  47. package/dist/orm/index.browser.js.map +1 -1
  48. package/dist/orm/index.d.ts +294 -215
  49. package/dist/orm/index.d.ts.map +1 -1
  50. package/dist/orm/index.js +522 -523
  51. package/dist/orm/index.js.map +1 -1
  52. package/dist/queue/redis/index.js +2 -4
  53. package/dist/queue/redis/index.js.map +1 -1
  54. package/dist/redis/index.d.ts +400 -29
  55. package/dist/redis/index.d.ts.map +1 -1
  56. package/dist/redis/index.js +412 -21
  57. package/dist/redis/index.js.map +1 -1
  58. package/dist/retry/index.js.map +1 -1
  59. package/dist/router/index.js.map +1 -1
  60. package/dist/scheduler/index.js.map +1 -1
  61. package/dist/security/index.d.ts.map +1 -1
  62. package/dist/security/index.js.map +1 -1
  63. package/dist/server/auth/index.d.ts +155 -155
  64. package/dist/server/auth/index.js.map +1 -1
  65. package/dist/server/cache/index.js.map +1 -1
  66. package/dist/server/cookies/index.browser.js.map +1 -1
  67. package/dist/server/cookies/index.js.map +1 -1
  68. package/dist/server/core/index.browser.js.map +1 -1
  69. package/dist/server/core/index.d.ts +0 -1
  70. package/dist/server/core/index.d.ts.map +1 -1
  71. package/dist/server/core/index.js.map +1 -1
  72. package/dist/server/helmet/index.d.ts +4 -1
  73. package/dist/server/helmet/index.d.ts.map +1 -1
  74. package/dist/server/helmet/index.js.map +1 -1
  75. package/dist/server/links/index.browser.js.map +1 -1
  76. package/dist/server/links/index.js.map +1 -1
  77. package/dist/server/multipart/index.d.ts.map +1 -1
  78. package/dist/server/multipart/index.js.map +1 -1
  79. package/dist/server/proxy/index.js.map +1 -1
  80. package/dist/server/rate-limit/index.js.map +1 -1
  81. package/dist/server/security/index.d.ts +9 -9
  82. package/dist/server/security/index.js.map +1 -1
  83. package/dist/server/swagger/index.js.map +1 -1
  84. package/dist/thread/index.js.map +1 -1
  85. package/dist/topic/core/index.js.map +1 -1
  86. package/dist/topic/redis/index.js +3 -3
  87. package/dist/topic/redis/index.js.map +1 -1
  88. package/dist/vite/index.js +9 -6
  89. package/dist/vite/index.js.map +1 -1
  90. package/dist/websocket/index.browser.js.map +1 -1
  91. package/dist/websocket/index.d.ts +7 -7
  92. package/dist/websocket/index.js.map +1 -1
  93. package/package.json +3 -3
  94. package/src/api/users/index.ts +4 -0
  95. package/src/cli/apps/AlephaCli.ts +36 -14
  96. package/src/cli/apps/AlephaPackageBuilderCli.ts +5 -1
  97. package/src/cli/assets/appRouterTs.ts +1 -1
  98. package/src/cli/atoms/changelogOptions.ts +45 -0
  99. package/src/cli/commands/{ViteCommands.ts → build.ts} +4 -93
  100. package/src/cli/commands/changelog.ts +244 -0
  101. package/src/cli/commands/clean.ts +14 -0
  102. package/src/cli/commands/{DrizzleCommands.ts → db.ts} +37 -124
  103. package/src/cli/commands/deploy.ts +118 -0
  104. package/src/cli/commands/dev.ts +57 -0
  105. package/src/cli/commands/format.ts +17 -0
  106. package/src/cli/commands/{CoreCommands.ts → init.ts} +2 -40
  107. package/src/cli/commands/lint.ts +17 -0
  108. package/src/cli/commands/root.ts +32 -0
  109. package/src/cli/commands/run.ts +24 -0
  110. package/src/cli/commands/test.ts +42 -0
  111. package/src/cli/commands/typecheck.ts +19 -0
  112. package/src/cli/commands/{VerifyCommands.ts → verify.ts} +1 -13
  113. package/src/cli/defineConfig.ts +24 -0
  114. package/src/cli/index.ts +17 -5
  115. package/src/cli/services/AlephaCliUtils.ts +4 -21
  116. package/src/cli/services/GitMessageParser.ts +77 -0
  117. package/src/command/helpers/EnvUtils.ts +37 -0
  118. package/src/command/index.ts +3 -1
  119. package/src/command/primitives/$command.ts +172 -6
  120. package/src/command/providers/CliProvider.ts +424 -91
  121. package/src/core/Alepha.ts +8 -5
  122. package/src/file/providers/NodeFileSystemProvider.ts +3 -1
  123. package/src/orm/index.browser.ts +1 -1
  124. package/src/orm/index.ts +18 -10
  125. package/src/orm/interfaces/PgQueryWhere.ts +1 -26
  126. package/src/orm/providers/{PostgresTypeProvider.ts → DatabaseTypeProvider.ts} +25 -3
  127. package/src/orm/providers/drivers/BunPostgresProvider.ts +225 -0
  128. package/src/orm/providers/drivers/BunSqliteProvider.ts +180 -0
  129. package/src/orm/providers/drivers/DatabaseProvider.ts +25 -0
  130. package/src/orm/providers/drivers/NodePostgresProvider.ts +0 -25
  131. package/src/orm/services/QueryManager.ts +10 -125
  132. package/src/queue/redis/providers/RedisQueueProvider.ts +2 -7
  133. package/src/redis/index.ts +65 -3
  134. package/src/redis/providers/BunRedisProvider.ts +304 -0
  135. package/src/redis/providers/BunRedisSubscriberProvider.ts +94 -0
  136. package/src/redis/providers/NodeRedisProvider.ts +280 -0
  137. package/src/redis/providers/NodeRedisSubscriberProvider.ts +94 -0
  138. package/src/redis/providers/RedisProvider.ts +134 -140
  139. package/src/redis/providers/RedisSubscriberProvider.ts +58 -49
  140. package/src/server/core/providers/BunHttpServerProvider.ts +0 -3
  141. package/src/server/core/providers/ServerBodyParserProvider.ts +3 -1
  142. package/src/server/core/providers/ServerProvider.ts +7 -4
  143. package/src/server/multipart/providers/ServerMultipartProvider.ts +3 -1
  144. package/src/server/proxy/providers/ServerProxyProvider.ts +1 -1
  145. package/src/topic/redis/providers/RedisTopicProvider.ts +3 -3
  146. package/src/vite/tasks/buildServer.ts +1 -0
  147. package/src/cli/commands/BiomeCommands.ts +0 -29
  148. package/src/cli/commands/ChangelogCommands.ts +0 -389
  149. package/src/orm/services/PgJsonQueryManager.ts +0 -511
@@ -38,10 +38,8 @@ import type {
38
38
  PgQueryWhere,
39
39
  PgQueryWhereOrSQL,
40
40
  } from "../interfaces/PgQueryWhere.ts";
41
- import { PgJsonQueryManager } from "./PgJsonQueryManager.ts";
42
41
 
43
42
  export class QueryManager {
44
- protected readonly jsonQueryManager = $inject(PgJsonQueryManager);
45
43
  protected readonly alepha = $inject(Alepha);
46
44
 
47
45
  /**
@@ -157,40 +155,16 @@ export class QueryManager {
157
155
  }
158
156
 
159
157
  if (operator) {
160
- // Check if this is a JSONB column with nested query
161
- // BUT skip primitive arrays - they should use native Drizzle operators
162
- if (
163
- this.jsonQueryManager.isJsonbColumn(schema, key) &&
164
- !this.jsonQueryManager.isPrimitiveArray(schema, key) &&
165
- typeof operator === "object" &&
166
- !Array.isArray(operator) &&
167
- this.jsonQueryManager.hasNestedQuery({ [key]: operator })
168
- ) {
169
- // Handle JSONB nested queries for objects and arrays of objects
170
- const column = col(key);
171
- const jsonbSql = this.buildJsonbQuery(
172
- column,
173
- operator,
174
- schema,
175
- key,
176
- options.dialect,
177
- );
178
- if (jsonbSql) {
179
- conditions.push(jsonbSql);
180
- }
181
- } else {
182
- // Regular column query (including primitive arrays)
183
- const column = col(key);
184
- const sql = this.mapOperatorToSql(
185
- operator,
186
- column,
187
- schema,
188
- key,
189
- options.dialect,
190
- );
191
- if (sql) {
192
- conditions.push(sql);
193
- }
158
+ const column = col(key);
159
+ const sql = this.mapOperatorToSql(
160
+ operator,
161
+ column,
162
+ schema,
163
+ key,
164
+ options.dialect,
165
+ );
166
+ if (sql) {
167
+ conditions.push(sql);
194
168
  }
195
169
  }
196
170
  }
@@ -203,95 +177,6 @@ export class QueryManager {
203
177
  return and(...conditions);
204
178
  }
205
179
 
206
- /**
207
- * Build a JSONB query for nested object/array queries.
208
- */
209
- protected buildJsonbQuery(
210
- column: PgColumn,
211
- nestedQuery: any,
212
- schema: TObject,
213
- columnName: string,
214
- dialect: "postgresql" | "sqlite",
215
- ): SQL | undefined {
216
- // Parse the nested query to extract paths and operators
217
- const queries = this.jsonQueryManager.parseNestedQuery(nestedQuery);
218
-
219
- if (queries.length === 0) {
220
- return undefined;
221
- }
222
-
223
- // Get the column schema for type inference
224
- const columnSchema = schema.properties[columnName];
225
-
226
- // Build conditions for each parsed query
227
- const conditions: SQL[] = [];
228
-
229
- for (const { path, operator } of queries) {
230
- // Check if the operator is an array operator (arrayContains, arrayContained, arrayOverlaps)
231
- const isArrayOperator =
232
- operator.arrayContains !== undefined ||
233
- operator.arrayContained !== undefined ||
234
- operator.arrayOverlaps !== undefined;
235
-
236
- // Check if this is an array property
237
- const isArrayProp = this.jsonQueryManager.isArrayProperty(schema, [
238
- columnName,
239
- ...path,
240
- ]);
241
-
242
- if (isArrayProp && isArrayOperator) {
243
- // Array operators on JSONB arrays should use buildJsonbCondition
244
- // This handles cases like: { metadata: { permissions: { arrayContains: [...] } } }
245
- const condition = this.jsonQueryManager.buildJsonbCondition(
246
- column,
247
- path,
248
- operator,
249
- dialect,
250
- columnSchema,
251
- );
252
- if (condition) {
253
- conditions.push(condition);
254
- }
255
- } else if (isArrayProp && !isArrayOperator) {
256
- // Non-array operators on array properties use buildJsonbArrayCondition
257
- // This handles cases like: { addresses: { city: { eq: "Wonderland" } } }
258
- const condition = this.jsonQueryManager.buildJsonbArrayCondition(
259
- column,
260
- path,
261
- "",
262
- operator,
263
- dialect,
264
- );
265
- if (condition) {
266
- conditions.push(condition);
267
- }
268
- } else {
269
- // Handle object queries
270
- const condition = this.jsonQueryManager.buildJsonbCondition(
271
- column,
272
- path,
273
- operator,
274
- dialect,
275
- columnSchema,
276
- );
277
- if (condition) {
278
- conditions.push(condition);
279
- }
280
- }
281
- }
282
-
283
- if (conditions.length === 0) {
284
- return undefined;
285
- }
286
-
287
- if (conditions.length === 1) {
288
- return conditions[0];
289
- }
290
-
291
- // Multiple conditions - AND them together
292
- return and(...conditions);
293
- }
294
-
295
180
  /**
296
181
  * Check if an object has any filter operator properties.
297
182
  */
@@ -17,15 +17,10 @@ export class RedisQueueProvider implements QueueProvider {
17
17
  }
18
18
 
19
19
  public async push(queue: string, message: string): Promise<void> {
20
- await this.redisProvider.publisher.LPUSH(this.prefix(queue), message);
20
+ await this.redisProvider.lpush(this.prefix(queue), message);
21
21
  }
22
22
 
23
23
  public async pop(queue: string): Promise<string | undefined> {
24
- const value = await this.redisProvider.publisher.RPOP(this.prefix(queue));
25
- if (value == null) {
26
- return undefined;
27
- }
28
-
29
- return String(value);
24
+ return this.redisProvider.rpop(this.prefix(queue));
30
25
  }
31
26
  }
@@ -1,9 +1,17 @@
1
1
  import { $module, type Alepha } from "alepha";
2
+ import { BunRedisProvider } from "./providers/BunRedisProvider.ts";
3
+ import { BunRedisSubscriberProvider } from "./providers/BunRedisSubscriberProvider.ts";
4
+ import { NodeRedisProvider } from "./providers/NodeRedisProvider.ts";
5
+ import { NodeRedisSubscriberProvider } from "./providers/NodeRedisSubscriberProvider.ts";
2
6
  import { RedisProvider } from "./providers/RedisProvider.ts";
3
7
  import { RedisSubscriberProvider } from "./providers/RedisSubscriberProvider.ts";
4
8
 
5
9
  // ---------------------------------------------------------------------------------------------------------------------
6
10
 
11
+ export * from "./providers/BunRedisProvider.ts";
12
+ export * from "./providers/BunRedisSubscriberProvider.ts";
13
+ export * from "./providers/NodeRedisProvider.ts";
14
+ export * from "./providers/NodeRedisSubscriberProvider.ts";
7
15
  export * from "./providers/RedisProvider.ts";
8
16
  export * from "./providers/RedisSubscriberProvider.ts";
9
17
 
@@ -12,11 +20,65 @@ export * from "./providers/RedisSubscriberProvider.ts";
12
20
  /**
13
21
  * Redis client provider for Alepha applications.
14
22
  *
15
- * @see {@link RedisProvider}
23
+ * Automatically selects the appropriate provider based on runtime:
24
+ * - Bun: Uses `BunRedisProvider` with Bun's native Redis client (7.9x faster than ioredis)
25
+ * - Node.js: Uses `NodeRedisProvider` with `@redis/client`
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // Inject the abstract provider - runtime selects the implementation
30
+ * const redis = alepha.inject(RedisProvider);
31
+ *
32
+ * // Use common operations
33
+ * await redis.set("key", "value");
34
+ * const value = await redis.get("key");
35
+ *
36
+ * // For pub/sub
37
+ * const subscriber = alepha.inject(RedisSubscriberProvider);
38
+ * await subscriber.subscribe("channel", (message, channel) => {
39
+ * console.log(`Received: ${message} on ${channel}`);
40
+ * });
41
+ * ```
42
+ *
43
+ * @see {@link RedisProvider} - Abstract base class
44
+ * @see {@link NodeRedisProvider} - Node.js implementation
45
+ * @see {@link BunRedisProvider} - Bun implementation
46
+ * @see {@link RedisSubscriberProvider} - Abstract subscriber base class
47
+ * @see {@link NodeRedisSubscriberProvider} - Node.js subscriber implementation
48
+ * @see {@link BunRedisSubscriberProvider} - Bun subscriber implementation
16
49
  * @module alepha.redis
17
50
  */
18
51
  export const AlephaRedis = $module({
19
52
  name: "alepha.redis",
20
- services: [RedisProvider, RedisSubscriberProvider],
21
- register: (alepha: Alepha) => alepha.with(RedisProvider),
53
+ services: [
54
+ NodeRedisProvider,
55
+ NodeRedisSubscriberProvider,
56
+ BunRedisProvider,
57
+ BunRedisSubscriberProvider,
58
+ RedisProvider,
59
+ RedisSubscriberProvider,
60
+ ],
61
+ register: (alepha: Alepha) => {
62
+ if (alepha.isBun()) {
63
+ alepha
64
+ .with({
65
+ provide: RedisProvider,
66
+ use: BunRedisProvider,
67
+ })
68
+ .with({
69
+ provide: RedisSubscriberProvider,
70
+ use: BunRedisSubscriberProvider,
71
+ });
72
+ } else {
73
+ alepha
74
+ .with({
75
+ provide: RedisProvider,
76
+ use: NodeRedisProvider,
77
+ })
78
+ .with({
79
+ provide: RedisSubscriberProvider,
80
+ use: NodeRedisSubscriberProvider,
81
+ });
82
+ }
83
+ },
22
84
  });
@@ -0,0 +1,304 @@
1
+ import {
2
+ $env,
3
+ $hook,
4
+ $inject,
5
+ Alepha,
6
+ AlephaError,
7
+ type Static,
8
+ t,
9
+ } from "alepha";
10
+ import { $logger } from "alepha/logger";
11
+ import type { RedisClient as BunRedisClient } from "bun";
12
+ import { RedisProvider, type RedisSetOptions } from "./RedisProvider.ts";
13
+
14
+ const envSchema = t.object({
15
+ REDIS_URL: t.optional(t.text()),
16
+ REDIS_PORT: t.integer({
17
+ default: "6379",
18
+ }),
19
+ REDIS_HOST: t.text({
20
+ default: "localhost",
21
+ }),
22
+ REDIS_PASSWORD: t.optional(t.text()),
23
+ });
24
+
25
+ declare module "alepha" {
26
+ interface Env extends Partial<Static<typeof envSchema>> {}
27
+ }
28
+
29
+ /**
30
+ * Bun Redis client provider using Bun's native Redis client.
31
+ *
32
+ * This provider uses Bun's built-in `RedisClient` class for Redis connections,
33
+ * which provides excellent performance (7.9x faster than ioredis) on the Bun runtime.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * // Set REDIS_URL environment variable
38
+ * // REDIS_URL=redis://localhost:6379
39
+ *
40
+ * // Or configure via REDIS_HOST, REDIS_PORT, REDIS_PASSWORD
41
+ *
42
+ * // Or configure programmatically
43
+ * alepha.with({
44
+ * provide: RedisProvider,
45
+ * use: BunRedisProvider,
46
+ * });
47
+ * ```
48
+ */
49
+ export class BunRedisProvider extends RedisProvider {
50
+ protected readonly log = $logger();
51
+ protected readonly alepha = $inject(Alepha);
52
+ protected readonly env = $env(envSchema);
53
+ protected client?: BunRedisClient;
54
+
55
+ public get publisher(): BunRedisClient {
56
+ if (!this.client?.connected) {
57
+ throw new AlephaError("Redis client is not ready");
58
+ }
59
+
60
+ return this.client;
61
+ }
62
+
63
+ public override get isReady(): boolean {
64
+ return this.client?.connected ?? false;
65
+ }
66
+
67
+ protected readonly start = $hook({
68
+ on: "start",
69
+ handler: () => this.connect(),
70
+ });
71
+
72
+ protected readonly stop = $hook({
73
+ on: "stop",
74
+ handler: () => this.close(),
75
+ });
76
+
77
+ /**
78
+ * Connect to the Redis server.
79
+ */
80
+ public override async connect(): Promise<void> {
81
+ // Check if we're running in Bun
82
+ if (typeof Bun === "undefined") {
83
+ throw new AlephaError(
84
+ "BunRedisProvider requires the Bun runtime. Use NodeRedisProvider for Node.js.",
85
+ );
86
+ }
87
+
88
+ this.log.debug("Connecting...");
89
+
90
+ const { RedisClient } = await import("bun");
91
+
92
+ this.client = new RedisClient(this.getUrl(), {
93
+ autoReconnect: true,
94
+ enableAutoPipelining: true,
95
+ });
96
+
97
+ this.client.onconnect = () => {
98
+ this.log.trace("Redis connected");
99
+ };
100
+
101
+ this.client.onclose = (error) => {
102
+ if (this.alepha.isStarted() && error) {
103
+ this.log.error("Redis connection closed", error);
104
+ }
105
+ };
106
+
107
+ await this.client.connect();
108
+
109
+ this.log.info("Connection OK");
110
+ }
111
+
112
+ /**
113
+ * Close the connection to the Redis server.
114
+ */
115
+ public override async close(): Promise<void> {
116
+ if (this.client) {
117
+ this.log.debug("Closing connection...");
118
+ this.client.close();
119
+ this.client = undefined;
120
+ this.log.info("Connection closed");
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Create a duplicate connection for pub/sub or other isolated operations.
126
+ */
127
+ public async duplicate(): Promise<BunRedisClient> {
128
+ if (typeof Bun === "undefined") {
129
+ throw new AlephaError("BunRedisProvider requires the Bun runtime.");
130
+ }
131
+
132
+ const { RedisClient } = await import("bun");
133
+
134
+ const client = new RedisClient(this.getUrl(), {
135
+ autoReconnect: true,
136
+ enableAutoPipelining: true,
137
+ });
138
+
139
+ client.onclose = (error) => {
140
+ if (this.alepha.isStarted() && error) {
141
+ this.log.error("Redis duplicate connection closed", error);
142
+ }
143
+ };
144
+
145
+ await client.connect();
146
+
147
+ return client;
148
+ }
149
+
150
+ public override async get(key: string): Promise<Buffer | undefined> {
151
+ this.log.trace(`Getting key ${key}`);
152
+ const resp = await this.publisher.getBuffer(key);
153
+
154
+ if (resp === null) {
155
+ return undefined;
156
+ }
157
+
158
+ return Buffer.from(resp);
159
+ }
160
+
161
+ public override async set(
162
+ key: string,
163
+ value: Buffer | string,
164
+ options?: RedisSetOptions,
165
+ ): Promise<Buffer> {
166
+ const buf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf-8");
167
+
168
+ // Build SET command arguments
169
+ const args: string[] = [key, buf.toString("binary")];
170
+
171
+ // Handle expiration object format (from alepha/cache-redis, alepha/lock-redis)
172
+ if (options?.expiration) {
173
+ if (options.expiration.type === "KEEPTTL") {
174
+ args.push("KEEPTTL");
175
+ } else {
176
+ args.push(options.expiration.type, String(options.expiration.value));
177
+ }
178
+ }
179
+
180
+ // Handle direct expiration properties
181
+ if (options?.EX !== undefined) {
182
+ args.push("EX", String(options.EX));
183
+ }
184
+ if (options?.PX !== undefined) {
185
+ args.push("PX", String(options.PX));
186
+ }
187
+ if (options?.EXAT !== undefined) {
188
+ args.push("EXAT", String(options.EXAT));
189
+ }
190
+ if (options?.PXAT !== undefined) {
191
+ args.push("PXAT", String(options.PXAT));
192
+ }
193
+ if (options?.KEEPTTL) {
194
+ args.push("KEEPTTL");
195
+ }
196
+
197
+ // Handle condition object format
198
+ if (options?.condition === "NX") {
199
+ args.push("NX");
200
+ } else if (options?.condition === "XX") {
201
+ args.push("XX");
202
+ }
203
+
204
+ // Handle direct condition properties
205
+ if (options?.NX) {
206
+ args.push("NX");
207
+ }
208
+ if (options?.XX) {
209
+ args.push("XX");
210
+ }
211
+ if (options?.GET) {
212
+ args.push("GET");
213
+ }
214
+
215
+ if (args.length === 2) {
216
+ // Simple set without options
217
+ await this.publisher.set(key, buf);
218
+ } else {
219
+ // Set with options via raw command
220
+ await this.publisher.send("SET", args);
221
+ }
222
+
223
+ return buf;
224
+ }
225
+
226
+ public override async has(key: string): Promise<boolean> {
227
+ return this.publisher.exists(key);
228
+ }
229
+
230
+ public override async keys(pattern: string): Promise<string[]> {
231
+ const keys = await this.publisher.send("KEYS", [pattern]);
232
+ if (!Array.isArray(keys)) {
233
+ return [];
234
+ }
235
+ return keys.map((key) =>
236
+ key instanceof Uint8Array ? Buffer.from(key).toString() : String(key),
237
+ );
238
+ }
239
+
240
+ public override async del(keys: string[]): Promise<void> {
241
+ if (keys.length === 0) {
242
+ return;
243
+ }
244
+
245
+ await this.publisher.send("DEL", keys);
246
+ }
247
+
248
+ // ---------------------------------------------------------
249
+ // Queue operations
250
+ // ---------------------------------------------------------
251
+
252
+ public override async lpush(key: string, value: string): Promise<void> {
253
+ await this.publisher.send("LPUSH", [key, value]);
254
+ }
255
+
256
+ public override async rpop(key: string): Promise<string | undefined> {
257
+ const value = await this.publisher.send("RPOP", [key]);
258
+ if (value == null) {
259
+ return undefined;
260
+ }
261
+ if (value instanceof Uint8Array) {
262
+ return Buffer.from(value).toString();
263
+ }
264
+ return String(value);
265
+ }
266
+
267
+ // ---------------------------------------------------------
268
+ // Pub/Sub operations
269
+ // ---------------------------------------------------------
270
+
271
+ public override async publish(
272
+ channel: string,
273
+ message: string,
274
+ ): Promise<void> {
275
+ await this.publisher.publish(channel, message);
276
+ }
277
+
278
+ /**
279
+ * Get the Redis connection URL.
280
+ */
281
+ protected getUrl(): string {
282
+ // Prefer REDIS_URL if set
283
+ if (this.env.REDIS_URL) {
284
+ return this.env.REDIS_URL;
285
+ }
286
+
287
+ // Build URL from components
288
+ const url = new URL("redis://127.0.0.1:6379");
289
+
290
+ if (this.env.REDIS_PASSWORD) {
291
+ url.password = this.env.REDIS_PASSWORD;
292
+ }
293
+
294
+ if (this.env.REDIS_HOST) {
295
+ url.hostname = this.env.REDIS_HOST;
296
+ }
297
+
298
+ if (this.env.REDIS_PORT) {
299
+ url.port = String(this.env.REDIS_PORT);
300
+ }
301
+
302
+ return url.toString();
303
+ }
304
+ }
@@ -0,0 +1,94 @@
1
+ import { $hook, $inject, Alepha, AlephaError } from "alepha";
2
+ import { $logger } from "alepha/logger";
3
+ import type { RedisClient as BunRedisClient } from "bun";
4
+ import { BunRedisProvider } from "./BunRedisProvider.ts";
5
+ import {
6
+ RedisSubscriberProvider,
7
+ type SubscribeCallback,
8
+ } from "./RedisSubscriberProvider.ts";
9
+
10
+ /**
11
+ * Bun Redis subscriber provider for pub/sub operations.
12
+ *
13
+ * This provider creates a dedicated Redis connection for subscriptions,
14
+ * as Redis requires separate connections for pub/sub operations.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const subscriber = alepha.inject(RedisSubscriberProvider);
19
+ * await subscriber.subscribe("channel", (message, channel) => {
20
+ * console.log(`Received: ${message} on ${channel}`);
21
+ * });
22
+ * ```
23
+ */
24
+ export class BunRedisSubscriberProvider extends RedisSubscriberProvider {
25
+ protected readonly log = $logger();
26
+ protected readonly alepha = $inject(Alepha);
27
+ protected readonly redisProvider = $inject(BunRedisProvider);
28
+ protected client?: BunRedisClient;
29
+
30
+ public get subscriber(): BunRedisClient {
31
+ if (!this.client?.connected) {
32
+ throw new AlephaError("Redis subscriber client is not ready");
33
+ }
34
+
35
+ return this.client;
36
+ }
37
+
38
+ public override get isReady(): boolean {
39
+ return this.client?.connected ?? false;
40
+ }
41
+
42
+ protected readonly start = $hook({
43
+ on: "start",
44
+ handler: () => this.connect(),
45
+ });
46
+
47
+ protected readonly stop = $hook({
48
+ on: "stop",
49
+ handler: () => this.close(),
50
+ });
51
+
52
+ /**
53
+ * Connect to the Redis server for subscriptions.
54
+ */
55
+ public override async connect(): Promise<void> {
56
+ this.log.debug("Connecting subscriber...");
57
+ this.client = await this.redisProvider.duplicate();
58
+ this.log.info("Subscriber connection OK");
59
+ }
60
+
61
+ /**
62
+ * Close the subscriber connection.
63
+ */
64
+ public override async close(): Promise<void> {
65
+ if (this.client) {
66
+ this.log.debug("Closing subscriber connection...");
67
+ this.client.close();
68
+ this.client = undefined;
69
+ this.log.info("Subscriber connection closed");
70
+ }
71
+ }
72
+
73
+ public override async subscribe(
74
+ channel: string,
75
+ callback: SubscribeCallback,
76
+ ): Promise<void> {
77
+ await this.subscriber.subscribe(channel, (message, ch) => {
78
+ // Bun's callback provides Buffer or string, normalize to string
79
+ const msg =
80
+ typeof message === "object" && message !== null
81
+ ? Buffer.from(message as Uint8Array).toString()
82
+ : String(message);
83
+ callback(msg, ch);
84
+ });
85
+ }
86
+
87
+ public override async unsubscribe(
88
+ channel: string,
89
+ _callback?: SubscribeCallback,
90
+ ): Promise<void> {
91
+ // Bun's unsubscribe doesn't support callback filtering
92
+ await this.subscriber.unsubscribe(channel);
93
+ }
94
+ }