@proteinjs/server 3.5.3 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/src/GracefulShutdown.d.ts +56 -3
- package/dist/src/GracefulShutdown.d.ts.map +1 -1
- package/dist/src/GracefulShutdown.js +148 -7
- package/dist/src/GracefulShutdown.js.map +1 -1
- package/generated/index.ts +1 -1
- package/package.json +3 -3
- package/src/GracefulShutdown.ts +133 -5
- package/test/fixture/gracefulShutdownServer.js +21 -2
- package/test/gracefulShutdown.test.ts +68 -1
- package/test/holdSeam.test.ts +82 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [3.6.0](https://github.com/proteinjs/server/compare/@proteinjs/server@3.5.3...@proteinjs/server@3.6.0) (2026-09-10)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **server:** GracefulShutdown holds — the SIGTERM drain waits for work no connection represents (a detached chat turn), bounded by shutdown.turnDrainMs ([b534d68](https://github.com/proteinjs/server/commit/b534d68ab14f925a686cb338466ebabd4b40ef75))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## [3.5.3](https://github.com/proteinjs/server/compare/@proteinjs/server@3.5.2...@proteinjs/server@3.5.3) (2026-09-01)
|
|
7
18
|
|
|
8
19
|
|
package/dist/generated/index.js
CHANGED
|
@@ -33,7 +33,7 @@ require("react-helmet");
|
|
|
33
33
|
require("socket.io");
|
|
34
34
|
require("typescript");
|
|
35
35
|
/** Generate Source Graph */
|
|
36
|
-
var sourceGraph = "{\"options\":{\"directed\":true,\"multigraph\":false,\"compound\":false},\"nodes\":[{\"v\":\"@proteinjs/server/NodeGlobalDataStorage\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"NodeGlobalDataStorage\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/NodeGlobalDataStorage.ts\",\"qualifiedName\":\"@proteinjs/server/NodeGlobalDataStorage\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"HOOK_INITIALIZED\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"GLOBAL_DATA\",\"type\":{\"packageName\":\"@proteinjs/server\",\"name\":\"{ [id: string]: GlobalData }\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server/{ [id: string]: GlobalData }\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"environment\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"setData\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"data\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalData\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getData\",\"returnType\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalData\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"initHook\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalDataStorage\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalDataStorage\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/server-api/GlobalDataStorage\"},{\"v\":\"@proteinjs/server/NodeSessionDataStorage\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"NodeSessionDataStorage\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/NodeSessionDataStorage.ts\",\"qualifiedName\":\"@proteinjs/server/NodeSessionDataStorage\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"HOOK_INITIALIZED\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"SESSION_DATA\",\"type\":{\"packageName\":\"@proteinjs/server\",\"name\":\"{ [id: string]: SessionData }\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server/{ [id: string]: SessionData }\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"environment\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"setData\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"data\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"SessionData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/SessionData\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getData\",\"returnType\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"SessionData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/SessionData\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"clearData\",\"returnType\":{\"packageName\":\"\",\"name\":\"void\",\"filePath\":null,\"qualifiedName\":\"/void\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"initHook\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"SessionDataStorage\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/SessionDataStorage\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/server-api/SessionDataStorage\"},{\"v\":\"@proteinjs/server/ExtendedIncomingMessage\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"ExtendedIncomingMessage\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts\",\"qualifiedName\":\"@proteinjs/server/ExtendedIncomingMessage\",\"properties\":[{\"name\":\"sessionID\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":true,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"user\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":true,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[],\"typeParameters\":[],\"directParents\":[{\"packageName\":\"http\",\"name\":\"IncomingMessage\",\"filePath\":null,\"qualifiedName\":\"http/IncomingMessage\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"sourceType\":3}},{\"v\":\"http/IncomingMessage\"},{\"v\":\"@proteinjs/server/ExtendedSocket\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"ExtendedSocket\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts\",\"qualifiedName\":\"@proteinjs/server/ExtendedSocket\",\"properties\":[{\"name\":\"request\",\"type\":{\"packageName\":\"@proteinjs/server\",\"name\":\"ExtendedIncomingMessage\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server/ExtendedIncomingMessage\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[],\"typeParameters\":[],\"directParents\":[{\"packageName\":\"socket.io\",\"name\":\"Socket\",\"filePath\":null,\"qualifiedName\":\"socket.io/Socket\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"sourceType\":3}},{\"v\":\"socket.io/Socket\"},{\"v\":\"@proteinjs/server/environmentCache\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"environmentCache\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/envCache.ts\",\"qualifiedName\":\"@proteinjs/server/environmentCache\",\"type\":{\"packageName\":\"\",\"name\":\"GlobalDataCache<Env>\",\"filePath\":null,\"qualifiedName\":\"/GlobalDataCache<Env>\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalDataCache\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalDataCache\",\"typeParameters\":[\"@proteinjs/server-api/Env\"],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/server-api/GlobalDataCache\"},{\"v\":\"@proteinjs/server/healthCheck\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"healthCheck\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/routes/healthCheck.ts\",\"qualifiedName\":\"@proteinjs/server/healthCheck\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/server-api/Route\"}],\"edges\":[{\"v\":\"@proteinjs/server/NodeGlobalDataStorage\",\"w\":\"@proteinjs/server-api/GlobalDataStorage\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/server/NodeSessionDataStorage\",\"w\":\"@proteinjs/server-api/SessionDataStorage\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/server/ExtendedIncomingMessage\",\"w\":\"http/IncomingMessage\",\"value\":\"extends interface\"},{\"v\":\"@proteinjs/server/ExtendedSocket\",\"w\":\"socket.io/Socket\",\"value\":\"extends interface\"},{\"v\":\"@proteinjs/server/environmentCache\",\"w\":\"@proteinjs/server-api/GlobalDataCache\",\"value\":\"has type\"},{\"v\":\"@proteinjs/server/healthCheck\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"}]}";
|
|
36
|
+
var sourceGraph = "{\"options\":{\"directed\":true,\"multigraph\":false,\"compound\":false},\"nodes\":[{\"v\":\"/() => void\"},{\"v\":\"@proteinjs/server/NodeGlobalDataStorage\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"NodeGlobalDataStorage\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/NodeGlobalDataStorage.ts\",\"qualifiedName\":\"@proteinjs/server/NodeGlobalDataStorage\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"HOOK_INITIALIZED\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"GLOBAL_DATA\",\"type\":{\"packageName\":\"@proteinjs/server\",\"name\":\"{ [id: string]: GlobalData }\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server/{ [id: string]: GlobalData }\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"environment\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"setData\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"data\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalData\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getData\",\"returnType\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalData\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"initHook\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalDataStorage\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalDataStorage\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/server-api/GlobalDataStorage\"},{\"v\":\"@proteinjs/server/NodeSessionDataStorage\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"NodeSessionDataStorage\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/NodeSessionDataStorage.ts\",\"qualifiedName\":\"@proteinjs/server/NodeSessionDataStorage\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"HOOK_INITIALIZED\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"SESSION_DATA\",\"type\":{\"packageName\":\"@proteinjs/server\",\"name\":\"{ [id: string]: SessionData }\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server/{ [id: string]: SessionData }\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"environment\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"setData\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"data\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"SessionData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/SessionData\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"getData\",\"returnType\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"SessionData\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/SessionData\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"clearData\",\"returnType\":{\"packageName\":\"\",\"name\":\"void\",\"filePath\":null,\"qualifiedName\":\"/void\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"initHook\",\"returnType\":null,\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"SessionDataStorage\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/SessionDataStorage\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/server-api/SessionDataStorage\"},{\"v\":\"@proteinjs/server/ExtendedIncomingMessage\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"ExtendedIncomingMessage\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts\",\"qualifiedName\":\"@proteinjs/server/ExtendedIncomingMessage\",\"properties\":[{\"name\":\"sessionID\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":true,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"user\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":true,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[],\"typeParameters\":[],\"directParents\":[{\"packageName\":\"http\",\"name\":\"IncomingMessage\",\"filePath\":null,\"qualifiedName\":\"http/IncomingMessage\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"sourceType\":3}},{\"v\":\"http/IncomingMessage\"},{\"v\":\"@proteinjs/server/ExtendedSocket\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"ExtendedSocket\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts\",\"qualifiedName\":\"@proteinjs/server/ExtendedSocket\",\"properties\":[{\"name\":\"request\",\"type\":{\"packageName\":\"@proteinjs/server\",\"name\":\"ExtendedIncomingMessage\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server/ExtendedIncomingMessage\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[],\"typeParameters\":[],\"directParents\":[{\"packageName\":\"socket.io\",\"name\":\"Socket\",\"filePath\":null,\"qualifiedName\":\"socket.io/Socket\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"sourceType\":3}},{\"v\":\"socket.io/Socket\"},{\"v\":\"@proteinjs/server/environmentCache\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"environmentCache\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/envCache.ts\",\"qualifiedName\":\"@proteinjs/server/environmentCache\",\"type\":{\"packageName\":\"\",\"name\":\"GlobalDataCache<Env>\",\"filePath\":null,\"qualifiedName\":\"/GlobalDataCache<Env>\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"GlobalDataCache\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/GlobalDataCache\",\"typeParameters\":[\"@proteinjs/server-api/Env\"],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/server-api/GlobalDataCache\"},{\"v\":\"@proteinjs/server/healthCheck\",\"value\":{\"packageName\":\"@proteinjs/server\",\"name\":\"healthCheck\",\"filePath\":\"/home/runner/work/server/server/packages/server/src/routes/healthCheck.ts\",\"qualifiedName\":\"@proteinjs/server/healthCheck\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/server-api/Route\"}],\"edges\":[{\"v\":\"@proteinjs/server/NodeGlobalDataStorage\",\"w\":\"@proteinjs/server-api/GlobalDataStorage\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/server/NodeSessionDataStorage\",\"w\":\"@proteinjs/server-api/SessionDataStorage\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/server/ExtendedIncomingMessage\",\"w\":\"http/IncomingMessage\",\"value\":\"extends interface\"},{\"v\":\"@proteinjs/server/ExtendedSocket\",\"w\":\"socket.io/Socket\",\"value\":\"extends interface\"},{\"v\":\"@proteinjs/server/environmentCache\",\"w\":\"@proteinjs/server-api/GlobalDataCache\",\"value\":\"has type\"},{\"v\":\"@proteinjs/server/healthCheck\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"}]}";
|
|
37
37
|
/** Generate Source Links */
|
|
38
38
|
var NodeGlobalDataStorage_1 = require("../src/NodeGlobalDataStorage");
|
|
39
39
|
var NodeSessionDataStorage_1 = require("../src/NodeSessionDataStorage");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../generated/index.ts"],"names":[],"mappings":";AAAA,oCAAoC;;;;;;;;;;;;;;;;AAEpC,4BAA0B;AAC1B,6BAA2B;AAC3B,iCAA+B;AAC/B,iCAA+B;AAC/B,gCAA8B;AAC9B,uBAAqB;AACrB,yBAAuB;AACvB,kBAAgB;AAChB,mBAAiB;AACjB,2BAAyB;AACzB,oBAAkB;AAClB,0BAAwB;AACxB,mBAAiB;AACjB,iBAAe;AACf,wBAAsB;AACtB,qBAAmB;AACnB,sBAAoB;AAGpB,4BAA4B;AAE5B,IAAM,WAAW,GAAG,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../generated/index.ts"],"names":[],"mappings":";AAAA,oCAAoC;;;;;;;;;;;;;;;;AAEpC,4BAA0B;AAC1B,6BAA2B;AAC3B,iCAA+B;AAC/B,iCAA+B;AAC/B,gCAA8B;AAC9B,uBAAqB;AACrB,yBAAuB;AACvB,kBAAgB;AAChB,mBAAiB;AACjB,2BAAyB;AACzB,oBAAkB;AAClB,0BAAwB;AACxB,mBAAiB;AACjB,iBAAe;AACf,wBAAsB;AACtB,qBAAmB;AACnB,sBAAoB;AAGpB,4BAA4B;AAE5B,IAAM,WAAW,GAAG,i+RAAi+R,CAAC;AAGt/R,4BAA4B;AAE5B,sEAAqE;AACrE,wEAAuE;AACvE,4CAAmD;AACnD,yDAAwD;AAExD,IAAM,WAAW,GAAG;IACnB,yCAAyC,EAAE,6CAAqB;IAChE,0CAA0C,EAAE,+CAAsB;IAClE,oCAAoC,EAAE,2BAAgB;IACtD,+BAA+B,EAAE,yBAAW;CAC5C,CAAC;AAGF,kCAAkC;AAElC,oDAAyD;AACzD,6BAAgB,CAAC,KAAK,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAGjD,2CAAyB"}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { Server as HttpServer } from 'http';
|
|
3
3
|
import { ServerConfig } from '@proteinjs/server-api';
|
|
4
|
+
/** Releases a hold taken with {@link GracefulShutdown.hold}. Idempotent. */
|
|
5
|
+
export type HoldRelease = () => void;
|
|
6
|
+
/** A hold outstanding on the process: its label plus whatever the holder attached for the logs. */
|
|
7
|
+
export type Hold = {
|
|
8
|
+
label: string;
|
|
9
|
+
context?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Follows the process's holds as they come and go — the dev supervisor's lease projection
|
|
13
|
+
* (@n3xah/util-server's ServePackageHold) is the one consumer; it never decides anything here.
|
|
14
|
+
*/
|
|
15
|
+
export type HoldObserver = {
|
|
16
|
+
acquired?: (label: string) => void;
|
|
17
|
+
released?: (label: string) => void;
|
|
18
|
+
};
|
|
4
19
|
/**
|
|
5
20
|
* The one owner of the server's shutdown state. Installed by `startServer` before the listener
|
|
6
21
|
* opens; the `/health-check` route reads `isShuttingDown()` — no other flag system exists.
|
|
@@ -15,25 +30,48 @@ import { ServerConfig } from '@proteinjs/server-api';
|
|
|
15
30
|
* 2. The listener closes (socket.io clients are disconnected, and `io.close()` closes the
|
|
16
31
|
* http server it is attached to), idle keep-alive connections are closed, and in-flight
|
|
17
32
|
* requests run to completion.
|
|
18
|
-
* 3. The
|
|
19
|
-
*
|
|
33
|
+
* 3. The process WAITS for its HOLDS (`hold(label)` below) to release, bounded by
|
|
34
|
+
* `shutdown.turnDrainMs`: work that must not die with the process and that no HTTP
|
|
35
|
+
* connection represents — the canonical holder is a chat turn whose client has gone
|
|
36
|
+
* (the response detached, the model still writing, nothing persisted yet). Past the
|
|
37
|
+
* bound every hold still outstanding is logged by label and abandoned.
|
|
38
|
+
* 4. The connection drain is BOUNDED by `shutdown.drainTimeoutMs`: past it, remaining
|
|
39
|
+
* connections are force-closed. Either way the exit code is 0 — a drained shutdown is
|
|
40
|
+
* not a crash.
|
|
20
41
|
* SIGINT → exit 0 immediately (dev ctrl-C: fast and quiet; nothing needs flushing).
|
|
21
42
|
* exit 86 → NOT this class's concern: `process.exit(86)` is the ServePackageSupervisor
|
|
22
43
|
* restart-request contract (a liveness monitor giving up on a dependency); it is not
|
|
23
44
|
* signal-driven and is untouched by these handlers.
|
|
24
45
|
*
|
|
46
|
+
* Holds are a process-wide static seam so a holder never needs the server instance (a turn
|
|
47
|
+
* registry, a suite with no listener): `hold(label, context)` returns the release; the same
|
|
48
|
+
* label held twice is one hold (the release is shared — the turn registry's idempotence);
|
|
49
|
+
* `holds()` lists what is outstanding, in acquisition order; `observeHolds` lets the dev
|
|
50
|
+
* supervisor's lease follow the same set, so a holder declares itself ONCE and both the
|
|
51
|
+
* pre-signal restart deferral (dev) and the post-SIGTERM drain (prod) honor it.
|
|
52
|
+
*
|
|
53
|
+
* Why holds and not connections: the 2026-09-05 prod kill — a phone locked mid-turn, the route
|
|
54
|
+
* detached the dead response and the turn ran on to persist, GKE's autoscaler evicted the pod,
|
|
55
|
+
* and this drain saw NO connection in flight, so `process.exit(0)` ran 5 s after SIGTERM with the
|
|
56
|
+
* model call 32 s in. Nothing persisted, nothing logged (plans/FREE_AGENT.md §M.14).
|
|
57
|
+
*
|
|
25
58
|
* A SIGTERM before the listener is open (mid-boot kill) exits 0 immediately — nothing is
|
|
26
59
|
* registered anywhere and nothing is in flight.
|
|
27
60
|
*
|
|
28
61
|
* Supervisor compatibility (dev): ServePackageSupervisor SIGTERMs the child's process group and
|
|
29
62
|
* escalates to SIGKILL after its grace period (default 10s); its exit handler ignores exit codes
|
|
30
63
|
* for kills it initiated, so the drain-then-exit-0 shape cannot be misread. DEVELOPMENT defaults
|
|
31
|
-
* `drainDelayMs` to 0 so supervised restarts stay fast
|
|
64
|
+
* `drainDelayMs` to 0 so supervised restarts stay fast; a turn in flight under the supervisor is
|
|
65
|
+
* covered BEFORE the signal by its lease (the hold projection), not by this drain.
|
|
32
66
|
*/
|
|
33
67
|
export declare class GracefulShutdown {
|
|
34
68
|
private server;
|
|
35
69
|
private config;
|
|
36
70
|
private static instance?;
|
|
71
|
+
private static holds;
|
|
72
|
+
private static holdObservers;
|
|
73
|
+
private static holdWaiters;
|
|
74
|
+
private static staticLogger;
|
|
37
75
|
private shuttingDown;
|
|
38
76
|
private logger;
|
|
39
77
|
private constructor();
|
|
@@ -41,10 +79,25 @@ export declare class GracefulShutdown {
|
|
|
41
79
|
static install(server: HttpServer, config: ServerConfig): void;
|
|
42
80
|
/** The readiness seam: `/health-check` reports 503 whenever this is true. */
|
|
43
81
|
static isShuttingDown(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Keep the process alive through a SIGTERM drain until the returned release is called (bounded
|
|
84
|
+
* by `shutdown.turnDrainMs`). `label` names the work in the drain's logs (`chat-turn:<id>`);
|
|
85
|
+
* `context` rides beside it (chat id, user id). Holding a label already held returns the
|
|
86
|
+
* existing release — one hold per label.
|
|
87
|
+
*/
|
|
88
|
+
static hold(label: string, context?: Record<string, unknown>): HoldRelease;
|
|
89
|
+
/** Every hold outstanding right now, in acquisition order. */
|
|
90
|
+
static outstandingHolds(): Hold[];
|
|
91
|
+
/** Follow holds as they are acquired and released. Returns the unsubscribe. */
|
|
92
|
+
static observeHolds(observer: HoldObserver): () => void;
|
|
44
93
|
private drainAndExit;
|
|
94
|
+
private drainHolds;
|
|
45
95
|
private exitNow;
|
|
46
96
|
private drainDelayMs;
|
|
47
97
|
private drainTimeoutMs;
|
|
98
|
+
private turnDrainMs;
|
|
99
|
+
private static whenNoHolds;
|
|
100
|
+
private static notify;
|
|
48
101
|
/** Await a promise with a deadline; true if it settled in time. */
|
|
49
102
|
private settled;
|
|
50
103
|
private sleep;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GracefulShutdown.d.ts","sourceRoot":"","sources":["../../src/GracefulShutdown.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAIrD
|
|
1
|
+
{"version":3,"file":"GracefulShutdown.d.ts","sourceRoot":"","sources":["../../src/GracefulShutdown.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAIrD,4EAA4E;AAC5E,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;AAErC,mGAAmG;AACnG,MAAM,MAAM,IAAI,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAExE;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,CAAC;AAEtG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,qBAAa,gBAAgB;IAUzB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM;IAVhB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAmB;IAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAA2D;IAC/E,OAAO,CAAC,MAAM,CAAC,aAAa,CAA2B;IACvD,OAAO,CAAC,MAAM,CAAC,WAAW,CAAyB;IACnD,OAAO,CAAC,MAAM,CAAC,YAAY,CAA4C;IACvE,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAA4C;IAE1D,OAAO;IAKP,iFAAiF;IACjF,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI;IAU9D,6EAA6E;IAC7E,MAAM,CAAC,cAAc,IAAI,OAAO;IAIhC;;;;;OAKG;IACH,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW;IAuB1E,8DAA8D;IAC9D,MAAM,CAAC,gBAAgB,IAAI,IAAI,EAAE;IAIjC,+EAA+E;IAC/E,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;YAOzC,YAAY;YA6CZ,UAAU;IAqBxB,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,MAAM,CAAC,WAAW;IAO1B,OAAO,CAAC,MAAM,CAAC,MAAM;IAcrB,mEAAmE;YACrD,OAAO;IAUrB,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
2
13
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
14
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
15
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -53,20 +64,39 @@ var SocketIOServerRepo_1 = require("./SocketIOServerRepo");
|
|
|
53
64
|
* 2. The listener closes (socket.io clients are disconnected, and `io.close()` closes the
|
|
54
65
|
* http server it is attached to), idle keep-alive connections are closed, and in-flight
|
|
55
66
|
* requests run to completion.
|
|
56
|
-
* 3. The
|
|
57
|
-
*
|
|
67
|
+
* 3. The process WAITS for its HOLDS (`hold(label)` below) to release, bounded by
|
|
68
|
+
* `shutdown.turnDrainMs`: work that must not die with the process and that no HTTP
|
|
69
|
+
* connection represents — the canonical holder is a chat turn whose client has gone
|
|
70
|
+
* (the response detached, the model still writing, nothing persisted yet). Past the
|
|
71
|
+
* bound every hold still outstanding is logged by label and abandoned.
|
|
72
|
+
* 4. The connection drain is BOUNDED by `shutdown.drainTimeoutMs`: past it, remaining
|
|
73
|
+
* connections are force-closed. Either way the exit code is 0 — a drained shutdown is
|
|
74
|
+
* not a crash.
|
|
58
75
|
* SIGINT → exit 0 immediately (dev ctrl-C: fast and quiet; nothing needs flushing).
|
|
59
76
|
* exit 86 → NOT this class's concern: `process.exit(86)` is the ServePackageSupervisor
|
|
60
77
|
* restart-request contract (a liveness monitor giving up on a dependency); it is not
|
|
61
78
|
* signal-driven and is untouched by these handlers.
|
|
62
79
|
*
|
|
80
|
+
* Holds are a process-wide static seam so a holder never needs the server instance (a turn
|
|
81
|
+
* registry, a suite with no listener): `hold(label, context)` returns the release; the same
|
|
82
|
+
* label held twice is one hold (the release is shared — the turn registry's idempotence);
|
|
83
|
+
* `holds()` lists what is outstanding, in acquisition order; `observeHolds` lets the dev
|
|
84
|
+
* supervisor's lease follow the same set, so a holder declares itself ONCE and both the
|
|
85
|
+
* pre-signal restart deferral (dev) and the post-SIGTERM drain (prod) honor it.
|
|
86
|
+
*
|
|
87
|
+
* Why holds and not connections: the 2026-09-05 prod kill — a phone locked mid-turn, the route
|
|
88
|
+
* detached the dead response and the turn ran on to persist, GKE's autoscaler evicted the pod,
|
|
89
|
+
* and this drain saw NO connection in flight, so `process.exit(0)` ran 5 s after SIGTERM with the
|
|
90
|
+
* model call 32 s in. Nothing persisted, nothing logged (plans/FREE_AGENT.md §M.14).
|
|
91
|
+
*
|
|
63
92
|
* A SIGTERM before the listener is open (mid-boot kill) exits 0 immediately — nothing is
|
|
64
93
|
* registered anywhere and nothing is in flight.
|
|
65
94
|
*
|
|
66
95
|
* Supervisor compatibility (dev): ServePackageSupervisor SIGTERMs the child's process group and
|
|
67
96
|
* escalates to SIGKILL after its grace period (default 10s); its exit handler ignores exit codes
|
|
68
97
|
* for kills it initiated, so the drain-then-exit-0 shape cannot be misread. DEVELOPMENT defaults
|
|
69
|
-
* `drainDelayMs` to 0 so supervised restarts stay fast
|
|
98
|
+
* `drainDelayMs` to 0 so supervised restarts stay fast; a turn in flight under the supervisor is
|
|
99
|
+
* covered BEFORE the signal by its lease (the hold projection), not by this drain.
|
|
70
100
|
*/
|
|
71
101
|
var GracefulShutdown = /** @class */ (function () {
|
|
72
102
|
function GracefulShutdown(server, config) {
|
|
@@ -90,9 +120,50 @@ var GracefulShutdown = /** @class */ (function () {
|
|
|
90
120
|
var _a, _b;
|
|
91
121
|
return (_b = (_a = GracefulShutdown.instance) === null || _a === void 0 ? void 0 : _a.shuttingDown) !== null && _b !== void 0 ? _b : false;
|
|
92
122
|
};
|
|
123
|
+
/**
|
|
124
|
+
* Keep the process alive through a SIGTERM drain until the returned release is called (bounded
|
|
125
|
+
* by `shutdown.turnDrainMs`). `label` names the work in the drain's logs (`chat-turn:<id>`);
|
|
126
|
+
* `context` rides beside it (chat id, user id). Holding a label already held returns the
|
|
127
|
+
* existing release — one hold per label.
|
|
128
|
+
*/
|
|
129
|
+
GracefulShutdown.hold = function (label, context) {
|
|
130
|
+
var existing = GracefulShutdown.holds.get(label);
|
|
131
|
+
if (existing) {
|
|
132
|
+
return existing.release;
|
|
133
|
+
}
|
|
134
|
+
var release = function () {
|
|
135
|
+
var _a;
|
|
136
|
+
if (((_a = GracefulShutdown.holds.get(label)) === null || _a === void 0 ? void 0 : _a.release) !== release) {
|
|
137
|
+
return; // already released (or the label re-held since — that hold has its own release)
|
|
138
|
+
}
|
|
139
|
+
GracefulShutdown.holds.delete(label);
|
|
140
|
+
GracefulShutdown.notify('released', label);
|
|
141
|
+
if (GracefulShutdown.holds.size === 0) {
|
|
142
|
+
var waiters = GracefulShutdown.holdWaiters.splice(0, GracefulShutdown.holdWaiters.length);
|
|
143
|
+
for (var _i = 0, waiters_1 = waiters; _i < waiters_1.length; _i++) {
|
|
144
|
+
var resolve = waiters_1[_i];
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
GracefulShutdown.holds.set(label, { hold: context ? { label: label, context: context } : { label: label }, release: release });
|
|
150
|
+
GracefulShutdown.notify('acquired', label);
|
|
151
|
+
return release;
|
|
152
|
+
};
|
|
153
|
+
/** Every hold outstanding right now, in acquisition order. */
|
|
154
|
+
GracefulShutdown.outstandingHolds = function () {
|
|
155
|
+
return Array.from(GracefulShutdown.holds.values()).map(function (entry) { return (__assign({}, entry.hold)); });
|
|
156
|
+
};
|
|
157
|
+
/** Follow holds as they are acquired and released. Returns the unsubscribe. */
|
|
158
|
+
GracefulShutdown.observeHolds = function (observer) {
|
|
159
|
+
GracefulShutdown.holdObservers.add(observer);
|
|
160
|
+
return function () {
|
|
161
|
+
GracefulShutdown.holdObservers.delete(observer);
|
|
162
|
+
};
|
|
163
|
+
};
|
|
93
164
|
GracefulShutdown.prototype.drainAndExit = function () {
|
|
94
165
|
return __awaiter(this, void 0, void 0, function () {
|
|
95
|
-
var drainDelayMs, drainTimeoutMs, drained, io;
|
|
166
|
+
var drainDelayMs, drainTimeoutMs, turnDrainMs, drained, io;
|
|
96
167
|
var _this = this;
|
|
97
168
|
return __generator(this, function (_a) {
|
|
98
169
|
switch (_a.label) {
|
|
@@ -107,8 +178,10 @@ var GracefulShutdown = /** @class */ (function () {
|
|
|
107
178
|
}
|
|
108
179
|
drainDelayMs = this.drainDelayMs();
|
|
109
180
|
drainTimeoutMs = this.drainTimeoutMs();
|
|
181
|
+
turnDrainMs = this.turnDrainMs();
|
|
110
182
|
this.logger.info({
|
|
111
|
-
message: "Received SIGTERM \u2014 /health-check now reports 503; accepting connections for another ".concat(drainDelayMs, "ms, then draining in-flight requests (bound: ").concat(drainTimeoutMs, "ms)"),
|
|
183
|
+
message: "Received SIGTERM \u2014 /health-check now reports 503; accepting connections for another ".concat(drainDelayMs, "ms, then draining in-flight requests (bound: ").concat(drainTimeoutMs, "ms) and ").concat(GracefulShutdown.holds.size, " hold(s) (bound: ").concat(turnDrainMs, "ms)"),
|
|
184
|
+
obj: { holds: GracefulShutdown.outstandingHolds() },
|
|
112
185
|
});
|
|
113
186
|
// 1. Readiness-propagation window: the LB observes the 503 while we still serve normally.
|
|
114
187
|
return [4 /*yield*/, this.sleep(drainDelayMs)];
|
|
@@ -124,9 +197,16 @@ var GracefulShutdown = /** @class */ (function () {
|
|
|
124
197
|
this.server.close();
|
|
125
198
|
}
|
|
126
199
|
this.server.closeIdleConnections();
|
|
127
|
-
|
|
200
|
+
// 3. Holds: work no connection represents (a detached chat turn). In-flight requests keep
|
|
201
|
+
// running meanwhile — the listener is closed, existing connections are not. Bounded.
|
|
202
|
+
return [4 /*yield*/, this.drainHolds(turnDrainMs)];
|
|
128
203
|
case 2:
|
|
129
|
-
// 3.
|
|
204
|
+
// 3. Holds: work no connection represents (a detached chat turn). In-flight requests keep
|
|
205
|
+
// running meanwhile — the listener is closed, existing connections are not. Bounded.
|
|
206
|
+
_a.sent();
|
|
207
|
+
return [4 /*yield*/, this.settled(drained, drainTimeoutMs)];
|
|
208
|
+
case 3:
|
|
209
|
+
// 4. Bounded: past the drain timeout, force-close what remains. Exit 0 either way.
|
|
130
210
|
if (!(_a.sent())) {
|
|
131
211
|
this.logger.warn({
|
|
132
212
|
message: "Drain exceeded its ".concat(drainTimeoutMs, "ms bound \u2014 force-closing the remaining connections"),
|
|
@@ -140,6 +220,37 @@ var GracefulShutdown = /** @class */ (function () {
|
|
|
140
220
|
});
|
|
141
221
|
});
|
|
142
222
|
};
|
|
223
|
+
GracefulShutdown.prototype.drainHolds = function (turnDrainMs) {
|
|
224
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
225
|
+
var abandoned;
|
|
226
|
+
return __generator(this, function (_a) {
|
|
227
|
+
switch (_a.label) {
|
|
228
|
+
case 0:
|
|
229
|
+
if (GracefulShutdown.holds.size === 0) {
|
|
230
|
+
return [2 /*return*/];
|
|
231
|
+
}
|
|
232
|
+
this.logger.info({
|
|
233
|
+
message: "Listener closed \u2014 waiting for ".concat(GracefulShutdown.holds.size, " hold(s) to release (bound: ").concat(turnDrainMs, "ms)"),
|
|
234
|
+
obj: { holds: GracefulShutdown.outstandingHolds() },
|
|
235
|
+
});
|
|
236
|
+
return [4 /*yield*/, this.settled(GracefulShutdown.whenNoHolds(), turnDrainMs)];
|
|
237
|
+
case 1:
|
|
238
|
+
if (_a.sent()) {
|
|
239
|
+
this.logger.info({ message: "Every hold released \u2014 continuing the drain" });
|
|
240
|
+
return [2 /*return*/];
|
|
241
|
+
}
|
|
242
|
+
abandoned = GracefulShutdown.outstandingHolds();
|
|
243
|
+
this.logger.warn({
|
|
244
|
+
message: "Hold drain exceeded its ".concat(turnDrainMs, "ms bound \u2014 abandoning ").concat(abandoned.length, " hold(s): ").concat(abandoned
|
|
245
|
+
.map(function (hold) { return hold.label; })
|
|
246
|
+
.join(', ')),
|
|
247
|
+
obj: { holds: abandoned },
|
|
248
|
+
});
|
|
249
|
+
return [2 /*return*/];
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
};
|
|
143
254
|
GracefulShutdown.prototype.exitNow = function () {
|
|
144
255
|
this.shuttingDown = true;
|
|
145
256
|
this.logger.info({ message: "Received SIGINT \u2014 exiting immediately" });
|
|
@@ -156,6 +267,32 @@ var GracefulShutdown = /** @class */ (function () {
|
|
|
156
267
|
var _a, _b;
|
|
157
268
|
return (_b = (_a = this.config.shutdown) === null || _a === void 0 ? void 0 : _a.drainTimeoutMs) !== null && _b !== void 0 ? _b : 30000;
|
|
158
269
|
};
|
|
270
|
+
GracefulShutdown.prototype.turnDrainMs = function () {
|
|
271
|
+
var _a, _b;
|
|
272
|
+
return (_b = (_a = this.config.shutdown) === null || _a === void 0 ? void 0 : _a.turnDrainMs) !== null && _b !== void 0 ? _b : 240000;
|
|
273
|
+
};
|
|
274
|
+
GracefulShutdown.whenNoHolds = function () {
|
|
275
|
+
if (GracefulShutdown.holds.size === 0) {
|
|
276
|
+
return Promise.resolve();
|
|
277
|
+
}
|
|
278
|
+
return new Promise(function (resolve) { return GracefulShutdown.holdWaiters.push(resolve); });
|
|
279
|
+
};
|
|
280
|
+
GracefulShutdown.notify = function (event, label) {
|
|
281
|
+
var _a;
|
|
282
|
+
for (var _i = 0, _b = Array.from(GracefulShutdown.holdObservers); _i < _b.length; _i++) {
|
|
283
|
+
var observer = _b[_i];
|
|
284
|
+
try {
|
|
285
|
+
(_a = observer[event]) === null || _a === void 0 ? void 0 : _a.call(observer, label);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
// An observer follows the holds; its failure never touches the hold itself.
|
|
289
|
+
GracefulShutdown.staticLogger.warn({
|
|
290
|
+
message: "A hold observer threw on ".concat(event),
|
|
291
|
+
obj: { label: label, error: error === null || error === void 0 ? void 0 : error.message },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
159
296
|
/** Await a promise with a deadline; true if it settled in time. */
|
|
160
297
|
GracefulShutdown.prototype.settled = function (promise, ms) {
|
|
161
298
|
return __awaiter(this, void 0, void 0, function () {
|
|
@@ -178,6 +315,10 @@ var GracefulShutdown = /** @class */ (function () {
|
|
|
178
315
|
GracefulShutdown.prototype.sleep = function (ms) {
|
|
179
316
|
return new Promise(function (resolve) { return setTimeout(resolve, ms); });
|
|
180
317
|
};
|
|
318
|
+
GracefulShutdown.holds = new Map();
|
|
319
|
+
GracefulShutdown.holdObservers = new Set();
|
|
320
|
+
GracefulShutdown.holdWaiters = [];
|
|
321
|
+
GracefulShutdown.staticLogger = new logger_1.Logger({ name: 'GracefulShutdown' });
|
|
181
322
|
return GracefulShutdown;
|
|
182
323
|
}());
|
|
183
324
|
exports.GracefulShutdown = GracefulShutdown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GracefulShutdown.js","sourceRoot":"","sources":["../../src/GracefulShutdown.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GracefulShutdown.js","sourceRoot":"","sources":["../../src/GracefulShutdown.ts"],"names":[],"mappings":"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"}
|
package/generated/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import 'typescript';
|
|
|
21
21
|
/** Generate Source Graph */
|
|
22
22
|
|
|
23
23
|
const sourceGraph =
|
|
24
|
-
'{"options":{"directed":true,"multigraph":false,"compound":false},"nodes":[{"v":"@proteinjs/server/NodeGlobalDataStorage","value":{"packageName":"@proteinjs/server","name":"NodeGlobalDataStorage","filePath":"/home/runner/work/server/server/packages/server/src/NodeGlobalDataStorage.ts","qualifiedName":"@proteinjs/server/NodeGlobalDataStorage","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"HOOK_INITIALIZED","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"GLOBAL_DATA","type":{"packageName":"@proteinjs/server","name":"{ [id: string]: GlobalData }","filePath":null,"qualifiedName":"@proteinjs/server/{ [id: string]: GlobalData }","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"environment","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"setData","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"data","type":{"packageName":"@proteinjs/server-api","name":"GlobalData","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalData","typeParameters":null,"directParents":null}}]},{"name":"getData","returnType":{"packageName":"@proteinjs/server-api","name":"GlobalData","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalData","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"initHook","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/server-api","name":"GlobalDataStorage","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalDataStorage","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/server-api/GlobalDataStorage"},{"v":"@proteinjs/server/NodeSessionDataStorage","value":{"packageName":"@proteinjs/server","name":"NodeSessionDataStorage","filePath":"/home/runner/work/server/server/packages/server/src/NodeSessionDataStorage.ts","qualifiedName":"@proteinjs/server/NodeSessionDataStorage","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"HOOK_INITIALIZED","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"SESSION_DATA","type":{"packageName":"@proteinjs/server","name":"{ [id: string]: SessionData }","filePath":null,"qualifiedName":"@proteinjs/server/{ [id: string]: SessionData }","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"environment","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"setData","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"data","type":{"packageName":"@proteinjs/server-api","name":"SessionData","filePath":null,"qualifiedName":"@proteinjs/server-api/SessionData","typeParameters":null,"directParents":null}}]},{"name":"getData","returnType":{"packageName":"@proteinjs/server-api","name":"SessionData","filePath":null,"qualifiedName":"@proteinjs/server-api/SessionData","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"clearData","returnType":{"packageName":"","name":"void","filePath":null,"qualifiedName":"/void","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"initHook","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/server-api","name":"SessionDataStorage","filePath":null,"qualifiedName":"@proteinjs/server-api/SessionDataStorage","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/server-api/SessionDataStorage"},{"v":"@proteinjs/server/ExtendedIncomingMessage","value":{"packageName":"@proteinjs/server","name":"ExtendedIncomingMessage","filePath":"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts","qualifiedName":"@proteinjs/server/ExtendedIncomingMessage","properties":[{"name":"sessionID","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"user","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[],"typeParameters":[],"directParents":[{"packageName":"http","name":"IncomingMessage","filePath":null,"qualifiedName":"http/IncomingMessage","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"sourceType":3}},{"v":"http/IncomingMessage"},{"v":"@proteinjs/server/ExtendedSocket","value":{"packageName":"@proteinjs/server","name":"ExtendedSocket","filePath":"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts","qualifiedName":"@proteinjs/server/ExtendedSocket","properties":[{"name":"request","type":{"packageName":"@proteinjs/server","name":"ExtendedIncomingMessage","filePath":null,"qualifiedName":"@proteinjs/server/ExtendedIncomingMessage","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[],"typeParameters":[],"directParents":[{"packageName":"socket.io","name":"Socket","filePath":null,"qualifiedName":"socket.io/Socket","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"sourceType":3}},{"v":"socket.io/Socket"},{"v":"@proteinjs/server/environmentCache","value":{"packageName":"@proteinjs/server","name":"environmentCache","filePath":"/home/runner/work/server/server/packages/server/src/envCache.ts","qualifiedName":"@proteinjs/server/environmentCache","type":{"packageName":"","name":"GlobalDataCache<Env>","filePath":null,"qualifiedName":"/GlobalDataCache<Env>","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"GlobalDataCache","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalDataCache","typeParameters":["@proteinjs/server-api/Env"],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/server-api/GlobalDataCache"},{"v":"@proteinjs/server/healthCheck","value":{"packageName":"@proteinjs/server","name":"healthCheck","filePath":"/home/runner/work/server/server/packages/server/src/routes/healthCheck.ts","qualifiedName":"@proteinjs/server/healthCheck","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/server-api/Route"}],"edges":[{"v":"@proteinjs/server/NodeGlobalDataStorage","w":"@proteinjs/server-api/GlobalDataStorage","value":"implements interface"},{"v":"@proteinjs/server/NodeSessionDataStorage","w":"@proteinjs/server-api/SessionDataStorage","value":"implements interface"},{"v":"@proteinjs/server/ExtendedIncomingMessage","w":"http/IncomingMessage","value":"extends interface"},{"v":"@proteinjs/server/ExtendedSocket","w":"socket.io/Socket","value":"extends interface"},{"v":"@proteinjs/server/environmentCache","w":"@proteinjs/server-api/GlobalDataCache","value":"has type"},{"v":"@proteinjs/server/healthCheck","w":"@proteinjs/server-api/Route","value":"has type"}]}';
|
|
24
|
+
'{"options":{"directed":true,"multigraph":false,"compound":false},"nodes":[{"v":"/() => void"},{"v":"@proteinjs/server/NodeGlobalDataStorage","value":{"packageName":"@proteinjs/server","name":"NodeGlobalDataStorage","filePath":"/home/runner/work/server/server/packages/server/src/NodeGlobalDataStorage.ts","qualifiedName":"@proteinjs/server/NodeGlobalDataStorage","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"HOOK_INITIALIZED","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"GLOBAL_DATA","type":{"packageName":"@proteinjs/server","name":"{ [id: string]: GlobalData }","filePath":null,"qualifiedName":"@proteinjs/server/{ [id: string]: GlobalData }","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"environment","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"setData","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"data","type":{"packageName":"@proteinjs/server-api","name":"GlobalData","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalData","typeParameters":null,"directParents":null}}]},{"name":"getData","returnType":{"packageName":"@proteinjs/server-api","name":"GlobalData","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalData","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"initHook","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/server-api","name":"GlobalDataStorage","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalDataStorage","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/server-api/GlobalDataStorage"},{"v":"@proteinjs/server/NodeSessionDataStorage","value":{"packageName":"@proteinjs/server","name":"NodeSessionDataStorage","filePath":"/home/runner/work/server/server/packages/server/src/NodeSessionDataStorage.ts","qualifiedName":"@proteinjs/server/NodeSessionDataStorage","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"HOOK_INITIALIZED","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"SESSION_DATA","type":{"packageName":"@proteinjs/server","name":"{ [id: string]: SessionData }","filePath":null,"qualifiedName":"@proteinjs/server/{ [id: string]: SessionData }","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"environment","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"setData","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"data","type":{"packageName":"@proteinjs/server-api","name":"SessionData","filePath":null,"qualifiedName":"@proteinjs/server-api/SessionData","typeParameters":null,"directParents":null}}]},{"name":"getData","returnType":{"packageName":"@proteinjs/server-api","name":"SessionData","filePath":null,"qualifiedName":"@proteinjs/server-api/SessionData","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"clearData","returnType":{"packageName":"","name":"void","filePath":null,"qualifiedName":"/void","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"initHook","returnType":null,"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/server-api","name":"SessionDataStorage","filePath":null,"qualifiedName":"@proteinjs/server-api/SessionDataStorage","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/server-api/SessionDataStorage"},{"v":"@proteinjs/server/ExtendedIncomingMessage","value":{"packageName":"@proteinjs/server","name":"ExtendedIncomingMessage","filePath":"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts","qualifiedName":"@proteinjs/server/ExtendedIncomingMessage","properties":[{"name":"sessionID","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"user","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isOptional":true,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[],"typeParameters":[],"directParents":[{"packageName":"http","name":"IncomingMessage","filePath":null,"qualifiedName":"http/IncomingMessage","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"sourceType":3}},{"v":"http/IncomingMessage"},{"v":"@proteinjs/server/ExtendedSocket","value":{"packageName":"@proteinjs/server","name":"ExtendedSocket","filePath":"/home/runner/work/server/server/packages/server/src/SocketIOServerRepo.ts","qualifiedName":"@proteinjs/server/ExtendedSocket","properties":[{"name":"request","type":{"packageName":"@proteinjs/server","name":"ExtendedIncomingMessage","filePath":null,"qualifiedName":"@proteinjs/server/ExtendedIncomingMessage","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[],"typeParameters":[],"directParents":[{"packageName":"socket.io","name":"Socket","filePath":null,"qualifiedName":"socket.io/Socket","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"sourceType":3}},{"v":"socket.io/Socket"},{"v":"@proteinjs/server/environmentCache","value":{"packageName":"@proteinjs/server","name":"environmentCache","filePath":"/home/runner/work/server/server/packages/server/src/envCache.ts","qualifiedName":"@proteinjs/server/environmentCache","type":{"packageName":"","name":"GlobalDataCache<Env>","filePath":null,"qualifiedName":"/GlobalDataCache<Env>","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"GlobalDataCache","filePath":null,"qualifiedName":"@proteinjs/server-api/GlobalDataCache","typeParameters":["@proteinjs/server-api/Env"],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/server-api/GlobalDataCache"},{"v":"@proteinjs/server/healthCheck","value":{"packageName":"@proteinjs/server","name":"healthCheck","filePath":"/home/runner/work/server/server/packages/server/src/routes/healthCheck.ts","qualifiedName":"@proteinjs/server/healthCheck","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/server-api/Route"}],"edges":[{"v":"@proteinjs/server/NodeGlobalDataStorage","w":"@proteinjs/server-api/GlobalDataStorage","value":"implements interface"},{"v":"@proteinjs/server/NodeSessionDataStorage","w":"@proteinjs/server-api/SessionDataStorage","value":"implements interface"},{"v":"@proteinjs/server/ExtendedIncomingMessage","w":"http/IncomingMessage","value":"extends interface"},{"v":"@proteinjs/server/ExtendedSocket","w":"socket.io/Socket","value":"extends interface"},{"v":"@proteinjs/server/environmentCache","w":"@proteinjs/server-api/GlobalDataCache","value":"has type"},{"v":"@proteinjs/server/healthCheck","w":"@proteinjs/server-api/Route","value":"has type"}]}';
|
|
25
25
|
|
|
26
26
|
/** Generate Source Links */
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "A server impl",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@proteinjs/event": "^1.0.18",
|
|
26
26
|
"@proteinjs/logger": "^1.0.21",
|
|
27
27
|
"@proteinjs/reflection": "^1.1.12",
|
|
28
|
-
"@proteinjs/server-api": "^3.
|
|
28
|
+
"@proteinjs/server-api": "^3.2.0",
|
|
29
29
|
"@proteinjs/util-node": "^1.10.1",
|
|
30
30
|
"body-parser": "1.19.0",
|
|
31
31
|
"cookie-parser": "1.4.5",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
},
|
|
65
65
|
"main": "./dist/generated/index.js",
|
|
66
66
|
"types": "./dist/generated/index.d.ts",
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "c1a14f62c4e023db66161869955baf616aab31bb"
|
|
68
68
|
}
|
package/src/GracefulShutdown.ts
CHANGED
|
@@ -3,6 +3,18 @@ import { ServerConfig } from '@proteinjs/server-api';
|
|
|
3
3
|
import { Logger } from '@proteinjs/logger';
|
|
4
4
|
import { SocketIOServerRepo } from './SocketIOServerRepo';
|
|
5
5
|
|
|
6
|
+
/** Releases a hold taken with {@link GracefulShutdown.hold}. Idempotent. */
|
|
7
|
+
export type HoldRelease = () => void;
|
|
8
|
+
|
|
9
|
+
/** A hold outstanding on the process: its label plus whatever the holder attached for the logs. */
|
|
10
|
+
export type Hold = { label: string; context?: Record<string, unknown> };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Follows the process's holds as they come and go — the dev supervisor's lease projection
|
|
14
|
+
* (@n3xah/util-server's ServePackageHold) is the one consumer; it never decides anything here.
|
|
15
|
+
*/
|
|
16
|
+
export type HoldObserver = { acquired?: (label: string) => void; released?: (label: string) => void };
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* The one owner of the server's shutdown state. Installed by `startServer` before the listener
|
|
8
20
|
* opens; the `/health-check` route reads `isShuttingDown()` — no other flag system exists.
|
|
@@ -17,23 +29,46 @@ import { SocketIOServerRepo } from './SocketIOServerRepo';
|
|
|
17
29
|
* 2. The listener closes (socket.io clients are disconnected, and `io.close()` closes the
|
|
18
30
|
* http server it is attached to), idle keep-alive connections are closed, and in-flight
|
|
19
31
|
* requests run to completion.
|
|
20
|
-
* 3. The
|
|
21
|
-
*
|
|
32
|
+
* 3. The process WAITS for its HOLDS (`hold(label)` below) to release, bounded by
|
|
33
|
+
* `shutdown.turnDrainMs`: work that must not die with the process and that no HTTP
|
|
34
|
+
* connection represents — the canonical holder is a chat turn whose client has gone
|
|
35
|
+
* (the response detached, the model still writing, nothing persisted yet). Past the
|
|
36
|
+
* bound every hold still outstanding is logged by label and abandoned.
|
|
37
|
+
* 4. The connection drain is BOUNDED by `shutdown.drainTimeoutMs`: past it, remaining
|
|
38
|
+
* connections are force-closed. Either way the exit code is 0 — a drained shutdown is
|
|
39
|
+
* not a crash.
|
|
22
40
|
* SIGINT → exit 0 immediately (dev ctrl-C: fast and quiet; nothing needs flushing).
|
|
23
41
|
* exit 86 → NOT this class's concern: `process.exit(86)` is the ServePackageSupervisor
|
|
24
42
|
* restart-request contract (a liveness monitor giving up on a dependency); it is not
|
|
25
43
|
* signal-driven and is untouched by these handlers.
|
|
26
44
|
*
|
|
45
|
+
* Holds are a process-wide static seam so a holder never needs the server instance (a turn
|
|
46
|
+
* registry, a suite with no listener): `hold(label, context)` returns the release; the same
|
|
47
|
+
* label held twice is one hold (the release is shared — the turn registry's idempotence);
|
|
48
|
+
* `holds()` lists what is outstanding, in acquisition order; `observeHolds` lets the dev
|
|
49
|
+
* supervisor's lease follow the same set, so a holder declares itself ONCE and both the
|
|
50
|
+
* pre-signal restart deferral (dev) and the post-SIGTERM drain (prod) honor it.
|
|
51
|
+
*
|
|
52
|
+
* Why holds and not connections: the 2026-09-05 prod kill — a phone locked mid-turn, the route
|
|
53
|
+
* detached the dead response and the turn ran on to persist, GKE's autoscaler evicted the pod,
|
|
54
|
+
* and this drain saw NO connection in flight, so `process.exit(0)` ran 5 s after SIGTERM with the
|
|
55
|
+
* model call 32 s in. Nothing persisted, nothing logged (plans/FREE_AGENT.md §M.14).
|
|
56
|
+
*
|
|
27
57
|
* A SIGTERM before the listener is open (mid-boot kill) exits 0 immediately — nothing is
|
|
28
58
|
* registered anywhere and nothing is in flight.
|
|
29
59
|
*
|
|
30
60
|
* Supervisor compatibility (dev): ServePackageSupervisor SIGTERMs the child's process group and
|
|
31
61
|
* escalates to SIGKILL after its grace period (default 10s); its exit handler ignores exit codes
|
|
32
62
|
* for kills it initiated, so the drain-then-exit-0 shape cannot be misread. DEVELOPMENT defaults
|
|
33
|
-
* `drainDelayMs` to 0 so supervised restarts stay fast
|
|
63
|
+
* `drainDelayMs` to 0 so supervised restarts stay fast; a turn in flight under the supervisor is
|
|
64
|
+
* covered BEFORE the signal by its lease (the hold projection), not by this drain.
|
|
34
65
|
*/
|
|
35
66
|
export class GracefulShutdown {
|
|
36
67
|
private static instance?: GracefulShutdown;
|
|
68
|
+
private static holds = new Map<string, { hold: Hold; release: HoldRelease }>();
|
|
69
|
+
private static holdObservers = new Set<HoldObserver>();
|
|
70
|
+
private static holdWaiters: Array<() => void> = [];
|
|
71
|
+
private static staticLogger = new Logger({ name: 'GracefulShutdown' });
|
|
37
72
|
private shuttingDown = false;
|
|
38
73
|
private logger = new Logger({ name: 'GracefulShutdown' });
|
|
39
74
|
|
|
@@ -58,6 +93,48 @@ export class GracefulShutdown {
|
|
|
58
93
|
return GracefulShutdown.instance?.shuttingDown ?? false;
|
|
59
94
|
}
|
|
60
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Keep the process alive through a SIGTERM drain until the returned release is called (bounded
|
|
98
|
+
* by `shutdown.turnDrainMs`). `label` names the work in the drain's logs (`chat-turn:<id>`);
|
|
99
|
+
* `context` rides beside it (chat id, user id). Holding a label already held returns the
|
|
100
|
+
* existing release — one hold per label.
|
|
101
|
+
*/
|
|
102
|
+
static hold(label: string, context?: Record<string, unknown>): HoldRelease {
|
|
103
|
+
const existing = GracefulShutdown.holds.get(label);
|
|
104
|
+
if (existing) {
|
|
105
|
+
return existing.release;
|
|
106
|
+
}
|
|
107
|
+
const release: HoldRelease = () => {
|
|
108
|
+
if (GracefulShutdown.holds.get(label)?.release !== release) {
|
|
109
|
+
return; // already released (or the label re-held since — that hold has its own release)
|
|
110
|
+
}
|
|
111
|
+
GracefulShutdown.holds.delete(label);
|
|
112
|
+
GracefulShutdown.notify('released', label);
|
|
113
|
+
if (GracefulShutdown.holds.size === 0) {
|
|
114
|
+
const waiters = GracefulShutdown.holdWaiters.splice(0, GracefulShutdown.holdWaiters.length);
|
|
115
|
+
for (const resolve of waiters) {
|
|
116
|
+
resolve();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
GracefulShutdown.holds.set(label, { hold: context ? { label, context } : { label }, release });
|
|
121
|
+
GracefulShutdown.notify('acquired', label);
|
|
122
|
+
return release;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Every hold outstanding right now, in acquisition order. */
|
|
126
|
+
static outstandingHolds(): Hold[] {
|
|
127
|
+
return Array.from(GracefulShutdown.holds.values()).map((entry) => ({ ...entry.hold }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Follow holds as they are acquired and released. Returns the unsubscribe. */
|
|
131
|
+
static observeHolds(observer: HoldObserver): () => void {
|
|
132
|
+
GracefulShutdown.holdObservers.add(observer);
|
|
133
|
+
return () => {
|
|
134
|
+
GracefulShutdown.holdObservers.delete(observer);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
61
138
|
private async drainAndExit(): Promise<void> {
|
|
62
139
|
if (this.shuttingDown) {
|
|
63
140
|
return; // already draining; the sender's escalation is SIGKILL, not a second drain
|
|
@@ -69,8 +146,10 @@ export class GracefulShutdown {
|
|
|
69
146
|
}
|
|
70
147
|
const drainDelayMs = this.drainDelayMs();
|
|
71
148
|
const drainTimeoutMs = this.drainTimeoutMs();
|
|
149
|
+
const turnDrainMs = this.turnDrainMs();
|
|
72
150
|
this.logger.info({
|
|
73
|
-
message: `Received SIGTERM — /health-check now reports 503; accepting connections for another ${drainDelayMs}ms, then draining in-flight requests (bound: ${drainTimeoutMs}ms)`,
|
|
151
|
+
message: `Received SIGTERM — /health-check now reports 503; accepting connections for another ${drainDelayMs}ms, then draining in-flight requests (bound: ${drainTimeoutMs}ms) and ${GracefulShutdown.holds.size} hold(s) (bound: ${turnDrainMs}ms)`,
|
|
152
|
+
obj: { holds: GracefulShutdown.outstandingHolds() },
|
|
74
153
|
});
|
|
75
154
|
// 1. Readiness-propagation window: the LB observes the 503 while we still serve normally.
|
|
76
155
|
await this.sleep(drainDelayMs);
|
|
@@ -87,7 +166,10 @@ export class GracefulShutdown {
|
|
|
87
166
|
this.server.close();
|
|
88
167
|
}
|
|
89
168
|
this.server.closeIdleConnections();
|
|
90
|
-
// 3.
|
|
169
|
+
// 3. Holds: work no connection represents (a detached chat turn). In-flight requests keep
|
|
170
|
+
// running meanwhile — the listener is closed, existing connections are not. Bounded.
|
|
171
|
+
await this.drainHolds(turnDrainMs);
|
|
172
|
+
// 4. Bounded: past the drain timeout, force-close what remains. Exit 0 either way.
|
|
91
173
|
if (!(await this.settled(drained, drainTimeoutMs))) {
|
|
92
174
|
this.logger.warn({
|
|
93
175
|
message: `Drain exceeded its ${drainTimeoutMs}ms bound — force-closing the remaining connections`,
|
|
@@ -98,6 +180,27 @@ export class GracefulShutdown {
|
|
|
98
180
|
process.exit(0);
|
|
99
181
|
}
|
|
100
182
|
|
|
183
|
+
private async drainHolds(turnDrainMs: number): Promise<void> {
|
|
184
|
+
if (GracefulShutdown.holds.size === 0) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.logger.info({
|
|
188
|
+
message: `Listener closed — waiting for ${GracefulShutdown.holds.size} hold(s) to release (bound: ${turnDrainMs}ms)`,
|
|
189
|
+
obj: { holds: GracefulShutdown.outstandingHolds() },
|
|
190
|
+
});
|
|
191
|
+
if (await this.settled(GracefulShutdown.whenNoHolds(), turnDrainMs)) {
|
|
192
|
+
this.logger.info({ message: `Every hold released — continuing the drain` });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const abandoned = GracefulShutdown.outstandingHolds();
|
|
196
|
+
this.logger.warn({
|
|
197
|
+
message: `Hold drain exceeded its ${turnDrainMs}ms bound — abandoning ${abandoned.length} hold(s): ${abandoned
|
|
198
|
+
.map((hold) => hold.label)
|
|
199
|
+
.join(', ')}`,
|
|
200
|
+
obj: { holds: abandoned },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
101
204
|
private exitNow(): void {
|
|
102
205
|
this.shuttingDown = true;
|
|
103
206
|
this.logger.info({ message: `Received SIGINT — exiting immediately` });
|
|
@@ -115,6 +218,31 @@ export class GracefulShutdown {
|
|
|
115
218
|
return this.config.shutdown?.drainTimeoutMs ?? 30_000;
|
|
116
219
|
}
|
|
117
220
|
|
|
221
|
+
private turnDrainMs(): number {
|
|
222
|
+
return this.config.shutdown?.turnDrainMs ?? 240_000;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private static whenNoHolds(): Promise<void> {
|
|
226
|
+
if (GracefulShutdown.holds.size === 0) {
|
|
227
|
+
return Promise.resolve();
|
|
228
|
+
}
|
|
229
|
+
return new Promise<void>((resolve) => GracefulShutdown.holdWaiters.push(resolve));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static notify(event: 'acquired' | 'released', label: string): void {
|
|
233
|
+
for (const observer of Array.from(GracefulShutdown.holdObservers)) {
|
|
234
|
+
try {
|
|
235
|
+
observer[event]?.(label);
|
|
236
|
+
} catch (error: any) {
|
|
237
|
+
// An observer follows the holds; its failure never touches the hold itself.
|
|
238
|
+
GracefulShutdown.staticLogger.warn({
|
|
239
|
+
message: `A hold observer threw on ${event}`,
|
|
240
|
+
obj: { label, error: error?.message },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
118
246
|
/** Await a promise with a deadline; true if it settled in time. */
|
|
119
247
|
private async settled(promise: Promise<void>, ms: number): Promise<boolean> {
|
|
120
248
|
let timer: NodeJS.Timeout | undefined;
|
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
* Test fixture: a REAL @proteinjs/server (the built dist — the same code prod runs) with a
|
|
3
3
|
* deliberately slow endpoint, so the graceful-shutdown suite can hold a request in flight
|
|
4
4
|
* while it signals the process. Run with:
|
|
5
|
-
* FIXTURE_PORT=<port> [FIXTURE_DRAIN_DELAY_MS=..] [FIXTURE_DRAIN_TIMEOUT_MS=..] node gracefulShutdownServer.js
|
|
5
|
+
* FIXTURE_PORT=<port> [FIXTURE_DRAIN_DELAY_MS=..] [FIXTURE_DRAIN_TIMEOUT_MS=..] [FIXTURE_TURN_DRAIN_MS=..] node gracefulShutdownServer.js
|
|
6
6
|
*
|
|
7
7
|
* Markers on stdout (the suite's synchronization points):
|
|
8
8
|
* FIXTURE_READY — startServer resolved (the listener is up)
|
|
9
9
|
* SLOW_REQUEST_STARTED — the /slow handler is executing (a request is now in flight)
|
|
10
|
+
* HOLD_ACQUIRED <label> — /hold took a process hold (GracefulShutdown.hold) that outlives
|
|
11
|
+
* its request: the response ends at once, the hold releases itself
|
|
12
|
+
* ?ms= later — a detached chat turn's shape (no connection, work live)
|
|
13
|
+
* HOLD_RELEASED <label> — that hold released
|
|
10
14
|
*
|
|
11
15
|
* Also serves /server-timeouts: the LIVE http.Server's keepAliveTimeout/headersTimeout (read off
|
|
12
16
|
* the request's own socket), so the keep-alive suite asserts the running instance through the
|
|
13
17
|
* front door instead of re-deriving values from source.
|
|
14
18
|
*/
|
|
15
19
|
const expressSession = require('express-session');
|
|
16
|
-
const { startServer } = require('../../dist/generated/index.js');
|
|
20
|
+
const { startServer, GracefulShutdown } = require('../../dist/generated/index.js');
|
|
17
21
|
|
|
18
22
|
const port = Number(process.env.FIXTURE_PORT);
|
|
19
23
|
if (!port) {
|
|
@@ -44,6 +48,20 @@ startServer({
|
|
|
44
48
|
response.status(200).send('session-cookie-set');
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
51
|
+
if (request.path === '/hold') {
|
|
52
|
+
// A hold that OUTLIVES its request — the detached-turn shape the drain must wait for:
|
|
53
|
+
// the response ends now (no connection remains), the work stays live for ?ms=.
|
|
54
|
+
const label = String(request.query.label ?? 'fixture-hold');
|
|
55
|
+
const ms = Number(request.query.ms ?? 5000);
|
|
56
|
+
const release = GracefulShutdown.hold(label, { source: 'fixture', ms });
|
|
57
|
+
console.log(`HOLD_ACQUIRED ${label}`);
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
release();
|
|
60
|
+
console.log(`HOLD_RELEASED ${label}`);
|
|
61
|
+
}, ms);
|
|
62
|
+
response.status(200).send('held');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
47
65
|
if (request.path !== '/slow') {
|
|
48
66
|
next();
|
|
49
67
|
return;
|
|
@@ -61,5 +79,6 @@ startServer({
|
|
|
61
79
|
shutdown: {
|
|
62
80
|
drainDelayMs: process.env.FIXTURE_DRAIN_DELAY_MS ? Number(process.env.FIXTURE_DRAIN_DELAY_MS) : undefined,
|
|
63
81
|
drainTimeoutMs: process.env.FIXTURE_DRAIN_TIMEOUT_MS ? Number(process.env.FIXTURE_DRAIN_TIMEOUT_MS) : undefined,
|
|
82
|
+
turnDrainMs: process.env.FIXTURE_TURN_DRAIN_MS ? Number(process.env.FIXTURE_TURN_DRAIN_MS) : undefined,
|
|
64
83
|
},
|
|
65
84
|
}).then(() => console.log('FIXTURE_READY'));
|
|
@@ -13,6 +13,11 @@ import { ChildProcess, spawn } from 'child_process';
|
|
|
13
13
|
* idle keep-alives are closed, and the process exits 0 — bounded by
|
|
14
14
|
* shutdown.drainTimeoutMs (past it, remaining connections are force-closed and
|
|
15
15
|
* the exit is still 0).
|
|
16
|
+
* HOLDS (GracefulShutdown.hold): work no connection represents — a chat turn whose
|
|
17
|
+
* client disconnected (the 2026-09-05 prod kill, plans/FREE_AGENT.md §M.14) — keeps
|
|
18
|
+
* the process alive past the connection drain until it releases, bounded by
|
|
19
|
+
* shutdown.turnDrainMs; past THAT bound the holds still outstanding are logged by
|
|
20
|
+
* label and abandoned, and the exit is still 0.
|
|
16
21
|
* SIGINT → immediate exit 0 (dev ctrl-C: fast, quiet).
|
|
17
22
|
* exit 86 → RESTART_REQUEST_EXIT_CODE, untouched by this feature: process.exit(86) is not
|
|
18
23
|
* signal-driven (ServePackageSupervisor's respawn contract rides it).
|
|
@@ -101,9 +106,70 @@ describe('graceful shutdown', () => {
|
|
|
101
106
|
expect(exit).toEqual({ code: 0, signal: null });
|
|
102
107
|
expect(Date.now() - sigintAt).toBeLessThan(2000); // no drain delay on the fast path
|
|
103
108
|
}, 30000);
|
|
109
|
+
|
|
110
|
+
describe('holds — work no connection represents (a detached chat turn)', () => {
|
|
111
|
+
it('SIGTERM: a hold whose request already ended keeps the process alive past the connection drain; its release lets the exit proceed', async () => {
|
|
112
|
+
// No delay, a short connection bound, a long hold bound: the ONLY thing that can keep this
|
|
113
|
+
// process alive after SIGTERM is the hold.
|
|
114
|
+
fixture = await startFixture({ drainDelayMs: 0, drainTimeoutMs: 1000, turnDrainMs: 20000 });
|
|
115
|
+
|
|
116
|
+
// The detached-turn shape: the request ends immediately (no connection remains), the work
|
|
117
|
+
// it started stays live for 5s and releases its hold when done.
|
|
118
|
+
const held = await request(fixture.port, '/hold?label=chat-turn:detached-turn&ms=5000');
|
|
119
|
+
expect(held).toEqual({ status: 200, body: 'held' });
|
|
120
|
+
await fixture.waitForMarker('HOLD_ACQUIRED chat-turn:detached-turn');
|
|
121
|
+
|
|
122
|
+
const sigtermAt = Date.now();
|
|
123
|
+
fixture.child.kill('SIGTERM');
|
|
124
|
+
|
|
125
|
+
// 1. Past the connection drain (nothing was in flight — it completes at once; the listener
|
|
126
|
+
// is closed) the process is STILL RUNNING: the hold is what keeps it.
|
|
127
|
+
await sleep(2500);
|
|
128
|
+
expect(fixture.child.exitCode).toBeNull();
|
|
129
|
+
expect(fixture.child.signalCode).toBeNull();
|
|
130
|
+
await expect(request(fixture.port, '/health-check')).rejects.toMatchObject({
|
|
131
|
+
code: expect.stringMatching(/ECONNREFUSED|ECONNRESET/),
|
|
132
|
+
});
|
|
133
|
+
expect(fixture.stdout()).toContain('waiting for 1 hold(s) to release (bound: 20000ms)');
|
|
134
|
+
|
|
135
|
+
// 2. The work finishes and releases; the exit follows the RELEASE (~5s), not the 20s bound.
|
|
136
|
+
const exit = await fixture.exited;
|
|
137
|
+
expect(exit).toEqual({ code: 0, signal: null });
|
|
138
|
+
const elapsed = Date.now() - sigtermAt;
|
|
139
|
+
expect(elapsed).toBeGreaterThanOrEqual(4500);
|
|
140
|
+
expect(elapsed).toBeLessThan(12000);
|
|
141
|
+
expect(fixture.stdout()).toContain('HOLD_RELEASED chat-turn:detached-turn');
|
|
142
|
+
expect(fixture.stdout()).toContain('Every hold released');
|
|
143
|
+
}, 40000);
|
|
144
|
+
|
|
145
|
+
it('SIGTERM: the hold drain is BOUNDED — past turnDrainMs the outstanding holds are logged by label, abandoned, and the exit is still 0', async () => {
|
|
146
|
+
fixture = await startFixture({ drainDelayMs: 0, drainTimeoutMs: 1000, turnDrainMs: 3000 });
|
|
147
|
+
|
|
148
|
+
// A hold that would outlive the bound by far.
|
|
149
|
+
const held = await request(fixture.port, '/hold?label=chat-turn:abandoned-turn&ms=60000');
|
|
150
|
+
expect(held.status).toBe(200);
|
|
151
|
+
await fixture.waitForMarker('HOLD_ACQUIRED chat-turn:abandoned-turn');
|
|
152
|
+
|
|
153
|
+
const sigtermAt = Date.now();
|
|
154
|
+
fixture.child.kill('SIGTERM');
|
|
155
|
+
|
|
156
|
+
const exit = await fixture.exited;
|
|
157
|
+
expect(exit).toEqual({ code: 0, signal: null });
|
|
158
|
+
const elapsed = Date.now() - sigtermAt;
|
|
159
|
+
expect(elapsed).toBeGreaterThanOrEqual(3000); // the bound was actually served
|
|
160
|
+
expect(elapsed).toBeLessThan(9000); // ...and it ended the wait, not the 60s hold
|
|
161
|
+
// The abandoned work is NAMED — the line the 2026-09-05 kill never left behind.
|
|
162
|
+
expect(fixture.stdout()).toContain('abandoning 1 hold(s): chat-turn:abandoned-turn');
|
|
163
|
+
expect(fixture.stdout()).not.toContain('HOLD_RELEASED');
|
|
164
|
+
}, 30000);
|
|
165
|
+
});
|
|
104
166
|
});
|
|
105
167
|
|
|
106
|
-
async function startFixture(shutdown: {
|
|
168
|
+
async function startFixture(shutdown: {
|
|
169
|
+
drainDelayMs: number;
|
|
170
|
+
drainTimeoutMs: number;
|
|
171
|
+
turnDrainMs?: number;
|
|
172
|
+
}): Promise<Fixture> {
|
|
107
173
|
const port = await ephemeralPort();
|
|
108
174
|
// Scrub the env vars startServer reads (dev machines export some of these): the fixture's
|
|
109
175
|
// behavior must come from its own config only.
|
|
@@ -119,6 +185,7 @@ async function startFixture(shutdown: { drainDelayMs: number; drainTimeoutMs: nu
|
|
|
119
185
|
FIXTURE_PORT: String(port),
|
|
120
186
|
FIXTURE_DRAIN_DELAY_MS: String(shutdown.drainDelayMs),
|
|
121
187
|
FIXTURE_DRAIN_TIMEOUT_MS: String(shutdown.drainTimeoutMs),
|
|
188
|
+
...(shutdown.turnDrainMs !== undefined ? { FIXTURE_TURN_DRAIN_MS: String(shutdown.turnDrainMs) } : {}),
|
|
122
189
|
},
|
|
123
190
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
124
191
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { GracefulShutdown } from '../src/GracefulShutdown';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The holds seam as a process-wide static contract — what a holder (a chat turn registry) and a
|
|
5
|
+
* follower (the dev supervisor's lease projection) can rely on without a server instance:
|
|
6
|
+
* one hold per label, idempotent release, acquisition-ordered listing with the holder's context,
|
|
7
|
+
* and observers told of every transition exactly once.
|
|
8
|
+
*/
|
|
9
|
+
describe('GracefulShutdown holds', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const hold of GracefulShutdown.outstandingHolds()) {
|
|
12
|
+
GracefulShutdown.hold(hold.label)(); // hold() on a held label returns its release
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('a hold is listed with its context until its release; releasing twice is a no-op', () => {
|
|
17
|
+
const release = GracefulShutdown.hold('chat-turn:t1', { chatId: 'c1', userId: 'u1' });
|
|
18
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([
|
|
19
|
+
{ label: 'chat-turn:t1', context: { chatId: 'c1', userId: 'u1' } },
|
|
20
|
+
]);
|
|
21
|
+
release();
|
|
22
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([]);
|
|
23
|
+
release();
|
|
24
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('holding a label already held is the SAME hold — one entry, one release', () => {
|
|
28
|
+
const first = GracefulShutdown.hold('chat-turn:t2', { chatId: 'c2' });
|
|
29
|
+
const second = GracefulShutdown.hold('chat-turn:t2', { chatId: 'other' });
|
|
30
|
+
expect(second).toBe(first);
|
|
31
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([{ label: 'chat-turn:t2', context: { chatId: 'c2' } }]);
|
|
32
|
+
second();
|
|
33
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('a stale release never drops a label re-held since', () => {
|
|
37
|
+
const stale = GracefulShutdown.hold('chat-turn:t3');
|
|
38
|
+
stale();
|
|
39
|
+
const fresh = GracefulShutdown.hold('chat-turn:t3');
|
|
40
|
+
stale(); // the first hold's release, called again after the label was re-held
|
|
41
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([{ label: 'chat-turn:t3' }]);
|
|
42
|
+
fresh();
|
|
43
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('lists holds in acquisition order', () => {
|
|
47
|
+
GracefulShutdown.hold('b');
|
|
48
|
+
GracefulShutdown.hold('a');
|
|
49
|
+
expect(GracefulShutdown.outstandingHolds().map((hold) => hold.label)).toEqual(['b', 'a']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('observers see each acquisition and release once; a re-hold of a held label is silent; unsubscribing stops the stream', () => {
|
|
53
|
+
const events: string[] = [];
|
|
54
|
+
const unsubscribe = GracefulShutdown.observeHolds({
|
|
55
|
+
acquired: (label) => events.push(`+${label}`),
|
|
56
|
+
released: (label) => events.push(`-${label}`),
|
|
57
|
+
});
|
|
58
|
+
const release = GracefulShutdown.hold('chat-turn:t4');
|
|
59
|
+
GracefulShutdown.hold('chat-turn:t4');
|
|
60
|
+
release();
|
|
61
|
+
release();
|
|
62
|
+
unsubscribe();
|
|
63
|
+
GracefulShutdown.hold('chat-turn:t5')();
|
|
64
|
+
expect(events).toEqual(['+chat-turn:t4', '-chat-turn:t4']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('a throwing observer never touches the hold', () => {
|
|
68
|
+
const unsubscribe = GracefulShutdown.observeHolds({
|
|
69
|
+
acquired: () => {
|
|
70
|
+
throw new Error('follower down');
|
|
71
|
+
},
|
|
72
|
+
released: () => {
|
|
73
|
+
throw new Error('follower down');
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const release = GracefulShutdown.hold('chat-turn:t6');
|
|
77
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([{ label: 'chat-turn:t6' }]);
|
|
78
|
+
release();
|
|
79
|
+
expect(GracefulShutdown.outstandingHolds()).toEqual([]);
|
|
80
|
+
unsubscribe();
|
|
81
|
+
});
|
|
82
|
+
});
|