├── .editorconfig ├── .gitignore ├── .prettierrc ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.4.1.cjs ├── .yarnrc.yml ├── README.md ├── media ├── CH-Logo-Black.png └── CH-Logo-White.png ├── package.json ├── packages ├── demo_app │ ├── README.md │ ├── docker-compose.yml │ ├── package.json │ ├── services │ │ ├── bidding │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ └── service.ts │ │ │ └── tsconfig.json │ │ └── frontend │ │ │ ├── .eslintrc.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── public │ │ │ ├── apirateslifeforme.png │ │ │ ├── dotsoncardstock.png │ │ │ ├── favicon.ico │ │ │ ├── heart.png │ │ │ ├── next.svg │ │ │ ├── seaturtle.png │ │ │ ├── starrynightmixedmedia.png │ │ │ ├── thirteen.svg │ │ │ ├── undertherainbow.png │ │ │ ├── underwaterseascape.png │ │ │ └── vercel.svg │ │ │ ├── src │ │ │ ├── components │ │ │ │ ├── BiddingForm │ │ │ │ │ ├── BiddingForm.tsx │ │ │ │ │ └── biddingform.module.scss │ │ │ │ ├── ImgDisplay │ │ │ │ │ ├── ImgDisplay.tsx │ │ │ │ │ └── imgstyledisplay.module.scss │ │ │ │ └── NavBar │ │ │ │ │ ├── NavBar.module.scss │ │ │ │ │ └── NavBar.tsx │ │ │ ├── pages │ │ │ │ ├── _app.tsx │ │ │ │ ├── _document.tsx │ │ │ │ ├── api │ │ │ │ │ └── bidRoute.ts │ │ │ │ ├── checkout.tsx │ │ │ │ ├── counter.tsx │ │ │ │ ├── imgbidding.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── login.tsx │ │ │ │ └── userhomepage.tsx │ │ │ ├── styles │ │ │ │ ├── Home.module.css │ │ │ │ ├── globals.scss │ │ │ │ └── index.module.scss │ │ │ └── types │ │ │ │ └── images.d.ts │ │ │ └── tsconfig.json │ └── yarn.lock ├── landing │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.html │ │ └── index.tsx │ ├── tsconfig.json │ └── webpack.config.js └── library │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ ├── brokers │ │ ├── MessageBroker.ts │ │ ├── kafka │ │ │ ├── main.ts │ │ │ └── utils │ │ │ │ └── utilFunctions.ts │ │ └── rabbit │ │ │ ├── main.ts │ │ │ └── utils │ │ │ └── utilFunctions.ts │ └── index.ts │ └── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/cache 2 | .yarn/install-state.gz 3 | # It would not block these with the above 4 | packages/demo_app/.yarn/cache 5 | packages/demo_app/.yarn/install-state.gz 6 | 7 | node_modules 8 | 9 | *.env 10 | .env.* 11 | !.env.sample 12 | 13 | .DS_Store 14 | 15 | build 16 | lib 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "jsxSingleQuote": true, 8 | "bracketSameLine": true, 9 | "semi": true 10 | } 11 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-workspace-tools", 5 | factory: function (require) { 6 | var plugin=(()=>{var _r=Object.create;var we=Object.defineProperty;var Er=Object.getOwnPropertyDescriptor;var br=Object.getOwnPropertyNames;var xr=Object.getPrototypeOf,Cr=Object.prototype.hasOwnProperty;var W=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});var q=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),wr=(e,r)=>{for(var t in r)we(e,t,{get:r[t],enumerable:!0})},Je=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of br(r))!Cr.call(e,s)&&s!==t&&we(e,s,{get:()=>r[s],enumerable:!(n=Er(r,s))||n.enumerable});return e};var Be=(e,r,t)=>(t=e!=null?_r(xr(e)):{},Je(r||!e||!e.__esModule?we(t,"default",{value:e,enumerable:!0}):t,e)),Sr=e=>Je(we({},"__esModule",{value:!0}),e);var ve=q(ee=>{"use strict";ee.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;ee.find=(e,r)=>e.nodes.find(t=>t.type===r);ee.exceedsLimit=(e,r,t=1,n)=>n===!1||!ee.isInteger(e)||!ee.isInteger(r)?!1:(Number(r)-Number(e))/Number(t)>=n;ee.escapeNode=(e,r=0,t)=>{let n=e.nodes[r];!n||(t&&n.type===t||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};ee.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0===0?(e.invalid=!0,!0):!1;ee.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0===0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;ee.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;ee.reduce=e=>e.reduce((r,t)=>(t.type==="text"&&r.push(t.value),t.type==="range"&&(t.type="text"),r),[]);ee.flatten=(...e)=>{let r=[],t=n=>{for(let s=0;s{"use strict";var tt=ve();rt.exports=(e,r={})=>{let t=(n,s={})=>{let i=r.escapeInvalid&&tt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c="";if(n.value)return(i||a)&&tt.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let p of n.nodes)c+=t(p);return c};return t(e)}});var st=q((Jn,nt)=>{"use strict";nt.exports=function(e){return typeof e=="number"?e-e===0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var ht=q((es,pt)=>{"use strict";var at=st(),le=(e,r,t)=>{if(at(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(r===void 0||e===r)return String(e);if(at(r)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n={relaxZeros:!0,...t};typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),i=String(n.shorthand),a=String(n.capture),c=String(n.wrap),p=e+":"+r+"="+s+i+a+c;if(le.cache.hasOwnProperty(p))return le.cache[p].result;let m=Math.min(e,r),h=Math.max(e,r);if(Math.abs(m-h)===1){let y=e+"|"+r;return n.capture?`(${y})`:n.wrap===!1?y:`(?:${y})`}let R=ft(e)||ft(r),f={min:e,max:r,a:m,b:h},$=[],_=[];if(R&&(f.isPadded=R,f.maxLen=String(f.max).length),m<0){let y=h<0?Math.abs(h):1;_=it(y,Math.abs(m),f,n),m=f.a=0}return h>=0&&($=it(m,h,f,n)),f.negatives=_,f.positives=$,f.result=vr(_,$,n),n.capture===!0?f.result=`(${f.result})`:n.wrap!==!1&&$.length+_.length>1&&(f.result=`(?:${f.result})`),le.cache[p]=f,f.result};function vr(e,r,t){let n=Me(e,r,"-",!1,t)||[],s=Me(r,e,"",!1,t)||[],i=Me(e,r,"-?",!0,t)||[];return n.concat(i).concat(s).join("|")}function Hr(e,r){let t=1,n=1,s=ut(e,t),i=new Set([r]);for(;e<=s&&s<=r;)i.add(s),t+=1,s=ut(e,t);for(s=ct(r+1,n)-1;e1&&c.count.pop(),c.count.push(h.count[0]),c.string=c.pattern+lt(c.count),a=m+1;continue}t.isPadded&&(R=Or(m,t,n)),h.string=R+h.pattern+lt(h.count),i.push(h),a=m+1,c=h}return i}function Me(e,r,t,n,s){let i=[];for(let a of e){let{string:c}=a;!n&&!ot(r,"string",c)&&i.push(t+c),n&&ot(r,"string",c)&&i.push(t+c)}return i}function Tr(e,r){let t=[];for(let n=0;nr?1:r>e?-1:0}function ot(e,r,t){return e.some(n=>n[r]===t)}function ut(e,r){return Number(String(e).slice(0,-r)+"9".repeat(r))}function ct(e,r){return e-e%Math.pow(10,r)}function lt(e){let[r=0,t=""]=e;return t||r>1?`{${r+(t?","+t:"")}}`:""}function Lr(e,r,t){return`[${e}${r-e===1?"":"-"}${r}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Or(e,r,t){if(!r.isPadded)return e;let n=Math.abs(r.maxLen-String(e).length),s=t.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}le.cache={};le.clearCache=()=>le.cache={};pt.exports=le});var Ue=q((ts,Et)=>{"use strict";var Nr=W("util"),At=ht(),dt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Ir=e=>r=>e===!0?Number(r):String(r),Pe=e=>typeof e=="number"||typeof e=="string"&&e!=="",Ae=e=>Number.isInteger(+e),De=e=>{let r=`${e}`,t=-1;if(r[0]==="-"&&(r=r.slice(1)),r==="0")return!1;for(;r[++t]==="0";);return t>0},Br=(e,r,t)=>typeof e=="string"||typeof r=="string"?!0:t.stringify===!0,Mr=(e,r,t)=>{if(r>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?r-1:r,"0")}return t===!1?String(e):e},gt=(e,r)=>{let t=e[0]==="-"?"-":"";for(t&&(e=e.slice(1),r--);e.length{e.negatives.sort((a,c)=>ac?1:0),e.positives.sort((a,c)=>ac?1:0);let t=r.capture?"":"?:",n="",s="",i;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${t}${e.negatives.join("|")})`),n&&s?i=`${n}|${s}`:i=n||s,r.wrap?`(${t}${i})`:i},mt=(e,r,t,n)=>{if(t)return At(e,r,{wrap:!1,...n});let s=String.fromCharCode(e);if(e===r)return s;let i=String.fromCharCode(r);return`[${s}-${i}]`},Rt=(e,r,t)=>{if(Array.isArray(e)){let n=t.wrap===!0,s=t.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return At(e,r,t)},yt=(...e)=>new RangeError("Invalid range arguments: "+Nr.inspect(...e)),_t=(e,r,t)=>{if(t.strictRanges===!0)throw yt([e,r]);return[]},Dr=(e,r)=>{if(r.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Ur=(e,r,t=1,n={})=>{let s=Number(e),i=Number(r);if(!Number.isInteger(s)||!Number.isInteger(i)){if(n.strictRanges===!0)throw yt([e,r]);return[]}s===0&&(s=0),i===0&&(i=0);let a=s>i,c=String(e),p=String(r),m=String(t);t=Math.max(Math.abs(t),1);let h=De(c)||De(p)||De(m),R=h?Math.max(c.length,p.length,m.length):0,f=h===!1&&Br(e,r,n)===!1,$=n.transform||Ir(f);if(n.toRegex&&t===1)return mt(gt(e,R),gt(r,R),!0,n);let _={negatives:[],positives:[]},y=T=>_[T<0?"negatives":"positives"].push(Math.abs(T)),E=[],S=0;for(;a?s>=i:s<=i;)n.toRegex===!0&&t>1?y(s):E.push(Mr($(s,S),R,f)),s=a?s-t:s+t,S++;return n.toRegex===!0?t>1?Pr(_,n):Rt(E,null,{wrap:!1,...n}):E},Gr=(e,r,t=1,n={})=>{if(!Ae(e)&&e.length>1||!Ae(r)&&r.length>1)return _t(e,r,n);let s=n.transform||(f=>String.fromCharCode(f)),i=`${e}`.charCodeAt(0),a=`${r}`.charCodeAt(0),c=i>a,p=Math.min(i,a),m=Math.max(i,a);if(n.toRegex&&t===1)return mt(p,m,!1,n);let h=[],R=0;for(;c?i>=a:i<=a;)h.push(s(i,R)),i=c?i-t:i+t,R++;return n.toRegex===!0?Rt(h,null,{wrap:!1,options:n}):h},$e=(e,r,t,n={})=>{if(r==null&&Pe(e))return[e];if(!Pe(e)||!Pe(r))return _t(e,r,n);if(typeof t=="function")return $e(e,r,1,{transform:t});if(dt(t))return $e(e,r,0,t);let s={...n};return s.capture===!0&&(s.wrap=!0),t=t||s.step||1,Ae(t)?Ae(e)&&Ae(r)?Ur(e,r,t,s):Gr(e,r,Math.max(Math.abs(t),1),s):t!=null&&!dt(t)?Dr(t,s):$e(e,r,1,t)};Et.exports=$e});var Ct=q((rs,xt)=>{"use strict";var qr=Ue(),bt=ve(),Kr=(e,r={})=>{let t=(n,s={})=>{let i=bt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c=i===!0||a===!0,p=r.escapeInvalid===!0?"\\":"",m="";if(n.isOpen===!0||n.isClose===!0)return p+n.value;if(n.type==="open")return c?p+n.value:"(";if(n.type==="close")return c?p+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":c?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let h=bt.reduce(n.nodes),R=qr(...h,{...r,wrap:!1,toRegex:!0});if(R.length!==0)return h.length>1&&R.length>1?`(${R})`:R}if(n.nodes)for(let h of n.nodes)m+=t(h,n);return m};return t(e)};xt.exports=Kr});var vt=q((ns,St)=>{"use strict";var Wr=Ue(),wt=He(),he=ve(),fe=(e="",r="",t=!1)=>{let n=[];if(e=[].concat(e),r=[].concat(r),!r.length)return e;if(!e.length)return t?he.flatten(r).map(s=>`{${s}}`):r;for(let s of e)if(Array.isArray(s))for(let i of s)n.push(fe(i,r,t));else for(let i of r)t===!0&&typeof i=="string"&&(i=`{${i}}`),n.push(Array.isArray(i)?fe(s,i,t):s+i);return he.flatten(n)},jr=(e,r={})=>{let t=r.rangeLimit===void 0?1e3:r.rangeLimit,n=(s,i={})=>{s.queue=[];let a=i,c=i.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,c=a.queue;if(s.invalid||s.dollar){c.push(fe(c.pop(),wt(s,r)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){c.push(fe(c.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let R=he.reduce(s.nodes);if(he.exceedsLimit(...R,r.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let f=Wr(...R,r);f.length===0&&(f=wt(s,r)),c.push(fe(c.pop(),f)),s.nodes=[];return}let p=he.encloseBrace(s),m=s.queue,h=s;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,m=h.queue;for(let R=0;R{"use strict";Ht.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` 7 | `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nt=q((as,Ot)=>{"use strict";var Fr=He(),{MAX_LENGTH:Tt,CHAR_BACKSLASH:Ge,CHAR_BACKTICK:Qr,CHAR_COMMA:Xr,CHAR_DOT:Zr,CHAR_LEFT_PARENTHESES:Yr,CHAR_RIGHT_PARENTHESES:zr,CHAR_LEFT_CURLY_BRACE:Vr,CHAR_RIGHT_CURLY_BRACE:Jr,CHAR_LEFT_SQUARE_BRACKET:kt,CHAR_RIGHT_SQUARE_BRACKET:Lt,CHAR_DOUBLE_QUOTE:en,CHAR_SINGLE_QUOTE:tn,CHAR_NO_BREAK_SPACE:rn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:nn}=$t(),sn=(e,r={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let t=r||{},n=typeof t.maxLength=="number"?Math.min(Tt,t.maxLength):Tt;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},i=[s],a=s,c=s,p=0,m=e.length,h=0,R=0,f,$={},_=()=>e[h++],y=E=>{if(E.type==="text"&&c.type==="dot"&&(c.type="text"),c&&c.type==="text"&&E.type==="text"){c.value+=E.value;return}return a.nodes.push(E),E.parent=a,E.prev=c,c=E,E};for(y({type:"bos"});h0){if(a.ranges>0){a.ranges=0;let E=a.nodes.shift();a.nodes=[E,{type:"text",value:Fr(a)}]}y({type:"comma",value:f}),a.commas++;continue}if(f===Zr&&R>0&&a.commas===0){let E=a.nodes;if(R===0||E.length===0){y({type:"text",value:f});continue}if(c.type==="dot"){if(a.range=[],c.value+=f,c.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,c.type="text";continue}a.ranges++,a.args=[];continue}if(c.type==="range"){E.pop();let S=E[E.length-1];S.value+=c.value+f,c=S,a.ranges--;continue}y({type:"dot",value:f});continue}y({type:"text",value:f})}do if(a=i.pop(),a.type!=="root"){a.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let E=i[i.length-1],S=E.nodes.indexOf(a);E.nodes.splice(S,1,...a.nodes)}while(i.length>0);return y({type:"eos"}),s};Ot.exports=sn});var Mt=q((is,Bt)=>{"use strict";var It=He(),an=Ct(),on=vt(),un=Nt(),X=(e,r={})=>{let t=[];if(Array.isArray(e))for(let n of e){let s=X.create(n,r);Array.isArray(s)?t.push(...s):t.push(s)}else t=[].concat(X.create(e,r));return r&&r.expand===!0&&r.nodupes===!0&&(t=[...new Set(t)]),t};X.parse=(e,r={})=>un(e,r);X.stringify=(e,r={})=>It(typeof e=="string"?X.parse(e,r):e,r);X.compile=(e,r={})=>(typeof e=="string"&&(e=X.parse(e,r)),an(e,r));X.expand=(e,r={})=>{typeof e=="string"&&(e=X.parse(e,r));let t=on(e,r);return r.noempty===!0&&(t=t.filter(Boolean)),r.nodupes===!0&&(t=[...new Set(t)]),t};X.create=(e,r={})=>e===""||e.length<3?[e]:r.expand!==!0?X.compile(e,r):X.expand(e,r);Bt.exports=X});var me=q((os,qt)=>{"use strict";var cn=W("path"),se="\\\\/",Pt=`[^${se}]`,ie="\\.",ln="\\+",fn="\\?",Te="\\/",pn="(?=.)",Dt="[^/]",qe=`(?:${Te}|$)`,Ut=`(?:^|${Te})`,Ke=`${ie}{1,2}${qe}`,hn=`(?!${ie})`,dn=`(?!${Ut}${Ke})`,gn=`(?!${ie}{0,1}${qe})`,An=`(?!${Ke})`,mn=`[^.${Te}]`,Rn=`${Dt}*?`,Gt={DOT_LITERAL:ie,PLUS_LITERAL:ln,QMARK_LITERAL:fn,SLASH_LITERAL:Te,ONE_CHAR:pn,QMARK:Dt,END_ANCHOR:qe,DOTS_SLASH:Ke,NO_DOT:hn,NO_DOTS:dn,NO_DOT_SLASH:gn,NO_DOTS_SLASH:An,QMARK_NO_DOT:mn,STAR:Rn,START_ANCHOR:Ut},yn={...Gt,SLASH_LITERAL:`[${se}]`,QMARK:Pt,STAR:`${Pt}*?`,DOTS_SLASH:`${ie}{1,2}(?:[${se}]|$)`,NO_DOT:`(?!${ie})`,NO_DOTS:`(?!(?:^|[${se}])${ie}{1,2}(?:[${se}]|$))`,NO_DOT_SLASH:`(?!${ie}{0,1}(?:[${se}]|$))`,NO_DOTS_SLASH:`(?!${ie}{1,2}(?:[${se}]|$))`,QMARK_NO_DOT:`[^.${se}]`,START_ANCHOR:`(?:^|[${se}])`,END_ANCHOR:`(?:[${se}]|$)`},_n={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:_n,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:cn.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?yn:Gt}}});var Re=q(F=>{"use strict";var En=W("path"),bn=process.platform==="win32",{REGEX_BACKSLASH:xn,REGEX_REMOVE_BACKSLASH:Cn,REGEX_SPECIAL_CHARS:wn,REGEX_SPECIAL_CHARS_GLOBAL:Sn}=me();F.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);F.hasRegexChars=e=>wn.test(e);F.isRegexChar=e=>e.length===1&&F.hasRegexChars(e);F.escapeRegex=e=>e.replace(Sn,"\\$1");F.toPosixSlashes=e=>e.replace(xn,"/");F.removeBackslashes=e=>e.replace(Cn,r=>r==="\\"?"":r);F.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};F.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:bn===!0||En.sep==="\\";F.escapeLast=(e,r,t)=>{let n=e.lastIndexOf(r,t);return n===-1?e:e[n-1]==="\\"?F.escapeLast(e,r,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};F.removePrefix=(e,r={})=>{let t=e;return t.startsWith("./")&&(t=t.slice(2),r.prefix="./"),t};F.wrapOutput=(e,r={},t={})=>{let n=t.contains?"":"^",s=t.contains?"":"$",i=`${n}(?:${e})${s}`;return r.negated===!0&&(i=`(?:^(?!${i}).*$)`),i}});var Yt=q((cs,Zt)=>{"use strict";var Kt=Re(),{CHAR_ASTERISK:We,CHAR_AT:vn,CHAR_BACKWARD_SLASH:ye,CHAR_COMMA:Hn,CHAR_DOT:je,CHAR_EXCLAMATION_MARK:Fe,CHAR_FORWARD_SLASH:Xt,CHAR_LEFT_CURLY_BRACE:Qe,CHAR_LEFT_PARENTHESES:Xe,CHAR_LEFT_SQUARE_BRACKET:$n,CHAR_PLUS:Tn,CHAR_QUESTION_MARK:Wt,CHAR_RIGHT_CURLY_BRACE:kn,CHAR_RIGHT_PARENTHESES:jt,CHAR_RIGHT_SQUARE_BRACKET:Ln}=me(),Ft=e=>e===Xt||e===ye,Qt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?1/0:1)},On=(e,r)=>{let t=r||{},n=e.length-1,s=t.parts===!0||t.scanToEnd===!0,i=[],a=[],c=[],p=e,m=-1,h=0,R=0,f=!1,$=!1,_=!1,y=!1,E=!1,S=!1,T=!1,L=!1,z=!1,I=!1,re=0,K,g,v={value:"",depth:0,isGlob:!1},k=()=>m>=n,l=()=>p.charCodeAt(m+1),H=()=>(K=g,p.charCodeAt(++m));for(;m0&&(B=p.slice(0,h),p=p.slice(h),R-=h),w&&_===!0&&R>0?(w=p.slice(0,R),o=p.slice(R)):_===!0?(w="",o=p):w=p,w&&w!==""&&w!=="/"&&w!==p&&Ft(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),t.unescape===!0&&(o&&(o=Kt.removeBackslashes(o)),w&&T===!0&&(w=Kt.removeBackslashes(w)));let u={prefix:B,input:e,start:h,base:w,glob:o,isBrace:f,isBracket:$,isGlob:_,isExtglob:y,isGlobstar:E,negated:L,negatedExtglob:z};if(t.tokens===!0&&(u.maxDepth=0,Ft(g)||a.push(v),u.tokens=a),t.parts===!0||t.tokens===!0){let M;for(let b=0;b{"use strict";var ke=me(),Z=Re(),{MAX_LENGTH:Le,POSIX_REGEX_SOURCE:Nn,REGEX_NON_SPECIAL_CHARS:In,REGEX_SPECIAL_CHARS_BACKREF:Bn,REPLACEMENTS:zt}=ke,Mn=(e,r)=>{if(typeof r.expandRange=="function")return r.expandRange(...e,r);e.sort();let t=`[${e.join("-")}]`;try{new RegExp(t)}catch{return e.map(s=>Z.escapeRegex(s)).join("..")}return t},de=(e,r)=>`Missing ${e}: "${r}" - use "\\\\${r}" to match literal characters`,Vt=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=zt[e]||e;let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let i={type:"bos",value:"",output:t.prepend||""},a=[i],c=t.capture?"":"?:",p=Z.isWindows(r),m=ke.globChars(p),h=ke.extglobChars(m),{DOT_LITERAL:R,PLUS_LITERAL:f,SLASH_LITERAL:$,ONE_CHAR:_,DOTS_SLASH:y,NO_DOT:E,NO_DOT_SLASH:S,NO_DOTS_SLASH:T,QMARK:L,QMARK_NO_DOT:z,STAR:I,START_ANCHOR:re}=m,K=A=>`(${c}(?:(?!${re}${A.dot?y:R}).)*?)`,g=t.dot?"":E,v=t.dot?L:z,k=t.bash===!0?K(t):I;t.capture&&(k=`(${k})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let l={input:e,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};e=Z.removePrefix(e,l),s=e.length;let H=[],w=[],B=[],o=i,u,M=()=>l.index===s-1,b=l.peek=(A=1)=>e[l.index+A],V=l.advance=()=>e[++l.index]||"",J=()=>e.slice(l.index+1),Q=(A="",O=0)=>{l.consumed+=A,l.index+=O},Ee=A=>{l.output+=A.output!=null?A.output:A.value,Q(A.value)},Rr=()=>{let A=1;for(;b()==="!"&&(b(2)!=="("||b(3)==="?");)V(),l.start++,A++;return A%2===0?!1:(l.negated=!0,l.start++,!0)},be=A=>{l[A]++,B.push(A)},oe=A=>{l[A]--,B.pop()},C=A=>{if(o.type==="globstar"){let O=l.braces>0&&(A.type==="comma"||A.type==="brace"),d=A.extglob===!0||H.length&&(A.type==="pipe"||A.type==="paren");A.type!=="slash"&&A.type!=="paren"&&!O&&!d&&(l.output=l.output.slice(0,-o.output.length),o.type="star",o.value="*",o.output=k,l.output+=o.output)}if(H.length&&A.type!=="paren"&&(H[H.length-1].inner+=A.value),(A.value||A.output)&&Ee(A),o&&o.type==="text"&&A.type==="text"){o.value+=A.value,o.output=(o.output||"")+A.value;return}A.prev=o,a.push(A),o=A},xe=(A,O)=>{let d={...h[O],conditions:1,inner:""};d.prev=o,d.parens=l.parens,d.output=l.output;let x=(t.capture?"(":"")+d.open;be("parens"),C({type:A,value:O,output:l.output?"":_}),C({type:"paren",extglob:!0,value:V(),output:x}),H.push(d)},yr=A=>{let O=A.close+(t.capture?")":""),d;if(A.type==="negate"){let x=k;A.inner&&A.inner.length>1&&A.inner.includes("/")&&(x=K(t)),(x!==k||M()||/^\)+$/.test(J()))&&(O=A.close=`)$))${x}`),A.inner.includes("*")&&(d=J())&&/^\.[^\\/.]+$/.test(d)&&(O=A.close=`)${d})${x})`),A.prev.type==="bos"&&(l.negatedExtglob=!0)}C({type:"paren",extglob:!0,value:u,output:O}),oe("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let A=!1,O=e.replace(Bn,(d,x,P,j,G,Ie)=>j==="\\"?(A=!0,d):j==="?"?x?x+j+(G?L.repeat(G.length):""):Ie===0?v+(G?L.repeat(G.length):""):L.repeat(P.length):j==="."?R.repeat(P.length):j==="*"?x?x+j+(G?k:""):k:x?d:`\\${d}`);return A===!0&&(t.unescape===!0?O=O.replace(/\\/g,""):O=O.replace(/\\+/g,d=>d.length%2===0?"\\\\":d?"\\":"")),O===e&&t.contains===!0?(l.output=e,l):(l.output=Z.wrapOutput(O,l,r),l)}for(;!M();){if(u=V(),u==="\0")continue;if(u==="\\"){let d=b();if(d==="/"&&t.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",C({type:"text",value:u});continue}let x=/^\\+/.exec(J()),P=0;if(x&&x[0].length>2&&(P=x[0].length,l.index+=P,P%2!==0&&(u+="\\")),t.unescape===!0?u=V():u+=V(),l.brackets===0){C({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||o.value==="["||o.value==="[^")){if(t.posix!==!1&&u===":"){let d=o.value.slice(1);if(d.includes("[")&&(o.posix=!0,d.includes(":"))){let x=o.value.lastIndexOf("["),P=o.value.slice(0,x),j=o.value.slice(x+2),G=Nn[j];if(G){o.value=P+G,l.backtrack=!0,V(),!i.output&&a.indexOf(o)===1&&(i.output=_);continue}}}(u==="["&&b()!==":"||u==="-"&&b()==="]")&&(u=`\\${u}`),u==="]"&&(o.value==="["||o.value==="[^")&&(u=`\\${u}`),t.posix===!0&&u==="!"&&o.value==="["&&(u="^"),o.value+=u,Ee({value:u});continue}if(l.quotes===1&&u!=='"'){u=Z.escapeRegex(u),o.value+=u,Ee({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,t.keepQuotes===!0&&C({type:"text",value:u});continue}if(u==="("){be("parens"),C({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&t.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){yr(H.pop());continue}C({type:"paren",value:u,output:l.parens?")":"\\)"}),oe("parens");continue}if(u==="["){if(t.nobracket===!0||!J().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else be("brackets");C({type:"bracket",value:u});continue}if(u==="]"){if(t.nobracket===!0||o&&o.type==="bracket"&&o.value.length===1){C({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(de("opening","["));C({type:"text",value:u,output:`\\${u}`});continue}oe("brackets");let d=o.value.slice(1);if(o.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),o.value+=u,Ee({value:u}),t.literalBrackets===!1||Z.hasRegexChars(d))continue;let x=Z.escapeRegex(o.value);if(l.output=l.output.slice(0,-o.value.length),t.literalBrackets===!0){l.output+=x,o.value=x;continue}o.value=`(${c}${x}|${o.value})`,l.output+=o.value;continue}if(u==="{"&&t.nobrace!==!0){be("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),C(d);continue}if(u==="}"){let d=w[w.length-1];if(t.nobrace===!0||!d){C({type:"text",value:u,output:u});continue}let x=")";if(d.dots===!0){let P=a.slice(),j=[];for(let G=P.length-1;G>=0&&(a.pop(),P[G].type!=="brace");G--)P[G].type!=="dots"&&j.unshift(P[G].value);x=Mn(j,t),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let P=l.output.slice(0,d.outputIndex),j=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=x="\\}",l.output=P;for(let G of j)l.output+=G.output||G.value}C({type:"brace",value:u,output:x}),oe("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,C({type:"text",value:u});continue}if(u===","){let d=u,x=w[w.length-1];x&&B[B.length-1]==="braces"&&(x.comma=!0,d="|"),C({type:"comma",value:u,output:d});continue}if(u==="/"){if(o.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",a.pop(),o=i;continue}C({type:"slash",value:u,output:$});continue}if(u==="."){if(l.braces>0&&o.type==="dot"){o.value==="."&&(o.output=R);let d=w[w.length-1];o.type="dots",o.output+=u,o.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&o.type!=="bos"&&o.type!=="slash"){C({type:"text",value:u,output:R});continue}C({type:"dot",value:u,output:R});continue}if(u==="?"){if(!(o&&o.value==="(")&&t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("qmark",u);continue}if(o&&o.type==="paren"){let x=b(),P=u;if(x==="<"&&!Z.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(o.value==="("&&!/[!=<:]/.test(x)||x==="<"&&!/<([!=]|\w+>)/.test(J()))&&(P=`\\${u}`),C({type:"text",value:u,output:P});continue}if(t.dot!==!0&&(o.type==="slash"||o.type==="bos")){C({type:"qmark",value:u,output:z});continue}C({type:"qmark",value:u,output:L});continue}if(u==="!"){if(t.noextglob!==!0&&b()==="("&&(b(2)!=="?"||!/[!=<:]/.test(b(3)))){xe("negate",u);continue}if(t.nonegate!==!0&&l.index===0){Rr();continue}}if(u==="+"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("plus",u);continue}if(o&&o.value==="("||t.regex===!1){C({type:"plus",value:u,output:f});continue}if(o&&(o.type==="bracket"||o.type==="paren"||o.type==="brace")||l.parens>0){C({type:"plus",value:u});continue}C({type:"plus",value:f});continue}if(u==="@"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){C({type:"at",extglob:!0,value:u,output:""});continue}C({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=In.exec(J());d&&(u+=d[0],l.index+=d[0].length),C({type:"text",value:u});continue}if(o&&(o.type==="globstar"||o.star===!0)){o.type="star",o.star=!0,o.value+=u,o.output=k,l.backtrack=!0,l.globstar=!0,Q(u);continue}let A=J();if(t.noextglob!==!0&&/^\([^?]/.test(A)){xe("star",u);continue}if(o.type==="star"){if(t.noglobstar===!0){Q(u);continue}let d=o.prev,x=d.prev,P=d.type==="slash"||d.type==="bos",j=x&&(x.type==="star"||x.type==="globstar");if(t.bash===!0&&(!P||A[0]&&A[0]!=="/")){C({type:"star",value:u,output:""});continue}let G=l.braces>0&&(d.type==="comma"||d.type==="brace"),Ie=H.length&&(d.type==="pipe"||d.type==="paren");if(!P&&d.type!=="paren"&&!G&&!Ie){C({type:"star",value:u,output:""});continue}for(;A.slice(0,3)==="/**";){let Ce=e[l.index+4];if(Ce&&Ce!=="/")break;A=A.slice(3),Q("/**",3)}if(d.type==="bos"&&M()){o.type="globstar",o.value+=u,o.output=K(t),l.output=o.output,l.globstar=!0,Q(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!j&&M()){l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=K(t)+(t.strictSlashes?")":"|$)"),o.value+=u,l.globstar=!0,l.output+=d.output+o.output,Q(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&A[0]==="/"){let Ce=A[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=`${K(t)}${$}|${$}${Ce})`,o.value+=u,l.output+=d.output+o.output,l.globstar=!0,Q(u+V()),C({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&A[0]==="/"){o.type="globstar",o.value+=u,o.output=`(?:^|${$}|${K(t)}${$})`,l.output=o.output,l.globstar=!0,Q(u+V()),C({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-o.output.length),o.type="globstar",o.output=K(t),o.value+=u,l.output+=o.output,l.globstar=!0,Q(u);continue}let O={type:"star",value:u,output:k};if(t.bash===!0){O.output=".*?",(o.type==="bos"||o.type==="slash")&&(O.output=g+O.output),C(O);continue}if(o&&(o.type==="bracket"||o.type==="paren")&&t.regex===!0){O.output=u,C(O);continue}(l.index===l.start||o.type==="slash"||o.type==="dot")&&(o.type==="dot"?(l.output+=S,o.output+=S):t.dot===!0?(l.output+=T,o.output+=T):(l.output+=g,o.output+=g),b()!=="*"&&(l.output+=_,o.output+=_)),C(O)}for(;l.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=Z.escapeLast(l.output,"["),oe("brackets")}for(;l.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=Z.escapeLast(l.output,"("),oe("parens")}for(;l.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=Z.escapeLast(l.output,"{"),oe("braces")}if(t.strictSlashes!==!0&&(o.type==="star"||o.type==="bracket")&&C({type:"maybe_slash",value:"",output:`${$}?`}),l.backtrack===!0){l.output="";for(let A of l.tokens)l.output+=A.output!=null?A.output:A.value,A.suffix&&(l.output+=A.suffix)}return l};Vt.fastpaths=(e,r)=>{let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=zt[e]||e;let i=Z.isWindows(r),{DOT_LITERAL:a,SLASH_LITERAL:c,ONE_CHAR:p,DOTS_SLASH:m,NO_DOT:h,NO_DOTS:R,NO_DOTS_SLASH:f,STAR:$,START_ANCHOR:_}=ke.globChars(i),y=t.dot?R:h,E=t.dot?f:h,S=t.capture?"":"?:",T={negated:!1,prefix:""},L=t.bash===!0?".*?":$;t.capture&&(L=`(${L})`);let z=g=>g.noglobstar===!0?L:`(${S}(?:(?!${_}${g.dot?m:a}).)*?)`,I=g=>{switch(g){case"*":return`${y}${p}${L}`;case".*":return`${a}${p}${L}`;case"*.*":return`${y}${L}${a}${p}${L}`;case"*/*":return`${y}${L}${c}${p}${E}${L}`;case"**":return y+z(t);case"**/*":return`(?:${y}${z(t)}${c})?${E}${p}${L}`;case"**/*.*":return`(?:${y}${z(t)}${c})?${E}${L}${a}${p}${L}`;case"**/.*":return`(?:${y}${z(t)}${c})?${a}${p}${L}`;default:{let v=/^(.*?)\.(\w+)$/.exec(g);if(!v)return;let k=I(v[1]);return k?k+a+v[2]:void 0}}},re=Z.removePrefix(e,T),K=I(re);return K&&t.strictSlashes!==!0&&(K+=`${c}?`),K};Jt.exports=Vt});var rr=q((fs,tr)=>{"use strict";var Pn=W("path"),Dn=Yt(),Ze=er(),Ye=Re(),Un=me(),Gn=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,r,t=!1)=>{if(Array.isArray(e)){let h=e.map(f=>D(f,r,t));return f=>{for(let $ of h){let _=$(f);if(_)return _}return!1}}let n=Gn(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=r||{},i=Ye.isWindows(r),a=n?D.compileRe(e,r):D.makeRe(e,r,!1,!0),c=a.state;delete a.state;let p=()=>!1;if(s.ignore){let h={...r,ignore:null,onMatch:null,onResult:null};p=D(s.ignore,h,t)}let m=(h,R=!1)=>{let{isMatch:f,match:$,output:_}=D.test(h,a,r,{glob:e,posix:i}),y={glob:e,state:c,regex:a,posix:i,input:h,output:_,match:$,isMatch:f};return typeof s.onResult=="function"&&s.onResult(y),f===!1?(y.isMatch=!1,R?y:!1):p(h)?(typeof s.onIgnore=="function"&&s.onIgnore(y),y.isMatch=!1,R?y:!1):(typeof s.onMatch=="function"&&s.onMatch(y),R?y:!0)};return t&&(m.state=c),m};D.test=(e,r,t,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let i=t||{},a=i.format||(s?Ye.toPosixSlashes:null),c=e===n,p=c&&a?a(e):e;return c===!1&&(p=a?a(e):e,c=p===n),(c===!1||i.capture===!0)&&(i.matchBase===!0||i.basename===!0?c=D.matchBase(e,r,t,s):c=r.exec(p)),{isMatch:Boolean(c),match:c,output:p}};D.matchBase=(e,r,t,n=Ye.isWindows(t))=>(r instanceof RegExp?r:D.makeRe(r,t)).test(Pn.basename(e));D.isMatch=(e,r,t)=>D(r,t)(e);D.parse=(e,r)=>Array.isArray(e)?e.map(t=>D.parse(t,r)):Ze(e,{...r,fastpaths:!1});D.scan=(e,r)=>Dn(e,r);D.compileRe=(e,r,t=!1,n=!1)=>{if(t===!0)return e.output;let s=r||{},i=s.contains?"":"^",a=s.contains?"":"$",c=`${i}(?:${e.output})${a}`;e&&e.negated===!0&&(c=`^(?!${c}).*$`);let p=D.toRegex(c,r);return n===!0&&(p.state=e),p};D.makeRe=(e,r={},t=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return r.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ze.fastpaths(e,r)),s.output||(s=Ze(e,r)),D.compileRe(s,r,t,n)};D.toRegex=(e,r)=>{try{let t=r||{};return new RegExp(e,t.flags||(t.nocase?"i":""))}catch(t){if(r&&r.debug===!0)throw t;return/$^/}};D.constants=Un;tr.exports=D});var sr=q((ps,nr)=>{"use strict";nr.exports=rr()});var cr=q((hs,ur)=>{"use strict";var ir=W("util"),or=Mt(),ae=sr(),ze=Re(),ar=e=>e===""||e==="./",N=(e,r,t)=>{r=[].concat(r),e=[].concat(e);let n=new Set,s=new Set,i=new Set,a=0,c=h=>{i.add(h.output),t&&t.onResult&&t.onResult(h)};for(let h=0;h!n.has(h));if(t&&m.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${r.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?r.map(h=>h.replace(/\\/g,"")):r}return m};N.match=N;N.matcher=(e,r)=>ae(e,r);N.isMatch=(e,r,t)=>ae(r,t)(e);N.any=N.isMatch;N.not=(e,r,t={})=>{r=[].concat(r).map(String);let n=new Set,s=[],a=N(e,r,{...t,onResult:c=>{t.onResult&&t.onResult(c),s.push(c.output)}});for(let c of s)a.includes(c)||n.add(c);return[...n]};N.contains=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);if(Array.isArray(r))return r.some(n=>N.contains(e,n,t));if(typeof r=="string"){if(ar(e)||ar(r))return!1;if(e.includes(r)||e.startsWith("./")&&e.slice(2).includes(r))return!0}return N.isMatch(e,r,{...t,contains:!0})};N.matchKeys=(e,r,t)=>{if(!ze.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),r,t),s={};for(let i of n)s[i]=e[i];return s};N.some=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(n.some(a=>i(a)))return!0}return!1};N.every=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(!n.every(a=>i(a)))return!1}return!0};N.all=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);return[].concat(r).every(n=>ae(n,t)(e))};N.capture=(e,r,t)=>{let n=ze.isWindows(t),i=ae.makeRe(String(e),{...t,capture:!0}).exec(n?ze.toPosixSlashes(r):r);if(i)return i.slice(1).map(a=>a===void 0?"":a)};N.makeRe=(...e)=>ae.makeRe(...e);N.scan=(...e)=>ae.scan(...e);N.parse=(e,r)=>{let t=[];for(let n of[].concat(e||[]))for(let s of or(String(n),r))t.push(ae.parse(s,r));return t};N.braces=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return r&&r.nobrace===!0||!/\{.*\}/.test(e)?[e]:or(e,r)};N.braceExpand=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,{...r,expand:!0})};ur.exports=N});var fr=q((ds,lr)=>{"use strict";lr.exports=(e,...r)=>new Promise(t=>{t(e(...r))})});var hr=q((gs,Ve)=>{"use strict";var qn=fr(),pr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let r=[],t=0,n=()=>{t--,r.length>0&&r.shift()()},s=(c,p,...m)=>{t++;let h=qn(c,...m);p(h),h.then(n,n)},i=(c,p,...m)=>{tnew Promise(m=>i(c,m,...p));return Object.defineProperties(a,{activeCount:{get:()=>t},pendingCount:{get:()=>r.length}}),a};Ve.exports=pr;Ve.exports.default=pr});var Fn={};wr(Fn,{default:()=>jn});var Se=W("@yarnpkg/cli"),ne=W("@yarnpkg/core"),et=W("@yarnpkg/core"),ue=W("clipanion"),ce=class extends Se.BaseCommand{constructor(){super(...arguments);this.json=ue.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=ue.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=ue.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=ue.Option.Rest()}async execute(){let t=await ne.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ne.Project.find(t,this.context.cwd),i=await ne.Cache.find(t);await n.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(n.workspaces);else if(this.workspaces.length===0){if(!s)throw new Se.WorkspaceRequiredError(n.cwd,this.context.cwd);a=new Set([s])}else a=new Set(this.workspaces.map(p=>n.getWorkspaceByIdent(et.structUtils.parseIdent(p))));for(let p of a)for(let m of this.production?["dependencies"]:ne.Manifest.hardDependencies)for(let h of p.manifest.getForScope(m).values()){let R=n.tryWorkspaceByDescriptor(h);R!==null&&a.add(R)}for(let p of n.workspaces)a.has(p)?this.production&&p.manifest.devDependencies.clear():(p.manifest.installConfig=p.manifest.installConfig||{},p.manifest.installConfig.selfReferences=!1,p.manifest.dependencies.clear(),p.manifest.devDependencies.clear(),p.manifest.peerDependencies.clear(),p.manifest.scripts.clear());return(await ne.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async p=>{await n.install({cache:i,report:p,persistProject:!1})})).exitCode()}};ce.paths=[["workspaces","focus"]],ce.usage=ue.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var Ne=W("@yarnpkg/cli"),ge=W("@yarnpkg/core"),_e=W("@yarnpkg/core"),Y=W("@yarnpkg/core"),gr=W("@yarnpkg/plugin-git"),U=W("clipanion"),Oe=Be(cr()),Ar=W("os"),mr=Be(hr()),te=Be(W("typanion")),pe=class extends Ne.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:te.isOneOf([te.isEnum(["unlimited"]),te.applyCascade(te.isNumber(),[te.isInteger(),te.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!s)throw new Ne.WorkspaceRequiredError(n.cwd,this.context.cwd);await n.restoreInstallState();let i=this.cli.process([this.commandName,...this.args]),a=i.path.length===1&&i.path[0]==="run"&&typeof i.scriptName<"u"?i.scriptName:null;if(i.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let c=this.all?n.topLevelWorkspace:s,p=this.since?Array.from(await gr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:n})):[c,...this.from.length>0?c.getRecursiveWorkspaceChildren():[]],m=g=>Oe.default.isMatch(Y.structUtils.stringifyIdent(g.locator),this.from),h=this.from.length>0?p.filter(m):p,R=new Set([...h,...h.map(g=>[...this.recursive?this.since?g.getRecursiveWorkspaceDependents():g.getRecursiveWorkspaceDependencies():g.getRecursiveWorkspaceChildren()]).flat()]),f=[],$=!1;if(a!=null&&a.includes(":")){for(let g of n.workspaces)if(g.manifest.scripts.has(a)&&($=!$,$===!1))break}for(let g of R)a&&!g.manifest.scripts.has(a)&&!$&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(g)).has(a)||a===process.env.npm_lifecycle_event&&g.cwd===s.cwd||this.include.length>0&&!Oe.default.isMatch(Y.structUtils.stringifyIdent(g.locator),this.include)||this.exclude.length>0&&Oe.default.isMatch(Y.structUtils.stringifyIdent(g.locator),this.exclude)||this.publicOnly&&g.manifest.private===!0||f.push(g);let _=this.parallel?this.jobs==="unlimited"?1/0:Number(this.jobs)||Math.max(1,(0,Ar.cpus)().length/2):1,y=_===1?!1:this.parallel,E=y?this.interlaced:!0,S=(0,mr.default)(_),T=new Map,L=new Set,z=0,I=null,re=!1,K=await _e.StreamReport.start({configuration:t,stdout:this.context.stdout},async g=>{let v=async(k,{commandIndex:l})=>{if(re)return-1;!y&&this.verbose&&l>1&&g.reportSeparator();let H=Kn(k,{configuration:t,verbose:this.verbose,commandIndex:l}),[w,B]=dr(g,{prefix:H,interlaced:E}),[o,u]=dr(g,{prefix:H,interlaced:E});try{this.verbose&&g.reportInfo(null,`${H} Process started`);let M=Date.now(),b=await this.cli.run([this.commandName,...this.args],{cwd:k.cwd,stdout:w,stderr:o})||0;w.end(),o.end(),await B,await u;let V=Date.now();if(this.verbose){let J=t.get("enableTimers")?`, completed in ${Y.formatUtils.pretty(t,V-M,Y.formatUtils.Type.DURATION)}`:"";g.reportInfo(null,`${H} Process exited (exit code ${b})${J}`)}return b===130&&(re=!0,I=b),b}catch(M){throw w.end(),o.end(),await B,await u,M}};for(let k of f)T.set(k.anchoredLocator.locatorHash,k);for(;T.size>0&&!g.hasErrors();){let k=[];for(let[w,B]of T){if(L.has(B.anchoredDescriptor.descriptorHash))continue;let o=!0;if(this.topological||this.topologicalDev){let u=this.topologicalDev?new Map([...B.manifest.dependencies,...B.manifest.devDependencies]):B.manifest.dependencies;for(let M of u.values()){let b=n.tryWorkspaceByDescriptor(M);if(o=b===null||!T.has(b.anchoredLocator.locatorHash),!o)break}}if(!!o&&(L.add(B.anchoredDescriptor.descriptorHash),k.push(S(async()=>{let u=await v(B,{commandIndex:++z});return T.delete(w),L.delete(B.anchoredDescriptor.descriptorHash),u})),!y))break}if(k.length===0){let w=Array.from(T.values()).map(B=>Y.structUtils.prettyLocator(t,B.anchoredLocator)).join(", ");g.reportError(_e.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${w})`);return}let H=(await Promise.all(k)).find(w=>w!==0);I===null&&(I=typeof H<"u"?1:I),(this.topological||this.topologicalDev)&&typeof H<"u"&&g.reportError(_e.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return I!==null?I:K.exitCode()}};pe.paths=[["workspaces","foreach"]],pe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});function dr(e,{prefix:r,interlaced:t}){let n=e.createStreamReporter(r),s=new Y.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let i=new Promise(c=>{n.on("finish",()=>{c(s.active)})});if(t)return[s,i];let a=new Y.miscUtils.BufferStream;return a.pipe(s,{end:!1}),a.on("finish",()=>{s.end()}),[a,i]}function Kn(e,{configuration:r,commandIndex:t,verbose:n}){if(!n)return null;let i=`[${Y.structUtils.stringifyIdent(e.locator)}]:`,a=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],c=a[t%a.length];return Y.formatUtils.pretty(r,i,c)}var Wn={commands:[ce,pe]},jn=Wn;return Sr(Fn);})(); 8 | /*! 9 | * fill-range 10 | * 11 | * Copyright (c) 2014-present, Jon Schlinkert. 12 | * Licensed under the MIT License. 13 | */ 14 | /*! 15 | * is-number 16 | * 17 | * Copyright (c) 2014-present, Jon Schlinkert. 18 | * Released under the MIT License. 19 | */ 20 | /*! 21 | * to-regex-range 22 | * 23 | * Copyright (c) 2015-present, Jon Schlinkert. 24 | * Released under the MIT License. 25 | */ 26 | return plugin; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: "@yarnpkg/plugin-workspace-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.4.1.cjs 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | Codename Hermes Logo 9 | 10 | 11 | 12 | 13 | # **Codename Hermes** 14 | 15 | ### A library to abstract away and standardize the initial setup and implementation of connecting with message brokers. 16 | 17 |
18 | 19 | 20 |
21 | 22 | ## Directory 23 | 24 | 25 | 26 | - [Standards](#standards) 27 | 28 | - [Broker Initilization](#standards-init) 29 | - [Producing](#standards-produce) 30 | - [Consuming](#standards-consume) 31 | 32 |
33 | 34 | - [Kafka](#kafka) 35 | 36 | - [Kafka Initilization](#kafka-init) 37 | - [Produce](#kafka-produce) 38 | - [Consume](#kafka-consume) 39 | 40 |
41 | 42 | - [Rabbit](#rabbit) 43 | - [Rabbit Initilization](#rabbit-init) 44 | - [Produce](#rabbit-produce) 45 | - [Consume](#rabbit-consume) 46 | 47 |
48 | 49 | - [Special Thanks](#special-thanks) 50 | - [Contribute](#contribute) 51 | - [Contributers](#contributers) 52 |
53 |
54 | 55 |
56 |
57 |
58 | 59 | 60 |
61 | 62 | ## **Standards** 63 | 64 | 65 | 66 | _Currently, these standards are in alpha and are subject to change as a result._ 67 | 68 | Our primary goal is to uniformize the implementation details of message brokers in Node.JS with the ultimate goal of expanding into other runtimes/languages in the future. 69 | 70 |
71 |
72 |
73 | 74 |
75 | 76 | ### **Initilization** 77 | 78 | Initilization of message brokers will follow the basic format shown below. 79 | 80 | ```TypeScript 81 | Broker(clientOptions: GenericClientOptions, topics: GenericTopic); 82 | ``` 83 | 84 | Currently, each broker will have it's own specific `clientOptions`, but will always contain `host` and `port` keys. 85 | 86 | ```TypeScript 87 | { 88 | host: string; 89 | port?: number; 90 | } 91 | ``` 92 | 93 | Additionally, each broker will have specific options for their topics. Though, they will always be in the format found below. 94 | 95 | ```TypeScript 96 | { 97 | [topicName: string]: brokerSpecificOptions; 98 | } 99 | ``` 100 | 101 |
102 | 103 |
104 |
105 |
106 | 107 |
108 | 109 | ### **Producing Events** 110 | 111 | Producing will be fairly straight forward for each message broker and will always follow the basic format shown below. 112 | 113 | ```TypeScript 114 | send(topicName: string, message: string) 115 | ``` 116 | 117 | We haven't implemented support for messaging multiple topics at once as of yet. 118 | 119 |
120 | 121 |
122 |
123 |
124 | 125 |
126 | 127 | ### **Consuming Events** 128 | 129 | Consuming will be more in-depth than producing, though just as simple. 130 | 131 | The first things first, we need to setup a listener for each topic we want to consume. This can be achieved through the basic syntax below. 132 | 133 | ```TypeScript 134 | listen(topics: string[], options: GenericListenerOptions) 135 | ``` 136 | 137 | GenericListenerOptions will always look like: 138 | 139 | ```TypeScript 140 | { 141 | autoCommit?: boolean; 142 | } 143 | ``` 144 | 145 |
146 | 147 | Creating a listener will then allow us to consume messages on that topic. This can be done via the basic syntax below. 148 | 149 | ```TypeScript 150 | consume(topicName: string, callback: MessageCallback) 151 | ``` 152 | 153 | A MessageCallback will always be based off the example below, although it may change from broker to broker. 154 | 155 | ```TypeScript 156 | (data: GenericMessage, error?: any) => void 157 | ``` 158 | 159 | Data will always look like: 160 | 161 | ```TypeScript 162 | { 163 | topic: string; 164 | message: string; 165 | } 166 | ``` 167 | 168 |
169 | 170 | A more complete example is as follows: 171 | 172 | ```TypeScript 173 | const topics = { 174 | topic1: {}, 175 | topic2: {}, 176 | } 177 | 178 | const broker = await Broker({ host: 'localhost' }, topics); 179 | 180 | broker.listen(['topic2'], { autoCommit: true }); 181 | 182 | broker.consume('topic2', (data, error) => console.log(data.message)); 183 | 184 | broker.send('topic1', 'Hello World!'); 185 | 186 | ``` 187 | 188 |
189 | 190 |
191 | 192 |
193 |
194 |
195 | 196 | 197 |
198 | 199 | ## **Kafka** 200 | 201 | 202 | 203 | Following the same [standards](#standards) as we've gone over previously; Our Kafka implementation will _hopefully_ seem fairly familiar. 204 | 205 |
206 |
207 |
208 | 209 | 210 | 211 |
212 | 213 | ## **Initilization** 214 | 215 | Much like you've seen, initializing Kafka will be the exact same as defined in the standards with the only differance being, we're assigning the returned value of the Kafka factory function to a variable. 216 | 217 | ```TypeScript 218 | const kafka = Kafka(clientOptions, topics); 219 | const kafka = Kafka(clientOptions, topics, callback); 220 | const kafka = Kafka(clientOptions, topics, producerOptions); 221 | const kafka = Kafka(clientOptions, topics, producerOptions, callback); 222 | ``` 223 | 224 | For now, another discrepancy would be Kafka specific `clientOptions` and `topics`. 225 | 226 | Speaking of, we have some new options! 227 | 228 |
229 | 230 | ### **`clientOptions`** 231 | 232 | ```TypeScript 233 | { 234 | host: string; 235 | port?: number; 236 | connectTimeout?: number; 237 | requestTimeout?: number; 238 | autoConnect?: boolean; 239 | connectRetryOptions?: RetryOptions; 240 | idleConnection?: number; 241 | reconnectOnIdle?: boolean; 242 | maxAsyncRequests?: number; 243 | } 244 | ``` 245 | 246 | But what do they do? Let's get into that. 247 | 248 |
249 | 250 | **`host`** - 251 | _Default: localhost_ 252 | 253 | The Kafka message broker hostname or IP address. 254 | 255 |
256 | 257 | **`port`** - 258 | _Default: 9092_ 259 | 260 | The port your Kafka message broker is on. 261 | 262 |
263 | 264 | **`connectTimeout`** - 265 | _Default: 10,000ms_ 266 | 267 | How long, in ms, it takes to wait for a successful connection. 268 | 269 |
270 | 271 | **`requestTimeout`** - 272 | _Default: 30,000ms_ 273 | 274 | How long, in ms, for a kafka request to timeout. 275 | 276 |
277 | 278 | **`autoConnect`** - 279 | _Default: true_ 280 | 281 | Should it automatically connect when Kafka is instantiated? 282 | 283 |
284 | 285 | **`connectRetryOptions`** - 286 | 287 | An object hash that applies to the initial connection to customize connection retries. 288 | 289 | ```TypeScript 290 | { 291 | retries?: number; /* (The maximum amount of times to retry the operation.) */ 292 | factor?: number; /* (The exponential factor to use.) */ 293 | minTimeout?: number; /* (The number of milliseconds before starting the first retry. Default is 1000.) */ 294 | maxTimeout?: number; /* (The maximum number of milliseconds between two retries. Default is Infinity.) */ 295 | randomize?: boolean; /* (Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false.) */ 296 | } 297 | ``` 298 | 299 | _For more information about `connectRetryOptions`, please visit [here](https://www.npmjs.com/package/retry#user-content-retrytimeoutsoptions)._ 300 | 301 |
302 | 303 | **`ildeConnection`** - 304 | _Default: 5 minutes_ 305 | 306 | Allows the broker to disconnect an idle connection from a client. 307 | 308 | The value is elapsed time in ms without any data written to the TCP socket. 309 | 310 |
311 | 312 | **`reconnectOnIdle`** - 313 | _Default: true_ 314 | 315 | When the connection is closed due to client idling, will the client attempt to auto-reconnect? 316 | 317 |
318 | 319 | **`maxAsyncRequests`** - 320 | _Default: 10_ 321 | 322 | The maximum async operations at a time toward the kafka cluster. 323 | 324 |
325 |
326 |
327 | 328 | ### **`topics`** 329 | 330 | Now that we've covered our Kafka specific `clientOptions`, let's get into our topic structure. 331 | 332 | As previously mentioned, we follow this format: 333 | 334 | ```TypeScript 335 | { 336 | [topicName: string]: KafkaTopic; 337 | } 338 | ``` 339 | 340 | What does that entail? 341 | 342 | Simply put, we're defining our topics as an object. This way we can have access to all of the information in our topics later on. 343 | 344 | An example of how a topic will look in your code is as follows: 345 | 346 | ```TypeScript 347 | const topics = { 348 | topic1: null, 349 | 350 | topic2: { 351 | partition: 2, 352 | }, 353 | 354 | topic3: { 355 | offset: 2, 356 | }, 357 | 358 | topic4: { 359 | partition: 2, 360 | offset: 2, 361 | }, 362 | } 363 | ``` 364 | 365 | Now, why do we have null? It's because we're using null to say "use the default options for this topic". 366 | 367 | The default options for a topic is: 368 | 369 | ```TypeScript 370 | { 371 | partition: 0, 372 | offset: 0, 373 | } 374 | ``` 375 | 376 | Alright, so you might be asking yourself: "What does the `partition` and `offset` do?" 377 | 378 | `partition` is just telling the Kafka broker "hey, this topic has X amount of sub-sections" and `offset` is saying "hey, start each partition at the offset X." 379 | 380 |
381 |
382 |
383 | 384 | ## **`producerOptions`** 385 | 386 | Producer options are a way for you to customize how you want to produce, or "send," messages to topics. 387 | 388 | ```TypeScript 389 | { 390 | requireAcks?: number; 391 | ackTimeoutMs?: number; 392 | partitionerType?: number; 393 | } 394 | ``` 395 | 396 | As usual, let's break down what everything does. 397 | 398 | **`requireAcks`** - 399 | _Default: 1_ 400 | 401 | The amount of acknowledgments the leader has to have received before considering a request complete. 402 | 403 |
404 | 405 | **`ackTimeoutMs`** - 406 | _Default: 100ms_ 407 | 408 | How long, in ms, to wait before considering acknowledgments. 409 | 410 |
411 | 412 | **`partitionerType`** - 413 | _Default: 2_ 414 | 415 | The algorithm that determines which partition to send messages to. 416 | 417 | default = 0 418 | 419 | random = 1 420 | 421 | cyclic = 2 422 | 423 | keyed = 3 424 | 425 | 426 | 427 | _Custom partitioners are currently in progess._ 428 | 429 |
430 | 431 | _For more information regarding these `producerOptions`, please refer to [this](https://kafka.apache.org/documentation/#producerconfigs)._ 432 | 433 |
434 |
435 |
436 | 437 | ## **`callback`** 438 | 439 | This callback is invoked when we successfully connect to the broker as a producer. It will also be invoked if there are any errors. 440 | 441 | If there is no error, the argument passed in will be `null`. 442 | 443 | ```TypeScript 444 | (error: any | null) => void; 445 | ``` 446 | 447 |
448 |
449 |
450 | 451 | ## **Example** 452 | 453 | Now that we have the background knowledge of what each argument is, let's see an example of how it would look in your code. 454 | 455 | ```TypeScript 456 | const clientOptions = { 457 | host: 'localhost', 458 | port: 9092, 459 | } 460 | 461 | const topics = { 462 | topic1: null, 463 | topic2: null, 464 | } 465 | 466 | const kafka = Kafka(clientOptions, topics); 467 | ``` 468 | 469 |
470 | 471 |
472 |
473 |
474 | 475 |
476 | 477 | ### **Producing** 478 | 479 | Sending messages to topics has been made simple and straight forward. 480 | 481 | The only information you need are the name of the topic and the message you want to send. 482 | 483 | Though, we do have options to specify where exactly you want the message to go as well as a callback to perform actions after the message has sent or if it errors out. 484 | 485 | _For now, we only support strings as the `message`._ 486 | 487 | ```TypeScript 488 | kafka.send(topicName, message); 489 | kafka.send(topicName, message, options); 490 | kafka.send(topicName, message, callback); 491 | kafka.send(topicName, message, options, callback); 492 | ``` 493 | 494 | ### **`options`** 495 | 496 | Send options are a way to specify where you want your message to be delivered. 497 | 498 | ```TypeScript 499 | { 500 | key?: string; 501 | partition?: number; 502 | attributes?: number; 503 | } 504 | ``` 505 | 506 | **`key`** - 507 | _Default: ''_ 508 | 509 | The key for the message. 510 | 511 | It's kind of like specifying the person you're sending a letter to. 512 | 513 |
514 | 515 | **`partition`** - 516 | _Default: 0_ 517 | 518 | What partition, or sub-section, you want to send the message to. 519 | 520 |
521 | 522 | **`attributes`** - 523 | _Default: 0_ 524 | 525 | Specifies whether or not you want to use compression for your message. 526 | 527 | No compression = 0 528 | 529 | GZip = 1 530 | 531 | snappy = 2 532 | 533 |
534 | 535 | ### **`callback`** 536 | 537 | This callback will be invoked when the message successfully send and if there is an error. 538 | 539 | If there was an error, that error will be passed as an argument. Otherwise it will be `null`. 540 | 541 | ```TypeScript 542 | (error: any | null) => void; 543 | ``` 544 | 545 |
546 | 547 |
548 |
549 |
550 | 551 |
552 | 553 | ## **Consuming** 554 | 555 | Compared to sending messages to topics, consuming them is a little more involve but still straight forward. 556 | 557 | As you can see below, you can provide listener options. Although, if you want to just use the defaults that's valid too. 558 | 559 | ```TypeScript 560 | kafka.consume(topics, callback) 561 | kafka.consume(topics, listenerOptions, callback) 562 | ``` 563 | 564 | First, let's overview what you can expect in the callback. 565 | 566 | **`callback`** - 567 | _Required_ 568 | 569 | ```TypeScript 570 | (message: KafkaMessage | null, error?: any) => void 571 | ``` 572 | 573 | A `KafkaMessage` contains the following: 574 | 575 | ```TypeScript 576 | { 577 | topic: string; 578 | message: string; 579 | offset?: number; 580 | partition?: number; 581 | highWaterOffset?: number; 582 | key?: string; 583 | } 584 | ``` 585 | 586 | Let's go a bit more in-depth about what each key does. 587 | 588 | **`topic`** 589 | 590 | The name of the topic a message was consumed from. 591 | 592 |
593 | 594 | **`message`** 595 | 596 | The message sent to the topic. 597 | 598 |
599 | 600 | **`offset`** 601 | 602 | The offset of the message. 603 | 604 | Think of appending to an array. 605 | 606 |
607 | 608 | **`partition`** 609 | 610 | The partition, or sub-section, on the topic a message was sent to. 611 | 612 |
613 | 614 | **`highWaterOffset`** 615 | 616 | The last offset that was successfully replicated to all topics. 617 | 618 |
619 | 620 | **`key`** 621 | 622 | The identifier for a message. 623 | 624 | For example: if you have a `gateway` topic and you need user data from the `auth` topic, you can specify a key to differenciate between other messages sent to the `gateway` topic. 625 | 626 |
627 | 628 | Now, the message may be null. This will only occur when there is an error. 629 | 630 | ### **`listenerOptions`** 631 | 632 | Finally, we can dive into what the listener options are and what they do. 633 | 634 | We provide a way to customize how you want listeners to behave. If you have no need for customization, there are also default options to fallback on. 635 | 636 | `KafkaListenerOptions` 637 | 638 | ```TypeScript 639 | { 640 | autoCommit?: boolean; 641 | groupId?: string; 642 | autoCommitIntervalMs?: number; 643 | fetchMaxWaitMs?: number; 644 | fetchMinBytes?: number; 645 | fetchMaxBytes?: number; 646 | fromOffset?: boolean; 647 | } 648 | ``` 649 | 650 | **`autoCommit`** - 651 | _Default: false_ 652 | 653 | `autoCommit` will automagically mark the consumed messages as "read" 654 | 655 |
656 | 657 | **`groupId`** - 658 | _Default: 'kafka-node-group'_ 659 | 660 | The consumer gorup id. 661 | 662 |
663 | 664 | **`autoCommitIntervalMs`** - 665 | _Default: 5000ms_ 666 | 667 | How often, in ms, will the consumer mark messages as "sent" 668 | 669 |
670 | 671 | **`fetchMaxWaitMs`** - 672 | _Default: 100ms_ 673 | 674 | The max time, in ms, to wait if there is insufficient data when receiving a message. 675 | 676 |
677 | 678 | **`fetchMinBytes`** - 679 | _Default: 1_ 680 | 681 | The minimum number of bytes that must be available to respond. 682 | 683 |
684 | 685 | **`fetchMaxBytes`** - 686 | _Default: 2048_ 687 | 688 | The maximum bytes to include in the message set for this partition. 689 | 690 |
691 | 692 | **`fromOffset`** - 693 | _Default: false_ 694 | 695 | If set true, consumer will fetch message from the given offset in the KafkaMessage. 696 | 697 |
698 | 699 | _As of now, we're working on allowing support for Buffers. The options for this will be provided on the `encoding` and `keyEncoding` keys._ 700 | 701 |
702 |
703 |
704 | 705 | ## **Example** 706 | 707 | Now, let's see an example of what we've learned so far and how it would look in your code. 708 | 709 | ```TypeScript 710 | const clientOptions = { 711 | host: 'localhost', 712 | port: 9092, 713 | } 714 | 715 | const topics = { 716 | topic1: null, 717 | topic2: null, 718 | } 719 | 720 | const kafka = Kafka(clientOptions, topics); 721 | 722 | kafka.consume('topic2', { autoCommit: true }, (data, error) => console.log(data.message)); 723 | 724 | kafka.send('topic2', 'Hello, topic2!'); 725 | ``` 726 | 727 |
728 | 729 |
730 | 731 |
732 |
733 |
734 | 735 | 736 |
737 | 738 | ## **RabbitMQ** 739 | 740 | ### **Initilization** 741 | 742 | Initilizing RabbitMQ is similar to the syntax you have seen previously with our standards and Kakfa. With RabbitMQ, we use the "createRabbitClass" factory function to instantiate a variable to act as our broker. 743 | 744 | Is that an await? Yes, yes it is. Our RabbitMQ message broker can be instantiated using promises. 745 | 746 | ```TypeScript 747 | 748 | const rabbit = await createRabbitClass(clientOptions, topics); 749 | 750 | ``` 751 | 752 |
753 | 754 | ### **`clientOptions`** 755 | 756 | With Codename Hermes, you can create a customaizable rabbit broker for any occasion thanks to the below client options. Continue reading to find out more! 757 | 758 | ```TypeScript 759 | { 760 | username?: string; 761 | password?: string; 762 | protocol?: 'amqp' | 'amqps'; 763 | vhost?: string; 764 | locale?: string; 765 | frameMax?: number; 766 | heartbeat?: number; 767 | } 768 | ``` 769 | 770 |
771 | 772 | **`username`** - 773 | _Default: guest_ 774 | 775 | User name to create when RabbitMQ creates a new database from scratch. 776 | 777 |
778 | 779 | **`password`** - 780 | _Default: guest_ 781 | 782 | Password for the default user. 783 | 784 |
785 | 786 | **`protocol`** - 787 | _Default: amqp_ 788 | 789 | Mandatory option to specify what messaging protocol is used for the broker. The Codename Hermes team, decided to utilize the `AMQP 0-9-1 messaging protocol` because it is a mature and widely adopted messaging protocol that provides the necessary features and capabilities for building robust, scalable, and interoperable message broker systems. 790 | 791 | Please refer to [AMQP 0-9-1](https://www.rabbitmq.com/tutorials/amqp-concepts.html#what-is-amqp) to learn more! 792 | 793 |
794 | 795 | **`vhost`** - 796 | _Default: '/'_ 797 | 798 | The name for the virtual host. RabbitMQ out of the box uses a virtual host named '/'. Changing this option beyond the default is needed when trying to create logical groupings and/or isolated messaging environments within your RabbitMQ broker. 799 | 800 |
801 | 802 | **`locale`** - 803 | _Default_: _en_US_ 804 | 805 | The desired locale for error messages. `NOTE:` RabbitMQ only ever uses en_US. 806 | 807 |
808 | 809 | **`frameMax`** - 810 | _Default: 0_ 811 | 812 | The size in bytes of the maximum frame allowed over the connection. 0 means no limit. Please refer to [frameMax](https://amqp-node.github.io/amqplib/channel_api.html#connect) for more inforamtion. 813 | 814 |
815 | 816 | **`heartbeat`** - 817 | _Default: 0_ 818 | 819 | The period of the connection heartbeat, in seconds. Please refer to [heartbeating](https://amqp-node.github.io/amqplib/channel_api.https://amqp-node.github.io/amqplib/channel_api.html#heartbeating) for more inforamtion. 820 | 821 |
822 | 823 | ### **`topics`** 824 | 825 | Now that you have decided on your options, it is time to determine what type of topics you will be. Let's keep reading to find out about our next topic, "topics". 826 | 827 | ```TypeScript 828 | { 829 | exchange: amqp.Options.AssertExchange & { 830 | name: string; 831 | type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match'; 832 | }; 833 | } & amqp.Options.AssertQueue & { 834 | key?: string; 835 | } 836 | 837 | ``` 838 | 839 |
840 | 841 | ### **_`exchange object`_** 842 | 843 | **`name`** - 844 | _Default: ' ' (empty string)_ 845 | 846 | Name of the topic exchange. Get creative and specific here. 847 | 848 |
849 | 850 | **`type`** - 851 | _Default: topic_ 852 | 853 | This option determines what type of exchange our RabbitMQ broker will use. Although a developer could use any rabbit exchange type with our library, the Codename Hermes team decided to default to using topics because topic exchanges support the publish-subscribe messaging model as well as supports the scalability and extensibility of messaging systems. 854 | 855 |
856 | 857 | #### **`amqp AssertExchange options`** 858 | 859 |
860 | 861 | **`durable`** - 862 | _Default: true_ 863 | 864 | if true, the exchange will survive broker restarts. 865 | 866 |
867 | 868 | **`internal`** - 869 | _Default: false_ 870 | 871 | if true, messages cannot be published directly to the exchange (i.e., it can only be the target of bindings, or possibly create messages ex-nihilo). 872 | 873 |
874 | 875 | **`autoDelete`** - 876 | _Default: false_ 877 | 878 | if true, the exchange will be destroyed once the number of bindings for which it is the source drop to zero. 879 | 880 |
881 | 882 | **`alternateExchange`** - 883 | _Default: ' ' (empty string)_ 884 | 885 | an exchange to send messages to if this exchange can’t route them to any queues. Think backup exchange. 886 | 887 |
888 | 889 | **`arguments`** - 890 | _Default: {} (empty object)_ 891 | 892 | any additional arguments that may be needed by an exchange type. 893 | 894 |
895 | 896 | Please refer to [AssertExchange options](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertExchange) for more inforamtion. 897 | 898 |
899 | 900 | ### **_`Additional Properties on topic object`_** 901 | 902 |
903 | 904 | **`key`** - 905 | _Default: ' ' (empty string)_ 906 | 907 | This value represents the binding and routing keys to ensure produced messages are consumed by the appropriate rabbit broker. 908 | 909 |
910 | 911 | #### **`amqp AssertQueue options`** 912 | 913 |
914 | 915 | **`exclusive`** - 916 | _Default: false_ 917 | 918 | if true, scopes the queue to the connection. 919 | 920 |
921 | 922 | **`durable`** - 923 | _Default: true_ 924 | 925 | if true, the queue will survive broker restarts, modulo the `effects` of exclusive and `autoDelete`. 926 | 927 |
928 | 929 | **`autoDelete`** - 930 | _Default: false_ 931 | 932 | if true, the queue will be deleted when the number of consumers drops to zero. 933 | 934 |
935 | 936 | **`arguments`** - 937 | _Default: {} (empty object)_ 938 | 939 | any additional arguments that may be needed by an exchange type. 940 | 941 |
942 | 943 | Please refer to [AssertQueue options](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue) for more inforamtion and potential arguments. 944 | 945 |
946 | An example of implementing a topic might look like: 947 | 948 | ```TypeScript 949 | const topics = { 950 | topic1: { 951 | exchange: { 952 | name: "topics", 953 | durable: false, 954 | type: "topic", 955 | }, 956 | durable: false, 957 | key: "hermes", 958 | }, 959 | }; 960 | ``` 961 | 962 |
963 | 964 |
965 | 966 | **`Produce`** 967 | 968 | In the wonderful world of Rabbit, we still produce messages with our broker using our standard `send` method. 969 | 970 |
971 | 972 | `NOTE:` the last argument is optional and is not required to send messages with your rabbit broker. 973 | 974 | ```TypeScript 975 | 976 | send(topic: string, message: string, options?: amqp.Options.Publish) 977 | ``` 978 | 979 |
980 | 981 | If you would like to enhance your messages, you are free to do so with the following publish options. 982 | 983 | #### **`amqp Publish options`** 984 | 985 |
986 | 987 | **`expiration`** - 988 | _Default: ' ' (empty string)_ 989 | 990 | If supplied, the message will be discarded from a queue once it’s been there longer than the given number of milliseconds. In the specification this is a string; numbers supplied here will be coerced to strings for transit. 991 | 992 |
993 | 994 | **`userId`** - 995 | _Default: ' ' (empty string)_ 996 | 997 | If supplied, RabbitMQ will compare it to the username supplied when opening the connection, and reject messages for which it does not match. 998 | 999 |
1000 | 1001 | **`CC`** - 1002 | _Default: ' ' (empty string)_ 1003 | 1004 | A routing key as a string or an array of routing keys as strings; messages will be routed to these routing keys in addition to that given as the routingKey parameter. A string will be implicitly treated as an array containing just that string. This will override any value given for CC in the headers parameter. NB The property names CC and BCC are case-sensitive. 1005 | 1006 |
1007 | 1008 | **`priority`** - 1009 | _Default: 0_ 1010 | 1011 | A priority for the message; ignored by versions of RabbitMQ older than 3.5.0, or if the queue is not a priority queue (see maxPriority above). 1012 | 1013 |
1014 | 1015 | **`persistent`** - 1016 | _Default: false_ 1017 | 1018 | If truthy, the message will survive broker restarts provided it’s in a queue that also survives restarts. Corresponds to, and overrides, the property deliveryMode. 1019 | 1020 |
1021 | 1022 | Please refer to [Publish options](https://amqp-node.github.io/amqplib/channel_api.html#channel_publish) for more inforamtion and potential arguments. 1023 | 1024 |
1025 | 1026 | _Quick example_ 1027 | 1028 | ```TypeScript 1029 | rabbit.send("topic1", "hello from sender.ts in ch lib test"); 1030 | 1031 | ``` 1032 | 1033 |
1034 | 1035 |
1036 | 1037 | **`Consume`** 1038 |
1039 | _Comming soon!_ 1040 | 1041 |
1042 |
1043 |
1044 | 1045 | ### **Example** 1046 | 1047 | ```TypeScript 1048 | 1049 | import ch, { RabbitTopic, createRabbitClass } from "library"; 1050 | 1051 | const clientOptions = { host: "localhost", port: 5672 }; 1052 | 1053 | const topics: RabbitTopic = { 1054 | topic1: { 1055 | exchange: { 1056 | name: "topics", 1057 | durable: false, 1058 | type: "topic", 1059 | }, 1060 | durable: false, 1061 | key: "hermes", 1062 | }, 1063 | }; 1064 | 1065 | const rabbit = await createRabbitClass(clientOptions,topics); 1066 | 1067 | rabbit.send("topic1", "hello from sender.ts in ch lib test"); 1068 | 1069 | ``` 1070 | 1071 | 1072 | 1073 |
1074 | 1075 |
1076 |
1077 |
1078 | 1079 |
1080 | 1081 | ## **Credits** 1082 | 1083 | A **major** special thanks to [kafka-node]() and [amqplib]() for allowing this project to have an accelerated launch! 1084 | 1085 |
1086 | 1087 | 1088 |
1089 | 1090 | ## **How to Contribute** 1091 | 1092 | Codename Hermes is currently in alpha, we would love to hear your feedback, encouragement, advice, suggestions, or problems! If you would like to contribute please contact us at info@hermeslib.com 1093 | 1094 |
1095 | 1096 |
1097 | 1098 | ## **Contributers** 1099 | 1100 | Aaron Allen - [LinkedIn](https://www.linkedin.com/in/aaronrallen/) [GitHub](https://github.com/H3R01A) 1101 | 1102 | Mathew Hultquist - [LinkedIn](https://www.linkedin.com/in/mathewjhultquist/) [GitHub](https://github.com/mjhult) 1103 | 1104 | Jose Mencos - [LinkedIn](https://www.linkedin.com/in/jmencos/) [GitHub](https://github.com/Jmencos) 1105 | 1106 | Samantha Mills - [LinkedIn](https://www.linkedin.com/in/samanthamills/) [GitHub](https://github.com/Slmills14) 1107 | 1108 |
1109 | -------------------------------------------------------------------------------- /media/CH-Logo-Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/media/CH-Logo-Black.png -------------------------------------------------------------------------------- /media/CH-Logo-White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/media/CH-Logo-White.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codename-hermes", 3 | "packageManager": "yarn@3.4.1", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "scripts": { 9 | "start": "yarn workspaces foreach -vpti run start", 10 | "start:dev": "yarn workspaces foreach -vpti run start:dev", 11 | "build": "yarn workspaces foreach -vpti run build", 12 | "library:install": "yarn workspaces foreach -vpti run library:install", 13 | "publish": "yarn workspaces library publish" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/demo_app/README.md: -------------------------------------------------------------------------------- 1 | # demo_app 2 | -------------------------------------------------------------------------------- /packages/demo_app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | # Needed for kafka 5 | zookeeper: 6 | image: wurstmeister/zookeeper 7 | ports: 8 | - '2181:2181' 9 | environment: 10 | - ALLOW_ANONYMOUS_LOGIN=yes 11 | 12 | # Kafka 13 | kafka: 14 | image: wurstmeister/kafka 15 | ports: 16 | - '9092:9092' 17 | environment: 18 | - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 19 | - KAFKA_ADVERTISED_HOST_NAME=localhost 20 | - KAFKA_CREATE_TOPICS=gateway:1:1,bidding:1:1 21 | - ALLOW_PLAINTEXT_LISTENER=yes 22 | depends_on: 23 | - zookeeper 24 | 25 | #TODO: Add services entries 26 | -------------------------------------------------------------------------------- /packages/demo_app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo_app", 3 | "packageManager": "yarn@3.4.1", 4 | "private": true, 5 | "workspaces": [ 6 | "services/*" 7 | ], 8 | "scripts": { 9 | "start": "yarn workspaces foreach -vpti run start", 10 | "docker:start": "yarn docker:build && docker-compose up", 11 | "docker:build": "yarn workspaces foreach -vpti run docker:build", 12 | "start:dev": "yarn workspaces foreach -vpti run start:dev", 13 | "build": "yarn workspaces foreach -vpti run build", 14 | "library:install": "yarn workspaces foreach -vpti run library:install" 15 | }, 16 | "resolutions": { 17 | "library": "portal:/Users/mathew/Documents/Code/Projects/Codename_Hermes/packages/library" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/demo_app/services/bidding/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | WORKDIR /src 3 | COPY ./* ./ 4 | RUN "yarn" "install" 5 | ENTRYPOINT [ "yarn", "start:dev" ] 6 | ENV KAFKA_HOST="localhost:9092" -------------------------------------------------------------------------------- /packages/demo_app/services/bidding/README.md: -------------------------------------------------------------------------------- 1 | # Billing 2 | -------------------------------------------------------------------------------- /packages/demo_app/services/bidding/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bidding", 3 | "packageManager": "yarn@3.3.1", 4 | "devDependencies": { 5 | "@types/kafka-node": "^3.0.0", 6 | "@types/node": "^18.14.0", 7 | "dotenv": "^16.0.3", 8 | "nodemon": "^2.0.20", 9 | "ts-node": "^10.9.1", 10 | "tsc": "^2.0.4", 11 | "typescript": "^4.9.5" 12 | }, 13 | "scripts": { 14 | "start": "yarn start:dev", 15 | "start:dev": "yarn nodemon src/service.ts", 16 | "build": "yarn tsc", 17 | "docker:build": "docker build . -t ch_demo-app_bidding", 18 | "library:install": "yarn add ../../../library && yarn link ../../../library" 19 | }, 20 | "dependencies": { 21 | "kafka-node": "^5.0.0", 22 | "library": "../../../library" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/demo_app/services/bidding/src/service.ts: -------------------------------------------------------------------------------- 1 | import { Kafka } from 'library'; 2 | 3 | const kafka = new Kafka( 4 | { 5 | host: 'localhost', 6 | port: 9092, 7 | }, 8 | { 9 | gateway: null, 10 | bidding: null, 11 | }, 12 | (err) => console.log(err ?? 'Bidding listening') 13 | ); 14 | 15 | type BidData = { 16 | id: number; 17 | currBid: number; 18 | newBid: number; 19 | code: string; 20 | }; 21 | 22 | kafka.consume('bidding', { autoCommit: true }, (err, message) => { 23 | // consumer.on('message', (message) => {}); 24 | 25 | if (err || !message) return; 26 | 27 | const bidData: BidData = JSON.parse(message.message as string); 28 | 29 | const newBidValue = 30 | bidData.newBid > bidData.currBid ? bidData.newBid : bidData.currBid; 31 | 32 | kafka.send( 33 | 'gateway', 34 | JSON.stringify({ newBidValue, code: bidData.code }), 35 | () => console.log(`Sent ${newBidValue} to gateway.`) 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/demo_app/services/bidding/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | "target": "ES6", 5 | "experimentalDecorators": true, 6 | "module": "commonjs", 7 | "rootDir": "./", 8 | "moduleResolution": "node", 9 | "baseUrl": "./src", 10 | "allowJs": false, 11 | "sourceMap": true, 12 | "outDir": "./build", 13 | "removeComments": true, 14 | "esModuleInterop": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "strict": true, 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | "strictFunctionTypes": true, 20 | "alwaysStrict": true, 21 | // "noUnusedLocals": true, 22 | // "noUnusedParameters": true, 23 | "noUncheckedIndexedAccess": true, 24 | "skipLibCheck": true 25 | }, 26 | 27 | "watchOptions": { 28 | "excludeDirectories": ["**/node_modules", "**/build"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start:dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "docker:build": "docker build . -t ch_demo-app_frontend", 11 | "library:install": "yarn add ../../../library && yarn link ../../../library" 12 | }, 13 | "dependencies": { 14 | "@next/font": "^13.2.4", 15 | "@types/node": "18.14.4", 16 | "@types/react": "18.0.28", 17 | "@types/react-dom": "18.0.11", 18 | "eslint": "8.35.0", 19 | "eslint-config-next": "13.2.3", 20 | "kafka-node": "^5.0.0", 21 | "library": "../../../library", 22 | "next": "13.2.3", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "sass": "^1.58.3", 26 | "typescript": "4.9.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/apirateslifeforme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/apirateslifeforme.png -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/dotsoncardstock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/dotsoncardstock.png -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/heart.png -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/seaturtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/seaturtle.png -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/starrynightmixedmedia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/starrynightmixedmedia.png -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/undertherainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/undertherainbow.png -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/underwaterseascape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Codename-Hermes/4b9b51faca473e577bcc2a9a13dc0cd8c94cf5ae/packages/demo_app/services/frontend/public/underwaterseascape.png -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/components/BiddingForm/BiddingForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import ImgDisplay from './ImageDisplay'; 3 | import styles from '../../styles/biddingform.module.css'; 4 | 5 | const BiddingForm = () => { 6 | return ( 7 |
8 | 9 |
10 | 15 |
16 | 17 |
18 | ); 19 | }; 20 | 21 | export default BiddingForm; 22 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/components/BiddingForm/biddingform.module.scss: -------------------------------------------------------------------------------- 1 | .biddingform { 2 | color: rgb(161, 131, 66); 3 | } -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/components/ImgDisplay/ImgDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import Image from 'next/image'; 3 | import styles from './imgstyledisplay.module.scss'; 4 | 5 | const ImgDisplay: FC<{ imgsrc: string; id: number; title: string }> = ({ 6 | imgsrc, 7 | id, 8 | title, 9 | }) => { 10 | const [currBid, setCurrBid] = useState(0); 11 | const [newBid, setNewBid] = useState(0); 12 | 13 | const handleClick = async () => { 14 | const response = await fetch('/api/bidRoute', { 15 | method: 'POST', 16 | body: JSON.stringify({ currBid, id, newBid }), 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | }); 21 | const data = await response.json(); 22 | console.log(data, 'frontend received'); 23 | setCurrBid(data); 24 | }; 25 | 26 | return ( 27 |
28 |
29 | 34 |
35 |

{title ?? 'Unnamed Work.'}

36 |

Current Bid: ${currBid}

37 | 38 | setNewBid(Number(e.target.value))}> 43 |
44 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default ImgDisplay; 51 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/components/ImgDisplay/imgstyledisplay.module.scss: -------------------------------------------------------------------------------- 1 | .imgdisplay { 2 | display: flex; 3 | flex-direction: row; 4 | background: radial-gradient(80% 40%, rgb(200, 45, 45), rgb(81, 28, 28)); 5 | /* background-color: rebeccapurple; */ 6 | padding: 10px; 7 | justify-content: space-evenly; 8 | 9 | div:first-of-type { 10 | border: solid #fefff3 2px; 11 | padding: 20px; 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | p, 17 | h2 { 18 | color: #fefff3; 19 | } 20 | 21 | img { 22 | margin: auto; 23 | width: 200px; 24 | height: auto; 25 | } 26 | } 27 | 28 | .bidimg { 29 | padding: 20px; 30 | } 31 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/components/NavBar/NavBar.module.scss: -------------------------------------------------------------------------------- 1 | .NavBar { 2 | display: flex; 3 | flex-direction: row; 4 | position: static; 5 | width: 100vw; 6 | background-color: rgb(246, 248, 250); 7 | box-shadow: 0px 12px 12px 12px #d0d3d8; 8 | margin-top: 2em; 9 | justify-content: center; 10 | height: 4em; 11 | div { 12 | display: flex; 13 | flex-grow: 1; 14 | height: 100%; 15 | p { 16 | margin: auto; 17 | text-align: center; 18 | font-size: x-large; 19 | // background-color: chartreuse; 20 | a:link{text-decoration: none;} 21 | } 22 | div{ 23 | p { 24 | font-size: 0.8em; 25 | transform: rotate(-90deg); 26 | margin: 1em 0; 27 | position: relative; 28 | 29 | } 30 | } 31 | } 32 | 33 | } 34 | 35 | 36 | .logo { 37 | font-size: larger; 38 | 39 | } -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './NavBar.module.scss'; 3 | import Link from 'next/link'; 4 | 5 | const NavBar = () => { 6 | return ( 7 |
8 |
9 |

CH Gallery

10 |
11 |

Est.

12 |

2023

13 |
14 |
15 |
16 |

Login

17 |
18 |
19 |

20 | 21 | Current Auction 22 | 23 |

24 |
25 |
26 |

Private Sales

27 |
28 |
29 |

Sell

30 |
31 |
32 | ); 33 | }; 34 | 35 | export default NavBar; 36 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.scss'; 2 | import type { AppProps } from 'next/app'; 3 | import { Poppins, Cinzel } from '@next/font/google'; 4 | 5 | const poppins = Poppins({ weight: '300', subsets: ['latin'] }); 6 | const cinzel = Cinzel({ weight: '500', subsets: ['latin'] }); 7 | 8 | export default function App({ Component, pageProps }: AppProps) { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | 16 | { 17 | /* */ 20 | } 21 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/api/bidRoute.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { Kafka } from 'library'; 4 | 5 | const kafka = new Kafka( 6 | { 7 | host: 'localhost', 8 | port: 9092, 9 | }, 10 | { 11 | gateway: null, 12 | bidding: null, 13 | }, 14 | (err) => console.log(err ?? 'Bidding listening') 15 | ); 16 | 17 | type KafkaData = { 18 | newBidValue: number; 19 | code: string; 20 | }; 21 | 22 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 23 | const secret = Math.random().toString(32).substring(3); 24 | const { currBid, id, newBid } = req.body; 25 | const message = { 26 | id, 27 | currBid, 28 | newBid, 29 | code: secret, 30 | }; 31 | 32 | // Send the message to the bidding service 33 | kafka.send('bidding', JSON.stringify(message), () => 34 | console.log(`\n\nSent ${newBid} to the bidding service.\n\n`) 35 | ); 36 | 37 | // When the gateway receives a response, send that to the client. 38 | // This is probobly really bad practice because this will accept any response even when there is no client waiting for a response. 39 | kafka.consume('gateway', { autoCommit: true }, (err, message) => { 40 | if (err || !message) return; 41 | const { newBidValue, code } = JSON.parse( 42 | message.message as string 43 | ) as KafkaData; 44 | if (code !== secret) { 45 | return; 46 | } 47 | console.log(`Received message: ${message.message}`); 48 | res.status(200).json(newBidValue); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/checkout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Checkout = () => { 4 | return
; 5 | }; 6 | 7 | export default Checkout; 8 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | type Count = { 4 | count: number; 5 | }; 6 | 7 | export default function Main() { 8 | const [count, setCount] = useState(null); 9 | 10 | useEffect(() => { 11 | if (!count) 12 | fetch('/api/count') 13 | .then((d) => d.json()) 14 | .then((d: Count) => setCount(d.count)); 15 | }); 16 | 17 | const increment = () => { 18 | fetch('/api/count', { 19 | method: 'POST', 20 | }) 21 | .then((d) => d.json()) 22 | .then((d: Count) => setCount(d.count)); 23 | }; 24 | 25 | return ( 26 |
27 |

Kafka counter test

28 |

29 | Your current count is:{' '} 30 | {count ?? 'Waiting for count.'} 31 |

32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/imgbidding.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import BiddingForm from '../src/components/BiddingForm'; 3 | import ImgDisplay from '../components/ImgDisplay/ImgDisplay'; 4 | import apirateslifeforme from '../../public/apirateslifeforme.png'; 5 | import dotsoncardstock from '../../public/dotsoncardstock.png'; 6 | import heart from '../../public/heart.png'; 7 | import seaturtle from '../../public/seaturtle.png'; 8 | import starrynightmixedmedia from '../../public/starrynightmixedmedia.png'; 9 | import undertherainbow from '../../public/undertherainbow.png'; 10 | import underwaterseascape from '../../public/underwaterseascape.png'; 11 | 12 | interface Art { 13 | title: string; 14 | file: string; 15 | } 16 | 17 | const artWork: Art[] = [ 18 | { title: 'A Pirates Life for Me', file: apirateslifeforme }, 19 | { title: 'Dots on Cardstock', file: dotsoncardstock }, 20 | { title: 'Heart', file: heart }, 21 | { title: 'Sea Turtle', file: seaturtle }, 22 | { title: 'Starry Night Mixed Media', file: starrynightmixedmedia }, 23 | { title: 'Under the Rainbow', file: undertherainbow }, 24 | { title: 'Underwater Seascape', file: underwaterseascape }, 25 | ]; 26 | 27 | const ImgBidding = () => { 28 | return ( 29 |
30 | {artWork.map((el, i) => ( 31 | 32 | ))} 33 |
34 | ); 35 | }; 36 | 37 | export default ImgBidding; 38 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | import styles from '../styles/index.module.scss'; 4 | import apirateslifeforme from '../../public/apirateslifeforme.png'; 5 | import dotsoncardstock from '../../public/dotsoncardstock.png'; 6 | import heart from '../../public/heart.png'; 7 | import seaturtle from '../../public/seaturtle.png'; 8 | import starrynightmixedmedia from '../../public/starrynightmixedmedia.png'; 9 | import undertherainbow from '../../public/undertherainbow.png'; 10 | import underwaterseascape from '../../public/underwaterseascape.png'; 11 | import NavBar from '../components/NavBar/NavBar'; 12 | import { Poppins } from '@next/font/google'; 13 | 14 | interface Art { 15 | title: string; 16 | file: string; 17 | } 18 | 19 | const artWork: Art[] = [ 20 | { title: 'A Pirates Life for Me', file: apirateslifeforme }, 21 | { title: 'Dots on Cardstock', file: dotsoncardstock }, 22 | { title: 'Heart', file: heart }, 23 | { title: 'Sea Turtle', file: seaturtle }, 24 | { title: 'Starry Night Mixed Media', file: starrynightmixedmedia }, 25 | { title: 'Under the Rainbow', file: undertherainbow }, 26 | { title: 'Underwater Seascape', file: underwaterseascape }, 27 | ]; 28 | 29 | const Gallery = () => { 30 | return ( 31 |
32 | 33 |
Welcome to CH Gallery
34 |
35 |

36 | This exhibit features a diverse array of works created with a variety of 37 | mediums, including crayon, marker, pencil, and even flower petals. The 38 | pieces on display showcase a range of subjects, from realistic 39 | depictions of everyday life to whimsical images of fantasy and 40 | imagination. Each piece in the collection is a testament to the 41 | creativity and skill of its artist. 42 |

43 |
44 |

45 | The crayon drawings evoke a sense of childhood nostalgia, with bright, 46 | bold colors and simple yet charming compositions. The collection also 47 | includes pieces created with unconventional materials, such as flower 48 | petals. These works incorporate natural elements in a way that is both 49 | beautiful and thought-provoking. 50 |

51 |
52 |

53 | Whether the images are based on real life or the imagination, each piece 54 | in this exhibit is a unique expression of the {"artist's"} vision. We 55 | invite you to take a closer look and discover the beauty and diversity 56 | of these wonderful works of art. 57 |

58 | 59 |
60 |
61 | {artWork.map((obj, index) => ( 62 |
63 | {"Mabel's 64 |

{obj.title}

65 |
66 | ))} 67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default Gallery; 74 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Login = () => { 4 | return
log in here!
; 5 | }; 6 | 7 | export default Login; 8 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/pages/userhomepage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UserPage = () => { 4 | return
; 5 | }; 6 | 7 | export default UserPage; 8 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | width: var(--max-width); 46 | max-width: 100%; 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo, 108 | .thirteen { 109 | position: relative; 110 | } 111 | 112 | .thirteen { 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | width: 75px; 117 | height: 75px; 118 | padding: 25px 10px; 119 | margin-left: 16px; 120 | transform: translateZ(0); 121 | border-radius: var(--border-radius); 122 | overflow: hidden; 123 | box-shadow: 0px 2px 8px -1px #0000001a; 124 | } 125 | 126 | .thirteen::before, 127 | .thirteen::after { 128 | content: ''; 129 | position: absolute; 130 | z-index: -1; 131 | } 132 | 133 | /* Conic Gradient Animation */ 134 | .thirteen::before { 135 | animation: 6s rotate linear infinite; 136 | width: 200%; 137 | height: 200%; 138 | background: var(--tile-border); 139 | } 140 | 141 | /* Inner Square */ 142 | .thirteen::after { 143 | inset: 0; 144 | padding: 1px; 145 | border-radius: var(--border-radius); 146 | background: linear-gradient( 147 | to bottom right, 148 | rgba(var(--tile-start-rgb), 1), 149 | rgba(var(--tile-end-rgb), 1) 150 | ); 151 | background-clip: content-box; 152 | } 153 | 154 | /* Enable hover only on non-touch devices */ 155 | @media (hover: hover) and (pointer: fine) { 156 | .card:hover { 157 | background: rgba(var(--card-rgb), 0.1); 158 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 159 | } 160 | 161 | .card:hover span { 162 | transform: translateX(4px); 163 | } 164 | } 165 | 166 | @media (prefers-reduced-motion) { 167 | .thirteen::before { 168 | animation: none; 169 | } 170 | 171 | .card:hover span { 172 | transform: none; 173 | } 174 | } 175 | 176 | /* Mobile */ 177 | @media (max-width: 700px) { 178 | .content { 179 | padding: 4rem; 180 | } 181 | 182 | .grid { 183 | grid-template-columns: 1fr; 184 | margin-bottom: 120px; 185 | max-width: 320px; 186 | text-align: center; 187 | } 188 | 189 | .card { 190 | padding: 1rem 2.5rem; 191 | } 192 | 193 | .card h2 { 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | .center { 198 | padding: 8rem 0 6rem; 199 | } 200 | 201 | .center::before { 202 | transform: none; 203 | height: 300px; 204 | } 205 | 206 | .description { 207 | font-size: 0.8rem; 208 | } 209 | 210 | .description a { 211 | padding: 1rem; 212 | } 213 | 214 | .description p, 215 | .description div { 216 | display: flex; 217 | justify-content: center; 218 | position: fixed; 219 | width: 100%; 220 | } 221 | 222 | .description p { 223 | align-items: center; 224 | inset: 0 0 auto; 225 | padding: 2rem 1rem 1.4rem; 226 | border-radius: 0; 227 | border: none; 228 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 229 | background: linear-gradient( 230 | to bottom, 231 | rgba(var(--background-start-rgb), 1), 232 | rgba(var(--callout-rgb), 0.5) 233 | ); 234 | background-clip: padding-box; 235 | backdrop-filter: blur(24px); 236 | } 237 | 238 | .description div { 239 | align-items: flex-end; 240 | pointer-events: none; 241 | inset: auto 0 0; 242 | padding: 2rem; 243 | height: 200px; 244 | background: linear-gradient( 245 | to bottom, 246 | transparent 0%, 247 | rgb(var(--background-end-rgb)) 40% 248 | ); 249 | z-index: 1; 250 | } 251 | } 252 | 253 | /* Tablet and Smaller Desktop */ 254 | @media (min-width: 701px) and (max-width: 1120px) { 255 | .grid { 256 | grid-template-columns: repeat(2, 50%); 257 | } 258 | } 259 | 260 | @media (prefers-color-scheme: dark) { 261 | .vercelLogo { 262 | filter: invert(1); 263 | } 264 | 265 | .logo, 266 | .thirteen img { 267 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 268 | } 269 | } 270 | 271 | @keyframes rotate { 272 | from { 273 | transform: rotate(360deg); 274 | } 275 | to { 276 | transform: rotate(0deg); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | .galImg { 2 | max-height: 300px; 3 | } 4 | 5 | .galDisp { 6 | columns: 500px 2; 7 | } 8 | 9 | body, 10 | html { 11 | padding: 0px; 12 | margin: 0px; 13 | } 14 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/styles/index.module.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .galImg { 4 | display: flex; 5 | flex-direction: column; 6 | margin: auto; 7 | margin-bottom: 5em; 8 | img{ 9 | height: 30em; 10 | width: auto; 11 | border: outset; 12 | border-width:1em; 13 | border-color: #63c2ee; 14 | margin: auto; 15 | // margin-bottom: 5em; 16 | } 17 | p{ 18 | text-align: center; 19 | font-size:2em; 20 | 21 | } 22 | } 23 | 24 | .galDisp { 25 | display: flex; 26 | flex-direction: row; 27 | flex-wrap: wrap; 28 | width: 80vw; 29 | margin: auto; 30 | margin-top: 5em; 31 | 32 | } 33 | 34 | .galHeading { 35 | width: 76vw; 36 | height: auto; 37 | font-weight: 900; 38 | font-size: 4em; 39 | margin: auto; 40 | margin-top: 1em; 41 | align-self: center; 42 | justify-content: center; 43 | text-align: center; 44 | } 45 | 46 | .galDescrip { 47 | width: 75vw; 48 | font-weight: 400; 49 | font-size: 2em; 50 | margin: auto; 51 | justify-content: center; 52 | } 53 | 54 | .text { 55 | font-family: var(--poppins-font); 56 | font-weight: 300; 57 | } 58 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' { 2 | const value: string; 3 | export = value; 4 | } 5 | 6 | declare module '*.jpeg' { 7 | const value: string; 8 | export = value; 9 | } 10 | 11 | declare module '*.png' { 12 | const value: string; 13 | export = value; 14 | } 15 | 16 | declare module '*.webp' { 17 | const value: string; 18 | export = value; 19 | } 20 | 21 | declare module '*.svg' { 22 | const value: string; 23 | export = value; 24 | } 25 | -------------------------------------------------------------------------------- /packages/demo_app/services/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/landing/README.md: -------------------------------------------------------------------------------- 1 | # landing 2 | -------------------------------------------------------------------------------- /packages/landing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "landing", 3 | "packageManager": "yarn@3.3.1", 4 | "scripts": { 5 | "start": "yarn start:dev", 6 | "start:dev": "yarn webpack serve", 7 | "build": "yarn webpack" 8 | }, 9 | "dependencies": { 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0" 12 | }, 13 | "devDependencies": { 14 | "@svgr/webpack": "^6.5.1", 15 | "@types/node": "^18.14.0", 16 | "@types/react": "^18.0.28", 17 | "@types/react-dom": "^18.0.11", 18 | "css-loader": "^6.7.3", 19 | "dotenv": "^16.0.3", 20 | "file-loader": "^6.2.0", 21 | "html-webpack-plugin": "^5.5.0", 22 | "nodemon": "^2.0.20", 23 | "sass-loader": "^13.2.0", 24 | "style-loader": "^3.3.1", 25 | "ts-loader": "^9.4.2", 26 | "ts-node": "^10.9.1", 27 | "tsc": "^2.0.4", 28 | "typescript": "^4.9.5", 29 | "webpack": "^5.75.0", 30 | "webpack-cli": "^5.0.1", 31 | "webpack-dev-server": "^4.11.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/landing/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Codeman Hermes 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/landing/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | const App = () =>

Hello world!

; 5 | 6 | createRoot(document.querySelector('#AppRoot')!).render(); 7 | -------------------------------------------------------------------------------- /packages/landing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | "target": "ES6", 5 | "lib": ["DOM"], 6 | "jsx": "react", 7 | "experimentalDecorators": true, 8 | "module": "commonjs", 9 | "rootDir": "./", 10 | "moduleResolution": "node", 11 | "baseUrl": "./src", 12 | "allowJs": false, 13 | "sourceMap": true, 14 | "outDir": "./build", 15 | "removeComments": true, 16 | "esModuleInterop": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "strict": true, 19 | "noImplicitAny": true, 20 | "strictNullChecks": true, 21 | "strictFunctionTypes": true, 22 | "alwaysStrict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noUncheckedIndexedAccess": true, 26 | "skipLibCheck": true 27 | }, 28 | 29 | "watchOptions": { 30 | "excludeDirectories": ["**/node_modules", "**/build"] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/landing/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | require('dotenv').config(); 3 | const process = require('process'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | // Determine which env to build for 8 | mode: process.env.TARGET ?? 'production', 9 | 10 | // The main file for the project 11 | entry: './src/index.tsx', 12 | 13 | module: { 14 | rules: [ 15 | /** 16 | * If the file is .tsx or .ts, run the ts-loader extension on it. 17 | * Ignore the node_modules directory 18 | */ 19 | { 20 | test: /\.tsx?$/, 21 | use: 'ts-loader', 22 | exclude: [/node_modules/], 23 | }, 24 | 25 | /** 26 | * If the file end with .sass, .scss, or .css then run the loaders defined in the "use" key from right to left. 27 | */ 28 | { 29 | test: /\.s[ac]ss$/i, 30 | use: ['style-loader', 'css-loader', 'sass-loader'], 31 | }, 32 | 33 | /** 34 | * This will check if the file is an image and it will get it, copy it to the build dir, then store a referance to it. 35 | */ 36 | { 37 | test: /\.(png|jpe?g|gif)$/i, 38 | use: [ 39 | { 40 | loader: 'file-loader', 41 | }, 42 | ], 43 | }, 44 | 45 | // This will transform any SVG into a react component to be used. 46 | { 47 | test: /\.svg$/, 48 | use: ['@svgr/webpack'], 49 | }, 50 | ], 51 | }, 52 | 53 | resolve: { 54 | // Which file extensions do we want to resolve from imports? 55 | extensions: ['.tsx', '.ts', '.js'], 56 | }, 57 | 58 | // Webpack dev server 59 | devServer: { 60 | static: './build', 61 | port: process.env.PORT ?? 8082, 62 | watchFiles: { 63 | // Which files do you want to monitor for changes so you can hot-reload them? 64 | paths: ['src/**/*'], 65 | }, 66 | }, 67 | 68 | // What do you want to have the final bundle be called? Where do you want it stored? 69 | output: { 70 | filename: 'bundle.js', 71 | path: path.resolve(__dirname, 'build'), 72 | }, 73 | 74 | // Any extra functionality? 75 | plugins: [ 76 | // This will create a new index.html file based on the specified template. 77 | new HtmlWebpackPlugin({ 78 | template: './src/index.html', 79 | }), 80 | ], 81 | }; 82 | -------------------------------------------------------------------------------- /packages/library/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | !lib 3 | node_modules 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /packages/library/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | Codename Hermes Logo 9 | 10 | 11 | 12 | 13 | # **Codename Hermes** 14 | 15 | ### A library to abstract away and standardize the initial setup and implementation of connecting with message brokers. 16 | 17 |
18 | 19 | 20 |
21 | 22 | ## Directory 23 | 24 | 25 | 26 | - [Standards](#standards) 27 | 28 | - [Broker Initilization](#standards-init) 29 | - [Producing](#standards-produce) 30 | - [Consuming](#standards-consume) 31 | 32 |
33 | 34 | - [Kafka](#kafka) 35 | 36 | - [Kafka Initilization](#kafka-init) 37 | - [Produce](#kafka-produce) 38 | - [Consume](#kafka-consume) 39 | 40 |
41 | 42 | - [Rabbit](#rabbit) 43 | - [Rabbit Initilization](#rabbit-init) 44 | - [Produce](#rabbit-produce) 45 | - [Consume](#rabbit-consume) 46 | 47 |
48 | 49 | - [Special Thanks](#special-thanks) 50 | - [Contribute](#contribute) 51 | - [Contributers](#contributers) 52 |
53 |
54 | 55 |
56 |
57 |
58 | 59 | 60 |
61 | 62 | ## **Standards** 63 | 64 | 65 | 66 | _Currently, these standards are in alpha and are subject to change as a result._ 67 | 68 | Our primary goal is to uniformize the implementation details of message brokers in Node.JS with the ultimate goal of expanding into other runtimes/languages in the future. 69 | 70 |
71 |
72 |
73 | 74 |
75 | 76 | ### **Initilization** 77 | 78 | Initilization of message brokers will follow the basic format shown below. 79 | 80 | ```TypeScript 81 | Broker(clientOptions: GenericClientOptions, topics: GenericTopic); 82 | ``` 83 | 84 | Currently, each broker will have it's own specific `clientOptions`, but will always contain `host` and `port` keys. 85 | 86 | ```TypeScript 87 | { 88 | host: string; 89 | port?: number; 90 | } 91 | ``` 92 | 93 | Additionally, each broker will have specific options for their topics. Though, they will always be in the format found below. 94 | 95 | ```TypeScript 96 | { 97 | [topicName: string]: brokerSpecificOptions; 98 | } 99 | ``` 100 | 101 |
102 | 103 |
104 |
105 |
106 | 107 |
108 | 109 | ### **Producing Events** 110 | 111 | Producing will be fairly straight forward for each message broker and will always follow the basic format shown below. 112 | 113 | ```TypeScript 114 | send(topicName: string, message: string) 115 | ``` 116 | 117 | We haven't implemented support for messaging multiple topics at once as of yet. 118 | 119 |
120 | 121 |
122 |
123 |
124 | 125 |
126 | 127 | ### **Consuming Events** 128 | 129 | Consuming will be more in-depth than producing, though just as simple. 130 | 131 | The first things first, we need to setup a listener for each topic we want to consume. This can be achieved through the basic syntax below. 132 | 133 | ```TypeScript 134 | listen(topics: string[], options: GenericListenerOptions) 135 | ``` 136 | 137 | GenericListenerOptions will always look like: 138 | 139 | ```TypeScript 140 | { 141 | autoCommit?: boolean; 142 | } 143 | ``` 144 | 145 |
146 | 147 | Creating a listener will then allow us to consume messages on that topic. This can be done via the basic syntax below. 148 | 149 | ```TypeScript 150 | consume(topicName: string, callback: MessageCallback) 151 | ``` 152 | 153 | A MessageCallback will always be based off the example below, although it may change from broker to broker. 154 | 155 | ```TypeScript 156 | (data: GenericMessage, error?: any) => void 157 | ``` 158 | 159 | Data will always look like: 160 | 161 | ```TypeScript 162 | { 163 | topic: string; 164 | message: string; 165 | } 166 | ``` 167 | 168 |
169 | 170 | A more complete example is as follows: 171 | 172 | ```TypeScript 173 | const topics = { 174 | topic1: {}, 175 | topic2: {}, 176 | } 177 | 178 | const broker = await Broker({ host: 'localhost' }, topics); 179 | 180 | broker.listen(['topic2'], { autoCommit: true }); 181 | 182 | broker.consume('topic2', (data, error) => console.log(data.message)); 183 | 184 | broker.send('topic1', 'Hello World!'); 185 | 186 | ``` 187 | 188 |
189 | 190 |
191 | 192 |
193 |
194 |
195 | 196 | 197 |
198 | 199 | ## **Kafka** 200 | 201 | 202 | 203 | Following the same [standards](#standards) as we've gone over previously; Our Kafka implementation will _hopefully_ seem fairly familiar. 204 | 205 |
206 |
207 |
208 | 209 | 210 | 211 |
212 | 213 | ## **Initilization** 214 | 215 | Much like you've seen, initializing Kafka will be the exact same as defined in the standards with the only differance being, we're assigning the returned value of the Kafka factory function to a variable. 216 | 217 | ```TypeScript 218 | const kafka = Kafka(clientOptions, topics); 219 | const kafka = Kafka(clientOptions, topics, callback); 220 | const kafka = Kafka(clientOptions, topics, producerOptions); 221 | const kafka = Kafka(clientOptions, topics, producerOptions, callback); 222 | ``` 223 | 224 | For now, another discrepancy would be Kafka specific `clientOptions` and `topics`. 225 | 226 | Speaking of, we have some new options! 227 | 228 |
229 | 230 | ### **`clientOptions`** 231 | 232 | ```TypeScript 233 | { 234 | host: string; 235 | port?: number; 236 | connectTimeout?: number; 237 | requestTimeout?: number; 238 | autoConnect?: boolean; 239 | connectRetryOptions?: RetryOptions; 240 | idleConnection?: number; 241 | reconnectOnIdle?: boolean; 242 | maxAsyncRequests?: number; 243 | } 244 | ``` 245 | 246 | But what do they do? Let's get into that. 247 | 248 |
249 | 250 | **`host`** - 251 | _Default: localhost_ 252 | 253 | The Kafka message broker hostname or IP address. 254 | 255 |
256 | 257 | **`port`** - 258 | _Default: 9092_ 259 | 260 | The port your Kafka message broker is on. 261 | 262 |
263 | 264 | **`connectTimeout`** - 265 | _Default: 10,000ms_ 266 | 267 | How long, in ms, it takes to wait for a successful connection. 268 | 269 |
270 | 271 | **`requestTimeout`** - 272 | _Default: 30,000ms_ 273 | 274 | How long, in ms, for a kafka request to timeout. 275 | 276 |
277 | 278 | **`autoConnect`** - 279 | _Default: true_ 280 | 281 | Should it automatically connect when Kafka is instantiated? 282 | 283 |
284 | 285 | **`connectRetryOptions`** - 286 | 287 | An object hash that applies to the initial connection to customize connection retries. 288 | 289 | ```TypeScript 290 | { 291 | retries?: number; /* (The maximum amount of times to retry the operation.) */ 292 | factor?: number; /* (The exponential factor to use.) */ 293 | minTimeout?: number; /* (The number of milliseconds before starting the first retry. Default is 1000.) */ 294 | maxTimeout?: number; /* (The maximum number of milliseconds between two retries. Default is Infinity.) */ 295 | randomize?: boolean; /* (Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false.) */ 296 | } 297 | ``` 298 | 299 | _For more information about `connectRetryOptions`, please visit [here](https://www.npmjs.com/package/retry#user-content-retrytimeoutsoptions)._ 300 | 301 |
302 | 303 | **`ildeConnection`** - 304 | _Default: 5 minutes_ 305 | 306 | Allows the broker to disconnect an idle connection from a client. 307 | 308 | The value is elapsed time in ms without any data written to the TCP socket. 309 | 310 |
311 | 312 | **`reconnectOnIdle`** - 313 | _Default: true_ 314 | 315 | When the connection is closed due to client idling, will the client attempt to auto-reconnect? 316 | 317 |
318 | 319 | **`maxAsyncRequests`** - 320 | _Default: 10_ 321 | 322 | The maximum async operations at a time toward the kafka cluster. 323 | 324 |
325 |
326 |
327 | 328 | ### **`topics`** 329 | 330 | Now that we've covered our Kafka specific `clientOptions`, let's get into our topic structure. 331 | 332 | As previously mentioned, we follow this format: 333 | 334 | ```TypeScript 335 | { 336 | [topicName: string]: KafkaTopic; 337 | } 338 | ``` 339 | 340 | What does that entail? 341 | 342 | Simply put, we're defining our topics as an object. This way we can have access to all of the information in our topics later on. 343 | 344 | An example of how a topic will look in your code is as follows: 345 | 346 | ```TypeScript 347 | const topics = { 348 | topic1: null, 349 | 350 | topic2: { 351 | partition: 2, 352 | }, 353 | 354 | topic3: { 355 | offset: 2, 356 | }, 357 | 358 | topic4: { 359 | partition: 2, 360 | offset: 2, 361 | }, 362 | } 363 | ``` 364 | 365 | Now, why do we have null? It's because we're using null to say "use the default options for this topic". 366 | 367 | The default options for a topic is: 368 | 369 | ```TypeScript 370 | { 371 | partition: 0, 372 | offset: 0, 373 | } 374 | ``` 375 | 376 | Alright, so you might be asking yourself: "What does the `partition` and `offset` do?" 377 | 378 | `partition` is just telling the Kafka broker "hey, this topic has X amount of sub-sections" and `offset` is saying "hey, start each partition at the offset X." 379 | 380 |
381 |
382 |
383 | 384 | ## **`producerOptions`** 385 | 386 | Producer options are a way for you to customize how you want to produce, or "send," messages to topics. 387 | 388 | ```TypeScript 389 | { 390 | requireAcks?: number; 391 | ackTimeoutMs?: number; 392 | partitionerType?: number; 393 | } 394 | ``` 395 | 396 | As usual, let's break down what everything does. 397 | 398 | **`requireAcks`** - 399 | _Default: 1_ 400 | 401 | The amount of acknowledgments the leader has to have received before considering a request complete. 402 | 403 |
404 | 405 | **`ackTimeoutMs`** - 406 | _Default: 100ms_ 407 | 408 | How long, in ms, to wait before considering acknowledgments. 409 | 410 |
411 | 412 | **`partitionerType`** - 413 | _Default: 2_ 414 | 415 | The algorithm that determines which partition to send messages to. 416 | 417 | default = 0 418 | 419 | random = 1 420 | 421 | cyclic = 2 422 | 423 | keyed = 3 424 | 425 | 426 | 427 | _Custom partitioners are currently in progess._ 428 | 429 |
430 | 431 | _For more information regarding these `producerOptions`, please refer to [this](https://kafka.apache.org/documentation/#producerconfigs)._ 432 | 433 |
434 |
435 |
436 | 437 | ## **`callback`** 438 | 439 | This callback is invoked when we successfully connect to the broker as a producer. It will also be invoked if there are any errors. 440 | 441 | If there is no error, the argument passed in will be `null`. 442 | 443 | ```TypeScript 444 | (error: any | null) => void; 445 | ``` 446 | 447 |
448 |
449 |
450 | 451 | ## **Example** 452 | 453 | Now that we have the background knowledge of what each argument is, let's see an example of how it would look in your code. 454 | 455 | ```TypeScript 456 | const clientOptions = { 457 | host: 'localhost', 458 | port: 9092, 459 | } 460 | 461 | const topics = { 462 | topic1: null, 463 | topic2: null, 464 | } 465 | 466 | const kafka = Kafka(clientOptions, topics); 467 | ``` 468 | 469 |
470 | 471 |
472 |
473 |
474 | 475 |
476 | 477 | ### **Producing** 478 | 479 | Sending messages to topics has been made simple and straight forward. 480 | 481 | The only information you need are the name of the topic and the message you want to send. 482 | 483 | Though, we do have options to specify where exactly you want the message to go as well as a callback to perform actions after the message has sent or if it errors out. 484 | 485 | _For now, we only support strings as the `message`._ 486 | 487 | ```TypeScript 488 | kafka.send(topicName, message); 489 | kafka.send(topicName, message, options); 490 | kafka.send(topicName, message, callback); 491 | kafka.send(topicName, message, options, callback); 492 | ``` 493 | 494 | ### **`options`** 495 | 496 | Send options are a way to specify where you want your message to be delivered. 497 | 498 | ```TypeScript 499 | { 500 | key?: string; 501 | partition?: number; 502 | attributes?: number; 503 | } 504 | ``` 505 | 506 | **`key`** - 507 | _Default: ''_ 508 | 509 | The key for the message. 510 | 511 | It's kind of like specifying the person you're sending a letter to. 512 | 513 |
514 | 515 | **`partition`** - 516 | _Default: 0_ 517 | 518 | What partition, or sub-section, you want to send the message to. 519 | 520 |
521 | 522 | **`attributes`** - 523 | _Default: 0_ 524 | 525 | Specifies whether or not you want to use compression for your message. 526 | 527 | No compression = 0 528 | 529 | GZip = 1 530 | 531 | snappy = 2 532 | 533 |
534 | 535 | ### **`callback`** 536 | 537 | This callback will be invoked when the message successfully send and if there is an error. 538 | 539 | If there was an error, that error will be passed as an argument. Otherwise it will be `null`. 540 | 541 | ```TypeScript 542 | (error: any | null) => void; 543 | ``` 544 | 545 |
546 | 547 |
548 |
549 |
550 | 551 |
552 | 553 | ## **Consuming** 554 | 555 | Compared to sending messages to topics, consuming them is a little more involve but still straight forward. 556 | 557 | As you can see below, you can provide listener options. Although, if you want to just use the defaults that's valid too. 558 | 559 | ```TypeScript 560 | kafka.consume(topics, callback) 561 | kafka.consume(topics, listenerOptions, callback) 562 | ``` 563 | 564 | First, let's overview what you can expect in the callback. 565 | 566 | **`callback`** - 567 | _Required_ 568 | 569 | ```TypeScript 570 | (message: KafkaMessage | null, error?: any) => void 571 | ``` 572 | 573 | A `KafkaMessage` contains the following: 574 | 575 | ```TypeScript 576 | { 577 | topic: string; 578 | message: string; 579 | offset?: number; 580 | partition?: number; 581 | highWaterOffset?: number; 582 | key?: string; 583 | } 584 | ``` 585 | 586 | Let's go a bit more in-depth about what each key does. 587 | 588 | **`topic`** 589 | 590 | The name of the topic a message was consumed from. 591 | 592 |
593 | 594 | **`message`** 595 | 596 | The message sent to the topic. 597 | 598 |
599 | 600 | **`offset`** 601 | 602 | The offset of the message. 603 | 604 | Think of appending to an array. 605 | 606 |
607 | 608 | **`partition`** 609 | 610 | The partition, or sub-section, on the topic a message was sent to. 611 | 612 |
613 | 614 | **`highWaterOffset`** 615 | 616 | The last offset that was successfully replicated to all topics. 617 | 618 |
619 | 620 | **`key`** 621 | 622 | The identifier for a message. 623 | 624 | For example: if you have a `gateway` topic and you need user data from the `auth` topic, you can specify a key to differenciate between other messages sent to the `gateway` topic. 625 | 626 |
627 | 628 | Now, the message may be null. This will only occur when there is an error. 629 | 630 | ### **`listenerOptions`** 631 | 632 | Finally, we can dive into what the listener options are and what they do. 633 | 634 | We provide a way to customize how you want listeners to behave. If you have no need for customization, there are also default options to fallback on. 635 | 636 | `KafkaListenerOptions` 637 | 638 | ```TypeScript 639 | { 640 | autoCommit?: boolean; 641 | groupId?: string; 642 | autoCommitIntervalMs?: number; 643 | fetchMaxWaitMs?: number; 644 | fetchMinBytes?: number; 645 | fetchMaxBytes?: number; 646 | fromOffset?: boolean; 647 | } 648 | ``` 649 | 650 | **`autoCommit`** - 651 | _Default: false_ 652 | 653 | `autoCommit` will automagically mark the consumed messages as "read" 654 | 655 |
656 | 657 | **`groupId`** - 658 | _Default: 'kafka-node-group'_ 659 | 660 | The consumer gorup id. 661 | 662 |
663 | 664 | **`autoCommitIntervalMs`** - 665 | _Default: 5000ms_ 666 | 667 | How often, in ms, will the consumer mark messages as "sent" 668 | 669 |
670 | 671 | **`fetchMaxWaitMs`** - 672 | _Default: 100ms_ 673 | 674 | The max time, in ms, to wait if there is insufficient data when receiving a message. 675 | 676 |
677 | 678 | **`fetchMinBytes`** - 679 | _Default: 1_ 680 | 681 | The minimum number of bytes that must be available to respond. 682 | 683 |
684 | 685 | **`fetchMaxBytes`** - 686 | _Default: 2048_ 687 | 688 | The maximum bytes to include in the message set for this partition. 689 | 690 |
691 | 692 | **`fromOffset`** - 693 | _Default: false_ 694 | 695 | If set true, consumer will fetch message from the given offset in the KafkaMessage. 696 | 697 |
698 | 699 | _As of now, we're working on allowing support for Buffers. The options for this will be provided on the `encoding` and `keyEncoding` keys._ 700 | 701 |
702 |
703 |
704 | 705 | ## **Example** 706 | 707 | Now, let's see an example of what we've learned so far and how it would look in your code. 708 | 709 | ```TypeScript 710 | const clientOptions = { 711 | host: 'localhost', 712 | port: 9092, 713 | } 714 | 715 | const topics = { 716 | topic1: null, 717 | topic2: null, 718 | } 719 | 720 | const kafka = Kafka(clientOptions, topics); 721 | 722 | kafka.consume('topic2', { autoCommit: true }, (data, error) => console.log(data.message)); 723 | 724 | kafka.send('topic2', 'Hello, topic2!'); 725 | ``` 726 | 727 |
728 | 729 |
730 | 731 |
732 |
733 |
734 | 735 | 736 |
737 | 738 | ## **RabbitMQ** 739 | 740 |
741 | 742 | ### **Initilization** 743 | 744 | Initilizing RabbitMQ is similar to the syntax you have seen previously with our standards and Kakfa. With RabbitMQ, we use the "createRabbitClass" factory function to instantiate a variable to act as our broker. 745 | 746 | Is that an await? Yes, yes it is. Our RabbitMQ message broker can be instantiated using promises. 747 | 748 | ```TypeScript 749 | 750 | const rabbit = await createRabbitClass(clientOptions, topics); 751 | 752 | ``` 753 | 754 |
755 | 756 | ### **`clientOptions`** 757 | 758 | With Codename Hermes, you can create a customaizable rabbit broker for any occasion thanks to the below client options. Continue reading to find out more! 759 | 760 | ```TypeScript 761 | { 762 | username?: string; 763 | password?: string; 764 | protocol?: 'amqp' | 'amqps'; 765 | vhost?: string; 766 | locale?: string; 767 | frameMax?: number; 768 | heartbeat?: number; 769 | } 770 | ``` 771 | 772 |
773 | 774 | **`username`** - 775 | _Default: guest_ 776 | 777 | User name to create when RabbitMQ creates a new database from scratch. 778 | 779 |
780 | 781 | **`password`** - 782 | _Default: guest_ 783 | 784 | Password for the default user. 785 | 786 |
787 | 788 | **`protocol`** - 789 | _Default: amqp_ 790 | 791 | Mandatory option to specify what messaging protocol is used for the broker. The Codename Hermes team, decided to utilize the `AMQP 0-9-1 messaging protocol` because it is a mature and widely adopted messaging protocol that provides the necessary features and capabilities for building robust, scalable, and interoperable message broker systems. 792 | 793 | Please refer to [AMQP 0-9-1](https://www.rabbitmq.com/tutorials/amqp-concepts.html#what-is-amqp) to learn more! 794 | 795 |
796 | 797 | **`vhost`** - 798 | _Default: '/'_ 799 | 800 | The name for the virtual host. RabbitMQ out of the box uses a virtual host named '/'. Changing this option beyond the default is needed when trying to create logical groupings and/or isolated messaging environments within your RabbitMQ broker. 801 | 802 |
803 | 804 | **`locale`** - 805 | _Default_: _en_US_ 806 | 807 | The desired locale for error messages. `NOTE:` RabbitMQ only ever uses en_US. 808 | 809 |
810 | 811 | **`frameMax`** - 812 | _Default: 0_ 813 | 814 | The size in bytes of the maximum frame allowed over the connection. 0 means no limit. Please refer to [frameMax](https://amqp-node.github.io/amqplib/channel_api.html#connect) for more inforamtion. 815 | 816 |
817 | 818 | **`heartbeat`** - 819 | _Default: 0_ 820 | 821 | The period of the connection heartbeat, in seconds. Please refer to [heartbeating](https://amqp-node.github.io/amqplib/channel_api.https://amqp-node.github.io/amqplib/channel_api.html#heartbeating) for more inforamtion. 822 | 823 |
824 | 825 | ### **`topics`** 826 | 827 | Now that you have decided on your options, it is time to determine what type of topics you will be. Let's keep reading to find out about our next topic, "topics". 828 | 829 | ```TypeScript 830 | { 831 | exchange: amqp.Options.AssertExchange & { 832 | name: string; 833 | type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match'; 834 | }; 835 | } & amqp.Options.AssertQueue & { 836 | key?: string; 837 | } 838 | 839 | ``` 840 | 841 |
842 | 843 | ### **_`exchange object`_** 844 | 845 | **`name`** - 846 | _Default: ' ' (empty string)_ 847 | 848 | Name of the topic exchange. Get creative and specific here. 849 | 850 |
851 | 852 | **`type`** - 853 | _Default: topic_ 854 | 855 | This option determines what type of exchange our RabbitMQ broker will use. Although a developer could use any rabbit exchange type with our library, the Codename Hermes team decided to default to using topics because topic exchanges support the publish-subscribe messaging model as well as supports the scalability and extensibility of messaging systems. 856 | 857 |
858 | 859 | #### **`amqp AssertExchange options`** 860 | 861 |
862 | 863 | **`durable`** - 864 | _Default: true_ 865 | 866 | if true, the exchange will survive broker restarts. 867 | 868 |
869 | 870 | **`internal`** - 871 | _Default: false_ 872 | 873 | if true, messages cannot be published directly to the exchange (i.e., it can only be the target of bindings, or possibly create messages ex-nihilo). 874 | 875 |
876 | 877 | **`autoDelete`** - 878 | _Default: false_ 879 | 880 | if true, the exchange will be destroyed once the number of bindings for which it is the source drop to zero. 881 | 882 |
883 | 884 | **`alternateExchange`** - 885 | _Default: ' ' (empty string)_ 886 | 887 | an exchange to send messages to if this exchange can’t route them to any queues. Think backup exchange. 888 | 889 |
890 | 891 | **`arguments`** - 892 | _Default: {} (empty object)_ 893 | 894 | any additional arguments that may be needed by an exchange type. 895 | 896 |
897 | 898 | Please refer to [AssertExchange options](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertExchange) for more inforamtion. 899 | 900 |
901 | 902 | ### **_`Additional Properties on topic object`_** 903 | 904 |
905 | 906 | **`key`** - 907 | _Default: ' ' (empty string)_ 908 | 909 | This value represents the binding and routing keys to ensure produced messages are consumed by the appropriate rabbit broker. 910 | 911 |
912 | 913 | #### **`amqp AssertQueue options`** 914 | 915 |
916 | 917 | **`exclusive`** - 918 | _Default: false_ 919 | 920 | if true, scopes the queue to the connection. 921 | 922 |
923 | 924 | **`durable`** - 925 | _Default: true_ 926 | 927 | if true, the queue will survive broker restarts, modulo the `effects` of exclusive and `autoDelete`. 928 | 929 |
930 | 931 | **`autoDelete`** - 932 | _Default: false_ 933 | 934 | if true, the queue will be deleted when the number of consumers drops to zero. 935 | 936 |
937 | 938 | **`arguments`** - 939 | _Default: {} (empty object)_ 940 | 941 | any additional arguments that may be needed by an exchange type. 942 | 943 |
944 | 945 | Please refer to [AssertQueue options](https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue) for more inforamtion and potential arguments. 946 | 947 |
948 | An example of implementing a topic might look like: 949 | 950 | ```TypeScript 951 | const topics = { 952 | topic1: { 953 | exchange: { 954 | name: "topics", 955 | durable: false, 956 | type: "topic", 957 | }, 958 | durable: false, 959 | key: "hermes", 960 | }, 961 | }; 962 | ``` 963 | 964 |
965 | 966 |
967 | 968 | **`Produce`** 969 | 970 | In the wonderful world of Rabbit, we still produce messages with our broker using our standard `send` method. 971 | 972 |
973 | 974 | `NOTE:` the last argument is optional and is not required to send messages with your rabbit broker. 975 | 976 | ```TypeScript 977 | 978 | send(topic: string, message: string, options?: amqp.Options.Publish) 979 | ``` 980 | 981 |
982 | 983 | If you would like to enhance your messages, you are free to do so with the following publish options. 984 | 985 | #### **`amqp Publish options`** 986 | 987 |
988 | 989 | **`expiration`** - 990 | _Default: ' ' (empty string)_ 991 | 992 | If supplied, the message will be discarded from a queue once it’s been there longer than the given number of milliseconds. In the specification this is a string; numbers supplied here will be coerced to strings for transit. 993 | 994 |
995 | 996 | **`userId`** - 997 | _Default: ' ' (empty string)_ 998 | 999 | If supplied, RabbitMQ will compare it to the username supplied when opening the connection, and reject messages for which it does not match. 1000 | 1001 |
1002 | 1003 | **`CC`** - 1004 | _Default: ' ' (empty string)_ 1005 | 1006 | A routing key as a string or an array of routing keys as strings; messages will be routed to these routing keys in addition to that given as the routingKey parameter. A string will be implicitly treated as an array containing just that string. This will override any value given for CC in the headers parameter. NB The property names CC and BCC are case-sensitive. 1007 | 1008 |
1009 | 1010 | **`priority`** - 1011 | _Default: 0_ 1012 | 1013 | A priority for the message; ignored by versions of RabbitMQ older than 3.5.0, or if the queue is not a priority queue (see maxPriority above). 1014 | 1015 |
1016 | 1017 | **`persistent`** - 1018 | _Default: false_ 1019 | 1020 | If truthy, the message will survive broker restarts provided it’s in a queue that also survives restarts. Corresponds to, and overrides, the property deliveryMode. 1021 | 1022 |
1023 | 1024 | Please refer to [Publish options](https://amqp-node.github.io/amqplib/channel_api.html#channel_publish) for more inforamtion and potential arguments. 1025 | 1026 |
1027 | 1028 | _Quick example_ 1029 | 1030 | ```TypeScript 1031 | rabbit.send("topic1", "hello from sender.ts in ch lib test"); 1032 | 1033 | ``` 1034 | 1035 |
1036 | 1037 |
1038 | 1039 | **`Consume`** 1040 |
1041 | _Comming soon!_ 1042 | 1043 |
1044 |
1045 |
1046 | 1047 | ### **Example** 1048 | 1049 | ```TypeScript 1050 | 1051 | import ch, { RabbitTopic, createRabbitClass } from "library"; 1052 | 1053 | const clientOptions = { host: "localhost", port: 5672 }; 1054 | 1055 | const topics: RabbitTopic = { 1056 | topic1: { 1057 | exchange: { 1058 | name: "topics", 1059 | durable: false, 1060 | type: "topic", 1061 | }, 1062 | durable: false, 1063 | key: "hermes", 1064 | }, 1065 | }; 1066 | 1067 | const rabbit = await createRabbitClass(clientOptions,topics); 1068 | 1069 | rabbit.send("topic1", "hello from sender.ts in ch lib test"); 1070 | 1071 | ``` 1072 | 1073 | 1074 | 1075 |
1076 |
1077 | 1078 |
1079 |
1080 |
1081 | 1082 |
1083 | 1084 | ## **Credits** 1085 | 1086 | A **major** special thanks to [kafka-node]() and [amqplib]() for allowing this project to have an accelerated launch! 1087 | 1088 |
1089 | 1090 | 1091 |
1092 | 1093 | ## **How to Contribute** 1094 | 1095 | Codename Hermes is currently in alpha, we would love to hear your feedback, encouragement, advice, suggestions, or problems! If you would like to contribute please contact us at info@hermeslib.com 1096 | 1097 |
1098 | 1099 |
1100 | 1101 | ## **Contributers** 1102 | 1103 | Aaron Allen - [LinkedIn](https://www.linkedin.com/in/aaronrallen/) [GitHub](https://github.com/H3R01A) 1104 | 1105 | Mathew Hultquist - [LinkedIn](https://www.linkedin.com/in/mathewjhultquist/) [GitHub](https://github.com/mjhult) 1106 | 1107 | Jose Mencos - [LinkedIn](https://www.linkedin.com/in/jmencos/) [GitHub](https://github.com/Jmencos) 1108 | 1109 | Samantha Mills - [LinkedIn](https://www.linkedin.com/in/samanthamills/) [GitHub](https://github.com/Slmills14) 1110 | 1111 |
1112 | -------------------------------------------------------------------------------- /packages/library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codename-hermes/library", 3 | "packageManager": "yarn@3.3.1", 4 | "version": "0.0.8", 5 | "main": "./lib/index.js", 6 | "description": "A library to abstract away and standardize the initial setup and implementation of connecting with message brokers.", 7 | "keywords": [ 8 | "kafka", 9 | "rabbit" 10 | ], 11 | "homepage": "https://hermeslib.com", 12 | "bugs": { 13 | "url": "https://github.com/oslabs-beta/Codename-Hermes/issues", 14 | "email": "bugs@hermeslib.com" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/oslabs-beta/Codename-Hermes" 19 | }, 20 | "author": "Codename Hermes (hermeslib.com)", 21 | "license": "MIT", 22 | "engines": { 23 | "node": ">=6.0.0" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "engineStrict": true, 29 | "scripts": { 30 | "build": "yarn tsc", 31 | "prepublish": "yarn build", 32 | "publish": "npm publish" 33 | }, 34 | "devDependencies": { 35 | "@types/amqplib": "^0.10.1", 36 | "tsc": "^2.0.4", 37 | "typescript": "^5.0.4" 38 | }, 39 | "dependencies": { 40 | "amqplib": "^0.10.3", 41 | "kafka-node": "^5.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/library/src/brokers/MessageBroker.ts: -------------------------------------------------------------------------------- 1 | // The Message type should be created by us at a later point. It must be compatible with Kafka and 2 | export type MessageCallback = (err: any, message: T) => void; 3 | export type ErrorCallback = (err: any) => void; 4 | 5 | // export type GenericTopicOptions = { 6 | // }; 7 | 8 | // A generic topic layout 9 | // 10 | export type GenericTopic = { 11 | [topicName: string]: T; 12 | }; 13 | 14 | // The generic options for "listeners" 15 | // Each broker will have their own specific options as well. 16 | // TODO: this 17 | export type GenericListenerOptions = { 18 | autoCommit?: boolean; 19 | }; 20 | 21 | // Kafka - Rabbit simularities 22 | // groupId = NULL 23 | // autoCommit = noAck 24 | // autoCommitIntervalMs = NULL 25 | // fetchMaxWaitMs = NULL 26 | // fetchMinBytes = NULL 27 | // fetchMaxBytes = NULL 28 | // fromOffset = NULL 29 | 30 | // The generic client options for every broker. 31 | // Each broker will have their own specific options as well. 32 | export type GenericClientOptions = { 33 | host: string; 34 | port?: number; 35 | }; 36 | 37 | // The generic message object 38 | // Each broker will have their own specific details added. 39 | export type GenericMessage = { 40 | topic: string; 41 | message: string; 42 | }; 43 | 44 | // TODO: add overloads to abstract class. 45 | 46 | export default abstract class MessageBroker { 47 | constructor(connection: GenericClientOptions, topics: GenericTopic) {} 48 | 49 | // This will be defined in the specific broker class 50 | abstract send(topic: string, message: string | string[]): void; 51 | 52 | // This will be defined in the specific broker class 53 | protected abstract listener( 54 | topics: string, 55 | options?: GenericListenerOptions 56 | ): void; 57 | 58 | // This will be defined in the specific broker class 59 | // abstract consume(topic: string): Promise; 60 | abstract consume( 61 | topic: string, 62 | listenerOptions: GenericListenerOptions, 63 | callback: MessageCallback 64 | ): void; 65 | } 66 | -------------------------------------------------------------------------------- /packages/library/src/brokers/kafka/main.ts: -------------------------------------------------------------------------------- 1 | import MessageBroker, { 2 | GenericClientOptions, 3 | GenericListenerOptions, 4 | GenericMessage, 5 | GenericTopic, 6 | MessageCallback, 7 | ErrorCallback, 8 | } from '../MessageBroker'; 9 | import { 10 | Consumer, 11 | KafkaClient, 12 | Message, 13 | Producer, 14 | ProducerOptions, 15 | RetryOptions, 16 | } from 'kafka-node'; 17 | import { 18 | formatMessageToKafkaMessage, 19 | formatTopics, 20 | } from './utils/utilFunctions'; 21 | 22 | export type KafkaClientOptions = GenericClientOptions & { 23 | connectTimeout?: number; 24 | requestTimeout?: number; 25 | autoConnect?: boolean; 26 | connectRetryOptions?: RetryOptions; 27 | idleConnection?: number; 28 | reconnectOnIdle?: boolean; 29 | maxAsyncRequests?: number; 30 | // Figure out kafka-node ssl options 31 | // sslOptions?: null; 32 | // Figure out kafka-node sasl options 33 | // sasl?: null; 34 | }; 35 | 36 | export type KafkaTopic = GenericTopic; 40 | 41 | export type KafkaSendOptions = { 42 | key?: string; 43 | partition?: number; 44 | attributes?: number; 45 | }; 46 | 47 | // Based off the ConsumerOptions from kafka-node 48 | export type KafkaListenerOptions = GenericListenerOptions & { 49 | groupId?: string; 50 | autoCommitIntervalMs?: number; 51 | fetchMaxWaitMs?: number; 52 | fetchMinBytes?: number; 53 | fetchMaxBytes?: number; 54 | fromOffset?: boolean; 55 | // encoding?: 'buffer' | 'utf8'; 56 | // keyEncoding?: 'buffer' | 'utf8'; 57 | }; 58 | 59 | export type KafkaMessage = GenericMessage & { 60 | offset?: number; 61 | partition?: number; 62 | highWaterOffset?: number; 63 | key?: string; 64 | }; 65 | 66 | //TODO: error handling lol 67 | //TODO: comments lol 68 | //TODO: unified class interface for kafka, rabbitmq, and redis (config options, broker host, etc) 69 | //TODO: kafka cluster host support 70 | //TODO: convert to promise based 71 | //TODO: clarify error messages. 72 | //TODO: REFACTOR: use types in seperate file 73 | 74 | /** 75 | * TODO: 76 | * - ConsumerStream 77 | * - ProducerStream 78 | * - Add topic (maybe auto-add missing topics) 79 | * - Full Consumer functionality 80 | * - Full Producer functionality 81 | * - Admin api 82 | * - Refactoring 83 | * - Abstraction of options to be interchangeable with rabbitmq/redis 84 | */ 85 | 86 | /** 87 | * Purpose: to produce new Kafka message broker with async code. 88 | * @param connection 89 | * @param topics 90 | * @returns Kafka object to be used as a message broker 91 | */ 92 | export async function createKafkaClass( 93 | connection: KafkaClientOptions, 94 | topics: KafkaTopic, 95 | producerOptions?: ProducerOptions, 96 | callback?: ErrorCallback 97 | ): Promise; 98 | export async function createKafkaClass( 99 | connection: KafkaClientOptions, 100 | topics: KafkaTopic, 101 | producerOptions?: ProducerOptions 102 | ): Promise; 103 | export async function createKafkaClass( 104 | connection: KafkaClientOptions, 105 | topics: KafkaTopic, 106 | callback?: ErrorCallback 107 | ): Promise; 108 | export async function createKafkaClass( 109 | connection: KafkaClientOptions, 110 | topics: KafkaTopic, 111 | producerOptionsOrCallback?: ProducerOptions | ErrorCallback, 112 | callback?: ErrorCallback 113 | ) { 114 | let producerOptions: ProducerOptions | undefined; 115 | if ( 116 | typeof producerOptionsOrCallback === 'object' || 117 | producerOptionsOrCallback === undefined 118 | ) { 119 | producerOptions = producerOptionsOrCallback; 120 | } else { 121 | callback = producerOptionsOrCallback; 122 | } 123 | 124 | const kafka = await new Promise((d) => 125 | d(new Kafka(connection, topics, producerOptions, callback)) 126 | ); 127 | return kafka; 128 | } 129 | 130 | /** 131 | * An abstraction to the kafka-node library. 132 | * @param topics Kafka topics 133 | * @param host Kafka host 134 | * @param options Kafka client options 135 | * @param callback Callback to be called when the producer has connected to kafka. 136 | */ 137 | export default class Kafka extends MessageBroker { 138 | private topics: KafkaTopic; 139 | private client: KafkaClient; 140 | private producer: Producer; 141 | private consumers: { 142 | [topic: string]: Consumer; 143 | }; 144 | private host: string; 145 | constructor( 146 | connection: KafkaClientOptions, 147 | topics: KafkaTopic, 148 | producerOptions?: ProducerOptions, 149 | callback?: ErrorCallback 150 | ); 151 | constructor( 152 | connection: KafkaClientOptions, 153 | topics: KafkaTopic, 154 | producerOptions?: ProducerOptions 155 | ); 156 | constructor( 157 | connection: KafkaClientOptions, 158 | topics: KafkaTopic, 159 | callback?: ErrorCallback 160 | ); 161 | constructor(connection: KafkaClientOptions, topics: KafkaTopic); 162 | constructor( 163 | connection: KafkaClientOptions, 164 | topics: KafkaTopic, 165 | producerOptionsOrCallback?: ProducerOptions | ErrorCallback, 166 | callback?: ErrorCallback 167 | ) { 168 | let producerOptions: ProducerOptions | undefined; 169 | if ( 170 | typeof producerOptionsOrCallback === 'object' || 171 | producerOptionsOrCallback === undefined 172 | ) { 173 | producerOptions = producerOptionsOrCallback; 174 | } else { 175 | callback = producerOptionsOrCallback; 176 | } 177 | 178 | super(connection, topics); 179 | 180 | this.topics = topics; 181 | this.host = `${connection.host}:${connection.port}`; 182 | 183 | this.client = new KafkaClient({ 184 | ...producerOptions, 185 | kafkaHost: this.host, 186 | }); 187 | 188 | this.producer = new Producer(this.client, producerOptions); 189 | this.consumers = {}; 190 | 191 | // Invoke the provided callback when the producer is ready or if there is an error. 192 | if (callback !== undefined) { 193 | this.producer.on('ready', () => callback!(null)); 194 | this.producer.on('error', callback); 195 | } 196 | } 197 | 198 | /** 199 | * Sends a message to a provided topic. 200 | * @param topic The topic to send a message to. 201 | * @param message The message to send to a topic. 202 | * @param callback The callback to invoke when the message has been sent or errors out. 203 | */ 204 | send( 205 | topic: string, 206 | message: string, 207 | options: KafkaSendOptions, 208 | callback: ErrorCallback 209 | ): void; 210 | send(topic: string, message: string, options: KafkaSendOptions): void; 211 | send(topic: string, message: string, callback: ErrorCallback): void; 212 | send(topic: string, message: string): void; 213 | send( 214 | topic: string, 215 | message: string, 216 | optionsOrCallback?: KafkaSendOptions | ErrorCallback, 217 | callback?: ErrorCallback 218 | ) { 219 | let options: KafkaSendOptions | undefined; 220 | if ( 221 | typeof optionsOrCallback === 'object' || 222 | optionsOrCallback === undefined 223 | ) { 224 | options = optionsOrCallback; 225 | } else { 226 | callback = optionsOrCallback; 227 | } 228 | 229 | this.producer.send( 230 | [ 231 | { 232 | topic, 233 | messages: message, 234 | ...(options ?? {}), 235 | }, 236 | ], 237 | callback ?? (() => {}) 238 | ); 239 | } 240 | 241 | /** 242 | * Creates a listener for provided topics. 243 | * @param topics The topics to create listeners for. 244 | * @param options Options for the consumer. 245 | */ 246 | protected listener(topic: string, options?: KafkaListenerOptions) { 247 | // Format the provided topic strings to the accepted topic type for Consumer(). 248 | const formattedTopic = { 249 | topic, 250 | ...this.topics[topic], 251 | }; 252 | // Is the topic valid? 253 | if (!this.topics.hasOwnProperty(topic)) 254 | throw new Error(`There is no registered topic "${topic}"`); 255 | 256 | this.consumers[topic] = new Consumer( 257 | this.client, 258 | [formattedTopic], 259 | options ?? {} 260 | ); 261 | } 262 | 263 | /** 264 | * Subscribes a function to a topic's message event. 265 | * @param topic The topic you want to listen to 266 | * @param callback The function to invoke when a message is received 267 | */ 268 | consume(topic: string, callback: MessageCallback): void; 269 | consume( 270 | topic: string, 271 | listenerOptions: KafkaListenerOptions, 272 | callback: MessageCallback 273 | ): void; 274 | consume( 275 | topic: string, 276 | optionsOrCallback?: 277 | | KafkaListenerOptions 278 | | MessageCallback, 279 | callback?: MessageCallback 280 | ): void { 281 | let listenerOptions; 282 | if ( 283 | typeof optionsOrCallback === 'object' || 284 | optionsOrCallback === undefined 285 | ) { 286 | listenerOptions = optionsOrCallback; 287 | } else { 288 | callback = optionsOrCallback; 289 | } 290 | 291 | if (!topic) throw new Error('No topic specified.'); 292 | this.listener(topic, listenerOptions); 293 | if (!this.consumers[topic]) 294 | throw new Error(`No listener found for topic "${topic}"`); 295 | 296 | this.consumers[topic]!.on('message', (msg: Message) => 297 | callback!(null, formatMessageToKafkaMessage(msg)) 298 | ); 299 | this.consumers[topic]!.on('error', (err: any) => callback!(err, null)); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /packages/library/src/brokers/kafka/utils/utilFunctions.ts: -------------------------------------------------------------------------------- 1 | import { Message, Topic } from 'kafka-node'; 2 | import { KafkaMessage, KafkaTopic } from '../main'; 3 | 4 | /** 5 | * Convert from KafkaTopic to kafka-node's Topic 6 | */ 7 | export const formatTopics = (topics: KafkaTopic): Topic[] => 8 | Object.keys(topics).map((topicName) => ({ 9 | topic: topicName, 10 | ...topics[topicName], 11 | })); 12 | 13 | /** 14 | * Convert from kafka-node's Message to our KafkaMessage 15 | */ 16 | export const formatMessageToKafkaMessage = ( 17 | message: Message 18 | ): KafkaMessage => ({ 19 | topic: message.topic, 20 | // We will add support for Buffers in the near future 21 | message: Buffer.isBuffer(message.value) 22 | ? message.value.toString() 23 | : message.value, 24 | offset: message.offset, 25 | partition: message.partition, 26 | highWaterOffset: message.highWaterOffset, 27 | // We will add support for Buffers in the near future 28 | key: Buffer.isBuffer(message.key) ? message.key.toString() : message.key, 29 | }); 30 | -------------------------------------------------------------------------------- /packages/library/src/brokers/rabbit/main.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'amqplib'; 2 | import MessageBroker, { 3 | GenericClientOptions, 4 | GenericListenerOptions, 5 | GenericMessage, 6 | GenericTopic, 7 | MessageCallback, 8 | } from '../MessageBroker'; 9 | 10 | import * as amqp from 'amqplib'; 11 | import { convertGenericListenerConfigToRabbitAmqpConfig } from './utils/utilFunctions'; 12 | 13 | export type RabbitClientOptions = GenericClientOptions & { 14 | username?: string; 15 | password?: string; 16 | // TODO: add support for secure connections 17 | // TODO: REFACTOR: Maybe we can make this a secure boolean to unify between Kafka and this 18 | protocol?: 'amqp' | 'amqps'; 19 | vhost?: string; 20 | locale?: string; 21 | frameMax?: number; 22 | heartbeat?: number; 23 | }; 24 | 25 | // TODO: add the rest of the options 26 | export type RabbitTopic = GenericTopic< 27 | { 28 | exchange: amqp.Options.AssertExchange & { 29 | name: string; 30 | type?: 'direct' | 'topic' | 'headers' | 'fanout' | 'match'; 31 | }; 32 | } & amqp.Options.AssertQueue & { 33 | key?: string; 34 | } 35 | >; 36 | 37 | export type RabbitListenerOptions = GenericListenerOptions & { 38 | consumerTag?: string; 39 | exclusive?: boolean; 40 | priority?: number; 41 | arguments?: any; 42 | }; 43 | 44 | export type RabbitMessage = GenericMessage & {}; 45 | 46 | //function to act as asynchronous factory function 47 | /** 48 | * Purpose: to produce new RabbitMQ message broker with async code. 49 | * @param connection 50 | * @param topics 51 | * @returns RabbitMQ object to be used as a message broker 52 | */ 53 | export async function createRabbitClass( 54 | connection: RabbitClientOptions, 55 | topics: RabbitTopic 56 | ) { 57 | const rabbit = new Rabbit(connection, topics); 58 | await rabbit.init(); 59 | return rabbit; 60 | } 61 | 62 | // TODO: Add error handling 63 | export default class Rabbit extends MessageBroker { 64 | private connection: Connection | null; 65 | private channel: amqp.Channel | null; 66 | private topics: RabbitTopic; 67 | private defaultConfig: RabbitClientOptions; 68 | private consumerTags: { [topicName: string]: RabbitListenerOptions }; 69 | constructor(connection: RabbitClientOptions, topics: RabbitTopic) { 70 | super(connection, topics); 71 | 72 | // Let's create a default config, we are also relying on default options for the connect method. 73 | //updated to add default config to the new object returned 74 | this.defaultConfig = { 75 | ...connection, 76 | port: connection.port ?? 5672, 77 | // protocol: connection.protocol ?? 'amqp', 78 | // TODO: remove this when we add secure connection support. This overwrites what the user wants. 79 | protocol: 'amqp', 80 | }; 81 | 82 | this.topics = topics; 83 | this.consumerTags = {}; 84 | 85 | // This is jank... I'm sorry 86 | // REFACTOR: all this 👇 87 | // TODO: fix channel null issue 88 | this.connection = null; 89 | this.channel = null; 90 | const that = this; 91 | } 92 | 93 | //initialize method on Rabbit Class constructor to be used in the asynchronous factory function 94 | //The method is used to setup the following 95 | /** 96 | * connection 97 | * create exchange 98 | * create queue 99 | * bind exchange to queue via key (NOTE: key is determined when creating topics to pass into new Rabbit instatiation. The key here is the "binding key". 100 | * in order for the queue to receive messages, the "routingKey" in the send function with publish MUST MATCH 101 | * reference: https://www.rabbitmq.com/tutorials/tutorial-four-javascript.html ) 102 | */ 103 | async init() { 104 | this.connection = await amqp.connect(this.defaultConfig); 105 | if (this.connection === null) 106 | throw new Error( 107 | 'No connection for Rabbit. Failed to initialize - connection' 108 | ); 109 | 110 | this.channel = await this.connection!.createChannel(); 111 | if (this.channel === null) 112 | throw new Error('No channel for Rabbit. Failed to initialize - channel'); 113 | 114 | Object.keys(this.topics).forEach(async (topic) => { 115 | // POSSIBLE REFACTOR: Don't know if we need await 116 | await this.channel?.assertExchange( 117 | this.topics[topic].exchange.name, 118 | this.topics[topic].exchange.type ?? 'topic', 119 | this.topics[topic].exchange ?? {} 120 | ); 121 | 122 | await this.channel?.assertQueue(topic, this.topics[topic] ?? {}); 123 | 124 | // TODO: research and implement "args" 125 | await this.channel?.bindQueue( 126 | topic, 127 | this.topics[topic].exchange.name, 128 | this.topics[topic].key ?? topic 129 | ); 130 | }); 131 | } 132 | 133 | // TODO: Add support for multi messages 134 | //! the second argument in the PUBLISH method NEEDS to be the "key". Before refactor it was the topic. The reason is because this is the routingkey that MUST MATCH the bindingKey 135 | //! that is found within the init function 136 | async send(topic: string, message: string, options?: amqp.Options.Publish) { 137 | this.channel?.publish( 138 | this.topics[topic].exchange.name, 139 | this.topics[topic].key ?? topic, 140 | Buffer.from(message), 141 | options 142 | ); 143 | } 144 | 145 | listener(topics: string, options?: RabbitListenerOptions) { 146 | const convertedOptions = convertGenericListenerConfigToRabbitAmqpConfig( 147 | options ?? {} 148 | ); 149 | 150 | // "listener" doesn't do anything except store options right now 151 | } 152 | 153 | consume(topic: string, callback: MessageCallback): void; 154 | consume( 155 | topic: string, 156 | listenerOptions: RabbitListenerOptions, 157 | callback: MessageCallback 158 | ): void; 159 | consume( 160 | topic: string, 161 | optionsOrCallback?: 162 | | RabbitListenerOptions 163 | | MessageCallback, 164 | callback?: MessageCallback 165 | ): void { 166 | const that = this; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /packages/library/src/brokers/rabbit/utils/utilFunctions.ts: -------------------------------------------------------------------------------- 1 | import { Options } from 'amqplib'; 2 | import { RabbitClientOptions, RabbitListenerOptions } from '../main'; 3 | 4 | export const convertGenericListenerConfigToRabbitAmqpConfig = ( 5 | genericConfig: RabbitListenerOptions 6 | ): Options.Consume => ({ 7 | consumerTag: genericConfig.consumerTag, 8 | noAck: genericConfig.autoCommit, 9 | exclusive: genericConfig.exclusive, 10 | priority: genericConfig.priority, 11 | arguments: genericConfig.arguments, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/library/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KafkaClientOptions, 3 | KafkaListenerOptions, 4 | KafkaMessage, 5 | KafkaSendOptions, 6 | KafkaTopic, 7 | createKafkaClass, 8 | } from './brokers/kafka/main'; 9 | 10 | import { 11 | RabbitClientOptions, 12 | RabbitListenerOptions, 13 | RabbitMessage, 14 | RabbitTopic, 15 | createRabbitClass, 16 | } from './brokers/rabbit/main'; 17 | 18 | const Rabbit = createRabbitClass; 19 | const Kafka = createKafkaClass; 20 | 21 | // Individual Kafka exports 22 | export { 23 | Kafka, 24 | KafkaClientOptions, 25 | KafkaListenerOptions, 26 | KafkaMessage, 27 | KafkaSendOptions, 28 | KafkaTopic, 29 | }; 30 | 31 | // Individual Rabbit exports 32 | export { 33 | Rabbit, 34 | RabbitClientOptions, 35 | RabbitListenerOptions, 36 | RabbitMessage, 37 | RabbitTopic, 38 | createRabbitClass, 39 | }; 40 | 41 | // All export 42 | const CodenameHermes = { 43 | Kafka, 44 | Rabbit, 45 | Redis: 'Not implemented.', 46 | }; 47 | 48 | export default CodenameHermes; 49 | -------------------------------------------------------------------------------- /packages/library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "**/__tests__/*"] 12 | } 13 | --------------------------------------------------------------------------------