bulltrackers-module 1.0.14 → 1.0.16

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.
@@ -5,9 +5,13 @@
5
5
  const firestoreUtils = require('./firestore_utils');
6
6
  const pubsubUtils = require('./pubsub_utils');
7
7
  const loggingWrapper = require('./logging_wrapper');
8
+ const { IntelligentHeaderManager } = require('./intelligent_header_manager'); // <-- ADD
9
+ const { IntelligentProxyManager } = require('./intelligent_proxy_manager'); // <-- ADD
8
10
 
9
11
  module.exports = {
10
12
  firestore: firestoreUtils,
11
13
  pubsub: pubsubUtils,
12
14
  logging: loggingWrapper,
15
+ IntelligentHeaderManager, // <-- ADD
16
+ IntelligentProxyManager, // <-- ADD
13
17
  };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * @fileoverview
3
+ * Manages a pool of different browser headers. It selects headers based on their historical success rate
4
+ * to mimic real user traffic and avoid being blocked. Performance data for each header
5
+ * is stored and retrieved from Firestore.
6
+ * * This module is designed to be reusable and receives all dependencies
7
+ * (firestore, logger) and configuration via its constructor.
8
+ */
9
+
10
+ const { FieldValue } = require('@google-cloud/firestore');
11
+
12
+ class IntelligentHeaderManager {
13
+ /**
14
+ * @param {object} firestore - An initialized Firestore instance.
15
+ * @param {object} logger - A logger instance (e.g., from sharedsetup).
16
+ * @param {object} config - Configuration object.
17
+ * @param {string} config.headersCollectionName - The name of the Firestore collection for headers.
18
+ * @param {number} config.cacheDurationMs - How long to cache headers in memory (in ms).
19
+ * @param {string} config.fallbackUserAgent - A fallback User-Agent if loading fails.
20
+ */
21
+ constructor(firestore, logger, config) {
22
+ if (!firestore || !logger || !config) {
23
+ throw new Error("IntelligentHeaderManager requires firestore, logger, and config objects.");
24
+ }
25
+ if (!config.headersCollectionName || !config.cacheDurationMs || !config.fallbackUserAgent) {
26
+ throw new Error("IntelligentHeaderManager config is missing required keys (headersCollectionName, cacheDurationMs, fallbackUserAgent).");
27
+ }
28
+
29
+ this.firestore = firestore;
30
+ this.logger = logger;
31
+
32
+ // Load from config
33
+ this.collectionName = config.headersCollectionName;
34
+ this.cacheDuration = config.cacheDurationMs;
35
+ this.fallbackUserAgent = config.fallbackUserAgent;
36
+
37
+ // Internal state
38
+ this.headers = [];
39
+ this.lastFetched = null;
40
+ this.performanceUpdates = {};
41
+ }
42
+
43
+ /**
44
+ * Fetches and caches header documents from Firestore. If the cache is fresh, it does nothing.
45
+ * @private
46
+ * @returns {Promise<void>}
47
+ */
48
+ async _loadHeaders() {
49
+ const now = new Date();
50
+ if (this.lastFetched && (now - this.lastFetched < this.cacheDuration) && this.headers.length > 0) {
51
+ return; // Cache is fresh
52
+ }
53
+
54
+ try {
55
+ this.logger.log('INFO', '[HeaderManager] Refreshing header performance data from Firestore...');
56
+ const snapshot = await this.firestore.collection(this.collectionName).get();
57
+
58
+ if (snapshot.empty) {
59
+ throw new Error(`No documents found in headers collection: ${this.collectionName}`);
60
+ }
61
+
62
+ this.headers = snapshot.docs.map(doc => ({
63
+ id: doc.id,
64
+ data: doc.data().header, // Assuming header object is stored in 'header' field
65
+ performance: {
66
+ total: doc.data().totalRequests || 0,
67
+ success: doc.data().successfulRequests || 0
68
+ }
69
+ }));
70
+
71
+ if (this.headers.length === 0) {
72
+ throw new Error("Header collection was queried but returned no usable headers.");
73
+ }
74
+
75
+ this.lastFetched = new Date();
76
+ this.logger.log('INFO', `[HeaderManager] Successfully loaded ${this.headers.length} headers.`);
77
+ } catch (error) {
78
+ this.logger.log('ERROR', '[HeaderManager] Failed to load headers from Firestore. Using fallback.', {
79
+ errorMessage: error.message,
80
+ collection: this.collectionName
81
+ });
82
+ // Ensure fallback
83
+ if (this.headers.length === 0) {
84
+ this.headers = [{
85
+ id: 'fallback',
86
+ data: { 'User-Agent': this.fallbackUserAgent },
87
+ performance: { total: 1, success: 1 }
88
+ }];
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Selects the best available header based on success rate.
95
+ * Uses a weighted random selection to balance exploration and exploitation.
96
+ * @returns {Promise<{id: string, header: object}>}
97
+ */
98
+ async selectHeader() {
99
+ await this._loadHeaders();
100
+
101
+ if (!this.headers || this.headers.length === 0) {
102
+ this.logger.log('WARN', '[HeaderManager] No headers available, returning fallback.');
103
+ return { id: 'fallback', header: { 'User-Agent': this.fallbackUserAgent } };
104
+ }
105
+
106
+ // Calculate total score for weighted random selection
107
+ let totalScore = 0;
108
+ const weightedHeaders = this.headers.map(h => {
109
+ const successRate = (h.performance.total === 0) ? 0.5 : (h.performance.success / h.performance.total);
110
+ // Add-1 smoothing to avoid zero probability for new headers
111
+ const score = (successRate + 1) * 10;
112
+ totalScore += score;
113
+ return { ...h, score };
114
+ });
115
+
116
+ // Select a random value
117
+ let random = Math.random() * totalScore;
118
+
119
+ for (const h of weightedHeaders) {
120
+ if (random < h.score) {
121
+ return { id: h.id, header: h.data };
122
+ }
123
+ random -= h.score;
124
+ }
125
+
126
+ // Fallback in case of rounding errors
127
+ const fallbackHeader = this.headers[0];
128
+ return { id: fallbackHeader.id, header: fallbackHeader.data };
129
+ }
130
+
131
+ /**
132
+ * Records the performance of a header request in memory.
133
+ * @param {string} headerId - The ID of the header used.
134
+ * @param {boolean} success - Whether the request was successful.
135
+ */
136
+ updatePerformance(headerId, success) {
137
+ if (headerId === 'fallback') return; // Do not track performance of the fallback
138
+
139
+ if (!this.performanceUpdates[headerId]) {
140
+ this.performanceUpdates[headerId] = { successes: 0, failures: 0 };
141
+ }
142
+ if (success) {
143
+ this.performanceUpdates[headerId].successes++;
144
+ } else {
145
+ this.performanceUpdates[headerId].failures++;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Flushes the aggregated performance updates to Firestore.
151
+ * @returns {Promise<void>}
152
+ */
153
+ async flushPerformanceUpdates() {
154
+ const updatesToFlush = this.performanceUpdates;
155
+ this.performanceUpdates = {}; // Reset the local cache
156
+
157
+ if (Object.keys(updatesToFlush).length === 0) {
158
+ return;
159
+ }
160
+
161
+ this.logger.log('INFO', `[HeaderManager] Flushing performance updates for ${Object.keys(updatesToFlush).length} headers.`);
162
+ const batch = this.firestore.batch();
163
+
164
+ for (const headerId in updatesToFlush) {
165
+ const updates = updatesToFlush[headerId];
166
+ const docRef = this.firestore.collection(this.collectionName).doc(headerId);
167
+ batch.update(docRef, {
168
+ totalRequests: FieldValue.increment(updates.successes + updates.failures),
169
+ successfulRequests: FieldValue.increment(updates.successes),
170
+ lastUsed: FieldValue.serverTimestamp()
171
+ });
172
+ }
173
+
174
+ try {
175
+ await batch.commit();
176
+ this.logger.log('SUCCESS', '[HeaderManager] Successfully flushed header performance updates to Firestore.');
177
+ } catch (error) {
178
+ this.logger.log('ERROR', '[HeaderManager] Failed to commit header performance batch.', { error: error.message });
179
+ // Put updates back in memory to try again next time
180
+ this.performanceUpdates = updatesToFlush;
181
+ }
182
+ }
183
+ }
184
+
185
+ module.exports = { IntelligentHeaderManager };
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @fileoverview Manages a pool of proxies (AppScript URLs) using a simple locking mechanism.
3
+ * It selects an available (unlocked) proxy for each request and locks it upon failure.
4
+ * * This module is designed to be reusable and receives all dependencies
5
+ * (firestore, logger) and configuration via its constructor.
6
+ */
7
+ const { FieldValue } = require('@google-cloud/firestore');
8
+
9
+ class IntelligentProxyManager {
10
+ /**
11
+ * @param {object} firestore - An initialized Firestore instance.
12
+ * @param {object} logger - A logger instance (e.g., from sharedsetup).
13
+ * @param {object} config - Configuration object.
14
+ * @param {string[]} config.proxyUrls - An array of AppScript proxy URLs.
15
+ * @param {number} config.cacheDurationMs - How long to cache proxy status in memory (in ms).
16
+ * @param {string} config.proxiesCollectionName - Firestore collection to check for locks.
17
+ * @param {string} config.proxyPerformanceDocPath - Firestore doc path for performance tracking.
18
+ */
19
+ constructor(firestore, logger, config) {
20
+ if (!firestore || !logger || !config) {
21
+ throw new Error("IntelligentProxyManager requires firestore, logger, and config objects.");
22
+ }
23
+ if (!config.proxyUrls || !config.cacheDurationMs || !config.proxiesCollectionName || !config.proxyPerformanceDocPath) {
24
+ throw new Error("IntelligentProxyManager config is missing required keys (proxyUrls, cacheDurationMs, proxiesCollectionName, proxyPerformanceDocPath).");
25
+ }
26
+
27
+ this.firestore = firestore;
28
+ this.logger = logger;
29
+
30
+ // Load from config
31
+ this.proxyUrls = config.proxyUrls.filter(Boolean); // Filter out any empty/null URLs
32
+ this.CONFIG_CACHE_DURATION_MS = config.cacheDurationMs;
33
+ this.PROXIES_COLLECTION = config.proxiesCollectionName;
34
+ this.PERFORMANCE_DOC_PATH = config.proxyPerformanceDocPath;
35
+
36
+ // Internal state
37
+ this.proxies = {}; // Stores { owner, url, status ('unlocked', 'locked') }
38
+ this.configLastLoaded = 0;
39
+
40
+ if (this.proxyUrls.length === 0) {
41
+ this.logger.log('WARN', '[ProxyManager] No proxy URLs provided in config.');
42
+ } else {
43
+ this.logger.log('INFO', `[ProxyManager] Initialized with ${this.proxyUrls.length} proxies and Locking Mechanism.`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Loads proxy configuration and lock status from Firestore.
49
+ * Caches the configuration to reduce frequent database reads.
50
+ */
51
+ async _loadConfig() {
52
+ if (Date.now() - this.configLastLoaded < this.CONFIG_CACHE_DURATION_MS) {
53
+ return; // Cache is fresh
54
+ }
55
+ if (this.proxyUrls.length === 0) {
56
+ return; // No proxies to load
57
+ }
58
+
59
+ this.logger.log('INFO', "[ProxyManager] Refreshing proxy configuration and lock status...");
60
+ try {
61
+ const tempProxyStatus = {};
62
+
63
+ // 1. Initialize all known proxies from config
64
+ for (const url of this.proxyUrls) {
65
+ const owner = new URL(url).hostname; // Or derive a unique ID differently
66
+ tempProxyStatus[owner] = { owner, url, status: 'unlocked' }; // Default to unlocked
67
+ }
68
+
69
+ // 2. Load performance doc to get lock statuses
70
+ const doc = await this.firestore.doc(this.PERFORMANCE_DOC_PATH).get();
71
+ if (doc.exists) {
72
+ const data = doc.data();
73
+ if (data.locks) {
74
+ for (const owner in data.locks) {
75
+ if (tempProxyStatus[owner] && data.locks[owner].locked === true) {
76
+ tempProxyStatus[owner].status = 'locked';
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ this.proxies = tempProxyStatus;
83
+ this.configLastLoaded = Date.now();
84
+ this.logger.log('SUCCESS', `[ProxyManager] Refreshed ${Object.keys(this.proxies).length} proxy statuses.`);
85
+
86
+ } catch (error) {
87
+ this.logger.log('ERROR', '[ProxyManager] Failed to load proxy config from Firestore.', {
88
+ errorMessage: error.message,
89
+ path: this.PERFORMANCE_DOC_PATH
90
+ });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Selects an available (unlocked) proxy from the in-memory cache.
96
+ * @returns {Promise<{owner: string, url: string}>}
97
+ */
98
+ async _selectProxy() {
99
+ await this._loadConfig();
100
+
101
+ const unlockedProxies = Object.values(this.proxies).filter(p => p.status === 'unlocked');
102
+
103
+ if (unlockedProxies.length === 0) {
104
+ this.logger.log('ERROR', '[ProxyManager] All proxies are locked. No proxy available.');
105
+ throw new Error("All proxies are locked. Cannot make request.");
106
+ }
107
+
108
+ // Return a random unlocked proxy
109
+ const selected = unlockedProxies[Math.floor(Math.random() * unlockedProxies.length)];
110
+ return { owner: selected.owner, url: selected.url };
111
+ }
112
+
113
+ /**
114
+ * Locks a proxy by setting its status in memory and writing to Firestore.
115
+ * @param {string} owner - The owner/ID of the proxy to lock.
116
+ */
117
+ async lockProxy(owner) {
118
+ // 1. Update in-memory cache immediately
119
+ if (this.proxies[owner]) {
120
+ this.proxies[owner].status = 'locked';
121
+ }
122
+
123
+ this.logger.log('WARN', `[ProxyManager] Locking proxy: ${owner}`);
124
+
125
+ // 2. Update Firestore
126
+ try {
127
+ const docRef = this.firestore.doc(this.PERFORMANCE_DOC_PATH);
128
+ // Use dot notation to update a specific field in the 'locks' map
129
+ await docRef.set({
130
+ locks: {
131
+ [owner]: {
132
+ locked: true,
133
+ lastLocked: FieldValue.serverTimestamp()
134
+ }
135
+ }
136
+ }, { merge: true });
137
+ } catch (error) {
138
+ this.logger.log('ERROR', `[ProxyManager] Failed to write lock for ${owner} to Firestore.`, { errorMessage: error.message });
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Makes a fetch request using a selected proxy.
144
+ * @param {string} targetUrl - The URL to fetch.
145
+ * @param {object} options - Fetch options (e.g., headers).
146
+ * @returns {Promise<object>} A mock Response object.
147
+ */
148
+ async fetch(targetUrl, options = {}) {
149
+ let proxy = null;
150
+ try {
151
+ // 1. Select Proxy
152
+ proxy = await this._selectProxy();
153
+ } catch (error) {
154
+ // No proxies available
155
+ return { ok: false, status: 503, error: { message: error.message }, headers: new Headers() };
156
+ }
157
+
158
+ // 2. Make Request
159
+ const response = await this._fetchViaAppsScript(proxy.url, targetUrl, options);
160
+
161
+ // 3. Handle Proxy Failure (e.g., quota error, network error)
162
+ if (!response.ok && response.isUrlFetchError) {
163
+ // isUrlFetchError is a custom flag from _fetchViaAppsScript
164
+ await this.lockProxy(proxy.owner);
165
+ }
166
+
167
+ return response;
168
+ }
169
+
170
+ /**
171
+ * Internal function to call the Google AppScript proxy.
172
+ * @private
173
+ */
174
+ async _fetchViaAppsScript(proxyUrl, targetUrl, options) {
175
+ const payload = {
176
+ url: targetUrl,
177
+ options: options
178
+ };
179
+
180
+ try {
181
+ const response = await fetch(proxyUrl, {
182
+ method: 'POST',
183
+ headers: { 'Content-Type': 'application/json' },
184
+ body: JSON.stringify(payload)
185
+ });
186
+
187
+ if (!response.ok) {
188
+ const errorText = await response.text();
189
+ this.logger.log('WARN', `[ProxyManager] Proxy infrastructure itself failed.`, {
190
+ status: response.status,
191
+ proxy: proxyUrl,
192
+ error: errorText
193
+ });
194
+ return {
195
+ ok: false,
196
+ status: response.status,
197
+ isUrlFetchError: true, // Flag this as a proxy infrastructure error
198
+ error: { message: `Proxy infrastructure failed with status ${response.status}` },
199
+ headers: response.headers,
200
+ text: () => Promise.resolve(errorText)
201
+ };
202
+ }
203
+
204
+ const proxyResponse = await response.json();
205
+
206
+ if (proxyResponse.error) {
207
+ const errorMsg = proxyResponse.error.message || '';
208
+ // Check for Google-side quota errors
209
+ if (errorMsg.toLowerCase().includes('service invoked too many times')) {
210
+ this.logger.log('WARN', `[ProxyManager] Proxy quota error: ${proxyUrl}`, { error: proxyResponse.error });
211
+ return { ok: false, status: 500, error: proxyResponse.error, isUrlFetchError: true, headers: new Headers() };
212
+ }
213
+ // Other errors returned by the AppScript
214
+ return { ok: false, status: 500, error: proxyResponse.error, headers: new Headers(), text: () => Promise.resolve(errorMsg) };
215
+ }
216
+
217
+ // Success
218
+ return {
219
+ ok: proxyResponse.statusCode >= 200 && proxyResponse.statusCode < 300,
220
+ status: proxyResponse.statusCode,
221
+ headers: new Headers(proxyResponse.headers || {}),
222
+ json: () => Promise.resolve(JSON.parse(proxyResponse.body)),
223
+ text: () => Promise.resolve(proxyResponse.body),
224
+ };
225
+ } catch (networkError) {
226
+ this.logger.log('ERROR', `[ProxyManager] Network error calling proxy: ${proxyUrl}`, { errorMessage: networkError.message });
227
+ return {
228
+ ok: false,
229
+ status: 0, // Network errors don't have a status
230
+ isUrlFetchError: true, // Flag this as a proxy infrastructure error
231
+ error: { message: `Network error: ${networkError.message}` },
232
+ headers: new Headers()
233
+ };
234
+ }
235
+ }
236
+ }
237
+
238
+ module.exports = { IntelligentProxyManager };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileoverview Contains the core logic for dispatching tasks in batches.
3
+ */
4
+ const { logger } = require("sharedsetup")(__filename); // Assuming sharedsetup is available
5
+
6
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
7
+
8
+ /**
9
+ * Publishes tasks in batches with delays.
10
+ * @param {Array} tasks - Array of tasks to publish.
11
+ * @param {object} pubsubClient - Initialized Google Cloud PubSub client.
12
+ * @param {object} config - Configuration object.
13
+ * @param {string} config.topicName - Target Pub/Sub topic name.
14
+ * @param {number} config.batchSize - Number of tasks per batch.
15
+ * @param {number} config.batchDelayMs - Delay between batches in milliseconds.
16
+ */
17
+ async function dispatchTasksInBatches(tasks, pubsubClient, config) {
18
+ const { topicName, batchSize, batchDelayMs } = config;
19
+ const topic = pubsubClient.topic(topicName);
20
+ let totalTasksQueued = 0;
21
+
22
+ logger.log('INFO', `[Module Dispatcher] Received ${tasks.length} tasks. Creating batches...`);
23
+
24
+ for (let i = 0; i < tasks.length; i += batchSize) {
25
+ const batch = tasks.slice(i, i + batchSize);
26
+
27
+ try {
28
+ // Publish messages for the current batch
29
+ await Promise.all(batch.map(task => topic.publishMessage({ json: task })));
30
+ totalTasksQueued += batch.length;
31
+ logger.log('INFO', `[Module Dispatcher] Dispatched batch ${Math.ceil((i + 1) / batchSize)} with ${batch.length} tasks.`);
32
+
33
+ // Apply delay if it's not the last batch
34
+ if (i + batchSize < tasks.length) {
35
+ await sleep(batchDelayMs);
36
+ }
37
+ } catch (publishError) {
38
+ logger.log('ERROR', `[Module Dispatcher] Failed to publish batch ${Math.ceil((i + 1) / batchSize)}. Error: ${publishError.message}`, { errorStack: publishError.stack });
39
+ // Decide on error handling: continue to next batch or throw?
40
+ // For now, log and continue, but this might need adjustment based on requirements.
41
+ }
42
+ }
43
+
44
+ logger.log('SUCCESS', `[Module Dispatcher] Successfully dispatched ${totalTasksQueued} tasks in ${Math.ceil(tasks.length / batchSize)} batches.`);
45
+ return totalTasksQueued; // Return count for confirmation
46
+ }
47
+
48
+ module.exports = {
49
+ dispatchTasksInBatches
50
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @fileoverview Main entry point for the Dispatcher function logic within the module.
3
+ */
4
+ const { logger } = require("sharedsetup")(__filename); // Assuming sharedsetup is available
5
+ const { dispatchTasksInBatches } = require('./helpers/dispatch_helpers');
6
+
7
+ /**
8
+ * Creates the Pub/Sub triggered Cloud Function handler for the Dispatcher.
9
+ * @param {object} pubsubClient - Initialized Google Cloud PubSub client.
10
+ * @param {object} config - Configuration object loaded from the calling function's context.
11
+ * @returns {Function} The Cloud Function handler.
12
+ */
13
+ function createDispatcherHandler(pubsubClient, config) {
14
+ return async (message, context) => {
15
+ try {
16
+ // 1. Decode Message
17
+ if (!message.data) {
18
+ logger.log('WARN', '[Module Dispatcher] Received message without data.');
19
+ return; // Acknowledge and exit gracefully
20
+ }
21
+ const decodedMessage = JSON.parse(Buffer.from(message.data, 'base64').toString());
22
+ const { tasks } = decodedMessage;
23
+
24
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
25
+ logger.log('WARN', '[Module Dispatcher] Received message with no valid tasks. Nothing to do.');
26
+ return; // Acknowledge and exit
27
+ }
28
+
29
+ // 2. Validate Config (Basic check)
30
+ if (!config || !config.topicName || !config.batchSize || !config.batchDelayMs) {
31
+ logger.log('ERROR', '[Module Dispatcher] Invalid configuration provided.', { config });
32
+ throw new Error("Dispatcher module received invalid configuration.");
33
+ }
34
+
35
+ // 3. Dispatch Tasks
36
+ await dispatchTasksInBatches(tasks, pubsubClient, config);
37
+
38
+ // If we reach here, the process (including logging success/errors) is handled within dispatchTasksInBatches
39
+
40
+ } catch (error) {
41
+ // Catch errors during decoding or major setup issues
42
+ logger.log('ERROR', '[Module Dispatcher] FATAL error processing message', { errorMessage: error.message, errorStack: error.stack });
43
+ // Re-throw to signal Pub/Sub to potentially retry (depending on subscription settings)
44
+ throw error;
45
+ }
46
+ };
47
+ }
48
+
49
+ module.exports = {
50
+ createDispatcherHandler,
51
+ helpers: { dispatchTasksInBatches } // Export helpers if needed elsewhere
52
+ };
package/index.js CHANGED
@@ -9,6 +9,7 @@ const Orchestrator = require('./functions/orchestrator');
9
9
  const TaskEngine = require('./functions/task-engine');
10
10
  const ComputationSystem = require('./functions/computation-system');
11
11
  const GenericAPI = require('./functions/generic-api'); // <-- ADD THIS
12
+ const Dispatcher = require('./functions/dispatcher'); // <-- ADD THIS
12
13
 
13
14
  module.exports = {
14
15
  core,
@@ -16,4 +17,5 @@ module.exports = {
16
17
  TaskEngine,
17
18
  ComputationSystem,
18
19
  GenericAPI, // <-- AND ADD THIS
20
+ Dispatcher, // <-- AND ADD THI
19
21
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -9,7 +9,8 @@
9
9
  "functions/task-engine/",
10
10
  "functions/core/",
11
11
  "functions/computation-system/",
12
- "functions/generic-api/"
12
+ "functions/generic-api/",
13
+ "functions/dispatcher/"
13
14
  ],
14
15
  "scripts": {
15
16
  "test": "echo \"Error: no test specified\" && exit 1"