@pioneer-platform/pioneer-cache 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
1
+ /*
2
+ Unified Refresh Worker
3
+
4
+ Single worker that processes refresh jobs for ALL cache types (balance, price, etc.)
5
+ Replaces separate balance-refresh.worker.ts and price-refresh.worker.ts
6
+ */
7
+
8
+ import type { RefreshJob } from '../types';
9
+ import type { BaseCache } from '../core/base-cache';
10
+
11
+ const log = require('@pioneer-platform/loggerdog')();
12
+ const TAG = ' | RefreshWorker | ';
13
+
14
+ /**
15
+ * Worker configuration
16
+ */
17
+ export interface WorkerConfig {
18
+ queueName: string;
19
+ maxRetries: number;
20
+ retryDelay: number;
21
+ pollInterval?: number; // Poll interval in ms (default: 100ms)
22
+ }
23
+
24
+ /**
25
+ * Unified refresh worker that processes jobs for any cache type
26
+ */
27
+ export class RefreshWorker {
28
+ private redis: any;
29
+ private redisQueue: any;
30
+ private cacheRegistry: Map<string, BaseCache<any>> = new Map();
31
+ private config: WorkerConfig;
32
+ private isRunning: boolean = false;
33
+ private isProcessing: boolean = false;
34
+ private pollTimeoutId: NodeJS.Timeout | null = null;
35
+
36
+ constructor(redis: any, config: WorkerConfig) {
37
+ this.redis = redis;
38
+ this.config = config;
39
+
40
+ try {
41
+ this.redisQueue = require('@pioneer-platform/redis-queue');
42
+ log.info(TAG, `✅ RefreshWorker initialized for queue: ${config.queueName}`);
43
+ } catch (error) {
44
+ log.error(TAG, '❌ Failed to initialize redis-queue:', error);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Register a cache instance with this worker
51
+ * The worker will route jobs to the appropriate cache based on job type
52
+ */
53
+ registerCache(cacheName: string, cache: BaseCache<any>): void {
54
+ this.cacheRegistry.set(cacheName.toLowerCase(), cache);
55
+ log.info(TAG, `Registered ${cacheName} cache with worker`);
56
+ }
57
+
58
+ /**
59
+ * Start processing jobs from the queue
60
+ */
61
+ async start(): Promise<void> {
62
+ const tag = TAG + 'start | ';
63
+
64
+ if (this.isRunning) {
65
+ log.warn(tag, 'Worker already running');
66
+ return;
67
+ }
68
+
69
+ log.info(tag, `🚀 Starting refresh worker for queue: ${this.config.queueName}`);
70
+ log.info(tag, `Registered caches: ${Array.from(this.cacheRegistry.keys()).join(', ')}`);
71
+
72
+ this.isRunning = true;
73
+ this.poll();
74
+
75
+ log.info(tag, '✅ Refresh worker started successfully');
76
+ }
77
+
78
+ /**
79
+ * Stop the worker gracefully
80
+ */
81
+ async stop(): Promise<void> {
82
+ const tag = TAG + 'stop | ';
83
+
84
+ log.info(tag, 'Stopping refresh worker...');
85
+
86
+ this.isRunning = false;
87
+
88
+ if (this.pollTimeoutId) {
89
+ clearTimeout(this.pollTimeoutId);
90
+ this.pollTimeoutId = null;
91
+ }
92
+
93
+ // Wait for current job to finish
94
+ while (this.isProcessing) {
95
+ await new Promise(resolve => setTimeout(resolve, 100));
96
+ }
97
+
98
+ log.info(tag, '✅ Refresh worker stopped');
99
+ }
100
+
101
+ /**
102
+ * Poll for next job from the queue
103
+ */
104
+ private async poll(): Promise<void> {
105
+ const tag = TAG + 'poll | ';
106
+
107
+ if (!this.isRunning) {
108
+ return;
109
+ }
110
+
111
+ try {
112
+ // Don't poll if already processing
113
+ if (this.isProcessing) {
114
+ this.schedulePoll();
115
+ return;
116
+ }
117
+
118
+ // Get next job from queue
119
+ const work = await this.redisQueue.getWork(this.config.queueName, 1);
120
+
121
+ if (work) {
122
+ this.isProcessing = true;
123
+ await this.processJob(work);
124
+ this.isProcessing = false;
125
+ }
126
+
127
+ } catch (error: any) {
128
+ log.error(tag, 'Error in poll loop:', error.message);
129
+ this.isProcessing = false;
130
+ } finally {
131
+ // Schedule next poll
132
+ this.schedulePoll();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Schedule next poll
138
+ */
139
+ private schedulePoll(): void {
140
+ if (this.pollTimeoutId) {
141
+ clearTimeout(this.pollTimeoutId);
142
+ }
143
+
144
+ const pollInterval = this.config.pollInterval || 100;
145
+ this.pollTimeoutId = setTimeout(() => {
146
+ this.poll();
147
+ }, pollInterval);
148
+ }
149
+
150
+ /**
151
+ * Process a single refresh job
152
+ */
153
+ private async processJob(job: RefreshJob): Promise<void> {
154
+ const tag = TAG + 'processJob | ';
155
+ const startTime = Date.now();
156
+
157
+ try {
158
+ const { type, key, params, retryCount = 0 } = job;
159
+
160
+ log.info(tag, `Processing ${type} for ${key} (retry: ${retryCount})`);
161
+
162
+ // Extract cache name from job type (e.g., "REFRESH_BALANCE" -> "balance")
163
+ const cacheName = type.replace('REFRESH_', '').toLowerCase();
164
+
165
+ // Get the appropriate cache instance
166
+ const cache = this.cacheRegistry.get(cacheName);
167
+
168
+ if (!cache) {
169
+ log.error(tag, `No cache registered for type: ${type}`);
170
+ return;
171
+ }
172
+
173
+ // Fetch fresh data using the cache's fetchFresh method
174
+ await cache.fetchFresh(params);
175
+
176
+ const processingTime = Date.now() - startTime;
177
+ log.info(tag, `✅ Processed ${type} in ${processingTime}ms: ${key}`);
178
+
179
+ } catch (error) {
180
+ const processingTime = Date.now() - startTime;
181
+ log.error(tag, `❌ Failed to process ${job.type} after ${processingTime}ms:`, error);
182
+
183
+ // Retry logic
184
+ if ((job.retryCount || 0) < this.config.maxRetries) {
185
+ const newRetryCount = (job.retryCount || 0) + 1;
186
+ log.info(tag, `Retrying job (attempt ${newRetryCount}/${this.config.maxRetries})`);
187
+
188
+ // Re-queue with incremented retry count and delay
189
+ setTimeout(async () => {
190
+ try {
191
+ await this.redisQueue.createWork(this.config.queueName, {
192
+ ...job,
193
+ retryCount: newRetryCount
194
+ });
195
+ } catch (requeueError) {
196
+ log.error(tag, 'Error re-queuing job:', requeueError);
197
+ }
198
+ }, this.config.retryDelay);
199
+
200
+ } else {
201
+ log.error(tag, `Max retries reached for ${job.type}, giving up`);
202
+ }
203
+
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get worker statistics
210
+ */
211
+ async getStats(): Promise<any> {
212
+ const tag = TAG + 'getStats | ';
213
+
214
+ try {
215
+ const queueLength = await this.redisQueue.count(this.config.queueName);
216
+
217
+ return {
218
+ isRunning: this.isRunning,
219
+ isProcessing: this.isProcessing,
220
+ queueName: this.config.queueName,
221
+ queueLength,
222
+ registeredCaches: Array.from(this.cacheRegistry.keys()),
223
+ config: {
224
+ maxRetries: this.config.maxRetries,
225
+ retryDelay: this.config.retryDelay,
226
+ pollInterval: this.config.pollInterval || 100
227
+ }
228
+ };
229
+
230
+ } catch (error) {
231
+ log.error(tag, 'Error getting worker stats:', error);
232
+ return {
233
+ error: error instanceof Error ? error.message : String(error)
234
+ };
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Start a unified refresh worker for multiple cache types
241
+ * Convenience function for common usage
242
+ */
243
+ export async function startUnifiedWorker(
244
+ redis: any,
245
+ caches: Map<string, BaseCache<any>>,
246
+ queueName: string,
247
+ config?: Partial<WorkerConfig>
248
+ ): Promise<RefreshWorker> {
249
+ const workerConfig: WorkerConfig = {
250
+ queueName,
251
+ maxRetries: config?.maxRetries || 3,
252
+ retryDelay: config?.retryDelay || 5000,
253
+ pollInterval: config?.pollInterval || 100
254
+ };
255
+
256
+ const worker = new RefreshWorker(redis, workerConfig);
257
+
258
+ // Register all caches
259
+ for (const [name, cache] of caches) {
260
+ worker.registerCache(name, cache);
261
+ }
262
+
263
+ // Start worker
264
+ await worker.start();
265
+
266
+ return worker;
267
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "moduleResolution": "node"
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
18
+ }