firebase-tools 13.15.4 → 13.16.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.
@@ -123,13 +123,14 @@ async function deleteConnector(name) {
123
123
  return;
124
124
  }
125
125
  exports.deleteConnector = deleteConnector;
126
- async function listConnectors(serviceName) {
126
+ async function listConnectors(serviceName, fields = []) {
127
127
  const connectors = [];
128
128
  const getNextPage = async (pageToken = "") => {
129
129
  const res = await dataconnectClient().get(`${serviceName}/connectors`, {
130
130
  queryParams: {
131
131
  pageSize: PAGE_SIZE_MAX,
132
132
  pageToken,
133
+ fields: fields.join(","),
133
134
  },
134
135
  });
135
136
  connectors.push(...(res.body.connectors || []));
@@ -27,7 +27,6 @@ const serviceusage_1 = require("../../gcp/serviceusage");
27
27
  const applyHash_1 = require("./cache/applyHash");
28
28
  const backend_1 = require("./backend");
29
29
  const functional_1 = require("../../functional");
30
- const prepare_1 = require("../extensions/prepare");
31
30
  exports.EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE";
32
31
  async function prepare(context, options, payload) {
33
32
  var _a, _b;
@@ -56,13 +55,6 @@ async function prepare(context, options, payload) {
56
55
  }
57
56
  context.codebaseDeployEvents = {};
58
57
  const wantBuilds = await loadCodebases(context.config, options, firebaseConfig, runtimeConfig, context.filters);
59
- if (Object.values(wantBuilds).some((b) => b.extensions)) {
60
- const extContext = {};
61
- const extPayload = {};
62
- await (0, prepare_1.prepareDynamicExtensions)(extContext, options, extPayload, wantBuilds);
63
- context.extensions = extContext;
64
- payload.extensions = extPayload;
65
- }
66
58
  const codebaseUsesEnvs = [];
67
59
  const wantBackends = {};
68
60
  for (const [codebase, wantBuild] of Object.entries(wantBuilds)) {
@@ -15,6 +15,7 @@ exports.DEFAULT_PORTS = {
15
15
  storage: 9199,
16
16
  eventarc: 9299,
17
17
  dataconnect: 9399,
18
+ tasks: 9499,
18
19
  };
19
20
  exports.FIND_AVAILBLE_PORT_BY_DEFAULT = {
20
21
  ui: true,
@@ -30,6 +31,7 @@ exports.FIND_AVAILBLE_PORT_BY_DEFAULT = {
30
31
  extensions: false,
31
32
  eventarc: true,
32
33
  dataconnect: true,
34
+ tasks: true,
33
35
  };
34
36
  exports.EMULATOR_DESCRIPTION = {
35
37
  ui: "Emulator UI",
@@ -45,6 +47,7 @@ exports.EMULATOR_DESCRIPTION = {
45
47
  extensions: "Extensions Emulator",
46
48
  eventarc: "Eventarc Emulator",
47
49
  dataconnect: "Data Connect Emulator",
50
+ tasks: "Cloud Tasks Emulator",
48
51
  };
49
52
  exports.DEFAULT_HOST = "localhost";
50
53
  class Constants {
@@ -70,6 +73,8 @@ class Constants {
70
73
  return "test lab";
71
74
  case this.SERVICE_EVENTARC:
72
75
  return "eventarc";
76
+ case this.SERVICE_CLOUD_TASKS:
77
+ return "tasks";
73
78
  default:
74
79
  return service;
75
80
  }
@@ -100,12 +105,14 @@ Constants.FIREBASE_STORAGE_EMULATOR_HOST = "FIREBASE_STORAGE_EMULATOR_HOST";
100
105
  Constants.CLOUD_STORAGE_EMULATOR_HOST = "STORAGE_EMULATOR_HOST";
101
106
  Constants.PUBSUB_EMULATOR_HOST = "PUBSUB_EMULATOR_HOST";
102
107
  Constants.CLOUD_EVENTARC_EMULATOR_HOST = "CLOUD_EVENTARC_EMULATOR_HOST";
108
+ Constants.CLOUD_TASKS_EMULATOR_HOST = "CLOUD_TASKS_EMULATOR_HOST";
103
109
  Constants.FIREBASE_EMULATOR_HUB = "FIREBASE_EMULATOR_HUB";
104
110
  Constants.FIREBASE_GA_SESSION = "FIREBASE_GA_SESSION";
105
111
  Constants.SERVICE_FIRESTORE = "firestore.googleapis.com";
106
112
  Constants.SERVICE_REALTIME_DATABASE = "firebaseio.com";
107
113
  Constants.SERVICE_PUBSUB = "pubsub.googleapis.com";
108
114
  Constants.SERVICE_EVENTARC = "eventarc.googleapis.com";
115
+ Constants.SERVICE_CLOUD_TASKS = "cloudtasks.googleapis.com";
109
116
  Constants.SERVICE_FIREALERTS = "firebasealerts.googleapis.com";
110
117
  Constants.SERVICE_ANALYTICS = "app-measurement.com";
111
118
  Constants.SERVICE_AUTH = "firebaseauth.googleapis.com";
@@ -44,6 +44,7 @@ const hostingEmulator_1 = require("./hostingEmulator");
44
44
  const pubsubEmulator_1 = require("./pubsubEmulator");
45
45
  const storage_1 = require("./storage");
46
46
  const fileUtils_1 = require("../dataconnect/fileUtils");
47
+ const tasksEmulator_1 = require("./tasksEmulator");
47
48
  const START_LOGGING_EMULATOR = utils.envOverride("START_LOGGING_EMULATOR", "false", (val) => val === "true");
48
49
  async function exportOnExit(options) {
49
50
  const exportOnExitDir = options.exportOnExit;
@@ -159,7 +160,7 @@ function findExportMetadata(importPath) {
159
160
  }
160
161
  }
161
162
  async function startAll(options, showUI = true, runningTestScript = false) {
162
- var _a, _b, _c, _d, _e, _f, _g;
163
+ var _a, _b, _c, _d, _e, _f, _g, _h;
163
164
  const targets = filterEmulatorTargets(options);
164
165
  options.targets = targets;
165
166
  const singleProjectModeEnabled = ((_a = options.config.src.emulators) === null || _a === void 0 ? void 0 : _a.singleProjectMode) === undefined ||
@@ -224,10 +225,12 @@ async function startAll(options, showUI = true, runningTestScript = false) {
224
225
  if (emulatableBackends.length) {
225
226
  listenConfig[types_1.Emulators.FUNCTIONS] = getListenConfig(options, types_1.Emulators.FUNCTIONS);
226
227
  listenConfig[types_1.Emulators.EVENTARC] = getListenConfig(options, types_1.Emulators.EVENTARC);
228
+ listenConfig[types_1.Emulators.TASKS] = getListenConfig(options, types_1.Emulators.TASKS);
227
229
  }
228
230
  for (const emulator of types_1.ALL_EMULATORS) {
229
231
  if (emulator === types_1.Emulators.FUNCTIONS ||
230
232
  emulator === types_1.Emulators.EVENTARC ||
233
+ emulator === types_1.Emulators.TASKS ||
231
234
  emulator === types_1.Emulators.EXTENSIONS ||
232
235
  (emulator === types_1.Emulators.UI && !showUI)) {
233
236
  continue;
@@ -330,8 +333,8 @@ async function startAll(options, showUI = true, runningTestScript = false) {
330
333
  await startEmulator(extensionEmulator);
331
334
  }
332
335
  if (emulatableBackends.length) {
333
- if (!listenForEmulator.functions || !listenForEmulator.eventarc) {
334
- listenForEmulator = await (0, portUtils_1.resolveHostAndAssignPorts)(Object.assign(Object.assign({}, listenForEmulator), { functions: (_f = listenForEmulator.functions) !== null && _f !== void 0 ? _f : getListenConfig(options, types_1.Emulators.FUNCTIONS), eventarc: (_g = listenForEmulator.eventarc) !== null && _g !== void 0 ? _g : getListenConfig(options, types_1.Emulators.EVENTARC) }));
336
+ if (!listenForEmulator.functions || !listenForEmulator.eventarc || !listenForEmulator.tasks) {
337
+ listenForEmulator = await (0, portUtils_1.resolveHostAndAssignPorts)(Object.assign(Object.assign({}, listenForEmulator), { functions: (_f = listenForEmulator.functions) !== null && _f !== void 0 ? _f : getListenConfig(options, types_1.Emulators.FUNCTIONS), eventarc: (_g = listenForEmulator.eventarc) !== null && _g !== void 0 ? _g : getListenConfig(options, types_1.Emulators.EVENTARC), tasks: (_h = listenForEmulator.eventarc) !== null && _h !== void 0 ? _h : getListenConfig(options, types_1.Emulators.TASKS) }));
335
338
  hubLogger.log("DEBUG", "late-assigned ports for functions and eventarc emulators", {
336
339
  user: listenForEmulator,
337
340
  });
@@ -368,6 +371,12 @@ async function startAll(options, showUI = true, runningTestScript = false) {
368
371
  port: eventarcAddr.port,
369
372
  });
370
373
  await startEmulator(eventarcEmulator);
374
+ const tasksAddr = legacyGetFirstAddr(types_1.Emulators.TASKS);
375
+ const tasksEmulator = new tasksEmulator_1.TasksEmulator({
376
+ host: tasksAddr.host,
377
+ port: tasksAddr.port,
378
+ });
379
+ await startEmulator(tasksEmulator);
371
380
  }
372
381
  if (listenForEmulator.firestore) {
373
382
  const firestoreLogger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.FIRESTORE);
@@ -23,9 +23,9 @@ const EMULATOR_UPDATE_DETAILS = {
23
23
  expectedChecksum: "2fd771101c0e1f7898c04c9204f2ce63",
24
24
  },
25
25
  firestore: {
26
- version: "1.19.7",
27
- expectedSize: 66438992,
28
- expectedChecksum: "aec233bea95c5cfab03881574ec16d6c",
26
+ version: "1.19.8",
27
+ expectedSize: 63634791,
28
+ expectedChecksum: "9b43a6daa590678de9b7df6d68260395",
29
29
  },
30
30
  storage: {
31
31
  version: "1.1.3",
@@ -31,6 +31,9 @@ function setEnvVarsForEmulators(env, emulators) {
31
31
  case types_1.Emulators.EVENTARC:
32
32
  env[constants_1.Constants.CLOUD_EVENTARC_EMULATOR_HOST] = `http://${host}`;
33
33
  break;
34
+ case types_1.Emulators.TASKS:
35
+ env[constants_1.Constants.CLOUD_TASKS_EMULATOR_HOST] = host;
36
+ break;
34
37
  }
35
38
  }
36
39
  }
@@ -386,6 +386,9 @@ class FunctionsEmulator {
386
386
  if (definition.httpsTrigger) {
387
387
  added = true;
388
388
  url = FunctionsEmulator.getHttpFunctionUrl(this.args.projectId, definition.name, definition.region);
389
+ if (definition.taskQueueTrigger) {
390
+ added = await this.addTaskQueueTrigger(this.args.projectId, definition.region, definition.name, url, definition.taskQueueTrigger);
391
+ }
389
392
  }
390
393
  else if (definition.eventTrigger) {
391
394
  const service = (0, functionsEmulatorShared_1.getFunctionService)(definition);
@@ -753,6 +756,22 @@ class FunctionsEmulator {
753
756
  };
754
757
  return true;
755
758
  }
759
+ async addTaskQueueTrigger(projectId, location, entryPoint, defaultUri, taskQueueTrigger) {
760
+ logger_1.logger.debug(`addTaskQueueTrigger`, JSON.stringify(taskQueueTrigger));
761
+ if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.TASKS)) {
762
+ logger_1.logger.debug(`addTaskQueueTrigger`, "TQ not running");
763
+ return Promise.resolve(false);
764
+ }
765
+ const bundle = Object.assign(Object.assign({}, taskQueueTrigger), { defaultUri });
766
+ try {
767
+ await registry_1.EmulatorRegistry.client(types_1.Emulators.TASKS).post(`/projects/${projectId}/locations/${location}/queues/${entryPoint}`, bundle);
768
+ return true;
769
+ }
770
+ catch (err) {
771
+ this.logger.log("WARN", "Error adding Task Queue function: " + err);
772
+ return false;
773
+ }
774
+ }
756
775
  getProjectId() {
757
776
  return this.args.projectId;
758
777
  }
@@ -61,6 +61,7 @@ function prepareEndpoints(endpoints) {
61
61
  }
62
62
  exports.prepareEndpoints = prepareEndpoints;
63
63
  function emulatedFunctionsFromEndpoints(endpoints) {
64
+ var _a, _b, _c, _d, _e, _f, _g;
64
65
  const regionDefinitions = [];
65
66
  for (const endpoint of endpoints) {
66
67
  if (!endpoint.region) {
@@ -131,6 +132,19 @@ function emulatedFunctionsFromEndpoints(endpoints) {
131
132
  }
132
133
  else if (backend.isTaskQueueTriggered(endpoint)) {
133
134
  def.httpsTrigger = {};
135
+ def.taskQueueTrigger = {
136
+ retryConfig: {
137
+ maxAttempts: (_a = endpoint.taskQueueTrigger.retryConfig) === null || _a === void 0 ? void 0 : _a.maxAttempts,
138
+ maxRetrySeconds: (_b = endpoint.taskQueueTrigger.retryConfig) === null || _b === void 0 ? void 0 : _b.maxRetrySeconds,
139
+ maxBackoffSeconds: (_c = endpoint.taskQueueTrigger.retryConfig) === null || _c === void 0 ? void 0 : _c.maxBackoffSeconds,
140
+ maxDoublings: (_d = endpoint.taskQueueTrigger.retryConfig) === null || _d === void 0 ? void 0 : _d.maxDoublings,
141
+ minBackoffSeconds: (_e = endpoint.taskQueueTrigger.retryConfig) === null || _e === void 0 ? void 0 : _e.minBackoffSeconds,
142
+ },
143
+ rateLimits: {
144
+ maxConcurrentDispatches: (_f = endpoint.taskQueueTrigger.rateLimits) === null || _f === void 0 ? void 0 : _f.maxConcurrentDispatches,
145
+ maxDispatchesPerSecond: (_g = endpoint.taskQueueTrigger.rateLimits) === null || _g === void 0 ? void 0 : _g.maxDispatchesPerSecond,
146
+ },
147
+ };
134
148
  }
135
149
  else {
136
150
  }
@@ -189,6 +203,9 @@ function getFunctionService(def) {
189
203
  if (def.httpsTrigger) {
190
204
  return "https";
191
205
  }
206
+ if (def.taskQueueTrigger) {
207
+ return constants_1.Constants.SERVICE_CLOUD_TASKS;
208
+ }
192
209
  return "unknown";
193
210
  }
194
211
  exports.getFunctionService = getFunctionService;
@@ -156,6 +156,7 @@ const EMULATOR_CAN_LISTEN_ON_PRIMARY_ONLY = {
156
156
  functions: true,
157
157
  logging: true,
158
158
  storage: true,
159
+ tasks: true,
159
160
  hosting: true,
160
161
  };
161
162
  const MAX_PORT = 65535;
@@ -49,6 +49,7 @@ class EmulatorRegistry {
49
49
  storage: 3.5,
50
50
  eventarc: 3.6,
51
51
  dataconnect: 3.7,
52
+ tasks: 3.8,
52
53
  hub: 4,
53
54
  logging: 5,
54
55
  };
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TaskQueue = exports.TaskStatus = exports.Queue = void 0;
4
+ const abort_controller_1 = require("abort-controller");
5
+ const emulatorLogger_1 = require("./emulatorLogger");
6
+ const types_1 = require("./types");
7
+ const node_fetch_1 = require("node-fetch");
8
+ class Node {
9
+ constructor(data) {
10
+ this.data = data;
11
+ this.next = null;
12
+ this.prev = null;
13
+ }
14
+ }
15
+ class Queue {
16
+ constructor(capacity = 10000) {
17
+ this.nodeMap = {};
18
+ this.count = 0;
19
+ this.first = null;
20
+ this.last = null;
21
+ this.capacity = capacity;
22
+ }
23
+ enqueue(id, item) {
24
+ if (this.count >= this.capacity) {
25
+ throw new Error("Queue has reached capacity");
26
+ }
27
+ const newNode = new Node(item);
28
+ if (this.nodeMap[id] !== undefined) {
29
+ throw new Error("Queue IDs must be unique");
30
+ }
31
+ this.nodeMap[id] = newNode;
32
+ if (!this.first) {
33
+ this.first = newNode;
34
+ }
35
+ if (this.last) {
36
+ this.last.next = newNode;
37
+ }
38
+ newNode.prev = this.last;
39
+ this.last = newNode;
40
+ this.count++;
41
+ }
42
+ peek() {
43
+ if (this.first) {
44
+ return this.first.data;
45
+ }
46
+ else {
47
+ throw new Error("Trying to peek into an empty queue");
48
+ }
49
+ }
50
+ dequeue() {
51
+ if (this.first) {
52
+ const currentFirst = this.first;
53
+ this.first = this.first.next;
54
+ if (this.last === currentFirst) {
55
+ this.last = null;
56
+ }
57
+ this.count--;
58
+ return currentFirst.data;
59
+ }
60
+ else {
61
+ throw new Error("Trying to dequeue from an empty queue");
62
+ }
63
+ }
64
+ remove(id) {
65
+ if (this.nodeMap[id] === undefined) {
66
+ throw new Error("Trying to remove a task that doesn't exist");
67
+ }
68
+ const toRemove = this.nodeMap[id];
69
+ if (toRemove.next === null && toRemove.prev === null) {
70
+ this.first = null;
71
+ this.last = null;
72
+ }
73
+ else if (toRemove.next === null) {
74
+ this.last = toRemove.prev;
75
+ toRemove.prev.next = null;
76
+ }
77
+ else if (toRemove.prev === null) {
78
+ this.first = toRemove.next;
79
+ toRemove.next.prev = null;
80
+ }
81
+ else {
82
+ const prev = toRemove.prev;
83
+ const next = toRemove.next;
84
+ prev.next = next;
85
+ next.prev = prev;
86
+ }
87
+ delete this.nodeMap[id];
88
+ this.count--;
89
+ }
90
+ getAll() {
91
+ const all = [];
92
+ let curr = this.first;
93
+ while (curr) {
94
+ all.push(curr.data);
95
+ curr = curr.next;
96
+ }
97
+ return all;
98
+ }
99
+ isEmpty() {
100
+ return this.first === null;
101
+ }
102
+ size() {
103
+ return this.count;
104
+ }
105
+ }
106
+ exports.Queue = Queue;
107
+ var TaskStatus;
108
+ (function (TaskStatus) {
109
+ TaskStatus[TaskStatus["NOT_STARTED"] = 0] = "NOT_STARTED";
110
+ TaskStatus[TaskStatus["RUNNING"] = 1] = "RUNNING";
111
+ TaskStatus[TaskStatus["RETRY"] = 2] = "RETRY";
112
+ TaskStatus[TaskStatus["FAILED"] = 3] = "FAILED";
113
+ TaskStatus[TaskStatus["FINISHED"] = 4] = "FINISHED";
114
+ })(TaskStatus = exports.TaskStatus || (exports.TaskStatus = {}));
115
+ class TaskQueue {
116
+ constructor(key, config) {
117
+ this.key = key;
118
+ this.config = config;
119
+ this.queue = new Queue();
120
+ this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.TASKS);
121
+ this.tokens = 0;
122
+ this.addedTimes = [];
123
+ this.completedTimes = [];
124
+ this.failedTimes = [];
125
+ this.maxTokens = Math.max(this.config.rateLimits.maxDispatchesPerSecond, 1.1);
126
+ this.lastTokenUpdate = Date.now();
127
+ this.queuedIds = new Set();
128
+ this.dispatches = new Array(this.config.rateLimits.maxConcurrentDispatches).fill(null);
129
+ this.openDispatches = Array.from(this.dispatches.keys());
130
+ }
131
+ dispatchTasks() {
132
+ while (!this.queue.isEmpty() && this.openDispatches.length > 0 && this.tokens >= 1) {
133
+ const dispatchLocation = this.openDispatches.pop();
134
+ if (dispatchLocation !== undefined) {
135
+ const dispatch = this.queue.dequeue();
136
+ dispatch.metadata.lastRunTime = null;
137
+ dispatch.metadata.currentAttempt = 1;
138
+ dispatch.metadata.status = TaskStatus.NOT_STARTED;
139
+ dispatch.metadata.startTime = Date.now();
140
+ this.dispatches[dispatchLocation] = dispatch;
141
+ this.tokens--;
142
+ }
143
+ }
144
+ }
145
+ setDispatch(dispatches) {
146
+ this.dispatches = dispatches;
147
+ const open = [];
148
+ for (let i = 0; i < this.dispatches.length; i++) {
149
+ if (dispatches[i] === null) {
150
+ open.push(i);
151
+ }
152
+ }
153
+ this.openDispatches = open;
154
+ }
155
+ getDispatch() {
156
+ return this.dispatches;
157
+ }
158
+ processDispatch() {
159
+ var _a;
160
+ for (let i = 0; i < this.dispatches.length; i++) {
161
+ if (this.dispatches[i] !== null) {
162
+ switch ((_a = this.dispatches[i]) === null || _a === void 0 ? void 0 : _a.metadata.status) {
163
+ case TaskStatus.FAILED:
164
+ this.dispatches[i] = null;
165
+ this.openDispatches.push(i);
166
+ this.completedTimes.push(Date.now());
167
+ this.failedTimes.push(Date.now());
168
+ break;
169
+ case TaskStatus.NOT_STARTED:
170
+ void this.runTask(i);
171
+ break;
172
+ case TaskStatus.RETRY:
173
+ this.handleRetry(i);
174
+ break;
175
+ case TaskStatus.FINISHED:
176
+ this.dispatches[i] = null;
177
+ this.openDispatches.push(i);
178
+ this.completedTimes.push(Date.now());
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ async runTask(dispatchIndex) {
185
+ if (this.dispatches[dispatchIndex] === null) {
186
+ throw new Error("Trying to dispatch a nonexistent task");
187
+ }
188
+ const emulatedTask = this.dispatches[dispatchIndex];
189
+ if (emulatedTask.metadata.lastRunTime !== null &&
190
+ Date.now() - emulatedTask.metadata.lastRunTime < emulatedTask.metadata.currentBackoff * 1000) {
191
+ return;
192
+ }
193
+ emulatedTask.metadata.status = TaskStatus.RUNNING;
194
+ try {
195
+ const headers = Object.assign({ "Content-Type": "application/json", "X-CloudTasks-QueueName": this.key, "X-CloudTasks-TaskName": emulatedTask.task.name.split("/").pop(), "X-CloudTasks-TaskRetryCount": `${emulatedTask.metadata.currentAttempt - 1}`, "X-CloudTasks-TaskExecutionCount": `${emulatedTask.metadata.executionCount}`, "X-CloudTasks-TaskETA": `${emulatedTask.task.scheduleTime || Date.now()}` }, emulatedTask.task.httpRequest.headers);
196
+ if (emulatedTask.metadata.previousResponse) {
197
+ headers["X-CloudTasks-TaskPreviousResponse"] = `${emulatedTask.metadata.previousResponse}`;
198
+ }
199
+ const controller = new abort_controller_1.default();
200
+ const signal = controller.signal;
201
+ const request = (0, node_fetch_1.default)(emulatedTask.task.httpRequest.url, {
202
+ method: "POST",
203
+ headers: headers,
204
+ body: JSON.stringify(emulatedTask.task.httpRequest.body),
205
+ signal: signal,
206
+ });
207
+ const dispatchDeadline = emulatedTask.task.dispatchDeadline;
208
+ const dispatchDeadlineSeconds = dispatchDeadline
209
+ ? parseInt(dispatchDeadline.substring(0, dispatchDeadline.length - 1))
210
+ : 60;
211
+ const abortId = setTimeout(() => {
212
+ controller.abort();
213
+ }, dispatchDeadlineSeconds * 1000);
214
+ const response = await request;
215
+ clearTimeout(abortId);
216
+ if (response.ok) {
217
+ emulatedTask.metadata.status = TaskStatus.FINISHED;
218
+ return;
219
+ }
220
+ else {
221
+ if (!(response.status >= 500 && response.status <= 599)) {
222
+ emulatedTask.metadata.executionCount++;
223
+ }
224
+ emulatedTask.metadata.previousResponse = response.status;
225
+ emulatedTask.metadata.status = TaskStatus.RETRY;
226
+ emulatedTask.metadata.lastRunTime = Date.now();
227
+ }
228
+ }
229
+ catch (e) {
230
+ this.logger.logLabeled("WARN", `${e}`);
231
+ emulatedTask.metadata.status = TaskStatus.RETRY;
232
+ emulatedTask.metadata.lastRunTime = Date.now();
233
+ }
234
+ }
235
+ handleRetry(dispatchIndex) {
236
+ if (this.dispatches[dispatchIndex] === null) {
237
+ throw new Error("Trying to retry a nonexistent task");
238
+ }
239
+ const { metadata } = this.dispatches[dispatchIndex];
240
+ const { retryConfig } = this.config;
241
+ if (this.shouldStopRetrying(metadata, retryConfig)) {
242
+ metadata.status = TaskStatus.FAILED;
243
+ return;
244
+ }
245
+ this.updateMetadata(metadata, retryConfig);
246
+ metadata.status = TaskStatus.NOT_STARTED;
247
+ }
248
+ shouldStopRetrying(metadata, retryOptions) {
249
+ if (metadata.currentAttempt > retryOptions.maxAttempts) {
250
+ if (retryOptions.maxRetrySeconds === null || retryOptions.maxRetrySeconds === 0) {
251
+ return true;
252
+ }
253
+ if (Date.now() - metadata.startTime > retryOptions.maxRetrySeconds * 1000) {
254
+ return true;
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ updateMetadata(metadata, retryOptions) {
260
+ const timeMultplier = Math.pow(2, Math.min(metadata.currentAttempt - 1, retryOptions.maxDoublings)) +
261
+ Math.max(0, metadata.currentAttempt - retryOptions.maxDoublings - 1) *
262
+ Math.pow(2, retryOptions.maxDoublings);
263
+ metadata.currentBackoff = Math.min(retryOptions.maxBackoffSeconds, timeMultplier * retryOptions.minBackoffSeconds);
264
+ metadata.currentAttempt++;
265
+ }
266
+ isActive() {
267
+ return !this.queue.isEmpty() || this.dispatches.some((e) => e !== null);
268
+ }
269
+ refillTokens() {
270
+ const tokensToAdd = ((Date.now() - this.lastTokenUpdate) / 1000) * this.config.rateLimits.maxDispatchesPerSecond;
271
+ this.addTokens(tokensToAdd);
272
+ this.lastTokenUpdate = Date.now();
273
+ }
274
+ addTokens(t) {
275
+ this.tokens += t;
276
+ this.tokens = Math.min(this.tokens, this.maxTokens);
277
+ }
278
+ setTokens(t) {
279
+ this.tokens = t;
280
+ }
281
+ getTokens() {
282
+ return this.tokens;
283
+ }
284
+ enqueue(task) {
285
+ if (this.queuedIds.has(task.name)) {
286
+ throw new Error(`A task has already been queued with id ${task.name}`);
287
+ }
288
+ const emulatedTask = {
289
+ task: task,
290
+ metadata: {
291
+ currentAttempt: 0,
292
+ currentBackoff: 0,
293
+ startTime: 0,
294
+ status: TaskStatus.NOT_STARTED,
295
+ lastRunTime: null,
296
+ previousResponse: null,
297
+ executionCount: 0,
298
+ },
299
+ };
300
+ emulatedTask.task.httpRequest.url =
301
+ emulatedTask.task.httpRequest.url === ""
302
+ ? this.config.defaultUri
303
+ : emulatedTask.task.httpRequest.url;
304
+ this.queue.enqueue(emulatedTask.task.name, emulatedTask);
305
+ this.queuedIds.add(task.name);
306
+ this.addedTimes.push(Date.now());
307
+ }
308
+ delete(taskId) {
309
+ this.queue.remove(taskId);
310
+ }
311
+ getDebugInfo() {
312
+ return `
313
+ Task Queue (${this.key}):
314
+ - Active: ${this.isActive().toString()}
315
+ - Tokens: ${this.tokens}
316
+ - In Queue: ${this.queue.size()}
317
+ - Dispatch: [
318
+ ${this.dispatches.map((t) => (t === null ? "empty" : t.task.name)).join(",\n")}
319
+ ]
320
+ - Open Locations: [${this.openDispatches.join(", ")}]
321
+ `;
322
+ }
323
+ getStatistics() {
324
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
325
+ const oneMinuteAgo = Date.now() - 60 * 1000;
326
+ this.addedTimes = this.addedTimes.filter((t) => t > fiveMinutesAgo);
327
+ this.failedTimes = this.failedTimes.filter((t) => t > fiveMinutesAgo);
328
+ this.completedTimes = this.completedTimes.filter((t) => t > oneMinuteAgo);
329
+ return {
330
+ numberOfTasks: this.queue.size(),
331
+ tasksAdded: this.addedTimes.length / 5,
332
+ completedLastMin: this.completedTimes.length,
333
+ failedTasks: this.failedTimes.length / 5,
334
+ runningTasks: this.dispatches.length,
335
+ maxRate: this.config.rateLimits.maxDispatchesPerSecond,
336
+ maxConcurrent: this.config.rateLimits.maxConcurrentDispatches,
337
+ };
338
+ }
339
+ }
340
+ exports.TaskQueue = TaskQueue;
341
+ TaskQueue.TASK_QUEUE_INTERVAL = 1000;
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TasksEmulator = exports.TaskQueueController = void 0;
4
+ const express = require("express");
5
+ const constants_1 = require("./constants");
6
+ const types_1 = require("./types");
7
+ const utils_1 = require("../utils");
8
+ const emulatorLogger_1 = require("./emulatorLogger");
9
+ const taskQueue_1 = require("./taskQueue");
10
+ const cors = require("cors");
11
+ const RETRY_CONFIG_DEFAULTS = {
12
+ maxAttempts: 3,
13
+ maxRetrySeconds: null,
14
+ maxBackoffSeconds: 60 * 60,
15
+ maxDoublings: 16,
16
+ minBackoffSeconds: 0.1,
17
+ };
18
+ const RATE_LIMITS_DEFAULT = {
19
+ maxConcurrentDispatches: 1000,
20
+ maxDispatchesPerSecond: 500,
21
+ };
22
+ class TaskQueueController {
23
+ constructor() {
24
+ this.queues = {};
25
+ this.tokenRefillIds = [];
26
+ this.running = false;
27
+ this.listenId = null;
28
+ }
29
+ enqueue(key, task) {
30
+ if (!this.queues[key]) {
31
+ throw new Error("Queue does not exist");
32
+ }
33
+ this.queues[key].enqueue(task);
34
+ }
35
+ delete(key, taskId) {
36
+ if (!this.queues[key]) {
37
+ throw new Error("Queue does not exist");
38
+ }
39
+ this.queues[key].delete(taskId);
40
+ }
41
+ createQueue(key, config) {
42
+ const newQueue = new taskQueue_1.TaskQueue(key, config);
43
+ const intervalID = setInterval(() => newQueue.refillTokens(), TaskQueueController.TOKEN_REFRESH_INTERVAL);
44
+ this.tokenRefillIds.push(intervalID);
45
+ this.queues[key] = newQueue;
46
+ }
47
+ listen() {
48
+ let shouldUpdate = false;
49
+ for (const [_key, queue] of Object.entries(this.queues)) {
50
+ shouldUpdate = shouldUpdate || queue.isActive();
51
+ }
52
+ if (shouldUpdate) {
53
+ this.updateQueues();
54
+ this.listenId = setTimeout(() => this.listen(), TaskQueueController.UPDATE_TIMEOUT);
55
+ }
56
+ else {
57
+ this.listenId = setTimeout(() => this.listen(), TaskQueueController.LISTEN_TIMEOUT);
58
+ }
59
+ }
60
+ updateQueues() {
61
+ for (const [_key, queue] of Object.entries(this.queues)) {
62
+ if (queue.isActive()) {
63
+ queue.dispatchTasks();
64
+ queue.processDispatch();
65
+ }
66
+ }
67
+ }
68
+ start() {
69
+ this.running = true;
70
+ this.listen();
71
+ }
72
+ stop() {
73
+ if (this.listenId) {
74
+ clearTimeout(this.listenId);
75
+ }
76
+ this.tokenRefillIds.forEach(clearInterval);
77
+ this.running = false;
78
+ }
79
+ isRunning() {
80
+ return this.running;
81
+ }
82
+ getStatistics() {
83
+ const stats = {};
84
+ for (const [key, queue] of Object.entries(this.queues)) {
85
+ stats[key] = queue.getStatistics();
86
+ }
87
+ return stats;
88
+ }
89
+ }
90
+ exports.TaskQueueController = TaskQueueController;
91
+ TaskQueueController.UPDATE_TIMEOUT = 0;
92
+ TaskQueueController.LISTEN_TIMEOUT = 1000;
93
+ TaskQueueController.TOKEN_REFRESH_INTERVAL = 1000;
94
+ class TasksEmulator {
95
+ constructor(args) {
96
+ this.args = args;
97
+ this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.TASKS);
98
+ this.controller = new TaskQueueController();
99
+ }
100
+ validateQueueId(queueId) {
101
+ if (typeof queueId !== "string") {
102
+ return false;
103
+ }
104
+ if (queueId.length > 100) {
105
+ return false;
106
+ }
107
+ const regex = /^[A-Za-z0-9-]+$/;
108
+ return regex.test(queueId);
109
+ }
110
+ createHubServer() {
111
+ const hub = express();
112
+ const createTaskQueueRoute = `/projects/:project_id/locations/:location_id/queues/:queue_name`;
113
+ const createTaskQueueHandler = (req, res) => {
114
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
115
+ const projectId = req.params.project_id;
116
+ const locationId = req.params.location_id;
117
+ const queueName = req.params.queue_name;
118
+ if (!this.validateQueueId(queueName)) {
119
+ res.status(400).send("Invalid Queue ID");
120
+ return;
121
+ }
122
+ const key = `queue:${projectId}-${locationId}-${queueName}`;
123
+ this.logger.logLabeled("SUCCESS", "tasks", `Created queue with key: ${key}`);
124
+ const body = req.body;
125
+ const taskQueueConfig = {
126
+ retryConfig: {
127
+ maxAttempts: (_b = (_a = body.retryConfig) === null || _a === void 0 ? void 0 : _a.maxAttempts) !== null && _b !== void 0 ? _b : RETRY_CONFIG_DEFAULTS.maxAttempts,
128
+ maxRetrySeconds: (_d = (_c = body.retryConfig) === null || _c === void 0 ? void 0 : _c.maxRetrySeconds) !== null && _d !== void 0 ? _d : RETRY_CONFIG_DEFAULTS.maxRetrySeconds,
129
+ maxBackoffSeconds: (_f = (_e = body.retryConfig) === null || _e === void 0 ? void 0 : _e.maxBackoffSeconds) !== null && _f !== void 0 ? _f : RETRY_CONFIG_DEFAULTS.maxBackoffSeconds,
130
+ maxDoublings: (_h = (_g = body.retryConfig) === null || _g === void 0 ? void 0 : _g.maxDoublings) !== null && _h !== void 0 ? _h : RETRY_CONFIG_DEFAULTS.maxDoublings,
131
+ minBackoffSeconds: (_k = (_j = body.retryConfig) === null || _j === void 0 ? void 0 : _j.minBackoffSeconds) !== null && _k !== void 0 ? _k : RETRY_CONFIG_DEFAULTS.minBackoffSeconds,
132
+ },
133
+ rateLimits: {
134
+ maxConcurrentDispatches: (_m = (_l = body.rateLimits) === null || _l === void 0 ? void 0 : _l.maxConcurrentDispatches) !== null && _m !== void 0 ? _m : RATE_LIMITS_DEFAULT.maxConcurrentDispatches,
135
+ maxDispatchesPerSecond: (_p = (_o = body.rateLimits) === null || _o === void 0 ? void 0 : _o.maxDispatchesPerSecond) !== null && _p !== void 0 ? _p : RATE_LIMITS_DEFAULT.maxDispatchesPerSecond,
136
+ },
137
+ timeoutSeconds: (_q = body.timeoutSeconds) !== null && _q !== void 0 ? _q : 10,
138
+ retry: (_r = body.retry) !== null && _r !== void 0 ? _r : false,
139
+ defaultUri: body.defaultUri,
140
+ };
141
+ if (taskQueueConfig.rateLimits.maxConcurrentDispatches > 5000) {
142
+ res.status(400).send("cannot set maxConcurrentDispatches to a value over 5000");
143
+ return;
144
+ }
145
+ this.controller.createQueue(key, taskQueueConfig);
146
+ this.logger.log("DEBUG", `Created task queue ${key} with configuration: ${JSON.stringify(taskQueueConfig)}`);
147
+ res.status(200).send({ taskQueueConfig });
148
+ };
149
+ const enqueueTasksRoute = `/projects/:project_id/locations/:location_id/queues/:queue_name/tasks`;
150
+ const enqueueTasksHandler = (req, res) => {
151
+ var _a;
152
+ if (!this.controller.isRunning()) {
153
+ this.controller.start();
154
+ }
155
+ const projectId = req.params.project_id;
156
+ const locationId = req.params.location_id;
157
+ const queueName = req.params.queue_name;
158
+ const queueKey = `queue:${projectId}-${locationId}-${queueName}`;
159
+ if (!this.controller.queues[queueKey]) {
160
+ this.logger.log("WARN", "Tried to queue a task into a non-existent queue");
161
+ res.status(404).send("Tried to queue a task from a non-existent queue");
162
+ return;
163
+ }
164
+ req.body.task.name =
165
+ (_a = req.body.task.name) !== null && _a !== void 0 ? _a : `/projects/${projectId}/locations/${locationId}/queues/${queueName}/tasks/${Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)}`;
166
+ req.body.task.httpRequest.body = JSON.parse(atob(req.body.task.httpRequest.body));
167
+ const task = req.body.task;
168
+ try {
169
+ this.controller.enqueue(queueKey, task);
170
+ this.logger.log("DEBUG", `Enqueueing task ${task.name} onto ${queueKey}`);
171
+ res.status(200).send({ task: task });
172
+ }
173
+ catch (e) {
174
+ res.status(409).send("A task with the same name already exists");
175
+ }
176
+ };
177
+ const deleteTasksRoute = `/projects/:project_id/locations/:location_id/queues/:queue_name/tasks/:task_id`;
178
+ const deleteTasksHandler = (req, res) => {
179
+ const projectId = req.params.project_id;
180
+ const locationId = req.params.location_id;
181
+ const queueName = req.params.queue_name;
182
+ const taskId = req.params.task_id;
183
+ const queueKey = `queue:${projectId}-${locationId}-${queueName}`;
184
+ if (!this.controller.queues[queueKey]) {
185
+ this.logger.log("WARN", "Tried to remove a task from a non-existent queue");
186
+ res.status(404).send("Tried to remove a task from a non-existent queue");
187
+ return;
188
+ }
189
+ try {
190
+ const taskName = `projects/${projectId}/locations/${locationId}/queues/${queueName}/tasks/${taskId}`;
191
+ this.logger.log("DEBUG", `removing: ${taskName}`);
192
+ this.controller.delete(queueKey, taskName);
193
+ res.status(200).send({ res: "OK" });
194
+ }
195
+ catch (e) {
196
+ this.logger.log("WARN", "Tried to remove a task that doesn't exist");
197
+ res.status(404).send("Tried to remove a task that doesn't exist");
198
+ }
199
+ };
200
+ const getStatsRoute = `/queueStats`;
201
+ const getStatsHandler = (req, res) => {
202
+ res.json(this.controller.getStatistics());
203
+ };
204
+ hub.get([getStatsRoute], cors({ origin: true }), getStatsHandler);
205
+ hub.post([createTaskQueueRoute], express.json(), createTaskQueueHandler);
206
+ hub.post([enqueueTasksRoute], express.json(), enqueueTasksHandler);
207
+ hub.delete([deleteTasksRoute], express.json(), deleteTasksHandler);
208
+ return hub;
209
+ }
210
+ async start() {
211
+ const { host, port } = this.getInfo();
212
+ const server = this.createHubServer().listen(port, host);
213
+ this.destroyServer = (0, utils_1.createDestroyer)(server);
214
+ return Promise.resolve();
215
+ }
216
+ async connect() {
217
+ return Promise.resolve();
218
+ }
219
+ async stop() {
220
+ if (this.destroyServer) {
221
+ await this.destroyServer();
222
+ }
223
+ this.controller.stop();
224
+ }
225
+ getInfo() {
226
+ const host = this.args.host || constants_1.Constants.getDefaultHost();
227
+ const port = this.args.port || constants_1.Constants.getDefaultPort(types_1.Emulators.TASKS);
228
+ return {
229
+ name: this.getName(),
230
+ host,
231
+ port,
232
+ };
233
+ }
234
+ getName() {
235
+ return types_1.Emulators.TASKS;
236
+ }
237
+ }
238
+ exports.TasksEmulator = TasksEmulator;
@@ -16,6 +16,7 @@ var Emulators;
16
16
  Emulators["EXTENSIONS"] = "extensions";
17
17
  Emulators["EVENTARC"] = "eventarc";
18
18
  Emulators["DATACONNECT"] = "dataconnect";
19
+ Emulators["TASKS"] = "tasks";
19
20
  })(Emulators = exports.Emulators || (exports.Emulators = {}));
20
21
  exports.DOWNLOADABLE_EMULATORS = [
21
22
  Emulators.FIRESTORE,
@@ -41,6 +42,7 @@ exports.ALL_SERVICE_EMULATORS = [
41
42
  Emulators.STORAGE,
42
43
  Emulators.EVENTARC,
43
44
  Emulators.DATACONNECT,
45
+ Emulators.TASKS,
44
46
  ].filter((v) => v);
45
47
  exports.EMULATORS_SUPPORTED_BY_FUNCTIONS = [
46
48
  Emulators.FIRESTORE,
@@ -48,6 +50,7 @@ exports.EMULATORS_SUPPORTED_BY_FUNCTIONS = [
48
50
  Emulators.PUBSUB,
49
51
  Emulators.STORAGE,
50
52
  Emulators.EVENTARC,
53
+ Emulators.TASKS,
51
54
  ];
52
55
  exports.EMULATORS_SUPPORTED_BY_UI = [
53
56
  Emulators.AUTH,
@@ -179,7 +179,10 @@ async function promptForService(setup, info) {
179
179
  info.schemaGql = choice.schema.source.files;
180
180
  }
181
181
  info.cloudSqlDatabase = (_d = (_c = choice.schema.primaryDatasource.postgresql) === null || _c === void 0 ? void 0 : _c.database) !== null && _d !== void 0 ? _d : "";
182
- const connectors = await (0, client_1.listConnectors)(choice.service.name);
182
+ const connectors = await (0, client_1.listConnectors)(choice.service.name, [
183
+ "connectors.name",
184
+ "connectors.source.files",
185
+ ]);
183
186
  if (connectors.length) {
184
187
  info.connectors = connectors.map((c) => {
185
188
  const id = c.name.split("/").pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.15.4",
3
+ "version": "13.16.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -500,6 +500,18 @@
500
500
  },
501
501
  "type": "object"
502
502
  },
503
+ "tasks": {
504
+ "additionalProperties": false,
505
+ "properties": {
506
+ "host": {
507
+ "type": "string"
508
+ },
509
+ "port": {
510
+ "type": "number"
511
+ }
512
+ },
513
+ "type": "object"
514
+ },
503
515
  "ui": {
504
516
  "additionalProperties": false,
505
517
  "properties": {