openvibers/openvibe.codes Commits

Move Codes' shutdown onto openvibe-sdk/service (#4)

OP OpenVibers committed 2 Oct 2026 · 8636125487 · 2 files, +86 −22

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

https://github.com/OpenVibers/OpenVibe.Codes/pull/4

Changes

ddiff --git a/server/index.js b/server/index.js
iindex 3a1732d..8a764ee 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -6,30 +6,40 @@
* client secret are set.
*/
const { createApp } = require('./app');
++const { gracefulStop } = require('openvibe-sdk/service');
--(async () => {
--const { app, ctx } = await createApp();
--const { config } = ctx;
++/**
++ * The process stop (openvibe-sdk/service, docs/service.md's 5 s family): the HTTP drain runs
++ * (defaults 4000/5000), then the outbox settles and the store closes; past the deadline the process
++ * exits 0, as the hand-rolled 5 s timer did. Exported so a test can inject `exit` and `signals: false`.
++ */
++function createLifecycle({ server, ctx, exit, signals }) {
++ return gracefulStop({
++ name: 'Codes', server, deadlineExitCode: 0, exit, signals,
++ close: [() => ctx.outbox.stop(), () => ctx.store.close()],
++ });
++}
--const server = app.listen(config.port, config.host, () => {
-- console.log(`[Codes] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.store.db.store})`);
-- console.log(`[Codes] docs from openvibe-contracts v${ctx.docs.contractsVersion} and openvibe-sdk v${ctx.docs.sdkVersion}; events relay ${ctx.outbox.enabled ? `on → ${config.events.url}` : 'off (events wait in event_outbox)'}`);
--});
--server.keepAliveTimeout = 65_000;
--ctx.outbox.start();
--// Keep the JWKS cache fresh in the background (openvibe-sdk/auth): one client per URL, the last
--// good keys through outages, exponential backoff, unknown-kid floods throttled, an unref'd timer.
--ctx.keys.client.start();
++async function start() {
++ const { app, ctx } = await createApp();
++ const { config } = ctx;
--function shutdown(signal) {
-- console.log(`[Codes] ${signal}: closing`);
-- server.close(async () => {
-- try { await ctx.outbox.stop(); } catch { /* best effort */ }
-- try { await ctx.store.close(); } catch { /* already closed */ }
-- process.exit(0);
++ const server = app.listen(config.port, config.host, () => {
++ console.log(`[Codes] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.store.db.store})`);
++ console.log(`[Codes] docs from openvibe-contracts v${ctx.docs.contractsVersion} and openvibe-sdk v${ctx.docs.sdkVersion}; events relay ${ctx.outbox.enabled ? `on → ${config.events.url}` : 'off (events wait in event_outbox)'}`);
});
-- setTimeout(() => process.exit(0), 5000).unref();
++ server.keepAliveTimeout = 65_000;
++ ctx.outbox.start();
++ // Keep the JWKS cache fresh in the background (openvibe-sdk/auth): one client per URL, the last
++ // good keys through outages, exponential backoff, unknown-kid floods throttled, an unref'd timer.
++ ctx.keys.client.start();
++
++ createLifecycle({ server, ctx });
++ return { server, ctx };
++}
++
++if (require.main === module) {
++ start().catch((err) => { console.error('[Codes] failed to start:', err); process.exit(1); });
}
--process.on('SIGTERM', () => shutdown('SIGTERM'));
--process.on('SIGINT', () => shutdown('SIGINT'));
--})().catch((err) => { console.error('[Codes] failed to start:', err); process.exit(1); });
++
++module.exports = { start, createLifecycle };
ddiff --git a/test/service-kit.test.js b/test/service-kit.test.js
nnew file mode 100644
iindex 0000000..bbb77b0
---- /dev/null
++++ b/test/service-kit.test.js
@@@ -0,0 +1,54 @@
++'use strict';
++/**
++ * openvibe-sdk/service (plan T16/J2): the entry point's graceful stop runs its close steps in order
++ * (outbox.stop, then store.close) and resolves exit 0; a second signal while stopping is a no-op.
++ */
++const assert = require('assert');
++const http = require('http');
++const { check, done } = require('./helpers/boot');
++const { createLifecycle } = require('../server/index');
++
++const listen = (server) => new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
++
++(async () => {
++ await check('the entry point stop runs outbox.stop then store.close, then exits 0', async () => {
++ const steps = [];
++ const ctx = {
++ outbox: { stop: async () => { steps.push('outbox.stop'); } },
++ store: { close: async () => { steps.push('store.close'); } },
++ };
++ const server = http.createServer((req, res) => res.end('ok'));
++ await listen(server);
++
++ const exits = [];
++ const lifecycle = createLifecycle({ server, ctx, exit: (code) => exits.push(code), signals: false });
++ const code = await lifecycle.stop('SIGTERM');
++
++ assert.strictEqual(code, 0);
++ assert.deepStrictEqual(exits, [0]);
++ assert.deepStrictEqual(steps, ['outbox.stop', 'store.close']);
++ assert.strictEqual(server.listening, false);
++ });
++
++ await check('a second signal while stopping is a no-op', async () => {
++ let stops = 0; let closes = 0; let exits = 0;
++ const ctx = {
++ outbox: { stop: async () => { stops += 1; } },
++ store: { close: async () => { closes += 1; } },
++ };
++ const server = http.createServer((req, res) => res.end('ok'));
++ await listen(server);
++
++ const lifecycle = createLifecycle({ server, ctx, exit: () => { exits += 1; }, signals: false });
++ const first = lifecycle.stop('SIGTERM');
++ const second = lifecycle.stop('SIGINT');
++
++ assert.strictEqual(first, second);
++ assert.deepStrictEqual(await Promise.all([first, second]), [0, 0]);
++ assert.strictEqual(stops, 1);
++ assert.strictEqual(closes, 1);
++ assert.strictEqual(exits, 1);
++ });
++
++ done();
++})().catch((err) => { console.error(err); process.exit(1); });