@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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/cache/cache-factory.ts","../../src/cache/cache-manager.ts"],"names":["cacheLogger","logger"],"mappings":";;;AAOA,IAAM,WAAA,GAAc,MAAA,CAAO,KAAA,CAAM,kBAAkB,CAAA;AAYnD,SAAS,cAAA,GACT;AACI,EAAA,OAAO,CAAC,EACJ,OAAA,CAAQ,GAAA,CAAI,aACZ,OAAA,CAAQ,GAAA,CAAI,eAAA,IACZ,OAAA,CAAQ,IAAI,cAAA,IACZ,OAAA,CAAQ,GAAA,CAAI,oBAAA,IACZ,QAAQ,GAAA,CAAI,mBAAA,CAAA;AAEpB;AAKA,SAAS,YAAA,CACL,aACA,GAAA,EAEJ;AACI,EAAA,MAAM,UAAwB,EAAC;AAG/B,EAAA,IAAI,GAAA,CAAI,UAAA,CAAW,WAAW,CAAA,EAC9B;AACI,IAAA,OAAA,CAAQ,GAAA,GAAM;AAAA,MACV,kBAAA,EAAoB,OAAA,CAAQ,GAAA,CAAI,6BAAA,KAAkC;AAAA,KACtE;AAAA,EACJ;AAEA,EAAA,OAAO,IAAI,WAAA,CAAY,GAAA,EAAK,OAAO,CAAA;AACvC;AAiCA,eAAsB,kBAAA,GACtB;AAEI,EAAA,IAAI,CAAC,gBAAe,EACpB;AACI,IAAA,WAAA,CAAY,KAAK,sDAAsD,CAAA;AACvE,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C;AAEA,EAAA,IACA;AAEI,IAAA,MAAM,OAAA,GAAU,MAAM,OAAO,SAAS,CAAA;AACtC,IAAA,MAAM,cAAc,OAAA,CAAQ,OAAA;AAG5B,IAAA,MAAM,SAAA,GAAY,QAAQ,GAAA,CAAI,SAAA;AAC9B,IAAA,MAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,eAAA;AAC7B,IAAA,MAAM,OAAA,GAAU,QAAQ,GAAA,CAAI,cAAA;AAC5B,IAAA,MAAM,YAAA,GAAe,QAAQ,GAAA,CAAI,mBAAA;AACjC,IAAA,MAAM,aAAA,GAAgB,QAAQ,GAAA,CAAI,oBAAA;AAClC,IAAA,MAAM,UAAA,GAAa,QAAQ,GAAA,CAAI,iBAAA;AAC/B,IAAA,MAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,cAAA;AAG7B,IAAA,IAAI,aAAa,CAAC,QAAA,IAAY,CAAC,OAAA,IAAW,CAAC,YAAA,EAC3C;AACI,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,WAAA,EAAa,SAAS,CAAA;AAClD,MAAA,WAAA,CAAY,KAAA,CAAM,iCAAiC,EAAE,GAAA,EAAK,UAAU,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA,EAAG,CAAA;AAClG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,IAAI,YAAY,OAAA,EAChB;AACI,MAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,WAAA,EAAa,QAAQ,CAAA;AAChD,MAAA,MAAM,IAAA,GAAO,YAAA,CAAa,WAAA,EAAa,OAAO,CAAA;AAC9C,MAAA,WAAA,CAAY,MAAM,wCAAwC,CAAA;AAC1D,MAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AAAA,IACzB;AAGA,IAAA,IAAI,iBAAiB,UAAA,EACrB;AACI,MAAA,MAAM,YAAY,aAAA,CAAc,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAChD;AACI,QAAA,MAAM,CAAC,UAAU,IAAI,CAAA,GAAI,KAAK,IAAA,EAAK,CAAE,MAAM,GAAG,CAAA;AAC9C,QAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,MAAM,MAAA,CAAO,IAAI,KAAK,KAAA,EAAM;AAAA,MACzD,CAAC,CAAA;AAED,MAAA,MAAM,OAAA,GAAwB;AAAA,QAC1B,SAAA;AAAA,QACA,IAAA,EAAM,UAAA;AAAA,QACN;AAAA,OACJ;AAEA,MAAA,MAAM,MAAA,GAAS,IAAI,WAAA,CAAY,OAAO,CAAA;AACtC,MAAA,WAAA,CAAY,MAAM,iCAAA,EAAmC,EAAE,YAAY,SAAA,EAAW,SAAA,CAAU,QAAQ,CAAA;AAChG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,IAAI,YAAA,EACJ;AACI,MAAA,MAAM,QAAQ,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAC3C;AACI,QAAA,MAAM,CAAC,MAAM,IAAI,CAAA,GAAI,KAAK,IAAA,EAAK,CAAE,MAAM,GAAG,CAAA;AAC1C,QAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAA,CAAO,IAAI,KAAK,IAAA,EAAK;AAAA,MAC9C,CAAC,CAAA;AAED,MAAA,MAAM,cAAA,GAAiC;AAAA,QACnC,YAAA,EAAc;AAAA,UACV;AAAA;AACJ,OACJ;AAEA,MAAA,MAAM,OAAA,GAAU,IAAI,WAAA,CAAY,OAAA,CAAQ,OAAO,cAAc,CAAA;AAC7D,MAAA,WAAA,CAAY,MAAM,gCAAA,EAAkC,EAAE,KAAA,EAAO,KAAA,CAAM,QAAQ,CAAA;AAC3E,MAAA,OAAO,EAAE,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM,OAAA,EAAQ;AAAA,IAC3C;AAGA,IAAA,IAAI,SAAA,EACJ;AACI,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,WAAA,EAAa,SAAS,CAAA;AAClD,MAAA,WAAA,CAAY,KAAA,CAAM,qCAAqC,EAAE,GAAA,EAAK,UAAU,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA,EAAG,CAAA;AACtG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,WAAA,CAAY,KAAK,4DAA4D,CAAA;AAC7E,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C,SACO,KAAA,EACP;AACI,IAAA,IAAI,iBAAiB,KAAA,EACrB;AAEI,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAA,CAAS,oBAAoB,CAAA,EAC/C;AACI,QAAA,WAAA,CAAY,IAAA;AAAA,UACR,oCAAA;AAAA,UACA,KAAA;AAAA,UACA;AAAA,YACI,UAAA,EAAY,uDAAA;AAAA,YACZ,IAAA,EAAM;AAAA;AACV,SACJ;AAAA,MACJ,CAAA,MAEA;AACI,QAAA,WAAA,CAAY,IAAA;AAAA,UACR,+BAAA;AAAA,UACA,KAAA;AAAA,UACA,EAAE,MAAM,UAAA;AAAW,SACvB;AAAA,MACJ;AAAA,IACJ,CAAA,MAEA;AACI,MAAA,WAAA,CAAY,IAAA;AAAA,QACR,+BAAA;AAAA,QACA,EAAE,KAAA,EAAO,MAAA,CAAO,KAAK,CAAA,EAAG,MAAM,UAAA;AAAW,OAC7C;AAAA,IACJ;AACA,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C;AACJ;AAMA,eAAsB,wBAAA,GACtB;AACI,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,kBAAA,EAAmB;AAC3C,EAAA,OAAO,KAAA;AACX;AC9MA,IAAMA,YAAAA,GAAcC,MAAAA,CAAO,KAAA,CAAM,kBAAkB,CAAA;AAEnD,IAAI,aAAA;AACJ,IAAI,YAAA;AACJ,IAAI,UAAA,GAAa,KAAA;AAoBV,SAAS,QAAA,GAChB;AACI,EAAA,OAAO,aAAA;AACX;AAiBO,SAAS,YAAA,GAChB;AACI,EAAA,OAAO,YAAA,IAAgB,aAAA;AAC3B;AAeO,SAAS,eAAA,GAChB;AACI,EAAA,OAAO,UAAA;AACX;AAkBO,SAAS,QAAA,CACZ,OACA,IAAA,EAEJ;AACI,EAAA,aAAA,GAAgB,KAAA;AAChB,EAAA,YAAA,GAAe,IAAA,IAAQ,KAAA;AACvB,EAAA,UAAA,GAAa,CAAC,KAAA;AAClB;AA2BA,eAAsB,SAAA,GAKtB;AAEI,EAAA,IAAI,aAAA,EACJ;AACI,IAAA,OAAO,EAAE,KAAA,EAAO,aAAA,EAAe,IAAA,EAAM,YAAA,EAAc,UAAU,UAAA,EAAW;AAAA,EAC5E;AAGA,EAAA,MAAM,EAAE,KAAA,EAAO,IAAA,EAAK,GAAI,MAAM,kBAAA,EAAmB;AAEjD,EAAA,IAAI,KAAA,EACJ;AACI,IAAA,IACA;AAEI,MAAA,MAAM,MAAM,IAAA,EAAK;AAGjB,MAAA,IAAI,IAAA,IAAQ,SAAS,KAAA,EACrB;AACI,QAAA,MAAM,KAAK,IAAA,EAAK;AAAA,MACpB;AAEA,MAAA,aAAA,GAAgB,KAAA;AAChB,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,UAAA,GAAa,KAAA;AAEb,MAAA,MAAM,UAAA,GAAa,QAAQ,IAAA,KAAS,KAAA;AACpC,MAAAD,YAAAA,CAAY,IAAA;AAAA,QACR,aACM,kCAAA,GACA,iBAAA;AAAA,QACN,EAAE,MAAM,SAAA;AAAU,OACtB;AAEA,MAAA,OAAO,EAAE,KAAA,EAAO,aAAA,EAAe,IAAA,EAAM,YAAA,EAAc,UAAU,KAAA,EAAM;AAAA,IACvE,SACO,KAAA,EACP;AACI,MAAAA,YAAAA,CAAY,KAAA;AAAA,QACR,oDAAA;AAAA,QACA,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,QACxD,EAAE,MAAM,UAAA;AAAW,OACvB;AAGA,MAAA,IACA;AACI,QAAA,MAAM,MAAM,IAAA,EAAK;AACjB,QAAA,IAAI,IAAA,IAAQ,SAAS,KAAA,EACrB;AACI,UAAA,MAAM,KAAK,IAAA,EAAK;AAAA,QACpB;AAAA,MACJ,CAAA,CAAA,MAEA;AAAA,MAEA;AAEA,MAAA,UAAA,GAAa,IAAA;AACb,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAW,UAAU,IAAA,EAAK;AAAA,IAC/D;AAAA,EACJ;AAGA,EAAA,UAAA,GAAa,IAAA;AACb,EAAAA,aAAY,IAAA,CAAK,4DAAA,EAA8D,EAAE,IAAA,EAAM,YAAY,CAAA;AACnG,EAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAW,UAAU,IAAA,EAAK;AAC/D;AAaA,eAAsB,UAAA,GACtB;AACI,EAAA,IAAI,UAAA,EACJ;AACI,IAAAA,YAAAA,CAAY,MAAM,0CAA0C,CAAA;AAC5D,IAAA;AAAA,EACJ;AAEA,EAAA,MAAM,gBAAoC,EAAC;AAE3C,EAAA,IAAI,aAAA,EACJ;AACI,IAAA,aAAA,CAAc,IAAA;AAAA,MACV,aAAA,CAAc,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAC5B;AACI,QAAAA,YAAAA,CAAY,KAAA,CAAM,oCAAA,EAAsC,GAAG,CAAA;AAAA,MAC/D,CAAC;AAAA,KACL;AAAA,EACJ;AAEA,EAAA,IAAI,YAAA,IAAgB,iBAAiB,aAAA,EACrC;AACI,IAAA,aAAA,CAAc,IAAA;AAAA,MACV,YAAA,CAAa,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAC3B;AACI,QAAAA,YAAAA,CAAY,KAAA,CAAM,mCAAA,EAAqC,GAAG,CAAA;AAAA,MAC9D,CAAC;AAAA,KACL;AAAA,EACJ;AAEA,EAAA,MAAM,OAAA,CAAQ,IAAI,aAAa,CAAA;AAE/B,EAAA,aAAA,GAAgB,MAAA;AAChB,EAAA,YAAA,GAAe,MAAA;AACf,EAAA,UAAA,GAAa,IAAA;AAEb,EAAAA,aAAY,IAAA,CAAK,0BAAA,EAA4B,EAAE,IAAA,EAAM,YAAY,CAAA;AACrE;AAmBO,SAAS,YAAA,GAMhB;AACI,EAAA,OAAO;AAAA,IACH,QAAA,EAAU,CAAC,CAAC,aAAA;AAAA,IACZ,OAAA,EAAS,CAAC,CAAC,YAAA;AAAA,IACX,SAAA,EAAW,CAAC,EAAE,YAAA,IAAgB,YAAA,KAAiB,aAAA,CAAA;AAAA,IAC/C,QAAA,EAAU;AAAA,GACd;AACJ","file":"index.js","sourcesContent":["/**\n * Cache factory with automatic environment variable detection\n */\n\nimport type { Redis, Cluster, RedisOptions, ClusterOptions } from 'ioredis';\nimport { logger } from '@spfn/core/logger';\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\nexport interface CacheClients {\n /** Primary cache for writes (or both read/write if no replica) */\n write?: Redis | Cluster;\n /** Replica cache for reads (optional, falls back to write) */\n read?: Redis | Cluster;\n}\n\n/**\n * Check if any cache configuration exists in environment\n */\nfunction hasCacheConfig(): boolean\n{\n return !!(\n process.env.CACHE_URL ||\n process.env.CACHE_WRITE_URL ||\n process.env.CACHE_READ_URL ||\n process.env.CACHE_SENTINEL_HOSTS ||\n process.env.CACHE_CLUSTER_NODES\n );\n}\n\n/**\n * Create cache client with TLS support\n */\nfunction createClient(\n RedisClient: new (url: string, options?: RedisOptions) => Redis,\n url: string\n): Redis\n{\n const options: RedisOptions = {};\n\n // TLS support for secure connections\n if (url.startsWith('rediss://'))\n {\n options.tls = {\n rejectUnauthorized: process.env.CACHE_TLS_REJECT_UNAUTHORIZED !== 'false',\n };\n }\n\n return new RedisClient(url, options);\n}\n\n/**\n * Create cache client(s) from environment variables\n *\n * Supported patterns (priority order):\n * 1. Single instance: CACHE_URL\n * 2. Master-Replica: CACHE_WRITE_URL + CACHE_READ_URL\n * 3. Sentinel: CACHE_SENTINEL_HOSTS + CACHE_MASTER_NAME\n * 4. Cluster: CACHE_CLUSTER_NODES\n *\n * @returns Cache client(s) or undefined if no configuration found\n *\n * @example\n * ```bash\n * # Single (most common)\n * CACHE_URL=redis://localhost:6379\n * CACHE_URL=rediss://secure.cache.com:6380 # TLS\n *\n * # Master-Replica\n * CACHE_WRITE_URL=redis://master:6379\n * CACHE_READ_URL=redis://replica:6379\n *\n * # Sentinel\n * CACHE_SENTINEL_HOSTS=sentinel1:26379,sentinel2:26379\n * CACHE_MASTER_NAME=mymaster\n * CACHE_PASSWORD=secret\n *\n * # Cluster\n * CACHE_CLUSTER_NODES=node1:6379,node2:6379,node3:6379\n * CACHE_PASSWORD=secret\n * ```\n */\nexport async function createCacheFromEnv(): Promise<CacheClients>\n{\n // Quick exit if no cache config\n if (!hasCacheConfig())\n {\n cacheLogger.info('No cache configuration found - running without cache');\n return { write: undefined, read: undefined };\n }\n\n try\n {\n // Dynamic import to avoid bundling if not used\n const ioredis = await import('ioredis');\n const RedisClient = ioredis.default;\n\n // Get environment variables\n const singleUrl = process.env.CACHE_URL;\n const writeUrl = process.env.CACHE_WRITE_URL;\n const readUrl = process.env.CACHE_READ_URL;\n const clusterNodes = process.env.CACHE_CLUSTER_NODES;\n const sentinelHosts = process.env.CACHE_SENTINEL_HOSTS;\n const masterName = process.env.CACHE_MASTER_NAME;\n const password = process.env.CACHE_PASSWORD;\n\n // 1. Single instance (most common - highest priority)\n if (singleUrl && !writeUrl && !readUrl && !clusterNodes)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created single cache instance', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n return { write: client, read: client };\n }\n\n // 2. Master-Replica pattern (both URLs required)\n if (writeUrl && readUrl)\n {\n const write = createClient(RedisClient, writeUrl);\n const read = createClient(RedisClient, readUrl);\n cacheLogger.debug('Created master-replica cache instances');\n return { write, read };\n }\n\n // 3. Sentinel pattern\n if (sentinelHosts && masterName)\n {\n const sentinels = sentinelHosts.split(',').map((host) =>\n {\n const [hostname, port] = host.trim().split(':');\n return { host: hostname, port: Number(port) || 26379 };\n });\n\n const options: RedisOptions = {\n sentinels,\n name: masterName,\n password,\n };\n\n const client = new RedisClient(options);\n cacheLogger.debug('Created sentinel cache instance', { masterName, sentinels: sentinels.length });\n return { write: client, read: client };\n }\n\n // 4. Cluster pattern\n if (clusterNodes)\n {\n const nodes = clusterNodes.split(',').map((node) =>\n {\n const [host, port] = node.trim().split(':');\n return { host, port: Number(port) || 6379 };\n });\n\n const clusterOptions: ClusterOptions = {\n redisOptions: {\n password,\n },\n };\n\n const cluster = new RedisClient.Cluster(nodes, clusterOptions);\n cacheLogger.debug('Created cluster cache instance', { nodes: nodes.length });\n return { write: cluster, read: cluster };\n }\n\n // 5. Fallback: Single URL with other configs present\n if (singleUrl)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created cache instance (fallback)', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n return { write: client, read: client };\n }\n\n // No valid configuration\n cacheLogger.info('No valid cache configuration found - running without cache');\n return { write: undefined, read: undefined };\n }\n catch (error)\n {\n if (error instanceof Error)\n {\n // Check if it's a missing dependency error\n if (error.message.includes('Cannot find module'))\n {\n cacheLogger.warn(\n 'Cache client library not installed',\n error,\n {\n suggestion: 'Install ioredis to enable cache: pnpm install ioredis',\n mode: 'disabled'\n }\n );\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n error,\n { mode: 'disabled' }\n );\n }\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n { error: String(error), mode: 'disabled' }\n );\n }\n return { write: undefined, read: undefined };\n }\n}\n\n/**\n * Create single cache client (backward compatibility)\n * Only returns write instance\n */\nexport async function createSingleCacheFromEnv(): Promise<Redis | Cluster | undefined>\n{\n const { write } = await createCacheFromEnv();\n return write;\n}","/**\n * Global cache instance manager\n * Provides singleton access to cache (Valkey/Redis) across all modules\n * Supports Master-Replica pattern with separate read/write instances\n *\n * When cache is unavailable, falls back to disabled mode gracefully\n */\n\nimport type { Redis, Cluster } from 'ioredis';\n\nimport { createCacheFromEnv } from './cache-factory';\nimport { logger } from '@spfn/core/logger';\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\nlet writeInstance: Redis | Cluster | undefined;\nlet readInstance: Redis | Cluster | undefined;\nlet isDisabled = false;\n\n/**\n * Get global cache write instance\n *\n * @returns Cache write instance or undefined if disabled/not initialized\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * await cache.set('key', 'value');\n * } else {\n * // Cache disabled - handle gracefully\n * console.log('Cache unavailable, skipping...');\n * }\n * ```\n */\nexport function getCache(): Redis | Cluster | undefined\n{\n return writeInstance;\n}\n\n/**\n * Get global cache read instance (falls back to write if no replica)\n *\n * @returns Cache read instance or write instance as fallback, undefined if disabled\n *\n * @example\n * ```typescript\n * import { getCacheRead } from '@spfn/core/cache';\n *\n * const cache = getCacheRead();\n * if (cache) {\n * const value = await cache.get('key');\n * }\n * ```\n */\nexport function getCacheRead(): Redis | Cluster | undefined\n{\n return readInstance ?? writeInstance;\n}\n\n/**\n * Check if cache is disabled (connection failed or not configured)\n *\n * @example\n * ```typescript\n * import { isCacheDisabled } from '@spfn/core/cache';\n *\n * if (isCacheDisabled()) {\n * // Use alternative strategy (e.g., in-memory cache, database)\n * return await fetchFromDatabase();\n * }\n * ```\n */\nexport function isCacheDisabled(): boolean\n{\n return isDisabled;\n}\n\n/**\n * Set global cache instances (for testing or manual configuration)\n *\n * @param write - Cache write instance\n * @param read - Cache read instance (optional, defaults to write)\n *\n * @example\n * ```typescript\n * import { setCache } from '@spfn/core/cache';\n * import Redis from 'ioredis';\n *\n * const write = new Redis('redis://master:6379');\n * const read = new Redis('redis://replica:6379');\n * setCache(write, read);\n * ```\n */\nexport function setCache(\n write: Redis | Cluster | undefined,\n read?: Redis | Cluster | undefined\n): void\n{\n writeInstance = write;\n readInstance = read ?? write;\n isDisabled = !write;\n}\n\n/**\n * Initialize cache from environment variables\n * Automatically called by startServer()\n *\n * Supported environment variables (priority order):\n * - VALKEY_URL / CACHE_URL (single instance)\n * - VALKEY_WRITE_URL + VALKEY_READ_URL (master-replica)\n * - VALKEY_SENTINEL_HOSTS + VALKEY_MASTER_NAME (sentinel)\n * - VALKEY_CLUSTER_NODES (cluster)\n * - VALKEY_TLS_REJECT_UNAUTHORIZED (TLS config)\n * - Legacy: REDIS_* (backward compatibility)\n *\n * @returns Object with write and read instances, or undefined if disabled\n *\n * @example\n * ```typescript\n * import { initCache } from '@spfn/core/cache';\n *\n * // Manual initialization (not needed if using startServer)\n * const { write, read, disabled } = await initCache();\n * if (!disabled) {\n * console.log('Cache available');\n * }\n * ```\n */\nexport async function initCache(): Promise<{\n write?: Redis | Cluster;\n read?: Redis | Cluster;\n disabled: boolean;\n}>\n{\n // Already initialized\n if (writeInstance)\n {\n return { write: writeInstance, read: readInstance, disabled: isDisabled };\n }\n\n // Auto-detect from environment\n const { write, read } = await createCacheFromEnv();\n\n if (write)\n {\n try\n {\n // Test connection\n await write.ping();\n\n // Test read instance if different\n if (read && read !== write)\n {\n await read.ping();\n }\n\n writeInstance = write;\n readInstance = read;\n isDisabled = false;\n\n const hasReplica = read && read !== write;\n cacheLogger.info(\n hasReplica\n ? 'Cache connected (Master-Replica)'\n : 'Cache connected',\n { mode: 'enabled' }\n );\n\n return { write: writeInstance, read: readInstance, disabled: false };\n }\n catch (error)\n {\n cacheLogger.error(\n 'Cache connection failed - running in disabled mode',\n error instanceof Error ? error : new Error(String(error)),\n { mode: 'disabled' }\n );\n\n // Clean up failed connections\n try\n {\n await write.quit();\n if (read && read !== write)\n {\n await read.quit();\n }\n }\n catch\n {\n // Ignore cleanup errors\n }\n\n isDisabled = true;\n return { write: undefined, read: undefined, disabled: true };\n }\n }\n\n // No configuration or library not installed\n isDisabled = true;\n cacheLogger.info('Cache disabled - no configuration or library not installed', { mode: 'disabled' });\n return { write: undefined, read: undefined, disabled: true };\n}\n\n/**\n * Close all cache connections and cleanup\n *\n * @example\n * ```typescript\n * import { closeCache } from '@spfn/core/cache';\n *\n * // During graceful shutdown\n * await closeCache();\n * ```\n */\nexport async function closeCache(): Promise<void>\n{\n if (isDisabled)\n {\n cacheLogger.debug('Cache already disabled, nothing to close');\n return;\n }\n\n const closePromises: Promise<unknown>[] = [];\n\n if (writeInstance)\n {\n closePromises.push(\n writeInstance.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache write instance', err);\n })\n );\n }\n\n if (readInstance && readInstance !== writeInstance)\n {\n closePromises.push(\n readInstance.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache read instance', err);\n })\n );\n }\n\n await Promise.all(closePromises);\n\n writeInstance = undefined;\n readInstance = undefined;\n isDisabled = true;\n\n cacheLogger.info('Cache connections closed', { mode: 'disabled' });\n}\n\n/**\n * Get cache connection info (for debugging)\n *\n * @example\n * ```typescript\n * import { getCacheInfo } from '@spfn/core/cache';\n *\n * const info = getCacheInfo();\n * console.log(info);\n * // {\n * // hasWrite: true,\n * // hasRead: true,\n * // isReplica: true,\n * // disabled: false\n * // }\n * ```\n */\nexport function getCacheInfo(): {\n hasWrite: boolean;\n hasRead: boolean;\n isReplica: boolean;\n disabled: boolean;\n}\n{\n return {\n hasWrite: !!writeInstance,\n hasRead: !!readInstance,\n isReplica: !!(readInstance && readInstance !== writeInstance),\n disabled: isDisabled,\n };\n}"]}
1
+ {"version":3,"sources":["../../src/cache/cache-factory.ts","../../src/cache/cache-manager.ts"],"names":["cacheLogger","logger"],"mappings":";;;;AAiBA,SAAS,iBAAA,GACT;AACI,EAAA,OAAO;AAAA,IACH,sBAAsB,GAAA,CAAI,6BAAA;AAAA,IAC1B,oBAAoB,GAAA,CAAI;AAAA,GAC5B;AACJ;AAEA,IAAM,WAAA,GAAc,MAAA,CAAO,KAAA,CAAM,kBAAkB,CAAA;AAYnD,SAAS,cAAA,GACT;AACI,EAAA,OAAO,CAAC,EACJ,OAAA,CAAQ,GAAA,CAAI,aACZ,OAAA,CAAQ,GAAA,CAAI,eAAA,IACZ,OAAA,CAAQ,IAAI,cAAA,IACZ,OAAA,CAAQ,GAAA,CAAI,oBAAA,IACZ,QAAQ,GAAA,CAAI,mBAAA,CAAA;AAEpB;AAKA,SAAS,YAAA,CACL,aACA,GAAA,EAEJ;AACI,EAAA,MAAM,OAAA,GAAwB,EAAE,GAAG,iBAAA,EAAkB,EAAE;AAGvD,EAAA,IAAI,GAAA,CAAI,UAAA,CAAW,WAAW,CAAA,EAC9B;AACI,IAAA,OAAA,CAAQ,GAAA,GAAM;AAAA,MACV,kBAAA,EAAoB,OAAA,CAAQ,GAAA,CAAI,6BAAA,KAAkC;AAAA,KACtE;AAAA,EACJ;AAEA,EAAA,OAAO,IAAI,WAAA,CAAY,GAAA,EAAK,OAAO,CAAA;AACvC;AAiCA,eAAsB,kBAAA,GACtB;AAEI,EAAA,IAAI,CAAC,gBAAe,EACpB;AACI,IAAA,WAAA,CAAY,KAAK,sDAAsD,CAAA;AAEvE,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C;AAEA,EAAA,IACA;AAEI,IAAA,MAAM,OAAA,GAAU,MAAM,OAAO,SAAS,CAAA;AACtC,IAAA,MAAM,cAAc,OAAA,CAAQ,OAAA;AAG5B,IAAA,MAAM,SAAA,GAAY,QAAQ,GAAA,CAAI,SAAA;AAC9B,IAAA,MAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,eAAA;AAC7B,IAAA,MAAM,OAAA,GAAU,QAAQ,GAAA,CAAI,cAAA;AAC5B,IAAA,MAAM,YAAA,GAAe,QAAQ,GAAA,CAAI,mBAAA;AACjC,IAAA,MAAM,aAAA,GAAgB,QAAQ,GAAA,CAAI,oBAAA;AAClC,IAAA,MAAM,UAAA,GAAa,QAAQ,GAAA,CAAI,iBAAA;AAC/B,IAAA,MAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,cAAA;AAG7B,IAAA,IAAI,aAAa,CAAC,QAAA,IAAY,CAAC,OAAA,IAAW,CAAC,YAAA,EAC3C;AACI,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,WAAA,EAAa,SAAS,CAAA;AAClD,MAAA,WAAA,CAAY,KAAA,CAAM,iCAAiC,EAAE,GAAA,EAAK,UAAU,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA,EAAG,CAAA;AAElG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,IAAI,YAAY,OAAA,EAChB;AACI,MAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,WAAA,EAAa,QAAQ,CAAA;AAChD,MAAA,MAAM,IAAA,GAAO,YAAA,CAAa,WAAA,EAAa,OAAO,CAAA;AAC9C,MAAA,WAAA,CAAY,MAAM,wCAAwC,CAAA;AAE1D,MAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AAAA,IACzB;AAGA,IAAA,IAAI,iBAAiB,UAAA,EACrB;AACI,MAAA,MAAM,YAAY,aAAA,CAAc,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAChD;AACI,QAAA,MAAM,CAAC,UAAU,IAAI,CAAA,GAAI,KAAK,IAAA,EAAK,CAAE,MAAM,GAAG,CAAA;AAE9C,QAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,MAAM,MAAA,CAAO,IAAI,KAAK,KAAA,EAAM;AAAA,MACzD,CAAC,CAAA;AAED,MAAA,MAAM,OAAA,GAAwB;AAAA,QAC1B,GAAG,iBAAA,EAAkB;AAAA,QACrB,SAAA;AAAA,QACA,IAAA,EAAM,UAAA;AAAA,QACN;AAAA,OACJ;AAEA,MAAA,MAAM,MAAA,GAAS,IAAI,WAAA,CAAY,OAAO,CAAA;AACtC,MAAA,WAAA,CAAY,MAAM,iCAAA,EAAmC,EAAE,YAAY,SAAA,EAAW,SAAA,CAAU,QAAQ,CAAA;AAEhG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,IAAI,YAAA,EACJ;AACI,MAAA,MAAM,QAAQ,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAC3C;AACI,QAAA,MAAM,CAAC,MAAM,IAAI,CAAA,GAAI,KAAK,IAAA,EAAK,CAAE,MAAM,GAAG,CAAA;AAE1C,QAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAA,CAAO,IAAI,KAAK,IAAA,EAAK;AAAA,MAC9C,CAAC,CAAA;AAED,MAAA,MAAM,cAAA,GAAiC;AAAA,QACnC,YAAA,EAAc;AAAA,UACV,GAAG,iBAAA,EAAkB;AAAA,UACrB;AAAA;AACJ,OACJ;AAEA,MAAA,MAAM,OAAA,GAAU,IAAI,WAAA,CAAY,OAAA,CAAQ,OAAO,cAAc,CAAA;AAC7D,MAAA,WAAA,CAAY,MAAM,gCAAA,EAAkC,EAAE,KAAA,EAAO,KAAA,CAAM,QAAQ,CAAA;AAE3E,MAAA,OAAO,EAAE,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM,OAAA,EAAQ;AAAA,IAC3C;AAGA,IAAA,IAAI,SAAA,EACJ;AACI,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,WAAA,EAAa,SAAS,CAAA;AAClD,MAAA,WAAA,CAAY,KAAA,CAAM,qCAAqC,EAAE,GAAA,EAAK,UAAU,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA,EAAG,CAAA;AAEtG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,WAAA,CAAY,KAAK,4DAA4D,CAAA;AAE7E,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C,SACO,KAAA,EACP;AACI,IAAA,IAAI,iBAAiB,KAAA,EACrB;AAEI,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAA,CAAS,oBAAoB,CAAA,EAC/C;AACI,QAAA,WAAA,CAAY,IAAA;AAAA,UACR,oCAAA;AAAA,UACA,KAAA;AAAA,UACA;AAAA,YACI,UAAA,EAAY,uDAAA;AAAA,YACZ,IAAA,EAAM;AAAA;AACV,SACJ;AAAA,MACJ,CAAA,MAEA;AACI,QAAA,WAAA,CAAY,IAAA;AAAA,UACR,+BAAA;AAAA,UACA,KAAA;AAAA,UACA,EAAE,MAAM,UAAA;AAAW,SACvB;AAAA,MACJ;AAAA,IACJ,CAAA,MAEA;AACI,MAAA,WAAA,CAAY,IAAA;AAAA,QACR,+BAAA;AAAA,QACA,EAAE,KAAA,EAAO,MAAA,CAAO,KAAK,CAAA,EAAG,MAAM,UAAA;AAAW,OAC7C;AAAA,IACJ;AAEA,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C;AACJ;AAMA,eAAsB,wBAAA,GACtB;AACI,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,kBAAA,EAAmB;AAE3C,EAAA,OAAO,KAAA;AACX;AC7OA,IAAMA,YAAAA,GAAcC,MAAAA,CAAO,KAAA,CAAM,kBAAkB,CAAA;AAYnD,IAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,kBAAkB,CAAA;AAE/C,IAAM,KAAA,GAAsB,UAAA,CAAmB,SAAS,CAAA,KAAM;AAAA,EAC1D,KAAA,EAAO,MAAA;AAAA,EACP,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU;AACd,CAAA;AAoBO,SAAS,QAAA,GAChB;AACI,EAAA,OAAO,KAAA,CAAM,KAAA;AACjB;AAiBO,SAAS,YAAA,GAChB;AACI,EAAA,OAAO,KAAA,CAAM,QAAQ,KAAA,CAAM,KAAA;AAC/B;AAeO,SAAS,eAAA,GAChB;AACI,EAAA,OAAO,KAAA,CAAM,QAAA;AACjB;AAkBO,SAAS,QAAA,CACZ,OACA,IAAA,EAEJ;AACI,EAAA,KAAA,CAAM,KAAA,GAAQ,KAAA;AACd,EAAA,KAAA,CAAM,OAAO,IAAA,IAAQ,KAAA;AACrB,EAAA,KAAA,CAAM,WAAW,CAAC,KAAA;AACtB;AA2BA,eAAsB,SAAA,GAKtB;AAEI,EAAA,IAAI,MAAM,KAAA,EACV;AACI,IAAA,OAAO,EAAE,OAAO,KAAA,CAAM,KAAA,EAAO,MAAM,KAAA,CAAM,IAAA,EAAM,QAAA,EAAU,KAAA,CAAM,QAAA,EAAS;AAAA,EAC5E;AAGA,EAAA,MAAM,EAAE,KAAA,EAAO,IAAA,EAAK,GAAI,MAAM,kBAAA,EAAmB;AAEjD,EAAA,IAAI,KAAA,EACJ;AACI,IAAA,IACA;AAEI,MAAA,MAAM,MAAM,IAAA,EAAK;AAGjB,MAAA,IAAI,IAAA,IAAQ,SAAS,KAAA,EACrB;AACI,QAAA,MAAM,KAAK,IAAA,EAAK;AAAA,MACpB;AAEA,MAAA,KAAA,CAAM,KAAA,GAAQ,KAAA;AACd,MAAA,KAAA,CAAM,IAAA,GAAO,IAAA;AACb,MAAA,KAAA,CAAM,QAAA,GAAW,KAAA;AAEjB,MAAA,MAAM,UAAA,GAAa,QAAQ,IAAA,KAAS,KAAA;AACpC,MAAAD,YAAAA,CAAY,IAAA;AAAA,QACR,aACM,kCAAA,GACA,iBAAA;AAAA,QACN,EAAE,MAAM,SAAA;AAAU,OACtB;AAEA,MAAA,OAAO,EAAE,OAAO,KAAA,CAAM,KAAA,EAAO,MAAM,KAAA,CAAM,IAAA,EAAM,UAAU,KAAA,EAAM;AAAA,IACnE,SACO,KAAA,EACP;AACI,MAAAA,YAAAA,CAAY,KAAA;AAAA,QACR,oDAAA;AAAA,QACA,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,QACxD,EAAE,MAAM,UAAA;AAAW,OACvB;AAGA,MAAA,IACA;AACI,QAAA,MAAM,MAAM,IAAA,EAAK;AACjB,QAAA,IAAI,IAAA,IAAQ,SAAS,KAAA,EACrB;AACI,UAAA,MAAM,KAAK,IAAA,EAAK;AAAA,QACpB;AAAA,MACJ,CAAA,CAAA,MAEA;AAAA,MAEA;AAEA,MAAA,KAAA,CAAM,QAAA,GAAW,IAAA;AAEjB,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAW,UAAU,IAAA,EAAK;AAAA,IAC/D;AAAA,EACJ;AAGA,EAAA,KAAA,CAAM,QAAA,GAAW,IAAA;AACjB,EAAAA,aAAY,IAAA,CAAK,4DAAA,EAA8D,EAAE,IAAA,EAAM,YAAY,CAAA;AAEnG,EAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAW,UAAU,IAAA,EAAK;AAC/D;AAaA,eAAsB,UAAA,GACtB;AACI,EAAA,IAAI,MAAM,QAAA,EACV;AACI,IAAAA,YAAAA,CAAY,MAAM,0CAA0C,CAAA;AAE5D,IAAA;AAAA,EACJ;AAEA,EAAA,MAAM,gBAAoC,EAAC;AAE3C,EAAA,IAAI,MAAM,KAAA,EACV;AACI,IAAA,aAAA,CAAc,IAAA;AAAA,MACV,MAAM,KAAA,CAAM,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAC1B;AACI,QAAAA,YAAAA,CAAY,KAAA,CAAM,oCAAA,EAAsC,GAAG,CAAA;AAAA,MAC/D,CAAC;AAAA,KACL;AAAA,EACJ;AAEA,EAAA,IAAI,KAAA,CAAM,IAAA,IAAQ,KAAA,CAAM,IAAA,KAAS,MAAM,KAAA,EACvC;AACI,IAAA,aAAA,CAAc,IAAA;AAAA,MACV,MAAM,IAAA,CAAK,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KACzB;AACI,QAAAA,YAAAA,CAAY,KAAA,CAAM,mCAAA,EAAqC,GAAG,CAAA;AAAA,MAC9D,CAAC;AAAA,KACL;AAAA,EACJ;AAEA,EAAA,MAAM,OAAA,CAAQ,IAAI,aAAa,CAAA;AAE/B,EAAA,KAAA,CAAM,KAAA,GAAQ,MAAA;AACd,EAAA,KAAA,CAAM,IAAA,GAAO,MAAA;AACb,EAAA,KAAA,CAAM,QAAA,GAAW,IAAA;AAEjB,EAAAA,aAAY,IAAA,CAAK,0BAAA,EAA4B,EAAE,IAAA,EAAM,YAAY,CAAA;AACrE;AAmBO,SAAS,YAAA,GAMhB;AACI,EAAA,OAAO;AAAA,IACH,QAAA,EAAU,CAAC,CAAC,KAAA,CAAM,KAAA;AAAA,IAClB,OAAA,EAAS,CAAC,CAAC,KAAA,CAAM,IAAA;AAAA,IACjB,WAAW,CAAC,EAAE,MAAM,IAAA,IAAQ,KAAA,CAAM,SAAS,KAAA,CAAM,KAAA,CAAA;AAAA,IACjD,UAAU,KAAA,CAAM;AAAA,GACpB;AACJ","file":"index.js","sourcesContent":["/**\n * Cache factory with automatic environment variable detection\n */\n\nimport type { Redis, Cluster, RedisOptions, ClusterOptions } from 'ioredis';\nimport { logger } from '@spfn/core/logger';\nimport { env } from '@spfn/core/config';\n\n/**\n * Connection-resilience options applied to every cache client.\n *\n * ioredis defaults to an unbounded offline command queue and 20 retries per\n * request, so a post-startup cache outage can buffer every command and hang\n * awaits. Bound retries (CACHE_MAX_RETRIES_PER_REQUEST, default 3) so commands\n * fail fast instead of hanging; CACHE_ENABLE_OFFLINE_QUEUE (default true) can be\n * set false for strict fail-fast (reject immediately while disconnected).\n */\nfunction resilienceOptions(): Pick<RedisOptions, 'maxRetriesPerRequest' | 'enableOfflineQueue'>\n{\n return {\n maxRetriesPerRequest: env.CACHE_MAX_RETRIES_PER_REQUEST,\n enableOfflineQueue: env.CACHE_ENABLE_OFFLINE_QUEUE,\n };\n}\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\nexport interface CacheClients {\n /** Primary cache for writes (or both read/write if no replica) */\n write?: Redis | Cluster;\n /** Replica cache for reads (optional, falls back to write) */\n read?: Redis | Cluster;\n}\n\n/**\n * Check if any cache configuration exists in environment\n */\nfunction hasCacheConfig(): boolean\n{\n return !!(\n process.env.CACHE_URL ||\n process.env.CACHE_WRITE_URL ||\n process.env.CACHE_READ_URL ||\n process.env.CACHE_SENTINEL_HOSTS ||\n process.env.CACHE_CLUSTER_NODES\n );\n}\n\n/**\n * Create cache client with TLS support\n */\nfunction createClient(\n RedisClient: new (url: string, options?: RedisOptions) => Redis,\n url: string,\n): Redis\n{\n const options: RedisOptions = { ...resilienceOptions() };\n\n // TLS support for secure connections\n if (url.startsWith('rediss://'))\n {\n options.tls = {\n rejectUnauthorized: process.env.CACHE_TLS_REJECT_UNAUTHORIZED !== 'false',\n };\n }\n\n return new RedisClient(url, options);\n}\n\n/**\n * Create cache client(s) from environment variables\n *\n * Supported patterns (priority order):\n * 1. Single instance: CACHE_URL\n * 2. Master-Replica: CACHE_WRITE_URL + CACHE_READ_URL\n * 3. Sentinel: CACHE_SENTINEL_HOSTS + CACHE_MASTER_NAME\n * 4. Cluster: CACHE_CLUSTER_NODES\n *\n * @returns Cache client(s) or undefined if no configuration found\n *\n * @example\n * ```bash\n * # Single (most common)\n * CACHE_URL=redis://localhost:6379\n * CACHE_URL=rediss://secure.cache.com:6380 # TLS\n *\n * # Master-Replica\n * CACHE_WRITE_URL=redis://master:6379\n * CACHE_READ_URL=redis://replica:6379\n *\n * # Sentinel\n * CACHE_SENTINEL_HOSTS=sentinel1:26379,sentinel2:26379\n * CACHE_MASTER_NAME=mymaster\n * CACHE_PASSWORD=secret\n *\n * # Cluster\n * CACHE_CLUSTER_NODES=node1:6379,node2:6379,node3:6379\n * CACHE_PASSWORD=secret\n * ```\n */\nexport async function createCacheFromEnv(): Promise<CacheClients>\n{\n // Quick exit if no cache config\n if (!hasCacheConfig())\n {\n cacheLogger.info('No cache configuration found - running without cache');\n\n return { write: undefined, read: undefined };\n }\n\n try\n {\n // Dynamic import to avoid bundling if not used\n const ioredis = await import('ioredis');\n const RedisClient = ioredis.default;\n\n // Get environment variables\n const singleUrl = process.env.CACHE_URL;\n const writeUrl = process.env.CACHE_WRITE_URL;\n const readUrl = process.env.CACHE_READ_URL;\n const clusterNodes = process.env.CACHE_CLUSTER_NODES;\n const sentinelHosts = process.env.CACHE_SENTINEL_HOSTS;\n const masterName = process.env.CACHE_MASTER_NAME;\n const password = process.env.CACHE_PASSWORD;\n\n // 1. Single instance (most common - highest priority)\n if (singleUrl && !writeUrl && !readUrl && !clusterNodes)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created single cache instance', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n\n return { write: client, read: client };\n }\n\n // 2. Master-Replica pattern (both URLs required)\n if (writeUrl && readUrl)\n {\n const write = createClient(RedisClient, writeUrl);\n const read = createClient(RedisClient, readUrl);\n cacheLogger.debug('Created master-replica cache instances');\n\n return { write, read };\n }\n\n // 3. Sentinel pattern\n if (sentinelHosts && masterName)\n {\n const sentinels = sentinelHosts.split(',').map((host) =>\n {\n const [hostname, port] = host.trim().split(':');\n\n return { host: hostname, port: Number(port) || 26379 };\n });\n\n const options: RedisOptions = {\n ...resilienceOptions(),\n sentinels,\n name: masterName,\n password,\n };\n\n const client = new RedisClient(options);\n cacheLogger.debug('Created sentinel cache instance', { masterName, sentinels: sentinels.length });\n\n return { write: client, read: client };\n }\n\n // 4. Cluster pattern\n if (clusterNodes)\n {\n const nodes = clusterNodes.split(',').map((node) =>\n {\n const [host, port] = node.trim().split(':');\n\n return { host, port: Number(port) || 6379 };\n });\n\n const clusterOptions: ClusterOptions = {\n redisOptions: {\n ...resilienceOptions(),\n password,\n },\n };\n\n const cluster = new RedisClient.Cluster(nodes, clusterOptions);\n cacheLogger.debug('Created cluster cache instance', { nodes: nodes.length });\n\n return { write: cluster, read: cluster };\n }\n\n // 5. Fallback: Single URL with other configs present\n if (singleUrl)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created cache instance (fallback)', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n\n return { write: client, read: client };\n }\n\n // No valid configuration\n cacheLogger.info('No valid cache configuration found - running without cache');\n\n return { write: undefined, read: undefined };\n }\n catch (error)\n {\n if (error instanceof Error)\n {\n // Check if it's a missing dependency error\n if (error.message.includes('Cannot find module'))\n {\n cacheLogger.warn(\n 'Cache client library not installed',\n error,\n {\n suggestion: 'Install ioredis to enable cache: pnpm install ioredis',\n mode: 'disabled',\n },\n );\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n error,\n { mode: 'disabled' },\n );\n }\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n { error: String(error), mode: 'disabled' },\n );\n }\n\n return { write: undefined, read: undefined };\n }\n}\n\n/**\n * Create single cache client (backward compatibility)\n * Only returns write instance\n */\nexport async function createSingleCacheFromEnv(): Promise<Redis | Cluster | undefined>\n{\n const { write } = await createCacheFromEnv();\n\n return write;\n}\n","/**\n * Global cache instance manager\n * Provides singleton access to cache (Valkey/Redis) across all modules\n * Supports Master-Replica pattern with separate read/write instances\n *\n * When cache is unavailable, falls back to disabled mode gracefully\n */\n\nimport type { Redis, Cluster } from 'ioredis';\n\nimport { createCacheFromEnv } from './cache-factory';\nimport { logger } from '@spfn/core/logger';\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\n// ── globalThis singleton ──────────────────────────────────────────\n// CJS/ESM dual-package hazard: 모듈이 두 번 로드되어도\n// Symbol.for() + globalThis로 동일 상태를 공유한다.\ninterface CacheState\n{\n write: Redis | Cluster | undefined;\n read: Redis | Cluster | undefined;\n disabled: boolean;\n}\n\nconst CACHE_KEY = Symbol.for('@spfn/core:cache');\n\nconst state: CacheState = ((globalThis as any)[CACHE_KEY] ??= {\n write: undefined,\n read: undefined,\n disabled: false,\n});\n\n/**\n * Get global cache write instance\n *\n * @returns Cache write instance or undefined if disabled/not initialized\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * await cache.set('key', 'value');\n * } else {\n * // Cache disabled - handle gracefully\n * console.log('Cache unavailable, skipping...');\n * }\n * ```\n */\nexport function getCache(): Redis | Cluster | undefined\n{\n return state.write;\n}\n\n/**\n * Get global cache read instance (falls back to write if no replica)\n *\n * @returns Cache read instance or write instance as fallback, undefined if disabled\n *\n * @example\n * ```typescript\n * import { getCacheRead } from '@spfn/core/cache';\n *\n * const cache = getCacheRead();\n * if (cache) {\n * const value = await cache.get('key');\n * }\n * ```\n */\nexport function getCacheRead(): Redis | Cluster | undefined\n{\n return state.read ?? state.write;\n}\n\n/**\n * Check if cache is disabled (connection failed or not configured)\n *\n * @example\n * ```typescript\n * import { isCacheDisabled } from '@spfn/core/cache';\n *\n * if (isCacheDisabled()) {\n * // Use alternative strategy (e.g., in-memory cache, database)\n * return await fetchFromDatabase();\n * }\n * ```\n */\nexport function isCacheDisabled(): boolean\n{\n return state.disabled;\n}\n\n/**\n * Set global cache instances (for testing or manual configuration)\n *\n * @param write - Cache write instance\n * @param read - Cache read instance (optional, defaults to write)\n *\n * @example\n * ```typescript\n * import { setCache } from '@spfn/core/cache';\n * import Redis from 'ioredis';\n *\n * const write = new Redis('redis://master:6379');\n * const read = new Redis('redis://replica:6379');\n * setCache(write, read);\n * ```\n */\nexport function setCache(\n write: Redis | Cluster | undefined,\n read?: Redis | Cluster | undefined,\n): void\n{\n state.write = write;\n state.read = read ?? write;\n state.disabled = !write;\n}\n\n/**\n * Initialize cache from environment variables\n * Automatically called by startServer()\n *\n * Supported environment variables (priority order):\n * - VALKEY_URL / CACHE_URL (single instance)\n * - VALKEY_WRITE_URL + VALKEY_READ_URL (master-replica)\n * - VALKEY_SENTINEL_HOSTS + VALKEY_MASTER_NAME (sentinel)\n * - VALKEY_CLUSTER_NODES (cluster)\n * - VALKEY_TLS_REJECT_UNAUTHORIZED (TLS config)\n * - Legacy: REDIS_* (backward compatibility)\n *\n * @returns Object with write and read instances, or undefined if disabled\n *\n * @example\n * ```typescript\n * import { initCache } from '@spfn/core/cache';\n *\n * // Manual initialization (not needed if using startServer)\n * const { write, read, disabled } = await initCache();\n * if (!disabled) {\n * console.log('Cache available');\n * }\n * ```\n */\nexport async function initCache(): Promise<{\n write?: Redis | Cluster;\n read?: Redis | Cluster;\n disabled: boolean;\n}>\n{\n // Already initialized\n if (state.write)\n {\n return { write: state.write, read: state.read, disabled: state.disabled };\n }\n\n // Auto-detect from environment\n const { write, read } = await createCacheFromEnv();\n\n if (write)\n {\n try\n {\n // Test connection\n await write.ping();\n\n // Test read instance if different\n if (read && read !== write)\n {\n await read.ping();\n }\n\n state.write = write;\n state.read = read;\n state.disabled = false;\n\n const hasReplica = read && read !== write;\n cacheLogger.info(\n hasReplica\n ? 'Cache connected (Master-Replica)'\n : 'Cache connected',\n { mode: 'enabled' },\n );\n\n return { write: state.write, read: state.read, disabled: false };\n }\n catch (error)\n {\n cacheLogger.error(\n 'Cache connection failed - running in disabled mode',\n error instanceof Error ? error : new Error(String(error)),\n { mode: 'disabled' },\n );\n\n // Clean up failed connections\n try\n {\n await write.quit();\n if (read && read !== write)\n {\n await read.quit();\n }\n }\n catch\n {\n // Ignore cleanup errors\n }\n\n state.disabled = true;\n\n return { write: undefined, read: undefined, disabled: true };\n }\n }\n\n // No configuration or library not installed\n state.disabled = true;\n cacheLogger.info('Cache disabled - no configuration or library not installed', { mode: 'disabled' });\n\n return { write: undefined, read: undefined, disabled: true };\n}\n\n/**\n * Close all cache connections and cleanup\n *\n * @example\n * ```typescript\n * import { closeCache } from '@spfn/core/cache';\n *\n * // During graceful shutdown\n * await closeCache();\n * ```\n */\nexport async function closeCache(): Promise<void>\n{\n if (state.disabled)\n {\n cacheLogger.debug('Cache already disabled, nothing to close');\n\n return;\n }\n\n const closePromises: Promise<unknown>[] = [];\n\n if (state.write)\n {\n closePromises.push(\n state.write.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache write instance', err);\n }),\n );\n }\n\n if (state.read && state.read !== state.write)\n {\n closePromises.push(\n state.read.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache read instance', err);\n }),\n );\n }\n\n await Promise.all(closePromises);\n\n state.write = undefined;\n state.read = undefined;\n state.disabled = true;\n\n cacheLogger.info('Cache connections closed', { mode: 'disabled' });\n}\n\n/**\n * Get cache connection info (for debugging)\n *\n * @example\n * ```typescript\n * import { getCacheInfo } from '@spfn/core/cache';\n *\n * const info = getCacheInfo();\n * console.log(info);\n * // {\n * // hasWrite: true,\n * // hasRead: true,\n * // isReplica: true,\n * // disabled: false\n * // }\n * ```\n */\nexport function getCacheInfo(): {\n hasWrite: boolean;\n hasRead: boolean;\n isReplica: boolean;\n disabled: boolean;\n}\n{\n return {\n hasWrite: !!state.write,\n hasRead: !!state.read,\n isReplica: !!(state.read && state.read !== state.write),\n disabled: state.disabled,\n };\n}\n"]}
@@ -7,6 +7,13 @@ import { HttpMethod } from '@spfn/core/route';
7
7
  */
8
8
  /**
9
9
  * Generator execution trigger types
10
+ *
11
+ * Who fires each one:
12
+ * - `watch` — `spfn dev`, on the initial pass and every file change
13
+ * - `manual` — `spfn codegen run` and `spfn contract`
14
+ * - `build` — `spfn build`
15
+ * - `start` — nothing in the shipped CLI; reserved for a programmatic caller that
16
+ * dispatches it through `orchestrator.generateAll('start')`
10
17
  */
11
18
  type GeneratorTrigger = 'watch' | 'manual' | 'build' | 'start';
12
19
  interface GeneratorOptions {
@@ -35,13 +42,14 @@ interface Generator {
35
42
  /**
36
43
  * When this generator should run
37
44
  *
38
- * @default ['watch', 'manual', 'build']
45
+ * @default ['watch', 'manual', 'build'] — every trigger the CLI fires
46
+ *
47
+ * Narrowing this is the easy mistake: drop `build` and the generator is silently
48
+ * skipped during `spfn build`, so whatever it produces ships stale.
39
49
  *
40
50
  * Examples:
41
- * - ['watch', 'build']: Run during development and build (e.g., admin-nav-generator)
42
- * - ['build', 'start']: Run during build and server start (e.g., db-migration)
43
- * - ['watch', 'manual']: Run during development and manual CLI (e.g., contract-generator)
44
- * - ['start']: Run only on server start (e.g., runtime config generator)
51
+ * - ['watch']: only while `spfn dev` is watching, never from `spfn codegen run`
52
+ * - ['manual']: only on an explicit run, never mid-edit — for something slow
45
53
  */
46
54
  runOn?: GeneratorTrigger[];
47
55
  /**
@@ -89,11 +97,21 @@ interface OrchestratorOptions {
89
97
  cwd?: string;
90
98
  /** Enable debug logging */
91
99
  debug?: boolean;
100
+ /**
101
+ * Rethrow a generator failure instead of only logging it
102
+ *
103
+ * Off by default, which keeps watch mode alive through a half-edited file.
104
+ * A build turns it on: a generator that refuses — a broken contract, a
105
+ * router that will not load — has to reach the exit code, or the refusal
106
+ * scrolls past and the build ships anyway.
107
+ */
108
+ throwOnError?: boolean;
92
109
  }
93
110
  declare class CodegenOrchestrator {
94
111
  private readonly generators;
95
112
  private readonly cwd;
96
113
  private readonly debug;
114
+ private readonly throwOnError;
97
115
  private isGenerating;
98
116
  private pendingRegenerations;
99
117
  private watcher?;
@@ -314,10 +332,15 @@ interface RouteMapGeneratorConfig {
314
332
  }
315
333
 
316
334
  /**
317
- * Built-in Generators Export
335
+ * Contract Generator
336
+ *
337
+ * Reads the router, writes `contracts/current.json`, and on a build compares it
338
+ * against the newest released snapshot.
318
339
  *
319
- * Provides a registry of all built-in generators.
320
- * Custom generators can be added via .spfnrc.ts configuration.
340
+ * It is a codegen plugin for the same reason the route map is one: hanging off
341
+ * `spfn build` and `spfn dev` removes "forgot to regenerate the contract" as a
342
+ * failure mode. On `dev` it only regenerates — refusing a half-finished route
343
+ * mid-edit would make the feature unusable — and on `build` it also gates.
321
344
  *
322
345
  * @example
323
346
  * ```typescript
@@ -325,18 +348,103 @@ interface RouteMapGeneratorConfig {
325
348
  * import { defineConfig, defineGenerator } from '@spfn/core/codegen';
326
349
  *
327
350
  * export default defineConfig({
351
+ * generators: [
352
+ * defineGenerator({
353
+ * name: '@spfn/core:contract',
354
+ * routerPath: './src/server/router.ts',
355
+ * outputDir: './contracts',
356
+ * })
357
+ * ]
358
+ * });
359
+ * ```
360
+ */
361
+
362
+ interface ContractGeneratorConfig {
363
+ /**
364
+ * Generator name (required for package-based loading)
365
+ */
366
+ name: '@spfn/core:contract';
367
+ /**
368
+ * Path to the router file (relative to project root)
369
+ * @example './src/server/router.ts'
370
+ */
371
+ routerPath: string;
372
+ /**
373
+ * Named export holding the router.
374
+ * @default 'appRouter', falling back to the default export
375
+ */
376
+ routerExport?: string;
377
+ /**
378
+ * Directory holding current.json, released/ and usage/ (relative to project root)
379
+ * @default './contracts'
380
+ */
381
+ outputDir?: string;
382
+ /**
383
+ * Extra file patterns to watch, for routes outside src/server/routes
384
+ */
385
+ additionalRouteDirs?: string[];
386
+ }
387
+ /** Thrown when the build must stop. */
388
+ declare class ContractGeneratorError extends Error {
389
+ constructor(message: string);
390
+ }
391
+
392
+ /**
393
+ * Router registration guard
394
+ *
395
+ * A contract has to describe what production serves. When a route is registered
396
+ * only under a feature flag or an environment check, the generated contract
397
+ * describes whichever way the generator happened to run — and the gate then
398
+ * compares a promise nobody made.
399
+ *
400
+ * Object spread is the one way a key can be conditionally present in the object
401
+ * `defineRouter()` receives, so that is what this reads. A spread of a plain
402
+ * identifier (`...baseRoutes`) is unconditional and passes; anything computed
403
+ * inside the spread does not.
404
+ */
405
+ /** Thrown when the router registers contracted routes conditionally. */
406
+ declare class ConditionalRegistrationError extends Error {
407
+ constructor(message: string);
408
+ }
409
+ /**
410
+ * Refuse a router whose route set depends on a condition.
411
+ *
412
+ * @param routerPath - path named in the error message
413
+ * @param source - contents of the router file
414
+ */
415
+ declare function assertUnconditionalRegistration(routerPath: string, source: string): void;
416
+
417
+ /**
418
+ * Built-in Generators Export
419
+ *
420
+ * @example
421
+ * ```typescript
422
+ * // .spfnrc.ts
423
+ * import { defineConfig, defineGenerator } from '@spfn/core/codegen';
424
+ * import type { RouteMapGeneratorConfig, ContractGeneratorConfig } from '@spfn/core/codegen';
425
+ *
426
+ * export default defineConfig({
328
427
  * generators: [
329
- * defineGenerator({ path: './my-generator.ts' })
428
+ * defineGenerator<RouteMapGeneratorConfig>({
429
+ * name: '@spfn/core:route-map',
430
+ * routerPath: './src/server/router.ts',
431
+ * outputPath: './src/generated/route-map.ts',
432
+ * }),
433
+ * defineGenerator<ContractGeneratorConfig>({
434
+ * name: '@spfn/core:contract',
435
+ * routerPath: './src/server/router.ts',
436
+ * outputDir: './contracts',
437
+ * }),
330
438
  * ]
331
439
  * });
332
440
  * ```
333
441
  */
334
442
 
335
443
  /**
336
- * Registry of available generators
337
- *
338
- * Used by package-based generator loading (e.g., "@spfn/core:route-map")
444
+ * @internal
445
+ * Registry of available generators for package-based loading.
446
+ * DO NOT use directly - use defineGenerator({ name: '@spfn/core:route-map', ... }) instead.
339
447
  */
340
448
  declare const generators: Record<string, unknown>;
341
449
 
342
- export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type ResourceRoutes, type RouteContractMapping, type RouteMapGeneratorConfig, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
450
+ export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, ConditionalRegistrationError, type ContractGeneratorConfig, ContractGeneratorError, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type ResourceRoutes, type RouteContractMapping, type RouteMapGeneratorConfig, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
@@ -1,9 +1,10 @@
1
1
  import { watch } from 'chokidar';
2
- import { join, dirname, resolve, relative } from 'path';
2
+ import { join, relative, sep, dirname, resolve } from 'path';
3
3
  import mm from 'micromatch';
4
4
  import { logger } from '@spfn/core/logger';
5
5
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
6
6
  import { createJiti } from 'jiti';
7
+ import { collectContractDocument, writeCurrentDocument, checkContract, formatViolations } from '@spfn/core/contract';
7
8
 
8
9
  // src/codegen/core/orchestrator.ts
9
10
  var orchestratorLogger = logger.child("@spfn/core:orchestrator");
@@ -11,6 +12,7 @@ var CodegenOrchestrator = class {
11
12
  generators;
12
13
  cwd;
13
14
  debug;
15
+ throwOnError;
14
16
  isGenerating = false;
15
17
  pendingRegenerations = /* @__PURE__ */ new Set();
16
18
  watcher;
@@ -19,6 +21,7 @@ var CodegenOrchestrator = class {
19
21
  this.generators = options.generators;
20
22
  this.cwd = options.cwd ?? process.cwd();
21
23
  this.debug = options.debug ?? false;
24
+ this.throwOnError = options.throwOnError ?? false;
22
25
  }
23
26
  /**
24
27
  * Close watcher and cleanup resources
@@ -80,6 +83,9 @@ var CodegenOrchestrator = class {
80
83
  } catch (error) {
81
84
  const err = error instanceof Error ? error : new Error(String(error));
82
85
  orchestratorLogger.error(`[${generator.name}] \u2717 Generation failed`, err);
86
+ if (this.throwOnError) {
87
+ throw err;
88
+ }
83
89
  }
84
90
  }
85
91
  }
@@ -111,8 +117,10 @@ var CodegenOrchestrator = class {
111
117
  });
112
118
  }
113
119
  this.watcher = watch(watchDirs, {
114
- ignored: /(^|[\/\\])\../,
115
- // ignore dotfiles
120
+ // Ignore dotfiles by path segments relative to cwd — matching the absolute
121
+ // path would ignore everything when the checkout itself lives under a dot
122
+ // directory (e.g. .claude/worktrees/<name>), leaving zero watched files
123
+ ignored: (watchedPath) => relative(this.cwd, watchedPath).split(sep).some((segment) => segment.startsWith(".") && segment !== "." && segment !== ".."),
116
124
  persistent: true,
117
125
  ignoreInitial: true,
118
126
  awaitWriteFinish: {
@@ -276,6 +284,11 @@ async function createGeneratorsFromConfig(config, cwd) {
276
284
  }
277
285
  for (const generatorConfig of config.generators) {
278
286
  try {
287
+ if ("generate" in generatorConfig && typeof generatorConfig.generate === "function") {
288
+ generators2.push(generatorConfig);
289
+ configLogger.info(`Generator instance added: ${generatorConfig.name}`);
290
+ continue;
291
+ }
279
292
  if ("path" in generatorConfig) {
280
293
  const generatorPath = generatorConfig.path.startsWith(".") ? join(cwd, generatorConfig.path) : generatorConfig.path;
281
294
  configLogger.info(`Loading custom generator: ${generatorPath}`);
@@ -351,17 +364,27 @@ function parseRouterFile(routerPath) {
351
364
  let match;
352
365
  while ((match = importPattern.exec(content)) !== null) {
353
366
  const importPath = match[1];
354
- if (importPath.includes("route")) {
367
+ if (importPath.startsWith(".")) {
355
368
  importPaths.push(importPath);
356
369
  }
357
370
  }
358
- const routerPattern = /defineRouter\s*\(\s*\{([^}]+)\}/s;
359
- const routerMatch = routerPattern.exec(content);
360
- if (routerMatch) {
361
- const routerContent = routerMatch[1];
362
- const namePattern = /(\w+)\s*[,}]/g;
363
- while ((match = namePattern.exec(routerContent)) !== null) {
364
- routeNames.push(match[1]);
371
+ const defineRouterStart = content.indexOf("defineRouter(");
372
+ if (defineRouterStart !== -1) {
373
+ const braceStart = content.indexOf("{", defineRouterStart);
374
+ if (braceStart !== -1) {
375
+ let depth = 1;
376
+ let braceEnd = braceStart + 1;
377
+ while (depth > 0 && braceEnd < content.length) {
378
+ if (content[braceEnd] === "{") depth++;
379
+ else if (content[braceEnd] === "}") depth--;
380
+ braceEnd++;
381
+ }
382
+ const routerContent = content.slice(braceStart + 1, braceEnd - 1);
383
+ const withoutComments = routerContent.replace(/\/\/[^\n]*/g, "");
384
+ const namePattern = /^\s*(\w+)\s*[,\n]/gm;
385
+ while ((match = namePattern.exec(withoutComments)) !== null) {
386
+ routeNames.push(match[1]);
387
+ }
365
388
  }
366
389
  }
367
390
  } catch (error) {
@@ -404,6 +427,17 @@ function createRouteMapGenerator(config) {
404
427
  outputPath = "./src/generated/route-map.ts",
405
428
  additionalRouteDirs = []
406
429
  } = config;
430
+ if (!routerPath) {
431
+ throw new Error(
432
+ `[@spfn/core:route-map] Missing required "routerPath" option.
433
+
434
+ Usage:
435
+ defineGenerator<RouteMapGeneratorConfig>({
436
+ name: '@spfn/core:route-map',
437
+ routerPath: './src/server/router.ts',
438
+ })`
439
+ );
440
+ }
407
441
  return {
408
442
  name: "@spfn/core:route-map",
409
443
  watchPatterns: [
@@ -412,7 +446,7 @@ function createRouteMapGenerator(config) {
412
446
  "src/server/routes/**/*.ts",
413
447
  ...additionalRouteDirs.map((dir) => `${dir}/**/*.ts`)
414
448
  ],
415
- runOn: ["watch", "build", "start", "manual"],
449
+ runOn: ["watch", "manual", "build"],
416
450
  async generate(options) {
417
451
  const { cwd, debug } = options;
418
452
  const absoluteRouterPath = join(cwd, routerPath);
@@ -433,7 +467,17 @@ function createRouteMapGenerator(config) {
433
467
  for (const importPath of importPaths) {
434
468
  let resolvedPath = resolve(routerDir, importPath);
435
469
  if (!resolvedPath.endsWith(".ts")) {
436
- resolvedPath += ".ts";
470
+ const withTs = resolvedPath + ".ts";
471
+ if (existsSync(withTs)) {
472
+ resolvedPath = withTs;
473
+ } else {
474
+ const indexPath = join(resolvedPath, "index.ts");
475
+ if (existsSync(indexPath)) {
476
+ resolvedPath = indexPath;
477
+ } else {
478
+ resolvedPath = withTs;
479
+ }
480
+ }
437
481
  }
438
482
  if (existsSync(resolvedPath)) {
439
483
  const routes = parseRouteFile(resolvedPath);
@@ -458,11 +502,164 @@ function createRouteMapGenerator(config) {
458
502
  };
459
503
  }
460
504
 
505
+ // src/codegen/generators/contract-guard.ts
506
+ var ConditionalRegistrationError = class extends Error {
507
+ constructor(message) {
508
+ super(message);
509
+ this.name = "ConditionalRegistrationError";
510
+ }
511
+ };
512
+ function defineRouterBlock(source) {
513
+ const start = source.indexOf("defineRouter(");
514
+ if (start === -1) {
515
+ return void 0;
516
+ }
517
+ const open = source.indexOf("{", start);
518
+ if (open === -1) {
519
+ return void 0;
520
+ }
521
+ let depth = 1;
522
+ let cursor = open + 1;
523
+ while (depth > 0 && cursor < source.length) {
524
+ if (source[cursor] === "{") depth++;
525
+ else if (source[cursor] === "}") depth--;
526
+ cursor++;
527
+ }
528
+ return source.slice(open + 1, cursor - 1).replace(/\/\/[^\n]*/g, "");
529
+ }
530
+ var PLAIN_REFERENCE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*\s*$/;
531
+ function assertUnconditionalRegistration(routerPath, source) {
532
+ const block = defineRouterBlock(source);
533
+ if (!block) {
534
+ return;
535
+ }
536
+ for (const [, expression] of block.matchAll(/\.\.\.\s*([^,\n]+)/g)) {
537
+ if (PLAIN_REFERENCE.test(expression)) {
538
+ continue;
539
+ }
540
+ throw new ConditionalRegistrationError(
541
+ `${routerPath} registers routes conditionally: "...${expression.trim()}".
542
+
543
+ A contract has to describe what production serves. When the route set depends on a flag or an environment, the generated contract describes whichever way the generator happened to run.
544
+ Register contracted routes unconditionally, and gate behaviour inside the handler instead.`
545
+ );
546
+ }
547
+ }
548
+
549
+ // src/codegen/generators/contract.ts
550
+ var genLogger2 = logger.child("@spfn/core:contract-generator");
551
+ var ContractGeneratorError = class extends Error {
552
+ constructor(message) {
553
+ super(message);
554
+ this.name = "ContractGeneratorError";
555
+ }
556
+ };
557
+ function isRouter(value) {
558
+ return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
559
+ }
560
+ function loadRouter(cwd, absoluteRouterPath, routerExport) {
561
+ let module;
562
+ try {
563
+ const jiti = createJiti(cwd, { interopDefault: true, moduleCache: false });
564
+ module = jiti(absoluteRouterPath);
565
+ } catch (error) {
566
+ const message = error instanceof Error ? error.message : String(error);
567
+ throw new ContractGeneratorError(
568
+ `Failed to load ${relative(cwd, absoluteRouterPath)}: ${message}
569
+
570
+ The contract is read from the loaded router, so a route module must be importable without side effects. Check that nothing at module scope opens a connection or reads a missing environment value.`
571
+ );
572
+ }
573
+ const candidates = routerExport ? [routerExport] : ["appRouter", "default", "router"];
574
+ for (const name of candidates) {
575
+ const candidate = module[name];
576
+ if (isRouter(candidate)) {
577
+ return candidate;
578
+ }
579
+ }
580
+ throw new ContractGeneratorError(
581
+ `No router found in ${relative(cwd, absoluteRouterPath)}. Looked for: ${candidates.join(", ")}. Set "routerExport" to the export holding the defineRouter() result.`
582
+ );
583
+ }
584
+ function pinNodeEnv() {
585
+ if (!process.env.NODE_ENV) {
586
+ process.env.NODE_ENV = "production";
587
+ genLogger2.info('NODE_ENV was unset; pinned to "production" so the contract does not depend on the shell');
588
+ }
589
+ }
590
+ function createContractGenerator(config) {
591
+ const {
592
+ routerPath,
593
+ routerExport,
594
+ outputDir = "./contracts",
595
+ additionalRouteDirs = []
596
+ } = config;
597
+ if (!routerPath) {
598
+ throw new Error(
599
+ `[@spfn/core:contract] Missing required "routerPath" option.
600
+
601
+ Usage:
602
+ defineGenerator<ContractGeneratorConfig>({
603
+ name: '@spfn/core:contract',
604
+ routerPath: './src/server/router.ts',
605
+ })`
606
+ );
607
+ }
608
+ return {
609
+ name: "@spfn/core:contract",
610
+ watchPatterns: [
611
+ routerPath,
612
+ "src/server/routes/**/*.ts",
613
+ ...additionalRouteDirs.map((dir) => `${dir}/**/*.ts`)
614
+ ],
615
+ runOn: ["watch", "build", "manual"],
616
+ async generate(options) {
617
+ const { cwd } = options;
618
+ const absoluteRouterPath = join(cwd, routerPath);
619
+ const contractsDir = join(cwd, outputDir);
620
+ if (!existsSync(absoluteRouterPath)) {
621
+ throw new ContractGeneratorError(
622
+ `Router file not found: ${routerPath}. The contract generator is configured but has nothing to read.`
623
+ );
624
+ }
625
+ pinNodeEnv();
626
+ assertUnconditionalRegistration(routerPath, readFileSync(absoluteRouterPath, "utf-8"));
627
+ const router = loadRouter(cwd, absoluteRouterPath, routerExport);
628
+ const document = collectContractDocument(router);
629
+ const changed = writeCurrentDocument(contractsDir, document);
630
+ genLogger2.info(
631
+ `${changed ? "Wrote" : "Verified"} ${relative(cwd, join(contractsDir, "current.json"))} (${document.operations.length} contracted operation(s))`
632
+ );
633
+ if (options.trigger?.type !== "build") {
634
+ return;
635
+ }
636
+ const result = checkContract(contractsDir, document);
637
+ for (const warning of result.warnings) {
638
+ genLogger2.warn(warning);
639
+ }
640
+ if (result.violations.length === 0) {
641
+ if (result.baselineVersion) {
642
+ genLogger2.info(`Contract is backward compatible with released ${result.baselineVersion}`);
643
+ }
644
+ return;
645
+ }
646
+ throw new ContractGeneratorError(
647
+ `This build breaks the contract released as ${result.baselineVersion}:
648
+
649
+ ${formatViolations(result.violations)}
650
+
651
+ A released client cannot be fixed by redeploying the server. Keep the promise, or cut a new contract version and let the old operation stay until no released app calls it.`
652
+ );
653
+ }
654
+ };
655
+ }
656
+
461
657
  // src/codegen/generators/index.ts
462
658
  var generators = {
463
- "route-map": createRouteMapGenerator
659
+ "route-map": createRouteMapGenerator,
660
+ contract: createContractGenerator
464
661
  };
465
662
 
466
- export { CodegenOrchestrator, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
663
+ export { CodegenOrchestrator, ConditionalRegistrationError, ContractGeneratorError, assertUnconditionalRegistration, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
467
664
  //# sourceMappingURL=index.js.map
468
665
  //# sourceMappingURL=index.js.map