firebase-functions 6.1.1 → 6.1.2
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.
|
@@ -90,8 +90,21 @@ export interface CallableRequest<T = any> {
|
|
|
90
90
|
* to allow writing partial, streaming responses back to the client.
|
|
91
91
|
*/
|
|
92
92
|
export interface CallableProxyResponse {
|
|
93
|
+
/**
|
|
94
|
+
* Writes a chunk of the response body to the client. This method can be called
|
|
95
|
+
* multiple times to stream data progressively.
|
|
96
|
+
*/
|
|
93
97
|
write: express.Response["write"];
|
|
98
|
+
/**
|
|
99
|
+
* Indicates whether the client has requested and can handle streaming responses.
|
|
100
|
+
* This should be checked before attempting to stream data to avoid compatibility issues.
|
|
101
|
+
*/
|
|
94
102
|
acceptsStreaming: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* An AbortSignal that is triggered when the client disconnects or the
|
|
105
|
+
* request is terminated prematurely.
|
|
106
|
+
*/
|
|
107
|
+
signal: AbortSignal;
|
|
95
108
|
}
|
|
96
109
|
/**
|
|
97
110
|
* The set of Firebase Functions status codes. The codes are the same at the
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
// SOFTWARE.
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.onCallHandler = exports.checkAuthToken = exports.unsafeDecodeAppCheckToken = exports.unsafeDecodeIdToken = exports.unsafeDecodeToken = exports.decode = exports.encode = exports.isValidRequest = exports.HttpsError = exports.ORIGINAL_AUTH_HEADER = exports.CALLABLE_AUTH_HEADER = void 0;
|
|
24
|
+
exports.onCallHandler = exports.checkAuthToken = exports.unsafeDecodeAppCheckToken = exports.unsafeDecodeIdToken = exports.unsafeDecodeToken = exports.decode = exports.encode = exports.isValidRequest = exports.HttpsError = exports.DEFAULT_HEARTBEAT_SECONDS = exports.ORIGINAL_AUTH_HEADER = exports.CALLABLE_AUTH_HEADER = void 0;
|
|
25
25
|
const cors = require("cors");
|
|
26
26
|
const logger = require("../../logger");
|
|
27
27
|
// TODO(inlined): Decide whether we want to un-version apps or whether we want a
|
|
@@ -35,6 +35,8 @@ const JWT_REGEX = /^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$/;
|
|
|
35
35
|
exports.CALLABLE_AUTH_HEADER = "x-callable-context-auth";
|
|
36
36
|
/** @internal */
|
|
37
37
|
exports.ORIGINAL_AUTH_HEADER = "x-original-auth";
|
|
38
|
+
/** @internal */
|
|
39
|
+
exports.DEFAULT_HEARTBEAT_SECONDS = 30;
|
|
38
40
|
/**
|
|
39
41
|
* Standard error codes and HTTP statuses for different ways a request can fail,
|
|
40
42
|
* as defined by:
|
|
@@ -403,6 +405,30 @@ function encodeSSE(data) {
|
|
|
403
405
|
/** @internal */
|
|
404
406
|
function wrapOnCallHandler(options, handler, version) {
|
|
405
407
|
return async (req, res) => {
|
|
408
|
+
const abortController = new AbortController();
|
|
409
|
+
let heartbeatInterval = null;
|
|
410
|
+
const heartbeatSeconds = options.heartbeatSeconds === undefined ? exports.DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds;
|
|
411
|
+
const clearScheduledHeartbeat = () => {
|
|
412
|
+
if (heartbeatInterval) {
|
|
413
|
+
clearTimeout(heartbeatInterval);
|
|
414
|
+
heartbeatInterval = null;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const scheduleHeartbeat = () => {
|
|
418
|
+
clearScheduledHeartbeat();
|
|
419
|
+
if (!abortController.signal.aborted) {
|
|
420
|
+
heartbeatInterval = setTimeout(() => {
|
|
421
|
+
if (!abortController.signal.aborted) {
|
|
422
|
+
res.write(": ping\n");
|
|
423
|
+
scheduleHeartbeat();
|
|
424
|
+
}
|
|
425
|
+
}, heartbeatSeconds * 1000);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
res.on("close", () => {
|
|
429
|
+
clearScheduledHeartbeat();
|
|
430
|
+
abortController.abort();
|
|
431
|
+
});
|
|
406
432
|
try {
|
|
407
433
|
if (!isValidRequest(req)) {
|
|
408
434
|
logger.error("Invalid request, unable to process.");
|
|
@@ -470,52 +496,79 @@ function wrapOnCallHandler(options, handler, version) {
|
|
|
470
496
|
...context,
|
|
471
497
|
data,
|
|
472
498
|
};
|
|
473
|
-
// TODO: set up optional heartbeat
|
|
474
499
|
const responseProxy = {
|
|
475
500
|
write(chunk) {
|
|
476
|
-
if (acceptsStreaming) {
|
|
477
|
-
const formattedData = encodeSSE({ message: chunk });
|
|
478
|
-
return res.write(formattedData);
|
|
479
|
-
}
|
|
480
501
|
// if client doesn't accept sse-protocol, response.write() is no-op.
|
|
502
|
+
if (!acceptsStreaming) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
// if connection is already closed, response.write() is no-op.
|
|
506
|
+
if (abortController.signal.aborted) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const formattedData = encodeSSE({ message: chunk });
|
|
510
|
+
const wrote = res.write(formattedData);
|
|
511
|
+
// Reset heartbeat timer after successful write
|
|
512
|
+
if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) {
|
|
513
|
+
scheduleHeartbeat();
|
|
514
|
+
}
|
|
515
|
+
return wrote;
|
|
481
516
|
},
|
|
482
517
|
acceptsStreaming,
|
|
518
|
+
signal: abortController.signal,
|
|
483
519
|
};
|
|
484
520
|
if (acceptsStreaming) {
|
|
485
521
|
// SSE always responds with 200
|
|
486
522
|
res.status(200);
|
|
523
|
+
if (heartbeatSeconds !== null && heartbeatSeconds > 0) {
|
|
524
|
+
scheduleHeartbeat();
|
|
525
|
+
}
|
|
487
526
|
}
|
|
488
527
|
// For some reason the type system isn't picking up that the handler
|
|
489
528
|
// is a one argument function.
|
|
490
529
|
result = await handler(arg, responseProxy);
|
|
530
|
+
clearScheduledHeartbeat();
|
|
491
531
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
532
|
+
if (!abortController.signal.aborted) {
|
|
533
|
+
// Encode the result as JSON to preserve types like Dates.
|
|
534
|
+
result = encode(result);
|
|
535
|
+
// If there was some result, encode it in the body.
|
|
536
|
+
const responseBody = { result };
|
|
537
|
+
if (acceptsStreaming) {
|
|
538
|
+
res.write(encodeSSE(responseBody));
|
|
539
|
+
res.end();
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
res.status(200).send(responseBody);
|
|
543
|
+
}
|
|
499
544
|
}
|
|
500
545
|
else {
|
|
501
|
-
res.
|
|
546
|
+
res.end();
|
|
502
547
|
}
|
|
503
548
|
}
|
|
504
549
|
catch (err) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
550
|
+
if (!abortController.signal.aborted) {
|
|
551
|
+
let httpErr = err;
|
|
552
|
+
if (!(err instanceof HttpsError)) {
|
|
553
|
+
// This doesn't count as an 'explicit' error.
|
|
554
|
+
logger.error("Unhandled error", err);
|
|
555
|
+
httpErr = new HttpsError("internal", "INTERNAL");
|
|
556
|
+
}
|
|
557
|
+
const { status } = httpErr.httpErrorCode;
|
|
558
|
+
const body = { error: httpErr.toJSON() };
|
|
559
|
+
if (version === "gcfv2" && req.header("accept") === "text/event-stream") {
|
|
560
|
+
res.send(encodeSSE(body));
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
res.status(status).send(body);
|
|
564
|
+
}
|
|
515
565
|
}
|
|
516
566
|
else {
|
|
517
|
-
res.
|
|
567
|
+
res.end();
|
|
518
568
|
}
|
|
519
569
|
}
|
|
570
|
+
finally {
|
|
571
|
+
clearScheduledHeartbeat();
|
|
572
|
+
}
|
|
520
573
|
};
|
|
521
574
|
}
|
|
@@ -132,6 +132,13 @@ export interface CallableOptions extends HttpsOptions {
|
|
|
132
132
|
* further decisions, such as requiring additional security checks or rejecting the request.
|
|
133
133
|
*/
|
|
134
134
|
consumeAppCheckToken?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Time in seconds between sending heartbeat messages to keep the connection
|
|
137
|
+
* alive. Set to `null` to disable heartbeats.
|
|
138
|
+
*
|
|
139
|
+
* Defaults to 30 seconds.
|
|
140
|
+
*/
|
|
141
|
+
heartbeatSeconds?: number | null;
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
144
|
* Handles HTTPS requests.
|
|
@@ -136,6 +136,7 @@ function onCall(optsOrHandler, handler) {
|
|
|
136
136
|
cors: { origin, methods: "POST" },
|
|
137
137
|
enforceAppCheck: (_a = opts.enforceAppCheck) !== null && _a !== void 0 ? _a : options.getGlobalOptions().enforceAppCheck,
|
|
138
138
|
consumeAppCheckToken: opts.consumeAppCheckToken,
|
|
139
|
+
heartbeatSeconds: opts.heartbeatSeconds,
|
|
139
140
|
}, fixedLen, "gcfv2");
|
|
140
141
|
func = (0, trace_1.wrapTraceContext)((0, onInit_1.withInit)(func));
|
|
141
142
|
Object.defineProperty(func, "__trigger", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firebase-functions",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2",
|
|
4
4
|
"description": "Firebase SDK for Cloud Functions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"firebase",
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
"@types/nock": "^10.0.3",
|
|
288
288
|
"@types/node": "^14.18.24",
|
|
289
289
|
"@types/node-fetch": "^3.0.3",
|
|
290
|
-
"@types/sinon": "^
|
|
290
|
+
"@types/sinon": "^9.0.11",
|
|
291
291
|
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
|
292
292
|
"@typescript-eslint/parser": "^5.33.1",
|
|
293
293
|
"api-extractor-model-me": "^0.1.1",
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
"prettier": "^2.7.1",
|
|
314
314
|
"protobufjs-cli": "^1.1.1",
|
|
315
315
|
"semver": "^7.3.5",
|
|
316
|
-
"sinon": "^
|
|
316
|
+
"sinon": "^9.2.4",
|
|
317
317
|
"ts-node": "^10.4.0",
|
|
318
318
|
"typescript": "^4.3.5",
|
|
319
319
|
"yargs": "^15.3.1"
|