openvibers/openvibe.events Commits

Move Events' shutdown onto openvibe-sdk/service gracefulStop (#2)

OP OpenVibers committed 2 Oct 2026 · d0ac01a05f · 4 files, +67 −12

checks run by GitHub CI (test, test-pg) · CI ✓ 3/3 · review SHIP by DeepSeek harness

https://github.com/OpenVibers/OpenVibe.Events/pull/2

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex 4349d51..adacfc1 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -13,7 +13,7 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
},
@@@ -1118,9 +1118,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.25.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "integrity": "sha512-c6K1riU7xT60qEd0YFn4QDPjWXTglo0LcE4cU8TSrmRTJL7TPAT7/iqQjS2UACt2zmQ1soNM8gfIZf7v9sVdZg==",
++ "version": "0.26.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
++ "integrity": "sha512-+RK89ZG4WAKIRh89yr3ieIvnyJVNSOI+dsbBqGNcr9UHsJStdxtM75zSDq8cUNpBPQ1X6ElatZrjU9S3s0aEkg==",
"license": "MIT",
"engines": {
"node": ">=20"
ddiff --git a/package.json b/package.json
iindex 3ef6a44..5ab3d2e 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.81.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
},
ddiff --git a/server/index.js b/server/index.js
iindex 606ff32..fc27413 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -18,6 +18,7 @@ const { createRealtime } = require('./realtime');
const { createApp } = require('./app');
const { createMetrics } = require('./metrics');
const { createGuardedPost } = require('./egress');
++const { gracefulStop } = require('openvibe-sdk/service');
async function start({
config, db: givenDb = null, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, deliveryFetch = fetchImpl,
@@@ -128,13 +129,10 @@ async function start({
if (require.main === module) {
require('dotenv').config();
start().then((handles) => {
-- const shutdown = (sig) => {
-- console.log(`[events] ${sig}: shutting down`);
-- handles.close().then(() => process.exit(0), () => process.exit(1));
-- setTimeout(() => process.exit(1), 10000).unref();
-- };
-- process.on('SIGTERM', () => shutdown('SIGTERM'));
-- process.on('SIGINT', () => shutdown('SIGINT'));
++ // SIGTERM/SIGINT (openvibe-sdk/service, docs/service.md's handles family): requests in flight get 8 s,
++ // then handles.close() (the timers stopped, the carriers/realtime/worker stopped, the server closed, the
++ // database closed; a rejection exits 1); past 10 s the process exits 1.
++ gracefulStop({ name: 'events', server: handles.server, handles, drainMs: 8000, deadlineMs: 10000 });
}).catch((err) => {
console.error(`[events] failed to start: ${err.stack || err}`);
process.exit(1);
ddiff --git a/test/service-kit.test.js b/test/service-kit.test.js
nnew file mode 100644
iindex 0000000..fc1c976
---- /dev/null
++++ b/test/service-kit.test.js
@@@ -0,0 +1,57 @@
++'use strict';
++/**
++ * openvibe-sdk/service in Events (plan T1, the handles family): the SIGTERM/SIGINT shutdown is the kit's
++ * gracefulStop, not a hand-written handler. It drains the server for 8 s, then closes the handles (the
++ * timers, carriers, realtime, worker, server, valkey and database; a rejection exits 1); past 10 s the
++ * process exits 1. The static half reads server/index.js; the behavioural half drives gracefulStop with
++ * the same options Events passes, exits stubbed.
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const http = require('http');
++const { gracefulStop } = require('openvibe-sdk/service');
++const { suite } = require('./helpers');
++
++const t = suite('service-kit');
++const quiet = { log() {}, warn() {}, error() {} };
++const source = fs.readFileSync(path.join(__dirname, '../server/index.js'), 'utf8');
++
++t("server/index.js imports gracefulStop from openvibe-sdk/service and owns no signal handler", () => {
++ assert.match(source, /const \{ gracefulStop \} = require\('openvibe-sdk\/service'\)/);
++ assert.doesNotMatch(source, /process\.on\('SIGTERM'/);
++ assert.doesNotMatch(source, /process\.on\('SIGINT'/);
++ assert.doesNotMatch(source, /setTimeout\(\(\) => process\.exit\(1\), 10000\)/, 'the old 10 s exit-1 timer is gone');
++});
++
++t("the one stop names events, passes the handles and uses drainMs 8000 / deadlineMs 10000", () => {
++ const call = source.slice(source.indexOf('gracefulStop('), source.indexOf('gracefulStop(') + 200);
++ assert.match(call, /name: 'events'/);
++ assert.match(call, /\bhandles\b/);
++ assert.match(call, /drainMs: 8000/);
++ assert.match(call, /deadlineMs: 10000/);
++ assert.doesNotMatch(call, /deadlineExitCode/, 'the default exit 1 past the deadline is kept');
++ assert.match(source, /server\.keepAliveTimeout = 65000/, 'the keep-alive timeout is kept');
++});
++
++t('the stop drains the server, then closes the handles; a failing close exits 1; a second call is a no-op', async () => {
++ for (const fails of [false, true]) {
++ const server = http.createServer((_req, res) => res.end('ok'));
++ await new Promise((r) => server.listen(0, '127.0.0.1', r));
++ const order = [];
++ const handles = { close: async () => { order.push(server.listening ? 'close while listening' : 'close'); if (fails) throw new Error('db'); } };
++ let exits = 0;
++ let exited = null;
++ const kit = gracefulStop({ name: 'events', server, handles, drainMs: 8000, deadlineMs: 10000, signals: false, exit: (c) => { exited = c; exits++; }, log: quiet });
++ const [code, again] = await Promise.all([kit.stop('SIGTERM'), kit.stop('SIGINT')]);
++ assert.deepStrictEqual(order, ['close'], 'the handles close after the server stopped listening');
++ assert.strictEqual(code, fails ? 1 : 0);
++ assert.strictEqual(again, code, 'a second signal changes nothing');
++ assert.strictEqual(exited, code);
++ assert.strictEqual(exits, 1, 'exit is called once');
++ assert.strictEqual(kit.stopping(), true);
++ assert.strictEqual(server.listening, false);
++ }
++});
++
++t.run();