@morojs/moro 1.8.8 → 1.8.10
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/dist/core/cluster/thread-cluster.d.ts +178 -0
- package/dist/core/cluster/thread-cluster.js +389 -0
- package/dist/core/cluster/thread-cluster.js.map +1 -0
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/moro-engine-server.d.ts +29 -1
- package/dist/core/http/moro-engine-server.js +375 -68
- package/dist/core/http/moro-engine-server.js.map +1 -1
- package/dist/core/http/utils/compression.js +12 -6
- package/dist/core/http/utils/compression.js.map +1 -1
- package/dist/core/http/utils/uws-worker-clustering.js +10 -6
- package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
- package/dist/core/http/uws-http-server.d.ts +1 -0
- package/dist/core/http/uws-http-server.js +105 -22
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/jobs/leader-election.js +16 -5
- package/dist/core/jobs/leader-election.js.map +1 -1
- package/dist/core/networking/websocket-manager.js +4 -2
- package/dist/core/networking/websocket-manager.js.map +1 -1
- package/dist/core/routing/unified-router.js +60 -29
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/runtime/node-adapter.d.ts +1 -1
- package/dist/core/runtime/node-adapter.js.map +1 -1
- package/dist/core/utilities/builtin.d.ts +1 -0
- package/dist/core/utilities/builtin.js +25 -0
- package/dist/core/utilities/builtin.js.map +1 -0
- package/dist/core/utilities/package-utils.d.ts +28 -3
- package/dist/core/utilities/package-utils.js +24 -12
- package/dist/core/utilities/package-utils.js.map +1 -1
- package/dist/core/workers/worker-manager.js +4 -3
- package/dist/core/workers/worker-manager.js.map +1 -1
- package/dist/moro.d.ts +5 -0
- package/dist/moro.js +364 -204
- package/dist/moro.js.map +1 -1
- package/dist/types/http.d.ts +1 -1
- package/dist/types/http.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,8 +5,14 @@ import crypto from 'crypto';
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
|
-
import
|
|
9
|
-
|
|
8
|
+
import { requireBuiltin } from '../utilities/builtin.js';
|
|
9
|
+
// worker_threads on first use (core/utilities/builtin.ts). node:cluster is
|
|
10
|
+
// never loaded here: a cluster worker is exactly a process with
|
|
11
|
+
// NODE_UNIQUE_ID in its environment (that is how node:cluster itself decides
|
|
12
|
+
// isWorker), and asking cluster would pull child_process/dgram/net (~7 MB)
|
|
13
|
+
// into every app that starts the job scheduler.
|
|
14
|
+
const wt = () => requireBuiltin('worker_threads');
|
|
15
|
+
const isClusterWorkerProcess = () => process.env.NODE_UNIQUE_ID !== undefined;
|
|
10
16
|
/**
|
|
11
17
|
* LeaderElection - Ensures only one job scheduler runs across distributed instances
|
|
12
18
|
* Supports:
|
|
@@ -111,11 +117,16 @@ return 0`;
|
|
|
111
117
|
* Check if this process should participate in leader election
|
|
112
118
|
*/
|
|
113
119
|
shouldParticipate() {
|
|
114
|
-
// Only main thread
|
|
115
|
-
|
|
120
|
+
// Only the main thread of a non-worker process can be leader. Under
|
|
121
|
+
// thread clustering that is exactly the cluster primary (its worker
|
|
122
|
+
// threads are !isMainThread); under process clustering it is the primary
|
|
123
|
+
// process (workers have cluster.isWorker). Either way the scheduler runs
|
|
124
|
+
// in the primary (Moro.startPrimaryServices), so one participant per
|
|
125
|
+
// cluster - no change needed for threads.
|
|
126
|
+
if (!wt().isMainThread) {
|
|
116
127
|
return false;
|
|
117
128
|
}
|
|
118
|
-
if (
|
|
129
|
+
if (isClusterWorkerProcess()) {
|
|
119
130
|
return false;
|
|
120
131
|
}
|
|
121
132
|
return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"leader-election.js","sourceRoot":"","sources":["../../../src/core/jobs/leader-election.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,iFAAiF;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAqB9C;;;;;;;GAOG;AACH,MAAM,OAAO,cAAe,SAAQ,YAAY;IACtC,QAAQ,GAAG,KAAK,CAAC;IACjB,QAAQ,CAA4B;IACpC,QAAQ,CAAU;IAClB,WAAW,CAAS;IACpB,iBAAiB,CAAS;IAC1B,cAAc,CAA8B;IAC5C,UAAU,CAA8B;IACxC,MAAM,CAAS;IACf,aAAa,GAAG,gBAAgB,CAAC;IACjC,UAAU,CAAS;IACnB,UAAU,CAAc;IACxB,WAAW,CAAO;IAClB,cAAc,GAAG,KAAK,CAAC;IACvB,mBAAmB,GAAG,CAAC,CAAC;IAExB,MAAM,CAAU,cAAc,GAAG,uBAAuB,CAAC;IAEjE,+EAA+E;IAC/E,2EAA2E;IACnE,MAAM,CAAU,oBAAoB,GAAG;;;;;;;;SAQxC,CAAC;IAER,kFAAkF;IAC1E,MAAM,CAAU,oBAAoB,GAAG;;;;;;;SAOxC,CAAC;IAER,YAAY,MAAc,EAAE,UAAiC,EAAE,QAAQ,EAAE,MAAM,EAAE;QAC/E,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC,cAAc;QAC/D,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,KAAK,CAAC,CAAC,cAAc;QAC3E,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QAEvC,8BAA8B;QAC9B,IAAI,CAAC,UAAU;YACb,OAAO,CAAC,UAAU;gBAClB,GAAG,EAAE,CAAC,QAAQ,EAAE,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAE3F,0CAA0C;QAC1C,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YAC7B,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACrB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;YACnC,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC;gBAC3B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC;QAED,sCAAsC;QACtC,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACxB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;QACjE,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,EAAE,IAAI,CAAC,aAAa,EAAE;YAClE,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,WAAW,EAAE,OAAO,CAAC,WAAW;SACjC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,KAAK;QAChB,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YAC7B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;YACpE,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAED,oDAAoD;QACpD,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;YACjF,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,6BAA6B,EAAE,IAAI,CAAC,aAAa,EAAE;YAClE,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC;QAEH,4BAA4B;QAC5B,MAAM,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAElC,wBAAwB;QACxB,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC5B,CAAC;IAED;;OAEG;IACK,iBAAiB;QACvB,6DAA6D;QAC7D,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,oBAAoB;QAChC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAE3B,IAAI,CAAC;YACH,IAAI,QAAQ,GAAG,KAAK,CAAC;YAErB,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;gBAC7B,QAAQ,GAAG,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC7C,CAAC;iBAAM,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACrC,QAAQ,GAAG,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC9C,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,IAAI,CAAC,YAAY,EAAE,CAAC;gBACpB,OAAO,IAAI,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,yCAAyC;gBACzC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBAC/B,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,EAAE,IAAI,CAAC,aAAa,EAAE;gBACpE,KAAK;gBACL,OAAO,EAAE,IAAI,CAAC,mBAAmB;aAClC,CAAC,CAAC;YACH,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACK,KAAK,CAAC,kBAAkB;QAC9B,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC9C,CAAC;QAED,MAAM,QAAQ,GAAe;YAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;YACvB,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,SAAS,EAAE,IAAI,IAAI,EAAE;YACrB,aAAa,EAAE,IAAI,IAAI,EAAE;YACzB,QAAQ,EAAE,IAAI,CAAC,mBAAmB,EAAE;SACrC,CAAC;QACF,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAE5C,4EAA4E;QAC5E,2CAA2C;QAC3C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC3D,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC9C,CAAC;oBAAS,CAAC;gBACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;YACD,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC;YAC3B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACxD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,IAAI,CAAC,aAAa,EAAE;oBACnE,KAAK;oBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;iBACxB,CAAC,CAAC;gBACH,OAAO,KAAK,CAAC;YACf,CAAC;YACD,wEAAwE;QAC1E,CAAC;QAED,IAAI,CAAC;YACH,6DAA6D;YAC7D,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACpE,MAAM,cAAc,GAAe,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAExD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC;YAE9E,IAAI,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;gBAC/B,oDAAoD;gBACpD,IAAI,CAAC,UAAU,GAAG,cAAc,CAAC;gBACjC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,EAAE,IAAI,CAAC,aAAa,EAAE;oBAC5E,MAAM,EAAE,cAAc,CAAC,UAAU;oBACjC,GAAG,EAAE,OAAO;iBACb,CAAC,CAAC;gBACH,OAAO,KAAK,CAAC;YACf,CAAC;YAED,+DAA+D;YAC/D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kCAAkC,EAAE,IAAI,CAAC,aAAa,EAAE;gBACvE,cAAc,EAAE,cAAc,CAAC,UAAU;gBACzC,OAAO;aACR,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;YACvC,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC;YAC3B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,IAAI,CAAC,aAAa,EAAE;gBACnE,KAAK;gBACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;aACxB,CAAC,CAAC;YACH,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,eAAe,CAAC,UAAkB;QAC9C,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC9C,CAAC;QAED,MAAM,QAAQ,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;QAChG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACnD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,mBAAmB;QAC/B,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QAED,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,cAAc,CAAC,cAAc,CAAC;YAC9C,MAAM,QAAQ,GAAe;gBAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;gBACvB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,aAAa,EAAE,IAAI,IAAI,EAAE;gBACzB,QAAQ,EAAE,IAAI,CAAC,mBAAmB,EAAE;aACrC,CAAC;YAEF,yEAAyE;YACzE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CACvC,OAAO,EACP,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EACxB,IAAI,EACJ,IAAI,CAAC,WAAW,EAChB,IAAI,CACL,CAAC;YAEF,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC;gBAC3B,OAAO,IAAI,CAAC;YACd,CAAC;YAED,uCAAuC;YACvC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACzD,IAAI,YAAY,EAAE,CAAC;gBACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAC7C,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8BAA8B,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YACjF,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED;;OAEG;IACK,YAAY;QAClB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,aAAa,EAAE;YACpD,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,QAAQ,EAAE,IAAI,CAAC,mBAAmB;SACnC,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE;YAC1B,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS;SACtC,CAAC,CAAC;QAEH,kBAAkB;QAClB,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,QAAQ;QACpB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE,IAAI,CAAC,aAAa,EAAE;YAC9D,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,aAAa,EAAE,CAAC;QAErB,yEAAyE;QACzE,uCAAuC;QACvC,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC9C,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;oBACpE,MAAM,aAAa,GAAe,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;oBACvD,IAAI,aAAa,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;wBACjD,MAAM,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;oBAC1D,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,wDAAwD;gBAC1D,CAAC;YACH,CAAC;iBAAM,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACzD,uEAAuE;gBACvE,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CACzB,cAAc,CAAC,oBAAoB,EACnC,CAAC,EACD,cAAc,CAAC,cAAc,EAC7B,IAAI,CAAC,UAAU,CAChB,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yCAAyC,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9F,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;IAChE,CAAC;IAED;;OAEG;IACK,cAAc;QACpB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;YACrC,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;oBACxB,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC7B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;oBACrE,kBAAkB;oBAClB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACtB,mBAAmB;oBACnB,MAAM,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE;wBACjC,KAAK,IAAI,CAAC,oBAAoB,EAAE,CAAC;oBACnC,CAAC,EAAE,IAAI,CAAC,CAAC;oBACT,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,2BAA2B;gBACjD,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAE3B,2BAA2B;QAC3B,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;IAED;;OAEG;IACK,aAAa;QACnB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACnC,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;QAClC,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,UAAU,CAAC,aAAa,GAAG,IAAI,IAAI,EAAE,CAAC;QAE3C,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9C,4EAA4E;YAC5E,wEAAwE;YACxE,mEAAmE;YACnE,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YACpE,MAAM,aAAa,GAAe,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACvD,IAAI,aAAa,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;gBACjD,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;YAC7E,CAAC;YACD,MAAM,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAC9D,CAAC;aAAM,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACzD,+EAA+E;YAC/E,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAC3C,cAAc,CAAC,oBAAoB,EACnC,CAAC,EACD,cAAc,CAAC,cAAc,EAC7B,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,EAC/B,IAAI,CAAC,WAAW,CACjB,CAAC;YACF,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;YACrF,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QACzF,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE;YAC5B,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,aAAa;SACzC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,kBAAkB;QACxB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO;QACT,CAAC;QAED,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAC,CAAC;QAE/D,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;YACjC,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;oBACxB,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACnB,4BAA4B;oBAC5B,MAAM,IAAI,CAAC,oBAAoB,EAAE,CAAC;gBACpC,CAAC;qBAAM,CAAC;oBACN,gCAAgC;oBAChC,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAChC,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,EAAE,aAAa,CAAC,CAAC;QAElB,2BAA2B;QAC3B,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;IAED;;OAEG;IACK,iBAAiB;QACvB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC9B,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,gBAAgB;QAC5B,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,IAAI,WAAW,GAAG,KAAK,CAAC;YAExB,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC9C,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBACpE,MAAM,aAAa,GAAe,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBACvD,WAAW,GAAG,aAAa,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,CAAC;YAC7D,CAAC;iBAAM,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACzD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;gBAC3E,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,aAAa,GAAe,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;oBACvD,WAAW,GAAG,aAAa,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,CAAC;gBAC7D,CAAC;YACH,CAAC;YAED,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;gBACzF,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;YACxB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAChF,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,iBAAiB;QAC7B,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YACvE,OAAO;QACT,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC;QAEpF,IAAI,YAAY,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACpC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kCAAkC,EAAE,IAAI,CAAC,aAAa,EAAE;gBACvE,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,UAAU;gBAClC,YAAY;aACb,CAAC,CAAC;YAEH,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE;gBAC5B,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,UAAU;gBAClC,YAAY;aACb,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;OAEG;IACK,mBAAmB;QACzB,MAAM,QAAQ,GAAwB;YACpC,WAAW,EAAE,OAAO,CAAC,OAAO;YAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE;SACzB,CAAC;QAEF,gBAAgB;QAChB,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,CAAC;YACxC,QAAQ,CAAC,UAAU,GAAG;gBACpB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE;gBAC9C,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;gBACpC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS;aAChC,CAAC;QACJ,CAAC;QAED,sBAAsB;QACtB,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;YAC1B,QAAQ,CAAC,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAC7C,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;OAEG;IACI,eAAe;QACpB,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED;;OAEG;IACI,aAAa;QAClB,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9D,CAAC;IAED;;OAEG;IACI,aAAa;QAClB,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,aAAa;QACxB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,4BAA4B,EAAE,IAAI,CAAC,aAAa,EAAE;YACjE,UAAU,EAAE,IAAI,CAAC,UAAU;SAC5B,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,QAAQ;QACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,EAAE,IAAI,CAAC,aAAa,EAAE;YACtE,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,SAAS,EAAE,IAAI,CAAC,QAAQ;SACzB,CAAC,CAAC;QAEH,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAE3B,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC;QAED,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;IACvD,CAAC","sourcesContent":["// Production-grade Leader Election for Distributed Job Scheduling\n// Supports file-based and Redis-based locking for K8s and clustered environments\n\nimport { EventEmitter } from 'events';\nimport crypto from 'crypto';\nimport { Logger } from '../../types/logger.js';\nimport fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport cluster from 'cluster';\nimport { isMainThread } from 'worker_threads';\n\nexport interface LeaderElectionOptions {\n strategy: 'file' | 'redis' | 'none';\n lockPath?: string;\n lockTimeout?: number;\n heartbeatInterval?: number;\n redisClient?: any; // Optional Redis client\n forceLeader?: boolean; // Force this instance to be leader (dev/testing)\n instanceId?: string; // Custom instance ID\n}\n\nexport interface LeaderInfo {\n instanceId: string;\n hostname: string;\n pid: number;\n electedAt: Date;\n lastHeartbeat: Date;\n metadata?: Record<string, any>;\n}\n\n/**\n * LeaderElection - Ensures only one job scheduler runs across distributed instances\n * Supports:\n * - File-based locking (for local/NFS environments)\n * - Redis-based locking (for distributed systems)\n * - Kubernetes pod detection\n * - Automatic failover with health checks\n */\nexport class LeaderElection extends EventEmitter {\n private isLeader = false;\n private strategy: 'file' | 'redis' | 'none';\n private lockPath?: string;\n private lockTimeout: number;\n private heartbeatInterval: number;\n private heartbeatTimer?: NodeJS.Timeout | undefined;\n private checkTimer?: NodeJS.Timeout | undefined;\n private logger: Logger;\n private loggerContext = 'LeaderElection';\n private instanceId: string;\n private leaderInfo?: LeaderInfo;\n private redisClient?: any;\n private isShuttingDown = false;\n private lockAcquireAttempts = 0;\n\n private static readonly REDIS_LOCK_KEY = 'moro:jobs:leader:lock';\n\n // Compare-and-refresh: rewrite the value and extend the TTL only if the stored\n // lock still belongs to this instance (ownership is fenced by instanceId).\n private static readonly REDIS_REFRESH_SCRIPT = `\nlocal cur = redis.call('GET', KEYS[1])\nif not cur then return 0 end\nlocal ok, decoded = pcall(cjson.decode, cur)\nif ok and decoded['instanceId'] == ARGV[1] then\n redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])\n return 1\nend\nreturn 0`;\n\n // Compare-and-delete: release the lock only if it still belongs to this instance.\n private static readonly REDIS_RELEASE_SCRIPT = `\nlocal cur = redis.call('GET', KEYS[1])\nif not cur then return 0 end\nlocal ok, decoded = pcall(cjson.decode, cur)\nif ok and decoded['instanceId'] == ARGV[1] then\n return redis.call('DEL', KEYS[1])\nend\nreturn 0`;\n\n constructor(logger: Logger, options: LeaderElectionOptions = { strategy: 'file' }) {\n super();\n this.logger = logger;\n this.strategy = options.strategy;\n this.lockTimeout = options.lockTimeout ?? 30000; // 30s default\n this.heartbeatInterval = options.heartbeatInterval ?? 10000; // 10s default\n this.redisClient = options.redisClient;\n\n // Generate unique instance ID\n this.instanceId =\n options.instanceId ||\n `${os.hostname()}-${process.pid}-${Date.now()}-${crypto.randomBytes(6).toString('hex')}`;\n\n // Setup lock path for file-based strategy\n if (this.strategy === 'file') {\n if (options.lockPath) {\n this.lockPath = options.lockPath;\n } else {\n const tmpDir = os.tmpdir();\n this.lockPath = path.join(tmpDir, 'moro-jobs-leader.lock');\n }\n }\n\n // Force leader mode (for dev/testing)\n if (options.forceLeader) {\n this.isLeader = true;\n this.logger.warn('Leader election forced - running as leader');\n }\n\n this.logger.debug('LeaderElection initialized', this.loggerContext, {\n strategy: this.strategy,\n instanceId: this.instanceId,\n lockPath: this.lockPath,\n forceLeader: options.forceLeader,\n });\n }\n\n /**\n * Start leader election process\n */\n public async start(): Promise<void> {\n if (this.strategy === 'none') {\n this.logger.debug('Leader election disabled, assuming leader role');\n this.becomeLeader();\n return;\n }\n\n // Check if we should participate in leader election\n if (!this.shouldParticipate()) {\n this.logger.info('Not participating in leader election (worker process/thread)');\n return;\n }\n\n this.logger.info('Starting leader election...', this.loggerContext, {\n strategy: this.strategy,\n instanceId: this.instanceId,\n });\n\n // Try to acquire leadership\n await this.tryAcquireLeadership();\n\n // Start periodic checks\n this.startPeriodicCheck();\n }\n\n /**\n * Check if this process should participate in leader election\n */\n private shouldParticipate(): boolean {\n // Only main thread and primary cluster process can be leader\n if (!isMainThread) {\n return false;\n }\n\n if (cluster.isWorker) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Try to acquire leadership\n */\n private async tryAcquireLeadership(): Promise<boolean> {\n if (this.isShuttingDown) {\n return false;\n }\n\n this.lockAcquireAttempts++;\n\n try {\n let acquired = false;\n\n if (this.strategy === 'file') {\n acquired = await this.tryAcquireFileLock();\n } else if (this.strategy === 'redis') {\n acquired = await this.tryAcquireRedisLock();\n }\n\n if (acquired) {\n this.becomeLeader();\n return true;\n } else {\n // Check if current leader is still alive\n await this.checkLeaderHealth();\n return false;\n }\n } catch (error) {\n this.logger.error('Error during leader election', this.loggerContext, {\n error,\n attempt: this.lockAcquireAttempts,\n });\n return false;\n }\n }\n\n /**\n * Try to acquire file-based lock\n *\n * Uses O_EXCL (open flag 'wx') for the common \"no lock yet\" case so exactly one\n * instance can win the create race. An expired lock is taken over via an atomic\n * temp-write + rename. A takeover race between two instances is a last-writer-wins\n * rename, which is then reconciled by the periodic verifyLeadership() check and the\n * ownership-checked heartbeat below, so a single leader is still guaranteed.\n */\n private async tryAcquireFileLock(): Promise<boolean> {\n if (!this.lockPath) {\n throw new Error('Lock path not configured');\n }\n\n const lockInfo: LeaderInfo = {\n instanceId: this.instanceId,\n hostname: os.hostname(),\n pid: process.pid,\n electedAt: new Date(),\n lastHeartbeat: new Date(),\n metadata: this.getInstanceMetadata(),\n };\n const serialized = JSON.stringify(lockInfo);\n\n // Fast path: atomically create the lock file (O_EXCL). Fails with EEXIST if\n // another instance already holds the lock.\n try {\n const handle = await fs.promises.open(this.lockPath, 'wx');\n try {\n await handle.writeFile(serialized, 'utf-8');\n } finally {\n await handle.close();\n }\n this.leaderInfo = lockInfo;\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code !== 'EEXIST') {\n this.logger.error('Failed to acquire file lock', this.loggerContext, {\n error,\n lockPath: this.lockPath,\n });\n return false;\n }\n // Lock file already exists — fall through to the expiry/takeover check.\n }\n\n try {\n // Read existing lock and decide whether we may take it over.\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const existingLeader: LeaderInfo = JSON.parse(lockData);\n\n const lockAge = Date.now() - new Date(existingLeader.lastHeartbeat).getTime();\n\n if (lockAge < this.lockTimeout) {\n // Lock is still valid and held by another instance.\n this.leaderInfo = existingLeader;\n this.logger.debug('Leader lock held by another instance', this.loggerContext, {\n leader: existingLeader.instanceId,\n age: lockAge,\n });\n return false;\n }\n\n // Lock expired — take over with an atomic temp-write + rename.\n this.logger.warn('Leader lock expired, taking over', this.loggerContext, {\n previousLeader: existingLeader.instanceId,\n lockAge,\n });\n\n await this.atomicWriteLock(serialized);\n this.leaderInfo = lockInfo;\n return true;\n } catch (error) {\n this.logger.error('Failed to acquire file lock', this.loggerContext, {\n error,\n lockPath: this.lockPath,\n });\n return false;\n }\n }\n\n /**\n * Atomically (re)write the lock file: write to a unique temp file, then rename\n * over the target. rename() is atomic on POSIX so readers never see a partial\n * lock file.\n */\n private async atomicWriteLock(serialized: string): Promise<void> {\n if (!this.lockPath) {\n throw new Error('Lock path not configured');\n }\n\n const tempPath = `${this.lockPath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;\n await fs.promises.writeFile(tempPath, serialized, 'utf-8');\n try {\n await fs.promises.rename(tempPath, this.lockPath);\n } catch (error) {\n await fs.promises.unlink(tempPath).catch(() => {});\n throw error;\n }\n }\n\n /**\n * Try to acquire Redis-based lock\n */\n private async tryAcquireRedisLock(): Promise<boolean> {\n if (!this.redisClient) {\n throw new Error('Redis client not configured');\n }\n\n try {\n const lockKey = LeaderElection.REDIS_LOCK_KEY;\n const lockInfo: LeaderInfo = {\n instanceId: this.instanceId,\n hostname: os.hostname(),\n pid: process.pid,\n electedAt: new Date(),\n lastHeartbeat: new Date(),\n metadata: this.getInstanceMetadata(),\n };\n\n // Try to set lock with NX (only if not exists) and PX (expiration in ms)\n const result = await this.redisClient.set(\n lockKey,\n JSON.stringify(lockInfo),\n 'PX',\n this.lockTimeout,\n 'NX'\n );\n\n if (result === 'OK') {\n this.leaderInfo = lockInfo;\n return true;\n }\n\n // Lock exists, get current leader info\n const existingLock = await this.redisClient.get(lockKey);\n if (existingLock) {\n this.leaderInfo = JSON.parse(existingLock);\n }\n\n return false;\n } catch (error) {\n this.logger.error('Failed to acquire Redis lock', this.loggerContext, { error });\n return false;\n }\n }\n\n /**\n * Become leader\n */\n private becomeLeader(): void {\n if (this.isLeader) {\n return;\n }\n\n this.isLeader = true;\n this.logger.info('Became leader', this.loggerContext, {\n instanceId: this.instanceId,\n strategy: this.strategy,\n attempts: this.lockAcquireAttempts,\n });\n\n this.emit('leader:elected', {\n instanceId: this.instanceId,\n electedAt: this.leaderInfo?.electedAt,\n });\n\n // Start heartbeat\n this.startHeartbeat();\n }\n\n /**\n * Step down as leader\n */\n private async stepDown(): Promise<void> {\n if (!this.isLeader) {\n return;\n }\n\n this.logger.info('Stepping down as leader', this.loggerContext, {\n instanceId: this.instanceId,\n });\n\n this.isLeader = false;\n this.stopHeartbeat();\n\n // Release lock — only if we still own it, so we never delete a lock that\n // another instance has since acquired.\n try {\n if (this.strategy === 'file' && this.lockPath) {\n try {\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n if (currentLeader.instanceId === this.instanceId) {\n await fs.promises.unlink(this.lockPath).catch(() => {});\n }\n } catch {\n // Lock file missing or unreadable — nothing to release.\n }\n } else if (this.strategy === 'redis' && this.redisClient) {\n // Compare-and-delete: only DEL if the stored lock still belongs to us.\n await this.redisClient.eval(\n LeaderElection.REDIS_RELEASE_SCRIPT,\n 1,\n LeaderElection.REDIS_LOCK_KEY,\n this.instanceId\n );\n }\n } catch (error) {\n this.logger.error('Failed to release lock during step down', this.loggerContext, { error });\n }\n\n this.emit('leader:stepdown', { instanceId: this.instanceId });\n }\n\n /**\n * Start heartbeat to maintain leadership\n */\n private startHeartbeat(): void {\n if (this.heartbeatTimer) {\n return;\n }\n\n this.heartbeatTimer = setInterval(() => {\n void (async () => {\n if (this.isShuttingDown) {\n return;\n }\n\n try {\n await this.sendHeartbeat();\n } catch (error) {\n this.logger.error('Heartbeat failed', this.loggerContext, { error });\n // Lost leadership\n await this.stepDown();\n // Try to reacquire\n const retryTimer = setTimeout(() => {\n void this.tryAcquireLeadership();\n }, 1000);\n retryTimer.unref(); // Don't keep process alive\n }\n })();\n }, this.heartbeatInterval);\n\n // Don't keep process alive\n this.heartbeatTimer.unref();\n }\n\n /**\n * Stop heartbeat\n */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = undefined;\n }\n }\n\n /**\n * Send heartbeat to maintain lock\n */\n private async sendHeartbeat(): Promise<void> {\n if (!this.leaderInfo) {\n return;\n }\n\n this.leaderInfo.lastHeartbeat = new Date();\n\n if (this.strategy === 'file' && this.lockPath) {\n // Verify we still own the lock before refreshing it. If the file is gone or\n // now owned by another instance, we have lost leadership — throw so the\n // heartbeat handler steps us down and attempts a clean re-acquire.\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n if (currentLeader.instanceId !== this.instanceId) {\n throw new Error('Lost leadership: file lock is owned by another instance');\n }\n await this.atomicWriteLock(JSON.stringify(this.leaderInfo));\n } else if (this.strategy === 'redis' && this.redisClient) {\n // Compare-and-refresh: only rewrite + extend the TTL if we still own the lock.\n const refreshed = await this.redisClient.eval(\n LeaderElection.REDIS_REFRESH_SCRIPT,\n 1,\n LeaderElection.REDIS_LOCK_KEY,\n this.instanceId,\n JSON.stringify(this.leaderInfo),\n this.lockTimeout\n );\n if (Number(refreshed) !== 1) {\n throw new Error('Lost leadership: redis lock is no longer owned by this instance');\n }\n }\n\n this.logger.debug('Heartbeat sent', this.loggerContext, { instanceId: this.instanceId });\n this.emit('leader:heartbeat', {\n instanceId: this.instanceId,\n timestamp: this.leaderInfo.lastHeartbeat,\n });\n }\n\n /**\n * Start periodic check for leadership changes\n */\n private startPeriodicCheck(): void {\n if (this.checkTimer) {\n return;\n }\n\n const checkInterval = Math.floor(this.heartbeatInterval * 1.5);\n\n this.checkTimer = setInterval(() => {\n void (async () => {\n if (this.isShuttingDown) {\n return;\n }\n\n if (!this.isLeader) {\n // Try to acquire leadership\n await this.tryAcquireLeadership();\n } else {\n // Verify we still hold the lock\n await this.verifyLeadership();\n }\n })();\n }, checkInterval);\n\n // Don't keep process alive\n this.checkTimer.unref();\n }\n\n /**\n * Stop periodic check\n */\n private stopPeriodicCheck(): void {\n if (this.checkTimer) {\n clearInterval(this.checkTimer);\n this.checkTimer = undefined;\n }\n }\n\n /**\n * Verify we still hold leadership\n */\n private async verifyLeadership(): Promise<void> {\n if (!this.isLeader) {\n return;\n }\n\n try {\n let stillLeader = false;\n\n if (this.strategy === 'file' && this.lockPath) {\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n stillLeader = currentLeader.instanceId === this.instanceId;\n } else if (this.strategy === 'redis' && this.redisClient) {\n const lockData = await this.redisClient.get(LeaderElection.REDIS_LOCK_KEY);\n if (lockData) {\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n stillLeader = currentLeader.instanceId === this.instanceId;\n }\n }\n\n if (!stillLeader) {\n this.logger.warn('Lost leadership', this.loggerContext, { instanceId: this.instanceId });\n await this.stepDown();\n }\n } catch (error) {\n this.logger.error('Failed to verify leadership', this.loggerContext, { error });\n await this.stepDown();\n }\n }\n\n /**\n * Check health of current leader\n */\n private async checkLeaderHealth(): Promise<void> {\n if (!this.leaderInfo || this.leaderInfo.instanceId === this.instanceId) {\n return;\n }\n\n const heartbeatAge = Date.now() - new Date(this.leaderInfo.lastHeartbeat).getTime();\n\n if (heartbeatAge > this.lockTimeout) {\n this.logger.warn('Current leader appears unhealthy', this.loggerContext, {\n leader: this.leaderInfo.instanceId,\n heartbeatAge,\n });\n\n this.emit('leader:unhealthy', {\n leader: this.leaderInfo.instanceId,\n heartbeatAge,\n });\n }\n }\n\n /**\n * Get instance metadata\n */\n private getInstanceMetadata(): Record<string, any> {\n const metadata: Record<string, any> = {\n nodeVersion: process.version,\n platform: process.platform,\n arch: process.arch,\n uptime: process.uptime(),\n };\n\n // K8s detection\n if (process.env.KUBERNETES_SERVICE_HOST) {\n metadata.kubernetes = {\n podName: process.env.HOSTNAME || os.hostname(),\n namespace: process.env.POD_NAMESPACE,\n nodeName: process.env.NODE_NAME,\n };\n }\n\n // Container detection\n if (process.env.CONTAINER) {\n metadata.container = process.env.CONTAINER;\n }\n\n return metadata;\n }\n\n /**\n * Check if current instance is leader\n */\n public isCurrentLeader(): boolean {\n return this.isLeader;\n }\n\n /**\n * Get current leader info\n */\n public getLeaderInfo(): LeaderInfo | undefined {\n return this.leaderInfo ? { ...this.leaderInfo } : undefined;\n }\n\n /**\n * Get instance ID\n */\n public getInstanceId(): string {\n return this.instanceId;\n }\n\n /**\n * Force step down (for testing/admin)\n */\n public async forceStepDown(): Promise<void> {\n if (!this.isLeader) {\n return;\n }\n\n this.logger.warn('Forced step down requested', this.loggerContext, {\n instanceId: this.instanceId,\n });\n await this.stepDown();\n }\n\n /**\n * Shutdown and cleanup\n */\n public async shutdown(): Promise<void> {\n this.logger.info('LeaderElection shutting down...', this.loggerContext, {\n instanceId: this.instanceId,\n wasLeader: this.isLeader,\n });\n\n this.isShuttingDown = true;\n\n this.stopHeartbeat();\n this.stopPeriodicCheck();\n\n if (this.isLeader) {\n await this.stepDown();\n }\n\n this.removeAllListeners();\n this.logger.info('LeaderElection shutdown complete');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"leader-election.js","sourceRoot":"","sources":["../../../src/core/jobs/leader-election.ts"],"names":[],"mappings":"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","sourcesContent":["// Production-grade Leader Election for Distributed Job Scheduling\n// Supports file-based and Redis-based locking for K8s and clustered environments\n\nimport { EventEmitter } from 'events';\nimport crypto from 'crypto';\nimport { Logger } from '../../types/logger.js';\nimport fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport { requireBuiltin } from '../utilities/builtin.js';\n\n// worker_threads on first use (core/utilities/builtin.ts). node:cluster is\n// never loaded here: a cluster worker is exactly a process with\n// NODE_UNIQUE_ID in its environment (that is how node:cluster itself decides\n// isWorker), and asking cluster would pull child_process/dgram/net (~7 MB)\n// into every app that starts the job scheduler.\nconst wt = () => requireBuiltin<typeof import('worker_threads')>('worker_threads');\nconst isClusterWorkerProcess = () => process.env.NODE_UNIQUE_ID !== undefined;\n\nexport interface LeaderElectionOptions {\n strategy: 'file' | 'redis' | 'none';\n lockPath?: string;\n lockTimeout?: number;\n heartbeatInterval?: number;\n redisClient?: any; // Optional Redis client\n forceLeader?: boolean; // Force this instance to be leader (dev/testing)\n instanceId?: string; // Custom instance ID\n}\n\nexport interface LeaderInfo {\n instanceId: string;\n hostname: string;\n pid: number;\n electedAt: Date;\n lastHeartbeat: Date;\n metadata?: Record<string, any>;\n}\n\n/**\n * LeaderElection - Ensures only one job scheduler runs across distributed instances\n * Supports:\n * - File-based locking (for local/NFS environments)\n * - Redis-based locking (for distributed systems)\n * - Kubernetes pod detection\n * - Automatic failover with health checks\n */\nexport class LeaderElection extends EventEmitter {\n private isLeader = false;\n private strategy: 'file' | 'redis' | 'none';\n private lockPath?: string;\n private lockTimeout: number;\n private heartbeatInterval: number;\n private heartbeatTimer?: NodeJS.Timeout | undefined;\n private checkTimer?: NodeJS.Timeout | undefined;\n private logger: Logger;\n private loggerContext = 'LeaderElection';\n private instanceId: string;\n private leaderInfo?: LeaderInfo;\n private redisClient?: any;\n private isShuttingDown = false;\n private lockAcquireAttempts = 0;\n\n private static readonly REDIS_LOCK_KEY = 'moro:jobs:leader:lock';\n\n // Compare-and-refresh: rewrite the value and extend the TTL only if the stored\n // lock still belongs to this instance (ownership is fenced by instanceId).\n private static readonly REDIS_REFRESH_SCRIPT = `\nlocal cur = redis.call('GET', KEYS[1])\nif not cur then return 0 end\nlocal ok, decoded = pcall(cjson.decode, cur)\nif ok and decoded['instanceId'] == ARGV[1] then\n redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])\n return 1\nend\nreturn 0`;\n\n // Compare-and-delete: release the lock only if it still belongs to this instance.\n private static readonly REDIS_RELEASE_SCRIPT = `\nlocal cur = redis.call('GET', KEYS[1])\nif not cur then return 0 end\nlocal ok, decoded = pcall(cjson.decode, cur)\nif ok and decoded['instanceId'] == ARGV[1] then\n return redis.call('DEL', KEYS[1])\nend\nreturn 0`;\n\n constructor(logger: Logger, options: LeaderElectionOptions = { strategy: 'file' }) {\n super();\n this.logger = logger;\n this.strategy = options.strategy;\n this.lockTimeout = options.lockTimeout ?? 30000; // 30s default\n this.heartbeatInterval = options.heartbeatInterval ?? 10000; // 10s default\n this.redisClient = options.redisClient;\n\n // Generate unique instance ID\n this.instanceId =\n options.instanceId ||\n `${os.hostname()}-${process.pid}-${Date.now()}-${crypto.randomBytes(6).toString('hex')}`;\n\n // Setup lock path for file-based strategy\n if (this.strategy === 'file') {\n if (options.lockPath) {\n this.lockPath = options.lockPath;\n } else {\n const tmpDir = os.tmpdir();\n this.lockPath = path.join(tmpDir, 'moro-jobs-leader.lock');\n }\n }\n\n // Force leader mode (for dev/testing)\n if (options.forceLeader) {\n this.isLeader = true;\n this.logger.warn('Leader election forced - running as leader');\n }\n\n this.logger.debug('LeaderElection initialized', this.loggerContext, {\n strategy: this.strategy,\n instanceId: this.instanceId,\n lockPath: this.lockPath,\n forceLeader: options.forceLeader,\n });\n }\n\n /**\n * Start leader election process\n */\n public async start(): Promise<void> {\n if (this.strategy === 'none') {\n this.logger.debug('Leader election disabled, assuming leader role');\n this.becomeLeader();\n return;\n }\n\n // Check if we should participate in leader election\n if (!this.shouldParticipate()) {\n this.logger.info('Not participating in leader election (worker process/thread)');\n return;\n }\n\n this.logger.info('Starting leader election...', this.loggerContext, {\n strategy: this.strategy,\n instanceId: this.instanceId,\n });\n\n // Try to acquire leadership\n await this.tryAcquireLeadership();\n\n // Start periodic checks\n this.startPeriodicCheck();\n }\n\n /**\n * Check if this process should participate in leader election\n */\n private shouldParticipate(): boolean {\n // Only the main thread of a non-worker process can be leader. Under\n // thread clustering that is exactly the cluster primary (its worker\n // threads are !isMainThread); under process clustering it is the primary\n // process (workers have cluster.isWorker). Either way the scheduler runs\n // in the primary (Moro.startPrimaryServices), so one participant per\n // cluster - no change needed for threads.\n if (!wt().isMainThread) {\n return false;\n }\n\n if (isClusterWorkerProcess()) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Try to acquire leadership\n */\n private async tryAcquireLeadership(): Promise<boolean> {\n if (this.isShuttingDown) {\n return false;\n }\n\n this.lockAcquireAttempts++;\n\n try {\n let acquired = false;\n\n if (this.strategy === 'file') {\n acquired = await this.tryAcquireFileLock();\n } else if (this.strategy === 'redis') {\n acquired = await this.tryAcquireRedisLock();\n }\n\n if (acquired) {\n this.becomeLeader();\n return true;\n } else {\n // Check if current leader is still alive\n await this.checkLeaderHealth();\n return false;\n }\n } catch (error) {\n this.logger.error('Error during leader election', this.loggerContext, {\n error,\n attempt: this.lockAcquireAttempts,\n });\n return false;\n }\n }\n\n /**\n * Try to acquire file-based lock\n *\n * Uses O_EXCL (open flag 'wx') for the common \"no lock yet\" case so exactly one\n * instance can win the create race. An expired lock is taken over via an atomic\n * temp-write + rename. A takeover race between two instances is a last-writer-wins\n * rename, which is then reconciled by the periodic verifyLeadership() check and the\n * ownership-checked heartbeat below, so a single leader is still guaranteed.\n */\n private async tryAcquireFileLock(): Promise<boolean> {\n if (!this.lockPath) {\n throw new Error('Lock path not configured');\n }\n\n const lockInfo: LeaderInfo = {\n instanceId: this.instanceId,\n hostname: os.hostname(),\n pid: process.pid,\n electedAt: new Date(),\n lastHeartbeat: new Date(),\n metadata: this.getInstanceMetadata(),\n };\n const serialized = JSON.stringify(lockInfo);\n\n // Fast path: atomically create the lock file (O_EXCL). Fails with EEXIST if\n // another instance already holds the lock.\n try {\n const handle = await fs.promises.open(this.lockPath, 'wx');\n try {\n await handle.writeFile(serialized, 'utf-8');\n } finally {\n await handle.close();\n }\n this.leaderInfo = lockInfo;\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code !== 'EEXIST') {\n this.logger.error('Failed to acquire file lock', this.loggerContext, {\n error,\n lockPath: this.lockPath,\n });\n return false;\n }\n // Lock file already exists — fall through to the expiry/takeover check.\n }\n\n try {\n // Read existing lock and decide whether we may take it over.\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const existingLeader: LeaderInfo = JSON.parse(lockData);\n\n const lockAge = Date.now() - new Date(existingLeader.lastHeartbeat).getTime();\n\n if (lockAge < this.lockTimeout) {\n // Lock is still valid and held by another instance.\n this.leaderInfo = existingLeader;\n this.logger.debug('Leader lock held by another instance', this.loggerContext, {\n leader: existingLeader.instanceId,\n age: lockAge,\n });\n return false;\n }\n\n // Lock expired — take over with an atomic temp-write + rename.\n this.logger.warn('Leader lock expired, taking over', this.loggerContext, {\n previousLeader: existingLeader.instanceId,\n lockAge,\n });\n\n await this.atomicWriteLock(serialized);\n this.leaderInfo = lockInfo;\n return true;\n } catch (error) {\n this.logger.error('Failed to acquire file lock', this.loggerContext, {\n error,\n lockPath: this.lockPath,\n });\n return false;\n }\n }\n\n /**\n * Atomically (re)write the lock file: write to a unique temp file, then rename\n * over the target. rename() is atomic on POSIX so readers never see a partial\n * lock file.\n */\n private async atomicWriteLock(serialized: string): Promise<void> {\n if (!this.lockPath) {\n throw new Error('Lock path not configured');\n }\n\n const tempPath = `${this.lockPath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;\n await fs.promises.writeFile(tempPath, serialized, 'utf-8');\n try {\n await fs.promises.rename(tempPath, this.lockPath);\n } catch (error) {\n await fs.promises.unlink(tempPath).catch(() => {});\n throw error;\n }\n }\n\n /**\n * Try to acquire Redis-based lock\n */\n private async tryAcquireRedisLock(): Promise<boolean> {\n if (!this.redisClient) {\n throw new Error('Redis client not configured');\n }\n\n try {\n const lockKey = LeaderElection.REDIS_LOCK_KEY;\n const lockInfo: LeaderInfo = {\n instanceId: this.instanceId,\n hostname: os.hostname(),\n pid: process.pid,\n electedAt: new Date(),\n lastHeartbeat: new Date(),\n metadata: this.getInstanceMetadata(),\n };\n\n // Try to set lock with NX (only if not exists) and PX (expiration in ms)\n const result = await this.redisClient.set(\n lockKey,\n JSON.stringify(lockInfo),\n 'PX',\n this.lockTimeout,\n 'NX'\n );\n\n if (result === 'OK') {\n this.leaderInfo = lockInfo;\n return true;\n }\n\n // Lock exists, get current leader info\n const existingLock = await this.redisClient.get(lockKey);\n if (existingLock) {\n this.leaderInfo = JSON.parse(existingLock);\n }\n\n return false;\n } catch (error) {\n this.logger.error('Failed to acquire Redis lock', this.loggerContext, { error });\n return false;\n }\n }\n\n /**\n * Become leader\n */\n private becomeLeader(): void {\n if (this.isLeader) {\n return;\n }\n\n this.isLeader = true;\n this.logger.info('Became leader', this.loggerContext, {\n instanceId: this.instanceId,\n strategy: this.strategy,\n attempts: this.lockAcquireAttempts,\n });\n\n this.emit('leader:elected', {\n instanceId: this.instanceId,\n electedAt: this.leaderInfo?.electedAt,\n });\n\n // Start heartbeat\n this.startHeartbeat();\n }\n\n /**\n * Step down as leader\n */\n private async stepDown(): Promise<void> {\n if (!this.isLeader) {\n return;\n }\n\n this.logger.info('Stepping down as leader', this.loggerContext, {\n instanceId: this.instanceId,\n });\n\n this.isLeader = false;\n this.stopHeartbeat();\n\n // Release lock — only if we still own it, so we never delete a lock that\n // another instance has since acquired.\n try {\n if (this.strategy === 'file' && this.lockPath) {\n try {\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n if (currentLeader.instanceId === this.instanceId) {\n await fs.promises.unlink(this.lockPath).catch(() => {});\n }\n } catch {\n // Lock file missing or unreadable — nothing to release.\n }\n } else if (this.strategy === 'redis' && this.redisClient) {\n // Compare-and-delete: only DEL if the stored lock still belongs to us.\n await this.redisClient.eval(\n LeaderElection.REDIS_RELEASE_SCRIPT,\n 1,\n LeaderElection.REDIS_LOCK_KEY,\n this.instanceId\n );\n }\n } catch (error) {\n this.logger.error('Failed to release lock during step down', this.loggerContext, { error });\n }\n\n this.emit('leader:stepdown', { instanceId: this.instanceId });\n }\n\n /**\n * Start heartbeat to maintain leadership\n */\n private startHeartbeat(): void {\n if (this.heartbeatTimer) {\n return;\n }\n\n this.heartbeatTimer = setInterval(() => {\n void (async () => {\n if (this.isShuttingDown) {\n return;\n }\n\n try {\n await this.sendHeartbeat();\n } catch (error) {\n this.logger.error('Heartbeat failed', this.loggerContext, { error });\n // Lost leadership\n await this.stepDown();\n // Try to reacquire\n const retryTimer = setTimeout(() => {\n void this.tryAcquireLeadership();\n }, 1000);\n retryTimer.unref(); // Don't keep process alive\n }\n })();\n }, this.heartbeatInterval);\n\n // Don't keep process alive\n this.heartbeatTimer.unref();\n }\n\n /**\n * Stop heartbeat\n */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = undefined;\n }\n }\n\n /**\n * Send heartbeat to maintain lock\n */\n private async sendHeartbeat(): Promise<void> {\n if (!this.leaderInfo) {\n return;\n }\n\n this.leaderInfo.lastHeartbeat = new Date();\n\n if (this.strategy === 'file' && this.lockPath) {\n // Verify we still own the lock before refreshing it. If the file is gone or\n // now owned by another instance, we have lost leadership — throw so the\n // heartbeat handler steps us down and attempts a clean re-acquire.\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n if (currentLeader.instanceId !== this.instanceId) {\n throw new Error('Lost leadership: file lock is owned by another instance');\n }\n await this.atomicWriteLock(JSON.stringify(this.leaderInfo));\n } else if (this.strategy === 'redis' && this.redisClient) {\n // Compare-and-refresh: only rewrite + extend the TTL if we still own the lock.\n const refreshed = await this.redisClient.eval(\n LeaderElection.REDIS_REFRESH_SCRIPT,\n 1,\n LeaderElection.REDIS_LOCK_KEY,\n this.instanceId,\n JSON.stringify(this.leaderInfo),\n this.lockTimeout\n );\n if (Number(refreshed) !== 1) {\n throw new Error('Lost leadership: redis lock is no longer owned by this instance');\n }\n }\n\n this.logger.debug('Heartbeat sent', this.loggerContext, { instanceId: this.instanceId });\n this.emit('leader:heartbeat', {\n instanceId: this.instanceId,\n timestamp: this.leaderInfo.lastHeartbeat,\n });\n }\n\n /**\n * Start periodic check for leadership changes\n */\n private startPeriodicCheck(): void {\n if (this.checkTimer) {\n return;\n }\n\n const checkInterval = Math.floor(this.heartbeatInterval * 1.5);\n\n this.checkTimer = setInterval(() => {\n void (async () => {\n if (this.isShuttingDown) {\n return;\n }\n\n if (!this.isLeader) {\n // Try to acquire leadership\n await this.tryAcquireLeadership();\n } else {\n // Verify we still hold the lock\n await this.verifyLeadership();\n }\n })();\n }, checkInterval);\n\n // Don't keep process alive\n this.checkTimer.unref();\n }\n\n /**\n * Stop periodic check\n */\n private stopPeriodicCheck(): void {\n if (this.checkTimer) {\n clearInterval(this.checkTimer);\n this.checkTimer = undefined;\n }\n }\n\n /**\n * Verify we still hold leadership\n */\n private async verifyLeadership(): Promise<void> {\n if (!this.isLeader) {\n return;\n }\n\n try {\n let stillLeader = false;\n\n if (this.strategy === 'file' && this.lockPath) {\n const lockData = await fs.promises.readFile(this.lockPath, 'utf-8');\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n stillLeader = currentLeader.instanceId === this.instanceId;\n } else if (this.strategy === 'redis' && this.redisClient) {\n const lockData = await this.redisClient.get(LeaderElection.REDIS_LOCK_KEY);\n if (lockData) {\n const currentLeader: LeaderInfo = JSON.parse(lockData);\n stillLeader = currentLeader.instanceId === this.instanceId;\n }\n }\n\n if (!stillLeader) {\n this.logger.warn('Lost leadership', this.loggerContext, { instanceId: this.instanceId });\n await this.stepDown();\n }\n } catch (error) {\n this.logger.error('Failed to verify leadership', this.loggerContext, { error });\n await this.stepDown();\n }\n }\n\n /**\n * Check health of current leader\n */\n private async checkLeaderHealth(): Promise<void> {\n if (!this.leaderInfo || this.leaderInfo.instanceId === this.instanceId) {\n return;\n }\n\n const heartbeatAge = Date.now() - new Date(this.leaderInfo.lastHeartbeat).getTime();\n\n if (heartbeatAge > this.lockTimeout) {\n this.logger.warn('Current leader appears unhealthy', this.loggerContext, {\n leader: this.leaderInfo.instanceId,\n heartbeatAge,\n });\n\n this.emit('leader:unhealthy', {\n leader: this.leaderInfo.instanceId,\n heartbeatAge,\n });\n }\n }\n\n /**\n * Get instance metadata\n */\n private getInstanceMetadata(): Record<string, any> {\n const metadata: Record<string, any> = {\n nodeVersion: process.version,\n platform: process.platform,\n arch: process.arch,\n uptime: process.uptime(),\n };\n\n // K8s detection\n if (process.env.KUBERNETES_SERVICE_HOST) {\n metadata.kubernetes = {\n podName: process.env.HOSTNAME || os.hostname(),\n namespace: process.env.POD_NAMESPACE,\n nodeName: process.env.NODE_NAME,\n };\n }\n\n // Container detection\n if (process.env.CONTAINER) {\n metadata.container = process.env.CONTAINER;\n }\n\n return metadata;\n }\n\n /**\n * Check if current instance is leader\n */\n public isCurrentLeader(): boolean {\n return this.isLeader;\n }\n\n /**\n * Get current leader info\n */\n public getLeaderInfo(): LeaderInfo | undefined {\n return this.leaderInfo ? { ...this.leaderInfo } : undefined;\n }\n\n /**\n * Get instance ID\n */\n public getInstanceId(): string {\n return this.instanceId;\n }\n\n /**\n * Force step down (for testing/admin)\n */\n public async forceStepDown(): Promise<void> {\n if (!this.isLeader) {\n return;\n }\n\n this.logger.warn('Forced step down requested', this.loggerContext, {\n instanceId: this.instanceId,\n });\n await this.stepDown();\n }\n\n /**\n * Shutdown and cleanup\n */\n public async shutdown(): Promise<void> {\n this.logger.info('LeaderElection shutting down...', this.loggerContext, {\n instanceId: this.instanceId,\n wasLeader: this.isLeader,\n });\n\n this.isShuttingDown = true;\n\n this.stopHeartbeat();\n this.stopPeriodicCheck();\n\n if (this.isLeader) {\n await this.stepDown();\n }\n\n this.removeAllListeners();\n this.logger.info('LeaderElection shutdown complete');\n }\n}\n"]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// WebSocket Manager for Moro Framework
|
|
2
2
|
// Manages WebSocket connections using pluggable adapters
|
|
3
|
-
import
|
|
3
|
+
import { requireBuiltin } from '../utilities/builtin.js';
|
|
4
|
+
// zlib on first use (core/utilities/builtin.ts).
|
|
5
|
+
const zlibMod = () => requireBuiltin('zlib');
|
|
4
6
|
import { CircuitBreaker } from '../utilities/index.js';
|
|
5
7
|
import { createFrameworkLogger } from '../logger/index.js';
|
|
6
8
|
import { normalizeValidationError } from '../validation/schema-interface.js';
|
|
@@ -234,7 +236,7 @@ export class WebSocketManager {
|
|
|
234
236
|
if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
|
|
235
237
|
// This is gzipped data
|
|
236
238
|
try {
|
|
237
|
-
return
|
|
239
|
+
return zlibMod().gunzipSync(buffer);
|
|
238
240
|
}
|
|
239
241
|
catch {
|
|
240
242
|
return buffer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-manager.js","sourceRoot":"","sources":["../../../src/core/networking/websocket-manager.ts"],"names":[],"mappings":"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","sourcesContent":["// WebSocket Manager for Moro Framework\n// Manages WebSocket connections using pluggable adapters\nimport * as zlib from 'zlib';\nimport { Container } from '../utilities/index.js';\nimport { CircuitBreaker } from '../utilities/index.js';\nimport { ModuleConfig, WebSocketDefinition } from '../../types/module.js';\nimport { WebSocketAdapter, WebSocketNamespace, WebSocketConnection } from './websocket-adapter.js';\nimport { createFrameworkLogger } from '../logger/index.js';\nimport { normalizeValidationError } from '../validation/schema-interface.js';\n\nexport class WebSocketManager {\n private circuitBreakers = new Map<string, CircuitBreaker>();\n private rateLimiters = new Map<string, Map<string, { count: number; resetTime: number }>>();\n private logger = createFrameworkLogger('WebSocketManager');\n\n constructor(\n private adapter: WebSocketAdapter,\n private container: Container\n ) {\n this.logger.info(`Initialized with ${adapter.getAdapterName()} adapter`, 'AdapterInit');\n }\n\n /**\n * Set custom ID generator for the adapter\n */\n setCustomIdGenerator(generator: () => string): void {\n this.adapter.setCustomIdGenerator(generator);\n }\n\n /**\n * Enable compression for the adapter\n */\n enableCompression(options?: any): void {\n this.adapter.setCompression(true, options);\n }\n\n /**\n * Get the underlying adapter\n */\n getAdapter(): WebSocketAdapter {\n return this.adapter;\n }\n\n /**\n * Get connection count across all namespaces\n */\n getConnectionCount(): number {\n return this.adapter.getConnectionCount();\n }\n\n /**\n * Create or get a namespace\n */\n getNamespace(namespace: string): WebSocketNamespace {\n return this.adapter.createNamespace(namespace);\n }\n\n /**\n * Register WebSocket handlers for a module\n */\n async registerHandler(\n namespace: WebSocketNamespace,\n wsConfig: WebSocketDefinition,\n moduleConfig: ModuleConfig\n ): Promise<void> {\n namespace.on('connection', (socket: WebSocketConnection) => {\n this.logger.debug(`New connection: ${socket.id}`, 'Connection', {\n namespace: namespace.constructor.name,\n module: moduleConfig.name,\n });\n\n this.setupSocketMiddleware(socket, moduleConfig.name);\n this.setupSocketHandlers(socket, wsConfig, moduleConfig);\n });\n }\n\n /**\n * Setup socket-specific middleware\n */\n private setupSocketMiddleware(socket: WebSocketConnection, moduleName: string): void {\n // Add module context to socket data\n socket.data.module = moduleName;\n socket.data.connectedAt = Date.now();\n\n // Setup heartbeat if supported\n if (socket.compressedEmit) {\n const heartbeatInterval = setInterval(() => {\n if (socket.connected) {\n socket.emit('heartbeat', { timestamp: Date.now() });\n } else {\n clearInterval(heartbeatInterval);\n }\n }, 30000); // 30 seconds\n\n socket.on('disconnect', () => {\n clearInterval(heartbeatInterval);\n });\n }\n\n // Log disconnection\n socket.on('disconnect', () => {\n this.logger.debug(`Socket disconnected: ${socket.id}`, 'Disconnect', {\n module: moduleName,\n duration: Date.now() - socket.data.connectedAt,\n });\n });\n }\n\n /**\n * Setup handlers for a specific WebSocket configuration\n */\n private setupSocketHandlers(\n socket: WebSocketConnection,\n wsConfig: WebSocketDefinition,\n moduleConfig: ModuleConfig\n ): void {\n socket.on(wsConfig.event, (data: any, callback?: CallableFunction) => {\n void (async () => {\n const handlerKey = `${moduleConfig.name}.${wsConfig.handler}`;\n\n try {\n // Rate limiting\n if (\n wsConfig.rateLimit &&\n !this.checkRateLimit(socket.id, handlerKey, wsConfig.rateLimit)\n ) {\n const error = {\n success: false,\n error: 'Rate limit exceeded',\n code: 'RATE_LIMIT',\n };\n if (callback) callback(error);\n else socket.emit('error', error);\n return;\n }\n\n // Universal validation (works with any ValidationSchema)\n if (wsConfig.validation) {\n try {\n data = await wsConfig.validation.parseAsync(data);\n } catch (validationError: any) {\n // Handle universal validation errors\n const normalizedError = normalizeValidationError(validationError);\n const error = {\n success: false,\n error: 'Validation failed',\n details: normalizedError.issues.map((issue: any) => ({\n field: issue.path.length > 0 ? issue.path.join('.') : 'data',\n message: issue.message,\n code: issue.code,\n })),\n };\n if (callback) callback(error);\n else socket.emit('error', error);\n return;\n }\n }\n\n // Circuit breaker protection\n const circuitBreaker = this.getCircuitBreaker(handlerKey);\n\n const result = await circuitBreaker.execute(async () => {\n const controller = this.container.resolve(moduleConfig.name);\n return await (controller as any)[wsConfig.handler](socket, data);\n });\n\n // Handle response\n if (callback) {\n callback({ success: true, data: result });\n } else if (result !== undefined) {\n socket.emit(`${wsConfig.event}:response`, { success: true, data: result });\n }\n\n // Handle room operations\n if (wsConfig.rooms) {\n wsConfig.rooms.forEach(room => socket.join(room));\n }\n\n // Handle broadcasting\n if (wsConfig.broadcast && result !== undefined) {\n socket.broadcast.emit(wsConfig.event, { success: true, data: result });\n }\n } catch (error) {\n this.logger.error('WebSocket handler error', 'HandlerError', {\n handler: handlerKey,\n error: error instanceof Error ? error.message : String(error),\n socketId: socket.id,\n });\n\n const errorResponse = {\n success: false,\n error: 'Internal server error',\n code: 'HANDLER_ERROR',\n };\n\n if (callback) callback(errorResponse);\n else socket.emit('error', errorResponse);\n }\n })();\n });\n }\n\n /**\n * Get or create circuit breaker for handler\n */\n private getCircuitBreaker(handlerKey: string): CircuitBreaker {\n if (!this.circuitBreakers.has(handlerKey)) {\n const circuitBreaker = new CircuitBreaker({\n failureThreshold: 5,\n resetTimeout: 60000,\n monitoringPeriod: 10000,\n });\n this.circuitBreakers.set(handlerKey, circuitBreaker);\n }\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return this.circuitBreakers.get(handlerKey)!;\n }\n\n /**\n * Check rate limit for socket and handler\n */\n private checkRateLimit(\n socketId: string,\n handlerKey: string,\n rateLimit: { requests: number; window: number }\n ): boolean {\n if (!this.rateLimiters.has(handlerKey)) {\n this.rateLimiters.set(handlerKey, new Map());\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const handlerLimiter = this.rateLimiters.get(handlerKey)!;\n const now = Date.now();\n const windowStart = now - rateLimit.window;\n\n // Clean old entries\n for (const [id, data] of handlerLimiter.entries()) {\n if (data.resetTime < windowStart) {\n handlerLimiter.delete(id);\n }\n }\n\n // Check current socket\n const socketData = handlerLimiter.get(socketId);\n if (!socketData) {\n handlerLimiter.set(socketId, { count: 1, resetTime: now });\n return true;\n }\n\n if (socketData.resetTime < windowStart) {\n socketData.count = 1;\n socketData.resetTime = now;\n return true;\n }\n\n if (socketData.count >= rateLimit.requests) {\n return false;\n }\n\n socketData.count++;\n return true;\n }\n\n /**\n * Process binary data efficiently\n */\n private processBinaryData(buffer: Buffer): any {\n // Example binary processing - can be extended based on needs\n if (buffer.length === 0) return buffer;\n\n // Simple compression check\n if (buffer[0] === 0x1f && buffer[1] === 0x8b) {\n // This is gzipped data\n try {\n return zlib.gunzipSync(buffer);\n } catch {\n return buffer;\n }\n }\n\n return buffer;\n }\n\n /**\n * Check if data should be compressed\n */\n private shouldCompress(data: any): boolean {\n if (typeof data === 'string') {\n return data.length > 1024;\n }\n if (Buffer.isBuffer(data)) {\n return data.length > 1024;\n }\n if (typeof data === 'object') {\n return JSON.stringify(data).length > 1024;\n }\n return false;\n }\n\n /**\n * Close the WebSocket manager and underlying adapter\n */\n async close(): Promise<void> {\n this.logger.info('Closing WebSocket manager', 'Shutdown');\n\n // Clear rate limiters\n this.rateLimiters.clear();\n\n // Clear circuit breakers\n this.circuitBreakers.clear();\n\n // Close adapter\n await this.adapter.close();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"websocket-manager.js","sourceRoot":"","sources":["../../../src/core/networking/websocket-manager.ts"],"names":[],"mappings":"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","sourcesContent":["// WebSocket Manager for Moro Framework\n// Manages WebSocket connections using pluggable adapters\nimport { requireBuiltin } from '../utilities/builtin.js';\n\n// zlib on first use (core/utilities/builtin.ts).\nconst zlibMod = () => requireBuiltin<typeof import('zlib')>('zlib');\nimport { Container } from '../utilities/index.js';\nimport { CircuitBreaker } from '../utilities/index.js';\nimport { ModuleConfig, WebSocketDefinition } from '../../types/module.js';\nimport { WebSocketAdapter, WebSocketNamespace, WebSocketConnection } from './websocket-adapter.js';\nimport { createFrameworkLogger } from '../logger/index.js';\nimport { normalizeValidationError } from '../validation/schema-interface.js';\n\nexport class WebSocketManager {\n private circuitBreakers = new Map<string, CircuitBreaker>();\n private rateLimiters = new Map<string, Map<string, { count: number; resetTime: number }>>();\n private logger = createFrameworkLogger('WebSocketManager');\n\n constructor(\n private adapter: WebSocketAdapter,\n private container: Container\n ) {\n this.logger.info(`Initialized with ${adapter.getAdapterName()} adapter`, 'AdapterInit');\n }\n\n /**\n * Set custom ID generator for the adapter\n */\n setCustomIdGenerator(generator: () => string): void {\n this.adapter.setCustomIdGenerator(generator);\n }\n\n /**\n * Enable compression for the adapter\n */\n enableCompression(options?: any): void {\n this.adapter.setCompression(true, options);\n }\n\n /**\n * Get the underlying adapter\n */\n getAdapter(): WebSocketAdapter {\n return this.adapter;\n }\n\n /**\n * Get connection count across all namespaces\n */\n getConnectionCount(): number {\n return this.adapter.getConnectionCount();\n }\n\n /**\n * Create or get a namespace\n */\n getNamespace(namespace: string): WebSocketNamespace {\n return this.adapter.createNamespace(namespace);\n }\n\n /**\n * Register WebSocket handlers for a module\n */\n async registerHandler(\n namespace: WebSocketNamespace,\n wsConfig: WebSocketDefinition,\n moduleConfig: ModuleConfig\n ): Promise<void> {\n namespace.on('connection', (socket: WebSocketConnection) => {\n this.logger.debug(`New connection: ${socket.id}`, 'Connection', {\n namespace: namespace.constructor.name,\n module: moduleConfig.name,\n });\n\n this.setupSocketMiddleware(socket, moduleConfig.name);\n this.setupSocketHandlers(socket, wsConfig, moduleConfig);\n });\n }\n\n /**\n * Setup socket-specific middleware\n */\n private setupSocketMiddleware(socket: WebSocketConnection, moduleName: string): void {\n // Add module context to socket data\n socket.data.module = moduleName;\n socket.data.connectedAt = Date.now();\n\n // Setup heartbeat if supported\n if (socket.compressedEmit) {\n const heartbeatInterval = setInterval(() => {\n if (socket.connected) {\n socket.emit('heartbeat', { timestamp: Date.now() });\n } else {\n clearInterval(heartbeatInterval);\n }\n }, 30000); // 30 seconds\n\n socket.on('disconnect', () => {\n clearInterval(heartbeatInterval);\n });\n }\n\n // Log disconnection\n socket.on('disconnect', () => {\n this.logger.debug(`Socket disconnected: ${socket.id}`, 'Disconnect', {\n module: moduleName,\n duration: Date.now() - socket.data.connectedAt,\n });\n });\n }\n\n /**\n * Setup handlers for a specific WebSocket configuration\n */\n private setupSocketHandlers(\n socket: WebSocketConnection,\n wsConfig: WebSocketDefinition,\n moduleConfig: ModuleConfig\n ): void {\n socket.on(wsConfig.event, (data: any, callback?: CallableFunction) => {\n void (async () => {\n const handlerKey = `${moduleConfig.name}.${wsConfig.handler}`;\n\n try {\n // Rate limiting\n if (\n wsConfig.rateLimit &&\n !this.checkRateLimit(socket.id, handlerKey, wsConfig.rateLimit)\n ) {\n const error = {\n success: false,\n error: 'Rate limit exceeded',\n code: 'RATE_LIMIT',\n };\n if (callback) callback(error);\n else socket.emit('error', error);\n return;\n }\n\n // Universal validation (works with any ValidationSchema)\n if (wsConfig.validation) {\n try {\n data = await wsConfig.validation.parseAsync(data);\n } catch (validationError: any) {\n // Handle universal validation errors\n const normalizedError = normalizeValidationError(validationError);\n const error = {\n success: false,\n error: 'Validation failed',\n details: normalizedError.issues.map((issue: any) => ({\n field: issue.path.length > 0 ? issue.path.join('.') : 'data',\n message: issue.message,\n code: issue.code,\n })),\n };\n if (callback) callback(error);\n else socket.emit('error', error);\n return;\n }\n }\n\n // Circuit breaker protection\n const circuitBreaker = this.getCircuitBreaker(handlerKey);\n\n const result = await circuitBreaker.execute(async () => {\n const controller = this.container.resolve(moduleConfig.name);\n return await (controller as any)[wsConfig.handler](socket, data);\n });\n\n // Handle response\n if (callback) {\n callback({ success: true, data: result });\n } else if (result !== undefined) {\n socket.emit(`${wsConfig.event}:response`, { success: true, data: result });\n }\n\n // Handle room operations\n if (wsConfig.rooms) {\n wsConfig.rooms.forEach(room => socket.join(room));\n }\n\n // Handle broadcasting\n if (wsConfig.broadcast && result !== undefined) {\n socket.broadcast.emit(wsConfig.event, { success: true, data: result });\n }\n } catch (error) {\n this.logger.error('WebSocket handler error', 'HandlerError', {\n handler: handlerKey,\n error: error instanceof Error ? error.message : String(error),\n socketId: socket.id,\n });\n\n const errorResponse = {\n success: false,\n error: 'Internal server error',\n code: 'HANDLER_ERROR',\n };\n\n if (callback) callback(errorResponse);\n else socket.emit('error', errorResponse);\n }\n })();\n });\n }\n\n /**\n * Get or create circuit breaker for handler\n */\n private getCircuitBreaker(handlerKey: string): CircuitBreaker {\n if (!this.circuitBreakers.has(handlerKey)) {\n const circuitBreaker = new CircuitBreaker({\n failureThreshold: 5,\n resetTimeout: 60000,\n monitoringPeriod: 10000,\n });\n this.circuitBreakers.set(handlerKey, circuitBreaker);\n }\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return this.circuitBreakers.get(handlerKey)!;\n }\n\n /**\n * Check rate limit for socket and handler\n */\n private checkRateLimit(\n socketId: string,\n handlerKey: string,\n rateLimit: { requests: number; window: number }\n ): boolean {\n if (!this.rateLimiters.has(handlerKey)) {\n this.rateLimiters.set(handlerKey, new Map());\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const handlerLimiter = this.rateLimiters.get(handlerKey)!;\n const now = Date.now();\n const windowStart = now - rateLimit.window;\n\n // Clean old entries\n for (const [id, data] of handlerLimiter.entries()) {\n if (data.resetTime < windowStart) {\n handlerLimiter.delete(id);\n }\n }\n\n // Check current socket\n const socketData = handlerLimiter.get(socketId);\n if (!socketData) {\n handlerLimiter.set(socketId, { count: 1, resetTime: now });\n return true;\n }\n\n if (socketData.resetTime < windowStart) {\n socketData.count = 1;\n socketData.resetTime = now;\n return true;\n }\n\n if (socketData.count >= rateLimit.requests) {\n return false;\n }\n\n socketData.count++;\n return true;\n }\n\n /**\n * Process binary data efficiently\n */\n private processBinaryData(buffer: Buffer): any {\n // Example binary processing - can be extended based on needs\n if (buffer.length === 0) return buffer;\n\n // Simple compression check\n if (buffer[0] === 0x1f && buffer[1] === 0x8b) {\n // This is gzipped data\n try {\n return zlibMod().gunzipSync(buffer);\n } catch {\n return buffer;\n }\n }\n\n return buffer;\n }\n\n /**\n * Check if data should be compressed\n */\n private shouldCompress(data: any): boolean {\n if (typeof data === 'string') {\n return data.length > 1024;\n }\n if (Buffer.isBuffer(data)) {\n return data.length > 1024;\n }\n if (typeof data === 'object') {\n return JSON.stringify(data).length > 1024;\n }\n return false;\n }\n\n /**\n * Close the WebSocket manager and underlying adapter\n */\n async close(): Promise<void> {\n this.logger.info('Closing WebSocket manager', 'Shutdown');\n\n // Clear rate limiters\n this.rateLimiters.clear();\n\n // Clear circuit breakers\n this.circuitBreakers.clear();\n\n // Close adapter\n await this.adapter.close();\n }\n}\n"]}
|
|
@@ -611,7 +611,9 @@ export class UnifiedRouter {
|
|
|
611
611
|
break;
|
|
612
612
|
const beforeLen = beforeMw.length;
|
|
613
613
|
for (let i = 0; i < beforeLen; i++) {
|
|
614
|
-
|
|
614
|
+
const r = this.executeMiddleware(beforeMw[i], req, res);
|
|
615
|
+
if (r)
|
|
616
|
+
await r;
|
|
615
617
|
if (res.headersSent)
|
|
616
618
|
return;
|
|
617
619
|
}
|
|
@@ -637,7 +639,9 @@ export class UnifiedRouter {
|
|
|
637
639
|
}),
|
|
638
640
|
});
|
|
639
641
|
}
|
|
640
|
-
|
|
642
|
+
const r = this.executeMiddleware(route.authMiddleware, req, res);
|
|
643
|
+
if (r)
|
|
644
|
+
await r;
|
|
641
645
|
}
|
|
642
646
|
break;
|
|
643
647
|
case 'validation':
|
|
@@ -658,7 +662,9 @@ export class UnifiedRouter {
|
|
|
658
662
|
break;
|
|
659
663
|
const transformLen = transformMw.length;
|
|
660
664
|
for (let i = 0; i < transformLen; i++) {
|
|
661
|
-
|
|
665
|
+
const r = this.executeMiddleware(transformMw[i], req, res);
|
|
666
|
+
if (r)
|
|
667
|
+
await r;
|
|
662
668
|
if (res.headersSent)
|
|
663
669
|
return;
|
|
664
670
|
}
|
|
@@ -682,7 +688,9 @@ export class UnifiedRouter {
|
|
|
682
688
|
break;
|
|
683
689
|
const afterLen = afterMw.length;
|
|
684
690
|
for (let i = 0; i < afterLen; i++) {
|
|
685
|
-
|
|
691
|
+
const r = this.executeMiddleware(afterMw[i], req, res);
|
|
692
|
+
if (r)
|
|
693
|
+
await r;
|
|
686
694
|
if (res.headersSent)
|
|
687
695
|
return;
|
|
688
696
|
}
|
|
@@ -695,7 +703,9 @@ export class UnifiedRouter {
|
|
|
695
703
|
break;
|
|
696
704
|
const middlewareLen = middleware.length;
|
|
697
705
|
for (let i = 0; i < middlewareLen; i++) {
|
|
698
|
-
|
|
706
|
+
const r = this.executeMiddleware(middleware[i], req, res);
|
|
707
|
+
if (r)
|
|
708
|
+
await r;
|
|
699
709
|
if (res.headersSent)
|
|
700
710
|
return;
|
|
701
711
|
}
|
|
@@ -703,31 +713,52 @@ export class UnifiedRouter {
|
|
|
703
713
|
}
|
|
704
714
|
}
|
|
705
715
|
}
|
|
706
|
-
|
|
716
|
+
// Sync-aware: a middleware that returns a non-thenable always settled before
|
|
717
|
+
// the caller could await it (next() called, or the auto-advance below), so
|
|
718
|
+
// the promise was pure overhead - one allocation plus an async suspension per
|
|
719
|
+
// middleware per request. Returns undefined in that case; callers skip the
|
|
720
|
+
// await. A promise is only built when the middleware is genuinely async.
|
|
721
|
+
// Error/advance semantics are unchanged.
|
|
722
|
+
executeMiddleware(middleware, req, res) {
|
|
723
|
+
let resolved = false;
|
|
724
|
+
let settle;
|
|
725
|
+
const next = () => {
|
|
726
|
+
if (resolved)
|
|
727
|
+
return;
|
|
728
|
+
resolved = true;
|
|
729
|
+
if (settle)
|
|
730
|
+
settle();
|
|
731
|
+
};
|
|
732
|
+
let result;
|
|
733
|
+
try {
|
|
734
|
+
result = middleware(req, res, next);
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
// Matches the previous promise form: a throw after next() already
|
|
738
|
+
// advanced the chain was swallowed; otherwise it rejects/throws.
|
|
739
|
+
if (resolved)
|
|
740
|
+
return;
|
|
741
|
+
resolved = true;
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
// Duck typing faster than instanceof
|
|
745
|
+
const isThenable = result && typeof result.then === 'function';
|
|
746
|
+
if (!isThenable) {
|
|
747
|
+
// Sync middleware: previously this either resolved via next() or was
|
|
748
|
+
// auto-advanced - settled either way before any await could observe it.
|
|
749
|
+
next();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (resolved) {
|
|
753
|
+
// next() ran synchronously, so the chain already advanced. Keep the
|
|
754
|
+
// returned promise's rejection handled, exactly as the old .catch(reject)
|
|
755
|
+
// did on an already-resolved promise, so it never surfaces as unhandled.
|
|
756
|
+
result.then(undefined, () => { });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
707
759
|
return new Promise((resolve, reject) => {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (!resolved) {
|
|
711
|
-
resolved = true;
|
|
712
|
-
resolve();
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
try {
|
|
716
|
-
const result = middleware(req, res, next);
|
|
717
|
-
// Duck typing faster than instanceof
|
|
718
|
-
if (result && typeof result.then === 'function') {
|
|
719
|
-
result.then(() => !resolved && next()).catch(reject);
|
|
720
|
-
}
|
|
721
|
-
else if (!resolved) {
|
|
722
|
-
next();
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
catch (error) {
|
|
726
|
-
if (!resolved) {
|
|
727
|
-
resolved = true;
|
|
728
|
-
reject(error);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
760
|
+
settle = resolve;
|
|
761
|
+
result.then(() => next(), reject);
|
|
731
762
|
});
|
|
732
763
|
}
|
|
733
764
|
// ===== Helper Methods =====
|