bulltrackers-module 1.0.821 → 1.0.823

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.
@@ -29,7 +29,7 @@ module.exports = {
29
29
  queueName: 'computation-triggers-new',
30
30
  dispatcherUrl: process.env.DISPATCHER_URL || 'https://europe-west1-stocks-12345.cloudfunctions.net/compute-dispatcher',
31
31
  workerUrl: process.env.WORKER_URL || process.env.DISPATCHER_URL || 'https://europe-west1-stocks-12345.cloudfunctions.net/compute-dispatcher',
32
- serviceAccountEmail: process.env.GCP_SERVICE_ACCOUNT_EMAIL || '879684846540-compute@developer.gserviceaccount.com'
32
+ serviceAccountEmail: process.env.CLOUD_TASKS_SA_EMAIL || '879684846540-compute@developer.gserviceaccount.com'
33
33
  },
34
34
 
35
35
  storage: {
@@ -1,10 +1,9 @@
1
1
  /**
2
- * @fileoverview Cloud Tasks Adapter (Extremely Verbose Debugging)
3
- * - Logs config on init.
4
- * - Logs full payload before creation.
5
- * - Logs raw gRPC errors.
2
+ * @fileoverview Cloud Tasks Adapter (V3 - Production Complete)
3
+ * Encapsulates Google Cloud Tasks interactions with full V2 feature parity.
6
4
  */
7
5
 
6
+
8
7
  class CloudTasksAdapter {
9
8
  constructor(client, config, logger = console) {
10
9
  this.client = client;
@@ -12,19 +11,22 @@ class CloudTasksAdapter {
12
11
  this.logger = logger;
13
12
  this.projectId = config.projectId;
14
13
  this.location = config.location || 'europe-west1';
15
- this.queueName = config.queueName || 'computation-triggers-new';
14
+ this.queueName = config.queueName || 'computation-tasks-new';
16
15
  this.dispatcherUrl = config.dispatcherUrl;
17
- this.workerUrl = config.workerUrl || config.dispatcherUrl;
18
- this.serviceAccountEmail = 'super-admin-debug@stocks-12345.iam.gserviceaccount.com';
16
+ this.workerUrl = config.workerUrl || config.dispatcherUrl; // Fallback to dispatcher if not set
17
+ this.serviceAccountEmail = config.serviceAccountEmail;
19
18
 
19
+ // Internal concurrency limit for bulk operations
20
20
  this.concurrency = 20;
21
- this._queuePath = null;
22
21
 
23
- // [DEBUG] Log Configuration on Startup
24
- this.logger.log(`\n[CloudTasks] INIT: Project=${this.projectId}, Location=${this.location}, Queue=${this.queueName}`);
25
- this.logger.log(`[CloudTasks] INIT: ServiceAccount=${this.serviceAccountEmail} (Target Audience: ${this.dispatcherUrl})\n`);
22
+ // Cache queue path
23
+ this._queuePath = null;
26
24
  }
27
25
 
26
+ /**
27
+ * Get the full queue path (cached).
28
+ * @returns {string} Full queue path
29
+ */
28
30
  getQueuePath() {
29
31
  if (!this._queuePath) {
30
32
  this._queuePath = this.client.queuePath(this.projectId, this.location, this.queueName);
@@ -32,61 +34,91 @@ class CloudTasksAdapter {
32
34
  return this._queuePath;
33
35
  }
34
36
 
37
+ /**
38
+ * Create a task with OIDC authentication.
39
+ * @param {string} fullQueuePath - Full queue path from getQueuePath()
40
+ * @param {Object} payload - JSON payload for the task
41
+ * @param {Object} options - { scheduleTime: epochSeconds, name: string }
42
+ * @returns {Promise<Object>} Task response or { status: 'exists' }
43
+ */
35
44
  async createTask(fullQueuePath, payload, options = {}) {
36
- const targetUrl = options.url || (payload.type === 'worker-batch' ? this.workerUrl : this.dispatcherUrl);
37
-
38
- // --- TEST MODIFICATION: STRIP COMPLEXITY ---
39
45
  const task = {
40
46
  httpRequest: {
41
47
  httpMethod: 'POST',
42
- url: targetUrl,
48
+ url: options.url || (payload.type === 'worker-batch' ? this.workerUrl : this.dispatcherUrl),
43
49
  headers: { 'Content-Type': 'application/json' },
44
50
  body: Buffer.from(JSON.stringify(payload)).toString('base64'),
45
- // TEST 1: COMMENT OUT OIDC TOKEN
46
- // oidcToken: {
47
- // serviceAccountEmail: this.serviceAccountEmail,
48
- // audience: targetUrl
49
- // }
51
+ oidcToken: {
52
+ serviceAccountEmail: this.serviceAccountEmail,
53
+ audience: options.url || (payload.type === 'worker-batch' ? this.workerUrl : this.dispatcherUrl)
54
+ }
50
55
  }
51
56
  };
52
57
 
53
- // TEST 2: DISABLE NAMING (Let Cloud Tasks assign a random ID)
54
- // if (options.name) task.name = `${fullQueuePath}/tasks/${options.name}`;
55
- if (options.scheduleTime) task.scheduleTime = { seconds: Math.floor(options.scheduleTime) };
58
+ // Optional: Named task for deduplication
59
+ if (options.name) {
60
+ task.name = `${fullQueuePath}/tasks/${options.name}`;
61
+ }
56
62
 
57
- this.logger.log(`[CloudTasks] TEST MODE: Sending Naked Task (No OIDC, No Name) to ${targetUrl}`);
63
+ // Optional: Scheduled execution
64
+ if (options.scheduleTime) {
65
+ task.scheduleTime = {
66
+ seconds: Math.floor(options.scheduleTime)
67
+ };
68
+ }
58
69
 
59
70
  try {
60
- const [response] = await this.client.createTask({ parent: fullQueuePath, task });
61
- this.logger.log(`[CloudTasks] TEST SUCCESS: Created Task ${response.name}`);
71
+ const [response] = await this.client.createTask({
72
+ parent: fullQueuePath,
73
+ task
74
+ });
62
75
  return response;
63
76
  } catch (e) {
64
- this.logger.error(`[CloudTasks] TEST FAILED: ${e.message}`);
77
+ // ALREADY_EXISTS (gRPC code 6 or HTTP 409)
78
+ if (e.code === 6 || e.code === 409) {
79
+ return { status: 'exists' };
80
+ }
65
81
  throw e;
66
82
  }
67
83
  }
68
84
 
85
+ /**
86
+ * Delete a task by full name.
87
+ * @param {string} taskName - Full task name
88
+ * @returns {Promise<boolean>} true if deleted, false if not found
89
+ */
69
90
  async deleteTask(taskName) {
70
91
  try {
71
92
  await this.client.deleteTask({ name: taskName });
72
93
  return true;
73
94
  } catch (e) {
74
- if (e.code === 5) return false;
95
+ if (e.code === 5) return false; // NOT_FOUND
75
96
  throw e;
76
97
  }
77
98
  }
78
99
 
100
+ /**
101
+ * List all tasks in the queue (async iterator).
102
+ * @param {string} fullQueuePath - Queue path
103
+ * @yields {Object} Task object
104
+ */
79
105
  async *listTasks(fullQueuePath) {
80
106
  const iterable = this.client.listTasksAsync({
81
107
  parent: fullQueuePath,
82
108
  responseView: 'BASIC',
83
109
  pageSize: 1000
84
110
  });
111
+
85
112
  for await (const task of iterable) {
86
113
  yield task;
87
114
  }
88
115
  }
89
116
 
117
+ /**
118
+ * Bulk delete tasks with concurrency control.
119
+ * @param {string[]} taskNames - Array of full task names
120
+ * @returns {Promise<Object>} { success: number, failed: number }
121
+ */
90
122
  async bulkDelete(taskNames) {
91
123
  const results = { success: 0, failed: 0 };
92
124
  const chunkSize = this.concurrency;
@@ -107,11 +139,19 @@ class CloudTasksAdapter {
107
139
  }
108
140
  }));
109
141
  }
142
+
110
143
  return results;
111
144
  }
112
145
 
146
+ /**
147
+ * Schedule a root computation task (Planner-compatible).
148
+ * @param {Object} taskData - { computation, date, hash, source, force }
149
+ * @returns {Promise<Object>} { status: 'scheduled' | 'exists' | 'error', id }
150
+ */
113
151
  async scheduleTask(taskData) {
114
152
  const { computation, targetDate, configHash, reason, runAtSeconds } = taskData;
153
+
154
+ // Generate deterministic task ID for deduplication
115
155
  const normalizedName = computation.toLowerCase().replace(/[^a-z0-9]/g, '');
116
156
  const hashSuffix = configHash ? configHash.substring(0, 8) : 'noHash';
117
157
  const taskId = `root-${normalizedName}-${targetDate}-${hashSuffix}`;
@@ -128,45 +168,82 @@ class CloudTasksAdapter {
128
168
  try {
129
169
  const queuePath = this.getQueuePath();
130
170
  const options = { name: taskId };
131
- if (runAtSeconds) options.scheduleTime = runAtSeconds;
171
+
172
+ if (runAtSeconds) {
173
+ options.scheduleTime = runAtSeconds;
174
+ }
132
175
 
133
176
  const result = await this.createTask(queuePath, payload, options);
134
- if (result.status === 'exists') return { status: 'exists', id: taskId };
177
+
178
+ if (result.status === 'exists') {
179
+ return { status: 'exists', id: taskId };
180
+ }
181
+
135
182
  return { status: 'scheduled', id: taskId };
183
+
136
184
  } catch (e) {
137
- // [FIX] ACTUALLY LOG THE ERROR HERE
138
- this.logger.error(`[CloudTasks] FAILED to schedule ${taskId}: ${e.message}`);
185
+ this.logger.error(`[CloudTasks] Schedule failed for ${computation}@${targetDate}: ${e.message}`);
139
186
  return { status: 'error', error: e.message, id: taskId };
140
187
  }
141
188
  }
142
189
 
190
+ /**
191
+ * List and clean stale tasks (Scheduler GC support).
192
+ * Removes tasks with outdated config hashes.
193
+ * @param {Map<string, string>} activeHashes - Map<computationName, currentHash>
194
+ * @returns {Promise<Object>} { deleted: number, activeKeys: Set<string> }
195
+ */
143
196
  async listAndClean(activeHashes) {
144
197
  const queuePath = this.getQueuePath();
145
198
  const tasksToDelete = [];
146
199
  const activeKeys = new Set();
200
+
147
201
  try {
148
202
  for await (const task of this.listTasks(queuePath)) {
203
+ // Parse task name to extract computation and hash
204
+ // Expected format: projects/.../tasks/root-{comp}-{date}-{hash}
149
205
  const taskNameParts = task.name.split('/');
150
206
  const taskId = taskNameParts[taskNameParts.length - 1];
207
+
208
+ // Extract hash from task ID
151
209
  const match = taskId.match(/root-(.+?)-(\d{4}-\d{2}-\d{2})-([a-f0-9]+)/);
152
- if (!match) { activeKeys.add(taskId); continue; }
210
+ if (!match) {
211
+ // Unknown format, skip
212
+ activeKeys.add(taskId);
213
+ continue;
214
+ }
215
+
153
216
  const [, compNormalized, date, hash] = match;
217
+
218
+ // Find matching computation in activeHashes
154
219
  let isStale = true;
155
220
  for (const [compName, currentHash] of activeHashes.entries()) {
156
221
  const normalizedComp = compName.toLowerCase().replace(/[^a-z0-9]/g, '');
157
222
  if (normalizedComp === compNormalized) {
158
- if (currentHash.startsWith(hash)) { isStale = false; activeKeys.add(taskId); }
223
+ if (currentHash.startsWith(hash)) {
224
+ // Task is current
225
+ isStale = false;
226
+ activeKeys.add(taskId);
227
+ }
159
228
  break;
160
229
  }
161
230
  }
162
- if (isStale) tasksToDelete.push(task.name);
231
+
232
+ if (isStale) {
233
+ tasksToDelete.push(task.name);
234
+ }
163
235
  }
236
+
237
+ // Bulk delete stale tasks
164
238
  let deleted = 0;
165
239
  if (tasksToDelete.length > 0) {
240
+ this.logger.log(`[CloudTasks] Cleaning ${tasksToDelete.length} stale tasks...`);
166
241
  const result = await this.bulkDelete(tasksToDelete);
167
242
  deleted = result.success;
168
243
  }
244
+
169
245
  return { deleted, activeKeys };
246
+
170
247
  } catch (e) {
171
248
  this.logger.error(`[CloudTasks] listAndClean failed: ${e.message}`);
172
249
  return { deleted: 0, activeKeys: new Set() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.821",
3
+ "version": "1.0.823",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [