├── .nvmrc ├── docs ├── plugins │ ├── WxLogsPlugin.md │ ├── index.md │ └── IframePlugin.md ├── internal │ ├── termui.png │ ├── internal.png │ ├── logs_sys_dev.md │ ├── termui.md │ └── index.md ├── operators │ ├── resizeObserver.md │ ├── task.md │ ├── newsTime.md │ ├── debounceTime.md │ ├── throttleTime.md │ ├── map.md │ ├── interval.md │ └── index.md ├── nav.md ├── other.md └── unmq.md ├── src ├── plugins │ ├── router │ │ └── index.ts │ ├── pipeline │ │ └── index.ts │ ├── websocket │ │ └── index.ts │ ├── process │ │ └── index.ts │ ├── tabs │ │ └── index.ts │ ├── index.ts │ ├── storage │ │ ├── StorageAdapterAbstract.ts │ │ ├── StorageSignAbstract.ts │ │ ├── storageTypeof.ts │ │ └── index.ts │ ├── store │ │ └── index.ts │ ├── wx-logs │ │ ├── config.ts │ │ ├── listener.ts │ │ ├── proxyApi.ts │ │ └── index.ts │ └── iframe │ │ └── index.ts ├── adapter │ ├── VuexStorageAdapter.ts │ └── PiniaStorageAdapter.ts ├── utils │ ├── types.ts │ └── tools.ts ├── operators │ ├── task │ │ └── index.ts │ ├── filter │ │ └── index.ts │ ├── of │ │ └── index.ts │ ├── newsTime │ │ └── index.ts │ ├── map │ │ └── index.ts │ ├── instant │ │ └── index.ts │ ├── removeDuplicates │ │ └── index.ts │ ├── index.ts │ ├── resizeObserver │ │ └── index.ts │ ├── throttleTime │ │ └── index.ts │ ├── debounceTime │ │ └── index.ts │ └── interval │ │ └── index.ts ├── internal │ ├── News.ts │ ├── Queue │ │ ├── operators.ts │ │ └── index.ts │ ├── Consumer.ts │ ├── Exchange.ts │ └── Logs.ts ├── index.ts └── core │ ├── ExchangeCollectionHandle.ts │ ├── QueueCollectionHandle.ts │ ├── SingleUNodeMQ.ts │ ├── QuickUNodeMQ.ts │ ├── UNodeMQ.ts │ └── Collection.ts ├── _config.yml ├── .eslintignore ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── .gitignore ├── termui ├── src │ ├── util │ │ └── util.go │ ├── view │ │ ├── tabsView.go │ │ ├── header.go │ │ ├── newsView.go │ │ ├── consumerView.go │ │ ├── exchangeView.go │ │ ├── queueView.go │ │ └── view.go │ ├── data │ │ ├── abstract.go │ │ ├── newsTable.go │ │ ├── consumerTable.go │ │ ├── queueTable.go │ │ └── exchangeTable.go │ └── controller │ │ └── controller.go ├── main.go ├── go.mod └── go.sum ├── jest.config.js ├── script ├── clear.js ├── dts-bundle-generator.js ├── build.js └── publish.js ├── .eslintrc.cjs ├── test ├── operators │ ├── instant.test.ts │ ├── of.test.ts │ ├── task.test.ts │ ├── filter.test.ts │ ├── map.test.ts │ ├── removeDuplicates.test.ts │ ├── newsTime.test.ts │ ├── debounceTime.test.ts │ ├── throttleTime.test.ts │ └── interval.test.ts ├── SingleUNodeMQ.test.ts ├── QuickUNodeMQ.test.ts └── UNodeMQ.test.ts ├── .prettierrc ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tsconfig.json ├── LICENSE ├── todo.md ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 2 | -------------------------------------------------------------------------------- /docs/plugins/WxLogsPlugin.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/router/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/pipeline/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/websocket/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /src/plugins/process/index.ts: -------------------------------------------------------------------------------- 1 | console.log("test"); 2 | -------------------------------------------------------------------------------- /src/plugins/tabs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TODO:使用BroadcastChannel进行浏览器tabs同域通信 3 | */ -------------------------------------------------------------------------------- /docs/internal/termui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juaoie/u-node-mq/HEAD/docs/internal/termui.png -------------------------------------------------------------------------------- /src/adapter/VuexStorageAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 适配vuex 的storage存储 3 | * 返回modules 集成进去 4 | */ 5 | -------------------------------------------------------------------------------- /docs/internal/internal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juaoie/u-node-mq/HEAD/docs/internal/internal.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | dist 4 | docs 5 | test-node 6 | node_modules 7 | u-node-mq 8 | packages -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | index.js 4 | index.js.map 5 | u-node-mq 6 | *.tgz 7 | vendor 8 | *.log 9 | *.exe 10 | u-node-mq-termui -------------------------------------------------------------------------------- /termui/src/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "time" 4 | 5 | var CstSh, _ = time.LoadLocation("Asia/Shanghai") //上海 6 | var FormatStamp = "15:04:05.000" 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | 3 | export default { 4 | preset: "ts-jest", 5 | testEnvironment: "jsdom", 6 | timers: "fake", 7 | }; 8 | -------------------------------------------------------------------------------- /docs/operators/resizeObserver.md: -------------------------------------------------------------------------------- 1 |

🚲 resizeObserver 监听 Element 的内容区域或 SVGElement的边界框改变

2 | 3 | ```javascript 4 | const singleUnmq = createSingleUnmq().add(resizeObserver()); 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/operators/task.md: -------------------------------------------------------------------------------- 1 |

🏆 task 设置队列能加入消息的数量

2 | 3 | ```javascript 4 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 5 | qu1: new Queue().add(task(2)), 6 | }); 7 | ``` 8 | -------------------------------------------------------------------------------- /termui/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "u-node-mq-termui/src/controller" 5 | "u-node-mq-termui/src/view" 6 | ) 7 | 8 | func main() { 9 | go controller.SetQueueData() 10 | view.Init() 11 | } 12 | -------------------------------------------------------------------------------- /docs/operators/newsTime.md: -------------------------------------------------------------------------------- 1 |

🚲 newsTime 设置消息最长存活时长

2 | 3 | ```javascript 4 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 5 | qu1: new Queue().add(newsTime(3000)), 6 | }); 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/operators/debounceTime.md: -------------------------------------------------------------------------------- 1 |

🚴 debounceTime 防抖功能

2 | 3 | ```javascript 4 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 5 | qu1: new Queue().add(debounceTime(1000, true)), 6 | }); 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/operators/throttleTime.md: -------------------------------------------------------------------------------- 1 |

🎾 throttleTime 节流功能

2 | 3 | ```javascript 4 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 5 | qu1: new Queue().add(throttleTime(1000, true)), 6 | }); 7 | ``` 8 | -------------------------------------------------------------------------------- /termui/src/view/tabsView.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | ui "github.com/gizak/termui/v3" 5 | ) 6 | 7 | //渲染tabs表格 8 | func renderTabsTable() { 9 | w, _ := ui.TerminalDimensions() 10 | //tabs切换 11 | tabs.Border = false 12 | tabs.SetRect(0, 1, w, 2) 13 | ui.Render(tabs) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 定时器id类型 3 | * 需要兼容dom,node,小程序环境 4 | */ 5 | export type IntTime = number; 6 | 7 | /** 8 | * 五个组件名称的枚举 9 | */ 10 | export enum ComponentEnum { 11 | "EXCHANGE" = "exchange", 12 | "QUEUE" = "queue", 13 | "NEWS" = "news", 14 | "CONSUMER" = "consumer", 15 | "LOGS" = "logs", 16 | } 17 | -------------------------------------------------------------------------------- /docs/operators/map.md: -------------------------------------------------------------------------------- 1 |

🌞 map 对队列消息进行映射

2 | 3 | ```javascript 4 | import UNodeMQ, { Exchange, Queue, ConsumMode, createQuickUnmq, map } from "u-node-mq"; 5 | 6 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 7 | qu1: new Queue().add(map((value, index) => value * 10)), 8 | }); 9 | ``` 10 | -------------------------------------------------------------------------------- /src/operators/task/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator } from "../.."; 2 | 3 | /** 4 | * task 控制队列能存入几条消息 5 | * @param count 6 | * @returns 7 | */ 8 | export default function task(count: number): Operator { 9 | let seen = 0; 10 | return { 11 | beforeAddNews() { 12 | return ++seen <= count; 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /script/clear.js: -------------------------------------------------------------------------------- 1 | // const chalk = require("chalk"); 2 | // const fs = require("fs-extra"); 3 | 4 | import chalk from "chalk"; 5 | import fs from "fs-extra"; 6 | 7 | (async () => { 8 | await Promise.all([fs.remove("u-node-mq"), fs.remove("dist"), fs.remove("termui/u-node-mq-termui.exe")]); 9 | console.log(chalk.blue("缓存清除成功!")); 10 | })(); 11 | -------------------------------------------------------------------------------- /src/operators/filter/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator, News } from "../.."; 2 | 3 | /** 4 | * filter 过滤 5 | * @param fun 6 | * @returns boolean 返回值控制是否加入队列 7 | */ 8 | export default function filter(fun: (res: D) => boolean | Promise): Operator { 9 | return { 10 | beforeAddNews(res: News) { 11 | return fun(res.content); 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/operators/of/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator, Queue } from "../.."; 2 | 3 | /** 4 | * 提前发射数据,可用来设置默认值或者用来快速测试 5 | * @param args 6 | * @returns 7 | */ 8 | export default function of(...args: D[]): Operator { 9 | return { 10 | mounted(queue: Queue) { 11 | args.forEach(item => { 12 | queue.pushContent(item); 13 | }); 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/operators/newsTime/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator } from "../.."; 2 | import News from "../../internal/News"; 3 | 4 | /** 5 | * newsTime 设置消息存活时长 6 | * @param time 存活时长毫秒数 7 | * @returns 8 | */ 9 | export default function newsTime(time: number): Operator { 10 | return { 11 | ejectNews(news: News) { 12 | return new Date().getTime() < time + news.createdTime; 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import IframePlugin from "./iframe/index"; 2 | import WxLogsPlugin from "./wx-logs/index"; 3 | export { IframePlugin, WxLogsPlugin }; 4 | 5 | /** 6 | * 安装插件的方法 7 | */ 8 | export type PluginInstallFunction = (unmq: any, ...options: any[]) => void; 9 | export type Plugin = 10 | | (PluginInstallFunction & { install?: PluginInstallFunction }) 11 | | { 12 | install: PluginInstallFunction; 13 | }; 14 | -------------------------------------------------------------------------------- /termui/src/view/header.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | ui "github.com/gizak/termui/v3" 5 | ) 6 | 7 | //渲染头部 8 | func renderHeaderTable() { 9 | w, _ := ui.TerminalDimensions() 10 | //头部提示 11 | 12 | header.Text = `q:退出 <-:tabs左移 ->:tabs右移 c:清空 ctrl+c:清空所有 监听端口号:9090` 13 | header.SetRect(0, 0, w, 1) 14 | header.Border = false 15 | header.TextStyle.Bg = ui.ColorBlue 16 | 17 | ui.Render(header) 18 | } 19 | -------------------------------------------------------------------------------- /src/operators/map/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator } from "../.."; 2 | /** 3 | * map 方法返回的数据类型和队列一致 4 | * @param project 5 | * @returns 6 | */ 7 | 8 | export default function map(project: (value: D, index: number) => D): Operator { 9 | let index = 0; 10 | return { 11 | beforeAddNews(num) { 12 | num.content = project(num.content, index); 13 | index++; 14 | return true; 15 | }, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/operators/instant/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator, Queue, ConsumMode } from "../../index"; 2 | 3 | /** 4 | * 扩展为观察者模式,即在同一事件循环内消费所有消息 5 | * 使用off方法可以移除此属性,开发时请注意协调 6 | * @returns 7 | */ 8 | export default function instant(): Operator { 9 | return { 10 | mounted(that: Queue) { 11 | if (that.mode !== ConsumMode.All) throw `${that.name} 队列 mode 需要设置成All`; 12 | that.pushConsume(() => true); 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /termui/src/data/abstract.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | //表接口 4 | type TableOper interface { 5 | add(TableField) bool 6 | dele() 7 | set() 8 | } 9 | 10 | //公共字段 11 | type BaseLogData struct { 12 | Id string `json:"id"` 13 | CreatedTime int64 `json:"createdTime"` //毫秒时间戳 14 | Name string `json:"name"` 15 | } 16 | 17 | //表公共字段 18 | type TableField struct { 19 | BaseLogData 20 | UpdateTime int64 `json:"updateTime"` //最后更新时间 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/storage/StorageAdapterAbstract.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 内存存储的中间适配器 3 | */ 4 | export default abstract class StorageAdapterAbstract { 5 | /** 6 | * 初始化数据 7 | * @param o 8 | */ 9 | abstract init(o: Record): void; 10 | /** 11 | * 获取数据 12 | * @param key 13 | */ 14 | abstract getData(key: string): string; 15 | /** 16 | * 设置数据 17 | * @param key 18 | * @param value 19 | */ 20 | abstract setData(key: string, value: string): void; 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/storage/StorageSignAbstract.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * storage加密适配器 3 | */ 4 | export default abstract class StorageSignAbstract { 5 | /** 6 | * 加密名称 7 | * @param plaintext 8 | */ 9 | abstract encryptName(plaintext: string): string; 10 | /** 11 | * 加密值 12 | * @param plaintext 13 | */ 14 | abstract encryptValue(plaintext: string): string; 15 | /** 16 | * 解密值 17 | * @param ciphertext 18 | */ 19 | abstract decryptValue(ciphertext: string): string; 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: false, 4 | browser: true, 5 | }, 6 | extends: ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], //定义文件继承的子规范 7 | parser: "@typescript-eslint/parser", //定义ESLint的解析器 8 | plugins: ["@typescript-eslint"], //定义了该eslint文件所依赖的插件 9 | 10 | globals: {}, 11 | 12 | rules: { 13 | "@typescript-eslint/no-explicit-any": "off", //允许使用any类型 14 | }, 15 | // parserOptions: { 16 | // project: "./tsconfig.json", 17 | // }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/plugins/store/index.ts: -------------------------------------------------------------------------------- 1 | import UNodeMQ, { Exchange, Queue } from "../../index"; 2 | 3 | export default class StorePlugin>, Record>>> { 4 | private unmq: D | null = null; 5 | private data: Record = {}; 6 | defineStore(name: string, defaultValue: T) { 7 | if (this.unmq === null) throw "StorePlugin 未安装"; 8 | if (this.data[name]) { 9 | } 10 | } 11 | install(unmq: D) { 12 | this.unmq = unmq; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/operators/instant.test.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "../../src/index"; 2 | import instant from "../../src/operators/instant"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,instant测试", function (done) { 7 | const qu1 = new Queue() 8 | //使用 operate 9 | .add(instant()); 10 | setTimeout(() => { 11 | expect(qu1.getNews().length).toEqual(0); 12 | done(); 13 | }, 100); 14 | [1, 2, 3, 4, 5, 6].forEach(res => { 15 | qu1.pushContent(res); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "ignore", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": true, 8 | "jsxSingleQuote": false, 9 | "printWidth": 150, 10 | "proseWrap": "preserve", 11 | "quoteProps": "preserve", 12 | "requirePragma": false, 13 | "semi": true, 14 | "singleQuote": false, 15 | "tabWidth": 2, 16 | "trailingComma": "all", 17 | "useTabs": false, 18 | "endOfLine": "auto" 19 | } 20 | -------------------------------------------------------------------------------- /docs/operators/interval.md: -------------------------------------------------------------------------------- 1 | ## interval 2 | 3 | 在项目中过度使用 setInterval 可能导致一些死循环或者没有 clearInterval 的问题 4 | 5 | 建议在项目中使用 unmq 全局设置一个 interval,再使用 throttleTime 去为每个需要定时循环的的地方做定时数据分发 6 | 7 | 将 interval 第二个参数设置为 true,可以在没有消费者的时候停止循环 8 | 9 | ```javascript 10 | import UNodeMQ, { Exchange, Queue, ConsumMode, createQuickUnmq,interval } from "u-node-mq"; 11 | 12 | const quickUnmq = createQuickUnmq(new Exchange(), { 13 | qu1: new Queue() 14 | //使用 operate 15 | .add(interval(1000, false)), 16 | }); 17 | ``` 18 | -------------------------------------------------------------------------------- /src/operators/removeDuplicates/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator, News } from "../.."; 2 | 3 | /** 4 | * removeDuplicates 去重 5 | * @param fun 根据fun 返回的id进行判断是否需要去重 6 | * @returns 7 | */ 8 | export default function removeDuplicates(fun: (res: any) => any): Operator { 9 | const list: any[] = []; 10 | return { 11 | beforeAddNews(res: News) { 12 | const id = fun(res.content); 13 | if (list.indexOf(id) === -1) { 14 | list.push(id); 15 | return true; 16 | } else return false; 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.autoRun": "off", 3 | "jest.showCoverageOnLoad": false, 4 | // Use the project's typescript version 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | 7 | 8 | // Use prettier to format typescript, javascript and JSON files 9 | "[typescript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[javascript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[json]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/operators/index.ts: -------------------------------------------------------------------------------- 1 | import debounceTime from "./debounceTime"; 2 | import filter from "./filter"; 3 | import instant from "./instant"; 4 | import interval from "./interval"; 5 | import map from "./map"; 6 | import newsTime from "./newsTime"; 7 | import of from "./of"; 8 | import removeDuplicates from "./removeDuplicates"; 9 | import resizeObserver from "./resizeObserver"; 10 | import task from "./task"; 11 | import throttleTime from "./throttleTime"; 12 | 13 | export { debounceTime, filter, instant, interval, map, newsTime, of, removeDuplicates, resizeObserver, task, throttleTime }; 14 | -------------------------------------------------------------------------------- /script/dts-bundle-generator.js: -------------------------------------------------------------------------------- 1 | import { generateDtsBundle } from "dts-bundle-generator"; 2 | import fs from "fs-extra"; 3 | 4 | const options = [ 5 | { 6 | filePath: "src/index.ts", 7 | outFile: "u-node-mq/index.d.ts", 8 | output: { 9 | sortNodes: true, 10 | exportReferencedTypes: false, 11 | }, 12 | }, 13 | ]; 14 | 15 | //generateDtsBundle 不能自动输出,需要手动输出 16 | const res = generateDtsBundle(options, { 17 | preferredConfigPath: "./tsconfig.json", 18 | }); 19 | 20 | options.forEach((item, index) => { 21 | fs.outputFile(item.outFile, res[index]); 22 | }); 23 | -------------------------------------------------------------------------------- /test/operators/of.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import of from "../../src/operators/of"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,of测试", function (done) { 7 | const quickUnmq = createQuickUnmq(new Exchange(), { 8 | qu1: new Queue() 9 | //使用 operate 10 | .add(of(1, 2, 3)), 11 | }); 12 | let num = ""; 13 | quickUnmq.on("qu1", (res: number) => { 14 | num += res; 15 | }); 16 | setTimeout(() => { 17 | expect(num).toEqual("123"); 18 | done(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /docs/nav.md: -------------------------------------------------------------------------------- 1 | # 导航预览 2 | 3 | ## 基础组件和工作原理 4 | 5 | 开发者需要先了解`u-node-mq`中的五大基础组件和工作原理,了解工作原理是灵活使用此工具的关键; 6 | 7 | [点击跳转组件介绍](./internal/index.md) 8 | 9 | ## 使用方法 10 | 11 | 通过对基础组件的封装能够让开发者快速实现需求功能; 12 | 13 | 如果需要一些更加灵活的使用操作,也可以单独去组合组件去实现; 14 | 15 | [点击跳转快速开发介绍](./unmq.md) 16 | 17 | ## 插件功能 18 | 19 | 插件是扩展或者集成`UNodeMQ`类的组件,例如`IframePlugin`实际是集成`UNodeMQ`类的发布订阅模型的工具,所以这里的插件理解为可以快速集成发布订阅模型的工具; 20 | 21 | [点击跳转插件功能](./plugins/index.md) 22 | 23 | ## 管道符操作方法 24 | 25 | 管道符操作方法实际是对队列中的数据进行操作的方法,在消息进入队列和被弹出队列中的生命周期过程中对消息数据进行处理,一些同步操作和异步操作的生命周期极便利的扩展操作符的可操作空间,内置的操作符集合提供了大多数常见的对消息数据进行操作的方法; 26 | 27 | ## 其他示例 28 | 29 | 更多简单示例代码可以在`jest`的测试方法中找到 30 | -------------------------------------------------------------------------------- /test/operators/task.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import task from "../../src/operators/task"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,task测试", function (done) { 7 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 8 | qu1: new Queue() 9 | //使用 operate 10 | .add(task(2)), 11 | }); 12 | let num = ""; 13 | quickUnmq.on("qu1", (res: number) => { 14 | num += res; 15 | }); 16 | quickUnmq.emit(1, 2, 3, 4); 17 | setTimeout(() => { 18 | expect(num).toEqual("12"); 19 | done(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/operators/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import filter from "../../src/operators/filter"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,filter测试", function (done) { 7 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 8 | qu1: new Queue() 9 | //使用 operate 10 | .add(filter(res => res > 3)), 11 | }); 12 | let n = ""; 13 | quickUnmq.on("qu1", (res: number) => { 14 | n += res; 15 | }); 16 | setTimeout(() => { 17 | expect(n).toEqual("456"); 18 | done(); 19 | }, 100); 20 | quickUnmq.emit(1, 2, 3, 4, 5, 6); 21 | }); 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/operators/map.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import map from "../../src/operators/map"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,map测试", function (done) { 7 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 8 | qu1: new Queue() 9 | //使用 operate 10 | .add( 11 | map((value, index) => { 12 | expect(index).toEqual(0); 13 | return value * 10; 14 | }), 15 | ), 16 | }); 17 | quickUnmq.on("qu1", (res: number) => { 18 | expect(res).toEqual(20); 19 | done(); 20 | }); 21 | quickUnmq.emit(2); 22 | }); 23 | -------------------------------------------------------------------------------- /src/plugins/storage/storageTypeof.ts: -------------------------------------------------------------------------------- 1 | import { isString } from "../../index"; 2 | const stringType = "s###"; 3 | const nostringType = "n###"; 4 | 5 | //编码 6 | export function envalue(value: any): string { 7 | if (isString(value)) return stringType + value; 8 | else return nostringType + JSON.stringify(value); 9 | } 10 | 11 | //解密 12 | export function devalue(value: string) { 13 | const type = value.slice(0, 4); 14 | if (type === stringType) { 15 | return value.slice(4); 16 | } else if (type === nostringType) { 17 | try { 18 | return JSON.parse(value.slice(4)); 19 | } catch (error) { 20 | console.log("JSON.pares error"); 21 | return ""; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/adapter/PiniaStorageAdapter.ts: -------------------------------------------------------------------------------- 1 | import { defineStore, StoreDefinition } from "pinia"; 2 | import StorageAdapterAbstract from "../plugins/storage/StorageAdapterAbstract"; 3 | /** 4 | * 实现简单状态管理 5 | */ 6 | 7 | export default class VueStorageAdapter implements StorageAdapterAbstract { 8 | private storeDefinition: StoreDefinition; 9 | init(o: Record) { 10 | this.storeDefinition = defineStore("__storage", { 11 | state: () => o, 12 | }); 13 | } 14 | getData(key: string) { 15 | const store = this.storeDefinition(); 16 | return store[key]; 17 | } 18 | setData(key: string, value: any) { 19 | const store = this.storeDefinition(); 20 | store[key] = value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/operators/removeDuplicates.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import removeDuplicates from "../../src/operators/removeDuplicates"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,removeDuplicates测试", function (done) { 7 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 8 | qu1: new Queue() 9 | //使用 operate 10 | .add(removeDuplicates(res => res)), 11 | }); 12 | let n = ""; 13 | quickUnmq.on("qu1", (res: number) => { 14 | n += res; 15 | }); 16 | setTimeout(() => { 17 | expect(n).toEqual("1234"); 18 | done(); 19 | }, 100); 20 | quickUnmq.emit(1, 2, 3, 3, 4, 4, 4, 4, 4, 4, 1); 21 | }); 22 | -------------------------------------------------------------------------------- /src/plugins/wx-logs/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 日志级别,日志类型,暂就这些类型,不可动态添加 3 | */ 4 | export enum LOG_LEVEL { 5 | Info = "info", 6 | Warn = "warn", 7 | Error = "error", 8 | } 9 | 10 | /** 11 | * 输出类型 12 | * 13 | */ 14 | export enum OUTPUT_TYPE { 15 | //控制台 16 | Console = "console", 17 | //实时日志 18 | Realtime = "realtime", 19 | //xhr请求 20 | Request = "request", 21 | } 22 | 23 | /** 24 | * 不同日志级别对应日志输出类型的配置 25 | */ 26 | export type LevelOutputOption = { 27 | [P in `${LOG_LEVEL}`]: Array<`${OUTPUT_TYPE}`>; 28 | }; 29 | 30 | export const defaultOption: LevelOutputOption = { 31 | [LOG_LEVEL.Info]: [OUTPUT_TYPE.Console], 32 | [LOG_LEVEL.Warn]: [OUTPUT_TYPE.Console, OUTPUT_TYPE.Realtime], 33 | [LOG_LEVEL.Error]: [OUTPUT_TYPE.Console, OUTPUT_TYPE.Request, OUTPUT_TYPE.Realtime], 34 | }; 35 | -------------------------------------------------------------------------------- /src/internal/News.ts: -------------------------------------------------------------------------------- 1 | import { random } from "src/utils/tools"; 2 | import { ComponentEnum } from "../utils/types"; 3 | import Logs from "./Logs"; 4 | 5 | export default class News { 6 | [k: string]: any; 7 | /** 8 | * id 9 | */ 10 | private readonly id: string = random(); 11 | getId() { 12 | return this.id; 13 | } 14 | /** 15 | * 消费者创建时间戳 16 | */ 17 | readonly createdTime: number; 18 | /** 19 | * 消息内容 20 | */ 21 | content: D; 22 | /** 23 | * 剩余可重复消费次数 24 | */ 25 | consumedTimes = -1; 26 | 27 | constructor(content: D) { 28 | this.createdTime = new Date().getTime(); 29 | this.content = content; 30 | Logs.getLogsInstance()?.setLogs(ComponentEnum.NEWS, { id: this.getId(), createdTime: this.createdTime }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/operators/newsTime.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import newsTime from "../../src/operators/newsTime"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,newsTime测试", function (done) { 7 | const qu1 = new Queue().add(newsTime(1000)).add(newsTime(1200)); 8 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 9 | qu1, 10 | }); 11 | let str = ""; 12 | setTimeout(() => { 13 | quickUnmq.once("qu1", () => (str += "a")); 14 | }, 500); 15 | setTimeout(() => { 16 | quickUnmq.once("qu1", () => (str += "b")); 17 | }, 1500); 18 | setTimeout(() => { 19 | expect(str).toEqual("a"); 20 | expect(qu1.getNews().length).toEqual(0); 21 | done(); 22 | }, 2000); 23 | quickUnmq.emit(1, 2, 3); 24 | }); 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests.v2", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand", 13 | "--watchAll=false", 14 | "--testNamePattern", 15 | "${jest.testNamePattern}", 16 | "--runTestsByPath", 17 | "${jest.testFile}" 18 | ], 19 | "cwd": "${workspaceFolder}", 20 | "console": "integratedTerminal", 21 | "internalConsoleOptions": "neverOpen", 22 | "program": "${workspaceFolder}/node_modules/.bin/jest", 23 | "windows": { 24 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /termui/src/view/newsView.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "time" 5 | "u-node-mq-termui/src/data" 6 | "u-node-mq-termui/src/util" 7 | 8 | ui "github.com/gizak/termui/v3" 9 | ) 10 | 11 | //渲染news表格 12 | func renderNewsTable() { 13 | list := data.N.FindList() 14 | 15 | arr := [][]string{ 16 | {"ID", "CreatedTime", "UpdateTime"}, 17 | } 18 | 19 | for _, v := range list { 20 | arr = append(arr, []string{ 21 | v.Id, 22 | time.UnixMilli(v.CreatedTime).Format(util.FormatStamp), 23 | time.UnixMilli(v.UpdateTime).Format(util.FormatStamp), 24 | }) 25 | } 26 | 27 | w, h := ui.TerminalDimensions() 28 | //交换机表格 29 | newsTable.Border = false 30 | newsTable.ColumnWidths = []int{20, 20, w - 40} 31 | newsTable.Rows = arr 32 | newsTable.TextStyle = ui.NewStyle(ui.ColorWhite) 33 | 34 | newsTable.SetRect(0, 2, w, h) 35 | ui.Render(newsTable) 36 | } 37 | -------------------------------------------------------------------------------- /test/operators/debounceTime.test.ts: -------------------------------------------------------------------------------- 1 | import { createSingleUnmq } from "../../src/index"; 2 | import debounceTime from "../../src/operators/debounceTime"; 3 | import { expect, test, jest } from "@jest/globals"; 4 | import { promiseSetTimeout } from "../../src/utils/tools"; 5 | 6 | jest.useFakeTimers(); 7 | 8 | test("debounceTime", async function () { 9 | const singleUnomq1 = createSingleUnmq({ operators: [debounceTime(1000, false)] }); 10 | 11 | const callback = jest.fn(); 12 | singleUnomq1.emit(1, 2, 3); 13 | // setInterval(() => { 14 | // }, 100); 15 | 16 | singleUnomq1.on(() => { 17 | console.log("-------------"); 18 | callback(); 19 | }); 20 | 21 | await promiseSetTimeout(); 22 | 23 | expect(callback).not.toBeCalled(); 24 | 25 | jest.runAllTimers(); 26 | 27 | expect(callback).toBeCalled(); 28 | expect(callback).toHaveBeenCalledTimes(1); 29 | }); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/index.ts", 4 | "src/core/**/*", 5 | "src/internal/**/*", 6 | "src/operators/**/*", 7 | "src/plugins/iframe/**/*", 8 | "src/plugins/wx-logs/**/*" 9 | ], 10 | "exclude": [ 11 | "node_modules" 12 | ], 13 | "compilerOptions": { 14 | "skipLibCheck": true, 15 | "target": "ES6", 16 | "module": "ES6", 17 | "moduleResolution": "Node", 18 | "diagnostics": true, 19 | "outDir": "./dist/", 20 | "lib": [ 21 | "DOM", 22 | "ESNext" 23 | ], 24 | "declaration": true, 25 | "removeComments": false, 26 | "esModuleInterop": true, 27 | "baseUrl": "./", // 设置基准目录 28 | "paths": { 29 | "@/*": [ 30 | "src/*" 31 | ] 32 | }, 33 | "strict": true, 34 | "downlevelIteration": true, 35 | "types": [ 36 | "miniprogram-api-typings" 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /termui/src/view/consumerView.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | "u-node-mq-termui/src/data" 7 | "u-node-mq-termui/src/util" 8 | 9 | ui "github.com/gizak/termui/v3" 10 | ) 11 | 12 | //渲染Consumer表格 13 | func renderConsumerTable() { 14 | list := data.C.FindList() 15 | 16 | arr := [][]string{ 17 | {"ID", "CreatedTime", "UpdateTime", "AcceptedCount"}, 18 | } 19 | 20 | for _, v := range list { 21 | arr = append(arr, []string{ 22 | v.Id, 23 | time.UnixMilli(v.CreatedTime).Format(util.FormatStamp), 24 | time.UnixMilli(v.UpdateTime).Format(util.FormatStamp), 25 | strconv.Itoa(v.AcceptedCount), 26 | }) 27 | } 28 | 29 | w, h := ui.TerminalDimensions() 30 | //交换机表格 31 | consumerTable.Border = false 32 | consumerTable.ColumnWidths = []int{20, 20, 20, w - 60} 33 | consumerTable.Rows = arr 34 | consumerTable.TextStyle = ui.NewStyle(ui.ColorWhite) 35 | 36 | consumerTable.SetRect(0, 2, w, h) 37 | ui.Render(consumerTable) 38 | } 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 主包 3 | */ 4 | import UNodeMQ, { createUnmq } from "./core/UNodeMQ"; 5 | export default UNodeMQ; 6 | export { createUnmq }; 7 | /** 8 | * 扩展包 9 | */ 10 | import SingleUNodeMQ, { createSingleUnmq } from "./core/SingleUNodeMQ"; 11 | import QuickUNodeMQ, { createQuickUnmq } from "./core/QuickUNodeMQ"; 12 | export { SingleUNodeMQ, createSingleUnmq, QuickUNodeMQ, createQuickUnmq }; 13 | /** 14 | * 组件 15 | */ 16 | import Exchange, { ExchangeOption } from "./internal/Exchange"; 17 | import Queue, { ConsumMode, QueueOption } from "./internal/Queue/index"; 18 | import { Operator } from "./internal/Queue/operators"; 19 | import Consumer from "./internal/Consumer"; 20 | import News from "./internal/News"; 21 | import Logs from "./internal/Logs"; 22 | export { Exchange, Queue, Consumer, News, Logs }; 23 | export { ConsumMode, Operator, ExchangeOption, QueueOption }; 24 | 25 | /** 26 | * 管道符 27 | */ 28 | export * from "./operators"; 29 | 30 | /** 31 | * 插件 32 | */ 33 | export * from "./plugins"; 34 | -------------------------------------------------------------------------------- /src/plugins/wx-logs/listener.ts: -------------------------------------------------------------------------------- 1 | // export default function listener() {} 2 | import WxLogsPlugin from "./index"; 3 | import { LOG_LEVEL } from "./config"; 4 | import { wxApi } from "./proxyApi"; 5 | 6 | const t = { 7 | [LOG_LEVEL.Info]: [wxApi("onThemeChange"), wxApi("onNetworkWeakChange"), wxApi("onNetworkStatusChange")], 8 | [LOG_LEVEL.Warn]: [wxApi("onAudioInterruptionEnd"), wxApi("onAudioInterruptionBegin"), wxApi("onMemoryWarning")], 9 | [LOG_LEVEL.Error]: [wxApi("onUnhandledRejection"), wxApi("onPageNotFound"), wxApi("onLazyLoadError"), wxApi("onError")], 10 | }; 11 | /** 12 | * 监听系统变化事件 13 | * @param this 14 | */ 15 | export function onListener(this: WxLogsPlugin) { 16 | t[LOG_LEVEL.Info].forEach(fun => { 17 | if (fun !== null) fun(this[LOG_LEVEL.Info].bind(this)); 18 | }); 19 | 20 | t[LOG_LEVEL.Warn].forEach(fun => { 21 | if (fun !== null) fun(this[LOG_LEVEL.Warn].bind(this)); 22 | }); 23 | 24 | t[LOG_LEVEL.Error].forEach(fun => { 25 | if (fun !== null) fun(this[LOG_LEVEL.Error].bind(this)); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /docs/other.md: -------------------------------------------------------------------------------- 1 | # 其他知识介绍 2 | 3 | 对其他知识的总结和对项目中的疑问的解答 4 | 5 | ## 方法和函数的区别 6 | 7 | 函数是一段在经过封装在单独作用域中的可执行代码; 8 | 9 | 方法是一段在对象上下文作用域中封装的可执行代码; 10 | 11 | ## 观察者模式和发布订阅模型的区别 12 | 13 | 总是有人问`观察者模式`和`发布订阅模式`有什么区别,网上也有一大堆解释,但是从来都没有统一、官方或者权威的描述,下面将就着我在这方面知识的理解做具体的解释; 14 | 15 | 不是`发布订阅模式`,实际上是`发布订阅模型`,它并不是一种`设计模式`; 16 | 17 | `模型`是一种构造系统架构的具体实施方案,`设计模式`则是一种架构抽象的思维方式; 18 | 19 | **观察者模式** 20 | 21 | `观察者模式`由其名一样,是指 `观察者` 主动观察到 `被观察者` 发生变化以后 `观察者` 自己做出相应操作的设计模式;其中`观察者`仅仅是观察(监听)`被观察者`,并不会干扰`被观察者`的任何状态; 22 | 23 | 例如:工厂里机器操作记录员,记录员仅仅是远远的看着(观察)机器运行,然后将机器的操作记录下来,记录员的任何操作都不会干扰机器的执行,但是机器的不同操作会导致记录员记录下不同的数据; 24 | 25 | **发布订阅模型** 26 | 27 | `发布订阅模型`其中是重点是订阅,`消费者`去订阅消息,`生产者`负责生产消息,通过第三方组件去存储、分发消息到`消费者`; 28 | 29 | 例如:人们去订阅报纸,报社生产报纸,而邮局这样的第三方则来临时存储报纸和准确分发不同的报纸到不同的人家中; 30 | 31 | 结论:观察者模式和发布订阅模型还是有很大区别的,观察者模式被观察者往往是具体的数据,且由观察者直接进行观察,发布订阅模型中的消费者具体的消费方法或者消费函数,并且消费的消息由第三方组件进行分发; 32 | 33 | **简单例子:** 34 | 35 | `观察者模式`:在vue中使用watch中的方法对data中的数据进行观察,在观察到数据变化以后执行相应的代码; 36 | 37 | `发布订阅模型`:使用u-node-mq中的emit发生数据到队列,再使用on方法订阅队列中的数据; 38 | 39 | ## 节流和防抖的区别 40 | 41 | `节流`和`防抖` -------------------------------------------------------------------------------- /src/operators/resizeObserver/index.ts: -------------------------------------------------------------------------------- 1 | import { isString } from "../../utils/tools"; 2 | import { Operator, Queue } from "../.."; 3 | 4 | /** 5 | * 使用ResizeObserver订阅内容区域大小变化 6 | * https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver 7 | * @param arg 需要订阅目标元素的id或者dom节点,默认为html元素 8 | * @returns 9 | */ 10 | export default function resizeObserver(arg?: string | HTMLElement): Operator { 11 | let dom: HTMLElement | null = null; 12 | if (arg === undefined) { 13 | dom = document.documentElement; 14 | } else if (isString(arg)) { 15 | dom = document.getElementById(arg); 16 | if (dom === null) throw `id:${arg}不存在`; 17 | } else { 18 | dom = arg; 19 | } 20 | return { 21 | mounted(queue: Queue) { 22 | if (dom === null) return; 23 | const resizeObserver = new ResizeObserver(entries => { 24 | for (const entry of entries) { 25 | queue.pushContent(entry); 26 | } 27 | }); 28 | resizeObserver.observe(dom); 29 | }, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/operators/throttleTime/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator } from "../.."; 2 | import { IntTime } from "../../utils/types"; 3 | 4 | /** 5 | * throttleTime 节流函数 6 | * @param duration 节流间隔时间,单位毫秒 7 | * @param immediate 是否立即执行,默认为false: 8 | * 如果为false,则会拿第一次触发的参数到结束时间执行,而不是拿结束时间之前的最后一次触发的参数去执行; 9 | * 如果为true,则会拿第一次触发的参数立即执行 10 | * @returns 11 | */ 12 | export default function throttleTime(duration: number, immediate?: boolean): Operator { 13 | let now = 0; 14 | let timeId: IntTime | null = null; 15 | return { 16 | beforeAddNews() { 17 | const t = new Date().getTime(); 18 | if (immediate) { 19 | //立即执行 20 | if (t <= now + duration) return false; 21 | now = t; 22 | return true; 23 | } else { 24 | // 25 | if (timeId !== null) return false; 26 | 27 | return new Promise(resolve => { 28 | timeId = setTimeout(() => { 29 | timeId = null; 30 | resolve(true); 31 | }, duration); 32 | }); 33 | } 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/plugins/wx-logs/proxyApi.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from "@/utils/tools"; 2 | 3 | type I = 4 | | "canIUse" 5 | | "onThemeChange" 6 | | "onNetworkWeakChange" 7 | | "onNetworkStatusChange" 8 | | "onAudioInterruptionEnd" 9 | | "onAudioInterruptionBegin" 10 | | "onMemoryWarning" 11 | | "onUnhandledRejection" 12 | | "onPageNotFound" 13 | | "onLazyLoadError" 14 | | "onError" 15 | | "getWindowInfo" 16 | | "getSystemSetting" 17 | | "getSkylineInfoSync" 18 | | "getDeviceInfo" 19 | | "getAppBaseInfo" 20 | | "getAppAuthorizeSetting" 21 | | "getLaunchOptionsSync" 22 | | "getApiCategory" 23 | | "getRealtimeLogManager" 24 | | "getAccountInfoSync"; 25 | 26 | type OptionWx = Pick; 27 | 28 | /** 29 | * 代理请求wx api ,需要基础库最低为1.1.1 30 | * @returns 31 | */ 32 | function proxyWxApi() { 33 | if (!isFunction(wx["canIUse"])) throw new Error("基础库低于 1.1.1"); 34 | // 35 | return function (w: J): OptionWx[J] | null { 36 | if (!isFunction(wx[w])) return null; 37 | 38 | return wx[w]; 39 | }; 40 | } 41 | 42 | export const wxApi = proxyWxApi(); 43 | -------------------------------------------------------------------------------- /script/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import chalk from "chalk"; 3 | import fs from "fs-extra"; 4 | import { execa } from "execa"; 5 | const _package = JSON.parse(fs.readFileSync("./package.json")); 6 | const platform = "neutral"; 7 | const now = new Date().getTime(); 8 | // const operatorsDirList = fs.readdirSync("src/operators"); 9 | 10 | async function buildMain() { 11 | const minify = _package.version.search("beta") === -1; 12 | //清除缓存 13 | // await execa("pnpm", ["clr"]); 14 | 15 | // 构建core 16 | const unmq = esbuild.build({ 17 | entryPoints: ["src/index.ts"], 18 | outfile: "u-node-mq/index.js", 19 | platform, 20 | bundle: true, 21 | minify, 22 | sourcemap: true, 23 | }); 24 | 25 | await Promise.all([ 26 | unmq, 27 | execa("pnpm", ["gdts"]), 28 | fs.copy("package.json", "u-node-mq/package.json"), 29 | fs.copy("LICENSE", "u-node-mq/LICENSE"), 30 | fs.copy("README.md", "u-node-mq/README.md"), 31 | ]); 32 | console.log(chalk.cyanBright("执行时长:" + (new Date().getTime() - now) / 1000 + "秒")); 33 | } 34 | buildMain(); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 松子 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /termui/src/view/exchangeView.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | "u-node-mq-termui/src/data" 7 | "u-node-mq-termui/src/util" 8 | 9 | ui "github.com/gizak/termui/v3" 10 | ) 11 | 12 | //渲染交换机表格 13 | func renderExchangeTable() { 14 | list := data.E.FindList() 15 | arr := [][]string{ 16 | {"ID", "Name", "CreatedTime", "UpdateTime", "AcceptedCount", "SendCount", "QueueNames"}, 17 | } 18 | 19 | for _, v := range list { 20 | arr = append(arr, []string{ 21 | v.Id, 22 | v.Name, 23 | time.UnixMilli(v.CreatedTime).Format(util.FormatStamp), 24 | time.UnixMilli(v.UpdateTime).Format(util.FormatStamp), 25 | strconv.Itoa(v.AcceptedCount), 26 | strconv.Itoa(v.SendCount), 27 | paseString(v.QueueNames), 28 | }) 29 | } 30 | 31 | w, h := ui.TerminalDimensions() 32 | //交换机表格 33 | exchangeTable.Border = false 34 | exchangeTable.ColumnWidths = []int{20, 20, 20, 20, 20, 20, w - 120} 35 | exchangeTable.Rows = arr 36 | exchangeTable.TextStyle = ui.NewStyle(ui.ColorWhite) 37 | 38 | exchangeTable.SetRect(0, 2, w, h) 39 | ui.Render(exchangeTable) 40 | } 41 | -------------------------------------------------------------------------------- /script/publish.js: -------------------------------------------------------------------------------- 1 | // const _package = require("../package.json"); 2 | import chalk from "chalk"; 3 | import fs from "fs-extra"; 4 | import { execaSync } from "execa"; 5 | 6 | const _package = JSON.parse(fs.readFileSync("./package.json")); 7 | 8 | const now = new Date().getTime(); 9 | /** 10 | * 发布项目文件 11 | */ 12 | 13 | execaSync("pnpm", ["build"]); 14 | console.log(chalk.blue("build成功!")); 15 | 16 | const { stdout } = execaSync("npm", ["view", "u-node-mq", "versions"]); 17 | console.log(chalk.blue("预发布版本号:", _package.version)); 18 | if (-1 !== stdout.indexOf(_package.version)) { 19 | console.log(chalk.redBright("版本号已存在!")); 20 | process.exit(1); 21 | } 22 | 23 | //生成包 24 | const data = execaSync("npm", ["pack", "./u-node-mq"]); 25 | try { 26 | //发布正式包 27 | if (_package.version.search("beta") === -1) execaSync("npm", ["publish", data.stdout, "--tag", "latest"]); 28 | //发布测试包 29 | else execaSync("npm", ["publish", data.stdout, "--tag", "beta"]); 30 | } finally { 31 | fs.removeSync(data.stdout); 32 | } 33 | 34 | console.log(chalk.blue("publish成功!")); 35 | 36 | console.log(chalk.cyanBright("执行时长:" + (new Date().getTime() - now) / 1000 + "秒")); 37 | -------------------------------------------------------------------------------- /docs/plugins/index.md: -------------------------------------------------------------------------------- 1 | # 插件介绍 2 | 3 | `u-node-mq`提供一些内置插件,用来解决前端开发场景下异步通信问题,也可以由开发者自行开发插件进行集成; 4 | 5 | ## Plugin 6 | 7 | 8 | 在您准备集成插件之前,请确保对UNodeMQ的工作流程有深入的了解。我们假设您对UNodeMQ的工作流程已经非常熟悉。 9 | 10 | 如果您已经多次使用过UNodeMQ类及其相关功能,您可能已经意识到,实际上UNodeMQ、QuickUNodeMQ和SingleUNodeMQ在应用中是与业务逻辑无关的,它们是独立于业务流程运行的。这种设计带来许多优点,例如易于扩展、跨平台兼容性和降低代码耦合度。 11 | 12 | 插件的出现是因为在某些情况下,将功能与业务逻辑结合使用能够更好地扩展功能。因此,通常情况下,每个UNodeMQ只能集成一个插件。由于插件引入了业务逻辑,它改变了UNodeMQ每个组件的本质,使每个组件能够模拟业务中的实际元素。您可以通过点击下面的链接,进入每个插件的介绍页面,了解它们与业务的对应关系。 13 | 14 | 15 | 开发插件 16 | 17 | ```javascript 18 | import UNodeMQ, { PluginInstallFunction } from "u-node-mq"; 19 | 20 | const plugin: PluginInstallFunction = (unmq: UNodeMQ) => { 21 | //插件功能 22 | }; 23 | 24 | //or 25 | 26 | const plugin: { install: PluginInstallFunction } = { 27 | install: (unmq: UNodeMQ) => { 28 | //插件功能 29 | }, 30 | }; 31 | ``` 32 | 33 | 使用插件 34 | 35 | ```javascript 36 | import UNodeMQ,{plugin} from "u-node-mq"; 37 | 38 | const unmq = new UNodeMQ(ExchangeCollection, QueueCollection); 39 | unmq.use(plugin); 40 | ``` 41 | 42 | 43 | ## [【IframePlugin】一个跨iframe通信的插件](./IframePlugin.md) 44 | 45 | ## [【WxLogsPlugin】一个微信小程序日志插件](./WxLogsPlugin.md) 46 | -------------------------------------------------------------------------------- /src/operators/debounceTime/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator } from "../.."; 2 | import { IntTime } from "../../utils/types"; 3 | 4 | /** 5 | * debounceTime 防抖函数 6 | * @param dueTime 抖动间隔时间,单位毫秒 7 | * @param immediate 是否立即执行,默认为false 8 | * @returns 9 | * 立即执行代表第一个回调函数会立马执行,防抖函数一般不需要立即执行 10 | */ 11 | export default function debounceTime(dueTime: number, immediate?: boolean): Operator { 12 | let now = 0; 13 | let timeId: IntTime | null = null; 14 | let res: (value: boolean | PromiseLike) => void; 15 | return { 16 | beforeAddNews() { 17 | const t = new Date().getTime(); 18 | if (immediate) { 19 | //立即执行 20 | const n = now; 21 | now = t; 22 | return t > n + dueTime; 23 | } else { 24 | // 25 | if (timeId !== null && t <= now + dueTime) { 26 | res(false); 27 | clearTimeout(timeId); 28 | } 29 | now = t; 30 | return new Promise(resolve => { 31 | res = resolve; 32 | timeId = setTimeout(() => { 33 | timeId = null; 34 | resolve(true); 35 | }, dueTime); 36 | }); 37 | } 38 | }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /docs/operators/index.md: -------------------------------------------------------------------------------- 1 | # 🎨 operators 2 | 3 | Queue类提供的钩子函数可以集成operators对消息和消费者进行操作; 4 | 5 | - [map](./map.md) 6 | - [task](./task.md) 7 | - [debounceTime](./debounceTime.md) 8 | - [throttleTime](./throttleTime.md) 9 | - [newsTime](./newsTime.md) 10 | - [of](./of.md) 11 | - [interval](./interval.md) 12 | - [filter](./filter.md) 13 | - [removeDuplicates](./removeDuplicates.md) 14 | - [instant](./instant.md) 15 | 16 | **operators 异步钩子函数说明** 17 | 18 | 异步钩子函数不会影响队列执行 19 | 20 | | 名称 | 参数 | 返回 | 说明 | 21 | | --------------- | ---------- | ------- | ------------------------ | 22 | | mounted | Queue | unknown | operate 安装成功以后执行 | 23 | | addedNews | News | unknown | 消息加入队列以后执行 | 24 | | addedConsumer | Consumer | unknown | 消费者订阅队列以后执行 | 25 | | removedConsumer | Consumer[] | unknown | 消费者成功被移除以后执行 | 26 | 27 | **operators 同步钩子函数说明** 28 | 29 | 同步的钩子函数返回值会影响队列执行 30 | 31 | | 名称 | 参数 | 返回 | 说明 | 32 | | ------------- | ---- | --------------------------- | ------------------------------------------------ | 33 | | beforeAddNews | News | boolean \| Promise | 消息加入队列之前执行,通过返回值控制是否加入队列 | 34 | | ejectNews | News | boolean \| Promise | 消息弹出来以后执行,返回值用于控制消息是否被丢弃 | 35 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | **基础功能** 2 | 3 | - 完成 IframeMessage vue 页面 4 | 5 | - 实现 logs 输出 include 和 exclude 功能 6 | 7 | - 暂时只打包 window 平台,后面使用 gox 打包其他平台 termui 8 | 9 | - termui 使用键盘上下键选中数据功能 10 | 11 | - 实现 WebSocket plugin 12 | 13 | - 集成 Intersection Observer api 14 | 15 | - QuickUNodeMQ 和 SingleUNodeMQ 添加 plugins 16 | 17 | --- 18 | 19 | **测试功能** 20 | 21 | - 完成 IframeMessage 测试代码功能 22 | 23 | - 完成 core 文件代码的测试代码 24 | 25 | - 补充 SingleUNodeMQ、QuickUNodeMQ 的测试文件 26 | 27 | - 完成 ts 类型测试 28 | 29 | --- 30 | 31 | **文档补充** 32 | 33 | - 完成 operators 新文档介绍 34 | 35 | - 补充 SingleUNodeMQ、QuickUNodeMQ 的文档 36 | 37 | --- 38 | 39 | **版本管理** 40 | 41 | ### 3.6.8 42 | 43 | - ~~修复autoSize bug~~ 44 | 45 | ### 3.6.7 46 | 47 | - ~~iframe 添加 autoSize 参数~~ 48 | 49 | - ~~添加 SingleUNodeMQ 的 fork 功能~~ 50 | 51 | ### 3.6.5 52 | 53 | - ~~修复 resizeObserver operators bug~~ 54 | 55 | ### 3.6.4-beta.1 56 | 57 | - ~~添加.nvmrc 文件~~ 58 | 59 | - ~~修复 News id 固定的 bug~~ 60 | 61 | - ~~添加 resizeObserver operators~~ 62 | 63 | - ~~修改 tools.ts 文字错误~~ 64 | 65 | ### 3.6.3 66 | 67 | - ~~配置使用自动生成.d.ts 文件的工具~~ 68 | 69 | - ~~兼容不使用 new 关键字创建组件~~ 70 | 71 | - ~~配置 prettier~~ 72 | 73 | - ~~termui 名称数据展示使用分隔符~~ 74 | 75 | ### 3.6.2 76 | 77 | - ~~完成 logs 控制台服务的 go 的开发和文档完善~~ 78 | 79 | - ~~termui 数据展示排序~~ 80 | 81 | - ~~完成 logs mackdown 文档功能~~ 82 | -------------------------------------------------------------------------------- /termui/src/view/queueView.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | "u-node-mq-termui/src/data" 7 | "u-node-mq-termui/src/util" 8 | 9 | ui "github.com/gizak/termui/v3" 10 | ) 11 | 12 | func paseString(list []string) string { 13 | s := "" 14 | l := len(list) 15 | for i, v := range list { 16 | s += v 17 | if i != l-1 { 18 | s += ", " 19 | } 20 | } 21 | return s 22 | } 23 | 24 | //渲染队列表格 25 | func renderQueueTable() { 26 | //队列表格 27 | list := data.Q.FindList() 28 | 29 | arr := [][]string{ 30 | {"ID", "Name", "CreatedTime", "UpdateTime", "NewsNum", "NewsIds", "ConsumerNum", "ConsumerIds"}, 31 | } 32 | 33 | for _, v := range list { 34 | arr = append(arr, []string{ 35 | v.Id, 36 | v.Name, 37 | time.UnixMilli(v.CreatedTime).Format(util.FormatStamp), 38 | time.UnixMilli(v.UpdateTime).Format(util.FormatStamp), 39 | strconv.Itoa(v.NewsNum), 40 | paseString(v.NewsIds), 41 | strconv.Itoa(v.ConsumerNum), 42 | paseString(v.ConsumerIds), 43 | }) 44 | } 45 | 46 | w, h := ui.TerminalDimensions() 47 | queueTable.Border = false 48 | queueTable.ColumnWidths = []int{20, 20, 20, 20, 20, 20, 20, w - 140} 49 | queueTable.Rows = arr 50 | 51 | queueTable.TextStyle = ui.NewStyle(ui.ColorWhite) 52 | 53 | queueTable.SetRect(0, 2, w, h) 54 | ui.Render(queueTable) 55 | } 56 | -------------------------------------------------------------------------------- /termui/go.mod: -------------------------------------------------------------------------------- 1 | module u-node-mq-termui 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.8.1 7 | github.com/gizak/termui/v3 v3.1.0 8 | ) 9 | 10 | require ( 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/go-playground/locales v0.14.0 // indirect 13 | github.com/go-playground/universal-translator v0.18.0 // indirect 14 | github.com/go-playground/validator/v10 v10.10.0 // indirect 15 | github.com/goccy/go-json v0.9.7 // indirect 16 | github.com/json-iterator/go v1.1.12 // indirect 17 | github.com/leodido/go-urn v1.2.1 // indirect 18 | github.com/mattn/go-isatty v0.0.14 // indirect 19 | github.com/mattn/go-runewidth v0.0.2 // indirect 20 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect 21 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 22 | github.com/modern-go/reflect2 v1.0.2 // indirect 23 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect 24 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 25 | github.com/ugorji/go/codec v1.2.7 // indirect 26 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 27 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect 28 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 29 | golang.org/x/text v0.3.6 // indirect 30 | google.golang.org/protobuf v1.28.0 // indirect 31 | gopkg.in/yaml.v2 v2.4.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "u-node-mq", 3 | "version": "4.0.0", 4 | "author": "hugaojie ", 5 | "description": "基于发布订阅模型的消息通信插件", 6 | "keywords": [ 7 | "发布订阅", 8 | "通信", 9 | "队列", 10 | "交换机", 11 | "消费者", 12 | "观察者" 13 | ], 14 | "bin": { 15 | "u-node-mq": "bin/u-node-mq-termui.exe" 16 | }, 17 | "type": "module", 18 | "main": "index.js", 19 | "types": "index.d.ts", 20 | "repository": "https://github.com/Juaoie/u-node-mq.git", 21 | "homepage": "https://github.com/Juaoie/u-node-mq/", 22 | "license": "MIT", 23 | "scripts": { 24 | "build": "node ./script/build.js", 25 | "gobuild": "cd termui && go build", 26 | "test": "jest", 27 | "eslint": "eslint", 28 | "clr": "node ./script/clear.js", 29 | "pub": "node ./script/publish.js", 30 | "gdts": "node ./script/dts-bundle-generator.js" 31 | }, 32 | "devDependencies": { 33 | "@jest/globals": "27.5.1", 34 | "@jest/types": "27.5.1", 35 | "@types/fs-extra": "^9.0.13", 36 | "@typescript-eslint/eslint-plugin": "^5.27.1", 37 | "@typescript-eslint/parser": "^5.27.1", 38 | "chalk": "^5.0.1", 39 | "dts-bundle-generator": "^6.12.0", 40 | "esbuild": "^0.14.34", 41 | "eslint": "^8.17.0", 42 | "eslint-config-prettier": "^8.5.0", 43 | "eslint-plugin-prettier": "^4.2.1", 44 | "execa": "^6.1.0", 45 | "fs-extra": "^10.1.0", 46 | "jest": "^27.5.1", 47 | "miniprogram-api-typings": "^3.12.2", 48 | "prettier": "^2.6.2", 49 | "ts-jest": "27.1.5", 50 | "typescript": "^4.5.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/plugins/IframePlugin.md: -------------------------------------------------------------------------------- 1 | # IframeMessage 2 | 3 | - `IframeMessage` 是用来解决同一个 `tabs` 下 `iframe` 通信的 `u-node-mq` 插件; 4 | - `u-node-mq` 集成 `IframeMessage` 以后,`unmq` 的每个 `Exchange` 将对应一个 `iframe` 容器,且非当前容器的 `Exchange` 路由和中继器将会被重写; 5 | - 一个 `iframe` 应用一般情况下应该只注册一个 IframeMessage 插件; 6 | - 被集成了 `IframeMessage` 插件的 `unmq`,开发者只需要维护自己 `Exchange` 下的队列; 7 | - 可以在其他 `Exchange` 应用上添加 `origin` 用来验证 `iframe` 的 `url`; 8 | - `new IframeMessage` 可以传递参数 `autoSize` 来控制当前`iframe`容器是否和父元素是否一致,默认`iframe`容器大小是不受父元素影响的 9 | 10 | ## IframeMessage 基本使用方法 11 | 12 | **iframe1 应用** 13 | 14 | ```javascript 15 | // https://iframeName1.com 16 | import UNodeMQ,{IframeMessage} from "u-node-mq"; 17 | const unmq = new UNodeMQ( 18 | { 19 | iframeName1: new Exchange({ routes: ["qu1"] }), 20 | //约束iframeName2的origin必须为https://iframeName2.com 21 | iframeName2: new Exchange({ origin: "https://iframeName2.com" }), 22 | }, 23 | { 24 | qu1: new Queue(), 25 | }, 26 | ); 27 | unmq.use(new IframeMessage("iframeName1",{autoSize:true})); 28 | unmq.emit("iframeName2", "发送给iframeName2的消息"); 29 | ``` 30 | 31 | **iframe2 应用** 32 | 33 | ```javascript 34 | // https://iframeName2.com 35 | import UNodeMQ,{IframeMessage} from "u-node-mq"; 36 | const unmq = new UNodeMQ( 37 | { 38 | iframeName1: new Exchange(), 39 | iframeName2: new Exchange({ routes: ["qu2"] }), 40 | }, 41 | { 42 | qu2: new Queue(), 43 | }, 44 | ); 45 | unmq.use(new IframeMessage("iframeName2")); 46 | unmq.on("qu2", res => { 47 | console.log("接受来自其他iframe容器的消息", res); 48 | }); 49 | ``` 50 | -------------------------------------------------------------------------------- /termui/src/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | "u-node-mq-termui/src/data" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func SetQueueData() { 13 | // 记录到文件。 14 | f, _ := os.Create("unmq-termui.log") 15 | gin.DefaultWriter = io.MultiWriter(f) 16 | 17 | r := gin.Default() 18 | r.POST("/queue", queue) 19 | r.POST("/exchange", exchange) 20 | r.POST("/news", news) 21 | r.POST("/consumer", consumer) 22 | r.Run(":9090") 23 | 24 | } 25 | 26 | func queue(c *gin.Context) { 27 | q := data.QueueLogData{} 28 | c.BindJSON(&q) 29 | if q.Id == "" { 30 | c.JSON(http.StatusBadRequest, gin.H{"message": "fail"}) 31 | } else { 32 | data.Q.Set(q) 33 | c.JSON(http.StatusOK, gin.H{"message": "ok!", "code": 200}) 34 | } 35 | } 36 | 37 | func exchange(c *gin.Context) { 38 | e := data.ExchangeLogData{} 39 | c.BindJSON(&e) 40 | if e.Id == "" { 41 | c.JSON(http.StatusBadRequest, gin.H{"message": "fail"}) 42 | } else { 43 | data.E.Set(e) 44 | c.JSON(http.StatusOK, gin.H{"message": "ok!", "code": 200}) 45 | } 46 | } 47 | 48 | func news(c *gin.Context) { 49 | n := data.NewsLogData{} 50 | c.BindJSON(&n) 51 | if n.Id == "" { 52 | c.JSON(http.StatusBadRequest, gin.H{"message": "fail"}) 53 | } else { 54 | data.N.Set(n) 55 | c.JSON(http.StatusOK, gin.H{"message": "ok!", "code": 200}) 56 | } 57 | } 58 | 59 | func consumer(c *gin.Context) { 60 | cc := data.ConsumerLogData{} 61 | c.BindJSON(&cc) 62 | if cc.Id == "" { 63 | c.JSON(http.StatusBadRequest, gin.H{"message": "fail"}) 64 | } else { 65 | data.C.Set(cc) 66 | c.JSON(http.StatusOK, gin.H{"message": "ok!", "code": 200}) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/operators/interval/index.ts: -------------------------------------------------------------------------------- 1 | import { Operator, Queue } from "../.."; 2 | import { IntTime } from "../../utils/types"; 3 | 4 | /** 5 | * setinterval发射数据,发射内容为从0开始的数字 6 | * @param period 1000 间隔时长 7 | * @param optimal true 是否在没有消费者的时候暂停发射数据,有消费者则会自动开启发射 8 | * @returns 9 | */ 10 | 11 | export default function interval(period = 1000, optimal = true): Operator { 12 | if (period < 0) period = 0; 13 | let num = 0; 14 | let id: IntTime | null = null; 15 | let interval = { 16 | go: () => { 17 | // 18 | }, 19 | stop: () => { 20 | // 21 | }, 22 | }; 23 | let queue: Queue; 24 | 25 | return { 26 | mounted(that: Queue) { 27 | // (function () { })(); 28 | queue = that; 29 | interval = { 30 | go() { 31 | id = setInterval(() => { 32 | num++; 33 | queue.pushContent(num); 34 | }, period); 35 | }, 36 | stop() { 37 | if (id === null) return; 38 | clearInterval(id); 39 | id = null; 40 | }, 41 | }; 42 | 43 | if (queue.getConsumerList.length > 0) { 44 | //有默认消费者 45 | interval.go(); 46 | } else if (!optimal) { 47 | interval.go(); 48 | } 49 | }, 50 | addedConsumer() { 51 | //如果不启用该属性则直接退出 52 | if (!optimal) return; 53 | //判断是否以及在循环执行了 54 | if (id !== null) return; 55 | 56 | interval.go(); 57 | }, 58 | removedConsumer() { 59 | if (!optimal) return; 60 | //判断是否以及在循环执行了 61 | if (id === null) return; 62 | 63 | if (queue.getConsumerList.length === 0) interval.stop(); 64 | return ""; 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /termui/src/data/newsTable.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | "time" 7 | "u-node-mq-termui/src/util" 8 | ) 9 | 10 | //前端传递的json 11 | type NewsLogData struct { 12 | BaseLogData 13 | } 14 | 15 | //表json 16 | type NewsTableField struct { 17 | TableField 18 | } 19 | 20 | type NewsTable struct { 21 | lock sync.Mutex 22 | list []NewsTableField 23 | State bool 24 | } 25 | 26 | var ( 27 | N = NewsTable{} 28 | ) 29 | 30 | //添加数据 31 | //一个id的组件只会创建一次 32 | func (nt *NewsTable) Add(NewsLogData NewsLogData) { 33 | n := NewsTableField{} 34 | n.Id = NewsLogData.Id 35 | n.CreatedTime = NewsLogData.CreatedTime 36 | n.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 37 | nt.list = append(nt.list, n) 38 | nt.State = true 39 | 40 | } 41 | 42 | //删除list 43 | func (nt *NewsTable) DeleAll() { 44 | nt.list = []NewsTableField{} 45 | nt.State = true 46 | 47 | } 48 | 49 | //设置值,如果id不存在,就添加一条 50 | func (nt *NewsTable) Set(NewsLogData NewsLogData) { 51 | n := nt.Find(NewsLogData.Id) 52 | //是否需要加锁呢? 53 | nt.lock.Lock() 54 | if n.Id == "" { 55 | nt.Add(NewsLogData) 56 | } else { 57 | 58 | n.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 59 | 60 | } 61 | nt.State = true 62 | 63 | nt.lock.Unlock() 64 | } 65 | 66 | //通过id查找一条数据,返回一条数据的指针 67 | func (nt *NewsTable) Find(id string) *NewsTableField { 68 | for i := 0; i < len(nt.list); i++ { 69 | if id == nt.list[i].Id { 70 | return &nt.list[i] 71 | } 72 | } 73 | return &NewsTableField{} 74 | } 75 | 76 | func (nt *NewsTable) FindList() []NewsTableField { 77 | sort.Slice(nt.list, func(i, j int) bool { 78 | return nt.list[i].UpdateTime > nt.list[j].UpdateTime 79 | }) 80 | 81 | return nt.list 82 | } 83 | -------------------------------------------------------------------------------- /src/internal/Queue/operators.ts: -------------------------------------------------------------------------------- 1 | import { News, Queue, Consumer } from "../.."; 2 | /** 3 | * 会异步执行的运算方法 4 | * 不需要通过返回值控制是否继续执行流程 5 | */ 6 | interface AsyncOperator { 7 | /** 8 | * 操作挂载执行方法 9 | * 多个操作符的同个钩子函数会同时执行,所以应该谨慎操作数据,避免产生异步操作数据的问题 10 | */ 11 | mounted?: (that: Queue) => unknown; 12 | /** 13 | * 消息成功添加到队列以后 14 | */ 15 | addedNews?: (news: News) => unknown; 16 | 17 | /** 18 | * 消费者成功加入到队列以后 19 | */ 20 | addedConsumer?: (consumer: Consumer) => unknown; 21 | /** 22 | * 消费者成功被移除以后 23 | * consumerList 被删除的消费列表 24 | */ 25 | removedConsumer?: (consumerList: Consumer[]) => unknown; 26 | } 27 | 28 | /** 29 | * 会同步的执行的运算符方法 30 | * 每个运算符方法列表都会同步执行,且一个返回false,后面则不会继续执行,用于控制流程是否继续 31 | */ 32 | interface SyncOperator { 33 | /** 34 | * 将消息添加到队列之前 35 | * 返回的boolean控制消息是否加入队列 36 | */ 37 | beforeAddNews?: (news: News) => boolean | Promise; 38 | /** 39 | * 加入消费者之前 40 | * 返回的boolean控制消费者是否能加入队列 41 | */ 42 | // beforeAddConsumer?: (consumer: Consumer) => boolean | Promise; 43 | 44 | /** 45 | * 控制消息是否可以被弹出,为false则移除消息 46 | */ 47 | ejectNews?: (news: News) => boolean | Promise; 48 | } 49 | 50 | /** 51 | * 异步运算符 52 | * @param arg 53 | * @returns 54 | */ 55 | export function isAsyncOperator(arg: keyof Operator): arg is keyof AsyncOperator { 56 | return ["mounted", "addedNews", "addedConsumer", "removedConsumer"].indexOf(arg) !== -1; 57 | } 58 | /** 59 | * 同步运算符 60 | * @param arg 61 | * @returns 62 | */ 63 | export function isSyncOperator(arg: keyof Operator): arg is keyof SyncOperator { 64 | return ["beforeAddNews", "ejectNews"].indexOf(arg) !== -1; 65 | } 66 | /** 67 | * 队列和消息在队列中的生命周期 68 | */ 69 | export type Operator = AsyncOperator & SyncOperator; 70 | -------------------------------------------------------------------------------- /src/core/ExchangeCollectionHandle.ts: -------------------------------------------------------------------------------- 1 | import { Exchange } from "../index"; 2 | 3 | export default class ExchangeCollectionHandle { 4 | /** 5 | * 交换机集合 6 | */ 7 | private exchangeCollection = new Map>(); 8 | /** 9 | * 通过名称判断交换机是否存在 10 | * @param exchangeName 11 | * @returns 返回是否存在 12 | */ 13 | has(exchangeName: string) { 14 | if (this.exchangeCollection.has(exchangeName)) return true; 15 | else { 16 | return false; 17 | } 18 | } 19 | /** 20 | * 设置交换机集合 21 | * @param exchangeCollection 22 | */ 23 | setExchangeCollection(exchangeCollection: Record>) { 24 | this.exchangeCollection = new Map(Object.entries(exchangeCollection)); 25 | } 26 | /** 27 | * 添加单条交换机 28 | * @param exchangeName 29 | * @param exchange 30 | * @returns 返回新的交换机集合 31 | */ 32 | addExchage(exchangeName: string, exchange: Exchange) { 33 | exchange.name = exchangeName; 34 | return this.exchangeCollection.set(exchangeName, exchange); 35 | } 36 | /** 37 | * 获取单个交换机 38 | * @param exchangeName 39 | * @returns 返回交换机名称对应的交换机 40 | */ 41 | getExchange(exchangeName: string): Exchange | null { 42 | const exchange = this.exchangeCollection.get(exchangeName); 43 | if (exchange === undefined) { 44 | return null; 45 | } 46 | return exchange; 47 | } 48 | /** 49 | * 获取所有交换机 50 | * @returns 51 | */ 52 | getExchangeList() { 53 | return [...this.exchangeCollection.values()]; 54 | } 55 | /** 56 | * 根据交换机名称获取队列名称列表 57 | * @param exchangeName 58 | * @param content 59 | * @returns 60 | */ 61 | async getQueueNameList(exchangeName: string, content: D) { 62 | const exchagne = this.getExchange(exchangeName); 63 | if (exchagne === null) return []; 64 | return exchagne.getQueueNameList(content); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/operators/throttleTime.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import throttleTime from "../../src/operators/throttleTime"; 3 | 4 | import { expect, test } from "@jest/globals"; 5 | 6 | test("快速unmq,throttleTime测试,立即执行", function (done) { 7 | const quickUnmq = createQuickUnmq(new Exchange({ routes: ["qu1"] }), { 8 | qu1: new Queue().add(throttleTime(1000, true)), 9 | }); 10 | let str = ""; 11 | quickUnmq.on("qu1", (res: number) => { 12 | str += res; 13 | }); 14 | quickUnmq.emit(2, 3, 4); 15 | setTimeout(() => { 16 | quickUnmq.emit(5); 17 | }, 1200); 18 | setTimeout(() => { 19 | expect(str).toEqual("25"); 20 | done(); 21 | }, 1500); 22 | }); 23 | 24 | test("queue,throttleTime测试,立即执行", function (done) { 25 | const queue = new Queue().add(throttleTime(400, true)); 26 | let str = ""; 27 | queue.pushConsume((res: number) => { 28 | str += res; 29 | }); 30 | queue.pushContent(2); 31 | setTimeout(() => { 32 | queue.pushContent(3); 33 | }, 200); 34 | setTimeout(() => { 35 | queue.pushContent(4); 36 | }, 500); 37 | setTimeout(() => { 38 | queue.pushContent(5); 39 | }, 1000); 40 | setTimeout(() => { 41 | expect(str).toEqual("245"); 42 | done(); 43 | }, 1500); 44 | }); 45 | 46 | test("queue,throttleTime测试,最后执行", function (done) { 47 | const queue = new Queue().add(throttleTime(400)); 48 | let str = ""; 49 | queue.pushConsume((res: number) => { 50 | str += res; 51 | }); 52 | queue.pushContent(2); 53 | setTimeout(() => { 54 | queue.pushContent(3); 55 | }, 200); 56 | setTimeout(() => { 57 | queue.pushContent(4); 58 | }, 500); 59 | setTimeout(() => { 60 | queue.pushContent(5); 61 | }, 1000); 62 | setTimeout(() => { 63 | expect(str).toEqual("245"); 64 | done(); 65 | }, 1500); 66 | }); 67 | -------------------------------------------------------------------------------- /termui/src/data/consumerTable.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | "time" 7 | "u-node-mq-termui/src/util" 8 | ) 9 | 10 | //前端传递的json 11 | type ConsumerLogData struct { 12 | BaseLogData 13 | Accepted int `json:"accepted"` //接收消息的数量, 14 | Message string `json:"message"` //文本说明日志,error 也会使用 message 字段输出 15 | } 16 | 17 | //表json 18 | type ConsumerTableField struct { 19 | TableField 20 | AcceptedCount int 21 | } 22 | 23 | type ConsumerTable struct { 24 | lock sync.Mutex 25 | list []ConsumerTableField 26 | State bool //控制是否需要重绘视图 27 | } 28 | 29 | var ( 30 | C = ConsumerTable{} 31 | ) 32 | 33 | //添加数据 34 | //一个id的组件只会创建一次 35 | func (ct *ConsumerTable) Add(ConsumerLogData ConsumerLogData) { 36 | c := ConsumerTableField{} 37 | c.Id = ConsumerLogData.Id 38 | c.CreatedTime = ConsumerLogData.CreatedTime 39 | c.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 40 | c.AcceptedCount += ConsumerLogData.Accepted 41 | ct.list = append(ct.list, c) 42 | ct.State = true 43 | 44 | } 45 | 46 | //删除list 47 | func (ct *ConsumerTable) DeleAll() { 48 | ct.list = []ConsumerTableField{} 49 | ct.State = true 50 | } 51 | 52 | //设置值,如果id不存在,就添加一条 53 | func (ct *ConsumerTable) Set(ConsumerLogData ConsumerLogData) { 54 | c := ct.Find(ConsumerLogData.Id) 55 | //是否需要加锁呢? 56 | ct.lock.Lock() 57 | if c.Id == "" { 58 | ct.Add(ConsumerLogData) 59 | } else { 60 | c.AcceptedCount += ConsumerLogData.Accepted 61 | 62 | c.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 63 | 64 | } 65 | ct.State = true 66 | 67 | ct.lock.Unlock() 68 | } 69 | 70 | //通过id查找一条数据,返回一条数据的指针 71 | func (ct *ConsumerTable) Find(id string) *ConsumerTableField { 72 | for i := 0; i < len(ct.list); i++ { 73 | if id == ct.list[i].Id { 74 | return &ct.list[i] 75 | } 76 | } 77 | return &ConsumerTableField{} 78 | } 79 | 80 | func (ct *ConsumerTable) FindList() []ConsumerTableField { 81 | sort.Slice(ct.list, func(i, j int) bool { 82 | return ct.list[i].UpdateTime > ct.list[j].UpdateTime 83 | }) 84 | 85 | return ct.list 86 | } 87 | -------------------------------------------------------------------------------- /docs/internal/logs_sys_dev.md: -------------------------------------------------------------------------------- 1 | # 自定义日志系统开发 2 | 3 | **CustomLogFunction 参数说明** 4 | 5 | `CustomLogFunction` 是用户自定义处理日志的方法,如需将日志输出到外部服务器,需先了解埋点在组件中的输出日志的方法提供的数据类型,开发者可根据日志数据类型自定义进行服务端日志系统构建; 6 | 7 | | 参数 | 类型 | 说明 | 8 | | ---- | ------- | ------------ | 9 | | name | string | 组件名称 | 10 | | data | LogData | 单条日志数据 | 11 | 12 | ## `LogData`类型说明 13 | 14 | `LogData`为四个组件关键点埋点日志的类型,分别为`ExchangeLogData`、`QueueLogData`、`NewsLogData`、`ConsumerLogData`; 15 | 16 | **BaseLogData 公共类型说明** 17 | 18 | | 名称 | 类型 | 说明 | 19 | | ----------- | ------ | --------------------------------------------------------- | 20 | | id | string | 组件的唯一 id,每条日志都会输出 | 21 | | createdTime | number | 组件的创建时间戳,每条日志都会输出 | 22 | | name | number | `Exchange`和`Queue`组件可能存在 name,如有 name,则会输出 | 23 | | message | string | 文本说明日志,error 也会使用 message 字段输出 | 24 | 25 | **ExchangeLogData 类型说明** 26 | 27 | | 名称 | 类型 | 说明 | 28 | | ---------- | -------- | ----------------------------------------------------------------------------------- | 29 | | accepted | number | 接收消息的数量,仅当有消息传入进来时才会输出 1 ,开发者可根据此字段统计接收消息总量 | 30 | | send | number | 消息路由到队列的队列数量 ,开发者可根据此字段统计路由总次数 | 31 | | queueNames | string[] | 消息路由到队列的队列名称数组,开发者可对此字段进行统计分组 | 32 | 33 | **QueueLogData 类型说明** 34 | 35 | | 名称 | 类型 | 说明 | 36 | | ----------- | -------- | -------------------- | 37 | | newsNum | number | 当前队列消息总数 | 38 | | newsIds | string[] | 当前队列消息 id 数组 | 39 | | consumerNum | number | 当前消费者数量 | 40 | | consumerIds | string[] | 当前消费者 id 数组 | 41 | 42 | **ConsumerLogData 类型说明** 43 | 44 | | 名称 | 类型 | 说明 | 45 | | -------- | ------ | ------------------------------------------------------------------------------- | 46 | | accepted | number | 消费消息的数量,仅当消费时才会输出 1 ,开发者可根据此字段统计单个消费者消费总数 | 47 | -------------------------------------------------------------------------------- /docs/internal/termui.md: -------------------------------------------------------------------------------- 1 | # termui 开发工具使用 2 | 3 | ![termui](./termui.png) 4 | 5 | `termui`是使用`go`开发的日志查看工具,皆在方便开发者查看组件之间数据交互情况;目前仅支持window使用; 6 | 7 | ## 安装使用 8 | 9 | 在安装`u-node-mq`以后,可在`package.json`中配置以下启动命令 10 | 11 | ```json 12 | "scripts": { 13 | "unmq": "u-node-mq", 14 | } 15 | ``` 16 | 17 | 然后在代码中开启自定义日志输出,并实现 CustomLogFunction 方法,`termui`默认监听端口号为`9090`,下面为示例代码; 18 | 19 | ```javascript 20 | import { Logs } from "u-node-mq"; 21 | Logs.setLogsConfig({ 22 | logs: true, //建议只在开发环境开启 23 | types: ["custom", "console"], 24 | customFunction: (name, data) => { 25 | //发送数据到termui 的 9090端口 26 | http.post("http://localhost:9090/" + name, { 27 | data, 28 | header: { 29 | "content-type": "application/json;charset=utf-8", 30 | }, 31 | }); 32 | }, 33 | }); 34 | ``` 35 | 36 | **Queue 日志说明** 37 | 38 | | 名称 | 说明 | 39 | | ----------- | ------------------------------ | 40 | | ID | 队列 id | 41 | | name | 队列名称,创建队列名称可能为空 | 42 | | createdTime | 创建时间 | 43 | | updateTime | 最近更新时间 | 44 | | NewsNum | 队列内消息总数 | 45 | | NewsIds | 队列内消息 Id 数组 | 46 | | ConsumerNum | 监听队列的消费者总数 | 47 | | ConsumerIds | 监听队列的消费者 Id 数组 | 48 | 49 | **Exchange 日志说明** 50 | 51 | | 名称 | 说明 | 52 | | ------------- | ---------------------------------- | 53 | | ID | 交换机 id | 54 | | name | 交换机名称,创建交换机名称可能为空 | 55 | | createdTime | 创建时间 | 56 | | updateTime | 最近更新时间 | 57 | | acceptedCount | 接收到消息的总数 | 58 | | sendCount | 发送消息到队列的总次数 | 59 | | queueNames | 发送消息到队列的队列名称数组 | 60 | 61 | **News 日志说明** 62 | 63 | | 名称 | 说明 | 64 | | ----------- | ------------ | 65 | | ID | 交换机 id | 66 | | createdTime | 创建时间 | 67 | | updateTime | 最近更新时间 | 68 | 69 | **Consumer 日志说明** 70 | 71 | | 名称 | 说明 | 72 | | ------------- | ---------------- | 73 | | ID | 交换机 id | 74 | | createdTime | 创建时间 | 75 | | updateTime | 最近更新时间 | 76 | | AcceptedCount | 消费消息的总数量 | 77 | 78 | - q 键退出 79 | - 键盘左右键切换 tabs 80 | - c 建清空当前页数据 81 | - ctrl+c 清空所有数据 82 | -------------------------------------------------------------------------------- /src/core/QueueCollectionHandle.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from "../utils/tools"; 2 | import { Queue, News } from "../index"; 3 | import { Consume } from "../internal/Consumer"; 4 | 5 | /** 6 | * 队列集合 7 | */ 8 | export default class QueueCollectionHandle { 9 | /** 10 | * 队列集合 11 | */ 12 | private queueCollection = new Map>(); 13 | /** 14 | * 根据队列名称判断队列是否存在 15 | * @param queueName 16 | * @returns 17 | */ 18 | has(queueName: string) { 19 | if (this.queueCollection.has(queueName)) return true; 20 | else { 21 | return false; 22 | } 23 | } 24 | /** 25 | * 设置队列集合 26 | * @param queueCollection 27 | */ 28 | setQueueCollection(queueCollection: Record>) { 29 | this.queueCollection = new Map(Object.entries(queueCollection)); 30 | } 31 | /** 32 | * 根据队列名称获取单个队列对象 33 | * @param queueName 34 | * @returns 35 | */ 36 | getQueue(queueName: string): Queue | null { 37 | const queue = this.queueCollection.get(queueName); 38 | if (queue === undefined) { 39 | return null; 40 | } 41 | return queue; 42 | } 43 | /** 44 | * 获取所有队列 45 | * @returns 46 | */ 47 | getQueueList() { 48 | return [...this.queueCollection.values()]; 49 | } 50 | /** 51 | * 添加单条队列 52 | * @param queue 53 | */ 54 | addQueue(queueName: string, queue: Queue) { 55 | queue.name = queueName; 56 | return this.queueCollection.set(queueName, queue); 57 | } 58 | /** 59 | * 向指定队列添加数据 60 | * @param queueName 61 | * @param news 62 | */ 63 | pushNewsToQueue(queueName: string, news: News) { 64 | this.getQueue(queueName)?.pushNews(news); 65 | } 66 | /** 67 | * 添加一个消息内容到队列 68 | * @param queueName 69 | * @param content 70 | * @returns 71 | */ 72 | pushContentToQueue(queueName: string, content: D) { 73 | this.getQueue(queueName)?.pushContent(content); 74 | } 75 | /** 76 | * 订阅队列 77 | * @param queueName 78 | * @param consume 79 | * @param payload 80 | * @returns 81 | */ 82 | subscribeQueue(queueName: string, consume: Consume, payload?: any) { 83 | this.getQueue(queueName)?.pushConsume(consume, payload); 84 | } 85 | /** 86 | * 取消订阅队列 87 | * @param queueName 88 | * @param consume 89 | */ 90 | unsubscribeQueue(queueName: string, consume?: Consume): boolean { 91 | if (!this.has(queueName)) return false; 92 | if (isFunction(consume)) { 93 | return !!this.getQueue(queueName)?.removeConsumer(consume); 94 | } else { 95 | return !!this.getQueue(queueName)?.removeAllConsumer(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/internal/Consumer.ts: -------------------------------------------------------------------------------- 1 | import { ComponentEnum } from "../utils/types"; 2 | import { isPromise, random } from "../utils/tools"; 3 | import Logs from "./Logs"; 4 | import News from "./News"; 5 | export type Next = (value?: boolean) => void; 6 | 7 | export interface Consume { 8 | (content: D, next?: Next, payload?: any): Promise | any; 9 | (content: D, payload?: any): any; 10 | } 11 | type ThenParameter = (isOk: boolean) => void; 12 | interface Payload { 13 | then: (res: ThenParameter) => void; 14 | } 15 | export default class Consumer { 16 | [k: string]: any; 17 | /** 18 | * id 19 | */ 20 | private readonly id: string = random(); 21 | getId() { 22 | return this.id; 23 | } 24 | /** 25 | * 消费者创建时间戳 26 | */ 27 | createdTime: number; 28 | /** 29 | * 消费方法 30 | */ 31 | consume: Consume; 32 | /** 33 | * 固定参数 34 | */ 35 | payload?: any; 36 | constructor(consume: Consume, payload?: any) { 37 | this.createdTime = new Date().getTime(); 38 | this.consume = consume; 39 | this.payload = payload; 40 | Logs.getLogsInstance()?.setLogs(ComponentEnum.CONSUMER, { id: this.getId(), createdTime: this.createdTime }); 41 | } 42 | /** 43 | * 消费消息 44 | * @param news 45 | * @param ask 46 | * @returns 47 | */ 48 | consumption(news: News, ask: boolean): Payload { 49 | Logs.getLogsInstance()?.setLogs(ComponentEnum.CONSUMER, { id: this.getId(), createdTime: this.createdTime, accepted: 1 }); 50 | const then = (thenParameter: ThenParameter) => { 51 | //不加入任务队列,会导致消费失败的数据重写到队列失败 52 | try { 53 | if (!ask) { 54 | //不需要确认的消费方法 55 | this.consume(news.content, this.payload); 56 | return thenParameter(true); 57 | } 58 | //构建消息确认的方法 59 | const confirm: Next = (value = true) => thenParameter(value); 60 | //需要确认的消费方法 61 | const res = this.consume(news.content, confirm, this.payload); 62 | //如果消息需要确认,且返回的内容为Promise 63 | if (isPromise(res)) { 64 | res 65 | .then(onfulfilled => { 66 | thenParameter(Boolean(onfulfilled)); 67 | }) 68 | .catch(() => { 69 | thenParameter(false); 70 | }); 71 | } else if (typeof res === "boolean") { 72 | thenParameter(res); 73 | } 74 | } catch (error) { 75 | Logs.getLogsInstance()?.setLogs(ComponentEnum.CONSUMER, { id: this.getId(), createdTime: this.createdTime, message: JSON.stringify(error) }); 76 | thenParameter(!ask); 77 | } 78 | }; 79 | return { 80 | then, 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /termui/src/data/queueTable.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | "time" 7 | "u-node-mq-termui/src/util" 8 | ) 9 | 10 | //前端传递的json 11 | type QueueLogData struct { 12 | BaseLogData 13 | QueueBaseField 14 | Message string `json:"message"` //文本说明日志,error 也会使用 message 字段输出 15 | } 16 | 17 | //表json 18 | type QueueTableField struct { 19 | TableField 20 | QueueBaseField 21 | } 22 | 23 | //队列公共字段 24 | type QueueBaseField struct { 25 | NewsNum int `json:"newsNum"` //当前队列消息总数 26 | NewsIds []string `json:"newsIds"` //当前队列消息 id 数组 27 | ConsumerNum int `json:"consumerNum"` //当前消费者数量 28 | ConsumerIds []string `json:"consumerIds"` //当前消费者 id 数组 29 | } 30 | 31 | type QueueTable struct { 32 | lock sync.Mutex 33 | list []QueueTableField 34 | State bool 35 | } 36 | 37 | var ( 38 | Q = QueueTable{} 39 | ) 40 | 41 | //添加数据 42 | //一个id的组件只会创建一次 43 | func (qt *QueueTable) Add(queueLogData QueueLogData) { 44 | q := QueueTableField{} 45 | q.Id = queueLogData.Id 46 | q.CreatedTime = queueLogData.CreatedTime 47 | q.Name = queueLogData.Name 48 | q.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 49 | q.NewsNum = queueLogData.NewsNum 50 | q.NewsIds = queueLogData.NewsIds 51 | q.ConsumerNum = queueLogData.ConsumerNum 52 | q.ConsumerIds = queueLogData.ConsumerIds 53 | qt.list = append(qt.list, q) 54 | qt.State = true 55 | 56 | } 57 | 58 | //删除list 59 | func (qt *QueueTable) DeleAll() { 60 | qt.list = []QueueTableField{} 61 | qt.State = true 62 | 63 | } 64 | 65 | //设置值,如果id不存在,就添加一条 66 | func (qt *QueueTable) Set(queueLogData QueueLogData) { 67 | q := qt.Find(queueLogData.Id) 68 | //是否需要加锁呢? 69 | qt.lock.Lock() 70 | if q.Id == "" { 71 | qt.Add(queueLogData) 72 | } else { 73 | q.Name = queueLogData.Name 74 | q.NewsNum = queueLogData.NewsNum 75 | q.NewsIds = queueLogData.NewsIds 76 | q.ConsumerNum = queueLogData.ConsumerNum 77 | q.ConsumerIds = queueLogData.ConsumerIds 78 | 79 | q.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 80 | 81 | } 82 | qt.State = true 83 | 84 | qt.lock.Unlock() 85 | } 86 | 87 | //通过id查找一条数据,返回一条数据的指针 88 | func (qt *QueueTable) Find(id string) *QueueTableField { 89 | for i := 0; i < len(qt.list); i++ { 90 | if id == qt.list[i].Id { 91 | return &qt.list[i] 92 | } 93 | } 94 | return &QueueTableField{} 95 | } 96 | 97 | func (qt *QueueTable) FindList() []QueueTableField { 98 | sort.Slice(qt.list, func(i, j int) bool { 99 | return qt.list[i].UpdateTime > qt.list[j].UpdateTime 100 | }) 101 | 102 | return qt.list 103 | } 104 | -------------------------------------------------------------------------------- /termui/src/view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "log" 5 | "time" 6 | "u-node-mq-termui/src/data" 7 | 8 | ui "github.com/gizak/termui/v3" 9 | "github.com/gizak/termui/v3/widgets" 10 | ) 11 | 12 | var ( 13 | //头部提示 14 | header = widgets.NewParagraph() 15 | //tabs切换 16 | tabs = widgets.NewTabPane("Queue", "Exchange", "News", "Consumer") 17 | //队列表格 18 | queueTable = widgets.NewTable() 19 | //交换机表格 20 | exchangeTable = widgets.NewTable() 21 | //news表格 22 | newsTable = widgets.NewTable() 23 | //consumer表格 24 | consumerTable = widgets.NewTable() 25 | ) 26 | 27 | func Init() { 28 | if err := ui.Init(); err != nil { 29 | log.Fatalf("failed to initialize termui: %v", err) 30 | } 31 | //延迟执行语句,会在函数退出时候执行 32 | defer ui.Close() 33 | 34 | //循环更新 35 | UpdateHead() 36 | go Repaint() 37 | uiEvents := ui.PollEvents() 38 | for { 39 | e := <-uiEvents 40 | switch e.ID { 41 | case "q": 42 | return 43 | case "": 44 | tabs.FocusRight() 45 | UpdateView() 46 | case "": 47 | tabs.FocusLeft() 48 | UpdateView() 49 | case "c": 50 | DelComponentView() 51 | UpdateView() 52 | case "": 53 | DelCompoentsView() 54 | UpdateView() 55 | } 56 | 57 | } 58 | } 59 | 60 | //更新头部样式 61 | func UpdateHead() { 62 | renderHeaderTable() 63 | renderTabsTable() 64 | } 65 | 66 | //重绘,数据更新就美隔一秒同步数据 67 | func Repaint() { 68 | w, h := ui.TerminalDimensions() 69 | for { 70 | time.Sleep(1 * time.Second) 71 | nw, nh := ui.TerminalDimensions() 72 | //如果一秒前后宽高发生变化就重绘 73 | if w != nw || h != nh || data.Q.State || data.E.State || data.N.State || data.C.State { 74 | data.Q.State = false 75 | data.E.State = false 76 | data.N.State = false 77 | data.C.State = false 78 | w = nw 79 | h = nh 80 | UpdateView() 81 | } 82 | 83 | } 84 | } 85 | 86 | //清空 组件视图方法 87 | func DelComponentView() { 88 | switch tabs.ActiveTabIndex { 89 | case 0: 90 | data.Q.DeleAll() 91 | case 1: 92 | data.E.DeleAll() 93 | case 2: 94 | data.N.DeleAll() 95 | case 3: 96 | data.C.DeleAll() 97 | } 98 | } 99 | 100 | //清空所有组件数据 101 | func DelCompoentsView() { 102 | data.Q.DeleAll() 103 | data.E.DeleAll() 104 | data.N.DeleAll() 105 | data.C.DeleAll() 106 | 107 | } 108 | 109 | //tabs切换方法 110 | func UpdateView() { 111 | ui.Clear() 112 | UpdateHead() 113 | switch tabs.ActiveTabIndex { 114 | case 0: 115 | renderQueueTable() 116 | case 1: 117 | renderExchangeTable() 118 | case 2: 119 | renderNewsTable() 120 | case 3: 121 | renderConsumerTable() 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /termui/src/data/exchangeTable.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | "time" 7 | "u-node-mq-termui/src/util" 8 | ) 9 | 10 | //控制器,前端传递的json 11 | type ExchangeLogData struct { 12 | BaseLogData 13 | Message string `json:"message"` //文本说明日志,error 也会使用 message 字段输出 14 | Accepted int `json:"accepted"` //接收消息的数量, 15 | Send int `json:"send"` //消息路由到队列的队列数量 16 | QueueNames []string `json:"queueNames"` //消息路由到队列的队列名称数组 17 | } 18 | 19 | //表json 20 | type ExchangeTableField struct { 21 | TableField 22 | //下面是统计数据 23 | AcceptedCount int 24 | SendCount int 25 | QueueNames []string 26 | } 27 | 28 | type ExchangeTable struct { 29 | lock sync.Mutex 30 | list []ExchangeTableField 31 | State bool 32 | } 33 | 34 | var ( 35 | E = ExchangeTable{} 36 | ) 37 | 38 | //添加数据 39 | //一个id的组件只会创建一次 40 | func (et *ExchangeTable) Add(ExchangeLogData ExchangeLogData) { 41 | e := ExchangeTableField{} 42 | e.Id = ExchangeLogData.Id 43 | e.Name = ExchangeLogData.Name 44 | e.CreatedTime = ExchangeLogData.CreatedTime 45 | e.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 46 | e.AcceptedCount += ExchangeLogData.Accepted 47 | e.SendCount += ExchangeLogData.Send 48 | e.QueueNames = ExchangeLogData.QueueNames 49 | et.list = append(et.list, e) 50 | et.State = true 51 | 52 | } 53 | 54 | //删除list 55 | func (et *ExchangeTable) DeleAll() { 56 | et.list = []ExchangeTableField{} 57 | et.State = true 58 | 59 | } 60 | 61 | //设置值,如果id不存在,就添加一条 62 | func (et *ExchangeTable) Set(ExchangeLogData ExchangeLogData) { 63 | e := et.Find(ExchangeLogData.Id) 64 | //是否需要加锁呢? 65 | et.lock.Lock() 66 | if e.Id == "" { 67 | et.Add(ExchangeLogData) 68 | } else { 69 | e.Name = ExchangeLogData.Name 70 | e.AcceptedCount += ExchangeLogData.Accepted 71 | e.SendCount += ExchangeLogData.Send 72 | //驱虫, 73 | for _, v1 := range ExchangeLogData.QueueNames { 74 | isRest := false 75 | for _, v2 := range e.QueueNames { 76 | if v1 == v2 { 77 | isRest = true 78 | break 79 | } 80 | } 81 | if !isRest { 82 | e.QueueNames = append(e.QueueNames, v1) 83 | } 84 | } 85 | 86 | e.UpdateTime = time.Now().In(util.CstSh).UnixMilli() 87 | 88 | } 89 | et.State = true 90 | 91 | et.lock.Unlock() 92 | } 93 | 94 | //通过id查找一条数据,返回一条数据的指针 95 | func (et *ExchangeTable) Find(id string) *ExchangeTableField { 96 | for i := 0; i < len(et.list); i++ { 97 | if id == et.list[i].Id { 98 | return &et.list[i] 99 | } 100 | } 101 | return &ExchangeTableField{} 102 | } 103 | 104 | func (et *ExchangeTable) FindList() []ExchangeTableField { 105 | sort.Slice(et.list, func(i, j int) bool { 106 | return et.list[i].UpdateTime > et.list[j].UpdateTime 107 | }) 108 | 109 | return et.list 110 | } 111 | -------------------------------------------------------------------------------- /src/core/SingleUNodeMQ.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from "../utils/tools"; 2 | import { Operator, Queue } from "../index"; 3 | import { Consume, Next } from "../internal/Consumer"; 4 | import { QueueOption } from "../internal/Queue"; 5 | 6 | /** 7 | * 创建SingleUNodeMQ函数 8 | * @param x 9 | */ 10 | function createSingleUnmq(x?: QueueOption | Queue) { 11 | return new SingleUNodeMQ(x); 12 | } 13 | export { createSingleUnmq }; 14 | /** 15 | * 单Queue的UNodeMQ类 16 | */ 17 | export default class SingleUNodeMQ { 18 | private queue: Queue; 19 | 20 | constructor(x?: QueueOption | Queue) { 21 | if (x instanceof Queue) this.queue = x; 22 | else this.queue = new Queue(x); 23 | } 24 | /** 25 | * 发送消息 26 | * @param contentList 27 | * @returns 28 | */ 29 | emit(...contentList: D[]) { 30 | for (const content of contentList) { 31 | this.queue.pushContent(content); 32 | } 33 | return this; 34 | } 35 | /** 36 | * 订阅消息 37 | * @param consume 38 | * @param payload 39 | * @returns 40 | */ 41 | on(consume: Consume, payload?: any) { 42 | this.queue.pushConsume(consume, payload); 43 | return () => this.off(consume); 44 | } 45 | /** 46 | * 移除消费者 47 | * @param consume 48 | */ 49 | off(consume: Consume): this; 50 | off(): this; 51 | off(x?: Consume): this { 52 | if (isFunction(x)) this.queue.removeConsumer(x); 53 | else this.queue.removeAllConsumer(); 54 | return this; 55 | } 56 | /** 57 | * 订阅一条消息 58 | * @param consume 59 | * @param payload 60 | */ 61 | once(consume: Consume, payload?: any): this; 62 | once(): Promise; 63 | once(consume?: Consume, payload?: any) { 64 | if (isFunction(consume)) { 65 | const consumeProxy = (content: any, next?: Next, payload?: any) => { 66 | this.off(consumeProxy); 67 | return consume(content, next, payload); 68 | }; 69 | this.on(consumeProxy, payload); 70 | return this; 71 | } else { 72 | return new Promise(resolve => { 73 | const consumeProxy = (content: any) => { 74 | this.off(consumeProxy); 75 | resolve(content); 76 | return true; 77 | }; 78 | this.on(consumeProxy, payload); 79 | }); 80 | } 81 | } 82 | /** 83 | * 添加operators 84 | * @param operators 85 | * @returns 86 | */ 87 | add(...operators: Operator[]) { 88 | this.queue.add(...operators); 89 | return this; 90 | } 91 | /** 92 | * fork一份队列,用于监听当前队列数据输出 93 | * @param x 94 | * @returns 95 | */ 96 | fork(x?: QueueOption | Queue) { 97 | const csu = createSingleUnmq(x); 98 | this.on(res => { 99 | csu.emit(res); 100 | return true; 101 | }); 102 | return csu; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/internal/Exchange.ts: -------------------------------------------------------------------------------- 1 | import { ComponentEnum } from "../utils/types"; 2 | import { random } from "../utils/tools"; 3 | import Logs from "./Logs"; 4 | /** 5 | * 中继器类型 6 | */ 7 | type Repeater = (content: D) => Promise | string[]; 8 | 9 | export type ExchangeOption = { 10 | routes?: string[]; 11 | repeater?: Repeater; 12 | name?: string; 13 | [k: string]: any; 14 | }; 15 | /** 16 | * 交换机 17 | */ 18 | export default class Exchange { 19 | [k: string]: any; 20 | name?: string; 21 | /** 22 | * 创建时间戳 23 | */ 24 | readonly createdTime: number; 25 | /** 26 | * id 27 | */ 28 | private readonly id: string = random(); 29 | getId() { 30 | return this.id; 31 | } 32 | /** 33 | * 静态路由 34 | */ 35 | private routes: string[] = []; 36 | getRoutes() { 37 | return this.routes; 38 | } 39 | pushRoutes(routes: string[]) { 40 | this.routes = Array.from(new Set(this.routes.concat(routes))); 41 | } 42 | setRoutes(routes: string[]) { 43 | this.routes = routes; 44 | } 45 | /** 46 | * 动态路由(中继器) 47 | */ 48 | private repeater: Repeater = () => this.getRoutes(); 49 | getRepeater() { 50 | return this.repeater; 51 | } 52 | setRepeater(repeater: Repeater) { 53 | this.repeater = repeater; 54 | } 55 | 56 | constructor(option?: ExchangeOption) { 57 | Object.assign(this, option); 58 | this.createdTime = new Date().getTime(); 59 | Logs.getLogsInstance()?.setLogs(ComponentEnum.EXCHANGE, { id: this.getId(), name: this.name, createdTime: this.createdTime }); 60 | } 61 | 62 | /** 63 | * 删除routes 64 | * @param routes 65 | */ 66 | removeRoutes(routes?: string[]) { 67 | if (routes === undefined) this.routes = []; 68 | else this.routes = this.routes.filter(item => routes.indexOf(item) !== -1); 69 | } 70 | 71 | /** 72 | * 获取队列名称列表 73 | * @param content 74 | * @returns 75 | */ 76 | async getQueueNameList(content: D): Promise { 77 | Logs.getLogsInstance()?.setLogs(ComponentEnum.EXCHANGE, { id: this.getId(), accepted: 1, name: this.name, createdTime: this.createdTime }); 78 | try { 79 | //中继器模式 80 | const queueNames = await this.repeater(content); 81 | Logs.getLogsInstance()?.setLogs(ComponentEnum.EXCHANGE, { 82 | id: this.getId(), 83 | send: queueNames.length, 84 | queueNames, 85 | name: this.name, 86 | createdTime: this.createdTime, 87 | }); 88 | return queueNames; 89 | } catch (error) { 90 | Logs.getLogsInstance()?.setLogs(ComponentEnum.EXCHANGE, { 91 | id: this.getId(), 92 | name: this.name, 93 | createdTime: this.createdTime, 94 | message: JSON.stringify(error), 95 | }); 96 | return []; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/operators/interval.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, createQuickUnmq } from "../../src/index"; 2 | import interval from "../../src/operators/interval"; 3 | import throttleTime from "../../src/operators/throttleTime"; 4 | 5 | import { expect, test } from "@jest/globals"; 6 | 7 | test("interval测试,一直循环", function (done) { 8 | const quickUnmq = createQuickUnmq(new Exchange(), { 9 | qu1: new Queue() 10 | //使用 operate 11 | .add(interval(1000, false)), 12 | }); 13 | 14 | let num = ""; 15 | quickUnmq.on("qu1", (res: number) => { 16 | num += res; 17 | }); 18 | 19 | const qu2 = new Queue().add(interval(1000, false)); 20 | 21 | let num2 = ""; 22 | qu2.pushConsume((res: number) => { 23 | num2 += res; 24 | }); 25 | setTimeout(() => qu2.removeAllConsumer(), 2500); 26 | setTimeout(() => { 27 | expect(num).toEqual("1234"); 28 | expect(num2).toEqual("12"); 29 | expect(qu2.getNews().length).toEqual(2); 30 | done(); 31 | }, 4500); 32 | }); 33 | 34 | test("interval测试,优化版", function (done) { 35 | const qu2 = new Queue().add(interval(100)); 36 | 37 | let num2 = ""; 38 | qu2.pushConsume((res: number) => { 39 | num2 += res; 40 | }); 41 | setTimeout(() => qu2.removeAllConsumer(), 250); 42 | 43 | setTimeout(() => { 44 | qu2.pushConsume((res: number) => { 45 | num2 += res; 46 | }); 47 | }, 750); 48 | setTimeout(() => { 49 | expect(num2).toEqual("1234"); 50 | expect(qu2.getNews().length).toEqual(0); 51 | done(); 52 | }, 1050); 53 | }); 54 | 55 | test("interval测试,简单组合使用技巧", function (done) { 56 | const qu1 = new Queue().add(interval(100)).add(throttleTime(200, true)); 57 | 58 | let num2 = ""; 59 | qu1.pushConsume((res: number) => { 60 | num2 += res; 61 | }); 62 | setTimeout(() => { 63 | expect(num2).toEqual("13579"); 64 | expect(qu1.getNews().length).toEqual(0); 65 | done(); 66 | }, 1050); 67 | }); 68 | 69 | test("interval测试,组合使用技巧", function (done) { 70 | const qu1 = new Queue().add(interval(100)); 71 | const qu2 = new Queue().add(throttleTime(200, true)); 72 | //将qu1的内容发射到qu2上,并在qu2上做其他业务操作 73 | qu1.pushConsume(qu2.pushContent.bind(qu2)); 74 | 75 | let num2 = ""; 76 | qu2.pushConsume((res: number) => { 77 | num2 += res; 78 | }); 79 | setTimeout(() => { 80 | expect(num2).toEqual("13579"); 81 | expect(qu2.getNews().length).toEqual(0); 82 | done(); 83 | }, 1050); 84 | }); 85 | 86 | test("interval测试,组合使用技巧quickUnmq版本", function (done) { 87 | const quickUnmq = createQuickUnmq(new Exchange(), { 88 | qu1: new Queue().add(interval(100)), 89 | }); 90 | const qu2 = new Queue().add(throttleTime(200, true)); 91 | //将qu1的内容发射到qu2上,并在qu2上做其他业务操作 92 | quickUnmq.on("qu1", (res: number) => { 93 | qu2.pushContent(res); 94 | }); 95 | 96 | let num2 = ""; 97 | qu2.pushConsume((res: number) => { 98 | num2 += res; 99 | }); 100 | setTimeout(() => { 101 | expect(num2).toEqual("13579"); 102 | expect(qu2.getNews().length).toEqual(0); 103 | done(); 104 | }, 1050); 105 | }); 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Stars 4 | Forks 5 | Size 6 | Version 7 | Languages 8 | Count 9 |

10 | 11 | ## 文档目录结构 12 | 13 | - [README.md](./README.md) 14 | - [todo.md](./todo.md) 15 | - docs 16 | - [nav.md](./docs/nav.md) 导航预览 17 | - [unmq.md](./docs/unmq.md) 快速开发 18 | - [other.md](./docs/other.md) 其他 19 | - internal 20 | - [index.md](./docs/internal/index.md) 组件介绍 21 | - [logs_sys_dev.md](./docs/internal/logs_sys_dev.md) 自定义日志系统开发 22 | - [termui.md](./docs/internal/termui.md) termui 使用 23 | - operators 24 | - [index.md](./docs/operators/index.md) 操作符介绍 25 | - plugins 26 | - [index.md](./docs/plugins/index.md) 插件介绍 27 | - [IframePlugin.md](./docs/plugins/IframePlugin.md) Iframe 通信插件 28 | 29 | ## 文档内容 30 | 31 | ### `u-node-mq` 是什么? 32 | 33 | `u-node-mq`是用来解决前端项目中数据异步通信问题的工具,可以准确的将一个模块的数据传到另一个模块,就像`rabbitMQ`使用发布订阅模型的中间件一样,使用`u-node-mq`可以完全解耦前端模块的耦合; 34 | 35 | ### 其他 36 | 37 | - `u-node-mq`在文档和代码注释中有时也会写成简写`unmq`; 38 | 39 | - `u-node-mq`中的`u`是标识词;`node`是最初创建项目的执行环境是 `node`,但是后面经过使用 `ts` 升级和重构,现在已经升级到可以在所有 `js` 环境中执行;`mq`是`message queue`的简写; 40 | 41 | - [其他信息](./docs/other.md) 42 | 43 | ### npm 安装 44 | 45 | `pnpm add u-node-mq` 46 | 47 | or 48 | 49 | `yarn add u-node-mq` 50 | 51 | or 52 | 53 | `npm install u-node-mq` 54 | 55 | ### `u-node-mq` 基本使用方法 56 | 57 | **unmq.js** 58 | 59 | ```javascript 60 | import UNodeMQ, { Exchange, Queue } from "u-node-mq"; 61 | 62 | //声明交换机ex1,以及队列qu1 63 | const unmq = new UNodeMQ({ ex1: new Exchange({ routes: ["qu1"] }) }, { qu1: new Queue() }); 64 | 65 | export default unmq; 66 | 67 | //可以挂到抬手就摸得到的位置 68 | 69 | // Vue.prototype.unmq = unmq; //(Vue 2.x) 70 | 71 | // const app = createApp({}) 72 | // app.config.globalProperties.unmq = unmq //(Vue 3.x) 73 | ``` 74 | 75 | **模块 A.js** 76 | 77 | ```javascript 78 | import unmq from "unmq.js"; 79 | 80 | //发送数据 81 | unmq.emit("ex1", "消息内容1", "消息内容2"); 82 | ``` 83 | 84 | **模块 B.js** 85 | 86 | ```javascript 87 | import unmq from "unmq.js"; 88 | 89 | //接收并消费数据 90 | unmq.on("qu1", getData); 91 | 92 | function getData(data) { 93 | console.log(data); 94 | } 95 | ``` 96 | 97 | ### [了解更多详细内容](./docs/nav.md) 98 | 99 | ### [TODO](./todo.md) 100 | -------------------------------------------------------------------------------- /src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取随机数 3 | * @returns 4 | */ 5 | export const random = (): string => String(Math.round(Math.random() * 10000000000)); 6 | 7 | /** 8 | * 获取uuid 9 | * @returns 10 | */ 11 | export function getUUID() { 12 | return "xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx" 13 | .replace(/[xy]/g, function (c) { 14 | const r = (Math.random() * 16) | 0; 15 | const v = c == "x" ? r : (r & 0x3) | 0x8; 16 | return v.toString(16); 17 | }) 18 | .toUpperCase(); 19 | } 20 | 21 | export const promiseSetTimeout = (time = 0) => new Promise(resolve => setTimeout(resolve, time)); 22 | 23 | /** 24 | * 获取格式化时间 25 | * @param time 26 | * @returns 27 | */ 28 | export const getTimeFormat = (time?: string | number): string => { 29 | let now = null; 30 | if (time) now = new Date(time); 31 | else now = new Date(); 32 | 33 | const year = now.getFullYear(); //年 34 | const month = now.getMonth() + 1; //月 35 | const day = now.getDate(); //日 36 | 37 | const hh = now.getHours(); //时 38 | const mm = now.getMinutes(); //分 39 | 40 | let clock = year + "-"; 41 | 42 | if (month < 10) clock += "0"; 43 | clock += month + "-"; 44 | 45 | if (day < 10) clock += "0"; 46 | clock += day + " "; 47 | 48 | if (hh < 10) clock += "0"; 49 | clock += hh + ":"; 50 | if (mm < 10) clock += "0"; 51 | clock += mm; 52 | return clock; 53 | }; 54 | /** 55 | * //字符编码数值对应的存储长度: 56 | //UCS-2编码(16进制) UTF-8 字节流(二进制) 57 | //0000 - 007F 0xxxxxxx (1字节) 58 | //0080 - 07FF 110xxxxx 10xxxxxx (2字节) 59 | //0800 - FFFF 1110xxxx 10xxxxxx 10xxxxxx (3字节) 60 | * @param str 61 | * @returns 62 | */ 63 | export const memorySize = (str: string): string => { 64 | let totalLength = 0; 65 | let charCode; 66 | for (let i = 0; i < str.length; i++) { 67 | charCode = str.charCodeAt(i); 68 | if (charCode < 0x007f) { 69 | totalLength++; 70 | } else if (0x0080 <= charCode && charCode <= 0x07ff) { 71 | totalLength += 2; 72 | } else if (0x0800 <= charCode && charCode <= 0xffff) { 73 | totalLength += 3; 74 | } else { 75 | totalLength += 4; 76 | } 77 | } 78 | if (totalLength >= 1024 * 1024) return (totalLength / (1024 * 1024)).toFixed(2) + "MB"; 79 | if (totalLength >= 1024 && totalLength < 1024 * 1024) return (totalLength / 1024).toFixed(2) + "KB"; 80 | else return totalLength + "B"; 81 | }; 82 | 83 | /** 84 | * 转换 85 | */ 86 | export const extend = Object.assign; 87 | export const objectToString = Object.prototype.toString; 88 | export const toTypeString = (value: unknown): string => objectToString.call(value); 89 | 90 | /** 91 | * 类型判断 92 | */ 93 | export const isArray = Array.isArray; 94 | export const isMap = (val: unknown): val is Map => toTypeString(val) === "[object Map]"; 95 | export const isSet = (val: unknown): val is Set => toTypeString(val) === "[object Set]"; 96 | export const isDate = (val: unknown): val is Date => val instanceof Date; 97 | // eslint-disable-next-line @typescript-eslint/ban-types 98 | export const isFunction = (val: unknown): val is Function => typeof val === "function"; 99 | export const isString = (val: unknown): val is string => typeof val === "string"; 100 | export const isNumber = (val: unknown): val is number => typeof val === "number"; 101 | export const isBoolean = (val: unknown): val is boolean => typeof val === "boolean"; 102 | export const isSymbol = (val: unknown): val is symbol => typeof val === "symbol"; 103 | export const isObject = (val: unknown): val is Record => val !== null && typeof val === "object"; 104 | export const isPromise = (val: unknown): val is Promise => { 105 | return isObject(val) && isFunction(val.then) && isFunction(val.catch); 106 | }; 107 | //获取构造函数第一个参数的类型 108 | export type ConstructorParameter any> = T extends new (args: infer P) => any ? P : never; 109 | -------------------------------------------------------------------------------- /src/internal/Logs.ts: -------------------------------------------------------------------------------- 1 | import { Operator, Queue } from "../index"; 2 | import { ComponentEnum } from "../utils/types"; 3 | /** 4 | * 应该尽量避免生产环境中把日志混入代码,减少代码量,除非生产环境中需要使用到日志 5 | * unmq:{ 6 | * log:false //默认false 7 | * type:'http'|'console' //默认'console' 8 | * components:['Exchange','Queue','News','Consumer'] //默认* 9 | * 10 | * } 11 | */ 12 | /** 13 | * 使用operator记录queue日志 14 | * @returns 15 | */ 16 | export function queueLogsOperator(): Operator { 17 | let queueInstance: Queue; 18 | function addQueueData() { 19 | Logs.getLogsInstance()?.setLogs(ComponentEnum.QUEUE, { 20 | name: queueInstance.name, 21 | createdTime: queueInstance.createdTime, 22 | id: queueInstance.getId(), 23 | newsNum: queueInstance.getNews().length, 24 | newsIds: queueInstance.getNews().map(item => item.getId()), 25 | consumerNum: queueInstance.getConsumerList().length, 26 | consumerIds: queueInstance.getConsumerList().map(item => item.getId()), 27 | message: "queue ok!", 28 | }); 29 | } 30 | return { 31 | mounted(queue) { 32 | queueInstance = queue; 33 | addQueueData(); 34 | }, 35 | addedNews() { 36 | addQueueData(); 37 | }, 38 | ejectNews() { 39 | addQueueData(); 40 | return true; 41 | }, 42 | addedConsumer() { 43 | addQueueData(); 44 | }, 45 | removedConsumer() { 46 | addQueueData(); 47 | }, 48 | }; 49 | } 50 | enum LogsEnum { 51 | "CUSTOM" = "custom", 52 | "CONSOLE" = "console", 53 | } 54 | type LogsType = LogsEnum.CUSTOM | LogsEnum.CONSOLE; 55 | type LogsComponent = ComponentEnum.EXCHANGE | ComponentEnum.QUEUE | ComponentEnum.NEWS | ComponentEnum.CONSUMER; 56 | interface LogsConfig { 57 | logs: boolean; 58 | types?: LogsType[]; 59 | logsComponents?: LogsComponent[]; 60 | customFunction?: (name: LogsComponent, data: D) => void; 61 | } 62 | interface BaseLogData { 63 | id: string; //id 64 | createdTime: number; //创建时间戳 65 | name?: string; //名称 66 | message?: string; //描述日志 67 | } 68 | interface ExchangeLogData { 69 | accepted?: number; //当前接收写入交换机的消息数量 70 | send?: number; //当前发送给队列的消息数量,一条消费发送给两个队列,send则为2 71 | queueNames?: string[]; //已发送给队列的队列名称列表 72 | } 73 | interface QueueLogData { 74 | newsNum: number; //当前消息数量 75 | newsIds: string[]; //当前消息的id列表 76 | consumerNum: number; //当前消费者数量 77 | consumerIds: string[]; //当前消费者id列表 78 | } 79 | // interface NewsLogData {} 80 | interface ConsumerLogData { 81 | accepted?: number; //当前消费数量 82 | } 83 | 84 | interface LogDataTypes { 85 | [ComponentEnum.EXCHANGE]: BaseLogData & ExchangeLogData; 86 | [ComponentEnum.QUEUE]: BaseLogData & QueueLogData; 87 | [ComponentEnum.NEWS]: BaseLogData; 88 | [ComponentEnum.CONSUMER]: BaseLogData & ConsumerLogData; 89 | } 90 | type CustomLogFunction = (name: LogsComponent, data: LogDataTypes[K]) => unknown; 91 | /** 92 | * 全局日志组件 93 | */ 94 | export default class Logs { 95 | private static logs = false; 96 | private static types: LogsType[]; 97 | private static logsComponents: LogsComponent[]; 98 | private static customFunction: CustomLogFunction; 99 | static setLogsConfig(logsConfig: LogsConfig) { 100 | this.logs = logsConfig.logs; 101 | this.types = logsConfig.types ?? [LogsEnum.CONSOLE]; 102 | this.logsComponents = logsConfig.logsComponents ?? [ComponentEnum.EXCHANGE, ComponentEnum.QUEUE, ComponentEnum.NEWS, ComponentEnum.CONSUMER]; 103 | this.customFunction = 104 | logsConfig.customFunction ?? 105 | function (res) { 106 | console.log(res); 107 | }; 108 | } 109 | /** 110 | * 获取日志实例 111 | * @returns 112 | */ 113 | static getLogsInstance() { 114 | if (!this.logs) return; 115 | return { 116 | setLogs: this.setLogs.bind(this), 117 | }; 118 | } 119 | /** 120 | * 设置日志 121 | * @param name 122 | * @param data 123 | * @returns 124 | */ 125 | private static setLogs(name: LogsComponent, data: LogDataTypes[K]) { 126 | if (this.logsComponents.indexOf(name) === -1) return; 127 | 128 | this.types.forEach(type => { 129 | if (type === LogsEnum.CUSTOM) { 130 | this.customFunction(name, data); 131 | } else if (type === LogsEnum.CONSOLE) { 132 | console.log(name, data); 133 | } 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/core/QuickUNodeMQ.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from "../utils/tools"; 2 | import { Exchange, Queue, Plugin } from "../index"; 3 | import { Consume, Next } from "../internal/Consumer"; 4 | import { ExchangeOption } from "../internal/Exchange"; 5 | 6 | /** 7 | * 创建QuickUNodeMQ函数 8 | * @param x 9 | * @param y 10 | * @returns 11 | */ 12 | function createQuickUnmq>>(x: ExchangeOption | Exchange, y: QueueCollection) { 13 | return new QuickUNodeMQ(x, y); 14 | } 15 | export { createQuickUnmq }; 16 | /** 17 | * 单交换机的UNodeMQ类 18 | */ 19 | export default class QuickUNodeMQ>> { 20 | private exchange: Exchange; 21 | private queueCollection: QueueCollection; 22 | private readonly installedPlugins: Set = new Set(); 23 | use(plugin: Plugin, ...options: any[]) { 24 | if (this.installedPlugins.has(plugin)) { 25 | console.log(`Plugin has already been applied to target unmq.`); 26 | } else if (plugin && isFunction(plugin.install)) { 27 | this.installedPlugins.add(plugin); 28 | plugin.install(this, ...options); 29 | } else if (isFunction(plugin)) { 30 | this.installedPlugins.add(plugin); 31 | plugin(this, ...options); 32 | } 33 | return this; 34 | } 35 | 36 | constructor(x: ExchangeOption | Exchange, y: QueueCollection) { 37 | if (x instanceof Exchange) this.exchange = x; 38 | else this.exchange = new Exchange(x); 39 | 40 | for (const name in y) { 41 | y[name].name = name; 42 | } 43 | this.queueCollection = y; 44 | } 45 | /** 46 | * 发射数据到交换机 47 | * @param contentList 消息体列表 48 | * @returns 49 | */ 50 | emit(...contentList: D[]) { 51 | for (const content of contentList) { 52 | this.exchange.getQueueNameList(content).then((queueNameList: string[]) => { 53 | for (const queueName of queueNameList) { 54 | if (this.queueCollection[queueName] === undefined) continue; 55 | this.queueCollection[queueName].pushContent(content); 56 | } 57 | }); 58 | } 59 | return this; 60 | } 61 | /** 62 | * 发射数据到队列 63 | * @param queueName 64 | * @param contentList 65 | * @returns 66 | */ 67 | emitToQueue(queueName: Q, ...contentList: D[]) { 68 | for (const content of contentList) { 69 | this.queueCollection[queueName].pushContent(content); 70 | } 71 | return this; 72 | } 73 | /** 74 | * 订阅队列消息 75 | * @param queueName 队列名称 76 | * @param consume 消费方法 77 | * @param payload 固定参数,有效载荷,在每次消费的时候都传给消费者 78 | * @returns 79 | */ 80 | on(queueName: Q, consume: Consume, payload?: any) { 81 | this.queueCollection[queueName].pushConsume(consume, payload); 82 | return () => this.off(queueName, consume); 83 | } 84 | 85 | /** 86 | * 移除消费者 87 | * @param queueName 88 | * @param consume 89 | */ 90 | off(queueName: Q, consume?: Consume): this { 91 | if (isFunction(consume)) { 92 | this.queueCollection[queueName].removeConsumer(consume); 93 | } else this.queueCollection[queueName].removeAllConsumer(); 94 | return this; 95 | } 96 | 97 | /** 98 | * 订阅一条消息 99 | * @param queueName 100 | * @param consume 101 | * @param payload 102 | * @returns 103 | */ 104 | once(queueName: Q, consume: Consume, payload?: any): this; 105 | once(queueName: Q): Promise; 106 | once(queueName: Q, consume?: Consume, payload?: any) { 107 | if (!isFunction(consume)) { 108 | return new Promise(resolve => { 109 | const consumeProxy = (content: any) => { 110 | this.off(queueName, consumeProxy); 111 | resolve(content); 112 | return true; 113 | }; 114 | this.on(queueName, consumeProxy, payload); 115 | }); 116 | } else { 117 | const consumeProxy = (content: any, next?: Next, payload?: any) => { 118 | this.off(queueName, consumeProxy); 119 | return consume(content, next, payload); 120 | }; 121 | this.on(queueName, consumeProxy, payload); 122 | return this; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /docs/unmq.md: -------------------------------------------------------------------------------- 1 | # 快速开发 2 | 3 | 开发中常用的两种方式 4 | 5 | - `UNodeMQ` 一个`UNodeMQ`类中集合了多个交换机和多个队列,使用场景为多个交换机中有队列进行数据交叉路由的情况;`createUnmq`为`UNodeMQ` 类的函数式方法; 6 | 7 | - `QuickUNodeMQ` 包含一个交换机和多个队列的类,在单一路由的情况中可以快速开发使用;`createQuickUnmq`为`QuickUNodeMQ` 类的函数式方法; 8 | 9 | ## 1、UNodeMQ 10 | 11 | ```javascript 12 | import UNodeMQ, { createUnmq } from "u-node-mq"; 13 | const unmq = new UNodeMQ(ExchangeCollection, QueueCollection); 14 | //or 15 | const unmq = createUnmq(ExchangeCollection, QueueCollection); 16 | ``` 17 | 18 | 创建模块 19 | 20 | **UNodeMQ constructor 参数说明** 21 | 22 | | 名称 | 类型 | 必填 | 说明 | 23 | | ------------------ | --------------------- | ---- | ---------- | 24 | | ExchangeCollection | { string : Exchange } | 是 | 交换机集合 | 25 | | QueueCollection | { string : Queue } | 是 | 队列集合 | 26 | 27 | **unmq 方法说明** 28 | 29 | | 名称 | 参数类型 | 说明 | 30 | | ----------- | ---------------------------------- | -------------------------------------------------------------- | 31 | | emit | (ExchangeName , ...消息) | 发送数据到交换机,返回 this | 32 | | emitToQueue | (QueueName , ...消息) | 发送数据到交换机,返回 this | 33 | | on | (QueueName , 消费方法 , ?载荷消息) | 订阅队列消息,载荷信息每次都会发送给消费者,返回取消订阅的函数 | 34 | | off | (QueueName , ?消费方法) | 移除队列上的指定消费者或者移除队列上所有消费者,返回 this | 35 | | once | (QueueName , 消费方法 , ?载荷消息) | 只消费一条消息,返回 this | 36 | | 更多 | 未知 | 更多的内部方法 | 37 | 38 | ## 2、QuickUNodeMQ 39 | 40 | ```javascript 41 | import { QuickUNodeMQ, createQuickUnmq, ExchangeOption, Exchange } from "u-node-mq"; 42 | const quickUnmq = new QuickUNodeMQ(ExchangeOption | Exchange, QueueCollection); 43 | //or 44 | const quickUnmq = createQuickUnmq(ExchangeOption | Exchange, QueueCollection); 45 | ``` 46 | 47 | 创建模块 48 | 49 | **QuickUNodeMQ constructor 参数说明** 50 | 51 | | 名称 | 类型 | 必填 | 说明 | 52 | | --------------- | ------------------ | ---- | -------------- | 53 | | ExchangeOption | QueueCollection | 是 | 交互机配置参数 | 54 | | Exchange | Exchange | 是 | 交换机 | 55 | | QueueCollection | { string : Queue } | 是 | 队列集合 | 56 | 57 | **quickUnmq 方法说明** 58 | 59 | | 名称 | 参数类型 | 说明 | 60 | | ----------- | ---------------------------------- | -------------------------------------------------------------- | 61 | | emit | (ExchangeName , ...消息) | 发送数据到交换机,返回 this | 62 | | emitToQueue | (QueueName , ...消息) | 发送数据到交换机,返回 this | 63 | | on | (QueueName , 消费方法 , ?载荷消息) | 订阅队列消息,载荷信息每次都会发送给消费者,返回取消订阅的函数 | 64 | | off | (QueueName , ?消费方法) | 移除队列上的指定消费者或者移除队列上所有消费者,返回 this | 65 | | once | (QueueName , 消费方法 , ?载荷消息) | 只消费一条消息,返回 this | 66 | 67 | ## 3、SingleUNodeMQ 68 | 69 | ```javascript 70 | import { SingleUNodeMQ, createSingleUnmq, QueueOption, Queue } from "u-node-mq"; 71 | const singleUnmq = new SingleUNodeMQ(QueueOption | Queue); 72 | //or 73 | const singleUnmq = createSingleUnmq(QueueOption | Queue); 74 | ``` 75 | 76 | 创建模块 77 | 78 | **SingleUNodeMQ constructor 参数说明** 79 | 80 | | 名称 | 类型 | 必填 | 说明 | 81 | | ----------- | -------- | ---- | ------------ | 82 | | QueueOption | Exchange | 是 | 队列配置参数 | 83 | | Queue | Exchange | 是 | 队列实例 | 84 | 85 | **singleUnmq 方法说明** 86 | 87 | | 名称 | 参数类型 | 说明 | 88 | | ---- | ---------------------------------------------------------- | -------------------------------------------------------------- | 89 | | emit | ( ...消息) | 发送数据到队列,返回 this | 90 | | on | ( 消费方法 , ?载荷消息) | 订阅队列消息,载荷信息每次都会发送给消费者,返回取消订阅的函数 | 91 | | off | ( ?消费方法) | 移除队列上的指定消费者或者移除队列上所有消费者,返回 this | 92 | | once | ( 消费方法 , ?载荷消息) | 只消费一条消息,返回 this | 93 | | add | 向当前队列添加 operators | 返回 this | 94 | | fork | 在当前队列队尾添加一个新的队列,使当前队列和新队列数据连通 | 返回新的队列 | 95 | -------------------------------------------------------------------------------- /src/core/UNodeMQ.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from "../utils/tools"; 2 | import { Exchange, Queue } from "../index"; 3 | import { Consume, Next } from "../internal/Consumer"; 4 | import Collection from "./Collection"; 5 | import { Plugin } from "@/plugins/index"; 6 | 7 | /** 8 | * 获取队列名称返回的promise导致消费事件加入微任务队列延迟消费(ios属于加入普通任务队列) 9 | * 这样同时保证了在观察者模式中数据能准确分发 10 | */ 11 | 12 | export type ReturnPanShapeExchange = T extends Exchange ? U : never; 13 | export type ReturnPanShapeQueue = T extends Queue ? U : never; 14 | 15 | /** 16 | * 使用普通函数创建unmq 17 | * @param exchangeCollection 18 | * @param queueCollection 19 | * @returns 20 | */ 21 | export function createUnmq>, QueueCollection extends Record>>( 22 | exchangeCollection: ExchangeCollection, 23 | queueCollection: QueueCollection, 24 | ) { 25 | return new UNodeMQ(exchangeCollection, queueCollection); 26 | } 27 | /** 28 | * UNodeMQ 发布订阅模型 29 | * 从3.7.0版本开始,一个u-node-mq仅支持一种消息类型 30 | */ 31 | export default class UNodeMQ< 32 | D, 33 | ExchangeCollection extends Record>, 34 | QueueCollection extends Record>, 35 | > extends Collection { 36 | constructor(exchangeCollection: ExchangeCollection, queueCollection: QueueCollection) { 37 | super(exchangeCollection, queueCollection); 38 | } 39 | private readonly installedPlugins: Set = new Set(); 40 | use(plugin: Plugin, ...options: any[]) { 41 | if (this.installedPlugins.has(plugin)) { 42 | console.log(`Plugin has already been applied to target unmq.`); 43 | } else if (plugin && isFunction(plugin.install)) { 44 | this.installedPlugins.add(plugin); 45 | plugin.install(this, ...options); 46 | } else if (isFunction(plugin)) { 47 | this.installedPlugins.add(plugin); 48 | plugin(this, ...options); 49 | } 50 | return this; 51 | } 52 | /** 53 | * 发射数据到交换机 54 | * @param contentList 消息体列表 55 | * @returns 56 | */ 57 | emit(exchangeName: E, ...contentList: ReturnPanShapeExchange[]) { 58 | super.pushContentListToExchange(exchangeName, ...contentList); 59 | return this; 60 | } 61 | /** 62 | * 发射数据到队列 63 | * @param queueName 64 | * @param contentList 65 | * @returns 66 | */ 67 | emitToQueue(queueName: Q, ...contentList: ReturnPanShapeQueue[]) { 68 | super.pushContentListToQueue(queueName, ...contentList); 69 | return this; 70 | } 71 | 72 | /** 73 | * 订阅队列消息 74 | * @param queueName 队列名称 75 | * @param consume 消费方法 76 | * @param payload 固定参数,有效载荷,在每次消费的时候都传给消费者 77 | * @returns 78 | */ 79 | on(queueName: Q, consume: Consume>, payload?: any) { 80 | super.subscribeQueue(queueName, consume, payload); 81 | return () => this.off(queueName, consume); 82 | } 83 | 84 | /** 85 | * 移除消费者 86 | * @param queueName 87 | * @param consume 88 | */ 89 | off(queueName: Q, consume?: Consume>): this { 90 | super.unsubscribeQueue(queueName, consume); 91 | return this; 92 | } 93 | 94 | /** 95 | * 订阅一条消息 96 | * 不传入消费方法,则返回Promise 97 | * @param queueName 98 | * @param consume 99 | * @param payload 100 | * @returns 101 | */ 102 | once(queueName: Q, consume: Consume>, payload?: any): this; 103 | once(queueName: Q): Promise>; 104 | once(queueName: Q, consume?: Consume>, payload?: any) { 105 | if (!isFunction(consume)) { 106 | return new Promise(resolve => { 107 | const consumeProxy = (content: any) => { 108 | this.off(queueName, consumeProxy); 109 | resolve(content); 110 | return true; 111 | }; 112 | this.on(queueName, consumeProxy, payload); 113 | }); 114 | } else { 115 | const consumeProxy = (content: any, next?: Next, payload?: any) => { 116 | this.off(queueName, consumeProxy); 117 | return consume(content, next, payload); 118 | }; 119 | this.on(queueName, consumeProxy, payload); 120 | return this; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /docs/internal/index.md: -------------------------------------------------------------------------------- 1 | # 工作原理 2 | 3 | 下面是一张工作原理的简图; 4 | 5 | 6 | 7 | ## 五大基础组件 8 | 9 | - `Exchange` 交换机,每个交换机就是一个分发数据到队列的路由; 10 | 11 | - `Queue` 队列,队列是一个能存储数据和配合 operators 处理数据的组件,丰富的配置和管道操作符可以实现更复杂的功能; 12 | 13 | - `News` 消息,存储内容的组件; 14 | 15 | - `Consumer` 消费者,处理消息的业务逻辑代码; 16 | 17 | - `Logs` 日志消息,方便调试开发,也可将日志发送到服务器; 18 | 19 | ## 1、Exchange 20 | 21 | ```javascript 22 | const exchange = new Exchange(Option); 23 | ``` 24 | 25 | 创建交换机 26 | 27 | **Option 参数说明** 28 | 29 | | 名称 | 类型 | 必填 | 说明 | 30 | | -------- | -------- | ---- | ---------------------------------------- | 31 | | name | string | 否 | 交换机名称 | 32 | | routes | string[] | 否 | 需要匹配的队列名称 | 33 | | repeater | Function | 否 | 自定义路由函数,填写该参数 routes 将失效 | 34 | 35 | ## 2、Queue 36 | 37 | ```javascript 38 | const queue = new Option(Option); 39 | ``` 40 | 41 | 创建队列 42 | 43 | **Option 参数说明** 44 | 45 | | 名称 | 类型 | 必填 | 默认 | 说明 | 46 | | ------- | ----------------- | ---- | ----- | --------------------------------------------------------------------------- | 47 | | name | string | 否 | | 队列名称 | 48 | | mode | "Random" \| "All" | 否 | "All" | 消费模式,Random 代表随机抽取一个消费者消费,All 代表所有消费者都会消费消息 | 49 | | ask | boolean | 否 | false | 是否需要消息确认,为 true,则需要手动确认消息 | 50 | | rcn | number | 否 | 3 | 消费失败后可重复消费次数 | 51 | | async | noolean | 否 | false | 是否是异步队列,为 false 则会一条消息消费完成或者失败才会消费下一条消息 | 52 | | maxTime | number | 否 | 3000 | 最长消费时长,单位毫秒,小于 0 代表不限时长 | 53 | 54 | ## 3、News 55 | 56 | ```javascript 57 | const news = new News(Any); 58 | ``` 59 | 60 | 创建消息 61 | 62 | **news 属性说明** 63 | 64 | | 名称 | 类型 | 说明 | 65 | | ------------- | ------ | ------------------ | 66 | | createTime | number | 消息创建时间戳 | 67 | | content | Any | 消息内容 | 68 | | consumedTimes | number | 剩余可重复消费次数 | 69 | 70 | ## 4、Consumer 71 | 72 | ```javascript 73 | const consumer = new Consumer(Consume, PayLoad); 74 | ``` 75 | 76 | 创建消费者 77 | 78 | **Consume 参数说明** 79 | 80 | | 参数 | 类型 | 说明 | 81 | | ------ | ------- | --------------------------------------------------------------- | 82 | | 参数 1 | D | 消息内容 | 83 | | 参数 2 | next | 是否确认消费,执行 next 默认为确认消费,传 false 则代表消费失败 | 84 | | 参数 3 | payload | 固定消费内容,每次消费都会传递 | 85 | 86 | **consumer 属性说明** 87 | 88 | | 名称 | 类型 | 说明 | 89 | | ---------- | -------- | ---------------- | 90 | | createTime | number | 消费者创建时间戳 | 91 | | consume | Function | 消费方法 | 92 | | payload | any | 固定载荷 | 93 | 94 | ## 5、Logs 95 | 96 | Logs 是一个不可实例化的静态类,开发者可以使用`setLogsConfig`对日志输出进行配置; 97 | 98 | ```javascript 99 | import { Logs } from "u-node-mq"; 100 | Logs.setLogsConfig(LogsConfig); 101 | ``` 102 | 103 | **LogsConfig 对象说明** 104 | 105 | | 参数 | 类型 | 默认值 | 说明 | 106 | | -------------- | --------------- | ----------------------------------------- | --------------------------------------------------------------------- | 107 | | logs | boolean | false | 控制是否输出日志 | 108 | | types | LogsType[] | ["console"] | LogsType 为 "custom" 或者 "console" | 109 | | logsComponents | LogsComponent[] | ["Exchange", "Queue", "News", "Consumer"] | 需要输出日志的组件 | 110 | | customFunction | Function | CustomLogFunction | 需要自定义处理日志的功能函数 | 111 | | include | string[] | [] | 包含要输出 Exchange 和 Queue 日志的 name ,为空数组代表包含所有组件 | 112 | | exclude | string[] | [] | 过滤要输出 Exchange 和 Queue 日志的 name ,为空数组代表不过滤任何组件 | 113 | 114 | 如需开发服务端日志系统,开发者需要实现`CustomLogFunction`方法,[点击查看`CustomLogFunction`实现细节](./logs_sys_dev.md) 115 | 116 | ## go 控制台快速开发日志 117 | 118 | `unmq`内置了`go`开发的控制台日志输出,开发者可快速响应开发;[点击查看使用方法](./termui.md) 119 | -------------------------------------------------------------------------------- /src/core/Collection.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, News } from "../index"; 2 | import { Consume } from "../internal/Consumer"; 3 | import ExchangeCollectionHandle from "./ExchangeCollectionHandle"; 4 | import QueueCollectionHandle from "./QueueCollectionHandle"; 5 | export default class Collection>, QueueCollection extends Record>> { 6 | /** 7 | * 交换机集合 8 | */ 9 | private readonly exchangeCollectionHandle = new ExchangeCollectionHandle(); 10 | /** 11 | * 队列集合 12 | */ 13 | private readonly queueCollectionHandle = new QueueCollectionHandle(); 14 | 15 | constructor(exchangeCollection: ExchangeCollection, queueCollection: QueueCollection) { 16 | for (const name in exchangeCollection) { 17 | exchangeCollection[name].name = name; 18 | } 19 | for (const name in queueCollection) { 20 | queueCollection[name].name = name; 21 | } 22 | this.exchangeCollectionHandle.setExchangeCollection(exchangeCollection); 23 | this.queueCollectionHandle.setQueueCollection(queueCollection); 24 | } 25 | /** 26 | * 根据交换机名称获取交换机 27 | * @param exchangeName 28 | * @returns 29 | */ 30 | getExchange(exchangeName: E) { 31 | return this.exchangeCollectionHandle.getExchange(exchangeName); 32 | } 33 | /** 34 | * 获取交换机集合列表 35 | * @returns 36 | */ 37 | getExchangeList() { 38 | return this.exchangeCollectionHandle.getExchangeList(); 39 | } 40 | /** 41 | * 添加交换机 42 | * @param exchangeName 43 | * @param exchange 44 | * @returns 45 | */ 46 | addExchage(exchangeName: string, exchange: Exchange) { 47 | return this.exchangeCollectionHandle.addExchage(exchangeName, exchange); 48 | } 49 | /** 50 | * 根据 51 | * @param queueName 52 | * @returns 53 | */ 54 | getQueue(queueName: Q) { 55 | return this.queueCollectionHandle.getQueue(queueName); 56 | } 57 | /** 58 | * 获取队列集合列表 59 | * @returns 60 | */ 61 | getQueueList() { 62 | return this.queueCollectionHandle.getQueueList(); 63 | } 64 | /** 65 | * 添加一个队列到队列集合 66 | * @param queurName 67 | * @param queue 68 | * @returns 69 | */ 70 | addQueue(queurName: string, queue: Queue) { 71 | return this.queueCollectionHandle.addQueue(queurName, queue); 72 | } 73 | /** 74 | * 发送消息到交换机 75 | * @param exchangeName 76 | * @param news 77 | */ 78 | pushNewsListToExchange(exchangeName: E, ...news: News[]) { 79 | for (const newsItem of news) { 80 | //分别发送每一条消息 81 | this.exchangeCollectionHandle.getQueueNameList(exchangeName, newsItem.content).then(queueNameList => { 82 | for (const queueName in queueNameList) { 83 | this.pushNewsListToQueue(queueName, newsItem); 84 | } 85 | }); 86 | } 87 | } 88 | /** 89 | * 发送消息到队列 90 | * @param queueName 91 | * @param news 92 | */ 93 | pushNewsListToQueue(queueName: Q, ...news: News[]) { 94 | for (const newsItem of news) { 95 | //分别发送每一条消息 96 | this.queueCollectionHandle.pushNewsToQueue(queueName, newsItem); 97 | } 98 | } 99 | /** 100 | * 发送消息内容到交换机 101 | * @param exchangeName 102 | * @param contentList 103 | */ 104 | pushContentListToExchange(exchangeName: E, ...contentList: D[]) { 105 | for (const content of contentList) { 106 | //分别发送每一条消息 107 | this.exchangeCollectionHandle.getQueueNameList(exchangeName, content).then(queueNameList => { 108 | for (const queueName of queueNameList) { 109 | this.pushContentListToQueue(queueName, content); 110 | } 111 | }); 112 | } 113 | } 114 | /** 115 | * 发送消息内容到队列 116 | * @param queueName 117 | * @param contentList 118 | */ 119 | pushContentListToQueue(queueName: Q, ...contentList: D[]) { 120 | for (const content of contentList) { 121 | //分别发送每一条消息 122 | this.queueCollectionHandle.pushContentToQueue(queueName, content); 123 | } 124 | } 125 | /** 126 | * 订阅队列 127 | * @param queueName 128 | * @param consume 129 | * @param payload 130 | */ 131 | subscribeQueue(queueName: Q, consume: Consume, payload?: any) { 132 | this.queueCollectionHandle.subscribeQueue(queueName, consume, payload); 133 | } 134 | /** 135 | * 取消订阅队列 136 | */ 137 | unsubscribeQueue(queueName: Q, consume?: Consume) { 138 | this.queueCollectionHandle.unsubscribeQueue(queueName, consume); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/plugins/wx-logs/index.ts: -------------------------------------------------------------------------------- 1 | import { LevelOutputOption, defaultOption, LOG_LEVEL, OUTPUT_TYPE } from "./config"; 2 | import { getUUID } from "@/utils/tools"; 3 | import { onListener } from "./listener"; 4 | import UNodeMQ, { Exchange, Queue } from "@/index"; 5 | import { wxApi } from "./proxyApi"; 6 | 7 | function proxyConsle(this: WxLogsPlugin, content: Message) { 8 | console.log("----consle 日志---"); 9 | console[content.type](content); 10 | } 11 | 12 | function proxyRealtime(this: WxLogsPlugin, content: Message) { 13 | console.log("----realtime 日志---"); 14 | this.addRealtimeLog(content.type, content); 15 | } 16 | 17 | function proxyRequest(this: WxLogsPlugin, content: Message) { 18 | console.log("----request 日志---"); 19 | console.log(content); 20 | } 21 | 22 | const ProxyList = { 23 | [OUTPUT_TYPE.Console]: proxyConsle, 24 | [OUTPUT_TYPE.Realtime]: proxyRealtime, 25 | [OUTPUT_TYPE.Request]: proxyRequest, 26 | }; 27 | 28 | function getMiniprogramInfo() { 29 | return { 30 | windowInfo: wxApi("getWindowInfo")?.(), 31 | skylineInfoSync: wxApi("getSkylineInfoSync")?.(), 32 | deviceInfo: wxApi("getDeviceInfo")?.(), 33 | appBaseInfo: wxApi("getAppBaseInfo")?.(), 34 | appAuthorizeSetting: wxApi("getAppAuthorizeSetting")?.(), 35 | launchOptionsSync: wxApi("getLaunchOptionsSync")?.(), 36 | apiCategory: wxApi("getApiCategory")?.(), 37 | accountInfoSync: wxApi("getAccountInfoSync")?.(), 38 | }; 39 | } 40 | type Message = { 41 | type: LOG_LEVEL; 42 | uuid: string; 43 | content?: unknown; 44 | }; 45 | /** 46 | * 微信小程序日志监控 47 | */ 48 | export default class WxLogsPlugin { 49 | private unmq: UNodeMQ>, Record>> | null = null; 50 | /** 51 | * 实时日志 52 | * 53 | 54 | 注意事项 55 | 由于后台资源限制,“实时日志”使用规则如下: 56 | 57 | 为了定位问题方便,日志是按页面划分的,某一个页面,在一定时间内(最短为5秒,最长为页面从显示到隐藏的时间间隔)打的日志,会聚合成一条日志上报,并且在小程序管理后台上可以根据页面路径搜索出该条日志。 58 | 每个小程序账号,We分析基础版每天限制5000条日志,We分析专业版为50000条,且支持购买配置升级或购买额外的上报扩充包。日志根据版本配置,会保留7天/14天/30天不等,建议遇到问题及时定位。 59 | 一条日志的上限是5KB,最多包含200次打印日志函数调用(info、warn、error调用都算),所以要谨慎打日志,避免在循环里面调用打日志接口,避免直接重写console.log的方式打日志。 60 | 意见反馈里面的日志,可根据OpenID搜索日志。 61 | setFilterMsg和addFilterMsg 可设置类似日志tag的过滤字段。如需添加多个关键字,建议使用addFilterMsg。例如addFilterMsg('scene1'), addFilterMsg('scene2'),addFilterMsg('scene3'),设置后在小程序管理后台可随机组合三个关键字进行检索,如:“scene1 scene2 scene3”、“scene1 scene2”、 “scene1 scene3” 或 “scene2”等(以空格分隔,故addFilterMsg不能带空格)。以上几种检索方法均可检索到该条日志,检索条件越多越精准。 62 | 目前为了方便做日志分析,插件端实时日志只支持 key-value 格式。 63 | 实时日志目前只支持在手机端测试。工具端的接口可以调用,但不会上报到后台。 64 | 开发版、体验版的实时日志,不计入相关quota,即无使用上限。 65 | 66 | */ 67 | private readonly realtimeLog = wxApi("getRealtimeLogManager")?.(); 68 | /** 69 | * 初始化获取当前系统信息 70 | */ 71 | private readonly systemInfo = getMiniprogramInfo(); 72 | /** 73 | * uuid 74 | */ 75 | private readonly uuid = getUUID(); 76 | /** 77 | * 78 | * @param option 79 | */ 80 | constructor(private readonly option: LevelOutputOption = defaultOption) {} 81 | /** 82 | * 83 | * @param unmq 84 | */ 85 | install(unmq: UNodeMQ>, Record>>) { 86 | // 87 | /** 88 | * 初始化交换机 89 | */ 90 | Object.entries(LOG_LEVEL).forEach(([, value]) => { 91 | unmq.addExchage(value, new Exchange()); 92 | }); 93 | 94 | /** 95 | * 初始化队列 96 | */ 97 | Object.entries(OUTPUT_TYPE).forEach(([, value]) => { 98 | unmq.addQueue(value, new Queue()); 99 | }); 100 | 101 | /** 102 | * 设置交换机路由 103 | */ 104 | Object.entries(this.option).forEach(item => { 105 | const e = unmq.getExchange(item[0] as LOG_LEVEL); 106 | if (e === null) return e; 107 | e.setRoutes(item[1]); 108 | }); 109 | 110 | /** 111 | * 添加默认的监听器 112 | */ 113 | Object.values(OUTPUT_TYPE).forEach(item => { 114 | unmq.on(item, ProxyList[item].bind(this)); 115 | }); 116 | 117 | this.unmq = unmq; 118 | 119 | onListener.apply(this); 120 | 121 | this[LOG_LEVEL.Warn](this.systemInfo); 122 | } 123 | /** 124 | * 添加日志到实时日志 125 | * @param logType 126 | * @param msg 127 | */ 128 | addRealtimeLog(logType: LOG_LEVEL, msg: Message) { 129 | this.realtimeLog?.[logType](msg); 130 | } 131 | 132 | [LOG_LEVEL.Info](content: unknown) { 133 | if (this.unmq === null) return false; 134 | this.unmq.emit(LOG_LEVEL.Info, { 135 | type: LOG_LEVEL.Info, 136 | uuid: this.uuid, 137 | content, 138 | }); 139 | return true; 140 | } 141 | [LOG_LEVEL.Warn](content: unknown) { 142 | if (this.unmq === null) return false; 143 | this.unmq.emit(LOG_LEVEL.Warn, { 144 | type: LOG_LEVEL.Warn, 145 | uuid: this.uuid, 146 | content, 147 | }); 148 | return true; 149 | } 150 | [LOG_LEVEL.Error](content: unknown) { 151 | if (this.unmq === null) return false; 152 | this.unmq.emit(LOG_LEVEL.Error, { 153 | type: LOG_LEVEL.Error, 154 | uuid: this.uuid, 155 | content, 156 | }); 157 | return true; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /test/SingleUNodeMQ.test.ts: -------------------------------------------------------------------------------- 1 | import { Queue, SingleUNodeMQ, Logs, createSingleUnmq } from "../src/index"; 2 | import { expect, test, describe } from "@jest/globals"; 3 | import { promiseSetTimeout } from "../src/utils/tools"; 4 | Logs.setLogsConfig({ logs: false }); 5 | describe("QuickUNodeMQ", () => { 6 | /** 7 | * 创建测试list的方法 8 | * @returns 9 | */ 10 | function createListEqual() { 11 | const list: T[] = []; 12 | return { 13 | list, 14 | equal(list: T[], time = 0, callback?: () => void) { 15 | setTimeout(() => { 16 | expect(this.list).toEqual(list); 17 | if (callback) callback(); 18 | }, time); 19 | }, 20 | equalDisorder() { 21 | //TODO: 22 | }, 23 | }; 24 | } 25 | 26 | test("SingleUNodeMQ.emit", function (done) { 27 | type T = number; 28 | const singleUnmq = new SingleUNodeMQ(new Queue()); 29 | 30 | singleUnmq.emit(1); 31 | singleUnmq.emit(2, 3, 4, 5); 32 | 33 | const listEqual = createListEqual(); 34 | singleUnmq.on((res: T) => listEqual.list.push(res)); 35 | listEqual.equal([1, 2, 3, 4, 5], 0, done); 36 | }); 37 | test("SingleUNodeMQ.on", function (done) { 38 | expect.assertions(10); 39 | 40 | type T = number; 41 | const singleUnmq1 = new SingleUNodeMQ(new Queue()); 42 | 43 | singleUnmq1.emit(1, 2, 3, 4, 5); 44 | 45 | singleUnmq1.on((res: T, payload: any) => { 46 | expect([1, 2, 3, 4, 5].indexOf(res) !== -1).toBe(true); 47 | expect(payload).toEqual("payload"); 48 | }, "payload"); 49 | 50 | setTimeout(done); 51 | }); 52 | test("SingleUNodeMQ.once", async function () { 53 | expect.assertions(5); 54 | 55 | type T = number; 56 | const singleUnmq1 = new SingleUNodeMQ(new Queue()); 57 | 58 | singleUnmq1.emit(1, 2, 3); 59 | 60 | const res1 = await singleUnmq1.once(); 61 | expect(res1).toEqual(1); 62 | /** 63 | 64 | 65 | 当队列设置为同步消费的时候 66 | 67 | 由于消费者在消费一条消息以后,队列并不会立马收到回调,所以会在下次事件循环将队列的消费方法放开,所以会积累一次事件循环的消费者进入队列 68 | 69 | 因此下次两次once方法实际上是订阅的同一条消息 70 | 71 | 72 | */ 73 | 74 | /** 75 | 76 | 77 | jest内部使用try catch捕获不到异步情况下的断言错误,所以异步代码即使断言错误报错了,只会阻塞下面的代码执行,不会被jest捕获到 78 | 79 | 因此这里需要将下面的once方法手动封装成promise方法 80 | 81 | 82 | ~~~typescript 83 | 84 | quickUnmq1 85 | .once("qu1", (res2: T) => { 86 | expect(res2).toEqual(2); 87 | quickUnmq1.once("qu1", (res3: T) => { 88 | expect(res3).toEqual(4); 89 | done(); 90 | }); 91 | }) 92 | .once( 93 | "qu1", 94 | (res2: T, payload: any) => { 95 | expect(res2).toEqual(2); 96 | expect(payload).toEqual("payload"); 97 | }, 98 | "payload", 99 | ); 100 | ~~~ 101 | 102 | */ 103 | 104 | const [res2_1, res2_2] = await Promise.all([ 105 | new Promise(res => { 106 | singleUnmq1.once((res2: T) => { 107 | res(res2); 108 | }); 109 | }), 110 | new Promise(res => { 111 | singleUnmq1.once((res2: T, payload) => { 112 | res({ res2, payload }); 113 | }, "payload"); 114 | }), 115 | ]); 116 | 117 | expect(res2_1).toEqual(2); 118 | expect(res2_2.res2).toEqual(2); 119 | expect(res2_2.payload).toEqual("payload"); 120 | 121 | const res3 = await new Promise(res => { 122 | singleUnmq1.once((res3: T) => { 123 | res(res3); 124 | }); 125 | }); 126 | 127 | expect(res3).toEqual(3); 128 | 129 | // 130 | }); 131 | 132 | test("SingleUNodeMQ.off", async function () { 133 | expect.assertions(2); 134 | 135 | type T = number; 136 | const singleUnmq1 = new SingleUNodeMQ(new Queue({ async: false })); 137 | const singleUnmq2 = new SingleUNodeMQ(new Queue({ async: true })); 138 | 139 | singleUnmq1.emit(1, 2); 140 | const res = await new Promise(res => { 141 | singleUnmq1.on((data: T) => { 142 | res(data); 143 | singleUnmq1.off(); 144 | }); 145 | }); 146 | expect(res).toEqual(1); 147 | 148 | singleUnmq2.emit(3, 4, 5); 149 | await promiseSetTimeout(0); 150 | const res_a = await new Promise(res => { 151 | const a: any[] = []; 152 | singleUnmq2.on((data: T) => { 153 | a.push(data); 154 | })(); 155 | setTimeout(() => { 156 | res(a); 157 | }); 158 | }); 159 | expect(res_a).toEqual([3, 4, 5]); 160 | }); 161 | }); 162 | 163 | describe("createSingleUnmq", () => { 164 | // 165 | test("createSingleUnmq_init", function () { 166 | // 167 | 168 | expect(createSingleUnmq() instanceof SingleUNodeMQ).toBeTruthy(); 169 | expect(createSingleUnmq({}) instanceof SingleUNodeMQ).toBeTruthy(); 170 | expect(createSingleUnmq(new SingleUNodeMQ()) instanceof SingleUNodeMQ).toBeTruthy(); 171 | }); 172 | 173 | /** 174 | * 测试一个消息返回promise false重复消费 175 | */ 176 | 177 | test("createSingleUnmq_reset", function () { 178 | const t = createSingleUnmq({ ask: true, rcn: 10 }); 179 | let i = 0; 180 | t.on(async () => { 181 | await promiseSetTimeout(100); 182 | i++; 183 | return false; 184 | }); 185 | setTimeout(() => { 186 | expect(i).toEqual(10); 187 | }, 1200); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /src/plugins/storage/index.ts: -------------------------------------------------------------------------------- 1 | import StorageAdapterAbstract from "./StorageAdapterAbstract"; 2 | import UNodeMQ, { isString, isObject, Exchange, Queue, PluginInstallFunction } from "../../index"; 3 | import StorageSignAbstract from "./StorageSignAbstract"; 4 | import { devalue, envalue } from "./storageTypeof"; 5 | 6 | //TODO:实现storage存储超出警告 7 | 8 | export enum StorageType { 9 | SESSION = "session", 10 | LOCAL = "local", 11 | } 12 | 13 | type StorageConfig = { 14 | storageType?: StorageType; 15 | storageMemory?: StorageAdapterAbstract; 16 | storageSign?: StorageSignAbstract; 17 | }; 18 | 19 | class StorageMemory implements StorageAdapterAbstract { 20 | private memoryData: Record = {}; 21 | init(o: Record): void { 22 | this.memoryData = o; 23 | } 24 | getData(key: string): string { 25 | return this.memoryData[key]; 26 | } 27 | setData(key: string, value: string): void { 28 | this.memoryData[key] = value; 29 | } 30 | } 31 | 32 | export type Plugin = { 33 | install: PluginInstallFunction; 34 | }; 35 | 36 | 37 | /** 38 | * storage plugin 39 | * StorageTypeList 40 | */ 41 | export default class StoragePlugin implements Plugin { 42 | private storageMemory: StorageAdapterAbstract = new StorageMemory(); //代理访问内存 43 | private storageSign: StorageSignAbstract | null = null; //加密方法 44 | private unmq: UNodeMQ>, Record>> | null = null; 45 | storage: Record = {}; 46 | constructor( storageType: Record, storageConfig?: StorageConfig) { 47 | if (storageConfig?.storageMemory) this.storageMemory = storageConfig.storageMemory; 48 | if (storageConfig?.storageSign) this.storageSign = storageConfig.storageSign; 49 | if (storageConfig?.storageType) this.storageType = storageConfig.storageType; 50 | this.storage = storageType; 51 | //init之前先直接从缓存中取 52 | for (const name in storage) { 53 | Object.defineProperty(storage, name, { 54 | get() { 55 | return this.getStorageSync(name); 56 | }, 57 | set(value: any) { 58 | this.setStorageSync(name, value); 59 | }, 60 | }); 61 | } 62 | } 63 | init() { 64 | //内存缓存初始化之前从storage里面获取 65 | if (this.unmq === null) throw "storage plugin 未安装"; 66 | const queueNameList = this.unmq.getQueueList().map((item) => item.name); 67 | for (const name of queueNameList) { 68 | if (name === undefined) continue; 69 | this.storageMemory.setData(name, this.getStorageSync(name)); 70 | Object.defineProperty(__storage, name, { 71 | get() { 72 | return this.storageMemory.getData(name); 73 | }, 74 | set(value: any) { 75 | this.setStorageSync(name, value); 76 | this.storageMemory.setData(name, value); 77 | }, 78 | }); 79 | } 80 | } 81 | install>, QueueCollection extends Record>>( 82 | // install( 83 | unmq: UNodeMQ, 84 | ...options: any[] 85 | ) { 86 | this.unmq = unmq; 87 | type B = { 88 | [K in keyof T]: unknown; 89 | }; 90 | this.storage = {} as B; 91 | const queueNameList = unmq.getQueueList().map((item) => item.name); 92 | for (const name of queueNameList) { 93 | if (name === undefined) continue; 94 | this.storage[name as keyof B] = {}; 95 | Object.defineProperty(this.storage, name, { 96 | get() { 97 | return this.getStorageSync(name); 98 | }, 99 | set(value: any) { 100 | this.setStorageSync(name, value); 101 | }, 102 | }); 103 | } 104 | } 105 | /** 106 | * 107 | * @param name 108 | * @returns 109 | */ 110 | private getStorageSync(name: string) { 111 | let value = null; 112 | const list = { [StorageType.SESSION]: sessionStorage, [StorageType.LOCAL]: localStorage }; 113 | if (this.storageSign) { 114 | const storage = list[this.storageType].getItem(this.storageSign.encryptName(name)); 115 | if (storage) value = this.storageSign.decryptValue(storage); 116 | } else value = list[this.storageType].getItem(name); 117 | //数据类型编码解码 118 | if (value) return devalue(value); 119 | else return null; 120 | } 121 | /** 122 | * 同步设置缓存 123 | * @param key 124 | * @param type 125 | * @param value 126 | */ 127 | private setStorageSync(name: string, value: any) { 128 | if (value === null || value === undefined) return this.removeStorageSync(name); 129 | const list = { [StorageType.SESSION]: sessionStorage, [StorageType.LOCAL]: localStorage }; 130 | value = envalue(value); 131 | if (this.storageSign) 132 | list[this.storageType].setItem(this.storageSign.encryptName(name), this.storageSign.encryptValue(value)); 133 | else list[this.storageType].setItem(name, value); 134 | } 135 | /** 136 | * 137 | * @param name 138 | */ 139 | private removeStorageSync(name: string) { 140 | const list = { [StorageType.SESSION]: sessionStorage, [StorageType.LOCAL]: localStorage }; 141 | if (this.storageSign) list[this.storageType].removeItem(this.storageSign.encryptName(name)); 142 | else list[this.storageType].removeItem(name); 143 | } 144 | } 145 | 146 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | uaoie@qq.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /test/QuickUNodeMQ.test.ts: -------------------------------------------------------------------------------- 1 | import { Exchange, Queue, QuickUNodeMQ, Logs, createQuickUnmq } from "../src/index"; 2 | import { expect, test, describe } from "@jest/globals"; 3 | import { promiseSetTimeout } from "../src/utils/tools"; 4 | Logs.setLogsConfig({ logs: false }); 5 | describe("QuickUNodeMQ", () => { 6 | /** 7 | * 创建测试list的方法 8 | * @returns 9 | */ 10 | function createListEqual() { 11 | const list: T[] = []; 12 | return { 13 | list, 14 | equal(list: T[], time = 0, callback?: () => void) { 15 | setTimeout(() => { 16 | expect(this.list).toEqual(list); 17 | if (callback) callback(); 18 | }, time); 19 | }, 20 | equalDisorder() { 21 | //TODO: 22 | }, 23 | }; 24 | } 25 | 26 | test("QuickUNodeMQ.emit", function (done) { 27 | type T = number; 28 | const quickUnmq = new QuickUNodeMQ(new Exchange({ routes: ["qu1"] }), { qu1: new Queue() }); 29 | 30 | quickUnmq.emit(1); 31 | quickUnmq.emit(2, 3, 4, 5); 32 | 33 | const listEqual = createListEqual(); 34 | quickUnmq.on("qu1", (res: T) => listEqual.list.push(res)); 35 | listEqual.equal([1, 2, 3, 4, 5], 0, done); 36 | }); 37 | test("QuickUNodeMQ.emitToQueue", function (done) { 38 | type T = number; 39 | const queColl = { qu1: new Queue() }; 40 | const quickUnmq = new QuickUNodeMQ({ routes: ["qu1"] }, queColl); 41 | 42 | quickUnmq.emitToQueue("qu1", 1); 43 | quickUnmq.emitToQueue("qu1", 2, 3, 4, 5); 44 | 45 | const listEqual = createListEqual(); 46 | quickUnmq.on("qu1", (res: T) => listEqual.list.push(res)); 47 | listEqual.equal([1, 2, 3, 4, 5], 0, done); 48 | 49 | setTimeout(done); 50 | }); 51 | test("QuickUNodeMQ.on", function (done) { 52 | expect.assertions(10); 53 | 54 | type T = number; 55 | const quickUnmq1 = new QuickUNodeMQ(new Exchange({ routes: ["qu1"] }), { qu1: new Queue() }); 56 | 57 | quickUnmq1.emit(1, 2, 3, 4, 5); 58 | 59 | quickUnmq1.on( 60 | "qu1", 61 | (res: T, payload: any) => { 62 | expect([1, 2, 3, 4, 5].indexOf(res) !== -1).toBe(true); 63 | expect(payload).toEqual("payload"); 64 | }, 65 | "payload", 66 | ); 67 | 68 | setTimeout(done); 69 | }); 70 | test("QuickUNodeMQ.once", async function () { 71 | expect.assertions(5); 72 | 73 | type T = number; 74 | const quickUnmq1 = new QuickUNodeMQ(new Exchange({ routes: ["qu1"] }), { qu1: new Queue() }); 75 | 76 | quickUnmq1.emit(1, 2, 3); 77 | 78 | const res1 = await quickUnmq1.once("qu1"); 79 | expect(res1).toEqual(1); 80 | /** 81 | 82 | 83 | 当队列设置为同步消费的时候 84 | 85 | 由于消费者在消费一条消息以后,队列并不会立马收到回调,所以会在下次事件循环将队列的消费方法放开,所以会积累一次事件循环的消费者进入队列 86 | 87 | 因此下次两次once方法实际上是订阅的同一条消息 88 | 89 | 90 | */ 91 | 92 | /** 93 | 94 | 95 | jest内部使用try catch捕获不到异步情况下的断言错误,所以异步代码即使断言错误报错了,只会阻塞下面的代码执行,不会被jest捕获到 96 | 97 | 因此这里需要将下面的once方法手动封装成promise方法 98 | 99 | 100 | ~~~typescript 101 | 102 | quickUnmq1 103 | .once("qu1", (res2: T) => { 104 | expect(res2).toEqual(2); 105 | quickUnmq1.once("qu1", (res3: T) => { 106 | expect(res3).toEqual(4); 107 | done(); 108 | }); 109 | }) 110 | .once( 111 | "qu1", 112 | (res2: T, payload: any) => { 113 | expect(res2).toEqual(2); 114 | expect(payload).toEqual("payload"); 115 | }, 116 | "payload", 117 | ); 118 | ~~~ 119 | 120 | */ 121 | 122 | const [res2_1, res2_2] = await Promise.all([ 123 | new Promise(res => { 124 | quickUnmq1.once("qu1", (res2: T) => { 125 | res(res2); 126 | }); 127 | }), 128 | new Promise(res => { 129 | quickUnmq1.once( 130 | "qu1", 131 | (res2: T, payload) => { 132 | res({ res2, payload }); 133 | }, 134 | "payload", 135 | ); 136 | }), 137 | ]); 138 | 139 | expect(res2_1).toEqual(2); 140 | expect(res2_2.res2).toEqual(2); 141 | expect(res2_2.payload).toEqual("payload"); 142 | 143 | const res3 = await new Promise(res => { 144 | quickUnmq1.once("qu1", (res3: T) => { 145 | res(res3); 146 | }); 147 | }); 148 | 149 | expect(res3).toEqual(3); 150 | 151 | // 152 | }); 153 | 154 | test("QuickUNodeMQ.off", async function () { 155 | expect.assertions(2); 156 | 157 | type T = number; 158 | const quickUnmq1 = new QuickUNodeMQ(new Exchange({ routes: ["qu1"] }), { qu1: new Queue({ async: false }) }); 159 | const quickUnmq2 = new QuickUNodeMQ(new Exchange({ routes: ["qu1"] }), { qu1: new Queue({ async: true }) }); 160 | 161 | quickUnmq1.emit(1, 2); 162 | const res = await new Promise(res => { 163 | quickUnmq1.on("qu1", (data: T) => { 164 | res(data); 165 | quickUnmq1.off("qu1"); 166 | }); 167 | }); 168 | expect(res).toEqual(1); 169 | 170 | quickUnmq2.emit(3, 4, 5); 171 | await promiseSetTimeout(0); 172 | const res_a = await new Promise(res => { 173 | const a: any[] = []; 174 | quickUnmq2.on("qu1", (data: T) => { 175 | a.push(data); 176 | })(); 177 | setTimeout(() => { 178 | res(a); 179 | }); 180 | }); 181 | expect(res_a).toEqual([3, 4, 5]); 182 | }); 183 | }); 184 | 185 | describe("createQuickUnmq", () => { 186 | // 187 | test("createQuickUnmq_init", function () { 188 | // 189 | 190 | expect(createQuickUnmq({ routes: ["qu1"] }, { qu1: new Queue() }) instanceof QuickUNodeMQ).toBeTruthy(); 191 | expect(createQuickUnmq(new Exchange({ routes: ["qu1"] }), { qu1: new Queue() }) instanceof QuickUNodeMQ).toBeTruthy(); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /termui/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 8 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 9 | github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= 10 | github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= 11 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 12 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 13 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 14 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 15 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 16 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 17 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 18 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 19 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 20 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 21 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 22 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 23 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 25 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 26 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 28 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 29 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 30 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 31 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 33 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 34 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 35 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 36 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 37 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 38 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 39 | github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= 40 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 41 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 42 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 43 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 44 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 45 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 46 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 47 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= 48 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= 49 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 50 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 51 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 55 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 56 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 59 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 62 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 63 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 64 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 65 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 66 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 67 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 68 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 69 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 70 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 74 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 76 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 78 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 79 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 80 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 81 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 83 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 84 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 88 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 89 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 90 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 91 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 92 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 93 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 94 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 95 | -------------------------------------------------------------------------------- /src/internal/Queue/index.ts: -------------------------------------------------------------------------------- 1 | import { News, Consumer } from "../.."; 2 | import { Consume } from "../Consumer"; 3 | import { random } from "../../utils/tools"; 4 | import { queueLogsOperator } from "../Logs"; 5 | import { Operator, isAsyncOperator, isSyncOperator } from "./operators"; 6 | 7 | export interface QueueOption { 8 | ask?: boolean; 9 | rcn?: number; 10 | mode?: ConsumMode; 11 | name?: string; 12 | async?: boolean; 13 | maxTime?: number; 14 | operators?: Operator[]; 15 | [k: string]: unknown; 16 | } 17 | /** 18 | * 队列消费类型 19 | * Random:随机选择一个消费者消费; 20 | * All:所有消费者都消费消息; 21 | */ 22 | export enum ConsumMode { 23 | "Random" = "Random", 24 | "All" = "All", 25 | } 26 | 27 | /** 28 | * 队列,理论上一个队列的数据格式应该具有一致性 29 | * 30 | */ 31 | export default class Queue { 32 | [k: string]: unknown; 33 | name?: string; 34 | /** 35 | * 创建时间戳 36 | */ 37 | readonly createdTime: number; 38 | /** 39 | * id 40 | */ 41 | private readonly id: string = random(); 42 | getId() { 43 | return this.id; 44 | } 45 | /** 46 | * 是否需要消息确认 47 | */ 48 | ask = false; 49 | /** 50 | * 可重新消费次数,消费失败会重复消费 51 | */ 52 | rcn = 3; 53 | /** 54 | * 消费模式 55 | * - Random 随机抽取消费者消费 56 | * - All 一条消息所有消费者都消费 57 | */ 58 | mode: ConsumMode = ConsumMode.All; 59 | /** 60 | * 是否是异步消费,默认false是同步消费 61 | * 62 | * 如果是同步消费,则一条消息消费完成或者消费失败才会消费下一条消息 63 | * 为同步消费时,mode为ALL则需要所有消费者都消费完或者消费失败才能消费下一条消息 64 | * 65 | * 如果是异步消费,则会在同一时间内去消费所有消息 66 | */ 67 | async = false; 68 | /** 69 | * 消费状态,true为正在消费,false为未在消费 70 | * 用于同步消费判断当前的消费状态 71 | */ 72 | private state = false; 73 | /** 74 | * 每个消费者最长消费时长 75 | * 同步阻塞型代码计算所花时长不计算入内,仅仅控制异步消费者消费代码所花时长 76 | * 设置为-1表示无时长限制 77 | * 在ask为true和async为false的情况下设置maxTime为-1可能会导致队列将被阻塞 78 | */ 79 | maxTime = 3000; 80 | /** 81 | * 消息 list 82 | */ 83 | private news: News[] = []; 84 | getNews() { 85 | return this.news; 86 | } 87 | 88 | /** 89 | * 移除所有消息 90 | * @returns 91 | */ 92 | removeAllNews() { 93 | this.news = []; 94 | return true; 95 | } 96 | 97 | /** 98 | * 加入消息 99 | * @param news 100 | */ 101 | pushNews(news: News) { 102 | if (news.consumedTimes === -1) news.consumedTimes = this.rcn; 103 | 104 | if (news.consumedTimes > 0) { 105 | //过滤重复的消息id 106 | if (this.news.findIndex(item => item.getId() === news.getId()) === -1) { 107 | this.operate("beforeAddNews", news).then(isOk => { 108 | if (!isOk) return; 109 | this.news.push(news); 110 | this.operate("addedNews", news); 111 | if (this.news.length > 0 && this.consumerList.length > 0) this.consumeNews(); 112 | }); 113 | } 114 | } 115 | } 116 | /** 117 | * 弹出一条消息 118 | * @returns 119 | * 120 | */ 121 | async eject(start = 0): Promise | null> { 122 | if (this.news.length > 0) { 123 | const news = this.news.splice(start, 1)[0]; 124 | if (!(await this.operate("ejectNews", news))) return null; 125 | 126 | return news; 127 | } else return null; 128 | } 129 | /** 130 | * 加入消息内容 131 | * @param content 132 | */ 133 | pushContent(content: D) { 134 | const news = new News(content); 135 | this.pushNews(news); 136 | } 137 | /** 138 | * 通过id移除指定消息 139 | * @param newsId 140 | * @returns 141 | */ 142 | removeNewsById(newsId: string) { 143 | const index = this.news.findIndex(item => item.getId() === newsId); 144 | if (index === -1) return false; 145 | this.eject(index); 146 | return true; 147 | } 148 | 149 | /** 150 | * 消费者 list 151 | */ 152 | private consumerList: Consumer[] = []; 153 | getConsumerList() { 154 | return this.consumerList; 155 | } 156 | /** 157 | * 加入消费者 158 | * @param consumerList 159 | */ 160 | pushConsumer(consumer: Consumer) { 161 | //过滤重复的消费者id 162 | if (this.consumerList.findIndex(item => item.getId() === consumer.getId()) === -1) { 163 | //TODO:暂不能限制开发者绑定消费者,此钩子函数会对同步once方法产生影响 164 | // this.operate("beforeAddConsumer", consumer).then((isOk) => { 165 | // if (!isOk) return; 166 | // this.consumerList.push(consumer); 167 | // if (this.news.length > 0 && this.consumerList.length > 0) this.consumeNews(); 168 | // this.operate("addedConsumer", consumer); 169 | // }); 170 | this.consumerList.push(consumer); 171 | this.operate("addedConsumer", consumer); 172 | if (this.news.length > 0 && this.consumerList.length > 0) this.consumeNews(); 173 | } 174 | } 175 | /** 176 | * 177 | * 通过消费方法移除指定消费者 178 | * @param consume 179 | * @returns 180 | */ 181 | removeConsumer(consume: Consume) { 182 | const index = this.consumerList.findIndex(item => item.consume === consume); 183 | if (index === -1) return false; 184 | const consumerList = this.consumerList.splice(index, 1); 185 | this.operate("removedConsumer", consumerList); 186 | return true; 187 | } 188 | /** 189 | * 加入消费者消费主体 190 | * 191 | * @param consume 192 | * @param payload 消费载体,每次消费都会传入给消费者 193 | */ 194 | pushConsume(consume: Consume, payload?: unknown) { 195 | const consumer = new Consumer(consume, payload); 196 | this.pushConsumer(consumer); 197 | } 198 | 199 | /** 200 | * 通过id移除指定消费者 201 | * @param consumerId 202 | * @returns 203 | */ 204 | removeConsumerById(consumerId: string) { 205 | const index = this.consumerList.findIndex(item => item.getId() === consumerId); 206 | if (index === -1) return false; 207 | const consumerList = this.consumerList.splice(index, 1); 208 | this.operate("removedConsumer", consumerList); 209 | return true; 210 | } 211 | /** 212 | * 移除所有消费者 213 | * @returns 214 | */ 215 | removeAllConsumer() { 216 | const consumerList = this.consumerList.splice(0); 217 | this.operate("removedConsumer", consumerList); 218 | return true; 219 | } 220 | /** 221 | * 操作符集合 222 | */ 223 | private operators: Operator[] = []; 224 | 225 | /** 226 | * 添加钩子函数方法 227 | * @param operators 228 | * @returns 229 | */ 230 | add(...operators: Operator[]) { 231 | operators.forEach(operator => { 232 | this.operators.push(operator ?? {}); 233 | if (operator?.mounted) operator.mounted(this); 234 | }); 235 | return this; 236 | } 237 | /** 238 | * 管道操作符执行 239 | * 其中需要阻塞获取返回值(boolea)的是按顺序执行的 240 | * @param fun 241 | * @param args 242 | * @returns 243 | */ 244 | private async operate(fun: keyof Operator, ...args: any[]) { 245 | //先过滤数据 246 | const list = this.operators 247 | .filter(operator => operator[fun]) 248 | // 249 | .map(operator => operator[fun]); 250 | if (isAsyncOperator(fun)) { 251 | for (const iterator of list) { 252 | //异步处理 253 | iterator?.(args[0]); 254 | } 255 | } else if (isSyncOperator(fun)) { 256 | //同步处理 257 | for (const iterator of list) { 258 | if (!(await iterator?.(args[0]))) return false; 259 | } 260 | } 261 | //类型异常 262 | else throw "operate error"; 263 | 264 | return true; 265 | } 266 | constructor(option?: QueueOption) { 267 | Object.assign(this, option); 268 | this.createdTime = new Date().getTime(); 269 | this.add(queueLogsOperator()); 270 | } 271 | 272 | /** 273 | * 消费方法 274 | * 每次执行消费一条消息 275 | * @returns 276 | */ 277 | consumeNews() { 278 | if (this.news.length === 0) return; 279 | if (this.consumerList.length === 0) return; 280 | if (!this.async && this.state) return; 281 | 282 | //先把状态设置为消费中 283 | this.state = true; 284 | 285 | const consumerList = 286 | this.mode === ConsumMode.Random ? [this.consumerList[Math.round(Math.random() * (this.consumerList.length - 1))]] : [...this.consumerList]; 287 | 288 | this.eject().then(news => { 289 | if (news === null) { 290 | this.state = false; 291 | this.consumeNews(); 292 | return; 293 | } 294 | 295 | Promise.all(consumerList.map(consumer => this.consumption(news, consumer))) 296 | .then(() => { 297 | //消息被成功消费 298 | }) 299 | .catch(() => { 300 | //消费失败 301 | news.consumedTimes--; 302 | this.pushNews(news); 303 | }) 304 | .finally(() => { 305 | this.state = false; 306 | this.consumeNews(); 307 | }); 308 | }); 309 | 310 | //如果是异步的就需要执行,因为此时消息已被弹出 311 | if (this.async) this.consumeNews(); 312 | } 313 | /** 314 | * 指定消费者消费某一条消息的方法 315 | * @param news 316 | * @param consumer 317 | * @returns 318 | */ 319 | consumption(news: News, consumer: Consumer): Promise { 320 | return new Promise((resolve, reject) => { 321 | const maxTime = this.maxTime; 322 | const id = 323 | maxTime >= 0 324 | ? setTimeout(() => { 325 | // Logs.log(`队列 消费超时`); 326 | reject(false); 327 | }, maxTime) 328 | : undefined; 329 | 330 | consumer.consumption(news, this.ask).then((isOk: boolean) => { 331 | if (isOk) { 332 | resolve(isOk); 333 | } else { 334 | reject(isOk); 335 | } 336 | if (maxTime >= 0) clearTimeout(id); 337 | }); 338 | }); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/plugins/iframe/index.ts: -------------------------------------------------------------------------------- 1 | import UNodeMQ, { Exchange, Queue, createSingleUnmq } from "../../index"; 2 | import { isObject, isString } from "../../utils/tools"; 3 | import resizeObserver from "../../operators/resizeObserver/index"; 4 | type IframeOption = { 5 | autoSize?: boolean; //是否自动大小 6 | arg?: string | HTMLElement; //当前iframe自动大小的节点元素id或者元素dom,默认为html元素 7 | }; 8 | const IFRAEE_MASK = "u-node-mq-plugin"; 9 | 10 | /** 11 | * 使用postMessage进行iframe跨域通信 12 | */ 13 | export enum MessageType { 14 | /** 15 | * 普通消息 16 | */ 17 | GeneralMessage, 18 | 19 | /** 20 | * 广播查找交换机消息 21 | */ 22 | FindExchangeMessage, 23 | 24 | /** 25 | * 发送exchange坐标消息 26 | */ 27 | SendCoordinateMessage, 28 | 29 | /** 30 | * 上线通知消息 31 | */ 32 | OnlineNotificationMessage, 33 | 34 | /** 35 | * 同步容器大小消息 36 | */ 37 | ResizeObserverMessage, 38 | } 39 | /** 40 | * 直发消息队列,即主动发送 41 | * @param queueName 42 | * @returns 43 | */ 44 | export const getInternalIframeMessageQueueName = (queueName: string) => queueName + "_Iframe_Message"; 45 | /** 46 | * 用来广播获取地址的消息 47 | * @param queueName 48 | * @returns 49 | */ 50 | export const getInternalIframeBroadcasMessageQueueName = (queueName: string) => queueName + "_Iframe_Wait_Message"; 51 | 52 | export default class IframePlugin { 53 | private unmq: UNodeMQ>, Record>> | null = null; 54 | constructor(private readonly name: string, option?: IframeOption) { 55 | if (option?.autoSize) { 56 | const resizeObserverInstance = createSingleUnmq().add(resizeObserver(option.arg)); 57 | resizeObserverInstance.on(res => { 58 | const selfFrame = getSelfIframeDoc(); 59 | if (selfFrame === undefined) throw "selfFrame is undefined"; 60 | // 61 | if (selfFrame.y === 0) return; 62 | if (selfFrame.window.parent === window) return; 63 | 64 | //隐藏iframe滚动条,或者在iframe元素上设置scrolling 为 no 65 | document.body.style.overflow = "hidden"; 66 | 67 | //Failed to execute 'postMessage' on 'Window': ResizeObserverEntry object could not be cloned 68 | this.postMessage(selfFrame.window.parent, MessageType.ResizeObserverMessage, { 69 | width: res.contentRect.width, 70 | height: res.contentRect.height, 71 | }); 72 | }); 73 | } 74 | } 75 | install(unmq: UNodeMQ>, Record>>) { 76 | const selfExchange = unmq.getExchange(this.name); 77 | if (!selfExchange) { 78 | throw `${this.name}交换机不存在`; 79 | } 80 | this.unmq = unmq; 81 | const list = unmq.getExchangeList(); 82 | const otherIframe = list.filter(item => item.name !== this.name); 83 | 84 | for (const iframe of otherIframe) { 85 | if (iframe.name === undefined) throw `系统错误`; 86 | 87 | const internalIframeMessageQueueName = getInternalIframeMessageQueueName(iframe.name); 88 | const internalIframeBroadcasMessageQueueName = getInternalIframeBroadcasMessageQueueName(iframe.name); 89 | 90 | iframe.setRepeater(() => [internalIframeMessageQueueName, internalIframeBroadcasMessageQueueName]); 91 | //用于存储消息的队列 92 | unmq.addQueue(internalIframeMessageQueueName, new Queue({ async: true })); 93 | //用来广播获取地址的消息 94 | unmq.addQueue(internalIframeBroadcasMessageQueueName, new Queue({ async: true })); 95 | 96 | //为广播消息挂载消费方法 97 | unmq.on(internalIframeBroadcasMessageQueueName, () => { 98 | //广播查找交换机地址消息 99 | this.broadcastMessage(MessageType.FindExchangeMessage, { 100 | exchangeName: iframe.name, 101 | msg: `who is ${iframe.name} ?`, 102 | }); 103 | }); 104 | } 105 | 106 | //广播发送上线通知 107 | this.broadcastMessage(MessageType.OnlineNotificationMessage, { msg: `${this.name} is online` }); 108 | 109 | //监听unmq的消息 110 | window.addEventListener("message", this.receiveMessage.bind(this), false); 111 | } 112 | /** 113 | * 接收消息 114 | * @param param0 115 | * @returns 116 | */ 117 | private receiveMessage({ source, data, origin }: MessageEvent) { 118 | if (this.unmq === null) throw `${this.name} iframe 未安装`; 119 | if (!isObject(data)) return false; 120 | const { mask, type, message, fromName } = data; 121 | 122 | if (mask !== IFRAEE_MASK) return false; 123 | 124 | if (source === null || source === undefined) return false; 125 | 126 | /** 127 | * 发送者是否存在 128 | * 仅接收初始化创建Exchange的iframe的消息 129 | */ 130 | const fromIframe = this.unmq.getExchange(fromName); 131 | if (fromIframe === null) return false; 132 | 133 | /** 134 | * 判断真实的origin 是否是我想要的 origin 135 | */ 136 | if (isString(fromIframe.origin) && fromIframe.origin !== origin) return false; 137 | 138 | //接收到有人上线 或者 有人发送坐标过来 139 | if ([MessageType.OnlineNotificationMessage, MessageType.SendCoordinateMessage].indexOf(type) !== -1) { 140 | const off = this.unmq.on(getInternalIframeMessageQueueName(fromName), data => { 141 | this.postMessage(source as Window, MessageType.GeneralMessage, data, origin); 142 | }); 143 | setTimeout(off); 144 | } 145 | 146 | //普通消息 147 | else if (type === MessageType.GeneralMessage) { 148 | this.unmq.emit(this.name, message); 149 | } 150 | 151 | //查找交换机消息 152 | else if (type === MessageType.FindExchangeMessage && message.exchangeName === this.name) { 153 | this.postMessage(source as Window, MessageType.SendCoordinateMessage, { msg: `my name is ${this.name}` }, origin); 154 | } 155 | 156 | //同步容器大小消息 157 | else if (type === MessageType.ResizeObserverMessage) { 158 | let index = 0; 159 | for (let i = 0; i < window.frames.length; i++) { 160 | if (source === window.frames) { 161 | index = i; 162 | break; 163 | } 164 | } 165 | const dos = document.getElementsByTagName("iframe"); 166 | if (dos.length - 1 < index) throw "dom节点与iframe数量不匹配"; 167 | dos[index].width = message.width; 168 | dos[index].height = message.height; 169 | // dos[index].scrolling = "no"; 170 | } 171 | 172 | // 173 | else return false; 174 | 175 | return true; 176 | } 177 | 178 | /** 179 | *发送消息 180 | * @param currentWindow 181 | * @param type 182 | * @param message 183 | * @param origin 184 | * @param transfer 185 | */ 186 | private postMessage(currentWindow: Window, type: MessageType, message: any, origin = "*", transfer?: Transferable[]) { 187 | currentWindow.postMessage( 188 | { 189 | mask: IFRAEE_MASK, 190 | type, 191 | message, 192 | fromName: this.name, 193 | }, 194 | origin, 195 | transfer, 196 | ); 197 | } 198 | /** 199 | *广播消息 200 | * @param type 201 | * @param message 202 | */ 203 | private broadcastMessage(type: MessageType, message: any) { 204 | const list = getOtherAllIframeDoc(); 205 | list.forEach(item => { 206 | this.postMessage(item.window, type, message, "*"); 207 | }); 208 | } 209 | } 210 | /** 211 | * 饼平化方便取值 212 | * 但是需要标记每个window在多维数组中的位置 213 | * window为第一层,记为0,0 214 | * window下第一个iframe记为0,1 215 | * y 是深度 216 | * 217 | * x 是索引 218 | * 219 | * 只要有一个window产生就全局核对坐标点 220 | * 只要有一个IframeMessage被创建则代表当前创建了一个window 221 | * 222 | * 223 | */ 224 | 225 | /** 226 | * 获取其他所有Iframe doc 227 | * @returns 228 | */ 229 | function getOtherAllIframeDoc(): T[] { 230 | if (window.top === null) throw "window.top is null"; 231 | const list = getAllIframeDoc(); 232 | return list.filter(item => item.window !== window.self); 233 | } 234 | /** 235 | * 获取自己的iframe doc 236 | * @returns 237 | */ 238 | function getSelfIframeDoc(): T | undefined { 239 | if (window.top === null) throw "window.top is null"; 240 | const list = getAllIframeDoc(); 241 | return list.find(item => item.window === window.self); 242 | } 243 | type T = { 244 | window: Window; 245 | x: number; 246 | y: number; 247 | }; 248 | /** 249 | * 获取所有node doc 250 | * @returns 251 | * https://www.typescriptlang.org/play?#code/C4TwDgpgBAKlC8UDeBYAUFTUDuAuKAdgK4C2ARhAE4Dc6WUAHvseVbRliM6RTegL7sAZkQIBjYAEsA9gSgBzCMACCAG1UBJIZQCGJCABFpYgBR4oOgiACUyOljGyAzsCirJL7q0oBtALoIUP72mKpKjIEADCFu4SBR6DEi4lKyCkoGEBBgAGK6+mb4liAANIxevGVchDxUtqgc9JgA9M1QjgRO0mEAdKrS8iYARAC0Y+MTE0PWMfSt7c7dEH0DJjazWPMdXb39g+4uPiB+M41NkkJQJgfARwHwD1CiACYQQpIEEM+2N3eBDBtMBBVE5oAxAgBqCG-Y6AhadJYrfYeYCnJoOZyuHSUSj4GD+QL+djozDYyg9MBEJwACxMDRJTWwJTh9AYzLODNKcP41mJJPiEMQAEY+eihNJKFcwq4ANZRahQOUAHhwfRRfQgBHkwGpCplUPqLKwZIpVNpPQtimAmWyeT0EDMasOMr8ZTZUBsvKNmHBgqgIu5cMoSiIlDkZNFUH4MWDwFDcitNty+QdTMYVTRmza20Re2uKNO0bQ6G2WJxgStak02ntRlMMXpJMkz3wkXZDJu+B83rsHIZWGb+CF7f76M7QR7DMbo5nUEHUAAzCPZ6Px8E+yuo2Vp5umy2oAAWZe7scort+Sckosn12JDdNfjHqeXzDzgBMT5na5fTR3J-o84AKyfv+bhnkEF73rO167pB0FwnBWA8neOa7KsZLWEAA 252 | */ 253 | function getAllIframeDoc(): T[] { 254 | const list: number[] = []; 255 | const x = 0; 256 | const y = 0; 257 | 258 | function getDeepFrame(w: Window, x: number, y: number) { 259 | if (list[y] === undefined) list[y] = x; 260 | else x = ++list[y]; 261 | 262 | const arr: T[] = []; 263 | arr.push({ 264 | window: w, 265 | x, 266 | y, 267 | }); 268 | y += 1; 269 | //window === window.frames 270 | for (let k = 0; k < w.frames.length; k++) { 271 | arr.push(...getDeepFrame(w[k], x, y)); 272 | x += 1; 273 | } 274 | return arr; 275 | } 276 | 277 | if (window.top === null) throw "window.top is null"; 278 | return getDeepFrame(window.top, x, y); 279 | } 280 | -------------------------------------------------------------------------------- /test/UNodeMQ.test.ts: -------------------------------------------------------------------------------- 1 | import UNodeMQ, { Exchange, Queue, ConsumMode, Logs } from "../src/index"; 2 | import { expect, test } from "@jest/globals"; 3 | import { promiseSetTimeout } from "../src/utils/tools"; 4 | Logs.setLogsConfig({ logs: false }); 5 | 6 | test("先挂载消费者,再发送消息", function (done) { 7 | const unmq = new UNodeMQ( 8 | { 9 | ex1: new Exchange({ routes: ["qu1"] }), 10 | }, 11 | { 12 | qu1: new Queue(), 13 | }, 14 | ); 15 | unmq.on("qu1", (res: any) => { 16 | expect(res).toBe("test"); 17 | done(); 18 | }); 19 | unmq.emit("ex1", "test"); 20 | }); 21 | 22 | test("先发送消息,再挂载消费者", function (done) { 23 | const unmq = new UNodeMQ( 24 | { 25 | ex1: new Exchange({ routes: ["qu1"] }), 26 | }, 27 | { 28 | qu1: new Queue(), 29 | }, 30 | ); 31 | unmq.emit("ex1", "test"); 32 | unmq.on("qu1", (res: any) => { 33 | expect(res).toBe("test"); 34 | done(); 35 | }); 36 | }); 37 | 38 | test("随机消费", function (done) { 39 | const unmq = new UNodeMQ( 40 | { 41 | ex1: new Exchange({ routes: ["qu1"] }), 42 | }, 43 | { 44 | qu1: new Queue({ mode: ConsumMode.Random }), 45 | }, 46 | ); 47 | unmq.emit("ex1", "test"); 48 | unmq.on("qu1", () => done()); 49 | unmq.on("qu1", () => done()); 50 | }); 51 | 52 | test("全部消费", function (done) { 53 | const unmq = new UNodeMQ( 54 | { 55 | ex1: new Exchange({ routes: ["qu1"] }), 56 | }, 57 | { 58 | qu1: new Queue({ mode: ConsumMode.All }), 59 | }, 60 | ); 61 | let num = 0; 62 | unmq.emit("ex1", "test"); 63 | unmq.on("qu1", () => { 64 | num++; 65 | }); 66 | unmq.on("qu1", () => { 67 | num++; 68 | }); 69 | setTimeout(() => { 70 | expect(num).toEqual(2); 71 | done(); 72 | }); 73 | }); 74 | 75 | test("routes分发到多个队列", function (done) { 76 | const unmq = new UNodeMQ( 77 | { 78 | ex1: new Exchange({ routes: ["qu1", "qu2"] }), 79 | }, 80 | { 81 | qu1: new Queue({ mode: ConsumMode.All }), 82 | qu2: new Queue({ mode: ConsumMode.All }), 83 | }, 84 | ); 85 | unmq.emit("ex1", "test"); 86 | let num = 0; 87 | unmq.on("qu1", () => { 88 | num++; 89 | }); 90 | unmq.on("qu2", () => { 91 | num++; 92 | }); 93 | setTimeout(() => { 94 | expect(num).toEqual(2); 95 | done(); 96 | }); 97 | }); 98 | 99 | test("检查名称是否自动填充", function (done) { 100 | const unmq = new UNodeMQ( 101 | { 102 | ex1: new Exchange(), 103 | }, 104 | { 105 | qu1: new Queue(), 106 | }, 107 | ); 108 | expect(unmq.getExchange("ex1")?.name).toEqual("ex1"); 109 | expect(unmq.getQueue("qu1")?.name).toBe("qu1"); 110 | done(); 111 | }); 112 | 113 | test("有且仅消费一条消息,emit异步,off同步", function (done) { 114 | const unmq = new UNodeMQ( 115 | { 116 | ex1: new Exchange({ routes: ["qu1"] }), 117 | }, 118 | { 119 | qu1: new Queue(), 120 | }, 121 | ); 122 | let num = 0; 123 | unmq.on("qu1", () => { 124 | num++; 125 | })(); 126 | unmq.emit("ex1", 1, 2, 3); 127 | unmq.on("qu1", () => { 128 | num++; 129 | })(); 130 | setTimeout(() => { 131 | unmq.on("qu1", () => { 132 | num++; 133 | done(); 134 | })(); 135 | }, 500); 136 | setTimeout(() => { 137 | expect(num).toEqual(1); 138 | done(); 139 | }, 1000); 140 | }); 141 | 142 | test("执行一次任务队列里面所有数据", function (done) { 143 | const unmq = new UNodeMQ( 144 | { 145 | ex1: new Exchange({ routes: ["qu1"] }), 146 | }, 147 | { 148 | qu1: new Queue({ async: true }), 149 | }, 150 | ); 151 | let num = 0; 152 | /** 153 | * 为什么加入消息必须是异步的呢??? 154 | * 因为加入消息前的钩子函数必须是异步的,钩子beforeAddNews 需要做节流防抖这些异步操作 155 | */ 156 | unmq.emit("ex1", 1, 2, 3); 157 | //发送消息是异步的,所以下面的代码会立即挂载,然后立即卸载 158 | unmq.on("qu1", () => { 159 | //还未执行就卸载了, 160 | num += 2; 161 | })(); 162 | setTimeout(() => { 163 | unmq.on("qu1", () => { 164 | num++; 165 | })(); 166 | }, 500); 167 | setTimeout(() => { 168 | expect(num).toEqual(3); 169 | done(); 170 | }, 1000); 171 | }); 172 | 173 | test("有且仅消费一条消息,once()方法", function (done) { 174 | const unmq = new UNodeMQ( 175 | { 176 | ex1: new Exchange({ routes: ["qu1"] }), 177 | }, 178 | { 179 | qu1: new Queue(), 180 | }, 181 | ); 182 | let num = 0; 183 | unmq.once("qu1", () => { 184 | num++; 185 | }); 186 | unmq.emit("ex1", 1, 2, 3); 187 | setTimeout(() => { 188 | expect(num).toEqual(1); 189 | expect(unmq.getQueue("qu1")?.getNews().length).toEqual(2); 190 | done(); 191 | }); 192 | }); 193 | 194 | test("移除方法", function (done) { 195 | const unmq = new UNodeMQ( 196 | { 197 | ex1: new Exchange({ routes: ["qu1"] }), 198 | }, 199 | { 200 | qu1: new Queue(), 201 | }, 202 | ); 203 | let num = 0; 204 | unmq.emit("ex1", 1, 2, 3); 205 | function fun() { 206 | num++; 207 | } 208 | unmq.on("qu1", fun); 209 | unmq.off("qu1", fun); 210 | unmq.on("qu1", fun)(); 211 | setTimeout(() => { 212 | expect(num).toEqual(0); 213 | done(); 214 | }); 215 | }); 216 | 217 | test("直接发送消息给队列的方法", function (done) { 218 | const unmq = new UNodeMQ( 219 | {}, 220 | { 221 | qu1: new Queue(), 222 | }, 223 | ); 224 | let num = 0; 225 | unmq.emitToQueue("qu1", 1, 2, 3); 226 | unmq.on("qu1", () => { 227 | num++; 228 | }); 229 | setTimeout(() => { 230 | expect(num).toEqual(3); 231 | done(); 232 | }, 1000); 233 | }); 234 | 235 | test("观察者模式", function (done) { 236 | const unmq = new UNodeMQ( 237 | { 238 | ex1: new Exchange({ routes: ["qu1"] }), 239 | }, 240 | { 241 | qu1: new Queue({ ask: true, mode: ConsumMode.All }), 242 | }, 243 | ); 244 | let num = 0; 245 | unmq.on("qu1", () => true); 246 | unmq.emit("ex1", 1, 2, 3); 247 | unmq.on("qu1", () => { 248 | num++; 249 | return true; 250 | }); 251 | setTimeout(() => { 252 | unmq.on("qu1", () => { 253 | num++; 254 | }); 255 | expect(num).toEqual(3); 256 | done(); 257 | }); 258 | }); 259 | 260 | test("测试中继器返回不存在的队列名称", function (done) { 261 | const unmq = new UNodeMQ( 262 | { 263 | ex1: new Exchange({ routes: ["qu1"] }), 264 | }, 265 | { 266 | qu1: new Queue({ ask: true, mode: ConsumMode.All }), 267 | }, 268 | ); 269 | let num = 0; 270 | unmq.emit("ex1", 1, 2, 3); 271 | unmq.on("qu1", () => { 272 | num++; 273 | return true; 274 | }); 275 | setTimeout(() => { 276 | expect(num).toEqual(3); 277 | done(); 278 | }); 279 | }); 280 | 281 | test("promise确认消费成功", function (done) { 282 | const unmq = new UNodeMQ( 283 | { 284 | ex1: new Exchange({ routes: ["qu1"] }), 285 | }, 286 | { 287 | qu1: new Queue({ ask: true }), 288 | }, 289 | ); 290 | unmq.on("qu1", () => { 291 | return new Promise(res => { 292 | setTimeout(() => { 293 | res(true); 294 | }, 500); 295 | }); 296 | }); 297 | setTimeout(() => { 298 | expect(unmq.getQueue("qu1")?.getNews()).toHaveLength(1); 299 | done(); 300 | }, 700); 301 | unmq.emit("ex1", 1, 2, 3); 302 | }); 303 | 304 | test("promise确认消费失败", function (done) { 305 | const unmq = new UNodeMQ( 306 | { 307 | ex1: new Exchange({ routes: ["qu1"] }), 308 | }, 309 | { 310 | qu1: new Queue({ ask: true }), 311 | }, 312 | ); 313 | let num = 0; 314 | unmq.on("qu1", (data: number) => { 315 | return new Promise(res => { 316 | setTimeout(() => { 317 | num++; 318 | if (data === 1) res(true); 319 | else res(false); 320 | }, 500); 321 | }); 322 | }); 323 | 324 | setTimeout(() => { 325 | //检查1是否被消费 326 | const news = unmq.getQueue("qu1")?.getNews(); 327 | expect(news).toHaveLength(1); 328 | //3应该被取出正在消费,2应该消费失败一次 329 | expect(news?.[0].consumedTimes).toBe(2); 330 | expect(news?.[0].content).toBe(2); 331 | expect(num).toBe(2); 332 | done(); 333 | }, 1200); 334 | unmq.emit("ex1", 1, 2, 3); 335 | }); 336 | 337 | test("异步队列消费", function (done) { 338 | const unmq = new UNodeMQ( 339 | { 340 | ex1: new Exchange({ routes: ["qu1"] }), 341 | }, 342 | { 343 | qu1: new Queue({ ask: true, async: true }), 344 | }, 345 | ); 346 | let num = 0; 347 | unmq.emit("ex1", 1, 2, 3); 348 | //消息是一条一条加入队列,则可以轻易实现异步消费功能,但如果队列里面本来就有消息,就需要单独处理异步消费功能 349 | setTimeout(() => { 350 | unmq.on("qu1", (data: number) => { 351 | return new Promise(res => { 352 | setTimeout(() => { 353 | num++; 354 | if (data === 1) res(true); 355 | else res(false); 356 | }, 500); 357 | }); 358 | }); 359 | }, 100); 360 | 361 | setTimeout(() => { 362 | //检查1是否被消费 363 | const news = unmq.getQueue("qu1")?.getNews(); 364 | expect(news).toHaveLength(0); 365 | expect(num).toBe(5); 366 | done(); 367 | }, 1200); 368 | }); 369 | 370 | test("消费一条消息是否会影响别的消息", function (done) { 371 | const unmq = new UNodeMQ( 372 | { 373 | ex1: new Exchange({ routes: ["qu1"] }), 374 | }, 375 | { 376 | qu1: new Queue({ ask: true }), 377 | }, 378 | ); 379 | unmq.once("qu1", () => { 380 | return new Promise(res => { 381 | setTimeout(() => { 382 | res(true); 383 | }, 500); 384 | }); 385 | }); 386 | setTimeout(() => { 387 | const news = unmq.getQueue("qu1")?.getNews(); 388 | expect(news).toHaveLength(2); 389 | news?.forEach(item => { 390 | expect(item.consumedTimes).toBe(3); 391 | }); 392 | done(); 393 | }, 1000); 394 | unmq.emit("ex1", 1, 2, 3); 395 | }); 396 | 397 | test("检测是否出现增量循环消费的问题", function (done) { 398 | const unmq = new UNodeMQ( 399 | { 400 | ex1: new Exchange({ routes: ["qu1"] }), 401 | }, 402 | { 403 | qu1: new Queue({ ask: true, rcn: 4, mode: ConsumMode.All }), 404 | }, 405 | ); 406 | let num = 0; 407 | unmq.on("qu1", () => { 408 | num++; 409 | return false; 410 | }); 411 | unmq.on("qu1", () => { 412 | num++; 413 | return false; 414 | }); 415 | setTimeout(() => { 416 | const news = unmq.getQueue("qu1")?.getNews(); 417 | expect(num).toBe(8); 418 | expect(news).toHaveLength(0); 419 | done(); 420 | }, 1000); 421 | unmq.emit("ex1", "test"); 422 | }); 423 | 424 | test("测试once promise返回的方法", function (done) { 425 | const unmq = new UNodeMQ( 426 | { 427 | ex1: new Exchange({ routes: ["qu1"] }), 428 | }, 429 | { 430 | qu1: new Queue({ ask: true, rcn: 4, mode: ConsumMode.All }), 431 | }, 432 | ); 433 | unmq.once("qu1").then((res: string) => { 434 | expect(res).toBe("test"); 435 | done(); 436 | }); 437 | setTimeout(() => { 438 | unmq.emit("ex1", "test"); 439 | }, 500); 440 | }); 441 | 442 | test("测试once this 返回数据", function (done) { 443 | const unmq = new UNodeMQ( 444 | { 445 | ex1: new Exchange({ routes: ["qu1"] }), 446 | }, 447 | { 448 | qu1: new Queue(), 449 | }, 450 | ); 451 | 452 | async function fun() { 453 | const data = await unmq.emit("ex1", "test").once("qu1"); 454 | expect(data).toBe("test"); 455 | done(); 456 | } 457 | fun(); 458 | }); 459 | 460 | test("测试最长消费时长", function (done) { 461 | const unmq = new UNodeMQ( 462 | { 463 | ex1: new Exchange({ routes: ["qu1", "qu2", "qu3"] }), 464 | }, 465 | { 466 | qu1: new Queue({ ask: true, maxTime: 100 }), 467 | qu2: new Queue({ ask: true, maxTime: -1 }), 468 | qu3: new Queue({ ask: true, maxTime: 0 }), 469 | }, 470 | ); 471 | let num = 0; 472 | unmq.emit("ex1", 1); 473 | //时间超出了,所以 num 加3 474 | unmq.on("qu1", (data: number, next?: (arg0: boolean) => void) => { 475 | num++; 476 | setTimeout(() => { 477 | if (next) next(true); 478 | }, 200); 479 | }); 480 | //消费失败,循环消费,同步消费,所以 num 加2 481 | unmq.on("qu2", (data: number, next?: (arg0: boolean) => void) => { 482 | setTimeout(() => { 483 | num++; 484 | if (next) next(false); 485 | }, 500); 486 | }); 487 | // num 加1 488 | unmq.on("qu3", () => { 489 | num++; 490 | return true; 491 | }); 492 | 493 | setTimeout(() => { 494 | const news1 = unmq.getQueue("qu1")?.getNews(); 495 | const news2 = unmq.getQueue("qu2")?.getNews(); 496 | const news3 = unmq.getQueue("qu3")?.getNews(); 497 | expect(news1).toHaveLength(0); 498 | expect(news2).toHaveLength(0); 499 | expect(news3).toHaveLength(0); 500 | expect(num).toBe(6); 501 | done(); 502 | }, 1200); 503 | }); 504 | 505 | test("测试operators 钩子函数为同步消费", function (done) { 506 | let num = 0; 507 | const unmq = new UNodeMQ( 508 | { 509 | ex1: new Exchange({ routes: ["qu1"] }), 510 | }, 511 | { 512 | qu1: new Queue() 513 | .add({ 514 | beforeAddNews: async () => { 515 | await promiseSetTimeout(2000); 516 | num++; 517 | return true; 518 | }, 519 | }) 520 | .add({ 521 | beforeAddNews: async () => { 522 | await promiseSetTimeout(2000); 523 | num++; 524 | return true; 525 | }, 526 | }), 527 | }, 528 | ); 529 | unmq.emit("ex1", "test"); 530 | setTimeout(() => { 531 | expect(num).toBe(1); 532 | }, 3000); 533 | done(); 534 | }); 535 | --------------------------------------------------------------------------------