sse-stream-plugin-sdk 1.0.0 → 1.0.2
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/CHANGELOG.md +36 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -996
- package/dist/index.mp.js +1 -0
- package/dist/index.umd.js +1 -1018
- package/dist/package.json +3 -0
- package/package.json +6 -4
- package/dist/index.cjs +0 -1012
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/index.umd.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
本文件记录 sse-stream-plugin-sdk 的版本变更。
|
|
4
|
+
|
|
5
|
+
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
6
|
+
|
|
7
|
+
## [1.0.2] - 2026-10-04
|
|
8
|
+
|
|
9
|
+
### 修复
|
|
10
|
+
|
|
11
|
+
- 修复微信小程序「构建 npm」解析产物时报 `SyntaxError: Unexpected token` 的问题:1.0.1 的 CJS 产物为 ES2020 语法,微信开发者工具内置解析器不支持可选链(`?.`)、空值合并(`??`)等新语法。
|
|
12
|
+
- 新增 ES5 兼容的小程序专用产物 `dist/index.mp.js`(CJS,async/await、迭代器、对象展开等通过 tslib helpers 降级),并在 package.json 增加 `miniprogram` 字段指向该文件;微信开发者工具构建 npm 时会自动优先使用此入口。
|
|
13
|
+
- 浏览器 ESM/UMD 与 Node CJS 入口仍保持 ES2020,不受影响。
|
|
14
|
+
|
|
15
|
+
## [1.0.1] - 2026-10-04
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- 修复微信小程序「构建 npm」时报 `Npm package entry file not found` 的问题:微信开发者工具不识别 `.cjs` 后缀入口(会将 `dist/index.cjs` 机械补全为 `dist/index.cjs.js` 查找),CJS 产物改为输出为 `dist/index.js`,`main` 与 `exports.require` 同步指向该文件。
|
|
20
|
+
- 新增 `dist/package.json`(`{"type": "commonjs"}`)为 dist 目录声明 CommonJS 作用域,确保 `dist/index.js` 在 Node.js 与微信小程序两侧均按 CJS 正确加载(ESM 入口 `dist/index.mjs` 不受影响)。
|
|
21
|
+
|
|
22
|
+
## [1.0.0] - 2026-10-04
|
|
23
|
+
|
|
24
|
+
### 新增
|
|
25
|
+
|
|
26
|
+
- 首次发布。插件化跨端 SSE 核心 SDK,框架无关、零运行时依赖。
|
|
27
|
+
- 核心类 `PluginSSE`:统一管理连接生命周期、事件订阅(`connecting` / `open` / `message` / `ping` / `end` / `closed` / `error` 等)与实例销毁。
|
|
28
|
+
- 可插拔传输层:
|
|
29
|
+
- `webFetchTransport`:基于 fetch + ReadableStream 的浏览器传输层(默认自动探测);
|
|
30
|
+
- `createMpTransport`:通用小程序传输层工厂,通过注入平台 request API(微信 `wx`、抖音 `tt`、快手 `ks` 等)支持跨小程序平台,基于 `enableChunked` + `onChunkReceived` 分片接收;
|
|
31
|
+
- `mockTransport`:本地模拟流式响应的测试传输层;
|
|
32
|
+
- 支持通过上下文 `registerTransport` 注册自定义传输层。
|
|
33
|
+
- 可替换的 SSE 解析器(`createSSEParser`),支持 `data` / `event` / `id` / 注释心跳行。
|
|
34
|
+
- 请求中间件与事件中间件机制。
|
|
35
|
+
- 内置插件:`authPlugin`(鉴权与 401 刷新)、`reconnectPlugin`(断线重连 + Last-Event-ID)、`heartbeatPlugin`(心跳超时检测)、`throttlePlugin`(消息节流合并)、`loggerPlugin`(日志)、`statsPlugin`(统计与关键词命中),以及一键装载的 `recommendedPlugins()`。
|
|
36
|
+
- 产物格式:ESM(`dist/index.mjs`)、CJS(`dist/index.js`)、UMD(`dist/index.umd.js`,全局名 `PluginSSESDK`)及合并的 TypeScript 类型声明(`dist/index.d.ts`)。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";class e{constructor(){Object.defineProperty(this,"listeners",{enumerable:!0,configurable:!0,writable:!0,value:new Map})}on(e,t){let r=this.listeners.get(e);return r||(r=new Set,this.listeners.set(e,r)),r.add(t),()=>this.off(e,t)}once(e,t){const r=this.on(e,(...e)=>{r(),t(...e)});return r}off(e,t){this.listeners.get(e)?.delete(t)}emit(e,...t){const r=this.listeners.get(e);if(r)for(const e of[...r])e(...t)}clear(){this.listeners.clear()}}function t(){let e="";return{write:(t,s)=>{e+=t;const n=e.replace(/\r\n?/g,"\n").split("\n\n");e=n.pop()??"";for(const e of n){if(!e.trim())continue;const t=r(e);t&&s(t)}},flush:t=>{const s=e.replace(/\r\n?/g,"\n").trim();if(e="",!s)return;const n=r(s);n&&t(n)}}}function r(e){let t="message";const r=[];let s,n=!1;for(const i of e.split("\n")){if(!i||i.startsWith(":"))continue;const e=i.indexOf(":");let o,a;switch(-1===e?(o=i,a=""):(o=i.slice(0,e),a=i.slice(e+1)," "===a.charAt(0)&&(a=a.slice(1))),o){case"event":t=a;break;case"data":r.push(a);break;case"id":a.includes("\0")||(s=a,n=!0)}}return 0===r.length?null:{name:t,data:r.join("\n"),...n?{id:s}:{}}}const s=()=>{let e=null,t=!1;return{name:"web-fetch",async connect(r,s){try{const i=await fetch(r.url,{method:r.method,headers:{"Content-Type":"application/json",...r.headers},body:"POST"===r.method?r.body:void 0});if(s.response?.({status:i.status,headers:n(i.headers)}),!i.ok||!i.body)throw new Error(`HTTP ${i.status} 流不可用`);e=i.body.getReader();const o=new TextDecoder("utf-8");for(;;){const{done:r,value:n}=await e.read();if(t)break;if(r){s.end();break}s.data(o.decode(n,{stream:!0}))}}catch(e){t||s.error(e)}},abort(){t=!0,e?.cancel().catch(()=>{}),e=null}}};function n(e){const t={};return e.forEach((e,r)=>{t[r]=e}),t}function i(){return[{id:"m1",data:"开始解释:\n"},{id:"m2",data:"输入内容:你好\n"},{name:"ping",data:"",gap:200},{id:"m3",data:"\n1. 语法解析完成"},{id:"m4",data:"\n2. 语义分析完成"},{id:"m5",data:"\n3. 生成解释结果"},{id:"m6",data:"\n✅ 解释完毕"}]}function o(e){const t=e?.events??[{id:"m1",data:"开始解释:\n"},{id:"m2",data:"输入内容:你好\n"},{name:"ping",data:"",gap:200},{id:"m3",data:"\n1. 语法解析完成"},{id:"m4",data:"\n2. 语义分析完成"},{id:"m5",data:"\n3. 生成解释结果"},{id:"m6",data:"\n✅ 解释完毕"}],r=e?.failAfter??0,s=e?.errorMessage??"模拟网络中断(连接被重置)";let n=!1;return({request:e})=>{const i=[];let o=!1,a=0;const l=e.headers["Last-Event-ID"]??e.headers["last-event-id"];if(l){const e=t.findIndex(e=>e.id===l);e>=0&&(a=e+1)}return{name:"mock",connect(e,l){const c=()=>{if(o)return;if(r>0&&a===r&&!n){n=!0;const e=setTimeout(()=>{o||l.error(new Error(s))},500);return void i.push(e)}if(a>=t.length){const e=setTimeout(()=>{o||l.end()},200);return void i.push(e)}const e=t[a],d=setTimeout(()=>{o||(l.data(function(e){const t=[];e.id&&t.push(`id:${e.id}`);t.push(`event:${e.name??"message"}`);for(const r of(e.data??"").split("\n"))t.push(`data:${r}`);return t.join("\n")+"\n\n"}(e)),a++,c())},e.gap??350);i.push(d)};c()},abort(){o=!0;for(const e of i)clearTimeout(e);i.length=0}}}}function a(){if("function"==typeof globalThis.fetch)return s;throw new Error("未检测到可用传输层,请通过 options.transport 显式指定,或用 createMpTransport 注册小程序传输层")}function l(e){const t=e?.maxRetries??3,r=e?.delay??1500;let s="",n=0,i=null;return{name:"reconnect",install(e){e.addRequestMiddleware(async(e,t)=>(s&&(e.headers["Last-Event-ID"]=s),t(e))),e.emitter.on("event",e=>{e.id&&(s=e.id)}),e.emitter.on("error",s=>{e.isDead()||(n>=t?e.emitter.emit("reconnect-failed",s):(n++,e.emitter.emit("reconnect",{attempt:n,inMs:r}),i=setTimeout(()=>{i=null,e.start()},r)))}),e.emitter.on("end",()=>{n=0,s=""}),e.onDispose(()=>{i&&clearTimeout(i),i=null})}}}function c(e){const t=e?.interval??1e4;let r=null;return{name:"heartbeat",install(e){const s=()=>{r&&(clearTimeout(r),r=null)},n=()=>{s(),r=setTimeout(()=>{e.emitter.emit("heartbeat-lost"),e.fail(new Error(`心跳丢失:${t}ms 内未收到任何数据`))},t)};e.emitter.on("connecting",n),e.emitter.on("data",n),e.emitter.on("end",s),e.emitter.on("error",s),e.onDispose(s)}}}function d(e){const t=e?.windowMs??100;let r,s="",n=null,i=null;return{name:"throttle",install(e){const o=()=>{if(n&&(clearTimeout(n),n=null),s&&i){const e=s,t=r;s="",r=void 0;const n=i;i=null,n({name:"message",data:e,...t?{id:t}:{}})}};e.addEventMiddleware((e,a)=>{if("message"!==e.name)return a(e);s+=e.data,e.id&&(r=e.id),i=a,n||(n=setTimeout(o,t))}),e.emitter.on("end",o),e.emitter.on("error",o),e.onDispose(o)}}}exports.Emitter=e,exports.PluginSSE=class{constructor(r){Object.defineProperty(this,"emitter",{enumerable:!0,configurable:!0,writable:!0,value:new e}),Object.defineProperty(this,"plugins",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"installed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"context",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"transportRegistry",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"requestMiddleware",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"eventMiddleware",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"disposeFns",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"parserFactory",{enumerable:!0,configurable:!0,writable:!0,value:t}),Object.defineProperty(this,"parser",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"activeTransport",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"currentRequest",{enumerable:!0,configurable:!0,writable:!0,value:{url:"",method:"GET",headers:{}}}),Object.defineProperty(this,"options",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"state",{enumerable:!0,configurable:!0,writable:!0,value:"idle"}),Object.defineProperty(this,"sessionId",{enumerable:!0,configurable:!0,writable:!0,value:0}),Object.defineProperty(this,"dead",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"dispatch",{enumerable:!0,configurable:!0,writable:!0,value:e=>{!function(e,t,r,s){let n=-1;const i=(t,s)=>{if(t<=n)return Promise.reject(new Error("next() 被多次调用"));if(n=t,t===e.length)return r(s),Promise.resolve();try{return Promise.resolve(e[t](s,e=>i(t+1,e??s)))}catch(e){return Promise.reject(e)}};i(0,t).catch(s)}(this.eventMiddleware,e,e=>{this.emitter.emit("event",e),this.emitter.emit(e.name,e.data,e)},e=>this.settleError(e))}}),this.options={url:r.url,method:r.method??"GET",body:r.body??"",headers:r.headers??{},transport:r.transport,plugins:r.plugins??[],parser:r.parser},this.options.parser&&(this.parserFactory=this.options.parser),this.transportRegistry.set("web",s),this.transportRegistry.set("mock",o());const n=this;this.context={emitter:this.emitter,get request(){return n.currentRequest},addRequestMiddleware:e=>this.requestMiddleware.push(e),addEventMiddleware:e=>this.eventMiddleware.push(e),setParser:e=>{this.parserFactory=e},registerTransport:(e,t)=>this.transportRegistry.set(e,t),start:()=>this.start(),fail:e=>this.settleError(e),getState:()=>this.state,isDead:()=>this.dead,onDispose:e=>this.disposeFns.push(e)};for(const e of this.options.plugins)this.use(e)}use(e){if(this.dead)throw new Error("实例已销毁,无法再注册插件");return this.plugins.some(t=>t.name===e.name)||(this.plugins.push(e),this.installed&&e.install(this.context)),this}async connect(){if(this.dead)throw new Error("实例已销毁,请创建新的 PluginSSE");await this.start()}async start(){if(this.dead)return;this.installed||this.installPlugins(),this.silentAbort();const e=++this.sessionId;let t;this.state="connecting",this.currentRequest={url:this.options.url,method:this.options.method,headers:{...this.options.headers},body:"POST"===this.options.method?this.options.body:void 0},this.emitter.emit("connecting",{session:e});try{t=await(r=this.requestMiddleware,e=>{let t=-1;const s=(e,n)=>{if(e<=t)return Promise.reject(new Error("next() 被多次调用"));if(t=e,e===r.length)return Promise.resolve(n);try{return Promise.resolve(r[e](n,t=>s(e+1,t??n)))}catch(e){return Promise.reject(e)}};return s(0,e)})(this.currentRequest)}catch(t){return void(e===this.sessionId&&this.settleError(t))}var r;if(e!==this.sessionId||this.dead)return;this.currentRequest=t,this.parser=this.parserFactory();const s=this.resolveTransportFactory()({request:t});this.activeTransport=s,s.connect(t,{data:t=>{e===this.sessionId&&(this.emitter.emit("data",t),this.parser?.write(t,this.dispatch))},end:()=>{e===this.sessionId&&(this.parser?.flush(this.dispatch),this.state="closed",this.emitter.emit("end"))},error:t=>{e===this.sessionId&&this.settleError(t)},response:t=>{e===this.sessionId&&this.emitter.emit("response",t)}}),this.state="open",this.emitter.emit("open",{transport:s.name})}destroy(){if(!this.dead){this.dead=!0,this.sessionId++,this.silentAbort(),this.state="closed";for(const e of this.disposeFns)try{e()}catch{}this.emitter.emit("closed"),this.emitter.clear()}}on(e,t){return this.emitter.on(e,t)}once(e,t){return this.emitter.once(e,t)}off(e,t){this.emitter.off(e,t)}installPlugins(){this.installed=!0;for(const e of this.plugins)e.install(this.context)}resolveTransportFactory(){const e=this.options.transport;if("string"==typeof e){const t=this.transportRegistry.get(e);if(!t)throw new Error(`未注册的传输层:${e}`);return t}return e||a()}settleError(e){this.dead||(this.sessionId++,this.silentAbort(),this.state="closed",this.emitter.emit("error",e))}silentAbort(){if(this.activeTransport){try{this.activeTransport.abort()}catch{}this.activeTransport=null}}},exports.authPlugin=function(e){const t=e.header??"Authorization",r=e.scheme??"Bearer";let s=!1;return{name:"auth",install(n){n.addRequestMiddleware(async(s,n)=>{const i=await e.getToken();return s.headers[t]=r?`${r} ${i}`:i,n(s)}),n.emitter.on("response",async({status:t})=>{if(401===t&&!s&&!n.isDead()){s=!0,n.emitter.emit("auth-refreshing");try{await e.refreshToken()}finally{s=!1}n.start()}})}}},exports.createMpTransport=function(e,t="miniprogram"){return()=>{let r,s=null,n=!1;return{name:t,connect(t,i){const o=e();s=o.request({url:t.url,method:t.method,header:{...t.headers},data:"POST"===t.method&&t.body?JSON.parse(t.body):void 0,enableChunked:!0,responseType:"arraybuffer",success:e=>{n||(i.response?.({status:e.statusCode}),i.end())},fail:e=>{n||i.error(function(e){if(e instanceof Error)return e;if(null!==e&&"object"==typeof e&&"errMsg"in e){const t=e.errMsg;return new Error("string"==typeof t&&t?t:"request 失败")}return new Error(String(e))}(e))}}),s.onChunkReceived(e=>{n||i.data(function(e){void 0===r&&(r="undefined"!=typeof TextDecoder?new TextDecoder("utf-8"):null);if(r)return r.decode(e,{stream:!0});let t="";for(let r=0;r<e.length;r++)t+="%"+e[r].toString(16).padStart(2,"0");return decodeURIComponent(t)}(new Uint8Array(e.data)))})},abort(){n=!0,s?.abort(),s=null}}}},exports.createSSEParser=t,exports.defaultScript=i,exports.detectTransport=a,exports.heartbeatPlugin=c,exports.loggerPlugin=function(e){const t=e?.sink??(e=>console.log(e));return{name:"logger",install(e){e.emitter.on("open",({transport:r})=>{const s=JSON.stringify(e.request.headers);t(`连接已打开(${r})请求头:${s}`)}),e.emitter.on("response",({status:e})=>{t(`收到响应,状态码:${e}`)}),e.emitter.on("event",e=>{const r=e.data.replace(/\n/g,"\\n"),s=r.length>40?r.slice(0,40)+"…":r;t(`事件 ${e.name}${e.id?" #"+e.id:""} (${e.data.length} 字): ${s}`)}),e.emitter.on("reconnect",({attempt:e,inMs:r})=>{t(`连接中断,${r}ms 后进行第 ${e} 次重连…`)}),e.emitter.on("heartbeat-lost",()=>{t("心跳丢失!")}),e.emitter.on("reconnect-failed",()=>{t("已达最大重试次数,不再重连")}),e.emitter.on("error",e=>{t(`发生错误:${e instanceof Error?e.message:String(e)}`)}),e.emitter.on("end",()=>{t("流正常结束")}),e.emitter.on("closed",()=>{t("实例已销毁,资源已释放")})}}},exports.mockTransport=o,exports.recommendedPlugins=function(){return[l(),c(),d()]},exports.reconnectPlugin=l,exports.statsPlugin=function(e){const t=e?.keyword??"✅";return{name:"stats",install(e){let r=0,s=0;e.emitter.on("message",n=>{r++,s+=n.length;const i={messages:r,chars:s};e.emitter.emit("stats",i),n.includes(t)&&e.emitter.emit("keyword",{keyword:t,at:s})}),e.emitter.on("connecting",()=>{r=0,s=0,e.emitter.emit("stats",{messages:r,chars:s})})}}},exports.throttlePlugin=d,exports.webFetchTransport=s;
|