├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-constraints.cjs │ │ ├── plugin-version.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.5.0.cjs ├── .yarnrc.yml ├── apps ├── docs │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── index.mdx │ │ └── meta.json │ ├── theme.config.js │ └── tsconfig.json ├── playground │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── app.tsx │ │ ├── index.tsx │ │ └── vanilla.ts │ └── vite.config.ts └── with-next │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── src │ └── pages │ │ ├── _app.tsx │ │ └── index.tsx │ └── tsconfig.json ├── babel.config.js ├── constraints.pro ├── package.json ├── packages ├── client │ ├── README.md │ ├── package.json │ ├── src │ │ ├── hop.ts │ │ ├── index.ts │ │ ├── leap │ │ │ ├── client.ts │ │ │ ├── handlers │ │ │ │ ├── AVAILABLE.ts │ │ │ │ ├── DIRECT_MESSAGE.ts │ │ │ │ ├── INIT.ts │ │ │ │ ├── MESSAGE.ts │ │ │ │ ├── PIPE_ROOM_AVAILABLE.ts │ │ │ │ ├── PIPE_ROOM_UNAVAILABLE.ts │ │ │ │ ├── PIPE_ROOM_UPDATE.ts │ │ │ │ ├── STATE_UPDATE.ts │ │ │ │ ├── TOKEN_STATE_UPDATE.ts │ │ │ │ ├── UNAVAILABLE.ts │ │ │ │ └── create.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── pipe │ │ │ ├── index.ts │ │ │ └── mount.ts │ │ └── util │ │ │ ├── atoms.ts │ │ │ ├── channels.ts │ │ │ ├── emitter.ts │ │ │ ├── index.ts │ │ │ ├── maps.ts │ │ │ ├── queues.ts │ │ │ └── types.ts │ └── tsconfig.json └── react │ ├── README.md │ ├── package.json │ ├── src │ ├── hooks │ │ ├── atoms.ts │ │ ├── channels.ts │ │ ├── leap.ts │ │ ├── maps.ts │ │ ├── pipe.ts │ │ └── timeout.ts │ ├── index.ts │ └── util │ │ └── state.ts │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const {join} = require('node:path'); 2 | 3 | module.exports = { 4 | extends: ['xo', 'xo-typescript'], 5 | plugins: ['@typescript-eslint'], 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | ecmaFeatures: {jsx: true}, 9 | ecmaVersion: 12, 10 | sourceType: 'module', 11 | project: join(__dirname, 'tsconfig.json'), 12 | }, 13 | ignorePatterns: ['**/*.js', '**/dist/**/*'], 14 | rules: { 15 | '@typescript-eslint/triple-slash-reference': 'off', 16 | '@typescript-eslint/ban-types': 'off', 17 | '@typescript-eslint/naming-convention': 'off', 18 | '@typescript-eslint/indent': 'off', 19 | '@typescript-eslint/quotes': 'off', 20 | 'operator-linebreak': 'off', 21 | 'jsx-quotes': 'off', 22 | 'no-mixed-spaces-and-tabs': 'off', 23 | 'capitalized-comments': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | dist 5 | .yarn/* 6 | !.yarn/releases 7 | !.yarn/plugins 8 | !.yarn/sdks 9 | node_modules 10 | .next 11 | yarn-error.log 12 | .swc 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "alistair/prettier" 2 | -------------------------------------------------------------------------------- /.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 wr=Object.create,me=Object.defineProperty,Sr=Object.defineProperties,vr=Object.getOwnPropertyDescriptor,Hr=Object.getOwnPropertyDescriptors,$r=Object.getOwnPropertyNames,et=Object.getOwnPropertySymbols,kr=Object.getPrototypeOf,tt=Object.prototype.hasOwnProperty,Tr=Object.prototype.propertyIsEnumerable;var rt=(e,t,r)=>t in e?me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,B=(e,t)=>{for(var r in t||(t={}))tt.call(t,r)&&rt(e,r,t[r]);if(et)for(var r of et(t))Tr.call(t,r)&&rt(e,r,t[r]);return e},Q=(e,t)=>Sr(e,Hr(t)),Lr=e=>me(e,"__esModule",{value:!0});var K=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Or=(e,t)=>{for(var r in t)me(e,r,{get:t[r],enumerable:!0})},Nr=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of $r(t))!tt.call(e,n)&&n!=="default"&&me(e,n,{get:()=>t[n],enumerable:!(r=vr(t,n))||r.enumerable});return e},X=e=>Nr(Lr(me(e!=null?wr(kr(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var $e=K(te=>{"use strict";te.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;te.find=(e,t)=>e.nodes.find(r=>r.type===t);te.exceedsLimit=(e,t,r=1,n)=>n===!1||!te.isInteger(e)||!te.isInteger(t)?!1:(Number(t)-Number(e))/Number(r)>=n;te.escapeNode=(e,t=0,r)=>{let n=e.nodes[t];!n||(r&&n.type===r||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};te.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0==0?(e.invalid=!0,!0):!1;te.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;te.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;te.reduce=e=>e.reduce((t,r)=>(r.type==="text"&&t.push(r.value),r.type==="range"&&(r.type="text"),t),[]);te.flatten=(...e)=>{let t=[],r=n=>{for(let s=0;s{"use strict";var it=$e();at.exports=(e,t={})=>{let r=(n,s={})=>{let a=t.escapeInvalid&&it.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o="";if(n.value)return(a||i)&&it.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let h of n.nodes)o+=r(h);return o};return r(e)}});var ct=K((os,ot)=>{"use strict";ot.exports=function(e){return typeof e=="number"?e-e==0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var At=K((cs,ut)=>{"use strict";var lt=ct(),pe=(e,t,r)=>{if(lt(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(t===void 0||e===t)return String(e);if(lt(t)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n=B({relaxZeros:!0},r);typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),a=String(n.shorthand),i=String(n.capture),o=String(n.wrap),h=e+":"+t+"="+s+a+i+o;if(pe.cache.hasOwnProperty(h))return pe.cache[h].result;let g=Math.min(e,t),f=Math.max(e,t);if(Math.abs(g-f)===1){let R=e+"|"+t;return n.capture?`(${R})`:n.wrap===!1?R:`(?:${R})`}let A=ft(e)||ft(t),p={min:e,max:t,a:g,b:f},k=[],y=[];if(A&&(p.isPadded=A,p.maxLen=String(p.max).length),g<0){let R=f<0?Math.abs(f):1;y=pt(R,Math.abs(g),p,n),g=p.a=0}return f>=0&&(k=pt(g,f,p,n)),p.negatives=y,p.positives=k,p.result=Ir(y,k,n),n.capture===!0?p.result=`(${p.result})`:n.wrap!==!1&&k.length+y.length>1&&(p.result=`(?:${p.result})`),pe.cache[h]=p,p.result};function Ir(e,t,r){let n=Pe(e,t,"-",!1,r)||[],s=Pe(t,e,"",!1,r)||[],a=Pe(e,t,"-?",!0,r)||[];return n.concat(a).concat(s).join("|")}function Mr(e,t){let r=1,n=1,s=ht(e,r),a=new Set([t]);for(;e<=s&&s<=t;)a.add(s),r+=1,s=ht(e,r);for(s=dt(t+1,n)-1;e1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+gt(o.count),i=g+1;continue}r.isPadded&&(A=Gr(g,r,n)),f.string=A+f.pattern+gt(f.count),a.push(f),i=g+1,o=f}return a}function Pe(e,t,r,n,s){let a=[];for(let i of e){let{string:o}=i;!n&&!mt(t,"string",o)&&a.push(r+o),n&&mt(t,"string",o)&&a.push(r+o)}return a}function Pr(e,t){let r=[];for(let n=0;nt?1:t>e?-1:0}function mt(e,t,r){return e.some(n=>n[t]===r)}function ht(e,t){return Number(String(e).slice(0,-t)+"9".repeat(t))}function dt(e,t){return e-e%Math.pow(10,t)}function gt(e){let[t=0,r=""]=e;return r||t>1?`{${t+(r?","+r:"")}}`:""}function Dr(e,t,r){return`[${e}${t-e==1?"":"-"}${t}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Gr(e,t,r){if(!t.isPadded)return e;let n=Math.abs(t.maxLen-String(e).length),s=r.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}}`}}pe.cache={};pe.clearCache=()=>pe.cache={};ut.exports=pe});var Ge=K((us,Rt)=>{"use strict";var qr=require("util"),yt=At(),bt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Kr=e=>t=>e===!0?Number(t):String(t),De=e=>typeof e=="number"||typeof e=="string"&&e!=="",Re=e=>Number.isInteger(+e),Ue=e=>{let t=`${e}`,r=-1;if(t[0]==="-"&&(t=t.slice(1)),t==="0")return!1;for(;t[++r]==="0";);return r>0},Wr=(e,t,r)=>typeof e=="string"||typeof t=="string"?!0:r.stringify===!0,jr=(e,t,r)=>{if(t>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?t-1:t,"0")}return r===!1?String(e):e},_t=(e,t)=>{let r=e[0]==="-"?"-":"";for(r&&(e=e.slice(1),t--);e.length{e.negatives.sort((i,o)=>io?1:0),e.positives.sort((i,o)=>io?1:0);let r=t.capture?"":"?:",n="",s="",a;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${r}${e.negatives.join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,t.wrap?`(${r}${a})`:a},Et=(e,t,r,n)=>{if(r)return yt(e,t,B({wrap:!1},n));let s=String.fromCharCode(e);if(e===t)return s;let a=String.fromCharCode(t);return`[${s}-${a}]`},xt=(e,t,r)=>{if(Array.isArray(e)){let n=r.wrap===!0,s=r.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return yt(e,t,r)},Ct=(...e)=>new RangeError("Invalid range arguments: "+qr.inspect(...e)),wt=(e,t,r)=>{if(r.strictRanges===!0)throw Ct([e,t]);return[]},Qr=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Xr=(e,t,r=1,n={})=>{let s=Number(e),a=Number(t);if(!Number.isInteger(s)||!Number.isInteger(a)){if(n.strictRanges===!0)throw Ct([e,t]);return[]}s===0&&(s=0),a===0&&(a=0);let i=s>a,o=String(e),h=String(t),g=String(r);r=Math.max(Math.abs(r),1);let f=Ue(o)||Ue(h)||Ue(g),A=f?Math.max(o.length,h.length,g.length):0,p=f===!1&&Wr(e,t,n)===!1,k=n.transform||Kr(p);if(n.toRegex&&r===1)return Et(_t(e,A),_t(t,A),!0,n);let y={negatives:[],positives:[]},R=T=>y[T<0?"negatives":"positives"].push(Math.abs(T)),_=[],x=0;for(;i?s>=a:s<=a;)n.toRegex===!0&&r>1?R(s):_.push(jr(k(s,x),A,p)),s=i?s-r:s+r,x++;return n.toRegex===!0?r>1?Fr(y,n):xt(_,null,B({wrap:!1},n)):_},Zr=(e,t,r=1,n={})=>{if(!Re(e)&&e.length>1||!Re(t)&&t.length>1)return wt(e,t,n);let s=n.transform||(p=>String.fromCharCode(p)),a=`${e}`.charCodeAt(0),i=`${t}`.charCodeAt(0),o=a>i,h=Math.min(a,i),g=Math.max(a,i);if(n.toRegex&&r===1)return Et(h,g,!1,n);let f=[],A=0;for(;o?a>=i:a<=i;)f.push(s(a,A)),a=o?a-r:a+r,A++;return n.toRegex===!0?xt(f,null,{wrap:!1,options:n}):f},Te=(e,t,r,n={})=>{if(t==null&&De(e))return[e];if(!De(e)||!De(t))return wt(e,t,n);if(typeof r=="function")return Te(e,t,1,{transform:r});if(bt(r))return Te(e,t,0,r);let s=B({},n);return s.capture===!0&&(s.wrap=!0),r=r||s.step||1,Re(r)?Re(e)&&Re(t)?Xr(e,t,r,s):Zr(e,t,Math.max(Math.abs(r),1),s):r!=null&&!bt(r)?Qr(r,s):Te(e,t,1,r)};Rt.exports=Te});var Ht=K((ls,St)=>{"use strict";var Yr=Ge(),vt=$e(),zr=(e,t={})=>{let r=(n,s={})=>{let a=vt.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o=a===!0||i===!0,h=t.escapeInvalid===!0?"\\":"",g="";if(n.isOpen===!0||n.isClose===!0)return h+n.value;if(n.type==="open")return o?h+n.value:"(";if(n.type==="close")return o?h+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":o?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let f=vt.reduce(n.nodes),A=Yr(...f,Q(B({},t),{wrap:!1,toRegex:!0}));if(A.length!==0)return f.length>1&&A.length>1?`(${A})`:A}if(n.nodes)for(let f of n.nodes)g+=r(f,n);return g};return r(e)};St.exports=zr});var Tt=K((ps,$t)=>{"use strict";var Vr=Ge(),kt=ke(),he=$e(),fe=(e="",t="",r=!1)=>{let n=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return r?he.flatten(t).map(s=>`{${s}}`):t;for(let s of e)if(Array.isArray(s))for(let a of s)n.push(fe(a,t,r));else for(let a of t)r===!0&&typeof a=="string"&&(a=`{${a}}`),n.push(Array.isArray(a)?fe(s,a,r):s+a);return he.flatten(n)},Jr=(e,t={})=>{let r=t.rangeLimit===void 0?1e3:t.rangeLimit,n=(s,a={})=>{s.queue=[];let i=a,o=a.queue;for(;i.type!=="brace"&&i.type!=="root"&&i.parent;)i=i.parent,o=i.queue;if(s.invalid||s.dollar){o.push(fe(o.pop(),kt(s,t)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){o.push(fe(o.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let A=he.reduce(s.nodes);if(he.exceedsLimit(...A,t.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=Vr(...A,t);p.length===0&&(p=kt(s,t)),o.push(fe(o.pop(),p)),s.nodes=[];return}let h=he.encloseBrace(s),g=s.queue,f=s;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,g=f.queue;for(let A=0;A{"use strict";Lt.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 Pt=K((hs,Nt)=>{"use strict";var en=ke(),{MAX_LENGTH:It,CHAR_BACKSLASH:qe,CHAR_BACKTICK:tn,CHAR_COMMA:rn,CHAR_DOT:nn,CHAR_LEFT_PARENTHESES:sn,CHAR_RIGHT_PARENTHESES:an,CHAR_LEFT_CURLY_BRACE:on,CHAR_RIGHT_CURLY_BRACE:cn,CHAR_LEFT_SQUARE_BRACKET:Bt,CHAR_RIGHT_SQUARE_BRACKET:Mt,CHAR_DOUBLE_QUOTE:un,CHAR_SINGLE_QUOTE:ln,CHAR_NO_BREAK_SPACE:pn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:fn}=Ot(),hn=(e,t={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let r=t||{},n=typeof r.maxLength=="number"?Math.min(It,r.maxLength):It;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},a=[s],i=s,o=s,h=0,g=e.length,f=0,A=0,p,k={},y=()=>e[f++],R=_=>{if(_.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&_.type==="text"){o.value+=_.value;return}return i.nodes.push(_),_.parent=i,_.prev=o,o=_,_};for(R({type:"bos"});f0){if(i.ranges>0){i.ranges=0;let _=i.nodes.shift();i.nodes=[_,{type:"text",value:en(i)}]}R({type:"comma",value:p}),i.commas++;continue}if(p===nn&&A>0&&i.commas===0){let _=i.nodes;if(A===0||_.length===0){R({type:"text",value:p});continue}if(o.type==="dot"){if(i.range=[],o.value+=p,o.type="range",i.nodes.length!==3&&i.nodes.length!==5){i.invalid=!0,i.ranges=0,o.type="text";continue}i.ranges++,i.args=[];continue}if(o.type==="range"){_.pop();let x=_[_.length-1];x.value+=o.value+p,o=x,i.ranges--;continue}R({type:"dot",value:p});continue}R({type:"text",value:p})}do if(i=a.pop(),i.type!=="root"){i.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 _=a[a.length-1],x=_.nodes.indexOf(i);_.nodes.splice(x,1,...i.nodes)}while(a.length>0);return R({type:"eos"}),s};Nt.exports=hn});var Gt=K((ds,Dt)=>{"use strict";var Ut=ke(),dn=Ht(),gn=Tt(),mn=Pt(),V=(e,t={})=>{let r=[];if(Array.isArray(e))for(let n of e){let s=V.create(n,t);Array.isArray(s)?r.push(...s):r.push(s)}else r=[].concat(V.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(r=[...new Set(r)]),r};V.parse=(e,t={})=>mn(e,t);V.stringify=(e,t={})=>typeof e=="string"?Ut(V.parse(e,t),t):Ut(e,t);V.compile=(e,t={})=>(typeof e=="string"&&(e=V.parse(e,t)),dn(e,t));V.expand=(e,t={})=>{typeof e=="string"&&(e=V.parse(e,t));let r=gn(e,t);return t.noempty===!0&&(r=r.filter(Boolean)),t.nodupes===!0&&(r=[...new Set(r)]),r};V.create=(e,t={})=>e===""||e.length<3?[e]:t.expand!==!0?V.compile(e,t):V.expand(e,t);Dt.exports=V});var ye=K((gs,qt)=>{"use strict";var An=require("path"),ie="\\\\/",Kt=`[^${ie}]`,ce="\\.",Rn="\\+",yn="\\?",Le="\\/",bn="(?=.)",Wt="[^/]",Ke=`(?:${Le}|$)`,jt=`(?:^|${Le})`,We=`${ce}{1,2}${Ke}`,_n=`(?!${ce})`,En=`(?!${jt}${We})`,xn=`(?!${ce}{0,1}${Ke})`,Cn=`(?!${We})`,wn=`[^.${Le}]`,Sn=`${Wt}*?`,Ft={DOT_LITERAL:ce,PLUS_LITERAL:Rn,QMARK_LITERAL:yn,SLASH_LITERAL:Le,ONE_CHAR:bn,QMARK:Wt,END_ANCHOR:Ke,DOTS_SLASH:We,NO_DOT:_n,NO_DOTS:En,NO_DOT_SLASH:xn,NO_DOTS_SLASH:Cn,QMARK_NO_DOT:wn,STAR:Sn,START_ANCHOR:jt},vn=Q(B({},Ft),{SLASH_LITERAL:`[${ie}]`,QMARK:Kt,STAR:`${Kt}*?`,DOTS_SLASH:`${ce}{1,2}(?:[${ie}]|$)`,NO_DOT:`(?!${ce})`,NO_DOTS:`(?!(?:^|[${ie}])${ce}{1,2}(?:[${ie}]|$))`,NO_DOT_SLASH:`(?!${ce}{0,1}(?:[${ie}]|$))`,NO_DOTS_SLASH:`(?!${ce}{1,2}(?:[${ie}]|$))`,QMARK_NO_DOT:`[^.${ie}]`,START_ANCHOR:`(?:^|[${ie}])`,END_ANCHOR:`(?:[${ie}]|$)`}),Hn={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:Hn,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:An.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?vn:Ft}}});var be=K(Z=>{"use strict";var $n=require("path"),kn=process.platform==="win32",{REGEX_BACKSLASH:Tn,REGEX_REMOVE_BACKSLASH:Ln,REGEX_SPECIAL_CHARS:On,REGEX_SPECIAL_CHARS_GLOBAL:Nn}=ye();Z.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Z.hasRegexChars=e=>On.test(e);Z.isRegexChar=e=>e.length===1&&Z.hasRegexChars(e);Z.escapeRegex=e=>e.replace(Nn,"\\$1");Z.toPosixSlashes=e=>e.replace(Tn,"/");Z.removeBackslashes=e=>e.replace(Ln,t=>t==="\\"?"":t);Z.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Z.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:kn===!0||$n.sep==="\\";Z.escapeLast=(e,t,r)=>{let n=e.lastIndexOf(t,r);return n===-1?e:e[n-1]==="\\"?Z.escapeLast(e,t,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Z.removePrefix=(e,t={})=>{let r=e;return r.startsWith("./")&&(r=r.slice(2),t.prefix="./"),r};Z.wrapOutput=(e,t={},r={})=>{let n=r.contains?"":"^",s=r.contains?"":"$",a=`${n}(?:${e})${s}`;return t.negated===!0&&(a=`(?:^(?!${a}).*$)`),a}});var er=K((As,Qt)=>{"use strict";var Xt=be(),{CHAR_ASTERISK:je,CHAR_AT:In,CHAR_BACKWARD_SLASH:_e,CHAR_COMMA:Bn,CHAR_DOT:Fe,CHAR_EXCLAMATION_MARK:Qe,CHAR_FORWARD_SLASH:Zt,CHAR_LEFT_CURLY_BRACE:Xe,CHAR_LEFT_PARENTHESES:Ze,CHAR_LEFT_SQUARE_BRACKET:Mn,CHAR_PLUS:Pn,CHAR_QUESTION_MARK:Yt,CHAR_RIGHT_CURLY_BRACE:Dn,CHAR_RIGHT_PARENTHESES:zt,CHAR_RIGHT_SQUARE_BRACKET:Un}=ye(),Vt=e=>e===Zt||e===_e,Jt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?Infinity:1)},Gn=(e,t)=>{let r=t||{},n=e.length-1,s=r.parts===!0||r.scanToEnd===!0,a=[],i=[],o=[],h=e,g=-1,f=0,A=0,p=!1,k=!1,y=!1,R=!1,_=!1,x=!1,T=!1,O=!1,W=!1,G=!1,ne=0,E,b,C={value:"",depth:0,isGlob:!1},M=()=>g>=n,l=()=>h.charCodeAt(g+1),H=()=>(E=b,h.charCodeAt(++g));for(;g0&&(j=h.slice(0,f),h=h.slice(f),A-=f),w&&y===!0&&A>0?(w=h.slice(0,A),c=h.slice(A)):y===!0?(w="",c=h):w=h,w&&w!==""&&w!=="/"&&w!==h&&Vt(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),r.unescape===!0&&(c&&(c=Xt.removeBackslashes(c)),w&&T===!0&&(w=Xt.removeBackslashes(w)));let u={prefix:j,input:e,start:f,base:w,glob:c,isBrace:p,isBracket:k,isGlob:y,isExtglob:R,isGlobstar:_,negated:O,negatedExtglob:W};if(r.tokens===!0&&(u.maxDepth=0,Vt(b)||i.push(C),u.tokens=i),r.parts===!0||r.tokens===!0){let I;for(let $=0;${"use strict";var Oe=ye(),J=be(),{MAX_LENGTH:Ne,POSIX_REGEX_SOURCE:qn,REGEX_NON_SPECIAL_CHARS:Kn,REGEX_SPECIAL_CHARS_BACKREF:Wn,REPLACEMENTS:rr}=Oe,jn=(e,t)=>{if(typeof t.expandRange=="function")return t.expandRange(...e,t);e.sort();let r=`[${e.join("-")}]`;try{new RegExp(r)}catch(n){return e.map(s=>J.escapeRegex(s)).join("..")}return r},de=(e,t)=>`Missing ${e}: "${t}" - use "\\\\${t}" to match literal characters`,nr=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=rr[e]||e;let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let a={type:"bos",value:"",output:r.prepend||""},i=[a],o=r.capture?"":"?:",h=J.isWindows(t),g=Oe.globChars(h),f=Oe.extglobChars(g),{DOT_LITERAL:A,PLUS_LITERAL:p,SLASH_LITERAL:k,ONE_CHAR:y,DOTS_SLASH:R,NO_DOT:_,NO_DOT_SLASH:x,NO_DOTS_SLASH:T,QMARK:O,QMARK_NO_DOT:W,STAR:G,START_ANCHOR:ne}=g,E=m=>`(${o}(?:(?!${ne}${m.dot?R:A}).)*?)`,b=r.dot?"":_,C=r.dot?O:W,M=r.bash===!0?E(r):G;r.capture&&(M=`(${M})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let l={input:e,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:i};e=J.removePrefix(e,l),s=e.length;let H=[],w=[],j=[],c=a,u,I=()=>l.index===s-1,$=l.peek=(m=1)=>e[l.index+m],ee=l.advance=()=>e[++l.index]||"",se=()=>e.slice(l.index+1),z=(m="",L=0)=>{l.consumed+=m,l.index+=L},Ce=m=>{l.output+=m.output!=null?m.output:m.value,z(m.value)},xr=()=>{let m=1;for(;$()==="!"&&($(2)!=="("||$(3)==="?");)ee(),l.start++,m++;return m%2==0?!1:(l.negated=!0,l.start++,!0)},we=m=>{l[m]++,j.push(m)},ue=m=>{l[m]--,j.pop()},v=m=>{if(c.type==="globstar"){let L=l.braces>0&&(m.type==="comma"||m.type==="brace"),d=m.extglob===!0||H.length&&(m.type==="pipe"||m.type==="paren");m.type!=="slash"&&m.type!=="paren"&&!L&&!d&&(l.output=l.output.slice(0,-c.output.length),c.type="star",c.value="*",c.output=M,l.output+=c.output)}if(H.length&&m.type!=="paren"&&(H[H.length-1].inner+=m.value),(m.value||m.output)&&Ce(m),c&&c.type==="text"&&m.type==="text"){c.value+=m.value,c.output=(c.output||"")+m.value;return}m.prev=c,i.push(m),c=m},Se=(m,L)=>{let d=Q(B({},f[L]),{conditions:1,inner:""});d.prev=c,d.parens=l.parens,d.output=l.output;let S=(r.capture?"(":"")+d.open;we("parens"),v({type:m,value:L,output:l.output?"":y}),v({type:"paren",extglob:!0,value:ee(),output:S}),H.push(d)},Cr=m=>{let L=m.close+(r.capture?")":""),d;if(m.type==="negate"){let S=M;m.inner&&m.inner.length>1&&m.inner.includes("/")&&(S=E(r)),(S!==M||I()||/^\)+$/.test(se()))&&(L=m.close=`)$))${S}`),m.inner.includes("*")&&(d=se())&&/^\.[^\\/.]+$/.test(d)&&(L=m.close=`)${d})${S})`),m.prev.type==="bos"&&(l.negatedExtglob=!0)}v({type:"paren",extglob:!0,value:u,output:L}),ue("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let m=!1,L=e.replace(Wn,(d,S,P,F,q,Me)=>F==="\\"?(m=!0,d):F==="?"?S?S+F+(q?O.repeat(q.length):""):Me===0?C+(q?O.repeat(q.length):""):O.repeat(P.length):F==="."?A.repeat(P.length):F==="*"?S?S+F+(q?M:""):M:S?d:`\\${d}`);return m===!0&&(r.unescape===!0?L=L.replace(/\\/g,""):L=L.replace(/\\+/g,d=>d.length%2==0?"\\\\":d?"\\":"")),L===e&&r.contains===!0?(l.output=e,l):(l.output=J.wrapOutput(L,l,t),l)}for(;!I();){if(u=ee(),u==="\0")continue;if(u==="\\"){let d=$();if(d==="/"&&r.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",v({type:"text",value:u});continue}let S=/^\\+/.exec(se()),P=0;if(S&&S[0].length>2&&(P=S[0].length,l.index+=P,P%2!=0&&(u+="\\")),r.unescape===!0?u=ee():u+=ee(),l.brackets===0){v({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||c.value==="["||c.value==="[^")){if(r.posix!==!1&&u===":"){let d=c.value.slice(1);if(d.includes("[")&&(c.posix=!0,d.includes(":"))){let S=c.value.lastIndexOf("["),P=c.value.slice(0,S),F=c.value.slice(S+2),q=qn[F];if(q){c.value=P+q,l.backtrack=!0,ee(),!a.output&&i.indexOf(c)===1&&(a.output=y);continue}}}(u==="["&&$()!==":"||u==="-"&&$()==="]")&&(u=`\\${u}`),u==="]"&&(c.value==="["||c.value==="[^")&&(u=`\\${u}`),r.posix===!0&&u==="!"&&c.value==="["&&(u="^"),c.value+=u,Ce({value:u});continue}if(l.quotes===1&&u!=='"'){u=J.escapeRegex(u),c.value+=u,Ce({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,r.keepQuotes===!0&&v({type:"text",value:u});continue}if(u==="("){we("parens"),v({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&r.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Cr(H.pop());continue}v({type:"paren",value:u,output:l.parens?")":"\\)"}),ue("parens");continue}if(u==="["){if(r.nobracket===!0||!se().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else we("brackets");v({type:"bracket",value:u});continue}if(u==="]"){if(r.nobracket===!0||c&&c.type==="bracket"&&c.value.length===1){v({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(de("opening","["));v({type:"text",value:u,output:`\\${u}`});continue}ue("brackets");let d=c.value.slice(1);if(c.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),c.value+=u,Ce({value:u}),r.literalBrackets===!1||J.hasRegexChars(d))continue;let S=J.escapeRegex(c.value);if(l.output=l.output.slice(0,-c.value.length),r.literalBrackets===!0){l.output+=S,c.value=S;continue}c.value=`(${o}${S}|${c.value})`,l.output+=c.value;continue}if(u==="{"&&r.nobrace!==!0){we("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),v(d);continue}if(u==="}"){let d=w[w.length-1];if(r.nobrace===!0||!d){v({type:"text",value:u,output:u});continue}let S=")";if(d.dots===!0){let P=i.slice(),F=[];for(let q=P.length-1;q>=0&&(i.pop(),P[q].type!=="brace");q--)P[q].type!=="dots"&&F.unshift(P[q].value);S=jn(F,r),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let P=l.output.slice(0,d.outputIndex),F=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=S="\\}",l.output=P;for(let q of F)l.output+=q.output||q.value}v({type:"brace",value:u,output:S}),ue("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,v({type:"text",value:u});continue}if(u===","){let d=u,S=w[w.length-1];S&&j[j.length-1]==="braces"&&(S.comma=!0,d="|"),v({type:"comma",value:u,output:d});continue}if(u==="/"){if(c.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",i.pop(),c=a;continue}v({type:"slash",value:u,output:k});continue}if(u==="."){if(l.braces>0&&c.type==="dot"){c.value==="."&&(c.output=A);let d=w[w.length-1];c.type="dots",c.output+=u,c.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&c.type!=="bos"&&c.type!=="slash"){v({type:"text",value:u,output:A});continue}v({type:"dot",value:u,output:A});continue}if(u==="?"){if(!(c&&c.value==="(")&&r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("qmark",u);continue}if(c&&c.type==="paren"){let S=$(),P=u;if(S==="<"&&!J.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(c.value==="("&&!/[!=<:]/.test(S)||S==="<"&&!/<([!=]|\w+>)/.test(se()))&&(P=`\\${u}`),v({type:"text",value:u,output:P});continue}if(r.dot!==!0&&(c.type==="slash"||c.type==="bos")){v({type:"qmark",value:u,output:W});continue}v({type:"qmark",value:u,output:O});continue}if(u==="!"){if(r.noextglob!==!0&&$()==="("&&($(2)!=="?"||!/[!=<:]/.test($(3)))){Se("negate",u);continue}if(r.nonegate!==!0&&l.index===0){xr();continue}}if(u==="+"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("plus",u);continue}if(c&&c.value==="("||r.regex===!1){v({type:"plus",value:u,output:p});continue}if(c&&(c.type==="bracket"||c.type==="paren"||c.type==="brace")||l.parens>0){v({type:"plus",value:u});continue}v({type:"plus",value:p});continue}if(u==="@"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){v({type:"at",extglob:!0,value:u,output:""});continue}v({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Kn.exec(se());d&&(u+=d[0],l.index+=d[0].length),v({type:"text",value:u});continue}if(c&&(c.type==="globstar"||c.star===!0)){c.type="star",c.star=!0,c.value+=u,c.output=M,l.backtrack=!0,l.globstar=!0,z(u);continue}let m=se();if(r.noextglob!==!0&&/^\([^?]/.test(m)){Se("star",u);continue}if(c.type==="star"){if(r.noglobstar===!0){z(u);continue}let d=c.prev,S=d.prev,P=d.type==="slash"||d.type==="bos",F=S&&(S.type==="star"||S.type==="globstar");if(r.bash===!0&&(!P||m[0]&&m[0]!=="/")){v({type:"star",value:u,output:""});continue}let q=l.braces>0&&(d.type==="comma"||d.type==="brace"),Me=H.length&&(d.type==="pipe"||d.type==="paren");if(!P&&d.type!=="paren"&&!q&&!Me){v({type:"star",value:u,output:""});continue}for(;m.slice(0,3)==="/**";){let ve=e[l.index+4];if(ve&&ve!=="/")break;m=m.slice(3),z("/**",3)}if(d.type==="bos"&&I()){c.type="globstar",c.value+=u,c.output=E(r),l.output=c.output,l.globstar=!0,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!F&&I()){l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=E(r)+(r.strictSlashes?")":"|$)"),c.value+=u,l.globstar=!0,l.output+=d.output+c.output,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&m[0]==="/"){let ve=m[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=`${E(r)}${k}|${k}${ve})`,c.value+=u,l.output+=d.output+c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&m[0]==="/"){c.type="globstar",c.value+=u,c.output=`(?:^|${k}|${E(r)}${k})`,l.output=c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-c.output.length),c.type="globstar",c.output=E(r),c.value+=u,l.output+=c.output,l.globstar=!0,z(u);continue}let L={type:"star",value:u,output:M};if(r.bash===!0){L.output=".*?",(c.type==="bos"||c.type==="slash")&&(L.output=b+L.output),v(L);continue}if(c&&(c.type==="bracket"||c.type==="paren")&&r.regex===!0){L.output=u,v(L);continue}(l.index===l.start||c.type==="slash"||c.type==="dot")&&(c.type==="dot"?(l.output+=x,c.output+=x):r.dot===!0?(l.output+=T,c.output+=T):(l.output+=b,c.output+=b),$()!=="*"&&(l.output+=y,c.output+=y)),v(L)}for(;l.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=J.escapeLast(l.output,"["),ue("brackets")}for(;l.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=J.escapeLast(l.output,"("),ue("parens")}for(;l.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=J.escapeLast(l.output,"{"),ue("braces")}if(r.strictSlashes!==!0&&(c.type==="star"||c.type==="bracket")&&v({type:"maybe_slash",value:"",output:`${k}?`}),l.backtrack===!0){l.output="";for(let m of l.tokens)l.output+=m.output!=null?m.output:m.value,m.suffix&&(l.output+=m.suffix)}return l};nr.fastpaths=(e,t)=>{let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=rr[e]||e;let a=J.isWindows(t),{DOT_LITERAL:i,SLASH_LITERAL:o,ONE_CHAR:h,DOTS_SLASH:g,NO_DOT:f,NO_DOTS:A,NO_DOTS_SLASH:p,STAR:k,START_ANCHOR:y}=Oe.globChars(a),R=r.dot?A:f,_=r.dot?p:f,x=r.capture?"":"?:",T={negated:!1,prefix:""},O=r.bash===!0?".*?":k;r.capture&&(O=`(${O})`);let W=b=>b.noglobstar===!0?O:`(${x}(?:(?!${y}${b.dot?g:i}).)*?)`,G=b=>{switch(b){case"*":return`${R}${h}${O}`;case".*":return`${i}${h}${O}`;case"*.*":return`${R}${O}${i}${h}${O}`;case"*/*":return`${R}${O}${o}${h}${_}${O}`;case"**":return R+W(r);case"**/*":return`(?:${R}${W(r)}${o})?${_}${h}${O}`;case"**/*.*":return`(?:${R}${W(r)}${o})?${_}${O}${i}${h}${O}`;case"**/.*":return`(?:${R}${W(r)}${o})?${i}${h}${O}`;default:{let C=/^(.*?)\.(\w+)$/.exec(b);if(!C)return;let M=G(C[1]);return M?M+i+C[2]:void 0}}},ne=J.removePrefix(e,T),E=G(ne);return E&&r.strictSlashes!==!0&&(E+=`${o}?`),E};tr.exports=nr});var ir=K((ys,ar)=>{"use strict";var Fn=require("path"),Qn=er(),Ye=sr(),ze=be(),Xn=ye(),Zn=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,t,r=!1)=>{if(Array.isArray(e)){let f=e.map(p=>D(p,t,r));return p=>{for(let k of f){let y=k(p);if(y)return y}return!1}}let n=Zn(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=t||{},a=ze.isWindows(t),i=n?D.compileRe(e,t):D.makeRe(e,t,!1,!0),o=i.state;delete i.state;let h=()=>!1;if(s.ignore){let f=Q(B({},t),{ignore:null,onMatch:null,onResult:null});h=D(s.ignore,f,r)}let g=(f,A=!1)=>{let{isMatch:p,match:k,output:y}=D.test(f,i,t,{glob:e,posix:a}),R={glob:e,state:o,regex:i,posix:a,input:f,output:y,match:k,isMatch:p};return typeof s.onResult=="function"&&s.onResult(R),p===!1?(R.isMatch=!1,A?R:!1):h(f)?(typeof s.onIgnore=="function"&&s.onIgnore(R),R.isMatch=!1,A?R:!1):(typeof s.onMatch=="function"&&s.onMatch(R),A?R:!0)};return r&&(g.state=o),g};D.test=(e,t,r,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let a=r||{},i=a.format||(s?ze.toPosixSlashes:null),o=e===n,h=o&&i?i(e):e;return o===!1&&(h=i?i(e):e,o=h===n),(o===!1||a.capture===!0)&&(a.matchBase===!0||a.basename===!0?o=D.matchBase(e,t,r,s):o=t.exec(h)),{isMatch:Boolean(o),match:o,output:h}};D.matchBase=(e,t,r,n=ze.isWindows(r))=>(t instanceof RegExp?t:D.makeRe(t,r)).test(Fn.basename(e));D.isMatch=(e,t,r)=>D(t,r)(e);D.parse=(e,t)=>Array.isArray(e)?e.map(r=>D.parse(r,t)):Ye(e,Q(B({},t),{fastpaths:!1}));D.scan=(e,t)=>Qn(e,t);D.compileRe=(e,t,r=!1,n=!1)=>{if(r===!0)return e.output;let s=t||{},a=s.contains?"":"^",i=s.contains?"":"$",o=`${a}(?:${e.output})${i}`;e&&e.negated===!0&&(o=`^(?!${o}).*$`);let h=D.toRegex(o,t);return n===!0&&(h.state=e),h};D.makeRe=(e,t={},r=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ye.fastpaths(e,t)),s.output||(s=Ye(e,t)),D.compileRe(s,t,r,n)};D.toRegex=(e,t)=>{try{let r=t||{};return new RegExp(e,r.flags||(r.nocase?"i":""))}catch(r){if(t&&t.debug===!0)throw r;return/$^/}};D.constants=Xn;ar.exports=D});var cr=K((bs,or)=>{"use strict";or.exports=ir()});var hr=K((_s,ur)=>{"use strict";var lr=require("util"),pr=Gt(),oe=cr(),Ve=be(),fr=e=>e===""||e==="./",N=(e,t,r)=>{t=[].concat(t),e=[].concat(e);let n=new Set,s=new Set,a=new Set,i=0,o=f=>{a.add(f.output),r&&r.onResult&&r.onResult(f)};for(let f=0;f!n.has(f));if(r&&g.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${t.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?t.map(f=>f.replace(/\\/g,"")):t}return g};N.match=N;N.matcher=(e,t)=>oe(e,t);N.isMatch=(e,t,r)=>oe(t,r)(e);N.any=N.isMatch;N.not=(e,t,r={})=>{t=[].concat(t).map(String);let n=new Set,s=[],a=o=>{r.onResult&&r.onResult(o),s.push(o.output)},i=N(e,t,Q(B({},r),{onResult:a}));for(let o of s)i.includes(o)||n.add(o);return[...n]};N.contains=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);if(Array.isArray(t))return t.some(n=>N.contains(e,n,r));if(typeof t=="string"){if(fr(e)||fr(t))return!1;if(e.includes(t)||e.startsWith("./")&&e.slice(2).includes(t))return!0}return N.isMatch(e,t,Q(B({},r),{contains:!0}))};N.matchKeys=(e,t,r)=>{if(!Ve.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),t,r),s={};for(let a of n)s[a]=e[a];return s};N.some=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(n.some(i=>a(i)))return!0}return!1};N.every=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(!n.every(i=>a(i)))return!1}return!0};N.all=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);return[].concat(t).every(n=>oe(n,r)(e))};N.capture=(e,t,r)=>{let n=Ve.isWindows(r),a=oe.makeRe(String(e),Q(B({},r),{capture:!0})).exec(n?Ve.toPosixSlashes(t):t);if(a)return a.slice(1).map(i=>i===void 0?"":i)};N.makeRe=(...e)=>oe.makeRe(...e);N.scan=(...e)=>oe.scan(...e);N.parse=(e,t)=>{let r=[];for(let n of[].concat(e||[]))for(let s of pr(String(n),t))r.push(oe.parse(s,t));return r};N.braces=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return t&&t.nobrace===!0||!/\{.*\}/.test(e)?[e]:pr(e,t)};N.braceExpand=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,Q(B({},t),{expand:!0}))};ur.exports=N});var gr=K((Es,dr)=>{"use strict";dr.exports=(e,...t)=>new Promise(r=>{r(e(...t))})});var Ar=K((xs,Je)=>{"use strict";var Yn=gr(),mr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let t=[],r=0,n=()=>{r--,t.length>0&&t.shift()()},s=(o,h,...g)=>{r++;let f=Yn(o,...g);h(f),f.then(n,n)},a=(o,h,...g)=>{rnew Promise(g=>a(o,g,...h));return Object.defineProperties(i,{activeCount:{get:()=>r},pendingCount:{get:()=>t.length}}),i};Je.exports=mr;Je.exports.default=mr});var Vn={};Or(Vn,{default:()=>es});var He=X(require("@yarnpkg/cli")),ae=X(require("@yarnpkg/core")),nt=X(require("@yarnpkg/core")),le=X(require("clipanion")),Ae=class extends He.BaseCommand{constructor(){super(...arguments);this.json=le.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=le.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=le.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=le.Option.Rest()}async execute(){let t=await ae.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ae.Project.find(t,this.context.cwd),s=await ae.Cache.find(t);await r.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(r.workspaces);else if(this.workspaces.length===0){if(!n)throw new He.WorkspaceRequiredError(r.cwd,this.context.cwd);a=new Set([n])}else a=new Set(this.workspaces.map(o=>r.getWorkspaceByIdent(nt.structUtils.parseIdent(o))));for(let o of a)for(let h of this.production?["dependencies"]:ae.Manifest.hardDependencies)for(let g of o.manifest.getForScope(h).values()){let f=r.tryWorkspaceByDescriptor(g);f!==null&&a.add(f)}for(let o of r.workspaces)a.has(o)?this.production&&o.manifest.devDependencies.clear():(o.manifest.installConfig=o.manifest.installConfig||{},o.manifest.installConfig.selfReferences=!1,o.manifest.dependencies.clear(),o.manifest.devDependencies.clear(),o.manifest.peerDependencies.clear(),o.manifest.scripts.clear());return(await ae.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async o=>{await r.install({cache:s,report:o,persistProject:!1})})).exitCode()}};Ae.paths=[["workspaces","focus"]],Ae.usage=le.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 st=Ae;var Ie=X(require("@yarnpkg/cli")),ge=X(require("@yarnpkg/core")),Ee=X(require("@yarnpkg/core")),Y=X(require("@yarnpkg/core")),Rr=X(require("@yarnpkg/plugin-git")),U=X(require("clipanion")),Be=X(hr()),yr=X(require("os")),br=X(Ar()),re=X(require("typanion")),xe=class extends Ie.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:re.isOneOf([re.isEnum(["unlimited"]),re.applyCascade(re.isNumber(),[re.isInteger(),re.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:r,workspace:n}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!n)throw new Ie.WorkspaceRequiredError(r.cwd,this.context.cwd);await r.restoreInstallState();let s=this.cli.process([this.commandName,...this.args]),a=s.path.length===1&&s.path[0]==="run"&&typeof s.scriptName!="undefined"?s.scriptName:null;if(s.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let i=this.all?r.topLevelWorkspace:n,o=this.since?Array.from(await Rr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:r})):[i,...this.from.length>0?i.getRecursiveWorkspaceChildren():[]],h=E=>Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.from),g=this.from.length>0?o.filter(h):o,f=new Set([...g,...g.map(E=>[...this.recursive?this.since?E.getRecursiveWorkspaceDependents():E.getRecursiveWorkspaceDependencies():E.getRecursiveWorkspaceChildren()]).flat()]),A=[],p=!1;if(a==null?void 0:a.includes(":")){for(let E of r.workspaces)if(E.manifest.scripts.has(a)&&(p=!p,p===!1))break}for(let E of f)a&&!E.manifest.scripts.has(a)&&!p&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(E)).has(a)||a===process.env.npm_lifecycle_event&&E.cwd===n.cwd||this.include.length>0&&!Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.include)||this.exclude.length>0&&Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.exclude)||this.publicOnly&&E.manifest.private===!0||A.push(E);let k=this.parallel?this.jobs==="unlimited"?Infinity:this.jobs||Math.max(1,(0,yr.cpus)().length/2):1,y=k===1?!1:this.parallel,R=y?this.interlaced:!0,_=(0,br.default)(k),x=new Map,T=new Set,O=0,W=null,G=!1,ne=await Ee.StreamReport.start({configuration:t,stdout:this.context.stdout},async E=>{let b=async(C,{commandIndex:M})=>{if(G)return-1;!y&&this.verbose&&M>1&&E.reportSeparator();let l=zn(C,{configuration:t,verbose:this.verbose,commandIndex:M}),[H,w]=_r(E,{prefix:l,interlaced:R}),[j,c]=_r(E,{prefix:l,interlaced:R});try{this.verbose&&E.reportInfo(null,`${l} Process started`);let u=Date.now(),I=await this.cli.run([this.commandName,...this.args],{cwd:C.cwd,stdout:H,stderr:j})||0;H.end(),j.end(),await w,await c;let $=Date.now();if(this.verbose){let ee=t.get("enableTimers")?`, completed in ${Y.formatUtils.pretty(t,$-u,Y.formatUtils.Type.DURATION)}`:"";E.reportInfo(null,`${l} Process exited (exit code ${I})${ee}`)}return I===130&&(G=!0,W=I),I}catch(u){throw H.end(),j.end(),await w,await c,u}};for(let C of A)x.set(C.anchoredLocator.locatorHash,C);for(;x.size>0&&!E.hasErrors();){let C=[];for(let[H,w]of x){if(T.has(w.anchoredDescriptor.descriptorHash))continue;let j=!0;if(this.topological||this.topologicalDev){let c=this.topologicalDev?new Map([...w.manifest.dependencies,...w.manifest.devDependencies]):w.manifest.dependencies;for(let u of c.values()){let I=r.tryWorkspaceByDescriptor(u);if(j=I===null||!x.has(I.anchoredLocator.locatorHash),!j)break}}if(!!j&&(T.add(w.anchoredDescriptor.descriptorHash),C.push(_(async()=>{let c=await b(w,{commandIndex:++O});return x.delete(H),T.delete(w.anchoredDescriptor.descriptorHash),c})),!y))break}if(C.length===0){let H=Array.from(x.values()).map(w=>Y.structUtils.prettyLocator(t,w.anchoredLocator)).join(", ");E.reportError(Ee.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${H})`);return}let l=(await Promise.all(C)).find(H=>H!==0);W===null&&(W=typeof l!="undefined"?1:W),(this.topological||this.topologicalDev)&&typeof l!="undefined"&&E.reportError(Ee.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return W!==null?W:ne.exitCode()}};xe.paths=[["workspaces","foreach"]],xe.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"]]});var Er=xe;function _r(e,{prefix:t,interlaced:r}){let n=e.createStreamReporter(t),s=new Y.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let a=new Promise(o=>{n.on("finish",()=>{o(s.active)})});if(r)return[s,a];let i=new Y.miscUtils.BufferStream;return i.pipe(s,{end:!1}),i.on("finish",()=>{s.end()}),[i,a]}function zn(e,{configuration:t,commandIndex:r,verbose:n}){if(!n)return null;let s=Y.structUtils.convertToIdent(e.locator),i=`[${Y.structUtils.stringifyIdent(s)}]:`,o=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],h=o[r%o.length];return Y.formatUtils.pretty(t,i,h)}var Jn={commands:[st,Er]},es=Jn;return Vn;})(); 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 | - path: .yarn/plugins/@yarnpkg/plugin-constraints.cjs 7 | spec: "@yarnpkg/plugin-constraints" 8 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 9 | spec: "@yarnpkg/plugin-version" 10 | 11 | yarnPath: .yarn/releases/yarn-3.5.0.cjs 12 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | -------------------------------------------------------------------------------- /apps/docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require('nextra')({ 2 | theme: 'nextra-theme-docs', 3 | themeConfig: './theme.config.js', 4 | }); 5 | 6 | module.exports = withNextra(); 7 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@onehop-client-apps/docs", 3 | "packageManager": "yarn@3.5.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start" 8 | }, 9 | "dependencies": { 10 | "next": "^13.1.4", 11 | "nextra": "alpha", 12 | "nextra-theme-docs": "alpha", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "author": "Hop Development Team", 17 | "homepage": "https://github.com/hopinc/hop-client-js/tree/master/apps/docs", 18 | "keywords": [ 19 | "realtime", 20 | "channels", 21 | "pipe", 22 | "client", 23 | "react" 24 | ], 25 | "license": "MIT", 26 | "repository": "https://github.com/hopinc/hop-client-js.git", 27 | "version": "1.6.5" 28 | } 29 | -------------------------------------------------------------------------------- /apps/docs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import {AppProps} from 'next/app'; 2 | import 'nextra-theme-docs/style.css'; 3 | 4 | type GetLayout = (page: JSX.Element) => JSX.Element; 5 | 6 | export default function Nextra({Component, pageProps}: AppProps) { 7 | const getLayout = 8 | (Component as {getLayout?: GetLayout}).getLayout ?? 9 | ((page: JSX.Element) => page); 10 | 11 | return getLayout(); 12 | } 13 | -------------------------------------------------------------------------------- /apps/docs/pages/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | --- 4 | 5 | # Hop JS Client SDK 6 | 7 | This site houses the documentation for Hop's JavaScript client SDK 8 | -------------------------------------------------------------------------------- /apps/docs/pages/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "Introduction" 3 | } 4 | -------------------------------------------------------------------------------- /apps/docs/theme.config.js: -------------------------------------------------------------------------------- 1 | // theme.config.js 2 | export default { 3 | projectLink: 'https://github.com/hopinc/hop-js', 4 | docsRepositoryBase: 5 | 'https://github.com/hopinc/hop-js/tree/master/apps/docs/pages', 6 | titleSuffix: ' — Hop JS Client SDK', 7 | nextLinks: true, 8 | prevLinks: true, 9 | search: true, 10 | customSearch: null, 11 | darkMode: true, 12 | footer: true, 13 | footerText: `MIT ${new Date().getFullYear()} © Hop, Inc.`, 14 | footerEditLink: 'Edit this page on GitHub', 15 | logo: Hop, 16 | unstable_faviconGlyph: '👋', 17 | head: ( 18 | <> 19 | 20 | 21 | 22 | 23 | ), 24 | }; 25 | -------------------------------------------------------------------------------- /apps/docs/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 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/playground/README.md: -------------------------------------------------------------------------------- 1 | # playground 2 | -------------------------------------------------------------------------------- /apps/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@onehop-client-apps/client-playground", 3 | "packageManager": "yarn@3.5.0", 4 | "dependencies": { 5 | "@onehop/client": "workspace:^", 6 | "@onehop/react": "workspace:^", 7 | "@types/react": "^18.0.27", 8 | "@types/react-dom": "^18.0.10", 9 | "@vitejs/plugin-react": "^3.0.1", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "vite": "^4.0.4" 13 | }, 14 | "author": "Hop Development Team", 15 | "homepage": "https://github.com/hopinc/hop-client-js/tree/master/apps/playground", 16 | "keywords": [ 17 | "realtime", 18 | "channels", 19 | "pipe", 20 | "client", 21 | "react" 22 | ], 23 | "license": "MIT", 24 | "repository": "https://github.com/hopinc/hop-client-js.git", 25 | "version": "1.6.5" 26 | } 27 | -------------------------------------------------------------------------------- /apps/playground/src/app.tsx: -------------------------------------------------------------------------------- 1 | import {hop} from '@onehop/client'; 2 | import {useChannelMessage, useReadChannelState} from '@onehop/react'; 3 | import {useEffect} from 'react'; 4 | 5 | const projectId = 'project_MjcxMDk5Nzg1MTM1MDYzMTA'; 6 | 7 | const instance = hop.init({projectId: '', token: ''}); 8 | 9 | instance.sendMessage('votes', 'add', 1); 10 | instance.sendMessage('public-chat', 'kashcafe', { 11 | user: 'Kashall', 12 | message: 'This is a message!', 13 | }); 14 | 15 | export function Main() { 16 | // useChannelMessage('project_NDc4MjU3NTE1MTE3MzYzMjE', 'ABC', console.log); 17 | const t = useReadChannelState('test'); 18 | 19 | console.log(t); 20 | 21 | useEffect(() => { 22 | hop.init({ 23 | projectId, 24 | }); 25 | }, []); 26 | 27 | return
hi
; 28 | } 29 | -------------------------------------------------------------------------------- /apps/playground/src/index.tsx: -------------------------------------------------------------------------------- 1 | import {createRoot} from 'react-dom/client'; 2 | import {Main} from './app'; 3 | 4 | function App() { 5 | return
; 6 | } 7 | 8 | createRoot(document.getElementById('root')!).render(); 9 | -------------------------------------------------------------------------------- /apps/playground/src/vanilla.ts: -------------------------------------------------------------------------------- 1 | import {hop, pipe} from '@onehop/client'; 2 | 3 | declare const node: HTMLVideoElement; 4 | 5 | const client = hop.init({ 6 | projectId: 'project_xxx', 7 | }); 8 | 9 | const joinToken = 'join_token_xxx'; 10 | 11 | client.subscribeToRoom(joinToken); 12 | 13 | client.getRoomStateMap().addListener(map => { 14 | const room = map.get(joinToken); 15 | 16 | if ( 17 | !room || 18 | room.subscription !== 'available' || 19 | !room.connection.llhls?.edge_endpoint 20 | ) { 21 | console.log('Not ready:', {room}); 22 | return; 23 | } 24 | 25 | pipe.mount(node, room.connection.llhls.edge_endpoint); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /apps/with-next/README.md: -------------------------------------------------------------------------------- 1 | # with-next 2 | -------------------------------------------------------------------------------- /apps/with-next/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/with-next/next.config.js: -------------------------------------------------------------------------------- 1 | // You don't need to use this file in your own project, this is simply to make Next play nicely with preconstruct (which is a tool we use to develop hop-client-js) 2 | 3 | module.exports = require('@preconstruct/next')({}); 4 | -------------------------------------------------------------------------------- /apps/with-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-next", 3 | "packageManager": "yarn@3.5.0", 4 | "dependencies": { 5 | "@onehop/client": "workspace:^", 6 | "@onehop/react": "workspace:^", 7 | "next": "^13.1.4", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0" 10 | }, 11 | "devDependencies": { 12 | "@preconstruct/next": "^4.0.0" 13 | }, 14 | "author": "Hop Development Team", 15 | "keywords": [ 16 | "realtime", 17 | "channels", 18 | "pipe", 19 | "client", 20 | "react" 21 | ], 22 | "license": "MIT", 23 | "repository": "https://github.com/hopinc/hop-client-js.git", 24 | "version": "1.6.5", 25 | "homepage": "https://github.com/hopinc/hop-client-js/tree/master/apps/with-next" 26 | } 27 | -------------------------------------------------------------------------------- /apps/with-next/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import {useInit} from '@onehop/react'; 2 | import {type AppProps} from 'next/app'; 3 | 4 | export default function App({Component, pageProps}: AppProps) { 5 | useInit({ 6 | projectId: 'project_MzMwMzI3NzAyMTcxNTY2MTc', 7 | }); 8 | 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /apps/with-next/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useChannelMessage, 3 | useConnectionState, 4 | useReadChannelState, 5 | } from '@onehop/react'; 6 | import {useState} from 'react'; 7 | 8 | export default function Index() { 9 | const channel = useReadChannelState('isla'); 10 | const connection = useConnectionState(); 11 | 12 | const [message, setMessage] = useState(null); 13 | 14 | useChannelMessage<{message: string}>('isla', 'MESSAGE', data => { 15 | setMessage(data.message); 16 | }); 17 | 18 | return
{JSON.stringify({channel, connection, message}, null, 4)}
; 19 | } 20 | -------------------------------------------------------------------------------- /apps/with-next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "moduleResolution": "node", 17 | "downlevelIteration": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-typescript', '@babel/preset-react'], 3 | }; 4 | -------------------------------------------------------------------------------- /constraints.pro: -------------------------------------------------------------------------------- 1 | gen_workspace_field(WorkspaceCwd, 'description'). 2 | gen_enforced_field(WorkspaceCwd, 'license', 'MIT'). 3 | gen_enforced_field(WorkspaceCwd, 'repository', 'https://github.com/hopinc/hop-client-js.git'). 4 | gen_enforced_field(WorkspaceCwd, 'author', 'Hop Development Team'). 5 | gen_enforced_field(WorkspaceCwd, 'keywords', ['realtime', 'channels', 'pipe', 'client', 'react']). 6 | gen_enforced_field(WorkspaceCwd, 'packageManager', 'yarn@3.5.0'). 7 | gen_enforced_field(WorkspaceCwd, 'version', '1.6.5'). 8 | gen_enforced_field(WorkspaceCwd, 'homepage', Homepage) :- 9 | workspace_field(WorkspaceCwd, 'version', _), 10 | atom_concat('https://github.com/hopinc/hop-client-js/tree/master/', WorkspaceCwd, Homepage). 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hop-client-js", 3 | "license": "MIT", 4 | "packageManager": "yarn@3.5.0", 5 | "description": "Root workspace for hop's client library", 6 | "scripts": { 7 | "build": "yarn preconstruct build", 8 | "dev": "yarn preconstruct dev", 9 | "lint": "yarn eslint \"./{apps,packages}/**/*.{ts,tsx}\"", 10 | "release": "yarn build && yarn constraints --fix && yarn workspaces foreach run release" 11 | }, 12 | "workspaces": [ 13 | "packages/*", 14 | "apps/*" 15 | ], 16 | "preconstruct": { 17 | "packages": [ 18 | "packages/*" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "@babel/preset-react": "^7.18.6", 23 | "@babel/preset-typescript": "^7.18.6", 24 | "@preconstruct/cli": "^2.3.0", 25 | "@typescript-eslint/eslint-plugin": "^5.48.2", 26 | "@typescript-eslint/parser": "^5.48.2", 27 | "alistair": "^1.4.3", 28 | "eslint": "^8.32.0", 29 | "eslint-config-xo": "^0.43.1", 30 | "eslint-config-xo-typescript": "^0.55.1", 31 | "prettier": "^2.8.3", 32 | "typescript": "^4.9.4" 33 | }, 34 | "author": "Hop Development Team", 35 | "homepage": "https://github.com/hopinc/hop-client-js/tree/master/.", 36 | "keywords": [ 37 | "realtime", 38 | "channels", 39 | "pipe", 40 | "client", 41 | "react" 42 | ], 43 | "repository": "https://github.com/hopinc/hop-client-js.git", 44 | "version": "1.6.5" 45 | } 46 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # @onehop/client 2 | 3 | Hop's client 4 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@onehop/client", 3 | "main": "dist/onehop-client.cjs.js", 4 | "module": "dist/onehop-client.esm.js", 5 | "typings": "dist/onehop-client.cjs.d.ts", 6 | "packageManager": "yarn@3.5.0", 7 | "description": "Client library for hop.io", 8 | "sideEffects": false, 9 | "scripts": { 10 | "release": "yarn npm publish --tolerate-republish" 11 | }, 12 | "files": [ 13 | "README.md", 14 | "dist", 15 | "package.json" 16 | ], 17 | "dependencies": { 18 | "@onehop/js": "^1.18.0", 19 | "@onehop/leap-edge-js": "^1.0.11", 20 | "hls.js": "^1.3.1" 21 | }, 22 | "author": "Hop Development Team", 23 | "homepage": "https://github.com/hopinc/hop-client-js/tree/master/packages/client", 24 | "keywords": [ 25 | "realtime", 26 | "channels", 27 | "pipe", 28 | "client", 29 | "react" 30 | ], 31 | "license": "MIT", 32 | "repository": "https://github.com/hopinc/hop-client-js.git", 33 | "version": "1.6.5", 34 | "publishConfig": { 35 | "access": "public" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/client/src/hop.ts: -------------------------------------------------------------------------------- 1 | import type {LeapEdgeAuthenticationParameters} from '@onehop/leap-edge-js'; 2 | import type {ClientInitOptions} from './leap'; 3 | import {instance} from './leap/client'; 4 | 5 | export function init( 6 | leapConnectionParams: LeapEdgeAuthenticationParameters, 7 | options?: ClientInitOptions, 8 | ) { 9 | instance.connect( 10 | leapConnectionParams, 11 | options?.leapSocketUrl 12 | ? {socketUrl: options.leapSocketUrl, debug: false} 13 | : undefined, 14 | ); 15 | 16 | return instance; 17 | } 18 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export {LeapConnectionState as ConnectionState} from '@onehop/leap-edge-js'; 2 | export * as hls from 'hls.js'; 3 | export * as hop from './hop'; 4 | export * as leap from './leap'; 5 | export * as pipe from './pipe'; 6 | export * as util from './util'; 7 | -------------------------------------------------------------------------------- /packages/client/src/leap/client.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | 3 | import type { 4 | EncapsulatingServicePayload, 5 | LeapEdgeAuthenticationParameters, 6 | LeapEdgeInitOptions, 7 | LeapServiceEvent, 8 | } from '@onehop/leap-edge-js'; 9 | import {LeapConnectionState, LeapEdgeClient} from '@onehop/leap-edge-js'; 10 | 11 | import * as util from '../util'; 12 | import type {ChannelStateData, RoomStateData} from './types'; 13 | 14 | import type {Unsubscribe} from '../util/types'; 15 | import {AVAILABLE} from './handlers/AVAILABLE'; 16 | import type {LeapHandler} from './handlers/create'; 17 | import {DIRECT_MESSAGE} from './handlers/DIRECT_MESSAGE'; 18 | import {INIT} from './handlers/INIT'; 19 | import {MESSAGE} from './handlers/MESSAGE'; 20 | import {PIPE_ROOM_AVAILABLE} from './handlers/PIPE_ROOM_AVAILABLE'; 21 | import {PIPE_ROOM_UNAVAILABLE} from './handlers/PIPE_ROOM_UNAVAILABLE'; 22 | import {PIPE_ROOM_UPDATE} from './handlers/PIPE_ROOM_UPDATE'; 23 | import {STATE_UPDATE} from './handlers/STATE_UPDATE'; 24 | import {TOKEN_STATE_UPDATE} from './handlers/TOKEN_STATE_UPDATE'; 25 | import {UNAVAILABLE} from './handlers/UNAVAILABLE'; 26 | 27 | export type ClientEvents = { 28 | MESSAGE: { 29 | event: string; 30 | data: unknown; 31 | channel: string | null; 32 | }; 33 | 34 | STATE_UPDATE: { 35 | channel: string; 36 | state: unknown; 37 | }; 38 | 39 | SERVICE_EVENT: LeapServiceEvent; 40 | 41 | CONNECTION_STATE_UPDATE: LeapConnectionState; 42 | }; 43 | 44 | export class Client extends util.emitter.HopEmitter { 45 | public static readonly SUPPORTED_EVENTS: Record = { 46 | INIT, 47 | AVAILABLE, 48 | UNAVAILABLE, 49 | STATE_UPDATE, 50 | TOKEN_STATE_UPDATE, 51 | MESSAGE, 52 | DIRECT_MESSAGE, 53 | 54 | PIPE_ROOM_UNAVAILABLE, 55 | PIPE_ROOM_AVAILABLE, 56 | PIPE_ROOM_UPDATE, 57 | }; 58 | 59 | public hasPreviouslyConnected = false; 60 | 61 | private readonly connectionState = util.atoms.create( 62 | LeapConnectionState.IDLE, 63 | ); 64 | 65 | private leap: LeapEdgeClient | null = null; 66 | 67 | private readonly channelStateMap = new util.maps.ObservableMap< 68 | API.Channels.Channel['id'], 69 | ChannelStateData 70 | >(); 71 | 72 | private readonly channelMessageListeners = new Map< 73 | util.channels.ChannelMessageListenerKey, 74 | Set<(data: unknown) => unknown> 75 | >(); 76 | 77 | private readonly directMessageListeners = new Map< 78 | string, 79 | Set<(data: unknown) => unknown> 80 | >(); 81 | 82 | private readonly roomStateMap = new util.maps.ObservableMap< 83 | API.Pipe.Room['join_token'], 84 | RoomStateData 85 | >(); 86 | 87 | private readonly rawServiceEventListeners = new Set< 88 | (message: LeapServiceEvent) => unknown 89 | >(); 90 | 91 | // Rule is broken — constructor is not useless because HopEmitter#constructor is protected 92 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 93 | constructor() { 94 | super(); 95 | } 96 | 97 | connect(auth: LeapEdgeAuthenticationParameters, opts?: LeapEdgeInitOptions) { 98 | if (this.leap) { 99 | return; 100 | } 101 | 102 | const leap = this.getLeap(auth, opts); 103 | 104 | const serviceEvent = async (message: LeapServiceEvent) => { 105 | this.emit('SERVICE_EVENT', message); 106 | await this.handleServiceEvent(message); 107 | }; 108 | 109 | const connectionStateUpdate = async (state: LeapConnectionState) => { 110 | this.emit('CONNECTION_STATE_UPDATE', state); 111 | 112 | await this.handleConnectionStateUpdate(state); 113 | }; 114 | 115 | leap.on('serviceEvent', serviceEvent); 116 | leap.on('connectionStateUpdate', connectionStateUpdate); 117 | } 118 | 119 | subscribeToRoom(joinToken: string) { 120 | const existingSubscription = this.roomStateMap.get(joinToken); 121 | 122 | const payload: EncapsulatingServicePayload = { 123 | e: 'PIPE_ROOM_SUBSCRIBE', 124 | c: null, 125 | d: {join_token: joinToken}, 126 | }; 127 | 128 | if (existingSubscription) { 129 | if (existingSubscription.subscription === 'unavailable') { 130 | this.send(payload); 131 | } 132 | 133 | return; 134 | } 135 | 136 | this.send(payload); 137 | 138 | this.roomStateMap.set(joinToken, { 139 | subscription: 'pending', 140 | room: null, 141 | }); 142 | } 143 | 144 | unsubscribeFromRoom(joinToken: API.Pipe.Room['join_token']) { 145 | const stream = this.roomStateMap.get(joinToken); 146 | 147 | if (!stream) { 148 | throw new Error('Not subscribed to that room!'); 149 | } 150 | 151 | // This condition is annoying, because we don't know the room ID 152 | // as we never received 'AVAILABLE' 153 | // but we still want to make sure its no longer in this.roomStateMap 154 | // and that the server isn't still processing our subscription 155 | 156 | // Context: An app where there are multiple streams of something 157 | // 1) User enters stream and subscribes (we don't receive PIPE_ROOM_AVAILABLE yet) 158 | // 2) User realises they pressed wrong stream and quickly hits back 159 | // 3) We never got the available message, so we never got the room id 160 | // 4) How unsibcsribe 161 | 162 | // This is a bit of a hack, but it works 163 | if (stream.subscription !== 'available') { 164 | const listener = (message: LeapServiceEvent) => { 165 | if (message.eventType !== 'PIPE_ROOM_AVAILABLE') { 166 | return; 167 | } 168 | 169 | this.unsubscribeFromRoom(joinToken); 170 | this.rawServiceEventListeners.delete(listener); 171 | }; 172 | 173 | this.rawServiceEventListeners.add(listener); 174 | 175 | return; 176 | } 177 | 178 | this.roomStateMap.delete(joinToken); 179 | 180 | this.send({ 181 | e: 'PIPE_ROOM_UNSUBSCRIBE', 182 | d: { 183 | id: stream.room.id, 184 | }, 185 | c: null, 186 | }); 187 | } 188 | 189 | addMessageSubscription( 190 | channel: API.Channels.Channel['id'], 191 | eventName: string, 192 | listener: (data: T) => unknown, 193 | ): Unsubscribe { 194 | const map = this.getChannelMessageListeners(); 195 | 196 | const key = util.channels.getMessageListenerKey(channel, eventName); 197 | 198 | const listeners = map.get(key) ?? new Set(); 199 | const castListener = listener as (data: unknown) => unknown; 200 | 201 | map.set(key, listeners.add(castListener)); 202 | 203 | return () => { 204 | const currentListeners = map.get(key); 205 | 206 | if (!currentListeners) { 207 | return; 208 | } 209 | 210 | currentListeners.delete(castListener); 211 | 212 | if (currentListeners.size === 0) { 213 | map.delete(key); 214 | } 215 | }; 216 | } 217 | 218 | /** 219 | * Get a list of pending subscriptions 220 | * @returns A list of all channel and room names we are currently subscribed to 221 | */ 222 | getCurrentPendingSubscriptions() { 223 | const filter = ( 224 | value: 225 | | util.maps.ObservableMap> 226 | | util.maps.ObservableMap, 227 | ) => 228 | [...value.entries()] 229 | .filter(([, entry]) => entry.subscription === 'pending') 230 | .map(entry => entry[0]); 231 | 232 | return { 233 | channels: filter(this.getChannelStateMap()), 234 | rooms: filter(this.getRoomStateMap()), 235 | }; 236 | } 237 | 238 | getChannelStateMap() { 239 | return this.channelStateMap; 240 | } 241 | 242 | getRoomStateMap() { 243 | return this.roomStateMap; 244 | } 245 | 246 | getChannelMessageListeners() { 247 | return this.channelMessageListeners; 248 | } 249 | 250 | getDirectMessageListeners() { 251 | return this.directMessageListeners; 252 | } 253 | 254 | getConnectionState(fullAtom?: false): LeapConnectionState; 255 | getConnectionState(fullAtom: true): util.atoms.Atom; 256 | getConnectionState(fullAtom = false) { 257 | if (fullAtom) { 258 | return this.connectionState; 259 | } 260 | 261 | return this.connectionState.get(); 262 | } 263 | 264 | subscribeToChannel(channel: API.Channels.Channel['id']) { 265 | const c = this.channelStateMap.get(channel); 266 | 267 | if (c && c.subscription === 'available') { 268 | return; 269 | } 270 | 271 | const state: ChannelStateData = { 272 | subscription: 'pending', 273 | state: c?.state ?? null, 274 | error: null, 275 | }; 276 | 277 | this.channelStateMap.set(channel, state); 278 | 279 | this.send({ 280 | e: 'SUBSCRIBE', 281 | d: null, 282 | c: channel, 283 | }); 284 | } 285 | 286 | unsubscribeFromChannel(channel: API.Channels.Channel['id']) { 287 | this.channelStateMap.delete(channel); 288 | 289 | this.send({ 290 | e: 'UNSUBSCRIBE', 291 | d: null, 292 | c: channel, 293 | }); 294 | } 295 | 296 | setChannelState( 297 | channel: API.Channels.Channel['id'], 298 | state: API.Channels.State, 299 | ) { 300 | this.send({ 301 | e: 'SET_CHANNEL_STATE', 302 | c: channel, 303 | d: state, 304 | }); 305 | } 306 | 307 | sendMessage( 308 | channel: API.Channels.Channel['id'], 309 | event: string, 310 | payload: unknown, 311 | ) { 312 | this.send({ 313 | e: 'MESSAGE', 314 | c: channel, 315 | d: { 316 | e: event, 317 | d: payload, 318 | }, 319 | }); 320 | } 321 | 322 | private async handleConnectionStateUpdate(state: LeapConnectionState) { 323 | if (state === LeapConnectionState.ERRORED && this.hasPreviouslyConnected) { 324 | for (const ch of this.channelStateMap.keys()) { 325 | this.channelStateMap.patch(ch, {subscription: 'pending'}); 326 | } 327 | 328 | const unsubscribe = this.connectionState.addListener(state => { 329 | if (state !== LeapConnectionState.CONNECTED) { 330 | return; 331 | } 332 | 333 | // If we have a leap token, subscriptions are persisted 334 | // on the server. There's no need to resubscribe. 335 | if (!this.getLeap().auth.token) { 336 | this.resubscribe(); 337 | } 338 | 339 | unsubscribe(); 340 | }); 341 | } 342 | 343 | if (!this.hasPreviouslyConnected) { 344 | this.hasPreviouslyConnected = true; 345 | } 346 | 347 | this.connectionState.set(state); 348 | } 349 | 350 | private resubscribe() { 351 | const {channels, rooms} = this.getCurrentPendingSubscriptions(); 352 | 353 | for (const channel of channels) { 354 | this.subscribeToChannel(channel); 355 | } 356 | 357 | for (const room of rooms) { 358 | this.subscribeToRoom(room); 359 | } 360 | } 361 | 362 | private async handleServiceEvent(event: LeapServiceEvent) { 363 | const handler = Client.SUPPORTED_EVENTS[event.eventType] as 364 | | LeapHandler 365 | | undefined; 366 | 367 | if (!handler) { 368 | console.warn( 369 | '[@onehop/client] Channels: Received unsupported opcode!', 370 | event, 371 | ); 372 | 373 | return; 374 | } 375 | 376 | try { 377 | await handler.handle(this, event); 378 | } catch (error: unknown) { 379 | console.warn('[@onehop/client] Handling service message failed'); 380 | console.warn(error); 381 | } 382 | } 383 | 384 | private send(data: EncapsulatingServicePayload) { 385 | this.getLeap().sendServicePayload(data); 386 | } 387 | 388 | private getLeap( 389 | auth?: LeapEdgeAuthenticationParameters, 390 | opts?: LeapEdgeInitOptions, 391 | ) { 392 | if (this.leap) { 393 | if (auth) { 394 | this.leap.auth = auth; 395 | } 396 | 397 | return this.leap; 398 | } 399 | 400 | if (!auth) { 401 | throw new Error( 402 | 'Cannot create a new Leap instance as no authentication params were provided', 403 | ); 404 | } 405 | 406 | this.leap = new LeapEdgeClient(auth, opts); 407 | void this.leap.connect(); 408 | 409 | return this.leap; 410 | } 411 | } 412 | 413 | export const instance = new Client(); 414 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/AVAILABLE.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | import {createLeapEvent} from './create'; 3 | 4 | export const AVAILABLE = createLeapEvent({ 5 | async handle( 6 | client, 7 | channelId, 8 | data: { 9 | channel: API.Channels.Channel; 10 | }, 11 | ) { 12 | client.getChannelStateMap().set(channelId, { 13 | error: null, 14 | state: data.channel.state, 15 | subscription: 'available', 16 | }); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/DIRECT_MESSAGE.ts: -------------------------------------------------------------------------------- 1 | import {createLeapEvent} from './create'; 2 | 3 | export const DIRECT_MESSAGE = createLeapEvent({ 4 | requireId: false, 5 | 6 | async handle(client, channel, data: {e: string; d: unknown}) { 7 | const {e: event, d: messageData} = data; 8 | 9 | client.emit('MESSAGE', { 10 | event, 11 | data: messageData, 12 | channel: null, 13 | }); 14 | 15 | const listeners = client.getDirectMessageListeners().get(event); 16 | 17 | if (!listeners) { 18 | return; 19 | } 20 | 21 | if (listeners.size === 0) { 22 | client.getDirectMessageListeners().delete(event); 23 | return; 24 | } 25 | 26 | for (const listener of listeners) { 27 | listener(messageData); 28 | } 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/INIT.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | import type {ChannelStateData} from '..'; 3 | import {createLeapEvent} from './create'; 4 | 5 | export type Data = { 6 | channels: API.Channels.Channel[]; 7 | metadata: API.Channels.State; 8 | cid: string; 9 | connection_count: number; 10 | scope: 'project' | 'token'; 11 | }; 12 | 13 | export const INIT = createLeapEvent({ 14 | requireId: false, 15 | 16 | async handle(client, channelId, data: Data) { 17 | const localState = new Map< 18 | API.Channels.Channel['id'], 19 | ChannelStateData 20 | >(); 21 | 22 | for (const channel of data.channels) { 23 | localState.set(channel.id, { 24 | state: channel.state, 25 | subscription: 'available', 26 | error: null, 27 | }); 28 | } 29 | 30 | client.getChannelStateMap().merge(localState); 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/MESSAGE.ts: -------------------------------------------------------------------------------- 1 | import {getMessageListenerKey} from '../../util/channels'; 2 | import {createLeapEvent} from './create'; 3 | 4 | export const MESSAGE = createLeapEvent({ 5 | async handle( 6 | client, 7 | channel, 8 | data: { 9 | e: string; 10 | d: unknown; 11 | }, 12 | ) { 13 | const {e: event, d: messageData} = data; 14 | 15 | client.emit('MESSAGE', { 16 | event, 17 | data: messageData, 18 | channel, 19 | }); 20 | 21 | const key = getMessageListenerKey(channel, event); 22 | 23 | const listeners = client.getChannelMessageListeners().get(key); 24 | 25 | if (!listeners) { 26 | return; 27 | } 28 | 29 | if (listeners.size === 0) { 30 | client.getChannelMessageListeners().delete(key); 31 | return; 32 | } 33 | 34 | for (const listener of listeners) { 35 | listener(messageData); 36 | } 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/PIPE_ROOM_AVAILABLE.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | import {createLeapEvent} from './create'; 3 | 4 | export type Connection = { 5 | edge_endpoint: string; 6 | }; 7 | 8 | export type Payload = { 9 | pipe_room: API.Pipe.Room; 10 | connection: {llhls?: Connection; webrtc?: Connection}; 11 | }; 12 | 13 | export const PIPE_ROOM_AVAILABLE = createLeapEvent({ 14 | requireId: false, 15 | 16 | async handle(client, _, data: Payload) { 17 | client.getRoomStateMap().set(data.pipe_room.join_token, { 18 | subscription: 'available', 19 | room: data.pipe_room, 20 | connection: data.connection, 21 | }); 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/PIPE_ROOM_UNAVAILABLE.ts: -------------------------------------------------------------------------------- 1 | import type {UnavailableError} from '../types'; 2 | import {createLeapEvent} from './create'; 3 | 4 | export const PIPE_ROOM_UNAVAILABLE = createLeapEvent({ 5 | requireId: false, 6 | 7 | async handle( 8 | client, 9 | _, 10 | data: UnavailableError & { 11 | join_token: string; 12 | }, 13 | ) { 14 | client.getRoomStateMap().set(data.join_token, { 15 | subscription: 'unavailable', 16 | error: data, 17 | room: null, 18 | }); 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/PIPE_ROOM_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import {PIPE_ROOM_AVAILABLE} from './PIPE_ROOM_AVAILABLE'; 2 | 3 | // Implementation for these two are the same 4 | export const PIPE_ROOM_UPDATE = PIPE_ROOM_AVAILABLE; 5 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/STATE_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | import {createLeapEvent} from './create'; 3 | 4 | export const STATE_UPDATE = createLeapEvent({ 5 | async handle(client, channel, data: {state: API.Channels.State}) { 6 | client.emit('STATE_UPDATE', { 7 | channel, 8 | state: data.state, 9 | }); 10 | 11 | client.getChannelStateMap().patch(channel, { 12 | state: data.state, 13 | }); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/TOKEN_STATE_UPDATE.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | import {createLeapEvent} from './create'; 3 | 4 | export const TOKEN_STATE_UPDATE = createLeapEvent({ 5 | async handle( 6 | client, 7 | channelId, 8 | data: { 9 | state: API.Channels.State; 10 | }, 11 | ) { 12 | client.getChannelStateMap().patch(channelId, { 13 | state: data.state, 14 | }); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/UNAVAILABLE.ts: -------------------------------------------------------------------------------- 1 | import type {LeapChannelSubscriptionError} from '../index'; 2 | import {createLeapEvent} from './create'; 3 | 4 | export const UNAVAILABLE = createLeapEvent({ 5 | async handle( 6 | client, 7 | channel, 8 | data: {graceful: boolean; error_code?: LeapChannelSubscriptionError}, 9 | ) { 10 | client.getChannelStateMap().set(channel, { 11 | state: null, 12 | subscription: 'unavailable', 13 | error: data, 14 | }); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/client/src/leap/handlers/create.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | import type {LeapServiceEvent} from '@onehop/leap-edge-js'; 3 | import type {Client} from '..'; 4 | 5 | export type LeapHandler = { 6 | handle(client: Client, event: LeapServiceEvent): Promise; 7 | }; 8 | 9 | export function createLeapEvent(config: { 10 | requireId?: G; 11 | 12 | handle: ( 13 | client: Client, 14 | channelId: G extends true 15 | ? API.Channels.Channel['id'] 16 | : API.Channels.Channel['id'] | null, 17 | data: D, 18 | ) => Promise; 19 | }): LeapHandler { 20 | return { 21 | async handle(client: Client, event: LeapServiceEvent) { 22 | const requireId = config.requireId !== false; 23 | 24 | if (!event.channelId && requireId) { 25 | throw new Error( 26 | `Received opcode for ${event.eventType} but expected an ID that was not there.`, 27 | ); 28 | } 29 | 30 | await config.handle(client, event.channelId!, event.data as any); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/client/src/leap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /packages/client/src/leap/types.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | import type {Payload as PIPE_ROOM_AVAILABLE_PAYLOAD} from './handlers/PIPE_ROOM_AVAILABLE'; 3 | 4 | export type ClientInitOptions = { 5 | leapSocketUrl?: string; 6 | }; 7 | 8 | export type LeapChannelSubscriptionError = 'NOT_GRANTED' | 'UNKNOWN'; 9 | 10 | export type GenericSubscriptionState = 11 | | 'available' 12 | | 'pending' 13 | | 'unavailable' 14 | | 'non_existent'; 15 | 16 | export type ChannelStateData = { 17 | state: T | null; 18 | subscription: GenericSubscriptionState; 19 | error: UnavailableError | null; 20 | }; 21 | 22 | export type RoomStateData = 23 | | { 24 | subscription: 'pending'; 25 | room: null; 26 | } 27 | | { 28 | subscription: 'unavailable'; 29 | room: null; 30 | error: UnavailableError; 31 | } 32 | | { 33 | subscription: 'available'; 34 | room: API.Pipe.Room; 35 | connection: PIPE_ROOM_AVAILABLE_PAYLOAD['connection']; 36 | }; 37 | 38 | export type UnavailableError = { 39 | graceful: boolean; 40 | error_code?: LeapChannelSubscriptionError; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/client/src/pipe/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mount'; 2 | -------------------------------------------------------------------------------- /packages/client/src/pipe/mount.ts: -------------------------------------------------------------------------------- 1 | import type {HlsConfig} from 'hls.js'; 2 | import Hls from 'hls.js'; 3 | 4 | export const APPLE_HLS_MIME = 'application/vnd.apple.mpegurl'; 5 | export const LIVE_LLHLS_SYNC_BCE = 3; 6 | export const WCL_DELAY_LES = 5; 7 | 8 | const defaultConfig: Partial = { 9 | lowLatencyMode: true, 10 | backBufferLength: 10, 11 | autoStartLoad: true, 12 | enableWorker: true, 13 | abrBandWidthFactor: 1, 14 | liveSyncDuration: LIVE_LLHLS_SYNC_BCE, 15 | }; 16 | 17 | export class Controls { 18 | private readonly node; 19 | private readonly _hls; 20 | 21 | public constructor(node: HTMLVideoElement, hls?: Hls) { 22 | this.node = node; 23 | this._hls = hls; 24 | } 25 | 26 | /** 27 | * Syncs to live edge 28 | * 29 | * @param distance The seconds to sync from live edge (e.g. a buffer) 30 | */ 31 | sync(distance = LIVE_LLHLS_SYNC_BCE) { 32 | this.node.currentTime = this.node.duration - distance; 33 | } 34 | 35 | async stop() { 36 | this.node.pause(); 37 | } 38 | 39 | async play() { 40 | await this.node.play(); 41 | this.sync(3.5); 42 | } 43 | 44 | get isPaused() { 45 | return this.node.paused; 46 | } 47 | 48 | destroy() { 49 | this.hls.destroy(); 50 | } 51 | 52 | get isNative() { 53 | return !this._hls; 54 | } 55 | 56 | get hls() { 57 | if (!this._hls) { 58 | throw new Error( 59 | 'Cannot get HLS instance as video is being streamed natively.', 60 | ); 61 | } 62 | 63 | return this._hls; 64 | } 65 | } 66 | 67 | export function mount( 68 | node: HTMLVideoElement, 69 | url: string, 70 | hlsConfigOverride?: Partial, 71 | ): Controls { 72 | let instance: Hls; 73 | 74 | if (Hls.isSupported()) { 75 | instance = new Hls({ 76 | ...defaultConfig, 77 | ...hlsConfigOverride, 78 | }); 79 | } else if (node.canPlayType(APPLE_HLS_MIME)) { 80 | node.src = url; 81 | return new Controls(node); 82 | } else { 83 | throw new Error('HLS Will not work in this browser', { 84 | cause: new Error( 85 | 'This browser does not support HLS or MSE: https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API', 86 | ), 87 | }); 88 | } 89 | 90 | const syncToBCE = () => { 91 | if (node.duration < LIVE_LLHLS_SYNC_BCE) { 92 | return; 93 | } 94 | 95 | node.currentTime = node.duration - LIVE_LLHLS_SYNC_BCE; 96 | }; 97 | 98 | node.onplay = () => { 99 | syncToBCE(); 100 | }; 101 | 102 | const liveSync = setInterval(() => { 103 | if ( 104 | node && 105 | !node.paused && 106 | instance.latency > LIVE_LLHLS_SYNC_BCE + WCL_DELAY_LES 107 | ) { 108 | syncToBCE(); 109 | } 110 | }, 1000); 111 | 112 | instance.on(Hls.Events.MEDIA_DETACHING, () => { 113 | clearInterval(liveSync); 114 | }); 115 | 116 | instance.loadSource(url); 117 | instance.attachMedia(node); 118 | 119 | return new Controls(node, instance); 120 | } 121 | -------------------------------------------------------------------------------- /packages/client/src/util/atoms.ts: -------------------------------------------------------------------------------- 1 | import type {Unsubscribe} from '../util/types'; 2 | 3 | export type Listener = (value: T) => void; 4 | 5 | export type AtomValue = 6 | | {uninitialized?: never; value: T} 7 | | {uninitialized: true; value: undefined}; 8 | 9 | export type Atom = { 10 | get(): T; 11 | set(value: T): void; 12 | addListener(listener: Listener): Unsubscribe; 13 | removeListener(listener: Listener): void; 14 | }; 15 | 16 | export type Infer = T extends Atom ? V : never; 17 | 18 | /** 19 | * An atom, inspired much by Jotai, is a single bit of readible 20 | * state that can be observed and written to. It's useful for 21 | * React as we can easily update state when the atom changes 22 | * and use it as a shared global state store. 23 | * 24 | * @param initialValue An initial value to assign to the atom 25 | * @returns A readible and observable state object 26 | */ 27 | function atom(initialValue?: T): Atom { 28 | let atomValue: AtomValue = 29 | initialValue === undefined 30 | ? {uninitialized: true, value: undefined} 31 | : {value: initialValue}; 32 | 33 | const listeners = new Set>(); 34 | 35 | const notify = () => { 36 | if ('uninitialized' in atomValue) { 37 | // In theory this would never happen 38 | // because the value would have 39 | // already been set and therefore 40 | // not unintialized 41 | return; 42 | } 43 | 44 | for (const listener of listeners) { 45 | listener(atomValue.value); 46 | } 47 | }; 48 | 49 | return { 50 | get() { 51 | if ('uninitialized' in atomValue) { 52 | throw new Error( 53 | 'Cannot read the value of an atom that has no value yet.', 54 | ); 55 | } 56 | 57 | return atomValue.value; 58 | }, 59 | 60 | set(value: T) { 61 | // Be efficient and don't update 62 | if (Object.is(atomValue.value, value)) { 63 | return; 64 | } 65 | 66 | atomValue = {value}; 67 | notify(); 68 | }, 69 | 70 | addListener(listener: Listener) { 71 | listeners.add(listener); 72 | 73 | return () => { 74 | listeners.delete(listener); 75 | }; 76 | }, 77 | 78 | removeListener(listener: Listener) { 79 | listeners.delete(listener); 80 | }, 81 | }; 82 | } 83 | 84 | export {atom as create}; 85 | -------------------------------------------------------------------------------- /packages/client/src/util/channels.ts: -------------------------------------------------------------------------------- 1 | import type {API} from '@onehop/js'; 2 | 3 | export const getMessageListenerKey = ( 4 | channel: API.Channels.Channel['id'], 5 | event: string, 6 | ) => `${channel}:${event}` as const; 7 | 8 | export type ChannelMessageListenerKey = ReturnType< 9 | typeof getMessageListenerKey 10 | >; 11 | -------------------------------------------------------------------------------- /packages/client/src/util/emitter.ts: -------------------------------------------------------------------------------- 1 | export type HopEmitterListener< 2 | P extends Record, 3 | Key extends keyof P, 4 | > = (event: P[Key]) => unknown; 5 | 6 | export type Unsubscribe = () => void; 7 | 8 | export class HopEmitter> { 9 | private readonly listeners; 10 | 11 | protected constructor() { 12 | this.listeners = new Map< 13 | keyof Payloads, 14 | Set> 15 | >(); 16 | } 17 | 18 | public createListener( 19 | _key: K, 20 | fn: HopEmitterListener, 21 | ) { 22 | return fn; 23 | } 24 | 25 | /** 26 | * Subscribe and listen to an event 27 | * @param key The event name to listen for 28 | * @param listener A listener for this event 29 | * @returns A function that can be called to unsubscribe the listener 30 | * @example 31 | * ``` 32 | * const unsubscribe = client.on('MESSAGE', console.log); 33 | * // Unsubscribe later... 34 | * unsubscribe(); 35 | * ``` 36 | */ 37 | public on( 38 | key: K, 39 | listener: HopEmitterListener, 40 | ): Unsubscribe { 41 | const existing = this.listeners.get(key) ?? []; 42 | 43 | const merged = new Set([...existing, listener]) as Set< 44 | HopEmitterListener 45 | >; 46 | 47 | this.listeners.set(key, merged); 48 | 49 | return () => { 50 | const set = this.listeners.get(key); 51 | 52 | if (!set) { 53 | return; 54 | } 55 | 56 | if (set.size === 0) { 57 | this.listeners.delete(key); 58 | } 59 | 60 | set.delete(listener as HopEmitterListener); 61 | }; 62 | } 63 | 64 | /** 65 | * Subscribe and listen to an event once only 66 | * @param key The event name to listen for 67 | * @param listener A listener for this event 68 | * @returns A function that can be called to unsubscribe the listener before it even runs 69 | */ 70 | public once( 71 | key: K, 72 | listener: HopEmitterListener, 73 | ): Unsubscribe { 74 | const unsubscribe = this.on(key, data => { 75 | unsubscribe(); 76 | 77 | return listener(data); 78 | }); 79 | 80 | return unsubscribe; 81 | } 82 | 83 | /** 84 | * Remove a listener from an event 85 | * @param key The event name to remove a listener from 86 | * @param listener The listener to remove 87 | */ 88 | public off( 89 | key: K, 90 | listener: HopEmitterListener, 91 | ) { 92 | const set = this.listeners.get(key); 93 | 94 | if (!set) { 95 | throw new Error("Cannot remove listener for key that doesn't exist."); 96 | } 97 | 98 | if (set.size === 0) { 99 | this.listeners.delete(key); 100 | return; 101 | } 102 | 103 | set.delete(listener as HopEmitterListener); 104 | } 105 | 106 | /** 107 | * Emit an event to all listeners 108 | * @param key The event name to emit 109 | * @param data The data to emit 110 | */ 111 | emit(key: K, data: Payloads[K]) { 112 | const listeners = this.listeners.get(key); 113 | 114 | if (!listeners) { 115 | return; 116 | } 117 | 118 | // In theory, shouldn't happen because we should have already checked 119 | // in the .off call 120 | if (listeners.size === 0) { 121 | this.listeners.delete(key); 122 | return; 123 | } 124 | 125 | for (const listener of listeners) { 126 | listener(data); 127 | } 128 | } 129 | } 130 | 131 | class HopEmitterInitialiser extends HopEmitter { 132 | public static create = >() => 133 | new HopEmitter(); 134 | } 135 | 136 | export const {create} = HopEmitterInitialiser; 137 | -------------------------------------------------------------------------------- /packages/client/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * as atoms from './atoms'; 2 | export * as channels from './channels'; 3 | export * as emitter from './emitter'; 4 | export * as maps from './maps'; 5 | export * as queues from './queues'; 6 | -------------------------------------------------------------------------------- /packages/client/src/util/maps.ts: -------------------------------------------------------------------------------- 1 | import type {Unsubscribe} from './types'; 2 | 3 | export type ListenerPayload = 4 | | {type: 'clear' | 'merge'} 5 | | {type: 'set'; key: K; value: V} 6 | | {type: 'delete'; key: K}; 7 | 8 | export type Listener = ( 9 | instance: ObservableMap, 10 | payload: ListenerPayload, 11 | ) => void; 12 | 13 | export class ObservableMap implements Map { 14 | private map = new Map(); 15 | private readonly listeners = new Set>(); 16 | 17 | get size(): number { 18 | return this.map.size; 19 | } 20 | 21 | get [Symbol.toStringTag](): string { 22 | return 'ObservableMap'; 23 | } 24 | 25 | clear(): void { 26 | this.map.clear(); 27 | this.notify({type: 'clear'}); 28 | } 29 | 30 | delete(key: K): boolean { 31 | const success = this.map.delete(key); 32 | this.notify({type: 'delete', key}); 33 | 34 | return success; 35 | } 36 | 37 | forEach( 38 | callbackfn: (value: V, key: K, map: Map) => void, 39 | thisArg?: any, 40 | ): void { 41 | this.map.forEach(callbackfn, thisArg); 42 | } 43 | 44 | get(key: K): V | undefined { 45 | return this.map.get(key); 46 | } 47 | 48 | has(key: K): boolean { 49 | return this.map.has(key); 50 | } 51 | 52 | set(key: K, value: V): this { 53 | this.map.set(key, value); 54 | this.notify({type: 'set', key, value}); 55 | 56 | return this; 57 | } 58 | 59 | patch(key: K, value: Partial): this { 60 | const old = this.map.get(key); 61 | 62 | if (old === undefined) { 63 | throw new Error( 64 | 'Cannot patch a value that does not already exist. Use `.set` instead.', 65 | ); 66 | } 67 | 68 | return this.set(key, { 69 | ...old, 70 | ...value, 71 | }); 72 | } 73 | 74 | /** 75 | * Merge with another map, with the new map overwriting members with the same key 76 | * @param map A map that has a matching set of keys and values 77 | */ 78 | merge(map: Map) { 79 | this.map = new Map([...this.map, ...map]); 80 | this.notify({type: 'merge'}); 81 | } 82 | 83 | entries(): IterableIterator<[K, V]> { 84 | return this.map.entries(); 85 | } 86 | 87 | keys(): IterableIterator { 88 | return this.map.keys(); 89 | } 90 | 91 | values(): IterableIterator { 92 | return this.map.values(); 93 | } 94 | 95 | [Symbol.iterator](): IterableIterator<[K, V]> { 96 | return this.map[Symbol.iterator](); 97 | } 98 | 99 | addListener(listener: Listener): Unsubscribe { 100 | this.listeners.add(listener); 101 | 102 | return () => { 103 | this.listeners.delete(listener); 104 | }; 105 | } 106 | 107 | removeListener(listener: Listener) { 108 | this.listeners.delete(listener); 109 | } 110 | 111 | private notify(payload: ListenerPayload) { 112 | for (const listener of this.listeners) { 113 | listener(this, payload); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/client/src/util/queues.ts: -------------------------------------------------------------------------------- 1 | export type Queue = { 2 | enqueue(item: T): void; 3 | dequeue(): T | undefined; 4 | peek(): T | undefined; 5 | get length(): number; 6 | isEmpty(): boolean; 7 | }; 8 | 9 | /** 10 | * A queue that can be used to store items in a first-in-first-out order. 11 | */ 12 | export class FIFOQueue implements Queue { 13 | private readonly items: T[] = []; 14 | 15 | public enqueue(item: T) { 16 | this.items.push(item); 17 | } 18 | 19 | public dequeue(): T | undefined { 20 | return this.items.shift(); 21 | } 22 | 23 | public peek(): T | undefined { 24 | return this.items[0]; 25 | } 26 | 27 | public get length() { 28 | return this.items.length; 29 | } 30 | 31 | public isEmpty() { 32 | return this.items.length === 0; 33 | } 34 | } 35 | 36 | /** 37 | * A queue that can be used to store items in a last-in-first-out order. 38 | */ 39 | export class LIFOQueue implements Queue { 40 | private readonly items: T[] = []; 41 | 42 | public enqueue(item: T) { 43 | this.items.push(item); 44 | } 45 | 46 | public dequeue(): T | undefined { 47 | return this.items.pop(); 48 | } 49 | 50 | public peek(): T | undefined { 51 | return this.items[this.items.length - 1]; 52 | } 53 | 54 | public get length() { 55 | return this.items.length; 56 | } 57 | 58 | public isEmpty() { 59 | return this.items.length === 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/client/src/util/types.ts: -------------------------------------------------------------------------------- 1 | export type Unsubscribe = () => void; 2 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # @onehop/react 2 | 3 | Hop's Client SDK for React 4 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@onehop/react", 3 | "main": "dist/onehop-react.cjs.js", 4 | "module": "dist/onehop-react.esm.js", 5 | "typings": "dist/onehop-react.cjs.d.ts", 6 | "packageManager": "yarn@3.5.0", 7 | "description": "React client library for hop.io", 8 | "sideEffects": false, 9 | "scripts": { 10 | "release": "yarn npm publish --tolerate-republish" 11 | }, 12 | "files": [ 13 | "README.md", 14 | "dist", 15 | "package.json" 16 | ], 17 | "peerDependencies": { 18 | "react": "*" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.27", 22 | "react": "^18.2.0" 23 | }, 24 | "dependencies": { 25 | "@onehop/client": "workspace:^", 26 | "@onehop/js": "^1.18.0", 27 | "@onehop/leap-edge-js": "^1.0.11" 28 | }, 29 | "author": "Hop Development Team", 30 | "homepage": "https://github.com/hopinc/hop-client-js/tree/master/packages/react", 31 | "keywords": [ 32 | "realtime", 33 | "channels", 34 | "pipe", 35 | "client", 36 | "react" 37 | ], 38 | "license": "MIT", 39 | "repository": "https://github.com/hopinc/hop-client-js.git", 40 | "version": "1.6.5", 41 | "publishConfig": { 42 | "access": "public" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/react/src/hooks/atoms.ts: -------------------------------------------------------------------------------- 1 | import type {util} from '@onehop/client'; 2 | import {useSyncExternalStore} from 'react'; 3 | 4 | export function useAtom(atom: util.atoms.Atom) { 5 | const get = () => atom.get(); 6 | return useSyncExternalStore(atom.addListener, get, get); 7 | } 8 | -------------------------------------------------------------------------------- /packages/react/src/hooks/channels.ts: -------------------------------------------------------------------------------- 1 | import type {leap} from '@onehop/client'; 2 | import {type API, IS_BROWSER} from '@onehop/js'; 3 | import {useEffect} from 'react'; 4 | import {ConnectionState} from '..'; 5 | import {useConnectionState, useLeap} from './leap'; 6 | import {useObservableMapGet} from './maps'; 7 | 8 | export function useChannelMessage( 9 | channel: API.Channels.Channel['id'], 10 | event: string, 11 | listener: (data: T) => unknown, 12 | ) { 13 | const client = useLeap(); 14 | const connectionState = useConnectionState(); 15 | 16 | const data = useObservableMapGet(client.getChannelStateMap(), channel); 17 | 18 | useEffect(() => { 19 | if ( 20 | IS_BROWSER && 21 | connectionState === ConnectionState.CONNECTED && 22 | (!data || data.subscription === 'non_existent') 23 | ) { 24 | client.subscribeToChannel(channel); 25 | } 26 | }, [connectionState, data?.state]); 27 | 28 | useEffect(() => { 29 | const unsubscribe = client.addMessageSubscription(channel, event, listener); 30 | 31 | return () => { 32 | unsubscribe(); 33 | }; 34 | }, [channel, event, listener]); 35 | } 36 | 37 | export function useDirectMessage( 38 | event: string, 39 | listener: (data: T) => unknown, 40 | ) { 41 | const client = useLeap(); 42 | 43 | const castListener = listener as (data: unknown) => unknown; 44 | 45 | useEffect(() => { 46 | const listeners = client.getDirectMessageListeners().get(event); 47 | 48 | if (listeners) { 49 | listeners.add(castListener); 50 | } else { 51 | client.getDirectMessageListeners().set(event, new Set([castListener])); 52 | } 53 | 54 | return () => { 55 | const listeners = client.getDirectMessageListeners().get(event); 56 | 57 | if (!listeners) { 58 | return; 59 | } 60 | 61 | if (listeners.size === 0) { 62 | client.getDirectMessageListeners().delete(event); 63 | return; 64 | } 65 | 66 | listeners.delete(castListener); 67 | }; 68 | }, [event]); 69 | } 70 | 71 | export function useReadChannelState< 72 | T extends API.Channels.State = API.Channels.State, 73 | >(channel: API.Channels.Channel['id']): leap.ChannelStateData { 74 | const client = useLeap(); 75 | const connectionState = useConnectionState(); 76 | 77 | const data = useObservableMapGet(client.getChannelStateMap(), channel) as 78 | | leap.ChannelStateData 79 | | undefined; 80 | 81 | useEffect(() => { 82 | if ( 83 | IS_BROWSER && 84 | connectionState === ConnectionState.CONNECTED && 85 | (!data || data.subscription === 'non_existent') 86 | ) { 87 | client.subscribeToChannel(channel); 88 | } 89 | }, [connectionState, data?.subscription, data?.state, channel]); 90 | 91 | if (!data) { 92 | const nonExistentData: leap.ChannelStateData = { 93 | state: null, 94 | error: null, 95 | subscription: 'non_existent', 96 | }; 97 | 98 | client.getChannelStateMap().set(channel, nonExistentData); 99 | 100 | return nonExistentData; 101 | } 102 | 103 | return data; 104 | } 105 | -------------------------------------------------------------------------------- /packages/react/src/hooks/leap.ts: -------------------------------------------------------------------------------- 1 | import {hop, leap} from '@onehop/client'; 2 | import type {LeapEdgeAuthenticationParameters} from '@onehop/leap-edge-js'; 3 | import {createContext, useContext, useEffect} from 'react'; 4 | import {useAtom} from './atoms'; 5 | 6 | const leapContext = createContext(leap.instance); 7 | 8 | export function useLeap(): leap.Client { 9 | return useContext(leapContext); 10 | } 11 | 12 | export function useConnect() { 13 | const leap = useLeap(); 14 | 15 | return (auth: LeapEdgeAuthenticationParameters) => { 16 | leap.connect(auth); 17 | }; 18 | } 19 | 20 | export function useConnectionState() { 21 | const client = useLeap(); 22 | 23 | return useAtom(client.getConnectionState(true)); 24 | } 25 | 26 | /** 27 | * Initialises Hop's client. This hook should only be rendered 28 | * at the top level of your app (e.g. so the component only mounts once). 29 | * 30 | * Extremely useful for things like Server Side Rendering, as it won't allow 31 | * hop to connect to leap until we're in the browser. 32 | * 33 | * If you want to initialise yourself feel free to copy the hook 34 | * into your own code. Or, if you're completely client side rendered 35 | * and don't have a React server rendering step, then you can call 36 | * hop.init outside of React lifecycle and all @onehop/react hooks will 37 | * still work exactly as expected. 38 | * 39 | * @param params Authentication parameters for leap 40 | */ 41 | export function useInit(params?: LeapEdgeAuthenticationParameters) { 42 | useEffect(() => { 43 | if (typeof window === 'undefined') { 44 | return; 45 | } 46 | 47 | if (!params) { 48 | throw new Error( 49 | 'Leap authentication params are required when using useInit in the browser', 50 | ); 51 | } 52 | 53 | hop.init(params); 54 | }, []); 55 | } 56 | -------------------------------------------------------------------------------- /packages/react/src/hooks/maps.ts: -------------------------------------------------------------------------------- 1 | import type {util} from '@onehop/client'; 2 | import {useEffect, useState} from 'react'; 3 | 4 | export function useObservableMap( 5 | map: util.maps.ObservableMap, 6 | listenOnlyFor?: Array['type']>, 7 | ) { 8 | const [storeState, setStoreState] = useState({map}); 9 | 10 | useEffect(() => { 11 | const unsubscribe = map.addListener((instance, payload) => { 12 | if (listenOnlyFor && !listenOnlyFor.includes(payload.type)) { 13 | return; 14 | } 15 | 16 | setStoreState({map}); 17 | }); 18 | 19 | return () => { 20 | unsubscribe(); 21 | }; 22 | }, [map]); 23 | 24 | return storeState.map; 25 | } 26 | 27 | export function useObservableMapGet( 28 | map: util.maps.ObservableMap, 29 | key: K | undefined, 30 | ) { 31 | const [storeState, setStoreState] = useState(() => 32 | key ? map.get(key) : undefined, 33 | ); 34 | 35 | useEffect(() => { 36 | if (!key) { 37 | return; 38 | } 39 | 40 | const unsubscribe = map.addListener((instance, payload) => { 41 | if ( 42 | ('key' in payload && payload.key === key) || 43 | (key && payload.type === 'merge') 44 | ) { 45 | setStoreState(map.get(key)); 46 | } else if (payload.type === 'clear') { 47 | setStoreState(undefined); 48 | } 49 | }); 50 | 51 | return () => { 52 | unsubscribe(); 53 | }; 54 | }, [key, map]); 55 | 56 | return storeState; 57 | } 58 | 59 | export function useObserveObservableMap( 60 | map: util.maps.ObservableMap, 61 | listener: (map: util.maps.ObservableMap) => unknown, 62 | ) { 63 | useEffect(() => { 64 | const unsubscribe = map.addListener(listener); 65 | 66 | return () => { 67 | unsubscribe(); 68 | }; 69 | }, [map]); 70 | } 71 | -------------------------------------------------------------------------------- /packages/react/src/hooks/pipe.ts: -------------------------------------------------------------------------------- 1 | import {hls, pipe, util} from '@onehop/client'; 2 | import type {API} from '@onehop/js'; 3 | import {LeapConnectionState} from '@onehop/leap-edge-js'; 4 | import type {RefObject} from 'react'; 5 | import { 6 | createContext, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useMemo, 11 | useState, 12 | } from 'react'; 13 | import {useConnectionState, useLeap} from './leap'; 14 | import {useObservableMapGet, useObserveObservableMap} from './maps'; 15 | import {useInterval} from './timeout'; 16 | 17 | export type Config = { 18 | joinToken: string | null; 19 | ref: RefObject; 20 | autojoin?: boolean; 21 | }; 22 | 23 | export const trackedPipeComponents = createContext( 24 | new util.maps.ObservableMap< 25 | API.Pipe.Room['join_token'], 26 | util.atoms.Atom 27 | >(), 28 | ); 29 | 30 | export function useTrackPipeComponentCount(joinToken: string | null) { 31 | const map = useContext(trackedPipeComponents); 32 | const state = useObservableMapGet(map, joinToken ?? undefined); 33 | 34 | const leap = useLeap(); 35 | 36 | useEffect(() => { 37 | if (!state && joinToken) { 38 | map.set(joinToken, util.atoms.create(0)); 39 | } 40 | }, [state, joinToken]); 41 | 42 | useEffect(() => { 43 | if (!joinToken || !state) { 44 | return; 45 | } 46 | 47 | state.set(state.get() + 1); 48 | 49 | return () => { 50 | const value = state.get() - 1; 51 | 52 | if (value === 0) { 53 | leap.unsubscribeFromRoom(joinToken); 54 | map.delete(joinToken); 55 | } else { 56 | state.set(value); 57 | } 58 | }; 59 | }, [joinToken, state]); 60 | } 61 | 62 | export function usePipeRoom({ref, autojoin = true, joinToken}: Config) { 63 | const leap = useLeap(); 64 | const connectionState = useConnectionState(); 65 | const [controls, setControls] = useState(null); 66 | const [buffering, setBuffering] = useState(false); 67 | const [lastLatencyEmit] = useState(() => util.atoms.create(-1)); 68 | 69 | const events = useMemo( 70 | () => 71 | util.emitter.create<{ 72 | ROOM_UPDATE: API.Pipe.Room; 73 | BUFFERING: {buffering: boolean}; 74 | ESTIMATED_LATENCY: {latency: number}; 75 | }>(), 76 | [], 77 | ); 78 | 79 | useInterval(500, () => { 80 | if (!controls?.hls) { 81 | return; 82 | } 83 | 84 | if (controls.hls.latency === lastLatencyEmit.get()) { 85 | return; 86 | } 87 | 88 | lastLatencyEmit.set(controls.hls.latency); 89 | 90 | events.emit('ESTIMATED_LATENCY', { 91 | latency: controls.hls.latency, 92 | }); 93 | }); 94 | 95 | const roomStateMap = leap.getRoomStateMap(); 96 | 97 | useObserveObservableMap( 98 | roomStateMap, 99 | useCallback( 100 | m => { 101 | if (!joinToken) { 102 | return; 103 | } 104 | 105 | const data = m.get(joinToken); 106 | 107 | if (!data || !data.room) { 108 | return; 109 | } 110 | 111 | events.emit('ROOM_UPDATE', data.room); 112 | }, 113 | [joinToken], 114 | ), 115 | ); 116 | 117 | const stream = useObservableMapGet(roomStateMap, joinToken ?? undefined); 118 | 119 | useTrackPipeComponentCount(joinToken); 120 | 121 | useEffect(() => { 122 | if (connectionState !== LeapConnectionState.CONNECTED) { 123 | return; 124 | } 125 | 126 | if (!autojoin) { 127 | return; 128 | } 129 | 130 | if (autojoin && stream?.subscription === 'available') { 131 | return; 132 | } 133 | 134 | if (!joinToken) { 135 | return; 136 | } 137 | 138 | if (leap.getRoomStateMap().has(joinToken)) { 139 | return; 140 | } 141 | 142 | leap.subscribeToRoom(joinToken); 143 | }, [connectionState, autojoin, joinToken, stream?.subscription]); 144 | 145 | const canPlay = 146 | connectionState === LeapConnectionState.CONNECTED && 147 | ref.current !== null && 148 | stream?.subscription === 'available' && 149 | stream.connection.llhls?.edge_endpoint !== undefined; 150 | 151 | useEffect(() => { 152 | if (!canPlay) { 153 | return; 154 | } 155 | 156 | const controls = pipe.mount( 157 | ref.current, 158 | stream.connection.llhls!.edge_endpoint, 159 | ); 160 | 161 | setControls(controls); 162 | 163 | const errorListener = (event: hls.Events.ERROR, data: hls.ErrorData) => { 164 | if (data.details !== hls.ErrorDetails.BUFFER_STALLED_ERROR) { 165 | return; 166 | } 167 | 168 | events.emit('BUFFERING', {buffering: true}); 169 | setBuffering(true); 170 | }; 171 | 172 | const fragBufferedListener = ( 173 | event: hls.Events.FRAG_BUFFERED, 174 | data: hls.FragBufferedData, 175 | ) => { 176 | events.emit('BUFFERING', {buffering: false}); 177 | setBuffering(false); 178 | }; 179 | 180 | if (controls.isNative) { 181 | // iOS Safari polyfills 182 | setBuffering(false); 183 | } else { 184 | controls.hls?.on(hls.Events.ERROR, errorListener); 185 | controls.hls?.on(hls.Events.FRAG_BUFFERED, fragBufferedListener); 186 | } 187 | 188 | return () => { 189 | controls.destroy(); 190 | setControls(null); 191 | 192 | controls.hls?.off(hls.Events.ERROR, errorListener); 193 | controls.hls?.off(hls.Events.FRAG_BUFFERED, fragBufferedListener); 194 | }; 195 | }, [canPlay, ref.current]); 196 | 197 | return { 198 | live: stream?.room?.state === 'live', 199 | canPlay, 200 | subscription: stream?.subscription ?? ('non_existent' as const), 201 | events, 202 | buffering, 203 | controls, 204 | 205 | /** 206 | * Gets the estimated position (in seconds) of live edge (ie edge of live playlist plus time sync playlist advanced) returns 0 before first playlist is loaded 207 | */ 208 | getLiveSync() { 209 | return controls?.hls?.liveSyncPosition ?? null; 210 | }, 211 | /** 212 | * Gets the estimated position (in seconds) of live edge (ie edge of live playlist plus time sync playlist advanced) returns 0 before first playlist is loaded 213 | */ 214 | getLatency() { 215 | return controls?.hls?.latency ?? null; 216 | }, 217 | 218 | /** 219 | * Requests a subscription to the pipe room. You only need to use this if you don't use the autojoin feature. 220 | */ 221 | join() { 222 | if (!joinToken) { 223 | throw new Error( 224 | 'Cannot join a room without a valid join token passed to `usePipeRoom`.', 225 | ); 226 | } 227 | 228 | leap.subscribeToRoom(joinToken); 229 | }, 230 | }; 231 | } 232 | -------------------------------------------------------------------------------- /packages/react/src/hooks/timeout.ts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | 3 | export function useInterval(ms: number, fn: () => unknown) { 4 | useEffect(() => { 5 | const interval = setInterval(fn, ms); 6 | 7 | return () => { 8 | clearInterval(interval); 9 | }; 10 | }, []); 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export {LeapConnectionState as ConnectionState} from '@onehop/leap-edge-js'; 2 | export * from './hooks/channels'; 3 | export * from './hooks/leap'; 4 | export * from './hooks/pipe'; 5 | -------------------------------------------------------------------------------- /packages/react/src/util/state.ts: -------------------------------------------------------------------------------- 1 | import type {SetStateAction} from 'react'; 2 | 3 | /** 4 | * Resolves a SetStateAction into a value 5 | * 6 | * @param oldState The old state 7 | * @param newState The new state or action 8 | * @returns Resolved new state 9 | */ 10 | export function resolveSetStateAction( 11 | oldState: T, 12 | newState: SetStateAction, 13 | ): T { 14 | if (newState instanceof Function) { 15 | return newState(oldState); 16 | } 17 | 18 | return newState; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "noFallthroughCasesInSwitch": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "useUnknownInCatchVariables": true, 19 | "allowUnreachableCode": false, 20 | "downlevelIteration": true, 21 | "strictNullChecks": true 22 | }, 23 | "exclude": ["node_modules", "dist"], 24 | "include": ["**/*.ts", "**/*.tsx"] 25 | } 26 | --------------------------------------------------------------------------------