add support heartbeat to couchdb-changes-stream
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
*.log
|
||||
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@hereconnect/message-delivery-method-web-push",
|
||||
"name": "@hereconnect/service-message-delivery-method-web-push",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/server.ts",
|
||||
@@ -24,16 +24,25 @@
|
||||
},
|
||||
"contributors": [],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"generate-keys": "node ./src/generate-keys.js",
|
||||
"test-send-notification": "node ./src/test-send-notification.js",
|
||||
"test-send-notification-cancel": "node ./src/test-send-notification-cancel.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hereconnect/couchdb-changes-stream": "workspace:^",
|
||||
"@hereconnect/types": "^1.0.0",
|
||||
"couchdb-changes-stream": "^1.0.1",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
"pouchdb-find": "^9.0.0",
|
||||
"typescript": "~5.6.3",
|
||||
"web-push": "^3.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/pouchdb-adapter-http": "^6.1.6",
|
||||
"@types/pouchdb-core": "^7.0.15",
|
||||
"@types/pouchdb-find": "^7.3.3",
|
||||
"@types/web-push": "^3.6.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import http from "http";
|
||||
|
||||
/**
|
||||
* Запускает HTTP-сервер для проверки здоровья.
|
||||
* @param controller - AbortController для управления состоянием здоровья.
|
||||
* @param defaultPort - Порт для сервера по умолчанию (если не указан в ENV HEALTH_PORT).
|
||||
* @param defaultHostName - Имя хоста для сервера по умолчанию (если не указан в ENV HEALTH_HOST).
|
||||
*/
|
||||
export function startHealthServer(
|
||||
controller: AbortController,
|
||||
defaultPort: number = 8000,
|
||||
defaultHostName: string = "127.0.0.1",
|
||||
): void {
|
||||
const port = parseInt(process.env.HEALTH_PORT || `${defaultPort}`, 10);
|
||||
const hostname = process.env.HEALTH_HOST || defaultHostName;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url === "/health") {
|
||||
const signal = controller.signal;
|
||||
|
||||
// Если сигнал отмены активирован, сервер помечается как "unhealthy"
|
||||
if (signal.aborted) {
|
||||
res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: "unhealthy",
|
||||
reason: "Service aborted",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Возвращаем стандартный статус "ok", если всё нормально
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: "ok",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||
res.end("Not Found");
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
console.log(
|
||||
`Healthcheck server running on http://${hostname}:${port}/health`,
|
||||
);
|
||||
});
|
||||
|
||||
// Закрываем сервер при активации AbortController
|
||||
controller.signal.addEventListener("abort", () => {
|
||||
console.log("Healthcheck server shutting down due to abort signal.");
|
||||
server.close();
|
||||
});
|
||||
}
|
||||
@@ -1,21 +1,27 @@
|
||||
import webpush from "web-push";
|
||||
|
||||
import { CouchDBChangesStream } from "couchdb-changes-stream";
|
||||
import { CouchDBChangesStream } from "@hereconnect/couchdb-changes-stream";
|
||||
import { qrDb, messagesDbUrl, webPushSubscriptionsDb } from "./dbs";
|
||||
import { setupVapidDetails } from "./setupVapidDetails";
|
||||
import type { MessageDocument } from "@hereconnect/types";
|
||||
import { ServiceSeqTracker } from "./serviceSeqTracker";
|
||||
import { startHealthServer } from "./health-server";
|
||||
|
||||
const serviceSeqTracker = new ServiceSeqTracker();
|
||||
const abortController = new AbortController();
|
||||
|
||||
startHealthServer(abortController);
|
||||
|
||||
const start = async () => {
|
||||
await setupVapidDetails();
|
||||
const since = await serviceSeqTracker.getSince();
|
||||
const serviceSeqTracker = await ServiceSeqTracker.Init(
|
||||
"_local/service:message-delivery-method-web-push",
|
||||
);
|
||||
|
||||
const changesStream = new CouchDBChangesStream<MessageDocument>(
|
||||
messagesDbUrl,
|
||||
{
|
||||
since,
|
||||
abortController,
|
||||
since: serviceSeqTracker.lastSeq,
|
||||
feed: "continuous",
|
||||
include_docs: true,
|
||||
heartbeat: 1000,
|
||||
@@ -28,12 +34,12 @@ const start = async () => {
|
||||
|
||||
// Gracefully handle SIGINT
|
||||
process.on("SIGINT", async () => {
|
||||
changesStream.stop();
|
||||
changesStream?.stop();
|
||||
process.stdout.write("\n");
|
||||
process.exit(2);
|
||||
});
|
||||
|
||||
console.info("Service started from sequence", since);
|
||||
console.info("Service started from sequence", serviceSeqTracker.lastSeq);
|
||||
|
||||
for await (const change of changesStream) {
|
||||
const { seq, doc: msgDoc } = change;
|
||||
@@ -68,7 +74,7 @@ const start = async () => {
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
const subscription = row.doc.subscription;
|
||||
const subscription = row.doc!.subscription;
|
||||
const topic = `chat-${msgDoc.qr_code_uri}-${msgDoc.chat}`;
|
||||
const tag = `${topic}-${(new Date(msgDoc.created_at).getTime() - new Date(qrDoc.created_at).getTime()).toString(36)}`;
|
||||
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
import { serviceDb, type ServiceDoc } from "./dbs";
|
||||
|
||||
const SERVICE_DOC_ID = "_local/service:message-delivery-method-web-push";
|
||||
|
||||
export class ServiceSeqTracker {
|
||||
private serviceDoc: ServiceDoc;
|
||||
|
||||
async getSince() {
|
||||
this.serviceDoc = await serviceDb.get(SERVICE_DOC_ID).catch((error) => {
|
||||
/**
|
||||
* @param _id
|
||||
* @constructor
|
||||
*/
|
||||
static async Init(_id: string) {
|
||||
const serviceDoc = await serviceDb.get(_id).catch((error) => {
|
||||
if (error.name === "not_found") {
|
||||
return {
|
||||
_id: SERVICE_DOC_ID,
|
||||
_id,
|
||||
lastSeq: "0",
|
||||
};
|
||||
}
|
||||
throw error; // Re-throw other errors
|
||||
});
|
||||
return new ServiceSeqTracker(serviceDoc);
|
||||
}
|
||||
|
||||
// this.serviceDoc.lastSeq =
|
||||
// "126-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M50SUXKMCebGlpnmxpiq4Yh_Y8FiDJ0ACk_kNNsQKbYpSakphkYYKuJwsAb2YrPw";
|
||||
private constructor(private serviceDoc: ServiceDoc) {}
|
||||
|
||||
get lastSeq() {
|
||||
return this.serviceDoc.lastSeq;
|
||||
}
|
||||
|
||||
checkNextSeq(seq: number | string) {
|
||||
if (!this.serviceDoc) {
|
||||
}
|
||||
const next = parseInt(seq as string, 10);
|
||||
const current = parseInt(this.serviceDoc.lastSeq as string, 10);
|
||||
if (next < current) {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { serverSettingsDb } from "./dbs";
|
||||
import { ServerSettingsDocument } from "@hereconnect/types";
|
||||
|
||||
function isType<N extends ServerSettingsDocument["name"]>(
|
||||
doc: ServerSettingsDocument,
|
||||
doc: ServerSettingsDocument | null | undefined,
|
||||
name: N,
|
||||
): doc is ServerSettingsDocument & { name: N } {
|
||||
return doc.name === name;
|
||||
return !!(doc && doc.name === name);
|
||||
}
|
||||
|
||||
export const setupVapidDetails = async () => {
|
||||
@@ -53,9 +53,10 @@ export const setupVapidDetails = async () => {
|
||||
);
|
||||
|
||||
changes.on("change", (change) => {
|
||||
if (!("doc" in change)) {
|
||||
if (!change.doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isType(change.doc, "message_delivery_method_web_push_vapid")) {
|
||||
console.log("update vapid details");
|
||||
webpush.setVapidDetails(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user