@spfn/core 0.3.0-beta.27 → 0.3.0-beta.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/index.d.ts +65 -2
- package/dist/codegen/index.js +160 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/contract/index.d.ts +2 -2
- package/dist/db/index.d.ts +39 -11
- package/dist/db/index.js +34 -14
- package/dist/db/index.js.map +1 -1
- package/dist/{error-TsUIjp_x.d.ts → error-CIZlYJlL.d.ts} +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js +10 -0
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/client.js +27 -17
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +3 -3
- package/dist/event/sse/index.js +115 -34
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/ops/index.d.ts +3 -3
- package/dist/ops/index.js +15 -5
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +2 -2
- package/dist/route/index.js +15 -5
- package/dist/route/index.js.map +1 -1
- package/dist/{router-4YoIy5fN.d.ts → router-C57y-vz1.d.ts} +12 -0
- package/dist/{router-D5B0cpVN.d.ts → router-CX5uVjmU.d.ts} +29 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +130 -39
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +73 -11
- package/dist/state/client.js +231 -31
- package/dist/state/client.js.map +1 -1
- package/dist/state/index.js +19 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/react.d.ts +18 -3
- package/dist/state/react.js +11 -5
- package/dist/state/react.js.map +1 -1
- package/dist/surface/index.d.ts +3 -3
- package/dist/surface/index.js +15 -5
- package/dist/surface/index.js.map +1 -1
- package/dist/{types-04gu3uMj.d.ts → types-CurMab2Q.d.ts} +19 -2
- package/dist/{types-CqTsDETV.d.ts → types-DncqXfWN.d.ts} +1 -1
- package/package.json +1 -1
- package/src/codegen/README.md +89 -2
- package/src/db/transaction/README.md +58 -35
- package/src/event/README.md +61 -14
- package/src/state/README.md +228 -35
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/event/sse/bounded-writer.ts","../../../src/event/sse/connection-registry.ts","../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":["logger"],"mappings":";;;;;;;AAgDO,SAAS,mBAAA,CACZ,MAAA,EACA,QAAA,EACA,OAAA,EAEJ;AACI,EAAA,MAAM,UAAsB,EAAC;AAC7B,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,IAAI,MAAA,GAAS,KAAA;AAEb,EAAA,MAAM,QAAQ,YACd;AACI,IAAA,IAAI,YAAY,MAAA,EAChB;AACI,MAAA;AAAA,IACJ;AAEA,IAAA,QAAA,GAAW,IAAA;AAEX,IAAA,OAAO,OAAA,CAAQ,MAAA,GAAS,CAAA,IAAK,CAAC,MAAA,EAC9B;AACI,MAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,EAAM;AAC5B,MAAA,IACA;AACI,QAAA,MAAM,MAAA,CAAO,SAAS,KAAK,CAAA;AAAA,MAC/B,SACO,GAAA,EACP;AACI,QAAA,QAAA,GAAW,KAAA;AACX,QAAA,OAAA,CAAQ,eAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAExD,QAAA;AAAA,MACJ;AAAA,IACJ;AAEA,IAAA,QAAA,GAAW,KAAA;AAAA,EACf,CAAA;AAEA,EAAA,OAAO;AAAA,IACH,QAAQ,KAAA,EACR;AACI,MAAA,IAAI,MAAA,EACJ;AACI,QAAA;AAAA,MACJ;AAEA,MAAA,OAAA,CAAQ,KAAK,KAAK,CAAA;AAIlB,MAAA,IAAI,OAAA,CAAQ,SAAS,QAAA,EACrB;AACI,QAAA,OAAA,CAAQ,yCAAyC,CAAA;AAEjD,QAAA;AAAA,MACJ;AAEA,MAAA,KAAK,KAAA,EAAM;AAAA,IACf,CAAA;AAAA,IAEA,KAAA,GACA;AACI,MAAA,MAAA,GAAS,IAAA;AACT,MAAA,OAAA,CAAQ,MAAA,GAAS,CAAA;AAAA,IACrB,CAAA;AAAA,IAEA,IAAI,MAAA,GACJ;AACI,MAAA,OAAO,OAAA,CAAQ,MAAA;AAAA,IACnB;AAAA,GACJ;AACJ;AClGA,IAAM,cAAA,GAAiB,MAAA,CAAO,KAAA,CAAM,gBAAgB,CAAA;AAG7C,IAAM,oBAAA,GAAuB,sBAAA;AAG7B,IAAM,oBAAA,GAAuB,sBAAA;AA8D7B,SAAS,wBAAA,GAChB;AACI,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAgC;AACtD,EAAA,MAAM,gBAAA,uBAAuB,GAAA,EAAkC;AAM/D,EAAA,MAAM,cAAA,GAAiB,CAAC,OAAA,EAAiB,MAAA,KACzC;AACI,IAAA,MAAM,UAA2B,EAAC;AAClC,IAAA,QAAA,CAAS,gBAAA,EAAkB,SAAS,OAAO,CAAA;AAC3C,IAAA,MAAM,GAAA,GAAM,QAAQ,MAAA,EAAQ,MAAM,cAAc,gBAAA,EAAkB,OAAA,EAAS,OAAO,CAAC,CAAA;AAEnF,IAAA,OAAO;AAAA,MACH,MAAA,EAAQ,GAAA;AAAA,MACR,IAAA,EAAM,CAAC,UAAA,KACP;AACI,QAAA,IAAI,CAAC,KAAI,EACT;AACI,UAAA,OAAO,KAAA;AAAA,QACX;AAEA,QAAA,aAAA,CAAc,YAAY,OAAO,CAAA;AACjC,QAAA,QAAA,CAAS,SAAA,EAAW,SAAS,UAAU,CAAA;AAEvC,QAAA,OAAO,IAAA;AAAA,MACX;AAAA,KACJ;AAAA,EACJ,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS,CAAC,IAAA,EAA0B,OAAA,EAAiB,WAAmB,KAAA,KAC9E;AACI,IAAA,MAAM,OAAA,GAAU,gBAAA,CAAiB,GAAA,CAAI,OAAO,CAAA;AAC5C,IAAA,MAAM,WAAA,GAAc,SAAA,CAAU,GAAA,CAAI,OAAO,CAAA;AACzC,IAAA,OAAA,EAAS,OAAA,CAAQ,aAAW,OAAA,CAAQ,IAAA,CAAK,EAAE,IAAA,EAAM,SAAA,EAAW,KAAA,EAAO,CAAC,CAAA;AACpE,IAAA,WAAA,EAAa,QAAQ,CAAA,UAAA,KACrB;AACI,MAAA,IAAI,cAAA,CAAe,KAAA,EAAO,UAAA,CAAW,OAAO,CAAA,EAC5C;AACI,QAAA,CAAC,IAAA,KAAS,OAAA,GAAU,OAAA,GAAU,QAAA,EAAU,YAAY,SAAS,CAAA;AAAA,MACjE;AAAA,IACJ,CAAC,CAAA;AAED,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,WAAA,EACjB;AACI,MAAA,cAAA,CAAe,MAAM,uEAAA,EAAyE;AAAA,QAC1F,KAAA,EAAO,SAAA;AAAA,QACP;AAAA,OACH,CAAA;AAAA,IACL;AAAA,EACJ,CAAA;AAEA,EAAA,OAAO;AAAA,IACH,cAAA;AAAA,IAEA,MAAA,EAAQ,CAAC,UAAA,KACT;AACI,MAAA,IAAI,WAAW,OAAA,EACf;AACI,QAAA,aAAA,CAAc,SAAA,EAAW,UAAA,CAAW,OAAA,EAAS,UAAU,CAAA;AAAA,MAC3D;AAAA,IACJ,CAAA;AAAA;AAAA,IAGA,OAAO,CAAC,OAAA,KAA4B,UAAU,GAAA,CAAI,OAAO,GAAG,IAAA,IAAQ,CAAA;AAAA,IAEpE,cAAc,CAAC,OAAA,KAA4B,iBAAiB,GAAA,CAAI,OAAO,GAAG,IAAA,IAAQ,CAAA;AAAA,IAElF,KAAA,EAAO,CAAC,OAAA,EAAiB,SAAA,EAAmB,UACxC,MAAA,CAAO,OAAA,EAAS,OAAA,EAAS,SAAA,EAAW,KAAK,CAAA;AAAA,IAE7C,MAAA,EAAQ,CAAC,OAAA,EAAiB,SAAA,EAAmB,UACzC,MAAA,CAAO,QAAA,EAAU,OAAA,EAAS,SAAA,EAAW,KAAK;AAAA,GAClD;AACJ;AAQA,SAAS,OAAA,CAAQ,QAAqB,MAAA,EACtC;AACI,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,MAAM,MAAM,MACZ;AACI,IAAA,IAAI,KAAA,EACJ;AACI,MAAA,OAAO,KAAA;AAAA,IACX;AAEA,IAAA,KAAA,GAAQ,IAAA;AACR,IAAA,MAAA,CAAO,mBAAA,CAAoB,SAAS,GAAG,CAAA;AACvC,IAAA,MAAA,EAAO;AAEP,IAAA,OAAO,IAAA;AAAA,EACX,CAAA;AAEA,EAAA,MAAA,CAAO,iBAAiB,OAAA,EAAS,GAAA,EAAK,EAAE,IAAA,EAAM,MAAM,CAAA;AACpD,EAAA,IAAI,OAAO,OAAA,EACX;AACI,IAAA,GAAA,EAAI;AAAA,EACR;AAEA,EAAA,OAAO,GAAA;AACX;AAEA,SAAS,QAAA,CAAY,GAAA,EAA0B,GAAA,EAAa,KAAA,EAC5D;AACI,EAAA,MAAM,MAAM,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA,wBAAS,GAAA,EAAO;AACvC,EAAA,GAAA,CAAI,IAAI,KAAK,CAAA;AACb,EAAA,GAAA,CAAI,GAAA,CAAI,KAAK,GAAG,CAAA;AACpB;AAGA,SAAS,aAAA,CAAiB,GAAA,EAA0B,GAAA,EAAa,KAAA,EACjE;AACI,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AACvB,EAAA,GAAA,EAAK,OAAO,KAAK,CAAA;AAEjB,EAAA,IAAI,GAAA,EAAK,SAAS,CAAA,EAClB;AACI,IAAA,GAAA,CAAI,OAAO,GAAG,CAAA;AAAA,EAClB;AACJ;AAOO,SAAS,cAAA,CAAe,OAAsC,OAAA,EACrE;AACI,EAAA,OAAO,KAAA,KAAU,UAAa,MAAA,CAAO,OAAA,CAAQ,MAAM,KAAK,CAAA,CAAE,KAAA,CAAM,CAAC,CAAC,KAAA,EAAO,KAAK,CAAA,KAC1E,MAAA,CAAO,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,KAAK,KAAK,OAAA,CAAQ,KAAA,CAAM,KAAK,CAAA,KAAM,KAAK,CAAA;AAC7E;AAQA,SAAS,aAAA,CAAc,YAA2B,OAAA,EAClD;AACI,EAAA,OAAA,CAAQ,QAAQ,CAAC,EAAE,IAAA,EAAM,SAAA,EAAW,OAAM,KAC1C;AACI,IAAA,IAAI,CAAC,cAAA,CAAe,KAAA,EAAO,UAAA,CAAW,OAAO,CAAA,EAC7C;AACI,MAAA;AAAA,IACJ;AAEA,IAAA,MAAM,CAAC,IAAA,EAAM,EAAE,CAAA,GAAI,IAAA,KAAS,UACtB,CAAC,UAAA,CAAW,MAAA,EAAQ,UAAA,CAAW,OAAO,CAAA,GACtC,CAAC,UAAA,CAAW,OAAA,EAAS,WAAW,MAAM,CAAA;AAE5C,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,SAAS,CAAA,EACzB;AACI,MAAA,EAAA,CAAG,IAAI,SAAS,CAAA;AAAA,IACpB;AAAA,EACJ,CAAC,CAAA;AACL;AAKA,SAAS,OAAA,CAAQ,YAA2B,SAAA,EAC5C;AACI,EAAA,IAAI,CAAC,UAAA,CAAW,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA,EACpC;AACI,IAAA;AAAA,EACJ;AAEA,EAAA,UAAA,CAAW,aAAa,GAAA,CAAI,SAAA,EAAW,UAAA,CAAW,SAAA,CAAU,SAAS,CAAC,CAAA;AACtE,EAAA,UAAA,CAAW,MAAA,CAAO,OAAO,SAAS,CAAA;AAClC,EAAA,UAAA,CAAW,OAAA,CAAQ,IAAI,SAAS,CAAA;AAChC,EAAA,UAAA,CAAW,MAAA,CAAO,sBAAsB,SAAS,CAAA;AACrD;AAQA,SAAS,QAAA,CAAS,YAA2B,SAAA,EAC7C;AACI,EAAA,IAAI,CAAC,UAAA,CAAW,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA,EACrC;AACI,IAAA;AAAA,EACJ;AAEA,EAAA,UAAA,CAAW,YAAA,CAAa,GAAA,CAAI,SAAS,CAAA,IAAI;AACzC,EAAA,UAAA,CAAW,YAAA,CAAa,OAAO,SAAS,CAAA;AACxC,EAAA,UAAA,CAAW,OAAA,CAAQ,OAAO,SAAS,CAAA;AACnC,EAAA,UAAA,CAAW,MAAA,CAAO,IAAI,SAAS,CAAA;AAC/B,EAAA,UAAA,CAAW,MAAA,CAAO,sBAAsB,SAAS,CAAA;AACrD;AAMO,SAAS,eAAe,UAAA,EAC/B;AACI,EAAA,UAAA,CAAW,YAAA,CAAa,OAAA,CAAQ,CAAA,WAAA,KAAe,WAAA,EAAa,CAAA;AAC5D,EAAA,UAAA,CAAW,aAAa,KAAA,EAAM;AAClC;;;AC5QA,IAAM,SAAA,GAAYA,MAAAA,CAAO,KAAA,CAAM,gBAAgB,CAAA;AAU/C,IAAM,UAAA,uBAAiB,OAAA,EAAqC;AAErD,SAAS,cAAA,CAAe,WAAmB,OAAA,EAClD;AACI,EAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAO,OAAA,KAAY,QAAA,EAC3C;AACI,IAAA,OAAO,KAAK,SAAA,CAAU,EAAE,OAAO,SAAA,EAAW,IAAA,EAAM,SAAS,CAAA;AAAA,EAC7D;AAEA,EAAA,IAAI,OAAA,GAAU,UAAA,CAAW,GAAA,CAAI,OAAO,CAAA;AACpC,EAAA,IAAI,CAAC,OAAA,EACL;AACI,IAAA,OAAA,uBAAc,GAAA,EAAoB;AAClC,IAAA,UAAA,CAAW,GAAA,CAAI,SAAS,OAAO,CAAA;AAAA,EACnC;AAEA,EAAA,IAAI,UAAA,GAAa,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA;AACtC,EAAA,IAAI,eAAe,MAAA,EACnB;AACI,IAAA,UAAA,GAAa,KAAK,SAAA,CAAU,EAAE,OAAO,SAAA,EAAW,IAAA,EAAM,SAAS,CAAA;AAC/D,IAAA,OAAA,CAAQ,GAAA,CAAI,WAAW,UAAU,CAAA;AAAA,EACrC;AAEA,EAAA,OAAO,UAAA;AACX;AAGA,IAAM,iBAAA,GAAoB,GAAA;AAgCnB,SAAS,gBAAA,CACZ,MAAA,EACA,MAAA,GAA2B,IAC3B,YAAA,EAEJ;AACI,EAAA,MAAM;AAAA;AAAA;AAAA;AAAA,IAIF,YAAA,GAAe,GAAA;AAAA,IACf,IAAA,EAAM,UAAA;AAAA,IACN,QAAA,GAAW;AAAA,GACf,GAAI,MAAA;AAEJ,EAAA,MAAM,WAAW,wBAAA,EAAyB;AAC1C,EAAA,4BAAA,CAA6B,QAAQ,QAAQ,CAAA;AAE7C,EAAA,MAAM,eAA6B,EAAE,MAAA,EAAQ,UAAA,EAAY,QAAA,EAAU,cAAc,QAAA,EAAS;AAE1F,EAAA,MAAM,OAAA,GAAU,OAAO,CAAA,KACvB;AACI,IAAA,MAAM,SAAA,GAAY,MAAM,eAAA,CAAgB,CAAA,EAAG,cAAc,YAAY,CAAA;AACrE,IAAA,IAAI,qBAAqB,QAAA,EACzB;AACI,MAAA,OAAO,SAAA;AAAA,IACX;AAEA,IAAA,IAAI,CAAA,CAAE,GAAA,CAAI,GAAA,CAAI,MAAA,CAAO,OAAA,EACrB;AAGI,MAAA,SAAA,CAAU,SAAS,MAAA,EAAO;AAE1B,MAAA,OAAO,IAAI,QAAA,CAAS,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAK,CAAA;AAAA,IAC7C;AAGA,IAAA,CAAA,CAAE,MAAA,CAAO,qBAAqB,IAAI,CAAA;AAElC,IAAA,OAAO,SAAA,CAAU,CAAA,EAAG,CAAA,MAAA,KAAU,SAAA,CAAU,CAAA,EAAG,QAAQ,SAAA,EAAW,YAAY,CAAA,EAAG,OAAO,GAAA,KACpF;AACI,MAAA,SAAA,CAAU,MAAM,kBAAA,EAAoB;AAAA,QAChC,OAAO,GAAA,CAAI;AAAA,OACd,CAAA;AAAA,IACL,CAAC,CAAA;AAAA,EACL,CAAA;AAEA,EAAA,OAAO,MAAA,CAAO,OAAO,OAAA,EAAS;AAAA;AAAA,IAE1B,gBAAA,EAAkB,CAAC,OAAA,KAA4B,QAAA,CAAS,MAAM,OAAO,CAAA;AAAA;AAAA,IAGrE,kBAAA,EAAoB,CAAC,OAAA,KAA4B,QAAA,CAAS,aAAa,OAAO;AAAA,GACjF,CAAA;AACL;AAiCA,SAAS,4BAAA,CAA6B,QAA6B,QAAA,EACnE;AACI,EAAA,KAAA,MAAW,SAAA,IAAa,OAAO,UAAA,EAC/B;AACI,IAAA,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA,CAAE,4BAAA,CAA6B;AAAA,MAClD,KAAA,EAAO,CAAC,OAAA,EAAiB,KAAA,KAA8B,SAAS,KAAA,CAAM,OAAA,EAAS,WAAW,KAAK,CAAA;AAAA,MAC/F,MAAA,EAAQ,CAAC,OAAA,EAAiB,KAAA,KAA8B,SAAS,MAAA,CAAO,OAAA,EAAS,WAAW,KAAK;AAAA,KACpG,CAAA;AAAA,EACL;AACJ;AAKA,eAAe,eAAA,CACX,CAAA,EACA,MAAA,EACA,YAAA,EAEJ;AACI,EAAA,MAAM,EAAE,MAAA,EAAQ,UAAA,EAAY,QAAA,EAAS,GAAI,MAAA;AAGzC,EAAA,MAAM,OAAA,GAAU,MAAM,iBAAA,CAAkB,CAAA,EAAG,YAAY,CAAA;AACvD,EAAA,IAAI,YAAY,KAAA,EAChB;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,yBAAA,IAA6B,GAAG,CAAA;AAAA,EAC3D;AACA,EAAA,IAAI,YAAY,IAAA,EAChB;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,0BAAA,IAA8B,GAAG,CAAA;AAAA,EAC5D;AACA,EAAA,IAAI,OAAA,EACJ;AACI,IAAA,CAAA,CAAE,GAAA,CAAI,cAAc,OAAO,CAAA;AAAA,EAC/B;AAGA,EAAA,MAAM,SAAA,GAAY,qBAAqB,CAAC,CAAA;AACxC,EAAA,IAAI,CAAC,SAAA,EACL;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,0BAAA,IAA8B,GAAG,CAAA;AAAA,EAC5D;AAEA,EAAA,MAAM,kBAAkB,MAAA,CAAO,UAAA;AAC/B,EAAA,MAAM,aAAA,GAAgB,UAAU,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,eAAA,CAAgB,QAAA,CAAS,CAAC,CAAC,CAAA;AACxE,EAAA,IAAI,aAAA,CAAc,SAAS,CAAA,EAC3B;AACI,IAAA,OAAO,EAAE,IAAA,CAAK;AAAA,MACV,KAAA,EAAO,qBAAA;AAAA,MACP,aAAA;AAAA,MACA,WAAA,EAAa;AAAA,OACd,GAAG,CAAA;AAAA,EACV;AAEA,EAAA,OAAO,YAAA;AAAA,IACH,OAAA,GAAU,SAAS,cAAA,CAAe,OAAA,EAAS,EAAE,GAAA,CAAI,GAAA,CAAI,MAAM,CAAA,GAAI,MAAA;AAAA,IAC/D,MAAM,mBAAA,CAAoB,CAAA,EAAG,MAAA,EAAQ,UAAA,EAAY,SAAS,SAAS;AAAA,GACvE;AACJ;AASA,eAAe,YAAA,CACX,SACA,KAAA,EAEJ;AACI,EAAA,IAAI,OAAA;AAEJ,EAAA,IACA;AACI,IAAA,OAAA,GAAU,MAAM,KAAA,EAAM;AAEtB,IAAA,OAAO,mBAAmB,QAAA,GAAW,OAAA,GAAU,EAAE,GAAG,SAAS,OAAA,EAAQ;AAAA,EACzE,CAAA,SACA;AAEI,IAAA,IAAI,CAAC,OAAA,IAAW,OAAA,YAAmB,QAAA,EACnC;AACI,MAAA,OAAA,EAAS,MAAA,EAAO;AAAA,IACpB;AAAA,EACJ;AACJ;AAOA,eAAe,mBAAA,CACX,CAAA,EACA,MAAA,EACA,UAAA,EACA,SACA,SAAA,EAEJ;AAEI,EAAA,MAAM,UAAA,GAAa,MAAM,eAAA,CAAgB,OAAA,EAAS,WAAW,UAAU,CAAA;AACvE,EAAA,IAAI,eAAe,IAAA,EACnB;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,yCAAA,IAA6C,GAAG,CAAA;AAAA,EAC3E;AAGA,EAAA,MAAM,OAAA,GAA4B,EAAE,KAAA,EAAO,CAAA,CAAE,GAAA,CAAI,KAAA,EAAM,EAAG,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,MAAA,EAAO,EAAE;AAClF,EAAA,MAAM,QAAA,GAAW,MAAM,0BAAA,CAA2B,OAAA,EAAS,SAAS,UAAU,CAAA;AAC9E,EAAA,IAAI,CAAC,QAAA,EACL;AACI,IAAA,OAAO,EAAE,IAAA,CAAK,EAAE,KAAA,EAAO,iCAAA,IAAqC,GAAG,CAAA;AAAA,EACnE;AAEA,EAAA,MAAM,OAAA,GAAU,OAAA,GACV,MAAM,iBAAA,CAAkB,MAAA,EAAQ,UAAA,EAAY,EAAE,OAAA,EAAS,SAAA,EAAW,QAAA,CAAS,SAAA,EAAW,OAAA,EAAS,CAAA,GAC/F,UAAA;AACN,EAAA,MAAM,MAAA,GAAS,UAAU,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,OAAA,CAAQ,QAAA,CAAS,CAAC,CAAC,CAAA;AAEzD,EAAA,SAAA,CAAU,MAAM,0BAAA,EAA4B;AAAA,IACxC,MAAA,EAAQ,OAAA;AAAA,IACR,YAAA,EAAc,MAAA;AAAA,IACd,SAAS,OAAA,IAAW,MAAA;AAAA,IACpB,QAAA,EAAU,EAAE,GAAA,CAAI,MAAA,CAAO,iBAAiB,CAAA,IAAK,CAAA,CAAE,GAAA,CAAI,MAAA,CAAO,WAAW;AAAA,GACxE,CAAA;AAED,EAAA,OAAO,EAAE,OAAA,EAAS,SAAA,EAAW,OAAA,EAAS,QAAQ,OAAA,EAAQ;AAC1D;AAOA,eAAe,0BAAA,CACX,OAAA,EACA,OAAA,EACA,UAAA,EAEJ;AACI,EAAA,IAAI,CAAC,OAAA,IAAW,CAAC,UAAA,EAAY,gBAAA,EAC7B;AACI,IAAA,OAAO,EAAE,WAAW,MAAA,EAAU;AAAA,EAClC;AAEA,EAAA,IACA;AACI,IAAA,OAAO,EAAE,SAAA,EAAW,MAAM,WAAW,gBAAA,CAAiB,OAAA,EAAS,OAAO,CAAA,EAAE;AAAA,EAC5E,CAAA,CAAA,MAEA;AACI,IAAA,SAAA,CAAU,IAAA,CAAK,+DAAA,EAA4D,EAAE,OAAA,EAAS,CAAA;AAEtF,IAAA,OAAO,IAAA;AAAA,EACX;AACJ;AAMA,eAAe,iBAAA,CACX,MAAA,EACA,MAAA,EACA,GAAA,EAEJ;AACI,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,GAAA,CAAI,OAAO,GAAA,CAAI,CAAA,SAAA,KAAa,eAAA,CAAgB,SAAA,EAAW,OAAO,MAAA,CAAO,SAAS,CAAA,EAAG,GAAG,CAAC,CAAC,CAAA;AAErH,EAAA,OAAO,OAAO,MAAA,CAAO,CAAC,GAAG,CAAA,KAAM,QAAA,CAAS,CAAC,CAAC,CAAA;AAC9C;AAMA,eAAe,eAAA,CACX,SAAA,EACA,QAAA,EACA,GAAA,EAEJ;AACI,EAAA,IAAI,CAAC,QAAA,EACL;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,IAAI,CAAC,SAAS,aAAA,EACd;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,IACA;AACI,IAAA,OAAO,MAAM,QAAA,CAAS,aAAA,CAAc,GAAG,CAAA,KAAM,IAAA;AAAA,EACjD,CAAA,CAAA,MAEA;AACI,IAAA,SAAA,CAAU,KAAK,+DAAA,EAA4D;AAAA,MACvE,KAAA,EAAO,SAAA;AAAA,MACP,SAAS,GAAA,CAAI;AAAA,KAChB,CAAA;AAED,IAAA,OAAO,KAAA;AAAA,EACX;AACJ;AAUA,eAAe,SAAA,CACX,CAAA,EACA,MAAA,EACA,SAAA,EACA,MAAA,EAEJ;AACI,EAAA,MAAM,EAAE,OAAA,EAAS,MAAA,EAAQ,OAAA,EAAS,SAAQ,GAAI,SAAA;AAC9C,EAAA,IAAI,cAAA,GAAiB,KAAA;AACrB,EAAA,IAAI,SAAA;AAGJ,EAAA,IAAI,QAAA;AAEJ,EAAA,MAAM,OAAA,GAAU,CAAC,MAAA,KACjB;AACI,IAAA,IAAI,cAAA,EAAgB;AACpB,IAAA,cAAA,GAAiB,IAAA;AACjB,IAAA,aAAA,CAAc,SAAS,CAAA;AACvB,IAAA,cAAA,CAAe,UAAU,CAAA;AACzB,IAAA,MAAA,CAAO,QAAA,CAAS,OAAO,UAAU,CAAA;AACjC,IAAA,MAAA,CAAO,KAAA,EAAM;AACb,IAAA,QAAA,IAAW;AACX,IAAA,SAAA,CAAU,KAAK,gCAAA,EAAkC;AAAA,MAC7C,MAAA,EAAQ,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA;AAAA,MAC9B;AAAA,KACH,CAAA;AAAA,EACL,CAAA;AAIA,EAAA,MAAM,SAAS,mBAAA,CAAoB,MAAA,EAAQ,MAAA,CAAO,QAAA,EAAU,CAAC,MAAA,KAC7D;AACI,IAAA,SAAA,CAAU,IAAA,CAAK,uBAAA,EAAyB,EAAE,MAAA,EAAQ,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA,EAAG,MAAA,EAAQ,CAAA;AACnF,IAAA,OAAA,CAAQ,MAAM,CAAA;AAAA,EAClB,CAAC,CAAA;AAED,EAAA,MAAM,UAAA,GAA4B;AAAA,IAC9B,OAAA;AAAA,IACA,WAAW,SAAA,CAAU,SAAA;AAAA,IACrB,OAAA,EAAS,IAAI,GAAA,CAAI,OAAO,CAAA;AAAA,IACxB,MAAA,EAAQ,IAAI,GAAA,CAAI,MAAM,CAAA;AAAA,IACtB,YAAA,sBAAkB,GAAA,EAAI;AAAA,IACtB,OAAA;AAAA,IACA,WAAW,qBAAA,CAAsB;AAAA,MAC7B,QAAQ,MAAA,CAAO,MAAA;AAAA,MACf,YAAY,MAAA,CAAO,UAAA;AAAA,MACnB,OAAA;AAAA,MACA,OAAA;AAAA,MACA,MAAA;AAAA,MACA,QAAQ,MAAM;AAAA,KACjB,CAAA;AAAA,IACD,MAAA,EAAQ,CAAC,KAAA,EAAO,SAAA,KAChB;AACI,MAAA,IAAI,cAAA,EAAgB;AAGpB,MAAA,MAAA,CAAO,OAAA,CAAQ,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,SAAA,EAAW,CAAA,EAAG,CAAA;AAAA,IAC/E;AAAA,GACJ;AAOA,EAAA,IAAI,SAAA,CAAU,OAAA,EAAS,IAAA,CAAK,UAAU,MAAM,KAAA,EAC5C;AACI,IAAA,OAAA,CAAQ,0BAA0B,CAAA;AAElC,IAAA;AAAA,EACJ;AAEA,EAAA,iBAAA,CAAkB,YAAY,MAAM,CAAA;AAGpC,EAAA,SAAA,GAAY,YAAY,MACxB;AACI,IAAA,IAAI,cAAA,EAAgB;AAEpB,IAAA,MAAA,CAAO,OAAA,CAAQ;AAAA,MACX,KAAA,EAAO,MAAA;AAAA,MACP,IAAA,EAAM,KAAK,SAAA,CAAU,EAAE,WAAW,IAAA,CAAK,GAAA,IAAO;AAAA,KACjD,CAAA;AAAA,EACL,CAAA,EAAG,OAAO,YAAY,CAAA;AAEtB,EAAA,MAAM,iBAAA,CAAkB,EAAE,GAAA,CAAI,GAAA,CAAI,QAAQ,MAAM,cAAA,EAAgB,CAAC,OAAA,KACjE;AACI,IAAA,QAAA,GAAW,OAAA;AAAA,EACf,CAAC,CAAA;AAGD,EAAA,OAAA,EAAQ;AACZ;AAKA,SAAS,iBAAA,CAAkB,YAA2B,MAAA,EACtD;AACI,EAAA,MAAA,CAAO,OAAA,CAAQ;AAAA,IACX,KAAA,EAAO,WAAA;AAAA,IACP,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,MACjB,gBAAA,EAAkB,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA;AAAA,MACxC,YAAA,EAAc,CAAC,GAAG,UAAA,CAAW,MAAM,CAAA;AAAA,MACnC,SAAA,EAAW,KAAK,GAAA;AAAI,KACvB;AAAA,GACJ,CAAA;AAED,EAAA,KAAA,MAAW,SAAA,IAAa,WAAW,OAAA,EACnC;AACI,IAAA,UAAA,CAAW,aAAa,GAAA,CAAI,SAAA,EAAW,UAAA,CAAW,SAAA,CAAU,SAAS,CAAC,CAAA;AAAA,EAC1E;AAEA,EAAA,SAAA,CAAU,KAAK,4BAAA,EAA8B;AAAA,IACzC,MAAA,EAAQ,CAAC,GAAG,UAAA,CAAW,OAAO,CAAA;AAAA,IAC9B,YAAA,EAAc,CAAC,GAAG,UAAA,CAAW,MAAM,CAAA;AAAA,IACnC,iBAAA,EAAmB,WAAW,YAAA,CAAa;AAAA,GAC9C,CAAA;AACL;AAOA,eAAe,iBAAA,CACX,WAAA,EACA,MAAA,EACA,aAAA,EAEJ;AACI,EAAA,IAAI,WAAA,CAAY,OAAA,IAAW,MAAA,EAAO,EAClC;AACI,IAAA;AAAA,EACJ;AAEA,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,KACzB;AACI,IAAA,MAAM,OAAA,GAAU,MAAM,OAAA,EAAQ;AAC9B,IAAA,WAAA,CAAY,iBAAiB,OAAA,EAAS,OAAA,EAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAE7D,IAAA,aAAA,CAAc,MACd;AACI,MAAA,WAAA,CAAY,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAChD,MAAA,OAAA,EAAQ;AAAA,IACZ,CAAC,CAAA;AAAA,EACL,CAAC,CAAA;AACL;AAoBA,SAAS,sBAAsB,GAAA,EAC/B;AACI,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,OAAO,CAAC,SAAA,KACR;AACI,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA;AAC5C,IAAA,IAAI,MAAA,GAAS,IAAA;AAEb,IAAA,MAAM,WAAA,GAAc,QAAA,CAAS,SAAA,CAAU,CAAC,OAAA,KACxC;AACI,MAAA,IAAI,CAAC,MAAA,IAAU,GAAA,CAAI,MAAA,EAAO,IAAK,CAAC,kBAAA,CAAmB,SAAA,EAAW,QAAA,EAAU,OAAA,EAAS,GAAG,CAAA,EACpF;AACI,QAAA;AAAA,MACJ;AAEA,MAAA,SAAA,EAAA;AAEA,MAAA,SAAA,CAAU,MAAM,mBAAA,EAAqB;AAAA,QACjC,KAAA,EAAO,SAAA;AAAA,QACP;AAAA,OACH,CAAA;AAED,MAAA,GAAA,CAAI,OAAO,OAAA,CAAQ;AAAA,QACf,EAAA,EAAI,OAAO,SAAS,CAAA;AAAA,QACpB,KAAA,EAAO,SAAA;AAAA,QACP,IAAA,EAAM,cAAA,CAAe,SAAA,EAAW,OAAO;AAAA,OAC1C,CAAA;AAAA,IACL,CAAC,CAAA;AAED,IAAA,OAAO,MACP;AACI,MAAA,MAAA,GAAS,KAAA;AACT,MAAA,WAAA,EAAY;AAAA,IAChB,CAAA;AAAA,EACJ,CAAA;AACJ;AAMA,SAAS,kBAAA,CACL,SAAA,EACA,QAAA,EACA,OAAA,EACA,GAAA,EAEJ;AACI,EAAA,MAAM,YAAA,GAAe,GAAA,CAAI,UAAA,EAAY,MAAA,GAAS,SAAS,CAAA;AACvD,EAAA,IAAI,GAAA,CAAI,WAAW,YAAA,IAAgB,CAAC,aAAa,GAAA,CAAI,OAAA,EAAS,OAAO,CAAA,EACrE;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,IAAI,CAAC,SAAS,OAAA,EACd;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,IACA;AACI,IAAA,OAAO,QAAA,CAAS,OAAA,CAAQ,OAAA,EAAS,EAAE,OAAA,EAAS,GAAA,CAAI,OAAA,EAAS,OAAA,EAAS,GAAA,CAAI,OAAA,EAAS,CAAA,KAAM,IAAA;AAAA,EACzF,CAAA,CAAA,MAEA;AACI,IAAA,SAAA,CAAU,IAAA,CAAK,+CAA0C,EAAE,KAAA,EAAO,WAAW,OAAA,EAAS,GAAA,CAAI,SAAS,CAAA;AAEnG,IAAA,OAAO,KAAA;AAAA,EACX;AACJ;AAWA,eAAe,iBAAA,CACX,GACA,YAAA,EAEJ;AACI,EAAA,IAAI,CAAC,YAAA,EACL;AACI,IAAA,OAAO,MAAA;AAAA,EACX;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,GAAA,CAAI,KAAA,CAAM,OAAO,CAAA;AACjC,EAAA,IAAI,CAAC,KAAA,EACL;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,OAAO,MAAM,YAAA,CAAa,MAAA,CAAO,KAAK,CAAA;AAC1C;AAKA,SAAS,qBAAqB,CAAA,EAC9B;AACI,EAAA,MAAM,WAAA,GAAc,CAAA,CAAE,GAAA,CAAI,KAAA,CAAM,QAAQ,CAAA;AACxC,EAAA,IAAI,CAAC,WAAA,EACL;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAGA,EAAA,OAAO,CAAC,GAAG,IAAI,GAAA,CAAI,YAAY,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,EAAM,CAAC,CAAC,CAAA;AACjE;AAMA,eAAe,eAAA,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,IAAI,OAAA,CAAQ,WAAW,CAAA,EACvB;AACI,IAAA,OAAO,IAAA;AAAA,EACX;AAEA,EAAA,OAAO,OAAA;AACX;AC3qBA,SAAS,UAAU,KAAA,EACnB;AACI,EAAA,OAAO,WAAW,QAAQ,CAAA,CAAE,OAAO,KAAK,CAAA,CAAE,OAAO,KAAK,CAAA;AAC1D;AAqEA,IAAM,qBAAN,MACA;AAAA,EACY,MAAA,uBAAa,GAAA,EAAsB;AAAA,EAE3C,MAAM,GAAA,CAAI,KAAA,EAAe,IAAA,EACzB;AACI,IAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,SAAA,CAAU,KAAK,GAAG,IAAI,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAM,QAAQ,KAAA,EACd;AACI,IAAA,MAAM,GAAA,GAAM,UAAU,KAAK,CAAA;AAC3B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAChC,IAAA,IAAI,CAAC,IAAA,EACL;AACI,MAAA,OAAO,IAAA;AAAA,IACX;AAEA,IAAA,IAAA,CAAK,MAAA,CAAO,OAAO,GAAG,CAAA;AAEtB,IAAA,OAAO,IAAA;AAAA,EACX;AAAA,EAEA,MAAM,OAAA,GACN;AACI,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AAErB,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,IAAI,CAAA,IAAK,KAAK,MAAA,EAC/B;AACI,MAAA,IAAI,IAAA,CAAK,aAAa,GAAA,EACtB;AACI,QAAA,IAAA,CAAK,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,MAC1B;AAAA,IACJ;AAAA,EACJ;AACJ,CAAA;AAuBO,IAAM,kBAAN,MACP;AAAA,EAGI,YAAoB,KAAA,EACpB;AADoB,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAAA,EACnB;AAAA,EAHO,MAAA,GAAS,YAAA;AAAA,EAKjB,MAAM,GAAA,CAAI,KAAA,EAAe,IAAA,EACzB;AACI,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,IAAA,CAAA,CAAM,IAAA,CAAK,SAAA,GAAY,IAAA,CAAK,GAAA,EAAI,IAAK,GAAI,CAAC,CAAA;AAC9E,IAAA,MAAM,KAAK,KAAA,CAAM,GAAA;AAAA,MACb,IAAA,CAAK,MAAA,GAAS,SAAA,CAAU,KAAK,CAAA;AAAA,MAC7B,IAAA,CAAK,UAAU,IAAI,CAAA;AAAA,MACnB,IAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AAAA,EAEA,MAAM,QAAQ,KAAA,EACd;AACI,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,MAAA,GAAS,SAAA,CAAU,KAAK,CAAA;AAGzC,IAAA,IAAI,GAAA,GAAqB,IAAA;AAEzB,IAAA,IAAI,IAAA,CAAK,MAAM,MAAA,EACf;AACI,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA;AAAA,IACrC,CAAA,MAEA;AACI,MAAA,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC9B,MAAA,IAAI,GAAA,EACJ;AACI,QAAA,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAAA,MAC5B;AAAA,IACJ;AAEA,IAAA,IAAI,CAAC,GAAA,EACL;AACI,MAAA,OAAO,IAAA;AAAA,IACX;AAEA,IAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACzB;AAAA,EAEA,MAAM,OAAA,GACN;AAAA,EAEA;AACJ;AAMO,IAAM,kBAAN,MACP;AAAA,EACY,KAAA;AAAA,EACA,GAAA;AAAA,EACA,YAAA,GAAsD,IAAA;AAAA,EAE9D,YAAY,MAAA,EACZ;AACI,IAAA,IAAA,CAAK,GAAA,GAAM,QAAQ,GAAA,IAAO,GAAA;AAC1B,IAAA,IAAA,CAAK,KAAA,GAAQ,MAAA,EAAQ,KAAA,IAAS,IAAI,kBAAA,EAAmB;AAErD,IAAA,MAAM,eAAA,GAAkB,QAAQ,eAAA,IAAmB,GAAA;AACnD,IAAA,IAAA,CAAK,YAAA,GAAe,YAAY,MAAM,KAAK,KAAK,KAAA,CAAM,OAAA,IAAW,eAAe,CAAA;AAChF,IAAA,IAAA,CAAK,aAAa,KAAA,EAAM;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,IAAI,KAAA,GACJ;AACI,IAAA,OAAO,IAAA,CAAK,GAAA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,OAAA,EACZ;AACI,IAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,EAAE,CAAA,CAAE,SAAS,KAAK,CAAA;AAG5C,IAAA,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,KAAA,EAAO;AAAA,MACxB,OAAA;AAAA,MACA,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA,CAAK;AAAA,KAChC,CAAA;AAED,IAAA,OAAO,KAAA;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,KAAA,EACb;AACI,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,KAAA,CAAM,QAAQ,KAAK,CAAA;AAE3C,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,SAAA,IAAa,IAAA,CAAK,KAAI,EACxC;AACI,MAAA,OAAO,IAAA;AAAA,IACX;AAEA,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAKA,OAAA,GACA;AACI,IAAA,IAAI,KAAK,YAAA,EACT;AACI,MAAA,aAAA,CAAc,KAAK,YAAY,CAAA;AAC/B,MAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAAA,IACxB;AAAA,EACJ;AACJ","file":"index.js","sourcesContent":["/**\n * Bounded SSE outbound writer\n *\n * Serializes writes to one SSE stream through a single drain loop that `await`s\n * each `writeSSE` — so a slow client applies real backpressure instead of letting\n * frames pile up unbounded in memory (OOM under a fast producer + slow consumer).\n *\n * The queue is per-connection, so a slow stream never blocks other connections.\n * When the queue exceeds `maxQueue`, the connection is closed (via `onClose`)\n * rather than silently dropping frames — dropping a chunk would corrupt an\n * ordered token/chat stream; the client reconnects and re-fetches instead.\n */\n\n/** A frame to write to the SSE stream (subset of Hono's SSEMessage). */\nexport interface SSEFrame\n{\n data: string;\n event?: string;\n id?: string;\n}\n\n/** Minimal stream surface this writer needs (Hono's SSEStreamingApi satisfies it). */\nexport interface SSEWritable\n{\n writeSSE(message: SSEFrame): Promise<void>;\n}\n\nexport interface BoundedWriter\n{\n /** Queue a frame for delivery; closes the connection if the queue overflows. */\n enqueue(frame: SSEFrame): void;\n\n /** Stop draining and drop any queued frames (called on connection cleanup). */\n close(): void;\n\n /** Current queue depth (for tests/diagnostics). */\n readonly queued: number;\n}\n\n/**\n * Create a bounded, backpressure-aware writer for one SSE stream.\n *\n * @param stream the SSE stream (`writeSSE` returns a promise that resolves when\n * the socket has drained — that is what gives us backpressure)\n * @param maxQueue max frames buffered before the connection is closed\n * @param onClose called once when the writer closes the connection (overflow or\n * a write error); the caller cleans up and logs\n */\nexport function createBoundedWriter(\n stream: SSEWritable,\n maxQueue: number,\n onClose: (reason: string) => void,\n): BoundedWriter\n{\n const pending: SSEFrame[] = [];\n let flushing = false;\n let closed = false;\n\n const flush = async (): Promise<void> =>\n {\n if (flushing || closed)\n {\n return;\n }\n\n flushing = true;\n\n while (pending.length > 0 && !closed)\n {\n const frame = pending.shift() as SSEFrame;\n try\n {\n await stream.writeSSE(frame);\n }\n catch (err)\n {\n flushing = false;\n onClose(err instanceof Error ? err.message : String(err));\n\n return;\n }\n }\n\n flushing = false;\n };\n\n return {\n enqueue(frame: SSEFrame): void\n {\n if (closed)\n {\n return;\n }\n\n pending.push(frame);\n\n // Slow consumer: the drain loop can't keep up, so the queue grew past\n // the bound. Close rather than buffer unboundedly or drop silently.\n if (pending.length > maxQueue)\n {\n onClose('outbound queue overflow (slow consumer)');\n\n return;\n }\n\n void flush();\n },\n\n close(): void\n {\n closed = true;\n pending.length = 0;\n },\n\n get queued(): number\n {\n return pending.length;\n },\n };\n}\n","/**\n * SSE connection registry\n *\n * What one `createSSEHandler` knows about its open connections, so that\n * `EventDef.grant` / `EventDef.revoke` can change a connection after it opened.\n * One registry per handler: two handlers in one process (tests) never share one.\n *\n * Neither operation consults `canSubscribe` or `authorize`: they are an\n * operator's statement about an open connection. Permission lives in app data\n * and is re-read at the next connect. A `match` narrows either to the\n * connections whose connect query carries its fields.\n *\n * A connect still reading app data (`authorize`, `resolvePrincipal`,\n * `canSubscribe`) is a pending admission: a `grant` / `revoke` that lands then\n * is recorded on it and applied, without a frame, when the connection opens —\n * so a call the app made is never lost to that window.\n */\n\nimport { logger } from '@spfn/core/logger';\nimport type { SubscribeRequest, SubscriptionMatch } from '../types';\n\nconst registryLogger = logger.child('@spfn/core:sse');\n\n/** Stream-owned frame: an event was activated on the open connection. */\nexport const SUBSCRIPTION_GRANTED = 'subscription.granted';\n\n/** Stream-owned frame: an event was deactivated on the open connection. */\nexport const SUBSCRIPTION_REVOKED = 'subscription.revoked';\n\nexport type SubscriptionFrame = typeof SUBSCRIPTION_GRANTED | typeof SUBSCRIPTION_REVOKED;\n\n/**\n * One open SSE connection. `allowed` and `denied` partition `requested`\n * (plus anything `authorize` added); only `allowed` events hold a live\n * subscription in `unsubscribes`.\n */\nexport interface SSEConnection\n{\n /** `undefined` when the stream runs without auth — such a connection is never registered. */\n readonly subject: string | undefined;\n readonly requested: readonly string[];\n readonly allowed: Set<string>;\n readonly denied: Set<string>;\n readonly unsubscribes: Map<string, () => void>;\n readonly request: SubscribeRequest;\n\n /** The per-event subscribe the connection opened with — `grant` reuses it, never a copy. */\n readonly subscribe: (eventName: string) => () => void;\n\n /** Enqueue a protocol frame about one event (dropped once the connection is dead). */\n readonly notify: (frame: SubscriptionFrame, eventName: string) => void;\n}\n\nexport type ConnectionRegistry = ReturnType<typeof createConnectionRegistry>;\n\ntype SubscriptionChange = 'grant' | 'revoke';\n\n/**\n * A connect of one subject between its first app-data read and its open.\n * The first of `open` / `cancel` ends it; both are synchronous. A client abort\n * cancels it at once — even while an app predicate never settles.\n */\nexport interface PendingAdmission\n{\n /**\n * Apply the recorded changes to `connection`, then swap this entry for the\n * open connection. `false`, with nothing applied or registered, once cancelled.\n */\n readonly open: (connection: SSEConnection) => boolean;\n\n /** Drop this entry (refused, aborted, or thrown); idempotent. */\n readonly cancel: () => void;\n}\n\n/** One `grant` / `revoke` recorded on a pending admission. */\ninterface RecordedChange\n{\n kind: SubscriptionChange;\n eventName: string;\n match?: SubscriptionMatch;\n}\n\n/** The changes recorded on one pending admission, in call order (the last one wins). */\ntype RecordedChanges = RecordedChange[];\n\n/**\n * Create the registry of one SSE handler, keyed by subject → open connections\n * (a subject with two tabs has two).\n */\nexport function createConnectionRegistry()\n{\n const bySubject = new Map<string, Set<SSEConnection>>();\n const pendingBySubject = new Map<string, Set<RecordedChanges>>();\n\n /**\n * Register a pending admission of `subject`; `signal` is the connect's\n * abort signal, listened to until the admission ends either way.\n */\n const beginAdmission = (subject: string, signal: AbortSignal): PendingAdmission =>\n {\n const changes: RecordedChanges = [];\n addToSet(pendingBySubject, subject, changes);\n const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));\n\n return {\n cancel: end,\n open: (connection: SSEConnection) =>\n {\n if (!end())\n {\n return false;\n }\n\n applyRecorded(connection, changes);\n addToSet(bySubject, subject, connection);\n\n return true;\n },\n };\n };\n\n const change = (kind: SubscriptionChange, subject: string, eventName: string, match?: SubscriptionMatch): void =>\n {\n const pending = pendingBySubject.get(subject);\n const connections = bySubject.get(subject);\n pending?.forEach(changes => changes.push({ kind, eventName, match }));\n connections?.forEach(connection =>\n {\n if (matchesRequest(match, connection.request))\n {\n (kind === 'grant' ? grantOn : revokeOn)(connection, eventName);\n }\n });\n\n if (!pending && !connections)\n {\n registryLogger.debug('SSE subscription change for a subject with no open connection ignored', {\n event: eventName,\n subject,\n });\n }\n };\n\n return {\n beginAdmission,\n\n remove: (connection: SSEConnection): void =>\n {\n if (connection.subject)\n {\n removeFromSet(bySubject, connection.subject, connection);\n }\n },\n\n /** Open connections only — a pending admission is not counted. */\n count: (subject: string): number => bySubject.get(subject)?.size ?? 0,\n\n pendingCount: (subject: string): number => pendingBySubject.get(subject)?.size ?? 0,\n\n grant: (subject: string, eventName: string, match?: SubscriptionMatch): void =>\n change('grant', subject, eventName, match),\n\n revoke: (subject: string, eventName: string, match?: SubscriptionMatch): void =>\n change('revoke', subject, eventName, match),\n };\n}\n\n/**\n * The end of one pending admission: `remove` runs on the first call only,\n * which returns `true`; later calls return `false`. An abort of `signal` is a\n * first call; the listener is dropped on every end, so it never outlives the\n * admission (an already aborted signal ends it at once).\n */\nfunction endOnce(signal: AbortSignal, remove: () => void): () => boolean\n{\n let ended = false;\n const end = (): boolean =>\n {\n if (ended)\n {\n return false;\n }\n\n ended = true;\n signal.removeEventListener('abort', end);\n remove();\n\n return true;\n };\n\n signal.addEventListener('abort', end, { once: true });\n if (signal.aborted)\n {\n end();\n }\n\n return end;\n}\n\nfunction addToSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key) ?? new Set<T>();\n set.add(value);\n map.set(key, set);\n}\n\n/** Remove `value` and drop the key once its set is empty, so neither map grows on reconnects. */\nfunction removeFromSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key);\n set?.delete(value);\n\n if (set?.size === 0)\n {\n map.delete(key);\n }\n}\n\n/**\n * Whether `match` reaches a connection: every named field equals the connect\n * query's value, string to string. A field the connection never declared\n * never matches, not even an empty string. No `match` reaches every one.\n */\nexport function matchesRequest(match: SubscriptionMatch | undefined, request: SubscribeRequest): boolean\n{\n return match === undefined || Object.entries(match.query).every(([field, value]) =>\n Object.hasOwn(request.query, field) && request.query[field] === value);\n}\n\n/**\n * Apply what was recorded during admission to a connection that has not\n * opened yet: move the event between `allowed` and `denied`, no frame. An\n * event the connection never requested is in neither set and stays out; a\n * change whose `match` the connection's query does not meet is skipped.\n */\nfunction applyRecorded(connection: SSEConnection, changes: RecordedChanges): void\n{\n changes.forEach(({ kind, eventName, match }) =>\n {\n if (!matchesRequest(match, connection.request))\n {\n return;\n }\n\n const [from, to] = kind === 'grant'\n ? [connection.denied, connection.allowed]\n : [connection.allowed, connection.denied];\n\n if (from.delete(eventName))\n {\n to.add(eventName);\n }\n });\n}\n\n/**\n * Activate a denied event on one connection; allowed or never requested → no change.\n */\nfunction grantOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.denied.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n connection.denied.delete(eventName);\n connection.allowed.add(eventName);\n connection.notify(SUBSCRIPTION_GRANTED, eventName);\n}\n\n/**\n * Deactivate an allowed event on one connection; denied or never requested → no change.\n *\n * A frame the event already enqueued before this call may still be written —\n * the queue is not rewritten — but no frame after the unsubscribe is.\n */\nfunction revokeOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.allowed.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.get(eventName)?.();\n connection.unsubscribes.delete(eventName);\n connection.allowed.delete(eventName);\n connection.denied.add(eventName);\n connection.notify(SUBSCRIPTION_REVOKED, eventName);\n}\n\n/**\n * Drop every live subscription of a closing connection. A revoked event was\n * already removed from `unsubscribes`, so nothing is unsubscribed twice.\n */\nexport function unsubscribeAll(connection: SSEConnection): void\n{\n connection.unsubscribes.forEach(unsubscribe => unsubscribe());\n connection.unsubscribes.clear();\n}\n","/**\n * SSE Handler for Hono\n *\n * Creates SSE stream endpoint for event subscription\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { createSSEHandler } from '@spfn/core/event/sse';\n * import { eventRouter } from './events';\n *\n * const app = new Hono();\n *\n * // GET /events/stream?events=userCreated,orderPlaced\n * app.get('/events/stream', createSSEHandler(eventRouter));\n * ```\n */\n\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport type { SSEStreamingApi } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef } from '../router';\nimport type { EventDef, SubscribeContext, SubscribeRequest, SubscriptionMatch } from '../types';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\nimport { createBoundedWriter } from './bounded-writer';\nimport type { BoundedWriter } from './bounded-writer';\nimport { createConnectionRegistry, unsubscribeAll } from './connection-registry';\nimport type { ConnectionRegistry, PendingAdmission, SSEConnection } from './connection-registry';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n/**\n * Serialize a fan-out frame once per (event, payload).\n *\n * One `emit` delivers the same payload object to every subscribed connection, so\n * without memoization each connection re-`JSON.stringify`s the identical payload\n * — O(N) for N subscribers on a hot broadcast. Keyed by the payload object\n * (WeakMap → auto-GC after the emit); primitives are cheap to stringify directly.\n */\nconst frameCache = new WeakMap<object, Map<string, string>>();\n\nexport function serializeFrame(eventName: string, payload: unknown): string\n{\n if (payload === null || typeof payload !== 'object')\n {\n return JSON.stringify({ event: eventName, data: payload });\n }\n\n let byEvent = frameCache.get(payload);\n if (!byEvent)\n {\n byEvent = new Map<string, string>();\n frameCache.set(payload, byEvent);\n }\n\n let serialized = byEvent.get(eventName);\n if (serialized === undefined)\n {\n serialized = JSON.stringify({ event: eventName, data: payload });\n byEvent.set(eventName, serialized);\n }\n\n return serialized;\n}\n\n/** Default outbound queue cap per connection before a slow consumer is dropped. */\nconst DEFAULT_MAX_QUEUE = 1000;\n\n// Extend Hono context with SSE subject\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n sseSubject?: string;\n }\n}\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\n * - token: One-time auth token (when auth is enabled)\n *\n * Who receives what, in order: the global `authorize` (once, at connect) →\n * each requested event's `.canSubscribe()` (once per event, at connect) → the\n * global `filter` map and the event's `.filter()` (sync, per frame). An event\n * denied at connect stays in `deniedEvents`, so `EventDef.grant` can activate\n * it later; `EventDef.revoke` deactivates an allowed one. The handler\n * registers that control on every event of the router.\n *\n * @example\n * ```typescript\n * app.get('/events/stream', createSSEHandler(eventRouter, {\n * pingInterval: 30000,\n * }));\n * ```\n */\nexport function createSSEHandler<TRouter extends EventRouterDef<any>>(\n router: TRouter,\n config: SSEHandlerConfig = {},\n tokenManager?: SSETokenManager,\n)\n{\n const {\n // 10s — keep-alive가 중간 프록시(GCP LB/nginx)의 idle 타임아웃(흔히 ~15-30s)보다 자주 와야\n // 첫 ping 전 침묵 구간에 연결이 끊기지 않는다. 30s 기본은 idle 타임아웃보다 길어 SSE가\n // 데이터 없는 동안 끊기던 문제가 있었다.\n pingInterval = 10000,\n auth: authConfig,\n maxQueue = DEFAULT_MAX_QUEUE,\n } = config;\n\n const registry = createConnectionRegistry();\n registerSubscriptionControls(router, registry);\n\n const streamConfig: StreamConfig = { router, authConfig, registry, pingInterval, maxQueue };\n\n const handler = async (c: Context) =>\n {\n const admission = await admitConnection(c, streamConfig, tokenManager);\n if (admission instanceof Response)\n {\n return admission;\n }\n\n if (c.req.raw.signal.aborted)\n {\n // The client left during admission: nobody reads this response.\n // Its abort listener already cancelled `pending`; cancel is idempotent.\n admission.pending?.cancel();\n\n return new Response(null, { status: 499 });\n }\n\n // ── 6. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, stream => runStream(c, stream, admission, streamConfig), async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n\n return Object.assign(handler, {\n /** @internal Open registered connections of a subject (tests). */\n _openConnections: (subject: string): number => registry.count(subject),\n\n /** @internal Connects of a subject still in admission (tests). */\n _pendingAdmissions: (subject: string): number => registry.pendingCount(subject),\n });\n}\n\n// ============================================================================\n// Connect\n// ============================================================================\n\n/** What a request that passed every connect check opens the stream with. */\ninterface Admission\n{\n subject: string | undefined;\n requested: string[];\n allowed: string[];\n denied: string[];\n request: SubscribeRequest;\n\n /** Registered before the first app-data read; `runStream` opens it. Absent without a subject. */\n pending?: PendingAdmission;\n}\n\n/** What every stream of one handler shares. */\ninterface StreamConfig\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n registry: ConnectionRegistry;\n pingInterval: number;\n maxQueue: number;\n}\n\n/**\n * Let `EventDef.grant` / `revoke` reach this handler's connections, once per\n * router key (an event under two keys gets two controls, each naming its key).\n */\nfunction registerSubscriptionControls(router: EventRouterDef<any>, registry: ConnectionRegistry): void\n{\n for (const eventName of router.eventNames as string[])\n {\n router.events[eventName]._registerSubscriptionControl({\n grant: (subject: string, match?: SubscriptionMatch) => registry.grant(subject, eventName, match),\n revoke: (subject: string, match?: SubscriptionMatch) => registry.revoke(subject, eventName, match),\n });\n }\n}\n\n/**\n * Run the connect checks; a `Response` is the refusal to return.\n */\nasync function admitConnection(\n c: Context,\n config: StreamConfig,\n tokenManager: SSETokenManager | undefined,\n): Promise<Admission | Response>\n{\n const { router, authConfig, registry } = config;\n\n // ── 1. Token Authentication ──\n const subject = await authenticateToken(c, tokenManager);\n if (subject === false)\n {\n return c.json({ error: 'Missing token parameter' }, 401);\n }\n if (subject === null)\n {\n return c.json({ error: 'Invalid or expired token' }, 401);\n }\n if (subject)\n {\n c.set('sseSubject', subject);\n }\n\n // ── 2-3. Requested events, validated against the router ──\n const requested = parseRequestedEvents(c);\n if (!requested)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requested.filter(e => !validEventNames.includes(e));\n if (invalidEvents.length > 0)\n {\n return c.json({\n error: 'Invalid event names',\n invalidEvents,\n validEvents: validEventNames,\n }, 400);\n }\n\n return whilePending(\n subject ? registry.beginAdmission(subject, c.req.raw.signal) : undefined,\n () => authorizeConnection(c, router, authConfig, subject, requested),\n );\n}\n\n/**\n * Run the app-data checks with the admission already registered — `pending`\n * is written synchronously, before the first `await`, so a `grant` / `revoke`\n * issued meanwhile is recorded on it. A refusal or a throw unregisters it; a\n * client abort already did, the moment it happened; an admitted connection\n * carries it to `runStream`.\n */\nasync function whilePending(\n pending: PendingAdmission | undefined,\n check: () => Promise<Admission | Response>,\n): Promise<Admission | Response>\n{\n let outcome: Admission | Response | undefined;\n\n try\n {\n outcome = await check();\n\n return outcome instanceof Response ? outcome : { ...outcome, pending };\n }\n finally\n {\n if (!outcome || outcome instanceof Response)\n {\n pending?.cancel();\n }\n }\n}\n\n/**\n * Steps 4-5: the global `authorize`, then the principal and each event's\n * `canSubscribe`. Only an empty `authorize` result or a failed principal\n * refuses; a `canSubscribe` denial only lands the event in `denied`.\n */\nasync function authorizeConnection(\n c: Context,\n router: EventRouterDef<any>,\n authConfig: SSEHandlerAuthConfig | undefined,\n subject: string | undefined,\n requested: string[],\n): Promise<Admission | Response>\n{\n // ── 4. Subscription Authorization ──\n const authorized = await authorizeEvents(subject, requested, authConfig);\n if (authorized === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n // ── 5. Principal + per-event canSubscribe ──\n const request: SubscribeRequest = { query: c.req.query(), headers: c.req.header() };\n const resolved = await resolveConnectionPrincipal(subject, request, authConfig);\n if (!resolved)\n {\n return c.json({ error: 'Principal could not be resolved' }, 403);\n }\n\n const allowed = subject\n ? await checkCanSubscribe(router, authorized, { subject, principal: resolved.principal, request })\n : authorized;\n const denied = requested.filter(e => !allowed.includes(e));\n\n sseLogger.debug('SSE connection requested', {\n events: allowed,\n deniedEvents: denied,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n return { subject, requested, allowed, denied, request };\n}\n\n/**\n * Resolve the principal once per connection; `null` refuses the connection.\n * The warning names the subject only — never the thrown error's text, which\n * may carry what the resolver read.\n */\nasync function resolveConnectionPrincipal(\n subject: string | undefined,\n request: SubscribeRequest,\n authConfig: SSEHandlerAuthConfig | undefined,\n): Promise<{ principal: unknown } | null>\n{\n if (!subject || !authConfig?.resolvePrincipal)\n {\n return { principal: undefined };\n }\n\n try\n {\n return { principal: await authConfig.resolvePrincipal(subject, request) };\n }\n catch\n {\n sseLogger.warn('SSE principal could not be resolved — connection refused', { subject });\n\n return null;\n }\n}\n\n/**\n * Ask each authorized event's `canSubscribe`, in parallel; returns the events\n * it allows. An event without a predicate is allowed.\n */\nasync function checkCanSubscribe(\n router: EventRouterDef<any>,\n events: string[],\n ctx: SubscribeContext,\n): Promise<string[]>\n{\n const verdicts = await Promise.all(events.map(eventName => askCanSubscribe(eventName, router.events[eventName], ctx)));\n\n return events.filter((_, i) => verdicts[i]);\n}\n\n/**\n * `true` → allowed; `false` or a throw → denied. A throw is warned with the\n * event name and never fails the connection.\n */\nasync function askCanSubscribe(\n eventName: string,\n eventDef: EventDef<any> | undefined,\n ctx: SubscribeContext,\n): Promise<boolean>\n{\n if (!eventDef)\n {\n return false;\n }\n\n if (!eventDef._canSubscribe)\n {\n return true;\n }\n\n try\n {\n return await eventDef._canSubscribe(ctx) === true;\n }\n catch\n {\n sseLogger.warn('SSE canSubscribe threw — event denied on this connection', {\n event: eventName,\n subject: ctx.subject,\n });\n\n return false;\n }\n}\n\n// ============================================================================\n// Stream\n// ============================================================================\n\n/**\n * The life of one open connection: connected frame, subscriptions, registry\n * entry, keep-alive, and teardown on disconnect or a server-side close.\n */\nasync function runStream(\n c: Context,\n stream: SSEStreamingApi,\n admission: Admission,\n config: StreamConfig,\n): Promise<void>\n{\n const { allowed, denied, subject, request } = admission;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n // Resolves the disconnect wait below when the connection is closed\n // server-side (overflow/error), so teardown doesn't wait for the loop.\n let onClosed: (() => void) | undefined;\n\n const cleanup = (reason?: string) =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribeAll(connection);\n config.registry.remove(connection);\n writer.close();\n onClosed?.();\n sseLogger.info('SSE dead connection cleaned up', {\n events: [...connection.allowed],\n reason,\n });\n };\n\n // All writes go through one bounded, backpressure-aware drain loop so a\n // slow client can't pile frames up in memory (it's closed on overflow).\n const writer = createBoundedWriter(stream, config.maxQueue, (reason) =>\n {\n sseLogger.warn('SSE connection closed', { events: [...connection.allowed], reason });\n cleanup(reason);\n });\n\n const connection: SSEConnection = {\n subject,\n requested: admission.requested,\n allowed: new Set(allowed),\n denied: new Set(denied),\n unsubscribes: new Map(),\n request,\n subscribe: createEventSubscriber({\n router: config.router,\n authConfig: config.authConfig,\n subject,\n request,\n writer,\n isDead: () => connectionDead,\n }),\n notify: (frame, eventName) =>\n {\n if (connectionDead) return;\n\n // A small literal — not routed through serializeFrame's per-payload cache.\n writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) });\n },\n };\n\n // Synchronously before the `connected` frame: the changes recorded during\n // admission land in `allowed` / `denied` first, and the entry turns open.\n // A cancelled admission (the client left) opens nothing and sends no frame.\n // Defensive: unreachable today, since `streamSSE` runs this synchronously up\n // to here; the handler's `signal.aborted` → 499 check is the real path.\n if (admission.pending?.open(connection) === false)\n {\n cleanup('aborted during admission');\n\n return;\n }\n\n openSubscriptions(connection, writer);\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n writer.enqueue({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n });\n }, config.pingInterval);\n\n await waitForDisconnect(c.req.raw.signal, () => connectionDead, (resolve) =>\n {\n onClosed = resolve;\n });\n\n // Cleanup (normal disconnect path; idempotent if already closed)\n cleanup();\n}\n\n/**\n * Send the `connected` frame (first in the queue) and subscribe every allowed event.\n */\nfunction openSubscriptions(connection: SSEConnection, writer: BoundedWriter): void\n{\n writer.enqueue({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: [...connection.allowed],\n deniedEvents: [...connection.denied],\n timestamp: Date.now(),\n }),\n });\n\n for (const eventName of connection.allowed)\n {\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n }\n\n sseLogger.info('SSE connection established', {\n events: [...connection.allowed],\n deniedEvents: [...connection.denied],\n subscriptionCount: connection.unsubscribes.size,\n });\n}\n\n/**\n * Wait for client disconnect (abort) or a server-side close. Waiting on the\n * abort event instead of polling sleep(pingInterval) means a dropped client is\n * torn down immediately, not up to one ping interval later.\n */\nasync function waitForDisconnect(\n abortSignal: AbortSignal,\n isDead: () => boolean,\n onServerClose: (resolve: () => void) => void,\n): Promise<void>\n{\n if (abortSignal.aborted || isDead())\n {\n return;\n }\n\n await new Promise<void>((resolve) =>\n {\n const onAbort = () => resolve();\n abortSignal.addEventListener('abort', onAbort, { once: true });\n // Server-side close (cleanup) resolves the wait and drops the listener.\n onServerClose(() =>\n {\n abortSignal.removeEventListener('abort', onAbort);\n resolve();\n });\n });\n}\n\n/** What the per-event subscribe of one connection closes over. */\ninterface SubscriberContext\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n subject: string | undefined;\n request: SubscribeRequest;\n writer: BoundedWriter;\n isDead: () => boolean;\n}\n\n/**\n * The per-event subscribe of one connection, used at connect and by `grant`\n * alike, so filter order and `messageId` numbering are one code path.\n *\n * The returned unsubscribe also deactivates the callback, so a frame of an\n * `emit` already fanning out when `revoke` lands is not enqueued after it.\n */\nfunction createEventSubscriber(ctx: SubscriberContext): (eventName: string) => () => void\n{\n let messageId = 0;\n\n return (eventName: string) =>\n {\n const eventDef = ctx.router.events[eventName];\n let active = true;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (!active || ctx.isDead() || !passesFrameFilters(eventName, eventDef, payload, ctx))\n {\n return;\n }\n\n messageId++;\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n ctx.writer.enqueue({\n id: String(messageId),\n event: eventName,\n data: serializeFrame(eventName, payload),\n });\n });\n\n return () =>\n {\n active = false;\n unsubscribe();\n };\n };\n}\n\n/**\n * The global `filter` map (with a subject, as before) and the event's own\n * `.filter()` — both must pass.\n */\nfunction passesFrameFilters(\n eventName: string,\n eventDef: EventDef<any>,\n payload: unknown,\n ctx: SubscriberContext,\n): boolean\n{\n const globalFilter = ctx.authConfig?.filter?.[eventName];\n if (ctx.subject && globalFilter && !globalFilter(ctx.subject, payload))\n {\n return false;\n }\n\n if (!eventDef._filter)\n {\n return true;\n }\n\n try\n {\n return eventDef._filter(payload, { subject: ctx.subject, request: ctx.request }) === true;\n }\n catch\n {\n sseLogger.warn('SSE event filter threw — frame dropped', { event: eventName, subject: ctx.subject });\n\n return false;\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Authenticate via one-time token\n * @returns subject string if authenticated, undefined if no auth required,\n * false if token missing, null if token invalid/expired\n */\nasync function authenticateToken(\n c: Context,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = c.req.query('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\n/**\n * Parse requested events from query parameter\n */\nfunction parseRequestedEvents(c: Context): string[] | null\n{\n const eventsParam = c.req.query('events');\n if (!eventsParam)\n {\n return null;\n }\n\n // Deduplicated: one subscription per event, keyed by name in the registry.\n return [...new Set(eventsParam.split(',').map(e => e.trim()))];\n}\n\n/**\n * Authorize event subscription via auth hook\n * @returns allowed events array, or null if rejected\n */\nasync function authorizeEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: SSEHandlerAuthConfig,\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 if (allowed.length === 0)\n {\n return null;\n }\n\n return allowed;\n}\n","/**\n * SSE Token Manager\n *\n * Auth-agnostic token issuance and verification for SSE connections.\n * Issues one-time-use tokens with TTL for Token Exchange pattern.\n *\n * @example\n * ```typescript\n * const manager = new SSETokenManager({ ttl: 30000 });\n *\n * // Issue token for authenticated user\n * const token = await manager.issue('user-123');\n *\n * // Verify and consume token (one-time use)\n * const subject = await manager.verify(token); // 'user-123'\n * const again = await manager.verify(token); // null (already consumed)\n *\n * // Cleanup on shutdown\n * manager.destroy();\n * ```\n */\n\nimport { randomBytes, createHash } from 'crypto';\n\n/**\n * Hash a token for storage. The raw token is a bearer secret — storing it at rest\n * (e.g. as a Redis key/value) means a store dump hands out live tokens. We persist\n * only sha256(token) and look up by the same hash; the raw token exists only in\n * transit and in the issue() return value.\n */\nfunction hashToken(token: string): string\n{\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Minimal cache client interface (compatible with ioredis Redis | Cluster)\n */\ntype CacheClient = {\n set(key: string, value: string, ...args: any[]): Promise<any>;\n getdel?(key: string): Promise<string | null>;\n get(key: string): Promise<string | null>;\n del(...keys: string[]): Promise<number>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored SSE token data\n */\nexport interface SSEToken\n{\n subject: string;\n expiresAt: number;\n}\n\n/**\n * Token storage interface\n *\n * Implement this for custom storage backends (e.g., Redis for multi-instance).\n */\nexport interface SSETokenStore\n{\n /** Store a token */\n set(token: string, data: SSEToken): Promise<void>;\n\n /** Get and delete a token (one-time use) */\n consume(token: string): Promise<SSEToken | null>;\n\n /** Remove expired tokens */\n cleanup(): Promise<void>;\n}\n\n/**\n * SSETokenManager configuration\n */\nexport interface SSETokenManagerConfig\n{\n /**\n * Token time-to-live in milliseconds\n * @default 30000\n */\n ttl?: number;\n\n /**\n * Custom token store (default: in-memory Map)\n */\n store?: SSETokenStore;\n\n /**\n * Cleanup interval in milliseconds\n * @default 60000\n */\n cleanupInterval?: number;\n}\n\n// ============================================================================\n// InMemoryTokenStore\n// ============================================================================\n\nclass InMemoryTokenStore implements SSETokenStore\n{\n private tokens = new Map<string, SSEToken>();\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n this.tokens.set(hashToken(token), data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = hashToken(token);\n const data = this.tokens.get(key);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(key);\n\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [key, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(key);\n }\n }\n }\n}\n\n// ============================================================================\n// CacheTokenStore (Redis/Valkey)\n// ============================================================================\n\n/**\n * Redis/Valkey-backed token store for multi-instance deployments.\n *\n * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.\n * No cleanup needed — Redis handles expiration automatically.\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * const store = new CacheTokenStore(cache);\n * const manager = new SSETokenManager({ store });\n * }\n * ```\n */\nexport class CacheTokenStore implements SSETokenStore\n{\n private prefix = 'sse:token:';\n\n constructor(private cache: CacheClient) \n {}\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n const ttlSeconds = Math.max(1, Math.ceil((data.expiresAt - Date.now()) / 1000));\n await this.cache.set(\n this.prefix + hashToken(token),\n JSON.stringify(data),\n 'EX',\n ttlSeconds,\n );\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = this.prefix + hashToken(token);\n\n // GETDEL (Redis 6.2+) for atomic consume, fallback to GET+DEL\n let raw: string | null = null;\n\n if (this.cache.getdel)\n {\n raw = await this.cache.getdel(key);\n }\n else\n {\n raw = await this.cache.get(key);\n if (raw)\n {\n await this.cache.del(key);\n }\n }\n\n if (!raw)\n {\n return null;\n }\n\n return JSON.parse(raw) as SSEToken;\n }\n\n async cleanup(): Promise<void>\n {\n // No-op: Redis TTL handles expiration automatically\n }\n}\n\n// ============================================================================\n// SSETokenManager\n// ============================================================================\n\nexport class SSETokenManager\n{\n private store: SSETokenStore;\n private ttl: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config?: SSETokenManagerConfig)\n {\n this.ttl = config?.ttl ?? 30000;\n this.store = config?.store ?? new InMemoryTokenStore();\n\n const cleanupInterval = config?.cleanupInterval ?? 60000;\n this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);\n this.cleanupTimer.unref();\n }\n\n /**\n * How long an issued token stays valid, in milliseconds.\n *\n * Exposed so a caller that has to report an expiry to its client computes the same\n * number this manager stamps on the token, instead of assuming the default.\n */\n get ttlMs(): number\n {\n return this.ttl;\n }\n\n /**\n * Issue a new one-time-use token for the given subject\n */\n async issue(subject: string): Promise<string>\n {\n const token = randomBytes(32).toString('hex');\n\n // The store keys by sha256(token); the raw token is never persisted.\n await this.store.set(token, {\n subject,\n expiresAt: Date.now() + this.ttl,\n });\n\n return token;\n }\n\n /**\n * Verify and consume a token\n * @returns subject string if valid, null if invalid/expired/already consumed\n */\n async verify(token: string): Promise<string | null>\n {\n const data = await this.store.consume(token);\n\n if (!data || data.expiresAt <= Date.now())\n {\n return null;\n }\n\n return data.subject;\n }\n\n /**\n * Cleanup timer and resources\n */\n destroy(): void\n {\n if (this.cleanupTimer)\n {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/event/sse/bounded-writer.ts","../../../src/event/sse/connection-registry.ts","../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bounded SSE outbound writer\n *\n * Serializes writes to one SSE stream through a single drain loop that `await`s\n * each `writeSSE` — so a slow client applies real backpressure instead of letting\n * frames pile up unbounded in memory (OOM under a fast producer + slow consumer).\n *\n * The queue is per-connection, so a slow stream never blocks other connections.\n * When the queue exceeds `maxQueue`, the connection is closed (via `onClose`)\n * rather than silently dropping frames — dropping a chunk would corrupt an\n * ordered token/chat stream; the client reconnects and re-fetches instead.\n */\n\n/** A frame to write to the SSE stream (subset of Hono's SSEMessage). */\nexport interface SSEFrame\n{\n data: string;\n event?: string;\n id?: string;\n}\n\n/** Minimal stream surface this writer needs (Hono's SSEStreamingApi satisfies it). */\nexport interface SSEWritable\n{\n writeSSE(message: SSEFrame): Promise<void>;\n}\n\nexport interface BoundedWriter\n{\n /** Queue a frame for delivery; closes the connection if the queue overflows. */\n enqueue(frame: SSEFrame): void;\n\n /**\n * Count one frame held outside the queue against `maxQueue`, until\n * `clearReserved()`; closes the connection as a slow consumer when it does\n * not fit. `false` when it did not fit or the writer is closed.\n */\n reserve(): boolean;\n\n /** Stop counting the reserved frames — their writer is about to enqueue them. */\n clearReserved(): void;\n\n /** Stop draining and drop any queued frames (called on connection cleanup). */\n close(): void;\n\n /** Current queue depth (for tests/diagnostics). */\n readonly queued: number;\n}\n\n/**\n * Create a bounded, backpressure-aware writer for one SSE stream.\n *\n * @param stream the SSE stream (`writeSSE` returns a promise that resolves when\n * the socket has drained — that is what gives us backpressure)\n * @param maxQueue max frames buffered before the connection is closed\n * @param onClose called once when the writer closes the connection (overflow or\n * a write error); the caller cleans up and logs\n */\nexport function createBoundedWriter(\n stream: SSEWritable,\n maxQueue: number,\n onClose: (reason: string) => void,\n): BoundedWriter\n{\n const pending: SSEFrame[] = [];\n let flushing = false;\n let reserved = 0;\n let closed = false;\n\n // Slow consumer: the drain loop can't keep up, so the queue — with the frames\n // reserved for it — grew past the bound. Close rather than buffer unboundedly\n // or drop silently.\n const overflows = (): boolean =>\n {\n if (pending.length + reserved <= maxQueue)\n {\n return false;\n }\n\n onClose('outbound queue overflow (slow consumer)');\n\n return true;\n };\n\n const flush = async (): Promise<void> =>\n {\n if (flushing || closed)\n {\n return;\n }\n\n flushing = true;\n\n while (pending.length > 0 && !closed)\n {\n const frame = pending.shift() as SSEFrame;\n try\n {\n await stream.writeSSE(frame);\n }\n catch (err)\n {\n flushing = false;\n onClose(err instanceof Error ? err.message : String(err));\n\n return;\n }\n }\n\n flushing = false;\n };\n\n return {\n enqueue(frame: SSEFrame): void\n {\n if (closed)\n {\n return;\n }\n\n pending.push(frame);\n\n if (!overflows())\n {\n void flush();\n }\n },\n\n reserve(): boolean\n {\n if (closed)\n {\n return false;\n }\n\n reserved++;\n\n return !overflows();\n },\n\n clearReserved(): void\n {\n reserved = 0;\n },\n\n close(): void\n {\n closed = true;\n pending.length = 0;\n },\n\n get queued(): number\n {\n return pending.length;\n },\n };\n}\n","/**\n * SSE connection registry\n *\n * What one `createSSEHandler` knows about its open connections, so that\n * `EventDef.grant` / `EventDef.revoke` can change a connection after it opened.\n * One registry per handler: two handlers in one process (tests) never share one.\n *\n * Neither operation consults `canSubscribe` or `authorize`: they are an\n * operator's statement about an open connection. Permission lives in app data\n * and is re-read at the next connect. A `match` narrows either to the\n * connections whose connect query carries its fields.\n *\n * A connect still reading app data (`authorize`, `resolvePrincipal`,\n * `canSubscribe`) is a pending admission: a `grant` / `revoke` that lands then\n * is recorded on it and applied, without a frame, when the connection opens —\n * so a call the app made is never lost to that window.\n */\n\nimport { logger } from '@spfn/core/logger';\nimport type { SubscribeRequest, SubscriptionMatch } from '../types';\n\nconst registryLogger = logger.child('@spfn/core:sse');\n\n/** Stream-owned frame: an event was activated on the open connection. */\nexport const SUBSCRIPTION_GRANTED = 'subscription.granted';\n\n/** Stream-owned frame: an event was deactivated on the open connection. */\nexport const SUBSCRIPTION_REVOKED = 'subscription.revoked';\n\nexport type SubscriptionFrame = typeof SUBSCRIPTION_GRANTED | typeof SUBSCRIPTION_REVOKED;\n\n/**\n * One open SSE connection. `allowed` and `denied` partition `requested`\n * (plus anything `authorize` added); only `allowed` events hold a live\n * subscription in `unsubscribes`.\n */\nexport interface SSEConnection\n{\n /** `undefined` when the stream runs without auth — such a connection is never registered. */\n readonly subject: string | undefined;\n readonly requested: readonly string[];\n readonly allowed: Set<string>;\n readonly denied: Set<string>;\n readonly unsubscribes: Map<string, () => void>;\n readonly request: SubscribeRequest;\n\n /** The per-event subscribe the connection opened with — `grant` reuses it, never a copy. */\n readonly subscribe: (eventName: string) => () => void;\n\n /** Enqueue a protocol frame about one event (dropped once the connection is dead). */\n readonly notify: (frame: SubscriptionFrame, eventName: string) => void;\n}\n\nexport type ConnectionRegistry = ReturnType<typeof createConnectionRegistry>;\n\ntype SubscriptionChange = 'grant' | 'revoke';\n\n/**\n * A connect of one subject between its first app-data read and its open.\n * The first of `open` / `cancel` ends it; both are synchronous. A client abort\n * cancels it at once — even while an app predicate never settles.\n */\nexport interface PendingAdmission\n{\n /**\n * Apply the recorded changes to `connection`, then swap this entry for the\n * open connection. `false`, with nothing applied or registered, once cancelled.\n */\n readonly open: (connection: SSEConnection) => boolean;\n\n /** Drop this entry (refused, aborted, or thrown); idempotent. */\n readonly cancel: () => void;\n}\n\n/** One `grant` / `revoke` recorded on a pending admission. */\ninterface RecordedChange\n{\n kind: SubscriptionChange;\n eventName: string;\n match?: SubscriptionMatch;\n}\n\n/** The changes recorded on one pending admission, in call order (the last one wins). */\ntype RecordedChanges = RecordedChange[];\n\n/**\n * Create the registry of one SSE handler, keyed by subject → open connections\n * (a subject with two tabs has two).\n */\nexport function createConnectionRegistry()\n{\n const bySubject = new Map<string, Set<SSEConnection>>();\n const pendingBySubject = new Map<string, Set<RecordedChanges>>();\n\n /**\n * Register a pending admission of `subject`; `signal` is the connect's\n * abort signal, listened to until the admission ends either way.\n */\n const beginAdmission = (subject: string, signal: AbortSignal): PendingAdmission =>\n {\n const changes: RecordedChanges = [];\n addToSet(pendingBySubject, subject, changes);\n const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));\n\n return {\n cancel: end,\n open: (connection: SSEConnection) =>\n {\n if (!end())\n {\n return false;\n }\n\n applyRecorded(connection, changes);\n addToSet(bySubject, subject, connection);\n\n return true;\n },\n };\n };\n\n const change = (kind: SubscriptionChange, subject: string, eventName: string, match?: SubscriptionMatch): void =>\n {\n const pending = pendingBySubject.get(subject);\n const connections = bySubject.get(subject);\n pending?.forEach(changes => changes.push({ kind, eventName, match }));\n connections?.forEach(connection =>\n {\n if (matchesRequest(match, connection.request))\n {\n (kind === 'grant' ? grantOn : revokeOn)(connection, eventName);\n }\n });\n\n if (!pending && !connections)\n {\n registryLogger.debug('SSE subscription change for a subject with no open connection ignored', {\n event: eventName,\n subject,\n });\n }\n };\n\n return {\n beginAdmission,\n\n remove: (connection: SSEConnection): void =>\n {\n if (connection.subject)\n {\n removeFromSet(bySubject, connection.subject, connection);\n }\n },\n\n /** Open connections only — a pending admission is not counted. */\n count: (subject: string): number => bySubject.get(subject)?.size ?? 0,\n\n pendingCount: (subject: string): number => pendingBySubject.get(subject)?.size ?? 0,\n\n grant: (subject: string, eventName: string, match?: SubscriptionMatch): void =>\n change('grant', subject, eventName, match),\n\n revoke: (subject: string, eventName: string, match?: SubscriptionMatch): void =>\n change('revoke', subject, eventName, match),\n };\n}\n\n/**\n * The end of one pending admission: `remove` runs on the first call only,\n * which returns `true`; later calls return `false`. An abort of `signal` is a\n * first call; the listener is dropped on every end, so it never outlives the\n * admission (an already aborted signal ends it at once).\n */\nfunction endOnce(signal: AbortSignal, remove: () => void): () => boolean\n{\n let ended = false;\n const end = (): boolean =>\n {\n if (ended)\n {\n return false;\n }\n\n ended = true;\n signal.removeEventListener('abort', end);\n remove();\n\n return true;\n };\n\n signal.addEventListener('abort', end, { once: true });\n if (signal.aborted)\n {\n end();\n }\n\n return end;\n}\n\nfunction addToSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key) ?? new Set<T>();\n set.add(value);\n map.set(key, set);\n}\n\n/** Remove `value` and drop the key once its set is empty, so neither map grows on reconnects. */\nfunction removeFromSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key);\n set?.delete(value);\n\n if (set?.size === 0)\n {\n map.delete(key);\n }\n}\n\n/**\n * Whether `match` reaches a connection: every named field equals the connect\n * query's value, string to string. A field the connection never declared\n * never matches, not even an empty string. No `match` reaches every one.\n */\nexport function matchesRequest(match: SubscriptionMatch | undefined, request: SubscribeRequest): boolean\n{\n return match === undefined || Object.entries(match.query).every(([field, value]) =>\n Object.hasOwn(request.query, field) && request.query[field] === value);\n}\n\n/**\n * Apply what was recorded during admission to a connection that has not\n * opened yet: move the event between `allowed` and `denied`, no frame. An\n * event the connection never requested is in neither set and stays out; a\n * change whose `match` the connection's query does not meet is skipped.\n */\nfunction applyRecorded(connection: SSEConnection, changes: RecordedChanges): void\n{\n changes.forEach(({ kind, eventName, match }) =>\n {\n if (!matchesRequest(match, connection.request))\n {\n return;\n }\n\n const [from, to] = kind === 'grant'\n ? [connection.denied, connection.allowed]\n : [connection.allowed, connection.denied];\n\n if (from.delete(eventName))\n {\n to.add(eventName);\n }\n });\n}\n\n/**\n * Activate a denied event on one connection; allowed or never requested → no change.\n */\nfunction grantOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.denied.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n connection.denied.delete(eventName);\n connection.allowed.add(eventName);\n connection.notify(SUBSCRIPTION_GRANTED, eventName);\n}\n\n/**\n * Deactivate an allowed event on one connection; denied or never requested → no change.\n *\n * A frame the event already enqueued before this call may still be written —\n * the queue is not rewritten — but no frame after the unsubscribe is.\n */\nfunction revokeOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.allowed.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.get(eventName)?.();\n connection.unsubscribes.delete(eventName);\n connection.allowed.delete(eventName);\n connection.denied.add(eventName);\n connection.notify(SUBSCRIPTION_REVOKED, eventName);\n}\n\n/**\n * Drop every live subscription of a closing connection. A revoked event was\n * already removed from `unsubscribes`, so nothing is unsubscribed twice.\n */\nexport function unsubscribeAll(connection: SSEConnection): void\n{\n connection.unsubscribes.forEach(unsubscribe => unsubscribe());\n connection.unsubscribes.clear();\n}\n","/**\n * SSE Handler for Hono\n *\n * Creates SSE stream endpoint for event subscription\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { createSSEHandler } from '@spfn/core/event/sse';\n * import { eventRouter } from './events';\n *\n * const app = new Hono();\n *\n * // GET /events/stream?events=userCreated,orderPlaced\n * app.get('/events/stream', createSSEHandler(eventRouter));\n * ```\n */\n\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport type { SSEStreamingApi } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef } from '../router';\nimport type { EventDef, SubscribeContext, SubscribeRequest, SubscriptionMatch } from '../types';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\nimport { createBoundedWriter } from './bounded-writer';\nimport type { BoundedWriter } from './bounded-writer';\nimport { createConnectionRegistry, unsubscribeAll } from './connection-registry';\nimport type { ConnectionRegistry, PendingAdmission, SSEConnection } from './connection-registry';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n/**\n * Serialize a fan-out frame once per (event, payload).\n *\n * One `emit` delivers the same payload object to every subscribed connection, so\n * without memoization each connection re-`JSON.stringify`s the identical payload\n * — O(N) for N subscribers on a hot broadcast. Keyed by the payload object\n * (WeakMap → auto-GC after the emit); primitives are cheap to stringify directly.\n */\nconst frameCache = new WeakMap<object, Map<string, string>>();\n\nexport function serializeFrame(eventName: string, payload: unknown): string\n{\n if (payload === null || typeof payload !== 'object')\n {\n return JSON.stringify({ event: eventName, data: payload });\n }\n\n let byEvent = frameCache.get(payload);\n if (!byEvent)\n {\n byEvent = new Map<string, string>();\n frameCache.set(payload, byEvent);\n }\n\n let serialized = byEvent.get(eventName);\n if (serialized === undefined)\n {\n serialized = JSON.stringify({ event: eventName, data: payload });\n byEvent.set(eventName, serialized);\n }\n\n return serialized;\n}\n\n/** Default outbound queue cap per connection before a slow consumer is dropped. */\nconst DEFAULT_MAX_QUEUE = 1000;\n\n// Extend Hono context with SSE subject\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n sseSubject?: string;\n }\n}\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\n * - token: One-time auth token (when auth is enabled)\n *\n * Who receives what, in order: the global `authorize` (once, at connect) →\n * each requested event's `.canSubscribe()` (once per event, at connect) → the\n * global `filter` map and the event's `.filter()` (sync, per frame). An event\n * denied at connect stays in `deniedEvents`, so `EventDef.grant` can activate\n * it later; `EventDef.revoke` deactivates an allowed one. The handler\n * registers that control on every event of the router.\n *\n * @example\n * ```typescript\n * app.get('/events/stream', createSSEHandler(eventRouter, {\n * pingInterval: 30000,\n * }));\n * ```\n */\nexport function createSSEHandler<TRouter extends EventRouterDef<any>>(\n router: TRouter,\n config: SSEHandlerConfig = {},\n tokenManager?: SSETokenManager,\n)\n{\n const {\n // 10s — keep-alive가 중간 프록시(GCP LB/nginx)의 idle 타임아웃(흔히 ~15-30s)보다 자주 와야\n // 첫 ping 전 침묵 구간에 연결이 끊기지 않는다. 30s 기본은 idle 타임아웃보다 길어 SSE가\n // 데이터 없는 동안 끊기던 문제가 있었다.\n pingInterval = 10000,\n auth: authConfig,\n maxQueue = DEFAULT_MAX_QUEUE,\n } = config;\n\n const registry = createConnectionRegistry();\n registerSubscriptionControls(router, registry);\n\n const streamConfig: StreamConfig = { router, authConfig, registry, pingInterval, maxQueue };\n\n const handler = async (c: Context) =>\n {\n const admission = await admitConnection(c, streamConfig, tokenManager);\n if (admission instanceof Response)\n {\n return admission;\n }\n\n if (c.req.raw.signal.aborted)\n {\n // The client left during admission: nobody reads this response.\n // Its abort listener already cancelled `pending`; cancel is idempotent.\n admission.pending?.cancel();\n\n return new Response(null, { status: 499 });\n }\n\n // ── 6. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, stream => runStream(c, stream, admission, streamConfig), async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n\n return Object.assign(handler, {\n /** @internal Open registered connections of a subject (tests). */\n _openConnections: (subject: string): number => registry.count(subject),\n\n /** @internal Connects of a subject still in admission (tests). */\n _pendingAdmissions: (subject: string): number => registry.pendingCount(subject),\n });\n}\n\n// ============================================================================\n// Connect\n// ============================================================================\n\n/** What a request that passed every connect check opens the stream with. */\ninterface Admission\n{\n subject: string | undefined;\n requested: string[];\n allowed: string[];\n denied: string[];\n request: SubscribeRequest;\n\n /** What `resolvePrincipal` returned, for the initial pass; never logged. */\n principal: unknown;\n\n /** Registered before the first app-data read; `runStream` opens it. Absent without a subject. */\n pending?: PendingAdmission;\n}\n\n/** What every stream of one handler shares. */\ninterface StreamConfig\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n registry: ConnectionRegistry;\n pingInterval: number;\n maxQueue: number;\n}\n\n/**\n * Let `EventDef.grant` / `revoke` reach this handler's connections, once per\n * router key (an event under two keys gets two controls, each naming its key).\n */\nfunction registerSubscriptionControls(router: EventRouterDef<any>, registry: ConnectionRegistry): void\n{\n for (const eventName of router.eventNames as string[])\n {\n router.events[eventName]._registerSubscriptionControl({\n grant: (subject: string, match?: SubscriptionMatch) => registry.grant(subject, eventName, match),\n revoke: (subject: string, match?: SubscriptionMatch) => registry.revoke(subject, eventName, match),\n });\n }\n}\n\n/**\n * Run the connect checks; a `Response` is the refusal to return.\n */\nasync function admitConnection(\n c: Context,\n config: StreamConfig,\n tokenManager: SSETokenManager | undefined,\n): Promise<Admission | Response>\n{\n const { router, authConfig, registry } = config;\n\n // ── 1. Token Authentication ──\n const subject = await authenticateToken(c, tokenManager);\n if (subject === false)\n {\n return c.json({ error: 'Missing token parameter' }, 401);\n }\n if (subject === null)\n {\n return c.json({ error: 'Invalid or expired token' }, 401);\n }\n if (subject)\n {\n c.set('sseSubject', subject);\n }\n\n // ── 2-3. Requested events, validated against the router ──\n const requested = parseRequestedEvents(c);\n if (!requested)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requested.filter(e => !validEventNames.includes(e));\n if (invalidEvents.length > 0)\n {\n return c.json({\n error: 'Invalid event names',\n invalidEvents,\n validEvents: validEventNames,\n }, 400);\n }\n\n return whilePending(\n subject ? registry.beginAdmission(subject, c.req.raw.signal) : undefined,\n () => authorizeConnection(c, router, authConfig, subject, requested),\n );\n}\n\n/**\n * Run the app-data checks with the admission already registered — `pending`\n * is written synchronously, before the first `await`, so a `grant` / `revoke`\n * issued meanwhile is recorded on it. A refusal or a throw unregisters it; a\n * client abort already did, the moment it happened; an admitted connection\n * carries it to `runStream`.\n */\nasync function whilePending(\n pending: PendingAdmission | undefined,\n check: () => Promise<Admission | Response>,\n): Promise<Admission | Response>\n{\n let outcome: Admission | Response | undefined;\n\n try\n {\n outcome = await check();\n\n return outcome instanceof Response ? outcome : { ...outcome, pending };\n }\n finally\n {\n if (!outcome || outcome instanceof Response)\n {\n pending?.cancel();\n }\n }\n}\n\n/**\n * Steps 4-5: the global `authorize`, then the principal and each event's\n * `canSubscribe`. Only an empty `authorize` result or a failed principal\n * refuses; a `canSubscribe` denial only lands the event in `denied`.\n */\nasync function authorizeConnection(\n c: Context,\n router: EventRouterDef<any>,\n authConfig: SSEHandlerAuthConfig | undefined,\n subject: string | undefined,\n requested: string[],\n): Promise<Admission | Response>\n{\n // ── 4. Subscription Authorization ──\n const authorized = await authorizeEvents(subject, requested, authConfig);\n if (authorized === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n // ── 5. Principal + per-event canSubscribe ──\n const request: SubscribeRequest = { query: c.req.query(), headers: c.req.header() };\n const resolved = await resolveConnectionPrincipal(subject, request, authConfig);\n if (!resolved)\n {\n return c.json({ error: 'Principal could not be resolved' }, 403);\n }\n\n const allowed = subject\n ? await checkCanSubscribe(router, authorized, { subject, principal: resolved.principal, request })\n : authorized;\n const denied = requested.filter(e => !allowed.includes(e));\n\n sseLogger.debug('SSE connection requested', {\n events: allowed,\n deniedEvents: denied,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n return { subject, requested, allowed, denied, request, principal: resolved.principal };\n}\n\n/**\n * Resolve the principal once per connection; `null` refuses the connection.\n * The warning names the subject only — never the thrown error's text, which\n * may carry what the resolver read.\n */\nasync function resolveConnectionPrincipal(\n subject: string | undefined,\n request: SubscribeRequest,\n authConfig: SSEHandlerAuthConfig | undefined,\n): Promise<{ principal: unknown } | null>\n{\n if (!subject || !authConfig?.resolvePrincipal)\n {\n return { principal: undefined };\n }\n\n try\n {\n return { principal: await authConfig.resolvePrincipal(subject, request) };\n }\n catch\n {\n sseLogger.warn('SSE principal could not be resolved — connection refused', { subject });\n\n return null;\n }\n}\n\n/**\n * Ask each authorized event's `canSubscribe`, in parallel; returns the events\n * it allows. An event without a predicate is allowed.\n */\nasync function checkCanSubscribe(\n router: EventRouterDef<any>,\n events: string[],\n ctx: SubscribeContext,\n): Promise<string[]>\n{\n const verdicts = await Promise.all(events.map(eventName => askCanSubscribe(eventName, router.events[eventName], ctx)));\n\n return events.filter((_, i) => verdicts[i]);\n}\n\n/**\n * `true` → allowed; `false` or a throw → denied. A throw is warned with the\n * event name and never fails the connection.\n */\nasync function askCanSubscribe(\n eventName: string,\n eventDef: EventDef<any> | undefined,\n ctx: SubscribeContext,\n): Promise<boolean>\n{\n if (!eventDef)\n {\n return false;\n }\n\n if (!eventDef._canSubscribe)\n {\n return true;\n }\n\n try\n {\n return await eventDef._canSubscribe(ctx) === true;\n }\n catch\n {\n sseLogger.warn('SSE canSubscribe threw — event denied on this connection', {\n event: eventName,\n subject: ctx.subject,\n });\n\n return false;\n }\n}\n\n// ============================================================================\n// Stream\n// ============================================================================\n\n/**\n * The life of one open connection: connected frame, subscriptions, initial\n * frames, registry entry, keep-alive, and teardown on disconnect or a\n * server-side close.\n */\nasync function runStream(\n c: Context,\n stream: SSEStreamingApi,\n admission: Admission,\n config: StreamConfig,\n): Promise<void>\n{\n const { allowed, denied, subject, request } = admission;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n // Resolves the disconnect wait below when the connection is closed\n // server-side (overflow/error), so teardown doesn't wait for the loop.\n let onClosed: (() => void) | undefined;\n\n const cleanup = (reason?: string) =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribeAll(connection);\n config.registry.remove(connection);\n writer.close();\n onClosed?.();\n sseLogger.info('SSE dead connection cleaned up', {\n events: [...connection.allowed],\n reason,\n });\n };\n\n // All writes go through one bounded, backpressure-aware drain loop so a\n // slow client can't pile frames up in memory (it's closed on overflow).\n const writer = createBoundedWriter(stream, config.maxQueue, (reason) =>\n {\n sseLogger.warn('SSE connection closed', { events: [...connection.allowed], reason });\n cleanup(reason);\n });\n\n // Every write after `connected` goes through `hold`, so the initial pass can\n // put `connected` and its frames ahead of whatever arrives while it runs.\n const hold = createFrameHold(writer);\n const subscriberContext: SubscriberContext = {\n router: config.router,\n authConfig: config.authConfig,\n subject,\n request,\n writeFrame: createFrameWriter(writer),\n send: hold.send,\n isDead: () => connectionDead,\n };\n\n const connection: SSEConnection = {\n subject,\n requested: admission.requested,\n allowed: new Set(allowed),\n denied: new Set(denied),\n unsubscribes: new Map(),\n request,\n subscribe: createEventSubscriber(subscriberContext),\n notify: (frame, eventName) =>\n {\n if (connectionDead) return;\n\n // A small literal — not routed through serializeFrame's per-payload cache.\n hold.send(() => writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) }));\n },\n };\n\n // Synchronously before the `connected` frame: the changes recorded during\n // admission land in `allowed` / `denied` first, and the entry turns open.\n // A cancelled admission (the client left) opens nothing and sends no frame.\n // Defensive: unreachable today, since `streamSSE` runs this synchronously up\n // to here; the handler's `signal.aborted` → 499 check is the real path.\n if (admission.pending?.open(connection) === false)\n {\n cleanup('aborted during admission');\n\n return;\n }\n\n // Not awaited: a producer that hangs holds the frames, never the disconnect wait.\n void openSubscriptions(connection, writer, hold, {\n ...subscriberContext,\n principal: admission.principal,\n });\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n hold.send(() => writer.enqueue({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n }));\n }, config.pingInterval);\n\n await waitForDisconnect(c.req.raw.signal, () => connectionDead, (resolve) =>\n {\n onClosed = resolve;\n });\n\n // Cleanup (normal disconnect path; idempotent if already closed)\n cleanup();\n}\n\n/**\n * Send the `connected` frame (first in the queue) and subscribe every allowed\n * event. For a connection with a subject whose allowed events declare\n * `.initial()`, `connected` waits for their frames and is followed by them;\n * live frames arriving meanwhile are held and follow both. Without one, all of\n * it happens synchronously, as before.\n */\nasync function openSubscriptions(\n connection: SSEConnection,\n writer: BoundedWriter,\n hold: FrameHold,\n ctx: InitialContext,\n): Promise<void>\n{\n const connected = { subscribedEvents: [...connection.allowed], deniedEvents: [...connection.denied] };\n const producing = connected.subscribedEvents.filter(eventName => ctx.router.events[eventName]._initial);\n\n if (!ctx.subject || producing.length === 0)\n {\n enqueueConnected(writer, { ...connected, initial: [] });\n subscribeAllowed(connection);\n\n return;\n }\n\n hold.hold();\n subscribeAllowed(connection);\n\n const served = await produceInitialFrames(producing, ctx.router, {\n subject: ctx.subject,\n principal: ctx.principal,\n request: ctx.request,\n });\n if (ctx.isDead())\n {\n return;\n }\n\n enqueueConnected(writer, { ...connected, initial: served.map(({ eventName }) => eventName) });\n served.forEach(({ eventName, frames }) => frames.forEach(data => ctx.writeFrame(eventName, data)));\n hold.release();\n}\n\n/** The `connected` frame's data, as the client's `ConnectedPayload` reads it. */\ninterface ConnectedFrame\n{\n subscribedEvents: string[];\n deniedEvents: string[];\n initial: string[];\n}\n\nfunction enqueueConnected(writer: BoundedWriter, connected: ConnectedFrame): void\n{\n writer.enqueue({\n event: 'connected',\n data: JSON.stringify({ ...connected, timestamp: Date.now() }),\n });\n}\n\nfunction subscribeAllowed(connection: SSEConnection): void\n{\n for (const eventName of connection.allowed)\n {\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n }\n\n sseLogger.info('SSE connection established', {\n events: [...connection.allowed],\n deniedEvents: [...connection.denied],\n subscriptionCount: connection.unsubscribes.size,\n });\n}\n\n/** What the initial pass of one connection reads: the subscriber's context and the `canSubscribe` one. */\ntype InitialContext = SubscriberContext & { principal: unknown };\n\n/** The serialized initial frames of one event whose producer returned. */\ninterface InitialFrames\n{\n eventName: string;\n frames: string[];\n}\n\n/**\n * Ask every producing event for its initial frames, in parallel; returns, in\n * `events` order, those whose producer returned — an empty list counts.\n */\nasync function produceInitialFrames(\n events: string[],\n router: EventRouterDef<any>,\n ctx: SubscribeContext,\n): Promise<InitialFrames[]>\n{\n const results = await Promise.all(events.map(eventName => askInitial(eventName, router.events[eventName], ctx)));\n\n return results.filter((frames): frames is InitialFrames => frames !== null);\n}\n\n/**\n * One event's producer, its payloads serialized here so that a payload that\n * cannot be written fails like a throw: warned with the event name — never the\n * error text — and `null`, so nothing of the event is written.\n */\nasync function askInitial(eventName: string, eventDef: EventDef<any>, ctx: SubscribeContext): Promise<InitialFrames | null>\n{\n try\n {\n const payloads = await eventDef._initial!(ctx);\n\n return { eventName, frames: payloads.map(payload => serializeFrame(eventName, payload)) };\n }\n catch\n {\n sseLogger.warn('SSE initial frames could not be produced — none sent for this event', {\n event: eventName,\n subject: ctx.subject,\n });\n\n return null;\n }\n}\n\n/**\n * The writes of one connection, held in arrival order between `hold()` and\n * `release()`; written at once outside. Released once: `release()` writes the\n * held ones and every later write goes straight through.\n *\n * A held write reserves its place in the writer's queue, so held and queued\n * frames share one `maxQueue`: a hold that outgrows it closes the connection\n * as a slow consumer, and what the initial pass enqueues before `release()`\n * counts the held frames too.\n */\ninterface FrameHold\n{\n send: (write: () => void) => void;\n hold: () => void;\n release: () => void;\n}\n\nfunction createFrameHold(writer: BoundedWriter): FrameHold\n{\n let pending: (() => void)[] | null = null;\n\n return {\n send: (write) =>\n {\n if (!pending)\n {\n write();\n }\n else if (writer.reserve())\n {\n pending.push(write);\n }\n },\n hold: () =>\n {\n pending = [];\n },\n release: () =>\n {\n const held = pending ?? [];\n pending = null;\n writer.clearReserved();\n held.forEach(write => write());\n },\n };\n}\n\n/**\n * Enqueue an event frame with the connection's next `messageId` — one counter\n * for initial and live frames, numbered in the order they are written.\n */\nfunction createFrameWriter(writer: BoundedWriter): (eventName: string, data: string) => void\n{\n let messageId = 0;\n\n return (eventName, data) =>\n {\n messageId++;\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n writer.enqueue({ id: String(messageId), event: eventName, data });\n };\n}\n\n/**\n * Wait for client disconnect (abort) or a server-side close. Waiting on the\n * abort event instead of polling sleep(pingInterval) means a dropped client is\n * torn down immediately, not up to one ping interval later.\n */\nasync function waitForDisconnect(\n abortSignal: AbortSignal,\n isDead: () => boolean,\n onServerClose: (resolve: () => void) => void,\n): Promise<void>\n{\n if (abortSignal.aborted || isDead())\n {\n return;\n }\n\n await new Promise<void>((resolve) =>\n {\n const onAbort = () => resolve();\n abortSignal.addEventListener('abort', onAbort, { once: true });\n // Server-side close (cleanup) resolves the wait and drops the listener.\n onServerClose(() =>\n {\n abortSignal.removeEventListener('abort', onAbort);\n resolve();\n });\n });\n}\n\n/** What the per-event subscribe of one connection closes over. */\ninterface SubscriberContext\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n subject: string | undefined;\n request: SubscribeRequest;\n\n /** Enqueue an event frame with the next `messageId`. */\n writeFrame: (eventName: string, data: string) => void;\n\n /** Write now, or hold until the initial pass ends. */\n send: (write: () => void) => void;\n\n isDead: () => boolean;\n}\n\n/**\n * The per-event subscribe of one connection, used at connect and by `grant`\n * alike, so filter order and `messageId` numbering are one code path.\n *\n * The returned unsubscribe also deactivates the callback, so a frame of an\n * `emit` already fanning out when `revoke` lands is not enqueued after it.\n */\nfunction createEventSubscriber(ctx: SubscriberContext): (eventName: string) => () => void\n{\n return (eventName: string) =>\n {\n const eventDef = ctx.router.events[eventName];\n let active = true;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (!active || ctx.isDead() || !passesFrameFilters(eventName, eventDef, payload, ctx))\n {\n return;\n }\n\n const data = serializeFrame(eventName, payload);\n ctx.send(() => ctx.writeFrame(eventName, data));\n });\n\n return () =>\n {\n active = false;\n unsubscribe();\n };\n };\n}\n\n/**\n * The global `filter` map (with a subject, as before) and the event's own\n * `.filter()` — both must pass.\n */\nfunction passesFrameFilters(\n eventName: string,\n eventDef: EventDef<any>,\n payload: unknown,\n ctx: SubscriberContext,\n): boolean\n{\n const globalFilter = ctx.authConfig?.filter?.[eventName];\n if (ctx.subject && globalFilter && !globalFilter(ctx.subject, payload))\n {\n return false;\n }\n\n if (!eventDef._filter)\n {\n return true;\n }\n\n try\n {\n return eventDef._filter(payload, { subject: ctx.subject, request: ctx.request }) === true;\n }\n catch\n {\n sseLogger.warn('SSE event filter threw — frame dropped', { event: eventName, subject: ctx.subject });\n\n return false;\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Authenticate via one-time token\n * @returns subject string if authenticated, undefined if no auth required,\n * false if token missing, null if token invalid/expired\n */\nasync function authenticateToken(\n c: Context,\n tokenManager?: SSETokenManager,\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = c.req.query('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\n/**\n * Parse requested events from query parameter\n */\nfunction parseRequestedEvents(c: Context): string[] | null\n{\n const eventsParam = c.req.query('events');\n if (!eventsParam)\n {\n return null;\n }\n\n // Deduplicated: one subscription per event, keyed by name in the registry.\n return [...new Set(eventsParam.split(',').map(e => e.trim()))];\n}\n\n/**\n * Authorize event subscription via auth hook\n * @returns allowed events array, or null if rejected\n */\nasync function authorizeEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: SSEHandlerAuthConfig,\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 if (allowed.length === 0)\n {\n return null;\n }\n\n return allowed;\n}\n","/**\n * SSE Token Manager\n *\n * Auth-agnostic token issuance and verification for SSE connections.\n * Issues one-time-use tokens with TTL for Token Exchange pattern.\n *\n * @example\n * ```typescript\n * const manager = new SSETokenManager({ ttl: 30000 });\n *\n * // Issue token for authenticated user\n * const token = await manager.issue('user-123');\n *\n * // Verify and consume token (one-time use)\n * const subject = await manager.verify(token); // 'user-123'\n * const again = await manager.verify(token); // null (already consumed)\n *\n * // Cleanup on shutdown\n * manager.destroy();\n * ```\n */\n\nimport { randomBytes, createHash } from 'crypto';\n\n/**\n * Hash a token for storage. The raw token is a bearer secret — storing it at rest\n * (e.g. as a Redis key/value) means a store dump hands out live tokens. We persist\n * only sha256(token) and look up by the same hash; the raw token exists only in\n * transit and in the issue() return value.\n */\nfunction hashToken(token: string): string\n{\n return createHash('sha256').update(token).digest('hex');\n}\n\n/**\n * Minimal cache client interface (compatible with ioredis Redis | Cluster)\n */\ntype CacheClient = {\n set(key: string, value: string, ...args: any[]): Promise<any>;\n getdel?(key: string): Promise<string | null>;\n get(key: string): Promise<string | null>;\n del(...keys: string[]): Promise<number>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored SSE token data\n */\nexport interface SSEToken\n{\n subject: string;\n expiresAt: number;\n}\n\n/**\n * Token storage interface\n *\n * Implement this for custom storage backends (e.g., Redis for multi-instance).\n */\nexport interface SSETokenStore\n{\n /** Store a token */\n set(token: string, data: SSEToken): Promise<void>;\n\n /** Get and delete a token (one-time use) */\n consume(token: string): Promise<SSEToken | null>;\n\n /** Remove expired tokens */\n cleanup(): Promise<void>;\n}\n\n/**\n * SSETokenManager configuration\n */\nexport interface SSETokenManagerConfig\n{\n /**\n * Token time-to-live in milliseconds\n * @default 30000\n */\n ttl?: number;\n\n /**\n * Custom token store (default: in-memory Map)\n */\n store?: SSETokenStore;\n\n /**\n * Cleanup interval in milliseconds\n * @default 60000\n */\n cleanupInterval?: number;\n}\n\n// ============================================================================\n// InMemoryTokenStore\n// ============================================================================\n\nclass InMemoryTokenStore implements SSETokenStore\n{\n private tokens = new Map<string, SSEToken>();\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n this.tokens.set(hashToken(token), data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = hashToken(token);\n const data = this.tokens.get(key);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(key);\n\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [key, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(key);\n }\n }\n }\n}\n\n// ============================================================================\n// CacheTokenStore (Redis/Valkey)\n// ============================================================================\n\n/**\n * Redis/Valkey-backed token store for multi-instance deployments.\n *\n * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.\n * No cleanup needed — Redis handles expiration automatically.\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * const store = new CacheTokenStore(cache);\n * const manager = new SSETokenManager({ store });\n * }\n * ```\n */\nexport class CacheTokenStore implements SSETokenStore\n{\n private prefix = 'sse:token:';\n\n constructor(private cache: CacheClient) \n {}\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n const ttlSeconds = Math.max(1, Math.ceil((data.expiresAt - Date.now()) / 1000));\n await this.cache.set(\n this.prefix + hashToken(token),\n JSON.stringify(data),\n 'EX',\n ttlSeconds,\n );\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = this.prefix + hashToken(token);\n\n // GETDEL (Redis 6.2+) for atomic consume, fallback to GET+DEL\n let raw: string | null = null;\n\n if (this.cache.getdel)\n {\n raw = await this.cache.getdel(key);\n }\n else\n {\n raw = await this.cache.get(key);\n if (raw)\n {\n await this.cache.del(key);\n }\n }\n\n if (!raw)\n {\n return null;\n }\n\n return JSON.parse(raw) as SSEToken;\n }\n\n async cleanup(): Promise<void>\n {\n // No-op: Redis TTL handles expiration automatically\n }\n}\n\n// ============================================================================\n// SSETokenManager\n// ============================================================================\n\nexport class SSETokenManager\n{\n private store: SSETokenStore;\n private ttl: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config?: SSETokenManagerConfig)\n {\n this.ttl = config?.ttl ?? 30000;\n this.store = config?.store ?? new InMemoryTokenStore();\n\n const cleanupInterval = config?.cleanupInterval ?? 60000;\n this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);\n this.cleanupTimer.unref();\n }\n\n /**\n * How long an issued token stays valid, in milliseconds.\n *\n * Exposed so a caller that has to report an expiry to its client computes the same\n * number this manager stamps on the token, instead of assuming the default.\n */\n get ttlMs(): number\n {\n return this.ttl;\n }\n\n /**\n * Issue a new one-time-use token for the given subject\n */\n async issue(subject: string): Promise<string>\n {\n const token = randomBytes(32).toString('hex');\n\n // The store keys by sha256(token); the raw token is never persisted.\n await this.store.set(token, {\n subject,\n expiresAt: Date.now() + this.ttl,\n });\n\n return token;\n }\n\n /**\n * Verify and consume a token\n * @returns subject string if valid, null if invalid/expired/already consumed\n */\n async verify(token: string): Promise<string | null>\n {\n const data = await this.store.consume(token);\n\n if (!data || data.expiresAt <= Date.now())\n {\n return null;\n }\n\n return data.subject;\n }\n\n /**\n * Cleanup timer and resources\n */\n destroy(): void\n {\n if (this.cleanupTimer)\n {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-
|
|
1
|
+
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-DncqXfWN.js';
|
|
2
2
|
import 'hono';
|
|
3
|
-
import '../../router-
|
|
3
|
+
import '../../router-CX5uVjmU.js';
|
|
4
4
|
import '@sinclair/typebox';
|
|
5
5
|
import '../../token-manager-Ckjt2-qq.js';
|
|
6
6
|
|
package/dist/event/ws/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as EventDef } from '../../router-
|
|
2
|
-
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-
|
|
3
|
-
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-
|
|
1
|
+
import { a as EventDef } from '../../router-CX5uVjmU.js';
|
|
2
|
+
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-DncqXfWN.js';
|
|
3
|
+
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-DncqXfWN.js';
|
|
4
4
|
import { Server } from 'node:http';
|
|
5
5
|
import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
|
|
6
6
|
import '@sinclair/typebox';
|
package/dist/ops/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { MiddlewareHandler } from 'hono';
|
|
2
2
|
import { a as NamedMiddleware } from '../define-middleware-CVKgqo8S.js';
|
|
3
3
|
import { R as RouteDef } from '../route-builder-DRW-MSCJ.js';
|
|
4
|
-
import { R as Router } from '../router-
|
|
5
|
-
import { S as SurfaceEffect, a as SurfaceModuleCommand, b as SurfaceModule, c as SurfaceRouteBuilder, d as SurfaceCommand, e as SurfaceManifest, f as SurfaceModuleDescriptor } from '../error-
|
|
6
|
-
export { g as OpsRouterError } from '../error-
|
|
4
|
+
import { R as Router } from '../router-C57y-vz1.js';
|
|
5
|
+
import { S as SurfaceEffect, a as SurfaceModuleCommand, b as SurfaceModule, c as SurfaceRouteBuilder, d as SurfaceCommand, e as SurfaceManifest, f as SurfaceModuleDescriptor } from '../error-CIZlYJlL.js';
|
|
6
|
+
export { g as OpsRouterError } from '../error-CIZlYJlL.js';
|
|
7
7
|
import '@sinclair/typebox';
|
|
8
8
|
import 'hono/utils/http-status';
|
|
9
9
|
import '../route/types.js';
|
package/dist/ops/index.js
CHANGED
|
@@ -318,7 +318,7 @@ var route = {
|
|
|
318
318
|
|
|
319
319
|
// src/route/router.ts
|
|
320
320
|
function createRouterInstance(state) {
|
|
321
|
-
|
|
321
|
+
const router = {
|
|
322
322
|
routes: state.routes,
|
|
323
323
|
_routes: state.routes,
|
|
324
324
|
_packageRouters: state.packageRouters,
|
|
@@ -333,19 +333,29 @@ function createRouterInstance(state) {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
assertDistinctSurfaces(newPackageRouters);
|
|
336
|
-
return createRouterInstance({ ...state, packageRouters: newPackageRouters });
|
|
336
|
+
return carryMarkers(router, createRouterInstance({ ...state, packageRouters: newPackageRouters }));
|
|
337
337
|
},
|
|
338
338
|
use(middlewares) {
|
|
339
|
-
return createRouterInstance({
|
|
339
|
+
return carryMarkers(router, createRouterInstance({
|
|
340
340
|
...state,
|
|
341
341
|
globalMiddlewares: [...state.globalMiddlewares, ...middlewares]
|
|
342
|
-
});
|
|
342
|
+
}));
|
|
343
343
|
},
|
|
344
344
|
contractVersion(version) {
|
|
345
345
|
assertContractVersion(version);
|
|
346
|
-
return createRouterInstance({ ...state, contractVersion: version });
|
|
346
|
+
return carryMarkers(router, createRouterInstance({ ...state, contractVersion: version }));
|
|
347
347
|
}
|
|
348
348
|
};
|
|
349
|
+
return router;
|
|
350
|
+
}
|
|
351
|
+
var MARKERS = ["_surfaceNamespace", "_stateManifest"];
|
|
352
|
+
function carryMarkers(from, to) {
|
|
353
|
+
for (const marker of MARKERS) {
|
|
354
|
+
if (from[marker] !== void 0) {
|
|
355
|
+
Object.defineProperty(to, marker, { value: from[marker], enumerable: false });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return to;
|
|
349
359
|
}
|
|
350
360
|
function assertContractVersion(version) {
|
|
351
361
|
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/.test(version)) {
|