How to detect in Deno that remote has closed (aborted) the TCP/IP (HTTP) connection?
const server = Deno.listen({ port: 8080 });
for await (const conn of server) {
conn.on('abort', () => { // <-- API I expect but doesn't exist
// ...
});
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
//
}
}
While Deno does not provide an API to know when a connection was closed, the most reliable way to detect a connection closure is to attempt to write to it, which will throw an error if it's closed.
The following snippet that tries to perform a zero-length write periodically will solve your issue:
const server = Deno.listen({ port: 8080 });
for await (const conn of server) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
let interval;
const stream = new ReadableStream({
start(controller) {
interval = setInterval(() =>
// attempt to write a 0 length buffer, it will fail if
// connection is closed
controller.enqueue(new Uint8Array(0)),
500); // tune interval depending on your needs
},
async pull(controller) {
/*
const result = await someComputation();
// in case you want to return some response
controller.enqueue(result);
// cleanup
clearInterval(interval);
controller.close();
*/
},
});
requestEvent.respondWith(new Response(stream))
.catch((err) => {
clearInterval(interval);
// check for <connection closed> error
if (err.message.includes('connection closed before message completed')) {
// stop your operation
console.log('connection closed');
}
});
}
}
The error logic can also be added to ReadableStreamDefaultController cancel method:
const stream = new ReadableStream({
start(controller) {
// ..
},
async pull(controller) {
// ...
},
cancel(reason) {
clearInterval(interval);
if (reason && reason.message.includes('connection closed before message completed')) {
// stop your operation
console.log('connection closed');
}
}
});
AFAIK there's not an event-oriented API, but when the connection's ReadableStream closes, you'll know that the connection has closed. This will also be reflected in Deno's internal resource map. Consider the following self-contained example:
A TCP listener is started, and is closed after 500ms. While it is open, three connections are created and closed (once every 100ms). When each connection is established:
The current TCP entries from Deno's resource map are printed to the console.
A reader is acquired on the connection's readable stream and a read is performed. Because the connection is closed from the client without any data being written, the first read is the final read (reflected in the read result's done property being true).
The reader's lock on the stream is released. The stream is closed.
The current TCP entries from Deno's resource map are printed to the console. Note that none appear at this point.
so-74228364.ts:
import { delay } from "https://deno.land/std#0.161.0/async/delay.ts";
function getTCPConnectionResources() {
return Object.fromEntries(
Object.entries(Deno.resources()).filter(([, type]) => type === "tcpStream"),
);
}
async function startServer(options: Deno.ListenOptions, signal: AbortSignal) {
const listener = Deno.listen(options);
signal.addEventListener("abort", () => listener.close());
for await (const conn of listener) {
console.log("Resources after open:", getTCPConnectionResources());
const reader = conn.readable.getReader();
reader.read()
.then(({ done }) => console.log({ done }))
.then(() => {
reader.releaseLock();
console.log("Resources after final read:", getTCPConnectionResources());
});
}
}
const controller = new AbortController();
delay(500).then(() => controller.abort());
const options: Deno.ListenOptions = {
hostname: "localhost",
port: 8080,
};
startServer(options, controller.signal);
for (let i = 0; i < 3; i += 1) {
await delay(100);
(await Deno.connect(options)).close();
}
% deno --version
deno 1.27.0 (release, x86_64-apple-darwin)
v8 10.8.168.4
typescript 4.8.3
% deno run --allow-net=localhost so-74228364.ts
Resources after open: { "7": "tcpStream" }
{ done: true }
Resources after final read: {}
Resources after open: { "10": "tcpStream" }
{ done: true }
Resources after final read: {}
Resources after open: { "13": "tcpStream" }
{ done: true }
Resources after final read: {}
Related
I'm setting up a new web project using Deno and oak.
I've passed an AbortSignal into the listen call and I'm listening for a SIGTERM from the OS and calling abort, in case this is not built-in behaviour.
Similar to setups described here: Deno and Docker how to listen for the SIGTERM signal and close the server
Question: Upon abort, will the await listen(...) call return immediately or after all remaining requests have completed?
If not then I guess I will need to accurately count concurrent requests using Atomics and wait until that counter drops to zero before ending the process.
Rather than rely on second hand information from someone else (which might not be correct), why not just do a test and find out for yourself (or review the source code)?
Here's a reproducible example which indicates that — when using Deno#1.28.2 with Oak#11.1.0 — the server gracefully shuts down: it still responds to a pending request even after the AbortSignal is aborted:
so-74600368.ts:
import {
Application,
type Context,
} from "https://deno.land/x/oak#v11.1.0/mod.ts";
import { delay } from "https://deno.land/std#0.166.0/async/delay.ts";
async function sendRequestAndLogResponseText(): Promise<void> {
try {
const response = await fetch("http://localhost:8000/");
if (!response.ok) {
throw new Error(`Response not OK (Status code: ${response.status})`);
}
const text = await response.text();
console.log(performance.now(), text);
} catch (ex) {
console.error(ex);
}
}
async function sendSquentialRequsets(numOfRequests: number): Promise<void> {
for (let i = 0; i < numOfRequests; i += 1) {
await sendRequestAndLogResponseText();
}
}
function printStartupMessage({ hostname, port, secure }: {
hostname: string;
port: number;
secure?: boolean;
}): void {
if (!hostname || hostname === "0.0.0.0") hostname = "localhost";
const address =
new URL(`http${secure ? "s" : ""}://${hostname}:${port}/`).href;
console.log(`Listening at ${address}`);
console.log("Use ctrl+c to stop");
}
async function main() {
const log = new Map<Context, boolean>();
const controller = new AbortController();
controller.signal.addEventListener("abort", () => {
console.log(performance.now(), "Abort method invoked");
});
const app = new Application();
app.use(async (ctx) => {
log.set(ctx, false);
if (log.size > 2) {
console.log(performance.now(), "Aborting");
controller.abort(new Error("Received third request. Aborting now."));
}
// A bit of artificial delay, to ensure that no unaccounted for latency
// might cause a non-deterministic/unexpected result:
await delay(300);
ctx.response.body = `Response OK: (#${log.size})`;
log.set(ctx, true);
});
app.addEventListener("listen", (ev) => {
console.log(performance.now(), "Server starting");
printStartupMessage(ev);
});
const listenerPromise = app.listen({
hostname: "localhost",
port: 8000,
signal: controller.signal,
})
.then(() => {
console.log(performance.now(), "Server stopped");
return { type: "server", ok: true };
})
.catch((reason) => ({ type: "server", ok: false, reason }));
const requestsPromise = sendSquentialRequsets(3)
.then(() => {
console.log(performance.now(), "All responses OK");
return { type: "requests", ok: true };
})
.catch((reason) => ({ type: "requests", ok: false, reason }));
const results = await Promise.allSettled([listenerPromise, requestsPromise]);
for (const result of results) console.log(result);
const allResponsesSent = [...log.values()].every(Boolean);
console.log({ allResponsesSent });
}
if (import.meta.main) main();
% deno --version
deno 1.28.2 (release, x86_64-apple-darwin)
v8 10.9.194.1
typescript 4.8.3
% deno run --allow-net=localhost so-74600368.ts
62 Server starting
Listening at http://127.0.0.1:8000/
Use ctrl+c to stop
378 Response OK: (#1)
682 Response OK: (#2)
682 Aborting
682 Abort method invoked
990 Server stopped
992 Response OK: (#3)
992 All responses OK
{ status: "fulfilled", value: { type: "server", ok: true } }
{ status: "fulfilled", value: { type: "requests", ok: true } }
{ allResponsesSent: true }
I have a problem with typeorm, function createConnection works only in index file, if I try to run it in any other file it gets stuck waiting for connection.
export async function saveKU(data: KUData) {
console.log("Foo");
let connection = await createConnection(typeormConfig);
console.log("Received!: " + connection);
const user = connection
.getRepository(KU)
.createQueryBuilder("ku")
.where("ku.ku_number = :ku_number", { ku_number: "54645" })
.getOne();
console.log(user);
}
Message received never gets logged, but then if I run the exact same script in the main function
const main = async () => {
// quit application when all windows are closed
app.on("window-all-closed", () => {
// on macOS it is common for applications to stay open until the user explicitly quits
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
// on macOS it is common to re-create a window even after all windows have been closed
if (mainWindow === null) {
mainWindow = createMainWindow();
}
});
app.allowRendererProcessReuse = true;
// create main BrowserWindow when electron is ready
app.on("ready", () => {
mainWindow = createMainWindow();
});
let connection = await createConnection(typeormConfig);
console.log("Received!: " + connection);
const user = connection
.getRepository(KU)
.createQueryBuilder("ku")
.where("ku.ku_number = :ku_number", { ku_number: "54645" })
.getOne();
console.log(user);
};
Everything works fine, no error is showing up in both cases, I have no idea what the problem could be, the closest to my example is the following link: https://senorihl.github.io/2019/03/electron-typescript-react-typeorm/
Except that I'm using electron-webpack and creating connections in an index.tsx file doesn't work for me, but rather index.ts
I'm using node.js as a backend server for sending push notification from the Firebase Cloud Messaging service. The notifications are working fine with local server but on live server, I get this error:
Error while making request: socket hang up. Error code: ECONNRESET
Things to consider are that...
Number of users are in the thousands on live server
Firebase version is firebase-admin#6.5.1
Previously unregistered tokens are still there. But now registered tokens are being stored.
This is my code for sending notifications:
for (let c = 0; c < tokens.length; c++)
{
let notifyTo = tokens[c];
const platform = platforms[c];
let payload;
if (platform === "ios") {
payload = {
notification: {
title: "title",
subtitle :"messgae",
sound: "default",
badge: "1"
},
data: {
sendFrom: "",
notificationType: "",
flag: "true"
}
};
} else if (platform === "android") {
payload = {
data: {
title: "",
message : "",
flag: "true"
}
};
}
const registrationtoken = notifyTo;
await admin.messaging().sendToDevice(registrationtoken, payload)
.then(function (response) {
console.log("Successfully sent message:");
})
.catch(function (error) {
console.log("Error sending message: ");
});
}
Your issue is caused by your function taking too long to respond to the client (more than 60 seconds) and is caused by the following line:
await admin.messaging().sendToDevice(registrationtoken, payload)
Because you are waiting for each call of sendToDevice() individually, you are running your for-loop in synchronous sequential order, rather than asynchronously in parallel.
To avoid this, you want to make use of array mapping and Promise.all() which will allow you to build a queue of sendToDevice() requests. As in your current code, any failed messages will be silently ignored, but we will also count them.
Your current code makes use of two arrays, tokens and platforms, so in the code below I use a callback for Array.prototype.map() that takes two arguments - the current mapped value (from tokens) and it's index (your for-loop's c value). The index is then used to get the correct platform entry.
let fcmPromisesArray = tokens.map((token, idx) => {
let platform = platforms[idx];
if (platform === "ios") {
payload = {
notification: {
title: "title",
subtitle :"messgae",
sound: "default",
badge: "1"
},
data: {
sendFrom: "",
notificationType: "",
flag: "true"
}
};
} else if (platform === "android") {
payload = {
data: {
title: "",
message : "",
flag: "true"
}
};
}
return admin.messaging().sendToDevice(token, payload) // note: 'await' was changed to 'return' here
.then(function (response) {
return true; // success
})
.catch(function (error) {
console.log("Error sending message to ", token);
return false; // failed
});
});
let results = await Promise.all(fcmPromisesArray); // wait here for all sendToDevice() requests to finish or fail
let successCount = results.reduce((acc, v) => v ? acc + 1 : acc, 0); // this minified line just counts the number of successful results
console.log(`Successfully sent messages to ${successCount}/${results.length} devices.`);
After this snippet has run, don't forget to send a result back to the client using res.send(...) or similar.
I need to put some delays between each axios POST call so POST calls are in sequence -- wait one finishes before issue the next POST.
The delay code I put in seems to delay -- console log shows "Delaying" and pauses there for some seconds, but on the server side, POST calls are still concurrent.
import * as Axios from "axios";
delay(delayTime:number) {
var tNow = Date.now();
var dateDiff = 0;
do {
dateDiff = Date.now() - tNow;
} while (dateDiff < delayTime); //milliseconds
}
// the code below is in a for loop
let axiosConfig = {
url: myurl,
method: ‘POST’,
timeout: 5,
headers: {
'Authorization' : AuthStr,
'Accept' : 'application/json',
'Content-Type' : 'application/json'
},
data: objContent
}
console.log(">>>>>>>>>>>>>>>>>>> Delaying”);
delay(10000);
let request = Axios.create().request(axiosConfig).
catch((rejection:any) => {
// some code
});
In order to wait one HTTP request finishes before issue the next request, you need to queue the HTTP request (queue async operation). The steps are:
When HTTP request is needed, add it to the queue.
Check whether there are any elements in the queue. If any, pick up one and execute it.
After that HTTP request is finished, goto step 2.
Example code would look like below:
const axios = require('axios');
let queue = [];
function sendRequest(callback) {
axios.get('http://example.com')
.then(function() {
callback();
}).catch(function () {
callback();
});
}
function addRequestToQueue() {
let id = (Math.random()*100000).toFixed(0);
if (queue.length === 0) {
sendRequest(function() {
queue.splice(queue.indexOf(id), 1);
consumeQueue();
});
}
queue.push(id);
}
function consumeQueue() {
if (!queue.length) {
return;
}
let id = queue[0];
sendRequest(function() {
queue.splice(queue.indexOf(id), 1);
consumeQueue();
});
}
addRequestToQueue();
addRequestToQueue();
addRequestToQueue();
addRequestToQueue();
addRequestToQueue();
I'm using Firebase Cloud Messaging + Service worker to handle background push notifications.
When the notification (which contains some data + a URL) is clicked, I want to either:
Focus the window if it's already on the desired URL
Navigate to the URL and focus it if there is already an active tab open
Open a new window to the URL if neither of the above conditions are met
Points 1 and 3 work with the below SW code.
For some reason point #2 isn't working. The client.navigate() promise is being rejected with:
Uncaught (in promise) TypeError: Cannot navigate to URL: http://localhost:4200/tasks/-KMcCHZdQ2YKCgTA4ddd
I thought it might be due to a lack of https, but from my reading it appears as though localhost is whitelisted while developing with SW.
firebase-messaging-sw.js:
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/3.5.3/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.5.3/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in the
// messagingSenderId.
firebase.initializeApp({
'messagingSenderId': 'XXXX'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(payload => {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
let notificationData = JSON.parse(payload.data.notification);
const notificationOptions = {
body: notificationData.body,
data: {
clickUrl: notificationData.clickUrl
}
};
return self.registration.showNotification(notificationData.title,
notificationOptions);
});
self.addEventListener('notificationclick', event => {
console.log('[firebase-messaging-sw.js] Notification OnClick: ', event);
// Android doesn’t close the notification when you click on it
// See: http://crbug.com/463146
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.notification.close();
let validUrls = /localhost:4200/;
let newUrl = event.notification.data.clickUrl || '';
function endsWith(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
event.waitUntil(
clients.matchAll({
includeUncontrolled: true,
type: 'window'
})
.then(windowClients => {
for (let i = 0; i < windowClients.length; i++) {
let client = windowClients[i];
if (validUrls.test(client.url) && 'focus' in client) {
if (endsWith(client.url, newUrl)) {
console.log('URL already open, focusing.');
return client.focus();
} else {
console.log('Navigate to URL and focus', client.url, newUrl);
return client.navigate(newUrl).then(client => client.focus());
}
}
}
if (clients.openWindow) {
console.log('Opening new window', newUrl);
return clients.openWindow(newUrl);
}
})
);
});
The vast majority of my SW code is taken from:
https://gist.github.com/vibgy/0c5f51a8c5756a5c408da214da5aa7b0
I'd recommend leaving out includeUncontrolled: true from your clients.matchAll().
The WindowClient that you're acting on might not have the current service worker as its active service worker. As per item 4 in the specification for WindowClient.navigate():
If the context object’s associated service worker client’s active
service worker is not the context object’s relevant global object’s
service worker, return a promise rejected with a TypeError.
If you can reproduce the issue when you're sure the client is currently controlled by the service worker, then there might be something else going on, but that's what I'd try as a first step.
This worked for me:
1- create an observable and make sure not to call the messaging API before it resolves.
2- register the service worker yourself, and check first if its already registered
3- call event.waitUntil(clients.claim()); in your service worker
private isMessagingInitialized$: Subject<void>;
constructor(private firebaseApp: firebase.app.App) {
navigator.serviceWorker.getRegistration('/').then(registration => {
if (registration) {
// optionally update your service worker to the latest firebase-messaging-sw.js
registration.update().then(() => {
firebase.messaging(this.firebaseApp).useServiceWorker(registration);
this.isMessagingInitialized$.next();
});
}
else {
navigator.serviceWorker.register('firebase-messaging-sw.js', { scope:'/'}).then(
registration => {
firebase.messaging(this.firebaseApp).useServiceWorker(registration);
this.isMessagingInitialized$.next();
}
);
}
});
this.isMessagingInitialized$.subscribe(
() => {
firebase.messaging(this.firebaseApp).usePublicVapidKey('Your public api key');
firebase.messaging(this.firebaseApp).onTokenRefresh(() => {
this.getToken().subscribe((token: string) => {
})
});
firebase.messaging(this.firebaseApp).onMessage((payload: any) => {
});
}
);
}
firebase-messaging-sw.js
self.addEventListener('notificationclick', function (event) {
event.notification.close();
switch (event.action) {
case 'close': {
break;
}
default: {
event.waitUntil(clients.claim());// this
event.waitUntil(clients.matchAll({
includeUncontrolled: true,
type: "window"
}).then(function (clientList) {
...
clientList[i].navigate('you url');
...
}
}
}
}