@ultrst-armada/krnl 1.0.1 → 1.0.3
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/README.md +67 -65
- package/package.json +13 -9
- package/packages/cli/dist/bin/krnl.cjs +1535 -9508
- package/packages/cli/dist/bin/krnl.js +55 -1915
- package/packages/cli/dist/chunk-6SHSECXR.js +1628 -0
- package/packages/cli/dist/index.cjs +1244 -7373
- package/packages/cli/dist/index.d.cts +45 -2
- package/packages/cli/dist/index.d.ts +45 -2
- package/packages/cli/dist/index.js +5 -20
- package/packages/cli/package.json +17 -10
- package/packages/core/dist/index.cjs +3039 -141
- package/packages/core/dist/index.d.cts +633 -27
- package/packages/core/dist/index.d.ts +633 -27
- package/packages/core/dist/index.js +3028 -139
- package/packages/core/package.json +15 -7
- package/packages/create-krnl/dist/index.cjs +322 -316
- package/packages/create-krnl/dist/index.js +322 -316
- package/packages/create-krnl/package.json +5 -3
- package/packages/eslint-plugin/dist/index.cjs +17 -55
- package/packages/eslint-plugin/dist/index.d.cts +8 -43
- package/packages/eslint-plugin/dist/index.d.ts +8 -43
- package/packages/eslint-plugin/dist/index.js +17 -55
- package/packages/eslint-plugin/package.json +5 -4
- package/packages/validate/dist/index.cjs +692 -361
- package/packages/validate/dist/index.d.cts +12 -14
- package/packages/validate/dist/index.d.ts +12 -14
- package/packages/validate/dist/index.js +692 -361
- package/packages/validate/package.json +14 -7
- package/packages/cli/dist/K_ENT_Entity-PQ5MURQY-ZG6FZXXX.js +0 -13
- package/packages/cli/dist/K_LIB_Krnl_Aiko_Client-OPZAVZCW-K4CRQYA5.js +0 -8
- package/packages/cli/dist/K_LIB_Krnl_Ep_Tools-ROOE2TQF-LNPXZYOE.js +0 -110
- package/packages/cli/dist/K_LIB_Krnl_Frontend_Builder-2ZDUH6PF-3ALZPNX4.js +0 -11
- package/packages/cli/dist/K_LIB_Krnl_Frontend_Generator-UYNRNX2X-PJTICYSW.js +0 -11
- package/packages/cli/dist/K_LIB_Krnl_Registry_Client-NXV6D2QW-M5TYU55O.js +0 -8
- package/packages/cli/dist/K_LIB_Krnl_SubApp_Engine-7HR5HWW2-H6STOXRK.js +0 -20
- package/packages/cli/dist/K_LIB_Krnl_SubApp_Registry-7XYGDKJK-XPJXSF7D.js +0 -19
- package/packages/cli/dist/K_LIB_Krnl_SubApp_Worker_Host-CQVTGGG7-CUYGTWOW.js +0 -7
- package/packages/cli/dist/K_LIB_Krnl_System_App_Adapter-M6FJKLGU-VR6T4N6J.js +0 -17
- package/packages/cli/dist/K_LIB_Krnl_System_Base_Functionality_APIs-ZZUZ6ZG5-RLJ4QWDP.js +0 -58
- package/packages/cli/dist/K_LIB_Krnl_System_Magnetic_Loader-XBQO2KXF-BOQGUASC.js +0 -22
- package/packages/cli/dist/K_LIB_Krnl_System_Unified_Http_Adapter-RCEGP3OG-XJI7KIT5.js +0 -8
- package/packages/cli/dist/K_LIB_Krnl_Telemetry-77FRZ3KV-K5VCWAFW.js +0 -9
- package/packages/cli/dist/chunk-3TRM6CYL.js +0 -152
- package/packages/cli/dist/chunk-5JDK35TG.js +0 -3872
- package/packages/cli/dist/chunk-63PTK33I.js +0 -997
- package/packages/cli/dist/chunk-6NIKAMUZ.js +0 -82
- package/packages/cli/dist/chunk-BOXWWXQG.js +0 -400
- package/packages/cli/dist/chunk-EEKLK7QJ.js +0 -565
- package/packages/cli/dist/chunk-EG7T2IAD.js +0 -107
- package/packages/cli/dist/chunk-F5EHXE2O.js +0 -1036
- package/packages/cli/dist/chunk-F65ANSQG.js +0 -269
- package/packages/cli/dist/chunk-FDNCADFE.js +0 -71
- package/packages/cli/dist/chunk-GDLRMDAN.js +0 -127
- package/packages/cli/dist/chunk-HWMTCMOS.js +0 -656
- package/packages/cli/dist/chunk-I74QIQOD.js +0 -5536
- package/packages/cli/dist/chunk-JTLSTO25.js +0 -71
- package/packages/cli/dist/chunk-JULBBJ27.js +0 -1033
- package/packages/cli/dist/chunk-OHVMVRBP.js +0 -117
- package/packages/cli/dist/chunk-P2PCLEKX.js +0 -666
- package/packages/cli/dist/chunk-PJQIUOX2.js +0 -7767
- package/packages/cli/dist/chunk-PPOF3DJW.js +0 -471
- package/packages/cli/dist/chunk-Q45257P6.js +0 -173
- package/packages/cli/dist/chunk-QNROWGBI.js +0 -1036
- package/packages/cli/dist/chunk-RJNYUXZD.js +0 -977
- package/packages/cli/dist/chunk-SRTOPJ5W.js +0 -104
- package/packages/cli/dist/chunk-TCEX3MPS.js +0 -702
- package/packages/cli/dist/chunk-TNOAJO2J.js +0 -675
- package/packages/cli/dist/chunk-UR2SUCSF.js +0 -701
- package/packages/cli/dist/chunk-X5LOIFM7.js +0 -68
- package/packages/cli/dist/chunk-XIRFOO3H.js +0 -589
- package/packages/cli/dist/chunk-ZAAV3JXQ.js +0 -809
- package/packages/core/dist/K_ENT_Entity-4MB7CU4A.js +0 -12
- package/packages/core/dist/K_ENT_Entity-DEJGIDGH.js +0 -12
- package/packages/core/dist/K_ENT_Entity-FXZO5UZ2.js +0 -12
- package/packages/core/dist/K_ENT_Entity-HDSCXC5F.js +0 -12
- package/packages/core/dist/K_ENT_Entity-JILABHLW.js +0 -12
- package/packages/core/dist/K_ENT_Entity-PQ5MURQY.js +0 -13
- package/packages/core/dist/K_ENT_Entity-ROAKDIHW.js +0 -12
- package/packages/core/dist/K_ENT_Entity-TBVFSKDV.js +0 -12
- package/packages/core/dist/K_ENT_Entity-ZFLKG3JO.js +0 -12
- package/packages/core/dist/K_LIB_Krnl_Aiko_Client-OPZAVZCW.js +0 -8
- package/packages/core/dist/K_LIB_Krnl_Ep_Tools-ROOE2TQF.js +0 -110
- package/packages/core/dist/K_LIB_Krnl_Frontend_Builder-2ZDUH6PF.js +0 -9
- package/packages/core/dist/K_LIB_Krnl_Frontend_Generator-UYNRNX2X.js +0 -9
- package/packages/core/dist/K_LIB_Krnl_Registry_Client-NXV6D2QW.js +0 -8
- package/packages/core/dist/K_LIB_Krnl_SubApp_Engine-7HR5HWW2.js +0 -20
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-4C6GBQAG.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-6V23A6F6.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-7XYGDKJK.js +0 -19
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-DYLLC5G2.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-F4FOFAUP.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-FBOXXMI3.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-Q73BWLNP.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-QM2C4MVA.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-QUGI6SMP.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-RGMTBHNP.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Registry-RWR7UNDW.js +0 -18
- package/packages/core/dist/K_LIB_Krnl_SubApp_Worker_Host-CQVTGGG7.js +0 -7
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-32MFJJ5Z.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-4J4CEWRY.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-A366YHF2.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-LK4LYJHI.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-M6FJKLGU.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-NW3K63II.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-QDHTI5ZV.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-VHCT6VIW.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-WBRKF24R.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-XKYQYT6Q.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_App_Adapter-Z2PWZYDZ.js +0 -17
- package/packages/core/dist/K_LIB_Krnl_System_Base_Functionality_APIs-ZZUZ6ZG5.js +0 -58
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-2EZU7QF4.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-6G3AQNAV.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-B2NZMQUG.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-C2RABMZP.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-HHE3NZ4F.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-LHFGR6WK.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-M74BLM3N.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-PT6ZA6EA.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-SP52V3CX.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-XBQO2KXF.js +0 -22
- package/packages/core/dist/K_LIB_Krnl_System_Magnetic_Loader-YNSEUHJJ.js +0 -21
- package/packages/core/dist/K_LIB_Krnl_System_Unified_Http_Adapter-M6SBHIVA.js +0 -8
- package/packages/core/dist/K_LIB_Krnl_System_Unified_Http_Adapter-RCEGP3OG.js +0 -8
- package/packages/core/dist/K_LIB_Krnl_Telemetry-77FRZ3KV.js +0 -9
- package/packages/core/dist/chunk-33L2J7V7.js +0 -82
- package/packages/core/dist/chunk-3ES4K2ZP.js +0 -817
- package/packages/core/dist/chunk-3SMHVVLL.js +0 -82
- package/packages/core/dist/chunk-3UEZ7JQA.js +0 -4877
- package/packages/core/dist/chunk-3XIE6KBD.js +0 -4877
- package/packages/core/dist/chunk-46ZLCML6.js +0 -107
- package/packages/core/dist/chunk-4UEL3DWJ.js +0 -107
- package/packages/core/dist/chunk-54GWZ5PC.js +0 -82
- package/packages/core/dist/chunk-6QCYRS5L.js +0 -127
- package/packages/core/dist/chunk-6ULMHNXR.js +0 -104
- package/packages/core/dist/chunk-7WUETJEV.js +0 -117
- package/packages/core/dist/chunk-A5KRAYDB.js +0 -107
- package/packages/core/dist/chunk-ANEAZ3PO.js +0 -107
- package/packages/core/dist/chunk-BE76RL6F.js +0 -82
- package/packages/core/dist/chunk-CIARVYDO.js +0 -107
- package/packages/core/dist/chunk-CR2ANE62.js +0 -107
- package/packages/core/dist/chunk-DI27MOR7.js +0 -173
- package/packages/core/dist/chunk-E6RLFHM5.js +0 -596
- package/packages/core/dist/chunk-EELAPMNV.js +0 -4877
- package/packages/core/dist/chunk-ENZ2RQVJ.js +0 -107
- package/packages/core/dist/chunk-F2U6CX4F.js +0 -4877
- package/packages/core/dist/chunk-FKG64OHL.js +0 -82
- package/packages/core/dist/chunk-FYAZJYM5.js +0 -772
- package/packages/core/dist/chunk-G4NP4RDQ.js +0 -82
- package/packages/core/dist/chunk-GADIRLRS.js +0 -473
- package/packages/core/dist/chunk-GR6CSLQJ.js +0 -772
- package/packages/core/dist/chunk-HDS7AT6O.js +0 -772
- package/packages/core/dist/chunk-HIMEZUU2.js +0 -71
- package/packages/core/dist/chunk-IB4UHG5I.js +0 -596
- package/packages/core/dist/chunk-J7ZYNVRS.js +0 -772
- package/packages/core/dist/chunk-JVLZU4K7.js +0 -604
- package/packages/core/dist/chunk-JYXXI7Q3.js +0 -82
- package/packages/core/dist/chunk-K54K2KZW.js +0 -192
- package/packages/core/dist/chunk-K6VHLBNH.js +0 -3316
- package/packages/core/dist/chunk-KCBP7ZYA.js +0 -596
- package/packages/core/dist/chunk-KFQGP6VL.js +0 -33
- package/packages/core/dist/chunk-KIUEUI7K.js +0 -82
- package/packages/core/dist/chunk-LB67NIZP.js +0 -949
- package/packages/core/dist/chunk-LDTYFUNW.js +0 -596
- package/packages/core/dist/chunk-MLVWARD4.js +0 -596
- package/packages/core/dist/chunk-MPYVXHKO.js +0 -2777
- package/packages/core/dist/chunk-MRQ3MITU.js +0 -772
- package/packages/core/dist/chunk-MVUN7I7O.js +0 -107
- package/packages/core/dist/chunk-NDDOIRW7.js +0 -596
- package/packages/core/dist/chunk-O3KDB5MH.js +0 -3330
- package/packages/core/dist/chunk-OEINUDSE.js +0 -400
- package/packages/core/dist/chunk-OPDENPT4.js +0 -4877
- package/packages/core/dist/chunk-P6XEM2LA.js +0 -429
- package/packages/core/dist/chunk-QKMRSQV4.js +0 -82
- package/packages/core/dist/chunk-R6CCWALL.js +0 -4877
- package/packages/core/dist/chunk-RAMMOO73.js +0 -774
- package/packages/core/dist/chunk-S3FTZFG6.js +0 -772
- package/packages/core/dist/chunk-T2ZVN434.js +0 -4877
- package/packages/core/dist/chunk-TFHLDMIA.js +0 -107
- package/packages/core/dist/chunk-TI3QUCIJ.js +0 -107
- package/packages/core/dist/chunk-TR2BAIGI.js +0 -107
- package/packages/core/dist/chunk-UGVVJKC6.js +0 -4877
- package/packages/core/dist/chunk-V2DX7YIU.js +0 -984
- package/packages/core/dist/chunk-W4IHYUYD.js +0 -772
- package/packages/core/dist/chunk-W6WBG5OY.js +0 -4877
- package/packages/core/dist/chunk-WN3M7O7H.js +0 -3312
- package/packages/core/dist/chunk-WR3NCU7U.js +0 -4877
- package/packages/core/dist/chunk-XC6B6L27.js +0 -82
- package/packages/core/dist/chunk-XNSE64ZY.js +0 -3316
- package/packages/core/dist/chunk-XROES2WN.js +0 -634
- package/packages/core/dist/chunk-XSCKNRCZ.js +0 -5020
- package/packages/core/dist/chunk-XVABPAUZ.js +0 -82
- package/packages/core/dist/chunk-YOBCB5C5.js +0 -596
- package/packages/core/dist/chunk-YUK6Q563.js +0 -666
- package/packages/core/dist/chunk-YXGL3ZEU.js +0 -1030
- package/packages/core/dist/chunk-ZINJ5OUX.js +0 -596
- package/packages/core/dist/chunk-ZPFD4BIQ.js +0 -604
- package/packages/core/dist/chunk-ZQZBFG6F.js +0 -4826
- package/packages/core/dist/chunk-ZZFRJGXM.js +0 -4877
- package/packages/create-krnl/dist/K_ENT_Entity-PQ5MURQY-R2OH4VXE.js +0 -14
- package/packages/create-krnl/dist/K_LIB_Krnl_Aiko_Client-OPZAVZCW-AZM5HF4M.js +0 -9
- package/packages/create-krnl/dist/K_LIB_Krnl_Ep_Tools-ROOE2TQF-X5N6SV2S.js +0 -111
- package/packages/create-krnl/dist/K_LIB_Krnl_Frontend_Builder-2ZDUH6PF-5PFHF4JU.js +0 -12
- package/packages/create-krnl/dist/K_LIB_Krnl_Frontend_Generator-UYNRNX2X-ED33OQPH.js +0 -12
- package/packages/create-krnl/dist/K_LIB_Krnl_Registry_Client-NXV6D2QW-YGQLVR4P.js +0 -9
- package/packages/create-krnl/dist/K_LIB_Krnl_SubApp_Engine-7HR5HWW2-VM7CTU2L.js +0 -21
- package/packages/create-krnl/dist/K_LIB_Krnl_SubApp_Registry-7XYGDKJK-Y2GQFJ7I.js +0 -21
- package/packages/create-krnl/dist/K_LIB_Krnl_SubApp_Worker_Host-CQVTGGG7-L3JQWO54.js +0 -8
- package/packages/create-krnl/dist/K_LIB_Krnl_System_App_Adapter-M6FJKLGU-KQARASPR.js +0 -19
- package/packages/create-krnl/dist/K_LIB_Krnl_System_Base_Functionality_APIs-ZZUZ6ZG5-OF3IGLVL.js +0 -59
- package/packages/create-krnl/dist/K_LIB_Krnl_System_Magnetic_Loader-XBQO2KXF-JOLHKOEX.js +0 -24
- package/packages/create-krnl/dist/K_LIB_Krnl_System_Unified_Http_Adapter-RCEGP3OG-L46LSHKQ.js +0 -10
- package/packages/create-krnl/dist/K_LIB_Krnl_Telemetry-77FRZ3KV-4JUPRR3X.js +0 -10
- package/packages/create-krnl/dist/chunk-3TRM6CYL.js +0 -152
- package/packages/create-krnl/dist/chunk-3URCB3QS.js +0 -127
- package/packages/create-krnl/dist/chunk-5JDK35TG.js +0 -3872
- package/packages/create-krnl/dist/chunk-A2XAXDOW.js +0 -812
- package/packages/create-krnl/dist/chunk-AN6TMQHG.js +0 -1033
- package/packages/create-krnl/dist/chunk-BOXWWXQG.js +0 -400
- package/packages/create-krnl/dist/chunk-BPTBREBG.js +0 -38
- package/packages/create-krnl/dist/chunk-FDNCADFE.js +0 -71
- package/packages/create-krnl/dist/chunk-JTLSTO25.js +0 -71
- package/packages/create-krnl/dist/chunk-M3NCUIPL.js +0 -107
- package/packages/create-krnl/dist/chunk-N2EONSOC.js +0 -7769
- package/packages/create-krnl/dist/chunk-OHVMVRBP.js +0 -117
- package/packages/create-krnl/dist/chunk-Q45257P6.js +0 -173
- package/packages/create-krnl/dist/chunk-QGM4M3NI.js +0 -37
- package/packages/create-krnl/dist/chunk-RJNYUXZD.js +0 -977
- package/packages/create-krnl/dist/chunk-SRTOPJ5W.js +0 -104
- package/packages/create-krnl/dist/chunk-UPBIGEC3.js +0 -666
- package/packages/create-krnl/dist/chunk-V2DB7UD7.js +0 -82
- package/packages/create-krnl/dist/chunk-WMHLIUI6.js +0 -3644
- package/packages/create-krnl/dist/chunk-XIRFOO3H.js +0 -589
- package/packages/create-krnl/dist/chunk-Y2CEEVZP.js +0 -5536
- package/packages/create-krnl/dist/main-DXUMPBND.js +0 -2217
- package/packages/create-krnl/dist/wrapper-SDF2VKX3.js +0 -31
|
@@ -30,13 +30,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
FunctionalityOrchestration: () => FunctionalityOrchestration,
|
|
33
34
|
HttpProtocolAdapter: () => HttpProtocolAdapter,
|
|
35
|
+
KrnlPackageReconciler: () => KrnlPackageReconciler,
|
|
36
|
+
KrnlQueue: () => KrnlQueue,
|
|
37
|
+
KrnlWorkerPool: () => KrnlWorkerPool,
|
|
34
38
|
LifecycleManager: () => LifecycleManager,
|
|
35
39
|
MemoryTransport: () => MemoryTransport,
|
|
36
40
|
P2PProtocolAdapter: () => P2PProtocolAdapter,
|
|
37
41
|
ProtocolResolver: () => ProtocolResolver,
|
|
38
42
|
ServiceDiscovery: () => ServiceDiscovery,
|
|
39
43
|
ServiceEventBus: () => ServiceEventBus,
|
|
44
|
+
ServiceFacade: () => ServiceFacade,
|
|
40
45
|
ServiceInstance: () => ServiceInstance,
|
|
41
46
|
ServiceProxy: () => ServiceProxy,
|
|
42
47
|
ServiceRegistry: () => ServiceRegistry,
|
|
@@ -44,9 +49,13 @@ __export(index_exports, {
|
|
|
44
49
|
ServiceStateStore: () => ServiceStateStore,
|
|
45
50
|
UdpProtocolAdapter: () => UdpProtocolAdapter,
|
|
46
51
|
WebSocketProtocolAdapter: () => WebSocketProtocolAdapter,
|
|
52
|
+
WorkerExecutionBoundary: () => WorkerExecutionBoundary,
|
|
47
53
|
createService: () => createService,
|
|
48
54
|
createServiceDefinition: () => createServiceDefinition,
|
|
49
|
-
|
|
55
|
+
createServiceFacade: () => createServiceFacade,
|
|
56
|
+
createServiceProxy: () => createServiceProxy,
|
|
57
|
+
validateOrchestrationDAG: () => validateOrchestrationDAG,
|
|
58
|
+
validateServiceDefinition: () => validateServiceDefinition
|
|
50
59
|
});
|
|
51
60
|
module.exports = __toCommonJS(index_exports);
|
|
52
61
|
|
|
@@ -108,171 +117,2541 @@ var ServiceEventBus = class {
|
|
|
108
117
|
};
|
|
109
118
|
|
|
110
119
|
// src/lifecycle/LifecycleManager.ts
|
|
120
|
+
var TRANSITIONS = {
|
|
121
|
+
uninitialized: ["initializing"],
|
|
122
|
+
initializing: ["ready", "uninitialized"],
|
|
123
|
+
ready: ["starting"],
|
|
124
|
+
starting: ["running", "ready"],
|
|
125
|
+
running: ["stopping"],
|
|
126
|
+
stopping: ["stopped", "running"],
|
|
127
|
+
stopped: ["starting"]
|
|
128
|
+
};
|
|
111
129
|
var LifecycleManager = class {
|
|
112
130
|
status = "uninitialized";
|
|
113
131
|
getStatus() {
|
|
114
132
|
return this.status;
|
|
115
133
|
}
|
|
116
|
-
|
|
134
|
+
canTransitionTo(next) {
|
|
135
|
+
return TRANSITIONS[this.status].includes(next);
|
|
136
|
+
}
|
|
137
|
+
transitionTo(next) {
|
|
138
|
+
if (!this.canTransitionTo(next)) {
|
|
139
|
+
throw new Error("[Lifecycle] Invalid transition '" + this.status + "' -> '" + next + "'.");
|
|
140
|
+
}
|
|
117
141
|
this.status = next;
|
|
118
142
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
setStatus(next) {
|
|
144
|
+
this.transitionTo(next);
|
|
145
|
+
}
|
|
146
|
+
assertRunning(serviceName) {
|
|
147
|
+
if (this.status !== "running") {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"[Service:" + serviceName + "] Cannot invoke operation while status is '" + this.status + "' (expected 'running')."
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// src/services/ServiceInstance.ts
|
|
156
|
+
var ServiceInstance = class {
|
|
157
|
+
constructor(definition) {
|
|
158
|
+
this.definition = definition;
|
|
159
|
+
this.id = definition.id;
|
|
160
|
+
this.name = definition.name;
|
|
161
|
+
this.version = definition.version;
|
|
162
|
+
this.lifecycle = new LifecycleManager();
|
|
163
|
+
this.eventBus = new ServiceEventBus();
|
|
164
|
+
if (definition.state) {
|
|
165
|
+
this.stateStore = new ServiceStateStore(definition.state.initial);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
definition;
|
|
169
|
+
id;
|
|
170
|
+
name;
|
|
171
|
+
version;
|
|
172
|
+
stateStore;
|
|
173
|
+
eventBus;
|
|
174
|
+
lifecycle;
|
|
175
|
+
initialized = false;
|
|
176
|
+
disposed = false;
|
|
177
|
+
getDefinition() {
|
|
178
|
+
return this.definition;
|
|
179
|
+
}
|
|
180
|
+
getStatus() {
|
|
181
|
+
return this.lifecycle.getStatus();
|
|
182
|
+
}
|
|
183
|
+
async start() {
|
|
184
|
+
if (this.disposed) throw new Error("[Service:" + this.name + "] Cannot start a disposed service.");
|
|
185
|
+
const current = this.lifecycle.getStatus();
|
|
186
|
+
if (current === "running") return;
|
|
187
|
+
if (current === "initializing" || current === "starting" || current === "stopping") {
|
|
188
|
+
throw new Error("[Service:" + this.name + "] Cannot start while status is '" + current + "'.");
|
|
189
|
+
}
|
|
190
|
+
if (!this.initialized) {
|
|
191
|
+
this.lifecycle.transitionTo("initializing");
|
|
192
|
+
try {
|
|
193
|
+
await this.definition.lifecycle?.onInit?.();
|
|
194
|
+
this.initialized = true;
|
|
195
|
+
this.lifecycle.transitionTo("ready");
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.lifecycle.transitionTo("uninitialized");
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.lifecycle.transitionTo("starting");
|
|
202
|
+
try {
|
|
203
|
+
await this.definition.lifecycle?.onStart?.();
|
|
204
|
+
this.lifecycle.transitionTo("running");
|
|
205
|
+
} catch (error) {
|
|
206
|
+
this.lifecycle.transitionTo("ready");
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async stop() {
|
|
211
|
+
const current = this.lifecycle.getStatus();
|
|
212
|
+
if (current === "stopped") return;
|
|
213
|
+
if (current !== "running") {
|
|
214
|
+
throw new Error("[Service:" + this.name + "] Cannot stop while status is '" + current + "'.");
|
|
215
|
+
}
|
|
216
|
+
this.lifecycle.transitionTo("stopping");
|
|
217
|
+
try {
|
|
218
|
+
await this.definition.lifecycle?.onStop?.();
|
|
219
|
+
this.lifecycle.transitionTo("stopped");
|
|
220
|
+
} catch (error) {
|
|
221
|
+
this.lifecycle.transitionTo("running");
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async dispose() {
|
|
226
|
+
if (this.disposed) return;
|
|
227
|
+
if (this.lifecycle.getStatus() === "running") await this.stop();
|
|
228
|
+
await this.definition.lifecycle?.onDispose?.();
|
|
229
|
+
this.disposed = true;
|
|
230
|
+
}
|
|
231
|
+
async call(op, input, ctx = {}) {
|
|
232
|
+
this.lifecycle.assertRunning(this.name);
|
|
233
|
+
const operation = this.definition.operations[op];
|
|
234
|
+
if (!operation) {
|
|
235
|
+
throw new Error("[Service:" + this.name + "] Operation '" + op + "' not found.");
|
|
236
|
+
}
|
|
237
|
+
return await operation.handler(input, ctx);
|
|
238
|
+
}
|
|
239
|
+
getState() {
|
|
240
|
+
return this.stateStore?.getState() ?? Object.freeze({});
|
|
241
|
+
}
|
|
242
|
+
setState(mutation) {
|
|
243
|
+
if (!this.stateStore) {
|
|
244
|
+
this.stateStore = new ServiceStateStore({ ...mutation });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.stateStore.setState(mutation);
|
|
248
|
+
}
|
|
249
|
+
on(event, handler) {
|
|
250
|
+
return this.eventBus.on(event, handler);
|
|
251
|
+
}
|
|
252
|
+
emit(event, payload) {
|
|
253
|
+
this.eventBus.emit(event, payload);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/services/ServiceProxy.ts
|
|
258
|
+
var ServiceProxy = class {
|
|
259
|
+
constructor(definition, adapter, endpoint) {
|
|
260
|
+
this.definition = definition;
|
|
261
|
+
this.adapter = adapter;
|
|
262
|
+
this.endpoint = endpoint;
|
|
263
|
+
if (!definition) {
|
|
264
|
+
throw new Error("[ServiceProxy] Missing service definition.");
|
|
265
|
+
}
|
|
266
|
+
if (!adapter || typeof adapter.send !== "function") {
|
|
267
|
+
throw new Error("[ServiceProxy:" + (definition.name || "unknown") + "] Missing or invalid protocol adapter.");
|
|
268
|
+
}
|
|
269
|
+
if (!endpoint || typeof endpoint.address !== "string" || endpoint.address.length === 0) {
|
|
270
|
+
throw new Error("[ServiceProxy:" + (definition.name || "unknown") + "] Missing or invalid endpoint descriptor.");
|
|
271
|
+
}
|
|
272
|
+
this.id = definition.id;
|
|
273
|
+
this.name = definition.name;
|
|
274
|
+
this.version = definition.version;
|
|
275
|
+
this.stateCache = { ...definition.state?.initial ?? {} };
|
|
276
|
+
}
|
|
277
|
+
definition;
|
|
278
|
+
adapter;
|
|
279
|
+
endpoint;
|
|
280
|
+
id;
|
|
281
|
+
name;
|
|
282
|
+
version;
|
|
283
|
+
stateCache;
|
|
284
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
285
|
+
lifecycle = new LifecycleManager();
|
|
286
|
+
disposed = false;
|
|
287
|
+
getDefinition() {
|
|
288
|
+
return this.definition;
|
|
289
|
+
}
|
|
290
|
+
getStatus() {
|
|
291
|
+
return this.lifecycle.getStatus();
|
|
292
|
+
}
|
|
293
|
+
async start() {
|
|
294
|
+
if (this.disposed) {
|
|
295
|
+
throw new Error("[ServiceProxy:" + this.name + "] Cannot start a disposed service proxy.");
|
|
296
|
+
}
|
|
297
|
+
const status = this.lifecycle.getStatus();
|
|
298
|
+
if (status === "running") return;
|
|
299
|
+
if (status === "stopping" || status === "starting" || status === "initializing") {
|
|
300
|
+
throw new Error("[ServiceProxy:" + this.name + "] Cannot start while status is '" + status + "'.");
|
|
301
|
+
}
|
|
302
|
+
if (status === "uninitialized") {
|
|
303
|
+
this.lifecycle.transitionTo("initializing");
|
|
304
|
+
this.lifecycle.transitionTo("ready");
|
|
305
|
+
}
|
|
306
|
+
this.lifecycle.transitionTo("starting");
|
|
307
|
+
this.lifecycle.transitionTo("running");
|
|
308
|
+
}
|
|
309
|
+
async stop() {
|
|
310
|
+
const status = this.lifecycle.getStatus();
|
|
311
|
+
if (status === "stopped") return;
|
|
312
|
+
if (status !== "running") {
|
|
313
|
+
throw new Error("[ServiceProxy:" + this.name + "] Cannot stop while status is '" + status + "'.");
|
|
314
|
+
}
|
|
315
|
+
this.lifecycle.transitionTo("stopping");
|
|
316
|
+
this.lifecycle.transitionTo("stopped");
|
|
317
|
+
}
|
|
318
|
+
async dispose() {
|
|
319
|
+
if (this.disposed) return;
|
|
320
|
+
if (this.lifecycle.getStatus() === "running") await this.stop();
|
|
321
|
+
this.disposed = true;
|
|
322
|
+
}
|
|
323
|
+
async call(op, input, ctx = {}) {
|
|
324
|
+
if (!this.definition.operations[op]) {
|
|
325
|
+
throw new Error("[ServiceProxy:" + this.name + "] Operation '" + op + "' not found.");
|
|
326
|
+
}
|
|
327
|
+
if (this.lifecycle.getStatus() !== "running") {
|
|
328
|
+
await this.start();
|
|
329
|
+
}
|
|
330
|
+
if (!this.adapter.send) {
|
|
331
|
+
throw new Error("[ServiceProxy:" + this.name + "] Adapter does not support send().");
|
|
332
|
+
}
|
|
333
|
+
const envelope = {
|
|
334
|
+
id: "req-" + Date.now() + "-" + Math.random().toString(36).slice(2, 7),
|
|
335
|
+
type: "request",
|
|
336
|
+
operation: op,
|
|
337
|
+
payload: input,
|
|
338
|
+
timestamp: Date.now()
|
|
339
|
+
};
|
|
340
|
+
const response = await this.adapter.send(this.endpoint, envelope);
|
|
341
|
+
if (response.error) {
|
|
342
|
+
throw new Error("[ServiceProxy:" + this.name + "] Remote error: " + response.error);
|
|
343
|
+
}
|
|
344
|
+
return response.payload;
|
|
345
|
+
}
|
|
346
|
+
getState() {
|
|
347
|
+
return Object.freeze({ ...this.stateCache });
|
|
348
|
+
}
|
|
349
|
+
setState(mutation) {
|
|
350
|
+
this.stateCache = { ...this.stateCache, ...mutation };
|
|
351
|
+
}
|
|
352
|
+
on(event, handler) {
|
|
353
|
+
if (!this.eventHandlers.has(event)) this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
354
|
+
const handlers = this.eventHandlers.get(event);
|
|
355
|
+
handlers.add(handler);
|
|
356
|
+
return () => handlers.delete(handler);
|
|
357
|
+
}
|
|
358
|
+
emit(event, payload) {
|
|
359
|
+
const handlers = this.eventHandlers.get(event);
|
|
360
|
+
if (!handlers) return;
|
|
361
|
+
for (const handler of Array.from(handlers)) {
|
|
362
|
+
try {
|
|
363
|
+
handler(payload);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error("[ServiceProxy:" + this.name + "] Event handler error for '" + event + "':", error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/services/ServiceGenerators.ts
|
|
372
|
+
function assertRecord(value, field) {
|
|
373
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
374
|
+
throw new Error("[KrnlTS] Invalid service definition: '" + field + "' must be an object.");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function assertNonEmptyString(value, field) {
|
|
378
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
379
|
+
throw new Error("[KrnlTS] Invalid service definition: '" + field + "' must be a non-empty string.");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function validateServiceDefinition(definition) {
|
|
383
|
+
if (!definition || typeof definition !== "object") {
|
|
384
|
+
throw new Error("[KrnlTS] Invalid service definition: expected an object.");
|
|
385
|
+
}
|
|
386
|
+
assertNonEmptyString(definition.id, "id");
|
|
387
|
+
assertNonEmptyString(definition.name, "name");
|
|
388
|
+
assertNonEmptyString(definition.version, "version");
|
|
389
|
+
if (!definition.operations || typeof definition.operations !== "object") {
|
|
390
|
+
throw new Error("[Service:" + definition.name + "] Invalid operations: expected an object.");
|
|
391
|
+
}
|
|
392
|
+
for (const [key, operation] of Object.entries(definition.operations)) {
|
|
393
|
+
if (!operation || typeof operation !== "object") {
|
|
394
|
+
throw new Error("[Service:" + definition.name + "] Invalid operation '" + key + "'.");
|
|
395
|
+
}
|
|
396
|
+
assertNonEmptyString(operation.id, "operations." + key + ".id");
|
|
397
|
+
assertNonEmptyString(operation.name, "operations." + key + ".name");
|
|
398
|
+
if (operation.inputSchema !== void 0) assertRecord(operation.inputSchema, "operations." + key + ".inputSchema");
|
|
399
|
+
if (operation.outputSchema !== void 0) assertRecord(operation.outputSchema, "operations." + key + ".outputSchema");
|
|
400
|
+
if (typeof operation.handler !== "function") {
|
|
401
|
+
throw new Error("[Service:" + definition.name + "] Operation '" + key + "' must define a handler.");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
for (const [key, event] of Object.entries(definition.events ?? {})) {
|
|
405
|
+
if (!event || typeof event !== "object") {
|
|
406
|
+
throw new Error("[Service:" + definition.name + "] Invalid event '" + key + "'.");
|
|
407
|
+
}
|
|
408
|
+
assertNonEmptyString(event.name, "events." + key + ".name");
|
|
409
|
+
}
|
|
410
|
+
for (const [key, schema] of Object.entries(definition.inputs ?? {})) assertRecord(schema, "inputs." + key);
|
|
411
|
+
for (const [key, schema] of Object.entries(definition.outputs ?? {})) assertRecord(schema, "outputs." + key);
|
|
412
|
+
if (definition.state?.schema !== void 0) assertRecord(definition.state.schema, "state.schema");
|
|
413
|
+
for (const capability of definition.capabilities ?? []) {
|
|
414
|
+
assertNonEmptyString(capability.id, "capability.id");
|
|
415
|
+
assertNonEmptyString(capability.name, "capability.name");
|
|
416
|
+
if (!Array.isArray(capability.protocols)) {
|
|
417
|
+
throw new Error("[Service:" + definition.name + "] Capability '" + capability.name + "' protocols must be an array.");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
for (const dependency of definition.dependencies ?? []) {
|
|
421
|
+
assertNonEmptyString(dependency.serviceId, "dependency.serviceId");
|
|
422
|
+
}
|
|
423
|
+
return definition;
|
|
424
|
+
}
|
|
425
|
+
function createServiceDefinition(definition) {
|
|
426
|
+
return validateServiceDefinition(definition);
|
|
427
|
+
}
|
|
428
|
+
function createService(definition) {
|
|
429
|
+
return new ServiceInstance(validateServiceDefinition(definition));
|
|
430
|
+
}
|
|
431
|
+
function createServiceProxy(definition, adapter, endpoint) {
|
|
432
|
+
validateServiceDefinition(definition);
|
|
433
|
+
if (!adapter || typeof adapter.send !== "function") {
|
|
434
|
+
throw new Error("[ServiceProxy:" + definition.name + "] Adapter must implement send().");
|
|
435
|
+
}
|
|
436
|
+
if (!endpoint || typeof endpoint.address !== "string" || endpoint.address.length === 0) {
|
|
437
|
+
throw new Error("[ServiceProxy:" + definition.name + "] Endpoint must have an address.");
|
|
438
|
+
}
|
|
439
|
+
return new ServiceProxy(definition, adapter, endpoint);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ../../node_modules/.pnpm/p-queue@10.0.0/node_modules/p-queue/dist/lower-bound.js
|
|
443
|
+
function lowerBound(array, value, comparator) {
|
|
444
|
+
let first = 0;
|
|
445
|
+
let count = array.length;
|
|
446
|
+
while (count > 0) {
|
|
447
|
+
const step = Math.trunc(count / 2);
|
|
448
|
+
let it = first + step;
|
|
449
|
+
if (comparator(array[it], value) <= 0) {
|
|
450
|
+
first = ++it;
|
|
451
|
+
count -= step + 1;
|
|
452
|
+
} else {
|
|
453
|
+
count = step;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return first;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ../../node_modules/.pnpm/p-queue@10.0.0/node_modules/p-queue/dist/priority-queue.js
|
|
460
|
+
var compactionThreshold = 100;
|
|
461
|
+
var PriorityQueue = class {
|
|
462
|
+
// The slots before `#head` are `undefined`.
|
|
463
|
+
#queue = [];
|
|
464
|
+
// The queue is stored as a sorted array, but dequeued slots are left before `#head` until compaction. Only items from `#head` onward are live, which keeps repeated dequeues amortized O(1).
|
|
465
|
+
#head = 0;
|
|
466
|
+
#compact() {
|
|
467
|
+
if (this.#head === 0) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
this.#queue.splice(0, this.#head);
|
|
471
|
+
this.#head = 0;
|
|
472
|
+
}
|
|
473
|
+
// Only the live items, because a dequeued item with the same id or function is no longer part of the queue.
|
|
474
|
+
#findIndex(isMatch) {
|
|
475
|
+
for (let index = this.#head; index < this.#queue.length; index++) {
|
|
476
|
+
if (isMatch(this.#queue[index])) {
|
|
477
|
+
return index;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return -1;
|
|
481
|
+
}
|
|
482
|
+
// Not `dequeue()`, which a subclass can override, because `remove()` also uses it.
|
|
483
|
+
#removeFirst() {
|
|
484
|
+
const item = this.#queue[this.#head];
|
|
485
|
+
this.#queue[this.#head] = void 0;
|
|
486
|
+
this.#head++;
|
|
487
|
+
if (this.#head === this.#queue.length) {
|
|
488
|
+
this.#queue.length = 0;
|
|
489
|
+
this.#head = 0;
|
|
490
|
+
} else if (this.#head > compactionThreshold && this.#head > this.#queue.length / 2) {
|
|
491
|
+
this.#compact();
|
|
492
|
+
}
|
|
493
|
+
return item.run;
|
|
494
|
+
}
|
|
495
|
+
enqueue(run, options) {
|
|
496
|
+
const { priority = 0, id } = options ?? {};
|
|
497
|
+
const { size } = this;
|
|
498
|
+
const element = {
|
|
499
|
+
priority,
|
|
500
|
+
id,
|
|
501
|
+
run
|
|
502
|
+
};
|
|
503
|
+
if (size === 0) {
|
|
504
|
+
this.#queue.length = 0;
|
|
505
|
+
this.#head = 0;
|
|
506
|
+
this.#queue.push(element);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (this.#queue.at(-1).priority >= priority) {
|
|
510
|
+
this.#queue.push(element);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.#compact();
|
|
514
|
+
const index = lowerBound(this.#queue, element, (a, b) => a.priority === b.priority ? 0 : b.priority - a.priority);
|
|
515
|
+
this.#queue.splice(index, 0, element);
|
|
516
|
+
}
|
|
517
|
+
setPriority(id, priority) {
|
|
518
|
+
const index = this.#findIndex((element) => element.id === id);
|
|
519
|
+
if (index === -1) {
|
|
520
|
+
throw new ReferenceError(`No promise function with the id "${id}" exists in the queue.`);
|
|
521
|
+
}
|
|
522
|
+
if (this.#queue[index].priority === priority) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const [item] = this.#queue.splice(index, 1);
|
|
526
|
+
this.enqueue(item.run, { priority, id });
|
|
527
|
+
}
|
|
528
|
+
remove(idOrRun) {
|
|
529
|
+
const index = this.#findIndex((element) => (typeof idOrRun === "string" ? element.id : element.run) === idOrRun);
|
|
530
|
+
if (index === this.#head) {
|
|
531
|
+
this.#removeFirst();
|
|
532
|
+
} else if (index !== -1) {
|
|
533
|
+
this.#queue.splice(index, 1);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
dequeue() {
|
|
537
|
+
return this.#head === this.#queue.length ? void 0 : this.#removeFirst();
|
|
538
|
+
}
|
|
539
|
+
filter(options) {
|
|
540
|
+
const result = [];
|
|
541
|
+
for (let index = this.#head; index < this.#queue.length; index++) {
|
|
542
|
+
const element = this.#queue[index];
|
|
543
|
+
if (element.priority === options.priority) {
|
|
544
|
+
result.push(element.run);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
get size() {
|
|
550
|
+
return this.#queue.length - this.#head;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// ../../node_modules/.pnpm/p-queue@10.0.0/node_modules/p-queue/dist/index.js
|
|
555
|
+
var maximumTimerDelay = 2147483647;
|
|
556
|
+
var asyncHooks = globalThis.process?.getBuiltinModule?.("node:async_hooks");
|
|
557
|
+
var bindAsyncContext = (function_) => {
|
|
558
|
+
if (asyncHooks === void 0) {
|
|
559
|
+
return function_;
|
|
560
|
+
}
|
|
561
|
+
const asyncResource = new asyncHooks.AsyncResource("PQueueTask");
|
|
562
|
+
return () => asyncResource.runInAsyncScope(function_);
|
|
563
|
+
};
|
|
564
|
+
var validateTimeout = (timeout) => {
|
|
565
|
+
if (timeout !== void 0 && !(Number.isFinite(timeout) && timeout > 0 && timeout <= maximumTimerDelay)) {
|
|
566
|
+
throw new TypeError(`Expected \`timeout\` to be a positive finite number up to ${maximumTimerDelay}, got \`${timeout}\` (${typeof timeout})`);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
var validateLimit = (name, value) => {
|
|
570
|
+
if (!(typeof value === "number" && value >= 1 && (value === Infinity || Number.isInteger(value)))) {
|
|
571
|
+
throw new TypeError(`Expected \`${name}\` to be an integer from 1 and up or \`Infinity\`, got \`${String(value)}\` (${typeof value})`);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
var validateRateLimit = (interval, intervalCap, isStrict) => {
|
|
575
|
+
validateLimit("intervalCap", intervalCap);
|
|
576
|
+
if (!(Number.isFinite(interval) && interval >= 0)) {
|
|
577
|
+
throw new TypeError(`Expected \`interval\` to be a finite number >= 0, got \`${String(interval)}\` (${typeof interval})`);
|
|
578
|
+
}
|
|
579
|
+
if (intervalCap !== Infinity && interval > maximumTimerDelay) {
|
|
580
|
+
throw new TypeError(`Expected \`interval\` to be at most ${maximumTimerDelay} with an \`intervalCap\`, got \`${interval}\``);
|
|
581
|
+
}
|
|
582
|
+
if (isStrict && interval === 0) {
|
|
583
|
+
throw new TypeError("The `strict` option requires a non-zero `interval`");
|
|
584
|
+
}
|
|
585
|
+
if (isStrict && intervalCap === Infinity) {
|
|
586
|
+
throw new TypeError("The `strict` option requires a finite `intervalCap`");
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
var PQueue = class extends EventTarget {
|
|
590
|
+
#carryoverIntervalCount;
|
|
591
|
+
#isIntervalIgnored;
|
|
592
|
+
#intervalCount = 0;
|
|
593
|
+
#intervalCap;
|
|
594
|
+
#rateLimitedInInterval = false;
|
|
595
|
+
#rateLimitFlushScheduled = false;
|
|
596
|
+
#interval;
|
|
597
|
+
/*
|
|
598
|
+
Timestamp when the current interval window ends. Kept accurate at every window transition: `#initializeIntervalIfNeeded` sets it when a window starts and `#onInterval` advances it as the recurring timer rolls into each new window. This is the source of truth for whether a task added after the queue goes idle belongs to a fresh window, so it must not go stale.
|
|
599
|
+
*/
|
|
600
|
+
#intervalEnd = 0;
|
|
601
|
+
// Recurring timer that drives windows while the queue is actively processing. Absent (`undefined`) once the queue goes idle.
|
|
602
|
+
#intervalId;
|
|
603
|
+
// One-shot timer used while idle to resume at the next window boundary, since `#intervalId` is cleared when idle.
|
|
604
|
+
#timeoutId;
|
|
605
|
+
#strict;
|
|
606
|
+
// Circular buffer implementation for better performance
|
|
607
|
+
#strictTicks = [];
|
|
608
|
+
#strictTicksStartIndex = 0;
|
|
609
|
+
#queue;
|
|
610
|
+
#queueClass;
|
|
611
|
+
#pending = 0;
|
|
612
|
+
// The `!` is needed because of https://github.com/microsoft/TypeScript/issues/32194
|
|
613
|
+
#concurrency;
|
|
614
|
+
#isPaused;
|
|
615
|
+
// Use to assign a unique identifier to a promise function, if not explicitly specified
|
|
616
|
+
#idAssigner = 1n;
|
|
617
|
+
// Track currently running tasks for debugging
|
|
618
|
+
#runningTasks = /* @__PURE__ */ new Set();
|
|
619
|
+
// The `abort` listener of each signal, with the callbacks of the tasks that use the signal.
|
|
620
|
+
#abortListeners = /* @__PURE__ */ new Map();
|
|
621
|
+
// The waiting tasks, so that `.clear()` can reject their promises.
|
|
622
|
+
#waitingTasks = /* @__PURE__ */ new Set();
|
|
623
|
+
#timeout;
|
|
624
|
+
constructor(options) {
|
|
625
|
+
super();
|
|
626
|
+
globalThis.process?.getBuiltinModule?.("node:events")?.setMaxListeners(Infinity, this);
|
|
627
|
+
const { carryoverIntervalCount = false, intervalCap = Infinity, interval = 0, concurrency = Infinity, autoStart = true, queueClass = PriorityQueue, strict = false, timeout } = options ?? {};
|
|
628
|
+
validateRateLimit(interval, intervalCap, strict);
|
|
629
|
+
this.#carryoverIntervalCount = carryoverIntervalCount;
|
|
630
|
+
this.#isIntervalIgnored = intervalCap === Infinity || interval === 0;
|
|
631
|
+
this.#intervalCap = intervalCap;
|
|
632
|
+
this.#interval = interval;
|
|
633
|
+
this.#strict = strict;
|
|
634
|
+
this.#queueClass = queueClass;
|
|
635
|
+
this.#queue = new this.#queueClass();
|
|
636
|
+
this.concurrency = concurrency;
|
|
637
|
+
this.timeout = timeout;
|
|
638
|
+
this.#isPaused = !autoStart;
|
|
639
|
+
this.#setupRateLimitTracking();
|
|
640
|
+
}
|
|
641
|
+
// Calls `callback` when `signal` aborts, and returns a function that stops it. The tasks that share a signal share one listener on it, because Node.js searches all the listeners of a signal to add or remove one, so a listener for each task takes O(n²) time with many tasks.
|
|
642
|
+
#onAbort(signal, callback) {
|
|
643
|
+
let listener = this.#abortListeners.get(signal);
|
|
644
|
+
if (!listener) {
|
|
645
|
+
const callbacks = /* @__PURE__ */ new Set();
|
|
646
|
+
const dispatch = () => {
|
|
647
|
+
this.#abortListeners.delete(signal);
|
|
648
|
+
for (const abortCallback of callbacks) {
|
|
649
|
+
abortCallback();
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
listener = { callbacks, dispatch };
|
|
653
|
+
this.#abortListeners.set(signal, listener);
|
|
654
|
+
signal.addEventListener("abort", dispatch, { once: true });
|
|
655
|
+
}
|
|
656
|
+
const currentListener = listener;
|
|
657
|
+
currentListener.callbacks.add(callback);
|
|
658
|
+
return () => {
|
|
659
|
+
currentListener.callbacks.delete(callback);
|
|
660
|
+
if (currentListener.callbacks.size > 0 || this.#abortListeners.get(signal) !== currentListener) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.#abortListeners.delete(signal);
|
|
664
|
+
signal.removeEventListener("abort", currentListener.dispatch);
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
#emit(event, detail) {
|
|
668
|
+
this.dispatchEvent(new CustomEvent(event, { detail }));
|
|
669
|
+
}
|
|
670
|
+
async #onEvent(events, shouldHandle) {
|
|
671
|
+
const eventList = Array.isArray(events) ? events : [events];
|
|
672
|
+
return new Promise((resolve) => {
|
|
673
|
+
const controller = new AbortController();
|
|
674
|
+
const listener = () => {
|
|
675
|
+
if (shouldHandle && !shouldHandle()) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
controller.abort();
|
|
679
|
+
resolve();
|
|
680
|
+
};
|
|
681
|
+
for (const event of eventList) {
|
|
682
|
+
this.addEventListener(event, listener, { signal: controller.signal });
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
// From the current state. `#rateLimitedInInterval` is what the events last reported, and it updates in a microtask.
|
|
687
|
+
get #isRateLimitedNow() {
|
|
688
|
+
if (this.#isIntervalIgnored || this.#queue.size === 0) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
const now = Date.now();
|
|
692
|
+
if (this.#strict) {
|
|
693
|
+
this.#cleanupStrictTicks(now);
|
|
694
|
+
return this.#getActiveTicksCount() >= this.#intervalCap;
|
|
695
|
+
}
|
|
696
|
+
const isWindowOver = this.#intervalId === void 0 && this.#intervalEnd <= now;
|
|
697
|
+
const count = isWindowOver ? this.#intervalCountAtWindowStart : this.#intervalCount;
|
|
698
|
+
return count >= this.#intervalCap;
|
|
699
|
+
}
|
|
700
|
+
// The tasks that still run count against the new window with `carryoverIntervalCount`.
|
|
701
|
+
get #intervalCountAtWindowStart() {
|
|
702
|
+
return this.#carryoverIntervalCount ? this.#pending : 0;
|
|
703
|
+
}
|
|
704
|
+
#updateRateLimitState() {
|
|
705
|
+
const shouldBeRateLimited = this.#isRateLimitedNow;
|
|
706
|
+
if (shouldBeRateLimited === this.#rateLimitedInInterval) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
this.#rateLimitedInInterval = shouldBeRateLimited;
|
|
710
|
+
this.#emit(shouldBeRateLimited ? "rateLimit" : "rateLimitCleared");
|
|
711
|
+
}
|
|
712
|
+
#rollbackIntervalConsumption() {
|
|
713
|
+
if (this.#isIntervalIgnored) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
this.#rollbackIntervalSlot();
|
|
717
|
+
this.#scheduleRateLimitUpdate();
|
|
718
|
+
}
|
|
719
|
+
#scheduleRateLimitUpdate() {
|
|
720
|
+
if (this.#isIntervalIgnored || this.#rateLimitFlushScheduled) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
this.#rateLimitFlushScheduled = true;
|
|
724
|
+
queueMicrotask(() => {
|
|
725
|
+
this.#rateLimitFlushScheduled = false;
|
|
726
|
+
this.#updateRateLimitState();
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
#setupRateLimitTracking() {
|
|
730
|
+
if (this.#isIntervalIgnored) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.addEventListener("add", () => {
|
|
734
|
+
if (this.#queue.size > 0) {
|
|
735
|
+
this.#scheduleRateLimitUpdate();
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
this.addEventListener("next", () => {
|
|
739
|
+
this.#scheduleRateLimitUpdate();
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
#cleanupStrictTicks(now) {
|
|
743
|
+
while (this.#strictTicksStartIndex < this.#strictTicks.length) {
|
|
744
|
+
const oldestTick = this.#strictTicks[this.#strictTicksStartIndex];
|
|
745
|
+
if (oldestTick !== void 0 && now - oldestTick >= this.#interval) {
|
|
746
|
+
this.#strictTicksStartIndex++;
|
|
747
|
+
} else {
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const shouldCompact = this.#strictTicksStartIndex > 100 && this.#strictTicksStartIndex > this.#strictTicks.length / 2 || this.#strictTicksStartIndex === this.#strictTicks.length;
|
|
752
|
+
if (!shouldCompact) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
this.#strictTicks = this.#strictTicks.slice(this.#strictTicksStartIndex);
|
|
756
|
+
this.#strictTicksStartIndex = 0;
|
|
757
|
+
}
|
|
758
|
+
// Helper methods for interval consumption
|
|
759
|
+
#consumeIntervalSlot(now) {
|
|
760
|
+
if (this.#strict) {
|
|
761
|
+
this.#strictTicks.push(now);
|
|
762
|
+
} else {
|
|
763
|
+
this.#intervalCount++;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
#rollbackIntervalSlot() {
|
|
767
|
+
if (this.#strict) {
|
|
768
|
+
if (this.#strictTicks.length > this.#strictTicksStartIndex) {
|
|
769
|
+
this.#strictTicks.pop();
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (this.#intervalCount > 0) {
|
|
774
|
+
this.#intervalCount--;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
#getActiveTicksCount() {
|
|
778
|
+
return this.#strictTicks.length - this.#strictTicksStartIndex;
|
|
779
|
+
}
|
|
780
|
+
get #doesIntervalAllowAnother() {
|
|
781
|
+
if (this.#isIntervalIgnored) {
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
const count = this.#strict ? this.#getActiveTicksCount() : this.#intervalCount;
|
|
785
|
+
return count < this.#intervalCap;
|
|
786
|
+
}
|
|
787
|
+
get #doesConcurrentAllowAnother() {
|
|
788
|
+
return this.#pending < this.#concurrency;
|
|
789
|
+
}
|
|
790
|
+
#next() {
|
|
791
|
+
this.#pending--;
|
|
792
|
+
if (this.#pending === 0) {
|
|
793
|
+
this.#emit("pendingZero");
|
|
794
|
+
}
|
|
795
|
+
this.#processQueue();
|
|
796
|
+
this.#emit("next");
|
|
797
|
+
}
|
|
798
|
+
#onResumeInterval() {
|
|
799
|
+
this.#timeoutId = void 0;
|
|
800
|
+
this.#onInterval();
|
|
801
|
+
if (!this.#isPaused) {
|
|
802
|
+
this.#initializeIntervalIfNeeded();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
#isIntervalPausedAt(now) {
|
|
806
|
+
if (this.#strict) {
|
|
807
|
+
this.#cleanupStrictTicks(now);
|
|
808
|
+
const activeTicksCount = this.#getActiveTicksCount();
|
|
809
|
+
if (activeTicksCount >= this.#intervalCap) {
|
|
810
|
+
const oldestTick = this.#strictTicks[this.#strictTicksStartIndex];
|
|
811
|
+
const delay = this.#interval - (now - oldestTick);
|
|
812
|
+
this.#createIntervalTimeout(delay);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
if (this.#intervalId === void 0) {
|
|
818
|
+
const delay = this.#intervalEnd - now;
|
|
819
|
+
if (delay <= 0) {
|
|
820
|
+
this.#intervalCount = this.#intervalCountAtWindowStart;
|
|
821
|
+
} else {
|
|
822
|
+
this.#createIntervalTimeout(delay);
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
#createIntervalTimeout(delay) {
|
|
829
|
+
if (this.#timeoutId !== void 0) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
this.#timeoutId = setTimeout(() => {
|
|
833
|
+
this.#onResumeInterval();
|
|
834
|
+
}, delay);
|
|
835
|
+
}
|
|
836
|
+
#clearIntervalTimer() {
|
|
837
|
+
if (this.#intervalId === void 0) {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
clearInterval(this.#intervalId);
|
|
841
|
+
this.#intervalId = void 0;
|
|
842
|
+
}
|
|
843
|
+
#clearTimeoutTimer() {
|
|
844
|
+
if (this.#timeoutId === void 0) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
clearTimeout(this.#timeoutId);
|
|
848
|
+
this.#timeoutId = void 0;
|
|
849
|
+
}
|
|
850
|
+
// Emits `empty`, and `idle` when no tasks run. For when the queue has no waiting tasks.
|
|
851
|
+
#emitEmptyAndIdle() {
|
|
852
|
+
this.#clearIntervalTimer();
|
|
853
|
+
this.#emit("empty");
|
|
854
|
+
if (this.#pending > 0 || this.#queue.size > 0) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
this.#clearTimeoutTimer();
|
|
858
|
+
if (this.#strict) {
|
|
859
|
+
this.#cleanupStrictTicks(Date.now());
|
|
860
|
+
}
|
|
861
|
+
this.#emit("idle");
|
|
862
|
+
}
|
|
863
|
+
#tryToStartAnother() {
|
|
864
|
+
let isStarted;
|
|
865
|
+
do {
|
|
866
|
+
isStarted = this.#startNextTask();
|
|
867
|
+
} while (isStarted === void 0);
|
|
868
|
+
return isStarted;
|
|
869
|
+
}
|
|
870
|
+
// Returns `undefined` when it skipped a task whose signal aborted while it waited.
|
|
871
|
+
#startNextTask() {
|
|
872
|
+
if (this.#queue.size === 0) {
|
|
873
|
+
this.#emitEmptyAndIdle();
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
if (this.#isPaused) {
|
|
877
|
+
if (!this.#doesIntervalAllowAnother) {
|
|
878
|
+
this.#isIntervalPausedAt(Date.now());
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
const now = Date.now();
|
|
883
|
+
const canInitializeInterval = !this.#isIntervalPausedAt(now);
|
|
884
|
+
if (this.#doesIntervalAllowAnother && this.#doesConcurrentAllowAnother) {
|
|
885
|
+
const job = this.#queue.dequeue();
|
|
886
|
+
if (!this.#isIntervalIgnored) {
|
|
887
|
+
this.#consumeIntervalSlot(now);
|
|
888
|
+
this.#scheduleRateLimitUpdate();
|
|
889
|
+
}
|
|
890
|
+
if (canInitializeInterval) {
|
|
891
|
+
this.#initializeIntervalIfNeeded();
|
|
892
|
+
}
|
|
893
|
+
const pendingBefore = this.#pending;
|
|
894
|
+
job();
|
|
895
|
+
if (this.#pending === pendingBefore) {
|
|
896
|
+
this.#emit("next");
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
this.#emit("active");
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
#initializeIntervalIfNeeded() {
|
|
905
|
+
if (this.#isIntervalIgnored || this.#intervalId !== void 0 || this.#strict) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
this.#clearTimeoutTimer();
|
|
909
|
+
this.#intervalId = setInterval(() => {
|
|
910
|
+
this.#onInterval();
|
|
911
|
+
}, this.#interval);
|
|
912
|
+
this.#intervalEnd = Date.now() + this.#interval;
|
|
913
|
+
}
|
|
914
|
+
#onInterval() {
|
|
915
|
+
if (!this.#strict) {
|
|
916
|
+
if (this.#intervalId !== void 0) {
|
|
917
|
+
if (this.#intervalCount === 0 && this.#pending === 0) {
|
|
918
|
+
this.#clearIntervalTimer();
|
|
919
|
+
} else {
|
|
920
|
+
this.#intervalEnd = Date.now() + this.#interval;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
this.#intervalCount = this.#intervalCountAtWindowStart;
|
|
924
|
+
}
|
|
925
|
+
this.#processQueue();
|
|
926
|
+
this.#scheduleRateLimitUpdate();
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
Executes all queued functions until it reaches the limit.
|
|
930
|
+
*/
|
|
931
|
+
#processQueue() {
|
|
932
|
+
while (this.#tryToStartAnother()) {
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// On an empty queue, `#processQueue()` emits `empty` and `idle`. An `idle` listener that sets `concurrency` or calls `.start()` would then recurse without end.
|
|
936
|
+
#processWaitingTasks() {
|
|
937
|
+
if (this.#queue.size > 0) {
|
|
938
|
+
this.#processQueue();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async #add(function_, options, waitingTask) {
|
|
942
|
+
const id = options.id ?? (this.#idAssigner++).toString();
|
|
943
|
+
options = {
|
|
944
|
+
timeout: this.#timeout,
|
|
945
|
+
...options,
|
|
946
|
+
id
|
|
947
|
+
};
|
|
948
|
+
validateTimeout(options.timeout);
|
|
949
|
+
if (options.priority !== void 0 && (typeof options.priority !== "number" || Number.isNaN(options.priority))) {
|
|
950
|
+
throw new TypeError(`Expected \`priority\` to be a number, got \`${String(options.priority)}\` (${typeof options.priority})`);
|
|
951
|
+
}
|
|
952
|
+
waitingTask.id = id;
|
|
953
|
+
waitingTask.priority = options.priority ?? 0;
|
|
954
|
+
return new Promise((resolve, reject) => {
|
|
955
|
+
if (options.signal?.aborted) {
|
|
956
|
+
reject(options.signal.reason);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const run = bindAsyncContext(async () => {
|
|
960
|
+
waitingTask.stopListeningForAbort?.();
|
|
961
|
+
this.#waitingTasks.delete(waitingTask);
|
|
962
|
+
if (options.signal?.aborted) {
|
|
963
|
+
this.#rollbackIntervalConsumption();
|
|
964
|
+
reject(options.signal.reason);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
this.#pending++;
|
|
968
|
+
const runningTask = {
|
|
969
|
+
id,
|
|
970
|
+
priority: waitingTask.priority,
|
|
971
|
+
startTime: Date.now(),
|
|
972
|
+
timeout: options.timeout
|
|
973
|
+
};
|
|
974
|
+
this.#runningTasks.add(runningTask);
|
|
975
|
+
let { signal } = options;
|
|
976
|
+
let stopListeningForAbort;
|
|
977
|
+
let stopForwardingAbort;
|
|
978
|
+
let timeoutId;
|
|
979
|
+
try {
|
|
980
|
+
if (options.timeout !== void 0) {
|
|
981
|
+
const { signal: userSignal, timeout } = options;
|
|
982
|
+
const taskController = new AbortController();
|
|
983
|
+
timeoutId = setTimeout(() => {
|
|
984
|
+
taskController.abort(new TimeoutError(`Task timed out after ${timeout}ms (queue has ${this.#pending} running, ${this.#queue.size} waiting)`));
|
|
985
|
+
}, timeout);
|
|
986
|
+
if (userSignal) {
|
|
987
|
+
stopForwardingAbort = this.#onAbort(userSignal, () => {
|
|
988
|
+
taskController.abort(userSignal.reason);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
signal = taskController.signal;
|
|
992
|
+
}
|
|
993
|
+
let operation;
|
|
994
|
+
if (signal) {
|
|
995
|
+
const abortSignal = signal;
|
|
996
|
+
const abortPromise = new Promise((_resolve, rejectWithReason) => {
|
|
997
|
+
stopListeningForAbort = this.#onAbort(abortSignal, () => {
|
|
998
|
+
rejectWithReason(abortSignal.reason);
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
operation = Promise.race([abortPromise, (async () => function_({ signal }))()]);
|
|
1002
|
+
} else {
|
|
1003
|
+
try {
|
|
1004
|
+
operation = function_({ signal });
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
operation = Promise.reject(error);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const result = await operation;
|
|
1010
|
+
resolve(result);
|
|
1011
|
+
this.#emit("completed", result);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
reject(error);
|
|
1014
|
+
this.#emit("error", error);
|
|
1015
|
+
} finally {
|
|
1016
|
+
clearTimeout(timeoutId);
|
|
1017
|
+
stopListeningForAbort?.();
|
|
1018
|
+
stopForwardingAbort?.();
|
|
1019
|
+
this.#runningTasks.delete(runningTask);
|
|
1020
|
+
queueMicrotask(() => {
|
|
1021
|
+
this.#next();
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
const hasWaitingTasks = this.#queue.size > 0;
|
|
1026
|
+
this.#queue.enqueue(run, options);
|
|
1027
|
+
waitingTask.reject = reject;
|
|
1028
|
+
this.#waitingTasks.add(waitingTask);
|
|
1029
|
+
if (options.signal) {
|
|
1030
|
+
const { signal } = options;
|
|
1031
|
+
waitingTask.stopListeningForAbort = this.#onAbort(signal, () => {
|
|
1032
|
+
this.#waitingTasks.delete(waitingTask);
|
|
1033
|
+
if (this.#queue instanceof PriorityQueue) {
|
|
1034
|
+
this.#queue.remove(run);
|
|
1035
|
+
} else {
|
|
1036
|
+
this.#queue.remove(id);
|
|
1037
|
+
}
|
|
1038
|
+
reject(signal.reason);
|
|
1039
|
+
this.#processQueue();
|
|
1040
|
+
this.#emit("next");
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
this.#emit("add");
|
|
1044
|
+
if (hasWaitingTasks) {
|
|
1045
|
+
this.#processQueue();
|
|
1046
|
+
} else {
|
|
1047
|
+
this.#tryToStartAnother();
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
Get or set the default timeout. A change applies to tasks added after it.
|
|
1053
|
+
|
|
1054
|
+
@example
|
|
1055
|
+
```
|
|
1056
|
+
const queue = new PQueue({timeout: 5000});
|
|
1057
|
+
|
|
1058
|
+
queue.timeout = 10000;
|
|
1059
|
+
```
|
|
1060
|
+
*/
|
|
1061
|
+
get timeout() {
|
|
1062
|
+
return this.#timeout;
|
|
1063
|
+
}
|
|
1064
|
+
// Validated here, not only in `.add()`, so that an invalid value throws where it is set, instead of making every later `.add()` reject.
|
|
1065
|
+
set timeout(timeout) {
|
|
1066
|
+
validateTimeout(timeout);
|
|
1067
|
+
this.#timeout = timeout;
|
|
1068
|
+
}
|
|
1069
|
+
get concurrency() {
|
|
1070
|
+
return this.#concurrency;
|
|
1071
|
+
}
|
|
1072
|
+
set concurrency(newConcurrency) {
|
|
1073
|
+
validateLimit("concurrency", newConcurrency);
|
|
1074
|
+
this.#concurrency = newConcurrency;
|
|
1075
|
+
this.#processWaitingTasks();
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
Changes the priority of a waiting task, found by its `id`. Throws a `ReferenceError` if no waiting task has that `id`, for example because it already started. Throws a `TypeError` if `priority` is not a finite number. Tasks only wait when the queue has a `concurrency` limit, is paused, or is rate-limited.
|
|
1079
|
+
|
|
1080
|
+
@example
|
|
1081
|
+
```
|
|
1082
|
+
// Raise the priority to run a task earlier
|
|
1083
|
+
import PQueue from 'p-queue';
|
|
1084
|
+
|
|
1085
|
+
const queue = new PQueue({concurrency: 1});
|
|
1086
|
+
|
|
1087
|
+
queue.add(async () => '🦄', {priority: 1});
|
|
1088
|
+
queue.add(async () => '🦀', {priority: 0, id: '🦀'});
|
|
1089
|
+
queue.add(async () => '🦄', {priority: 1});
|
|
1090
|
+
queue.add(async () => '🦄', {priority: 1});
|
|
1091
|
+
|
|
1092
|
+
queue.setPriority('🦀', 2);
|
|
1093
|
+
// '🦀' runs second
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
@example
|
|
1097
|
+
```
|
|
1098
|
+
// Lower the priority to run a task later
|
|
1099
|
+
import PQueue from 'p-queue';
|
|
1100
|
+
|
|
1101
|
+
const queue = new PQueue({concurrency: 1});
|
|
1102
|
+
|
|
1103
|
+
queue.add(async () => '🦄', {priority: 1});
|
|
1104
|
+
queue.add(async () => '🦀', {priority: 1, id: '🦀'});
|
|
1105
|
+
queue.add(async () => '🦄');
|
|
1106
|
+
queue.add(async () => '🦄', {priority: 0});
|
|
1107
|
+
|
|
1108
|
+
queue.setPriority('🦀', -1);
|
|
1109
|
+
// '🦀' runs last
|
|
1110
|
+
```
|
|
1111
|
+
*/
|
|
1112
|
+
setPriority(id, priority) {
|
|
1113
|
+
if (typeof priority !== "number" || !Number.isFinite(priority)) {
|
|
1114
|
+
throw new TypeError(`Expected \`priority\` to be a finite number, got \`${priority}\` (${typeof priority})`);
|
|
1115
|
+
}
|
|
1116
|
+
for (const waitingTask of this.#waitingTasks) {
|
|
1117
|
+
if (waitingTask.id === id) {
|
|
1118
|
+
this.#queue.setPriority(id, priority);
|
|
1119
|
+
waitingTask.priority = priority;
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
throw new ReferenceError(`No promise function with the id "${id}" exists in the queue.`);
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
Adds a sync or async task to the queue.
|
|
1127
|
+
|
|
1128
|
+
Returns a promise that settles with the result of `fn` when the task completes, not when it is added. The function gets `{signal}` as its first argument. The signal is set when the task has a `signal` or a `timeout`, and aborts when either one does. Pass it on, for example to `fetch()`, to stop the work. The function runs in the async context of the `.add()` call (in Node.js 22.3 and later, Deno, and Bun), so it sees the [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) values of the caller, also when it starts later.
|
|
1129
|
+
|
|
1130
|
+
If you `await` each `.add()` call before the next, the tasks run one after the other, whatever the `concurrency`.
|
|
1131
|
+
|
|
1132
|
+
Handle rejections of the returned promise. Otherwise, they are unhandled rejections, which can make the process exit.
|
|
1133
|
+
*/
|
|
1134
|
+
add(function_, options = {}) {
|
|
1135
|
+
const waitingTask = {};
|
|
1136
|
+
const promise = this.#add(function_, options, waitingTask);
|
|
1137
|
+
waitingTask.promise = promise;
|
|
1138
|
+
if (waitingTask.isCleared) {
|
|
1139
|
+
promise.catch(() => void 0);
|
|
1140
|
+
}
|
|
1141
|
+
return promise;
|
|
1142
|
+
}
|
|
1143
|
+
async addAll(functions, options) {
|
|
1144
|
+
return Promise.all(functions.map(async (function_) => this.add(function_, options)));
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
Starts (or resumes) running queued tasks. Only needed if the queue is paused, with `autoStart: false` or `.pause()`.
|
|
1148
|
+
|
|
1149
|
+
@returns `this` (the instance).
|
|
1150
|
+
*/
|
|
1151
|
+
start() {
|
|
1152
|
+
if (!this.#isPaused) {
|
|
1153
|
+
return this;
|
|
1154
|
+
}
|
|
1155
|
+
this.#isPaused = false;
|
|
1156
|
+
this.#processWaitingTasks();
|
|
1157
|
+
return this;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
Stops starting new tasks. Running tasks continue.
|
|
1161
|
+
*/
|
|
1162
|
+
pause() {
|
|
1163
|
+
this.#isPaused = true;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
Removes all waiting tasks. Running tasks continue.
|
|
1167
|
+
|
|
1168
|
+
The promises returned by `.add()` for the removed tasks reject with an `AbortError`, which is a `DOMException`. The removed tasks do not run, and they do not emit the `error` event.
|
|
1169
|
+
|
|
1170
|
+
If you ignore such a promise, its rejection is not reported as unhandled. This does not apply to a promise made from it, like the one from `queue.add(fn).then(…)` or from `.addAll()`, so handle the rejection of that promise.
|
|
1171
|
+
|
|
1172
|
+
@example
|
|
1173
|
+
```
|
|
1174
|
+
import PQueue from 'p-queue';
|
|
1175
|
+
|
|
1176
|
+
const queue = new PQueue({concurrency: 1});
|
|
1177
|
+
|
|
1178
|
+
queue.add(() => doWork());
|
|
1179
|
+
const promise = queue.add(() => doWork());
|
|
1180
|
+
|
|
1181
|
+
queue.clear();
|
|
1182
|
+
|
|
1183
|
+
try {
|
|
1184
|
+
await promise;
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
console.log(error.name);
|
|
1187
|
+
//=> 'AbortError'
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
*/
|
|
1191
|
+
clear() {
|
|
1192
|
+
if (this.#queue.size === 0) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
this.#queue = new this.#queueClass();
|
|
1196
|
+
const error = new DOMException("The queue was cleared", "AbortError");
|
|
1197
|
+
for (const waitingTask of this.#waitingTasks) {
|
|
1198
|
+
waitingTask.stopListeningForAbort?.();
|
|
1199
|
+
waitingTask.isCleared = true;
|
|
1200
|
+
waitingTask.promise?.catch(() => void 0);
|
|
1201
|
+
waitingTask.reject(error);
|
|
1202
|
+
}
|
|
1203
|
+
this.#waitingTasks.clear();
|
|
1204
|
+
this.#emitEmptyAndIdle();
|
|
1205
|
+
this.#updateRateLimitState();
|
|
1206
|
+
this.#emit("next");
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
Returns a promise that resolves when the queue is empty. Tasks can still be running.
|
|
1210
|
+
|
|
1211
|
+
The promise resolves once. Call `.onEmpty()` again to wait for the next time, or use the `empty` event to get every time.
|
|
1212
|
+
*/
|
|
1213
|
+
async onEmpty() {
|
|
1214
|
+
if (this.#queue.size === 0) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
await this.#onEvent(["empty", "active"], () => this.#queue.size === 0);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
Returns a promise that resolves when `queue.size < limit`.
|
|
1221
|
+
|
|
1222
|
+
Await it before you add a task, to keep the queue from growing beyond a size. It counts only waiting tasks, not running tasks.
|
|
1223
|
+
*/
|
|
1224
|
+
async onSizeLessThan(limit) {
|
|
1225
|
+
if (this.#queue.size < limit) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
await this.#onEvent(["next", "active"], () => this.#queue.size < limit);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
Returns a promise that resolves when the queue is empty and all tasks have completed: `queue.size === 0 && queue.pending === 0`.
|
|
1232
|
+
|
|
1233
|
+
Unlike `.onEmpty()`, it waits for running tasks to finish.
|
|
1234
|
+
|
|
1235
|
+
The promise resolves once. Call `.onIdle()` again to wait for the next time, or use the `idle` event to get every time.
|
|
1236
|
+
*/
|
|
1237
|
+
async onIdle() {
|
|
1238
|
+
if (this.#pending === 0 && this.#queue.size === 0) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
await this.#onEvent("idle");
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
Returns a promise that resolves when no tasks are running: `queue.pending === 0`.
|
|
1245
|
+
|
|
1246
|
+
Unlike `.onIdle()`, it ignores queued tasks. Use it to let running tasks finish before you change shared state. A task that timed out, or whose `signal` aborted while it ran, no longer counts, although its function can still run.
|
|
1247
|
+
|
|
1248
|
+
@example
|
|
1249
|
+
```
|
|
1250
|
+
queue.pause();
|
|
1251
|
+
await queue.onPendingZero();
|
|
1252
|
+
// No tasks are running, but the queue can still have tasks
|
|
1253
|
+
```
|
|
1254
|
+
*/
|
|
1255
|
+
async onPendingZero() {
|
|
1256
|
+
if (this.#pending === 0) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
await this.#onEvent("pendingZero");
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
Returns a promise that resolves when the queue becomes rate-limited by `intervalCap`. If the queue is already rate-limited, it resolves immediately.
|
|
1263
|
+
|
|
1264
|
+
Useful for backpressure, when tasks are added faster than they run.
|
|
1265
|
+
|
|
1266
|
+
@example
|
|
1267
|
+
```
|
|
1268
|
+
const queue = new PQueue({intervalCap: 5, interval: 1000});
|
|
1269
|
+
|
|
1270
|
+
for (let index = 0; index < 10; index++) {
|
|
1271
|
+
queue.add(() => someTask());
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
await queue.onRateLimit();
|
|
1275
|
+
console.log('The queue is rate-limited');
|
|
1276
|
+
```
|
|
1277
|
+
*/
|
|
1278
|
+
async onRateLimit() {
|
|
1279
|
+
this.#updateRateLimitState();
|
|
1280
|
+
if (this.isRateLimited) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
await this.#onEvent("rateLimit");
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
Returns a promise that resolves when the queue is no longer rate-limited. If the queue is not rate-limited, it resolves immediately.
|
|
1287
|
+
|
|
1288
|
+
@example
|
|
1289
|
+
```
|
|
1290
|
+
const queue = new PQueue({intervalCap: 5, interval: 1000});
|
|
1291
|
+
|
|
1292
|
+
for (let index = 0; index < 10; index++) {
|
|
1293
|
+
queue.add(() => someTask());
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
await queue.onRateLimitCleared();
|
|
1297
|
+
console.log('The rate limit is cleared');
|
|
1298
|
+
```
|
|
1299
|
+
*/
|
|
1300
|
+
async onRateLimitCleared() {
|
|
1301
|
+
this.#updateRateLimitState();
|
|
1302
|
+
if (!this.isRateLimited) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
await this.#onEvent("rateLimitCleared");
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
Returns a promise that rejects when a task fails: it throws, times out, or its `signal` aborts while it runs. It rejects with the error of the task, or with `null` if the error is `undefined`, like the `error` event.
|
|
1309
|
+
|
|
1310
|
+
Use `Promise.race([queue.onError(), queue.onIdle()])` to stop at the first error, and to resolve normally when the queue becomes idle. Each call adds an `error` listener that stays until a task fails, so to get every error, for example in a loop, use the `error` event instead.
|
|
1311
|
+
|
|
1312
|
+
Important: The promises returned by `.add()` still reject. Handle each of them (for example with `.catch(() => {})`) to prevent unhandled rejections.
|
|
1313
|
+
|
|
1314
|
+
@example
|
|
1315
|
+
```
|
|
1316
|
+
import PQueue from 'p-queue';
|
|
1317
|
+
|
|
1318
|
+
const queue = new PQueue({concurrency: 2});
|
|
1319
|
+
|
|
1320
|
+
// Do not start the remaining tasks. Pause in an `error` listener, because it runs before the next task starts, and the code after `await` runs later.
|
|
1321
|
+
queue.addEventListener('error', () => {
|
|
1322
|
+
queue.pause();
|
|
1323
|
+
}, {once: true});
|
|
1324
|
+
|
|
1325
|
+
queue.add(() => fetchData(1)).catch(() => {});
|
|
1326
|
+
queue.add(() => fetchData(2)).catch(() => {});
|
|
1327
|
+
queue.add(() => fetchData(3)).catch(() => {});
|
|
1328
|
+
|
|
1329
|
+
try {
|
|
1330
|
+
await Promise.race([
|
|
1331
|
+
queue.onError(),
|
|
1332
|
+
queue.onIdle()
|
|
1333
|
+
]);
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
console.error('Queue failed:', error);
|
|
1336
|
+
}
|
|
1337
|
+
```
|
|
1338
|
+
*/
|
|
1339
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
1340
|
+
onError() {
|
|
1341
|
+
return new Promise((_resolve, reject) => {
|
|
1342
|
+
this.addEventListener("error", (event) => {
|
|
1343
|
+
reject(event.detail);
|
|
1344
|
+
}, { once: true });
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
The number of tasks waiting to run.
|
|
1349
|
+
*/
|
|
1350
|
+
get size() {
|
|
1351
|
+
return this.#queue.size;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
The number of waiting tasks with the given `priority`.
|
|
1355
|
+
|
|
1356
|
+
@example
|
|
1357
|
+
```
|
|
1358
|
+
import PQueue from 'p-queue';
|
|
1359
|
+
|
|
1360
|
+
const queue = new PQueue();
|
|
1361
|
+
|
|
1362
|
+
// Paused, so the tasks stay queued. Without it they all start synchronously and the queue is empty by the time `sizeBy` reads it.
|
|
1363
|
+
queue.pause();
|
|
1364
|
+
|
|
1365
|
+
queue.add(async () => '🦄', {priority: 1});
|
|
1366
|
+
queue.add(async () => '🦄', {priority: 0});
|
|
1367
|
+
queue.add(async () => '🦄', {priority: 1});
|
|
1368
|
+
|
|
1369
|
+
console.log(queue.sizeBy({priority: 1}));
|
|
1370
|
+
//=> 2
|
|
1371
|
+
|
|
1372
|
+
console.log(queue.sizeBy({priority: 0}));
|
|
1373
|
+
//=> 1
|
|
1374
|
+
```
|
|
1375
|
+
*/
|
|
1376
|
+
sizeBy(options) {
|
|
1377
|
+
return this.#queue.filter(options).length;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
The number of running tasks.
|
|
1381
|
+
*/
|
|
1382
|
+
get pending() {
|
|
1383
|
+
return this.#pending;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
Whether the queue is paused.
|
|
1387
|
+
*/
|
|
1388
|
+
get isPaused() {
|
|
1389
|
+
return this.#isPaused;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
Whether the queue is rate-limited: the number of tasks started in the current interval, plus the tasks still running from earlier intervals with `carryoverIntervalCount`, has reached `intervalCap`, and tasks are waiting.
|
|
1393
|
+
*/
|
|
1394
|
+
get isRateLimited() {
|
|
1395
|
+
return this.#isRateLimitedNow;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
Whether tasks are waiting because all concurrency slots are in use, or because the queue is rate-limited.
|
|
1399
|
+
|
|
1400
|
+
Useful to detect backpressure and tasks that hang.
|
|
1401
|
+
|
|
1402
|
+
@example
|
|
1403
|
+
```
|
|
1404
|
+
import PQueue from 'p-queue';
|
|
1405
|
+
|
|
1406
|
+
const queue = new PQueue({concurrency: 2});
|
|
1407
|
+
|
|
1408
|
+
// Backpressure
|
|
1409
|
+
if (queue.isSaturated) {
|
|
1410
|
+
await queue.onSizeLessThan(queue.concurrency);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// Monitoring
|
|
1414
|
+
setInterval(() => {
|
|
1415
|
+
if (queue.isSaturated) {
|
|
1416
|
+
console.warn(`Queue saturated: ${queue.pending} running, ${queue.size} waiting`);
|
|
1417
|
+
}
|
|
1418
|
+
}, 60000);
|
|
1419
|
+
```
|
|
1420
|
+
*/
|
|
1421
|
+
get isSaturated() {
|
|
1422
|
+
return this.#queue.size > 0 && (this.#pending >= this.#concurrency || this.isRateLimited);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
An array with the running tasks. Each task has its `id`, `priority`, `startTime`, `timeout`, and `timeoutRemaining` (the milliseconds until it times out). Without a timeout, `timeout` and `timeoutRemaining` are `undefined`.
|
|
1426
|
+
|
|
1427
|
+
@example
|
|
1428
|
+
```
|
|
1429
|
+
import PQueue from 'p-queue';
|
|
1430
|
+
|
|
1431
|
+
const queue = new PQueue({concurrency: 2, timeout: 10000});
|
|
1432
|
+
|
|
1433
|
+
queue.add(() => fetchUser(123), {id: 'user-123'});
|
|
1434
|
+
queue.add(() => fetchPosts(456), {id: 'posts-456', priority: 1});
|
|
1435
|
+
|
|
1436
|
+
console.log(queue.runningTasks);
|
|
1437
|
+
// => [{
|
|
1438
|
+
// id: 'user-123',
|
|
1439
|
+
// priority: 0,
|
|
1440
|
+
// startTime: 1759253001716,
|
|
1441
|
+
// timeout: 10000,
|
|
1442
|
+
// timeoutRemaining: 10000
|
|
1443
|
+
// }, {
|
|
1444
|
+
// id: 'posts-456',
|
|
1445
|
+
// priority: 1,
|
|
1446
|
+
// startTime: 1759253001716,
|
|
1447
|
+
// timeout: 10000,
|
|
1448
|
+
// timeoutRemaining: 10000
|
|
1449
|
+
// }]
|
|
1450
|
+
```
|
|
1451
|
+
*/
|
|
1452
|
+
get runningTasks() {
|
|
1453
|
+
return Array.from(this.#runningTasks, (task) => ({
|
|
1454
|
+
...task,
|
|
1455
|
+
timeoutRemaining: task.timeout === void 0 ? void 0 : Math.max(0, task.startTime + task.timeout - Date.now())
|
|
1456
|
+
}));
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
var TimeoutError = class extends DOMException {
|
|
1460
|
+
constructor(message) {
|
|
1461
|
+
super(message, "TimeoutError");
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
// src/orchestration/canonical/KrnlQ.ts
|
|
1466
|
+
var import_node_crypto = require("crypto");
|
|
1467
|
+
|
|
1468
|
+
// src/orchestration/canonical/model.ts
|
|
1469
|
+
function validateOrchestrationDAG(steps) {
|
|
1470
|
+
const errors = [];
|
|
1471
|
+
const stepMap = /* @__PURE__ */ new Map();
|
|
1472
|
+
for (const step of steps) {
|
|
1473
|
+
if (!step.id || typeof step.id !== "string" || step.id.trim() === "") {
|
|
1474
|
+
errors.push("Step must have a non-empty string ID.");
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (stepMap.has(step.id)) {
|
|
1478
|
+
errors.push(`Duplicate step ID detected: "${step.id}"`);
|
|
1479
|
+
}
|
|
1480
|
+
stepMap.set(step.id, step);
|
|
1481
|
+
if (step.timeoutPolicy) {
|
|
1482
|
+
if (typeof step.timeoutPolicy.timeoutMs !== "number" || step.timeoutPolicy.timeoutMs <= 0) {
|
|
1483
|
+
errors.push(`Step "${step.id}" has invalid timeoutMs: must be a positive number.`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (step.retryPolicy) {
|
|
1487
|
+
if (typeof step.retryPolicy.maxAttempts !== "number" || step.retryPolicy.maxAttempts < 1) {
|
|
1488
|
+
errors.push(`Step "${step.id}" has invalid maxAttempts: must be >= 1.`);
|
|
1489
|
+
}
|
|
1490
|
+
if (step.retryPolicy.backoffMs !== void 0 && step.retryPolicy.backoffMs < 0) {
|
|
1491
|
+
errors.push(`Step "${step.id}" has negative backoffMs.`);
|
|
1492
|
+
}
|
|
1493
|
+
if (step.retryPolicy.initialDelayMs !== void 0 && step.retryPolicy.initialDelayMs < 0) {
|
|
1494
|
+
errors.push(`Step "${step.id}" has negative initialDelayMs.`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
if (step.concurrency !== void 0 && (typeof step.concurrency !== "number" || step.concurrency < 1)) {
|
|
1498
|
+
errors.push(`Step "${step.id}" has invalid concurrency: must be >= 1.`);
|
|
1499
|
+
}
|
|
1500
|
+
if (step.type === "task" || step.type === "sequential") {
|
|
1501
|
+
if (!step.target && !step.handler) {
|
|
1502
|
+
errors.push(`Step "${step.id}" requires a non-empty target or handler.`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
for (const step of steps) {
|
|
1507
|
+
if (step.dependsOn) {
|
|
1508
|
+
for (const dep of step.dependsOn) {
|
|
1509
|
+
if (!stepMap.has(dep)) {
|
|
1510
|
+
errors.push(`Step "${step.id}" depends on unknown step "${dep}".`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (errors.length > 0) {
|
|
1516
|
+
return { valid: false, order: [], errors };
|
|
1517
|
+
}
|
|
1518
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1519
|
+
const graph = /* @__PURE__ */ new Map();
|
|
1520
|
+
for (const step of steps) {
|
|
1521
|
+
inDegree.set(step.id, 0);
|
|
1522
|
+
graph.set(step.id, []);
|
|
1523
|
+
}
|
|
1524
|
+
for (const step of steps) {
|
|
1525
|
+
if (step.dependsOn) {
|
|
1526
|
+
for (const dep of step.dependsOn) {
|
|
1527
|
+
inDegree.set(step.id, (inDegree.get(step.id) || 0) + 1);
|
|
1528
|
+
graph.get(dep)?.push(step.id);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const queue = [];
|
|
1533
|
+
for (const [id, deg] of inDegree.entries()) {
|
|
1534
|
+
if (deg === 0) queue.push(id);
|
|
1535
|
+
}
|
|
1536
|
+
const order = [];
|
|
1537
|
+
while (queue.length > 0) {
|
|
1538
|
+
const current = queue.shift();
|
|
1539
|
+
order.push(current);
|
|
1540
|
+
const neighbors = graph.get(current) || [];
|
|
1541
|
+
for (const neighbor of neighbors) {
|
|
1542
|
+
const newDeg = (inDegree.get(neighbor) || 0) - 1;
|
|
1543
|
+
inDegree.set(neighbor, newDeg);
|
|
1544
|
+
if (newDeg === 0) {
|
|
1545
|
+
queue.push(neighbor);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (order.length !== steps.length) {
|
|
1550
|
+
errors.push("Circular dependency cycle detected in orchestration DAG.");
|
|
1551
|
+
return { valid: false, order: [], errors };
|
|
1552
|
+
}
|
|
1553
|
+
return { valid: true, order, errors: [] };
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/orchestration/canonical/WorkerExecutionBoundary.ts
|
|
1557
|
+
var WorkerExecutionBoundary = class _WorkerExecutionBoundary {
|
|
1558
|
+
constructor(workerId, handler) {
|
|
1559
|
+
this.workerId = workerId;
|
|
1560
|
+
this.handler = handler;
|
|
1561
|
+
}
|
|
1562
|
+
workerId;
|
|
1563
|
+
handler;
|
|
1564
|
+
async processJob(command, metadata) {
|
|
1565
|
+
if (!this.handler) {
|
|
1566
|
+
return {
|
|
1567
|
+
correlationId: command.correlationId,
|
|
1568
|
+
status: "COMPLETED",
|
|
1569
|
+
data: command.payload,
|
|
1570
|
+
executionTimestamp: Date.now()
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
const context = {
|
|
1574
|
+
isWorkerContext: true,
|
|
1575
|
+
workerId: this.workerId,
|
|
1576
|
+
timestamp: Date.now(),
|
|
1577
|
+
correlationId: command.correlationId,
|
|
1578
|
+
metadata
|
|
1579
|
+
};
|
|
1580
|
+
_WorkerExecutionBoundary.assertWorkerContext(context);
|
|
1581
|
+
return await this.handler(command, context);
|
|
1582
|
+
}
|
|
1583
|
+
static assertWorkerContext(context) {
|
|
1584
|
+
if (!context || !context.isWorkerContext) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
"[WorkerExecutionBoundary] Violation: DIRECT_CALL -> SERVICE_BOUNDARY is strictly forbidden. All operations must execute inside KrnlWorker context."
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
// src/orchestration/canonical/KrnlWorkerPool.ts
|
|
1593
|
+
var KrnlWorkerPool = class {
|
|
1594
|
+
constructor(queueName, workerId = `worker_${queueName}_${Math.random().toString(36).substring(2, 7)}`) {
|
|
1595
|
+
this.queueName = queueName;
|
|
1596
|
+
this.workerId = workerId;
|
|
1597
|
+
this.workerBoundary = new WorkerExecutionBoundary(this.workerId);
|
|
1598
|
+
}
|
|
1599
|
+
queueName;
|
|
1600
|
+
workerId;
|
|
1601
|
+
state = "created";
|
|
1602
|
+
activeJobsCount = 0;
|
|
1603
|
+
startedAt = Date.now();
|
|
1604
|
+
workerBoundary;
|
|
1605
|
+
getState() {
|
|
1606
|
+
return this.state;
|
|
1607
|
+
}
|
|
1608
|
+
getActiveJobs() {
|
|
1609
|
+
return this.activeJobsCount;
|
|
1610
|
+
}
|
|
1611
|
+
getWorkerBoundary() {
|
|
1612
|
+
return this.workerBoundary;
|
|
1613
|
+
}
|
|
1614
|
+
getInfo() {
|
|
1615
|
+
return {
|
|
1616
|
+
id: this.workerId,
|
|
1617
|
+
queueName: this.queueName,
|
|
1618
|
+
state: this.state,
|
|
1619
|
+
startedAt: this.startedAt,
|
|
1620
|
+
activeJobs: this.activeJobsCount
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
start() {
|
|
1624
|
+
if (this.state === "stopped") {
|
|
1625
|
+
throw new Error(`[KrnlWorkerPool:${this.workerId}] Cannot start stopped worker.`);
|
|
1626
|
+
}
|
|
1627
|
+
this.state = "started";
|
|
1628
|
+
}
|
|
1629
|
+
createContext(correlationId, metadata) {
|
|
1630
|
+
return {
|
|
1631
|
+
isWorkerContext: true,
|
|
1632
|
+
workerId: this.workerId,
|
|
1633
|
+
timestamp: Date.now(),
|
|
1634
|
+
correlationId: correlationId || `corr_${Math.random().toString(36).substring(2, 11)}`,
|
|
1635
|
+
metadata
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
async runWithContext(correlationId, metadata, fn) {
|
|
1639
|
+
if (this.state === "stopped" || this.state === "draining") {
|
|
1640
|
+
throw new Error(`[KrnlWorkerPool:${this.workerId}] Worker is not accepting new jobs (state: ${this.state}).`);
|
|
1641
|
+
}
|
|
1642
|
+
this.state = "running";
|
|
1643
|
+
this.activeJobsCount++;
|
|
1644
|
+
const ctx = this.createContext(correlationId, metadata);
|
|
1645
|
+
WorkerExecutionBoundary.assertWorkerContext(ctx);
|
|
1646
|
+
try {
|
|
1647
|
+
return await fn(ctx);
|
|
1648
|
+
} finally {
|
|
1649
|
+
this.activeJobsCount--;
|
|
1650
|
+
if (this.activeJobsCount === 0 && this.state === "running") {
|
|
1651
|
+
this.state = "started";
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
async drain() {
|
|
1656
|
+
this.state = "draining";
|
|
1657
|
+
while (this.activeJobsCount > 0) {
|
|
1658
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
async stop() {
|
|
1662
|
+
await this.drain();
|
|
1663
|
+
this.state = "stopped";
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
// src/orchestration/canonical/KrnlQ.ts
|
|
1668
|
+
function generateSecureId(prefix) {
|
|
1669
|
+
try {
|
|
1670
|
+
return `${prefix}_${(0, import_node_crypto.randomUUID)()}`;
|
|
1671
|
+
} catch {
|
|
1672
|
+
return `${prefix}_${Math.random().toString(36).substring(2, 11)}_${Date.now()}`;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
var KrnlQueue = class {
|
|
1676
|
+
queueName;
|
|
1677
|
+
queue;
|
|
1678
|
+
workerPool;
|
|
1679
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1680
|
+
idempotencyStore = /* @__PURE__ */ new Map();
|
|
1681
|
+
activeOrchestrations = /* @__PURE__ */ new Map();
|
|
1682
|
+
maxRetainedJobs;
|
|
1683
|
+
constructor(options = {}) {
|
|
1684
|
+
this.queueName = options.queueName ?? "krnl_default_queue";
|
|
1685
|
+
this.maxRetainedJobs = options.maxRetainedJobs ?? 1e3;
|
|
1686
|
+
const concurrency = options.concurrency ?? 10;
|
|
1687
|
+
this.queue = new PQueue({
|
|
1688
|
+
concurrency,
|
|
1689
|
+
autoStart: options.autoStart ?? true
|
|
1690
|
+
});
|
|
1691
|
+
this.workerPool = new KrnlWorkerPool(this.queueName);
|
|
1692
|
+
this.workerPool.start();
|
|
1693
|
+
}
|
|
1694
|
+
get concurrency() {
|
|
1695
|
+
return this.queue.concurrency;
|
|
1696
|
+
}
|
|
1697
|
+
set concurrency(value) {
|
|
1698
|
+
this.queue.concurrency = value;
|
|
1699
|
+
}
|
|
1700
|
+
get size() {
|
|
1701
|
+
return this.queue.size;
|
|
1702
|
+
}
|
|
1703
|
+
get pending() {
|
|
1704
|
+
return this.queue.pending;
|
|
1705
|
+
}
|
|
1706
|
+
get isPaused() {
|
|
1707
|
+
return this.queue.isPaused;
|
|
1708
|
+
}
|
|
1709
|
+
getWorkerBoundary() {
|
|
1710
|
+
return this.workerPool.getWorkerBoundary();
|
|
1711
|
+
}
|
|
1712
|
+
getWorkerPool() {
|
|
1713
|
+
return this.workerPool;
|
|
1714
|
+
}
|
|
1715
|
+
pause() {
|
|
1716
|
+
this.queue.pause();
|
|
1717
|
+
}
|
|
1718
|
+
resume() {
|
|
1719
|
+
this.queue.start();
|
|
1720
|
+
}
|
|
1721
|
+
clear() {
|
|
1722
|
+
this.queue.clear();
|
|
1723
|
+
for (const job of this.jobs.values()) {
|
|
1724
|
+
if (job.status === "waiting") {
|
|
1725
|
+
job.status = "cancelled";
|
|
1726
|
+
job.completedAt = Date.now();
|
|
1727
|
+
job.abortController.abort();
|
|
1728
|
+
const cancelErr = new Error("The operation was aborted");
|
|
1729
|
+
cancelErr.name = "AbortError";
|
|
1730
|
+
job.rejectPromise(cancelErr);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
pruneJobs() {
|
|
1735
|
+
if (this.jobs.size <= this.maxRetainedJobs) return;
|
|
1736
|
+
const excess = this.jobs.size - this.maxRetainedJobs;
|
|
1737
|
+
let pruned = 0;
|
|
1738
|
+
for (const [id, record] of this.jobs.entries()) {
|
|
1739
|
+
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled") {
|
|
1740
|
+
this.jobs.delete(id);
|
|
1741
|
+
pruned++;
|
|
1742
|
+
if (pruned >= excess) break;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async drain() {
|
|
1747
|
+
await this.queue.onIdle();
|
|
1748
|
+
}
|
|
1749
|
+
async shutdown(options = { drain: true }) {
|
|
1750
|
+
if (options.drain) {
|
|
1751
|
+
await this.drain();
|
|
1752
|
+
} else {
|
|
1753
|
+
this.clear();
|
|
1754
|
+
}
|
|
1755
|
+
for (const active of this.activeOrchestrations.values()) {
|
|
1756
|
+
active.abortController.abort();
|
|
1757
|
+
active.context.isCancelled = true;
|
|
1758
|
+
}
|
|
1759
|
+
this.activeOrchestrations.clear();
|
|
1760
|
+
await this.workerPool.stop();
|
|
1761
|
+
}
|
|
1762
|
+
async close() {
|
|
1763
|
+
await this.shutdown({ drain: false });
|
|
1764
|
+
}
|
|
1765
|
+
async dispose() {
|
|
1766
|
+
await this.shutdown({ drain: false });
|
|
1767
|
+
}
|
|
1768
|
+
async start() {
|
|
1769
|
+
this.resume();
|
|
1770
|
+
}
|
|
1771
|
+
async stop() {
|
|
1772
|
+
this.pause();
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Enqueues a job returning a strong handle.
|
|
1776
|
+
*/
|
|
1777
|
+
enqueue(jobDef) {
|
|
1778
|
+
this.pruneJobs();
|
|
1779
|
+
const jobId = jobDef.id || generateSecureId("job");
|
|
1780
|
+
const abortController = new AbortController();
|
|
1781
|
+
if (jobDef.options?.abortSignal) {
|
|
1782
|
+
if (jobDef.options.abortSignal.aborted) {
|
|
1783
|
+
abortController.abort();
|
|
1784
|
+
} else {
|
|
1785
|
+
jobDef.options.abortSignal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
let resolvePromise;
|
|
1789
|
+
let rejectPromise;
|
|
1790
|
+
const promise = new Promise((res, rej) => {
|
|
1791
|
+
resolvePromise = res;
|
|
1792
|
+
rejectPromise = rej;
|
|
1793
|
+
});
|
|
1794
|
+
const record = {
|
|
1795
|
+
id: jobId,
|
|
1796
|
+
name: jobDef.name,
|
|
1797
|
+
status: "waiting",
|
|
1798
|
+
createdAt: Date.now(),
|
|
1799
|
+
abortController,
|
|
1800
|
+
resolvePromise,
|
|
1801
|
+
rejectPromise,
|
|
1802
|
+
promise
|
|
1803
|
+
};
|
|
1804
|
+
this.jobs.set(jobId, record);
|
|
1805
|
+
const priority = jobDef.options?.priority ?? 0;
|
|
1806
|
+
this.queue.add(
|
|
1807
|
+
async () => {
|
|
1808
|
+
if (record.status === "cancelled" || abortController.signal.aborted) {
|
|
1809
|
+
record.status = "cancelled";
|
|
1810
|
+
const cancelErr = new Error("The operation was aborted");
|
|
1811
|
+
cancelErr.name = "AbortError";
|
|
1812
|
+
rejectPromise(cancelErr);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
record.status = "active";
|
|
1816
|
+
record.startedAt = Date.now();
|
|
1817
|
+
const idemKey = jobDef.options?.idempotencyKey;
|
|
1818
|
+
if (idemKey && this.idempotencyStore.has(idemKey)) {
|
|
1819
|
+
const cached = this.idempotencyStore.get(idemKey);
|
|
1820
|
+
record.status = cached.status === "COMPLETED" ? "completed" : "failed";
|
|
1821
|
+
record.completedAt = Date.now();
|
|
1822
|
+
if (cached.status === "COMPLETED") {
|
|
1823
|
+
record.result = cached.data;
|
|
1824
|
+
resolvePromise(cached.data);
|
|
1825
|
+
} else {
|
|
1826
|
+
record.error = cached.error;
|
|
1827
|
+
rejectPromise(new Error(cached.error));
|
|
1828
|
+
}
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const retries = jobDef.options?.retries ?? 0;
|
|
1832
|
+
const maxAttempts = retries + 1;
|
|
1833
|
+
const timeoutMs = jobDef.options?.timeoutMs;
|
|
1834
|
+
const backoffMs = jobDef.options?.backoffMs ?? 100;
|
|
1835
|
+
const backoffPolicy = jobDef.options?.backoffPolicy ?? "exponential";
|
|
1836
|
+
let attempt = 0;
|
|
1837
|
+
let lastError;
|
|
1838
|
+
while (attempt < maxAttempts) {
|
|
1839
|
+
if (abortController.signal.aborted) {
|
|
1840
|
+
record.status = "cancelled";
|
|
1841
|
+
const cancelErr = new Error("The operation was aborted");
|
|
1842
|
+
cancelErr.name = "AbortError";
|
|
1843
|
+
rejectPromise(cancelErr);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
attempt++;
|
|
1847
|
+
try {
|
|
1848
|
+
const result = await this.workerPool.runWithContext(
|
|
1849
|
+
jobDef.options?.correlationId,
|
|
1850
|
+
jobDef.options?.metadata,
|
|
1851
|
+
async (workerCtx) => {
|
|
1852
|
+
let timer;
|
|
1853
|
+
try {
|
|
1854
|
+
const workPromise = jobDef.handler(jobDef.payload, workerCtx);
|
|
1855
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
1856
|
+
const timeoutPromise = new Promise((_, rej) => {
|
|
1857
|
+
timer = setTimeout(() => {
|
|
1858
|
+
const err = new Error(`Job "${jobDef.name}" timed out after ${timeoutMs}ms.`);
|
|
1859
|
+
err.name = "TimeoutError";
|
|
1860
|
+
rej(err);
|
|
1861
|
+
}, timeoutMs);
|
|
1862
|
+
});
|
|
1863
|
+
return await Promise.race([workPromise, timeoutPromise]);
|
|
1864
|
+
}
|
|
1865
|
+
return await workPromise;
|
|
1866
|
+
} finally {
|
|
1867
|
+
if (timer) clearTimeout(timer);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
);
|
|
1871
|
+
record.status = "completed";
|
|
1872
|
+
record.completedAt = Date.now();
|
|
1873
|
+
record.result = result;
|
|
1874
|
+
if (idemKey) {
|
|
1875
|
+
this.idempotencyStore.set(idemKey, {
|
|
1876
|
+
correlationId: jobDef.options?.correlationId || jobId,
|
|
1877
|
+
status: "COMPLETED",
|
|
1878
|
+
data: result,
|
|
1879
|
+
executionTimestamp: Date.now()
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
resolvePromise(result);
|
|
1883
|
+
return;
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
lastError = err;
|
|
1886
|
+
if (abortController.signal.aborted || err?.name === "AbortError") {
|
|
1887
|
+
record.status = "cancelled";
|
|
1888
|
+
record.completedAt = Date.now();
|
|
1889
|
+
rejectPromise(err);
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
if (attempt < maxAttempts) {
|
|
1893
|
+
const delay = backoffPolicy === "exponential" ? backoffMs * Math.pow(2, attempt - 1) : backoffMs * attempt;
|
|
1894
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
record.status = "failed";
|
|
1899
|
+
record.completedAt = Date.now();
|
|
1900
|
+
record.error = lastError?.message || String(lastError);
|
|
1901
|
+
if (idemKey) {
|
|
1902
|
+
this.idempotencyStore.set(idemKey, {
|
|
1903
|
+
correlationId: jobDef.options?.correlationId || jobId,
|
|
1904
|
+
status: "FAILED",
|
|
1905
|
+
error: record.error,
|
|
1906
|
+
executionTimestamp: Date.now()
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
rejectPromise(lastError);
|
|
1910
|
+
},
|
|
1911
|
+
{ priority }
|
|
1912
|
+
);
|
|
1913
|
+
return {
|
|
1914
|
+
id: jobId,
|
|
1915
|
+
name: jobDef.name,
|
|
1916
|
+
getStatus: () => record.status,
|
|
1917
|
+
result: () => record.promise,
|
|
1918
|
+
cancel: () => this.cancel(jobId)
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
cancel(jobId) {
|
|
1922
|
+
const job = this.jobs.get(jobId);
|
|
1923
|
+
if (!job) return false;
|
|
1924
|
+
if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
job.status = "cancelled";
|
|
1928
|
+
job.abortController.abort();
|
|
1929
|
+
return true;
|
|
1930
|
+
}
|
|
1931
|
+
getJob(jobId) {
|
|
1932
|
+
return this.jobs.get(jobId);
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Dispatches a single command through the KrnlQ worker boundary.
|
|
1936
|
+
*/
|
|
1937
|
+
async executeCommand(command, handler) {
|
|
1938
|
+
if (command.idempotencyKey && this.idempotencyStore.has(command.idempotencyKey)) {
|
|
1939
|
+
return this.idempotencyStore.get(command.idempotencyKey);
|
|
1940
|
+
}
|
|
1941
|
+
try {
|
|
1942
|
+
const handle = this.enqueue({
|
|
1943
|
+
id: command.id,
|
|
1944
|
+
name: command.type,
|
|
1945
|
+
payload: command.payload,
|
|
1946
|
+
handler,
|
|
1947
|
+
options: {
|
|
1948
|
+
idempotencyKey: command.idempotencyKey,
|
|
1949
|
+
correlationId: command.correlationId
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
const data = await handle.result();
|
|
1953
|
+
const res = {
|
|
1954
|
+
correlationId: command.correlationId,
|
|
1955
|
+
status: "COMPLETED",
|
|
1956
|
+
data,
|
|
1957
|
+
executionTimestamp: Date.now()
|
|
1958
|
+
};
|
|
1959
|
+
return res;
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
const isAbort = err?.name === "AbortError" || err?.message === "The operation was aborted";
|
|
1962
|
+
const res = {
|
|
1963
|
+
correlationId: command.correlationId,
|
|
1964
|
+
status: isAbort ? "CANCELLED" : "FAILED",
|
|
1965
|
+
error: err?.message || String(err),
|
|
1966
|
+
executionTimestamp: Date.now()
|
|
1967
|
+
};
|
|
1968
|
+
return res;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Executes a full Canonical Orchestration DAG through KrnlQ and worker boundary.
|
|
1973
|
+
* This is a first-class queued job scheduled via this.enqueue() to respect concurrency and worker limits.
|
|
1974
|
+
*/
|
|
1975
|
+
async executeOrchestration(descriptor, steps, input, options = {}) {
|
|
1976
|
+
const startedAt = Date.now();
|
|
1977
|
+
const correlationId = options.correlationId || generateSecureId("corr");
|
|
1978
|
+
const executionId = generateSecureId("exec");
|
|
1979
|
+
if (options.idempotencyKey && this.idempotencyStore.has(options.idempotencyKey)) {
|
|
1980
|
+
const cached = this.idempotencyStore.get(options.idempotencyKey);
|
|
1981
|
+
return cached.data;
|
|
1982
|
+
}
|
|
1983
|
+
const validation = validateOrchestrationDAG(steps);
|
|
1984
|
+
if (!validation.valid) {
|
|
1985
|
+
return {
|
|
1986
|
+
orchestrationId: descriptor.id,
|
|
1987
|
+
correlationId,
|
|
1988
|
+
status: "FAILED",
|
|
1989
|
+
stepResults: {},
|
|
1990
|
+
error: `Invalid DAG: ${validation.errors.join("; ")}`,
|
|
1991
|
+
startedAt,
|
|
1992
|
+
completedAt: Date.now(),
|
|
1993
|
+
executionMetadata: { validationErrors: validation.errors }
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
const abortController = new AbortController();
|
|
1997
|
+
if (options.abortSignal) {
|
|
1998
|
+
if (options.abortSignal.aborted) {
|
|
1999
|
+
abortController.abort();
|
|
2000
|
+
} else {
|
|
2001
|
+
options.abortSignal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
const context = {
|
|
2005
|
+
correlationId,
|
|
2006
|
+
parentCorrelationId: options.parentCorrelationId,
|
|
2007
|
+
orchestrationId: descriptor.id,
|
|
2008
|
+
executionId,
|
|
2009
|
+
idempotencyKey: options.idempotencyKey,
|
|
2010
|
+
scope: descriptor.scope || "functionality",
|
|
2011
|
+
results: { input },
|
|
2012
|
+
metadata: { ...options.metadata },
|
|
2013
|
+
timestamp: startedAt,
|
|
2014
|
+
isCancelled: abortController.signal.aborted,
|
|
2015
|
+
abortSignal: abortController.signal
|
|
2016
|
+
};
|
|
2017
|
+
abortController.signal.addEventListener("abort", () => {
|
|
2018
|
+
context.isCancelled = true;
|
|
2019
|
+
});
|
|
2020
|
+
this.activeOrchestrations.set(executionId, { abortController, context });
|
|
2021
|
+
const handle = this.enqueue({
|
|
2022
|
+
id: executionId,
|
|
2023
|
+
name: descriptor.name || descriptor.id,
|
|
2024
|
+
payload: input,
|
|
2025
|
+
options: {
|
|
2026
|
+
idempotencyKey: options.idempotencyKey,
|
|
2027
|
+
correlationId,
|
|
2028
|
+
metadata: options.metadata,
|
|
2029
|
+
abortSignal: abortController.signal
|
|
2030
|
+
},
|
|
2031
|
+
handler: async () => {
|
|
2032
|
+
const timeoutMs = options.timeoutMs || descriptor.timeoutMs;
|
|
2033
|
+
let timeoutTimer;
|
|
2034
|
+
let isWorkflowTimeout = false;
|
|
2035
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
2036
|
+
timeoutTimer = setTimeout(() => {
|
|
2037
|
+
isWorkflowTimeout = true;
|
|
2038
|
+
context.isCancelled = true;
|
|
2039
|
+
abortController.abort();
|
|
2040
|
+
}, timeoutMs);
|
|
2041
|
+
}
|
|
2042
|
+
const stepResults = {};
|
|
2043
|
+
const executedSteps = /* @__PURE__ */ new Set();
|
|
2044
|
+
const executedComps = [];
|
|
2045
|
+
let finalOutput = void 0;
|
|
2046
|
+
let orchestrationStatus = "COMPLETED";
|
|
2047
|
+
let errorMessage;
|
|
2048
|
+
try {
|
|
2049
|
+
while (executedSteps.size < steps.length) {
|
|
2050
|
+
if (context.isCancelled) {
|
|
2051
|
+
orchestrationStatus = "CANCELLED";
|
|
2052
|
+
errorMessage = "Orchestration execution cancelled.";
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
const readySteps = steps.filter((step) => {
|
|
2056
|
+
if (executedSteps.has(step.id)) return false;
|
|
2057
|
+
if (!step.dependsOn || step.dependsOn.length === 0) return true;
|
|
2058
|
+
return step.dependsOn.every((dep) => executedSteps.has(dep));
|
|
2059
|
+
});
|
|
2060
|
+
if (readySteps.length === 0) {
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
const batch = readySteps.slice(0, this.concurrency);
|
|
2064
|
+
const batchPromises = batch.map(async (step) => {
|
|
2065
|
+
const stepRes = await this.executeSingleStep(step, context, options.nestedRunner);
|
|
2066
|
+
stepResults[step.id] = stepRes;
|
|
2067
|
+
executedSteps.add(step.id);
|
|
2068
|
+
if (stepRes.status === "COMPLETED") {
|
|
2069
|
+
context.results[step.id] = stepRes.output;
|
|
2070
|
+
finalOutput = stepRes.output;
|
|
2071
|
+
if (step.compensation?.handler) {
|
|
2072
|
+
executedComps.push(() => step.compensation.handler(context));
|
|
2073
|
+
}
|
|
2074
|
+
} else if (stepRes.status === "SKIPPED") {
|
|
2075
|
+
} else if (stepRes.status === "CANCELLED") {
|
|
2076
|
+
const cancelErr = new Error("The operation was aborted");
|
|
2077
|
+
cancelErr.name = "AbortError";
|
|
2078
|
+
throw cancelErr;
|
|
2079
|
+
} else {
|
|
2080
|
+
throw new Error(stepRes.error || `Step "${step.id}" failed with status ${stepRes.status}`);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
await Promise.all(batchPromises);
|
|
2084
|
+
}
|
|
2085
|
+
} catch (err) {
|
|
2086
|
+
const isExplicitStepFailure = err && err.name !== "AbortError" && err.message !== "The operation was aborted";
|
|
2087
|
+
const wasCancelled = (context.isCancelled || abortController.signal.aborted || err?.name === "AbortError") && !isExplicitStepFailure;
|
|
2088
|
+
if (isWorkflowTimeout) {
|
|
2089
|
+
orchestrationStatus = "TIMED_OUT";
|
|
2090
|
+
errorMessage = `Orchestration timed out after ${timeoutMs}ms.`;
|
|
2091
|
+
} else if (isExplicitStepFailure) {
|
|
2092
|
+
orchestrationStatus = "FAILED";
|
|
2093
|
+
errorMessage = err?.message || String(err);
|
|
2094
|
+
} else if (wasCancelled) {
|
|
2095
|
+
orchestrationStatus = "CANCELLED";
|
|
2096
|
+
errorMessage = "The operation was aborted";
|
|
2097
|
+
} else {
|
|
2098
|
+
orchestrationStatus = "FAILED";
|
|
2099
|
+
errorMessage = err?.message || String(err);
|
|
2100
|
+
}
|
|
2101
|
+
if (executedComps.length > 0) {
|
|
2102
|
+
for (let i = executedComps.length - 1; i >= 0; i--) {
|
|
2103
|
+
try {
|
|
2104
|
+
await executedComps[i]();
|
|
2105
|
+
} catch (compErr) {
|
|
2106
|
+
console.error(`[KrnlQ] Compensation failed:`, compErr);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
} finally {
|
|
2111
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
2112
|
+
this.activeOrchestrations.delete(executionId);
|
|
2113
|
+
}
|
|
2114
|
+
const result = {
|
|
2115
|
+
orchestrationId: descriptor.id,
|
|
2116
|
+
correlationId,
|
|
2117
|
+
status: orchestrationStatus,
|
|
2118
|
+
output: finalOutput,
|
|
2119
|
+
stepResults,
|
|
2120
|
+
error: errorMessage,
|
|
2121
|
+
startedAt,
|
|
2122
|
+
completedAt: Date.now(),
|
|
2123
|
+
executionMetadata: {
|
|
2124
|
+
executionId,
|
|
2125
|
+
scope: descriptor.scope,
|
|
2126
|
+
stepsCount: steps.length,
|
|
2127
|
+
executedCount: executedSteps.size
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
if (options.idempotencyKey) {
|
|
2131
|
+
this.idempotencyStore.set(options.idempotencyKey, {
|
|
2132
|
+
correlationId,
|
|
2133
|
+
status: orchestrationStatus === "COMPLETED" ? "COMPLETED" : "FAILED",
|
|
2134
|
+
data: result,
|
|
2135
|
+
executionTimestamp: Date.now()
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
return result;
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
return await handle.result();
|
|
2142
|
+
}
|
|
2143
|
+
async executeSingleStep(step, context, nestedRunner) {
|
|
2144
|
+
const stepStart = Date.now();
|
|
2145
|
+
let attempts = 0;
|
|
2146
|
+
const maxAttempts = step.retryPolicy?.maxAttempts ?? 1;
|
|
2147
|
+
if (step.condition) {
|
|
2148
|
+
try {
|
|
2149
|
+
const conditionPass = await step.condition(context);
|
|
2150
|
+
if (!conditionPass) {
|
|
2151
|
+
return {
|
|
2152
|
+
stepId: step.id,
|
|
2153
|
+
status: "SKIPPED",
|
|
2154
|
+
attempts: 0,
|
|
2155
|
+
startedAt: stepStart,
|
|
2156
|
+
completedAt: Date.now()
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
} catch (condErr) {
|
|
2160
|
+
return {
|
|
2161
|
+
stepId: step.id,
|
|
2162
|
+
status: "FAILED",
|
|
2163
|
+
attempts: 1,
|
|
2164
|
+
error: `Condition evaluation failed: ${condErr?.message || String(condErr)}`,
|
|
2165
|
+
startedAt: stepStart,
|
|
2166
|
+
completedAt: Date.now()
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
let lastError;
|
|
2171
|
+
let stepCancelled = false;
|
|
2172
|
+
const stepTimeoutMs = step.timeoutPolicy?.timeoutMs;
|
|
2173
|
+
while (attempts < maxAttempts) {
|
|
2174
|
+
if (context.isCancelled) {
|
|
2175
|
+
return {
|
|
2176
|
+
stepId: step.id,
|
|
2177
|
+
status: "CANCELLED",
|
|
2178
|
+
attempts,
|
|
2179
|
+
startedAt: stepStart,
|
|
2180
|
+
completedAt: Date.now()
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
attempts++;
|
|
2184
|
+
const stepAbortController = new AbortController();
|
|
2185
|
+
if (context.abortSignal) {
|
|
2186
|
+
if (context.abortSignal.aborted) {
|
|
2187
|
+
stepAbortController.abort();
|
|
2188
|
+
} else {
|
|
2189
|
+
context.abortSignal.addEventListener("abort", () => stepAbortController.abort(), { once: true });
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
const stepContext = {
|
|
2193
|
+
...context,
|
|
2194
|
+
abortSignal: stepAbortController.signal
|
|
2195
|
+
};
|
|
2196
|
+
try {
|
|
2197
|
+
let stepOutput;
|
|
2198
|
+
let stepTimer;
|
|
2199
|
+
stepCancelled = false;
|
|
2200
|
+
const runPromise = (async () => {
|
|
2201
|
+
if (step.type === "nested" && step.nestedOrchestrationId && nestedRunner) {
|
|
2202
|
+
const childContext = {
|
|
2203
|
+
...stepContext,
|
|
2204
|
+
parentCorrelationId: context.correlationId,
|
|
2205
|
+
correlationId: `${context.correlationId}.${step.id}`
|
|
2206
|
+
};
|
|
2207
|
+
return await nestedRunner(step.nestedOrchestrationId, context.results, childContext);
|
|
2208
|
+
}
|
|
2209
|
+
if (step.type === "fanOut" && step.getItems) {
|
|
2210
|
+
const items = await step.getItems(stepContext);
|
|
2211
|
+
if (!Array.isArray(items)) throw new Error(`[fanOut] getItems must return an array.`);
|
|
2212
|
+
const concurrency = step.concurrency || 5;
|
|
2213
|
+
const results = [];
|
|
2214
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
2215
|
+
const slice = items.slice(i, i + concurrency);
|
|
2216
|
+
const sliceRes = await Promise.all(
|
|
2217
|
+
slice.map((item) => step.handler ? step.handler(item, stepContext) : Promise.resolve(item))
|
|
2218
|
+
);
|
|
2219
|
+
results.push(...sliceRes);
|
|
2220
|
+
}
|
|
2221
|
+
return results;
|
|
2222
|
+
}
|
|
2223
|
+
if (step.handler) {
|
|
2224
|
+
return await step.handler(context.results, stepContext);
|
|
2225
|
+
}
|
|
2226
|
+
return context.results;
|
|
2227
|
+
})();
|
|
2228
|
+
const abortPromise = new Promise((_, reject) => {
|
|
2229
|
+
if (stepAbortController.signal.aborted || context.isCancelled) {
|
|
2230
|
+
const err = new Error("The operation was aborted");
|
|
2231
|
+
err.name = "AbortError";
|
|
2232
|
+
reject(err);
|
|
2233
|
+
}
|
|
2234
|
+
stepAbortController.signal.addEventListener("abort", () => {
|
|
2235
|
+
const err = new Error("The operation was aborted");
|
|
2236
|
+
err.name = "AbortError";
|
|
2237
|
+
reject(err);
|
|
2238
|
+
});
|
|
2239
|
+
});
|
|
2240
|
+
const promisesToRace = [runPromise, abortPromise];
|
|
2241
|
+
if (stepTimeoutMs && stepTimeoutMs > 0) {
|
|
2242
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2243
|
+
stepTimer = setTimeout(() => {
|
|
2244
|
+
stepCancelled = true;
|
|
2245
|
+
const tErr = new Error(`Step "${step.id}" timed out after ${stepTimeoutMs}ms.`);
|
|
2246
|
+
tErr.name = "TimeoutError";
|
|
2247
|
+
reject(tErr);
|
|
2248
|
+
}, stepTimeoutMs);
|
|
2249
|
+
});
|
|
2250
|
+
promisesToRace.push(timeoutPromise);
|
|
2251
|
+
}
|
|
2252
|
+
stepOutput = await Promise.race(promisesToRace);
|
|
2253
|
+
if (stepTimer) clearTimeout(stepTimer);
|
|
2254
|
+
return {
|
|
2255
|
+
stepId: step.id,
|
|
2256
|
+
status: "COMPLETED",
|
|
2257
|
+
attempts,
|
|
2258
|
+
output: stepOutput,
|
|
2259
|
+
startedAt: stepStart,
|
|
2260
|
+
completedAt: Date.now()
|
|
2261
|
+
};
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
if (stepCancelled || err?.name === "TimeoutError") {
|
|
2264
|
+
return {
|
|
2265
|
+
stepId: step.id,
|
|
2266
|
+
status: "TIMED_OUT",
|
|
2267
|
+
attempts,
|
|
2268
|
+
error: err?.message || `Step "${step.id}" timed out after ${stepTimeoutMs}ms.`,
|
|
2269
|
+
startedAt: stepStart,
|
|
2270
|
+
completedAt: Date.now()
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
if (context.isCancelled || context.abortSignal?.aborted || err?.name === "AbortError" || err?.message === "Operation cancelled" || err?.message === "The operation was aborted") {
|
|
2274
|
+
return {
|
|
2275
|
+
stepId: step.id,
|
|
2276
|
+
status: "CANCELLED",
|
|
2277
|
+
attempts,
|
|
2278
|
+
startedAt: stepStart,
|
|
2279
|
+
completedAt: Date.now()
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
lastError = err;
|
|
2283
|
+
if (step.catchHandler && attempts >= maxAttempts) {
|
|
2284
|
+
try {
|
|
2285
|
+
const recovered = await step.catchHandler(err, stepContext);
|
|
2286
|
+
return {
|
|
2287
|
+
stepId: step.id,
|
|
2288
|
+
status: "COMPLETED",
|
|
2289
|
+
attempts,
|
|
2290
|
+
output: recovered,
|
|
2291
|
+
startedAt: stepStart,
|
|
2292
|
+
completedAt: Date.now()
|
|
2293
|
+
};
|
|
2294
|
+
} catch (catchErr) {
|
|
2295
|
+
lastError = catchErr;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
if (attempts < maxAttempts && step.retryPolicy) {
|
|
2299
|
+
const backoffDelay = this.computeBackoff(step.retryPolicy, attempts);
|
|
2300
|
+
await new Promise((r) => setTimeout(r, backoffDelay));
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return {
|
|
2305
|
+
stepId: step.id,
|
|
2306
|
+
status: "FAILED",
|
|
2307
|
+
attempts,
|
|
2308
|
+
error: lastError?.message || String(lastError),
|
|
2309
|
+
startedAt: stepStart,
|
|
2310
|
+
completedAt: Date.now()
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
computeBackoff(policy, attempt) {
|
|
2314
|
+
const base = policy.backoffMs || 100;
|
|
2315
|
+
let delay;
|
|
2316
|
+
if (policy.backoff === "exponential") {
|
|
2317
|
+
delay = base * Math.pow(2, attempt - 1);
|
|
2318
|
+
} else {
|
|
2319
|
+
delay = base * attempt;
|
|
2320
|
+
}
|
|
2321
|
+
if (policy.maxBackoffMs && delay > policy.maxBackoffMs) {
|
|
2322
|
+
delay = policy.maxBackoffMs;
|
|
2323
|
+
}
|
|
2324
|
+
if (policy.jitter) {
|
|
2325
|
+
delay += Math.floor(Math.random() * (base * 0.5));
|
|
2326
|
+
}
|
|
2327
|
+
return delay;
|
|
2328
|
+
}
|
|
2329
|
+
cancelExecution(executionId) {
|
|
2330
|
+
const active = this.activeOrchestrations.get(executionId);
|
|
2331
|
+
if (active) {
|
|
2332
|
+
active.context.isCancelled = true;
|
|
2333
|
+
active.abortController.abort();
|
|
2334
|
+
return true;
|
|
122
2335
|
}
|
|
2336
|
+
return false;
|
|
123
2337
|
}
|
|
124
2338
|
};
|
|
125
2339
|
|
|
126
|
-
// src/services/
|
|
127
|
-
var
|
|
128
|
-
constructor(definition) {
|
|
129
|
-
this.definition = definition;
|
|
130
|
-
this.id = definition.id;
|
|
131
|
-
this.name = definition.name;
|
|
132
|
-
this.version = definition.version;
|
|
133
|
-
this.lifecycle = new LifecycleManager();
|
|
134
|
-
this.eventBus = new ServiceEventBus();
|
|
135
|
-
if (definition.state?.initial) {
|
|
136
|
-
this.stateStore = new ServiceStateStore(definition.state.initial);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
definition;
|
|
2340
|
+
// src/services/ServiceFacade.ts
|
|
2341
|
+
var ServiceFacade = class {
|
|
140
2342
|
id;
|
|
141
2343
|
name;
|
|
142
2344
|
version;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
2345
|
+
description;
|
|
2346
|
+
functionalities;
|
|
2347
|
+
functionalityOrchestrations;
|
|
2348
|
+
libraries;
|
|
2349
|
+
dependencies;
|
|
2350
|
+
capabilities;
|
|
2351
|
+
defaultFrontend;
|
|
2352
|
+
instance;
|
|
2353
|
+
queue;
|
|
2354
|
+
constructor(config) {
|
|
2355
|
+
if (!config || !config.id || typeof config.id !== "string") {
|
|
2356
|
+
throw new Error("[ServiceFacade] ServiceFacade requires a valid id.");
|
|
153
2357
|
}
|
|
154
|
-
|
|
155
|
-
|
|
2358
|
+
this.id = config.id;
|
|
2359
|
+
this.name = config.name || config.id;
|
|
2360
|
+
this.version = config.version || "1.0.0";
|
|
2361
|
+
this.description = config.description || "";
|
|
2362
|
+
this.functionalities = config.functionalities ? { ...config.functionalities } : {};
|
|
2363
|
+
this.functionalityOrchestrations = config.functionalityOrchestrations ? { ...config.functionalityOrchestrations } : config.orchestrations ? { ...config.orchestrations } : {};
|
|
2364
|
+
this.libraries = config.libraries ? { ...config.libraries } : {};
|
|
2365
|
+
this.dependencies = Object.freeze(config.dependencies ? [...config.dependencies] : []);
|
|
2366
|
+
this.capabilities = Object.freeze(config.capabilities ? [...config.capabilities] : []);
|
|
2367
|
+
this.defaultFrontend = config.defaultFrontend ? Object.freeze({ ...config.defaultFrontend }) : void 0;
|
|
2368
|
+
if (config.instance) {
|
|
2369
|
+
this.instance = config.instance;
|
|
2370
|
+
} else if (config.definition) {
|
|
2371
|
+
this.instance = new ServiceInstance(config.definition);
|
|
2372
|
+
} else {
|
|
2373
|
+
this.instance = new ServiceInstance({
|
|
2374
|
+
id: this.id,
|
|
2375
|
+
name: this.name,
|
|
2376
|
+
version: this.version,
|
|
2377
|
+
operations: {}
|
|
2378
|
+
});
|
|
156
2379
|
}
|
|
157
|
-
this.
|
|
2380
|
+
this.queue = new KrnlQueue({
|
|
2381
|
+
queueName: `krnl_queue_${this.id}`,
|
|
2382
|
+
concurrency: config.concurrency ?? config.queueOptions?.concurrency ?? 10,
|
|
2383
|
+
autoStart: config.queueOptions?.autoStart ?? true
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
get orchestrations() {
|
|
2387
|
+
return this.functionalityOrchestrations;
|
|
2388
|
+
}
|
|
2389
|
+
getInstance() {
|
|
2390
|
+
return this.instance;
|
|
2391
|
+
}
|
|
2392
|
+
getQueue() {
|
|
2393
|
+
return this.queue;
|
|
2394
|
+
}
|
|
2395
|
+
getExecutionEngine() {
|
|
2396
|
+
return this.queue;
|
|
2397
|
+
}
|
|
2398
|
+
getWorkerBoundary() {
|
|
2399
|
+
return this.queue.getWorkerBoundary();
|
|
2400
|
+
}
|
|
2401
|
+
getStatus() {
|
|
2402
|
+
return this.instance.getStatus();
|
|
2403
|
+
}
|
|
2404
|
+
async start() {
|
|
2405
|
+
await this.instance.start();
|
|
2406
|
+
await this.queue.start();
|
|
158
2407
|
}
|
|
159
2408
|
async stop() {
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
|
|
2409
|
+
await this.queue.stop();
|
|
2410
|
+
await this.instance.stop();
|
|
2411
|
+
}
|
|
2412
|
+
async dispose() {
|
|
2413
|
+
await this.queue.dispose();
|
|
2414
|
+
await this.instance.dispose();
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* External caller callable boundary for single functionality / operation execution.
|
|
2418
|
+
* Path: External Caller -> Service Facade -> KrnlQ Job -> Worker -> Functionality -> Result.
|
|
2419
|
+
* Strictly avoids synchronous unmediated execution outside the queue.
|
|
2420
|
+
*/
|
|
2421
|
+
async call(op, input, ctx) {
|
|
2422
|
+
const correlationId = ctx?.correlationId || `corr_${Math.random().toString(36).substring(2, 11)}`;
|
|
2423
|
+
const commandId = `cmd_${this.id}_${op}_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
2424
|
+
const command = {
|
|
2425
|
+
id: commandId,
|
|
2426
|
+
type: `${this.id}.${op}`,
|
|
2427
|
+
payload: input ?? {},
|
|
2428
|
+
correlationId,
|
|
2429
|
+
idempotencyKey: ctx?.idempotencyKey,
|
|
2430
|
+
createdAt: Date.now()
|
|
2431
|
+
};
|
|
2432
|
+
const result = await this.queue.executeCommand(command, async (payload, workerCtx) => {
|
|
2433
|
+
WorkerExecutionBoundary.assertWorkerContext(workerCtx);
|
|
2434
|
+
if (this.functionalities[op] && typeof this.functionalities[op].execute === "function") {
|
|
2435
|
+
return await this.functionalities[op].execute(payload, {
|
|
2436
|
+
correlationId: workerCtx.correlationId,
|
|
2437
|
+
state: this.instance.getState()
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
return await this.instance.call(op, payload, {
|
|
2441
|
+
correlationId: workerCtx.correlationId,
|
|
2442
|
+
idempotencyKey: command.idempotencyKey
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
if (result.status === "FAILED") {
|
|
2446
|
+
throw new Error(result.error || `[ServiceFacade:${this.id}] Operation '${op}' execution failed.`);
|
|
2447
|
+
}
|
|
2448
|
+
return result.data;
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* External caller callable boundary for functionality orchestration execution.
|
|
2452
|
+
* Path: External Caller -> Service Facade -> KrnlQ Command -> Worker -> FunctionalityOrchestration -> Result.
|
|
2453
|
+
*/
|
|
2454
|
+
async orchestrate(orchestrationName, input, options = {}) {
|
|
2455
|
+
const orch = this.functionalityOrchestrations[orchestrationName];
|
|
2456
|
+
if (!orch) {
|
|
2457
|
+
throw new Error(
|
|
2458
|
+
`[ServiceFacade:${this.id}] FunctionalityOrchestration "${orchestrationName}" not found.`
|
|
2459
|
+
);
|
|
163
2460
|
}
|
|
164
|
-
|
|
165
|
-
|
|
2461
|
+
const correlationId = options.correlationId || `corr_${Math.random().toString(36).substring(2, 11)}`;
|
|
2462
|
+
const commandId = `cmd_orch_${this.id}_${orchestrationName}_${Date.now()}`;
|
|
2463
|
+
const command = {
|
|
2464
|
+
id: commandId,
|
|
2465
|
+
type: `${this.id}.orchestrations.${orchestrationName}`,
|
|
2466
|
+
payload: input ?? {},
|
|
2467
|
+
correlationId,
|
|
2468
|
+
idempotencyKey: options.idempotencyKey,
|
|
2469
|
+
createdAt: Date.now()
|
|
2470
|
+
};
|
|
2471
|
+
const cmdResult = await this.queue.executeCommand(
|
|
2472
|
+
command,
|
|
2473
|
+
async (payload, workerCtx) => {
|
|
2474
|
+
WorkerExecutionBoundary.assertWorkerContext(workerCtx);
|
|
2475
|
+
const resolveStepInput = (step, results) => {
|
|
2476
|
+
const deps = step.dependsOn;
|
|
2477
|
+
if (!deps || deps.length === 0) {
|
|
2478
|
+
return results["input"];
|
|
2479
|
+
}
|
|
2480
|
+
if (deps.length === 1) {
|
|
2481
|
+
const depRes = results[deps[0]];
|
|
2482
|
+
const initialInput = results["input"];
|
|
2483
|
+
if (typeof depRes === "object" && depRes !== null && typeof initialInput === "object" && initialInput !== null && !Array.isArray(depRes) && !Array.isArray(initialInput)) {
|
|
2484
|
+
return { ...initialInput, ...depRes };
|
|
2485
|
+
}
|
|
2486
|
+
return depRes !== void 0 ? depRes : results["input"];
|
|
2487
|
+
}
|
|
2488
|
+
const joined = {};
|
|
2489
|
+
for (const dep of deps) {
|
|
2490
|
+
joined[dep] = results[dep];
|
|
2491
|
+
}
|
|
2492
|
+
return joined;
|
|
2493
|
+
};
|
|
2494
|
+
const rawSteps = orch.getSteps();
|
|
2495
|
+
const boundSteps = rawSteps.map((s) => {
|
|
2496
|
+
let compensation = s.compensation;
|
|
2497
|
+
if (s.compensation?.target && !s.compensation.handler) {
|
|
2498
|
+
compensation = {
|
|
2499
|
+
...s.compensation,
|
|
2500
|
+
handler: async (compCtx) => {
|
|
2501
|
+
await this.call(s.compensation.target, compCtx.results[s.id], {
|
|
2502
|
+
correlationId: compCtx.correlationId
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
if (s.type === "task" && s.target) {
|
|
2508
|
+
return {
|
|
2509
|
+
...s,
|
|
2510
|
+
compensation,
|
|
2511
|
+
handler: async (_stepInput, stepCtx) => {
|
|
2512
|
+
const resolvedInput = resolveStepInput(s, stepCtx.results);
|
|
2513
|
+
const stepPayload = typeof resolvedInput === "object" && resolvedInput !== null && !Array.isArray(resolvedInput) ? { ...stepCtx.results, ...resolvedInput } : { ...stepCtx.results };
|
|
2514
|
+
if (stepCtx.results.input) {
|
|
2515
|
+
stepPayload.input = stepCtx.results.input;
|
|
2516
|
+
}
|
|
2517
|
+
return await this.call(s.target, stepPayload, {
|
|
2518
|
+
correlationId: stepCtx.correlationId
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
...s,
|
|
2525
|
+
compensation
|
|
2526
|
+
};
|
|
2527
|
+
});
|
|
2528
|
+
const nestedRunner = async (childOrchId, childInput, childCtx) => {
|
|
2529
|
+
const childOrch = this.functionalityOrchestrations[childOrchId];
|
|
2530
|
+
if (!childOrch) {
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
`[ServiceFacade:${this.id}] Nested FunctionalityOrchestration "${childOrchId}" not found in service.`
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const nestedResult = await this.orchestrate(childOrchId, childInput, {
|
|
2536
|
+
correlationId: childCtx.correlationId,
|
|
2537
|
+
timeoutMs: childOrch.timeoutMs,
|
|
2538
|
+
abortSignal: options.abortSignal
|
|
2539
|
+
});
|
|
2540
|
+
if (nestedResult.status !== "COMPLETED") {
|
|
2541
|
+
throw new Error(nestedResult.error || `Nested orchestration "${childOrchId}" failed.`);
|
|
2542
|
+
}
|
|
2543
|
+
return nestedResult.output;
|
|
2544
|
+
};
|
|
2545
|
+
return await this.queue.executeOrchestration(
|
|
2546
|
+
{ id: orch.id, name: orch.name, version: orch.version, timeoutMs: orch.timeoutMs, scope: "functionality" },
|
|
2547
|
+
boundSteps,
|
|
2548
|
+
payload,
|
|
2549
|
+
{
|
|
2550
|
+
correlationId: workerCtx.correlationId,
|
|
2551
|
+
idempotencyKey: command.idempotencyKey,
|
|
2552
|
+
timeoutMs: options.timeoutMs || orch.timeoutMs,
|
|
2553
|
+
metadata: options.metadata,
|
|
2554
|
+
nestedRunner,
|
|
2555
|
+
abortSignal: options.abortSignal
|
|
2556
|
+
}
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
);
|
|
2560
|
+
if (cmdResult.status === "FAILED") {
|
|
2561
|
+
throw new Error(cmdResult.error || `[ServiceFacade:${this.id}] Orchestration failed.`);
|
|
166
2562
|
}
|
|
167
|
-
|
|
2563
|
+
return cmdResult.data;
|
|
168
2564
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
2565
|
+
/**
|
|
2566
|
+
* Executes a callable operation or orchestration through KrnlQ and worker boundary.
|
|
2567
|
+
* Path: External Caller -> Service Facade -> KrnlQ Command -> Worker -> Functionality / Orchestration -> Result.
|
|
2568
|
+
* Strictly queue-mediated; zero unmediated bypasses.
|
|
2569
|
+
*/
|
|
2570
|
+
async execute(target, input, options) {
|
|
2571
|
+
if (this.functionalityOrchestrations[target]) {
|
|
2572
|
+
const res = await this.orchestrate(target, input, options);
|
|
2573
|
+
if (res.status === "FAILED") {
|
|
2574
|
+
throw new Error(res.error || `[ServiceFacade:${this.id}] Orchestration "${target}" failed.`);
|
|
2575
|
+
}
|
|
2576
|
+
return res;
|
|
174
2577
|
}
|
|
175
|
-
return
|
|
2578
|
+
return this.call(target, input, {
|
|
2579
|
+
correlationId: options?.correlationId,
|
|
2580
|
+
idempotencyKey: options?.idempotencyKey
|
|
2581
|
+
});
|
|
176
2582
|
}
|
|
177
2583
|
getState() {
|
|
178
|
-
|
|
179
|
-
return Object.freeze({});
|
|
180
|
-
}
|
|
181
|
-
return this.stateStore.getState();
|
|
2584
|
+
return this.instance.getState();
|
|
182
2585
|
}
|
|
183
2586
|
setState(mutation) {
|
|
184
|
-
|
|
185
|
-
this.stateStore = new ServiceStateStore(mutation);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
this.stateStore.setState(mutation);
|
|
2587
|
+
this.instance.setState(mutation);
|
|
189
2588
|
}
|
|
190
2589
|
on(event, handler) {
|
|
191
|
-
return this.
|
|
2590
|
+
return this.instance.on(event, handler);
|
|
192
2591
|
}
|
|
193
2592
|
emit(event, payload) {
|
|
194
|
-
this.
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// src/services/ServiceProxy.ts
|
|
199
|
-
var ServiceProxy = class {
|
|
200
|
-
constructor(definition, adapter, endpoint) {
|
|
201
|
-
this.definition = definition;
|
|
202
|
-
this.adapter = adapter;
|
|
203
|
-
this.endpoint = endpoint;
|
|
204
|
-
this.id = definition.id;
|
|
205
|
-
this.name = definition.name;
|
|
206
|
-
this.version = definition.version;
|
|
207
|
-
this.stateCache = definition.state?.initial || {};
|
|
2593
|
+
this.instance.emit(event, payload);
|
|
208
2594
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
stateCache;
|
|
216
|
-
eventHandlers = /* @__PURE__ */ new Map();
|
|
217
|
-
getDefinition() {
|
|
218
|
-
return this.definition;
|
|
2595
|
+
registerFunctionality(name, functionality) {
|
|
2596
|
+
if (!name || typeof name !== "string") {
|
|
2597
|
+
throw new Error("[ServiceFacade] Functionality name must be a non-empty string.");
|
|
2598
|
+
}
|
|
2599
|
+
this.functionalities[name] = functionality;
|
|
2600
|
+
return this;
|
|
219
2601
|
}
|
|
220
|
-
|
|
2602
|
+
registerFunctionalityOrchestration(name, orchestration) {
|
|
2603
|
+
if (!name || typeof name !== "string") {
|
|
2604
|
+
throw new Error("[ServiceFacade] FunctionalityOrchestration name must be a non-empty string.");
|
|
2605
|
+
}
|
|
2606
|
+
if (!orchestration) {
|
|
2607
|
+
throw new Error("[ServiceFacade] FunctionalityOrchestration instance is required.");
|
|
2608
|
+
}
|
|
2609
|
+
this.functionalityOrchestrations[name] = orchestration;
|
|
2610
|
+
return this;
|
|
221
2611
|
}
|
|
222
|
-
|
|
2612
|
+
/** @deprecated Use registerFunctionalityOrchestration */
|
|
2613
|
+
registerOrchestration(name, orchestration) {
|
|
2614
|
+
return this.registerFunctionalityOrchestration(name, orchestration);
|
|
223
2615
|
}
|
|
224
|
-
|
|
225
|
-
if (!
|
|
226
|
-
throw new Error(
|
|
2616
|
+
registerLibrary(name, library) {
|
|
2617
|
+
if (!name || typeof name !== "string") {
|
|
2618
|
+
throw new Error("[ServiceFacade] Library name must be a non-empty string.");
|
|
227
2619
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
type: "request",
|
|
231
|
-
operation: op,
|
|
232
|
-
payload: input,
|
|
233
|
-
timestamp: Date.now()
|
|
234
|
-
};
|
|
235
|
-
const response = await this.adapter.send(this.endpoint, envelope);
|
|
236
|
-
if (response.error) {
|
|
237
|
-
throw new Error(`[ServiceProxy:${this.name}] Remote error: ${response.error}`);
|
|
2620
|
+
if (library === void 0 || library === null) {
|
|
2621
|
+
throw new Error(`[ServiceFacade] Library "${name}" must not be null or undefined.`);
|
|
238
2622
|
}
|
|
239
|
-
|
|
2623
|
+
this.libraries[name] = library;
|
|
2624
|
+
return this;
|
|
240
2625
|
}
|
|
241
|
-
|
|
242
|
-
|
|
2626
|
+
getLibrary(name) {
|
|
2627
|
+
const lib = this.libraries[name];
|
|
2628
|
+
if (lib === void 0 || lib === null) {
|
|
2629
|
+
throw new Error(`[ServiceFacade:${this.id}] Library "${name}" not found on service facade.`);
|
|
2630
|
+
}
|
|
2631
|
+
return lib;
|
|
243
2632
|
}
|
|
244
|
-
|
|
245
|
-
|
|
2633
|
+
get namespace() {
|
|
2634
|
+
return this.getNamespace();
|
|
246
2635
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
2636
|
+
getNamespace() {
|
|
2637
|
+
return {
|
|
2638
|
+
serviceId: this.id,
|
|
2639
|
+
serviceName: this.name,
|
|
2640
|
+
version: this.version,
|
|
2641
|
+
description: this.description,
|
|
2642
|
+
libraries: { ...this.libraries },
|
|
2643
|
+
functionalities: { ...this.functionalities },
|
|
2644
|
+
functionalityOrchestrations: { ...this.functionalityOrchestrations },
|
|
2645
|
+
capabilities: this.capabilities,
|
|
2646
|
+
dependencies: this.dependencies
|
|
255
2647
|
};
|
|
256
2648
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (handlers) {
|
|
260
|
-
for (const handler of Array.from(handlers)) {
|
|
261
|
-
handler(payload);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
2649
|
+
getServiceFacadeMetadata() {
|
|
2650
|
+
return this.getNamespace();
|
|
264
2651
|
}
|
|
265
2652
|
};
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
function createServiceDefinition(definition) {
|
|
269
|
-
return definition;
|
|
270
|
-
}
|
|
271
|
-
function createService(definition) {
|
|
272
|
-
return new ServiceInstance(definition);
|
|
273
|
-
}
|
|
274
|
-
function createServiceProxy(definition, adapter, endpoint) {
|
|
275
|
-
return new ServiceProxy(definition, adapter, endpoint);
|
|
2653
|
+
function createServiceFacade(config) {
|
|
2654
|
+
return new ServiceFacade(config);
|
|
276
2655
|
}
|
|
277
2656
|
|
|
278
2657
|
// src/protocols/HttpProtocolAdapter.ts
|
|
@@ -382,7 +2761,6 @@ var HttpProtocolAdapter = class {
|
|
|
382
2761
|
};
|
|
383
2762
|
|
|
384
2763
|
// src/protocols/WebSocketProtocolAdapter.ts
|
|
385
|
-
var import_http2 = __toESM(require("http"), 1);
|
|
386
2764
|
var WebSocketProtocolAdapter = class {
|
|
387
2765
|
protocol = "ws";
|
|
388
2766
|
capabilities = {
|
|
@@ -398,9 +2776,14 @@ var WebSocketProtocolAdapter = class {
|
|
|
398
2776
|
wss;
|
|
399
2777
|
port;
|
|
400
2778
|
async expose(service, port = 0) {
|
|
401
|
-
const
|
|
2779
|
+
const httpMod = await import("http");
|
|
2780
|
+
const http2 = httpMod.default || httpMod;
|
|
2781
|
+
const wsMod = await import("ws").catch(() => null);
|
|
2782
|
+
if (!wsMod) {
|
|
2783
|
+
throw new Error('WebSocketProtocolAdapter.expose requires "ws" package in Node.js environment.');
|
|
2784
|
+
}
|
|
402
2785
|
const WebSocketServer = wsMod.WebSocketServer || wsMod.default?.WebSocketServer || wsMod.Server;
|
|
403
|
-
this.server =
|
|
2786
|
+
this.server = http2.createServer();
|
|
404
2787
|
this.wss = new WebSocketServer({ server: this.server });
|
|
405
2788
|
this.wss.on("connection", (ws) => {
|
|
406
2789
|
ws.on("message", async (raw) => {
|
|
@@ -446,8 +2829,18 @@ var WebSocketProtocolAdapter = class {
|
|
|
446
2829
|
};
|
|
447
2830
|
}
|
|
448
2831
|
async send(endpoint, envelope) {
|
|
449
|
-
|
|
450
|
-
const
|
|
2832
|
+
let WS;
|
|
2833
|
+
const isNode = Boolean(globalThis.process?.versions?.node);
|
|
2834
|
+
if (isNode) {
|
|
2835
|
+
const wsMod = await import("ws").catch(() => null);
|
|
2836
|
+
WS = wsMod?.WebSocket || wsMod?.default?.WebSocket || wsMod?.default || wsMod;
|
|
2837
|
+
}
|
|
2838
|
+
if (!WS) {
|
|
2839
|
+
WS = globalThis.WebSocket;
|
|
2840
|
+
}
|
|
2841
|
+
if (!WS) {
|
|
2842
|
+
throw new Error("No WebSocket client implementation available in this environment.");
|
|
2843
|
+
}
|
|
451
2844
|
return new Promise((resolve, reject) => {
|
|
452
2845
|
const ws = new WS(`ws://${endpoint.address}:${endpoint.port}`);
|
|
453
2846
|
ws.on("open", () => {
|
|
@@ -649,23 +3042,74 @@ var MemoryTransport = class {
|
|
|
649
3042
|
};
|
|
650
3043
|
|
|
651
3044
|
// src/registry/ServiceRegistry.ts
|
|
3045
|
+
var PROTOCOLS = ["http", "https", "ws", "wss", "udp", "p2p", "signaling", "memory"];
|
|
3046
|
+
function validateEndpoint(endpoint) {
|
|
3047
|
+
if (!endpoint || typeof endpoint !== "object") throw new Error("[ServiceRegistry] Invalid endpoint.");
|
|
3048
|
+
if (!PROTOCOLS.includes(endpoint.protocol)) throw new Error("[ServiceRegistry] Unsupported endpoint protocol.");
|
|
3049
|
+
if (typeof endpoint.address !== "string" || endpoint.address.trim() === "") throw new Error("[ServiceRegistry] Endpoint address must be non-empty.");
|
|
3050
|
+
if (endpoint.port !== void 0 && (!Number.isInteger(endpoint.port) || endpoint.port < 0 || endpoint.port > 65535)) throw new Error("[ServiceRegistry] Endpoint port is invalid.");
|
|
3051
|
+
if (!Array.isArray(endpoint.capabilities)) throw new Error("[ServiceRegistry] Endpoint capabilities must be an array.");
|
|
3052
|
+
}
|
|
652
3053
|
var ServiceRegistry = class {
|
|
653
3054
|
services = /* @__PURE__ */ new Map();
|
|
3055
|
+
/**
|
|
3056
|
+
* Registers a service definition and its endpoints into the registry.
|
|
3057
|
+
*
|
|
3058
|
+
* @param definition Validated service definition.
|
|
3059
|
+
* @param endpoints Optional list of active endpoint descriptors.
|
|
3060
|
+
* @throws Error if definition or endpoints fail validation.
|
|
3061
|
+
* @throws Error if service is already registered under the same ID.
|
|
3062
|
+
*/
|
|
654
3063
|
register(definition, endpoints = []) {
|
|
3064
|
+
validateServiceDefinition(definition);
|
|
3065
|
+
for (const endpoint of endpoints) validateEndpoint(endpoint);
|
|
3066
|
+
const existing = this.services.get(definition.id);
|
|
3067
|
+
if (existing) {
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
"[ServiceRegistry] Service '" + definition.id + "' is already registered at version '" + existing.definition.version + "'. Use replace() to explicitly replace an existing service."
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
655
3072
|
this.services.set(definition.id, {
|
|
656
3073
|
definition,
|
|
657
|
-
endpoints,
|
|
3074
|
+
endpoints: endpoints.map((endpoint) => ({ ...endpoint, capabilities: [...endpoint.capabilities] })),
|
|
658
3075
|
registeredAt: Date.now()
|
|
659
3076
|
});
|
|
660
3077
|
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Explicitly replaces or updates a service definition and its endpoints in the registry.
|
|
3080
|
+
* Preserves original registeredAt timestamp if the service was previously registered.
|
|
3081
|
+
*
|
|
3082
|
+
* @param definition Validated service definition.
|
|
3083
|
+
* @param endpoints Optional list of active endpoint descriptors.
|
|
3084
|
+
*/
|
|
3085
|
+
replace(definition, endpoints = []) {
|
|
3086
|
+
validateServiceDefinition(definition);
|
|
3087
|
+
for (const endpoint of endpoints) validateEndpoint(endpoint);
|
|
3088
|
+
const existing = this.services.get(definition.id);
|
|
3089
|
+
this.services.set(definition.id, {
|
|
3090
|
+
definition,
|
|
3091
|
+
endpoints: endpoints.map((endpoint) => ({ ...endpoint, capabilities: [...endpoint.capabilities] })),
|
|
3092
|
+
registeredAt: existing?.registeredAt ?? Date.now()
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
661
3095
|
unregister(serviceId) {
|
|
662
|
-
this.services.delete(serviceId);
|
|
3096
|
+
return this.services.delete(serviceId);
|
|
663
3097
|
}
|
|
664
3098
|
get(serviceId) {
|
|
665
|
-
|
|
3099
|
+
const record = this.services.get(serviceId);
|
|
3100
|
+
if (!record) return void 0;
|
|
3101
|
+
return {
|
|
3102
|
+
definition: record.definition,
|
|
3103
|
+
endpoints: record.endpoints.map((endpoint) => ({ ...endpoint, capabilities: [...endpoint.capabilities] })),
|
|
3104
|
+
registeredAt: record.registeredAt
|
|
3105
|
+
};
|
|
666
3106
|
}
|
|
667
3107
|
list() {
|
|
668
|
-
return Array.from(this.services.values())
|
|
3108
|
+
return Array.from(this.services.values()).map((record) => ({
|
|
3109
|
+
definition: record.definition,
|
|
3110
|
+
endpoints: record.endpoints.map((endpoint) => ({ ...endpoint, capabilities: [...endpoint.capabilities] })),
|
|
3111
|
+
registeredAt: record.registeredAt
|
|
3112
|
+
}));
|
|
669
3113
|
}
|
|
670
3114
|
findByCapability(capabilityName) {
|
|
671
3115
|
return this.list().filter(
|
|
@@ -679,32 +3123,75 @@ var ServiceRuntime = class {
|
|
|
679
3123
|
instances = /* @__PURE__ */ new Map();
|
|
680
3124
|
registry;
|
|
681
3125
|
constructor(registry) {
|
|
682
|
-
this.registry = registry
|
|
3126
|
+
this.registry = registry ?? new ServiceRegistry();
|
|
683
3127
|
}
|
|
684
3128
|
register(service) {
|
|
685
|
-
this.instances.
|
|
3129
|
+
const existing = this.instances.get(service.id);
|
|
3130
|
+
if (existing && existing !== service) {
|
|
3131
|
+
throw new Error("[ServiceRuntime] Service '" + service.id + "' is already registered.");
|
|
3132
|
+
}
|
|
686
3133
|
this.registry.register(service.getDefinition());
|
|
3134
|
+
this.instances.set(service.id, service);
|
|
687
3135
|
return this;
|
|
688
3136
|
}
|
|
3137
|
+
unregister(serviceId) {
|
|
3138
|
+
const removed = this.instances.delete(serviceId);
|
|
3139
|
+
this.registry.unregister(serviceId);
|
|
3140
|
+
return removed;
|
|
3141
|
+
}
|
|
689
3142
|
get(serviceId) {
|
|
690
3143
|
return this.instances.get(serviceId);
|
|
691
3144
|
}
|
|
3145
|
+
list() {
|
|
3146
|
+
return Array.from(this.instances.values());
|
|
3147
|
+
}
|
|
692
3148
|
async execute(target, operation, input, ctx) {
|
|
693
3149
|
const service = this.instances.get(target);
|
|
694
3150
|
if (!service) {
|
|
695
|
-
throw new Error(
|
|
3151
|
+
throw new Error("[ServiceRuntime] Target service '" + target + "' not found.");
|
|
696
3152
|
}
|
|
697
|
-
return
|
|
3153
|
+
return service.call(operation, input, ctx);
|
|
698
3154
|
}
|
|
699
3155
|
async startAll() {
|
|
700
|
-
|
|
701
|
-
|
|
3156
|
+
const started = [];
|
|
3157
|
+
try {
|
|
3158
|
+
for (const service of this.instances.values()) {
|
|
3159
|
+
await service.start();
|
|
3160
|
+
started.push(service);
|
|
3161
|
+
}
|
|
3162
|
+
} catch (error) {
|
|
3163
|
+
for (const service of started.reverse()) {
|
|
3164
|
+
try {
|
|
3165
|
+
await service.stop();
|
|
3166
|
+
} catch {
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
throw error;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
async disposeAll() {
|
|
3173
|
+
let firstError;
|
|
3174
|
+
const services = Array.from(this.instances.values()).reverse();
|
|
3175
|
+
for (const service of services) {
|
|
3176
|
+
try {
|
|
3177
|
+
await service.dispose();
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
firstError ??= error;
|
|
3180
|
+
}
|
|
702
3181
|
}
|
|
3182
|
+
if (firstError) throw firstError;
|
|
703
3183
|
}
|
|
704
3184
|
async stopAll() {
|
|
705
|
-
|
|
706
|
-
|
|
3185
|
+
let firstError;
|
|
3186
|
+
const services = Array.from(this.instances.values()).reverse();
|
|
3187
|
+
for (const service of services) {
|
|
3188
|
+
try {
|
|
3189
|
+
await service.stop();
|
|
3190
|
+
} catch (error) {
|
|
3191
|
+
firstError ??= error;
|
|
3192
|
+
}
|
|
707
3193
|
}
|
|
3194
|
+
if (firstError) throw firstError;
|
|
708
3195
|
}
|
|
709
3196
|
};
|
|
710
3197
|
|
|
@@ -727,6 +3214,129 @@ var ServiceDiscovery = class {
|
|
|
727
3214
|
}
|
|
728
3215
|
};
|
|
729
3216
|
|
|
3217
|
+
// src/discovery/KrnlPackageReconciler.ts
|
|
3218
|
+
var fs = __toESM(require("fs"), 1);
|
|
3219
|
+
var path = __toESM(require("path"), 1);
|
|
3220
|
+
var KrnlPackageReconciler = class {
|
|
3221
|
+
/**
|
|
3222
|
+
* Scans all manifest.json files across backend and frontend and reconciles root package.json.
|
|
3223
|
+
*/
|
|
3224
|
+
static reconcile(projectRoot = process.cwd(), writeChanges = true) {
|
|
3225
|
+
const manifests = this.findManifests(projectRoot);
|
|
3226
|
+
const aggregatedDeps = {};
|
|
3227
|
+
const aggregatedDevDeps = {};
|
|
3228
|
+
for (const mPath of manifests) {
|
|
3229
|
+
try {
|
|
3230
|
+
const raw = fs.readFileSync(mPath, "utf8");
|
|
3231
|
+
const json = JSON.parse(raw);
|
|
3232
|
+
if (json.dependencies && typeof json.dependencies === "object") {
|
|
3233
|
+
for (const [pkg, ver] of Object.entries(json.dependencies)) {
|
|
3234
|
+
if (!aggregatedDeps[pkg]) aggregatedDeps[pkg] = [];
|
|
3235
|
+
if (!aggregatedDeps[pkg].includes(String(ver))) {
|
|
3236
|
+
aggregatedDeps[pkg].push(String(ver));
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
if (json.devDependencies && typeof json.devDependencies === "object") {
|
|
3241
|
+
for (const [pkg, ver] of Object.entries(json.devDependencies)) {
|
|
3242
|
+
if (!aggregatedDevDeps[pkg]) aggregatedDevDeps[pkg] = [];
|
|
3243
|
+
if (!aggregatedDevDeps[pkg].includes(String(ver))) {
|
|
3244
|
+
aggregatedDevDeps[pkg].push(String(ver));
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
} catch (err) {
|
|
3249
|
+
console.warn(`[KrnlPackageReconciler] Warning: Could not parse ${mPath}:`, err);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
const conflicts = [];
|
|
3253
|
+
const resolvedDeps = {};
|
|
3254
|
+
const resolvedDevDeps = {};
|
|
3255
|
+
for (const [pkg, versions] of Object.entries(aggregatedDeps)) {
|
|
3256
|
+
if (versions.length > 1) {
|
|
3257
|
+
conflicts.push({ package: pkg, versions });
|
|
3258
|
+
}
|
|
3259
|
+
resolvedDeps[pkg] = versions[versions.length - 1];
|
|
3260
|
+
}
|
|
3261
|
+
for (const [pkg, versions] of Object.entries(aggregatedDevDeps)) {
|
|
3262
|
+
if (versions.length > 1 && !conflicts.some((c) => c.package === pkg)) {
|
|
3263
|
+
conflicts.push({ package: pkg, versions });
|
|
3264
|
+
}
|
|
3265
|
+
resolvedDevDeps[pkg] = versions[versions.length - 1];
|
|
3266
|
+
}
|
|
3267
|
+
const pkgJsonPath = path.join(projectRoot, "package.json");
|
|
3268
|
+
let updated = false;
|
|
3269
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
3270
|
+
const pkgContent = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
3271
|
+
pkgContent.dependencies = pkgContent.dependencies || {};
|
|
3272
|
+
pkgContent.devDependencies = pkgContent.devDependencies || {};
|
|
3273
|
+
let hasChanges = false;
|
|
3274
|
+
for (const [pkg, ver] of Object.entries(resolvedDeps)) {
|
|
3275
|
+
if (pkgContent.dependencies[pkg] !== ver) {
|
|
3276
|
+
pkgContent.dependencies[pkg] = ver;
|
|
3277
|
+
hasChanges = true;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
for (const [pkg, ver] of Object.entries(resolvedDevDeps)) {
|
|
3281
|
+
if (!pkgContent.dependencies[pkg] && pkgContent.devDependencies[pkg] !== ver) {
|
|
3282
|
+
pkgContent.devDependencies[pkg] = ver;
|
|
3283
|
+
hasChanges = true;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
if (hasChanges && writeChanges) {
|
|
3287
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgContent, null, 2) + "\n");
|
|
3288
|
+
updated = true;
|
|
3289
|
+
console.log(`[KrnlPackageReconciler] Reconciled package.json from ${manifests.length} manifest(s).`);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
return {
|
|
3293
|
+
updated,
|
|
3294
|
+
totalManifests: manifests.length,
|
|
3295
|
+
addedDependencies: resolvedDeps,
|
|
3296
|
+
addedDevDependencies: resolvedDevDeps,
|
|
3297
|
+
conflicts
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Discovers all manifest.json files in krnl/ (or src/)
|
|
3302
|
+
*/
|
|
3303
|
+
static findManifests(projectRoot) {
|
|
3304
|
+
const searchDirs = [
|
|
3305
|
+
path.join(projectRoot, "krnl", "backend", "services"),
|
|
3306
|
+
path.join(projectRoot, "krnl", "backend", "libraries"),
|
|
3307
|
+
path.join(projectRoot, "krnl", "backend", "functionalities"),
|
|
3308
|
+
path.join(projectRoot, "krnl", "frontend", "libraries"),
|
|
3309
|
+
path.join(projectRoot, "krnl", "frontend", "functionalities"),
|
|
3310
|
+
path.join(projectRoot, "src", "backend", "services"),
|
|
3311
|
+
path.join(projectRoot, "src", "services"),
|
|
3312
|
+
path.join(projectRoot, "src", "functionalities"),
|
|
3313
|
+
path.join(projectRoot, "src", "libraries")
|
|
3314
|
+
];
|
|
3315
|
+
const found = [];
|
|
3316
|
+
for (const dir of searchDirs) {
|
|
3317
|
+
if (fs.existsSync(dir)) {
|
|
3318
|
+
this.scanRecursive(dir, found);
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
return found;
|
|
3322
|
+
}
|
|
3323
|
+
static scanRecursive(dir, found) {
|
|
3324
|
+
try {
|
|
3325
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3326
|
+
for (const entry of entries) {
|
|
3327
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") continue;
|
|
3328
|
+
const full = path.join(dir, entry.name);
|
|
3329
|
+
if (entry.isDirectory()) {
|
|
3330
|
+
this.scanRecursive(full, found);
|
|
3331
|
+
} else if (entry.isFile() && entry.name === "manifest.json") {
|
|
3332
|
+
found.push(full);
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
} catch {
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
};
|
|
3339
|
+
|
|
730
3340
|
// src/connectivity/ProtocolResolver.ts
|
|
731
3341
|
var ProtocolResolver = class {
|
|
732
3342
|
static selectProtocol(requiredCapabilities, availableEndpoints) {
|
|
@@ -738,15 +3348,299 @@ var ProtocolResolver = class {
|
|
|
738
3348
|
return definition.protocols || ["http"];
|
|
739
3349
|
}
|
|
740
3350
|
};
|
|
3351
|
+
|
|
3352
|
+
// src/orchestration/FunctionalityOrchestration.ts
|
|
3353
|
+
var FunctionalityOrchestration = class {
|
|
3354
|
+
id;
|
|
3355
|
+
name;
|
|
3356
|
+
version;
|
|
3357
|
+
description;
|
|
3358
|
+
timeoutMs;
|
|
3359
|
+
scope = "functionality";
|
|
3360
|
+
steps = [];
|
|
3361
|
+
flows = /* @__PURE__ */ new Map();
|
|
3362
|
+
constructor(descriptor) {
|
|
3363
|
+
if (!descriptor || typeof descriptor !== "object" || !descriptor.id) {
|
|
3364
|
+
throw new Error("[FunctionalityOrchestration] Valid descriptor with non-empty id is required.");
|
|
3365
|
+
}
|
|
3366
|
+
this.id = descriptor.id;
|
|
3367
|
+
this.name = descriptor.name || descriptor.id;
|
|
3368
|
+
this.version = descriptor.version || "1.0.0";
|
|
3369
|
+
this.description = descriptor.description || "";
|
|
3370
|
+
this.timeoutMs = descriptor.timeoutMs;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Adds a sequential functionality step to the DAG.
|
|
3374
|
+
*/
|
|
3375
|
+
thenFunctionality(funcName, options = {}) {
|
|
3376
|
+
if (!funcName) {
|
|
3377
|
+
throw new Error("[FunctionalityOrchestration] Functionality name must be a non-empty string.");
|
|
3378
|
+
}
|
|
3379
|
+
if (typeof funcName !== "string") {
|
|
3380
|
+
if (typeof funcName === "object" && ("call" in funcName || "getStatus" in funcName || "definition" in funcName || "instance" in funcName || "id" in funcName)) {
|
|
3381
|
+
throw new Error(
|
|
3382
|
+
"[FunctionalityOrchestration] Invariant Violation: FunctionalityOrchestration only composes functionalities within the service, not independent services."
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
throw new Error("[FunctionalityOrchestration] Functionality name must be a non-empty string.");
|
|
3386
|
+
}
|
|
3387
|
+
this.assertServiceBoundary(funcName);
|
|
3388
|
+
const stepId = options.id || `step_${funcName}_${this.steps.length + 1}`;
|
|
3389
|
+
const previousStep = this.steps.length > 0 ? this.steps[this.steps.length - 1].id : void 0;
|
|
3390
|
+
const dependsOn = options.dependsOn || (previousStep ? [previousStep] : []);
|
|
3391
|
+
const step = {
|
|
3392
|
+
id: stepId,
|
|
3393
|
+
name: funcName,
|
|
3394
|
+
type: "task",
|
|
3395
|
+
target: funcName,
|
|
3396
|
+
dependsOn,
|
|
3397
|
+
retryPolicy: options.retry,
|
|
3398
|
+
timeoutPolicy: options.timeoutMs ? { timeoutMs: options.timeoutMs } : void 0,
|
|
3399
|
+
compensation: options.compensation ? { name: options.compensation, target: options.compensation } : void 0,
|
|
3400
|
+
catchHandler: options.catchHandler
|
|
3401
|
+
};
|
|
3402
|
+
this.steps.push(step);
|
|
3403
|
+
return this;
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Adds parallel functionality steps to the DAG.
|
|
3407
|
+
*/
|
|
3408
|
+
parallel(name, functionalityNames, options = {}) {
|
|
3409
|
+
if (!name || typeof name !== "string") {
|
|
3410
|
+
throw new Error("[FunctionalityOrchestration] Parallel group name must be a non-empty string.");
|
|
3411
|
+
}
|
|
3412
|
+
if (!functionalityNames || functionalityNames.length === 0) {
|
|
3413
|
+
throw new Error("[FunctionalityOrchestration] Parallel step requires at least one functionality.");
|
|
3414
|
+
}
|
|
3415
|
+
const previousStep = this.steps.length > 0 ? this.steps[this.steps.length - 1].id : void 0;
|
|
3416
|
+
const baseDependsOn = options.dependsOn || (previousStep ? [previousStep] : []);
|
|
3417
|
+
const parallelStepIds = [];
|
|
3418
|
+
for (const funcName of functionalityNames) {
|
|
3419
|
+
this.assertServiceBoundary(funcName);
|
|
3420
|
+
const stepId = `${name}_${funcName}`;
|
|
3421
|
+
parallelStepIds.push(stepId);
|
|
3422
|
+
this.steps.push({
|
|
3423
|
+
id: stepId,
|
|
3424
|
+
name: funcName,
|
|
3425
|
+
type: "task",
|
|
3426
|
+
target: funcName,
|
|
3427
|
+
dependsOn: baseDependsOn
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
const joinId = `${name}_join`;
|
|
3431
|
+
this.steps.push({
|
|
3432
|
+
id: joinId,
|
|
3433
|
+
name: `${name} Join`,
|
|
3434
|
+
type: "fanIn",
|
|
3435
|
+
dependsOn: parallelStepIds
|
|
3436
|
+
});
|
|
3437
|
+
return this;
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Adds conditional branching to the DAG.
|
|
3441
|
+
*/
|
|
3442
|
+
branch(name, condition, thenFunc, elseFunc, options = {}) {
|
|
3443
|
+
this.assertServiceBoundary(thenFunc);
|
|
3444
|
+
if (elseFunc) this.assertServiceBoundary(elseFunc);
|
|
3445
|
+
const previousStep = this.steps.length > 0 ? this.steps[this.steps.length - 1].id : void 0;
|
|
3446
|
+
const baseDependsOn = options.dependsOn || (previousStep ? [previousStep] : []);
|
|
3447
|
+
const thenStepId = `${name}_then_${thenFunc}`;
|
|
3448
|
+
this.steps.push({
|
|
3449
|
+
id: thenStepId,
|
|
3450
|
+
name: thenFunc,
|
|
3451
|
+
type: "task",
|
|
3452
|
+
target: thenFunc,
|
|
3453
|
+
condition,
|
|
3454
|
+
dependsOn: baseDependsOn
|
|
3455
|
+
});
|
|
3456
|
+
let joinDeps = [thenStepId];
|
|
3457
|
+
if (elseFunc) {
|
|
3458
|
+
const elseStepId = `${name}_else_${elseFunc}`;
|
|
3459
|
+
this.steps.push({
|
|
3460
|
+
id: elseStepId,
|
|
3461
|
+
name: elseFunc,
|
|
3462
|
+
type: "task",
|
|
3463
|
+
target: elseFunc,
|
|
3464
|
+
condition: async (ctx) => !await condition(ctx),
|
|
3465
|
+
dependsOn: baseDependsOn
|
|
3466
|
+
});
|
|
3467
|
+
joinDeps.push(elseStepId);
|
|
3468
|
+
}
|
|
3469
|
+
const joinId = `${name}_join`;
|
|
3470
|
+
this.steps.push({
|
|
3471
|
+
id: joinId,
|
|
3472
|
+
name: `${name} Join`,
|
|
3473
|
+
type: "fanIn",
|
|
3474
|
+
dependsOn: joinDeps
|
|
3475
|
+
});
|
|
3476
|
+
return this;
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Adds fan-out step processing items through a functionality.
|
|
3480
|
+
*/
|
|
3481
|
+
fanOut(name, funcName, getItems, options = {}) {
|
|
3482
|
+
this.assertServiceBoundary(funcName);
|
|
3483
|
+
const previousStep = this.steps.length > 0 ? this.steps[this.steps.length - 1].id : void 0;
|
|
3484
|
+
const dependsOn = options.dependsOn || (previousStep ? [previousStep] : []);
|
|
3485
|
+
this.steps.push({
|
|
3486
|
+
id: name,
|
|
3487
|
+
name,
|
|
3488
|
+
type: "fanOut",
|
|
3489
|
+
target: funcName,
|
|
3490
|
+
getItems,
|
|
3491
|
+
concurrency: options.concurrency ?? 5,
|
|
3492
|
+
dependsOn
|
|
3493
|
+
});
|
|
3494
|
+
return this;
|
|
3495
|
+
}
|
|
3496
|
+
/**
|
|
3497
|
+
* Nested orchestration: Invokes a child FunctionalityOrchestration within the same service boundary.
|
|
3498
|
+
*/
|
|
3499
|
+
thenFunctionalityOrchestration(childOrchId, options = {}) {
|
|
3500
|
+
if (!childOrchId || typeof childOrchId !== "string") {
|
|
3501
|
+
throw new Error("[FunctionalityOrchestration] Child orchestration id must be a non-empty string.");
|
|
3502
|
+
}
|
|
3503
|
+
const stepId = options.id || `nested_${childOrchId}_${this.steps.length + 1}`;
|
|
3504
|
+
const previousStep = this.steps.length > 0 ? this.steps[this.steps.length - 1].id : void 0;
|
|
3505
|
+
const dependsOn = options.dependsOn || (previousStep ? [previousStep] : []);
|
|
3506
|
+
this.steps.push({
|
|
3507
|
+
id: stepId,
|
|
3508
|
+
name: childOrchId,
|
|
3509
|
+
type: "nested",
|
|
3510
|
+
nestedOrchestrationId: childOrchId,
|
|
3511
|
+
dependsOn,
|
|
3512
|
+
retryPolicy: options.retry,
|
|
3513
|
+
timeoutPolicy: options.timeoutMs ? { timeoutMs: options.timeoutMs } : void 0
|
|
3514
|
+
});
|
|
3515
|
+
return this;
|
|
3516
|
+
}
|
|
3517
|
+
getSteps() {
|
|
3518
|
+
return Object.freeze([...this.steps]);
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Converts this FunctionalityOrchestration to a pure serializable OrchestrationDefinition.
|
|
3522
|
+
*/
|
|
3523
|
+
toDefinition() {
|
|
3524
|
+
return {
|
|
3525
|
+
id: this.id,
|
|
3526
|
+
name: this.name,
|
|
3527
|
+
version: this.version,
|
|
3528
|
+
description: this.description,
|
|
3529
|
+
timeoutMs: this.timeoutMs,
|
|
3530
|
+
scope: "functionality",
|
|
3531
|
+
dependencies: [],
|
|
3532
|
+
steps: this.steps.map((s) => ({
|
|
3533
|
+
id: s.id,
|
|
3534
|
+
name: s.name,
|
|
3535
|
+
type: s.type,
|
|
3536
|
+
target: s.target,
|
|
3537
|
+
dependsOn: s.dependsOn ? [...s.dependsOn] : void 0,
|
|
3538
|
+
retryPolicy: s.retryPolicy ? { ...s.retryPolicy } : void 0,
|
|
3539
|
+
timeoutPolicy: s.timeoutPolicy ? { ...s.timeoutPolicy } : void 0,
|
|
3540
|
+
compensationTarget: s.compensation?.target ?? s.compensation?.name,
|
|
3541
|
+
compensationOperation: s.compensation?.operation ?? s.compensation?.name,
|
|
3542
|
+
nestedOrchestrationId: s.nestedOrchestrationId,
|
|
3543
|
+
concurrency: s.concurrency
|
|
3544
|
+
}))
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Named flow builder interface.
|
|
3549
|
+
*/
|
|
3550
|
+
flow(name) {
|
|
3551
|
+
if (!name || typeof name !== "string" || name.trim() === "") {
|
|
3552
|
+
throw new Error("[FunctionalityOrchestration] Flow name must be a non-empty string.");
|
|
3553
|
+
}
|
|
3554
|
+
const steps = [];
|
|
3555
|
+
const self = this;
|
|
3556
|
+
const flowInstance = {
|
|
3557
|
+
name,
|
|
3558
|
+
steps,
|
|
3559
|
+
visibility: "internal",
|
|
3560
|
+
thenFunctionality(funcName, options) {
|
|
3561
|
+
if (!funcName) {
|
|
3562
|
+
throw new Error("[FunctionalityFlow] Functionality name must be a non-empty string.");
|
|
3563
|
+
}
|
|
3564
|
+
if (typeof funcName !== "string") {
|
|
3565
|
+
if (typeof funcName === "object" && ("call" in funcName || "getStatus" in funcName || "definition" in funcName || "instance" in funcName || "id" in funcName)) {
|
|
3566
|
+
throw new Error(
|
|
3567
|
+
"[FunctionalityOrchestration] Invariant Violation: FunctionalityOrchestration only composes functionalities within the service, not independent services."
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
throw new Error("[FunctionalityFlow] Functionality name must be a non-empty string.");
|
|
3571
|
+
}
|
|
3572
|
+
self.assertServiceBoundary(funcName);
|
|
3573
|
+
const stepId = `${name}_${funcName}_${steps.length + 1}`;
|
|
3574
|
+
steps.push({
|
|
3575
|
+
id: stepId,
|
|
3576
|
+
type: "functionality",
|
|
3577
|
+
target: funcName,
|
|
3578
|
+
dependsOn: options?.dependsOn,
|
|
3579
|
+
retryPolicy: options?.retry,
|
|
3580
|
+
timeoutPolicy: options?.timeoutMs ? { timeoutMs: options?.timeoutMs } : void 0
|
|
3581
|
+
});
|
|
3582
|
+
self.thenFunctionality(funcName, {
|
|
3583
|
+
id: stepId,
|
|
3584
|
+
retry: options?.retry,
|
|
3585
|
+
timeoutMs: options?.timeoutMs,
|
|
3586
|
+
dependsOn: options?.dependsOn
|
|
3587
|
+
});
|
|
3588
|
+
return flowInstance;
|
|
3589
|
+
},
|
|
3590
|
+
thenFunctionalityOrchestration(childName, flowName) {
|
|
3591
|
+
if (!childName || typeof childName !== "string") {
|
|
3592
|
+
throw new Error("[FunctionalityFlow] Orchestration name must be a non-empty string.");
|
|
3593
|
+
}
|
|
3594
|
+
const stepId = `${name}_orch_${childName}_${steps.length + 1}`;
|
|
3595
|
+
steps.push({
|
|
3596
|
+
id: stepId,
|
|
3597
|
+
type: "functionalityOrchestration",
|
|
3598
|
+
target: childName,
|
|
3599
|
+
flow: flowName
|
|
3600
|
+
});
|
|
3601
|
+
self.thenFunctionalityOrchestration(childName, { id: stepId });
|
|
3602
|
+
return flowInstance;
|
|
3603
|
+
},
|
|
3604
|
+
register(options) {
|
|
3605
|
+
if (options?.visibility) flowInstance.visibility = options.visibility;
|
|
3606
|
+
return flowInstance;
|
|
3607
|
+
}
|
|
3608
|
+
};
|
|
3609
|
+
this.flows.set(name, flowInstance);
|
|
3610
|
+
return flowInstance;
|
|
3611
|
+
}
|
|
3612
|
+
getFlow(name) {
|
|
3613
|
+
return this.flows.get(name);
|
|
3614
|
+
}
|
|
3615
|
+
listFlows() {
|
|
3616
|
+
return Array.from(this.flows.values());
|
|
3617
|
+
}
|
|
3618
|
+
clearFlows() {
|
|
3619
|
+
this.flows.clear();
|
|
3620
|
+
this.steps.length = 0;
|
|
3621
|
+
}
|
|
3622
|
+
assertServiceBoundary(target) {
|
|
3623
|
+
if (target.endsWith("ServiceFacade") || target.startsWith("service.") || target.includes("Service.") || target.endsWith("Service") && !target.includes("Functionality") && !target.includes("Atom") || target.startsWith("Service") && !target.includes("Functionality") && !target.includes("Atom")) {
|
|
3624
|
+
throw new Error(
|
|
3625
|
+
`[FunctionalityOrchestration] Plane Isolation Invariant Violation: Invariant Violation: FunctionalityOrchestration only composes functionalities within the service, not independent services. External Service composition is forbidden. Target "${target}" is not an internal functionality.`
|
|
3626
|
+
);
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
};
|
|
741
3630
|
// Annotate the CommonJS export names for ESM import in node:
|
|
742
3631
|
0 && (module.exports = {
|
|
3632
|
+
FunctionalityOrchestration,
|
|
743
3633
|
HttpProtocolAdapter,
|
|
3634
|
+
KrnlPackageReconciler,
|
|
3635
|
+
KrnlQueue,
|
|
3636
|
+
KrnlWorkerPool,
|
|
744
3637
|
LifecycleManager,
|
|
745
3638
|
MemoryTransport,
|
|
746
3639
|
P2PProtocolAdapter,
|
|
747
3640
|
ProtocolResolver,
|
|
748
3641
|
ServiceDiscovery,
|
|
749
3642
|
ServiceEventBus,
|
|
3643
|
+
ServiceFacade,
|
|
750
3644
|
ServiceInstance,
|
|
751
3645
|
ServiceProxy,
|
|
752
3646
|
ServiceRegistry,
|
|
@@ -754,7 +3648,11 @@ var ProtocolResolver = class {
|
|
|
754
3648
|
ServiceStateStore,
|
|
755
3649
|
UdpProtocolAdapter,
|
|
756
3650
|
WebSocketProtocolAdapter,
|
|
3651
|
+
WorkerExecutionBoundary,
|
|
757
3652
|
createService,
|
|
758
3653
|
createServiceDefinition,
|
|
759
|
-
|
|
3654
|
+
createServiceFacade,
|
|
3655
|
+
createServiceProxy,
|
|
3656
|
+
validateOrchestrationDAG,
|
|
3657
|
+
validateServiceDefinition
|
|
760
3658
|
});
|