@spfn/core 0.2.0-beta.9 → 0.3.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +693 -279
  3. package/dist/app-config/index.d.ts +96 -0
  4. package/dist/app-config/index.js +58 -0
  5. package/dist/app-config/index.js.map +1 -0
  6. package/dist/authz/index.d.ts +34 -0
  7. package/dist/authz/index.js +430 -0
  8. package/dist/authz/index.js.map +1 -0
  9. package/dist/boss-BiDCbbAQ.d.ts +337 -0
  10. package/dist/cache/index.js +42 -30
  11. package/dist/cache/index.js.map +1 -1
  12. package/dist/codegen/index.d.ts +121 -13
  13. package/dist/codegen/index.js +212 -15
  14. package/dist/codegen/index.js.map +1 -1
  15. package/dist/config/index.d.ts +576 -30
  16. package/dist/config/index.js +136 -17
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/contract/index.d.ts +221 -0
  19. package/dist/contract/index.js +558 -0
  20. package/dist/contract/index.js.map +1 -0
  21. package/dist/db/index.d.ts +704 -94
  22. package/dist/db/index.js +1105 -260
  23. package/dist/db/index.js.map +1 -1
  24. package/dist/define-middleware-CVKgqo8S.d.ts +167 -0
  25. package/dist/env/index.d.ts +26 -2
  26. package/dist/env/index.js +15 -5
  27. package/dist/env/index.js.map +1 -1
  28. package/dist/env/loader.d.ts +26 -19
  29. package/dist/env/loader.js +55 -25
  30. package/dist/env/loader.js.map +1 -1
  31. package/dist/errors/index.d.ts +10 -0
  32. package/dist/errors/index.js +40 -7
  33. package/dist/errors/index.js.map +1 -1
  34. package/dist/event/index.d.ts +33 -3
  35. package/dist/event/index.js +24 -3
  36. package/dist/event/index.js.map +1 -1
  37. package/dist/event/sse/client.d.ts +55 -3
  38. package/dist/event/sse/client.js +164 -45
  39. package/dist/event/sse/client.js.map +1 -1
  40. package/dist/event/sse/index.d.ts +12 -5
  41. package/dist/event/sse/index.js +280 -32
  42. package/dist/event/sse/index.js.map +1 -1
  43. package/dist/event/ws/client.d.ts +59 -0
  44. package/dist/event/ws/client.js +273 -0
  45. package/dist/event/ws/client.js.map +1 -0
  46. package/dist/event/ws/index.d.ts +94 -0
  47. package/dist/event/ws/index.js +272 -0
  48. package/dist/event/ws/index.js.map +1 -0
  49. package/dist/job/index.d.ts +48 -3
  50. package/dist/job/index.js +265 -74
  51. package/dist/job/index.js.map +1 -1
  52. package/dist/logger/index.d.ts +5 -0
  53. package/dist/logger/index.js +14 -12
  54. package/dist/logger/index.js.map +1 -1
  55. package/dist/middleware/index.d.ts +352 -10
  56. package/dist/middleware/index.js +1079 -13
  57. package/dist/middleware/index.js.map +1 -1
  58. package/dist/nextjs/index.d.ts +39 -3
  59. package/dist/nextjs/index.js +82 -29
  60. package/dist/nextjs/index.js.map +1 -1
  61. package/dist/nextjs/server.d.ts +69 -52
  62. package/dist/nextjs/server.js +140 -60
  63. package/dist/nextjs/server.js.map +1 -1
  64. package/dist/ops/index.d.ts +208 -0
  65. package/dist/ops/index.js +800 -0
  66. package/dist/ops/index.js.map +1 -0
  67. package/dist/route/index.d.ts +10 -694
  68. package/dist/route/index.js +112 -23
  69. package/dist/route/index.js.map +1 -1
  70. package/dist/route-builder-2ani2jEI.d.ts +553 -0
  71. package/dist/router-DJdpwuB6.d.ts +127 -0
  72. package/dist/security/index.d.ts +83 -0
  73. package/dist/security/index.js +173 -0
  74. package/dist/security/index.js.map +1 -0
  75. package/dist/server/index.d.ts +619 -31
  76. package/dist/server/index.js +2583 -360
  77. package/dist/server/index.js.map +1 -1
  78. package/dist/token-manager-BT5EnUAR.d.ts +278 -0
  79. package/dist/types-2AbaW4Ie.d.ts +205 -0
  80. package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
  81. package/dist/types-ClQVomgV.d.ts +115 -0
  82. package/dist/types-ZQODsBft.d.ts +282 -0
  83. package/docs/file-upload.md +195 -333
  84. package/package.json +250 -208
  85. package/src/cache/README.md +330 -0
  86. package/src/codegen/README.md +516 -0
  87. package/src/config/README.md +326 -0
  88. package/src/contract/README.md +326 -0
  89. package/src/db/README.md +589 -0
  90. package/src/db/manager/README.md +500 -0
  91. package/src/db/schema/README.md +354 -0
  92. package/src/db/transaction/README.md +822 -0
  93. package/src/env/README.md +651 -0
  94. package/src/errors/README.md +429 -0
  95. package/src/event/README.md +741 -0
  96. package/src/job/README.md +613 -0
  97. package/src/logger/README.md +321 -0
  98. package/src/middleware/README.md +634 -0
  99. package/src/nextjs/README.md +608 -0
  100. package/src/route/README.md +738 -0
  101. package/src/security/README.md +100 -0
  102. package/src/server/README.md +704 -0
  103. package/dist/boss-DI1r4kTS.d.ts +0 -244
  104. package/dist/router-Di7ENoah.d.ts +0 -151
  105. package/dist/types-B-e_f2dQ.d.ts +0 -121
  106. package/docs/cache.md +0 -133
  107. package/docs/codegen.md +0 -74
  108. package/docs/database.md +0 -346
  109. package/docs/entity.md +0 -539
  110. package/docs/env.md +0 -477
  111. package/docs/errors.md +0 -319
  112. package/docs/event.md +0 -116
  113. package/docs/job.md +0 -131
  114. package/docs/logger.md +0 -108
  115. package/docs/middleware.md +0 -337
  116. package/docs/nextjs.md +0 -241
  117. package/docs/repository.md +0 -496
  118. package/docs/route.md +0 -497
  119. package/docs/server.md +0 -307
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/event/ws/handler.ts","../../../src/event/ws/index.ts"],"names":[],"mappings":";;;AAmBA,IAAM,QAAA,GAAW,MAAA,CAAO,KAAA,CAAM,eAAe,CAAA;AAStC,SAAS,eAAA,CACZ,QACA,SAAA,EAEJ;AACI,EAAA,IAAI,CAAC,SAAA,EACL;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,IAAI,CAAC,MAAA,EACL;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,OAAO,SAAA,CAAU,IAAI,MAAM,CAAA;AAC/B;AAWA,eAAsB,gBAIlB,MAAA,EACA,MAAA,EACA,MAAA,GAA8C,IAC9C,YAAA,EAEJ;AACI,EAAA,MAAM,eAAA,GAAkB,MAAM,YAAA,EAAa;AAE3C,EAAA,MAAM;AAAA,IACF,YAAA,GAAe,GAAA;AAAA,IACf,IAAA,GAAO,KAAA;AAAA,IACP,UAAA,GAAa,OAAA;AAAA,IACb,gBAAA,GAAmB,OAAA;AAAA,IACnB,cAAA,GAAiB,GAAA;AAAA,IACjB,wBAAA,GAA2B,CAAA;AAAA,IAC3B,cAAA;AAAA,IACA,IAAA,EAAM;AAAA,GACV,GAAI,MAAA;AAEJ,EAAA,MAAM,eAAA,GAAkB,kBAAkB,cAAA,CAAe,MAAA,GAAS,IAC5D,IAAI,GAAA,CAAI,cAAc,CAAA,GACtB,IAAA;AAEN,EAAA,IAAI,UAAA,EAAY,OAAA,IAAW,CAAC,YAAA,EAC5B;AACI,IAAA,MAAM,IAAI,KAAA;AAAA,MACN;AAAA,KAEJ;AAAA,EACJ;AAEA,EAAA,MAAM,MAAM,IAAI,eAAA,CAAgB,EAAE,MAAA,EAAQ,IAAA,EAAM,YAAY,CAAA;AAG5D,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAS;AAE7B,EAAA,MAAM,aAAA,uBAAoB,GAAA,EAAoB;AAE9C,EAAA,GAAA,CAAI,EAAA,CAAG,YAAA,EAAc,CAAC,EAAA,EAAS,GAAA,KAC/B;AAKI,IAAA,IAAI,CAAC,eAAA,CAAgB,GAAA,CAAI,OAAA,EAAS,MAAA,EAAQ,eAAe,CAAA,EACzD;AACI,MAAA,QAAA,CAAS,KAAK,uCAAA,EAAyC,EAAE,QAAQ,GAAA,CAAI,OAAA,EAAS,QAAQ,CAAA;AACtF,MAAA,EAAA,CAAG,KAAA,CAAM,MAAM,oBAAoB,CAAA;AAEnC,MAAA;AAAA,IACJ;AAGA,IAAA,IAAI,OAAA,CAAQ,QAAQ,cAAA,EACpB;AACI,MAAA,EAAA,CAAG,KAAA,CAAM,MAAM,oBAAoB,CAAA;AAEnC,MAAA;AAAA,IACJ;AAEA,IAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AACd,IAAA,EAAA,CAAG,GAAG,OAAA,EAAS,MAAM,OAAA,CAAQ,MAAA,CAAO,EAAE,CAAC,CAAA;AACvC,IAAA,gBAAA,CAAiB,EAAA,EAAI,GAAA,EAAK,MAAA,EAAQ,UAAA,EAAY,YAAA,EAAc;AAAA,MACxD,YAAA;AAAA,MACA,gBAAA;AAAA,MACA,wBAAA;AAAA,MACA;AAAA,KACH,CAAA,CACI,KAAA,CAAM,CAAC,GAAA,KACR;AACI,MAAA,QAAA,CAAS,KAAA,CAAM,sCAAsC,GAAG,CAAA;AACxD,MAAA,IAAI,GAAG,UAAA,KAAe,CAAA,EAAG,EAAA,CAAG,KAAA,CAAM,MAAM,uBAAuB,CAAA;AAAA,IACnE,CAAC,CAAA;AAAA,EACT,CAAC,CAAA;AAED,EAAA,GAAA,CAAI,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KACjB;AACI,IAAA,QAAA,CAAS,KAAA,CAAM,0BAA0B,GAAG,CAAA;AAAA,EAChD,CAAC,CAAA;AAED,EAAA,QAAA,CAAS,IAAA,CAAK,CAAA,wCAAA,EAAsC,IAAI,CAAA,CAAA,EAAI;AAAA,IACxD,QAAQ,MAAA,CAAO,UAAA;AAAA,IACf,IAAA,EAAM,CAAC,CAAC,UAAA,EAAY;AAAA,GACvB,CAAA;AAED,EAAA,OAAO,MAAM,IAAI,OAAA,CAAc,CAAC,SAAS,MAAA,KACzC;AAEI,IAAA,KAAA,MAAW,UAAU,OAAA,EACrB;AACI,MAAA,MAAA,CAAO,KAAA,CAAM,MAAM,sBAAsB,CAAA;AAAA,IAC7C;AACA,IAAA,OAAA,CAAQ,KAAA,EAAM;AAEd,IAAA,GAAA,CAAI,KAAA,CAAM,CAAC,GAAA,KACX;AACI,MAAA,IAAI,GAAA,SAAY,GAAG,CAAA;AAAA,WACd,OAAA,EAAQ;AAAA,IACjB,CAAC,CAAA;AAAA,EACL,CAAC,CAAA;AACL;AAcA,eAAe,iBACX,EAAA,EACA,GAAA,EACA,MAAA,EACA,UAAA,EACA,cACA,IAAA,EAEJ;AACI,EAAA,MAAM,EAAE,YAAA,EAAc,gBAAA,EAAkB,wBAAA,EAA0B,eAAc,GAAI,IAAA;AAEpF,EAAA,IAAI,SAAA;AACJ,EAAA,IAAI,yBAAyC,EAAC;AAC9C,EAAA,IAAI,mBAA6B,EAAC;AAClC,EAAA,EAAA,CAAG,EAAA,CAAG,SAAS,MACf;AACI,IAAA,aAAA,CAAc,SAAS,CAAA;AACvB,IAAA,sBAAA,CAAuB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,EAAI,CAAA;AACzC,IAAA,IAAI,iBAAiB,MAAA,GAAS,CAAA;AAC1B,MAAA,QAAA,CAAS,IAAA,CAAK,6BAAA,EAA+B,EAAE,MAAA,EAAQ,kBAAkB,CAAA;AAAA,EACjF,CAAC,CAAA;AAED,EAAA,MAAM,GAAA,GAAM,SAAS,GAAG,CAAA;AACxB,EAAA,IAAI,CAAC,GAAA,EACL;AACI,IAAA,EAAA,CAAG,KAAA,CAAM,MAAM,qBAAqB,CAAA;AAEpC,IAAA;AAAA,EACJ;AAGA,EAAA,MAAM,UAAU,MAAM,cAAA,CAAe,KAAK,UAAA,EAAY,OAAA,GAAU,eAAe,MAAS,CAAA;AACxF,EAAA,IAAI,YAAY,KAAA,EAChB;AACI,IAAA,EAAA,CAAG,KAAA,CAAM,MAAM,eAAe,CAAA;AAE9B,IAAA;AAAA,EACJ;AACA,EAAA,IAAI,YAAY,IAAA,EAChB;AACI,IAAA,EAAA,CAAG,KAAA,CAAM,MAAM,0BAA0B,CAAA;AAEzC,IAAA;AAAA,EACJ;AAGA,EAAA,MAAM,eAAA,GAAkB,oBAAA,CAAqB,GAAA,EAAK,MAAA,CAAO,UAAsB,CAAA;AAC/E,EAAA,IAAI,eAAA,CAAgB,WAAW,CAAA,EAC/B;AACI,IAAA,EAAA,CAAG,KAAA,CAAM,KAAM,gCAAgC,CAAA;AAE/C,IAAA;AAAA,EACJ;AAGA,EAAA,MAAM,aAAA,GAAgB,MAAM,oBAAA,CAAqB,OAAA,EAAS,iBAAiB,UAAU,CAAA;AACrF,EAAA,IAAI,kBAAkB,IAAA,EACtB;AACI,IAAA,EAAA,CAAG,KAAA,CAAM,MAAM,yCAAyC,CAAA;AAExD,IAAA;AAAA,EACJ;AAGA,EAAA,IAAI,wBAAA,GAA2B,CAAA,IAAK,OAAO,OAAA,KAAY,QAAA,EACvD;AACI,IAAA,MAAM,OAAA,GAAU,aAAA,CAAc,GAAA,CAAI,OAAO,CAAA,IAAK,CAAA;AAC9C,IAAA,IAAI,WAAW,wBAAA,EACf;AACI,MAAA,EAAA,CAAG,KAAA,CAAM,MAAM,uCAAuC,CAAA;AAEtD,MAAA;AAAA,IACJ;AAEA,IAAA,aAAA,CAAc,GAAA,CAAI,OAAA,EAAS,OAAA,GAAU,CAAC,CAAA;AACtC,IAAA,EAAA,CAAG,EAAA,CAAG,SAAS,MACf;AACI,MAAA,MAAM,SAAA,GAAA,CAAa,aAAA,CAAc,GAAA,CAAI,OAAO,KAAK,CAAA,IAAK,CAAA;AACtD,MAAA,IAAI,SAAA,IAAa,CAAA,EAAG,aAAA,CAAc,MAAA,CAAO,OAAO,CAAA;AAAA,WAC3C,aAAA,CAAc,GAAA,CAAI,OAAA,EAAS,SAAS,CAAA;AAAA,IAC7C,CAAC,CAAA;AAAA,EACL;AAEA,EAAA,gBAAA,GAAmB,aAAA;AACnB,EAAA,QAAA,CAAS,KAAK,kCAAA,EAAoC;AAAA,IAC9C,MAAA,EAAQ,aAAA;AAAA,IACR,SAAS,OAAA,IAAW;AAAA,GACvB,CAAA;AAGD,EAAA,MAAM,UAAA,GAAa,gBAAA,CAAiB,EAAA,EAAI,gBAAgB,CAAA;AAGxD,EAAA,sBAAA,GAAyB,gBAAgB,EAAA,EAAI,MAAA,EAAQ,aAAA,EAAe,OAAA,EAAS,YAAY,gBAAgB,CAAA;AAGzG,EAAA,IAAI,EAAA,CAAG,eAAe,CAAA,EACtB;AACI,IAAA,sBAAA,CAAuB,OAAA,CAAQ,CAAA,EAAA,KAAM,EAAA,EAAI,CAAA;AACzC,IAAA,sBAAA,GAAyB,EAAC;AAE1B,IAAA;AAAA,EACJ;AAGA,EAAA,EAAA,CAAG,EAAA,CAAG,SAAA,EAAW,CAAC,IAAA,KAClB;AACI,IAAA,eAAA,CAAgB,IAAA,EAAM,MAAA,EAAQ,UAAA,EAAY,OAAO,CAAA,CAC5C,KAAA,CAAM,CAAC,GAAA,KAAe,QAAA,CAAS,KAAA,CAAM,yBAAA,EAA2B,GAAG,CAAC,CAAA;AAAA,EAC7E,CAAC,CAAA;AAKD,EAAA,IAAI,eAAe,CAAA,EACnB;AACI,IAAA,EAAA,CAAG,OAAA,GAAU,IAAA;AACb,IAAA,EAAA,CAAG,EAAA,CAAG,QAAQ,MACd;AACI,MAAA,EAAA,CAAG,OAAA,GAAU,IAAA;AAAA,IACjB,CAAC,CAAA;AAED,IAAA,SAAA,GAAY,YAAY,MACxB;AACI,MAAA,IAAI,EAAA,CAAG,eAAe,CAAA,EAAG;AAEzB,MAAA,IAAI,EAAA,CAAG,YAAY,KAAA,EACnB;AAEI,QAAA,EAAA,CAAG,SAAA,EAAU;AAEb,QAAA;AAAA,MACJ;AAEA,MAAA,EAAA,CAAG,OAAA,GAAU,KAAA;AACb,MAAA,EAAA,CAAG,IAAA,EAAK;AAAA,IACZ,GAAG,YAAY,CAAA;AAAA,EACnB;AAGA,EAAA,UAAA,CAAW,KAAK,aAAA,EAAe;AAAA,IAC3B,gBAAA,EAAkB,aAAA;AAAA,IAClB,SAAA,EAAW,KAAK,GAAA;AAAI,GACvB,CAAA;AACL;AAMA,SAAS,SAAS,GAAA,EAClB;AACI,EAAA,IACA;AACI,IAAA,OAAO,IAAI,GAAA,CAAI,GAAA,CAAI,GAAA,IAAO,KAAK,gBAAgB,CAAA;AAAA,EACnD,CAAA,CAAA,MAEA;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AASA,eAAe,cAAA,CACX,KACA,YAAA,EAEJ;AACI,EAAA,IAAI,CAAC,YAAA,EACL;AACI,IAAA,OAAO,MAAA;AAAA,EACX;AAEA,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,OAAO,CAAA;AAC1C,EAAA,IAAI,CAAC,KAAA,EACL;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,OAAO,MAAM,YAAA,CAAa,MAAA,CAAO,KAAK,CAAA;AAC1C;AAEA,SAAS,oBAAA,CAAqB,KAAU,eAAA,EACxC;AACI,EAAA,MAAM,WAAA,GAAc,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,QAAQ,CAAA;AACjD,EAAA,IAAI,CAAC,WAAA,EACL;AACI,IAAA,OAAO,EAAC;AAAA,EACZ;AAEA,EAAA,OAAO,WAAA,CACF,KAAA,CAAM,GAAG,CAAA,CACT,IAAI,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,EAAM,EACjB,MAAA,CAAO,CAAA,CAAA,KAAK,eAAA,CAAgB,QAAA,CAAS,CAAC,CAAC,CAAA;AAChD;AAEA,eAAe,oBAAA,CACX,OAAA,EACA,eAAA,EACA,UAAA,EAEJ;AACI,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,UAAA,EAAY,SAAA,EAC7B;AACI,IAAA,OAAO,eAAA;AAAA,EACX;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,UAAA,CAAW,SAAA,CAAU,SAAS,eAAe,CAAA;AAEnE,EAAA,OAAO,OAAA,CAAQ,MAAA,KAAW,CAAA,GAAI,IAAA,GAAO,OAAA;AACzC;AAQO,SAAS,QAAA,CAAS,EAAA,EAAS,KAAA,EAAgB,gBAAA,EAClD;AACI,EAAA,IAAI,EAAA,CAAG,eAAe,CAAA,EAAG;AAEzB,EAAA,IAAI,EAAA,CAAG,iBAAiB,gBAAA,EACxB;AACI,IAAA,EAAA,CAAG,KAAA,CAAM,MAAM,sBAAsB,CAAA;AAErC,IAAA;AAAA,EACJ;AAEA,EAAA,IACA;AACI,IAAA,EAAA,CAAG,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,EACjC,CAAA,CAAA,MAEA;AAAA,EAEA;AACJ;AAEA,SAAS,gBAAA,CAAiB,IAAS,gBAAA,EACnC;AACI,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,CAAC,IAAA,EAAM,OAAA,KAAY,QAAA,CAAS,EAAA,EAAI,EAAE,IAAA,EAAM,IAAA,EAAM,OAAA,EAAQ,EAAG,gBAAgB,CAAA;AAAA,IAC/E,OAAO,CAAC,IAAA,EAAM,WAAW,EAAA,CAAG,KAAA,CAAM,MAAM,MAAM;AAAA,GAClD;AACJ;AAEA,SAAS,gBACL,EAAA,EACA,MAAA,EACA,aAAA,EACA,OAAA,EACA,YACA,gBAAA,EAEJ;AACI,EAAA,MAAM,eAA+B,EAAC;AAEtC,EAAA,KAAA,MAAW,aAAa,aAAA,EACxB;AACI,IAAA,MAAM,QAAA,GAAW,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA;AACxC,IAAA,IAAI,CAAC,QAAA,EAAU;AAEf,IAAA,MAAM,WAAA,GAAc,QAAA,CAAS,SAAA,CAAU,CAAC,OAAA,KACxC;AACI,MAAA,IAAI,EAAA,CAAG,eAAe,CAAA,EAAG;AAEzB,MAAA,IAAI,OAAA,IAAW,UAAA,EAAY,MAAA,GAAS,SAAS,CAAA,EAC7C;AACI,QAAA,IAAI,CAAC,UAAA,CAAW,MAAA,CAAO,SAAS,CAAA,CAAE,OAAA,EAAS,OAAO,CAAA,EAAG;AAAA,MACzD;AAEA,MAAA,QAAA,CAAS,IAAI,EAAE,IAAA,EAAM,WAAW,IAAA,EAAM,OAAA,IAAW,gBAAgB,CAAA;AAAA,IACrE,CAAC,CAAA;AAED,IAAA,YAAA,CAAa,KAAK,WAAW,CAAA;AAAA,EACjC;AAEA,EAAA,OAAO,YAAA;AACX;AAEA,eAAe,eAAA,CACX,IAAA,EACA,MAAA,EACA,UAAA,EACA,OAAA,EAEJ;AACI,EAAA,IAAI,OAAA;AAEJ,EAAA,IACA;AACI,IAAA,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,QAAA,EAAU,CAAA;AAAA,EACxC,CAAA,CAAA,MAEA;AACI,IAAA;AAAA,EACJ;AAEA,EAAA,MAAM,EAAE,IAAA,EAAM,IAAA,EAAM,OAAA,EAAQ,GAAI,OAAA;AAChC,EAAA,IAAI,CAAC,IAAA,EAAM;AAEX,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,QAAA,CAAS,IAAI,CAAA;AACpC,EAAA,IAAI,CAAC,OAAA,EAAS;AAEd,EAAA,IACA;AACI,IAAA,MAAM,QAAQ,EAAE,OAAA,EAAS,OAAA,EAAS,EAAA,EAAI,YAAY,CAAA;AAAA,EACtD,SACO,GAAA,EACP;AACI,IAAA,QAAA,CAAS,KAAA,CAAM,CAAA,iCAAA,EAAoC,IAAI,CAAA,CAAA,EAAI,GAAY,CAAA;AAAA,EAC3E;AACJ;AAgBO,SAAS,oBAAoB,GAAA,EACpC;AACI,EAAA,OAAO,GAAA,CAAI,mBAAmB,GAAA,CAAI,MAAA,IAC3B,IAAI,OAAA,EAAS,eAAA,IAAmB,IAAI,OAAA,EAAS,MAAA;AACxD;AAEA,eAAe,YAAA,GACf;AACI,EAAA,IAAI,GAAA;AAEJ,EAAA,IACA;AACI,IAAA,GAAA,GAAM,MAAM,OAAO,IAAI,CAAA;AAAA,EAC3B,CAAA,CAAA,MAEA;AACI,IAAA,MAAM,IAAI,KAAA;AAAA,MACN;AAAA,KAEJ;AAAA,EACJ;AAEA,EAAA,MAAM,GAAA,GAAM,oBAAoB,GAAG,CAAA;AAEnC,EAAA,IAAI,OAAO,QAAQ,UAAA,EACnB;AACI,IAAA,MAAM,IAAI,KAAA;AAAA,MACN;AAAA,KAEJ;AAAA,EACJ;AAEA,EAAA,OAAO,GAAA;AACX;;;ACxcO,SAAS,eAGd,GAAA,EAIF;AACI,EAAA,OAAO;AAAA,IACH,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,UAAA,EAAY,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAAA,IAClC,QAAA,EAAW,GAAA,CAAI,QAAA,IAAY,EAAC;AAAA,IAC5B,QAAQ;AAAC,GACb;AACJ","file":"index.js","sourcesContent":["/**\n * WebSocket Handler\n *\n * Attaches a WebSocket server to an existing Node.js http.Server.\n * Handles authentication, event subscription, and client message routing.\n */\n\nimport type { Server } from 'node:http';\nimport type { EventDef } from '../types';\nimport type {\n WSRouterDef,\n WSHandlerConfig,\n WSHandlerAuthConfig,\n WSMessageHandlers,\n WSRawConnection,\n} from './types';\nimport type { SSETokenManager } from '../sse/token-manager';\nimport { logger } from '@spfn/core/logger';\n\nconst wsLogger = logger.child('@spfn/core:ws');\n\n/**\n * Decide whether a WebSocket upgrade's Origin is permitted.\n *\n * - no allow-list configured → allowed (check disabled)\n * - no Origin header (native/non-browser client) → allowed\n * - Origin present → must be in the allow-list\n */\nexport function isOriginAllowed(\n origin: string | undefined,\n allowList: Set<string> | null,\n): boolean\n{\n if (!allowList)\n {\n return true;\n }\n\n if (!origin)\n {\n return true;\n }\n\n return allowList.has(origin);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Attach a WebSocket server to a Node.js http.Server\n *\n * @returns cleanup function that closes the WebSocket server\n */\nexport async function attachWSHandler<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers,\n>(\n server: Server,\n router: WSRouterDef<TEvents, TMessages>,\n config: WSHandlerConfig & { path?: string } = {},\n tokenManager?: SSETokenManager,\n): Promise<() => Promise<void>>\n{\n const WebSocketServer = await loadWSServer();\n\n const {\n pingInterval = 30000,\n path = '/ws',\n maxPayload = 1_048_576,\n maxBufferedBytes = 1_048_576,\n maxConnections = 10_000,\n maxConnectionsPerSubject = 0,\n allowedOrigins,\n auth: authConfig,\n } = config;\n\n const originAllowList = allowedOrigins && allowedOrigins.length > 0\n ? new Set(allowedOrigins)\n : null;\n\n if (authConfig?.enabled && !tokenManager)\n {\n throw new Error(\n 'WebSocket auth.enabled=true requires a tokenManager. ' +\n 'Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer.',\n );\n }\n\n const wss = new WebSocketServer({ server, path, maxPayload });\n\n // Track live connections for graceful shutdown\n const clients = new Set<any>();\n // Live connection count per authenticated subject (for the per-subject cap)\n const subjectCounts = new Map<string, number>();\n\n wss.on('connection', (ws: any, req: any) =>\n {\n // Origin allow-list (cross-site WebSocket hijacking protection). Browsers\n // always send Origin on the upgrade; reject one that isn't allowed before\n // doing any work. A missing Origin (native/non-browser client, which can't\n // be CSRF'd into using ambient cookies) passes through.\n if (!isOriginAllowed(req.headers?.origin, originAllowList))\n {\n wsLogger.warn('WebSocket rejected: disallowed origin', { origin: req.headers?.origin });\n ws.close(1008, 'Origin not allowed');\n\n return;\n }\n\n // Global connection cap — reject before doing any work\n if (clients.size >= maxConnections)\n {\n ws.close(1013, 'Server at capacity');\n\n return;\n }\n\n clients.add(ws);\n ws.on('close', () => clients.delete(ws));\n handleConnection(ws, req, router, authConfig, tokenManager, {\n pingInterval,\n maxBufferedBytes,\n maxConnectionsPerSubject,\n subjectCounts,\n })\n .catch((err: Error) =>\n {\n wsLogger.error('WebSocket connection handler error', err);\n if (ws.readyState === 1) ws.close(1011, 'Internal server error');\n });\n });\n\n wss.on('error', (err: Error) =>\n {\n wsLogger.error('WebSocket server error', err);\n });\n\n wsLogger.info(`✓ WebSocket endpoint registered at ${path}`, {\n events: router.eventNames,\n auth: !!authConfig?.enabled,\n });\n\n return () => new Promise<void>((resolve, reject) =>\n {\n // Close all existing connections with 1001 Going Away\n for (const client of clients)\n {\n client.close(1001, 'Server shutting down');\n }\n clients.clear();\n\n wss.close((err?: Error) =>\n {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n\n// ============================================================================\n// Connection Handler\n// ============================================================================\n\ninterface ConnectionOptions\n{\n pingInterval: number;\n maxBufferedBytes: number;\n maxConnectionsPerSubject: number;\n subjectCounts: Map<string, number>;\n}\n\nasync function handleConnection(\n ws: any,\n req: any,\n router: WSRouterDef<any, any>,\n authConfig: WSHandlerAuthConfig | undefined,\n tokenManager: SSETokenManager | undefined,\n opts: ConnectionOptions,\n): Promise<void>\n{\n const { pingInterval, maxBufferedBytes, maxConnectionsPerSubject, subjectCounts } = opts;\n // Register close handler before any await — ensures we never miss the event even during auth\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let connectionUnsubscribes: (() => void)[] = [];\n let subscribedEvents: string[] = [];\n ws.on('close', () =>\n {\n clearInterval(pingTimer);\n connectionUnsubscribes.forEach(fn => fn());\n if (subscribedEvents.length > 0)\n wsLogger.info('WebSocket connection closed', { events: subscribedEvents });\n });\n\n const url = parseURL(req);\n if (!url)\n {\n ws.close(1002, 'Invalid request URL');\n\n return;\n }\n\n // ── 1. Authenticate ──\n const subject = await resolveSubject(url, authConfig?.enabled ? tokenManager : undefined);\n if (subject === false)\n {\n ws.close(4001, 'Missing token');\n\n return;\n }\n if (subject === null)\n {\n ws.close(4001, 'Invalid or expired token');\n\n return;\n }\n\n // ── 2. Resolve subscribed events ──\n const requestedEvents = parseRequestedEvents(url, router.eventNames as string[]);\n if (requestedEvents.length === 0)\n {\n ws.close(4000, 'No valid event names specified');\n\n return;\n }\n\n // ── 3. Authorize ──\n const allowedEvents = await resolveAllowedEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n ws.close(4003, 'Not authorized for any requested events');\n\n return;\n }\n\n // ── Per-subject connection cap (authenticated subjects only) ──\n if (maxConnectionsPerSubject > 0 && typeof subject === 'string')\n {\n const current = subjectCounts.get(subject) ?? 0;\n if (current >= maxConnectionsPerSubject)\n {\n ws.close(1013, 'Too many connections for this subject');\n\n return;\n }\n\n subjectCounts.set(subject, current + 1);\n ws.on('close', () =>\n {\n const remaining = (subjectCounts.get(subject) ?? 1) - 1;\n if (remaining <= 0) subjectCounts.delete(subject);\n else subjectCounts.set(subject, remaining);\n });\n }\n\n subscribedEvents = allowedEvents;\n wsLogger.info('WebSocket connection established', {\n events: allowedEvents,\n subject: subject ?? undefined,\n });\n\n // ── 4. Build connection wrapper ──\n const connection = createConnection(ws, maxBufferedBytes);\n\n // ── 5. Subscribe to server-push events ──\n connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig, maxBufferedBytes);\n\n // If socket closed during auth awaits, clean up and bail\n if (ws.readyState !== 1)\n {\n connectionUnsubscribes.forEach(fn => fn());\n connectionUnsubscribes = [];\n\n return;\n }\n\n // ── 6. Handle incoming messages ──\n ws.on('message', (data: Buffer | string) =>\n {\n onClientMessage(data, router, connection, subject)\n .catch((err: Error) => wsLogger.error('Unhandled message error', err));\n });\n\n // ── 7. Keep-alive ping with liveness (pong) tracking ──\n // Without reaping un-ponged sockets, a half-open connection (sleeping device,\n // NAT drop with no FIN) lingers with its subscriptions, timer, and buffers.\n if (pingInterval > 0)\n {\n ws.isAlive = true;\n ws.on('pong', () =>\n {\n ws.isAlive = true;\n });\n\n pingTimer = setInterval(() =>\n {\n if (ws.readyState !== 1) return;\n\n if (ws.isAlive === false)\n {\n // No pong since the previous tick — drop the dead/half-open socket\n ws.terminate();\n\n return;\n }\n\n ws.isAlive = false;\n ws.ping();\n }, pingInterval);\n }\n\n // ── 9. Send connected ack ──\n connection.send('__connected', {\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n });\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction parseURL(req: any): URL | null\n{\n try\n {\n return new URL(req.url ?? '/', 'ws://localhost');\n }\n catch\n {\n return null;\n }\n}\n\n/**\n * Resolve subject from token\n * - undefined: no auth required\n * - false: token param missing (when required)\n * - null: token invalid/expired\n * - string: authenticated subject\n */\nasync function resolveSubject(\n url: URL,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = url.searchParams.get('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\nfunction parseRequestedEvents(url: URL, validEventNames: string[]): string[]\n{\n const eventsParam = url.searchParams.get('events');\n if (!eventsParam)\n {\n return [];\n }\n\n return eventsParam\n .split(',')\n .map(e => e.trim())\n .filter(e => validEventNames.includes(e));\n}\n\nasync function resolveAllowedEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: WSHandlerAuthConfig,\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n return allowed.length === 0 ? null : allowed;\n}\n\n/**\n * Send a JSON frame with backpressure protection. If the socket's outbound\n * buffer is already past the cap, the consumer is too slow — close the\n * connection (1013) instead of buffering more and risking OOM. The client\n * reconnects and re-subscribes.\n */\nexport function safeSend(ws: any, frame: unknown, maxBufferedBytes: number): void\n{\n if (ws.readyState !== 1) return;\n\n if (ws.bufferedAmount > maxBufferedBytes)\n {\n ws.close(1013, 'Send buffer overflow');\n\n return;\n }\n\n try\n {\n ws.send(JSON.stringify(frame));\n }\n catch\n {\n // Socket closed between the readyState check and send — ignore\n }\n}\n\nfunction createConnection(ws: any, maxBufferedBytes: number): WSRawConnection\n{\n return {\n send: (type, payload) => safeSend(ws, { type, data: payload }, maxBufferedBytes),\n close: (code, reason) => ws.close(code, reason),\n };\n}\n\nfunction subscribeEvents(\n ws: any,\n router: WSRouterDef<any, any>,\n allowedEvents: string[],\n subject: string | undefined,\n authConfig: WSHandlerAuthConfig | undefined,\n maxBufferedBytes: number,\n): (() => void)[]\n{\n const unsubscribes: (() => void)[] = [];\n\n for (const eventName of allowedEvents)\n {\n const eventDef = router.events[eventName];\n if (!eventDef) continue;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (ws.readyState !== 1) return;\n\n if (subject && authConfig?.filter?.[eventName])\n {\n if (!authConfig.filter[eventName](subject, payload)) return;\n }\n\n safeSend(ws, { type: eventName, data: payload }, maxBufferedBytes);\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return unsubscribes;\n}\n\nasync function onClientMessage(\n data: Buffer | string,\n router: WSRouterDef<any, any>,\n connection: WSRawConnection,\n subject: string | undefined,\n): Promise<void>\n{\n let message: { type?: string; data?: unknown };\n\n try\n {\n message = JSON.parse(data.toString());\n }\n catch\n {\n return;\n }\n\n const { type, data: payload } = message;\n if (!type) return;\n\n const handler = router.messages[type];\n if (!handler) return;\n\n try\n {\n await handler({ payload, subject, ws: connection });\n }\n catch (err)\n {\n wsLogger.error(`WebSocket message handler error: ${type}`, err as Error);\n }\n}\n\n// ============================================================================\n// Dynamic import for optional 'ws' dependency\n// ============================================================================\n\n/**\n * Resolve the WebSocketServer constructor from the 'ws' module object.\n *\n * ws is a dual package, so the module shape depends on the loader path:\n * - ESM (exports.import → wrapper.mjs): WebSocketServer is a named export;\n * the default export is the bare WebSocket class with no properties on it.\n * - CJS (require interop): module.exports = WebSocket with\n * WebSocket.WebSocketServer attached, surfaced under .default.\n * Named exports must win — checking .default first crashes on the ESM path.\n */\nexport function resolveWSServerCtor(mod: any): any\n{\n return mod.WebSocketServer ?? mod.Server\n ?? mod.default?.WebSocketServer ?? mod.default?.Server;\n}\n\nasync function loadWSServer(): Promise<any>\n{\n let mod: any;\n\n try\n {\n mod = await import('ws');\n }\n catch\n {\n throw new Error(\n '@spfn/core WebSocket support requires the \"ws\" package.\\n' +\n 'Install it with: pnpm add ws',\n );\n }\n\n const WSS = resolveWSServerCtor(mod);\n\n if (typeof WSS !== 'function')\n {\n throw new Error(\n 'WebSocketServer not found in ws module. ' +\n 'Ensure ws@^8 is installed: pnpm add ws',\n );\n }\n\n return WSS;\n}\n","/**\n * WebSocket Module\n *\n * Type-safe WebSocket server with event-based pub/sub and bidirectional messaging.\n *\n * @example Server setup\n * ```typescript\n * // src/server/ws.ts\n * import { defineWSRouter } from '@spfn/core/event/ws';\n * import { defineEvent } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userUpdated = defineEvent('user.updated', Type.Object({ userId: Type.String() }));\n * const notification = defineEvent('notification', Type.Object({ message: Type.String() }));\n *\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * },\n * });\n *\n * export type WSRouter = typeof wsRouter;\n *\n * // server.config.ts\n * defineServerConfig()\n * .websockets(wsRouter)\n * .build();\n * ```\n *\n * @example Client usage\n * ```typescript\n * import { createWSClient } from '@spfn/core/event/ws/client';\n * import type { WSRouter } from '@/server/ws';\n *\n * const client = createWSClient<WSRouter>();\n *\n * client.subscribe({\n * events: ['userUpdated', 'notification'],\n * handlers: {\n * userUpdated: ({ userId }) => console.log(userId),\n * notification: ({ message }) => console.log(message),\n * },\n * });\n *\n * client.send('ping', {});\n * ```\n */\n\nimport type { EventDef } from '../types';\nimport type { WSRouterDef, WSMessageHandlers } from './types';\n\nexport { attachWSHandler } from './handler';\nexport type {\n WSRouterDef,\n WSHandlerConfig,\n WSAuthConfig,\n WSHandlerAuthConfig,\n WSMessageContext,\n WSMessageHandlerFn,\n WSMessageHandlers,\n WSRawConnection,\n WSClientConfig,\n WSConnectionState,\n WSEventHandlers,\n WSSubscribeOptions,\n WSUnsubscribe,\n} from './types';\n\n/**\n * Define a WebSocket router\n *\n * Combines server→client event push with client→server message handlers.\n *\n * @example\n * ```typescript\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * 'chat.send': ({ payload, subject }) => handleChat(payload, subject),\n * },\n * });\n * ```\n */\nexport function defineWSRouter<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers = WSMessageHandlers,\n>(def: {\n events: TEvents;\n messages?: TMessages;\n}): WSRouterDef<TEvents, TMessages>\n{\n return {\n events: def.events,\n eventNames: Object.keys(def.events) as (keyof TEvents)[],\n messages: (def.messages ?? {}) as TMessages,\n _types: {} as WSRouterDef<TEvents, TMessages>['_types'],\n };\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { a as JobOptions, C as CompensateHandler, b as JobHandler, c as JobDef, d as JobRouterEntry, J as JobRouter } from '../boss-DI1r4kTS.js';
2
- export { k as BossConfig, B as BossOptions, I as InferJobInput, f as InferJobOutput, e as JobSendOptions, g as getBoss, i as initBoss, h as isBossRunning, j as shouldClearOnStart, s as stopBoss } from '../boss-DI1r4kTS.js';
1
+ import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry, e as JobQueuePolicy } from '../boss-BiDCbbAQ.js';
2
+ export { B as BossConfig, f as BossOptions, I as InferJobInput, g as InferJobOutput, h as JobSendOptions, i as getBoss, j as initBoss, k as isBossRunning, s as shouldClearOnStart, l as stopBoss } from '../boss-BiDCbbAQ.js';
3
3
  import * as _sinclair_typebox from '@sinclair/typebox';
4
4
  import { Static } from '@sinclair/typebox';
5
5
  import { EventDef, InferEventPayload } from '@spfn/core/event';
@@ -168,11 +168,56 @@ declare function isJobRouter(value: unknown): value is JobRouter<any>;
168
168
  * ```
169
169
  */
170
170
  declare function defineJobRouter<TJobs extends Record<string, JobRouterEntry>>(jobs: TJobs): JobRouter<TJobs>;
171
+ /**
172
+ * Merge two job routers into a new one
173
+ *
174
+ * `defineServerConfig().jobs()` may be called once per domain router, the way
175
+ * `.routes()` is; each call merges into what is already registered instead of
176
+ * replacing it. Keys are the router's own namespace, so a key present in both
177
+ * routers is a mistake the app cannot recover from silently — one of the two
178
+ * would disappear.
179
+ *
180
+ * Neither input is mutated: a fresh router is returned.
181
+ *
182
+ * @throws if a top-level key exists in both routers
183
+ */
184
+ declare function mergeJobRouters(base: JobRouter<any>, extra: JobRouter<any>): JobRouter<any>;
171
185
  /**
172
186
  * Collect all JobDefs from a JobRouter (including nested)
173
187
  */
174
188
  declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<any>[];
175
189
 
190
+ /**
191
+ * Queue Policy Resolution
192
+ *
193
+ * Lives apart from register-jobs.ts and job-builder.ts so both can read the
194
+ * policy of a job definition without importing each other.
195
+ */
196
+
197
+ /**
198
+ * The part of a job definition that decides its queue policy
199
+ *
200
+ * `JobDef` satisfies this; the builder can also pass its own fields before the
201
+ * definition object exists.
202
+ */
203
+ interface QueuePolicySource {
204
+ readonly runOnce?: boolean;
205
+ readonly options?: JobOptions;
206
+ }
207
+ /**
208
+ * Resolve the pg-boss queue policy for a job definition
209
+ *
210
+ * An explicit `.options({ policy })` always wins. Otherwise a job that asks
211
+ * for deduplication — `.runOnce()` or an `options.singletonKey` — gets
212
+ * `exclusive`, the only policy under which a second enqueue is dropped while
213
+ * the first is still pending or running.
214
+ *
215
+ * Everything else stays `standard`: `exclusive` without a `singletonKey`
216
+ * deduplicates on `COALESCE(singleton_key, '')` and would collapse a plain
217
+ * queue to a single pending job.
218
+ */
219
+ declare function resolveQueuePolicy(job: QueuePolicySource): JobQueuePolicy;
220
+
176
221
  /**
177
222
  * Job Registration
178
223
  *
@@ -215,4 +260,4 @@ declare function collectJobs(router: JobRouter<any>, prefix?: string): JobDef<an
215
260
  */
216
261
  declare function registerJobs(router: JobRouter<any>): Promise<void>;
217
262
 
218
- export { CompensateHandler, JobDef, JobHandler, JobOptions, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, registerJobs };
263
+ export { CompensateHandler, JobDef, JobHandler, JobOptions, JobQueuePolicy, JobRouter, JobRouterEntry, collectJobs, defineJobRouter, isJobDef, isJobRouter, job, mergeJobRouters, registerJobs, resolveQueuePolicy };
package/dist/job/index.js CHANGED
@@ -1,60 +1,122 @@
1
- import PgBoss from 'pg-boss';
2
1
  import { logger } from '@spfn/core/logger';
2
+ import PgBoss from 'pg-boss';
3
+ import { env } from '@spfn/core/config';
3
4
 
4
- // src/job/boss.ts
5
+ // src/job/job-builder.ts
5
6
  var jobLogger = logger.child("@spfn/core:job");
6
- var bossInstance = null;
7
- var bossConfig = null;
7
+ function requiresSSLWithoutVerification(connectionString) {
8
+ try {
9
+ const url = new URL(connectionString);
10
+ const sslmode = url.searchParams.get("sslmode");
11
+ return sslmode === "require" || sslmode === "prefer";
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+ function stripSslModeFromUrl(connectionString) {
17
+ const url = new URL(connectionString);
18
+ url.searchParams.delete("sslmode");
19
+ return url.toString();
20
+ }
21
+ var BOSS_KEY = Symbol.for("spfn:boss-instance");
22
+ var CONFIG_KEY = Symbol.for("spfn:boss-config");
23
+ var g = globalThis;
24
+ function getBossInstance() {
25
+ return g[BOSS_KEY] ?? null;
26
+ }
27
+ function setBossInstance(instance) {
28
+ g[BOSS_KEY] = instance;
29
+ }
30
+ function getBossConfig() {
31
+ return g[CONFIG_KEY] ?? null;
32
+ }
33
+ function setBossConfig(config) {
34
+ g[CONFIG_KEY] = config;
35
+ }
8
36
  async function initBoss(options) {
9
- if (bossInstance) {
37
+ const existing = getBossInstance();
38
+ if (existing) {
10
39
  jobLogger.warn("pg-boss already initialized, returning existing instance");
11
- return bossInstance;
40
+ return existing;
12
41
  }
13
42
  jobLogger.info("Initializing pg-boss...");
14
- bossConfig = options;
43
+ setBossConfig(options);
44
+ const needsSSL = requiresSSLWithoutVerification(options.connectionString);
15
45
  const pgBossOptions = {
16
- connectionString: options.connectionString,
46
+ // pg 드라이버가 URL의 sslmode=require를 verify-full로 해석해서
47
+ // ssl 옵션을 무시하므로, URL에서 sslmode를 빼고 ssl 객체만 전달
48
+ connectionString: needsSSL ? stripSslModeFromUrl(options.connectionString) : options.connectionString,
17
49
  schema: options.schema ?? "spfn_queue",
18
50
  maintenanceIntervalSeconds: options.maintenanceIntervalSeconds ?? 120
19
51
  };
52
+ if (needsSSL) {
53
+ pgBossOptions.ssl = { rejectUnauthorized: false };
54
+ }
20
55
  if (options.monitorIntervalSeconds !== void 0 && options.monitorIntervalSeconds >= 1) {
21
56
  pgBossOptions.monitorIntervalSeconds = options.monitorIntervalSeconds;
22
57
  }
23
- bossInstance = new PgBoss(pgBossOptions);
24
- bossInstance.on("error", (error) => {
58
+ const boss = new PgBoss(pgBossOptions);
59
+ boss.on("error", (error) => {
25
60
  jobLogger.error("pg-boss error:", error);
26
61
  });
27
- await bossInstance.start();
62
+ await boss.start();
63
+ setBossInstance(boss);
28
64
  jobLogger.info("pg-boss started successfully");
29
- return bossInstance;
65
+ return boss;
30
66
  }
31
67
  function getBoss() {
32
- return bossInstance;
68
+ return getBossInstance();
33
69
  }
34
70
  async function stopBoss() {
35
- if (!bossInstance) {
71
+ const boss = getBossInstance();
72
+ if (!boss) {
36
73
  return;
37
74
  }
38
75
  jobLogger.info("Stopping pg-boss...");
39
76
  try {
40
- await bossInstance.stop({ graceful: true, timeout: 3e4 });
77
+ await boss.stop({ graceful: true, timeout: 3e4 });
41
78
  jobLogger.info("pg-boss stopped gracefully");
42
79
  } catch (error) {
43
80
  jobLogger.error("Error stopping pg-boss:", error);
44
81
  throw error;
45
82
  } finally {
46
- bossInstance = null;
47
- bossConfig = null;
83
+ setBossInstance(null);
84
+ setBossConfig(null);
48
85
  }
49
86
  }
50
87
  function isBossRunning() {
51
- return bossInstance !== null;
88
+ return getBossInstance() !== null;
52
89
  }
53
90
  function shouldClearOnStart() {
54
- return bossConfig?.clearOnStart ?? false;
91
+ return getBossConfig()?.clearOnStart ?? false;
92
+ }
93
+ function shouldSweepOrphanSchedules() {
94
+ return getBossConfig()?.sweepOrphanSchedules ?? false;
95
+ }
96
+
97
+ // src/job/queue-policy.ts
98
+ function resolveQueuePolicy(job2) {
99
+ if (job2.options?.policy) {
100
+ return job2.options.policy;
101
+ }
102
+ if (job2.runOnce || job2.options?.singletonKey) {
103
+ return "exclusive";
104
+ }
105
+ return "standard";
55
106
  }
56
107
 
57
108
  // src/job/job-builder.ts
109
+ var jobLogger2 = logger.child("@spfn/core:job");
110
+ var warnedIgnoredSingletonKeys = /* @__PURE__ */ new Set();
111
+ function warnIfSingletonKeyIgnored(name, source) {
112
+ if (resolveQueuePolicy(source) !== "standard" || warnedIgnoredSingletonKeys.has(name)) {
113
+ return;
114
+ }
115
+ warnedIgnoredSingletonKeys.add(name);
116
+ jobLogger2.warn(
117
+ `[Job:${name}] singletonKey ignored: queue policy is "standard"; set .options({ policy: 'exclusive' }) (or another non-standard policy) to deduplicate`
118
+ );
119
+ }
58
120
  function buildPgBossOptions(defaults, sendOptions) {
59
121
  const options = {};
60
122
  if (sendOptions?.startAfter) {
@@ -215,6 +277,9 @@ var JobBuilder = class _JobBuilder {
215
277
  );
216
278
  }
217
279
  const [input, sendOptions] = inputSchema ? [inputOrOptions, maybeOptions] : [void 0, inputOrOptions];
280
+ if (sendOptions?.singletonKey) {
281
+ warnIfSingletonKeyIgnored(name, { runOnce, options });
282
+ }
218
283
  return await boss.send(
219
284
  name,
220
285
  input ?? {},
@@ -228,6 +293,25 @@ var JobBuilder = class _JobBuilder {
228
293
  return await handler();
229
294
  }
230
295
  };
296
+ const sendBatch = async (inputsOrOptions, maybeOptions) => {
297
+ const boss = getBoss();
298
+ if (!boss) {
299
+ throw new Error(
300
+ `[Job:${name}] pg-boss not initialized. Ensure jobs are registered with defineServerConfig().jobs()`
301
+ );
302
+ }
303
+ const [inputs, sendOptions] = inputSchema ? [inputsOrOptions, maybeOptions] : [void 0, inputsOrOptions];
304
+ if (sendOptions?.singletonKey) {
305
+ warnIfSingletonKeyIgnored(name, { runOnce, options });
306
+ }
307
+ const pgBossOptions = buildPgBossOptions(options, sendOptions);
308
+ const jobs = (inputs ?? [{}]).map((data) => ({
309
+ name,
310
+ ...pgBossOptions,
311
+ data
312
+ }));
313
+ await boss.insert(name, jobs);
314
+ };
231
315
  return {
232
316
  name,
233
317
  inputSchema,
@@ -240,6 +324,7 @@ var JobBuilder = class _JobBuilder {
240
324
  handler,
241
325
  compensate,
242
326
  send,
327
+ sendBatch,
243
328
  run,
244
329
  _input: void 0,
245
330
  _output: void 0
@@ -263,19 +348,32 @@ function defineJobRouter(jobs) {
263
348
  _jobs: jobs
264
349
  };
265
350
  }
266
- function collectJobs(router, prefix = "") {
267
- const jobs = [];
351
+ function mergeJobRouters(base, extra) {
352
+ for (const key of Object.keys(extra.jobs)) {
353
+ if (key in base.jobs) {
354
+ throw new Error(
355
+ `jobs(): key "${key}" is already registered by an earlier .jobs() call; rename it or nest it under a domain router`
356
+ );
357
+ }
358
+ }
359
+ return defineJobRouter({ ...base.jobs, ...extra.jobs });
360
+ }
361
+ function collectJobEntries(router, prefix = "") {
362
+ const entries = [];
268
363
  for (const [key, value] of Object.entries(router.jobs)) {
269
- const name = prefix ? `${prefix}.${key}` : key;
364
+ const path = prefix ? `${prefix}.${key}` : key;
270
365
  if (isJobRouter(value)) {
271
- jobs.push(...collectJobs(value, name));
366
+ entries.push(...collectJobEntries(value, path));
272
367
  } else if (isJobDef(value)) {
273
- jobs.push(value);
368
+ entries.push({ key: path, job: value });
274
369
  }
275
370
  }
276
- return jobs;
371
+ return entries;
277
372
  }
278
- var jobLogger2 = logger.child("@spfn/core:job");
373
+ function collectJobs(router, prefix = "") {
374
+ return collectJobEntries(router, prefix).map((entry) => entry.job);
375
+ }
376
+ var jobLogger3 = logger.child("@spfn/core:job");
279
377
  function getEventQueueName(eventName) {
280
378
  return `event:${eventName}`;
281
379
  }
@@ -283,6 +381,9 @@ function getDefaultJobOptions(options) {
283
381
  return {
284
382
  retryLimit: options?.retryLimit ?? 3,
285
383
  retryDelay: options?.retryDelay ?? 1e3,
384
+ // Exponential backoff by default — a failed cohort (e.g. a provider 429)
385
+ // would otherwise all retry at the same fixed offset (thundering herd).
386
+ retryBackoff: options?.retryBackoff ?? true,
286
387
  expireInSeconds: options?.expireInSeconds ?? 300
287
388
  };
288
389
  }
@@ -293,11 +394,12 @@ async function registerJobs(router) {
293
394
  "pg-boss not initialized. Call initBoss() before registerJobs()"
294
395
  );
295
396
  }
397
+ assertUniqueJobNames(router);
296
398
  const jobs = collectJobs(router);
297
399
  const clearOnStart = shouldClearOnStart();
298
- jobLogger2.info(`Registering ${jobs.length} job(s)...`);
400
+ jobLogger3.info(`Registering ${jobs.length} job(s)...`);
299
401
  if (clearOnStart) {
300
- jobLogger2.info("Clearing existing jobs before registration...");
402
+ jobLogger3.info("Clearing existing jobs before registration...");
301
403
  for (const job2 of jobs) {
302
404
  await boss.deleteAllJobs(job2.name);
303
405
  if (job2.subscribedEvent) {
@@ -305,46 +407,134 @@ async function registerJobs(router) {
305
407
  await boss.deleteAllJobs(eventQueue);
306
408
  }
307
409
  }
308
- jobLogger2.info("Existing jobs cleared");
410
+ jobLogger3.info("Existing jobs cleared");
309
411
  }
412
+ await Promise.all(jobs.map((job2) => registerJob(job2)));
310
413
  for (const job2 of jobs) {
311
- await registerJob(job2);
414
+ if (job2.cronExpression) {
415
+ registeredCronNames.add(job2.name);
416
+ }
417
+ }
418
+ if (shouldSweepOrphanSchedules()) {
419
+ await sweepOrphanSchedules(boss);
420
+ }
421
+ jobLogger3.info("All jobs registered successfully");
422
+ }
423
+ function assertUniqueJobNames(router) {
424
+ const keysByName = /* @__PURE__ */ new Map();
425
+ for (const { key, job: job2 } of collectJobEntries(router)) {
426
+ const existingKey = keysByName.get(job2.name);
427
+ if (existingKey) {
428
+ throw new Error(
429
+ `registerJobs(): job name "${job2.name}" is defined twice (keys "${existingKey}" and "${key}"); job names are pg-boss queue names and must be unique`
430
+ );
431
+ }
432
+ keysByName.set(job2.name, key);
433
+ }
434
+ }
435
+ var registeredCronNames = /* @__PURE__ */ new Set();
436
+ async function sweepOrphanSchedules(boss) {
437
+ if (registeredCronNames.size === 0) {
438
+ jobLogger3.debug("No cron jobs declared; skipping orphan schedule sweep");
439
+ return;
440
+ }
441
+ try {
442
+ const schedules = await boss.getSchedules();
443
+ const orphans = schedules.filter((schedule) => !registeredCronNames.has(schedule.name));
444
+ if (orphans.length === 0) {
445
+ jobLogger3.debug("No orphan schedules found");
446
+ return;
447
+ }
448
+ await Promise.all(orphans.map(async (orphan) => {
449
+ try {
450
+ await boss.unschedule(orphan.name, orphan.key);
451
+ jobLogger3.info(`Removed orphan schedule: ${orphan.name}`);
452
+ } catch (error) {
453
+ jobLogger3.error(`Failed to remove orphan schedule: ${orphan.name}`, {
454
+ error: error instanceof Error ? error.message : String(error)
455
+ });
456
+ }
457
+ }));
458
+ } catch (error) {
459
+ jobLogger3.error("Orphan schedule sweep failed", {
460
+ error: error instanceof Error ? error.message : String(error)
461
+ });
312
462
  }
313
- jobLogger2.info("All jobs registered successfully");
314
463
  }
315
- async function ensureQueue(boss, queueName) {
316
- await boss.createQueue(queueName);
464
+ async function ensureQueue(boss, queueName, policy) {
465
+ await boss.createQueue(queueName, { policy });
466
+ const existing = await boss.getQueue(queueName);
467
+ if (!existing || existing.policy === policy) {
468
+ return;
469
+ }
470
+ if (shouldClearOnStart()) {
471
+ await boss.deleteQueue(queueName);
472
+ await boss.createQueue(queueName, { policy });
473
+ jobLogger3.info(`[Queue:${queueName}] policy ${existing.policy} \u2192 ${policy} (recreated: clearOnStart)`);
474
+ return;
475
+ }
476
+ jobLogger3.error(
477
+ `[Queue:${queueName}] policy mismatch: the queue exists as "${existing.policy}" but its jobs require "${policy}". Delete the queue in a quiet window (getBoss().deleteQueue('${queueName}') drops its job rows) or set clearOnStart in development; until then singletonKey/runOnce on this queue do not deduplicate`
478
+ );
479
+ }
480
+ function createQueueEnsurer(boss, policy) {
481
+ const ensured = /* @__PURE__ */ new Set();
482
+ return async (queueName) => {
483
+ if (ensured.has(queueName)) {
484
+ return;
485
+ }
486
+ ensured.add(queueName);
487
+ await ensureQueue(boss, queueName, policy);
488
+ };
489
+ }
490
+ async function executeJobHandler(job2, pgBossJob) {
491
+ jobLogger3.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
492
+ const startTime = Date.now();
493
+ try {
494
+ if (job2.inputSchema) {
495
+ await job2.handler(pgBossJob.data);
496
+ } else {
497
+ await job2.handler();
498
+ }
499
+ const duration = Date.now() - startTime;
500
+ jobLogger3.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
501
+ jobId: pgBossJob.id,
502
+ duration
503
+ });
504
+ } catch (error) {
505
+ const duration = Date.now() - startTime;
506
+ jobLogger3.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
507
+ jobId: pgBossJob.id,
508
+ duration,
509
+ error: error instanceof Error ? error.message : String(error)
510
+ });
511
+ throw error;
512
+ }
317
513
  }
318
- async function registerWorker(boss, job2, queueName) {
319
- await ensureQueue(boss, queueName);
514
+ async function registerWorker(boss, job2, queueName, ensureQueueOnce) {
515
+ await ensureQueueOnce(queueName);
516
+ const batchSize = job2.options?.batchSize ?? 1;
517
+ const pollingIntervalSeconds = job2.options?.pollingIntervalSeconds ?? env.JOB_POLLING_INTERVAL_SECONDS;
320
518
  await boss.work(
321
519
  queueName,
322
- { batchSize: 1 },
323
- async (jobs) => {
324
- for (const pgBossJob of jobs) {
325
- jobLogger2.debug(`[Job:${job2.name}] Executing...`, { jobId: pgBossJob.id });
326
- const startTime = Date.now();
327
- try {
328
- if (job2.inputSchema) {
329
- await job2.handler(pgBossJob.data);
330
- } else {
331
- await job2.handler();
332
- }
333
- const duration = Date.now() - startTime;
334
- jobLogger2.info(`[Job:${job2.name}] Completed in ${duration}ms`, {
335
- jobId: pgBossJob.id,
336
- duration
337
- });
338
- } catch (error) {
339
- const duration = Date.now() - startTime;
340
- jobLogger2.error(`[Job:${job2.name}] Failed after ${duration}ms`, {
341
- jobId: pgBossJob.id,
342
- duration,
343
- error: error instanceof Error ? error.message : String(error)
344
- });
345
- throw error;
520
+ { batchSize, pollingIntervalSeconds },
521
+ async (pgBossJobs) => {
522
+ if (batchSize <= 1) {
523
+ await executeJobHandler(job2, pgBossJobs[0]);
524
+ return;
525
+ }
526
+ const results = await Promise.allSettled(
527
+ pgBossJobs.map((pgBossJob) => executeJobHandler(job2, pgBossJob))
528
+ );
529
+ const failedIds = [];
530
+ for (let i = 0; i < results.length; i++) {
531
+ if (results[i].status === "rejected") {
532
+ failedIds.push(pgBossJobs[i].id);
346
533
  }
347
534
  }
535
+ if (failedIds.length > 0) {
536
+ await boss.fail(queueName, failedIds);
537
+ }
348
538
  }
349
539
  );
350
540
  }
@@ -356,28 +546,28 @@ function connectEventToQueue(boss, job2, queueName) {
356
546
  eventDef._registerJobQueue(queueName, async (queue, payload) => {
357
547
  await boss.send(queue, payload, getDefaultJobOptions(job2.options));
358
548
  });
359
- jobLogger2.debug(`[Job:${job2.name}] Connected to event: ${job2.subscribedEvent}`);
549
+ jobLogger3.debug(`[Job:${job2.name}] Connected to event: ${job2.subscribedEvent}`);
360
550
  }
361
- async function registerCronSchedule(boss, job2) {
551
+ async function registerCronSchedule(boss, job2, ensureQueueOnce) {
362
552
  if (!job2.cronExpression) {
363
553
  return;
364
554
  }
365
- jobLogger2.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
366
- await ensureQueue(boss, job2.name);
555
+ jobLogger3.debug(`[Job:${job2.name}] Scheduling cron: ${job2.cronExpression}`);
556
+ await ensureQueueOnce(job2.name);
367
557
  await boss.schedule(
368
558
  job2.name,
369
559
  job2.cronExpression,
370
560
  {},
371
561
  getDefaultJobOptions(job2.options)
372
562
  );
373
- jobLogger2.info(`[Job:${job2.name}] Cron scheduled: ${job2.cronExpression}`);
563
+ jobLogger3.info(`[Job:${job2.name}] Cron scheduled: ${job2.cronExpression}`);
374
564
  }
375
- async function queueRunOnceJob(boss, job2) {
565
+ async function queueRunOnceJob(boss, job2, ensureQueueOnce) {
376
566
  if (!job2.runOnce) {
377
567
  return;
378
568
  }
379
- jobLogger2.debug(`[Job:${job2.name}] Queuing runOnce job`);
380
- await ensureQueue(boss, job2.name);
569
+ jobLogger3.debug(`[Job:${job2.name}] Queuing runOnce job`);
570
+ await ensureQueueOnce(job2.name);
381
571
  await boss.send(
382
572
  job2.name,
383
573
  {},
@@ -386,7 +576,7 @@ async function queueRunOnceJob(boss, job2) {
386
576
  singletonKey: `runOnce:${job2.name}`
387
577
  }
388
578
  );
389
- jobLogger2.info(`[Job:${job2.name}] runOnce job queued`);
579
+ jobLogger3.info(`[Job:${job2.name}] runOnce job queued`);
390
580
  }
391
581
  async function registerJob(job2) {
392
582
  const boss = getBoss();
@@ -394,17 +584,18 @@ async function registerJob(job2) {
394
584
  throw new Error("pg-boss not initialized");
395
585
  }
396
586
  const queueName = job2.subscribedEvent ? getEventQueueName(job2.subscribedEvent) : job2.name;
397
- jobLogger2.debug(`Registering job: ${job2.name}`, {
587
+ jobLogger3.debug(`Registering job: ${job2.name}`, {
398
588
  queueName,
399
589
  subscribedEvent: job2.subscribedEvent
400
590
  });
401
- await registerWorker(boss, job2, queueName);
591
+ const ensureQueueOnce = createQueueEnsurer(boss, resolveQueuePolicy(job2));
592
+ await registerWorker(boss, job2, queueName, ensureQueueOnce);
402
593
  connectEventToQueue(boss, job2, queueName);
403
- await registerCronSchedule(boss, job2);
404
- await queueRunOnceJob(boss, job2);
405
- jobLogger2.debug(`Job registered: ${job2.name}`);
594
+ await registerCronSchedule(boss, job2, ensureQueueOnce);
595
+ await queueRunOnceJob(boss, job2, ensureQueueOnce);
596
+ jobLogger3.debug(`Job registered: ${job2.name}`);
406
597
  }
407
598
 
408
- export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, registerJobs, shouldClearOnStart, stopBoss };
599
+ export { collectJobs, defineJobRouter, getBoss, initBoss, isBossRunning, isJobDef, isJobRouter, job, mergeJobRouters, registerJobs, resolveQueuePolicy, shouldClearOnStart, stopBoss };
409
600
  //# sourceMappingURL=index.js.map
410
601
  //# sourceMappingURL=index.js.map