@motiadev/adapter-redis-cron 0.13.2-beta.164-681857 → 0.14.0-beta.165-707935

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.
@@ -0,0 +1,3 @@
1
+ import { RedisCronAdapterOptions } from "./types.mjs";
2
+ import { RedisCronAdapter } from "./redis-cron-adapter.mjs";
3
+ export { RedisCronAdapter, type RedisCronAdapterOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { RedisCronAdapter } from "./redis-cron-adapter.mjs";
2
+
3
+ export { RedisCronAdapter };
@@ -0,0 +1,30 @@
1
+ import { RedisCronAdapterOptions } from "./types.mjs";
2
+ import { RedisClientOptions, RedisClientType } from "redis";
3
+ import { CronAdapter, CronLock, CronLockInfo } from "@motiadev/core";
4
+
5
+ //#region src/redis-cron-adapter.d.ts
6
+ declare class RedisCronAdapter implements CronAdapter {
7
+ private client;
8
+ private keyPrefix;
9
+ private lockTTL;
10
+ private lockRetryDelay;
11
+ private lockRetryAttempts;
12
+ private instanceId;
13
+ private enableHealthCheck;
14
+ private connected;
15
+ private isExternalClient;
16
+ constructor(redisConnection: RedisClientType | RedisClientOptions, options?: RedisCronAdapterOptions);
17
+ private connect;
18
+ private ensureConnected;
19
+ private makeKey;
20
+ acquireLock(jobName: string, ttl?: number): Promise<CronLock | null>;
21
+ releaseLock(lock: CronLock): Promise<void>;
22
+ renewLock(lock: CronLock, ttl: number): Promise<boolean>;
23
+ isHealthy(): Promise<boolean>;
24
+ shutdown(): Promise<void>;
25
+ getActiveLocks(): Promise<CronLockInfo[]>;
26
+ private scanKeys;
27
+ }
28
+ //#endregion
29
+ export { RedisCronAdapter };
30
+ //# sourceMappingURL=redis-cron-adapter.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-cron-adapter.d.mts","names":[],"sources":["../src/redis-cron-adapter.ts"],"sourcesContent":[],"mappings":";;;;;cASa,gBAAA,YAA4B;;EAA5B,QAAA,SAAA;EAWkB,QAAA,OAAA;EAAkB,QAAA,cAAA;EAA8B,QAAA,iBAAA;EA4DnB,QAAA,UAAA;EAAR,QAAA,iBAAA;EA8C1B,QAAA,SAAA;EAAW,QAAA,gBAAA;EA6Bb,WAAA,CAAA,eAAA,EAvIO,eAuIP,GAvIyB,kBAuIzB,EAAA,OAAA,CAAA,EAvIuD,uBAuIvD;EAAwB,QAAA,OAAA;EAwC3B,QAAA,eAAA;EAcD,QAAA,OAAA;EAyBc,WAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,CAAA,EAAA,MAAA,CAAA,EA1JkB,OA0JlB,CA1J0B,QA0J1B,GAAA,IAAA,CAAA;EAAR,WAAA,CAAA,IAAA,EA5GA,QA4GA,CAAA,EA5GW,OA4GX,CAAA,IAAA,CAAA;EAjOe,SAAA,CAAA,IAAA,EAkJjB,QAlJiB,EAAA,GAAA,EAAA,MAAA,CAAA,EAkJO,OAlJP,CAAA,OAAA,CAAA;EAAW,SAAA,CAAA,CAAA,EA0L/B,OA1L+B,CAAA,OAAA,CAAA;cAwMhC;oBAyBM,QAAQ"}
@@ -0,0 +1,205 @@
1
+ import { createClient } from "redis";
2
+ import { v4 } from "uuid";
3
+
4
+ //#region src/redis-cron-adapter.ts
5
+ function isRedisClient(input) {
6
+ return typeof input === "object" && "isOpen" in input && "connect" in input;
7
+ }
8
+ var RedisCronAdapter = class {
9
+ constructor(redisConnection, options) {
10
+ this.connected = false;
11
+ this.keyPrefix = options?.keyPrefix || "motia:cron:lock:";
12
+ this.lockTTL = options?.lockTTL || 3e5;
13
+ this.lockRetryDelay = options?.lockRetryDelay || 1e3;
14
+ this.lockRetryAttempts = options?.lockRetryAttempts || 0;
15
+ this.instanceId = options?.instanceId || `motia-${v4()}`;
16
+ this.enableHealthCheck = options?.enableHealthCheck ?? true;
17
+ if (isRedisClient(redisConnection)) {
18
+ this.client = redisConnection;
19
+ this.isExternalClient = true;
20
+ this.connected = this.client.isOpen;
21
+ } else {
22
+ const config = redisConnection;
23
+ this.isExternalClient = false;
24
+ this.client = createClient(config);
25
+ this.client.on("error", (err) => {
26
+ console.error("[Redis Cron] Client error:", err);
27
+ });
28
+ this.client.on("connect", () => {
29
+ this.connected = true;
30
+ });
31
+ this.client.on("disconnect", () => {
32
+ console.warn("[Redis Cron] Disconnected");
33
+ this.connected = false;
34
+ });
35
+ this.client.on("reconnecting", () => {
36
+ console.log("[Redis Cron] Reconnecting...");
37
+ });
38
+ this.connect();
39
+ }
40
+ }
41
+ async connect() {
42
+ if (!this.connected && !this.client.isOpen) try {
43
+ await this.client.connect();
44
+ } catch (error) {
45
+ console.error("[Redis Cron] Failed to connect:", error);
46
+ throw error;
47
+ }
48
+ }
49
+ async ensureConnected() {
50
+ if (!this.client.isOpen) await this.connect();
51
+ }
52
+ makeKey(jobName) {
53
+ return `${this.keyPrefix}${jobName}`;
54
+ }
55
+ async acquireLock(jobName, ttl) {
56
+ await this.ensureConnected();
57
+ const lockTTL = ttl || this.lockTTL;
58
+ const lockId = v4();
59
+ const key = this.makeKey(jobName);
60
+ const now = Date.now();
61
+ const lock = {
62
+ jobName,
63
+ lockId,
64
+ acquiredAt: now,
65
+ expiresAt: now + lockTTL,
66
+ instanceId: this.instanceId
67
+ };
68
+ const lockData = JSON.stringify(lock);
69
+ if (await this.client.set(key, lockData, {
70
+ PX: lockTTL,
71
+ NX: true
72
+ }) === "OK") return lock;
73
+ if (this.lockRetryAttempts > 0) for (let attempt = 0; attempt < this.lockRetryAttempts; attempt++) {
74
+ await new Promise((resolve) => setTimeout(resolve, this.lockRetryDelay));
75
+ if (await this.client.set(key, lockData, {
76
+ PX: lockTTL,
77
+ NX: true
78
+ }) === "OK") return lock;
79
+ }
80
+ return null;
81
+ }
82
+ async releaseLock(lock) {
83
+ await this.ensureConnected();
84
+ const key = this.makeKey(lock.jobName);
85
+ const luaScript = `
86
+ local current = redis.call('GET', KEYS[1])
87
+ if not current then
88
+ return 0
89
+ end
90
+
91
+ local lock = cjson.decode(current)
92
+ if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then
93
+ return redis.call('DEL', KEYS[1])
94
+ end
95
+
96
+ return 0
97
+ `;
98
+ try {
99
+ await this.client.eval(luaScript, {
100
+ keys: [key],
101
+ arguments: [lock.lockId, lock.instanceId]
102
+ });
103
+ } catch (error) {
104
+ console.error("[Redis Cron] Error releasing lock:", error);
105
+ }
106
+ }
107
+ async renewLock(lock, ttl) {
108
+ await this.ensureConnected();
109
+ const key = this.makeKey(lock.jobName);
110
+ const expiresAt = Date.now() + ttl;
111
+ const renewedLock = {
112
+ ...lock,
113
+ expiresAt
114
+ };
115
+ const luaScript = `
116
+ local current = redis.call('GET', KEYS[1])
117
+ if not current then
118
+ return 0
119
+ end
120
+
121
+ local lock = cjson.decode(current)
122
+ if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then
123
+ redis.call('SET', KEYS[1], ARGV[3], 'PX', ARGV[4])
124
+ return 1
125
+ end
126
+
127
+ return 0
128
+ `;
129
+ try {
130
+ return await this.client.eval(luaScript, {
131
+ keys: [key],
132
+ arguments: [
133
+ lock.lockId,
134
+ lock.instanceId,
135
+ JSON.stringify(renewedLock),
136
+ ttl.toString()
137
+ ]
138
+ }) === 1;
139
+ } catch (error) {
140
+ console.error("[Redis Cron] Error renewing lock:", error);
141
+ return false;
142
+ }
143
+ }
144
+ async isHealthy() {
145
+ if (!this.enableHealthCheck) return true;
146
+ try {
147
+ await this.ensureConnected();
148
+ return await this.client.ping() === "PONG";
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+ async shutdown() {
154
+ await this.ensureConnected();
155
+ const pattern = `${this.keyPrefix}*`;
156
+ const keys = await this.scanKeys(pattern);
157
+ for (const key of keys) {
158
+ const lockData = await this.client.get(key);
159
+ if (lockData) try {
160
+ if (JSON.parse(lockData).instanceId === this.instanceId) await this.client.del(key);
161
+ } catch (error) {
162
+ console.error("[Redis Cron] Error cleaning up lock during shutdown:", error);
163
+ }
164
+ }
165
+ if (!this.isExternalClient && this.client.isOpen) await this.client.quit();
166
+ }
167
+ async getActiveLocks() {
168
+ await this.ensureConnected();
169
+ const pattern = `${this.keyPrefix}*`;
170
+ const keys = await this.scanKeys(pattern);
171
+ const locks = [];
172
+ for (const key of keys) {
173
+ const lockData = await this.client.get(key);
174
+ if (lockData) try {
175
+ const lock = JSON.parse(lockData);
176
+ locks.push({
177
+ jobName: lock.jobName,
178
+ instanceId: lock.instanceId,
179
+ acquiredAt: lock.acquiredAt,
180
+ expiresAt: lock.expiresAt
181
+ });
182
+ } catch (error) {
183
+ console.error("[Redis Cron] Error parsing lock data:", error);
184
+ }
185
+ }
186
+ return locks;
187
+ }
188
+ async scanKeys(pattern) {
189
+ const keys = [];
190
+ let cursor = "0";
191
+ do {
192
+ const result = await this.client.scan(cursor.toString(), {
193
+ MATCH: pattern,
194
+ COUNT: 100
195
+ });
196
+ cursor = result.cursor;
197
+ keys.push(...result.keys);
198
+ } while (String(cursor) !== "0");
199
+ return keys;
200
+ }
201
+ };
202
+
203
+ //#endregion
204
+ export { RedisCronAdapter };
205
+ //# sourceMappingURL=redis-cron-adapter.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-cron-adapter.mjs","names":["uuidv4","config: RedisClientOptions","lock: CronLock","renewedLock: CronLock","locks: CronLockInfo[]","keys: string[]","cursor: string | number"],"sources":["../src/redis-cron-adapter.ts"],"sourcesContent":["import type { CronAdapter, CronLock, CronLockInfo } from '@motiadev/core'\nimport { createClient, type RedisClientOptions, type RedisClientType } from 'redis'\nimport { v4 as uuidv4 } from 'uuid'\nimport type { RedisCronAdapterOptions } from './types'\n\nfunction isRedisClient(input: RedisClientType | RedisClientOptions): input is RedisClientType {\n return typeof input === 'object' && 'isOpen' in input && 'connect' in input\n}\n\nexport class RedisCronAdapter implements CronAdapter {\n private client: RedisClientType\n private keyPrefix: string\n private lockTTL: number\n private lockRetryDelay: number\n private lockRetryAttempts: number\n private instanceId: string\n private enableHealthCheck: boolean\n private connected = false\n private isExternalClient: boolean\n\n constructor(redisConnection: RedisClientType | RedisClientOptions, options?: RedisCronAdapterOptions) {\n this.keyPrefix = options?.keyPrefix || 'motia:cron:lock:'\n this.lockTTL = options?.lockTTL || 300000\n this.lockRetryDelay = options?.lockRetryDelay || 1000\n this.lockRetryAttempts = options?.lockRetryAttempts || 0\n this.instanceId = options?.instanceId || `motia-${uuidv4()}`\n this.enableHealthCheck = options?.enableHealthCheck ?? true\n\n if (isRedisClient(redisConnection)) {\n this.client = redisConnection\n this.isExternalClient = true\n this.connected = this.client.isOpen\n } else {\n const config: RedisClientOptions = redisConnection\n this.isExternalClient = false\n\n this.client = createClient(config) as RedisClientType\n\n this.client.on('error', (err) => {\n console.error('[Redis Cron] Client error:', err)\n })\n\n this.client.on('connect', () => {\n this.connected = true\n })\n\n this.client.on('disconnect', () => {\n console.warn('[Redis Cron] Disconnected')\n this.connected = false\n })\n\n this.client.on('reconnecting', () => {\n console.log('[Redis Cron] Reconnecting...')\n })\n\n this.connect()\n }\n }\n\n private async connect(): Promise<void> {\n if (!this.connected && !this.client.isOpen) {\n try {\n await this.client.connect()\n } catch (error) {\n console.error('[Redis Cron] Failed to connect:', error)\n throw error\n }\n }\n }\n\n private async ensureConnected(): Promise<void> {\n if (!this.client.isOpen) {\n await this.connect()\n }\n }\n\n private makeKey(jobName: string): string {\n return `${this.keyPrefix}${jobName}`\n }\n\n async acquireLock(jobName: string, ttl?: number): Promise<CronLock | null> {\n await this.ensureConnected()\n\n const lockTTL = ttl || this.lockTTL\n const lockId = uuidv4()\n const key = this.makeKey(jobName)\n const now = Date.now()\n const expiresAt = now + lockTTL\n\n const lock: CronLock = {\n jobName,\n lockId,\n acquiredAt: now,\n expiresAt,\n instanceId: this.instanceId,\n }\n\n const lockData = JSON.stringify(lock)\n\n const result = await this.client.set(key, lockData, {\n PX: lockTTL,\n NX: true,\n })\n\n if (result === 'OK') {\n return lock\n }\n\n if (this.lockRetryAttempts > 0) {\n for (let attempt = 0; attempt < this.lockRetryAttempts; attempt++) {\n await new Promise((resolve) => setTimeout(resolve, this.lockRetryDelay))\n\n const retryResult = await this.client.set(key, lockData, {\n PX: lockTTL,\n NX: true,\n })\n\n if (retryResult === 'OK') {\n return lock\n }\n }\n }\n\n return null\n }\n\n async releaseLock(lock: CronLock): Promise<void> {\n await this.ensureConnected()\n\n const key = this.makeKey(lock.jobName)\n\n const luaScript = `\n local current = redis.call('GET', KEYS[1])\n if not current then\n return 0\n end\n \n local lock = cjson.decode(current)\n if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then\n return redis.call('DEL', KEYS[1])\n end\n \n return 0\n `\n\n try {\n await this.client.eval(luaScript, {\n keys: [key],\n arguments: [lock.lockId, lock.instanceId],\n })\n } catch (error) {\n console.error('[Redis Cron] Error releasing lock:', error)\n }\n }\n\n async renewLock(lock: CronLock, ttl: number): Promise<boolean> {\n await this.ensureConnected()\n\n const key = this.makeKey(lock.jobName)\n const now = Date.now()\n const expiresAt = now + ttl\n\n const renewedLock: CronLock = {\n ...lock,\n expiresAt,\n }\n\n const luaScript = `\n local current = redis.call('GET', KEYS[1])\n if not current then\n return 0\n end\n \n local lock = cjson.decode(current)\n if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then\n redis.call('SET', KEYS[1], ARGV[3], 'PX', ARGV[4])\n return 1\n end\n \n return 0\n `\n\n try {\n const result = await this.client.eval(luaScript, {\n keys: [key],\n arguments: [lock.lockId, lock.instanceId, JSON.stringify(renewedLock), ttl.toString()],\n })\n\n return result === 1\n } catch (error) {\n console.error('[Redis Cron] Error renewing lock:', error)\n return false\n }\n }\n\n async isHealthy(): Promise<boolean> {\n if (!this.enableHealthCheck) {\n return true\n }\n\n try {\n await this.ensureConnected()\n const result = await this.client.ping()\n return result === 'PONG'\n } catch {\n return false\n }\n }\n\n async shutdown(): Promise<void> {\n await this.ensureConnected()\n\n const pattern = `${this.keyPrefix}*`\n const keys = await this.scanKeys(pattern)\n\n for (const key of keys) {\n const lockData = await this.client.get(key)\n if (lockData) {\n try {\n const lock: CronLock = JSON.parse(lockData)\n if (lock.instanceId === this.instanceId) {\n await this.client.del(key)\n }\n } catch (error) {\n console.error('[Redis Cron] Error cleaning up lock during shutdown:', error)\n }\n }\n }\n\n if (!this.isExternalClient && this.client.isOpen) {\n await this.client.quit()\n }\n }\n\n async getActiveLocks(): Promise<CronLockInfo[]> {\n await this.ensureConnected()\n\n const pattern = `${this.keyPrefix}*`\n const keys = await this.scanKeys(pattern)\n const locks: CronLockInfo[] = []\n\n for (const key of keys) {\n const lockData = await this.client.get(key)\n if (lockData) {\n try {\n const lock: CronLock = JSON.parse(lockData)\n locks.push({\n jobName: lock.jobName,\n instanceId: lock.instanceId,\n acquiredAt: lock.acquiredAt,\n expiresAt: lock.expiresAt,\n })\n } catch (error) {\n console.error('[Redis Cron] Error parsing lock data:', error)\n }\n }\n }\n\n return locks\n }\n\n private async scanKeys(pattern: string): Promise<string[]> {\n const keys: string[] = []\n let cursor: string | number = '0'\n\n do {\n const result = await this.client.scan(cursor.toString(), {\n MATCH: pattern,\n COUNT: 100,\n })\n cursor = result.cursor\n keys.push(...result.keys)\n } while (String(cursor) !== '0')\n\n return keys\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ //#region src/types.d.ts
2
+ interface RedisCronAdapterOptions {
3
+ keyPrefix?: string;
4
+ lockTTL?: number;
5
+ lockRetryDelay?: number;
6
+ lockRetryAttempts?: number;
7
+ instanceId?: string;
8
+ enableHealthCheck?: boolean;
9
+ }
10
+ //#endregion
11
+ export { RedisCronAdapterOptions };
12
+ //# sourceMappingURL=types.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";UAAiB,uBAAA;EAAA,SAAA,CAAA,EAAA,MAAA"}
package/package.json CHANGED
@@ -1,24 +1,28 @@
1
1
  {
2
2
  "name": "@motiadev/adapter-redis-cron",
3
3
  "description": "Redis cron adapter for Motia framework, enabling distributed cron job coordination to prevent duplicate executions across multiple instances.",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "version": "0.13.2-beta.164-681857",
4
+ "type": "module",
5
+ "main": "dist/index.mjs",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.mts",
8
+ "version": "0.14.0-beta.165-707935",
7
9
  "dependencies": {
8
10
  "redis": "^5.9.0",
9
11
  "uuid": "^11.1.0",
10
- "@motiadev/core": "0.13.2-beta.164-681857"
12
+ "@motiadev/core": "0.14.0-beta.165-707935"
11
13
  },
12
14
  "devDependencies": {
13
15
  "@types/node": "^22.10.2",
16
+ "tsdown": "^0.16.6",
14
17
  "typescript": "^5.7.2"
15
18
  },
16
19
  "peerDependencies": {
17
- "@motiadev/core": "^0.8.0"
20
+ "@motiadev/core": ">=0.8.0"
18
21
  },
19
22
  "scripts": {
20
- "build": "rm -rf dist && tsc",
23
+ "build": "tsdown",
24
+ "dev": "tsdown --watch",
21
25
  "lint": "biome check .",
22
- "watch": "tsc --watch"
26
+ "clean": "rm -rf dist"
23
27
  }
24
28
  }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'tsdown'
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: './src/index.ts',
6
+ },
7
+ format: 'esm',
8
+ platform: 'node',
9
+ external: ['@motiadev/core', 'redis', 'uuid'],
10
+ dts: {
11
+ build: true,
12
+ },
13
+ clean: true,
14
+ outDir: 'dist',
15
+ sourcemap: true,
16
+ unbundle: true,
17
+ })
package/dist/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export { RedisCronAdapter } from './redis-cron-adapter';
2
- export type { RedisCronAdapterOptions } from './types';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,YAAY,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA"}
package/dist/index.js DELETED
@@ -1,5 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RedisCronAdapter = void 0;
4
- var redis_cron_adapter_1 = require("./redis-cron-adapter");
5
- Object.defineProperty(exports, "RedisCronAdapter", { enumerable: true, get: function () { return redis_cron_adapter_1.RedisCronAdapter; } });
@@ -1,26 +0,0 @@
1
- import type { CronAdapter, CronLock, CronLockInfo } from '@motiadev/core';
2
- import { type RedisClientOptions, type RedisClientType } from 'redis';
3
- import type { RedisCronAdapterOptions } from './types';
4
- export declare class RedisCronAdapter implements CronAdapter {
5
- private client;
6
- private keyPrefix;
7
- private lockTTL;
8
- private lockRetryDelay;
9
- private lockRetryAttempts;
10
- private instanceId;
11
- private enableHealthCheck;
12
- private connected;
13
- private isExternalClient;
14
- constructor(redisConnection: RedisClientType | RedisClientOptions, options?: RedisCronAdapterOptions);
15
- private connect;
16
- private ensureConnected;
17
- private makeKey;
18
- acquireLock(jobName: string, ttl?: number): Promise<CronLock | null>;
19
- releaseLock(lock: CronLock): Promise<void>;
20
- renewLock(lock: CronLock, ttl: number): Promise<boolean>;
21
- isHealthy(): Promise<boolean>;
22
- shutdown(): Promise<void>;
23
- getActiveLocks(): Promise<CronLockInfo[]>;
24
- private scanKeys;
25
- }
26
- //# sourceMappingURL=redis-cron-adapter.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redis-cron-adapter.d.ts","sourceRoot":"","sources":["../src/redis-cron-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EAAgB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,OAAO,CAAA;AAEnF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAMtD,qBAAa,gBAAiB,YAAW,WAAW;IAClD,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,gBAAgB,CAAS;gBAErB,eAAe,EAAE,eAAe,GAAG,kBAAkB,EAAE,OAAO,CAAC,EAAE,uBAAuB;YAuCtF,OAAO;YAWP,eAAe;IAM7B,OAAO,CAAC,OAAO;IAIT,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IA8CpE,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B1C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAwCxD,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAc7B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBzB,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;YA2BjC,QAAQ;CAevB"}
@@ -1,232 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RedisCronAdapter = void 0;
4
- const redis_1 = require("redis");
5
- const uuid_1 = require("uuid");
6
- function isRedisClient(input) {
7
- return typeof input === 'object' && 'isOpen' in input && 'connect' in input;
8
- }
9
- class RedisCronAdapter {
10
- constructor(redisConnection, options) {
11
- this.connected = false;
12
- this.keyPrefix = options?.keyPrefix || 'motia:cron:lock:';
13
- this.lockTTL = options?.lockTTL || 300000;
14
- this.lockRetryDelay = options?.lockRetryDelay || 1000;
15
- this.lockRetryAttempts = options?.lockRetryAttempts || 0;
16
- this.instanceId = options?.instanceId || `motia-${(0, uuid_1.v4)()}`;
17
- this.enableHealthCheck = options?.enableHealthCheck ?? true;
18
- if (isRedisClient(redisConnection)) {
19
- this.client = redisConnection;
20
- this.isExternalClient = true;
21
- this.connected = this.client.isOpen;
22
- }
23
- else {
24
- const config = redisConnection;
25
- this.isExternalClient = false;
26
- this.client = (0, redis_1.createClient)(config);
27
- this.client.on('error', (err) => {
28
- console.error('[Redis Cron] Client error:', err);
29
- });
30
- this.client.on('connect', () => {
31
- this.connected = true;
32
- });
33
- this.client.on('disconnect', () => {
34
- console.warn('[Redis Cron] Disconnected');
35
- this.connected = false;
36
- });
37
- this.client.on('reconnecting', () => {
38
- console.log('[Redis Cron] Reconnecting...');
39
- });
40
- this.connect();
41
- }
42
- }
43
- async connect() {
44
- if (!this.connected && !this.client.isOpen) {
45
- try {
46
- await this.client.connect();
47
- }
48
- catch (error) {
49
- console.error('[Redis Cron] Failed to connect:', error);
50
- throw error;
51
- }
52
- }
53
- }
54
- async ensureConnected() {
55
- if (!this.client.isOpen) {
56
- await this.connect();
57
- }
58
- }
59
- makeKey(jobName) {
60
- return `${this.keyPrefix}${jobName}`;
61
- }
62
- async acquireLock(jobName, ttl) {
63
- await this.ensureConnected();
64
- const lockTTL = ttl || this.lockTTL;
65
- const lockId = (0, uuid_1.v4)();
66
- const key = this.makeKey(jobName);
67
- const now = Date.now();
68
- const expiresAt = now + lockTTL;
69
- const lock = {
70
- jobName,
71
- lockId,
72
- acquiredAt: now,
73
- expiresAt,
74
- instanceId: this.instanceId,
75
- };
76
- const lockData = JSON.stringify(lock);
77
- const result = await this.client.set(key, lockData, {
78
- PX: lockTTL,
79
- NX: true,
80
- });
81
- if (result === 'OK') {
82
- return lock;
83
- }
84
- if (this.lockRetryAttempts > 0) {
85
- for (let attempt = 0; attempt < this.lockRetryAttempts; attempt++) {
86
- await new Promise((resolve) => setTimeout(resolve, this.lockRetryDelay));
87
- const retryResult = await this.client.set(key, lockData, {
88
- PX: lockTTL,
89
- NX: true,
90
- });
91
- if (retryResult === 'OK') {
92
- return lock;
93
- }
94
- }
95
- }
96
- return null;
97
- }
98
- async releaseLock(lock) {
99
- await this.ensureConnected();
100
- const key = this.makeKey(lock.jobName);
101
- const luaScript = `
102
- local current = redis.call('GET', KEYS[1])
103
- if not current then
104
- return 0
105
- end
106
-
107
- local lock = cjson.decode(current)
108
- if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then
109
- return redis.call('DEL', KEYS[1])
110
- end
111
-
112
- return 0
113
- `;
114
- try {
115
- await this.client.eval(luaScript, {
116
- keys: [key],
117
- arguments: [lock.lockId, lock.instanceId],
118
- });
119
- }
120
- catch (error) {
121
- console.error('[Redis Cron] Error releasing lock:', error);
122
- }
123
- }
124
- async renewLock(lock, ttl) {
125
- await this.ensureConnected();
126
- const key = this.makeKey(lock.jobName);
127
- const now = Date.now();
128
- const expiresAt = now + ttl;
129
- const renewedLock = {
130
- ...lock,
131
- expiresAt,
132
- };
133
- const luaScript = `
134
- local current = redis.call('GET', KEYS[1])
135
- if not current then
136
- return 0
137
- end
138
-
139
- local lock = cjson.decode(current)
140
- if lock.lockId == ARGV[1] and lock.instanceId == ARGV[2] then
141
- redis.call('SET', KEYS[1], ARGV[3], 'PX', ARGV[4])
142
- return 1
143
- end
144
-
145
- return 0
146
- `;
147
- try {
148
- const result = await this.client.eval(luaScript, {
149
- keys: [key],
150
- arguments: [lock.lockId, lock.instanceId, JSON.stringify(renewedLock), ttl.toString()],
151
- });
152
- return result === 1;
153
- }
154
- catch (error) {
155
- console.error('[Redis Cron] Error renewing lock:', error);
156
- return false;
157
- }
158
- }
159
- async isHealthy() {
160
- if (!this.enableHealthCheck) {
161
- return true;
162
- }
163
- try {
164
- await this.ensureConnected();
165
- const result = await this.client.ping();
166
- return result === 'PONG';
167
- }
168
- catch {
169
- return false;
170
- }
171
- }
172
- async shutdown() {
173
- await this.ensureConnected();
174
- const pattern = `${this.keyPrefix}*`;
175
- const keys = await this.scanKeys(pattern);
176
- for (const key of keys) {
177
- const lockData = await this.client.get(key);
178
- if (lockData) {
179
- try {
180
- const lock = JSON.parse(lockData);
181
- if (lock.instanceId === this.instanceId) {
182
- await this.client.del(key);
183
- }
184
- }
185
- catch (error) {
186
- console.error('[Redis Cron] Error cleaning up lock during shutdown:', error);
187
- }
188
- }
189
- }
190
- if (!this.isExternalClient && this.client.isOpen) {
191
- await this.client.quit();
192
- }
193
- }
194
- async getActiveLocks() {
195
- await this.ensureConnected();
196
- const pattern = `${this.keyPrefix}*`;
197
- const keys = await this.scanKeys(pattern);
198
- const locks = [];
199
- for (const key of keys) {
200
- const lockData = await this.client.get(key);
201
- if (lockData) {
202
- try {
203
- const lock = JSON.parse(lockData);
204
- locks.push({
205
- jobName: lock.jobName,
206
- instanceId: lock.instanceId,
207
- acquiredAt: lock.acquiredAt,
208
- expiresAt: lock.expiresAt,
209
- });
210
- }
211
- catch (error) {
212
- console.error('[Redis Cron] Error parsing lock data:', error);
213
- }
214
- }
215
- }
216
- return locks;
217
- }
218
- async scanKeys(pattern) {
219
- const keys = [];
220
- let cursor = '0';
221
- do {
222
- const result = await this.client.scan(cursor.toString(), {
223
- MATCH: pattern,
224
- COUNT: 100,
225
- });
226
- cursor = result.cursor;
227
- keys.push(...result.keys);
228
- } while (String(cursor) !== '0');
229
- return keys;
230
- }
231
- }
232
- exports.RedisCronAdapter = RedisCronAdapter;
package/dist/types.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export interface RedisCronAdapterOptions {
2
- keyPrefix?: string;
3
- lockTTL?: number;
4
- lockRetryDelay?: number;
5
- lockRetryAttempts?: number;
6
- instanceId?: string;
7
- enableHealthCheck?: boolean;
8
- }
9
- //# sourceMappingURL=types.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B"}
package/dist/types.js DELETED
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });