firebase-functions
Advanced tools
Comparing version
@@ -93,4 +93,17 @@ /// <reference types="node" /> | ||
export interface CallableProxyResponse { | ||
/** | ||
* Writes a chunk of the response body to the client. This method can be called | ||
* multiple times to stream data progressively. | ||
*/ | ||
write: express.Response["write"]; | ||
/** | ||
* Indicates whether the client has requested and can handle streaming responses. | ||
* This should be checked before attempting to stream data to avoid compatibility issues. | ||
*/ | ||
acceptsStreaming: boolean; | ||
/** | ||
* An AbortSignal that is triggered when the client disconnects or the | ||
* request is terminated prematurely. | ||
*/ | ||
signal: AbortSignal; | ||
} | ||
@@ -97,0 +110,0 @@ /** |
@@ -24,3 +24,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
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; | ||
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; | ||
const cors = require("cors"); | ||
@@ -39,2 +39,4 @@ const logger = require("../../logger"); | ||
exports.ORIGINAL_AUTH_HEADER = "x-original-auth"; | ||
/** @internal */ | ||
exports.DEFAULT_HEARTBEAT_SECONDS = 30; | ||
/** | ||
@@ -408,2 +410,26 @@ * Standard error codes and HTTP statuses for different ways a request can fail, | ||
return async (req, res) => { | ||
const abortController = new AbortController(); | ||
let heartbeatInterval = null; | ||
const heartbeatSeconds = options.heartbeatSeconds === undefined ? exports.DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds; | ||
const clearScheduledHeartbeat = () => { | ||
if (heartbeatInterval) { | ||
clearTimeout(heartbeatInterval); | ||
heartbeatInterval = null; | ||
} | ||
}; | ||
const scheduleHeartbeat = () => { | ||
clearScheduledHeartbeat(); | ||
if (!abortController.signal.aborted) { | ||
heartbeatInterval = setTimeout(() => { | ||
if (!abortController.signal.aborted) { | ||
res.write(": ping\n"); | ||
scheduleHeartbeat(); | ||
} | ||
}, heartbeatSeconds * 1000); | ||
} | ||
}; | ||
res.on("close", () => { | ||
clearScheduledHeartbeat(); | ||
abortController.abort(); | ||
}); | ||
try { | ||
@@ -476,12 +502,22 @@ if (!isValidRequest(req)) { | ||
}; | ||
// TODO: set up optional heartbeat | ||
const responseProxy = { | ||
write(chunk) { | ||
if (acceptsStreaming) { | ||
const formattedData = encodeSSE({ message: chunk }); | ||
return res.write(formattedData); | ||
// if client doesn't accept sse-protocol, response.write() is no-op. | ||
if (!acceptsStreaming) { | ||
return false; | ||
} | ||
// if client doesn't accept sse-protocol, response.write() is no-op. | ||
// if connection is already closed, response.write() is no-op. | ||
if (abortController.signal.aborted) { | ||
return false; | ||
} | ||
const formattedData = encodeSSE({ message: chunk }); | ||
const wrote = res.write(formattedData); | ||
// Reset heartbeat timer after successful write | ||
if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) { | ||
scheduleHeartbeat(); | ||
} | ||
return wrote; | ||
}, | ||
acceptsStreaming, | ||
signal: abortController.signal, | ||
}; | ||
@@ -491,2 +527,5 @@ if (acceptsStreaming) { | ||
res.status(200); | ||
if (heartbeatSeconds !== null && heartbeatSeconds > 0) { | ||
scheduleHeartbeat(); | ||
} | ||
} | ||
@@ -496,32 +535,46 @@ // For some reason the type system isn't picking up that the handler | ||
result = await handler(arg, responseProxy); | ||
clearScheduledHeartbeat(); | ||
} | ||
// Encode the result as JSON to preserve types like Dates. | ||
result = encode(result); | ||
// If there was some result, encode it in the body. | ||
const responseBody = { result }; | ||
if (acceptsStreaming) { | ||
res.write(encodeSSE(responseBody)); | ||
res.end(); | ||
if (!abortController.signal.aborted) { | ||
// Encode the result as JSON to preserve types like Dates. | ||
result = encode(result); | ||
// If there was some result, encode it in the body. | ||
const responseBody = { result }; | ||
if (acceptsStreaming) { | ||
res.write(encodeSSE(responseBody)); | ||
res.end(); | ||
} | ||
else { | ||
res.status(200).send(responseBody); | ||
} | ||
} | ||
else { | ||
res.status(200).send(responseBody); | ||
res.end(); | ||
} | ||
} | ||
catch (err) { | ||
let httpErr = err; | ||
if (!(err instanceof HttpsError)) { | ||
// This doesn't count as an 'explicit' error. | ||
logger.error("Unhandled error", err); | ||
httpErr = new HttpsError("internal", "INTERNAL"); | ||
if (!abortController.signal.aborted) { | ||
let httpErr = err; | ||
if (!(err instanceof HttpsError)) { | ||
// This doesn't count as an 'explicit' error. | ||
logger.error("Unhandled error", err); | ||
httpErr = new HttpsError("internal", "INTERNAL"); | ||
} | ||
const { status } = httpErr.httpErrorCode; | ||
const body = { error: httpErr.toJSON() }; | ||
if (version === "gcfv2" && req.header("accept") === "text/event-stream") { | ||
res.send(encodeSSE(body)); | ||
} | ||
else { | ||
res.status(status).send(body); | ||
} | ||
} | ||
const { status } = httpErr.httpErrorCode; | ||
const body = { error: httpErr.toJSON() }; | ||
if (version === "gcfv2" && req.header("accept") === "text/event-stream") { | ||
res.send(encodeSSE(body)); | ||
} | ||
else { | ||
res.status(status).send(body); | ||
res.end(); | ||
} | ||
} | ||
finally { | ||
clearScheduledHeartbeat(); | ||
} | ||
}; | ||
} |
@@ -135,2 +135,9 @@ import * as express from "express"; | ||
consumeAppCheckToken?: boolean; | ||
/** | ||
* Time in seconds between sending heartbeat messages to keep the connection | ||
* alive. Set to `null` to disable heartbeats. | ||
* | ||
* Defaults to 30 seconds. | ||
*/ | ||
heartbeatSeconds?: number | null; | ||
} | ||
@@ -137,0 +144,0 @@ /** |
@@ -139,2 +139,3 @@ "use strict"; | ||
consumeAppCheckToken: opts.consumeAppCheckToken, | ||
heartbeatSeconds: opts.heartbeatSeconds, | ||
}, fixedLen, "gcfv2"); | ||
@@ -141,0 +142,0 @@ func = (0, trace_1.wrapTraceContext)((0, onInit_1.withInit)(func)); |
{ | ||
"name": "firebase-functions", | ||
"version": "6.1.1", | ||
"version": "6.1.2", | ||
"description": "Firebase SDK for Cloud Functions", | ||
@@ -290,3 +290,3 @@ "keywords": [ | ||
"@types/node-fetch": "^3.0.3", | ||
"@types/sinon": "^7.0.13", | ||
"@types/sinon": "^9.0.11", | ||
"@typescript-eslint/eslint-plugin": "^5.33.1", | ||
@@ -317,3 +317,3 @@ "@typescript-eslint/parser": "^5.33.1", | ||
"semver": "^7.3.5", | ||
"sinon": "^7.3.2", | ||
"sinon": "^9.2.4", | ||
"ts-node": "^10.4.0", | ||
@@ -320,0 +320,0 @@ "typescript": "^4.3.5", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
894268
0.34%19429
0.38%